diff --git a/.copilot/image_build_run.sh b/.copilot/image_build_run.sh index f089c13..59f28bf 100755 --- a/.copilot/image_build_run.sh +++ b/.copilot/image_build_run.sh @@ -6,6 +6,6 @@ set -e # Add commands below to run inside the container after all the other buildpacks have been applied export BUILD_STEP='True' export COPILOT_ENVIRONMENT_NAME='build' -export DJANGO_SETTINGS_MODULE="config.settings.base" +export DJANGO_SETTINGS_MODULE="fbr.settings" -poetry run python fbr/manage.py collectstatic --noinput +poetry run python manage.py collectstatic --noinput diff --git a/.github/workflows/code_quality.yml b/.github/workflows/code_quality.yml index 4ad2462..28b5c5b 100644 --- a/.github/workflows/code_quality.yml +++ b/.github/workflows/code_quality.yml @@ -65,9 +65,9 @@ jobs: run: | npm install npm run build - DJANGO_SETTINGS_MODULE=config.settings.local poetry run fbr/manage.py collectstatic --noinput + DJANGO_SETTINGS_MODULE=fbr.settings poetry run python manage.py collectstatic --noinput - # poetry run fbr/manage.py makemigrations --check --dry-run + # poetry run manage.py makemigrations --check --dry-run # - name: Run tests # run: poetry run pytest fbr/tests diff --git a/.secrets.baseline b/.secrets.baseline index 316ee80..9dafacc 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -133,9 +133,9 @@ "filename": "Makefile", "hashed_secret": "afc848c316af1a89d49826c5ae9d00ed769415f3", "is_verified": false, - "line_number": 102 + "line_number": 65 } ] }, - "generated_at": "2024-09-09T12:08:54Z" + "generated_at": "2024-12-10T03:50:08Z" } diff --git a/local_deployment/Dockerfile b/Dockerfile similarity index 79% rename from local_deployment/Dockerfile rename to Dockerfile index e149339..0627c9d 100644 --- a/local_deployment/Dockerfile +++ b/Dockerfile @@ -25,7 +25,7 @@ ENV PYTHONUNBUFFERED=1 \ PYTHONDONTWRITEBYTECODE=1 \ DEBUG=1 \ DJANGO_ADMIN=1 \ - DJANGO_SETTINGS_MODULE=config.settings.local + DJANGO_SETTINGS_MODULE=fbr.settings # Install nodejs RUN apt install -y curl && \ @@ -33,10 +33,17 @@ RUN apt install -y curl && \ apt install -y nodejs WORKDIR /app -COPY . /app -# Install poetry and project dependencies -RUN pip install poetry==1.8.3 && \ - poetry install --without dev +RUN pip install poetry + +# Copy only the requirements.txt into the container +COPY requirements.txt /app/ + +# Install the dependencies specified in requirements.txt +RUN pip install --no-cache-dir -r requirements.txt + +COPY . /app -CMD ["local_deployment/entry.sh"] +COPY entry.sh /entry.sh +RUN chmod +x /entry.sh +ENTRYPOINT ["/entry.sh"] diff --git a/Makefile b/Makefile index 09264c6..2588f53 100644 --- a/Makefile +++ b/Makefile @@ -34,14 +34,14 @@ drop-database: # Delete project's postgres database fi build: # Build docker containers for local execution - docker build --no-cache -f local_deployment/Dockerfile -t local_deployment . + docker build --no-cache -f Dockerfile -t local_deployment . docker compose build collectstatic: # Run Django collectstatic - docker compose run --rm web poetry run python fbr/manage.py collectstatic --noinput + docker compose run --rm web poetry run python manage.py collectstatic --noinput admin: # Create a superuser - docker compose exec web poetry run python fbr/manage.py createsuperuser --username admin --email admin@localhost + docker compose exec web poetry run python manage.py createsuperuser --username admin --email admin@localhost first-use: # Initialise for local execution @echo "$(COLOUR_GREEN)Preparing for first use$(COLOUR_NONE)" @@ -62,6 +62,7 @@ first-use: # Initialise for local execution @echo "$(COLOUR_GREEN)Destroy containers with 'make down'$(COLOUR_NONE)" up: # Build, (re)create and start containers + export DATABASE_URL=postgres://postgres:postgres@localhost:5432/fbr docker compose up -d @echo "$(COLOUR_GREEN)Services are up - use 'make logs' to view service logs$(COLOUR_NONE)" @@ -90,27 +91,29 @@ logs: # View container logs docker compose logs -f -t test: # Run tests - pytest fbr/tests --cov-report term + pytest app/tests --cov-report term bdd: # Run BDD tests - HEADLESS_MODE=false SLOW_MO_MS=500 behave ./fbr/tests/bdd/features/ --tags=LOCAL + HEADLESS_MODE=false SLOW_MO_MS=500 behave ./app/tests/bdd/features/ --tags=LOCAL django-shell: # Run a Django shell (on container) - docker compose run web poetry run python fbr/manage.py shell + docker compose run web poetry run python manage.py shell django-shell-local: # Run a Django shell (local django instance) DATABASE_URL=postgres://postgres:postgres@localhost:5432/fbr \ DEBUG=True \ DJANGO_ADMIN=False \ DJANGO_SECRET_KEY=walls-have-ears \ - DJANGO_SETTINGS_MODULE=config.settings.local \ - poetry run python fbr/manage.py shell + DJANGO_SETTINGS_MODULE=fbr.settings \ + poetry run python manage.py shell migrate: # Run Django migrate - docker compose run --rm web poetry run python fbr/manage.py migrate --noinput + export DATABASE_URL=postgres://postgres:postgres@localhost:5432/fbr && \ + python manage.py migrate migrations: # Run Django makemigrations - docker compose run --rm web poetry run python fbr/manage.py makemigrations --noinput + export DATABASE_URL=postgres://postgres:postgres@localhost:5432/fbr && \ + python manage.py makemigrations lint: # Run all linting make black @@ -126,8 +129,8 @@ secrets-baseline: # Generate a new secrets baseline file poetry run detect-secrets scan > .secrets.baseline rebuild_cache: - export PYTHONPATH=./fbr && \ - export DJANGO_SETTINGS_MODULE='fbr.config.settings.local' && \ + export PYTHONPATH=. && \ + export DJANGO_SETTINGS_MODULE='fbr.settings' && \ export DATABASE_URL=postgres://postgres:postgres@localhost:5432/fbr && \ poetry install && \ poetry run rebuild-cache @@ -137,5 +140,11 @@ setup_local: # Set up the local environment $(MAKE) first-use $(MAKE) start $(MAKE) migrate - $(MAKE) rebuild_cache @echo "$(COLOUR_GREEN)Local setup complete.$(COLOUR_NONE)" + +setup_local_force_rebuild: + @echo "$(COLOUR_GREEN)Will run initial setup, followed by cache rebuild for local environment...$(COLOUR_NONE)" + $(MAKE) setup_local + @echo "$(COLOUR_GREEN)Manual cache rebuild (not using Celery task)...$(COLOUR_NONE)" + $(MAKE) rebuild_cache + @echo "$(COLOUR_GREEN)Cache rebuilt complete.$(COLOUR_NONE)" diff --git a/Procfile b/Procfile index 3ff34cd..801ed80 100644 --- a/Procfile +++ b/Procfile @@ -1,3 +1,4 @@ web: bash paas_entrypoint.sh -celery-worker: celery --app fbr.config.celery worker --task-events --loglevel INFO -celery-beat: celery --app fbr.config.celery beat --loglevel INFO +celery-worker: celery --app fbr.celery_app worker --task-events --loglevel INFO +celery-beat: celery --app fbr.celery_app beat --loglevel INFO +check: python manage.py check diff --git a/fbr/cache/__init__.py b/app/__init__.py similarity index 100% rename from fbr/cache/__init__.py rename to app/__init__.py diff --git a/fbr/core/__init__.py b/app/cache/__init__.py similarity index 100% rename from fbr/core/__init__.py rename to app/cache/__init__.py diff --git a/fbr/cache/construction_legislation.py b/app/cache/construction_legislation.py similarity index 100% rename from fbr/cache/construction_legislation.py rename to app/cache/construction_legislation.py diff --git a/fbr/cache/legislation.py b/app/cache/legislation.py similarity index 93% rename from fbr/cache/legislation.py rename to app/cache/legislation.py index 949f2db..152c2ac 100644 --- a/fbr/cache/legislation.py +++ b/app/cache/legislation.py @@ -4,16 +4,17 @@ import logging import re +import time import xml.etree.ElementTree as ET # nosec BXXX from typing import Optional import requests # type: ignore -from fbr.cache.construction_legislation import construction_legislation_dataframe -from fbr.search.config import SearchDocumentConfig -from fbr.search.utils.date import convert_date_string_to_obj -from fbr.search.utils.documents import ( # noqa: E501 +from app.cache.construction_legislation import construction_legislation_dataframe +from app.search.config import SearchDocumentConfig +from app.search.utils.date import convert_date_string_to_obj +from app.search.utils.documents import ( # noqa: E501 generate_short_uuid, insert_or_update_document, ) @@ -123,6 +124,8 @@ def build_cache(self, config: SearchDocumentConfig): logger.info("building legislation cache...") dataset = construction_legislation_dataframe() + failed_url_fetches = [] + # For each row, get the URL from the column named # 'URI to Extract XML Data' # and store the XML data in a list @@ -185,10 +188,15 @@ def build_cache(self, config: SearchDocumentConfig): # Insert or update the document insert_or_update_document(document_json) + + # # Sleep for a short time to avoid rate limiting + # time.sleep(0.5) except Exception as e: logger.error(f"error fetching data from {url}: {e}") - raise e + failed_url_fetches.append(url) + if failed_url_fetches: + logger.warning(f"failed to fetch data {len(failed_url_fetches)} legislation sources: {failed_url_fetches}") def _to_json( self, description, diff --git a/fbr/cache/manage_cache.py b/app/cache/manage_cache.py similarity index 54% rename from fbr/cache/manage_cache.py rename to app/cache/manage_cache.py index 361b279..2225175 100644 --- a/fbr/cache/manage_cache.py +++ b/app/cache/manage_cache.py @@ -3,23 +3,24 @@ import django -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local") -django.setup() +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "fbr.settings") +# Initialize Django setup +django.setup() import time -from fbr.cache.legislation import Legislation -from fbr.cache.public_gateway import PublicGateway -from fbr.search.config import SearchDocumentConfig -from fbr.search.utils.documents import clear_all_documents +from app.cache.legislation import Legislation +from app.cache.public_gateway import PublicGateway +from app.search.config import SearchDocumentConfig +from app.search.utils.documents import clear_all_documents def rebuild_cache(): try: start = time.time() clear_all_documents() - config = SearchDocumentConfig(search_query="", timeout=20) + config = SearchDocumentConfig(search_query="", timeout=3) Legislation().build_cache(config) PublicGateway().build_cache(config) end = time.time() diff --git a/fbr/cache/public_gateway.py b/app/cache/public_gateway.py similarity index 96% rename from fbr/cache/public_gateway.py rename to app/cache/public_gateway.py index 3a1e788..99483f6 100644 --- a/fbr/cache/public_gateway.py +++ b/app/cache/public_gateway.py @@ -4,8 +4,8 @@ import requests # type: ignore -from fbr.search.utils.date import convert_date_string_to_obj -from fbr.search.utils.documents import ( # noqa: E501 +from app.search.utils.date import convert_date_string_to_obj +from app.search.utils.documents import ( # noqa: E501 generate_short_uuid, insert_or_update_document, ) diff --git a/fbr/core/models/__init__.py b/app/core/__init__.py similarity index 100% rename from fbr/core/models/__init__.py rename to app/core/__init__.py diff --git a/fbr/core/admin.py b/app/core/admin.py similarity index 100% rename from fbr/core/admin.py rename to app/core/admin.py diff --git a/fbr/core/apps.py b/app/core/apps.py similarity index 90% rename from fbr/core/apps.py rename to app/core/apps.py index 5ef4a89..de290af 100644 --- a/fbr/core/apps.py +++ b/app/core/apps.py @@ -2,6 +2,6 @@ class CoreConfig(AppConfig): - name = "core" + name = "app.core" verbose_name = "Find business regulations core application functionality" default_auto_field = "django.db.models.BigAutoField" diff --git a/fbr/core/cookies.py b/app/core/cookies.py similarity index 100% rename from fbr/core/cookies.py rename to app/core/cookies.py diff --git a/fbr/core/forms.py b/app/core/forms.py similarity index 100% rename from fbr/core/forms.py rename to app/core/forms.py diff --git a/fbr/core/healthcheck.py b/app/core/healthcheck.py similarity index 100% rename from fbr/core/healthcheck.py rename to app/core/healthcheck.py diff --git a/fbr/core/templatetags/__init__.py b/app/core/models/__init__.py similarity index 100% rename from fbr/core/templatetags/__init__.py rename to app/core/models/__init__.py diff --git a/fbr/core/templates/accessibility_statement.html b/app/core/templates/accessibility_statement.html similarity index 100% rename from fbr/core/templates/accessibility_statement.html rename to app/core/templates/accessibility_statement.html diff --git a/fbr/core/templates/cookies.html b/app/core/templates/cookies.html similarity index 100% rename from fbr/core/templates/cookies.html rename to app/core/templates/cookies.html diff --git a/fbr/core/templates/home.html b/app/core/templates/home.html similarity index 100% rename from fbr/core/templates/home.html rename to app/core/templates/home.html diff --git a/fbr/core/templates/privacy_notice.html b/app/core/templates/privacy_notice.html similarity index 100% rename from fbr/core/templates/privacy_notice.html rename to app/core/templates/privacy_notice.html diff --git a/fbr/search/__init__.py b/app/core/templatetags/__init__.py similarity index 100% rename from fbr/search/__init__.py rename to app/core/templatetags/__init__.py diff --git a/fbr/core/templatetags/cookie_tags.py b/app/core/templatetags/cookie_tags.py similarity index 100% rename from fbr/core/templatetags/cookie_tags.py rename to app/core/templatetags/cookie_tags.py diff --git a/fbr/core/templatetags/service_problem_tags.py b/app/core/templatetags/service_problem_tags.py similarity index 100% rename from fbr/core/templatetags/service_problem_tags.py rename to app/core/templatetags/service_problem_tags.py diff --git a/fbr/core/templatetags/url_replace_tags.py b/app/core/templatetags/url_replace_tags.py similarity index 100% rename from fbr/core/templatetags/url_replace_tags.py rename to app/core/templatetags/url_replace_tags.py diff --git a/fbr/core/views.py b/app/core/views.py similarity index 100% rename from fbr/core/views.py rename to app/core/views.py diff --git a/fbr/search/migrations/__init__.py b/app/search/__init__.py similarity index 100% rename from fbr/search/migrations/__init__.py rename to app/search/__init__.py diff --git a/fbr/search/apps.py b/app/search/apps.py similarity index 95% rename from fbr/search/apps.py rename to app/search/apps.py index 608fabf..d2ff9c8 100644 --- a/fbr/search/apps.py +++ b/app/search/apps.py @@ -13,6 +13,6 @@ class SearchConfig(AppConfig): """ - name = "search" + name = "app.search" verbose_name = "Find business regulations application functionality" default_auto_field = "django.db.models.BigAutoField" diff --git a/fbr/search/config.py b/app/search/config.py similarity index 100% rename from fbr/search/config.py rename to app/search/config.py diff --git a/fbr/search/migrations/0001_initial.py b/app/search/migrations/0001_initial.py similarity index 100% rename from fbr/search/migrations/0001_initial.py rename to app/search/migrations/0001_initial.py diff --git a/fbr/search/tests/__init__.py b/app/search/migrations/__init__.py similarity index 100% rename from fbr/search/tests/__init__.py rename to app/search/migrations/__init__.py diff --git a/fbr/search/models.py b/app/search/models.py similarity index 100% rename from fbr/search/models.py rename to app/search/models.py diff --git a/fbr/search/templates/django-fbr.html b/app/search/templates/django-fbr.html similarity index 100% rename from fbr/search/templates/django-fbr.html rename to app/search/templates/django-fbr.html diff --git a/fbr/search/templates/document.html b/app/search/templates/document.html similarity index 100% rename from fbr/search/templates/document.html rename to app/search/templates/document.html diff --git a/fbr/search/templates/intro.html b/app/search/templates/intro.html similarity index 100% rename from fbr/search/templates/intro.html rename to app/search/templates/intro.html diff --git a/fbr/search/templates/react-fbr.html b/app/search/templates/react-fbr.html similarity index 100% rename from fbr/search/templates/react-fbr.html rename to app/search/templates/react-fbr.html diff --git a/fbr/search/utils/__init__.py b/app/search/tests/__init__.py similarity index 100% rename from fbr/search/utils/__init__.py rename to app/search/tests/__init__.py diff --git a/fbr/search/tests/test_search.py b/app/search/tests/test_search.py similarity index 92% rename from fbr/search/tests/test_search.py rename to app/search/tests/test_search.py index fcc1161..f81d997 100644 --- a/fbr/search/tests/test_search.py +++ b/app/search/tests/test_search.py @@ -2,18 +2,18 @@ from unittest.mock import MagicMock, call, patch -from search.utils.search import create_search_query +from app.search.utils.search import create_search_query class TestCreateSearchQuery(unittest.TestCase): - @patch("search.utils.search.SearchQuery", autospec=True) + @patch("app.search.utils.search.SearchQuery", autospec=True) def test_single_word_query(self, mock_search_query): result = create_search_query("test") mock_search_query.assert_called_with("test", search_type="plain") self.assertEqual(result, mock_search_query.return_value) - @patch("search.utils.search.SearchQuery", autospec=True) + @patch("app.search.utils.search.SearchQuery", autospec=True) def test_implicit_and_search_operator_query(self, mock_search_query): # Mock SearchQuery instances mock_query1 = MagicMock(name="MockQuery1") @@ -35,7 +35,7 @@ def test_implicit_and_search_operator_query(self, mock_search_query): # Assert the AND operation was applied mock_query1.__and__.assert_called_once_with(mock_query2) - @patch("search.utils.search.SearchQuery", autospec=True) + @patch("app.search.utils.search.SearchQuery", autospec=True) def test_multiple_implicit_and_search_operator_query( self, mock_search_query ): @@ -61,7 +61,7 @@ def test_multiple_implicit_and_search_operator_query( # Assert the AND operation was applied mock_query1.__and__.assert_called_with(mock_query3) - @patch("search.utils.search.SearchQuery", autospec=True) + @patch("app.search.utils.search.SearchQuery", autospec=True) def test_and_search_operator_query(self, mock_search_query): # Mock SearchQuery instances mock_query1 = MagicMock(name="MockQuery1") @@ -83,7 +83,7 @@ def test_and_search_operator_query(self, mock_search_query): # Assert the AND operation was applied mock_query1.__and__.assert_called_once_with(mock_query2) - @patch("search.utils.search.SearchQuery", autospec=True) + @patch("app.search.utils.search.SearchQuery", autospec=True) def test_multiple_and_search_operator_query(self, mock_search_query): # Mock SearchQuery instances mock_query1 = MagicMock(name="MockQuery1") @@ -107,7 +107,7 @@ def test_multiple_and_search_operator_query(self, mock_search_query): # Assert the AND operation was applied mock_query1.__and__.assert_called_with(mock_query3) - @patch("search.utils.search.SearchQuery", autospec=True) + @patch("app.search.utils.search.SearchQuery", autospec=True) def test_or_search_operator_query(self, mock_search_query): # Mock SearchQuery instances mock_query1 = MagicMock(name="MockQuery1") @@ -129,7 +129,7 @@ def test_or_search_operator_query(self, mock_search_query): # Assert the AND operation was applied mock_query1.__or__.assert_called_once_with(mock_query2) - @patch("search.utils.search.SearchQuery", autospec=True) + @patch("app.search.utils.search.SearchQuery", autospec=True) def test_multple_or_search_operator_query(self, mock_search_query): # Mock SearchQuery instances mock_query1 = MagicMock(name="MockQuery1") @@ -153,7 +153,7 @@ def test_multple_or_search_operator_query(self, mock_search_query): # Assert the AND operation was applied mock_query1.__or__.assert_called_with(mock_query3) - @patch("search.utils.search.SearchQuery", autospec=True) + @patch("app.search.utils.search.SearchQuery", autospec=True) def test_multiple_or_search_operator_query(self, mock_search_query): # Mock SearchQuery instances mock_query1 = MagicMock(name="MockQuery1") @@ -177,7 +177,7 @@ def test_multiple_or_search_operator_query(self, mock_search_query): # Assert the AND operation was applied mock_query1.__or__.assert_called_with(mock_query3) - @patch("search.utils.search.SearchQuery", autospec=True) + @patch("app.search.utils.search.SearchQuery", autospec=True) def test_phrase_search_query(self, mock_search_query): result = create_search_query('"test trial"') mock_search_query.assert_called_with( @@ -185,7 +185,7 @@ def test_phrase_search_query(self, mock_search_query): ) self.assertEqual(result, mock_search_query.return_value) - @patch("search.utils.search.SearchQuery", autospec=True) + @patch("app.search.utils.search.SearchQuery", autospec=True) def test_and_multiple_single_single_phrase_search_query( self, mock_search_query ): @@ -221,7 +221,7 @@ def test_and_multiple_single_single_phrase_search_query( # Assert the AND operation was applied mock_query1.__and__.assert_called_with(mock_query5) - @patch("search.utils.search.SearchQuery", autospec=True) + @patch("app.search.utils.search.SearchQuery", autospec=True) def test_single_or_and_search_operator_query(self, mock_search_query): # Mock SearchQuery instances mock_query1 = MagicMock(name="MockQuery1") diff --git a/app/search/utils/__init__.py b/app/search/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/fbr/search/utils/date.py b/app/search/utils/date.py similarity index 100% rename from fbr/search/utils/date.py rename to app/search/utils/date.py diff --git a/fbr/search/utils/documents.py b/app/search/utils/documents.py similarity index 98% rename from fbr/search/utils/documents.py rename to app/search/utils/documents.py index 28207b6..8ee2161 100644 --- a/fbr/search/utils/documents.py +++ b/app/search/utils/documents.py @@ -6,7 +6,7 @@ from django.db.models import QuerySet -from search.models import DataResponseModel, logger +from app.search.models import DataResponseModel, logger def clear_all_documents(): diff --git a/fbr/search/utils/paginate.py b/app/search/utils/paginate.py similarity index 98% rename from fbr/search/utils/paginate.py rename to app/search/utils/paginate.py index 2a90bbd..649e53b 100644 --- a/fbr/search/utils/paginate.py +++ b/app/search/utils/paginate.py @@ -4,7 +4,7 @@ from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator from django.db.models import QuerySet -from search.config import SearchDocumentConfig +from app.search.config import SearchDocumentConfig logger = logging.getLogger(__name__) diff --git a/fbr/search/utils/search.py b/app/search/utils/search.py similarity index 94% rename from fbr/search/utils/search.py rename to app/search/utils/search.py index a01f47e..3400a89 100644 --- a/fbr/search/utils/search.py +++ b/app/search/utils/search.py @@ -6,11 +6,11 @@ from django.db.models import F, Func, Q, QuerySet from django.http import HttpRequest -from search.config import SearchDocumentConfig -from search.models import DataResponseModel -from search.utils.documents import calculate_score -from search.utils.paginate import paginate -from search.utils.terms import sanitize_input +from app.search.config import SearchDocumentConfig +from app.search.models import DataResponseModel +from app.search.utils.documents import calculate_score +from app.search.utils.paginate import paginate +from app.search.utils.terms import sanitize_input logger = logging.getLogger(__name__) @@ -145,7 +145,9 @@ def search_database( return queryset -def search(context: dict, request: HttpRequest) -> dict: +def search( + context: dict, request: HttpRequest +) -> dict | QuerySet[DataResponseModel]: logger.debug("received search request: %s", request) start_time = time.time() @@ -174,6 +176,9 @@ def search(context: dict, request: HttpRequest) -> dict: # Search across specific fields results = search_database(config) + if config.limit == "*": + return results + # convert search_results into json pag_start_time = time.time() context = paginate(context, config, results) diff --git a/fbr/search/utils/terms.py b/app/search/utils/terms.py similarity index 100% rename from fbr/search/utils/terms.py rename to app/search/utils/terms.py diff --git a/app/search/views.py b/app/search/views.py new file mode 100644 index 0000000..5424f32 --- /dev/null +++ b/app/search/views.py @@ -0,0 +1,74 @@ +import logging + +from django.conf import settings +from django.http import HttpRequest, HttpResponse +from django.shortcuts import render +from django.views.decorators.http import require_http_methods + +from app.search.config import SearchDocumentConfig +from app.search.utils.search import search, search_database + +logger = logging.getLogger(__name__) + + +@require_http_methods(["GET"]) +def document(request: HttpRequest, id) -> HttpResponse: + """ + Document details view. + + Handles the GET request to fetch details based on the provided id. + """ + context = { + "service_name": settings.SERVICE_NAME_SEARCH, + } + + if not id: + context["error"] = "id parameter is required" + return render(request, template_name="document.html", context=context) + + # Create a search configuration object with the provided id + config = SearchDocumentConfig(search_query="", id=id) + + try: + queryset = search_database(config) + context["result"] = queryset + except Exception as e: + logger.error("error fetching details: %s", e) + context["error"] = f"error fetching details: {e}" + + return render(request, template_name="document.html", context=context) + + +@require_http_methods(["GET"]) +def search_django(request: HttpRequest): + """ + Search view. + + Renders the Django based search page. + """ + context = { + "service_name": settings.SERVICE_NAME_SEARCH, + } + + context = search(context, request) + return render(request, template_name="django-fbr.html", context=context) + + +@require_http_methods(["GET"]) +def search_react(request: HttpRequest) -> HttpResponse: + """ + Search view. + + Renders the React based search page. + """ + + context = { + "service_name": settings.SERVICE_NAME_SEARCH, + "document_types": { + "standard": "Standard", + "guidance": "Guidance", + "legislation": "Legislation", + }, + } + + return render(request, template_name="react-fbr.html", context=context) diff --git a/fbr/templates/base.html b/app/templates/base.html similarity index 100% rename from fbr/templates/base.html rename to app/templates/base.html diff --git a/fbr/templates/cookie_banner.html b/app/templates/cookie_banner.html similarity index 100% rename from fbr/templates/cookie_banner.html rename to app/templates/cookie_banner.html diff --git a/fbr/templates/service_problem.html b/app/templates/service_problem.html similarity index 100% rename from fbr/templates/service_problem.html rename to app/templates/service_problem.html diff --git a/celery_worker/__init__.py b/celery_worker/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/fbr/cache/tasks.py b/celery_worker/tasks.py similarity index 75% rename from fbr/cache/tasks.py rename to celery_worker/tasks.py index 096783f..28d22c9 100644 --- a/fbr/cache/tasks.py +++ b/celery_worker/tasks.py @@ -1,14 +1,15 @@ import time -from config.celery import celery_app -from fbr.cache.legislation import Legislation -from fbr.cache.public_gateway import PublicGateway -from fbr.search.config import SearchDocumentConfig -from fbr.search.utils.documents import clear_all_documents +from celery import shared_task +from app.cache.legislation import Legislation +from app.cache.public_gateway import PublicGateway +from app.search.config import SearchDocumentConfig +from app.search.utils.documents import clear_all_documents -@celery_app.task(name="fbr.cache.tasks.rebuild_cache") -def rebuild_cache() -> None: + +@shared_task(name="celery_worker.tasks.rebuild_cache") +def rebuild_cache(): """ Rebuilds the cache for search documents across various components by clearing all existing documents. The process is timed, and the diff --git a/docker-compose.yml b/docker-compose.yml index 6519f5c..4ab610b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,6 +2,8 @@ volumes: postgres_data: driver: local + redis_data: + driver: local services: db: @@ -21,10 +23,13 @@ services: retries: 3 networks: - proxynet + web: build: context: . - dockerfile: local_deployment/Dockerfile + cache_from: + - fbr/application:latest + image: fbr/application:latest env_file: - local.env ports: @@ -37,6 +42,63 @@ services: networks: - proxynet + redis: + image: redis + # Expose port so we can query it for debugging + ports: + - "6379:6379" + + celery-worker: + build: + context: . + cache_from: + - fbr/application:latest + image: fbr/application:latest + command: celery --app fbr.celery_app worker --task-events --loglevel INFO + entrypoint: '' + volumes: + - .:/app + healthcheck: + test: [ "CMD-SHELL", "python -m dbt_copilot_python.celery_health_check.healthcheck" ] + interval: 10s + timeout: 5s + retries: 2 + start_period: 5s + depends_on: + - redis + - db + environment: + REDIS_ENDPOINT: redis://redis:6379 + DEBUG: true + DJANGO_SETTINGS_MODULE: fbr.settings + RDS_POSTGRES_CREDENTIALS: '{"password":"postgres","dbname":"fbr","engine":"postgres","port":5432,"dbInstanceIdentifier":"xxx","host":"db","username":"postgres"}' + DATABASE_URL: postgres://postgres:postgres@host.docker.internal:5432/fbr # pragma: allowlist secret + + celery-beats: + build: + context: . + cache_from: + - fbr/application:latest + image: fbr/application:latest + command: celery --app fbr.celery_app beat --loglevel INFO + entrypoint: '' + volumes: + - .:/app + healthcheck: + test: [ "CMD-SHELL", "python -m dbt_copilot_python.celery_health_check.healthcheck" ] + interval: 10s + timeout: 5s + retries: 2 + start_period: 5s + depends_on: + - redis + - db + environment: + REDIS_ENDPOINT: redis://redis:6379 + DEBUG: true + DJANGO_SETTINGS_MODULE: fbr.settings + RDS_POSTGRES_CREDENTIALS: '{"password":"postgres","dbname":"fbr","engine":"postgres","port":5432,"dbInstanceIdentifier":"xxx","host":"db","username":"postgres"}' + networks: proxynet: name: fbr_network diff --git a/local_deployment/entry.sh b/entry.sh similarity index 53% rename from local_deployment/entry.sh rename to entry.sh index b644ad4..2c5b594 100755 --- a/local_deployment/entry.sh +++ b/entry.sh @@ -6,10 +6,8 @@ npm install echo "Bundling WebPack" npm run build -. "$(poetry env info --path)/bin/activate" - echo "Collecting Static Files" -python fbr/manage.py collectstatic --noinput +python manage.py collectstatic --noinput echo "Starting server" -python fbr/manage.py runserver 0.0.0.0:8080 +python manage.py runserver 0.0.0.0:8080 diff --git a/fbr/__init__.py b/fbr/__init__.py index 0062f8d..23bb48f 100644 --- a/fbr/__init__.py +++ b/fbr/__init__.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import, unicode_literals - -# This ensures the app is always imported when Django starts -# allowing shared_task to utilize this app. -from .config.celery import celery_app +from .celery_app import celery_app __all__ = ("celery_app",) diff --git a/fbr/config/asgi.py b/fbr/asgi.py similarity index 100% rename from fbr/config/asgi.py rename to fbr/asgi.py diff --git a/fbr/celery_app.py b/fbr/celery_app.py new file mode 100644 index 0000000..6e33420 --- /dev/null +++ b/fbr/celery_app.py @@ -0,0 +1,24 @@ +# isort:skip_file +# flake8: noqa + +import os + +from celery import Celery +from celery.schedules import crontab + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "fbr.settings") + +from dbt_copilot_python.celery_health_check import healthcheck + +celery_app = Celery("fbr_celery") +celery_app.config_from_object("django.conf:settings", namespace="CELERY") +celery_app.autodiscover_tasks() + +celery_app = healthcheck.setup(celery_app) + +celery_app.conf.beat_schedule = { + "schedule-fbr-cache-task": { + "task": "celery_worker.tasks.rebuild_cache", + "schedule": crontab(hour="1", minute="0"), # Runs daily at 1:00 AM + }, +} diff --git a/fbr/config/celery.py b/fbr/config/celery.py deleted file mode 100644 index cd0473b..0000000 --- a/fbr/config/celery.py +++ /dev/null @@ -1,34 +0,0 @@ -# isort:skip_file - -from __future__ import absolute_import, unicode_literals - -import os - -from celery import Celery -from celery.schedules import crontab -from dbt_copilot_python.celery_health_check import healthcheck - -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "fbr.config.settings.local") - -celery_app = Celery("fbr_celery") - -celery_app.config_from_object("django.conf:settings", namespace="CELERY") - -celery_app.autodiscover_tasks() - -celery_app = healthcheck.setup(celery_app) - - -@celery_app.on_after_configure.connect -def setup_periodic_tasks(sender, **kwargs): - schedule = get_cache_beat_schedule() - celery_app.conf.beat_schedule = schedule - - -def get_cache_beat_schedule(): - return { - "schedule-fbr-cache-task": { - "task": "fbr.cache.tasks.rebuild_cache", - "schedule": crontab(hour="1", minute="0"), # Runs daily at 1:00 AM - }, - } diff --git a/fbr/config/settings/base.py b/fbr/config/settings/base.py index 711de8b..7d20553 100644 --- a/fbr/config/settings/base.py +++ b/fbr/config/settings/base.py @@ -54,6 +54,7 @@ "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", + "rest_framework", ] LOCAL_APPS = [ diff --git a/fbr/config/settings/local.py b/fbr/config/settings/local.py index e6d9ab5..840912c 100644 --- a/fbr/config/settings/local.py +++ b/fbr/config/settings/local.py @@ -5,7 +5,6 @@ # Applications that are required to load before DJANGO_APPS BASE_APPS = [ "whitenoise.runserver_nostatic", # Serve static files via WhiteNoise - "rest_framework", ] INSTALLED_APPS = BASE_APPS + INSTALLED_APPS # noqa diff --git a/fbr/config/context_processors.py b/fbr/context_processors.py similarity index 100% rename from fbr/config/context_processors.py rename to fbr/context_processors.py diff --git a/fbr/search/views.py b/fbr/search/views.py deleted file mode 100644 index 9a7280b..0000000 --- a/fbr/search/views.py +++ /dev/null @@ -1,150 +0,0 @@ -import csv -import logging - -import pandas as pd - -from django.conf import settings -from django.http import HttpRequest, HttpResponse -from django.shortcuts import render -from django.views.decorators.http import require_http_methods - -from search.config import SearchDocumentConfig -from search.models import DataResponseModel -from search.utils.search import search, search_database - -logger = logging.getLogger(__name__) - - -@require_http_methods(["GET"]) -def document(request: HttpRequest, id) -> HttpResponse: - """ - Document details view. - - Handles the GET request to fetch details based on the provided id. - """ - context = { - "service_name": settings.SERVICE_NAME_SEARCH, - } - - if not id: - context["error"] = "id parameter is required" - return render(request, template_name="document.html", context=context) - - # Create a search configuration object with the provided id - config = SearchDocumentConfig(search_query="", id=id) - - try: - queryset = search_database(config) - context["result"] = queryset - except Exception as e: - logger.error("error fetching details: %s", e) - context["error"] = f"error fetching details: {e}" - - return render(request, template_name="document.html", context=context) - - -@require_http_methods(["GET"]) -def download_search_csv(request: HttpRequest) -> HttpResponse: - """ - Handles the download of search results as a CSV file. - - This view function is restricted to the GET HTTP method. - It accepts several query - - parameters to configure the search: - - `search`: A string to search within the documents. - - `document_type`: - A list of document types to filter the search results. - - `publisher`: A list of publishers to filter the search results. - - `sort`: A field name to sort the search results. - - The function constructs a `SearchDocumentConfig` object using the - received query parameters and performs a search using this - configuration. `DataResponseModel` objects from the search results - are retrieved and compiled into a list of dictionaries, which is - then converted into a DataFrame for demonstration purposes. - Finally, the ataFrame is written into a CSV file and returned as - an HTTP response with the appropriate content type and file - attachment headers. - """ - search_query = request.GET.get("search", "") - document_types = request.GET.getlist("document_type", "") - publishers = request.GET.getlist("publisher", None) - sort_by = request.GET.get("sort", None) - config = SearchDocumentConfig( - search_query, - document_types, - publisher_names=publishers, - sort_by=sort_by, - ) - # Perform search - search(config) - - # Get DataResponseModel objects from the search results - documents = DataResponseModel.objects.all() - - search_results = [] - for result in documents: - search_results.append( - { - "id": result["id"], - "title": result["title"], - "publisher": result["publisher"], - "description": result["description"], - "type": result["type"], - "date_modified": result["date_modified"], - } - ) - - # Convert search_results JSON object to DataFrame - # (for demonstration purposes) - search_results_df = pd.DataFrame(search_results) - - response = HttpResponse(content_type="text/csv") - response["Content-Disposition"] = ( - 'attachment; filename="search_results.csv"' - ) - - # Write the DataFrame to the response - writer = csv.writer(response) - writer.writerow(search_results_df.columns) # Write the header - - for _, row in search_results_df.iterrows(): - writer.writerow(row) - - return response - - -@require_http_methods(["GET"]) -def search_django(request: HttpRequest): - """ - Search view. - - Renders the Django based search page. - """ - context = { - "service_name": settings.SERVICE_NAME_SEARCH, - } - - context = search(context, request) - return render(request, template_name="django-fbr.html", context=context) - - -@require_http_methods(["GET"]) -def search_react(request: HttpRequest) -> HttpResponse: - """ - Search view. - - Renders the React based search page. - """ - - context = { - "service_name": settings.SERVICE_NAME_SEARCH, - "document_types": { - "standard": "Standard", - "guidance": "Guidance", - "legislation": "Legislation", - }, - } - - return render(request, template_name="react-fbr.html", context=context) diff --git a/fbr/settings.py b/fbr/settings.py new file mode 100644 index 0000000..9a82355 --- /dev/null +++ b/fbr/settings.py @@ -0,0 +1,290 @@ +"""Django base settings for Find business regulations project. + +Environment: +We use django-environ but do not read a `.env` file. Locally we provide +docker-compose an environment from `local.env` file in the project root, +using `env_file` field in `docker-compose.yml`. There is a `local.env.example` +in this repo. When deployed, the service retrieves these values from the +environment. + +NB: Some settings acquired using `env()` deliberately *do not* have defaults +as we want to get an `ImproperlyConfigured` exception. This highlights badly +configured deployments. +""" + +import os + +from pathlib import Path +from typing import Any + +import dj_database_url +import environ + +from django_log_formatter_asim import ASIMFormatter + +# Define the root directory (i.e. ) +root = environ.Path(__file__) - 4 # i.e. Repository root +SITE_ROOT = Path(root()) + +# Define the project base directory (i.e. /fbr) +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Get environment variables +env = environ.Env( + DEBUG=(bool, False), +) + +# Must be provided by the environment +SECRET_KEY = env( + "DJANGO_SECRET_KEY", default="find-business-regulations-secret-key" +) + +DEBUG = env("DEBUG", default=False) +DJANGO_ADMIN = env("DJANGO_ADMIN", default=False) +ALLOWED_HOSTS = env.list("ALLOWED_HOSTS", default=["localhost"]) +ENVIRONMENT = env( + "COPILOT_ENVIRONMENT_NAME", default="local" +) # TODO: Change to APP_ENV, updates required in deploy repo + +# Application definition +DJANGO_APPS = [ + "django_celery_beat", + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "rest_framework", + "app", + "app.core", + "app.search", + "celery_worker", +] + +THIRD_PARTY_APPS: list = [ + "webpack_loader", +] + +INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + +MIDDLEWARE = [ + "django.middleware.security.SecurityMiddleware", + "whitenoise.middleware.WhiteNoiseMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", +] + +DEFAULT_AUTO_FIELD = "django.db.models.AutoField" + +ROOT_URLCONF = "fbr.urls" + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [ + os.path.join(BASE_DIR, "app", "search", "templates"), + os.path.join(BASE_DIR, "app", "templates"), + ], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + "fbr.context_processors.google_tag_manager", + ], + }, + }, +] + +WSGI_APPLICATION = "fbr.wsgi.application" + +DATABASES: dict = {} +if DATABASE_URL := env("DATABASE_URL", default=None): + DATABASES = { + "default": { + **dj_database_url.parse( + DATABASE_URL, + engine="postgresql", + ), + "ENGINE": "django.db.backends.postgresql", + } + } +else: + DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "db.sqlite3", + } + } + +AUTH_PASSWORD_VALIDATORS = [ + { + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", # noqa: E501 + }, + { + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", # noqa: E501 + }, + { + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", # noqa: E501 + }, + { + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", # noqa: E501 + }, +] + +REDIS_ENDPOINT = env("REDIS_ENDPOINT", default="") + +# Celery +CELERY_BROKER_URL = env("REDIS_ENDPOINT", default="") +if CELERY_BROKER_URL and CELERY_BROKER_URL.startswith("rediss://"): + CELERY_BROKER_URL = f"{CELERY_BROKER_URL}?ssl_cert_reqs=CERT_REQUIRED" +CELERY_RESULT_BACKEND = CELERY_BROKER_URL +CELERY_ACCEPT_CONTENT = ["application/json"] +CELERY_RESULT_SERIALIZER = "json" +CELERY_BROKER_CONNECTION_RETRY_ON_STARTUP = True +CELERY_BEAT_SCHEDULER = "django_celery_beat.schedulers.DatabaseScheduler" +CELERY_RESULT_EXTENDED = True +CELERY_TASK_TIME_LIMIT = ( + 450 # Maximum runtime for a task in seconds (e.g., 7.5 minutes) +) +CELERY_TASK_SOFT_TIME_LIMIT = ( + 270 # Optional: Grace period before forced termination +) +CELERY_TIMEZONE = "UTC" +CELERY_ENABLE_UTC = True +USE_DEPRECATED_PYTZ = True + +# Internationalisation +LANGUAGE_CODE = "en-gb" +TIME_ZONE = "Europe/London" +USE_I18N = True +USE_TZ = True + +# Static files +STATICFILES_FINDERS = [ + "django.contrib.staticfiles.finders.FileSystemFinder", + "django.contrib.staticfiles.finders.AppDirectoriesFinder", +] +STATIC_ROOT = BASE_DIR / "static" +STATIC_URL = "static/" + +STORAGES = { + "staticfiles": { + "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage", + }, +} + +# Webpack + +STATICFILES_DIRS = [ + BASE_DIR / "front_end/", +] + +WEBPACK_LOADER = { + "DEFAULT": { + "CACHE": not DEBUG, + "BUNDLE_DIR_NAME": "webpack_bundles/", # must end with slash + "STATS_FILE": os.path.join(BASE_DIR, "webpack-stats.json"), + "POLL_INTERVAL": 0.1, + "TIMEOUT": None, + "LOADER_CLASS": "webpack_loader.loader.WebpackLoader", + } +} + + +# TODO: Use redis for cache? +CACHES = { + "default": { + "BACKEND": "django.core.cache.backends.locmem.LocMemCache", + } +} + +# Logging +LOGGING: dict[str, Any] = { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "asim_formatter": { + "()": ASIMFormatter, + }, + "simple": { + "style": "{", + "format": "{asctime} {levelname} {message}", + }, + }, + "handlers": { + "asim": { + "class": "logging.StreamHandler", + "formatter": "asim_formatter", + }, + "console": { + "class": "logging.StreamHandler", + "formatter": "simple", + }, + }, + "root": { + "handlers": ["console"], + "level": "INFO", + }, + "loggers": { + "django": { + "handlers": ["console"], + "level": "INFO", + "propagate": False, + }, + }, +} + +# Django Log Formatter ASIM settings +# See https://github.com/uktrade/django-log-formatter-asim#settings +DLFA_TRACE_HEADERS = ("X-B3-TraceId", "X-B3-SpanId") + +# Set the correct handlers when running in DBT Platform +# console handler set as default as it's easier to read +LOGGING["root"]["handlers"] = ["asim"] +LOGGING["loggers"]["django"]["handlers"] = ["asim"] + +# ------------------------------------------------------------------------------ +# The Find business regulations zone - specific service settings. +# ------------------------------------------------------------------------------ + +# Service + +SERVICE_NAME: str = "Find business regulations" +SERVICE_NAME_SEARCH: str = "Find business regulations" +CONTACT_EMAIL: str = "findbusinessregulations@businessandtrade.gov.uk" + +# Cookies +ANALYTICS_CONSENT_NAME: str = "analytics_consent" + +# DBT Data API +# DBT_DATA_API_URL = env( +# "DBT_DATA_API_URL", +# default="https://data.api.trade.gov.uk/v1/datasets/market-barriers/versions/v1.0.10/data?format=json", # noqa: E501 +# ) + +# HOSTNAME +HOSTNAME_MAP = { + "local": "http://localhost:8081", + "dev": "https://dev.find-business-regulations.uktrade.digital/", + "staging": "https://staging.find-business-regulations.uktrade.digital/", + "prod": "https://find-business-regulations.uktrade.digital/", +} + +HOSTNAME = HOSTNAME_MAP.get(ENVIRONMENT.lower(), HOSTNAME_MAP["prod"]) + +# Google Analytics (GA) +# Note: please consult the performance team before changing these settings +COOKIE_PREFERENCES_SET_NAME: str = "cookie_preferences_set" +COOKIE_ACCEPTED_GA_NAME: str = "accepted_ga_cookies" +GOOGLE_ANALYTICS_TAG_MANAGER_ID = env( + "GOOGLE_ANALYTICS_TAG_MANAGER_ID", default=None +) diff --git a/fbr/config/urls.py b/fbr/urls.py similarity index 62% rename from fbr/config/urls.py rename to fbr/urls.py index 0ab1934..e48bce0 100644 --- a/fbr/config/urls.py +++ b/fbr/urls.py @@ -1,62 +1,27 @@ """Find business regulations URL configuration.""" +import csv import logging -from rest_framework import routers, serializers, status, viewsets +import pandas as pd + +from rest_framework import routers, status, viewsets from rest_framework.decorators import action from rest_framework.response import Response from django.conf import settings from django.contrib import admin +from django.http import HttpResponse from django.urls import include, path -import core.views as core_views -import search.views as search_views +import app.core.views as core_views +import app.search.views as search_views -from search.models import DataResponseModel -from search.utils.search import get_publisher_names, search +from app.search.utils.search import get_publisher_names, search urls_logger = logging.getLogger(__name__) -# Serializers define the API representation. -class DataResponseSerializer(serializers.HyperlinkedModelSerializer): - class Meta: - model = DataResponseModel - fields = [ - "id", - "title", - "link", - "publisher", - "language", - "format", - "description", - "date_issued", - "date_modified", - "date_valid", - "audience", - "coverage", - "subject", - "type", - "license", - "regulatory_topics", - "status", - "date_uploaded_to_orp", - "has_format", - "is_format_of", - "has_version", - "is_version_of", - "references", - "is_referenced_by", - "has_part", - "is_part_of", - "is_replaced_by", - "replaces", - "related_legislation", - "id", - ] - - class DataResponseViewSet(viewsets.ModelViewSet): @action(detail=False, methods=["get"], url_path="search") def search(self, request, *args, **kwargs): @@ -113,9 +78,65 @@ def publishers(self, request, *args, **kwargs): ) +class DownloadDataResponseViewSet(viewsets.ModelViewSet): + @action(detail=False, methods=["get"], url_path="download_csv") + def download_csv(self, request, *args, **kwargs): + context = { + "service_name": settings.SERVICE_NAME_SEARCH, + } + + urls_logger.debug(f"download_csv - request: {request}") + + try: + # set the limit to '*' to get all results + request.GET = request.GET.copy() + request.GET["limit"] = "*" + + response_data = search(context, request) + urls_logger.debug(f"response_data: {response_data}") + + search_results = [] + for result in response_data: + urls_logger.debug(f"result: {result}") + + search_results.append( + { + "title": result["title"], + "publisher": result["publisher"], + "description": result["description"], + "type": result["type"], + "date": result["date_valid"], + } + ) + + search_results_df = pd.DataFrame(search_results) + + response = HttpResponse(content_type="text/csv") + response["Content-Disposition"] = ( + 'attachment; filename="search_results.csv"' + ) + + writer = csv.writer(response) + + # Write the DataFrame to the response + writer.writerow(search_results_df.columns) # Write the header + for _, row in search_results_df.iterrows(): + writer.writerow(row) + + return Response(response, status=status.HTTP_200_OK) + except Exception as e: + return Response( + data={"message": f"error building csv download response: {e}"}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) + + # Routers provide an easy way of automatically determining the URL conf. router = routers.DefaultRouter() router.register(r"v1", DataResponseViewSet, basename="search") +router.register( + r"v1/download_csv", DownloadDataResponseViewSet, basename="download_csv" +) router.register(r"v1/retrieve", PublishersViewSet, basename="publishers") urlpatterns = [ @@ -124,11 +145,6 @@ def publishers(self, request, *args, **kwargs): path("nojs/", search_views.search_django, name="search_django"), # If we choose to have a start page with green button, this is it: # path("", core_views.home, name="home"), - path( - "download_csv/", - search_views.download_search_csv, - name="download_csv", - ), path("document/", search_views.document, name="document"), path("healthcheck/", core_views.health_check, name="healthcheck"), path( diff --git a/fbr/config/version.py b/fbr/version.py similarity index 100% rename from fbr/config/version.py rename to fbr/version.py diff --git a/fbr/config/wsgi.py b/fbr/wsgi.py similarity index 82% rename from fbr/config/wsgi.py rename to fbr/wsgi.py index 5f3bd7e..1ff65b7 100644 --- a/fbr/config/wsgi.py +++ b/fbr/wsgi.py @@ -11,6 +11,6 @@ from django.core.wsgi import get_wsgi_application -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local") +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "fbr.settings") application = get_wsgi_application() diff --git a/local.env.example b/local.env.example index 40a5d87..303a204 100644 --- a/local.env.example +++ b/local.env.example @@ -6,7 +6,7 @@ ALLOWED_HOSTS=localhost DEBUG=True DJANGO_ADMIN=True -DJANGO_SETTINGS_MODULE=config.settings.local +DJANGO_SETTINGS_MODULE=fbr.settings DJANGO_SECRET_KEY=find-business-regulations-secret # Database Settings diff --git a/fbr/manage.py b/manage.py similarity index 88% rename from fbr/manage.py rename to manage.py index 37003e1..58ea086 100755 --- a/fbr/manage.py +++ b/manage.py @@ -6,8 +6,7 @@ def main(): """Run administrative tasks.""" - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local") - + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "fbr.settings") try: from django.core.management import execute_from_command_line except ImportError as exc: diff --git a/poetry.lock b/poetry.lock index ae26841..eba32c1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand. [[package]] name = "amqp" @@ -116,17 +116,17 @@ uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "boto3" -version = "1.35.71" +version = "1.35.77" description = "The AWS SDK for Python" optional = false python-versions = ">=3.8" files = [ - {file = "boto3-1.35.71-py3-none-any.whl", hash = "sha256:e2969a246bb3208122b3c349c49cc6604c6fc3fc2b2f65d99d3e8ccd745b0c16"}, - {file = "boto3-1.35.71.tar.gz", hash = "sha256:3ed7172b3d4fceb6218bb0ec3668c4d40c03690939c2fca4f22bb875d741a07f"}, + {file = "boto3-1.35.77-py3-none-any.whl", hash = "sha256:a09871805f8e462349a1c33c23eb413668df0bf68424e61d53518e1a7d883b2f"}, + {file = "boto3-1.35.77.tar.gz", hash = "sha256:cc819cdbccbc2d0dc185f1dcfe74cf3809489c4cae63c2e5d6a557aa0c5ab928"}, ] [package.dependencies] -botocore = ">=1.35.71,<1.36.0" +botocore = ">=1.35.77,<1.36.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.10.0,<0.11.0" @@ -135,13 +135,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.35.71" +version = "1.35.77" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">=3.8" files = [ - {file = "botocore-1.35.71-py3-none-any.whl", hash = "sha256:fc46e7ab1df3cef66dfba1633f4da77c75e07365b36f03bd64a3793634be8fc1"}, - {file = "botocore-1.35.71.tar.gz", hash = "sha256:f9fa058e0393660c3fe53c1e044751beb64b586def0bd2212448a7c328b0cbba"}, + {file = "botocore-1.35.77-py3-none-any.whl", hash = "sha256:3faa27d65841499762228902d7e215fa99a4c2fdc76c9113e1c3f339bdf685b8"}, + {file = "botocore-1.35.77.tar.gz", hash = "sha256:17b778016644e9342ca3ff2f430c1d1db0c6126e9b41a57cff52ac58e7a455e0"}, ] [package.dependencies] @@ -524,73 +524,73 @@ files = [ [[package]] name = "coverage" -version = "7.6.8" +version = "7.6.9" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.9" files = [ - {file = "coverage-7.6.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b39e6011cd06822eb964d038d5dff5da5d98652b81f5ecd439277b32361a3a50"}, - {file = "coverage-7.6.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:63c19702db10ad79151a059d2d6336fe0c470f2e18d0d4d1a57f7f9713875dcf"}, - {file = "coverage-7.6.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3985b9be361d8fb6b2d1adc9924d01dec575a1d7453a14cccd73225cb79243ee"}, - {file = "coverage-7.6.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:644ec81edec0f4ad17d51c838a7d01e42811054543b76d4ba2c5d6af741ce2a6"}, - {file = "coverage-7.6.8-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f188a2402f8359cf0c4b1fe89eea40dc13b52e7b4fd4812450da9fcd210181d"}, - {file = "coverage-7.6.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e19122296822deafce89a0c5e8685704c067ae65d45e79718c92df7b3ec3d331"}, - {file = "coverage-7.6.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:13618bed0c38acc418896005732e565b317aa9e98d855a0e9f211a7ffc2d6638"}, - {file = "coverage-7.6.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:193e3bffca48ad74b8c764fb4492dd875038a2f9925530cb094db92bb5e47bed"}, - {file = "coverage-7.6.8-cp310-cp310-win32.whl", hash = "sha256:3988665ee376abce49613701336544041f2117de7b7fbfe91b93d8ff8b151c8e"}, - {file = "coverage-7.6.8-cp310-cp310-win_amd64.whl", hash = "sha256:f56f49b2553d7dd85fd86e029515a221e5c1f8cb3d9c38b470bc38bde7b8445a"}, - {file = "coverage-7.6.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:86cffe9c6dfcfe22e28027069725c7f57f4b868a3f86e81d1c62462764dc46d4"}, - {file = "coverage-7.6.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d82ab6816c3277dc962cfcdc85b1efa0e5f50fb2c449432deaf2398a2928ab94"}, - {file = "coverage-7.6.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13690e923a3932e4fad4c0ebfb9cb5988e03d9dcb4c5150b5fcbf58fd8bddfc4"}, - {file = "coverage-7.6.8-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4be32da0c3827ac9132bb488d331cb32e8d9638dd41a0557c5569d57cf22c9c1"}, - {file = "coverage-7.6.8-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44e6c85bbdc809383b509d732b06419fb4544dca29ebe18480379633623baafb"}, - {file = "coverage-7.6.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:768939f7c4353c0fac2f7c37897e10b1414b571fd85dd9fc49e6a87e37a2e0d8"}, - {file = "coverage-7.6.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e44961e36cb13c495806d4cac67640ac2866cb99044e210895b506c26ee63d3a"}, - {file = "coverage-7.6.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ea8bb1ab9558374c0ab591783808511d135a833c3ca64a18ec927f20c4030f0"}, - {file = "coverage-7.6.8-cp311-cp311-win32.whl", hash = "sha256:629a1ba2115dce8bf75a5cce9f2486ae483cb89c0145795603d6554bdc83e801"}, - {file = "coverage-7.6.8-cp311-cp311-win_amd64.whl", hash = "sha256:fb9fc32399dca861584d96eccd6c980b69bbcd7c228d06fb74fe53e007aa8ef9"}, - {file = "coverage-7.6.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e683e6ecc587643f8cde8f5da6768e9d165cd31edf39ee90ed7034f9ca0eefee"}, - {file = "coverage-7.6.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1defe91d41ce1bd44b40fabf071e6a01a5aa14de4a31b986aa9dfd1b3e3e414a"}, - {file = "coverage-7.6.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7ad66e8e50225ebf4236368cc43c37f59d5e6728f15f6e258c8639fa0dd8e6d"}, - {file = "coverage-7.6.8-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fe47da3e4fda5f1abb5709c156eca207eacf8007304ce3019eb001e7a7204cb"}, - {file = "coverage-7.6.8-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:202a2d645c5a46b84992f55b0a3affe4f0ba6b4c611abec32ee88358db4bb649"}, - {file = "coverage-7.6.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4674f0daa1823c295845b6a740d98a840d7a1c11df00d1fd62614545c1583787"}, - {file = "coverage-7.6.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:74610105ebd6f33d7c10f8907afed696e79c59e3043c5f20eaa3a46fddf33b4c"}, - {file = "coverage-7.6.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37cda8712145917105e07aab96388ae76e787270ec04bcb9d5cc786d7cbb8443"}, - {file = "coverage-7.6.8-cp312-cp312-win32.whl", hash = "sha256:9e89d5c8509fbd6c03d0dd1972925b22f50db0792ce06324ba069f10787429ad"}, - {file = "coverage-7.6.8-cp312-cp312-win_amd64.whl", hash = "sha256:379c111d3558272a2cae3d8e57e6b6e6f4fe652905692d54bad5ea0ca37c5ad4"}, - {file = "coverage-7.6.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0b0c69f4f724c64dfbfe79f5dfb503b42fe6127b8d479b2677f2b227478db2eb"}, - {file = "coverage-7.6.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c15b32a7aca8038ed7644f854bf17b663bc38e1671b5d6f43f9a2b2bd0c46f63"}, - {file = "coverage-7.6.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63068a11171e4276f6ece913bde059e77c713b48c3a848814a6537f35afb8365"}, - {file = "coverage-7.6.8-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f4548c5ead23ad13fb7a2c8ea541357474ec13c2b736feb02e19a3085fac002"}, - {file = "coverage-7.6.8-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b4b4299dd0d2c67caaaf286d58aef5e75b125b95615dda4542561a5a566a1e3"}, - {file = "coverage-7.6.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9ebfb2507751f7196995142f057d1324afdab56db1d9743aab7f50289abd022"}, - {file = "coverage-7.6.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c1b4474beee02ede1eef86c25ad4600a424fe36cff01a6103cb4533c6bf0169e"}, - {file = "coverage-7.6.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d9fd2547e6decdbf985d579cf3fc78e4c1d662b9b0ff7cc7862baaab71c9cc5b"}, - {file = "coverage-7.6.8-cp313-cp313-win32.whl", hash = "sha256:8aae5aea53cbfe024919715eca696b1a3201886ce83790537d1c3668459c7146"}, - {file = "coverage-7.6.8-cp313-cp313-win_amd64.whl", hash = "sha256:ae270e79f7e169ccfe23284ff5ea2d52a6f401dc01b337efb54b3783e2ce3f28"}, - {file = "coverage-7.6.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:de38add67a0af869b0d79c525d3e4588ac1ffa92f39116dbe0ed9753f26eba7d"}, - {file = "coverage-7.6.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b07c25d52b1c16ce5de088046cd2432b30f9ad5e224ff17c8f496d9cb7d1d451"}, - {file = "coverage-7.6.8-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62a66ff235e4c2e37ed3b6104d8b478d767ff73838d1222132a7a026aa548764"}, - {file = "coverage-7.6.8-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09b9f848b28081e7b975a3626e9081574a7b9196cde26604540582da60235fdf"}, - {file = "coverage-7.6.8-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:093896e530c38c8e9c996901858ac63f3d4171268db2c9c8b373a228f459bbc5"}, - {file = "coverage-7.6.8-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a7b8ac36fd688c8361cbc7bf1cb5866977ece6e0b17c34aa0df58bda4fa18a4"}, - {file = "coverage-7.6.8-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:38c51297b35b3ed91670e1e4efb702b790002e3245a28c76e627478aa3c10d83"}, - {file = "coverage-7.6.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2e4e0f60cb4bd7396108823548e82fdab72d4d8a65e58e2c19bbbc2f1e2bfa4b"}, - {file = "coverage-7.6.8-cp313-cp313t-win32.whl", hash = "sha256:6535d996f6537ecb298b4e287a855f37deaf64ff007162ec0afb9ab8ba3b8b71"}, - {file = "coverage-7.6.8-cp313-cp313t-win_amd64.whl", hash = "sha256:c79c0685f142ca53256722a384540832420dff4ab15fec1863d7e5bc8691bdcc"}, - {file = "coverage-7.6.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3ac47fa29d8d41059ea3df65bd3ade92f97ee4910ed638e87075b8e8ce69599e"}, - {file = "coverage-7.6.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:24eda3a24a38157eee639ca9afe45eefa8d2420d49468819ac5f88b10de84f4c"}, - {file = "coverage-7.6.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4c81ed2820b9023a9a90717020315e63b17b18c274a332e3b6437d7ff70abe0"}, - {file = "coverage-7.6.8-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd55f8fc8fa494958772a2a7302b0354ab16e0b9272b3c3d83cdb5bec5bd1779"}, - {file = "coverage-7.6.8-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f39e2f3530ed1626c66e7493be7a8423b023ca852aacdc91fb30162c350d2a92"}, - {file = "coverage-7.6.8-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:716a78a342679cd1177bc8c2fe957e0ab91405bd43a17094324845200b2fddf4"}, - {file = "coverage-7.6.8-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:177f01eeaa3aee4a5ffb0d1439c5952b53d5010f86e9d2667963e632e30082cc"}, - {file = "coverage-7.6.8-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:912e95017ff51dc3d7b6e2be158dedc889d9a5cc3382445589ce554f1a34c0ea"}, - {file = "coverage-7.6.8-cp39-cp39-win32.whl", hash = "sha256:4db3ed6a907b555e57cc2e6f14dc3a4c2458cdad8919e40b5357ab9b6db6c43e"}, - {file = "coverage-7.6.8-cp39-cp39-win_amd64.whl", hash = "sha256:428ac484592f780e8cd7b6b14eb568f7c85460c92e2a37cb0c0e5186e1a0d076"}, - {file = "coverage-7.6.8-pp39.pp310-none-any.whl", hash = "sha256:5c52a036535d12590c32c49209e79cabaad9f9ad8aa4cbd875b68c4d67a9cbce"}, - {file = "coverage-7.6.8.tar.gz", hash = "sha256:8b2b8503edb06822c86d82fa64a4a5cb0760bb8f31f26e138ec743f422f37cfc"}, + {file = "coverage-7.6.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:85d9636f72e8991a1706b2b55b06c27545448baf9f6dbf51c4004609aacd7dcb"}, + {file = "coverage-7.6.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:608a7fd78c67bee8936378299a6cb9f5149bb80238c7a566fc3e6717a4e68710"}, + {file = "coverage-7.6.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96d636c77af18b5cb664ddf12dab9b15a0cfe9c0bde715da38698c8cea748bfa"}, + {file = "coverage-7.6.9-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d75cded8a3cff93da9edc31446872d2997e327921d8eed86641efafd350e1df1"}, + {file = "coverage-7.6.9-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7b15f589593110ae767ce997775d645b47e5cbbf54fd322f8ebea6277466cec"}, + {file = "coverage-7.6.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:44349150f6811b44b25574839b39ae35291f6496eb795b7366fef3bd3cf112d3"}, + {file = "coverage-7.6.9-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:d891c136b5b310d0e702e186d70cd16d1119ea8927347045124cb286b29297e5"}, + {file = "coverage-7.6.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:db1dab894cc139f67822a92910466531de5ea6034ddfd2b11c0d4c6257168073"}, + {file = "coverage-7.6.9-cp310-cp310-win32.whl", hash = "sha256:41ff7b0da5af71a51b53f501a3bac65fb0ec311ebed1632e58fc6107f03b9198"}, + {file = "coverage-7.6.9-cp310-cp310-win_amd64.whl", hash = "sha256:35371f8438028fdccfaf3570b31d98e8d9eda8bb1d6ab9473f5a390969e98717"}, + {file = "coverage-7.6.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:932fc826442132dde42ee52cf66d941f581c685a6313feebed358411238f60f9"}, + {file = "coverage-7.6.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:085161be5f3b30fd9b3e7b9a8c301f935c8313dcf928a07b116324abea2c1c2c"}, + {file = "coverage-7.6.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ccc660a77e1c2bf24ddbce969af9447a9474790160cfb23de6be4fa88e3951c7"}, + {file = "coverage-7.6.9-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c69e42c892c018cd3c8d90da61d845f50a8243062b19d228189b0224150018a9"}, + {file = "coverage-7.6.9-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0824a28ec542a0be22f60c6ac36d679e0e262e5353203bea81d44ee81fe9c6d4"}, + {file = "coverage-7.6.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4401ae5fc52ad8d26d2a5d8a7428b0f0c72431683f8e63e42e70606374c311a1"}, + {file = "coverage-7.6.9-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:98caba4476a6c8d59ec1eb00c7dd862ba9beca34085642d46ed503cc2d440d4b"}, + {file = "coverage-7.6.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ee5defd1733fd6ec08b168bd4f5387d5b322f45ca9e0e6c817ea6c4cd36313e3"}, + {file = "coverage-7.6.9-cp311-cp311-win32.whl", hash = "sha256:f2d1ec60d6d256bdf298cb86b78dd715980828f50c46701abc3b0a2b3f8a0dc0"}, + {file = "coverage-7.6.9-cp311-cp311-win_amd64.whl", hash = "sha256:0d59fd927b1f04de57a2ba0137166d31c1a6dd9e764ad4af552912d70428c92b"}, + {file = "coverage-7.6.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:99e266ae0b5d15f1ca8d278a668df6f51cc4b854513daab5cae695ed7b721cf8"}, + {file = "coverage-7.6.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9901d36492009a0a9b94b20e52ebfc8453bf49bb2b27bca2c9706f8b4f5a554a"}, + {file = "coverage-7.6.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abd3e72dd5b97e3af4246cdada7738ef0e608168de952b837b8dd7e90341f015"}, + {file = "coverage-7.6.9-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff74026a461eb0660366fb01c650c1d00f833a086b336bdad7ab00cc952072b3"}, + {file = "coverage-7.6.9-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65dad5a248823a4996724a88eb51d4b31587aa7aa428562dbe459c684e5787ae"}, + {file = "coverage-7.6.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:22be16571504c9ccea919fcedb459d5ab20d41172056206eb2994e2ff06118a4"}, + {file = "coverage-7.6.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f957943bc718b87144ecaee70762bc2bc3f1a7a53c7b861103546d3a403f0a6"}, + {file = "coverage-7.6.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ae1387db4aecb1f485fb70a6c0148c6cdaebb6038f1d40089b1fc84a5db556f"}, + {file = "coverage-7.6.9-cp312-cp312-win32.whl", hash = "sha256:1a330812d9cc7ac2182586f6d41b4d0fadf9be9049f350e0efb275c8ee8eb692"}, + {file = "coverage-7.6.9-cp312-cp312-win_amd64.whl", hash = "sha256:b12c6b18269ca471eedd41c1b6a1065b2f7827508edb9a7ed5555e9a56dcfc97"}, + {file = "coverage-7.6.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:899b8cd4781c400454f2f64f7776a5d87bbd7b3e7f7bda0cb18f857bb1334664"}, + {file = "coverage-7.6.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:61f70dc68bd36810972e55bbbe83674ea073dd1dcc121040a08cdf3416c5349c"}, + {file = "coverage-7.6.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a289d23d4c46f1a82d5db4abeb40b9b5be91731ee19a379d15790e53031c014"}, + {file = "coverage-7.6.9-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e216d8044a356fc0337c7a2a0536d6de07888d7bcda76febcb8adc50bdbbd00"}, + {file = "coverage-7.6.9-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c026eb44f744acaa2bda7493dad903aa5bf5fc4f2554293a798d5606710055d"}, + {file = "coverage-7.6.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e77363e8425325384f9d49272c54045bbed2f478e9dd698dbc65dbc37860eb0a"}, + {file = "coverage-7.6.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:777abfab476cf83b5177b84d7486497e034eb9eaea0d746ce0c1268c71652077"}, + {file = "coverage-7.6.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:447af20e25fdbe16f26e84eb714ba21d98868705cb138252d28bc400381f6ffb"}, + {file = "coverage-7.6.9-cp313-cp313-win32.whl", hash = "sha256:d872ec5aeb086cbea771c573600d47944eea2dcba8be5f3ee649bfe3cb8dc9ba"}, + {file = "coverage-7.6.9-cp313-cp313-win_amd64.whl", hash = "sha256:fd1213c86e48dfdc5a0cc676551db467495a95a662d2396ecd58e719191446e1"}, + {file = "coverage-7.6.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ba9e7484d286cd5a43744e5f47b0b3fb457865baf07bafc6bee91896364e1419"}, + {file = "coverage-7.6.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e5ea1cf0872ee455c03e5674b5bca5e3e68e159379c1af0903e89f5eba9ccc3a"}, + {file = "coverage-7.6.9-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d10e07aa2b91835d6abec555ec8b2733347956991901eea6ffac295f83a30e4"}, + {file = "coverage-7.6.9-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:13a9e2d3ee855db3dd6ea1ba5203316a1b1fd8eaeffc37c5b54987e61e4194ae"}, + {file = "coverage-7.6.9-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c38bf15a40ccf5619fa2fe8f26106c7e8e080d7760aeccb3722664c8656b030"}, + {file = "coverage-7.6.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d5275455b3e4627c8e7154feaf7ee0743c2e7af82f6e3b561967b1cca755a0be"}, + {file = "coverage-7.6.9-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8f8770dfc6e2c6a2d4569f411015c8d751c980d17a14b0530da2d7f27ffdd88e"}, + {file = "coverage-7.6.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8d2dfa71665a29b153a9681edb1c8d9c1ea50dfc2375fb4dac99ea7e21a0bcd9"}, + {file = "coverage-7.6.9-cp313-cp313t-win32.whl", hash = "sha256:5e6b86b5847a016d0fbd31ffe1001b63355ed309651851295315031ea7eb5a9b"}, + {file = "coverage-7.6.9-cp313-cp313t-win_amd64.whl", hash = "sha256:97ddc94d46088304772d21b060041c97fc16bdda13c6c7f9d8fcd8d5ae0d8611"}, + {file = "coverage-7.6.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:adb697c0bd35100dc690de83154627fbab1f4f3c0386df266dded865fc50a902"}, + {file = "coverage-7.6.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:be57b6d56e49c2739cdf776839a92330e933dd5e5d929966fbbd380c77f060be"}, + {file = "coverage-7.6.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1592791f8204ae9166de22ba7e6705fa4ebd02936c09436a1bb85aabca3e599"}, + {file = "coverage-7.6.9-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e12ae8cc979cf83d258acb5e1f1cf2f3f83524d1564a49d20b8bec14b637f08"}, + {file = "coverage-7.6.9-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb5555cff66c4d3d6213a296b360f9e1a8e323e74e0426b6c10ed7f4d021e464"}, + {file = "coverage-7.6.9-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:b9389a429e0e5142e69d5bf4a435dd688c14478a19bb901735cdf75e57b13845"}, + {file = "coverage-7.6.9-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:592ac539812e9b46046620341498caf09ca21023c41c893e1eb9dbda00a70cbf"}, + {file = "coverage-7.6.9-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a27801adef24cc30871da98a105f77995e13a25a505a0161911f6aafbd66e678"}, + {file = "coverage-7.6.9-cp39-cp39-win32.whl", hash = "sha256:8e3c3e38930cfb729cb8137d7f055e5a473ddaf1217966aa6238c88bd9fd50e6"}, + {file = "coverage-7.6.9-cp39-cp39-win_amd64.whl", hash = "sha256:e28bf44afa2b187cc9f41749138a64435bf340adfcacb5b2290c070ce99839d4"}, + {file = "coverage-7.6.9-pp39.pp310-none-any.whl", hash = "sha256:f3ca78518bc6bc92828cd11867b121891d75cae4ea9e908d72030609b996db1b"}, + {file = "coverage-7.6.9.tar.gz", hash = "sha256:4a8d8977b0c6ef5aeadcb644da9e69ae0dcfe66ec7f368c89c72e058bd71164d"}, ] [package.extras] @@ -694,13 +694,13 @@ typing_extensions = ">=3.10.0.0" [[package]] name = "django" -version = "4.2.16" +version = "4.2.17" description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." optional = false python-versions = ">=3.8" files = [ - {file = "Django-4.2.16-py3-none-any.whl", hash = "sha256:1ddc333a16fc139fd253035a1606bb24261951bbc3a6ca256717fa06cc41a898"}, - {file = "Django-4.2.16.tar.gz", hash = "sha256:6f1616c2786c408ce86ab7e10f792b8f15742f7b7b7460243929cb371e7f1dad"}, + {file = "Django-4.2.17-py3-none-any.whl", hash = "sha256:3a93350214ba25f178d4045c0786c61573e7dbfa3c509b3551374f1e11ba8de0"}, + {file = "Django-4.2.17.tar.gz", hash = "sha256:6b56d834cc94c8b21a8f4e775064896be3b4a4ca387f2612d4406a5927cd2fdc"}, ] [package.dependencies] @@ -938,70 +938,70 @@ test = ["objgraph", "psutil"] [[package]] name = "grpcio" -version = "1.68.0" +version = "1.68.1" description = "HTTP/2-based RPC framework" optional = false python-versions = ">=3.8" files = [ - {file = "grpcio-1.68.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:619b5d0f29f4f5351440e9343224c3e19912c21aeda44e0c49d0d147a8d01544"}, - {file = "grpcio-1.68.0-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:a59f5822f9459bed098ffbceb2713abbf7c6fd13f2b9243461da5c338d0cd6c3"}, - {file = "grpcio-1.68.0-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:c03d89df516128febc5a7e760d675b478ba25802447624edf7aa13b1e7b11e2a"}, - {file = "grpcio-1.68.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44bcbebb24363d587472089b89e2ea0ab2e2b4df0e4856ba4c0b087c82412121"}, - {file = "grpcio-1.68.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79f81b7fbfb136247b70465bd836fa1733043fdee539cd6031cb499e9608a110"}, - {file = "grpcio-1.68.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:88fb2925789cfe6daa20900260ef0a1d0a61283dfb2d2fffe6194396a354c618"}, - {file = "grpcio-1.68.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:99f06232b5c9138593ae6f2e355054318717d32a9c09cdc5a2885540835067a1"}, - {file = "grpcio-1.68.0-cp310-cp310-win32.whl", hash = "sha256:a6213d2f7a22c3c30a479fb5e249b6b7e648e17f364598ff64d08a5136fe488b"}, - {file = "grpcio-1.68.0-cp310-cp310-win_amd64.whl", hash = "sha256:15327ab81131ef9b94cb9f45b5bd98803a179c7c61205c8c0ac9aff9d6c4e82a"}, - {file = "grpcio-1.68.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:3b2b559beb2d433129441783e5f42e3be40a9e1a89ec906efabf26591c5cd415"}, - {file = "grpcio-1.68.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e46541de8425a4d6829ac6c5d9b16c03c292105fe9ebf78cb1c31e8d242f9155"}, - {file = "grpcio-1.68.0-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:c1245651f3c9ea92a2db4f95d37b7597db6b246d5892bca6ee8c0e90d76fb73c"}, - {file = "grpcio-1.68.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f1931c7aa85be0fa6cea6af388e576f3bf6baee9e5d481c586980c774debcb4"}, - {file = "grpcio-1.68.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b0ff09c81e3aded7a183bc6473639b46b6caa9c1901d6f5e2cba24b95e59e30"}, - {file = "grpcio-1.68.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:8c73f9fbbaee1a132487e31585aa83987ddf626426d703ebcb9a528cf231c9b1"}, - {file = "grpcio-1.68.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6b2f98165ea2790ea159393a2246b56f580d24d7da0d0342c18a085299c40a75"}, - {file = "grpcio-1.68.0-cp311-cp311-win32.whl", hash = "sha256:e1e7ed311afb351ff0d0e583a66fcb39675be112d61e7cfd6c8269884a98afbc"}, - {file = "grpcio-1.68.0-cp311-cp311-win_amd64.whl", hash = "sha256:e0d2f68eaa0a755edd9a47d40e50dba6df2bceda66960dee1218da81a2834d27"}, - {file = "grpcio-1.68.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:8af6137cc4ae8e421690d276e7627cfc726d4293f6607acf9ea7260bd8fc3d7d"}, - {file = "grpcio-1.68.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:4028b8e9a3bff6f377698587d642e24bd221810c06579a18420a17688e421af7"}, - {file = "grpcio-1.68.0-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:f60fa2adf281fd73ae3a50677572521edca34ba373a45b457b5ebe87c2d01e1d"}, - {file = "grpcio-1.68.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e18589e747c1e70b60fab6767ff99b2d0c359ea1db8a2cb524477f93cdbedf5b"}, - {file = "grpcio-1.68.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0d30f3fee9372796f54d3100b31ee70972eaadcc87314be369360248a3dcffe"}, - {file = "grpcio-1.68.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:7e0a3e72c0e9a1acab77bef14a73a416630b7fd2cbd893c0a873edc47c42c8cd"}, - {file = "grpcio-1.68.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a831dcc343440969aaa812004685ed322cdb526cd197112d0db303b0da1e8659"}, - {file = "grpcio-1.68.0-cp312-cp312-win32.whl", hash = "sha256:5a180328e92b9a0050958ced34dddcb86fec5a8b332f5a229e353dafc16cd332"}, - {file = "grpcio-1.68.0-cp312-cp312-win_amd64.whl", hash = "sha256:2bddd04a790b69f7a7385f6a112f46ea0b34c4746f361ebafe9ca0be567c78e9"}, - {file = "grpcio-1.68.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:fc05759ffbd7875e0ff2bd877be1438dfe97c9312bbc558c8284a9afa1d0f40e"}, - {file = "grpcio-1.68.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:15fa1fe25d365a13bc6d52fcac0e3ee1f9baebdde2c9b3b2425f8a4979fccea1"}, - {file = "grpcio-1.68.0-cp313-cp313-manylinux_2_17_aarch64.whl", hash = "sha256:32a9cb4686eb2e89d97022ecb9e1606d132f85c444354c17a7dbde4a455e4a3b"}, - {file = "grpcio-1.68.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dba037ff8d284c8e7ea9a510c8ae0f5b016004f13c3648f72411c464b67ff2fb"}, - {file = "grpcio-1.68.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0efbbd849867e0e569af09e165363ade75cf84f5229b2698d53cf22c7a4f9e21"}, - {file = "grpcio-1.68.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:4e300e6978df0b65cc2d100c54e097c10dfc7018b9bd890bbbf08022d47f766d"}, - {file = "grpcio-1.68.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:6f9c7ad1a23e1047f827385f4713b5b8c6c7d325705be1dd3e31fb00dcb2f665"}, - {file = "grpcio-1.68.0-cp313-cp313-win32.whl", hash = "sha256:3ac7f10850fd0487fcce169c3c55509101c3bde2a3b454869639df2176b60a03"}, - {file = "grpcio-1.68.0-cp313-cp313-win_amd64.whl", hash = "sha256:afbf45a62ba85a720491bfe9b2642f8761ff348006f5ef67e4622621f116b04a"}, - {file = "grpcio-1.68.0-cp38-cp38-linux_armv7l.whl", hash = "sha256:f8f695d9576ce836eab27ba7401c60acaf9ef6cf2f70dfe5462055ba3df02cc3"}, - {file = "grpcio-1.68.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:9fe1b141cda52f2ca73e17d2d3c6a9f3f3a0c255c216b50ce616e9dca7e3441d"}, - {file = "grpcio-1.68.0-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:4df81d78fd1646bf94ced4fb4cd0a7fe2e91608089c522ef17bc7db26e64effd"}, - {file = "grpcio-1.68.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:46a2d74d4dd8993151c6cd585594c082abe74112c8e4175ddda4106f2ceb022f"}, - {file = "grpcio-1.68.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a17278d977746472698460c63abf333e1d806bd41f2224f90dbe9460101c9796"}, - {file = "grpcio-1.68.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:15377bce516b1c861c35e18eaa1c280692bf563264836cece693c0f169b48829"}, - {file = "grpcio-1.68.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:cc5f0a4f5904b8c25729a0498886b797feb817d1fd3812554ffa39551112c161"}, - {file = "grpcio-1.68.0-cp38-cp38-win32.whl", hash = "sha256:def1a60a111d24376e4b753db39705adbe9483ef4ca4761f825639d884d5da78"}, - {file = "grpcio-1.68.0-cp38-cp38-win_amd64.whl", hash = "sha256:55d3b52fd41ec5772a953612db4e70ae741a6d6ed640c4c89a64f017a1ac02b5"}, - {file = "grpcio-1.68.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:0d230852ba97654453d290e98d6aa61cb48fa5fafb474fb4c4298d8721809354"}, - {file = "grpcio-1.68.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:50992f214264e207e07222703c17d9cfdcc2c46ed5a1ea86843d440148ebbe10"}, - {file = "grpcio-1.68.0-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:14331e5c27ed3545360464a139ed279aa09db088f6e9502e95ad4bfa852bb116"}, - {file = "grpcio-1.68.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f84890b205692ea813653ece4ac9afa2139eae136e419231b0eec7c39fdbe4c2"}, - {file = "grpcio-1.68.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0cf343c6f4f6aa44863e13ec9ddfe299e0be68f87d68e777328bff785897b05"}, - {file = "grpcio-1.68.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:fd2c2d47969daa0e27eadaf15c13b5e92605c5e5953d23c06d0b5239a2f176d3"}, - {file = "grpcio-1.68.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:18668e36e7f4045820f069997834e94e8275910b1f03e078a6020bd464cb2363"}, - {file = "grpcio-1.68.0-cp39-cp39-win32.whl", hash = "sha256:2af76ab7c427aaa26aa9187c3e3c42f38d3771f91a20f99657d992afada2294a"}, - {file = "grpcio-1.68.0-cp39-cp39-win_amd64.whl", hash = "sha256:e694b5928b7b33ca2d3b4d5f9bf8b5888906f181daff6b406f4938f3a997a490"}, - {file = "grpcio-1.68.0.tar.gz", hash = "sha256:7e7483d39b4a4fddb9906671e9ea21aaad4f031cdfc349fec76bdfa1e404543a"}, + {file = "grpcio-1.68.1-cp310-cp310-linux_armv7l.whl", hash = "sha256:d35740e3f45f60f3c37b1e6f2f4702c23867b9ce21c6410254c9c682237da68d"}, + {file = "grpcio-1.68.1-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:d99abcd61760ebb34bdff37e5a3ba333c5cc09feda8c1ad42547bea0416ada78"}, + {file = "grpcio-1.68.1-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:f8261fa2a5f679abeb2a0a93ad056d765cdca1c47745eda3f2d87f874ff4b8c9"}, + {file = "grpcio-1.68.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0feb02205a27caca128627bd1df4ee7212db051019a9afa76f4bb6a1a80ca95e"}, + {file = "grpcio-1.68.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:919d7f18f63bcad3a0f81146188e90274fde800a94e35d42ffe9eadf6a9a6330"}, + {file = "grpcio-1.68.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:963cc8d7d79b12c56008aabd8b457f400952dbea8997dd185f155e2f228db079"}, + {file = "grpcio-1.68.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ccf2ebd2de2d6661e2520dae293298a3803a98ebfc099275f113ce1f6c2a80f1"}, + {file = "grpcio-1.68.1-cp310-cp310-win32.whl", hash = "sha256:2cc1fd04af8399971bcd4f43bd98c22d01029ea2e56e69c34daf2bf8470e47f5"}, + {file = "grpcio-1.68.1-cp310-cp310-win_amd64.whl", hash = "sha256:ee2e743e51cb964b4975de572aa8fb95b633f496f9fcb5e257893df3be854746"}, + {file = "grpcio-1.68.1-cp311-cp311-linux_armv7l.whl", hash = "sha256:55857c71641064f01ff0541a1776bfe04a59db5558e82897d35a7793e525774c"}, + {file = "grpcio-1.68.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4b177f5547f1b995826ef529d2eef89cca2f830dd8b2c99ffd5fde4da734ba73"}, + {file = "grpcio-1.68.1-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:3522c77d7e6606d6665ec8d50e867f13f946a4e00c7df46768f1c85089eae515"}, + {file = "grpcio-1.68.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9d1fae6bbf0816415b81db1e82fb3bf56f7857273c84dcbe68cbe046e58e1ccd"}, + {file = "grpcio-1.68.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:298ee7f80e26f9483f0b6f94cc0a046caf54400a11b644713bb5b3d8eb387600"}, + {file = "grpcio-1.68.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cbb5780e2e740b6b4f2d208e90453591036ff80c02cc605fea1af8e6fc6b1bbe"}, + {file = "grpcio-1.68.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ddda1aa22495d8acd9dfbafff2866438d12faec4d024ebc2e656784d96328ad0"}, + {file = "grpcio-1.68.1-cp311-cp311-win32.whl", hash = "sha256:b33bd114fa5a83f03ec6b7b262ef9f5cac549d4126f1dc702078767b10c46ed9"}, + {file = "grpcio-1.68.1-cp311-cp311-win_amd64.whl", hash = "sha256:7f20ebec257af55694d8f993e162ddf0d36bd82d4e57f74b31c67b3c6d63d8b2"}, + {file = "grpcio-1.68.1-cp312-cp312-linux_armv7l.whl", hash = "sha256:8829924fffb25386995a31998ccbbeaa7367223e647e0122043dfc485a87c666"}, + {file = "grpcio-1.68.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:3aed6544e4d523cd6b3119b0916cef3d15ef2da51e088211e4d1eb91a6c7f4f1"}, + {file = "grpcio-1.68.1-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:4efac5481c696d5cb124ff1c119a78bddbfdd13fc499e3bc0ca81e95fc573684"}, + {file = "grpcio-1.68.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ab2d912ca39c51f46baf2a0d92aa265aa96b2443266fc50d234fa88bf877d8e"}, + {file = "grpcio-1.68.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95c87ce2a97434dffe7327a4071839ab8e8bffd0054cc74cbe971fba98aedd60"}, + {file = "grpcio-1.68.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:e4842e4872ae4ae0f5497bf60a0498fa778c192cc7a9e87877abd2814aca9475"}, + {file = "grpcio-1.68.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:255b1635b0ed81e9f91da4fcc8d43b7ea5520090b9a9ad9340d147066d1d3613"}, + {file = "grpcio-1.68.1-cp312-cp312-win32.whl", hash = "sha256:7dfc914cc31c906297b30463dde0b9be48e36939575eaf2a0a22a8096e69afe5"}, + {file = "grpcio-1.68.1-cp312-cp312-win_amd64.whl", hash = "sha256:a0c8ddabef9c8f41617f213e527254c41e8b96ea9d387c632af878d05db9229c"}, + {file = "grpcio-1.68.1-cp313-cp313-linux_armv7l.whl", hash = "sha256:a47faedc9ea2e7a3b6569795c040aae5895a19dde0c728a48d3c5d7995fda385"}, + {file = "grpcio-1.68.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:390eee4225a661c5cd133c09f5da1ee3c84498dc265fd292a6912b65c421c78c"}, + {file = "grpcio-1.68.1-cp313-cp313-manylinux_2_17_aarch64.whl", hash = "sha256:66a24f3d45c33550703f0abb8b656515b0ab777970fa275693a2f6dc8e35f1c1"}, + {file = "grpcio-1.68.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c08079b4934b0bf0a8847f42c197b1d12cba6495a3d43febd7e99ecd1cdc8d54"}, + {file = "grpcio-1.68.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8720c25cd9ac25dd04ee02b69256d0ce35bf8a0f29e20577427355272230965a"}, + {file = "grpcio-1.68.1-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:04cfd68bf4f38f5bb959ee2361a7546916bd9a50f78617a346b3aeb2b42e2161"}, + {file = "grpcio-1.68.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c28848761a6520c5c6071d2904a18d339a796ebe6b800adc8b3f474c5ce3c3ad"}, + {file = "grpcio-1.68.1-cp313-cp313-win32.whl", hash = "sha256:77d65165fc35cff6e954e7fd4229e05ec76102d4406d4576528d3a3635fc6172"}, + {file = "grpcio-1.68.1-cp313-cp313-win_amd64.whl", hash = "sha256:a8040f85dcb9830d8bbb033ae66d272614cec6faceee88d37a88a9bd1a7a704e"}, + {file = "grpcio-1.68.1-cp38-cp38-linux_armv7l.whl", hash = "sha256:eeb38ff04ab6e5756a2aef6ad8d94e89bb4a51ef96e20f45c44ba190fa0bcaad"}, + {file = "grpcio-1.68.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8a3869a6661ec8f81d93f4597da50336718bde9eb13267a699ac7e0a1d6d0bea"}, + {file = "grpcio-1.68.1-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:2c4cec6177bf325eb6faa6bd834d2ff6aa8bb3b29012cceb4937b86f8b74323c"}, + {file = "grpcio-1.68.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:12941d533f3cd45d46f202e3667be8ebf6bcb3573629c7ec12c3e211d99cfccf"}, + {file = "grpcio-1.68.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80af6f1e69c5e68a2be529990684abdd31ed6622e988bf18850075c81bb1ad6e"}, + {file = "grpcio-1.68.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:e8dbe3e00771bfe3d04feed8210fc6617006d06d9a2679b74605b9fed3e8362c"}, + {file = "grpcio-1.68.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:83bbf5807dc3ee94ce1de2dfe8a356e1d74101e4b9d7aa8c720cc4818a34aded"}, + {file = "grpcio-1.68.1-cp38-cp38-win32.whl", hash = "sha256:8cb620037a2fd9eeee97b4531880e439ebfcd6d7d78f2e7dcc3726428ab5ef63"}, + {file = "grpcio-1.68.1-cp38-cp38-win_amd64.whl", hash = "sha256:52fbf85aa71263380d330f4fce9f013c0798242e31ede05fcee7fbe40ccfc20d"}, + {file = "grpcio-1.68.1-cp39-cp39-linux_armv7l.whl", hash = "sha256:cb400138e73969eb5e0535d1d06cae6a6f7a15f2cc74add320e2130b8179211a"}, + {file = "grpcio-1.68.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a1b988b40f2fd9de5c820f3a701a43339d8dcf2cb2f1ca137e2c02671cc83ac1"}, + {file = "grpcio-1.68.1-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:96f473cdacfdd506008a5d7579c9f6a7ff245a9ade92c3c0265eb76cc591914f"}, + {file = "grpcio-1.68.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:37ea3be171f3cf3e7b7e412a98b77685eba9d4fd67421f4a34686a63a65d99f9"}, + {file = "grpcio-1.68.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ceb56c4285754e33bb3c2fa777d055e96e6932351a3082ce3559be47f8024f0"}, + {file = "grpcio-1.68.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:dffd29a2961f3263a16d73945b57cd44a8fd0b235740cb14056f0612329b345e"}, + {file = "grpcio-1.68.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:025f790c056815b3bf53da850dd70ebb849fd755a4b1ac822cb65cd631e37d43"}, + {file = "grpcio-1.68.1-cp39-cp39-win32.whl", hash = "sha256:1098f03dedc3b9810810568060dea4ac0822b4062f537b0f53aa015269be0a76"}, + {file = "grpcio-1.68.1-cp39-cp39-win_amd64.whl", hash = "sha256:334ab917792904245a028f10e803fcd5b6f36a7b2173a820c0b5b076555825e1"}, + {file = "grpcio-1.68.1.tar.gz", hash = "sha256:44a8502dd5de653ae6a73e2de50a401d84184f0331d0ac3daeb044e66d5c5054"}, ] [package.extras] -protobuf = ["grpcio-tools (>=1.68.0)"] +protobuf = ["grpcio-tools (>=1.68.1)"] [[package]] name = "gunicorn" @@ -1309,66 +1309,66 @@ files = [ [[package]] name = "numpy" -version = "2.1.3" +version = "2.2.0" description = "Fundamental package for array computing in Python" optional = false python-versions = ">=3.10" files = [ - {file = "numpy-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c894b4305373b9c5576d7a12b473702afdf48ce5369c074ba304cc5ad8730dff"}, - {file = "numpy-2.1.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b47fbb433d3260adcd51eb54f92a2ffbc90a4595f8970ee00e064c644ac788f5"}, - {file = "numpy-2.1.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:825656d0743699c529c5943554d223c021ff0494ff1442152ce887ef4f7561a1"}, - {file = "numpy-2.1.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:6a4825252fcc430a182ac4dee5a505053d262c807f8a924603d411f6718b88fd"}, - {file = "numpy-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e711e02f49e176a01d0349d82cb5f05ba4db7d5e7e0defd026328e5cfb3226d3"}, - {file = "numpy-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78574ac2d1a4a02421f25da9559850d59457bac82f2b8d7a44fe83a64f770098"}, - {file = "numpy-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c7662f0e3673fe4e832fe07b65c50342ea27d989f92c80355658c7f888fcc83c"}, - {file = "numpy-2.1.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fa2d1337dc61c8dc417fbccf20f6d1e139896a30721b7f1e832b2bb6ef4eb6c4"}, - {file = "numpy-2.1.3-cp310-cp310-win32.whl", hash = "sha256:72dcc4a35a8515d83e76b58fdf8113a5c969ccd505c8a946759b24e3182d1f23"}, - {file = "numpy-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:ecc76a9ba2911d8d37ac01de72834d8849e55473457558e12995f4cd53e778e0"}, - {file = "numpy-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4d1167c53b93f1f5d8a139a742b3c6f4d429b54e74e6b57d0eff40045187b15d"}, - {file = "numpy-2.1.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c80e4a09b3d95b4e1cac08643f1152fa71a0a821a2d4277334c88d54b2219a41"}, - {file = "numpy-2.1.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:576a1c1d25e9e02ed7fa5477f30a127fe56debd53b8d2c89d5578f9857d03ca9"}, - {file = "numpy-2.1.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:973faafebaae4c0aaa1a1ca1ce02434554d67e628b8d805e61f874b84e136b09"}, - {file = "numpy-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:762479be47a4863e261a840e8e01608d124ee1361e48b96916f38b119cfda04a"}, - {file = "numpy-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc6f24b3d1ecc1eebfbf5d6051faa49af40b03be1aaa781ebdadcbc090b4539b"}, - {file = "numpy-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:17ee83a1f4fef3c94d16dc1802b998668b5419362c8a4f4e8a491de1b41cc3ee"}, - {file = "numpy-2.1.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15cb89f39fa6d0bdfb600ea24b250e5f1a3df23f901f51c8debaa6a5d122b2f0"}, - {file = "numpy-2.1.3-cp311-cp311-win32.whl", hash = "sha256:d9beb777a78c331580705326d2367488d5bc473b49a9bc3036c154832520aca9"}, - {file = "numpy-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:d89dd2b6da69c4fff5e39c28a382199ddedc3a5be5390115608345dec660b9e2"}, - {file = "numpy-2.1.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f55ba01150f52b1027829b50d70ef1dafd9821ea82905b63936668403c3b471e"}, - {file = "numpy-2.1.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13138eadd4f4da03074851a698ffa7e405f41a0845a6b1ad135b81596e4e9958"}, - {file = "numpy-2.1.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:a6b46587b14b888e95e4a24d7b13ae91fa22386c199ee7b418f449032b2fa3b8"}, - {file = "numpy-2.1.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:0fa14563cc46422e99daef53d725d0c326e99e468a9320a240affffe87852564"}, - {file = "numpy-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8637dcd2caa676e475503d1f8fdb327bc495554e10838019651b76d17b98e512"}, - {file = "numpy-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2312b2aa89e1f43ecea6da6ea9a810d06aae08321609d8dc0d0eda6d946a541b"}, - {file = "numpy-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a38c19106902bb19351b83802531fea19dee18e5b37b36454f27f11ff956f7fc"}, - {file = "numpy-2.1.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:02135ade8b8a84011cbb67dc44e07c58f28575cf9ecf8ab304e51c05528c19f0"}, - {file = "numpy-2.1.3-cp312-cp312-win32.whl", hash = "sha256:e6988e90fcf617da2b5c78902fe8e668361b43b4fe26dbf2d7b0f8034d4cafb9"}, - {file = "numpy-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:0d30c543f02e84e92c4b1f415b7c6b5326cbe45ee7882b6b77db7195fb971e3a"}, - {file = "numpy-2.1.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96fe52fcdb9345b7cd82ecd34547fca4321f7656d500eca497eb7ea5a926692f"}, - {file = "numpy-2.1.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f653490b33e9c3a4c1c01d41bc2aef08f9475af51146e4a7710c450cf9761598"}, - {file = "numpy-2.1.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:dc258a761a16daa791081d026f0ed4399b582712e6fc887a95af09df10c5ca57"}, - {file = "numpy-2.1.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:016d0f6f5e77b0f0d45d77387ffa4bb89816b57c835580c3ce8e099ef830befe"}, - {file = "numpy-2.1.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c181ba05ce8299c7aa3125c27b9c2167bca4a4445b7ce73d5febc411ca692e43"}, - {file = "numpy-2.1.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5641516794ca9e5f8a4d17bb45446998c6554704d888f86df9b200e66bdcce56"}, - {file = "numpy-2.1.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ea4dedd6e394a9c180b33c2c872b92f7ce0f8e7ad93e9585312b0c5a04777a4a"}, - {file = "numpy-2.1.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0df3635b9c8ef48bd3be5f862cf71b0a4716fa0e702155c45067c6b711ddcef"}, - {file = "numpy-2.1.3-cp313-cp313-win32.whl", hash = "sha256:50ca6aba6e163363f132b5c101ba078b8cbd3fa92c7865fd7d4d62d9779ac29f"}, - {file = "numpy-2.1.3-cp313-cp313-win_amd64.whl", hash = "sha256:747641635d3d44bcb380d950679462fae44f54b131be347d5ec2bce47d3df9ed"}, - {file = "numpy-2.1.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:996bb9399059c5b82f76b53ff8bb686069c05acc94656bb259b1d63d04a9506f"}, - {file = "numpy-2.1.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:45966d859916ad02b779706bb43b954281db43e185015df6eb3323120188f9e4"}, - {file = "numpy-2.1.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:baed7e8d7481bfe0874b566850cb0b85243e982388b7b23348c6db2ee2b2ae8e"}, - {file = "numpy-2.1.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a9f7f672a3388133335589cfca93ed468509cb7b93ba3105fce780d04a6576a0"}, - {file = "numpy-2.1.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7aac50327da5d208db2eec22eb11e491e3fe13d22653dce51b0f4109101b408"}, - {file = "numpy-2.1.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4394bc0dbd074b7f9b52024832d16e019decebf86caf909d94f6b3f77a8ee3b6"}, - {file = "numpy-2.1.3-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:50d18c4358a0a8a53f12a8ba9d772ab2d460321e6a93d6064fc22443d189853f"}, - {file = "numpy-2.1.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:14e253bd43fc6b37af4921b10f6add6925878a42a0c5fe83daee390bca80bc17"}, - {file = "numpy-2.1.3-cp313-cp313t-win32.whl", hash = "sha256:08788d27a5fd867a663f6fc753fd7c3ad7e92747efc73c53bca2f19f8bc06f48"}, - {file = "numpy-2.1.3-cp313-cp313t-win_amd64.whl", hash = "sha256:2564fbdf2b99b3f815f2107c1bbc93e2de8ee655a69c261363a1172a79a257d4"}, - {file = "numpy-2.1.3-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4f2015dfe437dfebbfce7c85c7b53d81ba49e71ba7eadbf1df40c915af75979f"}, - {file = "numpy-2.1.3-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:3522b0dfe983a575e6a9ab3a4a4dfe156c3e428468ff08ce582b9bb6bd1d71d4"}, - {file = "numpy-2.1.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c006b607a865b07cd981ccb218a04fc86b600411d83d6fc261357f1c0966755d"}, - {file = "numpy-2.1.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e14e26956e6f1696070788252dcdff11b4aca4c3e8bd166e0df1bb8f315a67cb"}, - {file = "numpy-2.1.3.tar.gz", hash = "sha256:aa08e04e08aaf974d4458def539dece0d28146d866a39da5639596f4921fd761"}, + {file = "numpy-2.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1e25507d85da11ff5066269d0bd25d06e0a0f2e908415534f3e603d2a78e4ffa"}, + {file = "numpy-2.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a62eb442011776e4036af5c8b1a00b706c5bc02dc15eb5344b0c750428c94219"}, + {file = "numpy-2.2.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:b606b1aaf802e6468c2608c65ff7ece53eae1a6874b3765f69b8ceb20c5fa78e"}, + {file = "numpy-2.2.0-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:36b2b43146f646642b425dd2027730f99bac962618ec2052932157e213a040e9"}, + {file = "numpy-2.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7fe8f3583e0607ad4e43a954e35c1748b553bfe9fdac8635c02058023277d1b3"}, + {file = "numpy-2.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:122fd2fcfafdefc889c64ad99c228d5a1f9692c3a83f56c292618a59aa60ae83"}, + {file = "numpy-2.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3f2f5cddeaa4424a0a118924b988746db6ffa8565e5829b1841a8a3bd73eb59a"}, + {file = "numpy-2.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7fe4bb0695fe986a9e4deec3b6857003b4cfe5c5e4aac0b95f6a658c14635e31"}, + {file = "numpy-2.2.0-cp310-cp310-win32.whl", hash = "sha256:b30042fe92dbd79f1ba7f6898fada10bdaad1847c44f2dff9a16147e00a93661"}, + {file = "numpy-2.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:54dc1d6d66f8d37843ed281773c7174f03bf7ad826523f73435deb88ba60d2d4"}, + {file = "numpy-2.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9874bc2ff574c40ab7a5cbb7464bf9b045d617e36754a7bc93f933d52bd9ffc6"}, + {file = "numpy-2.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0da8495970f6b101ddd0c38ace92edea30e7e12b9a926b57f5fabb1ecc25bb90"}, + {file = "numpy-2.2.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:0557eebc699c1c34cccdd8c3778c9294e8196df27d713706895edc6f57d29608"}, + {file = "numpy-2.2.0-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:3579eaeb5e07f3ded59298ce22b65f877a86ba8e9fe701f5576c99bb17c283da"}, + {file = "numpy-2.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40deb10198bbaa531509aad0cd2f9fadb26c8b94070831e2208e7df543562b74"}, + {file = "numpy-2.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2aed8fcf8abc3020d6a9ccb31dbc9e7d7819c56a348cc88fd44be269b37427e"}, + {file = "numpy-2.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a222d764352c773aa5ebde02dd84dba3279c81c6db2e482d62a3fa54e5ece69b"}, + {file = "numpy-2.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4e58666988605e251d42c2818c7d3d8991555381be26399303053b58a5bbf30d"}, + {file = "numpy-2.2.0-cp311-cp311-win32.whl", hash = "sha256:4723a50e1523e1de4fccd1b9a6dcea750c2102461e9a02b2ac55ffeae09a4410"}, + {file = "numpy-2.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:16757cf28621e43e252c560d25b15f18a2f11da94fea344bf26c599b9cf54b73"}, + {file = "numpy-2.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cff210198bb4cae3f3c100444c5eaa573a823f05c253e7188e1362a5555235b3"}, + {file = "numpy-2.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:58b92a5828bd4d9aa0952492b7de803135038de47343b2aa3cc23f3b71a3dc4e"}, + {file = "numpy-2.2.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:ebe5e59545401fbb1b24da76f006ab19734ae71e703cdb4a8b347e84a0cece67"}, + {file = "numpy-2.2.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e2b8cd48a9942ed3f85b95ca4105c45758438c7ed28fff1e4ce3e57c3b589d8e"}, + {file = "numpy-2.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57fcc997ffc0bef234b8875a54d4058afa92b0b0c4223fc1f62f24b3b5e86038"}, + {file = "numpy-2.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85ad7d11b309bd132d74397fcf2920933c9d1dc865487128f5c03d580f2c3d03"}, + {file = "numpy-2.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cb24cca1968b21355cc6f3da1a20cd1cebd8a023e3c5b09b432444617949085a"}, + {file = "numpy-2.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0798b138c291d792f8ea40fe3768610f3c7dd2574389e37c3f26573757c8f7ef"}, + {file = "numpy-2.2.0-cp312-cp312-win32.whl", hash = "sha256:afe8fb968743d40435c3827632fd36c5fbde633b0423da7692e426529b1759b1"}, + {file = "numpy-2.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:3a4199f519e57d517ebd48cb76b36c82da0360781c6a0353e64c0cac30ecaad3"}, + {file = "numpy-2.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f8c8b141ef9699ae777c6278b52c706b653bf15d135d302754f6b2e90eb30367"}, + {file = "numpy-2.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0f0986e917aca18f7a567b812ef7ca9391288e2acb7a4308aa9d265bd724bdae"}, + {file = "numpy-2.2.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:1c92113619f7b272838b8d6702a7f8ebe5edea0df48166c47929611d0b4dea69"}, + {file = "numpy-2.2.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:5a145e956b374e72ad1dff82779177d4a3c62bc8248f41b80cb5122e68f22d13"}, + {file = "numpy-2.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18142b497d70a34b01642b9feabb70156311b326fdddd875a9981f34a369b671"}, + {file = "numpy-2.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7d41d1612c1a82b64697e894b75db6758d4f21c3ec069d841e60ebe54b5b571"}, + {file = "numpy-2.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a98f6f20465e7618c83252c02041517bd2f7ea29be5378f09667a8f654a5918d"}, + {file = "numpy-2.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e09d40edfdb4e260cb1567d8ae770ccf3b8b7e9f0d9b5c2a9992696b30ce2742"}, + {file = "numpy-2.2.0-cp313-cp313-win32.whl", hash = "sha256:3905a5fffcc23e597ee4d9fb3fcd209bd658c352657548db7316e810ca80458e"}, + {file = "numpy-2.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:a184288538e6ad699cbe6b24859206e38ce5fba28f3bcfa51c90d0502c1582b2"}, + {file = "numpy-2.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7832f9e8eb00be32f15fdfb9a981d6955ea9adc8574c521d48710171b6c55e95"}, + {file = "numpy-2.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f0dd071b95bbca244f4cb7f70b77d2ff3aaaba7fa16dc41f58d14854a6204e6c"}, + {file = "numpy-2.2.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:b0b227dcff8cdc3efbce66d4e50891f04d0a387cce282fe1e66199146a6a8fca"}, + {file = "numpy-2.2.0-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:6ab153263a7c5ccaf6dfe7e53447b74f77789f28ecb278c3b5d49db7ece10d6d"}, + {file = "numpy-2.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e500aba968a48e9019e42c0c199b7ec0696a97fa69037bea163b55398e390529"}, + {file = "numpy-2.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:440cfb3db4c5029775803794f8638fbdbf71ec702caf32735f53b008e1eaece3"}, + {file = "numpy-2.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a55dc7a7f0b6198b07ec0cd445fbb98b05234e8b00c5ac4874a63372ba98d4ab"}, + {file = "numpy-2.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4bddbaa30d78c86329b26bd6aaaea06b1e47444da99eddac7bf1e2fab717bd72"}, + {file = "numpy-2.2.0-cp313-cp313t-win32.whl", hash = "sha256:30bf971c12e4365153afb31fc73f441d4da157153f3400b82db32d04de1e4066"}, + {file = "numpy-2.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:d35717333b39d1b6bb8433fa758a55f1081543de527171543a2b710551d40881"}, + {file = "numpy-2.2.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e12c6c1ce84628c52d6367863773f7c8c8241be554e8b79686e91a43f1733773"}, + {file = "numpy-2.2.0-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:b6207dc8fb3c8cb5668e885cef9ec7f70189bec4e276f0ff70d5aa078d32c88e"}, + {file = "numpy-2.2.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a50aeff71d0f97b6450d33940c7181b08be1441c6c193e678211bff11aa725e7"}, + {file = "numpy-2.2.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:df12a1f99b99f569a7c2ae59aa2d31724e8d835fc7f33e14f4792e3071d11221"}, + {file = "numpy-2.2.0.tar.gz", hash = "sha256:140dd80ff8981a583a60980be1a655068f8adebf7a45a06a6858c873fcdcd4a0"}, ] [[package]] @@ -1988,13 +1988,13 @@ files = [ [[package]] name = "pytest" -version = "8.3.3" +version = "8.3.4" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"}, - {file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"}, + {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, + {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, ] [package.dependencies] @@ -2229,13 +2229,13 @@ crt = ["botocore[crt] (>=1.33.2,<2.0a.0)"] [[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] @@ -2303,13 +2303,13 @@ type = ["importlib_metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (>=1.12 [[package]] name = "six" -version = "1.16.0" +version = "1.17.0" description = "Python 2 and 3 compatibility utilities" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" files = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, ] [[package]] diff --git a/pyproject.toml b/pyproject.toml index b573fdb..ef8f1b9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,7 +53,7 @@ playwright = "^1.45.0" behave = "^1.2.6" [tool.pytest.ini_options] -DJANGO_SETTINGS_MODULE = "config.settings.local" +DJANGO_SETTINGS_MODULE = "fbr.settings" xfail_strict=true addopts = """ -rsxXw @@ -72,7 +72,7 @@ filterwarnings = [ [tool.coverage.run] omit = [ - "fbr/manage.py", + "manage.py", "fbr/config/*sgi.py", "fbr/config/urls.py", "fbr/config/settings/*.py", @@ -105,4 +105,4 @@ max-line-length = 79 max-complexity = 10 [tool.poetry.scripts] -rebuild-cache = "fbr.cache.manage_cache:rebuild_cache" +rebuild-cache = "app.cache.manage_cache:rebuild_cache" diff --git a/pytest.ini b/pytest.ini index 7b775ce..67c6085 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,3 +1,3 @@ [pytest] -DJANGO_SETTINGS_MODULE = config.settings.local +DJANGO_SETTINGS_MODULE = fbr.settings DJANGO_FIND_PROJECT = false" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..339f00c --- /dev/null +++ b/requirements.txt @@ -0,0 +1,89 @@ +amqp==5.3.1 ; python_version >= "3.12" and python_version < "4.0" +asgiref==3.8.1 ; python_version >= "3.12" and python_version < "4.0" +backoff==2.2.1 ; python_version >= "3.12" and python_version < "4.0" +billiard==4.2.1 ; python_version >= "3.12" and python_version < "4.0" +boto3==1.35.77 ; python_version >= "3.12" and python_version < "4.0" +botocore==1.35.77 ; python_version >= "3.12" and python_version < "4.0" +brotli==1.1.0 ; python_version >= "3.12" and python_version < "4.0" +celery==5.4.0 ; python_version >= "3.12" and python_version < "4.0" +celery[redis]==5.4.0 ; python_version >= "3.12" and python_version < "4.0" +certifi==2024.8.30 ; python_version >= "3.12" and python_version < "4.0" +cfgv==3.4.0 ; python_version >= "3.12" and python_version < "4" +charset-normalizer==3.4.0 ; python_version >= "3.12" and python_version < "4.0" +click-didyoumean==0.3.1 ; python_version >= "3.12" and python_version < "4.0" +click-plugins==1.1.1 ; python_version >= "3.12" and python_version < "4.0" +click-repl==0.3.0 ; python_version >= "3.12" and python_version < "4.0" +click==8.1.7 ; python_version >= "3.12" and python_version < "4.0" +colorama==0.4.6 ; python_version >= "3.12" and python_version < "4.0" and platform_system == "Windows" +config==0.5.1 ; python_version >= "3.12" and python_version < "4.0" +cron-descriptor==1.4.5 ; python_version >= "3.12" and python_version < "4.0" +dbt-copilot-python==0.2.2 ; python_version >= "3.12" and python_version < "4.0" +deprecated==1.2.15 ; python_version >= "3.12" and python_version < "4.0" +distlib==0.3.9 ; python_version >= "3.12" and python_version < "4" +dj-database-url==2.3.0 ; python_version >= "3.12" and python_version < "4.0" +django-celery-beat==2.7.0 ; python_version >= "3.12" and python_version < "4.0" +django-environ==0.11.2 ; python_version >= "3.12" and python_version < "4" +django-log-formatter-asim==0.0.4 ; python_version >= "3.12" and python_version < "4" +django-timezone-field==7.0 ; python_version >= "3.12" and python_version < "4.0" +django-webpack-loader==3.1.1 ; python_version >= "3.12" and python_version < "4.0" +django==4.2.17 ; python_version >= "3.12" and python_version < "4.0" +djangorestframework==3.15.2 ; python_version >= "3.12" and python_version < "4.0" +filelock==3.16.1 ; python_version >= "3.12" and python_version < "4" +googleapis-common-protos==1.66.0 ; python_version >= "3.12" and python_version < "4.0" +grpcio==1.68.1 ; python_version >= "3.12" and python_version < "4.0" +gunicorn==22.0.0 ; python_version >= "3.12" and python_version < "4.0" +identify==2.6.3 ; python_version >= "3.12" and python_version < "4" +idna==3.10 ; python_version >= "3.12" and python_version < "4.0" +importlib-metadata==6.11.0 ; python_version >= "3.12" and python_version < "4.0" +jinja2==3.1.4 ; python_version >= "3.12" and python_version < "4.0" +jmespath==1.0.1 ; python_version >= "3.12" and python_version < "4.0" +kombu==5.4.2 ; python_version >= "3.12" and python_version < "4.0" +kombu[redis]==5.4.2 ; python_version >= "3.12" and python_version < "4.0" +markupsafe==3.0.2 ; python_version >= "3.12" and python_version < "4.0" +nodeenv==1.9.1 ; python_version >= "3.12" and python_version < "4" +numpy==2.2.0 ; python_version >= "3.12" and python_version < "4.0" +opentelemetry-api==1.22.0 ; python_version >= "3.12" and python_version < "4.0" +opentelemetry-distro==0.43b0 ; python_version >= "3.12" and python_version < "4.0" +opentelemetry-exporter-otlp-proto-common==1.22.0 ; python_version >= "3.12" and python_version < "4.0" +opentelemetry-exporter-otlp-proto-grpc==1.22.0 ; python_version >= "3.12" and python_version < "4.0" +opentelemetry-exporter-otlp-proto-http==1.22.0 ; python_version >= "3.12" and python_version < "4.0" +opentelemetry-exporter-otlp==1.22.0 ; python_version >= "3.12" and python_version < "4.0" +opentelemetry-instrumentation-wsgi==0.43b0 ; python_version >= "3.12" and python_version < "4.0" +opentelemetry-instrumentation==0.43b0 ; python_version >= "3.12" and python_version < "4.0" +opentelemetry-propagator-aws-xray==1.0.2 ; python_version >= "3.12" and python_version < "4.0" +opentelemetry-proto==1.22.0 ; python_version >= "3.12" and python_version < "4.0" +opentelemetry-sdk-extension-aws==2.0.2 ; python_version >= "3.12" and python_version < "4.0" +opentelemetry-sdk==1.22.0 ; python_version >= "3.12" and python_version < "4.0" +opentelemetry-semantic-conventions==0.43b0 ; python_version >= "3.12" and python_version < "4.0" +opentelemetry-util-http==0.43b0 ; python_version >= "3.12" and python_version < "4.0" +packaging==24.2 ; python_version >= "3.12" and python_version < "4.0" +pandas==2.2.3 ; python_version >= "3.12" and python_version < "4.0" +platformdirs==4.3.6 ; python_version >= "3.12" and python_version < "4" +pre-commit==3.8.0 ; python_version >= "3.12" and python_version < "4" +prompt-toolkit==3.0.48 ; python_version >= "3.12" and python_version < "4.0" +protobuf==4.25.5 ; python_version >= "3.12" and python_version < "4.0" +psycopg-c==3.2.3 ; python_version >= "3.12" and python_version < "4.0" +psycopg2-binary==2.9.10 ; python_version >= "3.12" and python_version < "4.0" +psycopg[c]==3.2.3 ; python_version >= "3.12" and python_version < "4.0" +python-crontab==3.2.0 ; python_version >= "3.12" and python_version < "4.0" +python-dateutil==2.9.0.post0 ; python_version >= "3.12" and python_version < "4.0" +python-environ==0.4.54 ; python_version >= "3.12" and python_version < "4.0" +pytz==2024.2 ; python_version >= "3.12" and python_version < "4.0" +pyyaml==6.0.2 ; python_version >= "3.12" and python_version < "4" +redis==5.2.1 ; python_version >= "3.12" and python_version < "4.0" +requests==2.32.3 ; python_version >= "3.12" and python_version < "4.0" +s3transfer==0.10.4 ; python_version >= "3.12" and python_version < "4.0" +sentry-sdk==2.19.2 ; python_version >= "3.12" and python_version < "4.0" +setuptools==75.6.0 ; python_version >= "3.12" and python_version < "4.0" +six==1.17.0 ; python_version >= "3.12" and python_version < "4.0" +sqlparse==0.5.2 ; python_version >= "3.12" and python_version < "4.0" +typing-extensions==4.12.2 ; python_version >= "3.12" and python_version < "4.0" +tzdata==2024.2 ; python_version >= "3.12" and python_version < "4.0" +urllib3==2.2.3 ; python_version >= "3.12" and python_version < "4.0" +utils==1.0.2 ; python_version >= "3.12" and python_version < "4.0" +vine==5.1.0 ; python_version >= "3.12" and python_version < "4.0" +virtualenv==20.28.0 ; python_version >= "3.12" and python_version < "4" +wcwidth==0.2.13 ; python_version >= "3.12" and python_version < "4.0" +whitenoise[brotli]==6.8.2 ; python_version >= "3.12" and python_version < "4.0" +wrapt==1.17.0 ; python_version >= "3.12" and python_version < "4.0" +zipp==3.21.0 ; python_version >= "3.12" and python_version < "4.0"