From d45a323757a197f7adf28aab4a018c4a0f72d610 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 12 Jun 2026 09:27:02 +0000 Subject: [PATCH] Tune Django settings for highload readiness - Enable native psycopg connection pooling for PostgreSQL (DJANGO_DB_POOL, min/max size and timeout via env); fall back to persistent connections with health checks for other engines - Move sessions to cached_db backend: reads served from Redis, write-through to DB so sessions survive cache flush - Tune Redis cache client: connection pool size, socket timeouts, retry_on_timeout, health checks, configurable key prefix - Switch default password hasher to Argon2 (argon2-cffi), keeping legacy hashers for transparent migration of existing passwords - Add UserRateThrottle alongside AnonRateThrottle with env-driven rates - Set NUM_PROXIES in prod for correct client IP behind reverse proxy - Reduce 4xx log noise via django.request logger level (default ERROR) - Tune gunicorn: workers default to CPU*2+1, gthread worker class with configurable threads, /dev/shm heartbeat dir, backlog 2048, graceful timeout, keep-alive 5 - Document new env vars in .env.example and README https://claude.ai/code/session_01YZr4TbzTw6EkkFqZz2onTF --- .env.example | 16 ++++++++++ README.md | 10 +++++- config/settings/base.py | 53 ++++++++++++++++++++++++++++++-- config/settings/prod.py | 4 +++ entrypoint.sh | 14 +++++++-- pyproject.toml | 1 + uv.lock | 67 +++++++++++++++++++++++++++++++++++++++++ 7 files changed, 158 insertions(+), 7 deletions(-) diff --git a/.env.example b/.env.example index 645ad68..6e67935 100644 --- a/.env.example +++ b/.env.example @@ -20,8 +20,24 @@ POSTGRES_PASSWORD=app REDIS_URL=redis://localhost:6379/1 REDIS_HOST=localhost REDIS_PORT=6379 +# REDIS_MAX_CONNECTIONS=100 + +# Database connection pool (только PostgreSQL, выключить: DJANGO_DB_POOL=False) +# DJANGO_DB_POOL=True +# DJANGO_DB_POOL_MIN_SIZE=2 +# DJANGO_DB_POOL_MAX_SIZE=10 +# DJANGO_DB_POOL_TIMEOUT=10 + +# API throttling +# DJANGO_THROTTLE_ANON=100/min +# DJANGO_THROTTLE_USER=1000/min # Production security (используются при ENVIRONMENT=prod) # DJANGO_SECURE_SSL_REDIRECT=True # DJANGO_SECURE_HSTS_SECONDS=2592000 +# DJANGO_NUM_PROXIES=1 + +# Gunicorn (по умолчанию workers = CPU * 2 + 1) # GUNICORN_WORKERS=4 +# GUNICORN_THREADS=4 +# GUNICORN_TIMEOUT=30 diff --git a/README.md b/README.md index 1d98784..eeb3354 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ | 📖 **OpenAPI из коробки** | Swagger UI и ReDoc (drf-spectacular) | | ⚙️ **12-factor конфигурация** | Всё через переменные окружения (django-environ) | | 🔒 **Безопасный production** | HSTS, secure cookies, SSL redirect, whitenoise, gunicorn | +| ⚡ **Highload-тюнинг** | Пул соединений psycopg, кэш-сессии, Argon2, rate limiting | | 👤 **Кастомный пользователь** | Email-логин + `external_id` (UUID) для интеграций | | 💓 **Health-check** | `/health/` с проверкой БД и Redis — готов для k8s-проб | | 🧹 **Качество кода** | ruff · bandit · ty · pre-commit · pytest · GitHub Actions | @@ -176,9 +177,16 @@ make up / down # 🐳 docker compose up/down | `DJANGO_DEBUG` | Режим отладки | `False` (prod), `True` (dev) | | `DJANGO_ALLOWED_HOSTS` | Разрешённые хосты через запятую | — | | `DATABASE_URL` | URL базы данных | SQLite | +| `DJANGO_DB_POOL` | Пул соединений psycopg (PostgreSQL) | `True` | +| `DJANGO_DB_POOL_MAX_SIZE` | Максимум соединений в пуле (на процесс) | `10` | | `REDIS_URL` | URL Redis для кэша | `redis://redis:6379/1` | +| `REDIS_MAX_CONNECTIONS` | Размер пула Redis-клиента | `100` | +| `DJANGO_THROTTLE_ANON` | Лимит запросов для анонимов | `100/min` | +| `DJANGO_THROTTLE_USER` | Лимит запросов для пользователей | `1000/min` | +| `DJANGO_NUM_PROXIES` | Прокси перед приложением (prod) | `1` | | `DJANGO_LOG_LEVEL` | Уровень логирования | `INFO` | -| `GUNICORN_WORKERS` | Количество воркеров gunicorn | `4` | +| `GUNICORN_WORKERS` | Воркеры gunicorn | `CPU × 2 + 1` | +| `GUNICORN_THREADS` | Потоки на воркер (gthread) | `4` | diff --git a/config/settings/base.py b/config/settings/base.py index 4b663ad..0ba2f4b 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -65,13 +65,35 @@ DATABASES = { 'default': env.db('DATABASE_URL', default=f'sqlite:///{BASE_DIR / "db.sqlite3"}'), } -DATABASES['default']['CONN_MAX_AGE'] = env.int('DJANGO_DB_CONN_MAX_AGE', default=60) -DATABASES['default']['CONN_HEALTH_CHECKS'] = True + +_db_is_postgres = DATABASES['default']['ENGINE'] == 'django.db.backends.postgresql' +if _db_is_postgres and env.bool('DJANGO_DB_POOL', default=True): + # Нативный пул psycopg: пул несовместим с CONN_MAX_AGE и health checks Django + DATABASES['default']['CONN_MAX_AGE'] = 0 + DATABASES['default']['CONN_HEALTH_CHECKS'] = False + DATABASES['default'].setdefault('OPTIONS', {})['pool'] = { + 'min_size': env.int('DJANGO_DB_POOL_MIN_SIZE', default=2), + 'max_size': env.int('DJANGO_DB_POOL_MAX_SIZE', default=10), + 'timeout': env.int('DJANGO_DB_POOL_TIMEOUT', default=10), + } +else: + DATABASES['default']['CONN_MAX_AGE'] = env.int('DJANGO_DB_CONN_MAX_AGE', default=60) + DATABASES['default']['CONN_HEALTH_CHECKS'] = True DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' AUTH_USER_MODEL = 'users.User' +# Argon2 — быстрее и устойчивее PBKDF2; остальные хэшеры нужны +# для прозрачной миграции уже существующих паролей +PASSWORD_HASHERS = [ + 'django.contrib.auth.hashers.Argon2PasswordHasher', + 'django.contrib.auth.hashers.PBKDF2PasswordHasher', + 'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher', + 'django.contrib.auth.hashers.BCryptSHA256PasswordHasher', + 'django.contrib.auth.hashers.ScryptPasswordHasher', +] + AUTH_PASSWORD_VALIDATORS = [ { 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', @@ -122,9 +144,13 @@ 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination', 'PAGE_SIZE': 20, 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', - 'DEFAULT_THROTTLE_CLASSES': ('rest_framework.throttling.AnonRateThrottle',), + 'DEFAULT_THROTTLE_CLASSES': ( + 'rest_framework.throttling.AnonRateThrottle', + 'rest_framework.throttling.UserRateThrottle', + ), 'DEFAULT_THROTTLE_RATES': { 'anon': env.str('DJANGO_THROTTLE_ANON', default='100/min'), + 'user': env.str('DJANGO_THROTTLE_USER', default='1000/min'), }, } @@ -139,9 +165,24 @@ 'default': { 'BACKEND': 'django.core.cache.backends.redis.RedisCache', 'LOCATION': env.str('REDIS_URL', default='redis://redis:6379/1'), + 'KEY_PREFIX': env.str('DJANGO_CACHE_KEY_PREFIX', default='app'), + 'OPTIONS': { + # Параметры пула соединений redis-py + 'max_connections': env.int('REDIS_MAX_CONNECTIONS', default=100), + 'socket_connect_timeout': 5, + 'socket_timeout': 5, + 'retry_on_timeout': True, + 'health_check_interval': 30, + }, }, } +# Сессии в кэше с write-through в БД: чтение не нагружает PostgreSQL, +# при сбросе Redis сессии не теряются +SESSION_ENGINE = 'django.contrib.sessions.backends.cached_db' + +EMAIL_TIMEOUT = env.int('DJANGO_EMAIL_TIMEOUT', default=10) + LOGGING = { 'version': 1, 'disable_existing_loggers': False, @@ -167,6 +208,12 @@ 'level': env.str('DJANGO_LOG_LEVEL', default='INFO'), 'propagate': False, }, + # Под нагрузкой 4xx-шум забивает логи: пишем только 5xx и медленные ответы + 'django.request': { + 'handlers': ['console'], + 'level': env.str('DJANGO_REQUEST_LOG_LEVEL', default='ERROR'), + 'propagate': False, + }, }, } diff --git a/config/settings/prod.py b/config/settings/prod.py index 1a40ac0..5224290 100644 --- a/config/settings/prod.py +++ b/config/settings/prod.py @@ -22,3 +22,7 @@ # В production браузерные формы DRF не нужны REST_FRAMEWORK['DEFAULT_RENDERER_CLASSES'] = ('rest_framework.renderers.JSONRenderer',) # noqa: F821 + +# Количество прокси перед приложением — для корректного определения IP клиента +# в троттлинге (X-Forwarded-For) +REST_FRAMEWORK['NUM_PROXIES'] = env.int('DJANGO_NUM_PROXIES', default=1) # noqa: F821 diff --git a/entrypoint.sh b/entrypoint.sh index f00de56..10b47a7 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -43,13 +43,21 @@ case "$1" in ;; "prod") echo "Запуск в продакшн режиме..." + # Формула по умолчанию: CPU * 2 + 1 + WORKERS="${GUNICORN_WORKERS:-$(( $(nproc) * 2 + 1 ))}" + echo "Gunicorn workers: $WORKERS, threads: ${GUNICORN_THREADS:-4}" exec gunicorn config.wsgi:application \ --bind 0.0.0.0:8000 \ - --workers "${GUNICORN_WORKERS:-4}" \ + --workers "$WORKERS" \ + --worker-class gthread \ + --threads "${GUNICORN_THREADS:-4}" \ + --worker-tmp-dir /dev/shm \ + --backlog 2048 \ --max-requests 1000 \ --max-requests-jitter 100 \ - --timeout 30 \ - --keep-alive 2 \ + --timeout "${GUNICORN_TIMEOUT:-30}" \ + --graceful-timeout 30 \ + --keep-alive 5 \ --log-level info \ --access-logfile - \ --error-logfile - diff --git a/pyproject.toml b/pyproject.toml index 31da175..38721cd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,6 +5,7 @@ description = "Add your description here" readme = "README.md" requires-python = "==3.13.*" dependencies = [ + "argon2-cffi==25.1.0", "django==6.0.6", "django-environ==0.13.0", "django-filter==25.2", diff --git a/uv.lock b/uv.lock index 41607e9..8ef6c7c 100644 --- a/uv.lock +++ b/uv.lock @@ -2,6 +2,39 @@ version = 1 revision = 3 requires-python = "==3.13.*" +[[package]] +name = "argon2-cffi" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "argon2-cffi-bindings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/89/ce5af8a7d472a67cc819d5d998aa8c82c5d860608c4db9f46f1162d7dab9/argon2_cffi-25.1.0.tar.gz", hash = "sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1", size = 45706, upload-time = "2025-06-03T06:55:32.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl", hash = "sha256:fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741", size = 14657, upload-time = "2025-06-03T06:55:30.804Z" }, +] + +[[package]] +name = "argon2-cffi-bindings" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/2d/db8af0df73c1cf454f71b2bbe5e356b8c1f8041c979f505b3d3186e520a9/argon2_cffi_bindings-25.1.0.tar.gz", hash = "sha256:b957f3e6ea4d55d820e40ff76f450952807013d361a65d7f28acc0acbf29229d", size = 1783441, upload-time = "2025-07-30T10:02:05.147Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/57/96b8b9f93166147826da5f90376e784a10582dd39a393c99bb62cfcf52f0/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:aecba1723ae35330a008418a91ea6cfcedf6d31e5fbaa056a166462ff066d500", size = 54121, upload-time = "2025-07-30T10:01:50.815Z" }, + { url = "https://files.pythonhosted.org/packages/0a/08/a9bebdb2e0e602dde230bdde8021b29f71f7841bd54801bcfd514acb5dcf/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2630b6240b495dfab90aebe159ff784d08ea999aa4b0d17efa734055a07d2f44", size = 29177, upload-time = "2025-07-30T10:01:51.681Z" }, + { url = "https://files.pythonhosted.org/packages/b6/02/d297943bcacf05e4f2a94ab6f462831dc20158614e5d067c35d4e63b9acb/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:7aef0c91e2c0fbca6fc68e7555aa60ef7008a739cbe045541e438373bc54d2b0", size = 31090, upload-time = "2025-07-30T10:01:53.184Z" }, + { url = "https://files.pythonhosted.org/packages/c1/93/44365f3d75053e53893ec6d733e4a5e3147502663554b4d864587c7828a7/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e021e87faa76ae0d413b619fe2b65ab9a037f24c60a1e6cc43457ae20de6dc6", size = 81246, upload-time = "2025-07-30T10:01:54.145Z" }, + { url = "https://files.pythonhosted.org/packages/09/52/94108adfdd6e2ddf58be64f959a0b9c7d4ef2fa71086c38356d22dc501ea/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e924cfc503018a714f94a49a149fdc0b644eaead5d1f089330399134fa028a", size = 87126, upload-time = "2025-07-30T10:01:55.074Z" }, + { url = "https://files.pythonhosted.org/packages/72/70/7a2993a12b0ffa2a9271259b79cc616e2389ed1a4d93842fac5a1f923ffd/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87b72589133f0346a1cb8d5ecca4b933e3c9b64656c9d175270a000e73b288d", size = 80343, upload-time = "2025-07-30T10:01:56.007Z" }, + { url = "https://files.pythonhosted.org/packages/78/9a/4e5157d893ffc712b74dbd868c7f62365618266982b64accab26bab01edc/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1db89609c06afa1a214a69a462ea741cf735b29a57530478c06eb81dd403de99", size = 86777, upload-time = "2025-07-30T10:01:56.943Z" }, + { url = "https://files.pythonhosted.org/packages/74/cd/15777dfde1c29d96de7f18edf4cc94c385646852e7c7b0320aa91ccca583/argon2_cffi_bindings-25.1.0-cp39-abi3-win32.whl", hash = "sha256:473bcb5f82924b1becbb637b63303ec8d10e84c8d241119419897a26116515d2", size = 27180, upload-time = "2025-07-30T10:01:57.759Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c6/a759ece8f1829d1f162261226fbfd2c6832b3ff7657384045286d2afa384/argon2_cffi_bindings-25.1.0-cp39-abi3-win_amd64.whl", hash = "sha256:a98cd7d17e9f7ce244c0803cad3c23a7d379c301ba618a5fa76a67d116618b98", size = 31715, upload-time = "2025-07-30T10:01:58.56Z" }, + { url = "https://files.pythonhosted.org/packages/42/b9/f8d6fa329ab25128b7e98fd83a3cb34d9db5b059a9847eddb840a0af45dd/argon2_cffi_bindings-25.1.0-cp39-abi3-win_arm64.whl", hash = "sha256:b0fdbcf513833809c882823f98dc2f931cf659d9a1429616ac3adebb49f5db94", size = 27149, upload-time = "2025-07-30T10:01:59.329Z" }, +] + [[package]] name = "asgiref" version = "3.11.0" @@ -35,6 +68,29 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/05/a4/a26d5b25671d27e03afb5401a0be5899d94ff8fab6a698b1ac5be3ec29ef/bandit-1.9.4-py3-none-any.whl", hash = "sha256:f89ffa663767f5a0585ea075f01020207e966a9c0f2b9ef56a57c7963a3f6f8e", size = 134741, upload-time = "2026-02-25T06:44:13.694Z" }, ] +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, +] + [[package]] name = "cfgv" version = "3.5.0" @@ -81,6 +137,7 @@ name = "django-boilerplate" version = "0.1.0" source = { virtual = "." } dependencies = [ + { name = "argon2-cffi" }, { name = "django" }, { name = "django-environ" }, { name = "django-filter" }, @@ -107,6 +164,7 @@ dev = [ [package.metadata] requires-dist = [ + { name = "argon2-cffi", specifier = "==25.1.0" }, { name = "django", specifier = "==6.0.6" }, { name = "django-environ", specifier = "==0.13.0" }, { name = "django-filter", specifier = "==25.2" }, @@ -409,6 +467,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/37/ed/89c2c620af0e1660354cd8aabf9f5b21f911597ce22acb37c805d6c86bc8/psycopg_pool-3.3.1-py3-none-any.whl", hash = "sha256:2af5b432941c4c9ad5c87b3fa410aec910ec8f7c122855897983a06c45f2e4b5", size = 40023, upload-time = "2026-05-01T23:31:53.136Z" }, ] +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + [[package]] name = "pygments" version = "2.19.2"