From 5ecd51dccecd32c89c2ccfd84a71477616eae094 Mon Sep 17 00:00:00 2001 From: Ashley Felton Date: Wed, 11 Dec 2024 09:26:14 +0800 Subject: [PATCH 1/6] Update security policy link. --- SECURITY.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index ac950f9..2b2495a 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -7,7 +7,7 @@ source code repositories managed through our GitHub organisation This repository takes guidance relating to Secure Software Development from the [WA Government Cyber Security -Policy](https://www.wa.gov.au/system/files/2022-01/WA%20Government%20Cyber%20Security%20Policy.pdf). +Policy](https://www.wa.gov.au/government/publications/2024-wa-government-cyber-security-policy). If you believe that you have found a security vulnerability in any DBCA-managed repository, please report it to us as described below. @@ -25,13 +25,13 @@ do not, please follow up via email to ensure we received your original message. Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: - * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) - * Full paths of source file(s) related to the manifestation of the issue - * The location of the affected source code (tag/branch/commit or direct URL) - * Any special configuration required to reproduce the issue - * Step-by-step instructions to reproduce the issue - * Proof-of-concept or exploit code (if possible) - * Impact of the issue, including how an attacker might exploit the issue +- Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) +- Full paths of source file(s) related to the manifestation of the issue +- The location of the affected source code (tag/branch/commit or direct URL) +- Any special configuration required to reproduce the issue +- Step-by-step instructions to reproduce the issue +- Proof-of-concept or exploit code (if possible) +- Impact of the issue, including how an attacker might exploit the issue This information will help us triage your report more quickly. Please note that we prefer all communications to be in English. From c2c1bd696f630146f34256a19994fb527291eff3 Mon Sep 17 00:00:00 2001 From: Ashley Felton Date: Mon, 30 Dec 2024 13:38:00 +0800 Subject: [PATCH 2/6] Bugfix DeviceResource.build_filters method. --- tracking/api.py | 83 ++++++++++++++++++++++++------------------------- 1 file changed, 41 insertions(+), 42 deletions(-) diff --git a/tracking/api.py b/tracking/api.py index a4d219b..b95cc1b 100644 --- a/tracking/api.py +++ b/tracking/api.py @@ -1,33 +1,32 @@ -from datetime import datetime, timedelta +from datetime import timedelta +from io import BytesIO + +import unicodecsv as csv from django.conf import settings from django.core.exceptions import FieldError from django.urls import path from django.utils import timezone -from io import BytesIO from tastypie import fields from tastypie.cache import NoCache from tastypie.http import HttpBadRequest -from tastypie.resources import ModelResource, ALL_WITH_RELATIONS +from tastypie.resources import ALL_WITH_RELATIONS, ModelResource from tastypie.serializers import Serializer from tastypie.utils import format_datetime -import unicodecsv as csv from tracking.models import Device class CSVSerializer(Serializer): - formats = settings.TASTYPIE_DEFAULT_FORMATS + ['csv'] + formats = settings.TASTYPIE_DEFAULT_FORMATS + ["csv"] - content_types = dict( - Serializer.content_types.items() | - [('csv', 'text/csv')]) + content_types = dict(Serializer.content_types.items() | [("csv", "text/csv")]) def format_datetime(self, data): # Override the default `format_datetime` method of the class # to return datetime as timezone-aware. - if self.datetime_formatting == 'rfc-2822': + if self.datetime_formatting == "rfc-2822": return format_datetime(data) - if self.datetime_formatting == 'iso-8601-strict': + if self.datetime_formatting == "iso-8601-strict": # Remove microseconds to strictly adhere to iso-8601 data = data - timedelta(microseconds=data.microsecond) @@ -37,12 +36,12 @@ def to_csv(self, data, options=None): options = options or {} data = self.to_simple(data, options) raw_data = BytesIO() - if 'objects' in data and data['objects']: - fields = data['objects'][0].keys() - writer = csv.DictWriter(raw_data, fields, dialect='excel', extrasaction='ignore') + if "objects" in data and data["objects"]: + fields = data["objects"][0].keys() + writer = csv.DictWriter(raw_data, fields, dialect="excel", extrasaction="ignore") header = dict(zip(fields, fields)) writer.writerow(header) - for item in data['objects']: + for item in data["objects"]: writer.writerow(item) return raw_data.getvalue() @@ -67,29 +66,28 @@ def generate_filtering(mdl): def generate_meta(klass, overrides={}): metaitems = { - 'queryset': klass.objects.all(), - 'resource_name': klass._meta.model_name, - 'filtering': generate_filtering(klass), + "queryset": klass.objects.all(), + "resource_name": klass._meta.model_name, + "filtering": generate_filtering(klass), } metaitems.update(overrides) - return type('Meta', (object,), metaitems) + return type("Meta", (object,), metaitems) class APIResource(ModelResource): - def prepend_urls(self): return [ path( - "/fields//".format(self._meta.resource_name), - self.wrap_view('field_values'), name="api_field_values"), + "/fields//".format(), self.wrap_view("field_values"), name="api_field_values" + ), ] def field_values(self, request, **kwargs): # Get a list of unique values for the field passed in kwargs. try: - qs = self._meta.queryset.values_list(kwargs['field_name'], flat=True).distinct() + qs = self._meta.queryset.values_list(kwargs["field_name"], flat=True).distinct() except FieldError as e: - return self.create_response(request, data={'error': str(e)}, response_class=HttpBadRequest) + return self.create_response(request, data={"error": str(e)}, response_class=HttpBadRequest) # Prepare return the HttpResponse. return self.create_response(request, data=list(qs)) @@ -98,8 +96,8 @@ class HttpCache(NoCache): """ Just set the cache control header to implement web cache """ - def __init__(self, timeout=0, public=None, - private=None, *args, **kwargs): + + def __init__(self, timeout=0, public=None, private=None, *args, **kwargs): """ Optionally accepts a ``timeout`` in seconds for the resource's cache. Defaults to ``0`` seconds. @@ -111,8 +109,8 @@ def __init__(self, timeout=0, public=None, def cache_control(self): control = { - 'max_age': self.timeout, - 's_maxage': self.timeout, + "max_age": self.timeout, + "s_maxage": self.timeout, } if self.public is not None: @@ -125,26 +123,27 @@ def cache_control(self): class DeviceResource(APIResource): - - def build_filters(self, filters=None): - """Override build_filters to allow filtering by seen_age__lte= - """ + def build_filters(self, filters=None, **kwargs): + """Override build_filters to allow filtering by seen_age__lte=""" if filters is None: filters = {} orm_filters = super(DeviceResource, self).build_filters(filters) - if 'seen_age__lte' in filters: + if "seen_age__lte" in filters: # Convert seen_age__lte to a timedelta - td = timedelta(minutes=int(filters['seen_age__lte'])) - orm_filters['seen__gte'] = timezone.now() - td + td = timedelta(minutes=int(filters["seen_age__lte"])) + orm_filters["seen__gte"] = timezone.now() - td return orm_filters - Meta = generate_meta(Device, { - 'cache': HttpCache(settings.DEVICE_HTTP_CACHE_TIMEOUT), - 'serializer': CSVSerializer(), - }) - age_minutes = fields.IntegerField(attribute='age_minutes', readonly=True, null=True) - age_colour = fields.CharField(attribute='age_colour', readonly=True, null=True) - age_text = fields.CharField(attribute='age_text', readonly=True, null=True) - icon = fields.CharField(attribute='icon', readonly=True) + Meta = generate_meta( + Device, + { + "cache": HttpCache(settings.DEVICE_HTTP_CACHE_TIMEOUT), + "serializer": CSVSerializer(), + }, + ) + age_minutes = fields.IntegerField(attribute="age_minutes", readonly=True, null=True) + age_colour = fields.CharField(attribute="age_colour", readonly=True, null=True) + age_text = fields.CharField(attribute="age_text", readonly=True, null=True) + icon = fields.CharField(attribute="icon", readonly=True) From 403d072454128360e6bedfeadad7a8483853d67f Mon Sep 17 00:00:00 2001 From: Ashley Felton Date: Tue, 31 Dec 2024 09:10:53 +0800 Subject: [PATCH 3/6] Basic async streaming view for device location. --- Dockerfile | 2 +- README.md | 4 +- gunicorn.py | 2 + poetry.lock | 456 +++++++++++++++- pyproject.toml | 5 +- resource_tracking/asgi.py | 20 + resource_tracking/workers.py | 9 + resource_tracking/wsgi.py | 9 +- tracking/static/js/resource_map.js | 497 +++++++++--------- .../templates/tracking/device_detail.html | 34 ++ tracking/test_harvest.py | 32 +- tracking/urls.py | 4 +- tracking/views.py | 71 ++- 13 files changed, 851 insertions(+), 294 deletions(-) create mode 100644 resource_tracking/asgi.py create mode 100644 resource_tracking/workers.py create mode 100644 tracking/templates/tracking/device_detail.html diff --git a/Dockerfile b/Dockerfile index 64d9a75..80247c3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -49,4 +49,4 @@ COPY tracking ./tracking RUN python manage.py collectstatic --noinput USER ${UID} EXPOSE 8080 -CMD ["gunicorn", "resource_tracking.wsgi", "--config", "gunicorn.py"] +CMD ["gunicorn", "resource_tracking.asgi:application", "--config", "gunicorn.py"] diff --git a/README.md b/README.md index 1b2c55b..de21e3d 100644 --- a/README.md +++ b/README.md @@ -47,9 +47,9 @@ Other environment variables will be required to run the project in production ## Running -Use `runserver` to run a local copy of the application: +Use `gunicorn` to run the local ASGI server (`runserver` doesn't support async responses yet): - python manage.py runserver 0:8080 + gunicorn resource_tracking.asgi:application --config gunicorn.py --reload Run console commands manually: diff --git a/gunicorn.py b/gunicorn.py index 0d91534..424d98f 100644 --- a/gunicorn.py +++ b/gunicorn.py @@ -12,3 +12,5 @@ timeout = 180 # Disable access logging. accesslog = None +# Use UvicornWorker as the worker class. +worker_class = "resource_tracking.workers.UvicornWorker" diff --git a/poetry.lock b/poetry.lock index a10fb37..dc85269 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,5 +1,26 @@ # This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +[[package]] +name = "anyio" +version = "4.7.0" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +optional = false +python-versions = ">=3.9" +files = [ + {file = "anyio-4.7.0-py3-none-any.whl", hash = "sha256:ea60c3723ab42ba6fff7e8ccb0488c898ec538ff4df1f1d5e642c3601d07e352"}, + {file = "anyio-4.7.0.tar.gz", hash = "sha256:2f834749c602966b7d456a7567cafcb309f96482b5081d14ac93ccd457f9dd48"}, +] + +[package.dependencies] +idna = ">=2.8" +sniffio = ">=1.1" +typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} + +[package.extras] +doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"] +trio = ["trio (>=0.26.1)"] + [[package]] name = "asgiref" version = "3.8.1" @@ -375,6 +396,20 @@ files = [ {file = "charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e"}, ] +[[package]] +name = "click" +version = "8.1.8" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, + {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + [[package]] name = "colorama" version = "0.4.6" @@ -618,6 +653,72 @@ setproctitle = ["setproctitle"] testing = ["coverage", "eventlet", "gevent", "pytest", "pytest-cov"] tornado = ["tornado (>=0.2)"] +[[package]] +name = "h11" +version = "0.14.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.7" +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + +[[package]] +name = "httptools" +version = "0.6.4" +description = "A collection of framework independent HTTP protocol utils." +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "httptools-0.6.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3c73ce323711a6ffb0d247dcd5a550b8babf0f757e86a52558fe5b86d6fefcc0"}, + {file = "httptools-0.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:345c288418f0944a6fe67be8e6afa9262b18c7626c3ef3c28adc5eabc06a68da"}, + {file = "httptools-0.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:deee0e3343f98ee8047e9f4c5bc7cedbf69f5734454a94c38ee829fb2d5fa3c1"}, + {file = "httptools-0.6.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca80b7485c76f768a3bc83ea58373f8db7b015551117375e4918e2aa77ea9b50"}, + {file = "httptools-0.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:90d96a385fa941283ebd231464045187a31ad932ebfa541be8edf5b3c2328959"}, + {file = "httptools-0.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:59e724f8b332319e2875efd360e61ac07f33b492889284a3e05e6d13746876f4"}, + {file = "httptools-0.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:c26f313951f6e26147833fc923f78f95604bbec812a43e5ee37f26dc9e5a686c"}, + {file = "httptools-0.6.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f47f8ed67cc0ff862b84a1189831d1d33c963fb3ce1ee0c65d3b0cbe7b711069"}, + {file = "httptools-0.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0614154d5454c21b6410fdf5262b4a3ddb0f53f1e1721cfd59d55f32138c578a"}, + {file = "httptools-0.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8787367fbdfccae38e35abf7641dafc5310310a5987b689f4c32cc8cc3ee975"}, + {file = "httptools-0.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40b0f7fe4fd38e6a507bdb751db0379df1e99120c65fbdc8ee6c1d044897a636"}, + {file = "httptools-0.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40a5ec98d3f49904b9fe36827dcf1aadfef3b89e2bd05b0e35e94f97c2b14721"}, + {file = "httptools-0.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dacdd3d10ea1b4ca9df97a0a303cbacafc04b5cd375fa98732678151643d4988"}, + {file = "httptools-0.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:288cd628406cc53f9a541cfaf06041b4c71d751856bab45e3702191f931ccd17"}, + {file = "httptools-0.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:df017d6c780287d5c80601dafa31f17bddb170232d85c066604d8558683711a2"}, + {file = "httptools-0.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:85071a1e8c2d051b507161f6c3e26155b5c790e4e28d7f236422dbacc2a9cc44"}, + {file = "httptools-0.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69422b7f458c5af875922cdb5bd586cc1f1033295aa9ff63ee196a87519ac8e1"}, + {file = "httptools-0.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16e603a3bff50db08cd578d54f07032ca1631450ceb972c2f834c2b860c28ea2"}, + {file = "httptools-0.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec4f178901fa1834d4a060320d2f3abc5c9e39766953d038f1458cb885f47e81"}, + {file = "httptools-0.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f9eb89ecf8b290f2e293325c646a211ff1c2493222798bb80a530c5e7502494f"}, + {file = "httptools-0.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:db78cb9ca56b59b016e64b6031eda5653be0589dba2b1b43453f6e8b405a0970"}, + {file = "httptools-0.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ade273d7e767d5fae13fa637f4d53b6e961fb7fd93c7797562663f0171c26660"}, + {file = "httptools-0.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:856f4bc0478ae143bad54a4242fccb1f3f86a6e1be5548fecfd4102061b3a083"}, + {file = "httptools-0.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:322d20ea9cdd1fa98bd6a74b77e2ec5b818abdc3d36695ab402a0de8ef2865a3"}, + {file = "httptools-0.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d87b29bd4486c0093fc64dea80231f7c7f7eb4dc70ae394d70a495ab8436071"}, + {file = "httptools-0.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:342dd6946aa6bda4b8f18c734576106b8a31f2fe31492881a9a160ec84ff4bd5"}, + {file = "httptools-0.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b36913ba52008249223042dca46e69967985fb4051951f94357ea681e1f5dc0"}, + {file = "httptools-0.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:28908df1b9bb8187393d5b5db91435ccc9c8e891657f9cbb42a2541b44c82fc8"}, + {file = "httptools-0.6.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:d3f0d369e7ffbe59c4b6116a44d6a8eb4783aae027f2c0b366cf0aa964185dba"}, + {file = "httptools-0.6.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:94978a49b8f4569ad607cd4946b759d90b285e39c0d4640c6b36ca7a3ddf2efc"}, + {file = "httptools-0.6.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40dc6a8e399e15ea525305a2ddba998b0af5caa2566bcd79dcbe8948181eeaff"}, + {file = "httptools-0.6.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab9ba8dcf59de5181f6be44a77458e45a578fc99c31510b8c65b7d5acc3cf490"}, + {file = "httptools-0.6.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:fc411e1c0a7dcd2f902c7c48cf079947a7e65b5485dea9decb82b9105ca71a43"}, + {file = "httptools-0.6.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:d54efd20338ac52ba31e7da78e4a72570cf729fac82bc31ff9199bedf1dc7440"}, + {file = "httptools-0.6.4-cp38-cp38-win_amd64.whl", hash = "sha256:df959752a0c2748a65ab5387d08287abf6779ae9165916fe053e68ae1fbdc47f"}, + {file = "httptools-0.6.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:85797e37e8eeaa5439d33e556662cc370e474445d5fab24dcadc65a8ffb04003"}, + {file = "httptools-0.6.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:db353d22843cf1028f43c3651581e4bb49374d85692a85f95f7b9a130e1b2cab"}, + {file = "httptools-0.6.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1ffd262a73d7c28424252381a5b854c19d9de5f56f075445d33919a637e3547"}, + {file = "httptools-0.6.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:703c346571fa50d2e9856a37d7cd9435a25e7fd15e236c397bf224afaa355fe9"}, + {file = "httptools-0.6.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:aafe0f1918ed07b67c1e838f950b1c1fabc683030477e60b335649b8020e1076"}, + {file = "httptools-0.6.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0e563e54979e97b6d13f1bbc05a96109923e76b901f786a5eae36e99c01237bd"}, + {file = "httptools-0.6.4-cp39-cp39-win_amd64.whl", hash = "sha256:b799de31416ecc589ad79dd85a0b2657a8fe39327944998dea368c1d4c9e55e6"}, + {file = "httptools-0.6.4.tar.gz", hash = "sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c"}, +] + +[package.extras] +test = ["Cython (>=0.29.24)"] + [[package]] name = "identify" version = "2.6.3" @@ -663,13 +764,13 @@ ipython = {version = ">=7.31.1", markers = "python_version >= \"3.11\""} [[package]] name = "ipython" -version = "8.30.0" +version = "8.31.0" description = "IPython: Productive Interactive Computing" optional = false python-versions = ">=3.10" files = [ - {file = "ipython-8.30.0-py3-none-any.whl", hash = "sha256:85ec56a7e20f6c38fce7727dcca699ae4ffc85985aa7b23635a8008f918ae321"}, - {file = "ipython-8.30.0.tar.gz", hash = "sha256:cb0a405a306d2995a5cbb9901894d240784a9f341394c6ba3f4fe8c6eb89ff6e"}, + {file = "ipython-8.31.0-py3-none-any.whl", hash = "sha256:46ec58f8d3d076a61d128fe517a51eb730e3aaf0c184ea8c17d16e366660c6a6"}, + {file = "ipython-8.31.0.tar.gz", hash = "sha256:b6a2274606bec6166405ff05e54932ed6e5cfecaca1fc05f2cacde7bb074d70b"}, ] [package.dependencies] @@ -769,6 +870,90 @@ files = [ {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, ] +[[package]] +name = "orjson" +version = "3.10.13" +description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" +optional = false +python-versions = ">=3.8" +files = [ + {file = "orjson-3.10.13-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1232c5e873a4d1638ef957c5564b4b0d6f2a6ab9e207a9b3de9de05a09d1d920"}, + {file = "orjson-3.10.13-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d26a0eca3035619fa366cbaf49af704c7cb1d4a0e6c79eced9f6a3f2437964b6"}, + {file = "orjson-3.10.13-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d4b6acd7c9c829895e50d385a357d4b8c3fafc19c5989da2bae11783b0fd4977"}, + {file = "orjson-3.10.13-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1884e53c6818686891cc6fc5a3a2540f2f35e8c76eac8dc3b40480fb59660b00"}, + {file = "orjson-3.10.13-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a428afb5720f12892f64920acd2eeb4d996595bf168a26dd9190115dbf1130d"}, + {file = "orjson-3.10.13-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba5b13b8739ce5b630c65cb1c85aedbd257bcc2b9c256b06ab2605209af75a2e"}, + {file = "orjson-3.10.13-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cab83e67f6aabda1b45882254b2598b48b80ecc112968fc6483fa6dae609e9f0"}, + {file = "orjson-3.10.13-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:62c3cc00c7e776c71c6b7b9c48c5d2701d4c04e7d1d7cdee3572998ee6dc57cc"}, + {file = "orjson-3.10.13-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:dc03db4922e75bbc870b03fc49734cefbd50fe975e0878327d200022210b82d8"}, + {file = "orjson-3.10.13-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:22f1c9a30b43d14a041a6ea190d9eca8a6b80c4beb0e8b67602c82d30d6eec3e"}, + {file = "orjson-3.10.13-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b42f56821c29e697c68d7d421410d7c1d8f064ae288b525af6a50cf99a4b1200"}, + {file = "orjson-3.10.13-cp310-cp310-win32.whl", hash = "sha256:0dbf3b97e52e093d7c3e93eb5eb5b31dc7535b33c2ad56872c83f0160f943487"}, + {file = "orjson-3.10.13-cp310-cp310-win_amd64.whl", hash = "sha256:46c249b4e934453be4ff2e518cd1adcd90467da7391c7a79eaf2fbb79c51e8c7"}, + {file = "orjson-3.10.13-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a36c0d48d2f084c800763473020a12976996f1109e2fcb66cfea442fdf88047f"}, + {file = "orjson-3.10.13-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0065896f85d9497990731dfd4a9991a45b0a524baec42ef0a63c34630ee26fd6"}, + {file = "orjson-3.10.13-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:92b4ec30d6025a9dcdfe0df77063cbce238c08d0404471ed7a79f309364a3d19"}, + {file = "orjson-3.10.13-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a94542d12271c30044dadad1125ee060e7a2048b6c7034e432e116077e1d13d2"}, + {file = "orjson-3.10.13-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3723e137772639af8adb68230f2aa4bcb27c48b3335b1b1e2d49328fed5e244c"}, + {file = "orjson-3.10.13-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f00c7fb18843bad2ac42dc1ce6dd214a083c53f1e324a0fd1c8137c6436269b"}, + {file = "orjson-3.10.13-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0e2759d3172300b2f892dee85500b22fca5ac49e0c42cfff101aaf9c12ac9617"}, + {file = "orjson-3.10.13-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ee948c6c01f6b337589c88f8e0bb11e78d32a15848b8b53d3f3b6fea48842c12"}, + {file = "orjson-3.10.13-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:aa6fe68f0981fba0d4bf9cdc666d297a7cdba0f1b380dcd075a9a3dd5649a69e"}, + {file = "orjson-3.10.13-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:dbcd7aad6bcff258f6896abfbc177d54d9b18149c4c561114f47ebfe74ae6bfd"}, + {file = "orjson-3.10.13-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2149e2fcd084c3fd584881c7f9d7f9e5ad1e2e006609d8b80649655e0d52cd02"}, + {file = "orjson-3.10.13-cp311-cp311-win32.whl", hash = "sha256:89367767ed27b33c25c026696507c76e3d01958406f51d3a2239fe9e91959df2"}, + {file = "orjson-3.10.13-cp311-cp311-win_amd64.whl", hash = "sha256:dca1d20f1af0daff511f6e26a27354a424f0b5cf00e04280279316df0f604a6f"}, + {file = "orjson-3.10.13-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a3614b00621c77f3f6487792238f9ed1dd8a42f2ec0e6540ee34c2d4e6db813a"}, + {file = "orjson-3.10.13-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c976bad3996aa027cd3aef78aa57873f3c959b6c38719de9724b71bdc7bd14b"}, + {file = "orjson-3.10.13-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f74d878d1efb97a930b8a9f9898890067707d683eb5c7e20730030ecb3fb930"}, + {file = "orjson-3.10.13-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:33ef84f7e9513fb13b3999c2a64b9ca9c8143f3da9722fbf9c9ce51ce0d8076e"}, + {file = "orjson-3.10.13-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd2bcde107221bb9c2fa0c4aaba735a537225104173d7e19cf73f70b3126c993"}, + {file = "orjson-3.10.13-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:064b9dbb0217fd64a8d016a8929f2fae6f3312d55ab3036b00b1d17399ab2f3e"}, + {file = "orjson-3.10.13-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c0044b0b8c85a565e7c3ce0a72acc5d35cda60793edf871ed94711e712cb637d"}, + {file = "orjson-3.10.13-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7184f608ad563032e398f311910bc536e62b9fbdca2041be889afcbc39500de8"}, + {file = "orjson-3.10.13-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d36f689e7e1b9b6fb39dbdebc16a6f07cbe994d3644fb1c22953020fc575935f"}, + {file = "orjson-3.10.13-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:54433e421618cd5873e51c0e9d0b9fb35f7bf76eb31c8eab20b3595bb713cd3d"}, + {file = "orjson-3.10.13-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e1ba0c5857dd743438acecc1cd0e1adf83f0a81fee558e32b2b36f89e40cee8b"}, + {file = "orjson-3.10.13-cp312-cp312-win32.whl", hash = "sha256:a42b9fe4b0114b51eb5cdf9887d8c94447bc59df6dbb9c5884434eab947888d8"}, + {file = "orjson-3.10.13-cp312-cp312-win_amd64.whl", hash = "sha256:3a7df63076435f39ec024bdfeb4c9767ebe7b49abc4949068d61cf4857fa6d6c"}, + {file = "orjson-3.10.13-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:2cdaf8b028a976ebab837a2c27b82810f7fc76ed9fb243755ba650cc83d07730"}, + {file = "orjson-3.10.13-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48a946796e390cbb803e069472de37f192b7a80f4ac82e16d6eb9909d9e39d56"}, + {file = "orjson-3.10.13-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a7d64f1db5ecbc21eb83097e5236d6ab7e86092c1cd4c216c02533332951afc"}, + {file = "orjson-3.10.13-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:711878da48f89df194edd2ba603ad42e7afed74abcd2bac164685e7ec15f96de"}, + {file = "orjson-3.10.13-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:cf16f06cb77ce8baf844bc222dbcb03838f61d0abda2c3341400c2b7604e436e"}, + {file = "orjson-3.10.13-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8257c3fb8dd7b0b446b5e87bf85a28e4071ac50f8c04b6ce2d38cb4abd7dff57"}, + {file = "orjson-3.10.13-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d9c3a87abe6f849a4a7ac8a8a1dede6320a4303d5304006b90da7a3cd2b70d2c"}, + {file = "orjson-3.10.13-cp313-cp313-win32.whl", hash = "sha256:527afb6ddb0fa3fe02f5d9fba4920d9d95da58917826a9be93e0242da8abe94a"}, + {file = "orjson-3.10.13-cp313-cp313-win_amd64.whl", hash = "sha256:b5f7c298d4b935b222f52d6c7f2ba5eafb59d690d9a3840b7b5c5cda97f6ec5c"}, + {file = "orjson-3.10.13-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e49333d1038bc03a25fdfe11c86360df9b890354bfe04215f1f54d030f33c342"}, + {file = "orjson-3.10.13-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:003721c72930dbb973f25c5d8e68d0f023d6ed138b14830cc94e57c6805a2eab"}, + {file = "orjson-3.10.13-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:63664bf12addb318dc8f032160e0f5dc17eb8471c93601e8f5e0d07f95003784"}, + {file = "orjson-3.10.13-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6066729cf9552d70de297b56556d14b4f49c8f638803ee3c90fd212fa43cc6af"}, + {file = "orjson-3.10.13-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8a1152e2761025c5d13b5e1908d4b1c57f3797ba662e485ae6f26e4e0c466388"}, + {file = "orjson-3.10.13-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69b21d91c5c5ef8a201036d207b1adf3aa596b930b6ca3c71484dd11386cf6c3"}, + {file = "orjson-3.10.13-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b12a63f48bb53dba8453d36ca2661f2330126d54e26c1661e550b32864b28ce3"}, + {file = "orjson-3.10.13-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a5a7624ab4d121c7e035708c8dd1f99c15ff155b69a1c0affc4d9d8b551281ba"}, + {file = "orjson-3.10.13-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:0fee076134398d4e6cb827002468679ad402b22269510cf228301b787fdff5ae"}, + {file = "orjson-3.10.13-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ae537fcf330b3947e82c6ae4271e092e6cf16b9bc2cef68b14ffd0df1fa8832a"}, + {file = "orjson-3.10.13-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:f81b26c03f5fb5f0d0ee48d83cea4d7bc5e67e420d209cc1a990f5d1c62f9be0"}, + {file = "orjson-3.10.13-cp38-cp38-win32.whl", hash = "sha256:0bc858086088b39dc622bc8219e73d3f246fb2bce70a6104abd04b3a080a66a8"}, + {file = "orjson-3.10.13-cp38-cp38-win_amd64.whl", hash = "sha256:3ca6f17467ebbd763f8862f1d89384a5051b461bb0e41074f583a0ebd7120e8e"}, + {file = "orjson-3.10.13-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:4a11532cbfc2f5752c37e84863ef8435b68b0e6d459b329933294f65fa4bda1a"}, + {file = "orjson-3.10.13-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c96d2fb80467d1d0dfc4d037b4e1c0f84f1fe6229aa7fea3f070083acef7f3d7"}, + {file = "orjson-3.10.13-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dda4ba4d3e6f6c53b6b9c35266788053b61656a716a7fef5c884629c2a52e7aa"}, + {file = "orjson-3.10.13-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4f998bbf300690be881772ee9c5281eb9c0044e295bcd4722504f5b5c6092ff"}, + {file = "orjson-3.10.13-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dce1cc42ed75b585c0c4dc5eb53a90a34ccb493c09a10750d1a1f9b9eff2bd12"}, + {file = "orjson-3.10.13-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03b0f29d485411e3c13d79604b740b14e4e5fb58811743f6f4f9693ee6480a8f"}, + {file = "orjson-3.10.13-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:233aae4474078d82f425134bb6a10fb2b3fc5a1a1b3420c6463ddd1b6a97eda8"}, + {file = "orjson-3.10.13-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e384e330a67cf52b3597ee2646de63407da6f8fc9e9beec3eaaaef5514c7a1c9"}, + {file = "orjson-3.10.13-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:4222881d0aab76224d7b003a8e5fdae4082e32c86768e0e8652de8afd6c4e2c1"}, + {file = "orjson-3.10.13-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e400436950ba42110a20c50c80dff4946c8e3ec09abc1c9cf5473467e83fd1c5"}, + {file = "orjson-3.10.13-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f47c9e7d224b86ffb086059cdcf634f4b3f32480f9838864aa09022fe2617ce2"}, + {file = "orjson-3.10.13-cp39-cp39-win32.whl", hash = "sha256:a9ecea472f3eb653e1c0a3d68085f031f18fc501ea392b98dcca3e87c24f9ebe"}, + {file = "orjson-3.10.13-cp39-cp39-win_amd64.whl", hash = "sha256:5385935a73adce85cc7faac9d396683fd813566d3857fa95a0b521ef84a5b588"}, + {file = "orjson-3.10.13.tar.gz", hash = "sha256:eb9bfb14ab8f68d9d9492d4817ae497788a15fd7da72e14dfabc289c3bb088ec"}, +] + [[package]] name = "packaging" version = "24.2" @@ -1210,6 +1395,17 @@ files = [ {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, ] +[[package]] +name = "sniffio" +version = "1.3.1" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + [[package]] name = "sqlparse" version = "0.5.2" @@ -1308,6 +1504,97 @@ h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] +[[package]] +name = "uvicorn" +version = "0.34.0" +description = "The lightning-fast ASGI server." +optional = false +python-versions = ">=3.9" +files = [ + {file = "uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4"}, + {file = "uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9"}, +] + +[package.dependencies] +click = ">=7.0" +colorama = {version = ">=0.4", optional = true, markers = "sys_platform == \"win32\" and extra == \"standard\""} +h11 = ">=0.8" +httptools = {version = ">=0.6.3", optional = true, markers = "extra == \"standard\""} +python-dotenv = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} +pyyaml = {version = ">=5.1", optional = true, markers = "extra == \"standard\""} +uvloop = {version = ">=0.14.0,<0.15.0 || >0.15.0,<0.15.1 || >0.15.1", optional = true, markers = "(sys_platform != \"win32\" and sys_platform != \"cygwin\") and platform_python_implementation != \"PyPy\" and extra == \"standard\""} +watchfiles = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} +websockets = {version = ">=10.4", optional = true, markers = "extra == \"standard\""} + +[package.extras] +standard = ["colorama (>=0.4)", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] + +[[package]] +name = "uvicorn-worker" +version = "0.3.0" +description = "Uvicorn worker for Gunicorn! ✨" +optional = false +python-versions = ">=3.9" +files = [ + {file = "uvicorn_worker-0.3.0-py3-none-any.whl", hash = "sha256:ef0fe8aad27b0290a9e602a256b03f5a5da3a9e5f942414ca587b645ec77dd52"}, + {file = "uvicorn_worker-0.3.0.tar.gz", hash = "sha256:6baeab7b2162ea6b9612cbe149aa670a76090ad65a267ce8e27316ed13c7de7b"}, +] + +[package.dependencies] +gunicorn = ">=20.1.0" +uvicorn = ">=0.15.0" + +[[package]] +name = "uvloop" +version = "0.21.0" +description = "Fast implementation of asyncio event loop on top of libuv" +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "uvloop-0.21.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ec7e6b09a6fdded42403182ab6b832b71f4edaf7f37a9a0e371a01db5f0cb45f"}, + {file = "uvloop-0.21.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:196274f2adb9689a289ad7d65700d37df0c0930fd8e4e743fa4834e850d7719d"}, + {file = "uvloop-0.21.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f38b2e090258d051d68a5b14d1da7203a3c3677321cf32a95a6f4db4dd8b6f26"}, + {file = "uvloop-0.21.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87c43e0f13022b998eb9b973b5e97200c8b90823454d4bc06ab33829e09fb9bb"}, + {file = "uvloop-0.21.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:10d66943def5fcb6e7b37310eb6b5639fd2ccbc38df1177262b0640c3ca68c1f"}, + {file = "uvloop-0.21.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:67dd654b8ca23aed0a8e99010b4c34aca62f4b7fce88f39d452ed7622c94845c"}, + {file = "uvloop-0.21.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c0f3fa6200b3108919f8bdabb9a7f87f20e7097ea3c543754cabc7d717d95cf8"}, + {file = "uvloop-0.21.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0878c2640cf341b269b7e128b1a5fed890adc4455513ca710d77d5e93aa6d6a0"}, + {file = "uvloop-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9fb766bb57b7388745d8bcc53a359b116b8a04c83a2288069809d2b3466c37e"}, + {file = "uvloop-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a375441696e2eda1c43c44ccb66e04d61ceeffcd76e4929e527b7fa401b90fb"}, + {file = "uvloop-0.21.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:baa0e6291d91649c6ba4ed4b2f982f9fa165b5bbd50a9e203c416a2797bab3c6"}, + {file = "uvloop-0.21.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4509360fcc4c3bd2c70d87573ad472de40c13387f5fda8cb58350a1d7475e58d"}, + {file = "uvloop-0.21.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c"}, + {file = "uvloop-0.21.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2"}, + {file = "uvloop-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d"}, + {file = "uvloop-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc"}, + {file = "uvloop-0.21.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb"}, + {file = "uvloop-0.21.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f"}, + {file = "uvloop-0.21.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281"}, + {file = "uvloop-0.21.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af"}, + {file = "uvloop-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6"}, + {file = "uvloop-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816"}, + {file = "uvloop-0.21.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc"}, + {file = "uvloop-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553"}, + {file = "uvloop-0.21.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:17df489689befc72c39a08359efac29bbee8eee5209650d4b9f34df73d22e414"}, + {file = "uvloop-0.21.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bc09f0ff191e61c2d592a752423c767b4ebb2986daa9ed62908e2b1b9a9ae206"}, + {file = "uvloop-0.21.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0ce1b49560b1d2d8a2977e3ba4afb2414fb46b86a1b64056bc4ab929efdafbe"}, + {file = "uvloop-0.21.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e678ad6fe52af2c58d2ae3c73dc85524ba8abe637f134bf3564ed07f555c5e79"}, + {file = "uvloop-0.21.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:460def4412e473896ef179a1671b40c039c7012184b627898eea5072ef6f017a"}, + {file = "uvloop-0.21.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:10da8046cc4a8f12c91a1c39d1dd1585c41162a15caaef165c2174db9ef18bdc"}, + {file = "uvloop-0.21.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c097078b8031190c934ed0ebfee8cc5f9ba9642e6eb88322b9958b649750f72b"}, + {file = "uvloop-0.21.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:46923b0b5ee7fc0020bef24afe7836cb068f5050ca04caf6b487c513dc1a20b2"}, + {file = "uvloop-0.21.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53e420a3afe22cdcf2a0f4846e377d16e718bc70103d7088a4f7623567ba5fb0"}, + {file = "uvloop-0.21.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88cb67cdbc0e483da00af0b2c3cdad4b7c61ceb1ee0f33fe00e09c81e3a6cb75"}, + {file = "uvloop-0.21.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:221f4f2a1f46032b403bf3be628011caf75428ee3cc204a22addf96f586b19fd"}, + {file = "uvloop-0.21.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2d1f581393673ce119355d56da84fe1dd9d2bb8b3d13ce792524e1607139feff"}, + {file = "uvloop-0.21.0.tar.gz", hash = "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3"}, +] + +[package.extras] +dev = ["Cython (>=3.0,<4.0)", "setuptools (>=60)"] +docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] +test = ["aiohttp (>=3.10.5)", "flake8 (>=5.0,<6.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=23.0.0,<23.1.0)", "pycodestyle (>=2.9.0,<2.10.0)"] + [[package]] name = "virtualenv" version = "20.28.0" @@ -1328,6 +1615,89 @@ platformdirs = ">=3.9.1,<5" docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] +[[package]] +name = "watchfiles" +version = "1.0.3" +description = "Simple, modern and high performance file watching and code reload in python." +optional = false +python-versions = ">=3.9" +files = [ + {file = "watchfiles-1.0.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:1da46bb1eefb5a37a8fb6fd52ad5d14822d67c498d99bda8754222396164ae42"}, + {file = "watchfiles-1.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2b961b86cd3973f5822826017cad7f5a75795168cb645c3a6b30c349094e02e3"}, + {file = "watchfiles-1.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34e87c7b3464d02af87f1059fedda5484e43b153ef519e4085fe1a03dd94801e"}, + {file = "watchfiles-1.0.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d9dd2b89a16cf7ab9c1170b5863e68de6bf83db51544875b25a5f05a7269e678"}, + {file = "watchfiles-1.0.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b4691234d31686dca133c920f94e478b548a8e7c750f28dbbc2e4333e0d3da9"}, + {file = "watchfiles-1.0.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:90b0fe1fcea9bd6e3084b44875e179b4adcc4057a3b81402658d0eb58c98edf8"}, + {file = "watchfiles-1.0.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0b90651b4cf9e158d01faa0833b073e2e37719264bcee3eac49fc3c74e7d304b"}, + {file = "watchfiles-1.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2e9fe695ff151b42ab06501820f40d01310fbd58ba24da8923ace79cf6d702d"}, + {file = "watchfiles-1.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62691f1c0894b001c7cde1195c03b7801aaa794a837bd6eef24da87d1542838d"}, + {file = "watchfiles-1.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:275c1b0e942d335fccb6014d79267d1b9fa45b5ac0639c297f1e856f2f532552"}, + {file = "watchfiles-1.0.3-cp310-cp310-win32.whl", hash = "sha256:06ce08549e49ba69ccc36fc5659a3d0ff4e3a07d542b895b8a9013fcab46c2dc"}, + {file = "watchfiles-1.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:f280b02827adc9d87f764972fbeb701cf5611f80b619c20568e1982a277d6146"}, + {file = "watchfiles-1.0.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ffe709b1d0bc2e9921257569675674cafb3a5f8af689ab9f3f2b3f88775b960f"}, + {file = "watchfiles-1.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:418c5ce332f74939ff60691e5293e27c206c8164ce2b8ce0d9abf013003fb7fe"}, + {file = "watchfiles-1.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f492d2907263d6d0d52f897a68647195bc093dafed14508a8d6817973586b6b"}, + {file = "watchfiles-1.0.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:48c9f3bc90c556a854f4cab6a79c16974099ccfa3e3e150673d82d47a4bc92c9"}, + {file = "watchfiles-1.0.3-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:75d3bcfa90454dba8df12adc86b13b6d85fda97d90e708efc036c2760cc6ba44"}, + {file = "watchfiles-1.0.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5691340f259b8f76b45fb31b98e594d46c36d1dc8285efa7975f7f50230c9093"}, + {file = "watchfiles-1.0.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1e263cc718545b7f897baeac1f00299ab6fabe3e18caaacacb0edf6d5f35513c"}, + {file = "watchfiles-1.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c6cf7709ed3e55704cc06f6e835bf43c03bc8e3cb8ff946bf69a2e0a78d9d77"}, + {file = "watchfiles-1.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:703aa5e50e465be901e0e0f9d5739add15e696d8c26c53bc6fc00eb65d7b9469"}, + {file = "watchfiles-1.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bfcae6aecd9e0cb425f5145afee871465b98b75862e038d42fe91fd753ddd780"}, + {file = "watchfiles-1.0.3-cp311-cp311-win32.whl", hash = "sha256:6a76494d2c5311584f22416c5a87c1e2cb954ff9b5f0988027bc4ef2a8a67181"}, + {file = "watchfiles-1.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:cf745cbfad6389c0e331786e5fe9ae3f06e9d9c2ce2432378e1267954793975c"}, + {file = "watchfiles-1.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:2dcc3f60c445f8ce14156854a072ceb36b83807ed803d37fdea2a50e898635d6"}, + {file = "watchfiles-1.0.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:93436ed550e429da007fbafb723e0769f25bae178fbb287a94cb4ccdf42d3af3"}, + {file = "watchfiles-1.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c18f3502ad0737813c7dad70e3e1cc966cc147fbaeef47a09463bbffe70b0a00"}, + {file = "watchfiles-1.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a5bc3ca468bb58a2ef50441f953e1f77b9a61bd1b8c347c8223403dc9b4ac9a"}, + {file = "watchfiles-1.0.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0d1ec043f02ca04bf21b1b32cab155ce90c651aaf5540db8eb8ad7f7e645cba8"}, + {file = "watchfiles-1.0.3-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f58d3bfafecf3d81c15d99fc0ecf4319e80ac712c77cf0ce2661c8cf8bf84066"}, + {file = "watchfiles-1.0.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1df924ba82ae9e77340101c28d56cbaff2c991bd6fe8444a545d24075abb0a87"}, + {file = "watchfiles-1.0.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:632a52dcaee44792d0965c17bdfe5dc0edad5b86d6a29e53d6ad4bf92dc0ff49"}, + {file = "watchfiles-1.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bf4b459d94a0387617a1b499f314aa04d8a64b7a0747d15d425b8c8b151da0"}, + {file = "watchfiles-1.0.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ca94c85911601b097d53caeeec30201736ad69a93f30d15672b967558df02885"}, + {file = "watchfiles-1.0.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:65ab1fb635476f6170b07e8e21db0424de94877e4b76b7feabfe11f9a5fc12b5"}, + {file = "watchfiles-1.0.3-cp312-cp312-win32.whl", hash = "sha256:49bc1bc26abf4f32e132652f4b3bfeec77d8f8f62f57652703ef127e85a3e38d"}, + {file = "watchfiles-1.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:48681c86f2cb08348631fed788a116c89c787fdf1e6381c5febafd782f6c3b44"}, + {file = "watchfiles-1.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:9e080cf917b35b20c889225a13f290f2716748362f6071b859b60b8847a6aa43"}, + {file = "watchfiles-1.0.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e153a690b7255c5ced17895394b4f109d5dcc2a4f35cb809374da50f0e5c456a"}, + {file = "watchfiles-1.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ac1be85fe43b4bf9a251978ce5c3bb30e1ada9784290441f5423a28633a958a7"}, + {file = "watchfiles-1.0.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2ec98e31e1844eac860e70d9247db9d75440fc8f5f679c37d01914568d18721"}, + {file = "watchfiles-1.0.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0179252846be03fa97d4d5f8233d1c620ef004855f0717712ae1c558f1974a16"}, + {file = "watchfiles-1.0.3-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:995c374e86fa82126c03c5b4630c4e312327ecfe27761accb25b5e1d7ab50ec8"}, + {file = "watchfiles-1.0.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:29b9cb35b7f290db1c31fb2fdf8fc6d3730cfa4bca4b49761083307f441cac5a"}, + {file = "watchfiles-1.0.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f8dc09ae69af50bead60783180f656ad96bd33ffbf6e7a6fce900f6d53b08f1"}, + {file = "watchfiles-1.0.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:489b80812f52a8d8c7b0d10f0d956db0efed25df2821c7a934f6143f76938bd6"}, + {file = "watchfiles-1.0.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:228e2247de583475d4cebf6b9af5dc9918abb99d1ef5ee737155bb39fb33f3c0"}, + {file = "watchfiles-1.0.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1550be1a5cb3be08a3fb84636eaafa9b7119b70c71b0bed48726fd1d5aa9b868"}, + {file = "watchfiles-1.0.3-cp313-cp313-win32.whl", hash = "sha256:16db2d7e12f94818cbf16d4c8938e4d8aaecee23826344addfaaa671a1527b07"}, + {file = "watchfiles-1.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:160eff7d1267d7b025e983ca8460e8cc67b328284967cbe29c05f3c3163711a3"}, + {file = "watchfiles-1.0.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c05b021f7b5aa333124f2a64d56e4cb9963b6efdf44e8d819152237bbd93ba15"}, + {file = "watchfiles-1.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:310505ad305e30cb6c5f55945858cdbe0eb297fc57378f29bacceb534ac34199"}, + {file = "watchfiles-1.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ddff3f8b9fa24a60527c137c852d0d9a7da2a02cf2151650029fdc97c852c974"}, + {file = "watchfiles-1.0.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46e86ed457c3486080a72bc837300dd200e18d08183f12b6ca63475ab64ed651"}, + {file = "watchfiles-1.0.3-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f79fe7993e230a12172ce7d7c7db061f046f672f2b946431c81aff8f60b2758b"}, + {file = "watchfiles-1.0.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ea2b51c5f38bad812da2ec0cd7eec09d25f521a8b6b6843cbccedd9a1d8a5c15"}, + {file = "watchfiles-1.0.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fe4e740ea94978b2b2ab308cbf9270a246bcbb44401f77cc8740348cbaeac3d"}, + {file = "watchfiles-1.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9af037d3df7188ae21dc1c7624501f2f90d81be6550904e07869d8d0e6766655"}, + {file = "watchfiles-1.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:52bb50a4c4ca2a689fdba84ba8ecc6a4e6210f03b6af93181bb61c4ec3abaf86"}, + {file = "watchfiles-1.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c14a07bdb475eb696f85c715dbd0f037918ccbb5248290448488a0b4ef201aad"}, + {file = "watchfiles-1.0.3-cp39-cp39-win32.whl", hash = "sha256:be37f9b1f8934cd9e7eccfcb5612af9fb728fecbe16248b082b709a9d1b348bf"}, + {file = "watchfiles-1.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:ef9ec8068cf23458dbf36a08e0c16f0a2df04b42a8827619646637be1769300a"}, + {file = "watchfiles-1.0.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:84fac88278f42d61c519a6c75fb5296fd56710b05bbdcc74bdf85db409a03780"}, + {file = "watchfiles-1.0.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:c68be72b1666d93b266714f2d4092d78dc53bd11cf91ed5a3c16527587a52e29"}, + {file = "watchfiles-1.0.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:889a37e2acf43c377b5124166bece139b4c731b61492ab22e64d371cce0e6e80"}, + {file = "watchfiles-1.0.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ca05cacf2e5c4a97d02a2878a24020daca21dbb8823b023b978210a75c79098"}, + {file = "watchfiles-1.0.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:8af4b582d5fc1b8465d1d2483e5e7b880cc1a4e99f6ff65c23d64d070867ac58"}, + {file = "watchfiles-1.0.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:127de3883bdb29dbd3b21f63126bb8fa6e773b74eaef46521025a9ce390e1073"}, + {file = "watchfiles-1.0.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:713f67132346bdcb4c12df185c30cf04bdf4bf6ea3acbc3ace0912cab6b7cb8c"}, + {file = "watchfiles-1.0.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abd85de513eb83f5ec153a802348e7a5baa4588b818043848247e3e8986094e8"}, + {file = "watchfiles-1.0.3.tar.gz", hash = "sha256:f3ff7da165c99a5412fe5dd2304dd2dbaaaa5da718aad942dcb3a178eaa70c56"}, +] + +[package.dependencies] +anyio = ">=3.0.0" + [[package]] name = "wcwidth" version = "0.2.13" @@ -1339,6 +1709,84 @@ files = [ {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, ] +[[package]] +name = "websockets" +version = "14.1" +description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" +optional = false +python-versions = ">=3.9" +files = [ + {file = "websockets-14.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a0adf84bc2e7c86e8a202537b4fd50e6f7f0e4a6b6bf64d7ccb96c4cd3330b29"}, + {file = "websockets-14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:90b5d9dfbb6d07a84ed3e696012610b6da074d97453bd01e0e30744b472c8179"}, + {file = "websockets-14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2177ee3901075167f01c5e335a6685e71b162a54a89a56001f1c3e9e3d2ad250"}, + {file = "websockets-14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f14a96a0034a27f9d47fd9788913924c89612225878f8078bb9d55f859272b0"}, + {file = "websockets-14.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f874ba705deea77bcf64a9da42c1f5fc2466d8f14daf410bc7d4ceae0a9fcb0"}, + {file = "websockets-14.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9607b9a442392e690a57909c362811184ea429585a71061cd5d3c2b98065c199"}, + {file = "websockets-14.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bea45f19b7ca000380fbd4e02552be86343080120d074b87f25593ce1700ad58"}, + {file = "websockets-14.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:219c8187b3ceeadbf2afcf0f25a4918d02da7b944d703b97d12fb01510869078"}, + {file = "websockets-14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ad2ab2547761d79926effe63de21479dfaf29834c50f98c4bf5b5480b5838434"}, + {file = "websockets-14.1-cp310-cp310-win32.whl", hash = "sha256:1288369a6a84e81b90da5dbed48610cd7e5d60af62df9851ed1d1d23a9069f10"}, + {file = "websockets-14.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0744623852f1497d825a49a99bfbec9bea4f3f946df6eb9d8a2f0c37a2fec2e"}, + {file = "websockets-14.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:449d77d636f8d9c17952628cc7e3b8faf6e92a17ec581ec0c0256300717e1512"}, + {file = "websockets-14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a35f704be14768cea9790d921c2c1cc4fc52700410b1c10948511039be824aac"}, + {file = "websockets-14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b1f3628a0510bd58968c0f60447e7a692933589b791a6b572fcef374053ca280"}, + {file = "websockets-14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c3deac3748ec73ef24fc7be0b68220d14d47d6647d2f85b2771cb35ea847aa1"}, + {file = "websockets-14.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7048eb4415d46368ef29d32133134c513f507fff7d953c18c91104738a68c3b3"}, + {file = "websockets-14.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6cf0ad281c979306a6a34242b371e90e891bce504509fb6bb5246bbbf31e7b6"}, + {file = "websockets-14.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cc1fc87428c1d18b643479caa7b15db7d544652e5bf610513d4a3478dbe823d0"}, + {file = "websockets-14.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f95ba34d71e2fa0c5d225bde3b3bdb152e957150100e75c86bc7f3964c450d89"}, + {file = "websockets-14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9481a6de29105d73cf4515f2bef8eb71e17ac184c19d0b9918a3701c6c9c4f23"}, + {file = "websockets-14.1-cp311-cp311-win32.whl", hash = "sha256:368a05465f49c5949e27afd6fbe0a77ce53082185bbb2ac096a3a8afaf4de52e"}, + {file = "websockets-14.1-cp311-cp311-win_amd64.whl", hash = "sha256:6d24fc337fc055c9e83414c94e1ee0dee902a486d19d2a7f0929e49d7d604b09"}, + {file = "websockets-14.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ed907449fe5e021933e46a3e65d651f641975a768d0649fee59f10c2985529ed"}, + {file = "websockets-14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:87e31011b5c14a33b29f17eb48932e63e1dcd3fa31d72209848652310d3d1f0d"}, + {file = "websockets-14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bc6ccf7d54c02ae47a48ddf9414c54d48af9c01076a2e1023e3b486b6e72c707"}, + {file = "websockets-14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9777564c0a72a1d457f0848977a1cbe15cfa75fa2f67ce267441e465717dcf1a"}, + {file = "websockets-14.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a655bde548ca98f55b43711b0ceefd2a88a71af6350b0c168aa77562104f3f45"}, + {file = "websockets-14.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3dfff83ca578cada2d19e665e9c8368e1598d4e787422a460ec70e531dbdd58"}, + {file = "websockets-14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6a6c9bcf7cdc0fd41cc7b7944447982e8acfd9f0d560ea6d6845428ed0562058"}, + {file = "websockets-14.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4b6caec8576e760f2c7dd878ba817653144d5f369200b6ddf9771d64385b84d4"}, + {file = "websockets-14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eb6d38971c800ff02e4a6afd791bbe3b923a9a57ca9aeab7314c21c84bf9ff05"}, + {file = "websockets-14.1-cp312-cp312-win32.whl", hash = "sha256:1d045cbe1358d76b24d5e20e7b1878efe578d9897a25c24e6006eef788c0fdf0"}, + {file = "websockets-14.1-cp312-cp312-win_amd64.whl", hash = "sha256:90f4c7a069c733d95c308380aae314f2cb45bd8a904fb03eb36d1a4983a4993f"}, + {file = "websockets-14.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:3630b670d5057cd9e08b9c4dab6493670e8e762a24c2c94ef312783870736ab9"}, + {file = "websockets-14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36ebd71db3b89e1f7b1a5deaa341a654852c3518ea7a8ddfdf69cc66acc2db1b"}, + {file = "websockets-14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5b918d288958dc3fa1c5a0b9aa3256cb2b2b84c54407f4813c45d52267600cd3"}, + {file = "websockets-14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00fe5da3f037041da1ee0cf8e308374e236883f9842c7c465aa65098b1c9af59"}, + {file = "websockets-14.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8149a0f5a72ca36720981418eeffeb5c2729ea55fa179091c81a0910a114a5d2"}, + {file = "websockets-14.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77569d19a13015e840b81550922056acabc25e3f52782625bc6843cfa034e1da"}, + {file = "websockets-14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cf5201a04550136ef870aa60ad3d29d2a59e452a7f96b94193bee6d73b8ad9a9"}, + {file = "websockets-14.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:88cf9163ef674b5be5736a584c999e98daf3aabac6e536e43286eb74c126b9c7"}, + {file = "websockets-14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:836bef7ae338a072e9d1863502026f01b14027250a4545672673057997d5c05a"}, + {file = "websockets-14.1-cp313-cp313-win32.whl", hash = "sha256:0d4290d559d68288da9f444089fd82490c8d2744309113fc26e2da6e48b65da6"}, + {file = "websockets-14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8621a07991add373c3c5c2cf89e1d277e49dc82ed72c75e3afc74bd0acc446f0"}, + {file = "websockets-14.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:01bb2d4f0a6d04538d3c5dfd27c0643269656c28045a53439cbf1c004f90897a"}, + {file = "websockets-14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:414ffe86f4d6f434a8c3b7913655a1a5383b617f9bf38720e7c0799fac3ab1c6"}, + {file = "websockets-14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8fda642151d5affdee8a430bd85496f2e2517be3a2b9d2484d633d5712b15c56"}, + {file = "websockets-14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd7c11968bc3860d5c78577f0dbc535257ccec41750675d58d8dc66aa47fe52c"}, + {file = "websockets-14.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a032855dc7db987dff813583d04f4950d14326665d7e714d584560b140ae6b8b"}, + {file = "websockets-14.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7e7ea2f782408c32d86b87a0d2c1fd8871b0399dd762364c731d86c86069a78"}, + {file = "websockets-14.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:39450e6215f7d9f6f7bc2a6da21d79374729f5d052333da4d5825af8a97e6735"}, + {file = "websockets-14.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ceada5be22fa5a5a4cdeec74e761c2ee7db287208f54c718f2df4b7e200b8d4a"}, + {file = "websockets-14.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3fc753451d471cff90b8f467a1fc0ae64031cf2d81b7b34e1811b7e2691bc4bc"}, + {file = "websockets-14.1-cp39-cp39-win32.whl", hash = "sha256:14839f54786987ccd9d03ed7f334baec0f02272e7ec4f6e9d427ff584aeea8b4"}, + {file = "websockets-14.1-cp39-cp39-win_amd64.whl", hash = "sha256:d9fd19ecc3a4d5ae82ddbfb30962cf6d874ff943e56e0c81f5169be2fda62979"}, + {file = "websockets-14.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e5dc25a9dbd1a7f61eca4b7cb04e74ae4b963d658f9e4f9aad9cd00b688692c8"}, + {file = "websockets-14.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:04a97aca96ca2acedf0d1f332c861c5a4486fdcba7bcef35873820f940c4231e"}, + {file = "websockets-14.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df174ece723b228d3e8734a6f2a6febbd413ddec39b3dc592f5a4aa0aff28098"}, + {file = "websockets-14.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:034feb9f4286476f273b9a245fb15f02c34d9586a5bc936aff108c3ba1b21beb"}, + {file = "websockets-14.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:660c308dabd2b380807ab64b62985eaccf923a78ebc572bd485375b9ca2b7dc7"}, + {file = "websockets-14.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5a42d3ecbb2db5080fc578314439b1d79eef71d323dc661aa616fb492436af5d"}, + {file = "websockets-14.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ddaa4a390af911da6f680be8be4ff5aaf31c4c834c1a9147bc21cbcbca2d4370"}, + {file = "websockets-14.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a4c805c6034206143fbabd2d259ec5e757f8b29d0a2f0bf3d2fe5d1f60147a4a"}, + {file = "websockets-14.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:205f672a6c2c671a86d33f6d47c9b35781a998728d2c7c2a3e1cf3333fcb62b7"}, + {file = "websockets-14.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef440054124728cc49b01c33469de06755e5a7a4e83ef61934ad95fc327fbb0"}, + {file = "websockets-14.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7591d6f440af7f73c4bd9404f3772bfee064e639d2b6cc8c94076e71b2471c1"}, + {file = "websockets-14.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:25225cc79cfebc95ba1d24cd3ab86aaa35bcd315d12fa4358939bd55e9bd74a5"}, + {file = "websockets-14.1-py3-none-any.whl", hash = "sha256:4d4fc827a20abe6d544a119896f6b78ee13fe81cbfef416f3f2ddf09a03f0e2e"}, + {file = "websockets-14.1.tar.gz", hash = "sha256:398b10c77d471c0aab20a845e7a60076b6390bfdaac7a6d2edb0d2c59d75e8d8"}, +] + [[package]] name = "whitenoise" version = "6.8.2" @@ -1359,4 +1807,4 @@ brotli = ["brotli"] [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "38fcb229f182dda456f891cafa2a224f4b1407265a763d26496db2e8a85d529b" +content-hash = "b67bb4a6349e34d78cca760f894dd0c8fd93fba01992e36b281e23ef238442a4" diff --git a/pyproject.toml b/pyproject.toml index 989838a..c8b239d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,9 +21,12 @@ unicodecsv = "0.14.1" whitenoise = { version = "6.8.2", extras = ["brotli"] } azure-storage-blob = "12.23.1" sentry-sdk = { version = "2.19.0", extras = ["django"] } +uvicorn = {extras = ["standard"], version = "^0.34.0"} +uvicorn-worker = "^0.3.0" +orjson = "3.10.13" [tool.poetry.group.dev.dependencies] -ipython = "^8.30.0" +ipython = "^8.31.0" ipdb = "^0.13.13" pre-commit = "^4.0.1" mixer = "^7.2.2" diff --git a/resource_tracking/asgi.py b/resource_tracking/asgi.py new file mode 100644 index 0000000..c559469 --- /dev/null +++ b/resource_tracking/asgi.py @@ -0,0 +1,20 @@ +""" +ASGI config for resource_tracking project. +It exposes the ASGI callable as a module-level variable named ``application``. +""" + +import os +from pathlib import Path + +from django.core.asgi import get_asgi_application + +# These lines are required for interoperability between local and container environments. +d = Path(__file__).resolve().parent.parent +dot_env = os.path.join(str(d), ".env") +if os.path.exists(dot_env): + from dotenv import load_dotenv + + load_dotenv() + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "resource_tracking.settings") +application = get_asgi_application() diff --git a/resource_tracking/workers.py b/resource_tracking/workers.py new file mode 100644 index 0000000..912b92e --- /dev/null +++ b/resource_tracking/workers.py @@ -0,0 +1,9 @@ +from typing import Any, Dict + +from uvicorn.workers import UvicornWorker as BaseUvicornWorker + + +class UvicornWorker(BaseUvicornWorker): + # UvicornWorker doesn't support the lifespan protocol. + # Reference: https://stackoverflow.com/a/75996092/14508 + CONFIG_KWARGS: Dict[str, Any] = {"loop": "auto", "http": "auto", "lifespan": "off"} diff --git a/resource_tracking/wsgi.py b/resource_tracking/wsgi.py index 7b192fb..02da888 100644 --- a/resource_tracking/wsgi.py +++ b/resource_tracking/wsgi.py @@ -2,15 +2,18 @@ WSGI config for resource_tracking project. It exposes the WSGI callable as a module-level variable named ``application``. """ + import os -from django.core.wsgi import get_wsgi_application from pathlib import Path +from django.core.wsgi import get_wsgi_application + # These lines are required for interoperability between local and container environments. -d = Path(__file__).resolve().parent -dot_env = os.path.join(str(d), '.env') +d = Path(__file__).resolve().parent.parent +dot_env = os.path.join(str(d), ".env") if os.path.exists(dot_env): from dotenv import load_dotenv + load_dotenv() os.environ.setdefault("DJANGO_SETTINGS_MODULE", "resource_tracking.settings") diff --git a/tracking/static/js/resource_map.js b/tracking/static/js/resource_map.js index 71b8c71..aa1ab03 100644 --- a/tracking/static/js/resource_map.js +++ b/tracking/static/js/resource_map.js @@ -1,261 +1,236 @@ -"use strict"; -const geoserver_wmts_url = - geoserver_url + - "/gwc/service/wmts?service=WMTS&request=GetTile&version=1.0.0&tilematrixset=gda94&tilematrix=gda94:{z}&tilecol={x}&tilerow={y}"; -const geoserver_wmts_url_base = geoserver_wmts_url + "&format=image/jpeg"; -const geoserver_wmts_url_overlay = geoserver_wmts_url + "&format=image/png"; - -// Base layers -const mapboxStreets = L.tileLayer( - geoserver_wmts_url_base + "&layer=dbca:mapbox-streets", - { - tileSize: 1024, - zoomOffset: -2, - }, -); -const landgateOrthomosaic = L.tileLayer( - geoserver_wmts_url_base + "&layer=landgate:virtual_mosaic", - { - tileSize: 1024, - zoomOffset: -2, - }, -); -const stateMapBase = L.tileLayer( - geoserver_wmts_url_base + "&layer=cddp:state_map_base", - { - tileSize: 1024, - zoomOffset: -2, - }, -); - -// Overlay layers -const dbcaBushfires = L.tileLayer( - geoserver_wmts_url_overlay + "&layer=landgate:dbca_going_bushfires_dbca-001", - { - tileSize: 1024, - zoomOffset: -2, - transparent: true, - opacity: 0.75, - }, -); -const dfesBushfires = L.tileLayer( - geoserver_wmts_url_overlay + "&layer=landgate:authorised_fireshape_dfes-032", - { - tileSize: 1024, - zoomOffset: -2, - transparent: true, - opacity: 0.75, - }, -); -const dbcaRegions = L.tileLayer( - geoserver_wmts_url_overlay + - "&layer=cddp:kaartdijin-boodja-public_CPT_DBCA_REGIONS", - { - tileSize: 1024, - zoomOffset: -2, - }, -); -const lgaBoundaries = L.tileLayer( - geoserver_wmts_url_overlay + "&layer=cddp:local_gov_authority", - { - tileSize: 1024, - zoomOffset: -2, - }, -); - -// Icon classes (note that URLs are injected into the base template.) -const iconCar = L.icon({ - iconUrl: car_icon_url, - iconSize: [32, 32], - iconAnchor: [16, 16], -}); -const iconUte = L.icon({ - iconUrl: ute_icon_url, - iconSize: [32, 32], - iconAnchor: [16, 16], -}); -const iconLightUnit = L.icon({ - iconUrl: light_unit_icon_url, - iconSize: [32, 32], - iconAnchor: [16, 16], -}); -const iconGangTruck = L.icon({ - iconUrl: gang_truck_icon_url, - iconSize: [32, 32], - iconAnchor: [16, 16], -}); -const iconCommsBus = L.icon({ - iconUrl: comms_bus_icon_url, - iconSize: [32, 32], - iconAnchor: [16, 16], -}); -const iconRotary = L.icon({ - iconUrl: rotary_aircraft_icon_url, - iconSize: [32, 32], - iconAnchor: [16, 16], -}); -const iconPlane = L.icon({ - iconUrl: plane_icon_url, - iconSize: [32, 32], - iconAnchor: [16, 16], -}); -const iconDozer = L.icon({ - iconUrl: dozer_icon_url, - iconSize: [32, 32], - iconAnchor: [16, 16], -}); -const iconLoader = L.icon({ - iconUrl: loader_icon_url, - iconSize: [32, 32], - iconAnchor: [16, 16], -}); -const iconFloat = L.icon({ - iconUrl: float_icon_url, - iconSize: [32, 32], - iconAnchor: [16, 16], -}); -const iconFuelTruck = L.icon({ - iconUrl: fuel_truck_icon_url, - iconSize: [32, 32], - iconAnchor: [16, 16], -}); -const iconPerson = L.icon({ - iconUrl: person_icon_url, - iconSize: [32, 32], - iconAnchor: [16, 16], -}); -const iconOther = L.icon({ - iconUrl: other_icon_url, - iconSize: [32, 32], - iconAnchor: [16, 16], -}); - -function setDeviceStyle(feature, layer) { - var callsign; - if (feature.properties.callsign) { - callsign = feature.properties.callsign; - } else { - callsign = ""; - } - layer.bindTooltip( - ` - ID: ${feature.properties.id}
- Registration: ${feature.properties.registration}
- Callsign: ${callsign}
- Type: ${feature.properties.symbol}
- Seen: ${feature.properties.age_text} - `, - ); - // Set the feature icon. - if (feature.properties.icon == "sss-2_wheel_drive") { - layer.setIcon(iconCar); - } else if (feature.properties.icon == "sss-4_wheel_drive_passenger") { - layer.setIcon(iconCar); - } else if (feature.properties.icon == "sss-4_wheel_drive_ute") { - layer.setIcon(iconUte); - } else if (feature.properties.icon == "sss-light_unit") { - layer.setIcon(iconLightUnit); - } else if (feature.properties.icon == "sss-gang_truck") { - layer.setIcon(iconGangTruck); - } else if (feature.properties.icon == "sss-comms_bus") { - layer.setIcon(iconCommsBus); - } else if (feature.properties.icon == "sss-rotary_aircraft") { - layer.setIcon(iconRotary); - } else if (feature.properties.icon == "sss-spotter_aircraft") { - layer.setIcon(iconPlane); - } else if (feature.properties.icon == "sss-dozer") { - layer.setIcon(iconDozer); - } else if (feature.properties.icon == "sss-float") { - layer.setIcon(iconFloat); - } else if (feature.properties.icon == "sss-loader") { - layer.setIcon(iconLoader); - } else if (feature.properties.icon == "sss-aviation_fuel_truck") { - layer.setIcon(iconFuelTruck); - } else if (feature.properties.icon == "sss-person") { - layer.setIcon(iconPerson); - } else { - layer.setIcon(iconOther); - } -} - -// Add the (initially) empty devices layer to the map. -const trackedDevices = L.geoJSON(null, { - onEachFeature: setDeviceStyle, -}); - -function refreshTrackedDevicesLayer(trackedDevicesLayer) { - // Remove any existing data from the GeoJSON layer. - trackedDevicesLayer.clearLayers(); - // Initial notification. - Toastify({ - text: "Refreshing tracked device data", - duration: 1500, - }).showToast(); - // Query the API endpoint for device data. - $.getJSON(device_geojson_url, function (data) { - // Add the device data to the GeoJSON layer. - trackedDevicesLayer.addData(data); - // Success notification. - Toastify({ - text: "Tracked device data refreshed", - duration: 1500, - }).showToast(); - }); -} -// Immediately run the function, once. -refreshTrackedDevicesLayer(trackedDevices); - -// Define map. -var map = L.map("map", { - crs: L.CRS.EPSG4326, // WGS 84 - center: [-31.96, 115.87], - zoom: 12, - minZoom: 4, - maxZoom: 18, - layers: [mapboxStreets, trackedDevices], // Sets default selections. - attributionControl: false, -}); - -// Define layer groups. -var baseMaps = { - "Mapbox streets": mapboxStreets, - "Landgate orthomosaic": landgateOrthomosaic, - "State map base 250K": stateMapBase, -}; -var overlayMaps = { - "Tracked devices": trackedDevices, - "DBCA Going Bushfires": dbcaBushfires, - "DFES Going Bushfires": dfesBushfires, - "DBCA regions": dbcaRegions, - "LGA boundaries": lgaBoundaries, -}; - -// Define layer control. -L.control.layers(baseMaps, overlayMaps).addTo(map); - -// Define scale bar -L.control.scale({ maxWidth: 500, imperial: false }).addTo(map); - -// Add a fullscreen control to the map. -const fullScreen = new L.control.fullscreen(); -map.addControl(fullScreen); - -// Device registration search -const searchControl = new L.Control.Search({ - layer: trackedDevices, - propertyName: "registration", - textPlaceholder: "Search registration", - delayType: 1000, - textErr: "Registration not found", - zoom: 16, - circleLocation: true, - autoCollapse: true, -}); -map.addControl(searchControl); - -const refreshButton = L.easyButton( - ``, - function (btn, map) { - refreshTrackedDevicesLayer(trackedDevices); - }, -).addTo(map); +"use strict"; +const geoserver_wmts_url = + geoserver_url + + "/gwc/service/wmts?service=WMTS&request=GetTile&version=1.0.0&tilematrixset=gda94&tilematrix=gda94:{z}&tilecol={x}&tilerow={y}"; +const geoserver_wmts_url_base = geoserver_wmts_url + "&format=image/jpeg"; +const geoserver_wmts_url_overlay = geoserver_wmts_url + "&format=image/png"; + +// Base layers +const mapboxStreets = L.tileLayer(geoserver_wmts_url_base + "&layer=dbca:mapbox-streets", { + tileSize: 1024, + zoomOffset: -2, +}); +const landgateOrthomosaic = L.tileLayer(geoserver_wmts_url_base + "&layer=landgate:virtual_mosaic", { + tileSize: 1024, + zoomOffset: -2, +}); +const stateMapBase = L.tileLayer(geoserver_wmts_url_base + "&layer=cddp:state_map_base", { + tileSize: 1024, + zoomOffset: -2, +}); + +// Overlay layers +const dbcaBushfires = L.tileLayer(geoserver_wmts_url_overlay + "&layer=landgate:dbca_going_bushfires_dbca-001", { + tileSize: 1024, + zoomOffset: -2, + transparent: true, + opacity: 0.75, +}); +const dfesBushfires = L.tileLayer(geoserver_wmts_url_overlay + "&layer=landgate:authorised_fireshape_dfes-032", { + tileSize: 1024, + zoomOffset: -2, + transparent: true, + opacity: 0.75, +}); +const dbcaRegions = L.tileLayer(geoserver_wmts_url_overlay + "&layer=cddp:kaartdijin-boodja-public_CPT_DBCA_REGIONS", { + tileSize: 1024, + zoomOffset: -2, +}); +const lgaBoundaries = L.tileLayer(geoserver_wmts_url_overlay + "&layer=cddp:local_gov_authority", { + tileSize: 1024, + zoomOffset: -2, +}); + +// Icon classes (note that URLs are injected into the base template.) +const iconCar = L.icon({ + iconUrl: car_icon_url, + iconSize: [32, 32], + iconAnchor: [16, 16], +}); +const iconUte = L.icon({ + iconUrl: ute_icon_url, + iconSize: [32, 32], + iconAnchor: [16, 16], +}); +const iconLightUnit = L.icon({ + iconUrl: light_unit_icon_url, + iconSize: [32, 32], + iconAnchor: [16, 16], +}); +const iconGangTruck = L.icon({ + iconUrl: gang_truck_icon_url, + iconSize: [32, 32], + iconAnchor: [16, 16], +}); +const iconCommsBus = L.icon({ + iconUrl: comms_bus_icon_url, + iconSize: [32, 32], + iconAnchor: [16, 16], +}); +const iconRotary = L.icon({ + iconUrl: rotary_aircraft_icon_url, + iconSize: [32, 32], + iconAnchor: [16, 16], +}); +const iconPlane = L.icon({ + iconUrl: plane_icon_url, + iconSize: [32, 32], + iconAnchor: [16, 16], +}); +const iconDozer = L.icon({ + iconUrl: dozer_icon_url, + iconSize: [32, 32], + iconAnchor: [16, 16], +}); +const iconLoader = L.icon({ + iconUrl: loader_icon_url, + iconSize: [32, 32], + iconAnchor: [16, 16], +}); +const iconFloat = L.icon({ + iconUrl: float_icon_url, + iconSize: [32, 32], + iconAnchor: [16, 16], +}); +const iconFuelTruck = L.icon({ + iconUrl: fuel_truck_icon_url, + iconSize: [32, 32], + iconAnchor: [16, 16], +}); +const iconPerson = L.icon({ + iconUrl: person_icon_url, + iconSize: [32, 32], + iconAnchor: [16, 16], +}); +const iconOther = L.icon({ + iconUrl: other_icon_url, + iconSize: [32, 32], + iconAnchor: [16, 16], +}); + +function setDeviceStyle(feature, layer) { + var callsign; + if (feature.properties.callsign) { + callsign = feature.properties.callsign; + } else { + callsign = ""; + } + layer.bindTooltip( + ` + ID: ${feature.properties.id}
+ Registration: ${feature.properties.registration}
+ Callsign: ${callsign}
+ Type: ${feature.properties.symbol}
+ Seen: ${feature.properties.age_text} + ` + ); + // Set the feature icon. + if (feature.properties.icon == "sss-2_wheel_drive") { + layer.setIcon(iconCar); + } else if (feature.properties.icon == "sss-4_wheel_drive_passenger") { + layer.setIcon(iconCar); + } else if (feature.properties.icon == "sss-4_wheel_drive_ute") { + layer.setIcon(iconUte); + } else if (feature.properties.icon == "sss-light_unit") { + layer.setIcon(iconLightUnit); + } else if (feature.properties.icon == "sss-gang_truck") { + layer.setIcon(iconGangTruck); + } else if (feature.properties.icon == "sss-comms_bus") { + layer.setIcon(iconCommsBus); + } else if (feature.properties.icon == "sss-rotary_aircraft") { + layer.setIcon(iconRotary); + } else if (feature.properties.icon == "sss-spotter_aircraft") { + layer.setIcon(iconPlane); + } else if (feature.properties.icon == "sss-dozer") { + layer.setIcon(iconDozer); + } else if (feature.properties.icon == "sss-float") { + layer.setIcon(iconFloat); + } else if (feature.properties.icon == "sss-loader") { + layer.setIcon(iconLoader); + } else if (feature.properties.icon == "sss-aviation_fuel_truck") { + layer.setIcon(iconFuelTruck); + } else if (feature.properties.icon == "sss-person") { + layer.setIcon(iconPerson); + } else { + layer.setIcon(iconOther); + } +} + +// Add the (initially) empty devices layer to the map. +const trackedDevices = L.geoJSON(null, { + onEachFeature: setDeviceStyle, +}); + +function refreshTrackedDevicesLayer(trackedDevicesLayer) { + // Remove any existing data from the GeoJSON layer. + trackedDevicesLayer.clearLayers(); + // Initial notification. + Toastify({ + text: "Refreshing tracked device data", + duration: 1500, + }).showToast(); + // Query the API endpoint for device data. + $.getJSON(device_geojson_url, function (data) { + // Add the device data to the GeoJSON layer. + trackedDevicesLayer.addData(data); + // Success notification. + Toastify({ + text: "Tracked device data refreshed", + duration: 1500, + }).showToast(); + }); +} +// Immediately run the function, once. +refreshTrackedDevicesLayer(trackedDevices); + +// Define map. +var map = L.map("map", { + crs: L.CRS.EPSG4326, // WGS 84 + center: [-31.96, 115.87], + zoom: 12, + minZoom: 4, + maxZoom: 18, + layers: [mapboxStreets, trackedDevices], // Sets default selections. + attributionControl: false, +}); + +// Define layer groups. +var baseMaps = { + "Mapbox streets": mapboxStreets, + "Landgate orthomosaic": landgateOrthomosaic, + "State map base 250K": stateMapBase, +}; +var overlayMaps = { + "Tracked devices": trackedDevices, + "DBCA Going Bushfires": dbcaBushfires, + "DFES Going Bushfires": dfesBushfires, + "DBCA regions": dbcaRegions, + "LGA boundaries": lgaBoundaries, +}; + +// Define layer control. +L.control.layers(baseMaps, overlayMaps).addTo(map); + +// Define scale bar +L.control.scale({ maxWidth: 500, imperial: false }).addTo(map); + +// Add a fullscreen control to the map. +const fullScreen = new L.control.fullscreen(); +map.addControl(fullScreen); + +// Device registration search +const searchControl = new L.Control.Search({ + layer: trackedDevices, + propertyName: "registration", + textPlaceholder: "Search registration", + delayType: 1000, + textErr: "Registration not found", + zoom: 16, + circleLocation: true, + autoCollapse: true, +}); +map.addControl(searchControl); + +const refreshButton = L.easyButton(``, function (btn, map) { + refreshTrackedDevicesLayer(trackedDevices); +}).addTo(map); diff --git a/tracking/templates/tracking/device_detail.html b/tracking/templates/tracking/device_detail.html new file mode 100644 index 0000000..bf74d02 --- /dev/null +++ b/tracking/templates/tracking/device_detail.html @@ -0,0 +1,34 @@ + + + + + Tracking device location + + +

Device ID {{ pk }}

+
+ + + diff --git a/tracking/test_harvest.py b/tracking/test_harvest.py index d44e01c..048e4bd 100644 --- a/tracking/test_harvest.py +++ b/tracking/test_harvest.py @@ -1,22 +1,22 @@ import csv -from datetime import datetime, timezone import email -import json import os +from datetime import datetime, timezone + +import orjson as json from django.conf import settings from django.test import TestCase from tracking.models import Device from tracking.utils import ( - validate_latitude_longitude, - parse_mp70_payload, - parse_iriditrak_message, parse_beam_payload, - parse_tracplus_row, parse_dfes_feature, + parse_iriditrak_message, + parse_mp70_payload, + parse_tracplus_row, + validate_latitude_longitude, ) - # MP70 payload with valid data. MP70_PAYLOAD_VALID = "\r\nN694470090021038,13.74,-031.99252,+115.88450,0,0,10/18/2023 03:12:45\r\n" MP70_TIMESTAMP = datetime(2023, 10, 18, 3, 12, 45, tzinfo=timezone.utc) @@ -77,16 +77,14 @@ class HarvestTestCase(TestCase): """ def test_validate_latitude_longitude(self): - """Test the validate_latitude_longitude function - """ + """Test the validate_latitude_longitude function""" data = parse_mp70_payload(MP70_PAYLOAD_INVALID) self.assertFalse(validate_latitude_longitude(data["latitude"], data["longitude"])) data = parse_mp70_payload(MP70_PAYLOAD_VALID) self.assertTrue(validate_latitude_longitude(data["latitude"], data["longitude"])) def test_parse_mp70_payload(self): - """Test the parse_mp70_payload function - """ + """Test the parse_mp70_payload function""" data = parse_mp70_payload(MP70_PAYLOAD_VALID) self.assertTrue(data) self.assertEqual(data["timestamp"], MP70_TIMESTAMP) @@ -95,8 +93,7 @@ def test_parse_mp70_payload(self): self.assertFalse(parse_mp70_payload(MP70_PAYLOAD_BAD)) def test_parse_beam_payload(self): - """Test the parse_beam_payload function - """ + """Test the parse_beam_payload function""" self.assertTrue(parse_beam_payload(IRIDITRAK_PAYLOAD_VALID)) def test_parse_iriditrak_message(self): @@ -107,13 +104,11 @@ def test_parse_iriditrak_message(self): self.assertEqual(IRIDTRAK_TIMESTAMP, data["timestamp"]) def test_parse_spot_message(self): - """TODO: test the parse_spot_message function - """ + """TODO: test the parse_spot_message function""" pass def test_parse_tracplus_row(self): - """Test the parse_tracplus_row function - """ + """Test the parse_tracplus_row function""" self.assertFalse(Device.objects.filter(source_device_type="tracplus").exists()) csv_data = csv.DictReader(TRAKPLUS_FEED_VALID.split("\r\n")) row = next(csv_data) @@ -122,8 +117,7 @@ def test_parse_tracplus_row(self): self.assertEqual(data["timestamp"], TRAKPLUS_TIMESTAMP) def test_parse_dfes_feature(self): - """Test the parse_dfes_feature function - """ + """Test the parse_dfes_feature function""" feature = json.loads(DFES_FEED_FEATURE_VALID) data = parse_dfes_feature(feature) self.assertTrue(data) diff --git a/tracking/urls.py b/tracking/urls.py index db1fcf0..494a2e3 100644 --- a/tracking/urls.py +++ b/tracking/urls.py @@ -1,6 +1,6 @@ from django.urls import path -from tracking import views +from tracking import views urlpatterns = [ path("map/", views.ResourceMap.as_view(), name="resource_map"), @@ -9,4 +9,6 @@ path("loggedpoint/.csv", views.DeviceHistoryView.as_view(format="csv"), name="device_history_csv"), path("loggedpoint/.geojson", views.DeviceHistoryView.as_view(), name="device_history_geojson"), path("route/.geojson", views.DeviceRouteView.as_view(), name="device_route_geojson"), + path("devices//", views.DeviceDetail.as_view(), name="device_detail"), + path("devices//stream/", views.DeviceDetailStream.as_view(), name="device_detail_stream"), ] diff --git a/tracking/views.py b/tracking/views.py index 61d0580..0349909 100644 --- a/tracking/views.py +++ b/tracking/views.py @@ -1,11 +1,14 @@ +import asyncio from datetime import datetime, timedelta + +import orjson as json from django.conf import settings from django.contrib.gis.geos import LineString from django.core.serializers import serialize from django.db.models import Q -from django.http import HttpResponse, HttpResponseBadRequest +from django.http import HttpResponse, HttpResponseBadRequest, StreamingHttpResponse from django.utils import timezone -from django.views.generic import View, TemplateView +from django.views.generic import TemplateView, View from tracking.api import CSVSerializer from tracking.models import Device, LoggedPoint @@ -239,3 +242,67 @@ def get_context_data(self, **kwargs): context["page_title"] = "DBCA Resource Tracking System" context["geoserver_url"] = settings.GEOSERVER_URL return context + + +class DeviceDetailStream(View): + """An experimental view that returns Server-Sent Events (SSE) consisting of + a tracking device location, forever. This is a one-way communication channel, + where the client browser is responsible for maintaining the connection. + """ + + async def stream(self, *args, **kwargs): + """Returns an iterator that queries and then yields tracking device data every n seconds.""" + last_update = None + device = None + + while True: + # Run an asynchronous query for the specifed device. + try: + device = await Device.objects.aget(pk=kwargs["pk"]) + data = json.dumps( + { + "id": device.pk, + "deviceid": device.deviceid, + "seen": device.seen.isoformat(), + "point": device.point.ewkt, + "icon": device.icon, + "registration": device.registration, + "type": device.symbol, + "callsign": device.callsign, + } + ).decode("utf-8") + except: + data = {} + + # Only send a message event if the device has been updated. + if device and device.seen != last_update: + last_update = device.seen + yield f"data: {data}\n\n" + else: + # Always send a ping to keep the connection open. + yield "event: ping\ndata: {}\n\n" + + # Sleep for a period before repeating. + await asyncio.sleep(30) + + async def get(self, request, *args, **kwargs): + return StreamingHttpResponse( + self.stream(*args, **kwargs), + content_type="text/event-stream", + headers={ + # The Cache-Control header need to be set thus to work behind Fastly caching. + "Cache-Control": "private, no-store", + "Connection": "keep-alive", + }, + ) + + +class DeviceDetail(TemplateView): + """Basic template view to test device streaming responses.""" + + template_name = "tracking/device_detail.html" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["pk"] = kwargs["pk"] + return context From 79137d76cf9f907c7acc128ecea5ecd264ed15ea Mon Sep 17 00:00:00 2001 From: Ashley Felton Date: Tue, 31 Dec 2024 14:02:51 +0800 Subject: [PATCH 4/6] Refine device detail view, add link on map popups. --- tracking/static/js/device_detail.js | 158 +++++++++++ tracking/static/js/resource_map.js | 17 +- .../templates/tracking/device_detail.html | 112 ++++++-- tracking/templates/tracking/resource_map.html | 250 +++++++++--------- tracking/urls.py | 4 +- tracking/views.py | 18 +- 6 files changed, 390 insertions(+), 169 deletions(-) create mode 100644 tracking/static/js/device_detail.js diff --git a/tracking/static/js/device_detail.js b/tracking/static/js/device_detail.js new file mode 100644 index 0000000..ef181f5 --- /dev/null +++ b/tracking/static/js/device_detail.js @@ -0,0 +1,158 @@ +"use strict"; + +// Define the (initially) empty device layer. +const trackedDeviceLayer = L.geoJSON(null, {}); + +// Icon classes (note that URLs are injected into the base template.) +const iconCar = L.icon({ + iconUrl: car_icon_url, + iconSize: [32, 32], + iconAnchor: [16, 16], +}); +const iconUte = L.icon({ + iconUrl: ute_icon_url, + iconSize: [32, 32], + iconAnchor: [16, 16], +}); +const iconLightUnit = L.icon({ + iconUrl: light_unit_icon_url, + iconSize: [32, 32], + iconAnchor: [16, 16], +}); +const iconGangTruck = L.icon({ + iconUrl: gang_truck_icon_url, + iconSize: [32, 32], + iconAnchor: [16, 16], +}); +const iconCommsBus = L.icon({ + iconUrl: comms_bus_icon_url, + iconSize: [32, 32], + iconAnchor: [16, 16], +}); +const iconRotary = L.icon({ + iconUrl: rotary_aircraft_icon_url, + iconSize: [32, 32], + iconAnchor: [16, 16], +}); +const iconPlane = L.icon({ + iconUrl: plane_icon_url, + iconSize: [32, 32], + iconAnchor: [16, 16], +}); +const iconDozer = L.icon({ + iconUrl: dozer_icon_url, + iconSize: [32, 32], + iconAnchor: [16, 16], +}); +const iconLoader = L.icon({ + iconUrl: loader_icon_url, + iconSize: [32, 32], + iconAnchor: [16, 16], +}); +const iconFloat = L.icon({ + iconUrl: float_icon_url, + iconSize: [32, 32], + iconAnchor: [16, 16], +}); +const iconFuelTruck = L.icon({ + iconUrl: fuel_truck_icon_url, + iconSize: [32, 32], + iconAnchor: [16, 16], +}); +const iconPerson = L.icon({ + iconUrl: person_icon_url, + iconSize: [32, 32], + iconAnchor: [16, 16], +}); +const iconOther = L.icon({ + iconUrl: other_icon_url, + iconSize: [32, 32], + iconAnchor: [16, 16], +}); + +// Function to style the marker icon. +function setDeviceMarkerIcon(device, marker) { + if (device.icon == "sss-2_wheel_drive") { + marker.setIcon(iconCar); + } else if (device.icon == "sss-4_wheel_drive_passenger") { + marker.setIcon(iconCar); + } else if (device.icon == "sss-4_wheel_drive_ute") { + marker.setIcon(iconUte); + } else if (device.icon == "sss-light_unit") { + marker.setIcon(iconLightUnit); + } else if (device.icon == "sss-gang_truck") { + marker.setIcon(iconGangTruck); + } else if (device.icon == "sss-comms_bus") { + marker.setIcon(iconCommsBus); + } else if (device.icon == "sss-rotary_aircraft") { + marker.setIcon(iconRotary); + } else if (device.icon == "sss-spotter_aircraft") { + marker.setIcon(iconPlane); + } else if (device.icon == "sss-dozer") { + marker.setIcon(iconDozer); + } else if (device.icon == "sss-float") { + marker.setIcon(iconFloat); + } else if (device.icon == "sss-loader") { + marker.setIcon(iconLoader); + } else if (device.icon == "sss-aviation_fuel_truck") { + marker.setIcon(iconFuelTruck); + } else if (device.icon == "sss-person") { + marker.setIcon(iconPerson); + } else { + marker.setIcon(iconOther); + } +} + +// Define map. +const map = L.map("map", { + center: [-31.96, 115.87], + zoom: 12, + minZoom: 4, + maxZoom: 18, + layers: [trackedDeviceLayer], +}); +L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", { + attribution: '© OpenStreetMap', +}).addTo(map); + +function refreshTrackedDeviceLayer(trackedDeviceLayer, device) { + // Declare a regex pattern to parse the EWKT string. + const pattern = /^.+\((?.+)\s(?.+)\)/; + const point = pattern.exec(device.point).groups; + // Remove any existing data from the layer. + trackedDeviceLayer.clearLayers(); + // Generate a marker for the device and add it to the layer. + //const marker = L.marker([point.lat, point.lon], {}); + const deviceMarker = L.marker([point.lat, point.lon], {}); + // Set the marker icon. + setDeviceMarkerIcon(device, deviceMarker); + // Add the marker to the layer. + deviceMarker.addTo(trackedDeviceLayer); + map.flyTo([point.lat, point.lon], map.getZoom()); +} + +const deviceDataEl = document.getElementById("device-data-stream"); +// Defined in the base template. +// const eventSource = new EventSource("{% url 'device_detail_stream' pk=pk %}"); +let ping = 0; + +// Ping event, to help maintain the connection. +eventSource.addEventListener("ping", function (event) { + ping++; + // console.log("ping"); +}); + +// The standard "message" event indicates that the device has updated. +eventSource.onmessage = function (event) { + const device = JSON.parse(event.data); + device.seen = new Date(device.seen); + deviceDataEl.innerHTML = `Device ID: ${device.deviceid}
+ Last seen: ${device.seen.toString()}
+ Registration: ${device.registration}
+ Type: ${device.type}`; + refreshTrackedDeviceLayer(trackedDeviceLayer, device); + Toastify({ + text: "Device location updated", + duration: 1500, + }).showToast(); +}; diff --git a/tracking/static/js/resource_map.js b/tracking/static/js/resource_map.js index aa1ab03..1af1055 100644 --- a/tracking/static/js/resource_map.js +++ b/tracking/static/js/resource_map.js @@ -109,20 +109,19 @@ const iconOther = L.icon({ }); function setDeviceStyle(feature, layer) { - var callsign; + let callsign; if (feature.properties.callsign) { callsign = feature.properties.callsign; } else { callsign = ""; } - layer.bindTooltip( - ` - ID: ${feature.properties.id}
+ layer.bindPopup( + `ID: ${feature.properties.id}
Registration: ${feature.properties.registration}
Callsign: ${callsign}
Type: ${feature.properties.symbol}
- Seen: ${feature.properties.age_text} - ` + Seen: ${feature.properties.age_text}
+ Follow` ); // Set the feature icon. if (feature.properties.icon == "sss-2_wheel_drive") { @@ -184,7 +183,7 @@ function refreshTrackedDevicesLayer(trackedDevicesLayer) { refreshTrackedDevicesLayer(trackedDevices); // Define map. -var map = L.map("map", { +const map = L.map("map", { crs: L.CRS.EPSG4326, // WGS 84 center: [-31.96, 115.87], zoom: 12, @@ -195,12 +194,12 @@ var map = L.map("map", { }); // Define layer groups. -var baseMaps = { +const baseMaps = { "Mapbox streets": mapboxStreets, "Landgate orthomosaic": landgateOrthomosaic, "State map base 250K": stateMapBase, }; -var overlayMaps = { +const overlayMaps = { "Tracked devices": trackedDevices, "DBCA Going Bushfires": dbcaBushfires, "DFES Going Bushfires": dfesBushfires, diff --git a/tracking/templates/tracking/device_detail.html b/tracking/templates/tracking/device_detail.html index bf74d02..e936ab4 100644 --- a/tracking/templates/tracking/device_detail.html +++ b/tracking/templates/tracking/device_detail.html @@ -1,34 +1,98 @@ +{% load static %} - - Tracking device location + Device {{ pk }} location + + + + + + + + + + + + -

Device ID {{ pk }}

-
+
+
+
+
+
+
+
+
+
+
+
+
+ + + + diff --git a/tracking/templates/tracking/resource_map.html b/tracking/templates/tracking/resource_map.html index a379cff..f614788 100644 --- a/tracking/templates/tracking/resource_map.html +++ b/tracking/templates/tracking/resource_map.html @@ -1,125 +1,125 @@ -{% load static %} - - - - {{ page_title }} - - - - - - - - - - - - - - - - - -
-
-
-
-
-
-
- - - - - - - - - - - +{% load static %} + + + + {{ page_title }} + + + + + + + + + + + + + + + + + +
+
+
+
+
+
+
+ + + + + + + + + + + diff --git a/tracking/urls.py b/tracking/urls.py index 494a2e3..10a712d 100644 --- a/tracking/urls.py +++ b/tracking/urls.py @@ -4,11 +4,11 @@ urlpatterns = [ path("map/", views.ResourceMap.as_view(), name="resource_map"), + path("map//", views.DeviceMap.as_view(), name="device_map"), + path("devices//stream/", views.DeviceStream.as_view(), name="device_detail_stream"), path("devices.csv", views.DeviceView.as_view(format="csv"), name="device_csv"), path("devices.geojson", views.DeviceView.as_view(), name="device_geojson"), path("loggedpoint/.csv", views.DeviceHistoryView.as_view(format="csv"), name="device_history_csv"), path("loggedpoint/.geojson", views.DeviceHistoryView.as_view(), name="device_history_geojson"), path("route/.geojson", views.DeviceRouteView.as_view(), name="device_route_geojson"), - path("devices//", views.DeviceDetail.as_view(), name="device_detail"), - path("devices//stream/", views.DeviceDetailStream.as_view(), name="device_detail_stream"), ] diff --git a/tracking/views.py b/tracking/views.py index 0349909..f51bea5 100644 --- a/tracking/views.py +++ b/tracking/views.py @@ -244,15 +244,15 @@ def get_context_data(self, **kwargs): return context -class DeviceDetailStream(View): - """An experimental view that returns Server-Sent Events (SSE) consisting of - a tracking device location, forever. This is a one-way communication channel, +class DeviceStream(View): + """A view that returns Server-Sent Events (SSE) consisting of + a tracking device's location, forever. This is a one-way communication channel, where the client browser is responsible for maintaining the connection. """ async def stream(self, *args, **kwargs): """Returns an iterator that queries and then yields tracking device data every n seconds.""" - last_update = None + last_location = None device = None while True: @@ -274,9 +274,9 @@ async def stream(self, *args, **kwargs): except: data = {} - # Only send a message event if the device has been updated. - if device and device.seen != last_update: - last_update = device.seen + # Only send a message event if the device location has changed. + if device and device.point.ewkt != last_location: + last_location = device.point.ewkt yield f"data: {data}\n\n" else: # Always send a ping to keep the connection open. @@ -297,8 +297,8 @@ async def get(self, request, *args, **kwargs): ) -class DeviceDetail(TemplateView): - """Basic template view to test device streaming responses.""" +class DeviceMap(TemplateView): + """A map view to show single device's location.""" template_name = "tracking/device_detail.html" From 870222740f62c15429c077d84bfa68631e273f80 Mon Sep 17 00:00:00 2001 From: Ashley Felton Date: Tue, 31 Dec 2024 14:09:35 +0800 Subject: [PATCH 5/6] Bump project minor version. --- kustomize/overlays/prod/kustomization.yaml | 2 +- poetry.lock | 8 ++++---- pyproject.toml | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/kustomize/overlays/prod/kustomization.yaml b/kustomize/overlays/prod/kustomization.yaml index 1dab878..f8b3f11 100644 --- a/kustomize/overlays/prod/kustomization.yaml +++ b/kustomize/overlays/prod/kustomization.yaml @@ -29,4 +29,4 @@ patches: - path: service_patch.yaml images: - name: ghcr.io/dbca-wa/resource_tracking - newTag: 1.4.21 + newTag: 1.4.22 diff --git a/poetry.lock b/poetry.lock index dc85269..f2de544 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1331,13 +1331,13 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "sentry-sdk" -version = "2.19.0" +version = "2.19.2" description = "Python client for Sentry (https://sentry.io)" optional = false python-versions = ">=3.6" files = [ - {file = "sentry_sdk-2.19.0-py2.py3-none-any.whl", hash = "sha256:7b0b3b709dee051337244a09a30dbf6e95afe0d34a1f8b430d45e0982a7c125b"}, - {file = "sentry_sdk-2.19.0.tar.gz", hash = "sha256:ee4a4d2ae8bfe3cac012dcf3e4607975904c137e1738116549fc3dbbb6ff0e36"}, + {file = "sentry_sdk-2.19.2-py2.py3-none-any.whl", hash = "sha256:ebdc08228b4d131128e568d696c210d846e5b9d70aa0327dec6b1272d9d40b84"}, + {file = "sentry_sdk-2.19.2.tar.gz", hash = "sha256:467df6e126ba242d39952375dd816fbee0f217d119bf454a8ce74cf1e7909e8d"}, ] [package.dependencies] @@ -1807,4 +1807,4 @@ brotli = ["brotli"] [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "b67bb4a6349e34d78cca760f894dd0c8fd93fba01992e36b281e23ef238442a4" +content-hash = "7a0d648470d66391eac3705a63b9e3f56d6fd7ae7e4e273b9958cb8bdc40e289" diff --git a/pyproject.toml b/pyproject.toml index c8b239d..75082a6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "resource_tracking" -version = "1.4.21" +version = "1.4.22" description = "DBCA internal corporate application to download and serve data from remote tracking devices." authors = ["DBCA OIM "] license = "Apache-2.0" @@ -20,8 +20,8 @@ django-geojson = "4.1.0" unicodecsv = "0.14.1" whitenoise = { version = "6.8.2", extras = ["brotli"] } azure-storage-blob = "12.23.1" -sentry-sdk = { version = "2.19.0", extras = ["django"] } -uvicorn = {extras = ["standard"], version = "^0.34.0"} +sentry-sdk = {version = "2.19.2", extras = ["django"]} +uvicorn = { extras = ["standard"], version = "^0.34.0" } uvicorn-worker = "^0.3.0" orjson = "3.10.13" From 6a3847b9882999e98b329972290018a00c6f63a5 Mon Sep 17 00:00:00 2001 From: Ashley Felton Date: Tue, 31 Dec 2024 14:23:23 +0800 Subject: [PATCH 6/6] Tweak unit tests. --- tracking/test_views.py | 38 +++++++++++++------------------------- 1 file changed, 13 insertions(+), 25 deletions(-) diff --git a/tracking/test_views.py b/tracking/test_views.py index d5aaf93..9c59600 100644 --- a/tracking/test_views.py +++ b/tracking/test_views.py @@ -1,73 +1,61 @@ +import random from datetime import timedelta + from django.contrib.auth.models import User from django.contrib.gis.geos import Point from django.test import TestCase from django.urls import reverse from django.utils import timezone from mixer.backend.django import mixer -import random from tracking.models import Device, LoggedPoint class ViewTestCase(TestCase): - def setUp(self): point = Point(random.uniform(32.0, 34.0), random.uniform(-115.0, -116.0)) self.device = mixer.blend(Device, seen=timezone.now(), point=point) - # Generate a short tracking history. mixer.blend(LoggedPoint, device=self.device, seen=self.device.seen, point=point) - point.x = point.x + random.uniform(-0.01, 0.01) - point.y = point.y + random.uniform(-0.01, 0.01) - mixer.blend(LoggedPoint, device=self.device, seen=self.device.seen - timedelta(minutes=3), point=point) - point.x = point.x + random.uniform(-0.01, 0.01) - point.y = point.y + random.uniform(-0.01, 0.01) - mixer.blend(LoggedPoint, device=self.device, seen=self.device.seen - timedelta(minutes=6), point=point) - point.x = point.x + random.uniform(-0.01, 0.01) - point.y = point.y + random.uniform(-0.01, 0.01) - mixer.blend(LoggedPoint, device=self.device, seen=self.device.seen - timedelta(minutes=9), point=point) - + # Generate a short tracking history. + for i in range(1, 5): + point.x = point.x + random.uniform(-0.01, 0.01) + point.y = point.y + random.uniform(-0.01, 0.01) + mixer.blend(LoggedPoint, device=self.device, seen=self.device.seen - timedelta(minutes=i), point=point) # Login self.client.force_login(User.objects.create(username="testuser")) def test_device_csv_download(self): - """Test the devices.csv download view - """ + """Test the devices.csv download view""" url = reverse("device_csv") response = self.client.get(url) self.assertEqual(response.status_code, 200) def test_device_geojson_download(self): - """Test the devices.geojson download view - """ + """Test the devices.geojson download view""" url = reverse("device_geojson") response = self.client.get(url) self.assertEqual(response.status_code, 200) def test_device_history_geojson_view(self): - """Test the device history GeoJSON view - """ + """Test the device history GeoJSON view""" url = reverse("device_history_geojson", kwargs={"device_id": self.device.pk}) response = self.client.get(url) self.assertEqual(response.status_code, 200) def test_device_history_csv_view(self): - """Test the device history CSV view - """ + """Test the device history CSV view""" url = reverse("device_history_csv", kwargs={"device_id": self.device.pk}) response = self.client.get(url) self.assertEqual(response.status_code, 200) def test_device_route_geojson_view(self): - """Test the device route GeoJSON view - """ + """Test the device route GeoJSON view""" url = reverse("device_route_geojson", kwargs={"device_id": self.device.pk}) response = self.client.get(url) self.assertEqual(response.status_code, 200) def test_resource_map_view(self): - """Test the resource map view - """ + """Test the resource map view""" url = reverse("resource_map") response = self.client.get(url) self.assertEqual(response.status_code, 200)