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.
+
+
+
+
+
+
+
+ {{ list.name }}
+
+
+
+
+
+
+
+
+
+
+
+
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
+
+ Log out
+
+
+
+
+
+ {{ errorMessage }}
+
+
+
+
+
+
+
+
+
+
+
+
+ You have no shopping lists yet.
+
+
+
+
+
+
+ {{ list.name }}
+
+
+
+
+
+
+
+
+
+
+
+
+
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..8ec0156 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())
-DEBUG = int(os.environ.get("DEBUG", default=0))
-ALLOWED_HOSTS = os.environ.get(
- "DJANGO_ALLOWED_HOSTS", default="127.0.0.1 localhost [::1]"
-).split(" ")
+
+# SECURITY WARNING: don't run with debug turned on in production!
+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]
# 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" },
+]