From 149de67fee7c456aec167e98d8799d3a2ed843c2 Mon Sep 17 00:00:00 2001 From: Girl LovesToCode Date: Wed, 10 Jun 2026 08:53:43 +0200 Subject: [PATCH 1/2] App updated in accordance with updated course --- .dockerignore | 4 +- .flake8 | 7 - .github/workflows/main.yml | 50 +- .gitignore | 97 ++- .python-version | 1 + Dockerfile | 47 +- README.md | 0 client.html | 133 ---- client_chapter_9.html | 168 +++++ client_finished_course.html | 364 +++++++++++ core/asgi.py | 2 +- core/settings.py | 57 +- core/urls.py | 2 +- core/wsgi.py | 2 +- docker-compose.yml | 8 +- fixtures/initial_data.json | 254 ++++++++ .../initial_shopping_lists_with_items.json | 228 +++++++ heroku.yml | 9 - manage.py | 1 + pyproject.toml | 35 ++ pytest.ini | 3 - requirements-dev.txt | 6 - requirements.txt | 8 - shopping_list/__init__.py | 1 - shopping_list/admin.py | 7 +- shopping_list/api/permissions.py | 9 +- shopping_list/api/serializers.py | 30 +- shopping_list/api/views.py | 57 +- shopping_list/api/viewsets.py | 43 -- shopping_list/apps.py | 6 +- shopping_list/fixtures/initial_data.json | 254 ++++++++ shopping_list/migrations/0001_initial.py | 171 +---- shopping_list/models.py | 17 +- shopping_list/receivers.py | 9 +- shopping_list/tests/conftest.py | 16 +- shopping_list/tests/tests.py | 486 +++++---------- shopping_list/urls.py | 41 +- start_app.sh | 8 + uv.lock | 587 ++++++++++++++++++ 39 files changed, 2307 insertions(+), 921 deletions(-) delete mode 100644 .flake8 create mode 100644 .python-version create mode 100644 README.md delete mode 100644 client.html create mode 100644 client_chapter_9.html create mode 100644 client_finished_course.html create mode 100644 fixtures/initial_data.json create mode 100644 fixtures/initial_shopping_lists_with_items.json delete mode 100644 heroku.yml create mode 100644 pyproject.toml delete mode 100644 pytest.ini delete mode 100644 requirements-dev.txt delete mode 100644 requirements.txt delete mode 100644 shopping_list/api/viewsets.py create mode 100644 shopping_list/fixtures/initial_data.json create mode 100755 start_app.sh create mode 100644 uv.lock diff --git a/.dockerignore b/.dockerignore index c501d46..c538b4b 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,7 +1,7 @@ -venv +.venv .dockerignore Dockerfile .git .gitignore .pytest_cache -.github \ No newline at end of file +.github diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 12c31e2..0000000 --- a/.flake8 +++ /dev/null @@ -1,7 +0,0 @@ -[flake8] -max-line-length = 120 -exclude = - .git, - __pycache__, - venv - migrations \ No newline at end of file diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d463d4f..4f99745 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,4 +1,4 @@ -name: Deploy to heroku. +name: CI on: push: branches: @@ -7,42 +7,28 @@ jobs: tests: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@v6 + - name: Install uv + uses: astral-sh/setup-uv@v8.1.0 with: - python-version: '3.12' + python-version: '3.13' + enable-cache: true - name: Install dependencies - run: pip install -r requirements.txt - - name: Install dev dependencies - run: pip install -r requirements-dev.txt + run: uv sync --locked - name: Run tests - run: pytest + run: uv run pytest code-quality: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@v6 + - name: Install uv + uses: astral-sh/setup-uv@v8.1.0 with: - python-version: '3.12' + python-version: '3.13' + enable-cache: true - name: Install dependencies - run: pip install -r requirements.txt - - name: Install dev dependencies - run: pip install -r requirements-dev.txt - - name: Run black - run: black . --check - - name: Run isort - run: isort . --check-only --profile black - - name: Run flake8 - run: flake8 . - deploy: - runs-on: ubuntu-latest - needs: [tests, code-quality] - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Deploy to Heroku - uses: akhileshns/heroku-deploy@v3.12.12 - with: - heroku_api_key: ${{ secrets.HEROKU_API_KEY }} - heroku_app_name: ${{ secrets.HEROKU_APP_NAME }} - heroku_email: ${{ secrets.HEROKU_EMAIL }} \ No newline at end of file + run: uv sync --locked + - name: Run Ruff linter + run: uv run ruff check . + - name: Run Ruff formatter + run: uv run ruff format --check . diff --git a/.gitignore b/.gitignore index 56bf8f4..f86813c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ # Byte-compiled / optimized / DLL files __pycache__/ -*.py[cod] +*.py[codz] *$py.class # C extensions @@ -27,8 +27,8 @@ share/python-wheels/ MANIFEST # PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec @@ -46,7 +46,8 @@ htmlcov/ nosetests.xml coverage.xml *.cover -*.py,cover +*.py.cover +*.lcov .hypothesis/ .pytest_cache/ cover/ @@ -92,37 +93,65 @@ ipython_config.py # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. -#Pipfile.lock +# Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# uv.lock # poetry # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. # This is especially recommended for binary packages to ensure reproducibility, and is more # commonly ignored for libraries. # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock +# poetry.lock +# poetry.toml # pdm # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -#pdm.lock -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it -# in version control. -# https://pdm.fming.dev/latest/usage/project/#working-with-version-control -.pdm.toml +# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. +# https://pdm-project.org/en/latest/usage/project/#working-with-version-control +# pdm.lock +# pdm.toml .pdm-python .pdm-build/ +# pixi +# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. +# pixi.lock +# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one +# in the .venv directory. It is recommended not to include this directory in version control. +.pixi/* +!.pixi/config.toml + # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm __pypackages__/ # Celery stuff -celerybeat-schedule +celerybeat-schedule* celerybeat.pid +# Redis +*.rdb +*.aof +*.pid + +# RabbitMQ +mnesia/ +rabbitmq/ +rabbitmq-data/ + +# ActiveMQ +activemq-data/ + # SageMath parsed files *.sage.py # Environments .env +.envrc .venv env/ venv/ @@ -155,8 +184,42 @@ dmypy.json cython_debug/ # PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -.idea/ \ No newline at end of file +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +# .idea/ + +# Abstra +# Abstra is an AI-powered process automation framework. +# Ignore directories containing user credentials, local state, and settings. +# Learn more at https://abstra.io/docs +.abstra/ + +# Visual Studio Code +# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore +# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore +# and can be added to the global gitignore or merged into this file. However, if you prefer, +# you could uncomment the following to ignore the entire vscode folder +# .vscode/ +# Temporary file for partial code execution +tempCodeRunnerFile.py + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + +# Marimo +marimo/_static/ +marimo/_lsp/ +__marimo__/ + +# Streamlit +.streamlit/secrets.toml + +.idea + +staticfiles/ + diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/Dockerfile b/Dockerfile index 2cc6b1f..35e6a6a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,24 +3,23 @@ ########### # pull official base image -FROM python:3.12-slim-bookworm as builder +FROM python:3.13-slim-bookworm AS builder -# install system dependencies -RUN apt-get update \ - && apt-get -y install g++ \ - && apt-get clean +# install uv +COPY --from=ghcr.io/astral-sh/uv:0.11.19 /uv /uvx /bin/ # set work directory WORKDIR /usr/src/app # set environment variables -ENV PYTHONDONTWRITEBYTECODE 1 -ENV PYTHONUNBUFFERED 1 +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 +ENV UV_COMPILE_BYTECODE=1 +ENV UV_LINK_MODE=copy -# install python dependencies -RUN pip install --upgrade pip -COPY ./requirements.txt . -RUN pip wheel --no-cache-dir --no-deps --wheel-dir /usr/src/app/wheels -r requirements.txt +# install python dependencies into a virtual environment (.venv) +COPY pyproject.toml uv.lock ./ +RUN uv sync --locked --no-dev --no-install-project ######### @@ -28,7 +27,7 @@ RUN pip wheel --no-cache-dir --no-deps --wheel-dir /usr/src/app/wheels -r requir ######### # pull official base image -FROM python:3.12-slim-bookworm +FROM python:3.13-slim-bookworm # upgrade system packages RUN apt-get update && apt-get upgrade -y && apt-get clean @@ -47,18 +46,16 @@ RUN mkdir $APP_HOME WORKDIR $APP_HOME # set environment variables -ENV PYTHONDONTWRITEBYTECODE 1 -ENV PYTHONUNBUFFERED 1 -ENV ENVIRONMENT prod -ENV TESTING 0 -ENV PYTHONPATH $APP_HOME +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 +ENV ENVIRONMENT=prod +ENV TESTING=0 +ENV PYTHONPATH=$APP_HOME +# activate the virtual environment by adding it to the PATH +ENV PATH="/usr/src/app/.venv/bin:$PATH" -# install dependencies -COPY --from=builder /usr/src/app/wheels /wheels -COPY --from=builder /usr/src/app/requirements.txt . - -RUN pip install --upgrade pip -RUN pip install --no-cache /wheels/* +# copy the pre-built virtual environment from the builder +COPY --from=builder /usr/src/app/.venv /usr/src/app/.venv # copy project COPY . $APP_HOME @@ -72,5 +69,5 @@ RUN chown -R app:app $HOME # change to the app user USER app -# serve the application -CMD gunicorn core.wsgi:application --bind 0.0.0.0:$PORT \ No newline at end of file +# perform the migrations, create a superuser, and serve the application +CMD ./start_app.sh diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/client.html b/client.html deleted file mode 100644 index 99df112..0000000 --- a/client.html +++ /dev/null @@ -1,133 +0,0 @@ - - - - - Awesome Shopping App - - - - -
-
- -
-
-
    -
  • - {{shopping_item.name}} -
  • -
-
-
- -
-
-
-
-
-
- - - - - - diff --git a/client_chapter_9.html b/client_chapter_9.html new file mode 100644 index 0000000..9d5a223 --- /dev/null +++ b/client_chapter_9.html @@ -0,0 +1,168 @@ + + + + + + Awesome Shopping App (Chapter 9) + + + + + +
+
+
+

🛒 Awesome Shopping App

+
+ +

+ You have no shopping lists yet. Add some in the Django admin. +

+ + +
+
+ + + + diff --git a/client_finished_course.html b/client_finished_course.html new file mode 100644 index 0000000..72b8d0f --- /dev/null +++ b/client_finished_course.html @@ -0,0 +1,364 @@ + + + + + + Awesome Shopping App + + + + + +
+
+
+

🛒 Awesome Shopping App

+ +
+ + + + + +
+

+ Sign in with your Django account to view your shopping lists. +

+
+ + +
+
+ + +
+ +
+ + + +
+
+ + + + diff --git a/core/asgi.py b/core/asgi.py index 0e1a541..ba43371 100644 --- a/core/asgi.py +++ b/core/asgi.py @@ -4,7 +4,7 @@ It exposes the ASGI callable as a module-level variable named ``application``. For more information on this file, see -https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/ +https://docs.djangoproject.com/en/6.0/howto/deployment/asgi/ """ import os diff --git a/core/settings.py b/core/settings.py index f1a4bd8..21a7416 100644 --- a/core/settings.py +++ b/core/settings.py @@ -1,13 +1,13 @@ """ Django settings for core project. -Generated by 'django-admin startproject' using Django 5.0.6. +Generated by 'django-admin startproject' using Django 6.0.6. For more information on this file, see -https://docs.djangoproject.com/en/5.0/topics/settings/ +https://docs.djangoproject.com/en/6.0/topics/settings/ For the full list of settings and their values, see -https://docs.djangoproject.com/en/5.0/ref/settings/ +https://docs.djangoproject.com/en/6.0/ref/settings/ """ import os @@ -21,13 +21,16 @@ # Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/ +# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/ +# SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = os.environ.get("SECRET_KEY", default=get_random_secret_key()) + +# SECURITY WARNING: don't run with debug turned on in production! DEBUG = int(os.environ.get("DEBUG", default=0)) -ALLOWED_HOSTS = os.environ.get( - "DJANGO_ALLOWED_HOSTS", default="127.0.0.1 localhost [::1]" -).split(" ") + +ALLOWED_HOSTS = os.environ.get("RENDER_EXTERNAL_HOSTNAME", default="127.0.0.1 [::1]").split() +CSRF_TRUSTED_ORIGINS = [f"https://{host}" for host in ALLOWED_HOSTS] # Application definition @@ -43,12 +46,12 @@ "rest_framework.authtoken", "corsheaders", "shopping_list", - "drf_spectacular", # NEW + "drf_spectacular", ] MIDDLEWARE = [ - "django.middleware.security.SecurityMiddleware", "corsheaders.middleware.CorsMiddleware", + "django.middleware.security.SecurityMiddleware", "whitenoise.middleware.WhiteNoiseMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", @@ -67,7 +70,6 @@ "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", @@ -80,7 +82,7 @@ # Database -# https://docs.djangoproject.com/en/5.0/ref/settings/#databases +# https://docs.djangoproject.com/en/6.0/ref/settings/#databases DATABASES = { "default": { @@ -93,7 +95,7 @@ # Password validation -# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators +# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ { @@ -112,7 +114,7 @@ # Internationalization -# https://docs.djangoproject.com/en/5.0/topics/i18n/ +# https://docs.djangoproject.com/en/6.0/topics/i18n/ LANGUAGE_CODE = "en-us" @@ -124,23 +126,24 @@ # Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/5.0/howto/static-files/ +# https://docs.djangoproject.com/en/6.0/howto/static-files/ STATIC_URL = "static/" -# Default primary key field type -# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field - -DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" - -AUTH_USER_MODEL = "shopping_list.User" - -CORS_ORIGIN_ALLOW_ALL = bool(DEBUG) - STATIC_ROOT = BASE_DIR / "staticfiles" -STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage" +STORAGES = { + "default": { + "BACKEND": "django.core.files.storage.FileSystemStorage", + }, + "staticfiles": { + "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage", + }, +} + +CORS_ALLOW_ALL_ORIGINS = bool(DEBUG) +LOGIN_REDIRECT_URL = "/api/shopping-lists/" REST_FRAMEWORK = { "DEFAULT_AUTHENTICATION_CLASSES": [ @@ -159,8 +162,8 @@ ], "DEFAULT_THROTTLE_RATES": { "anon": "10/hour", - "user_day": "10000/day", # UPDATED - "user_minute": "200/minute", # UPDATED + "user_day": "10000/day", + "user_minute": "200/minute", }, "DEFAULT_RENDERER_CLASSES": [ "rest_framework.renderers.JSONRenderer", @@ -175,4 +178,4 @@ "SERVE_PERMISSIONS": ["rest_framework.permissions.IsAuthenticated"], } -CSRF_TRUSTED_ORIGINS = ["https://drf-app-2024-80f7e0696ec6.herokuapp.com"] +AUTH_USER_MODEL = "shopping_list.User" diff --git a/core/urls.py b/core/urls.py index 4f457e1..ec0487a 100644 --- a/core/urls.py +++ b/core/urls.py @@ -2,7 +2,7 @@ URL configuration for core project. The `urlpatterns` list routes URLs to views. For more information please see: - https://docs.djangoproject.com/en/5.0/topics/http/urls/ + https://docs.djangoproject.com/en/6.0/topics/http/urls/ Examples: Function views 1. Add an import: from my_app import views diff --git a/core/wsgi.py b/core/wsgi.py index 983bcbd..2dd4f3e 100644 --- a/core/wsgi.py +++ b/core/wsgi.py @@ -4,7 +4,7 @@ It exposes the WSGI callable as a module-level variable named ``application``. For more information on this file, see -https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/ +https://docs.djangoproject.com/en/6.0/howto/deployment/wsgi/ """ import os diff --git a/docker-compose.yml b/docker-compose.yml index 3cf4e7c..59f841a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,11 +1,9 @@ -version: '3.9' - services: shopping_list_db: - image: postgres:16-alpine + image: postgres:18-alpine volumes: - - postgres_data:/var/lib/postgresql/data/ + - postgres_data:/var/lib/postgresql environment: - POSTGRES_USER=shopping_user - POSTGRES_PASSWORD=shopping_password @@ -22,4 +20,4 @@ services: - shopping_list_db volumes: - postgres_data: \ No newline at end of file + postgres_data: diff --git a/fixtures/initial_data.json b/fixtures/initial_data.json new file mode 100644 index 0000000..0a7bb7b --- /dev/null +++ b/fixtures/initial_data.json @@ -0,0 +1,254 @@ +[ + { + "model": "shopping_list.user", + "pk": 2, + "fields": { + "password": "pbkdf2_sha256$870000$muhHRub8KucJDgGbvcD9Ug$qBC0GXDVnN1diWJz7ak72VDGWcKqHsfvkP1er1v0yYU=", + "last_login": null, + "is_superuser": false, + "username": "marcus", + "first_name": "Marcus", + "last_name": "Lee", + "email": "marcus@example.com", + "is_staff": false, + "is_active": true, + "date_joined": "2026-05-01T10:00:00Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "shopping_list.shoppinglist", + "pk": "11111111-0000-4000-8000-000000000000", + "fields": { + "name": "Weekly Groceries", + "last_interaction": "2026-06-03T09:15:00Z", + "members": [1] + } + }, + { + "model": "shopping_list.shoppinglist", + "pk": "22222222-0000-4000-8000-000000000000", + "fields": { + "name": "Hardware Store", + "last_interaction": "2026-05-28T14:30:00Z", + "members": [1] + } + }, + { + "model": "shopping_list.shoppinglist", + "pk": "33333333-0000-4000-8000-000000000000", + "fields": { + "name": "Pharmacy", + "last_interaction": "2026-05-20T08:00:00Z", + "members": [1] + } + }, + { + "model": "shopping_list.shoppinglist", + "pk": "44444444-0000-4000-8000-000000000000", + "fields": { + "name": "Birthday BBQ", + "last_interaction": "2026-06-01T18:45:00Z", + "members": [2] + } + }, + { + "model": "shopping_list.shoppingitem", + "pk": "11111111-0000-4000-8000-000000000001", + "fields": { + "name": "Whole milk", + "purchased": true, + "shopping_list": "11111111-0000-4000-8000-000000000000" + } + }, + { + "model": "shopping_list.shoppingitem", + "pk": "11111111-0000-4000-8000-000000000002", + "fields": { + "name": "Sourdough bread", + "purchased": false, + "shopping_list": "11111111-0000-4000-8000-000000000000" + } + }, + { + "model": "shopping_list.shoppingitem", + "pk": "11111111-0000-4000-8000-000000000003", + "fields": { + "name": "Free-range eggs", + "purchased": true, + "shopping_list": "11111111-0000-4000-8000-000000000000" + } + }, + { + "model": "shopping_list.shoppingitem", + "pk": "11111111-0000-4000-8000-000000000004", + "fields": { + "name": "Bananas", + "purchased": false, + "shopping_list": "11111111-0000-4000-8000-000000000000" + } + }, + { + "model": "shopping_list.shoppingitem", + "pk": "11111111-0000-4000-8000-000000000005", + "fields": { + "name": "Cheddar cheese", + "purchased": false, + "shopping_list": "11111111-0000-4000-8000-000000000000" + } + }, + { + "model": "shopping_list.shoppingitem", + "pk": "11111111-0000-4000-8000-000000000006", + "fields": { + "name": "Olive oil", + "purchased": true, + "shopping_list": "11111111-0000-4000-8000-000000000000" + } + }, + { + "model": "shopping_list.shoppingitem", + "pk": "22222222-0000-4000-8000-000000000001", + "fields": { + "name": "AA batteries", + "purchased": false, + "shopping_list": "22222222-0000-4000-8000-000000000000" + } + }, + { + "model": "shopping_list.shoppingitem", + "pk": "22222222-0000-4000-8000-000000000002", + "fields": { + "name": "Painter's tape", + "purchased": false, + "shopping_list": "22222222-0000-4000-8000-000000000000" + } + }, + { + "model": "shopping_list.shoppingitem", + "pk": "22222222-0000-4000-8000-000000000003", + "fields": { + "name": "Wood screws (1in)", + "purchased": true, + "shopping_list": "22222222-0000-4000-8000-000000000000" + } + }, + { + "model": "shopping_list.shoppingitem", + "pk": "22222222-0000-4000-8000-000000000004", + "fields": { + "name": "LED bulbs", + "purchased": false, + "shopping_list": "22222222-0000-4000-8000-000000000000" + } + }, + { + "model": "shopping_list.shoppingitem", + "pk": "22222222-0000-4000-8000-000000000005", + "fields": { + "name": "Duct tape", + "purchased": true, + "shopping_list": "22222222-0000-4000-8000-000000000000" + } + }, + { + "model": "shopping_list.shoppingitem", + "pk": "33333333-0000-4000-8000-000000000001", + "fields": { + "name": "Toothpaste", + "purchased": true, + "shopping_list": "33333333-0000-4000-8000-000000000000" + } + }, + { + "model": "shopping_list.shoppingitem", + "pk": "33333333-0000-4000-8000-000000000002", + "fields": { + "name": "Ibuprofen", + "purchased": false, + "shopping_list": "33333333-0000-4000-8000-000000000000" + } + }, + { + "model": "shopping_list.shoppingitem", + "pk": "33333333-0000-4000-8000-000000000003", + "fields": { + "name": "Band-aids", + "purchased": false, + "shopping_list": "33333333-0000-4000-8000-000000000000" + } + }, + { + "model": "shopping_list.shoppingitem", + "pk": "33333333-0000-4000-8000-000000000004", + "fields": { + "name": "Vitamin C", + "purchased": true, + "shopping_list": "33333333-0000-4000-8000-000000000000" + } + }, + { + "model": "shopping_list.shoppingitem", + "pk": "33333333-0000-4000-8000-000000000005", + "fields": { + "name": "Sunscreen SPF 50", + "purchased": false, + "shopping_list": "33333333-0000-4000-8000-000000000000" + } + }, + { + "model": "shopping_list.shoppingitem", + "pk": "44444444-0000-4000-8000-000000000001", + "fields": { + "name": "Charcoal", + "purchased": false, + "shopping_list": "44444444-0000-4000-8000-000000000000" + } + }, + { + "model": "shopping_list.shoppingitem", + "pk": "44444444-0000-4000-8000-000000000002", + "fields": { + "name": "Beef burgers", + "purchased": false, + "shopping_list": "44444444-0000-4000-8000-000000000000" + } + }, + { + "model": "shopping_list.shoppingitem", + "pk": "44444444-0000-4000-8000-000000000003", + "fields": { + "name": "Burger buns", + "purchased": false, + "shopping_list": "44444444-0000-4000-8000-000000000000" + } + }, + { + "model": "shopping_list.shoppingitem", + "pk": "44444444-0000-4000-8000-000000000004", + "fields": { + "name": "Ketchup", + "purchased": true, + "shopping_list": "44444444-0000-4000-8000-000000000000" + } + }, + { + "model": "shopping_list.shoppingitem", + "pk": "44444444-0000-4000-8000-000000000005", + "fields": { + "name": "Paper plates", + "purchased": false, + "shopping_list": "44444444-0000-4000-8000-000000000000" + } + }, + { + "model": "shopping_list.shoppingitem", + "pk": "44444444-0000-4000-8000-000000000006", + "fields": { + "name": "Lemonade", + "purchased": false, + "shopping_list": "44444444-0000-4000-8000-000000000000" + } + } +] diff --git a/fixtures/initial_shopping_lists_with_items.json b/fixtures/initial_shopping_lists_with_items.json new file mode 100644 index 0000000..2a446fd --- /dev/null +++ b/fixtures/initial_shopping_lists_with_items.json @@ -0,0 +1,228 @@ +[ + { + "model": "shopping_list.shoppinglist", + "pk": "11111111-0000-4000-8000-000000000000", + "fields": { + "name": "Weekly Groceries" + } + }, + { + "model": "shopping_list.shoppinglist", + "pk": "22222222-0000-4000-8000-000000000000", + "fields": { + "name": "Hardware Store" + } + }, + { + "model": "shopping_list.shoppinglist", + "pk": "33333333-0000-4000-8000-000000000000", + "fields": { + "name": "Pharmacy" + } + }, + { + "model": "shopping_list.shoppinglist", + "pk": "44444444-0000-4000-8000-000000000000", + "fields": { + "name": "Birthday BBQ" + } + }, + { + "model": "shopping_list.shoppingitem", + "pk": "11111111-0000-4000-8000-000000000001", + "fields": { + "name": "Whole milk", + "purchased": true, + "shopping_list": "11111111-0000-4000-8000-000000000000" + } + }, + { + "model": "shopping_list.shoppingitem", + "pk": "11111111-0000-4000-8000-000000000002", + "fields": { + "name": "Sourdough bread", + "purchased": false, + "shopping_list": "11111111-0000-4000-8000-000000000000" + } + }, + { + "model": "shopping_list.shoppingitem", + "pk": "11111111-0000-4000-8000-000000000003", + "fields": { + "name": "Free-range eggs", + "purchased": true, + "shopping_list": "11111111-0000-4000-8000-000000000000" + } + }, + { + "model": "shopping_list.shoppingitem", + "pk": "11111111-0000-4000-8000-000000000004", + "fields": { + "name": "Bananas", + "purchased": false, + "shopping_list": "11111111-0000-4000-8000-000000000000" + } + }, + { + "model": "shopping_list.shoppingitem", + "pk": "11111111-0000-4000-8000-000000000005", + "fields": { + "name": "Cheddar cheese", + "purchased": false, + "shopping_list": "11111111-0000-4000-8000-000000000000" + } + }, + { + "model": "shopping_list.shoppingitem", + "pk": "11111111-0000-4000-8000-000000000006", + "fields": { + "name": "Olive oil", + "purchased": true, + "shopping_list": "11111111-0000-4000-8000-000000000000" + } + }, + { + "model": "shopping_list.shoppingitem", + "pk": "22222222-0000-4000-8000-000000000001", + "fields": { + "name": "AA batteries", + "purchased": false, + "shopping_list": "22222222-0000-4000-8000-000000000000" + } + }, + { + "model": "shopping_list.shoppingitem", + "pk": "22222222-0000-4000-8000-000000000002", + "fields": { + "name": "Painter's tape", + "purchased": false, + "shopping_list": "22222222-0000-4000-8000-000000000000" + } + }, + { + "model": "shopping_list.shoppingitem", + "pk": "22222222-0000-4000-8000-000000000003", + "fields": { + "name": "Wood screws (1in)", + "purchased": true, + "shopping_list": "22222222-0000-4000-8000-000000000000" + } + }, + { + "model": "shopping_list.shoppingitem", + "pk": "22222222-0000-4000-8000-000000000004", + "fields": { + "name": "LED bulbs", + "purchased": false, + "shopping_list": "22222222-0000-4000-8000-000000000000" + } + }, + { + "model": "shopping_list.shoppingitem", + "pk": "22222222-0000-4000-8000-000000000005", + "fields": { + "name": "Duct tape", + "purchased": true, + "shopping_list": "22222222-0000-4000-8000-000000000000" + } + }, + { + "model": "shopping_list.shoppingitem", + "pk": "33333333-0000-4000-8000-000000000001", + "fields": { + "name": "Toothpaste", + "purchased": true, + "shopping_list": "33333333-0000-4000-8000-000000000000" + } + }, + { + "model": "shopping_list.shoppingitem", + "pk": "33333333-0000-4000-8000-000000000002", + "fields": { + "name": "Ibuprofen", + "purchased": false, + "shopping_list": "33333333-0000-4000-8000-000000000000" + } + }, + { + "model": "shopping_list.shoppingitem", + "pk": "33333333-0000-4000-8000-000000000003", + "fields": { + "name": "Band-aids", + "purchased": false, + "shopping_list": "33333333-0000-4000-8000-000000000000" + } + }, + { + "model": "shopping_list.shoppingitem", + "pk": "33333333-0000-4000-8000-000000000004", + "fields": { + "name": "Vitamin C", + "purchased": true, + "shopping_list": "33333333-0000-4000-8000-000000000000" + } + }, + { + "model": "shopping_list.shoppingitem", + "pk": "33333333-0000-4000-8000-000000000005", + "fields": { + "name": "Sunscreen SPF 50", + "purchased": false, + "shopping_list": "33333333-0000-4000-8000-000000000000" + } + }, + { + "model": "shopping_list.shoppingitem", + "pk": "44444444-0000-4000-8000-000000000001", + "fields": { + "name": "Charcoal", + "purchased": false, + "shopping_list": "44444444-0000-4000-8000-000000000000" + } + }, + { + "model": "shopping_list.shoppingitem", + "pk": "44444444-0000-4000-8000-000000000002", + "fields": { + "name": "Beef burgers", + "purchased": false, + "shopping_list": "44444444-0000-4000-8000-000000000000" + } + }, + { + "model": "shopping_list.shoppingitem", + "pk": "44444444-0000-4000-8000-000000000003", + "fields": { + "name": "Burger buns", + "purchased": false, + "shopping_list": "44444444-0000-4000-8000-000000000000" + } + }, + { + "model": "shopping_list.shoppingitem", + "pk": "44444444-0000-4000-8000-000000000004", + "fields": { + "name": "Ketchup", + "purchased": true, + "shopping_list": "44444444-0000-4000-8000-000000000000" + } + }, + { + "model": "shopping_list.shoppingitem", + "pk": "44444444-0000-4000-8000-000000000005", + "fields": { + "name": "Paper plates", + "purchased": false, + "shopping_list": "44444444-0000-4000-8000-000000000000" + } + }, + { + "model": "shopping_list.shoppingitem", + "pk": "44444444-0000-4000-8000-000000000006", + "fields": { + "name": "Lemonade", + "purchased": false, + "shopping_list": "44444444-0000-4000-8000-000000000000" + } + } +] diff --git a/heroku.yml b/heroku.yml deleted file mode 100644 index b5c911d..0000000 --- a/heroku.yml +++ /dev/null @@ -1,9 +0,0 @@ -setup: - addons: - - plan: heroku-postgresql - as: DATABASE -build: - docker: - web: Dockerfile -run: - web: gunicorn core.wsgi:application --bind 0.0.0.0:$PORT \ No newline at end of file diff --git a/manage.py b/manage.py index 4e20ce5..ccea163 100755 --- a/manage.py +++ b/manage.py @@ -1,5 +1,6 @@ #!/usr/bin/env python """Django's command-line utility for administrative tasks.""" + import os import sys diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..4fb8d1d --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,35 @@ +[project] +name = "drf-shopping" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.13" +dependencies = [ + "dj-database-url==3.1.2", + "django==6.0.6", + "django-cors-headers==4.9.0", + "djangorestframework==3.17.1", + "drf-spectacular==0.29.0", + "gunicorn==26.0.0", + "psycopg[binary]==3.3.4", + "whitenoise==6.12.0", +] + +[dependency-groups] +dev = [ + "pytest==9.0.3", + "pytest-cov==7.1.0", + "pytest-django==4.12.0", + "ruff==0.15.15", +] + +[tool.pytest.ini_options] +DJANGO_SETTINGS_MODULE = "core.settings" +python_files = "tests.py test_*.py" + +[tool.ruff] +line-length = 120 +extend-exclude = ["migrations"] + +[tool.ruff.lint] +extend-select = ["I"] diff --git a/pytest.ini b/pytest.ini deleted file mode 100644 index 9a4dd45..0000000 --- a/pytest.ini +++ /dev/null @@ -1,3 +0,0 @@ -[pytest] -DJANGO_SETTINGS_MODULE = core.settings -python_files = tests.py test_*.py \ No newline at end of file diff --git a/requirements-dev.txt b/requirements-dev.txt deleted file mode 100644 index d393acb..0000000 --- a/requirements-dev.txt +++ /dev/null @@ -1,6 +0,0 @@ -pytest==8.2.1 -pytest-cov==5.0.0 -pytest-django==4.8.0 -black==24.4.2 -flake8==7.0.0 -isort==5.13.2 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 523773a..0000000 --- a/requirements.txt +++ /dev/null @@ -1,8 +0,0 @@ -Django==5.0.6 -django-cors-headers==4.3.1 -djangorestframework==3.15.1 -drf-spectacular==0.27.2 -whitenoise==6.6.0 -gunicorn==22.0.0 -dj-database-url==2.1.0 -psycopg2-binary==2.9.9 \ No newline at end of file diff --git a/shopping_list/__init__.py b/shopping_list/__init__.py index 2531e0a..e69de29 100644 --- a/shopping_list/__init__.py +++ b/shopping_list/__init__.py @@ -1 +0,0 @@ -default_app_config = "shopping_list.apps.ApiConfig" diff --git a/shopping_list/admin.py b/shopping_list/admin.py index a72e8b6..c3d5dab 100644 --- a/shopping_list/admin.py +++ b/shopping_list/admin.py @@ -1,9 +1,8 @@ -# shopping_list/admin.py - - from django.contrib import admin +from django.contrib.auth.admin import UserAdmin -from shopping_list.models import ShoppingItem, ShoppingList +from shopping_list.models import ShoppingItem, ShoppingList, User admin.site.register(ShoppingItem) admin.site.register(ShoppingList) +admin.site.register(User, UserAdmin) diff --git a/shopping_list/api/permissions.py b/shopping_list/api/permissions.py index a78d64e..25b9fce 100644 --- a/shopping_list/api/permissions.py +++ b/shopping_list/api/permissions.py @@ -1,13 +1,10 @@ -# shopping_list/api/permissions.py - - from rest_framework import permissions +from rest_framework.generics import get_object_or_404 from shopping_list.models import ShoppingList class ShoppingListMembersOnly(permissions.BasePermission): - def has_object_permission(self, request, view, obj): if request.user.is_superuser: @@ -20,7 +17,6 @@ def has_object_permission(self, request, view, obj): class ShoppingItemShoppingListMembersOnly(permissions.BasePermission): - def has_object_permission(self, request, view, obj): if request.user.is_superuser: @@ -33,12 +29,11 @@ def has_object_permission(self, request, view, obj): class AllShoppingItemsShoppingListMembersOnly(permissions.BasePermission): - def has_permission(self, request, view): if request.user.is_superuser: return True - current_shopping_list = ShoppingList.objects.get(pk=view.kwargs.get("pk")) + current_shopping_list = get_object_or_404(ShoppingList, pk=view.kwargs.get("pk")) if request.user in current_shopping_list.members.all(): return True diff --git a/shopping_list/api/serializers.py b/shopping_list/api/serializers.py index 4246fa8..f524a27 100644 --- a/shopping_list/api/serializers.py +++ b/shopping_list/api/serializers.py @@ -1,14 +1,11 @@ -# shopping_list/api/serializers.py - -from typing import List, TypedDict +from typing import TypedDict from rest_framework import serializers -from shopping_list.models import User # NEW! -from shopping_list.models import ShoppingItem, ShoppingList +from shopping_list.models import ShoppingItem, ShoppingList, User -class UserSerializer(serializers.ModelSerializer): # NEW! +class UserSerializer(serializers.ModelSerializer): class Meta: model = User fields = ["id", "username"] @@ -22,19 +19,17 @@ class Meta: read_only_fields = ("id", "shopping_list") def create(self, validated_data, **kwargs): - validated_data["shopping_list_id"] = self.context["request"].parser_context[ - "kwargs" - ]["pk"] + validated_data["shopping_list_id"] = self.context["request"].parser_context["kwargs"]["pk"] - if ShoppingList.objects.get( - id=self.context["request"].parser_context["kwargs"]["pk"] - ).shopping_items.filter(name=validated_data["name"], purchased=False): + if ShoppingList.objects.get(id=self.context["request"].parser_context["kwargs"]["pk"]).shopping_items.filter( + name=validated_data["name"], purchased=False + ): raise serializers.ValidationError("There's already this item on the list") - return super(ShoppingItemSerializer, self).create(validated_data) + return super().create(validated_data) -class UnpurchasedItem(TypedDict): # NEW +class UnpurchasedItem(TypedDict): name: str @@ -46,11 +41,8 @@ class Meta: model = ShoppingList fields = ["id", "name", "unpurchased_items", "members"] - def get_unpurchased_items(self, obj) -> List[UnpurchasedItem]: # UPDATED - return [ - {"name": shopping_item.name} - for shopping_item in obj.shopping_items.filter(purchased=False) - ][:3] + def get_unpurchased_items(self, obj) -> list[UnpurchasedItem]: + return [{"name": shopping_item.name} for shopping_item in obj.shopping_items.filter(purchased=False)][:3] class AddMemberSerializer(serializers.ModelSerializer): diff --git a/shopping_list/api/views.py b/shopping_list/api/views.py index c737214..bef6002 100644 --- a/shopping_list/api/views.py +++ b/shopping_list/api/views.py @@ -1,7 +1,6 @@ -# shopping_list/api/views.py - from drf_spectacular.utils import extend_schema from rest_framework import filters, generics, status +from rest_framework.generics import get_object_or_404 from rest_framework.response import Response from rest_framework.views import APIView @@ -20,24 +19,16 @@ from shopping_list.models import ShoppingItem, ShoppingList -@extend_schema( - summary="List all the shopping lists.", - description="Returns the list of all shopping lists user is a member of. \ - Each shopping list includes a few unpurchased shopping items. Users can add a new shopping list.", -) class ListAddShoppingList(generics.ListCreateAPIView): - queryset = ShoppingList.objects.all() serializer_class = ShoppingListSerializer - def perform_create(self, serializer): # NEW! + def perform_create(self, serializer): shopping_list = serializer.save() shopping_list.members.add(self.request.user) return shopping_list def get_queryset(self): - return ShoppingList.objects.filter(members=self.request.user).order_by( - "-last_interaction" - ) + return ShoppingList.objects.filter(members=self.request.user).order_by("-last_interaction") class ShoppingListDetail(generics.RetrieveUpdateDestroyAPIView): @@ -52,31 +43,43 @@ class ListAddShoppingItem(generics.ListCreateAPIView): permission_classes = [AllShoppingItemsShoppingListMembersOnly] pagination_class = LargerResultsSetPagination filter_backends = (filters.OrderingFilter,) - ordering_fields = ["name", "purchased"] # NEW + ordering_fields = ["name", "purchased"] def get_queryset(self): shopping_list = self.kwargs["pk"] - queryset = ShoppingItem.objects.filter(shopping_list=shopping_list).order_by( - "purchased" - ) + queryset = ShoppingItem.objects.filter(shopping_list=shopping_list).order_by("purchased") return queryset class ShoppingItemDetail(generics.RetrieveUpdateDestroyAPIView): - queryset = ShoppingItem.objects.all() serializer_class = ShoppingItemSerializer permission_classes = [ShoppingItemShoppingListMembersOnly] lookup_url_kwarg = "item_pk" + def get_queryset(self): + return ShoppingItem.objects.filter(shopping_list_id=self.kwargs["pk"]) + + +class SearchShoppingItems(generics.ListAPIView): + serializer_class = ShoppingItemSerializer + + search_fields = ["name"] + filter_backends = (filters.SearchFilter,) + + def get_queryset(self): + users_shopping_lists = ShoppingList.objects.filter(members=self.request.user) + queryset = ShoppingItem.objects.filter(shopping_list__in=users_shopping_lists).order_by("name") + + return queryset + class ShoppingListAddMembers(APIView): permission_classes = [ShoppingListMembersOnly] - # NEW @extend_schema(request=AddMemberSerializer, responses=AddMemberSerializer) def put(self, request, pk, format=None): - shopping_list = ShoppingList.objects.get(pk=pk) + shopping_list = get_object_or_404(ShoppingList, pk=pk) serializer = AddMemberSerializer(shopping_list, data=request.data) self.check_object_permissions(request, shopping_list) @@ -90,10 +93,9 @@ def put(self, request, pk, format=None): class ShoppingListRemoveMembers(APIView): permission_classes = [ShoppingListMembersOnly] - # NEW @extend_schema(request=RemoveMemberSerializer, responses=RemoveMemberSerializer) def put(self, request, pk, format=None): - shopping_list = ShoppingList.objects.get(pk=pk) + shopping_list = get_object_or_404(ShoppingList, pk=pk) serializer = RemoveMemberSerializer(shopping_list, data=request.data) self.check_object_permissions(request, shopping_list) @@ -102,16 +104,3 @@ def put(self, request, pk, format=None): return Response(serializer.data) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - -class SearchShoppingItems(generics.ListAPIView): - serializer_class = ShoppingItemSerializer - - search_fields = ["name"] - filter_backends = (filters.SearchFilter,) - - def get_queryset(self): - users_shopping_lists = ShoppingList.objects.filter(members=self.request.user) - queryset = ShoppingItem.objects.filter(shopping_list__in=users_shopping_lists) - - return queryset diff --git a/shopping_list/api/viewsets.py b/shopping_list/api/viewsets.py deleted file mode 100644 index 9051a95..0000000 --- a/shopping_list/api/viewsets.py +++ /dev/null @@ -1,43 +0,0 @@ -# shopping_list/api/viewsets.py - -from rest_framework import status -from rest_framework.decorators import action -from rest_framework.response import Response -from rest_framework.viewsets import ModelViewSet - -from shopping_list.api.serializers import ShoppingItemSerializer -from shopping_list.models import ShoppingItem - - -class ShoppingItemViewSet(ModelViewSet): - queryset = ShoppingItem.objects.all() - serializer_class = ShoppingItemSerializer - - @action( - detail=False, - methods=["DELETE"], - url_path="delete-all-purchased", - url_name="delete-all-purchased", - ) - def delete_purchased(self, request): - ShoppingItem.objects.filter(purchased=True).delete() - - return Response(status=status.HTTP_204_NO_CONTENT) - - @action( - detail=False, - methods=["PATCH"], - url_path="mark-bulk-purchased", - url_name="mark-bulk-purchased", - ) - def mark_bulk_purchased(self, request): - try: - queryset = ShoppingItem.objects.filter( - id__in=request.data["shopping_items"] - ) - queryset.update(purchased=True) - - except Exception: - return Response(status=status.HTTP_400_BAD_REQUEST) - - return Response(status=status.HTTP_200_OK) diff --git a/shopping_list/apps.py b/shopping_list/apps.py index 9aacaba..daa408a 100644 --- a/shopping_list/apps.py +++ b/shopping_list/apps.py @@ -1,9 +1,7 @@ -# shopping_list/apps.py -from django.apps.config import AppConfig +from django.apps import AppConfig -class ApiConfig(AppConfig): - default_auto_field = "django.db.models.BigAutoField" +class ShoppingListConfig(AppConfig): name = "shopping_list" def ready(self): diff --git a/shopping_list/fixtures/initial_data.json b/shopping_list/fixtures/initial_data.json new file mode 100644 index 0000000..0a7bb7b --- /dev/null +++ b/shopping_list/fixtures/initial_data.json @@ -0,0 +1,254 @@ +[ + { + "model": "shopping_list.user", + "pk": 2, + "fields": { + "password": "pbkdf2_sha256$870000$muhHRub8KucJDgGbvcD9Ug$qBC0GXDVnN1diWJz7ak72VDGWcKqHsfvkP1er1v0yYU=", + "last_login": null, + "is_superuser": false, + "username": "marcus", + "first_name": "Marcus", + "last_name": "Lee", + "email": "marcus@example.com", + "is_staff": false, + "is_active": true, + "date_joined": "2026-05-01T10:00:00Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "shopping_list.shoppinglist", + "pk": "11111111-0000-4000-8000-000000000000", + "fields": { + "name": "Weekly Groceries", + "last_interaction": "2026-06-03T09:15:00Z", + "members": [1] + } + }, + { + "model": "shopping_list.shoppinglist", + "pk": "22222222-0000-4000-8000-000000000000", + "fields": { + "name": "Hardware Store", + "last_interaction": "2026-05-28T14:30:00Z", + "members": [1] + } + }, + { + "model": "shopping_list.shoppinglist", + "pk": "33333333-0000-4000-8000-000000000000", + "fields": { + "name": "Pharmacy", + "last_interaction": "2026-05-20T08:00:00Z", + "members": [1] + } + }, + { + "model": "shopping_list.shoppinglist", + "pk": "44444444-0000-4000-8000-000000000000", + "fields": { + "name": "Birthday BBQ", + "last_interaction": "2026-06-01T18:45:00Z", + "members": [2] + } + }, + { + "model": "shopping_list.shoppingitem", + "pk": "11111111-0000-4000-8000-000000000001", + "fields": { + "name": "Whole milk", + "purchased": true, + "shopping_list": "11111111-0000-4000-8000-000000000000" + } + }, + { + "model": "shopping_list.shoppingitem", + "pk": "11111111-0000-4000-8000-000000000002", + "fields": { + "name": "Sourdough bread", + "purchased": false, + "shopping_list": "11111111-0000-4000-8000-000000000000" + } + }, + { + "model": "shopping_list.shoppingitem", + "pk": "11111111-0000-4000-8000-000000000003", + "fields": { + "name": "Free-range eggs", + "purchased": true, + "shopping_list": "11111111-0000-4000-8000-000000000000" + } + }, + { + "model": "shopping_list.shoppingitem", + "pk": "11111111-0000-4000-8000-000000000004", + "fields": { + "name": "Bananas", + "purchased": false, + "shopping_list": "11111111-0000-4000-8000-000000000000" + } + }, + { + "model": "shopping_list.shoppingitem", + "pk": "11111111-0000-4000-8000-000000000005", + "fields": { + "name": "Cheddar cheese", + "purchased": false, + "shopping_list": "11111111-0000-4000-8000-000000000000" + } + }, + { + "model": "shopping_list.shoppingitem", + "pk": "11111111-0000-4000-8000-000000000006", + "fields": { + "name": "Olive oil", + "purchased": true, + "shopping_list": "11111111-0000-4000-8000-000000000000" + } + }, + { + "model": "shopping_list.shoppingitem", + "pk": "22222222-0000-4000-8000-000000000001", + "fields": { + "name": "AA batteries", + "purchased": false, + "shopping_list": "22222222-0000-4000-8000-000000000000" + } + }, + { + "model": "shopping_list.shoppingitem", + "pk": "22222222-0000-4000-8000-000000000002", + "fields": { + "name": "Painter's tape", + "purchased": false, + "shopping_list": "22222222-0000-4000-8000-000000000000" + } + }, + { + "model": "shopping_list.shoppingitem", + "pk": "22222222-0000-4000-8000-000000000003", + "fields": { + "name": "Wood screws (1in)", + "purchased": true, + "shopping_list": "22222222-0000-4000-8000-000000000000" + } + }, + { + "model": "shopping_list.shoppingitem", + "pk": "22222222-0000-4000-8000-000000000004", + "fields": { + "name": "LED bulbs", + "purchased": false, + "shopping_list": "22222222-0000-4000-8000-000000000000" + } + }, + { + "model": "shopping_list.shoppingitem", + "pk": "22222222-0000-4000-8000-000000000005", + "fields": { + "name": "Duct tape", + "purchased": true, + "shopping_list": "22222222-0000-4000-8000-000000000000" + } + }, + { + "model": "shopping_list.shoppingitem", + "pk": "33333333-0000-4000-8000-000000000001", + "fields": { + "name": "Toothpaste", + "purchased": true, + "shopping_list": "33333333-0000-4000-8000-000000000000" + } + }, + { + "model": "shopping_list.shoppingitem", + "pk": "33333333-0000-4000-8000-000000000002", + "fields": { + "name": "Ibuprofen", + "purchased": false, + "shopping_list": "33333333-0000-4000-8000-000000000000" + } + }, + { + "model": "shopping_list.shoppingitem", + "pk": "33333333-0000-4000-8000-000000000003", + "fields": { + "name": "Band-aids", + "purchased": false, + "shopping_list": "33333333-0000-4000-8000-000000000000" + } + }, + { + "model": "shopping_list.shoppingitem", + "pk": "33333333-0000-4000-8000-000000000004", + "fields": { + "name": "Vitamin C", + "purchased": true, + "shopping_list": "33333333-0000-4000-8000-000000000000" + } + }, + { + "model": "shopping_list.shoppingitem", + "pk": "33333333-0000-4000-8000-000000000005", + "fields": { + "name": "Sunscreen SPF 50", + "purchased": false, + "shopping_list": "33333333-0000-4000-8000-000000000000" + } + }, + { + "model": "shopping_list.shoppingitem", + "pk": "44444444-0000-4000-8000-000000000001", + "fields": { + "name": "Charcoal", + "purchased": false, + "shopping_list": "44444444-0000-4000-8000-000000000000" + } + }, + { + "model": "shopping_list.shoppingitem", + "pk": "44444444-0000-4000-8000-000000000002", + "fields": { + "name": "Beef burgers", + "purchased": false, + "shopping_list": "44444444-0000-4000-8000-000000000000" + } + }, + { + "model": "shopping_list.shoppingitem", + "pk": "44444444-0000-4000-8000-000000000003", + "fields": { + "name": "Burger buns", + "purchased": false, + "shopping_list": "44444444-0000-4000-8000-000000000000" + } + }, + { + "model": "shopping_list.shoppingitem", + "pk": "44444444-0000-4000-8000-000000000004", + "fields": { + "name": "Ketchup", + "purchased": true, + "shopping_list": "44444444-0000-4000-8000-000000000000" + } + }, + { + "model": "shopping_list.shoppingitem", + "pk": "44444444-0000-4000-8000-000000000005", + "fields": { + "name": "Paper plates", + "purchased": false, + "shopping_list": "44444444-0000-4000-8000-000000000000" + } + }, + { + "model": "shopping_list.shoppingitem", + "pk": "44444444-0000-4000-8000-000000000006", + "fields": { + "name": "Lemonade", + "purchased": false, + "shopping_list": "44444444-0000-4000-8000-000000000000" + } + } +] diff --git a/shopping_list/migrations/0001_initial.py b/shopping_list/migrations/0001_initial.py index 16b0f73..96854c1 100644 --- a/shopping_list/migrations/0001_initial.py +++ b/shopping_list/migrations/0001_initial.py @@ -1,11 +1,10 @@ -# Generated by Django 5.0.6 on 2024-05-24 07:56 - -import uuid +# Generated by Django 6.0.6 on 2026-06-10 05:20 import django.contrib.auth.models import django.contrib.auth.validators import django.db.models.deletion import django.utils.timezone +import uuid from django.conf import settings from django.db import migrations, models @@ -15,160 +14,52 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ("auth", "0012_alter_user_first_name_max_length"), + ('auth', '0012_alter_user_first_name_max_length'), ] operations = [ migrations.CreateModel( - name="User", + name='User', fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("password", models.CharField(max_length=128, verbose_name="password")), - ( - "last_login", - models.DateTimeField( - blank=True, null=True, verbose_name="last login" - ), - ), - ( - "is_superuser", - models.BooleanField( - default=False, - help_text="Designates that this user has all permissions without explicitly assigning them.", - verbose_name="superuser status", - ), - ), - ( - "username", - models.CharField( - error_messages={ - "unique": "A user with that username already exists." - }, - help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.", - max_length=150, - unique=True, - validators=[ - django.contrib.auth.validators.UnicodeUsernameValidator() - ], - verbose_name="username", - ), - ), - ( - "first_name", - models.CharField( - blank=True, max_length=150, verbose_name="first name" - ), - ), - ( - "last_name", - models.CharField( - blank=True, max_length=150, verbose_name="last name" - ), - ), - ( - "email", - models.EmailField( - blank=True, max_length=254, verbose_name="email address" - ), - ), - ( - "is_staff", - models.BooleanField( - default=False, - help_text="Designates whether the user can log into this admin site.", - verbose_name="staff status", - ), - ), - ( - "is_active", - models.BooleanField( - default=True, - help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", - verbose_name="active", - ), - ), - ( - "date_joined", - models.DateTimeField( - default=django.utils.timezone.now, verbose_name="date joined" - ), - ), - ( - "groups", - models.ManyToManyField( - blank=True, - help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", - related_name="user_set", - related_query_name="user", - to="auth.group", - verbose_name="groups", - ), - ), - ( - "user_permissions", - models.ManyToManyField( - blank=True, - help_text="Specific permissions for this user.", - related_name="user_set", - related_query_name="user", - to="auth.permission", - verbose_name="user permissions", - ), - ), + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), + ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), + ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), + ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), + ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), + ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), + ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), + ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), + ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), ], options={ - "verbose_name": "user", - "verbose_name_plural": "users", - "abstract": False, + 'verbose_name': 'user', + 'verbose_name_plural': 'users', + 'abstract': False, }, managers=[ - ("objects", django.contrib.auth.models.UserManager()), + ('objects', django.contrib.auth.models.UserManager()), ], ), migrations.CreateModel( - name="ShoppingList", + name='ShoppingList', fields=[ - ( - "id", - models.UUIDField( - default=uuid.uuid4, - editable=False, - primary_key=True, - serialize=False, - ), - ), - ("name", models.CharField(max_length=200)), - ("last_interaction", models.DateTimeField(auto_now=True)), - ("members", models.ManyToManyField(to=settings.AUTH_USER_MODEL)), + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('name', models.CharField(max_length=200)), + ('last_interaction', models.DateTimeField(auto_now=True)), + ('members', models.ManyToManyField(to=settings.AUTH_USER_MODEL)), ], ), migrations.CreateModel( - name="ShoppingItem", + name='ShoppingItem', fields=[ - ( - "id", - models.UUIDField( - default=uuid.uuid4, primary_key=True, serialize=False - ), - ), - ("name", models.CharField(max_length=100)), - ("purchased", models.BooleanField()), - ( - "shopping_list", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="shopping_items", - to="shopping_list.shoppinglist", - ), - ), + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('name', models.CharField(max_length=100)), + ('purchased', models.BooleanField()), + ('shopping_list', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='shopping_items', to='shopping_list.shoppinglist')), ], ), ] diff --git a/shopping_list/models.py b/shopping_list/models.py index 9d6a8e1..8d8aaea 100644 --- a/shopping_list/models.py +++ b/shopping_list/models.py @@ -1,6 +1,3 @@ -# shopping_list/models.py - - import uuid from django.conf import settings @@ -8,10 +5,14 @@ from django.db import models +class User(AbstractUser): + pass + + class ShoppingList(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) name = models.CharField(max_length=200) - members = models.ManyToManyField(settings.AUTH_USER_MODEL) # UPDATED + members = models.ManyToManyField(settings.AUTH_USER_MODEL) last_interaction = models.DateTimeField(auto_now=True) def __str__(self): @@ -22,13 +23,7 @@ class ShoppingItem(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4) name = models.CharField(max_length=100) purchased = models.BooleanField() - shopping_list = models.ForeignKey( - ShoppingList, on_delete=models.CASCADE, related_name="shopping_items" - ) + shopping_list = models.ForeignKey(ShoppingList, on_delete=models.CASCADE, related_name="shopping_items") def __str__(self): return self.name - - -class User(AbstractUser): - pass diff --git a/shopping_list/receivers.py b/shopping_list/receivers.py index 3bff2d3..a9dc394 100644 --- a/shopping_list/receivers.py +++ b/shopping_list/receivers.py @@ -1,14 +1,9 @@ -# shopping_list/receivers.py - - from django.db.models.signals import post_save from django.dispatch import receiver -from shopping_list.models import ShoppingItem, ShoppingList +from shopping_list.models import ShoppingItem @receiver(post_save, sender=ShoppingItem) def interaction_with_shopping_list(sender, instance, **kwargs): - ShoppingList.objects.get(id=instance.shopping_list.id).save( - update_fields=["last_interaction"] - ) + instance.shopping_list.save(update_fields=["last_interaction"]) diff --git a/shopping_list/tests/conftest.py b/shopping_list/tests/conftest.py index 1393d9b..1594389 100644 --- a/shopping_list/tests/conftest.py +++ b/shopping_list/tests/conftest.py @@ -1,6 +1,3 @@ -# shopping_list/tests/conftest.py - - import pytest from rest_framework.test import APIClient @@ -9,12 +6,10 @@ @pytest.fixture(scope="session") def create_shopping_item(): - def _create_shopping_item(name, user): # UPDATED! + def _create_shopping_item(name, user): shopping_list = ShoppingList.objects.create(name="My shopping list") - shopping_list.members.add(user) # UPDATED! - shopping_item = ShoppingItem.objects.create( - name=name, purchased=False, shopping_list=shopping_list - ) + shopping_list.members.add(user) + shopping_item = ShoppingItem.objects.create(name=name, purchased=False, shopping_list=shopping_list) return shopping_item @@ -24,9 +19,7 @@ def _create_shopping_item(name, user): # UPDATED! @pytest.fixture(scope="session") def create_user(): def _create_user(): - return User.objects.create_user( - "GirlThatLovesToCode", "girl@lovescode.com", "something" - ) + return User.objects.create_user("GirlThatLovesToCode", "girl@lovescode.com", "something") return _create_user @@ -42,7 +35,6 @@ def _create_authenticated_client(user): return _create_authenticated_client -# NEW! @pytest.fixture(scope="session") def create_shopping_list(): def _create_shopping_list(user): diff --git a/shopping_list/tests/tests.py b/shopping_list/tests/tests.py index 67aea53..abdc9f6 100644 --- a/shopping_list/tests/tests.py +++ b/shopping_list/tests/tests.py @@ -1,9 +1,9 @@ -# shopping_list/tests.py -from datetime import datetime, timedelta +from datetime import timedelta from unittest import mock import pytest from django.urls import reverse +from django.utils import timezone from rest_framework import status from rest_framework.test import APIClient @@ -24,9 +24,7 @@ def test_valid_shopping_list_is_created(create_user, create_authenticated_client @pytest.mark.django_db -def test_shopping_list_name_missing_returns_bad_request( - create_user, create_authenticated_client -): +def test_shopping_list_name_missing_returns_bad_request(create_user, create_authenticated_client): url = reverse("all-shopping-lists") data = {"something_else": "blahblah"} client = create_authenticated_client(create_user()) @@ -36,9 +34,7 @@ def test_shopping_list_name_missing_returns_bad_request( @pytest.mark.django_db -def test_client_retrieves_only_shopping_lists_they_are_member_of( - create_user, create_authenticated_client -): +def test_client_retrieves_only_shopping_lists_they_are_member_of(create_user, create_authenticated_client): user = create_user() client = create_authenticated_client(user) @@ -46,9 +42,7 @@ def test_client_retrieves_only_shopping_lists_they_are_member_of( shopping_list_1 = ShoppingList.objects.create(name="Groceries") shopping_list_1.members.add(user) - another_user = User.objects.create_user( - "SomeoneElse", "someone@else.com", "something" - ) + another_user = User.objects.create_user("SomeoneElse", "someone@else.com", "something") shopping_list_2 = ShoppingList.objects.create(name="Books") shopping_list_2.members.add(another_user) @@ -61,9 +55,7 @@ def test_client_retrieves_only_shopping_lists_they_are_member_of( @pytest.mark.django_db -def test_shopping_list_is_retrieved_by_id( - create_user, create_authenticated_client, create_shopping_list -): +def test_shopping_list_is_retrieved_by_id(create_user, create_authenticated_client, create_shopping_list): user = create_user() client = create_authenticated_client(user) shopping_list = create_shopping_list(user) @@ -77,20 +69,17 @@ def test_shopping_list_is_retrieved_by_id( @pytest.mark.django_db -def test_shopping_list_includes_only_corresponding_items( - create_user, create_authenticated_client, create_shopping_list -): +def test_shopping_list_includes_only_corresponding_items(create_user, create_authenticated_client): user = create_user() client = create_authenticated_client(user) - shopping_list = create_shopping_list(user) + + shopping_list = ShoppingList.objects.create(name="Groceries") + shopping_list.members.add(user) another_shopping_list = ShoppingList.objects.create(name="Books") + another_shopping_list.members.add(user) - ShoppingItem.objects.create( - shopping_list=shopping_list, name="Eggs", purchased=False - ) - ShoppingItem.objects.create( - shopping_list=another_shopping_list, name="The seven sisters", purchased=False - ) + ShoppingItem.objects.create(shopping_list=shopping_list, name="Eggs", purchased=False) + ShoppingItem.objects.create(shopping_list=another_shopping_list, name="The seven sisters", purchased=False) url = reverse("shopping-list-detail", args=[shopping_list.id]) response = client.get(url) @@ -100,9 +89,7 @@ def test_shopping_list_includes_only_corresponding_items( @pytest.mark.django_db -def test_shopping_list_name_is_changed( - create_user, create_authenticated_client, create_shopping_list -): +def test_shopping_list_name_is_changed(create_user, create_authenticated_client, create_shopping_list): user = create_user() client = create_authenticated_client(user) shopping_list = create_shopping_list(user) @@ -120,9 +107,7 @@ def test_shopping_list_name_is_changed( @pytest.mark.django_db -def test_shopping_list_not_changed_because_name_missing( - create_user, create_authenticated_client, create_shopping_list -): +def test_shopping_list_not_changed_because_name_missing(create_user, create_authenticated_client, create_shopping_list): user = create_user() client = create_authenticated_client(user) shopping_list = create_shopping_list(user) @@ -157,9 +142,7 @@ def test_shopping_list_name_is_changed_with_partial_update( @pytest.mark.django_db -def test_partial_update_with_missing_name_has_no_impact( - create_user, create_authenticated_client, create_shopping_list -): +def test_partial_update_with_missing_name_has_no_impact(create_user, create_authenticated_client, create_shopping_list): user = create_user() client = create_authenticated_client(user) shopping_list = create_shopping_list(user) @@ -174,9 +157,7 @@ def test_partial_update_with_missing_name_has_no_impact( @pytest.mark.django_db -def test_shopping_list_is_deleted( - create_user, create_authenticated_client, create_shopping_list -): +def test_shopping_list_is_deleted(create_user, create_authenticated_client, create_shopping_list): user = create_user() client = create_authenticated_client(user) shopping_list = create_shopping_list(user) @@ -190,9 +171,72 @@ def test_shopping_list_is_deleted( @pytest.mark.django_db -def test_valid_shopping_item_is_created( +def test_update_shopping_list_restricted_if_not_member(create_user, create_authenticated_client, create_shopping_list): + user = create_user() + shopping_list_creator = User.objects.create_user("Creator", "creator@list.com", "something") + client = create_authenticated_client(user) + shopping_list = create_shopping_list(shopping_list_creator) + + url = reverse("shopping-list-detail", args=[shopping_list.id]) + + data = { + "name": "Food", + } + + response = client.put(url, data=data, format="json") + + assert response.status_code == status.HTTP_403_FORBIDDEN + + +@pytest.mark.django_db +def test_partial_update_shopping_list_restricted_if_not_member( create_user, create_authenticated_client, create_shopping_list ): + user = create_user() + shopping_list_creator = User.objects.create_user("Creator", "creator@list.com", "something") + client = create_authenticated_client(user) + shopping_list = create_shopping_list(shopping_list_creator) + + url = reverse("shopping-list-detail", args=[shopping_list.id]) + + data = { + "name": "Food", + } + + response = client.patch(url, data=data, format="json") + + assert response.status_code == status.HTTP_403_FORBIDDEN + + +@pytest.mark.django_db +def test_delete_shopping_list_restricted_if_not_member(create_user, create_authenticated_client, create_shopping_list): + user = create_user() + shopping_list_creator = User.objects.create_user("Creator", "creator@list.com", "something") + client = create_authenticated_client(user) + shopping_list = create_shopping_list(shopping_list_creator) + + url = reverse("shopping-list-detail", args=[shopping_list.id]) + + response = client.delete(url, format="json") + + assert response.status_code == status.HTTP_403_FORBIDDEN + + +@pytest.mark.django_db +def test_admin_can_retrieve_shopping_list(create_user, create_shopping_list, admin_client): + + user = create_user() + shopping_list = create_shopping_list(user) + + url = reverse("shopping-list-detail", args=[shopping_list.id]) + + response = admin_client.get(url, format="json") + + assert response.status_code == status.HTTP_200_OK + + +@pytest.mark.django_db +def test_valid_shopping_item_is_created(create_user, create_authenticated_client, create_shopping_list): user = create_user() client = create_authenticated_client(user) shopping_list = create_shopping_list(user) @@ -224,17 +268,12 @@ def test_create_shopping_item_missing_data_returns_bad_request( @pytest.mark.django_db -def test_shopping_item_is_retrieved_by_id( - create_user, create_authenticated_client, create_shopping_item -): +def test_shopping_item_is_retrieved_by_id(create_user, create_authenticated_client, create_shopping_item): user = create_user() client = create_authenticated_client(user) shopping_item = create_shopping_item(name="Chocolate", user=user) - url = reverse( - "shopping-item-detail", - kwargs={"pk": shopping_item.shopping_list.id, "item_pk": shopping_item.id}, - ) + url = reverse("shopping-item-detail", kwargs={"pk": shopping_item.shopping_list.id, "item_pk": shopping_item.id}) response = client.get(url) @@ -243,17 +282,12 @@ def test_shopping_item_is_retrieved_by_id( @pytest.mark.django_db -def test_change_shopping_item_purchased_status( - create_user, create_authenticated_client, create_shopping_item -): +def test_change_shopping_item_purchased_status(create_user, create_authenticated_client, create_shopping_item): user = create_user() client = create_authenticated_client(user) shopping_item = create_shopping_item(name="Chocolate", user=user) - url = reverse( - "shopping-item-detail", - kwargs={"pk": shopping_item.shopping_list.id, "item_pk": shopping_item.id}, - ) + url = reverse("shopping-item-detail", kwargs={"pk": shopping_item.shopping_list.id, "item_pk": shopping_item.id}) data = {"name": "Chocolate", "purchased": True} response = client.put(url, data, format="json") @@ -270,10 +304,7 @@ def test_change_shopping_item_purchased_status_with_missing_data_returns_bad_req client = create_authenticated_client(user) shopping_item = create_shopping_item(name="Chocolate", user=user) - url = reverse( - "shopping-item-detail", - kwargs={"pk": shopping_item.shopping_list.id, "item_pk": shopping_item.id}, - ) + url = reverse("shopping-item-detail", kwargs={"pk": shopping_item.shopping_list.id, "item_pk": shopping_item.id}) data = {"purchased": True} response = client.put(url, data, format="json") @@ -289,10 +320,7 @@ def test_change_shopping_item_purchased_status_with_partial_update( client = create_authenticated_client(user) shopping_item = create_shopping_item(name="Chocolate", user=user) - url = reverse( - "shopping-item-detail", - kwargs={"pk": shopping_item.shopping_list.id, "item_pk": shopping_item.id}, - ) + url = reverse("shopping-item-detail", kwargs={"pk": shopping_item.shopping_list.id, "item_pk": shopping_item.id}) data = {"purchased": True} response = client.patch(url, data, format="json") @@ -302,17 +330,12 @@ def test_change_shopping_item_purchased_status_with_partial_update( @pytest.mark.django_db -def test_shopping_item_is_deleted( - create_user, create_authenticated_client, create_shopping_item -): +def test_shopping_item_is_deleted(create_user, create_authenticated_client, create_shopping_item): user = create_user() client = create_authenticated_client(user) shopping_item = create_shopping_item(name="Chocolate", user=user) - url = reverse( - "shopping-item-detail", - kwargs={"pk": shopping_item.shopping_list.id, "item_pk": shopping_item.id}, - ) + url = reverse("shopping-item-detail", kwargs={"pk": shopping_item.shopping_list.id, "item_pk": shopping_item.id}) response = client.delete(url) @@ -321,92 +344,11 @@ def test_shopping_item_is_deleted( @pytest.mark.django_db -def test_update_shopping_list_restricted_if_not_member( - create_user, create_authenticated_client, create_shopping_list -): +def test_not_member_of_list_can_not_add_shopping_item(create_user, create_authenticated_client, create_shopping_list): user = create_user() - shopping_list_creator = User.objects.create_user( - "Creator", "creator@list.com", "something" - ) client = create_authenticated_client(user) - shopping_list = create_shopping_list(shopping_list_creator) - - url = reverse("shopping-list-detail", args=[shopping_list.id]) - - data = { - "name": "Food", - } - - response = client.put(url, data=data, format="json") - assert response.status_code == status.HTTP_403_FORBIDDEN - - -@pytest.mark.django_db -def test_partial_update_shopping_list_restricted_if_not_member( - create_user, create_authenticated_client, create_shopping_list -): - user = create_user() - shopping_list_creator = User.objects.create_user( - "Creator", "creator@list.com", "something" - ) - client = create_authenticated_client(user) - shopping_list = create_shopping_list(shopping_list_creator) - - url = reverse("shopping-list-detail", args=[shopping_list.id]) - - data = { - "name": "Food", - } - - response = client.patch(url, data=data, format="json") - - assert response.status_code == status.HTTP_403_FORBIDDEN - - -@pytest.mark.django_db -def test_delete_shopping_list_restricted_if_not_member( - create_user, create_authenticated_client, create_shopping_list -): - user = create_user() - shopping_list_creator = User.objects.create_user( - "Creator", "creator@list.com", "something" - ) - client = create_authenticated_client(user) - shopping_list = create_shopping_list(shopping_list_creator) - - url = reverse("shopping-list-detail", args=[shopping_list.id]) - - response = client.delete(url, format="json") - - assert response.status_code == status.HTTP_403_FORBIDDEN - - -@pytest.mark.django_db -def test_admin_can_retrieve_shopping_list( - create_user, create_shopping_list, admin_client -): - - user = create_user() - shopping_list = create_shopping_list(user) - - url = reverse("shopping-list-detail", args=[shopping_list.id]) - - response = admin_client.get(url, format="json") - - assert response.status_code == status.HTTP_200_OK - - -@pytest.mark.django_db -def test_not_member_of_list_can_not_add_shopping_item( - create_user, create_authenticated_client, create_shopping_list -): - user = create_user() - client = create_authenticated_client(user) - - shopping_list_creator = User.objects.create_user( - "Creator", "creator@list.com", "something" - ) + shopping_list_creator = User.objects.create_user("Creator", "creator@list.com", "something") shopping_list = create_shopping_list(shopping_list_creator) url = reverse("list-add-shopping-item", args=[shopping_list.id]) @@ -437,16 +379,11 @@ def test_shopping_item_detail_access_restricted_if_not_member_of_shopping_list( create_user, create_authenticated_client, create_shopping_item ): user = create_user() - shopping_list_creator = User.objects.create_user( - "Creator", "creator@list.com", "something" - ) + shopping_list_creator = User.objects.create_user("Creator", "creator@list.com", "something") client = create_authenticated_client(user) shopping_item = create_shopping_item(name="Chocolate", user=shopping_list_creator) - url = reverse( - "shopping-item-detail", - kwargs={"pk": shopping_item.shopping_list.id, "item_pk": shopping_item.id}, - ) + url = reverse("shopping-item-detail", kwargs={"pk": shopping_item.shopping_list.id, "item_pk": shopping_item.id}) response = client.get(url, format="json") @@ -458,16 +395,11 @@ def test_shopping_item_update_restricted_if_not_member_of_shopping_list( create_user, create_authenticated_client, create_shopping_item ): user = create_user() - shopping_list_creator = User.objects.create_user( - "Creator", "creator@list.com", "something" - ) + shopping_list_creator = User.objects.create_user("Creator", "creator@list.com", "something") client = create_authenticated_client(user) shopping_item = create_shopping_item(name="Chocolate", user=shopping_list_creator) - url = reverse( - "shopping-item-detail", - kwargs={"pk": shopping_item.shopping_list.id, "item_pk": shopping_item.id}, - ) + url = reverse("shopping-item-detail", kwargs={"pk": shopping_item.shopping_list.id, "item_pk": shopping_item.id}) data = {"name": "Chocolate", "purchased": True} @@ -481,16 +413,11 @@ def test_shopping_item_partial_update_restricted_if_not_member_of_shopping_list( create_user, create_authenticated_client, create_shopping_item ): user = create_user() - shopping_list_creator = User.objects.create_user( - "Creator", "creator@list.com", "something" - ) + shopping_list_creator = User.objects.create_user("Creator", "creator@list.com", "something") client = create_authenticated_client(user) shopping_item = create_shopping_item(name="Chocolate", user=shopping_list_creator) - url = reverse( - "shopping-item-detail", - kwargs={"pk": shopping_item.shopping_list.id, "item_pk": shopping_item.id}, - ) + url = reverse("shopping-item-detail", kwargs={"pk": shopping_item.shopping_list.id, "item_pk": shopping_item.id}) data = {"purchased": True} @@ -504,16 +431,11 @@ def test_shopping_item_delete_restricted_if_not_member_of_shopping_list( create_user, create_authenticated_client, create_shopping_item ): user = create_user() - shopping_list_creator = User.objects.create_user( - "Creator", "creator@list.com", "something" - ) + shopping_list_creator = User.objects.create_user("Creator", "creator@list.com", "something") client = create_authenticated_client(user) shopping_item = create_shopping_item(name="Chocolate", user=shopping_list_creator) - url = reverse( - "shopping-item-detail", - kwargs={"pk": shopping_item.shopping_list.id, "item_pk": shopping_item.id}, - ) + url = reverse("shopping-item-detail", kwargs={"pk": shopping_item.shopping_list.id, "item_pk": shopping_item.id}) response = client.delete(url) @@ -521,16 +443,11 @@ def test_shopping_item_delete_restricted_if_not_member_of_shopping_list( @pytest.mark.django_db -def test_admin_can_retrieve_single_shopping_item( - create_user, create_shopping_item, admin_client -): +def test_admin_can_retrieve_single_shopping_item(create_user, create_shopping_item, admin_client): user = create_user() shopping_item = create_shopping_item("Milk", user) - url = reverse( - "shopping-item-detail", - kwargs={"pk": shopping_item.shopping_list.id, "item_pk": shopping_item.id}, - ) + url = reverse("shopping-item-detail", kwargs={"pk": shopping_item.shopping_list.id, "item_pk": shopping_item.id}) response = admin_client.get(url) @@ -543,12 +460,8 @@ def test_list_shopping_items_is_retrieved_by_shopping_list_member( ): user = create_user() shopping_list = create_shopping_list(user) - shopping_item_1 = ShoppingItem.objects.create( - name="Oranges", purchased=False, shopping_list=shopping_list - ) - shopping_item_2 = ShoppingItem.objects.create( - name="Milk", purchased=False, shopping_list=shopping_list - ) + shopping_item_1 = ShoppingItem.objects.create(name="Oranges", purchased=False, shopping_list=shopping_list) + shopping_item_2 = ShoppingItem.objects.create(name="Milk", purchased=False, shopping_list=shopping_list) client = create_authenticated_client(user) url = reverse("list-add-shopping-item", kwargs={"pk": shopping_list.id}) @@ -560,19 +473,13 @@ def test_list_shopping_items_is_retrieved_by_shopping_list_member( @pytest.mark.django_db -def test_not_member_can_not_retrieve_shopping_items( - create_user, create_authenticated_client, create_shopping_item -): - shopping_list_creator = User.objects.create_user( - "SomeoneElse", "someone@else.com", "something" - ) +def test_not_member_can_not_retrieve_shopping_items(create_user, create_authenticated_client, create_shopping_item): + shopping_list_creator = User.objects.create_user("SomeoneElse", "someone@else.com", "something") shopping_item = create_shopping_item("Milk", shopping_list_creator) user = create_user() client = create_authenticated_client(user) - url = reverse( - "list-add-shopping-item", kwargs={"pk": shopping_item.shopping_list.id} - ) + url = reverse("list-add-shopping-item", kwargs={"pk": shopping_item.shopping_list.id}) response = client.get(url) @@ -593,11 +500,7 @@ def test_list_shopping_items_only_the_ones_belonging_to_the_same_shopping_list( another_shopping_list = ShoppingList.objects.create(name="Another list") another_shopping_list.members.add(user) - ShoppingItem.objects.create( - name="Item from another list", - purchased=False, - shopping_list=another_shopping_list, - ) + ShoppingItem.objects.create(name="Item from another list", purchased=False, shopping_list=another_shopping_list) client = create_authenticated_client(user) url = reverse("list-add-shopping-item", kwargs={"pk": shopping_list.id}) @@ -609,26 +512,16 @@ def test_list_shopping_items_only_the_ones_belonging_to_the_same_shopping_list( @pytest.mark.django_db -def test_max_3_shopping_items_on_shopping_list( - create_user, create_authenticated_client, create_shopping_list -): +def test_max_3_shopping_items_on_shopping_list(create_user, create_authenticated_client, create_shopping_list): user = create_user() client = create_authenticated_client(user) shopping_list = create_shopping_list(user) - ShoppingItem.objects.create( - shopping_list=shopping_list, name="Eggs", purchased=False - ) - ShoppingItem.objects.create( - shopping_list=shopping_list, name="Chocolate", purchased=False - ) - ShoppingItem.objects.create( - shopping_list=shopping_list, name="Milk", purchased=False - ) - ShoppingItem.objects.create( - shopping_list=shopping_list, name="Mango", purchased=False - ) + ShoppingItem.objects.create(shopping_list=shopping_list, name="Eggs", purchased=False) + ShoppingItem.objects.create(shopping_list=shopping_list, name="Chocolate", purchased=False) + ShoppingItem.objects.create(shopping_list=shopping_list, name="Milk", purchased=False) + ShoppingItem.objects.create(shopping_list=shopping_list, name="Mango", purchased=False) url = reverse("shopping-list-detail", args=[shopping_list.id]) @@ -646,15 +539,9 @@ def test_all_shopping_items_on_shopping_list_unpurchased( shopping_list = create_shopping_list(user) - ShoppingItem.objects.create( - shopping_list=shopping_list, name="Eggs", purchased=False - ) - ShoppingItem.objects.create( - shopping_list=shopping_list, name="Chocolate", purchased=True - ) - ShoppingItem.objects.create( - shopping_list=shopping_list, name="Milk", purchased=False - ) + ShoppingItem.objects.create(shopping_list=shopping_list, name="Eggs", purchased=False) + ShoppingItem.objects.create(shopping_list=shopping_list, name="Chocolate", purchased=True) + ShoppingItem.objects.create(shopping_list=shopping_list, name="Milk", purchased=False) url = reverse("shopping-list-detail", args=[shopping_list.id]) @@ -664,17 +551,13 @@ def test_all_shopping_items_on_shopping_list_unpurchased( @pytest.mark.django_db -def test_duplicate_item_on_list_bad_request( - create_user, create_authenticated_client, create_shopping_list -): +def test_duplicate_item_on_list_bad_request(create_user, create_authenticated_client, create_shopping_list): user = create_user() client = create_authenticated_client(user) shopping_list = create_shopping_list(user) - ShoppingItem.objects.create( - shopping_list=shopping_list, name="Milk", purchased=False - ) + ShoppingItem.objects.create(shopping_list=shopping_list, name="Milk", purchased=False) url = reverse("list-add-shopping-item", args=[shopping_list.id]) @@ -692,8 +575,8 @@ def test_correct_order_shopping_lists(create_user, create_authenticated_client): user = create_user() client = create_authenticated_client(user) - old_time = datetime.now() - timedelta(days=1) - older_time = datetime.now() - timedelta(days=100) + old_time = timezone.now() - timedelta(days=1) + older_time = timezone.now() - timedelta(days=100) with mock.patch("django.utils.timezone.now") as mock_now: mock_now.return_value = old_time @@ -712,15 +595,13 @@ def test_correct_order_shopping_lists(create_user, create_authenticated_client): @pytest.mark.django_db -def test_shopping_lists_order_changed_when_item_marked_purchased( - create_user, create_authenticated_client -): +def test_shopping_lists_order_changed_when_item_marked_purchased(create_user, create_authenticated_client): user = create_user() client = create_authenticated_client(user) - more_recent_time = datetime.now() - timedelta(days=1) - older_time = datetime.now() - timedelta(days=20) + more_recent_time = timezone.now() - timedelta(days=1) + older_time = timezone.now() - timedelta(days=20) with mock.patch("django.utils.timezone.now") as mock_now: mock_now.return_value = older_time @@ -731,13 +612,10 @@ def test_shopping_lists_order_changed_when_item_marked_purchased( ) mock_now.return_value = more_recent_time - ShoppingList.objects.create( - name="Recent", last_interaction=datetime.now() - timedelta(days=100) - ).members.add(user) + ShoppingList.objects.create(name="Recent").members.add(user) shopping_item_url = reverse( - "shopping-item-detail", - kwargs={"pk": older_list.id, "item_pk": shopping_item_on_older_list.id}, + "shopping-item-detail", kwargs={"pk": older_list.id, "item_pk": shopping_item_on_older_list.id} ) shopping_lists_url = reverse("all-shopping-lists") @@ -773,15 +651,13 @@ def test_call_with_token_authentication(): @pytest.mark.django_db -def test_add_members_list_member( - create_user, create_authenticated_client, create_shopping_list -): +def test_add_members_list_member(create_user, create_authenticated_client, create_shopping_list): user = create_user() client = create_authenticated_client(user) shopping_list = create_shopping_list(user) - another_member = User.objects.create(username="another_member", password="whocares") - third_member = User.objects.create(username="third_member", password="whocares") + another_member = User.objects.create_user(username="another_member", password="whocares") + third_member = User.objects.create_user(username="third_member", password="whocares") data = {"members": [another_member.id, third_member.id]} @@ -795,13 +671,11 @@ def test_add_members_list_member( @pytest.mark.django_db -def test_add_members_not_list_member( - create_user, create_authenticated_client, create_shopping_list -): +def test_add_members_not_list_member(create_user, create_authenticated_client, create_shopping_list): user = create_user() client = create_authenticated_client(user) - list_creator = User.objects.create(username="list_creator", password="whocares") + list_creator = User.objects.create_user(username="list_creator", password="whocares") shopping_list = create_shopping_list(list_creator) data = {"members": [user.id]} @@ -814,9 +688,7 @@ def test_add_members_not_list_member( @pytest.mark.django_db -def test_add_members_wrong_data( - create_user, create_authenticated_client, create_shopping_list -): +def test_add_members_wrong_data(create_user, create_authenticated_client, create_shopping_list): user = create_user() client = create_authenticated_client(user) shopping_list = create_shopping_list(user) @@ -831,15 +703,13 @@ def test_add_members_wrong_data( @pytest.mark.django_db -def test_remove_members_list_member( - create_user, create_authenticated_client, create_shopping_list -): +def test_remove_members_list_member(create_user, create_authenticated_client, create_shopping_list): user = create_user() client = create_authenticated_client(user) shopping_list = create_shopping_list(user) - another_member = User.objects.create(username="another_member", password="whocares") - third_member = User.objects.create(username="third_member", password="whocares") + another_member = User.objects.create_user(username="another_member", password="whocares") + third_member = User.objects.create_user(username="third_member", password="whocares") shopping_list.members.add(another_member) shopping_list.members.add(third_member) @@ -855,13 +725,11 @@ def test_remove_members_list_member( @pytest.mark.django_db -def test_remove_members_not_list_member( - create_user, create_authenticated_client, create_shopping_list -): +def test_remove_members_not_list_member(create_user, create_authenticated_client, create_shopping_list): user = create_user() client = create_authenticated_client(user) - list_creator = User.objects.create(username="list_creator", password="whocares") + list_creator = User.objects.create_user(username="list_creator", password="whocares") shopping_list = create_shopping_list(list_creator) data = {"members": [user.id]} @@ -874,9 +742,7 @@ def test_remove_members_not_list_member( @pytest.mark.django_db -def test_remove_members_wrong_data( - create_user, create_authenticated_client, create_shopping_list -): +def test_remove_members_wrong_data(create_user, create_authenticated_client, create_shopping_list): user = create_user() client = create_authenticated_client(user) shopping_list = create_shopping_list(user) @@ -891,9 +757,7 @@ def test_remove_members_wrong_data( @pytest.mark.django_db -def test_search_returns_corresponding_shopping_item( - create_user, create_authenticated_client, create_shopping_item -): +def test_search_returns_corresponding_shopping_item(create_user, create_authenticated_client, create_shopping_item): user = create_user() client = create_authenticated_client(user) @@ -910,14 +774,10 @@ def test_search_returns_corresponding_shopping_item( @pytest.mark.django_db -def test_search_returns_only_users_results( - create_user, create_authenticated_client, create_shopping_item -): +def test_search_returns_only_users_results(create_user, create_authenticated_client, create_shopping_item): user = create_user() client = create_authenticated_client(user) - another_user = User.objects.create_user( - "SomeOtherUser", "someother@user.com", "something" - ) + another_user = User.objects.create_user("SomeOtherUser", "someother@user.com", "something") create_shopping_item("Milk", user) create_shopping_item("Milk", another_user) @@ -931,19 +791,13 @@ def test_search_returns_only_users_results( @pytest.mark.django_db -def test_order_shopping_items_names_ascending( - create_user, create_authenticated_client, create_shopping_list -): +def test_order_shopping_items_names_ascending(create_user, create_authenticated_client, create_shopping_list): user = create_user() client = create_authenticated_client(user) shopping_list = create_shopping_list(user) - ShoppingItem.objects.create( - name="Bananas", purchased=False, shopping_list=shopping_list - ) - ShoppingItem.objects.create( - name="Apples", purchased=False, shopping_list=shopping_list - ) + ShoppingItem.objects.create(name="Bananas", purchased=False, shopping_list=shopping_list) + ShoppingItem.objects.create(name="Apples", purchased=False, shopping_list=shopping_list) order_param = "?ordering=name" url = reverse("list-add-shopping-item", args=[shopping_list.id]) + order_param @@ -955,19 +809,13 @@ def test_order_shopping_items_names_ascending( @pytest.mark.django_db -def test_order_shopping_items_names_descending( - create_user, create_authenticated_client, create_shopping_list -): +def test_order_shopping_items_names_descending(create_user, create_authenticated_client, create_shopping_list): user = create_user() client = create_authenticated_client(user) shopping_list = create_shopping_list(user) - ShoppingItem.objects.create( - name="Apples", purchased=False, shopping_list=shopping_list - ) - ShoppingItem.objects.create( - name="Bananas", purchased=False, shopping_list=shopping_list - ) + ShoppingItem.objects.create(name="Apples", purchased=False, shopping_list=shopping_list) + ShoppingItem.objects.create(name="Bananas", purchased=False, shopping_list=shopping_list) order_param = "?ordering=-name" url = reverse("list-add-shopping-item", args=[shopping_list.id]) + order_param @@ -979,19 +827,13 @@ def test_order_shopping_items_names_descending( @pytest.mark.django_db -def test_order_shopping_items_unpurchased_first( - create_user, create_authenticated_client, create_shopping_list -): +def test_order_shopping_items_unpurchased_first(create_user, create_authenticated_client, create_shopping_list): user = create_user() client = create_authenticated_client(user) shopping_list = create_shopping_list(user) - ShoppingItem.objects.create( - name="Apples", purchased=False, shopping_list=shopping_list - ) - ShoppingItem.objects.create( - name="Bananas", purchased=True, shopping_list=shopping_list - ) + ShoppingItem.objects.create(name="Apples", purchased=False, shopping_list=shopping_list) + ShoppingItem.objects.create(name="Bananas", purchased=True, shopping_list=shopping_list) order_param = "?ordering=purchased" url = reverse("list-add-shopping-item", args=[shopping_list.id]) + order_param @@ -1003,19 +845,13 @@ def test_order_shopping_items_unpurchased_first( @pytest.mark.django_db -def test_order_shopping_items_purchased_first( - create_user, create_authenticated_client, create_shopping_list -): +def test_order_shopping_items_purchased_first(create_user, create_authenticated_client, create_shopping_list): user = create_user() client = create_authenticated_client(user) shopping_list = create_shopping_list(user) - ShoppingItem.objects.create( - name="Apples", purchased=False, shopping_list=shopping_list - ) - ShoppingItem.objects.create( - name="Bananas", purchased=True, shopping_list=shopping_list - ) + ShoppingItem.objects.create(name="Apples", purchased=False, shopping_list=shopping_list) + ShoppingItem.objects.create(name="Bananas", purchased=True, shopping_list=shopping_list) order_param = "?ordering=-purchased" url = reverse("list-add-shopping-item", args=[shopping_list.id]) + order_param @@ -1027,25 +863,15 @@ def test_order_shopping_items_purchased_first( @pytest.mark.django_db -def test_order_shopping_items_purchased_and_names( - create_user, create_authenticated_client, create_shopping_list -): +def test_order_shopping_items_purchased_and_names(create_user, create_authenticated_client, create_shopping_list): user = create_user() client = create_authenticated_client(user) shopping_list = create_shopping_list(user) - ShoppingItem.objects.create( - name="Apples", purchased=True, shopping_list=shopping_list - ) - ShoppingItem.objects.create( - name="Bananas", purchased=False, shopping_list=shopping_list - ) - ShoppingItem.objects.create( - name="Coconut", purchased=True, shopping_list=shopping_list - ) - ShoppingItem.objects.create( - name="Dates", purchased=False, shopping_list=shopping_list - ) + ShoppingItem.objects.create(name="Apples", purchased=True, shopping_list=shopping_list) + ShoppingItem.objects.create(name="Bananas", purchased=False, shopping_list=shopping_list) + ShoppingItem.objects.create(name="Coconut", purchased=True, shopping_list=shopping_list) + ShoppingItem.objects.create(name="Dates", purchased=False, shopping_list=shopping_list) order_param = "?ordering=purchased,name" url = reverse("list-add-shopping-item", args=[shopping_list.id]) + order_param diff --git a/shopping_list/urls.py b/shopping_list/urls.py index 5992ae6..d4c89ee 100644 --- a/shopping_list/urls.py +++ b/shopping_list/urls.py @@ -1,8 +1,5 @@ -# shopping_list/urls.py - - from django.urls import include, path -from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView # NEW +from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView from rest_framework.authtoken.views import obtain_auth_token from shopping_list.api.views import ( @@ -18,43 +15,23 @@ urlpatterns = [ path("api-auth/", include("rest_framework.urls", namespace="rest_framework")), path("api-token-auth/", obtain_auth_token, name="api_token_auth"), + path("api/search-shopping-items/", SearchShoppingItems.as_view(), name="search-shopping-items"), + path("api/shopping-lists/", ListAddShoppingList.as_view(), name="all-shopping-lists"), + path("api/shopping-lists//", ShoppingListDetail.as_view(), name="shopping-list-detail"), path( - "api/search-shopping-items/", - SearchShoppingItems.as_view(), - name="search-shopping-items", - ), # NEW - path( - "api/shopping-lists/", ListAddShoppingList.as_view(), name="all-shopping-lists" - ), - path( - "api/shopping-lists//", - ShoppingListDetail.as_view(), - name="shopping-list-detail", + "api/shopping-lists//add-members/", ShoppingListAddMembers.as_view(), name="shopping-list-add-members" ), - path( - "api/shopping-lists//add-members/", - ShoppingListAddMembers.as_view(), - name="shopping-list-add-members", - ), # NEW path( "api/shopping-lists//remove-members/", ShoppingListRemoveMembers.as_view(), name="shopping-list-remove-members", - ), # NEW - path( - "api/shopping-lists//shopping-items/", - ListAddShoppingItem.as_view(), - name="list-add-shopping-item", - ), # UPDATED + ), + path("api/shopping-lists//shopping-items/", ListAddShoppingItem.as_view(), name="list-add-shopping-item"), path( "api/shopping-lists//shopping-items//", ShoppingItemDetail.as_view(), name="shopping-item-detail", ), - path("api/schema/", SpectacularAPIView.as_view(), name="schema"), # NEW - path( - "api/docs/", - SpectacularSwaggerView.as_view(url_name="schema"), - name="swagger-ui", - ), # NEW + path("api/schema/", SpectacularAPIView.as_view(), name="schema"), + path("api/docs/", SpectacularSwaggerView.as_view(url_name="schema"), name="swagger-ui"), ] diff --git a/start_app.sh b/start_app.sh new file mode 100755 index 0000000..0201cce --- /dev/null +++ b/start_app.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# exit on error +set -o errexit +echo "Running migrations, creating super user, and collecting static" +python manage.py migrate +python manage.py createsuperuser --noinput || echo "Superuser already exists" +echo "Ready for app server" +gunicorn core.wsgi:application --bind 0.0.0.0:$PORT diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..a777aad --- /dev/null +++ b/uv.lock @@ -0,0 +1,587 @@ +version = 1 +revision = 3 +requires-python = ">=3.13" + +[[package]] +name = "asgiref" +version = "3.11.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/63/40/f03da1264ae8f7cfdbf9146542e5e7e8100a4c66ab48e791df9a03d3f6c0/asgiref-3.11.1.tar.gz", hash = "sha256:5f184dc43b7e763efe848065441eac62229c9f7b0475f41f80e207a114eda4ce", size = 38550, upload-time = "2026-02-03T13:30:14.33Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/0a/a72d10ed65068e115044937873362e6e32fab1b7dce0046aeb224682c989/asgiref-3.11.1-py3-none-any.whl", hash = "sha256:e8667a091e69529631969fd45dc268fa79b99c92c5fcdda727757e52146ec133", size = 24345, upload-time = "2026-02-03T13:30:13.039Z" }, +] + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.14.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/fd/0ab2772530e946e1be1abd0bc09e647ec9b02e88f0867857601fefca8953/coverage-7.14.1.tar.gz", hash = "sha256:30c08f7d90415aa98b3c990385dea2939b0da55f38515e5b369b83655f8523be", size = 920132, upload-time = "2026-05-26T20:41:36.783Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/9e/5f6d56327c62b185225d145191c607e07515294a0aa6338e58805cd4a5ac/coverage-7.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:be9f2c802dcfce3f71298303aa5dad0dce440a76c52f2f60dacd8656dab78793", size = 220044, upload-time = "2026-05-26T20:39:29.902Z" }, + { url = "https://files.pythonhosted.org/packages/75/92/e82aca356744cbbc0f77a0b623e38918c1872361963413a3bab5d0340393/coverage-7.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6223a72fd0e4c7156353ec0f08a5f93623e1d3034d0e2683b9bb8ea674131b1d", size = 220412, upload-time = "2026-05-26T20:39:31.561Z" }, + { url = "https://files.pythonhosted.org/packages/27/c9/385bde0bf7ed0f4bf3a7ee5367060a86b5d218718cfd6fb943c0f836b34f/coverage-7.14.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7279d2110a28cebc738b6459ecda2771735a4c18465fbbd36b3288fe5ed92247", size = 251412, upload-time = "2026-05-26T20:39:33.337Z" }, + { url = "https://files.pythonhosted.org/packages/51/8c/23faf6a2343a0d17f960a4bd56c43bc7eb4cf312f774dd6ceebd82c7d8fc/coverage-7.14.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9eeb3fcbc13ba40dfbdb22d01d196a28e9cef9ed4c29b60061a1e0e823a9929d", size = 254008, upload-time = "2026-05-26T20:39:35.009Z" }, + { url = "https://files.pythonhosted.org/packages/42/06/36f4aa9ca8a815e6036156e80706a67828bb97bd826948244f6996dda957/coverage-7.14.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f0cfc27c539f07cf5c0a4cfe211d0b6cae039f8f40526dbaa71944e64b50a7b", size = 255241, upload-time = "2026-05-26T20:39:36.71Z" }, + { url = "https://files.pythonhosted.org/packages/ca/79/95266316352f90f6b1c6736bb413302edfde2453fb32422d3911642691b3/coverage-7.14.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:221c70f316241a78e77e607c227cefc8808d4e08f28d99c04f35694690e940be", size = 257373, upload-time = "2026-05-26T20:39:38.412Z" }, + { url = "https://files.pythonhosted.org/packages/e3/9c/58316d1f66c488b5fca8a0eb3e98348807813efa8a0d0833b9021be27488/coverage-7.14.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:da028256b04ec30e5e0114b6f76172938c313991f0a2d3d894271315cf5d5e43", size = 251635, upload-time = "2026-05-26T20:39:40.268Z" }, + { url = "https://files.pythonhosted.org/packages/ef/5a/ca2398a568e16fed7bb713e84ba3603a7164fb65779abe645c565ec890d5/coverage-7.14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76a085d7005236a767e3426148b2c407e53ad61695c562f8a81da2d373324901", size = 253373, upload-time = "2026-05-26T20:39:42.145Z" }, + { url = "https://files.pythonhosted.org/packages/6e/2c/0396562c32deaebe7be51d865b3a41e9a87d7561acafe1a28f53b07e019a/coverage-7.14.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b553d04b5e778a8e56d57eb134aff42a92718ecba45e79c4764ecfa40efd92ff", size = 251341, upload-time = "2026-05-26T20:39:43.907Z" }, + { url = "https://files.pythonhosted.org/packages/fd/8f/a94f9221184c9cae1ee115820e3798e48b6b17777a9f19e46fb9a0c8dc74/coverage-7.14.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:46f714d2fb8ae2f4f29f23ada7f1e79b759fff5a70f94a1dac23af204c3ec9e4", size = 255497, upload-time = "2026-05-26T20:39:46.166Z" }, + { url = "https://files.pythonhosted.org/packages/71/69/505d70e47db1eaebcd002c39759707621ef184cd6b1ae084d9f41293f323/coverage-7.14.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:1896f5e19ff3f0431c7ce2172adc54890fd97f86b59ced8ca1649145d9ffe35d", size = 251159, upload-time = "2026-05-26T20:39:48.03Z" }, + { url = "https://files.pythonhosted.org/packages/e0/aa/58681c383aa33a9d2ed40a02d7a22fbf780d1fa4d575396365777828198c/coverage-7.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:62fd185ef9df3c33d1c8178c5af105f762afbad96038de9a4ae100aa6297ca33", size = 252934, upload-time = "2026-05-26T20:39:49.872Z" }, + { url = "https://files.pythonhosted.org/packages/eb/fd/11c928cd6bdffc7074bb5965c173d9ebf517fb00205e1da524b98d29ef92/coverage-7.14.1-cp313-cp313-win32.whl", hash = "sha256:ab4af6352741a604c431c6072fce5bee33bf0f20dc7a56618d6bf6bb89e9810c", size = 222584, upload-time = "2026-05-26T20:39:51.68Z" }, + { url = "https://files.pythonhosted.org/packages/6f/92/fb416fc26d340dcba19518c418d6048e913186e17243982c5e435e41fa7a/coverage-7.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:7af486dabe8954d03b087f0021540897afe084f04e16ff5579e08cc46f871416", size = 223394, upload-time = "2026-05-26T20:39:53.472Z" }, + { url = "https://files.pythonhosted.org/packages/73/c6/02d56e3867972f77d5036de924643f26c056e848f00452cafb4dbc3c29b4/coverage-7.14.1-cp313-cp313-win_arm64.whl", hash = "sha256:2224f89ffd0c5605ccce1ed7a584da162bc7c55f601ab1c946bc9de31a486b42", size = 222015, upload-time = "2026-05-26T20:39:55.374Z" }, + { url = "https://files.pythonhosted.org/packages/4d/9e/fcc77914050df73f7662fa1f00902774c79c075a8388ab334074574bf77e/coverage-7.14.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:de286598cc65d2b489411174b1faec2f5a7775fb3201fd925db2a76b4030f37d", size = 220733, upload-time = "2026-05-26T20:39:57.189Z" }, + { url = "https://files.pythonhosted.org/packages/f7/67/2963cbdaf5cbadec44efa3a1e39eaa1f02df4079585f05387607a221e126/coverage-7.14.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:042c46ded7c288aeb07cf14a28b6c1e10b78fcba40171c3fa1e939377eeef0b5", size = 221086, upload-time = "2026-05-26T20:39:59.019Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c5/8701645574e11881f2f47d8930f98bc48b5d43b25eb5b4430dfc4a2f9f48/coverage-7.14.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f4ddbe407477f04c45115d1a4e5bc480f753553b534d338d4c3358b1cdd0ea52", size = 262381, upload-time = "2026-05-26T20:40:00.822Z" }, + { url = "https://files.pythonhosted.org/packages/7c/28/7a64d73598263e0c5abd5084211a8474488d31b3c552ff531c719dfcff62/coverage-7.14.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d13e6725992e2d2fd7d81d4f5241952d13740121dfd501da09201be39b2c003a", size = 264458, upload-time = "2026-05-26T20:40:02.506Z" }, + { url = "https://files.pythonhosted.org/packages/fa/d8/4969179db9f7eb4df218e69540adf829d1c835f59452513d065d15446802/coverage-7.14.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f747dc8edcfe740130f28f32f3995e955494285717e86ee25af51db2219df08a", size = 266884, upload-time = "2026-05-26T20:40:04.421Z" }, + { url = "https://files.pythonhosted.org/packages/a6/78/a45d5794dbc9bafd97afc96a4377c86c7820d78b6cf51b89bc1d4e919275/coverage-7.14.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ced2f09ef276fd58611a1ef502164ad266d2b75174e5a40cabbdb4033f9f6cf2", size = 268022, upload-time = "2026-05-26T20:40:06.298Z" }, + { url = "https://files.pythonhosted.org/packages/21/cb/4f5e354e9e3e67af96bd4e57113e6db6b22298c7168b13eec408a549903d/coverage-7.14.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b84800013769a78ccb9ef4659402e26d06867e337b61ec365f77ad008adea80e", size = 261631, upload-time = "2026-05-26T20:40:08.226Z" }, + { url = "https://files.pythonhosted.org/packages/ec/49/eced49af4cb996d5d8b7e94e736175c513e4facd3398507b89892b4326d8/coverage-7.14.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ea8cd6ca0ee9f616aaef3afc6882e32c2cbf18b00d96313ffd76af650574034d", size = 264443, upload-time = "2026-05-26T20:40:10.137Z" }, + { url = "https://files.pythonhosted.org/packages/f1/d8/5603a88a7c5913a6b54f6cb1a8c46f7b39cbb30f27cd3f492908da09b2d7/coverage-7.14.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:aa5e304a873fabddc11e484e9b6b738bd38bd7bed17b09aa84eecf5332e8b8bb", size = 262069, upload-time = "2026-05-26T20:40:11.999Z" }, + { url = "https://files.pythonhosted.org/packages/f0/59/2ae3cb79da554a06c8619d6c88ea19dd1e4aed4b834b6a83bb1fa243bdc5/coverage-7.14.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:5a1c5215be81035e629d5bc756650634d0bf31991038db7a0eccb90f025ce16d", size = 265780, upload-time = "2026-05-26T20:40:13.858Z" }, + { url = "https://files.pythonhosted.org/packages/af/5f/b130c1dc999031f2648bd25317fbce505ad8d5562079b4ed81e736a84967/coverage-7.14.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:79058c47dae6788504b5effb319961bcd72d7240551464b91d474bc0ed186d69", size = 260970, upload-time = "2026-05-26T20:40:16.142Z" }, + { url = "https://files.pythonhosted.org/packages/87/d1/ec13ccddeb48ec963bdfa72a11224bac2584bd045ba13beca82f8113e9c7/coverage-7.14.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:370c5afae3fa0658e11694a32b24c2778f6bc2d17718121f94ee185e69f26b54", size = 263157, upload-time = "2026-05-26T20:40:18.382Z" }, + { url = "https://files.pythonhosted.org/packages/cf/c2/cd91ead503045161092d3845f7bb95ea2f25131ce96d3e314dd835d91b9c/coverage-7.14.1-cp313-cp313t-win32.whl", hash = "sha256:3758dd0a7f1fa57365ef2e781df0f0731d38b6e3772259d13dae4bd8a958d4b1", size = 223259, upload-time = "2026-05-26T20:40:20.381Z" }, + { url = "https://files.pythonhosted.org/packages/71/9f/1e28d97e6bd2c76b07f38b7c02870f1371255ff6717f54eca578fcbbdd0e/coverage-7.14.1-cp313-cp313t-win_amd64.whl", hash = "sha256:6ff665fb023a77386fe11685190cee1f60a7d635994a30d9b0a061533d470fce", size = 224320, upload-time = "2026-05-26T20:40:22.316Z" }, + { url = "https://files.pythonhosted.org/packages/a9/e0/d936e908f0e1efa55e52b91e01b52f1055cef5e1ab2718493390ed8e2fb8/coverage-7.14.1-cp313-cp313t-win_arm64.whl", hash = "sha256:17a5a241e5997621a956a7f402a7433ef4221e5152809b785bec79e2323799f1", size = 222577, upload-time = "2026-05-26T20:40:24.894Z" }, + { url = "https://files.pythonhosted.org/packages/d6/34/fc2f101b151af3799a101f0550b0454aa008afdc0add677394ec4aa8ea10/coverage-7.14.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d5ed429d0b8edaac649e889b4ffcedb6c80b06629a3f93050e3dddfb99235bee", size = 220091, upload-time = "2026-05-26T20:40:27.249Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a7/1ebae2ab5b961b5c79bb09fe7b3ac99edb190d8be4a8c510b2cf66f46468/coverage-7.14.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8011224a62280e50dab346960c03cf47aca1a1e09e608c0fb33fd6e0cc8e9500", size = 220421, upload-time = "2026-05-26T20:40:30.084Z" }, + { url = "https://files.pythonhosted.org/packages/5e/90/92aca9cf0acc95123c96cd1eb1f08917897a7f5dee01e15738922971ec31/coverage-7.14.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:12c42ec1e14f553c4f817e989365982e646e27211f10a0f717855b94a79c8906", size = 251466, upload-time = "2026-05-26T20:40:32.542Z" }, + { url = "https://files.pythonhosted.org/packages/26/2b/78048cbe3b999f6cbf9cc0d90abba6a88a3e0863a8c1c6cbc762f3f8802f/coverage-7.14.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:06144cd511cf2624873a035c5069cf297144f6e77a73ee3d7a55b605ec5efb42", size = 253973, upload-time = "2026-05-26T20:40:34.473Z" }, + { url = "https://files.pythonhosted.org/packages/8e/21/c2e33b29d1cfde484a19d437afc343c6cd30b08d78cbbf9f5aff14e57b2b/coverage-7.14.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a311d8e1da24be5c1ccf85cbfb06315dbaa1703d5a1eab3f6432c72b837917c8", size = 255318, upload-time = "2026-05-26T20:40:38.154Z" }, + { url = "https://files.pythonhosted.org/packages/8e/ee/aad2f108d63b769121005302f16bf66db8625c88ceaba466942e09a2607e/coverage-7.14.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c79cead5b5bc584d9c71451cb984d0e3a84e0c0937379c8efcbf27c8d661b851", size = 257633, upload-time = "2026-05-26T20:40:40.164Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f8/11a2c29b4fd76d9849f81d0bb812ec0017a9396df3217214e38934a8c837/coverage-7.14.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dcbf65f1f66a26cdd88c35cf68fb4729c5d1cd2e88added72420541dfb212034", size = 251488, upload-time = "2026-05-26T20:40:42.631Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b8/9a5820de4b8ac2b71d85e3b5fb49108d7469c665f0e2ad0dd7569023e305/coverage-7.14.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fd86572566fb40189a8260446158235159bc7a82dfbc87a3b39cf4fb57fcec1c", size = 253329, upload-time = "2026-05-26T20:40:45.208Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ff/f33e4823667e27548e8fd8df44217515303f9808d0ff29817db56f87d990/coverage-7.14.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:7771b601718fdde84832c3a434ca9bbf4ae9adbc49d84198b4110700c3c77c36", size = 251291, upload-time = "2026-05-26T20:40:47.502Z" }, + { url = "https://files.pythonhosted.org/packages/68/9b/489db0ebb209054766b90a9014a45f6d26eb724c02ec21311c3733b5a644/coverage-7.14.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:39b21e212c55af06fa375e3dbf90a8a8e38792f3a910c580066d23563830ddd5", size = 255564, upload-time = "2026-05-26T20:40:49.372Z" }, + { url = "https://files.pythonhosted.org/packages/27/b5/16bc2d4c2409b23c7737edb68c83bc89e345f378050549fe1d75ac7d34d5/coverage-7.14.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f2302660e32562a532b442480121aef8aa61a5bdb20b30bf0adab29f10a5a4b4", size = 251107, upload-time = "2026-05-26T20:40:51.677Z" }, + { url = "https://files.pythonhosted.org/packages/7d/0c/2629997469a00cd069d588a41c9dc887610f2775ae89d250c4791e65272a/coverage-7.14.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:03a6f93c1ec3b7f2e77b5dbcc5573a2c21f12529a5c6bbe0f16f72303cc2fa4d", size = 252764, upload-time = "2026-05-26T20:40:54.267Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ee/f78d63c8f079e0d7211c7e2401fa17e311514534ba61bae03e4b287ce4ab/coverage-7.14.1-cp314-cp314-win32.whl", hash = "sha256:8a3ce026d73290f42f08dafecbd82c193a74df280461fbf97300fec51fd133ee", size = 222837, upload-time = "2026-05-26T20:40:56.496Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b9/be539854f93a70dfbeec69117f33ec70dc42ff0b65b5b07ab8d40d04228e/coverage-7.14.1-cp314-cp314-win_amd64.whl", hash = "sha256:114c95ef29302423b87d159075805f4ab973254a2638a5d7d046c94887cc87d7", size = 223650, upload-time = "2026-05-26T20:40:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/fe/9e/24e2842fef40f35ac82ba3a7719c8023d011bf3bf652d0675316a9d088a1/coverage-7.14.1-cp314-cp314-win_arm64.whl", hash = "sha256:a07891c3f4805442b31b71e84ba3cf29ed1aa9a428284e06deeb4b23e5b46343", size = 222218, upload-time = "2026-05-26T20:41:00.321Z" }, + { url = "https://files.pythonhosted.org/packages/0a/1d/ac0a9df5fe31c1e8bdd658074905fc12844a05c1a7e3fdb8417e97c31e23/coverage-7.14.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1101a5ebb083aecb625ebb6209d4105b58f647b093cb2dc8122d7b33f743cfe1", size = 220822, upload-time = "2026-05-26T20:41:02.281Z" }, + { url = "https://files.pythonhosted.org/packages/32/cf/f964fd9aff20323f9f1a726c97135f8a76bcd87b92dad141a456a43f3c64/coverage-7.14.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:851b9e1e4e8a4608e77c79714b2e77c0970d2ed7202a05e92ae407817481887b", size = 221084, upload-time = "2026-05-26T20:41:04.593Z" }, + { url = "https://files.pythonhosted.org/packages/d8/5e/7e5ef2aba844de2b80d678619fcf0841b42e3f37f16411226f3fe4c1016f/coverage-7.14.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d5b89cdfb2ee051b71e8c3c70bd81a9eff81100f736a269136fe1a68efe00474", size = 262454, upload-time = "2026-05-26T20:41:06.641Z" }, + { url = "https://files.pythonhosted.org/packages/64/62/75809bded87015cc4935524218a2a8ed8dd1a8498bfed30a2f4f7a4b4d34/coverage-7.14.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0177614a0370f227888b4e436a7c55686d6a9f90eb1ade2b624ba685a1686e86", size = 264578, upload-time = "2026-05-26T20:41:08.556Z" }, + { url = "https://files.pythonhosted.org/packages/f3/42/d33392dc14633525012d2d504fa1a33b05538bf535f5c1d64675e5754b78/coverage-7.14.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d69af5dea2de76fc485a83032a630523f985198b7e25be901ec60181587b01e", size = 266981, upload-time = "2026-05-26T20:41:10.824Z" }, + { url = "https://files.pythonhosted.org/packages/2a/49/0157c4428c2aca7f1e09d5565930586fd5ae36f1655f08b0daa7cf1fcae1/coverage-7.14.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:35ab22d91de736e8966b980dc355cbcdd2c6dbbcfe275f9a2991bc8a91b3df65", size = 268112, upload-time = "2026-05-26T20:41:12.966Z" }, + { url = "https://files.pythonhosted.org/packages/96/26/86b9ce71f4092b1ed325ce1421698081df1286b833400b6836912834d6e0/coverage-7.14.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:357d4e32935c36588aaba057d734fa32428c360c9fc2e4442afbf1b646beee6e", size = 261558, upload-time = "2026-05-26T20:41:15Z" }, + { url = "https://files.pythonhosted.org/packages/20/4c/c311210c5472cf5401d8422b0d7812cdd520f24417673afabda6c323faca/coverage-7.14.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:51bd64741cc6fa065abd300ede1afe5a5291ece9c31da8b24884deda48bcc3f8", size = 264447, upload-time = "2026-05-26T20:41:17.369Z" }, + { url = "https://files.pythonhosted.org/packages/fb/71/59513f8710ed3e6b0ac0a050a5b7e977bb9c9e880354863b5d00d8809256/coverage-7.14.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:9132cd363a68a4c3daa7c8704a654b1e39d3360f6f5b8ddd470608a945236c07", size = 262048, upload-time = "2026-05-26T20:41:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/84/8d/bceed32dc494f5bbf50f775cd2e78ca814953942b5ea28d3c1c3ac316f14/coverage-7.14.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:07c6290b1697b862c0478eab545eec949a0d0e4d6d03497f446d706da3b4f2de", size = 265781, upload-time = "2026-05-26T20:41:21.559Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c5/9348fe40dbfd4991aaf78df2c6c3098bfb2cc834d1fd362a64b4efef855a/coverage-7.14.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5ea0c297e27133853b4d8a3eb799bff5a2dbd9f2f41537a240d337ac9b4df890", size = 260896, upload-time = "2026-05-26T20:41:23.428Z" }, + { url = "https://files.pythonhosted.org/packages/ca/92/1ea0f03929da7cf87206b1fa24f4c8e9c158be0455481af29ec0a1f3503f/coverage-7.14.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:01b7733daad0237daa01ef80fe2dfceffc911e6a17fa7b55d14aa8214eaaaecd", size = 263214, upload-time = "2026-05-26T20:41:25.419Z" }, + { url = "https://files.pythonhosted.org/packages/f6/a9/b2493c054c0e01a643266742ab45e15744e60743f9260cd930c7142b1124/coverage-7.14.1-cp314-cp314t-win32.whl", hash = "sha256:6adc5a36984624a70bf11d7184e20fa0a49aa7c47ffab43804106a1a695ea22e", size = 223624, upload-time = "2026-05-26T20:41:27.795Z" }, + { url = "https://files.pythonhosted.org/packages/fc/bd/3e1e6a57fccd2d7c83fcdf338e93ba98eb85c6e877dd34731ac585375490/coverage-7.14.1-cp314-cp314t-win_amd64.whl", hash = "sha256:ddf799247318f34dbcd2efa8c95a8d0642674e926bb1774cf9b63dfd2a389d1c", size = 224728, upload-time = "2026-05-26T20:41:30.098Z" }, + { url = "https://files.pythonhosted.org/packages/bb/d7/31066cf1d2f0c6c797fce911bcfa01dd35642dc6da992a950256097c5860/coverage-7.14.1-cp314-cp314t-win_arm64.whl", hash = "sha256:145986fe66647eb489f18d9a997567a3fd358584c4b5a808769113abc07466af", size = 222752, upload-time = "2026-05-26T20:41:32.123Z" }, + { url = "https://files.pythonhosted.org/packages/8a/3c/1a983b9a745d7f83d53f057bcc5bf79ba6a2bbc08266b3f0c7d6fe630c9b/coverage-7.14.1-py3-none-any.whl", hash = "sha256:a252f21c27e38347e60111a3266b03827422a7d5525951aceee313aa68bab1d2", size = 211815, upload-time = "2026-05-26T20:41:34.078Z" }, +] + +[[package]] +name = "dj-database-url" +version = "3.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/03/f6/00b625e9d371b980aa261011d0dc906a16444cb688f94215e0dc86996eb5/dj_database_url-3.1.2.tar.gz", hash = "sha256:63c20e4bbaa51690dfd4c8d189521f6bf6bc9da9fcdb23d95d2ee8ee87f9ec62", size = 11490, upload-time = "2026-02-19T15:30:23.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/a9/57c66006373381f1d3e5bd94216f1d371228a89f443d3030e010f73dd198/dj_database_url-3.1.2-py3-none-any.whl", hash = "sha256:544e015fee3efa5127a1eb1cca465f4ace578265b3671fe61d0ed7dbafb5ec8a", size = 8953, upload-time = "2026-02-19T15:30:39.37Z" }, +] + +[[package]] +name = "django" +version = "6.0.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asgiref" }, + { name = "sqlparse" }, + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/29/ac41e16097af67066d97a7d5775c5d8e7efc5d0284f6b0a159e07b9adb92/django-6.0.6.tar.gz", hash = "sha256:ad03916ba59523d781ae5c3f631960c23d69a9d9c43cecda52fc23b47e953713", size = 10905525, upload-time = "2026-06-03T13:02:46.503Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/50/23f9dc45483419a3cc2085b498b25adfbf10642b2941c73e6d2dfaffc9ab/django-6.0.6-py3-none-any.whl", hash = "sha256:25148b1194c47c2e685e5f5e9c5d59c78b075dfd282cb9618861ba6c1708f4d2", size = 8373354, upload-time = "2026-06-03T13:02:41.72Z" }, +] + +[[package]] +name = "django-cors-headers" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asgiref" }, + { name = "django" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/21/39/55822b15b7ec87410f34cd16ce04065ff390e50f9e29f31d6d116fc80456/django_cors_headers-4.9.0.tar.gz", hash = "sha256:fe5d7cb59fdc2c8c646ce84b727ac2bca8912a247e6e68e1fb507372178e59e8", size = 21458, upload-time = "2025-09-18T10:40:52.326Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/d8/19ed1e47badf477d17fb177c1c19b5a21da0fd2d9f093f23be3fb86c5fab/django_cors_headers-4.9.0-py3-none-any.whl", hash = "sha256:15c7f20727f90044dcee2216a9fd7303741a864865f0c3657e28b7056f61b449", size = 12809, upload-time = "2025-09-18T10:40:50.843Z" }, +] + +[[package]] +name = "djangorestframework" +version = "3.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ca/d7/c016e69fac19ff8afdc89db9d31d9ae43ae031e4d1993b20aca179b8301a/djangorestframework-3.17.1.tar.gz", hash = "sha256:a6def5f447fe78ff853bff1d47a3c59bf38f5434b031780b351b0c73a62db1a5", size = 905742, upload-time = "2026-03-24T16:58:33.705Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/e1/2c516bdc83652b1a60c6119366ac2c0607b479ed05cd6093f916ca8928f8/djangorestframework-3.17.1-py3-none-any.whl", hash = "sha256:c3c74dd3e83a5a3efc37b3c18d92bd6f86a6791c7b7d4dff62bb068500e76457", size = 898844, upload-time = "2026-03-24T16:58:31.845Z" }, +] + +[[package]] +name = "drf-shopping" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "dj-database-url" }, + { name = "django" }, + { name = "django-cors-headers" }, + { name = "djangorestframework" }, + { name = "drf-spectacular" }, + { name = "gunicorn" }, + { name = "psycopg", extra = ["binary"] }, + { name = "whitenoise" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-django" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "dj-database-url", specifier = "==3.1.2" }, + { name = "django", specifier = "==6.0.6" }, + { name = "django-cors-headers", specifier = "==4.9.0" }, + { name = "djangorestframework", specifier = "==3.17.1" }, + { name = "drf-spectacular", specifier = "==0.29.0" }, + { name = "gunicorn", specifier = "==26.0.0" }, + { name = "psycopg", extras = ["binary"], specifier = "==3.3.4" }, + { name = "whitenoise", specifier = "==6.12.0" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "pytest", specifier = "==9.0.3" }, + { name = "pytest-cov", specifier = "==7.1.0" }, + { name = "pytest-django", specifier = "==4.12.0" }, + { name = "ruff", specifier = "==0.15.15" }, +] + +[[package]] +name = "drf-spectacular" +version = "0.29.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, + { name = "djangorestframework" }, + { name = "inflection" }, + { name = "jsonschema" }, + { name = "pyyaml" }, + { name = "uritemplate" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/0e/a4f50d83e76cbe797eda88fc0083c8ca970cfa362b5586359ef06ec6f70a/drf_spectacular-0.29.0.tar.gz", hash = "sha256:0a069339ea390ce7f14a75e8b5af4a0860a46e833fd4af027411a3e94fc1a0cc", size = 241722, upload-time = "2025-11-02T03:40:26.348Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/d9/502c56fc3ca960075d00956283f1c44e8cafe433dada03f9ed2821f3073b/drf_spectacular-0.29.0-py3-none-any.whl", hash = "sha256:d1ee7c9535d89848affb4427347f7c4a22c5d22530b8842ef133d7b72e19b41a", size = 105433, upload-time = "2025-11-02T03:40:24.823Z" }, +] + +[[package]] +name = "gunicorn" +version = "26.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/b7/a4a3f632f823e432ce6bc65f62961b7980c898c77f075a2f7118cb3846fe/gunicorn-26.0.0.tar.gz", hash = "sha256:ca9346f85e3a4aeeb64d491045c16b9a35647abd37ea15efe53080eb8b090baf", size = 727286, upload-time = "2026-05-05T06:38:25.529Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/40/9c2384fc2be4ad25dd4a49decd5ad9ea5a3639814c11bd40ab77cb9f0a14/gunicorn-26.0.0-py3-none-any.whl", hash = "sha256:40233d26a5f0d1872916188c276e21641155111c2853f0c2cd55260aec0d24fc", size = 212009, upload-time = "2026-05-05T06:38:23.007Z" }, +] + +[[package]] +name = "inflection" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/7e/691d061b7329bc8d54edbf0ec22fbfb2afe61facb681f9aaa9bff7a27d04/inflection-0.5.1.tar.gz", hash = "sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417", size = 15091, upload-time = "2020-08-22T08:16:29.139Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/91/aa6bde563e0085a02a435aa99b49ef75b0a4b062635e606dab23ce18d720/inflection-0.5.1-py2.py3-none-any.whl", hash = "sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2", size = 9454, upload-time = "2020-08-22T08:16:27.816Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "psycopg" +version = "3.3.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/2f/cb91e5502ec9de1de6f1b76cfbf69531932725361168bb06963620c77e2e/psycopg-3.3.4.tar.gz", hash = "sha256:e21207764952cff81b6b8bdacad9a3939f2793367fdac2987b3aac36a651b5bc", size = 165799, upload-time = "2026-05-01T23:31:55.179Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/e0/7b3dee031daae7743609ce3c746565d4a3ed7c2c186479eb48e34e838c64/psycopg-3.3.4-py3-none-any.whl", hash = "sha256:b6bbc25ccf05c8fad3b061d9db2ef0909a555171b84b07f29458a447253d679a", size = 213001, upload-time = "2026-05-01T23:20:50.816Z" }, +] + +[package.optional-dependencies] +binary = [ + { name = "psycopg-binary", marker = "implementation_name != 'pypy'" }, +] + +[[package]] +name = "psycopg-binary" +version = "3.3.4" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/09/43/13e9c406fbbf354580476e248a16b64802a376873ebe6339e30bb655572d/psycopg_binary-3.3.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fbd1d4ed566895ad2d3bf4ddfd8bae90026930ddf29df3b9d91d32c8c47866a7", size = 4590377, upload-time = "2026-05-01T23:29:18.782Z" }, + { url = "https://files.pythonhosted.org/packages/22/be/2923cd7c3683e7afdecf4f10796a18de02f5c5ddc0969aa2ad0a8cdd3bbd/psycopg_binary-3.3.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:75a9067e236f9b9ae3535b66fe99bddb33d39c0de10112e49b9ab11eee53dc31", size = 4669023, upload-time = "2026-05-01T23:29:25.884Z" }, + { url = "https://files.pythonhosted.org/packages/96/a0/2c913d6fe13d6a8bd13597d36739bf47af063ad9399e402cfecab16f3c1e/psycopg_binary-3.3.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:b56b603ebcea8aa10b46228b8410ba7f13e7c2ee54389d4d9be0927fd8ce2a70", size = 5467423, upload-time = "2026-05-01T23:29:33.416Z" }, + { url = "https://files.pythonhosted.org/packages/e7/38/205d10bc1ad0df4a21c5c51659126bd3ea0ef98fcad1e852f78c249bb9c3/psycopg_binary-3.3.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c677c4ad433cb7150c8cd304a0769ae3bcfbe5ea0676eb53faa7b1443b16d0d3", size = 5151137, upload-time = "2026-05-01T23:29:42.013Z" }, + { url = "https://files.pythonhosted.org/packages/36/fc/f0381ddcd45eff3bb70dbca6823a996048d7f507b2ec3fc92c6fabc0fe87/psycopg_binary-3.3.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:26df2717e59c0473e4465a97dfb1b7afebaa479277870fd5784d1436470db47c", size = 6736671, upload-time = "2026-05-01T23:29:51.626Z" }, + { url = "https://files.pythonhosted.org/packages/95/40/fa545ae152c24327651e5624e4902121e808270be36c10b12e9939be09bc/psycopg_binary-3.3.4-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dc1f79fd16bb1f3f4421417a514607539f17804d95c7ed617265369d1981cae", size = 4979601, upload-time = "2026-05-01T23:29:56.961Z" }, + { url = "https://files.pythonhosted.org/packages/86/e4/2f8a47ee97f90cd2b933d0463081d35631ff419de2b8c984a5f369857de0/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:136f199a407b5348b9b857c504aff60c77622a28482e7195839ce1b51238c4cc", size = 4510513, upload-time = "2026-05-01T23:30:07.243Z" }, + { url = "https://files.pythonhosted.org/packages/0e/0e/94e842ff4a7f98ed162580ca2e8b8864b28c1e0350f2443f8ee47f821167/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b6f5a29e9c775b9f12a1a717aa7a2c80f9e1db6f27ba44a5b59c80ac61d2ffcf", size = 4187243, upload-time = "2026-05-01T23:30:15.352Z" }, + { url = "https://files.pythonhosted.org/packages/d0/83/fc6c174b672e29b7de996ea77b6cbddf46c891751c3355f6974292baa6b4/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ee17a2cf4943cde261adfad1bbc5bf38d6b3776d7afff74c7cabcbeaeb08c260", size = 3927347, upload-time = "2026-05-01T23:30:21.186Z" }, + { url = "https://files.pythonhosted.org/packages/e9/65/768364d4a97a15b1a7f47ba52688c1686f22941d8332a8398cefc468e25f/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5c4ab71be17bdca30cb34c34c4e1496e2f5d6f20c199c12bad226070b22ef9bf", size = 4236393, upload-time = "2026-05-01T23:30:26.211Z" }, + { url = "https://files.pythonhosted.org/packages/bd/3b/218efbc9e645becd80cdf651acda05f85cfe546b7a9c0458c7cbc8fe1f74/psycopg_binary-3.3.4-cp313-cp313-win_amd64.whl", hash = "sha256:dbfdb9b6cc79f31104a7b162a2b921b765fcc62af6c00540a167a8de47e4ed38", size = 3564592, upload-time = "2026-05-01T23:30:31.764Z" }, + { url = "https://files.pythonhosted.org/packages/48/a6/828c9185701dab71b234c2a76c38a08b098ebfec5020716b4e93807492b5/psycopg_binary-3.3.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:28b7398fdd19db3232c884fb24550bdfe951221f510e195e233299e4c9b78f97", size = 4607292, upload-time = "2026-05-01T23:30:38.962Z" }, + { url = "https://files.pythonhosted.org/packages/92/58/5b40dbc9d839045c9dae956960e4fb6d20bcabe6c59a2aa34fc3a371913f/psycopg_binary-3.3.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1fbaa292a3c8bb61b45df1ad3da1908ccee7cb889db9425e3557d9e34e2a4829", size = 4687023, upload-time = "2026-05-01T23:30:47.227Z" }, + { url = "https://files.pythonhosted.org/packages/85/a9/793f0ac107a9003b48441d0d1f9f616d96e0f37458dd8dc12528ceff55fb/psycopg_binary-3.3.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:94596f9e7633ee3f6440711d43bb70aa31cc0a46a900ab8b4201a366ace5c9e7", size = 5486985, upload-time = "2026-05-01T23:30:55.517Z" }, + { url = "https://files.pythonhosted.org/packages/8f/26/42e8533497e2592334f68ec529cf5f840f7fa4e99575a4bb61aa184dbfbf/psycopg_binary-3.3.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8c0056529e68dbe9184cd4019a1f3d8f3a4ead2f6fc7a5afcf27d3314edd1277", size = 5168745, upload-time = "2026-05-01T23:31:01.904Z" }, + { url = "https://files.pythonhosted.org/packages/15/af/b7151776cc08d5935d45c833ec818a9beb417cf7c08239af1aafbdae78ee/psycopg_binary-3.3.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c09aad7051326e7603c14e50636db9c01f78272dc54b3accff03d46370461e6", size = 6761486, upload-time = "2026-05-01T23:31:14.511Z" }, + { url = "https://files.pythonhosted.org/packages/d0/ed/c92533b9124712d592cbf1cd6c76da933a2e0acea81dfe1fbe7e735f0cff/psycopg_binary-3.3.4-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:514404ed543efd620c85602b747df2a23cf1241b4067199e1a66f2d2757aaa41", size = 4997427, upload-time = "2026-05-01T23:31:20.901Z" }, + { url = "https://files.pythonhosted.org/packages/a2/23/ccadfd0de416aa188356daa199453af24087b042e296088706d190ae0295/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:46893c26858be12cc49ca4226ed6a60b4bfccadd946b3bebb783a60b38788228", size = 4533549, upload-time = "2026-05-01T23:31:26.204Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a0/c8f43cee36386f7bc891ab41a9d31ea07cf9826038e732da79f26b1e5f34/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:df1d567fc430f6df15c9fcf67d87685fc49bdb325adc0db5af1adfb2f44eb5c9", size = 4210256, upload-time = "2026-05-01T23:31:33.884Z" }, + { url = "https://files.pythonhosted.org/packages/4e/2c/c1547871be3790676e8868b38655496422f94f0978dfb66b74bdba2f1676/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:6b9016b1714da4dd5ecaaa75b82098aa5a0b87854ce9b092e21c27c4ae23e014", size = 3946204, upload-time = "2026-05-01T23:31:39.626Z" }, + { url = "https://files.pythonhosted.org/packages/c4/b1/f6670f00fa7ea601584623f6c11602ab92117d83eaff885e0210f6de7418/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:47c656a8a7ba6eb0cff1801a4caaa9c8bdc12d03080e273aff1c8ac39971a77e", size = 4255811, upload-time = "2026-05-01T23:31:44.986Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e6/5fff07a70d1f945ed90ae131c3bd76cab32beff7c58c6db15ad5820b6d1f/psycopg_binary-3.3.4-cp314-cp314-win_amd64.whl", hash = "sha256:c37e024c07308cd06cf3ec51bfd0e7f6157585a4d84d1bce4a7f5f7913719bf8", size = 3666849, upload-time = "2026-05-01T23:31:51.165Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, +] + +[[package]] +name = "pytest-django" +version = "4.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/13/2b/db9a193df89e5660137f5428063bcc2ced7ad790003b26974adf5c5ceb3b/pytest_django-4.12.0.tar.gz", hash = "sha256:df94ec819a83c8979c8f6de13d9cdfbe76e8c21d39473cfe2b40c9fc9be3c758", size = 91156, upload-time = "2026-02-14T18:40:49.235Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/a5/41d091f697c09609e7ef1d5d61925494e0454ebf51de7de05f0f0a728f1d/pytest_django-4.12.0-py3-none-any.whl", hash = "sha256:3ff300c49f8350ba2953b90297d23bf5f589db69545f56f1ec5f8cff5da83e85", size = 26123, upload-time = "2026-02-14T18:40:47.381Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "rpds-py" +version = "2026.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/43/25a8dcd3feedd735039a8f0b5b7e3b118232b5eae288c4fd9ab200d41094/rpds_py-2026.5.1.tar.gz", hash = "sha256:07b24fea40541e28570e5b795a4a38fbdcd12550c06bd0748005ecc8116ca256", size = 64459, upload-time = "2026-05-28T12:02:13.232Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/32/14c961ad295f490eb0849ada8b79683e93a59b9de3afdd983eaf55fa6867/rpds_py-2026.5.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:efef4ac29c6ff495531eb17ee705b62841ecaa291b7c7077e848ea03e237164d", size = 352787, upload-time = "2026-05-28T11:59:33.655Z" }, + { url = "https://files.pythonhosted.org/packages/ca/bb/d1b85117967c11191441a7274ae616c65d93901d082c588f89a50a8da5ae/rpds_py-2026.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c39f5b67a8a2e67179ada2a954227d670fe65fa9098457f698f56ddf248709b3", size = 345179, upload-time = "2026-05-28T11:59:35Z" }, + { url = "https://files.pythonhosted.org/packages/7c/46/d84105f062e626a1b233f863907288a4708c2d833b8b4c6fb2764bc080c0/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5c30f3f04eef4fbd362226a6f31d7c8895ca4fbb6e0b790f6890a98d8da8559", size = 376173, upload-time = "2026-05-28T11:59:36.43Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ae/469d7959ce5b1201e1de135dc735b86db3b35dd0d1734f6a44246d5f061c/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:277f6c82f0580848796c7ecc8a7173aa3bfb928e4ff831261c2f60a81dc270db", size = 383162, upload-time = "2026-05-28T11:59:37.995Z" }, + { url = "https://files.pythonhosted.org/packages/dc/a2/57853d31a1116a561aa072794602ad3f6341e18d70a8523f1bd5b9fc1e5a/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:63c2c4c213f1a4e3f3de28ecab029dbdee976324e729c0d7a55211be72576b02", size = 495093, upload-time = "2026-05-28T11:59:39.453Z" }, + { url = "https://files.pythonhosted.org/packages/99/63/3a8eabcad9314b7daf5c65f451d2c33d989235cd8a5762186cf2c3f5a4f8/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3350ec808fb538fe71a1f94dfaa0e29c598dfad805ce49f0caec5ae3183c652b", size = 389829, upload-time = "2026-05-28T11:59:40.896Z" }, + { url = "https://files.pythonhosted.org/packages/4b/25/05678d97fc25e2622df14dc530fb82023174ecfff6733991ed0d78f167bd/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1b964e3ab599e718dc46c018d104b1ebc007cbc6567d827c94a687fca56d77e", size = 374786, upload-time = "2026-05-28T11:59:42.626Z" }, + { url = "https://files.pythonhosted.org/packages/88/d1/8c90b6431e80a3b91b284a5c7c8c0c4f9c006444d90477a740d6e0f9c694/rpds_py-2026.5.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:19cb09fab7b7fc96b2a6e28f2e34b72a3705ff27b37edb77455316e5d3f3dc9b", size = 386920, upload-time = "2026-05-28T11:59:44.124Z" }, + { url = "https://files.pythonhosted.org/packages/ff/99/4638f672ab356682d633ee0da9255f5b67ce6efd0b85eb94ad3e255e65a5/rpds_py-2026.5.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:abe76bcdba31e576cb83eeb8797aa0d882b738fef6dc65d0601fc753806a5b46", size = 405059, upload-time = "2026-05-28T11:59:47.177Z" }, + { url = "https://files.pythonhosted.org/packages/66/3f/3546524b6eb4cc2e1f363a3d638fa52f6c24faae3500c25fb488b02f1740/rpds_py-2026.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8bff7073db3899158fff55ebf57b113a67030af26f80a18978f9f0aa60250ddf", size = 553030, upload-time = "2026-05-28T11:59:48.603Z" }, + { url = "https://files.pythonhosted.org/packages/c6/c3/7b3388c796fcf471bd17194242d4dc1a7608567c0fa422bcc1c5e79f9c1e/rpds_py-2026.5.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8ba264fa49be666cd9cc56bf34ec7002fb3d27a4aee5bcb4d43d0d18feb1bb6f", size = 618975, upload-time = "2026-05-28T11:59:50.314Z" }, + { url = "https://files.pythonhosted.org/packages/61/1e/a3cb07f2795075d1d88efddae2f541359fde5f08c81ee114c29c2949c90a/rpds_py-2026.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4860b603ddda0475a8885499b3729e90229d480105b42651962a5397d995fa89", size = 581178, upload-time = "2026-05-28T11:59:51.673Z" }, + { url = "https://files.pythonhosted.org/packages/a1/74/e758c03a5ef46f04c37f2651a2893db846d569ba8a7bca469d4b58939bcd/rpds_py-2026.5.1-cp313-cp313-win32.whl", hash = "sha256:7944270ae71383f6e2657dd7d5ce4eeb4ac2d0059a6738f0510583d462ab4842", size = 212481, upload-time = "2026-05-28T11:59:53.148Z" }, + { url = "https://files.pythonhosted.org/packages/70/ec/a2aca432db9c7359b40fa393eeeaa0d166c2f70175be956e75fa24197c44/rpds_py-2026.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:88647f43a73c4e01be19b04ceef0c8d3a1958153604d13c773becd8016f2a0cf", size = 228519, upload-time = "2026-05-28T11:59:54.505Z" }, + { url = "https://files.pythonhosted.org/packages/29/60/a73bfdd45b096574556acf303bbd9fa9eed36ca8a818b514e2a5d5fe2b9d/rpds_py-2026.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:453895624ecf7db7063b1004e44037522bbaef9ff6a945e59bc71662d7a03abd", size = 223446, upload-time = "2026-05-28T11:59:56.081Z" }, + { url = "https://files.pythonhosted.org/packages/18/e2/408105fd611823f00882aea810f3989a30d26b1bab8b6beb20f98c724e0e/rpds_py-2026.5.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:b4e4bc98639ec915f512fde3aa7a95e0041d95d9c3cc86eea841fa63cb1e8600", size = 355287, upload-time = "2026-05-28T11:59:57.448Z" }, + { url = "https://files.pythonhosted.org/packages/8d/58/5c4a43436843c90d0f6d19f82c200c80e3843ca9fa07b237623327f6d384/rpds_py-2026.5.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cacedb7a6e167680acba45ad5716e89067d225dc80da0d7040cae8c81d4572fa", size = 347033, upload-time = "2026-05-28T11:59:58.881Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c2/1a71acdacaf4e259b10278fb87b039ded3cf80041bcd89dd8a3ea702ded6/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68700371c5d7ae1412862ddfa719090925c93ecf351c566d66f09d04b136ea00", size = 376891, upload-time = "2026-05-28T12:00:00.516Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c8/535f3d9b65addd8e28aa87b83c6e526799c3717a88273db8ea795beeef7a/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:296c799becfa849c779c8725494fe9ed94959ed886787df4364b058465bad7f0", size = 385646, upload-time = "2026-05-28T12:00:02.394Z" }, + { url = "https://files.pythonhosted.org/packages/1c/91/dc033f313345c354ade914dbe73cdb90b615a4409ea02430d5356794f3d8/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d3858b908218ee108d0bbfb2095ccc237648053c9bf98affad7cb079acaf1d97", size = 498830, upload-time = "2026-05-28T12:00:04.189Z" }, + { url = "https://files.pythonhosted.org/packages/27/fc/90fcbea459dbb8ddc18a2e0fd1de9412b48bc84ffff2db771cf714bacfd6/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4fb8d2e7cb2f850b169806d61d1b991738acec96500a75c30f49caf064ce7cef", size = 392830, upload-time = "2026-05-28T12:00:05.797Z" }, + { url = "https://files.pythonhosted.org/packages/b2/1d/46cd11a228c9750684a798d98f878be6f614aa762438da7378f035e79e35/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27b74c10ed6a8f190f4287f53bcfea348b92a84a9c9f70d30183d1e6172d580d", size = 379613, upload-time = "2026-05-28T12:00:07.433Z" }, + { url = "https://files.pythonhosted.org/packages/24/4a/d9b0c6af3a1de03eb93741bbe8be2bdce84d8fda8224f3005451d86df389/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:b9a6528956191c48c52294a592dbd4a8386d7048bdb25c0efcb6b966466c6d83", size = 388183, upload-time = "2026-05-28T12:00:09.227Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b4/db7aaabdda6d020afc87d981bcc2f57a434c7dec60ecfc2ab3dd50b20351/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:af03e34e860047bc7a352b842856fcf78798fbb81132cc98bd2f907ab4eb9cd2", size = 408578, upload-time = "2026-05-28T12:00:10.779Z" }, + { url = "https://files.pythonhosted.org/packages/08/d6/070f6a41cbb343e2ac4171859bf3f3623e0ab002f72619d6d505313ec2de/rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fea6e836d10abbe191d557d33bd58bd5987725fe63aa1eefe557d230209855bd", size = 553573, upload-time = "2026-05-28T12:00:12.443Z" }, + { url = "https://files.pythonhosted.org/packages/75/ab/1a71ea3589c4345dac0a0518f0e6a031cb42689277851b683c46d27463a5/rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:fc0c0f878ea770a0a8a462456c5ad36fc9fe6358e6b76fdadc7f17575e0b8bf1", size = 620861, upload-time = "2026-05-28T12:00:14.09Z" }, + { url = "https://files.pythonhosted.org/packages/8a/22/9bf80a56069c0c443fcfefac639a86a744550a2898817a6dfd3e26654924/rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e0b360f316d966b048b085857630b3cc51f3db2f07b06f440eac8f695374d1e3", size = 585633, upload-time = "2026-05-28T12:00:15.66Z" }, + { url = "https://files.pythonhosted.org/packages/da/68/3b2c0a75c9e04125696f84ebdbbf304acf5a40b58ba4481cdb98a922c3ba/rpds_py-2026.5.1-cp313-cp313t-win32.whl", hash = "sha256:a2999883eedf72fdfb7520b92c7d4ec2572a71ff40239377aa604cc529eecafc", size = 210074, upload-time = "2026-05-28T12:00:17.291Z" }, + { url = "https://files.pythonhosted.org/packages/e7/8b/609157d5a25d37d4f29f92840ba531f416907c34ae5c5739dd21fc2bef98/rpds_py-2026.5.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e07be2a9d7122bd6e82dea89814ef8dc893feb1aae97fec1630f3263bbb30e55", size = 228635, upload-time = "2026-05-28T12:00:18.73Z" }, + { url = "https://files.pythonhosted.org/packages/d4/6f/19c1918a4b590d8de87e712e4abe4b3875771eff60216fb6153cf6665c68/rpds_py-2026.5.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:1f2c391c3059798093b65df23aca2cac150460ae9c630d99dec83d703d9485b9", size = 349756, upload-time = "2026-05-28T12:00:20.217Z" }, + { url = "https://files.pythonhosted.org/packages/e5/60/a06fe7da34eca79dacbf958a2ba0c6eea85bc2b29de20080bf40f72f66fa/rpds_py-2026.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:413b424f7c4ee65ab5e5be91f5731be0f8b41a1ee2b12dfe810d716312e95a78", size = 343831, upload-time = "2026-05-28T12:00:21.711Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ec/b2333b97b90e2a6ef6ca8ad386ee284968e74bcfe113b3f1a8d9036429a9/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c595a1d9255dce0599e13130d1440ab2506654f2b50294226ee06402f8fef63", size = 375127, upload-time = "2026-05-28T12:00:23.326Z" }, + { url = "https://files.pythonhosted.org/packages/14/7f/e00aae54067f2b488c4637961d5f58204d470795fc791085fa3f15060d2e/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1c27c5f6102eac8c03e7595a00827a53b271ba40a53b59ff8709170e0855ea4a", size = 379034, upload-time = "2026-05-28T12:00:24.89Z" }, + { url = "https://files.pythonhosted.org/packages/be/cc/423999bbb8ae8dc93c77fc1d5e984ade5eb89d237d3bb884ccfa72ae2890/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c7fcf61d44cacecaf3aea542b0e053db77972a4573e7ceda16fb2b399161195", size = 490823, upload-time = "2026-05-28T12:00:26.676Z" }, + { url = "https://files.pythonhosted.org/packages/0f/aa/c671bf660f12e68d3c52ff86c7066ed1372df5a0f4f2ff584e419b8207e7/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c817a189d4ee14290420e5ff051e4dd6baa13f3edf84685071dee07a6d538ee", size = 388144, upload-time = "2026-05-28T12:00:28.577Z" }, + { url = "https://files.pythonhosted.org/packages/19/c8/d63bb75b68afe77b229e3021c6031bcaf01da5db5b0e69d0d10f9ba679a7/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21846aac0ed2e0589f38c12dc44e77bb64e494b771eadbcf169cba00566ba7ba", size = 371959, upload-time = "2026-05-28T12:00:30.304Z" }, + { url = "https://files.pythonhosted.org/packages/82/35/c51122014d8274ff37dc606d60049c3db7d83da02b5b282511e5a906a9a6/rpds_py-2026.5.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b317c87a13f769a4e787819bd508aaa5d69aa09b0880de9af6d3a8a54571cdec", size = 383558, upload-time = "2026-05-28T12:00:31.764Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f9/2790cb99c136a5363acdeacf5c27c56f3de0d4118a1f48fca83404c99c89/rpds_py-2026.5.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ce87129d9f2c14fa6c4a8601fb80eb4488c80d38a20cd13758ef11123e14995d", size = 402789, upload-time = "2026-05-28T12:00:33.247Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1b/e4fb584f8c75d35c38150ff6a332cda949e6f97acba1f4fd123b14ab56fe/rpds_py-2026.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9cdddb6c1207d284d94fd1530adf57fbd797fe7c4b8704ba85f49414f2557e7d", size = 551405, upload-time = "2026-05-28T12:00:34.819Z" }, + { url = "https://files.pythonhosted.org/packages/d8/f7/a6731b4216cb3793ea1af5391da240f5683dacc0d13e034fe5fc3503f240/rpds_py-2026.5.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:4e237e139f94d3c036fd28eb9f564c99055476ff4ff05cd42be55ce349b5aa02", size = 616975, upload-time = "2026-05-28T12:00:36.268Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/2e051a81d95d8e63f4b35a1c463a87e8766bc3d083c067c5dfb6bf220747/rpds_py-2026.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ed0954b524873214369184a9c82b0eaa45a3fbb9a798cd95b17e0d98499e7ea0", size = 578701, upload-time = "2026-05-28T12:00:37.82Z" }, + { url = "https://files.pythonhosted.org/packages/65/56/b5f6fdb2083e32bca8a8993d89e70db114b4756c9e2c38421328126689d2/rpds_py-2026.5.1-cp314-cp314-win32.whl", hash = "sha256:2d88621d6a7d4dfa633d21abe90f280bb205274e16b1d1e61c6ad4640b2453b7", size = 209806, upload-time = "2026-05-28T12:00:39.492Z" }, + { url = "https://files.pythonhosted.org/packages/fb/80/65a5aa96c155e611d1ed844e4e1f57f3e36b021f396d9f8585d756e6b90d/rpds_py-2026.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:cef8ac28d26f4dda3533060c20fbf80a325458fa9fd23ea72a73cdfa8e978838", size = 225985, upload-time = "2026-05-28T12:00:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/27/7c/ad185212e87b05f196daef92bc5f3caf07298eb47c295b5585c3dd3093ac/rpds_py-2026.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:eaaea962c68cdc68d4a533ba985ab8e9484277910bbfaa2ab3ef7732667bfed8", size = 221219, upload-time = "2026-05-28T12:00:43.15Z" }, + { url = "https://files.pythonhosted.org/packages/23/58/e14ae18759020334646b031e708ab4158d653a938822bfb7b95ef2e93aa3/rpds_py-2026.5.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:21942f52dbbd5f8758bf021213d28bd45c39e873e65e2407faf5f1846f5761ad", size = 352148, upload-time = "2026-05-28T12:00:44.638Z" }, + { url = "https://files.pythonhosted.org/packages/31/9b/5f4a1e2f960bca3ac5d052b139dd31eed97b259f9d909173821760d542e8/rpds_py-2026.5.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f414556f6e3958300ff941e40c9f97e3dc9774ddd1b3434c475d73dd354bbed3", size = 345196, upload-time = "2026-05-28T12:00:46.14Z" }, + { url = "https://files.pythonhosted.org/packages/1a/71/1d9574d6a2fa20ab60eaa55c7467f5aa20cbc770f341a05f09c0876f59e2/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef1013a8625c74043210190b246f5b1551e09757c1f356c6e4160ef96c5bc081", size = 374981, upload-time = "2026-05-28T12:00:47.531Z" }, + { url = "https://files.pythonhosted.org/packages/0c/9a/37e99f4915a80aa71670263c1267f7ae0af95f53a3f61e6c3bdc016d4515/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cc68e231a77a5f0d774ae278a1f8e55c0456501820847c1e4efb3829f3441df6", size = 379961, upload-time = "2026-05-28T12:00:49.216Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ff/6e73f74b89d2e0715e0fc86b7dde893f9a61ae2f9b256ff3bdfe41ac4e94/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9baffb505aff33acc69b422a19f77806680f3c8632227d79f48de8a810d1c2c5", size = 495965, upload-time = "2026-05-28T12:00:51.111Z" }, + { url = "https://files.pythonhosted.org/packages/ea/e0/425faba25f59d74d4638b267f7c7a80e8649d2ef4db10a19b0c4a71e6e6f/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8d2f912928d426e8cfa396f7f3f8d29a59e6689c86dcca3c420730c1096322b", size = 389526, upload-time = "2026-05-28T12:00:52.77Z" }, + { url = "https://files.pythonhosted.org/packages/c6/76/7a41960e3fddae47fab43a28684d5da981401dffd88253de0944148654cb/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90f628283be835db980c941767d41c9a27b5239e54ba0a9c1335247e82406964", size = 376190, upload-time = "2026-05-28T12:00:54.215Z" }, + { url = "https://files.pythonhosted.org/packages/27/60/5f38dc70824fc6951b51d35377e577a3a3a4c81a6769cc5a2de25ebe0ad1/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:1ebb2f0ab7e16132995a72de805170e0203df0c3dd22e1ef1cd1fdd90bd7a131", size = 383921, upload-time = "2026-05-28T12:00:55.673Z" }, + { url = "https://files.pythonhosted.org/packages/60/1a/d60a38caa1505f4b9483c3fbbde12c94e1079154f4f401a6da96f7e77621/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f3df3d16ded76f1f8c9cdebd0e1ea55fdf4c23b812de189814da7cf229c22a81", size = 404766, upload-time = "2026-05-28T12:00:57.518Z" }, + { url = "https://files.pythonhosted.org/packages/87/ff/602fd3f174d6425f0bce05ad0dfbec0e96b38d0f7d08a79af5aa20083885/rpds_py-2026.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9af8905b8f854990e40d5206aa5ac58d9b0fe0b7f351ff2bb086c20f6c8c6a47", size = 551343, upload-time = "2026-05-28T12:00:58.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c1/1be13327acdbead3eca1fde03b6a34dbb011f1e864e217f0d32cc1779a7f/rpds_py-2026.5.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:036a36a87fb1cd3b214d11c4b3c4f7d2ddad933625dca1c900b56a057c07740a", size = 618502, upload-time = "2026-05-28T12:01:00.656Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d7/afb49b49d7f2be8b7ba1a9f0977fa5168003437b93086726f066544e8351/rpds_py-2026.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:62ae3853454fe9ef283a03c96c2d835d39e84b14643a9d62c82ef0fb87d702ca", size = 581916, upload-time = "2026-05-28T12:01:02.22Z" }, + { url = "https://files.pythonhosted.org/packages/25/d1/dbef8c1f8a10f07beb62b5f054e20099fd9924b3ec001b8f0b6ac7813a85/rpds_py-2026.5.1-cp314-cp314t-win32.whl", hash = "sha256:6c3d771a46ec18b12af06ce36243a9a80b07a5d0515236332d90863ca8bb326a", size = 207855, upload-time = "2026-05-28T12:01:03.821Z" }, + { url = "https://files.pythonhosted.org/packages/2a/72/bfa4e61ab8e7dc1c8adf397e05e6cbdd4239357bd72b248d3de662f23915/rpds_py-2026.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c93c629be4636cf54337bd5f06c104d55e42ced54d681f6fe21ae510a65116f6", size = 225422, upload-time = "2026-05-28T12:01:05.194Z" }, + { url = "https://files.pythonhosted.org/packages/27/3a/7b5da92b640f67b6717ccafc83cdd06bfa7ff2395c3685c68922bb54d703/rpds_py-2026.5.1-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:3574b55c604b8f75dacb007136508bbc0db406e626301778096a133327e7f2fb", size = 349576, upload-time = "2026-05-28T12:01:06.722Z" }, + { url = "https://files.pythonhosted.org/packages/d7/8a/2aafd7ad355a1bd48ca76e2262b74b15e6432b5a1efe150efd4d779cd55d/rpds_py-2026.5.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:94068eb3ae6d43f5a786b7db96a406a34e6d5c24489feef32fd6e8946ea7b291", size = 343640, upload-time = "2026-05-28T12:01:08.441Z" }, + { url = "https://files.pythonhosted.org/packages/f7/7d/6c9523c1abbe840a1b7fba3c516d48e1d3487cc80fea4366c4071cf56784/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3a5b10e8ce894825f380a8f1b6444cf73c294dfea62afbb2d13e3a9e630cec1", size = 375322, upload-time = "2026-05-28T12:01:09.934Z" }, + { url = "https://files.pythonhosted.org/packages/5a/5d/0b7b03fb1dc509321f01de3149784ab773e34c8573022029af8076afcb9c/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fc09f82e63d4bcd58149572f857a431bae851dc747e313c3b5bdf7abb907fda8", size = 379066, upload-time = "2026-05-28T12:01:11.48Z" }, + { url = "https://files.pythonhosted.org/packages/d7/e2/8ef6012999ebf1cb1c22f876d9ce5e63d960fd4631d2af3202d3f480aa25/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e10464d17df3b582745c25cec695cb9558bca2cb6ddb631aee1787fc72c767b2", size = 494586, upload-time = "2026-05-28T12:01:13.051Z" }, + { url = "https://files.pythonhosted.org/packages/80/af/1eeb029bec67582c226b7809172207cd005073af4ebd906e65ff494f4983/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ba05adbf15d994c38ec0b7ab32e858e5110c21e9009a00a86545fd220f84e038", size = 388415, upload-time = "2026-05-28T12:01:14.631Z" }, + { url = "https://files.pythonhosted.org/packages/18/23/ffbe10711c4d766c1cab0557d6906c074f795814863c67b351355d29354a/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77c004fdc7b891967106f78ddfd7b076bfe6813c6139c6fff6aed3bcaa960b26", size = 372427, upload-time = "2026-05-28T12:01:16.153Z" }, + { url = "https://files.pythonhosted.org/packages/bd/3a/30ba4a6ad457e5b070c18d742a33fb77d8d922b565cc881f8a5313d63bfe/rpds_py-2026.5.1-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:83bcf894486c9d78dd290d3c0124ff6dd8875d3025e2090a8ec49fcc37c55fdd", size = 383615, upload-time = "2026-05-28T12:01:17.809Z" }, + { url = "https://files.pythonhosted.org/packages/d3/69/62e242b53ce39c0814bd24e1a6e6eba6c92be716277745f317f9540a2e7b/rpds_py-2026.5.1-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c3df104083952a0e0c6f10de33e440eabe98fb6317d23e1a58c68f6df08d01b9", size = 402786, upload-time = "2026-05-28T12:01:19.419Z" }, + { url = "https://files.pythonhosted.org/packages/38/c1/a770b9c186928a1ed0f7e6d7ae50e7f3950ed23e3f9e366dbc8e38cb55de/rpds_py-2026.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:980450826cf22e133c57e0835070bdd0dd3f73b9b708c3ce223def2cb9469e14", size = 551583, upload-time = "2026-05-28T12:01:21.013Z" }, + { url = "https://files.pythonhosted.org/packages/21/7c/68e8579b95375b70d2a963103c42e705856cdb98569258bd807f4423891c/rpds_py-2026.5.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:205dde846f24332ab0c1188699a043b8d165b79bb84529ce272c45048ff6be01", size = 616941, upload-time = "2026-05-28T12:01:22.548Z" }, + { url = "https://files.pythonhosted.org/packages/70/a1/a6135aed5730ff03ab957182259987ac11e55fb392a28dc6f0592048a280/rpds_py-2026.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:3966b82dd563176396df030f3dd52a6e54cb69b718e95e78bd555ed3d1e0185d", size = 578349, upload-time = "2026-05-28T12:01:24.118Z" }, + { url = "https://files.pythonhosted.org/packages/09/6e/f24201a76a84e6c49d0bdfdfcb735210e21701e9b21c5bfc0ba497dd62f6/rpds_py-2026.5.1-cp315-cp315-win32.whl", hash = "sha256:7818f8d0a415be74d2be3590b0a1c1f463a642f4d0217e7d10602dceef5b79aa", size = 209922, upload-time = "2026-05-28T12:01:25.522Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e4/966bc240bb0485fc265278f6de44d05834bf0b3618886e0b22e33d54c49a/rpds_py-2026.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:b3cc20c0d800af78fd0fac68086e28c1856cec51ea528bb81ea851aa40d39325", size = 226003, upload-time = "2026-05-28T12:01:27.062Z" }, + { url = "https://files.pythonhosted.org/packages/5c/5c/a15a59269cd5e74472734516c73795c15eccfc841b3d4b0228c3f53f19d0/rpds_py-2026.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:3609e9939a8a76cd904cf98a3f1f13b5dc7e150adeaee89e0ea09652ea213e16", size = 221245, upload-time = "2026-05-28T12:01:28.51Z" }, + { url = "https://files.pythonhosted.org/packages/e0/22/135ce03804e179a71ceb13be095deda4a279bc88f7a6b8fa161c5ad44e12/rpds_py-2026.5.1-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:5d333a7127d4b307601ac37792bee01bb95c867cbfacf21b6375b804d6bbd723", size = 352015, upload-time = "2026-05-28T12:01:30.214Z" }, + { url = "https://files.pythonhosted.org/packages/3b/5f/f1f6d2652eb9d848f6eb369d8db83a2da6249bb49ad2c2a48f45d54538d3/rpds_py-2026.5.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:b5f077b44a4f7808520f66dae234988d867deb9aed9be5da057ce9ba831b2a41", size = 345016, upload-time = "2026-05-28T12:01:31.656Z" }, + { url = "https://files.pythonhosted.org/packages/88/66/b74182775691ea2290c99e52ac8d5db844e56fbec90ce421f107658c8314/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55d8f9b7b78c9538fc9e04e82ec0e888ff0c3cffcfad152c77e57cd09351a98a", size = 374775, upload-time = "2026-05-28T12:01:33.136Z" }, + { url = "https://files.pythonhosted.org/packages/ff/8f/15e5a61d9f0a43902d36561d4f07cae6ae9f4716be825159fd72717f33af/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e3a8ae58895ac107ed934a6bf51e5846f95c53b9b940c2c6d310838fd5846358", size = 380270, upload-time = "2026-05-28T12:01:34.574Z" }, + { url = "https://files.pythonhosted.org/packages/02/c3/f859b12763a80540cdf2af0f15b19904cf756a71d7bdd3f82ff3e5b1bbf9/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0957cf3c2b8632ec7aaebffebea8005b353cc2a237b6e2ae3c2cac0820704cfb", size = 495285, upload-time = "2026-05-28T12:01:36.127Z" }, + { url = "https://files.pythonhosted.org/packages/1c/c7/ff27c2ac8411d30b03b1829fd88cae8dad1a4d0da48dd25e57c4038042e6/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c396c1304de421050b3681ea70f371874b54d41b0151e96109758144c231e30b", size = 389581, upload-time = "2026-05-28T12:01:37.635Z" }, + { url = "https://files.pythonhosted.org/packages/6e/67/fe92ee32a6cc05c77228a2f8b1762e7124f386ec20ff83d0757b762d58d0/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aad1bff7f666b9598e573815affd666aac6a13a585dde336f843e33350c7fadc", size = 376041, upload-time = "2026-05-28T12:01:39.307Z" }, + { url = "https://files.pythonhosted.org/packages/f8/91/b4d6685c27aba55bd82f25b278be8237038117d05f9659a6213ad3408130/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:656a042550878f12d45752452d47094b7cfe5ad1e9d7b87b5a22ad3ae5ff8015", size = 383946, upload-time = "2026-05-28T12:01:41.043Z" }, + { url = "https://files.pythonhosted.org/packages/bd/79/2c1d832a53c8e0f8e98fc970ec257b950fecd4f62be2ab7182b500a0cbc8/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:73c4bd4f70294737b5206a3e8e30ccadbf8a60301831c8ea23eec5dbeea1ecfa", size = 405526, upload-time = "2026-05-28T12:01:43.032Z" }, + { url = "https://files.pythonhosted.org/packages/78/c4/c98117b03c6a8581ab2c2dfccfe9a5ad82bd8128a3c28b46a6ad2d97c393/rpds_py-2026.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:43bca78665423cabae77146f2fe7ce55272b6c8d55d82cca83effd42c7e13972", size = 551165, upload-time = "2026-05-28T12:01:44.648Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c1/bc479ca069200af730881b1bd525e3114b2b391a351509fcb1b772f28086/rpds_py-2026.5.1-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:42d0f20e85e549c870749d0e247f0c10d318a45b7e9676d575d2dcb04a1b2e66", size = 618778, upload-time = "2026-05-28T12:01:46.337Z" }, + { url = "https://files.pythonhosted.org/packages/77/65/38ab2f90df44c2febfb63cc10ced40763d9b4bc94d173e734528663fe7f5/rpds_py-2026.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:b1be5c35683684d5331b93600c210e8367c254683d8a6df6bd21bd2da3a334fb", size = 581839, upload-time = "2026-05-28T12:01:48.109Z" }, + { url = "https://files.pythonhosted.org/packages/15/2d/ce1f605fe036aadd460e5822e578c6c7ec3a860936cca37d6e0f299daa77/rpds_py-2026.5.1-cp315-cp315t-win32.whl", hash = "sha256:75808f6c38ce7749bb68cc2770161aae5045e6c6f6781a9782e74b93304399df", size = 207866, upload-time = "2026-05-28T12:01:49.648Z" }, + { url = "https://files.pythonhosted.org/packages/79/cb/966040123eb102371559746908ef2c9471f4d43e17ec9a645a2258dab64b/rpds_py-2026.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:90bd6630002a1c7f09e7843dd79f0d24f3d2897cc25a753480917865d14f15b3", size = 225441, upload-time = "2026-05-28T12:01:51.408Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/84/6f/a76f7d96e5c962f5b69cee865e49c15c1116897c01990faa8a57edb62e7f/ruff-0.15.15.tar.gz", hash = "sha256:b8dff018130b46d8e5bf0f926ef6b60cf871d6d5ae45fc9334e09632daa741d6", size = 4706985, upload-time = "2026-05-28T14:16:57.784Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/9d/3a45c05b8ab04b4705989de70a79008e27c8003296a0feaee9edc18dd7e9/ruff-0.15.15-py3-none-linux_armv6l.whl", hash = "sha256:cf93e5388f412e1b108b1f8b34a6e036b70fe8aff89393befad96fe48670311b", size = 10710652, upload-time = "2026-05-28T14:16:06.701Z" }, + { url = "https://files.pythonhosted.org/packages/05/66/da974431624bf3b49f6ee1f9543c02d929ff1cba78b0d5a79c38cf21f744/ruff-0.15.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ac5a646d1f6a7dadd5d50842dae2c1f9862ac887ef5d1b1375e02def791fde6e", size = 11096615, upload-time = "2026-05-28T14:16:23.313Z" }, + { url = "https://files.pythonhosted.org/packages/8c/09/7443452e5d290230a712103f2fdceeef7184f3ec99a2bd01c8be78aaceb5/ruff-0.15.15-py3-none-macosx_11_0_arm64.whl", hash = "sha256:77d955a431430c66f72dd94e379ad38a16daea3d25094872ac4edf9e797be530", size = 10436683, upload-time = "2026-05-28T14:16:40.974Z" }, + { url = "https://files.pythonhosted.org/packages/53/01/d330c26a57fa4f3943a14424904027428315b700fe4d14a84bb123a649e5/ruff-0.15.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7614ee79c69788cf6cedd568069ade9cecc22a1ad20494efe8d0c9ebb4b622d4", size = 10769064, upload-time = "2026-05-28T14:16:28.905Z" }, + { url = "https://files.pythonhosted.org/packages/1d/85/cc8770f8bdff541b1da8392d1634141fe4a0e3f4ee596605959b7906c27f/ruff-0.15.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3cdb1679e06a1f6b47bc384714ae96f6e2fb65ca441eb78c43d2ca554176ce1f", size = 10511987, upload-time = "2026-05-28T14:16:43.732Z" }, + { url = "https://files.pythonhosted.org/packages/7c/29/8c190c1472b63013583ba391f3342036e02010544c1270455ed8e519bdf3/ruff-0.15.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2728b93d7b23a603ea2c0ac6eb73d760bd38ec9de35f35fb41e18f7a3fee7622", size = 11275100, upload-time = "2026-05-28T14:16:55.244Z" }, + { url = "https://files.pythonhosted.org/packages/9f/6b/7e145ce2cc8e63d6834eca03d83a0e18d121def5c69f91b4cf4011ed4879/ruff-0.15.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be582fcc0db438902c7792b08d6ddf6c9b9e21addaa10092c2c741cfb09e5a45", size = 12176903, upload-time = "2026-05-28T14:16:14.368Z" }, + { url = "https://files.pythonhosted.org/packages/80/a3/d5974637f68e451f7fadf015cf3101d1cd7d8ba5027cffe0b9e3826ebe6b/ruff-0.15.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7aa77465b8ecaf1a27bea098d696f7fed5e1eccbd10b321b682d6de586ae5627", size = 11404550, upload-time = "2026-05-28T14:16:20.138Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1c/e6e5e568f22be4fb05d6244234aba384c06b451252453b821e1a529263cf/ruff-0.15.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48decfa11d740de4889de623be1463308346312f2409a56e24aa280c86162dc4", size = 11382027, upload-time = "2026-05-28T14:16:46.615Z" }, + { url = "https://files.pythonhosted.org/packages/1d/01/170921b49fcd2e8858825593f91cf7146c3e40a5c3e6df763e4bb0484dde/ruff-0.15.15-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a5015088452ca0081387063649ec67f06d3d1d6b8b936a1f836b5e9657ecd48c", size = 11366041, upload-time = "2026-05-28T14:16:26.247Z" }, + { url = "https://files.pythonhosted.org/packages/87/54/a7bad711d7de93254e15e06a4c375b89a03d18de45d3e5dcc86a4472fb1a/ruff-0.15.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f5294aab6356c81600fcdea3a62bb1b924dfd5e91767c12318d3f68f86af57cd", size = 10741795, upload-time = "2026-05-28T14:16:17.11Z" }, + { url = "https://files.pythonhosted.org/packages/c9/31/38c075963668f8b41c6914ee0f6f318727fbe30ab9145cb29e6df464c5fa/ruff-0.15.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:db5bd4d802415cca656dc1616070b725952d6ae95eb5d4831e49fbd94a38f75f", size = 10511117, upload-time = "2026-05-28T14:16:31.767Z" }, + { url = "https://files.pythonhosted.org/packages/9d/96/6ff689e1f7e375d1d97075eca022f74c2bab59554a432fe4d2e6f091986a/ruff-0.15.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:587a6278ed42059191c1a466e490bd7930fb50bd2e255398bc29616c895a61cb", size = 10994867, upload-time = "2026-05-28T14:16:35.149Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c2/5dce0ab9f92a8d534fa62b9bf9caca3eddb8c1a81b616f5e195ada4f0d6e/ruff-0.15.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:df0c1c084f5f4be9812f61518a45c440d3c30d69ce4bf6c5270e66d38338f02a", size = 11482101, upload-time = "2026-05-28T14:16:49.598Z" }, + { url = "https://files.pythonhosted.org/packages/b1/c0/1003b60edd697c649faf61f1a34094b1abb38fb3d1181e3f895781250a08/ruff-0.15.15-py3-none-win32.whl", hash = "sha256:29428ea79694afbe756d45fd59b36f22b6b020dc0443cf7de0173046236964b9", size = 10716774, upload-time = "2026-05-28T14:16:52.337Z" }, + { url = "https://files.pythonhosted.org/packages/02/a8/1269eddd6945a06c23f055ef7848886e37cf9d6a8bebb386a3115f01470c/ruff-0.15.15-py3-none-win_amd64.whl", hash = "sha256:8df0323902e15e24bc4bf246da830573d3cf3352bd0b9a164eab335d111ff4a4", size = 11868463, upload-time = "2026-05-28T14:16:11.333Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b2/920464c907b191e37469d477a1aa8bc048b8f36c4c1610dfa4ab87b39e18/ruff-0.15.15-py3-none-win_arm64.whl", hash = "sha256:3c8ceca6792f38196b8f589bc92eccd03eef286602da92e5dc05cc42ef6441b7", size = 11138498, upload-time = "2026-05-28T14:16:38.425Z" }, +] + +[[package]] +name = "sqlparse" +version = "0.5.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/90/76/437d71068094df0726366574cf3432a4ed754217b436eb7429415cf2d480/sqlparse-0.5.5.tar.gz", hash = "sha256:e20d4a9b0b8585fdf63b10d30066c7c94c5d7a7ec47c889a2d83a3caa93ff28e", size = 120815, upload-time = "2025-12-19T07:17:45.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/4b/359f28a903c13438ef59ebeee215fb25da53066db67b305c125f1c6d2a25/sqlparse-0.5.5-py3-none-any.whl", hash = "sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba", size = 46138, upload-time = "2025-12-19T07:17:46.573Z" }, +] + +[[package]] +name = "tzdata" +version = "2026.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254, upload-time = "2026-04-24T15:22:08.651Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" }, +] + +[[package]] +name = "uritemplate" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/60/f174043244c5306c9988380d2cb10009f91563fc4b31293d27e17201af56/uritemplate-4.2.0.tar.gz", hash = "sha256:480c2ed180878955863323eea31b0ede668795de182617fef9c6ca09e6ec9d0e", size = 33267, upload-time = "2025-06-02T15:12:06.318Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/99/3ae339466c9183ea5b8ae87b34c0b897eda475d2aec2307cae60e5cd4f29/uritemplate-4.2.0-py3-none-any.whl", hash = "sha256:962201ba1c4edcab02e60f9a0d3821e82dfc5d2d6662a21abd533879bdb8a686", size = 11488, upload-time = "2025-06-02T15:12:03.405Z" }, +] + +[[package]] +name = "whitenoise" +version = "6.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/2a/55b3f3a4ec326cd077c1c3defeee656b9298372a69229134d930151acd01/whitenoise-6.12.0.tar.gz", hash = "sha256:f723ebb76a112e98816ff80fcea0a6c9b8ecde835f8ddda25df7a30a3c2db6ad", size = 26841, upload-time = "2026-02-27T00:05:42.028Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/eb/d5583a11486211f3ebd4b385545ae787f32363d453c19fffd81106c9c138/whitenoise-6.12.0-py3-none-any.whl", hash = "sha256:fc5e8c572e33ebf24795b47b6a7da8da3c00cff2349f5b04c02f28d0cc5a3cc2", size = 20302, upload-time = "2026-02-27T00:05:40.086Z" }, +] From eb01a81f15af7dfdc78ee1c312f09dd7439d1d0c Mon Sep 17 00:00:00 2001 From: Girl LovesToCode Date: Wed, 10 Jun 2026 09:00:42 +0200 Subject: [PATCH 2/2] debug default set to 1 --- core/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/settings.py b/core/settings.py index 21a7416..8ec0156 100644 --- a/core/settings.py +++ b/core/settings.py @@ -27,7 +27,7 @@ SECRET_KEY = os.environ.get("SECRET_KEY", default=get_random_secret_key()) # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = int(os.environ.get("DEBUG", default=0)) +DEBUG = int(os.environ.get("DEBUG", default=1)) ALLOWED_HOSTS = os.environ.get("RENDER_EXTERNAL_HOSTNAME", default="127.0.0.1 [::1]").split() CSRF_TRUSTED_ORIGINS = [f"https://{host}" for host in ALLOWED_HOSTS]