├── .env.example ├── .github ├── dependabot.yml └── workflows │ └── ci.yaml ├── .gitignore ├── .pre-commit-config.yaml ├── Dockerfile ├── Makefile ├── README.md ├── api ├── __init__.py ├── common │ ├── __init__.py │ └── routers.py ├── config │ ├── __app_template__ │ │ ├── __init__.py-tpl │ │ ├── admin.py-tpl │ │ ├── apps.py-tpl │ │ ├── migrations │ │ │ └── __init__.py-tpl │ │ ├── models.py-tpl │ │ ├── serializers.py-tpl │ │ ├── urls.py-tpl │ │ └── views.py-tpl │ ├── __init__.py │ ├── application.py │ ├── auth.py │ ├── axes.py │ ├── base.py │ ├── cache.py │ ├── celery.py │ ├── database.py │ ├── logging.py │ ├── rest.py │ ├── security.py │ ├── sentry.py │ ├── settings.py │ ├── silk.py │ ├── spectacular.py │ └── storage.py ├── user │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_createsuperuser.py │ │ └── __init__.py │ ├── models.py │ ├── permissions.py │ ├── serializers.py │ ├── urls.py │ └── views.py └── web │ ├── __init__.py │ ├── asgi.py │ ├── urls.py │ └── wsgi.py ├── docker-compose.yaml ├── logs └── .gitkeep ├── manage.py ├── pyproject.toml ├── run-local.sh ├── tasks ├── __init__.py └── app.py ├── tests ├── __init__.py ├── integration │ └── __init__.py ├── load │ └── __init__.py └── unit │ ├── __init__.py │ └── test_example.py └── uv.lock /.env.example: -------------------------------------------------------------------------------- 1 | ############ 2 | # Secrets 3 | # YOU MUST CHANGE THESE BEFORE GOING INTO PRODUCTION 4 | ############ 5 | POSTGRES_PASSWORD=your-super-secret-and-long-postgres-password 6 | RABBITMQ_DEFAULT_PASS=your-super-secret-and-long-rabbitmq-password 7 | DJANGO_SECRET_KEY=your-super-secret-and-long-django-secret-key 8 | DJANGO_ADMIN_PASSWORD=your-super-secret-and-long-django-admin-password 9 | 10 | # Port that will be exposed to the host machine 11 | API_PORT=8010 12 | RABBITMQ_DASHBOARD_PORT=15672 13 | 14 | WORKERS=4 15 | THREADS=16 16 | 17 | ############ 18 | # Application 19 | ############ 20 | HOST=localhost 21 | 22 | ENVIRONMENT=local 23 | 24 | PROJECT_NAME=django_template 25 | PROJECT_VERBOSE_NAME="Django Template" 26 | 27 | COMPOSE_PROJECT_NAME=${PROJECT_NAME}_${ENVIRONMENT} 28 | 29 | DJANGO_DEBUG=true 30 | LOG_LEVEL=DEBUG 31 | 32 | LANGUAGE_CODE=en-us 33 | TIME_ZONE=Europe/Berlin 34 | 35 | ALLOWED_HOSTS=${HOST},127.0.0.1,localhost 36 | 37 | # https://django-axes.readthedocs.io/en/latest/4_configuration.html 38 | AXES_ENABLED=true 39 | AXES_FAILURE_LIMIT=3 40 | 41 | CORS_ORIGIN_ALLOW_ALL=false 42 | CORS_ALLOW_CREDENTIALS=false 43 | CORS_ALLOWED_ORIGINS=http://${HOST},https://${HOST},http://localhost 44 | 45 | CSRF_TRUSTED_ORIGINS=http://${HOST},https://${HOST},http://localhost 46 | 47 | DJANGO_ADMIN_USERNAME=admin 48 | DJANGO_ADMIN_EMAIL=admin@admin.com 49 | 50 | ############ 51 | # RabbitMQ 52 | ############ 53 | RABBITMQ_DEFAULT_USER=rabbit 54 | RABBITMQ_HOST=rabbitmq 55 | RABBITMQ_PORT=5672 56 | RABBITMQ_URL=amqp://${RABBITMQ_DEFAULT_USER}:${RABBITMQ_DEFAULT_PASS}@${RABBITMQ_HOST}:${RABBITMQ_PORT}/ 57 | 58 | ############ 59 | # Redis 60 | ############ 61 | USE_REDIS_FOR_CACHE=true 62 | REDIS_HOST=redis 63 | REDIS_PORT=6379 64 | REDIS_URL=redis://${REDIS_HOST}:${REDIS_PORT}/0 65 | 66 | ############ 67 | # Celery 68 | # https://docs.celeryproject.org/en/stable/userguide/configuration.html 69 | ############ 70 | CELERY_BROKER_URL=${RABBITMQ_URL} 71 | CELERY_RESULT_BACKEND=${REDIS_URL} 72 | CELERY_TASK_ALWAYS_EAGER=true 73 | CELERY_TASK_EAGER_PROPAGATES=true 74 | CELERY_TASK_IGNORE_RESULT=true 75 | CELERY_TIMEZONE=${TIME_ZONE} 76 | CELERY_ENABLE_UTC=true 77 | 78 | ############ 79 | # Sentry 80 | # https://docs.sentry.io/platforms/python/integrations/django/ 81 | ############ 82 | USE_SENTRY=false 83 | SENTRY_DSN=change 84 | SENTRY_TRACES_SAMPLE_RATE=1.0 85 | SENTRY_PROFILE_SAMPLE_RATE=1.0 86 | 87 | ############ 88 | # Silk 89 | # https://github.com/jazzband/django-silk/ 90 | ############ 91 | USE_SILK=false 92 | 93 | ############ 94 | # AWS S3 95 | ############ 96 | USE_S3_FOR_MEDIA=false 97 | USE_S3_FOR_STATIC=false 98 | AWS_STORAGE_BUCKET_NAME=change 99 | AWS_S3_CUSTOM_DOMAIN=${AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com 100 | AWS_S3_ACCESS_KEY_ID=change 101 | AWS_S3_SECRET_ACCESS_KEY=change 102 | 103 | ############ 104 | # Database 105 | ############ 106 | POSTGRES_USER=postgres 107 | POSTGRES_DB=postgres 108 | POSTGRES_HOST=pgbouncer 109 | POSTGRES_PORT=5432 110 | DATABASE_URL=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB} 111 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "pip" 4 | directory: "/" # Location of package manifests 5 | schedule: 6 | interval: "daily" 7 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: Checks 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | permissions: 10 | contents: write 11 | 12 | jobs: 13 | check-code: 14 | runs-on: ${{ matrix.os }} 15 | 16 | strategy: 17 | matrix: 18 | python-version: [ '3.12' ] 19 | os: [ ubuntu-latest ] 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | 24 | - name: Set up uv 25 | # Install latest uv version using the installer 26 | run: curl -LsSf https://astral.sh/uv/install.sh | sh 27 | 28 | - name: Install Python ${{ matrix.python-version }} 29 | run: uv python install ${{ matrix.python-version }} 30 | 31 | - name: Install the project 32 | run: uv sync --all-extras --dev 33 | 34 | - name: Run linters 35 | run: make lint 36 | 37 | - name: Run tests 38 | run: make test 39 | 40 | dependabot-auto-merge: 41 | name: 'Dependabot auto merge' 42 | needs: [ check-code ] 43 | runs-on: ubuntu-latest 44 | if: ${{ github.actor == 'dependabot[bot]' && github.event_name == 'pull_request'}} # Detect that the PR author is dependabot 45 | steps: 46 | - name: Enable auto-merge for Dependabot PRs 47 | run: gh pr merge --auto --merge "$PR_URL" # Use GitHub CLI to merge automatically the PR 48 | env: 49 | PR_URL: ${{ github.event.pull_request.html_url }} 50 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | # pytype static type analyzer 135 | .pytype/ 136 | 137 | # Cython debug symbols 138 | cython_debug/ 139 | 140 | # PyCharm 141 | .idea 142 | 143 | tmp/ 144 | media/ 145 | staticfiles/ 146 | backups/ 147 | 148 | .DS_Store 149 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.5.0 4 | hooks: 5 | - id: check-yaml 6 | - id: end-of-file-fixer 7 | - id: trailing-whitespace 8 | 9 | - id: ruff 10 | name: ruff 11 | entry: ruff check --force-exclude migrations 12 | language: system 13 | require_serial: true 14 | types_or: [python, pyi] 15 | 16 | - id: mypy 17 | name: mypy 18 | entry: mypy 19 | language: system 20 | require_serial: true 21 | types_or: [ python, pyi ] 22 | 23 | - id: pytest 24 | name: pytest 25 | entry: pytest 26 | language: system 27 | pass_filenames: false 28 | always_run: true 29 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.12 2 | 3 | # Install uv 4 | COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv 5 | 6 | WORKDIR /application 7 | 8 | COPY . . 9 | 10 | RUN uv sync --frozen 11 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # include `.env` file if exists 2 | ifneq ("$(wildcard .env)","") 3 | include .env 4 | export 5 | endif 6 | 7 | run.server.local: 8 | sh ./run-local.sh 9 | 10 | run.server.prod: 11 | uv run gunicorn api.web.wsgi:application \ 12 | --bind 0.0.0.0:80 \ 13 | --workers ${WORKERS} \ 14 | --threads ${THREADS} \ 15 | --timeout 480 16 | 17 | run.celery.local: 18 | OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES uv run celery -A tasks.app worker --loglevel=DEBUG 19 | 20 | run.celery.prod: 21 | uv run celery -A tasks.app worker --loglevel=INFO 22 | 23 | makemigrations: 24 | uv run manage.py makemigrations 25 | 26 | migrate: 27 | uv run manage.py migrate 28 | 29 | collectstatic: 30 | uv run collectstatic --no-input 31 | 32 | createsuperuser: 33 | uv run createsuperuser --email "" --username admin 34 | 35 | # Tests, linters & formatters 36 | format: 37 | make -k ruff-format ruff-fix 38 | 39 | ruff-format: 40 | uv run ruff format . 41 | 42 | ruff-fix: 43 | uv run ruff check --fix-only . 44 | 45 | ruff-unsafe-fixes: 46 | uv run ruff check --fix-only --unsafe-fixes . 47 | 48 | lint: 49 | make -k ruff-check mypy 50 | 51 | ruff-check: 52 | uv run ruff check . 53 | 54 | test: 55 | uv run pytest 56 | 57 | mypy: 58 | uv run mypy . 59 | 60 | # Docker 61 | logs: 62 | docker compose logs -f --tail=100 63 | 64 | logs.errors: 65 | docker compose logs -f --tail=100 | grep -E 'ERROR|WARNING|EXCEPTION|CRITICAL|FATAL|TRACEBACK' 66 | 67 | db.dump.all: 68 | @mkdir -p backups 69 | $(eval BACKUP_PATH := backups/dump_$(shell date +%Y-%m-%d-%H-%M-%S).sql) 70 | @docker exec -e PGPASSWORD=${POSTGRES_PASSWORD} ${COMPOSE_PROJECT_NAME}-db-1 pg_dumpall -c -U ${POSTGRES_USER} > ${BACKUP_PATH} 71 | @echo "Database dumped to '${BACKUP_PATH}'" 72 | 73 | db.restore: 74 | @test -n "$(BACKUP_PATH)" || (echo '`BACKUP_PATH` is not set. Use `make db.restore BACKUP_PATH=`' && exit 1) 75 | 76 | @docker exec -i -e PGPASSWORD=${POSTGRES_PASSWORD} ${COMPOSE_PROJECT_NAME}-db-1 psql -U ${POSTGRES_USER} < ${BACKUP_PATH} 77 | @echo "Database restored from '${BACKUP_PATH}'" 78 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Django API Template 2 | 3 | --- 4 | 5 | ## Table of Contents 6 | - [Feature Highlights](#feature-highlights) 7 | - [Configuration Guide](#configuration-guide) 8 | - [Additional Notes](#additional-notes) 9 | - [Quick Start Guide](#quick-start-guide) 10 | - [Setting Up Locally](#setting-up-locally) 11 | - [Setting Up with Docker](#setting-up-with-docker) 12 | 13 | --- 14 | 15 | ## Feature Highlights 16 | 17 | This Django API Template is designed to be robust, scalable, and secure, with features that cater to modern application development needs. Here's an overview of the advanced features and how they benefit your project: 18 | 19 | - **[Docker & Docker Compose Integration](https://docs.docker.com/compose/)**: Easily set up and scale your application using Docker containers, ensuring consistent environments across development and production. 20 | 21 | - **[Celery](https://docs.celeryq.dev/en/stable/django/first-steps-with-django.html) with [RabbitMQ](https://rabbitmq.com/) and [Redis](https://redis.io/)**: Leverage Celery for asynchronous task processing, using RabbitMQ as a message broker and Redis as a backend for storing results. 22 | 23 | - **[Sentry for Error Tracking](https://sentry.io/)**: Integrate with Sentry for real-time error tracking and monitoring, helping you identify and fix issues rapidly. 24 | 25 | - **[Django Rest Framework (DRF)](https://www.django-rest-framework.org/)**: Use Django Rest Framework for building RESTful APIs, with support for authentication, serialization, and more. 26 | - **[DRF Spectacular for OpenAPI](https://drf-spectacular.readthedocs.io/)**: Use DRF Spectacular for OpenAPI documentation, with support for customizing the schema and UI. 27 | - **[DRF Simple JWT for Authentication](https://django-rest-framework-simplejwt.readthedocs.io/)**: Use DRF Simple JWT for JSON Web Token authentication, with support for customizing token claims and expiration. 28 | 29 | - **[Django CORS Headers](https://pypi.org/project/django-cors-headers/)**: Use Django CORS Headers for handling Cross-Origin Resource Sharing (CORS) headers, with support for customizing origins. 30 | 31 | - **[Django Silk for Profiling](https://pypi.org/project/django-silk/)**: Utilize Django Silk for profiling and monitoring Django applications, offering insights into performance and optimization. 32 | 33 | - **[Django Axes for Security](https://django-axes.readthedocs.io/)**: Use Django Axes for security, with support for blocking brute force attacks and monitoring login attempts. 34 | 35 | - **[AWS S3 Integration](https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html)**: Option to use Amazon S3 for static and media file storage, enhancing scalability and performance. 36 | 37 | - **Scalability Options**: Configure workers and threads to optimize performance under different load conditions. 38 | 39 | - **Up-to-Date Dependencies**: All dependencies are up-to-date as of the latest release. Thanks to [Dependabot](https://dependabot.com/). 40 | 41 | --- 42 | 43 | ## Configuration Guide 44 | 45 | The `.env` file is a central place to manage environment variables. It's pre-configured to work with Docker Compose out of the box, without any changes required for initial setup. However, for production deployment, certain secrets must be updated for security reasons. 46 | 47 | 1. **Secrets**: 48 | - **PostgreSQL, RabbitMQ, Django Secrets**: These are critical for the security of your application. Ensure to replace the placeholder values with strong, unique passwords. 49 | 50 | 2. **Ports**: 51 | - **API Port and RabbitMQ Dashboard Port**: Set these ports according to your infrastructure needs. They are exposed to the host machine. 52 | 53 | 3. **Performance Tuning**: 54 | - **Workers and Threads**: Adjust these values based on your server's capacity and expected load. 55 | 56 | 4. **Application Settings**: 57 | - **Host and Environment**: Set these to match your deployment environment. 58 | - **Debug and Logging**: Control debug mode and log levels. Set `DJANGO_DEBUG` to `false` in production. 59 | - **Localization**: Configure `LANGUAGE_CODE` and `TIME_ZONE` as per your requirements. 60 | 61 | 5. **CORS and CSRF Settings**: 62 | - Configure these settings to enhance the security of your application by specifying trusted origins. 63 | 64 | 6. **Database Configuration**: 65 | - **Postgres Connection**: Set up the database connection using the `DATABASE_URL` variable. 66 | 67 | --- 68 | 69 | ## Additional Notes 70 | - **Security**: Always prioritize security, especially when handling environment variables and secrets. 71 | - **Scalability**: Adjust the Docker and Celery configurations as your application scales. 72 | - **Monitoring**: Regularly monitor the performance and health of your application using integrated tools like Sentry and Silk. 73 | 74 | By following this guide and utilizing the advanced features, you'll be able to set up a powerful, efficient, and secure Django API environment. Happy coding! 75 | 76 | --- 77 | 78 | ## Quick Start Guide 79 | 80 | ### Setting Up Locally 81 | 82 | #### Prerequisites 83 | 84 | - **uv**: Install `uv` by following the instructions in the [official documentation](https://github.com/astral-sh/uv?tab=readme-ov-file#installation). 85 | 86 | #### 1. Repository Initialization 87 | - **Clone the Repository** 88 | 89 | #### 2. Install the project dependencies 90 | ```bash 91 | uv sync --all-extras --dev 92 | ``` 93 | 94 | #### 3. Configuration 95 | - **Environment Variables**: 96 | - Copy the example environment file: 97 | ```bash 98 | cp .env.example .env 99 | ``` 100 | - _Note: The API can operate without this step, but configuring the environment variables is recommended for full functionality._ 101 | 102 | #### 4. Database Setup 103 | - **Run Migrations**: 104 | ```bash 105 | make migrate 106 | ``` 107 | 108 | #### 5. Launching the Server 109 | - **Start the Local Server**: 110 | ```bash 111 | make run.server.local 112 | ``` 113 | 114 | ### Setting Up with Docker 115 | 116 | #### 1. Repository Initialization 117 | - **Clone the Repository** 118 | 119 | #### 2. Configuration 120 | - Follow the steps in the [Configuration Guide](#configuration-guide) to set up the `.env` file. 121 | 122 | #### 3. Docker Compose 123 | - **Run Docker Compose**: 124 | ```bash 125 | docker compose up -d 126 | ``` 127 | -------------------------------------------------------------------------------- /api/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | 5 | import django 6 | 7 | try: 8 | _version = ( 9 | os.popen("git describe --tags --dirty --always") # noqa: S605, S607 10 | .read() 11 | .strip() 12 | ) 13 | except Exception: # noqa: BLE001 14 | _version = "0.0.0" 15 | 16 | __version__ = _version 17 | 18 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "api.config.settings") 19 | django.setup() 20 | -------------------------------------------------------------------------------- /api/common/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaksimZayats/python-django-template/ef4cb5d83e43c27cfcc29ff38e5930f561497b9c/api/common/__init__.py -------------------------------------------------------------------------------- /api/common/routers.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from dataclasses import dataclass, field 4 | from typing import TYPE_CHECKING, Any, TypeVar, cast 5 | 6 | from django.urls import URLResolver, path 7 | from rest_framework.routers import SimpleRouter 8 | from rest_framework.viewsets import GenericViewSet, ViewSet 9 | 10 | if TYPE_CHECKING: 11 | from collections.abc import Callable 12 | 13 | T = TypeVar("T", bound=Any) 14 | 15 | 16 | @dataclass 17 | class CustomViewRouter: 18 | """Router for APIView and ViewSet.""" 19 | 20 | url_prefix: str = "" 21 | 22 | _drf_router: SimpleRouter = field(default_factory=SimpleRouter) 23 | _paths: list[URLResolver] = field(default_factory=list) 24 | 25 | def register( 26 | self, 27 | route: str, 28 | name: str | None = None, 29 | basename: str | None = None, 30 | as_view_kwargs: dict[str, Any] | None = None, 31 | **kwargs: Any, 32 | ) -> Callable[[T], T]: 33 | route = f"{self.url_prefix}{route}" 34 | 35 | def decorator(view: T) -> T: 36 | if issubclass(view, ViewSet | GenericViewSet): 37 | kwargs.setdefault("basename", basename or name) 38 | self._drf_router.register(route, view, **kwargs) 39 | else: 40 | kwargs.setdefault("name", name or basename) 41 | self._paths.append( 42 | path(route, view.as_view(**(as_view_kwargs or {})), **kwargs), 43 | ) 44 | 45 | return cast(T, view) 46 | 47 | return decorator 48 | 49 | @property 50 | def urls(self) -> list[Any]: 51 | return cast(list[Any], self._paths + self._drf_router.urls) 52 | -------------------------------------------------------------------------------- /api/config/__app_template__/__init__.py-tpl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaksimZayats/python-django-template/ef4cb5d83e43c27cfcc29ff38e5930f561497b9c/api/config/__app_template__/__init__.py-tpl -------------------------------------------------------------------------------- /api/config/__app_template__/admin.py-tpl: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from django.contrib import admin 4 | from django.contrib.admin import ModelAdmin 5 | 6 | from api.{{ app_name }}.models import {{ camel_case_app_name }}Model 7 | 8 | 9 | @admin.register({{ camel_case_app_name }}Model) 10 | class {{ camel_case_app_name }}Admin(ModelAdmin): 11 | pass 12 | -------------------------------------------------------------------------------- /api/config/__app_template__/apps.py-tpl: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from django.apps import AppConfig 4 | 5 | 6 | class {{ camel_case_app_name }}Config(AppConfig): 7 | default_auto_field = "django.db.models.BigAutoField" 8 | name = "api.{{ app_name }}" 9 | -------------------------------------------------------------------------------- /api/config/__app_template__/migrations/__init__.py-tpl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaksimZayats/python-django-template/ef4cb5d83e43c27cfcc29ff38e5930f561497b9c/api/config/__app_template__/migrations/__init__.py-tpl -------------------------------------------------------------------------------- /api/config/__app_template__/models.py-tpl: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from django.db import models 4 | 5 | 6 | class {{ camel_case_app_name }}Model(models.Model): 7 | def __str__(self) -> str: 8 | raise NotImplementedError 9 | -------------------------------------------------------------------------------- /api/config/__app_template__/serializers.py-tpl: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from rest_framework import serializers 4 | 5 | 6 | class {{ camel_case_app_name }}Serializer(serializers.Serializer): 7 | message = serializers.CharField() 8 | -------------------------------------------------------------------------------- /api/config/__app_template__/urls.py-tpl: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from api.{{ app_name }}.views import router 4 | 5 | urlpatterns = [ 6 | *router.urls, 7 | ] 8 | -------------------------------------------------------------------------------- /api/config/__app_template__/views.py-tpl: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING 4 | 5 | from rest_framework.generics import GenericAPIView 6 | from rest_framework.response import Response 7 | 8 | from api.common.routers import CustomViewRouter 9 | from api.{{ app_name }} import serializers 10 | 11 | if TYPE_CHECKING: 12 | from rest_framework.request import Request 13 | 14 | router = CustomViewRouter() 15 | 16 | 17 | @router.register(r"{{ app_name }}/", name="{{ app_name }}") 18 | class {{ camel_case_app_name }}View(GenericAPIView): 19 | serializer_class = serializers.{{ camel_case_app_name }}Serializer 20 | 21 | def post(self, request: Request) -> Response: 22 | serializer = self.get_serializer(data=request.data) 23 | serializer.is_valid(raise_exception=True) 24 | return Response(serializer.data) 25 | -------------------------------------------------------------------------------- /api/config/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from dotenv import load_dotenv 4 | 5 | load_dotenv() 6 | -------------------------------------------------------------------------------- /api/config/application.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from os import getenv 4 | 5 | from api.config.silk import SILKY_MIDDLEWARE_CLASS, USE_SILK 6 | 7 | PROJECT_NAME = getenv("PROJECT_NAME", "django_template") 8 | PROJECT_VERBOSE_NAME = getenv("PROJECT_VERBOSE_NAME", "Django Template").strip("'\"") 9 | 10 | ENVIRONMENT = getenv("ENVIRONMENT", "local") 11 | HOST = getenv("HOST", "localhost") 12 | 13 | INSTALLED_APPS = [ 14 | "django.contrib.admin", 15 | "django.contrib.auth", 16 | "django.contrib.contenttypes", 17 | "django.contrib.sessions", 18 | "django.contrib.messages", 19 | "django.contrib.staticfiles", 20 | "corsheaders", 21 | "axes", 22 | "silk", 23 | "rest_framework", 24 | "drf_spectacular", 25 | "api.user.apps.UserConfig", 26 | ] 27 | 28 | MIDDLEWARE = [ 29 | "django.middleware.security.SecurityMiddleware", 30 | "django.contrib.sessions.middleware.SessionMiddleware", 31 | "corsheaders.middleware.CorsMiddleware", 32 | "django.middleware.common.CommonMiddleware", 33 | "django.middleware.csrf.CsrfViewMiddleware", 34 | "django.contrib.auth.middleware.AuthenticationMiddleware", 35 | "django.contrib.messages.middleware.MessageMiddleware", 36 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 37 | SILKY_MIDDLEWARE_CLASS, 38 | "axes.middleware.AxesMiddleware", 39 | ] 40 | 41 | if not USE_SILK: 42 | INSTALLED_APPS.remove("silk") 43 | MIDDLEWARE.remove(SILKY_MIDDLEWARE_CLASS) 44 | 45 | ROOT_URLCONF = "api.web.urls" 46 | 47 | TEMPLATES = [ 48 | { 49 | "BACKEND": "django.template.backends.django.DjangoTemplates", 50 | "DIRS": [], 51 | "APP_DIRS": True, 52 | "OPTIONS": { 53 | "context_processors": [ 54 | "django.template.context_processors.debug", 55 | "django.template.context_processors.request", 56 | "django.contrib.auth.context_processors.auth", 57 | "django.contrib.messages.context_processors.messages", 58 | ], 59 | }, 60 | }, 61 | ] 62 | 63 | WSGI_APPLICATION = "api.web.wsgi.application" 64 | 65 | LANGUAGE_CODE = getenv("LANGUAGE_CODE", "en-us") 66 | 67 | USE_TZ = True 68 | TIME_ZONE = getenv("TIME_ZONE", "UTC") 69 | 70 | USE_I18N = True 71 | -------------------------------------------------------------------------------- /api/config/auth.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | AUTH_USER_MODEL = "user.User" 4 | 5 | AUTH_PASSWORD_VALIDATORS = [ 6 | { 7 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", 8 | }, 9 | { 10 | "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", 11 | }, 12 | { 13 | "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", 14 | }, 15 | { 16 | "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", 17 | }, 18 | ] 19 | -------------------------------------------------------------------------------- /api/config/axes.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from os import getenv 4 | 5 | AXES_ENABLED = getenv("AXES_ENABLED", default="true").lower() == "true" 6 | AXES_FAILURE_LIMIT = int(getenv("AXES_FAILURE_LIMIT", default="5")) 7 | -------------------------------------------------------------------------------- /api/config/base.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from pathlib import Path 4 | 5 | BASE_DIR = Path(__file__).resolve().parent.parent.parent 6 | -------------------------------------------------------------------------------- /api/config/cache.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | from os import getenv 5 | from typing import Any 6 | 7 | from django.core.cache import cache 8 | from redis.exceptions import RedisError 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | USE_REDIS_FOR_CACHE = getenv("USE_REDIS_FOR_CACHE", default="true").lower() == "true" 13 | REDIS_URL = getenv("REDIS_URL", default="redis://localhost:6379/0") 14 | 15 | CACHES: dict[str, Any] = {} 16 | 17 | if USE_REDIS_FOR_CACHE: 18 | logger.info("Using Redis for cache") 19 | CACHES["default"] = { 20 | "BACKEND": "django_redis.cache.RedisCache", 21 | "LOCATION": REDIS_URL, 22 | "OPTIONS": { 23 | "CLIENT_CLASS": "django_redis.client.DefaultClient", 24 | }, 25 | } 26 | 27 | # Ping the cache to see if it's working 28 | try: 29 | cache.set("ping", "pong") 30 | 31 | if cache.get("ping") != "pong": 32 | msg = "Cache is not working properly." 33 | raise ValueError(msg) # noqa: TRY301 34 | 35 | cache.delete("ping") 36 | 37 | logger.info("Cache is working properly") 38 | except (ValueError, RedisError): 39 | logger.exception("Cache is not working. Using dummy cache instead") 40 | CACHES["default"] = { 41 | "BACKEND": "django.core.cache.backends.dummy.DummyCache", 42 | } 43 | else: 44 | logger.warning("Using dummy cache") 45 | CACHES["default"] = { 46 | "BACKEND": "django.core.cache.backends.dummy.DummyCache", 47 | } 48 | -------------------------------------------------------------------------------- /api/config/celery.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from os import getenv 4 | 5 | from api.config.application import TIME_ZONE 6 | 7 | broker_url = getenv("CELERY_BROKER_URL", "redis://localhost:6379/0") 8 | result_backend = getenv("CELERY_RESULT_BACKEND", "redis://localhost:6379/0") 9 | 10 | task_serializer = "json" 11 | result_serializer = "json" 12 | accept_content = ["json"] 13 | 14 | task_always_eager = getenv("CELERY_TASK_ALWAYS_EAGER", "false").lower() == "true" 15 | task_eager_propagates = getenv("CELERY_TASK_EAGER_PROPAGATES", "false").lower() == "true" 16 | task_ignore_result = getenv("CELERY_TASK_IGNORE_RESULT", "false").lower() == "true" 17 | 18 | timezone = TIME_ZONE 19 | enable_utc = True 20 | -------------------------------------------------------------------------------- /api/config/database.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | from os import getenv 5 | 6 | import dj_database_url 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" 11 | 12 | _default_db_url = "sqlite:///db.sqlite3" 13 | DB_URL = getenv("DATABASE_URL", default=_default_db_url) 14 | 15 | if _default_db_url == DB_URL: 16 | logger.warning("Using default database url: '%s'", DB_URL) 17 | 18 | CONN_MAX_AGE = int(getenv("CONN_MAX_AGE", default="600")) 19 | DATABASES = { 20 | "default": dj_database_url.parse(DB_URL, conn_max_age=CONN_MAX_AGE), 21 | } 22 | -------------------------------------------------------------------------------- /api/config/logging.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging.config 4 | from os import getenv 5 | 6 | LOG_LEVEL = getenv("LOG_LEVEL", default="INFO") 7 | 8 | LOGGING = { 9 | "version": 1, 10 | "disable_existing_loggers": False, 11 | "formatters": { 12 | "colored": { 13 | "()": "colorlog.ColoredFormatter", 14 | "format": "%(asctime)s %(log_color)s%(levelname)s %(name)s %(message)s", 15 | "datefmt": "%Y-%m-%d %H:%M:%S", 16 | }, 17 | }, 18 | "handlers": { 19 | "console": { 20 | "class": "logging.StreamHandler", 21 | "formatter": "colored", 22 | }, 23 | }, 24 | "loggers": { 25 | "": { 26 | "handlers": ["console"], 27 | "level": LOG_LEVEL, 28 | "propagate": True, 29 | }, 30 | }, 31 | } 32 | 33 | logging.config.dictConfig(LOGGING) 34 | -------------------------------------------------------------------------------- /api/config/rest.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | REST_FRAMEWORK = { 4 | "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", 5 | "DEFAULT_AUTHENTICATION_CLASSES": ( 6 | "rest_framework.authentication.SessionAuthentication", 7 | "rest_framework.authentication.BasicAuthentication", 8 | "rest_framework_simplejwt.authentication.JWTAuthentication", 9 | ), 10 | "DEFAULT_PARSER_CLASSES": [ 11 | "rest_framework.parsers.MultiPartParser", 12 | "rest_framework.parsers.JSONParser", 13 | "rest_framework.parsers.FormParser", 14 | ], 15 | } 16 | -------------------------------------------------------------------------------- /api/config/security.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | from os import getenv 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | _default_secret_key = "your-super-secret-and-long-django-secret-key" # noqa: S105 9 | SECRET_KEY = getenv("DJANGO_SECRET_KEY", _default_secret_key) 10 | 11 | if _default_secret_key == SECRET_KEY: 12 | logger.warning("You are using a default Django secret key") 13 | 14 | # SECURITY WARNING: don't run with debug turned on in production! 15 | DEBUG = getenv("DJANGO_DEBUG", "false").lower() == "true" 16 | 17 | ALLOWED_HOSTS = [ 18 | host.strip() for host in getenv("ALLOWED_HOSTS", "127.0.0.1,localhost").split(",") 19 | ] 20 | 21 | AUTHENTICATION_BACKENDS = [ 22 | "axes.backends.AxesStandaloneBackend", 23 | "django.contrib.auth.backends.ModelBackend", 24 | ] 25 | 26 | CSRF_TRUSTED_ORIGINS = [ 27 | host.strip() for host in getenv("CSRF_TRUSTED_ORIGINS", "http://localhost").split(",") 28 | ] 29 | 30 | CORS_ALLOW_ALL_ORIGINS = getenv("CORS_ALLOW_ALL_ORIGINS", "False").lower() == "true" 31 | CORS_ALLOW_CREDENTIALS = getenv("CORS_ALLOW_CREDENTIALS", "False").lower() == "true" 32 | CORS_ALLOWED_ORIGINS = getenv("CORS_ALLOWED_ORIGINS", "http://localhost").split( 33 | ",", 34 | ) 35 | -------------------------------------------------------------------------------- /api/config/sentry.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | from os import environ, getenv 5 | 6 | from sentry_sdk.integrations.celery import CeleryIntegration 7 | from sentry_sdk.integrations.django import DjangoIntegration 8 | 9 | from api.config.application import ENVIRONMENT 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | USE_SENTRY = getenv("USE_SENTRY", default="false").lower() == "true" 14 | 15 | if USE_SENTRY: 16 | import sentry_sdk 17 | 18 | DSN = environ["SENTRY_DSN"] 19 | TRACES_SAMPLE_RATE = float(getenv("SENTRY_TRACES_SAMPLE_RATE", default="1.0")) 20 | PROFILE_SAMPLE_RATE = float( 21 | getenv("SENTRY_PROFILE_SAMPLE_RATE", default="1.0"), 22 | ) 23 | 24 | sentry_sdk.init( 25 | dsn=DSN, 26 | environment=ENVIRONMENT, 27 | integrations=[ 28 | DjangoIntegration(), 29 | CeleryIntegration(), 30 | ], 31 | # Set traces_sample_rate to 1.0 to capture 100% 32 | # of transactions for performance monitoring. 33 | traces_sample_rate=TRACES_SAMPLE_RATE, 34 | # Set profiles_sample_rate to 1.0 to profile 100% 35 | # of sampled transactions. 36 | # We recommend adjusting this value in production. 37 | profiles_sample_rate=PROFILE_SAMPLE_RATE, 38 | # If you wish to associate users to errors (assuming you are using 39 | # django.contrib.auth) you may enable sending PII data. 40 | send_default_pii=True, 41 | ) 42 | 43 | logger.info("Sentry is initialized") 44 | else: 45 | logger.info("Sentry is not initialized") 46 | -------------------------------------------------------------------------------- /api/config/settings.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from split_settings.tools import include 4 | 5 | include( 6 | "base.py", 7 | "logging.py", 8 | "application.py", 9 | "auth.py", 10 | "database.py", 11 | "security.py", 12 | "storage.py", 13 | "rest.py", 14 | "sentry.py", 15 | "silk.py", 16 | "spectacular.py", 17 | "celery.py", 18 | "cache.py", 19 | "axes.py", 20 | ) 21 | -------------------------------------------------------------------------------- /api/config/silk.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from os import getenv 4 | 5 | USE_SILK = getenv("USE_SILK", default="false").lower() == "true" 6 | SILKY_MIDDLEWARE_CLASS = "silk.middleware.SilkyMiddleware" 7 | 8 | SILKY_AUTHENTICATION = True # User must login 9 | SILKY_AUTHORISATION = True # User must have permissions 10 | -------------------------------------------------------------------------------- /api/config/spectacular.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from api import __version__ 4 | from api.config.application import PROJECT_VERBOSE_NAME 5 | 6 | SPECTACULAR_SETTINGS = { 7 | "TITLE": PROJECT_VERBOSE_NAME, 8 | "SCHEMA_PATH_PREFIX": r"/api/v[0-9]", 9 | "COMPONENT_SPLIT_REQUEST": True, 10 | "VERSION": __version__, 11 | } 12 | -------------------------------------------------------------------------------- /api/config/storage.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | from os import getenv 5 | from typing import Any, cast 6 | 7 | from storages.backends.s3 import S3Storage 8 | 9 | from api.config.base import BASE_DIR 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | USE_S3_FOR_MEDIA = getenv("USE_S3_FOR_MEDIA", default="false").lower() == "true" 14 | USE_S3_FOR_STATIC = getenv("USE_S3_FOR_STATIC", default="false").lower() == "true" 15 | 16 | STATIC_ROOT = BASE_DIR / "staticfiles" 17 | STATIC_URL = "static/" 18 | 19 | MEDIA_ROOT = BASE_DIR / "media" 20 | MEDIA_URL = "media/" 21 | 22 | AWS_STORAGE_BUCKET_NAME = getenv("AWS_STORAGE_BUCKET_NAME", "bucket") 23 | AWS_S3_CUSTOM_DOMAIN = getenv( 24 | "AWS_S3_CUSTOM_DOMAIN", 25 | f"{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com", 26 | ) 27 | AWS_S3_ENDPOINT_URL = f"https://{AWS_S3_CUSTOM_DOMAIN}" 28 | 29 | AWS_S3_ACCESS_KEY_ID = getenv("AWS_S3_ACCESS_KEY_ID", "access_key") 30 | AWS_S3_SECRET_ACCESS_KEY = getenv("AWS_S3_SECRET_ACCESS_KEY", "secret_key") 31 | 32 | AWS_S3_CONFIG = { 33 | "BACKEND": "api.config.storage.CustomDomainS3Storage", 34 | "OPTIONS": { 35 | "bucket_name": AWS_STORAGE_BUCKET_NAME, 36 | "access_key": AWS_S3_ACCESS_KEY_ID, 37 | "secret_key": AWS_S3_SECRET_ACCESS_KEY, 38 | "endpoint_url": AWS_S3_ENDPOINT_URL, 39 | }, 40 | } 41 | 42 | STORAGES: dict[str, Any] = {} 43 | 44 | if USE_S3_FOR_STATIC: 45 | logger.info("Serving static files from S3") 46 | STORAGES["staticfiles"] = AWS_S3_CONFIG 47 | else: 48 | logger.info("Serving static files locally") 49 | STORAGES["staticfiles"] = { 50 | "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage", 51 | } 52 | 53 | 54 | if USE_S3_FOR_MEDIA: 55 | logger.info("Serving media files from S3") 56 | STORAGES["default"] = AWS_S3_CONFIG 57 | else: 58 | logger.info("Serving media files locally") 59 | STORAGES["default"] = { 60 | "BACKEND": "django.core.files.storage.FileSystemStorage", 61 | "LOCATION": MEDIA_ROOT.as_posix(), 62 | "BASE_URL": MEDIA_URL, 63 | } 64 | 65 | 66 | class CustomDomainS3Storage(S3Storage): 67 | """Extend S3 with signed URLs for custom domains.""" 68 | 69 | custom_domain = False 70 | 71 | def url( 72 | self, 73 | name: str, 74 | parameters: Any = None, 75 | expire: Any = None, 76 | http_method: Any = None, 77 | ) -> str: 78 | """Replace internal domain with custom domain for signed URLs.""" 79 | url = cast(str, super().url(name, parameters, expire, http_method)) 80 | 81 | return url.replace( 82 | f"https://{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com", 83 | AWS_S3_ENDPOINT_URL, 84 | ) 85 | -------------------------------------------------------------------------------- /api/user/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaksimZayats/python-django-template/ef4cb5d83e43c27cfcc29ff38e5930f561497b9c/api/user/__init__.py -------------------------------------------------------------------------------- /api/user/admin.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Any 4 | 5 | from django.contrib import admin 6 | 7 | from api.user.models import User 8 | 9 | 10 | @admin.register(User) 11 | class UserAdmin(admin.ModelAdmin): 12 | filter_horizontal = ("groups", "user_permissions") 13 | 14 | list_display = ( 15 | "username", 16 | "is_active", 17 | "is_staff", 18 | "is_superuser", 19 | ) 20 | 21 | def save_model( 22 | self, 23 | request: Any, 24 | obj: User, 25 | form: None, 26 | change: bool, # noqa: FBT001 27 | ) -> None: 28 | """Update user password if it is not raw. 29 | 30 | This is needed to hash password when updating user from admin panel. 31 | """ 32 | has_raw_password = obj.password.startswith("pbkdf2_sha256") 33 | if not has_raw_password: 34 | obj.set_password(obj.password) 35 | 36 | super().save_model(request, obj, form, change) 37 | -------------------------------------------------------------------------------- /api/user/apps.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from django.apps import AppConfig 4 | 5 | 6 | class UserConfig(AppConfig): 7 | default_auto_field = "django.db.models.BigAutoField" 8 | name = "api.user" 9 | -------------------------------------------------------------------------------- /api/user/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0 on 2023-12-25 12:56 2 | 3 | import django.contrib.auth.models 4 | import django.contrib.auth.validators 5 | import django.utils.timezone 6 | from django.db import migrations, models 7 | 8 | 9 | class Migration(migrations.Migration): 10 | initial = True 11 | 12 | dependencies = [ 13 | ("auth", "0012_alter_user_first_name_max_length"), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name="User", 19 | fields=[ 20 | ( 21 | "id", 22 | models.BigAutoField( 23 | auto_created=True, 24 | primary_key=True, 25 | serialize=False, 26 | verbose_name="ID", 27 | ), 28 | ), 29 | ("password", models.CharField(max_length=128, verbose_name="password")), 30 | ( 31 | "last_login", 32 | models.DateTimeField( 33 | blank=True, null=True, verbose_name="last login" 34 | ), 35 | ), 36 | ( 37 | "is_superuser", 38 | models.BooleanField( 39 | default=False, 40 | help_text="Designates that this user has all permissions without explicitly assigning them.", 41 | verbose_name="superuser status", 42 | ), 43 | ), 44 | ( 45 | "username", 46 | models.CharField( 47 | error_messages={ 48 | "unique": "A user with that username already exists." 49 | }, 50 | help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.", 51 | max_length=150, 52 | unique=True, 53 | validators=[ 54 | django.contrib.auth.validators.UnicodeUsernameValidator() 55 | ], 56 | verbose_name="username", 57 | ), 58 | ), 59 | ( 60 | "first_name", 61 | models.CharField( 62 | blank=True, max_length=150, verbose_name="first name" 63 | ), 64 | ), 65 | ( 66 | "last_name", 67 | models.CharField( 68 | blank=True, max_length=150, verbose_name="last name" 69 | ), 70 | ), 71 | ( 72 | "email", 73 | models.EmailField( 74 | blank=True, max_length=254, verbose_name="email address" 75 | ), 76 | ), 77 | ( 78 | "is_staff", 79 | models.BooleanField( 80 | default=False, 81 | help_text="Designates whether the user can log into this admin site.", 82 | verbose_name="staff status", 83 | ), 84 | ), 85 | ( 86 | "is_active", 87 | models.BooleanField( 88 | default=True, 89 | help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", 90 | verbose_name="active", 91 | ), 92 | ), 93 | ( 94 | "date_joined", 95 | models.DateTimeField( 96 | default=django.utils.timezone.now, verbose_name="date joined" 97 | ), 98 | ), 99 | ( 100 | "groups", 101 | models.ManyToManyField( 102 | blank=True, 103 | help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", 104 | related_name="user_set", 105 | related_query_name="user", 106 | to="auth.group", 107 | verbose_name="groups", 108 | ), 109 | ), 110 | ( 111 | "user_permissions", 112 | models.ManyToManyField( 113 | blank=True, 114 | help_text="Specific permissions for this user.", 115 | related_name="user_set", 116 | related_query_name="user", 117 | to="auth.permission", 118 | verbose_name="user permissions", 119 | ), 120 | ), 121 | ], 122 | options={ 123 | "verbose_name": "user", 124 | "verbose_name_plural": "users", 125 | "abstract": False, 126 | }, 127 | managers=[ 128 | ("objects", django.contrib.auth.models.UserManager()), 129 | ], 130 | ), 131 | ] 132 | -------------------------------------------------------------------------------- /api/user/migrations/0002_createsuperuser.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0 on 2023-12-06 17:16 2 | 3 | from os import getenv 4 | from typing import TYPE_CHECKING, cast 5 | from django.db import migrations 6 | 7 | 8 | def create_superuser(apps, schema_editor): 9 | User = cast("UserModel", apps.get_model("user", "User")) 10 | User.objects.create_superuser( 11 | username=getenv("DJANGO_ADMIN_USERNAME", "admin"), 12 | password=getenv("DJANGO_ADMIN_PASSWORD", "admin"), 13 | email=getenv("DJANGO_ADMIN_EMAIL", "admin@admin.com"), 14 | ) 15 | 16 | 17 | class Migration(migrations.Migration): 18 | dependencies = [ 19 | ("user", "0001_initial"), 20 | ] 21 | 22 | operations = [ 23 | migrations.RunPython(create_superuser), 24 | ] 25 | -------------------------------------------------------------------------------- /api/user/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaksimZayats/python-django-template/ef4cb5d83e43c27cfcc29ff38e5930f561497b9c/api/user/migrations/__init__.py -------------------------------------------------------------------------------- /api/user/models.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from django.contrib.auth.models import AbstractUser 4 | 5 | 6 | class User(AbstractUser): 7 | pass 8 | -------------------------------------------------------------------------------- /api/user/permissions.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING, Any, cast 4 | 5 | from rest_framework import permissions 6 | 7 | if TYPE_CHECKING: 8 | from rest_framework.request import Request 9 | 10 | 11 | class IsStaffPermission(permissions.BasePermission): 12 | def has_permission(self, request: Request, view: Any) -> bool: # noqa: ARG002 13 | return cast(bool, request.user.is_staff) 14 | -------------------------------------------------------------------------------- /api/user/serializers.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from rest_framework import serializers 4 | 5 | from api.user.models import User 6 | 7 | 8 | class UserSerializer(serializers.ModelSerializer): 9 | class Meta: 10 | model = User 11 | fields = ("id", "username", "email", "password") 12 | extra_kwargs = {"password": {"write_only": True}} # noqa: RUF012 13 | -------------------------------------------------------------------------------- /api/user/urls.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from api.user.views import router 4 | 5 | urlpatterns = [ 6 | *router.urls, 7 | ] 8 | -------------------------------------------------------------------------------- /api/user/views.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | from typing import TYPE_CHECKING 5 | 6 | from rest_framework import mixins 7 | from rest_framework.generics import GenericAPIView 8 | from rest_framework.response import Response 9 | from rest_framework.viewsets import GenericViewSet 10 | 11 | from api.common.routers import CustomViewRouter 12 | from api.user import serializers 13 | from api.user.models import User 14 | from api.user.permissions import IsStaffPermission 15 | 16 | if TYPE_CHECKING: 17 | from rest_framework.request import Request 18 | 19 | router = CustomViewRouter() 20 | 21 | logger = logging.getLogger(__name__) 22 | 23 | 24 | @router.register(r"users/me/", name="users") 25 | class MyUserView(GenericAPIView): 26 | serializer_class = serializers.UserSerializer 27 | 28 | def get(self, request: Request) -> Response: 29 | serializer = self.get_serializer(request.user) 30 | return Response(serializer.data) 31 | 32 | 33 | @router.register(r"users", name="users") 34 | class UserViewSet( 35 | mixins.CreateModelMixin, 36 | mixins.RetrieveModelMixin, 37 | mixins.ListModelMixin, 38 | mixins.UpdateModelMixin, 39 | GenericViewSet, 40 | ): 41 | serializer_class = serializers.UserSerializer 42 | queryset = User.objects.all() 43 | permission_classes = (IsStaffPermission,) 44 | -------------------------------------------------------------------------------- /api/web/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaksimZayats/python-django-template/ef4cb5d83e43c27cfcc29ff38e5930f561497b9c/api/web/__init__.py -------------------------------------------------------------------------------- /api/web/asgi.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | 5 | from django.core.asgi import get_asgi_application 6 | 7 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "api.config.settings") 8 | 9 | application = get_asgi_application() 10 | -------------------------------------------------------------------------------- /api/web/urls.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | 5 | from django.conf import settings 6 | from django.conf.urls.static import static 7 | from django.contrib import admin 8 | from django.shortcuts import redirect 9 | from django.urls import include, path 10 | from drf_spectacular.utils import extend_schema 11 | from drf_spectacular.views import ( 12 | SpectacularAPIView, 13 | SpectacularRedocView, 14 | SpectacularSwaggerView, 15 | ) 16 | 17 | from api.config.silk import USE_SILK 18 | from api.config.storage import ( 19 | USE_S3_FOR_MEDIA, 20 | USE_S3_FOR_STATIC, 21 | ) 22 | 23 | logger = logging.getLogger(__name__) 24 | 25 | _swagger_urlpatterns = [ 26 | path( 27 | "api/v1/schema/", 28 | extend_schema(exclude=True)(SpectacularAPIView).as_view(), 29 | name="schema", 30 | ), 31 | path( 32 | "docs/", 33 | extend_schema(exclude=True)(SpectacularSwaggerView).as_view(url_name="schema"), 34 | name="swagger-ui", 35 | ), 36 | path( 37 | "redoc/", 38 | extend_schema(exclude=True)(SpectacularRedocView).as_view(url_name="schema"), 39 | name="redoc", 40 | ), 41 | ] 42 | 43 | urlpatterns = [ 44 | *_swagger_urlpatterns, 45 | path("", lambda _request: redirect("docs/"), name="home"), 46 | path("admin/", admin.site.urls), 47 | path("", include("api.user.urls")), 48 | ] 49 | 50 | if USE_SILK: 51 | urlpatterns.append(path("silk/", include("silk.urls"))) 52 | 53 | if not USE_S3_FOR_STATIC: 54 | urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) 55 | 56 | if not USE_S3_FOR_MEDIA: 57 | logger.warning("S3 is disabled, serving media files locally. Consider using S3.") 58 | urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 59 | -------------------------------------------------------------------------------- /api/web/wsgi.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | 5 | from django.core.wsgi import get_wsgi_application 6 | 7 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "api.config.settings") 8 | 9 | application = get_wsgi_application() 10 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | x-common: &common 2 | env_file: 3 | - .env 4 | 5 | logging: 6 | driver: "json-file" 7 | options: 8 | max-size: "10m" 9 | max-file: "3" 10 | 11 | networks: 12 | - main 13 | 14 | services: 15 | api: 16 | <<: *common 17 | build: . 18 | environment: 19 | WORKERS: ${WORKERS:-2} 20 | THREADS: ${THREADS:-2} 21 | command: make migrate run.server.prod 22 | ports: 23 | - "${API_PORT}:80" 24 | depends_on: 25 | - pgbouncer 26 | - migrations 27 | - collectstatic 28 | - redis 29 | - rabbitmq 30 | restart: always 31 | volumes: 32 | - .:/application 33 | 34 | db: 35 | <<: *common 36 | image: postgres:latest 37 | restart: unless-stopped 38 | environment: 39 | POSTGRES_INITDB_ARGS: --auth=md5 40 | volumes: 41 | - postgres-data:/var/lib/postgresql/data 42 | 43 | pgbouncer: 44 | <<: *common 45 | image: edoburu/pgbouncer:latest 46 | restart: unless-stopped 47 | depends_on: 48 | - db 49 | environment: 50 | - DATABASE_URL=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB} 51 | - POOL_MODE=transaction 52 | - MAX_CLIENT_CONN=1000 53 | - DEFAULT_POOL_SIZE=20 54 | - MIN_POOL_SIZE=5 55 | - RESERVE_POOL_SIZE=5 56 | - RESERVE_POOL_TIMEOUT=5 57 | - SERVER_RESET_QUERY=DISCARD ALL 58 | - SERVER_CHECK_QUERY=select 1 59 | - LOG_CONNECTIONS=1 60 | - LOG_DISCONNECTIONS=1 61 | - LOG_POOLER_ERRORS=1 62 | - STATS_PERIOD=60 63 | - AUTH_TYPE=md5 64 | volumes: 65 | - pgbouncer-data:/var/lib/pgbouncer 66 | 67 | redis: 68 | <<: *common 69 | image: redis:latest 70 | restart: unless-stopped 71 | volumes: 72 | - redis-data:/data 73 | 74 | rabbitmq: 75 | <<: *common 76 | image: rabbitmq:3.13-management 77 | restart: unless-stopped 78 | environment: 79 | RABBITMQ_DEFAULT_USER: ${RABBITMQ_DEFAULT_USER:-guest} 80 | RABBITMQ_DEFAULT_PASS: ${RABBITMQ_DEFAULT_PASS:-guest} 81 | volumes: 82 | - rabbitmq-data:/var/lib/rabbitmq 83 | ports: 84 | - "${RABBITMQ_DASHBOARD_PORT:-15672}:15672" 85 | 86 | celery: 87 | <<: *common 88 | build: . 89 | command: make run.celery.prod 90 | restart: unless-stopped 91 | depends_on: 92 | - api 93 | volumes: 94 | - .:/application 95 | 96 | migrations: 97 | <<: *common 98 | build: . 99 | command: make migrate 100 | depends_on: 101 | - db 102 | volumes: 103 | - .:/application 104 | 105 | collectstatic: 106 | <<: *common 107 | build: . 108 | command: make collectstatic 109 | depends_on: 110 | - db 111 | volumes: 112 | - .:/application 113 | 114 | networks: 115 | main: 116 | ipam: 117 | driver: default 118 | 119 | volumes: 120 | postgres-data: 121 | pgbouncer-data: 122 | redis-data: 123 | rabbitmq-data: 124 | -------------------------------------------------------------------------------- /logs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaksimZayats/python-django-template/ef4cb5d83e43c27cfcc29ff38e5930f561497b9c/logs/.gitkeep -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | 4 | from __future__ import annotations 5 | 6 | import os 7 | import sys 8 | from typing import TypeAlias 9 | 10 | from api.config.base import BASE_DIR 11 | 12 | _APPS_DIR = BASE_DIR / "api" 13 | _TEMPLATE_DIR = BASE_DIR / "api" / "config" / "__app_template__" 14 | 15 | _AppName: TypeAlias = str 16 | _AppDirectory: TypeAlias = str 17 | 18 | 19 | def main() -> None: 20 | """Run administrative tasks.""" 21 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "api.config.settings") 22 | try: 23 | from django.core.management import execute_from_command_line 24 | except ImportError as exc: 25 | msg = ( 26 | "Couldn't import Django. Are you sure it's installed and " 27 | "available on your PYTHONPATH environment variable? Did you " 28 | "forget to activate a virtual environment?" 29 | ) 30 | raise ImportError(msg) from exc 31 | 32 | _modify_startapp_args() 33 | execute_from_command_line(sys.argv) 34 | 35 | 36 | def _modify_startapp_args() -> None: 37 | if "startapp" not in sys.argv: 38 | return 39 | 40 | _add_app_directory_if_not_provided() 41 | _add_template_if_not_provided() 42 | 43 | 44 | def _add_template_if_not_provided() -> None: 45 | if "--no-template" in sys.argv: 46 | sys.argv.remove("--no-template") 47 | elif "--template" not in sys.argv: 48 | sys.argv.extend(("--template", str(_TEMPLATE_DIR))) 49 | 50 | 51 | def _add_app_directory_if_not_provided() -> None: 52 | app_name, _app_directory = _get_app_parameters() 53 | if _app_directory: 54 | return 55 | 56 | app_directory = _APPS_DIR / app_name 57 | app_directory.mkdir(parents=True, exist_ok=True) 58 | 59 | sys.argv.insert(sys.argv.index(app_name) + 1, str(app_directory)) 60 | 61 | 62 | def _get_app_parameters() -> tuple[_AppName, _AppDirectory]: 63 | app_name = "" 64 | app_directory = "" 65 | 66 | startapp_index = sys.argv.index("startapp") 67 | for index in range(startapp_index + 1, len(sys.argv)): 68 | if sys.argv[index - 1].startswith("-") or sys.argv[index].startswith("-"): 69 | continue 70 | 71 | if not app_name: 72 | app_name = sys.argv[index] 73 | elif not app_directory: 74 | app_directory = sys.argv[index] 75 | else: 76 | msg = "Too many positional arguments for startapp command." 77 | raise ValueError(msg) 78 | 79 | return app_name, app_directory 80 | 81 | 82 | if __name__ == "__main__": 83 | main() 84 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "python-django-template" 3 | version = "0.1.0" 4 | description = "Python Django Template" 5 | requires-python = ">=3.12" 6 | dependencies = [ 7 | "boto3==1.38.31", 8 | "cachetools==6.0.0", 9 | "celery==5.5.3", 10 | "celery-types==0.23.0", 11 | "colorlog==6.9.0", 12 | "dj_database_url==3.0.0", 13 | "Django==5.2.2", 14 | "django-axes[ipware]==8.0.0", 15 | "django-cors-headers==4.7.0", 16 | "django-silk==5.4.0", 17 | "django-split-settings==1.3.2", 18 | "django-storages==1.14.6", 19 | "django_redis==5.4.0", 20 | "djangorestframework==3.16.0", 21 | "djangorestframework_simplejwt==5.4.0", 22 | "drf-spectacular==0.28.0", 23 | "gunicorn==23.0.0", 24 | "psycopg==3.2.9", 25 | "pyjwt==2.10.1", 26 | "python-dotenv==1.1.0", 27 | "pytz==2025.2", 28 | "redis==6.2.0", 29 | "sentry-sdk==2.29.1", 30 | ] 31 | 32 | [tool.uv] 33 | dev-dependencies = [ 34 | "mypy==1.11.1", 35 | "pre-commit==3.8.0", 36 | "pytest==8.3.2", 37 | "pytest-cov==5.0.0", 38 | "pytest-django==4.8.0", 39 | "ruff==0.6.2", 40 | ] 41 | 42 | [tool.setuptools] 43 | py-modules = [] 44 | 45 | [tool.ruff] 46 | show-fixes = true 47 | line-length = 90 48 | exclude = [ 49 | "migrations", 50 | ".venv", 51 | ] 52 | 53 | [tool.ruff.lint] 54 | select = ["ALL"] 55 | unfixable = ["T201"] 56 | ignore = [ 57 | "E501", # Line too long # Will be fixed by black 58 | "A003", 59 | "RUF001", 60 | "ERA001", 61 | "F841", # Local variable is assigned to but never used 62 | 63 | "ANN101", 64 | "ANN102", # Missing type annotation for `cls` in classmethod 65 | "ANN401", # Dynamically typed expressions (typing.Any) are disallowed in `{name}` 66 | 67 | "D10", 68 | 69 | "D203", 70 | "D213", 71 | "EXE002", 72 | 73 | "PD011", 74 | "UP040", # Type alias uses `TypeAlias` annotation instead of the `type` keyword 75 | ] 76 | 77 | [tool.ruff.lint.isort] 78 | required-imports = ["from __future__ import annotations"] 79 | 80 | [tool.ruff.lint.per-file-ignores] 81 | "__init__.py" = [ 82 | "F401", # Module imported but unused 83 | ] 84 | "tests/**" = [ 85 | "S101", # Use of assert detected 86 | ] 87 | 88 | [tool.mypy] 89 | python_version = "3.12" 90 | ignore_missing_imports_per_module = true 91 | ignore_missing_imports = true 92 | warn_return_any = true 93 | warn_unused_configs = true 94 | strict = false 95 | implicit_reexport = true 96 | exclude = [ 97 | "venv", 98 | "migrations", 99 | ] 100 | 101 | [tool.pytest.ini_options] 102 | minversion = "7.0" 103 | DJANGO_SETTINGS_MODULE = "api.config.settings" 104 | addopts = "--exitfirst -vv --cov --cov-report=html --cov-fail-under=80" 105 | testpaths = [ 106 | "tests", 107 | ] 108 | -------------------------------------------------------------------------------- /run-local.sh: -------------------------------------------------------------------------------- 1 | while true; do 2 | uv run manage.py runserver 3 | 4 | # Check the exit status of the command 5 | if [ $? -eq 0 ]; then 6 | # If the command succeeds, break out of the loop 7 | break 8 | else 9 | # If the command fails, display an error message 10 | echo "Command failed. Retrying..." 11 | sleep 0.5 # Add a delay between retries if desired 12 | fi 13 | done 14 | -------------------------------------------------------------------------------- /tasks/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from celery.app.task import Task 4 | 5 | Task.__class_getitem__ = classmethod(lambda cls, *_, **__: cls) # type: ignore[attr-defined] 6 | -------------------------------------------------------------------------------- /tasks/app.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from celery import Celery 4 | 5 | from api.config import celery as config 6 | 7 | app = Celery("main") 8 | app.config_from_object(config) 9 | app.autodiscover_tasks() 10 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaksimZayats/python-django-template/ef4cb5d83e43c27cfcc29ff38e5930f561497b9c/tests/__init__.py -------------------------------------------------------------------------------- /tests/integration/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaksimZayats/python-django-template/ef4cb5d83e43c27cfcc29ff38e5930f561497b9c/tests/integration/__init__.py -------------------------------------------------------------------------------- /tests/load/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaksimZayats/python-django-template/ef4cb5d83e43c27cfcc29ff38e5930f561497b9c/tests/load/__init__.py -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaksimZayats/python-django-template/ef4cb5d83e43c27cfcc29ff38e5930f561497b9c/tests/unit/__init__.py -------------------------------------------------------------------------------- /tests/unit/test_example.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | 4 | def test_example() -> None: 5 | assert True 6 | -------------------------------------------------------------------------------- /uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | requires-python = ">=3.12" 3 | 4 | [[package]] 5 | name = "amqp" 6 | version = "5.2.0" 7 | source = { registry = "https://pypi.org/simple" } 8 | dependencies = [ 9 | { name = "vine" }, 10 | ] 11 | sdist = { url = "https://files.pythonhosted.org/packages/32/2c/6eb09fbdeb3c060b37bd33f8873832897a83e7a428afe01aad333fc405ec/amqp-5.2.0.tar.gz", hash = "sha256:a1ecff425ad063ad42a486c902807d1482311481c8ad95a72694b2975e75f7fd", size = 128754 } 12 | wheels = [ 13 | { url = "https://files.pythonhosted.org/packages/b3/f0/8e5be5d5e0653d9e1d02b1144efa33ff7d2963dfad07049e02c0fa9b2e8d/amqp-5.2.0-py3-none-any.whl", hash = "sha256:827cb12fb0baa892aad844fd95258143bce4027fdac4fccddbc43330fd281637", size = 50917 }, 14 | ] 15 | 16 | [[package]] 17 | name = "asgiref" 18 | version = "3.8.1" 19 | source = { registry = "https://pypi.org/simple" } 20 | sdist = { url = "https://files.pythonhosted.org/packages/29/38/b3395cc9ad1b56d2ddac9970bc8f4141312dbaec28bc7c218b0dfafd0f42/asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590", size = 35186 } 21 | wheels = [ 22 | { url = "https://files.pythonhosted.org/packages/39/e3/893e8757be2612e6c266d9bb58ad2e3651524b5b40cf56761e985a28b13e/asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47", size = 23828 }, 23 | ] 24 | 25 | [[package]] 26 | name = "attrs" 27 | version = "24.2.0" 28 | source = { registry = "https://pypi.org/simple" } 29 | sdist = { url = "https://files.pythonhosted.org/packages/fc/0f/aafca9af9315aee06a89ffde799a10a582fe8de76c563ee80bbcdc08b3fb/attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346", size = 792678 } 30 | wheels = [ 31 | { url = "https://files.pythonhosted.org/packages/6a/21/5b6702a7f963e95456c0de2d495f67bf5fd62840ac655dc451586d23d39a/attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2", size = 63001 }, 32 | ] 33 | 34 | [[package]] 35 | name = "autopep8" 36 | version = "2.3.1" 37 | source = { registry = "https://pypi.org/simple" } 38 | dependencies = [ 39 | { name = "pycodestyle" }, 40 | ] 41 | sdist = { url = "https://files.pythonhosted.org/packages/6c/52/65556a5f917a4b273fd1b705f98687a6bd721dbc45966f0f6687e90a18b0/autopep8-2.3.1.tar.gz", hash = "sha256:8d6c87eba648fdcfc83e29b788910b8643171c395d9c4bcf115ece035b9c9dda", size = 92064 } 42 | wheels = [ 43 | { url = "https://files.pythonhosted.org/packages/ad/9e/f0beffe45b507dca9d7540fad42b316b2fd1076dc484c9b1f23d9da570d7/autopep8-2.3.1-py2.py3-none-any.whl", hash = "sha256:a203fe0fcad7939987422140ab17a930f684763bf7335bdb6709991dd7ef6c2d", size = 45667 }, 44 | ] 45 | 46 | [[package]] 47 | name = "billiard" 48 | version = "4.2.0" 49 | source = { registry = "https://pypi.org/simple" } 50 | sdist = { url = "https://files.pythonhosted.org/packages/09/52/f10d74fd56e73b430c37417658158ad8386202b069b70ff97d945c3ab67a/billiard-4.2.0.tar.gz", hash = "sha256:9a3c3184cb275aa17a732f93f65b20c525d3d9f253722d26a82194803ade5a2c", size = 154665 } 51 | wheels = [ 52 | { url = "https://files.pythonhosted.org/packages/50/8d/6e9fdeeab04d803abc5a715175f87e88893934d5590595eacff23ca12b07/billiard-4.2.0-py3-none-any.whl", hash = "sha256:07aa978b308f334ff8282bd4a746e681b3513db5c9a514cbdd810cbbdc19714d", size = 86720 }, 53 | ] 54 | 55 | [[package]] 56 | name = "boto3" 57 | version = "1.35.4" 58 | source = { registry = "https://pypi.org/simple" } 59 | dependencies = [ 60 | { name = "botocore" }, 61 | { name = "jmespath" }, 62 | { name = "s3transfer" }, 63 | ] 64 | sdist = { url = "https://files.pythonhosted.org/packages/ca/59/baeef26b73a6fd71602370b6e1ba3eb79d0d43dff6efcf552065548c49a0/boto3-1.35.4.tar.gz", hash = "sha256:d997b82c468bd5c2d5cd29810d47079b66b178d2b5ae021aebe262c4d78d4c94", size = 108591 } 65 | wheels = [ 66 | { url = "https://files.pythonhosted.org/packages/76/88/73608e1f1c4f4276213a0d8658cfe799a3b2d6e4189543676b8b852a788e/boto3-1.35.4-py3-none-any.whl", hash = "sha256:96c39593afb7b55ebb74d08c8e3201041d105b557c8c8536c9054c9f13da5f2a", size = 139142 }, 67 | ] 68 | 69 | [[package]] 70 | name = "botocore" 71 | version = "1.35.5" 72 | source = { registry = "https://pypi.org/simple" } 73 | dependencies = [ 74 | { name = "jmespath" }, 75 | { name = "python-dateutil" }, 76 | { name = "urllib3" }, 77 | ] 78 | sdist = { url = "https://files.pythonhosted.org/packages/6f/2f/3c1f7ded64677c05d0cfdb2c8f333d672ab4bb6fa38e7e3087953059db27/botocore-1.35.5.tar.gz", hash = "sha256:3a0086c7124cb3b0d9f98563d00ffd14a942c3f9e731d8d1ccf0d3a1ac7ed884", size = 12684618 } 79 | wheels = [ 80 | { url = "https://files.pythonhosted.org/packages/bb/0d/18e2c131baefd7841177c1169eb06bee6f346e81576fc480649251d36f11/botocore-1.35.5-py3-none-any.whl", hash = "sha256:8116b72c7ae845c195146e437e2afd9d17538a37b3f3548dcf67c12c86ba0742", size = 12473725 }, 81 | ] 82 | 83 | [[package]] 84 | name = "cachetools" 85 | version = "5.5.0" 86 | source = { registry = "https://pypi.org/simple" } 87 | sdist = { url = "https://files.pythonhosted.org/packages/c3/38/a0f315319737ecf45b4319a8cd1f3a908e29d9277b46942263292115eee7/cachetools-5.5.0.tar.gz", hash = "sha256:2cc24fb4cbe39633fb7badd9db9ca6295d766d9c2995f245725a46715d050f2a", size = 27661 } 88 | wheels = [ 89 | { url = "https://files.pythonhosted.org/packages/a4/07/14f8ad37f2d12a5ce41206c21820d8cb6561b728e51fad4530dff0552a67/cachetools-5.5.0-py3-none-any.whl", hash = "sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292", size = 9524 }, 90 | ] 91 | 92 | [[package]] 93 | name = "celery" 94 | version = "5.4.0" 95 | source = { registry = "https://pypi.org/simple" } 96 | dependencies = [ 97 | { name = "billiard" }, 98 | { name = "click" }, 99 | { name = "click-didyoumean" }, 100 | { name = "click-plugins" }, 101 | { name = "click-repl" }, 102 | { name = "kombu" }, 103 | { name = "python-dateutil" }, 104 | { name = "tzdata" }, 105 | { name = "vine" }, 106 | ] 107 | sdist = { url = "https://files.pythonhosted.org/packages/8a/9c/cf0bce2cc1c8971bf56629d8f180e4ca35612c7e79e6e432e785261a8be4/celery-5.4.0.tar.gz", hash = "sha256:504a19140e8d3029d5acad88330c541d4c3f64c789d85f94756762d8bca7e706", size = 1575692 } 108 | wheels = [ 109 | { url = "https://files.pythonhosted.org/packages/90/c4/6a4d3772e5407622feb93dd25c86ce3c0fee746fa822a777a627d56b4f2a/celery-5.4.0-py3-none-any.whl", hash = "sha256:369631eb580cf8c51a82721ec538684994f8277637edde2dfc0dacd73ed97f64", size = 425983 }, 110 | ] 111 | 112 | [[package]] 113 | name = "celery-types" 114 | version = "0.22.0" 115 | source = { registry = "https://pypi.org/simple" } 116 | dependencies = [ 117 | { name = "typing-extensions" }, 118 | ] 119 | sdist = { url = "https://files.pythonhosted.org/packages/57/0a/f7d6089e39b43528d74f99f3f58b9900fe76894e8208ec4f22ffa71e4a73/celery_types-0.22.0.tar.gz", hash = "sha256:0ecad2fa5a6eded0a1f919e5e1e381cc2ff0635fe4b21db53b4661b6876d5b30", size = 26654 } 120 | wheels = [ 121 | { url = "https://files.pythonhosted.org/packages/b4/fc/ab9ed137f6a7a54746cb27410e475f6b375dbb9e20f8c2d3317186d0a63e/celery_types-0.22.0-py3-none-any.whl", hash = "sha256:79a66637d1d6af5992d1dc80259d9538869941325e966006f1e795220519b9ac", size = 41166 }, 122 | ] 123 | 124 | [[package]] 125 | name = "certifi" 126 | version = "2024.7.4" 127 | source = { registry = "https://pypi.org/simple" } 128 | sdist = { url = "https://files.pythonhosted.org/packages/c2/02/a95f2b11e207f68bc64d7aae9666fed2e2b3f307748d5123dffb72a1bbea/certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b", size = 164065 } 129 | wheels = [ 130 | { url = "https://files.pythonhosted.org/packages/1c/d5/c84e1a17bf61d4df64ca866a1c9a913874b4e9bdc131ec689a0ad013fb36/certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90", size = 162960 }, 131 | ] 132 | 133 | [[package]] 134 | name = "cfgv" 135 | version = "3.4.0" 136 | source = { registry = "https://pypi.org/simple" } 137 | sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114 } 138 | wheels = [ 139 | { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249 }, 140 | ] 141 | 142 | [[package]] 143 | name = "click" 144 | version = "8.1.7" 145 | source = { registry = "https://pypi.org/simple" } 146 | dependencies = [ 147 | { name = "colorama", marker = "platform_system == 'Windows'" }, 148 | ] 149 | sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 } 150 | wheels = [ 151 | { url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941 }, 152 | ] 153 | 154 | [[package]] 155 | name = "click-didyoumean" 156 | version = "0.3.1" 157 | source = { registry = "https://pypi.org/simple" } 158 | dependencies = [ 159 | { name = "click" }, 160 | ] 161 | sdist = { url = "https://files.pythonhosted.org/packages/30/ce/217289b77c590ea1e7c24242d9ddd6e249e52c795ff10fac2c50062c48cb/click_didyoumean-0.3.1.tar.gz", hash = "sha256:4f82fdff0dbe64ef8ab2279bd6aa3f6a99c3b28c05aa09cbfc07c9d7fbb5a463", size = 3089 } 162 | wheels = [ 163 | { url = "https://files.pythonhosted.org/packages/1b/5b/974430b5ffdb7a4f1941d13d83c64a0395114503cc357c6b9ae4ce5047ed/click_didyoumean-0.3.1-py3-none-any.whl", hash = "sha256:5c4bb6007cfea5f2fd6583a2fb6701a22a41eb98957e63d0fac41c10e7c3117c", size = 3631 }, 164 | ] 165 | 166 | [[package]] 167 | name = "click-plugins" 168 | version = "1.1.1" 169 | source = { registry = "https://pypi.org/simple" } 170 | dependencies = [ 171 | { name = "click" }, 172 | ] 173 | sdist = { url = "https://files.pythonhosted.org/packages/5f/1d/45434f64ed749540af821fd7e42b8e4d23ac04b1eda7c26613288d6cd8a8/click-plugins-1.1.1.tar.gz", hash = "sha256:46ab999744a9d831159c3411bb0c79346d94a444df9a3a3742e9ed63645f264b", size = 8164 } 174 | wheels = [ 175 | { url = "https://files.pythonhosted.org/packages/e9/da/824b92d9942f4e472702488857914bdd50f73021efea15b4cad9aca8ecef/click_plugins-1.1.1-py2.py3-none-any.whl", hash = "sha256:5d262006d3222f5057fd81e1623d4443e41dcda5dc815c06b442aa3c02889fc8", size = 7497 }, 176 | ] 177 | 178 | [[package]] 179 | name = "click-repl" 180 | version = "0.3.0" 181 | source = { registry = "https://pypi.org/simple" } 182 | dependencies = [ 183 | { name = "click" }, 184 | { name = "prompt-toolkit" }, 185 | ] 186 | sdist = { url = "https://files.pythonhosted.org/packages/cb/a2/57f4ac79838cfae6912f997b4d1a64a858fb0c86d7fcaae6f7b58d267fca/click-repl-0.3.0.tar.gz", hash = "sha256:17849c23dba3d667247dc4defe1757fff98694e90fe37474f3feebb69ced26a9", size = 10449 } 187 | wheels = [ 188 | { url = "https://files.pythonhosted.org/packages/52/40/9d857001228658f0d59e97ebd4c346fe73e138c6de1bce61dc568a57c7f8/click_repl-0.3.0-py3-none-any.whl", hash = "sha256:fb7e06deb8da8de86180a33a9da97ac316751c094c6899382da7feeeeb51b812", size = 10289 }, 189 | ] 190 | 191 | [[package]] 192 | name = "colorama" 193 | version = "0.4.6" 194 | source = { registry = "https://pypi.org/simple" } 195 | sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } 196 | wheels = [ 197 | { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, 198 | ] 199 | 200 | [[package]] 201 | name = "colorlog" 202 | version = "6.8.2" 203 | source = { registry = "https://pypi.org/simple" } 204 | dependencies = [ 205 | { name = "colorama", marker = "sys_platform == 'win32'" }, 206 | ] 207 | sdist = { url = "https://files.pythonhosted.org/packages/db/38/2992ff192eaa7dd5a793f8b6570d6bbe887c4fbbf7e72702eb0a693a01c8/colorlog-6.8.2.tar.gz", hash = "sha256:3e3e079a41feb5a1b64f978b5ea4f46040a94f11f0e8bbb8261e3dbbeca64d44", size = 16529 } 208 | wheels = [ 209 | { url = "https://files.pythonhosted.org/packages/f3/18/3e867ab37a24fdf073c1617b9c7830e06ec270b1ea4694a624038fc40a03/colorlog-6.8.2-py3-none-any.whl", hash = "sha256:4dcbb62368e2800cb3c5abd348da7e53f6c362dda502ec27c560b2e58a66bd33", size = 11357 }, 210 | ] 211 | 212 | [[package]] 213 | name = "coverage" 214 | version = "7.6.1" 215 | source = { registry = "https://pypi.org/simple" } 216 | sdist = { url = "https://files.pythonhosted.org/packages/f7/08/7e37f82e4d1aead42a7443ff06a1e406aabf7302c4f00a546e4b320b994c/coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d", size = 798791 } 217 | wheels = [ 218 | { url = "https://files.pythonhosted.org/packages/7e/d4/300fc921dff243cd518c7db3a4c614b7e4b2431b0d1145c1e274fd99bd70/coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778", size = 206983 }, 219 | { url = "https://files.pythonhosted.org/packages/e1/ab/6bf00de5327ecb8db205f9ae596885417a31535eeda6e7b99463108782e1/coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391", size = 207221 }, 220 | { url = "https://files.pythonhosted.org/packages/92/8f/2ead05e735022d1a7f3a0a683ac7f737de14850395a826192f0288703472/coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8", size = 240342 }, 221 | { url = "https://files.pythonhosted.org/packages/0f/ef/94043e478201ffa85b8ae2d2c79b4081e5a1b73438aafafccf3e9bafb6b5/coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d", size = 237371 }, 222 | { url = "https://files.pythonhosted.org/packages/1f/0f/c890339dd605f3ebc269543247bdd43b703cce6825b5ed42ff5f2d6122c7/coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca", size = 239455 }, 223 | { url = "https://files.pythonhosted.org/packages/d1/04/7fd7b39ec7372a04efb0f70c70e35857a99b6a9188b5205efb4c77d6a57a/coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163", size = 238924 }, 224 | { url = "https://files.pythonhosted.org/packages/ed/bf/73ce346a9d32a09cf369f14d2a06651329c984e106f5992c89579d25b27e/coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a", size = 237252 }, 225 | { url = "https://files.pythonhosted.org/packages/86/74/1dc7a20969725e917b1e07fe71a955eb34bc606b938316bcc799f228374b/coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d", size = 238897 }, 226 | { url = "https://files.pythonhosted.org/packages/b6/e9/d9cc3deceb361c491b81005c668578b0dfa51eed02cd081620e9a62f24ec/coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5", size = 209606 }, 227 | { url = "https://files.pythonhosted.org/packages/47/c8/5a2e41922ea6740f77d555c4d47544acd7dc3f251fe14199c09c0f5958d3/coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb", size = 210373 }, 228 | { url = "https://files.pythonhosted.org/packages/8c/f9/9aa4dfb751cb01c949c990d136a0f92027fbcc5781c6e921df1cb1563f20/coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106", size = 207007 }, 229 | { url = "https://files.pythonhosted.org/packages/b9/67/e1413d5a8591622a46dd04ff80873b04c849268831ed5c304c16433e7e30/coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9", size = 207269 }, 230 | { url = "https://files.pythonhosted.org/packages/14/5b/9dec847b305e44a5634d0fb8498d135ab1d88330482b74065fcec0622224/coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c", size = 239886 }, 231 | { url = "https://files.pythonhosted.org/packages/7b/b7/35760a67c168e29f454928f51f970342d23cf75a2bb0323e0f07334c85f3/coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a", size = 237037 }, 232 | { url = "https://files.pythonhosted.org/packages/f7/95/d2fd31f1d638df806cae59d7daea5abf2b15b5234016a5ebb502c2f3f7ee/coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060", size = 239038 }, 233 | { url = "https://files.pythonhosted.org/packages/6e/bd/110689ff5752b67924efd5e2aedf5190cbbe245fc81b8dec1abaffba619d/coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862", size = 238690 }, 234 | { url = "https://files.pythonhosted.org/packages/d3/a8/08d7b38e6ff8df52331c83130d0ab92d9c9a8b5462f9e99c9f051a4ae206/coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388", size = 236765 }, 235 | { url = "https://files.pythonhosted.org/packages/d6/6a/9cf96839d3147d55ae713eb2d877f4d777e7dc5ba2bce227167d0118dfe8/coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155", size = 238611 }, 236 | { url = "https://files.pythonhosted.org/packages/74/e4/7ff20d6a0b59eeaab40b3140a71e38cf52547ba21dbcf1d79c5a32bba61b/coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a", size = 209671 }, 237 | { url = "https://files.pythonhosted.org/packages/35/59/1812f08a85b57c9fdb6d0b383d779e47b6f643bc278ed682859512517e83/coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129", size = 210368 }, 238 | { url = "https://files.pythonhosted.org/packages/9c/15/08913be1c59d7562a3e39fce20661a98c0a3f59d5754312899acc6cb8a2d/coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e", size = 207758 }, 239 | { url = "https://files.pythonhosted.org/packages/c4/ae/b5d58dff26cade02ada6ca612a76447acd69dccdbb3a478e9e088eb3d4b9/coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962", size = 208035 }, 240 | { url = "https://files.pythonhosted.org/packages/b8/d7/62095e355ec0613b08dfb19206ce3033a0eedb6f4a67af5ed267a8800642/coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb", size = 250839 }, 241 | { url = "https://files.pythonhosted.org/packages/7c/1e/c2967cb7991b112ba3766df0d9c21de46b476d103e32bb401b1b2adf3380/coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704", size = 246569 }, 242 | { url = "https://files.pythonhosted.org/packages/8b/61/a7a6a55dd266007ed3b1df7a3386a0d760d014542d72f7c2c6938483b7bd/coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b", size = 248927 }, 243 | { url = "https://files.pythonhosted.org/packages/c8/fa/13a6f56d72b429f56ef612eb3bc5ce1b75b7ee12864b3bd12526ab794847/coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f", size = 248401 }, 244 | { url = "https://files.pythonhosted.org/packages/75/06/0429c652aa0fb761fc60e8c6b291338c9173c6aa0f4e40e1902345b42830/coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223", size = 246301 }, 245 | { url = "https://files.pythonhosted.org/packages/52/76/1766bb8b803a88f93c3a2d07e30ffa359467810e5cbc68e375ebe6906efb/coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3", size = 247598 }, 246 | { url = "https://files.pythonhosted.org/packages/66/8b/f54f8db2ae17188be9566e8166ac6df105c1c611e25da755738025708d54/coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f", size = 210307 }, 247 | { url = "https://files.pythonhosted.org/packages/9f/b0/e0dca6da9170aefc07515cce067b97178cefafb512d00a87a1c717d2efd5/coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657", size = 211453 }, 248 | ] 249 | 250 | [[package]] 251 | name = "distlib" 252 | version = "0.3.8" 253 | source = { registry = "https://pypi.org/simple" } 254 | sdist = { url = "https://files.pythonhosted.org/packages/c4/91/e2df406fb4efacdf46871c25cde65d3c6ee5e173b7e5a4547a47bae91920/distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64", size = 609931 } 255 | wheels = [ 256 | { url = "https://files.pythonhosted.org/packages/8e/41/9307e4f5f9976bc8b7fea0b66367734e8faf3ec84bc0d412d8cfabbb66cd/distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784", size = 468850 }, 257 | ] 258 | 259 | [[package]] 260 | name = "dj-database-url" 261 | version = "2.2.0" 262 | source = { registry = "https://pypi.org/simple" } 263 | dependencies = [ 264 | { name = "django" }, 265 | { name = "typing-extensions" }, 266 | ] 267 | sdist = { url = "https://files.pythonhosted.org/packages/1a/48/51f398a47c197f584b3445de886986ddc40de18bdb6e168f325a8d2c7bca/dj_database_url-2.2.0.tar.gz", hash = "sha256:9f9b05058ddf888f1e6f840048b8d705ff9395e3b52a07165daa3d8b9360551b", size = 10874 } 268 | wheels = [ 269 | { url = "https://files.pythonhosted.org/packages/6f/9a/13f173c716d07283661e821f7e1624d0904835151b4f099687455dbef81e/dj_database_url-2.2.0-py3-none-any.whl", hash = "sha256:3e792567b0aa9a4884860af05fe2aa4968071ad351e033b6db632f97ac6db9de", size = 7764 }, 270 | ] 271 | 272 | [[package]] 273 | name = "django" 274 | version = "5.1" 275 | source = { registry = "https://pypi.org/simple" } 276 | dependencies = [ 277 | { name = "asgiref" }, 278 | { name = "sqlparse" }, 279 | { name = "tzdata", marker = "sys_platform == 'win32'" }, 280 | ] 281 | sdist = { url = "https://files.pythonhosted.org/packages/1e/0c/d854d25bb74a8a3b41e642bbd27fe6af12fadd0edfd07d487809cf0ef719/Django-5.1.tar.gz", hash = "sha256:848a5980e8efb76eea70872fb0e4bc5e371619c70fffbe48e3e1b50b2c09455d", size = 10681050 } 282 | wheels = [ 283 | { url = "https://files.pythonhosted.org/packages/28/b4/110532cebfea2244d76119904da98c6fa045ebb202aee9ec7cbf36ea3cad/Django-5.1-py3-none-any.whl", hash = "sha256:d3b811bf5371a26def053d7ee42a9df1267ef7622323fe70a601936725aa4557", size = 8246099 }, 284 | ] 285 | 286 | [[package]] 287 | name = "django-axes" 288 | version = "6.5.1" 289 | source = { registry = "https://pypi.org/simple" } 290 | dependencies = [ 291 | { name = "asgiref" }, 292 | { name = "django" }, 293 | ] 294 | sdist = { url = "https://files.pythonhosted.org/packages/6e/a5/31721dc9777fe7f01b4bd710f93d031ff03603b960bc282c53edd5578bf2/django_axes-6.5.1.tar.gz", hash = "sha256:d57f0fc95d581a602c642b3fe5bc31488b9401bd7441f3bec1fef0e599028499", size = 246679 } 295 | wheels = [ 296 | { url = "https://files.pythonhosted.org/packages/96/39/316e4b5a4c931698480953ea5f43df0657f8c47b9e981cfc331b8ed9eef5/django_axes-6.5.1-py3-none-any.whl", hash = "sha256:7435068cc8523bfa3f34faa62bb3a772b76d00925c3ff54aef43e4316e74bf05", size = 68409 }, 297 | ] 298 | 299 | [package.optional-dependencies] 300 | ipware = [ 301 | { name = "django-ipware" }, 302 | ] 303 | 304 | [[package]] 305 | name = "django-cors-headers" 306 | version = "4.4.0" 307 | source = { registry = "https://pypi.org/simple" } 308 | dependencies = [ 309 | { name = "asgiref" }, 310 | { name = "django" }, 311 | ] 312 | sdist = { url = "https://files.pythonhosted.org/packages/d3/34/f0c7a7241f885cbfc99b1edef0acc7915dd7a3fb749fe27de5e8a9fb2ccb/django_cors_headers-4.4.0.tar.gz", hash = "sha256:92cf4633e22af67a230a1456cb1b7a02bb213d6536d2dcb2a4a24092ea9cebc2", size = 21151 } 313 | wheels = [ 314 | { url = "https://files.pythonhosted.org/packages/9d/0c/4201d5650199b3a36ef3f2ab91f44c4527a70685f3003ce9f3ed8c30780c/django_cors_headers-4.4.0-py3-none-any.whl", hash = "sha256:5c6e3b7fe870876a1efdfeb4f433782c3524078fa0dc9e0195f6706ce7a242f6", size = 12789 }, 315 | ] 316 | 317 | [[package]] 318 | name = "django-ipware" 319 | version = "7.0.1" 320 | source = { registry = "https://pypi.org/simple" } 321 | dependencies = [ 322 | { name = "python-ipware" }, 323 | ] 324 | sdist = { url = "https://files.pythonhosted.org/packages/23/64/c7e4791edf01ba483cce444770b3e6a930ba12195ba1eeb37b5bf6dce8a8/django-ipware-7.0.1.tar.gz", hash = "sha256:d9ec43d2bf7cdf216fed8d494a084deb5761a54860a53b2e74346a4f384cff47", size = 6827 } 325 | wheels = [ 326 | { url = "https://files.pythonhosted.org/packages/11/33/bf539925b102d68200da5b1d3eacb8aa5d5d9a065972e8b8724d0d53bb0d/django_ipware-7.0.1-py2.py3-none-any.whl", hash = "sha256:db16bbee920f661ae7f678e4270460c85850f03c6761a4eaeb489bdc91f64709", size = 6425 }, 327 | ] 328 | 329 | [[package]] 330 | name = "django-redis" 331 | version = "5.4.0" 332 | source = { registry = "https://pypi.org/simple" } 333 | dependencies = [ 334 | { name = "django" }, 335 | { name = "redis" }, 336 | ] 337 | sdist = { url = "https://files.pythonhosted.org/packages/83/9d/2272742fdd9d0a9f0b28cd995b0539430c9467a2192e4de2cea9ea6ad38c/django-redis-5.4.0.tar.gz", hash = "sha256:6a02abaa34b0fea8bf9b707d2c363ab6adc7409950b2db93602e6cb292818c42", size = 52567 } 338 | wheels = [ 339 | { url = "https://files.pythonhosted.org/packages/b7/f1/63caad7c9222c26a62082f4f777de26389233b7574629996098bf6d25a4d/django_redis-5.4.0-py3-none-any.whl", hash = "sha256:ebc88df7da810732e2af9987f7f426c96204bf89319df4c6da6ca9a2942edd5b", size = 31119 }, 340 | ] 341 | 342 | [[package]] 343 | name = "django-silk" 344 | version = "5.2.0" 345 | source = { registry = "https://pypi.org/simple" } 346 | dependencies = [ 347 | { name = "autopep8" }, 348 | { name = "django" }, 349 | { name = "gprof2dot" }, 350 | { name = "sqlparse" }, 351 | ] 352 | sdist = { url = "https://files.pythonhosted.org/packages/0e/94/0ae2ec9c00e68f002d3f8a6365442d250ddf1522f7d386597dbb9076b6e7/django_silk-5.2.0.tar.gz", hash = "sha256:39ddeda80469d5495d1cbb53590a9bdd4ce30a7474dc16ac26b6cbc0d9521f50", size = 4394287 } 353 | wheels = [ 354 | { url = "https://files.pythonhosted.org/packages/b7/5f/16010e42a8982a5977bb7395bad04e8c59874a15a9d1e2e4f9306be796aa/django_silk-5.2.0-py3-none-any.whl", hash = "sha256:b3f01ccbf46611073603a6ac2b84f578bde978ad44785f42994c3d6f81398fdc", size = 1840272 }, 355 | ] 356 | 357 | [[package]] 358 | name = "django-split-settings" 359 | version = "1.3.2" 360 | source = { registry = "https://pypi.org/simple" } 361 | sdist = { url = "https://files.pythonhosted.org/packages/de/b9/1c13089454afd7a42d492b8aa8a0c7e49eeca58c0f2ad331f361a067c876/django_split_settings-1.3.2.tar.gz", hash = "sha256:d3975aa3601e37f65c59b9977b6bcb1de8bc27496930054078589c7d56998a9d", size = 5751 } 362 | wheels = [ 363 | { url = "https://files.pythonhosted.org/packages/63/69/d94db8dac55bcfb6b3243578a3096cfda6c42ea5da292c36919768152ec6/django_split_settings-1.3.2-py3-none-any.whl", hash = "sha256:72bd7dd9f12602585681074d1f859643fb4f6b196b584688fab86bdd73a57dff", size = 6435 }, 364 | ] 365 | 366 | [[package]] 367 | name = "django-storages" 368 | version = "1.14.4" 369 | source = { registry = "https://pypi.org/simple" } 370 | dependencies = [ 371 | { name = "django" }, 372 | ] 373 | sdist = { url = "https://files.pythonhosted.org/packages/6d/88/77b5ede11147941dece064bd80ac618f6a4b91b4c9d5d305a2660014941e/django-storages-1.14.4.tar.gz", hash = "sha256:69aca94d26e6714d14ad63f33d13619e697508ee33ede184e462ed766dc2a73f", size = 83496 } 374 | wheels = [ 375 | { url = "https://files.pythonhosted.org/packages/e0/69/a4b2f2dfa51fd18fd898e10cc41d73a30965da7cb3b683f1375b1dc3dd5c/django_storages-1.14.4-py3-none-any.whl", hash = "sha256:d61930acb4a25e3aebebc6addaf946a3b1df31c803a6bf1af2f31c9047febaa3", size = 31809 }, 376 | ] 377 | 378 | [[package]] 379 | name = "djangorestframework" 380 | version = "3.15.2" 381 | source = { registry = "https://pypi.org/simple" } 382 | dependencies = [ 383 | { name = "django" }, 384 | ] 385 | sdist = { url = "https://files.pythonhosted.org/packages/2c/ce/31482eb688bdb4e271027076199e1aa8d02507e530b6d272ab8b4481557c/djangorestframework-3.15.2.tar.gz", hash = "sha256:36fe88cd2d6c6bec23dca9804bab2ba5517a8bb9d8f47ebc68981b56840107ad", size = 1067420 } 386 | wheels = [ 387 | { url = "https://files.pythonhosted.org/packages/7c/b6/fa99d8f05eff3a9310286ae84c4059b08c301ae4ab33ae32e46e8ef76491/djangorestframework-3.15.2-py3-none-any.whl", hash = "sha256:2b8871b062ba1aefc2de01f773875441a961fefbf79f5eed1e32b2f096944b20", size = 1071235 }, 388 | ] 389 | 390 | [[package]] 391 | name = "djangorestframework-simplejwt" 392 | version = "5.3.1" 393 | source = { registry = "https://pypi.org/simple" } 394 | dependencies = [ 395 | { name = "django" }, 396 | { name = "djangorestframework" }, 397 | { name = "pyjwt" }, 398 | ] 399 | sdist = { url = "https://files.pythonhosted.org/packages/ac/f3/f2ce06fcd1c53e12b26cc5a3ec9e0acd47eb4be02e1d24de50edee5c5abf/djangorestframework_simplejwt-5.3.1.tar.gz", hash = "sha256:6c4bd37537440bc439564ebf7d6085e74c5411485197073f508ebdfa34bc9fae", size = 94266 } 400 | wheels = [ 401 | { url = "https://files.pythonhosted.org/packages/f2/ab/88f73cf08d2ad3fb9f71b956dceca5680a57f121e5ce9a604f365877d57e/djangorestframework_simplejwt-5.3.1-py3-none-any.whl", hash = "sha256:381bc966aa46913905629d472cd72ad45faa265509764e20ffd440164c88d220", size = 101339 }, 402 | ] 403 | 404 | [[package]] 405 | name = "drf-spectacular" 406 | version = "0.27.2" 407 | source = { registry = "https://pypi.org/simple" } 408 | dependencies = [ 409 | { name = "django" }, 410 | { name = "djangorestframework" }, 411 | { name = "inflection" }, 412 | { name = "jsonschema" }, 413 | { name = "pyyaml" }, 414 | { name = "uritemplate" }, 415 | ] 416 | sdist = { url = "https://files.pythonhosted.org/packages/b1/c9/55311dcbdae9a437eeb8f898f8421a6a3eabf08725c23ddf458cf2479b78/drf-spectacular-0.27.2.tar.gz", hash = "sha256:a199492f2163c4101055075ebdbb037d59c6e0030692fc83a1a8c0fc65929981", size = 235131 } 417 | wheels = [ 418 | { url = "https://files.pythonhosted.org/packages/b2/cd/84c44a5d435f6544e58a9b138305f59bca232157ae4ecb658f9787f87d1c/drf_spectacular-0.27.2-py3-none-any.whl", hash = "sha256:b1c04bf8b2fbbeaf6f59414b4ea448c8787aba4d32f76055c3b13335cf7ec37b", size = 102930 }, 419 | ] 420 | 421 | [[package]] 422 | name = "filelock" 423 | version = "3.15.4" 424 | source = { registry = "https://pypi.org/simple" } 425 | sdist = { url = "https://files.pythonhosted.org/packages/08/dd/49e06f09b6645156550fb9aee9cc1e59aba7efbc972d665a1bd6ae0435d4/filelock-3.15.4.tar.gz", hash = "sha256:2207938cbc1844345cb01a5a95524dae30f0ce089eba5b00378295a17e3e90cb", size = 18007 } 426 | wheels = [ 427 | { url = "https://files.pythonhosted.org/packages/ae/f0/48285f0262fe47103a4a45972ed2f9b93e4c80b8fd609fa98da78b2a5706/filelock-3.15.4-py3-none-any.whl", hash = "sha256:6ca1fffae96225dab4c6eaf1c4f4f28cd2568d3ec2a44e15a08520504de468e7", size = 16159 }, 428 | ] 429 | 430 | [[package]] 431 | name = "gprof2dot" 432 | version = "2024.6.6" 433 | source = { registry = "https://pypi.org/simple" } 434 | sdist = { url = "https://files.pythonhosted.org/packages/32/11/16fc5b985741378812223f2c6213b0a95cda333b797def622ac702d28e81/gprof2dot-2024.6.6.tar.gz", hash = "sha256:fa1420c60025a9eb7734f65225b4da02a10fc6dd741b37fa129bc6b41951e5ab", size = 36536 } 435 | wheels = [ 436 | { url = "https://files.pythonhosted.org/packages/ae/27/15c4d20871a86281e2bacde9e9f634225d1c2ed0db072f98acf201022411/gprof2dot-2024.6.6-py2.py3-none-any.whl", hash = "sha256:45b14ad7ce64e299c8f526881007b9eb2c6b75505d5613e96e66ee4d5ab33696", size = 34763 }, 437 | ] 438 | 439 | [[package]] 440 | name = "gunicorn" 441 | version = "23.0.0" 442 | source = { registry = "https://pypi.org/simple" } 443 | dependencies = [ 444 | { name = "packaging" }, 445 | ] 446 | sdist = { url = "https://files.pythonhosted.org/packages/34/72/9614c465dc206155d93eff0ca20d42e1e35afc533971379482de953521a4/gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec", size = 375031 } 447 | wheels = [ 448 | { url = "https://files.pythonhosted.org/packages/cb/7d/6dac2a6e1eba33ee43f318edbed4ff29151a49b5d37f080aad1e6469bca4/gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", size = 85029 }, 449 | ] 450 | 451 | [[package]] 452 | name = "identify" 453 | version = "2.6.0" 454 | source = { registry = "https://pypi.org/simple" } 455 | sdist = { url = "https://files.pythonhosted.org/packages/32/f4/8e8f7db397a7ce20fbdeac5f25adaf567fc362472432938d25556008e03a/identify-2.6.0.tar.gz", hash = "sha256:cb171c685bdc31bcc4c1734698736a7d5b6c8bf2e0c15117f4d469c8640ae5cf", size = 99116 } 456 | wheels = [ 457 | { url = "https://files.pythonhosted.org/packages/24/6c/a4f39abe7f19600b74528d0c717b52fff0b300bb0161081510d39c53cb00/identify-2.6.0-py2.py3-none-any.whl", hash = "sha256:e79ae4406387a9d300332b5fd366d8994f1525e8414984e1a59e058b2eda2dd0", size = 98962 }, 458 | ] 459 | 460 | [[package]] 461 | name = "inflection" 462 | version = "0.5.1" 463 | source = { registry = "https://pypi.org/simple" } 464 | sdist = { url = "https://files.pythonhosted.org/packages/e1/7e/691d061b7329bc8d54edbf0ec22fbfb2afe61facb681f9aaa9bff7a27d04/inflection-0.5.1.tar.gz", hash = "sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417", size = 15091 } 465 | wheels = [ 466 | { url = "https://files.pythonhosted.org/packages/59/91/aa6bde563e0085a02a435aa99b49ef75b0a4b062635e606dab23ce18d720/inflection-0.5.1-py2.py3-none-any.whl", hash = "sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2", size = 9454 }, 467 | ] 468 | 469 | [[package]] 470 | name = "iniconfig" 471 | version = "2.0.0" 472 | source = { registry = "https://pypi.org/simple" } 473 | sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } 474 | wheels = [ 475 | { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, 476 | ] 477 | 478 | [[package]] 479 | name = "jmespath" 480 | version = "1.0.1" 481 | source = { registry = "https://pypi.org/simple" } 482 | sdist = { url = "https://files.pythonhosted.org/packages/00/2a/e867e8531cf3e36b41201936b7fa7ba7b5702dbef42922193f05c8976cd6/jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe", size = 25843 } 483 | wheels = [ 484 | { url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", size = 20256 }, 485 | ] 486 | 487 | [[package]] 488 | name = "jsonschema" 489 | version = "4.23.0" 490 | source = { registry = "https://pypi.org/simple" } 491 | dependencies = [ 492 | { name = "attrs" }, 493 | { name = "jsonschema-specifications" }, 494 | { name = "referencing" }, 495 | { name = "rpds-py" }, 496 | ] 497 | sdist = { url = "https://files.pythonhosted.org/packages/38/2e/03362ee4034a4c917f697890ccd4aec0800ccf9ded7f511971c75451deec/jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4", size = 325778 } 498 | wheels = [ 499 | { url = "https://files.pythonhosted.org/packages/69/4a/4f9dbeb84e8850557c02365a0eee0649abe5eb1d84af92a25731c6c0f922/jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566", size = 88462 }, 500 | ] 501 | 502 | [[package]] 503 | name = "jsonschema-specifications" 504 | version = "2023.12.1" 505 | source = { registry = "https://pypi.org/simple" } 506 | dependencies = [ 507 | { name = "referencing" }, 508 | ] 509 | sdist = { url = "https://files.pythonhosted.org/packages/f8/b9/cc0cc592e7c195fb8a650c1d5990b10175cf13b4c97465c72ec841de9e4b/jsonschema_specifications-2023.12.1.tar.gz", hash = "sha256:48a76787b3e70f5ed53f1160d2b81f586e4ca6d1548c5de7085d1682674764cc", size = 13983 } 510 | wheels = [ 511 | { url = "https://files.pythonhosted.org/packages/ee/07/44bd408781594c4d0a027666ef27fab1e441b109dc3b76b4f836f8fd04fe/jsonschema_specifications-2023.12.1-py3-none-any.whl", hash = "sha256:87e4fdf3a94858b8a2ba2778d9ba57d8a9cafca7c7489c46ba0d30a8bc6a9c3c", size = 18482 }, 512 | ] 513 | 514 | [[package]] 515 | name = "kombu" 516 | version = "5.4.0" 517 | source = { registry = "https://pypi.org/simple" } 518 | dependencies = [ 519 | { name = "amqp" }, 520 | { name = "vine" }, 521 | ] 522 | sdist = { url = "https://files.pythonhosted.org/packages/b6/f4/d3e57b1c351bb47ce25b16e1cf6ea05df4613dbe56e3cf32ea80df1a8b4d/kombu-5.4.0.tar.gz", hash = "sha256:ad200a8dbdaaa2bbc5f26d2ee7d707d9a1fded353a0f4bd751ce8c7d9f449c60", size = 442120 } 523 | wheels = [ 524 | { url = "https://files.pythonhosted.org/packages/df/17/34f8ec5b9d46a1ddb598b7bf8f779c567421d05cd73742d09e549254c782/kombu-5.4.0-py3-none-any.whl", hash = "sha256:c8dd99820467610b4febbc7a9e8a0d3d7da2d35116b67184418b51cc520ea6b6", size = 200870 }, 525 | ] 526 | 527 | [[package]] 528 | name = "mypy" 529 | version = "1.11.1" 530 | source = { registry = "https://pypi.org/simple" } 531 | dependencies = [ 532 | { name = "mypy-extensions" }, 533 | { name = "typing-extensions" }, 534 | ] 535 | sdist = { url = "https://files.pythonhosted.org/packages/b6/9c/a4b3bda53823439cf395db8ecdda6229a83f9bf201714a68a15190bb2919/mypy-1.11.1.tar.gz", hash = "sha256:f404a0b069709f18bbdb702eb3dcfe51910602995de00bd39cea3050b5772d08", size = 3078369 } 536 | wheels = [ 537 | { url = "https://files.pythonhosted.org/packages/3a/34/69638cee2e87303f19a0c35e80d42757e14d9aba328f272fdcdc0bf3c9b8/mypy-1.11.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f39918a50f74dc5969807dcfaecafa804fa7f90c9d60506835036cc1bc891dc8", size = 10995789 }, 538 | { url = "https://files.pythonhosted.org/packages/c4/3c/3e0611348fc53a4a7c80485959478b4f6eae706baf3b7c03cafa22639216/mypy-1.11.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0bc71d1fb27a428139dd78621953effe0d208aed9857cb08d002280b0422003a", size = 10002696 }, 539 | { url = "https://files.pythonhosted.org/packages/1c/21/a6b46c91b4c9d1918ee59c305f46850cde7cbea748635a352e7c3c8ed204/mypy-1.11.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b868d3bcff720dd7217c383474008ddabaf048fad8d78ed948bb4b624870a417", size = 12505772 }, 540 | { url = "https://files.pythonhosted.org/packages/c4/55/07904d4c8f408e70308015edcbff067eaa77514475938a9dd81b063de2a8/mypy-1.11.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a707ec1527ffcdd1c784d0924bf5cb15cd7f22683b919668a04d2b9c34549d2e", size = 12954190 }, 541 | { url = "https://files.pythonhosted.org/packages/1e/b7/3a50f318979c8c541428c2f1ee973cda813bcc89614de982dafdd0df2b3e/mypy-1.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:64f4a90e3ea07f590c5bcf9029035cf0efeae5ba8be511a8caada1a4893f5525", size = 9663138 }, 542 | { url = "https://files.pythonhosted.org/packages/f8/d4/4960d0df55f30a7625d9c3c9414dfd42f779caabae137ef73ffaed0c97b9/mypy-1.11.1-py3-none-any.whl", hash = "sha256:0624bdb940255d2dd24e829d99a13cfeb72e4e9031f9492148f410ed30bcab54", size = 2619257 }, 543 | ] 544 | 545 | [[package]] 546 | name = "mypy-extensions" 547 | version = "1.0.0" 548 | source = { registry = "https://pypi.org/simple" } 549 | sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 } 550 | wheels = [ 551 | { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, 552 | ] 553 | 554 | [[package]] 555 | name = "nodeenv" 556 | version = "1.9.1" 557 | source = { registry = "https://pypi.org/simple" } 558 | sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 } 559 | wheels = [ 560 | { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, 561 | ] 562 | 563 | [[package]] 564 | name = "packaging" 565 | version = "24.1" 566 | source = { registry = "https://pypi.org/simple" } 567 | sdist = { url = "https://files.pythonhosted.org/packages/51/65/50db4dda066951078f0a96cf12f4b9ada6e4b811516bf0262c0f4f7064d4/packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", size = 148788 } 568 | wheels = [ 569 | { url = "https://files.pythonhosted.org/packages/08/aa/cc0199a5f0ad350994d660967a8efb233fe0416e4639146c089643407ce6/packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124", size = 53985 }, 570 | ] 571 | 572 | [[package]] 573 | name = "platformdirs" 574 | version = "4.2.2" 575 | source = { registry = "https://pypi.org/simple" } 576 | sdist = { url = "https://files.pythonhosted.org/packages/f5/52/0763d1d976d5c262df53ddda8d8d4719eedf9594d046f117c25a27261a19/platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3", size = 20916 } 577 | wheels = [ 578 | { url = "https://files.pythonhosted.org/packages/68/13/2aa1f0e1364feb2c9ef45302f387ac0bd81484e9c9a4c5688a322fbdfd08/platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee", size = 18146 }, 579 | ] 580 | 581 | [[package]] 582 | name = "pluggy" 583 | version = "1.5.0" 584 | source = { registry = "https://pypi.org/simple" } 585 | sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } 586 | wheels = [ 587 | { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, 588 | ] 589 | 590 | [[package]] 591 | name = "pre-commit" 592 | version = "3.8.0" 593 | source = { registry = "https://pypi.org/simple" } 594 | dependencies = [ 595 | { name = "cfgv" }, 596 | { name = "identify" }, 597 | { name = "nodeenv" }, 598 | { name = "pyyaml" }, 599 | { name = "virtualenv" }, 600 | ] 601 | sdist = { url = "https://files.pythonhosted.org/packages/64/10/97ee2fa54dff1e9da9badbc5e35d0bbaef0776271ea5907eccf64140f72f/pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af", size = 177815 } 602 | wheels = [ 603 | { url = "https://files.pythonhosted.org/packages/07/92/caae8c86e94681b42c246f0bca35c059a2f0529e5b92619f6aba4cf7e7b6/pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f", size = 204643 }, 604 | ] 605 | 606 | [[package]] 607 | name = "prompt-toolkit" 608 | version = "3.0.47" 609 | source = { registry = "https://pypi.org/simple" } 610 | dependencies = [ 611 | { name = "wcwidth" }, 612 | ] 613 | sdist = { url = "https://files.pythonhosted.org/packages/47/6d/0279b119dafc74c1220420028d490c4399b790fc1256998666e3a341879f/prompt_toolkit-3.0.47.tar.gz", hash = "sha256:1e1b29cb58080b1e69f207c893a1a7bf16d127a5c30c9d17a25a5d77792e5360", size = 425859 } 614 | wheels = [ 615 | { url = "https://files.pythonhosted.org/packages/e8/23/22750c4b768f09386d1c3cc4337953e8936f48a888fa6dddfb669b2c9088/prompt_toolkit-3.0.47-py3-none-any.whl", hash = "sha256:0d7bfa67001d5e39d02c224b663abc33687405033a8c422d0d675a5a13361d10", size = 386411 }, 616 | ] 617 | 618 | [[package]] 619 | name = "psycopg" 620 | version = "3.2.1" 621 | source = { registry = "https://pypi.org/simple" } 622 | dependencies = [ 623 | { name = "typing-extensions" }, 624 | { name = "tzdata", marker = "sys_platform == 'win32'" }, 625 | ] 626 | sdist = { url = "https://files.pythonhosted.org/packages/ff/8e/f176997fd790d3dce9fa0ca695391beaeee39af7ecd6d426c4c063cf6744/psycopg-3.2.1.tar.gz", hash = "sha256:dc8da6dc8729dacacda3cc2f17d2c9397a70a66cf0d2b69c91065d60d5f00cb7", size = 155313 } 627 | wheels = [ 628 | { url = "https://files.pythonhosted.org/packages/8a/0e/0f755db36f47f96464463385552f8f132a981731356837c9a30a11ab2d35/psycopg-3.2.1-py3-none-any.whl", hash = "sha256:ece385fb413a37db332f97c49208b36cf030ff02b199d7635ed2fbd378724175", size = 197743 }, 629 | ] 630 | 631 | [[package]] 632 | name = "pycodestyle" 633 | version = "2.12.1" 634 | source = { registry = "https://pypi.org/simple" } 635 | sdist = { url = "https://files.pythonhosted.org/packages/43/aa/210b2c9aedd8c1cbeea31a50e42050ad56187754b34eb214c46709445801/pycodestyle-2.12.1.tar.gz", hash = "sha256:6838eae08bbce4f6accd5d5572075c63626a15ee3e6f842df996bf62f6d73521", size = 39232 } 636 | wheels = [ 637 | { url = "https://files.pythonhosted.org/packages/3a/d8/a211b3f85e99a0daa2ddec96c949cac6824bd305b040571b82a03dd62636/pycodestyle-2.12.1-py2.py3-none-any.whl", hash = "sha256:46f0fb92069a7c28ab7bb558f05bfc0110dac69a0cd23c61ea0040283a9d78b3", size = 31284 }, 638 | ] 639 | 640 | [[package]] 641 | name = "pyjwt" 642 | version = "2.9.0" 643 | source = { registry = "https://pypi.org/simple" } 644 | sdist = { url = "https://files.pythonhosted.org/packages/fb/68/ce067f09fca4abeca8771fe667d89cc347d1e99da3e093112ac329c6020e/pyjwt-2.9.0.tar.gz", hash = "sha256:7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c", size = 78825 } 645 | wheels = [ 646 | { url = "https://files.pythonhosted.org/packages/79/84/0fdf9b18ba31d69877bd39c9cd6052b47f3761e9910c15de788e519f079f/PyJWT-2.9.0-py3-none-any.whl", hash = "sha256:3b02fb0f44517787776cf48f2ae25d8e14f300e6d7545a4315cee571a415e850", size = 22344 }, 647 | ] 648 | 649 | [[package]] 650 | name = "pytest" 651 | version = "8.3.2" 652 | source = { registry = "https://pypi.org/simple" } 653 | dependencies = [ 654 | { name = "colorama", marker = "sys_platform == 'win32'" }, 655 | { name = "iniconfig" }, 656 | { name = "packaging" }, 657 | { name = "pluggy" }, 658 | ] 659 | sdist = { url = "https://files.pythonhosted.org/packages/b4/8c/9862305bdcd6020bc7b45b1b5e7397a6caf1a33d3025b9a003b39075ffb2/pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce", size = 1439314 } 660 | wheels = [ 661 | { url = "https://files.pythonhosted.org/packages/0f/f9/cf155cf32ca7d6fa3601bc4c5dd19086af4b320b706919d48a4c79081cf9/pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5", size = 341802 }, 662 | ] 663 | 664 | [[package]] 665 | name = "pytest-cov" 666 | version = "5.0.0" 667 | source = { registry = "https://pypi.org/simple" } 668 | dependencies = [ 669 | { name = "coverage" }, 670 | { name = "pytest" }, 671 | ] 672 | sdist = { url = "https://files.pythonhosted.org/packages/74/67/00efc8d11b630c56f15f4ad9c7f9223f1e5ec275aaae3fa9118c6a223ad2/pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857", size = 63042 } 673 | wheels = [ 674 | { url = "https://files.pythonhosted.org/packages/78/3a/af5b4fa5961d9a1e6237b530eb87dd04aea6eb83da09d2a4073d81b54ccf/pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652", size = 21990 }, 675 | ] 676 | 677 | [[package]] 678 | name = "pytest-django" 679 | version = "4.8.0" 680 | source = { registry = "https://pypi.org/simple" } 681 | dependencies = [ 682 | { name = "pytest" }, 683 | ] 684 | sdist = { url = "https://files.pythonhosted.org/packages/bd/cf/44510ac5479f281d6663a08dff0d93f56b21f4ee091980ea4d4b64491ad6/pytest-django-4.8.0.tar.gz", hash = "sha256:5d054fe011c56f3b10f978f41a8efb2e5adfc7e680ef36fb571ada1f24779d90", size = 83291 } 685 | wheels = [ 686 | { url = "https://files.pythonhosted.org/packages/93/5b/29555191e903881d05e1f7184205ec534c7021e0ee077d1e6a1ee8f1b1eb/pytest_django-4.8.0-py3-none-any.whl", hash = "sha256:ca1ddd1e0e4c227cf9e3e40a6afc6d106b3e70868fd2ac5798a22501271cd0c7", size = 23432 }, 687 | ] 688 | 689 | [[package]] 690 | name = "python-dateutil" 691 | version = "2.9.0.post0" 692 | source = { registry = "https://pypi.org/simple" } 693 | dependencies = [ 694 | { name = "six" }, 695 | ] 696 | sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } 697 | wheels = [ 698 | { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, 699 | ] 700 | 701 | [[package]] 702 | name = "python-django-template" 703 | version = "0.1.0" 704 | source = { editable = "." } 705 | dependencies = [ 706 | { name = "boto3" }, 707 | { name = "cachetools" }, 708 | { name = "celery" }, 709 | { name = "celery-types" }, 710 | { name = "colorlog" }, 711 | { name = "dj-database-url" }, 712 | { name = "django" }, 713 | { name = "django-axes", extra = ["ipware"] }, 714 | { name = "django-cors-headers" }, 715 | { name = "django-redis" }, 716 | { name = "django-silk" }, 717 | { name = "django-split-settings" }, 718 | { name = "django-storages" }, 719 | { name = "djangorestframework" }, 720 | { name = "djangorestframework-simplejwt" }, 721 | { name = "drf-spectacular" }, 722 | { name = "gunicorn" }, 723 | { name = "psycopg" }, 724 | { name = "pyjwt" }, 725 | { name = "python-dotenv" }, 726 | { name = "pytz" }, 727 | { name = "redis" }, 728 | { name = "sentry-sdk" }, 729 | ] 730 | 731 | [package.dev-dependencies] 732 | dev = [ 733 | { name = "mypy" }, 734 | { name = "pre-commit" }, 735 | { name = "pytest" }, 736 | { name = "pytest-cov" }, 737 | { name = "pytest-django" }, 738 | { name = "ruff" }, 739 | ] 740 | 741 | [package.metadata] 742 | requires-dist = [ 743 | { name = "boto3", specifier = "==1.35.4" }, 744 | { name = "cachetools", specifier = "==5.5.0" }, 745 | { name = "celery", specifier = "==5.4.0" }, 746 | { name = "celery-types", specifier = "==0.22.0" }, 747 | { name = "colorlog", specifier = "==6.8.2" }, 748 | { name = "dj-database-url", specifier = "==2.2.0" }, 749 | { name = "django", specifier = "==5.1" }, 750 | { name = "django-axes", extras = ["ipware"], specifier = "==6.5.1" }, 751 | { name = "django-cors-headers", specifier = "==4.4.0" }, 752 | { name = "django-redis", specifier = "==5.4.0" }, 753 | { name = "django-silk", specifier = "==5.2.0" }, 754 | { name = "django-split-settings", specifier = "==1.3.2" }, 755 | { name = "django-storages", specifier = "==1.14.4" }, 756 | { name = "djangorestframework", specifier = "==3.15.2" }, 757 | { name = "djangorestframework-simplejwt", specifier = "==5.3.1" }, 758 | { name = "drf-spectacular", specifier = "==0.27.2" }, 759 | { name = "gunicorn", specifier = "==23.0.0" }, 760 | { name = "psycopg", specifier = "==3.2.1" }, 761 | { name = "pyjwt", specifier = "==2.9.0" }, 762 | { name = "python-dotenv", specifier = "==1.0.1" }, 763 | { name = "pytz", specifier = "==2024.1" }, 764 | { name = "redis", specifier = "==5.0.8" }, 765 | { name = "sentry-sdk", specifier = "==2.13.0" }, 766 | ] 767 | 768 | [package.metadata.requires-dev] 769 | dev = [ 770 | { name = "mypy", specifier = "==1.11.1" }, 771 | { name = "pre-commit", specifier = "==3.8.0" }, 772 | { name = "pytest", specifier = "==8.3.2" }, 773 | { name = "pytest-cov", specifier = "==5.0.0" }, 774 | { name = "pytest-django", specifier = "==4.8.0" }, 775 | { name = "ruff", specifier = "==0.6.2" }, 776 | ] 777 | 778 | [[package]] 779 | name = "python-dotenv" 780 | version = "1.0.1" 781 | source = { registry = "https://pypi.org/simple" } 782 | sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 } 783 | wheels = [ 784 | { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 }, 785 | ] 786 | 787 | [[package]] 788 | name = "python-ipware" 789 | version = "3.0.0" 790 | source = { registry = "https://pypi.org/simple" } 791 | sdist = { url = "https://files.pythonhosted.org/packages/9e/60/da4426c3e9aee56f08b24091a9e85a0414260f928f97afd0013dfbd0332f/python_ipware-3.0.0.tar.gz", hash = "sha256:9117b1c4dddcb5d5ca49e6a9617de2fc66aec2ef35394563ac4eecabdf58c062", size = 16609 } 792 | wheels = [ 793 | { url = "https://files.pythonhosted.org/packages/08/bd/ccd7416fdb30f104ddf6cfd8ee9f699441c7d9880a26f9b3089438adee05/python_ipware-3.0.0-py3-none-any.whl", hash = "sha256:fc936e6e7ec9fcc107f9315df40658f468ac72f739482a707181742882e36b60", size = 10761 }, 794 | ] 795 | 796 | [[package]] 797 | name = "pytz" 798 | version = "2024.1" 799 | source = { registry = "https://pypi.org/simple" } 800 | sdist = { url = "https://files.pythonhosted.org/packages/90/26/9f1f00a5d021fff16dee3de13d43e5e978f3d58928e129c3a62cf7eb9738/pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812", size = 316214 } 801 | wheels = [ 802 | { url = "https://files.pythonhosted.org/packages/9c/3d/a121f284241f08268b21359bd425f7d4825cffc5ac5cd0e1b3d82ffd2b10/pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319", size = 505474 }, 803 | ] 804 | 805 | [[package]] 806 | name = "pyyaml" 807 | version = "6.0.2" 808 | source = { registry = "https://pypi.org/simple" } 809 | sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } 810 | wheels = [ 811 | { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, 812 | { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, 813 | { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, 814 | { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, 815 | { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, 816 | { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, 817 | { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, 818 | { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, 819 | { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, 820 | { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, 821 | { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, 822 | { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, 823 | { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, 824 | { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, 825 | { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, 826 | { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, 827 | { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, 828 | { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, 829 | ] 830 | 831 | [[package]] 832 | name = "redis" 833 | version = "5.0.8" 834 | source = { registry = "https://pypi.org/simple" } 835 | sdist = { url = "https://files.pythonhosted.org/packages/48/10/defc227d65ea9c2ff5244645870859865cba34da7373477c8376629746ec/redis-5.0.8.tar.gz", hash = "sha256:0c5b10d387568dfe0698c6fad6615750c24170e548ca2deac10c649d463e9870", size = 4595651 } 836 | wheels = [ 837 | { url = "https://files.pythonhosted.org/packages/c5/d1/19a9c76811757684a0f74adc25765c8a901d67f9f6472ac9d57c844a23c8/redis-5.0.8-py3-none-any.whl", hash = "sha256:56134ee08ea909106090934adc36f65c9bcbbaecea5b21ba704ba6fb561f8eb4", size = 255608 }, 838 | ] 839 | 840 | [[package]] 841 | name = "referencing" 842 | version = "0.35.1" 843 | source = { registry = "https://pypi.org/simple" } 844 | dependencies = [ 845 | { name = "attrs" }, 846 | { name = "rpds-py" }, 847 | ] 848 | sdist = { url = "https://files.pythonhosted.org/packages/99/5b/73ca1f8e72fff6fa52119dbd185f73a907b1989428917b24cff660129b6d/referencing-0.35.1.tar.gz", hash = "sha256:25b42124a6c8b632a425174f24087783efb348a6f1e0008e63cd4466fedf703c", size = 62991 } 849 | wheels = [ 850 | { url = "https://files.pythonhosted.org/packages/b7/59/2056f61236782a2c86b33906c025d4f4a0b17be0161b63b70fd9e8775d36/referencing-0.35.1-py3-none-any.whl", hash = "sha256:eda6d3234d62814d1c64e305c1331c9a3a6132da475ab6382eaa997b21ee75de", size = 26684 }, 851 | ] 852 | 853 | [[package]] 854 | name = "rpds-py" 855 | version = "0.20.0" 856 | source = { registry = "https://pypi.org/simple" } 857 | sdist = { url = "https://files.pythonhosted.org/packages/55/64/b693f262791b818880d17268f3f8181ef799b0d187f6f731b1772e05a29a/rpds_py-0.20.0.tar.gz", hash = "sha256:d72a210824facfdaf8768cf2d7ca25a042c30320b3020de2fa04640920d4e121", size = 25814 } 858 | wheels = [ 859 | { url = "https://files.pythonhosted.org/packages/89/b7/f9682c5cc37fcc035f4a0fc33c1fe92ec9cbfdee0cdfd071cf948f53e0df/rpds_py-0.20.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a84ab91cbe7aab97f7446652d0ed37d35b68a465aeef8fc41932a9d7eee2c1a6", size = 321468 }, 860 | { url = "https://files.pythonhosted.org/packages/b8/ad/fc82be4eaceb8d444cb6fc1956ce972b3a0795104279de05e0e4131d0a47/rpds_py-0.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:56e27147a5a4c2c21633ff8475d185734c0e4befd1c989b5b95a5d0db699b21b", size = 313062 }, 861 | { url = "https://files.pythonhosted.org/packages/0e/1c/6039e80b13a08569a304dc13476dc986352dca4598e909384db043b4e2bb/rpds_py-0.20.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2580b0c34583b85efec8c5c5ec9edf2dfe817330cc882ee972ae650e7b5ef739", size = 370168 }, 862 | { url = "https://files.pythonhosted.org/packages/dc/c9/5b9aa35acfb58946b4b785bc8e700ac313669e02fb100f3efa6176a83e81/rpds_py-0.20.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b80d4a7900cf6b66bb9cee5c352b2d708e29e5a37fe9bf784fa97fc11504bf6c", size = 371376 }, 863 | { url = "https://files.pythonhosted.org/packages/7b/dd/0e0dbeb70d8a5357d2814764d467ded98d81d90d3570de4fb05ec7224f6b/rpds_py-0.20.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50eccbf054e62a7b2209b28dc7a22d6254860209d6753e6b78cfaeb0075d7bee", size = 397200 }, 864 | { url = "https://files.pythonhosted.org/packages/e4/da/a47d931eb688ccfd77a7389e45935c79c41e8098d984d87335004baccb1d/rpds_py-0.20.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:49a8063ea4296b3a7e81a5dfb8f7b2d73f0b1c20c2af401fb0cdf22e14711a96", size = 426824 }, 865 | { url = "https://files.pythonhosted.org/packages/0f/f7/a59a673594e6c2ff2dbc44b00fd4ecdec2fc399bb6a7bd82d612699a0121/rpds_py-0.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea438162a9fcbee3ecf36c23e6c68237479f89f962f82dae83dc15feeceb37e4", size = 357967 }, 866 | { url = "https://files.pythonhosted.org/packages/5f/61/3ba1905396b2cb7088f9503a460b87da33452da54d478cb9241f6ad16d00/rpds_py-0.20.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:18d7585c463087bddcfa74c2ba267339f14f2515158ac4db30b1f9cbdb62c8ef", size = 378905 }, 867 | { url = "https://files.pythonhosted.org/packages/08/31/6d0df9356b4edb0a3a077f1ef714e25ad21f9f5382fc490c2383691885ea/rpds_py-0.20.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d4c7d1a051eeb39f5c9547e82ea27cbcc28338482242e3e0b7768033cb083821", size = 546348 }, 868 | { url = "https://files.pythonhosted.org/packages/ae/15/d33c021de5cb793101df9961c3c746dfc476953dbbf5db337d8010dffd4e/rpds_py-0.20.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e4df1e3b3bec320790f699890d41c59d250f6beda159ea3c44c3f5bac1976940", size = 553152 }, 869 | { url = "https://files.pythonhosted.org/packages/70/2d/5536d28c507a4679179ab15aa0049440e4d3dd6752050fa0843ed11e9354/rpds_py-0.20.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2cf126d33a91ee6eedc7f3197b53e87a2acdac63602c0f03a02dd69e4b138174", size = 528807 }, 870 | { url = "https://files.pythonhosted.org/packages/e3/62/7ebe6ec0d3dd6130921f8cffb7e34afb7f71b3819aa0446a24c5e81245ec/rpds_py-0.20.0-cp312-none-win32.whl", hash = "sha256:8bc7690f7caee50b04a79bf017a8d020c1f48c2a1077ffe172abec59870f1139", size = 200993 }, 871 | { url = "https://files.pythonhosted.org/packages/ec/2f/b938864d66b86a6e4acadefdc56de75ef56f7cafdfd568a6464605457bd5/rpds_py-0.20.0-cp312-none-win_amd64.whl", hash = "sha256:0e13e6952ef264c40587d510ad676a988df19adea20444c2b295e536457bc585", size = 214458 }, 872 | { url = "https://files.pythonhosted.org/packages/99/32/43b919a0a423c270a838ac2726b1c7168b946f2563fd99a51aaa9692d00f/rpds_py-0.20.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:aa9a0521aeca7d4941499a73ad7d4f8ffa3d1affc50b9ea11d992cd7eff18a29", size = 321465 }, 873 | { url = "https://files.pythonhosted.org/packages/58/a9/c4d899cb28e9e47b0ff12462e8f827381f243176036f17bef9c1604667f2/rpds_py-0.20.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a1f1d51eccb7e6c32ae89243cb352389228ea62f89cd80823ea7dd1b98e0b91", size = 312900 }, 874 | { url = "https://files.pythonhosted.org/packages/8f/90/9e51670575b5dfaa8c823369ef7d943087bfb73d4f124a99ad6ef19a2b26/rpds_py-0.20.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a86a9b96070674fc88b6f9f71a97d2c1d3e5165574615d1f9168ecba4cecb24", size = 370973 }, 875 | { url = "https://files.pythonhosted.org/packages/fc/c1/523f2a03f853fc0d4c1acbef161747e9ab7df0a8abf6236106e333540921/rpds_py-0.20.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6c8ef2ebf76df43f5750b46851ed1cdf8f109d7787ca40035fe19fbdc1acc5a7", size = 370890 }, 876 | { url = "https://files.pythonhosted.org/packages/51/ca/2458a771f16b0931de4d384decbe43016710bc948036c8f4562d6e063437/rpds_py-0.20.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b74b25f024b421d5859d156750ea9a65651793d51b76a2e9238c05c9d5f203a9", size = 397174 }, 877 | { url = "https://files.pythonhosted.org/packages/00/7d/6e06807f6305ea2408b364efb0eef83a6e21b5e7b5267ad6b473b9a7e416/rpds_py-0.20.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57eb94a8c16ab08fef6404301c38318e2c5a32216bf5de453e2714c964c125c8", size = 426449 }, 878 | { url = "https://files.pythonhosted.org/packages/8c/d1/6c9e65260a819a1714510a7d69ac1d68aa23ee9ce8a2d9da12187263c8fc/rpds_py-0.20.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1940dae14e715e2e02dfd5b0f64a52e8374a517a1e531ad9412319dc3ac7879", size = 357698 }, 879 | { url = "https://files.pythonhosted.org/packages/5d/fb/ecea8b5286d2f03eec922be7173a03ed17278944f7c124348f535116db15/rpds_py-0.20.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d20277fd62e1b992a50c43f13fbe13277a31f8c9f70d59759c88f644d66c619f", size = 378530 }, 880 | { url = "https://files.pythonhosted.org/packages/e3/e3/ac72f858957f52a109c588589b73bd2fad4a0fc82387fb55fb34aeb0f9cd/rpds_py-0.20.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:06db23d43f26478303e954c34c75182356ca9aa7797d22c5345b16871ab9c45c", size = 545753 }, 881 | { url = "https://files.pythonhosted.org/packages/b2/a4/a27683b519d5fc98e4390a3b130117d80fd475c67aeda8aac83c0e8e326a/rpds_py-0.20.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b2a5db5397d82fa847e4c624b0c98fe59d2d9b7cf0ce6de09e4d2e80f8f5b3f2", size = 552443 }, 882 | { url = "https://files.pythonhosted.org/packages/a1/ed/c074d248409b4432b1ccb2056974175fa0af2d1bc1f9c21121f80a358fa3/rpds_py-0.20.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5a35df9f5548fd79cb2f52d27182108c3e6641a4feb0f39067911bf2adaa3e57", size = 528380 }, 883 | { url = "https://files.pythonhosted.org/packages/d5/bd/04caf938895d2d78201e89c0c8a94dfd9990c34a19ff52fb01d0912343e3/rpds_py-0.20.0-cp313-none-win32.whl", hash = "sha256:fd2d84f40633bc475ef2d5490b9c19543fbf18596dcb1b291e3a12ea5d722f7a", size = 200540 }, 884 | { url = "https://files.pythonhosted.org/packages/95/cc/109eb8b9863680411ae703664abacaa035820c7755acc9686d5dd02cdd2e/rpds_py-0.20.0-cp313-none-win_amd64.whl", hash = "sha256:9bc2d153989e3216b0559251b0c260cfd168ec78b1fac33dd485750a228db5a2", size = 214111 }, 885 | ] 886 | 887 | [[package]] 888 | name = "ruff" 889 | version = "0.6.2" 890 | source = { registry = "https://pypi.org/simple" } 891 | sdist = { url = "https://files.pythonhosted.org/packages/23/f4/279d044f66b79261fd37df76bf72b64471afab5d3b7906a01499c4451910/ruff-0.6.2.tar.gz", hash = "sha256:239ee6beb9e91feb8e0ec384204a763f36cb53fb895a1a364618c6abb076b3be", size = 2460281 } 892 | wheels = [ 893 | { url = "https://files.pythonhosted.org/packages/72/4b/47dd7a69287afb4069fa42c198e899463605460a58120196711bfcf0446b/ruff-0.6.2-py3-none-linux_armv6l.whl", hash = "sha256:5c8cbc6252deb3ea840ad6a20b0f8583caab0c5ef4f9cca21adc5a92b8f79f3c", size = 9695871 }, 894 | { url = "https://files.pythonhosted.org/packages/ae/c3/8aac62ac4638c14a740ee76a755a925f2d0d04580ab790a9887accb729f6/ruff-0.6.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:17002fe241e76544448a8e1e6118abecbe8cd10cf68fde635dad480dba594570", size = 9459354 }, 895 | { url = "https://files.pythonhosted.org/packages/2f/cf/77fbd8d4617b9b9c503f9bffb8552c4e3ea1a58dc36975e7a9104ffb0f85/ruff-0.6.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3dbeac76ed13456f8158b8f4fe087bf87882e645c8e8b606dd17b0b66c2c1158", size = 9163871 }, 896 | { url = "https://files.pythonhosted.org/packages/05/1c/765192bab32b79efbb498b06f0b9dcb3629112b53b8777ae1d19b8209e09/ruff-0.6.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:094600ee88cda325988d3f54e3588c46de5c18dae09d683ace278b11f9d4d534", size = 10096250 }, 897 | { url = "https://files.pythonhosted.org/packages/08/d0/86f3cb0f6934c99f759c232984a5204d67a26745cad2d9edff6248adf7d2/ruff-0.6.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:316d418fe258c036ba05fbf7dfc1f7d3d4096db63431546163b472285668132b", size = 9475376 }, 898 | { url = "https://files.pythonhosted.org/packages/cd/cc/4c8d0e225b559a3fae6092ec310d7150d3b02b4669e9223f783ef64d82c0/ruff-0.6.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d72b8b3abf8a2d51b7b9944a41307d2f442558ccb3859bbd87e6ae9be1694a5d", size = 10295634 }, 899 | { url = "https://files.pythonhosted.org/packages/db/96/d2699cfb1bb5a01c68122af43454c76c31331e1c8a9bd97d653d7c82524b/ruff-0.6.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2aed7e243be68487aa8982e91c6e260982d00da3f38955873aecd5a9204b1d66", size = 11024941 }, 900 | { url = "https://files.pythonhosted.org/packages/8b/a9/6ecd66af8929e0f2a1ed308a4137f3521789f28f0eb97d32c2ca3aa7000c/ruff-0.6.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d371f7fc9cec83497fe7cf5eaf5b76e22a8efce463de5f775a1826197feb9df8", size = 10606894 }, 901 | { url = "https://files.pythonhosted.org/packages/e4/73/2ee4cd19f44992fedac1cc6db9e3d825966072f6dcbd4032f21cbd063170/ruff-0.6.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8f310d63af08f583363dfb844ba8f9417b558199c58a5999215082036d795a1", size = 11552886 }, 902 | { url = "https://files.pythonhosted.org/packages/60/4c/c0f1cd35ce4a93c54a6bb1ee6934a3a205fa02198dd076678193853ceea1/ruff-0.6.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7db6880c53c56addb8638fe444818183385ec85eeada1d48fc5abe045301b2f1", size = 10264945 }, 903 | { url = "https://files.pythonhosted.org/packages/c4/89/e45c9359b9cdd4245512ea2b9f2bb128a997feaa5f726fc9e8c7a66afadf/ruff-0.6.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1175d39faadd9a50718f478d23bfc1d4da5743f1ab56af81a2b6caf0a2394f23", size = 10100007 }, 904 | { url = "https://files.pythonhosted.org/packages/06/74/0bd4e0a7ed5f6908df87892f9bf60a2356c0fd74102d8097298bd9b4f346/ruff-0.6.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5b939f9c86d51635fe486585389f54582f0d65b8238e08c327c1534844b3bb9a", size = 9559267 }, 905 | { url = "https://files.pythonhosted.org/packages/54/03/3dc6dc9419f276f05805bf888c279e3e0b631284abd548d9e87cebb93aec/ruff-0.6.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d0d62ca91219f906caf9b187dea50d17353f15ec9bb15aae4a606cd697b49b4c", size = 9905304 }, 906 | { url = "https://files.pythonhosted.org/packages/5c/5b/d6a72a6a6bbf097c09de468326ef5fa1c9e7aa5e6e45979bc0d984b0dbe7/ruff-0.6.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7438a7288f9d67ed3c8ce4d059e67f7ed65e9fe3aa2ab6f5b4b3610e57e3cb56", size = 10341480 }, 907 | { url = "https://files.pythonhosted.org/packages/79/a9/0f2f21fe15ba537c46598f96aa9ae4a3d4b9ec64926664617ca6a8c772f4/ruff-0.6.2-py3-none-win32.whl", hash = "sha256:279d5f7d86696df5f9549b56b9b6a7f6c72961b619022b5b7999b15db392a4da", size = 7961901 }, 908 | { url = "https://files.pythonhosted.org/packages/b0/80/fff12ffe11853d9f4ea3e5221e6dd2e93640a161c05c9579833e09ad40a7/ruff-0.6.2-py3-none-win_amd64.whl", hash = "sha256:d9f3469c7dd43cd22eb1c3fc16926fb8258d50cb1b216658a07be95dd117b0f2", size = 8783320 }, 909 | { url = "https://files.pythonhosted.org/packages/56/91/577cdd64cce5e74d3f8b5ecb93f29566def569c741eb008aed4f331ef821/ruff-0.6.2-py3-none-win_arm64.whl", hash = "sha256:f28fcd2cd0e02bdf739297516d5643a945cc7caf09bd9bcb4d932540a5ea4fa9", size = 8225886 }, 910 | ] 911 | 912 | [[package]] 913 | name = "s3transfer" 914 | version = "0.10.2" 915 | source = { registry = "https://pypi.org/simple" } 916 | dependencies = [ 917 | { name = "botocore" }, 918 | ] 919 | sdist = { url = "https://files.pythonhosted.org/packages/cb/67/94c6730ee4c34505b14d94040e2f31edf144c230b6b49e971b4f25ff8fab/s3transfer-0.10.2.tar.gz", hash = "sha256:0711534e9356d3cc692fdde846b4a1e4b0cb6519971860796e6bc4c7aea00ef6", size = 144095 } 920 | wheels = [ 921 | { url = "https://files.pythonhosted.org/packages/3c/4a/b221409913760d26cf4498b7b1741d510c82d3ad38381984a3ddc135ec66/s3transfer-0.10.2-py3-none-any.whl", hash = "sha256:eca1c20de70a39daee580aef4986996620f365c4e0fda6a86100231d62f1bf69", size = 82716 }, 922 | ] 923 | 924 | [[package]] 925 | name = "sentry-sdk" 926 | version = "2.13.0" 927 | source = { registry = "https://pypi.org/simple" } 928 | dependencies = [ 929 | { name = "certifi" }, 930 | { name = "urllib3" }, 931 | ] 932 | sdist = { url = "https://files.pythonhosted.org/packages/bb/41/97f673384dae5ed81cc2a568cc5c28e76deee85f8ba50def862e86150a5a/sentry_sdk-2.13.0.tar.gz", hash = "sha256:8d4a576f7a98eb2fdb40e13106e41f330e5c79d72a68be1316e7852cf4995260", size = 279937 } 933 | wheels = [ 934 | { url = "https://files.pythonhosted.org/packages/ad/7e/e9ca09f24a6c334286631a2d32c267cdc5edad5ac03fd9d20a01a82f1c35/sentry_sdk-2.13.0-py2.py3-none-any.whl", hash = "sha256:6beede8fc2ab4043da7f69d95534e320944690680dd9a963178a49de71d726c6", size = 309078 }, 935 | ] 936 | 937 | [[package]] 938 | name = "six" 939 | version = "1.16.0" 940 | source = { registry = "https://pypi.org/simple" } 941 | sdist = { url = "https://files.pythonhosted.org/packages/71/39/171f1c67cd00715f190ba0b100d606d440a28c93c7714febeca8b79af85e/six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", size = 34041 } 942 | wheels = [ 943 | { url = "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254", size = 11053 }, 944 | ] 945 | 946 | [[package]] 947 | name = "sqlparse" 948 | version = "0.5.1" 949 | source = { registry = "https://pypi.org/simple" } 950 | sdist = { url = "https://files.pythonhosted.org/packages/73/82/dfa23ec2cbed08a801deab02fe7c904bfb00765256b155941d789a338c68/sqlparse-0.5.1.tar.gz", hash = "sha256:bb6b4df465655ef332548e24f08e205afc81b9ab86cb1c45657a7ff173a3a00e", size = 84502 } 951 | wheels = [ 952 | { url = "https://files.pythonhosted.org/packages/5d/a5/b2860373aa8de1e626b2bdfdd6df4355f0565b47e51f7d0c54fe70faf8fe/sqlparse-0.5.1-py3-none-any.whl", hash = "sha256:773dcbf9a5ab44a090f3441e2180efe2560220203dc2f8c0b0fa141e18b505e4", size = 44156 }, 953 | ] 954 | 955 | [[package]] 956 | name = "typing-extensions" 957 | version = "4.12.2" 958 | source = { registry = "https://pypi.org/simple" } 959 | sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } 960 | wheels = [ 961 | { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, 962 | ] 963 | 964 | [[package]] 965 | name = "tzdata" 966 | version = "2024.1" 967 | source = { registry = "https://pypi.org/simple" } 968 | sdist = { url = "https://files.pythonhosted.org/packages/74/5b/e025d02cb3b66b7b76093404392d4b44343c69101cc85f4d180dd5784717/tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd", size = 190559 } 969 | wheels = [ 970 | { url = "https://files.pythonhosted.org/packages/65/58/f9c9e6be752e9fcb8b6a0ee9fb87e6e7a1f6bcab2cdc73f02bb7ba91ada0/tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252", size = 345370 }, 971 | ] 972 | 973 | [[package]] 974 | name = "uritemplate" 975 | version = "4.1.1" 976 | source = { registry = "https://pypi.org/simple" } 977 | sdist = { url = "https://files.pythonhosted.org/packages/d2/5a/4742fdba39cd02a56226815abfa72fe0aa81c33bed16ed045647d6000eba/uritemplate-4.1.1.tar.gz", hash = "sha256:4346edfc5c3b79f694bccd6d6099a322bbeb628dbf2cd86eea55a456ce5124f0", size = 273898 } 978 | wheels = [ 979 | { url = "https://files.pythonhosted.org/packages/81/c0/7461b49cd25aeece13766f02ee576d1db528f1c37ce69aee300e075b485b/uritemplate-4.1.1-py2.py3-none-any.whl", hash = "sha256:830c08b8d99bdd312ea4ead05994a38e8936266f84b9a7878232db50b044e02e", size = 10356 }, 980 | ] 981 | 982 | [[package]] 983 | name = "urllib3" 984 | version = "2.2.2" 985 | source = { registry = "https://pypi.org/simple" } 986 | sdist = { url = "https://files.pythonhosted.org/packages/43/6d/fa469ae21497ddc8bc93e5877702dca7cb8f911e337aca7452b5724f1bb6/urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168", size = 292266 } 987 | wheels = [ 988 | { url = "https://files.pythonhosted.org/packages/ca/1c/89ffc63a9605b583d5df2be791a27bc1a42b7c32bab68d3c8f2f73a98cd4/urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472", size = 121444 }, 989 | ] 990 | 991 | [[package]] 992 | name = "vine" 993 | version = "5.1.0" 994 | source = { registry = "https://pypi.org/simple" } 995 | sdist = { url = "https://files.pythonhosted.org/packages/bd/e4/d07b5f29d283596b9727dd5275ccbceb63c44a1a82aa9e4bfd20426762ac/vine-5.1.0.tar.gz", hash = "sha256:8b62e981d35c41049211cf62a0a1242d8c1ee9bd15bb196ce38aefd6799e61e0", size = 48980 } 996 | wheels = [ 997 | { url = "https://files.pythonhosted.org/packages/03/ff/7c0c86c43b3cbb927e0ccc0255cb4057ceba4799cd44ae95174ce8e8b5b2/vine-5.1.0-py3-none-any.whl", hash = "sha256:40fdf3c48b2cfe1c38a49e9ae2da6fda88e4794c810050a728bd7413811fb1dc", size = 9636 }, 998 | ] 999 | 1000 | [[package]] 1001 | name = "virtualenv" 1002 | version = "20.26.3" 1003 | source = { registry = "https://pypi.org/simple" } 1004 | dependencies = [ 1005 | { name = "distlib" }, 1006 | { name = "filelock" }, 1007 | { name = "platformdirs" }, 1008 | ] 1009 | sdist = { url = "https://files.pythonhosted.org/packages/68/60/db9f95e6ad456f1872486769c55628c7901fb4de5a72c2f7bdd912abf0c1/virtualenv-20.26.3.tar.gz", hash = "sha256:4c43a2a236279d9ea36a0d76f98d84bd6ca94ac4e0f4a3b9d46d05e10fea542a", size = 9057588 } 1010 | wheels = [ 1011 | { url = "https://files.pythonhosted.org/packages/07/4d/410156100224c5e2f0011d435e477b57aed9576fc7fe137abcf14ec16e11/virtualenv-20.26.3-py3-none-any.whl", hash = "sha256:8cc4a31139e796e9a7de2cd5cf2489de1217193116a8fd42328f1bd65f434589", size = 5684792 }, 1012 | ] 1013 | 1014 | [[package]] 1015 | name = "wcwidth" 1016 | version = "0.2.13" 1017 | source = { registry = "https://pypi.org/simple" } 1018 | sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301 } 1019 | wheels = [ 1020 | { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166 }, 1021 | ] 1022 | --------------------------------------------------------------------------------