Merge branch 'dev' (staging/no-tag)

This commit is contained in:
badblocks 2025-05-23 19:41:48 -07:00
commit 35894ab545
No known key found for this signature in database
14 changed files with 229 additions and 122 deletions

0
.envrc Normal file → Executable file
View file

View file

@ -62,6 +62,7 @@ ENV PATH=/app/bin:$PATH
ENV PYTHONPATH=/app ENV PYTHONPATH=/app
ENV PYTHONUNBUFFERED=1 ENV PYTHONUNBUFFERED=1
ENV HOME=/app ENV HOME=/app
ENV DJANGO_SETTINGS_MODULE=pkmntrade_club.django_project.settings
WORKDIR /app WORKDIR /app
@ -94,11 +95,13 @@ COPY --chown=app:app --chmod=700 /manage.py /app/manage.py
ENTRYPOINT ["/entrypoint.sh"] ENTRYPOINT ["/entrypoint.sh"]
RUN --mount=type=cache,target=${CACHE_DIR} \ RUN --mount=type=cache,target=${CACHE_DIR} \
mkdir -p /app/.cursor-server && chown app:app /app /app/.cursor-server mkdir -p /app/.cursor-server && chmod 755 /app/.cursor-server && chown app:app /app /app/.cursor-server
RUN --mount=type=cache,target=${CACHE_DIR} \
mkdir -p /flags && chmod 700 /flags && chown app:app /flags
USER app USER app
EXPOSE 8000 EXPOSE 8000
CMD ["granian", "--interface", "wsgi", "pkmntrade_club.django_project.wsgi:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "1", "--backpressure", "16", "--workers-kill-timeout", "180", "--access-log"] CMD ["granian", "--interface", "wsgi", "pkmntrade_club.django_project.wsgi:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "1", "--backpressure", "16", "--workers-kill-timeout", "180", "--access-log"]
#, "--static-path-mount", "./staticfiles"

View file

@ -1,7 +1,7 @@
services: services:
web: web:
build: . build: .
command: ["django-admin", "runserver", "0.0.0.0:8000"] #command: ["django-admin", "runserver", "0.0.0.0:8000"]
ports: ports:
- 8000:8000 - 8000:8000
restart: always restart: always
@ -10,13 +10,6 @@ services:
# DANGEROUS DUE TO DOCKERFILE PACKAGE BUILDING/INSTALLATION # DANGEROUS DUE TO DOCKERFILE PACKAGE BUILDING/INSTALLATION
# will need to use editable package instead somehow # will need to use editable package instead somehow
#- ./src/pkmntrade_club:/app/lib/python3.12/site-packages/pkmntrade_club:ro #- ./src/pkmntrade_club:/app/lib/python3.12/site-packages/pkmntrade_club:ro
env_file:
- .env
environment:
- DEBUG=true
- PUBLIC_HOST=localhost
- ALLOWED_HOSTS=127.0.0.1,localhost
- DISABLE_CACHE=false
depends_on: depends_on:
db: db:
condition: service_healthy condition: service_healthy
@ -24,13 +17,6 @@ services:
build: . build: .
command: ["celery", "-A", "pkmntrade_club.django_project", "worker", "-l", "INFO", "-B", "-E"] command: ["celery", "-A", "pkmntrade_club.django_project", "worker", "-l", "INFO", "-B", "-E"]
restart: always restart: always
env_file:
- .env
environment:
- DEBUG=true
- PUBLIC_HOST=localhost
- ALLOWED_HOSTS=127.0.0.1,localhost
- DISABLE_CACHE=false
depends_on: depends_on:
db: db:
condition: service_healthy condition: service_healthy

View file

@ -49,7 +49,7 @@ dependencies = [
"django-tailwind-4[reload]==0.1.4", "django-tailwind-4[reload]==0.1.4",
"django-widget-tweaks==1.5.0", "django-widget-tweaks==1.5.0",
"gevent==25.4.1", "gevent==25.4.1",
"granian==2.2.5", "granian==2.3.1",
"gunicorn==23.0.0", "gunicorn==23.0.0",
"idna==3.4", "idna==3.4",
"oauthlib==3.2.2", "oauthlib==3.2.2",

View file

@ -1,5 +1,10 @@
#!/bin/bash #!/bin/bash
if [[ -f /flags/.deployed && "$FORCE_DEPLOY" != "true" ]]; then
echo "*** Previously deployed successfully."
exit 0
fi
echo "*** Running makemigrations --check to make sure migrations are up to date..." echo "*** Running makemigrations --check to make sure migrations are up to date..."
django-admin makemigrations --noinput --check 2>&1 || exit 1 django-admin makemigrations --noinput --check 2>&1 || exit 1
@ -12,4 +17,7 @@ django-admin clear_cache 2>&1
echo "*** Running collectstatic..." echo "*** Running collectstatic..."
django-admin collectstatic -c --no-input 2>&1 django-admin collectstatic -c --no-input 2>&1
echo "*** Marking as deployed..."
touch /flags/.deployed
echo "*** Deployed successfully!" echo "*** Deployed successfully!"

View file

@ -6,12 +6,16 @@ if [ "$1" == "" ]; then
exit; exit;
fi fi
if [ "$DJANGO_SETTINGS_MODULE" == "" ]; then /deploy.sh
echo "Environment variable 'DJANGO_SETTINGS_MODULE' not set. Exiting."
exit; # show command version before running it
else $1 --version
export DJANGO_SETTINGS_MODULE=$DJANGO_SETTINGS_MODULE
if [ "$1" == "granian" ]; then
echo "Appending static files path to granian command (requires granian >= 2.3.0)"
STATIC_ROOT=$(python -c 'import os; import pkmntrade_club; from django.conf import settings; print(settings.STATIC_ROOT)')
set -- "$@" --static-path-mount "$STATIC_ROOT"
fi fi
echo "Environment is correct - executing command: '$@'" echo "Environment is correct - executing command: '$@'"
exec "$@" && exit 0 exec "$@"

View file

@ -21,7 +21,7 @@ services:
--connect-timeout 10 \ --connect-timeout 10 \
--max-time 15 \ --max-time 15 \
--header "Authorization: Bearer ${GATUS_TOKEN}" \ --header "Authorization: Bearer ${GATUS_TOKEN}" \
http://health:8080/api/v1/endpoints/services_database/external?success=$$pg_success&error=$$pg_error; http://health:8080/api/v1/endpoints/services_database/external?success=$$pg_success&error=$$pg_error || true
if [ "$$pg_success" = "true" ]; then if [ "$$pg_success" = "true" ]; then
echo " Database is OK"; echo " Database is OK";
else else
@ -42,7 +42,7 @@ services:
--connect-timeout 10 \ --connect-timeout 10 \
--max-time 15 \ --max-time 15 \
--header "Authorization: Bearer ${GATUS_TOKEN}" \ --header "Authorization: Bearer ${GATUS_TOKEN}" \
http://health:8080/api/v1/endpoints/services_redis/external?success=$$redis_success&error=$$redis_error; http://health:8080/api/v1/endpoints/services_cache/external?success=$$redis_success&error=$$redis_error;
if [ "$$redis_success" = "true" ]; then if [ "$$redis_success" = "true" ]; then
echo " Redis is OK"; echo " Redis is OK";
else else
@ -100,7 +100,7 @@ services:
- ./gatus:/gatus - ./gatus:/gatus
dockergen-gatekeeper: dockergen-gatekeeper:
image: nginxproxy/docker-gen:latest image: nginxproxy/docker-gen:latest
command: -wait 15s -watch /gatekeeper/docker-compose_gatekeeper.template.yml /gatekeeper/docker-compose_gatekeeper.yml -notify-sighup gatekeeper-manager command: -wait 15s -watch /gatekeeper/gatekeepers.template.yml /gatekeeper/gatekeepers.yml -notify-sighup pkmntrade-club-gatekeeper-manager-1
restart: unless-stopped restart: unless-stopped
volumes: volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro - /var/run/docker.sock:/tmp/docker.sock:ro
@ -118,39 +118,107 @@ services:
command: command:
- | - |
set -eu -o pipefail set -eu -o pipefail
apk add --no-cache curl
COMPOSE_FILE_PATH="/srv/pkmntrade-club/docker-compose_gatekeeper.yml" COMPOSE_FILE_PATH="/srv/pkmntrade-club/gatekeepers.yml"
PROJECT_DIR_PATH="/srv/pkmntrade-club" PROJECT_DIR_PATH="/srv/pkmntrade-club"
PROJECT_NAME_TAG="gatekeepers" PROJECT_NAME_TAG="gatekeepers"
TERMINATING="false"
RESTARTING="false"
STARTED="false"
gatekeeper_down() { gatekeeper_down() {
echo "$(date +'%Y-%m-%d %H:%M:%S') [INFO]: Taking gatekeepers down (Project: $$PROJECT_NAME_TAG)..." echo "$(date +'%Y-%m-%d %H:%M:%S') [INFO]: Downing gatekeepers (Project: $$PROJECT_NAME_TAG)..."
cd "$$PROJECT_DIR_PATH" cd "$$PROJECT_DIR_PATH"
if ! docker compose -p "$$PROJECT_NAME_TAG" -f "$$COMPOSE_FILE_PATH" down; then if ! docker compose -p "$$PROJECT_NAME_TAG" -f "$$COMPOSE_FILE_PATH" down; then
echo "$(date +'%Y-%m-%d %H:%M:%S') [WARN]: 'docker compose down' for $$PROJECT_NAME_TAG encountered an issue, but proceeding." echo "$(date +'%Y-%m-%d %H:%M:%S') [WARN]: 'docker compose down' for $$PROJECT_NAME_TAG encountered an issue, but proceeding."
else
STARTED="false"
fi fi
} }
gatekeeper_up() { gatekeeper_up() {
echo "$(date +'%Y-%m-%d %H:%M:%S') [INFO]: Bringing gatekeepers up/updating (Project: $$PROJECT_NAME_TAG, File: $$COMPOSE_FILE_PATH)..." if [ "$$TERMINATING" = "true" ]; then return; fi
echo "$(date +'%Y-%m-%d %H:%M:%S') [INFO]: Upping gatekeepers (Project: $$PROJECT_NAME_TAG, File: $$COMPOSE_FILE_PATH)..."
cd "$$PROJECT_DIR_PATH" cd "$$PROJECT_DIR_PATH"
if ! docker compose -p "$$PROJECT_NAME_TAG" -f "$$COMPOSE_FILE_PATH" up -d --remove-orphans; then if ! docker compose -p "$$PROJECT_NAME_TAG" -f "$$COMPOSE_FILE_PATH" up -d --remove-orphans; then
echo "$(date +'%Y-%m-%d %H:%M:%S') [ERROR]: 'docker compose up' for $$PROJECT_NAME_TAG failed. Will retry." echo "$(date +'%Y-%m-%d %H:%M:%S') [ERROR]: 'docker compose up' for $$PROJECT_NAME_TAG failed. Will retry."
else
STARTED="true"
fi
}
restart_gatekeepers() {
if [ "$$TERMINATING" = "true" -o "$$RESTARTING" = "true" -o "$$STARTED" = "false" ]; then return; fi
RESTARTING="true"
echo "$(date +'%Y-%m-%d %H:%M:%S') [INFO]: Restarting gatekeepers."
gatekeeper_down
gatekeeper_up
echo "$(date +'%Y-%m-%d %H:%M:%S') [INFO]: Gatekeepers restarted."
RESTARTING="false"
}
gatekeeper_healthcheck() {
if [ "$$TERMINATING" = "true" -o "$$RESTARTING" = "true" -o "$$STARTED" = "false" ]; then
echo "$(date +'%Y-%m-%d %H:%M:%S') [INFO]: Gatekeeper Manager is terminating/restarting/not started. Skipping healthcheck."
return 0
fi
ERROR_MSG=""
echo "$(date +'%Y-%m-%d %H:%M:%S') [INFO]: Checking gatekeepers health..."
num_containers=$$(docker ps -q -a --filter "label=gatekeeper" | wc -l)
if [ "$$num_containers" -eq 0 ]; then
ERROR_MSG="No gatekeepers found. Healthcheck failed."
elif [ $(docker ps -q -a --filter "label=gatekeeper" --filter "status=running" | wc -l) -ne "$$num_containers" ]; then
ERROR_MSG="Gatekeeper containers are missing or not running. Healthcheck failed."
else
# check for 200 status code from each gatekeeper container
for container in $$(docker ps -q -a --filter "label=gatekeeper"); do
if [ $$(curl -s -o /dev/null -w "%{http_code}" -H "X-Real-Ip: 127.0.0.1" http://$$container:9090/metrics) -ne 200 ]; then
container_name=$$(docker ps -a --filter "label=gatekeeper" --filter "id=$$container" --format "{{.Names}}")
ERROR_MSG="Gatekeeper container $$container_name is unhealthy. Healthcheck failed."
fi
done
fi
if [ "$$ERROR_MSG" != "" ]; then
echo "$(date +'%Y-%m-%d %H:%M:%S') [ERROR]: $$ERROR_MSG"
curl -s -f -X POST \
--connect-timeout 10 \
--max-time 15 \
--header "Authorization: Bearer ${GATUS_TOKEN}" \
"http://health:8080/api/v1/endpoints/services_gatekeeper/external?success=false&error=$$ERROR_MSG" || true
restart_gatekeepers
return 1
else
echo "$(date +'%Y-%m-%d %H:%M:%S') [INFO]: All gatekeepers are OK/HEALTHY."
curl -s -f -X POST \
--connect-timeout 10 \
--max-time 15 \
--header "Authorization: Bearer ${GATUS_TOKEN}" \
http://health:8080/api/v1/endpoints/services_gatekeeper/external?success=true&error=HEALTHY || true
fi fi
} }
handle_sigterm() { handle_sigterm() {
if [ "$$TERMINATING" = "true" ]; then return; fi
TERMINATING="true"
echo "$(date +'%Y-%m-%d %H:%M:%S') [INFO]: SIGTERM received. Initiating graceful shutdown for gatekeepers." echo "$(date +'%Y-%m-%d %H:%M:%S') [INFO]: SIGTERM received. Initiating graceful shutdown for gatekeepers."
curl -s -f -X POST \
--connect-timeout 10 \
--max-time 15 \
--header "Authorization: Bearer ${GATUS_TOKEN}" \
http://health:8080/api/v1/endpoints/services_gatekeeper/external?success=false&error=SIGTERM%20received.%20Shutting%20down. || true
gatekeeper_down gatekeeper_down
echo "$(date +'%Y-%m-%d %H:%M:%S') [INFO]: Gatekeepers shut down. Gatekeeper Manager exiting." echo "$(date +'%Y-%m-%d %H:%M:%S') [INFO]: Gatekeepers shut down. Gatekeeper Manager exiting."
exit 0 exit 0
} }
handle_sighup() { handle_sighup() {
echo "$(date +'%Y-%m-%d %H:%M:%S') [INFO]: SIGHUP received. Restarting gatekeepers." if [ "$$TERMINATING" = "true" -o "$$RESTARTING" = "true" -o "$$STARTED" = "false" ]; then return; fi
gatekeeper_down echo "$(date +'%Y-%m-%d %H:%M:%S') [INFO]: SIGHUP received."
gatekeeper_up restart_gatekeepers
echo "$(date +'%Y-%m-%d %H:%M:%S') [INFO]: Gatekeepers restarted following SIGHUP."
} }
trap 'handle_sigterm' SIGTERM trap 'handle_sigterm' SIGTERM
@ -158,17 +226,28 @@ services:
echo "$(date +'%Y-%m-%d %H:%M:%S') [INFO]: Gatekeeper Manager started." echo "$(date +'%Y-%m-%d %H:%M:%S') [INFO]: Gatekeeper Manager started."
echo "$(date +'%Y-%m-%d %H:%M:%S') [INFO]: Periodic refresh enabled: $$REFRESH_INTERVAL seconds." echo "$(date +'%Y-%m-%d %H:%M:%S') [INFO]: Periodic refresh enabled: $$REFRESH_INTERVAL seconds. Initial wait started."
while true; do
gatekeeper_up
# 'sleep 60 &' and 'wait $!' allows signals to interrupt the sleep. while [ "$$TERMINATING" = "false" ]; do
sleep $$REFRESH_INTERVAL & # 'sleep x &' and 'wait $!' allows signals to interrupt the sleep.
# '|| true' ensures the loop continues if 'wait' is killed by a handled signal (SIGHUP/SIGTERM) # '|| true' ensures the loop continues if 'wait' is killed by a handled signal (SIGHUP/SIGTERM)
# SIGTERM handler exits completely, so loop won't continue. SIGHUP handler doesn't exit. # SIGTERM handler exits completely, so loop won't continue. SIGHUP handler doesn't exit.
sleep $$REFRESH_INTERVAL &
wait $! || true wait $! || true
echo "$(date +'%Y-%m-%d %H:%M:%S') [INFO]: Periodic refresh triggered." echo "$(date +'%Y-%m-%d %H:%M:%S') [INFO]: Periodic healthcheck and refresh triggered."
if [ ! -f "$$COMPOSE_FILE_PATH" ]; then
echo "$(date +'%Y-%m-%d %H:%M:%S') [ERROR]: Gatekeepers.yml has not been generated after $$REFRESH_INTERVAL seconds. Please check dockergen-gatekeeper is running correctly. Exiting."
exit 1
fi
if gatekeeper_healthcheck && [ "$$RESTARTING" = "false" ]; then
gatekeeper_up
fi
done done
health: health:
image: twinproduction/gatus:latest image: twinproduction/gatus:latest

View file

@ -3,7 +3,6 @@ x-common: &common
restart: always restart: always
env_file: env_file:
- .env - .env
services: services:
web-staging: web-staging:
<<: *common <<: *common
@ -11,7 +10,6 @@ services:
- DEBUG=False - DEBUG=False
- DISABLE_SIGNUPS=True - DISABLE_SIGNUPS=True
- PUBLIC_HOST=staging.pkmntrade.club - PUBLIC_HOST=staging.pkmntrade.club
- ALLOWED_HOSTS=staging.pkmntrade.club,127.0.0.1,pkmntrade-club-web-staging-1,pkmntrade-club-web-staging-2
labels: labels:
- "enable_gatekeeper=true" - "enable_gatekeeper=true"
deploy: deploy:
@ -29,5 +27,4 @@ services:
- DEBUG=False - DEBUG=False
- DISABLE_SIGNUPS=True - DISABLE_SIGNUPS=True
- PUBLIC_HOST=staging.pkmntrade.club - PUBLIC_HOST=staging.pkmntrade.club
- ALLOWED_HOSTS=staging.pkmntrade.club,127.0.0.1,pkmntrade-club-celery-staging-1
command: ["celery", "-A", "pkmntrade_club.django_project", "worker", "-l", "INFO", "-B", "-E"] command: ["celery", "-A", "pkmntrade_club.django_project", "worker", "-l", "INFO", "-B", "-E"]

View file

@ -14,7 +14,6 @@ services:
- DEBUG=False - DEBUG=False
- DISABLE_SIGNUPS=True - DISABLE_SIGNUPS=True
- PUBLIC_HOST=pkmntrade.club - PUBLIC_HOST=pkmntrade.club
- ALLOWED_HOSTS=pkmntrade.club,127.0.0.1,pkmntrade-club-web-1,pkmntrade-club-web-2,pkmntrade-club-web-3,pkmntrade-club-web-4
labels: labels:
- "enable_gatekeeper=true" - "enable_gatekeeper=true"
deploy: deploy:
@ -33,5 +32,4 @@ services:
# - DEBUG=False # - DEBUG=False
# - DISABLE_SIGNUPS=True # - DISABLE_SIGNUPS=True
# - PUBLIC_HOST=pkmntrade.club # - PUBLIC_HOST=pkmntrade.club
# - ALLOWED_HOSTS=pkmntrade.club,127.0.0.1,pkmntrade-club-celery-1,pkmntrade-club-celery-2
# command: ["celery", "-A", "pkmntrade_club.django_project", "worker", "-l", "INFO", "-B", "-E"] # command: ["celery", "-A", "pkmntrade_club.django_project", "worker", "-l", "INFO", "-B", "-E"]

View file

@ -25,6 +25,14 @@ services:
- .env - .env
environment: environment:
- TARGET=http://{{ $container.Name }}{{ $port }} - TARGET=http://{{ $container.Name }}{{ $port }}
{{ if eq $serviceLabel "web" }}
- TARGET_HOST=pkmntrade.club # pass this host to django, which checks it with ALLOWED_HOSTS
{{ end }}
{{ if eq $serviceLabel "web-staging" }}
- TARGET_HOST=staging.pkmntrade.club # pass this host to django, which checks it with ALLOWED_HOSTS
{{ end }}
labels:
- gatekeeper=true
networks: networks:
default: default:
aliases: aliases:

View file

@ -13,7 +13,12 @@ external-endpoints:
token: "${GATUS_TOKEN}" token: "${GATUS_TOKEN}"
alerts: alerts:
- type: email - type: email
- name: Redis - name: Cache
group: Services
token: "${GATUS_TOKEN}"
alerts:
- type: email
- name: Gatekeeper
group: Services group: Services
token: "${GATUS_TOKEN}" token: "${GATUS_TOKEN}"
alerts: alerts:
@ -68,8 +73,8 @@ endpoints:
- "[DNS_RCODE] == NOERROR" - "[DNS_RCODE] == NOERROR"
alerts: alerts:
- type: email - type: email
- name: HAProxy - name: Load Balancer
group: Load Balancer group: Services
url: "http://loba/" url: "http://loba/"
interval: 60s interval: 60s
conditions: conditions:
@ -78,8 +83,8 @@ endpoints:
alerts: alerts:
- type: email - type: email
- name: Feedback - name: Feedback
group: Services group: Main
url: "http://feedback:3000/" url: "http://pkmntrade-club-feedback-1:3000/"
interval: 60s interval: 60s
conditions: conditions:
- "[STATUS] == 200" - "[STATUS] == 200"
@ -107,6 +112,8 @@ endpoints:
- name: "Web Worker {{ $containerNumber }}" - name: "Web Worker {{ $containerNumber }}"
group: Main group: Main
url: "http://{{ $container.Name }}:8000/health/" url: "http://{{ $container.Name }}:8000/health/"
headers:
Host: "pkmntrade.club"
interval: 60s interval: 60s
conditions: conditions:
- "[STATUS] == 200" - "[STATUS] == 200"
@ -120,6 +127,8 @@ endpoints:
- name: "Web Worker {{ $containerNumber }}" - name: "Web Worker {{ $containerNumber }}"
group: Staging group: Staging
url: "http://{{ $container.Name }}:8000/health/" url: "http://{{ $container.Name }}:8000/health/"
headers:
Host: "staging.pkmntrade.club"
interval: 60s interval: 60s
conditions: conditions:
- "[STATUS] == 200" - "[STATUS] == 200"

View file

@ -34,16 +34,22 @@ backend basic_check
backend pkmntrade.club backend pkmntrade.club
balance leastconn balance leastconn
http-request set-header Host pkmntrade.club
server-template gatekeeper-web- 4 gatekeeper-web:8000 check resolvers docker_resolver init-addr libc,none server-template gatekeeper-web- 4 gatekeeper-web:8000 check resolvers docker_resolver init-addr libc,none
backend staging.pkmntrade.club backend staging.pkmntrade.club
balance leastconn balance leastconn
http-request set-header Host staging.pkmntrade.club
server-template gatekeeper-web-staging- 4 gatekeeper-web-staging:8000 check resolvers docker_resolver init-addr libc,none server-template gatekeeper-web-staging- 4 gatekeeper-web-staging:8000 check resolvers docker_resolver init-addr libc,none
backend feedback.pkmntrade.club backend feedback.pkmntrade.club
balance leastconn
http-request set-header Host feedback.pkmntrade.club
server-template gatekeeper-feedback- 4 gatekeeper-feedback:8000 check resolvers docker_resolver init-addr libc,none server-template gatekeeper-feedback- 4 gatekeeper-feedback:8000 check resolvers docker_resolver init-addr libc,none
backend health.pkmntrade.club backend health.pkmntrade.club
balance leastconn
http-request set-header Host health.pkmntrade.club
server-template gatekeeper-health- 4 gatekeeper-health:8000 check resolvers docker_resolver init-addr libc,none server-template gatekeeper-health- 4 gatekeeper-health:8000 check resolvers docker_resolver init-addr libc,none
#EOF - trailing newline required #EOF - trailing newline required

View file

@ -5,8 +5,27 @@ import os
import logging import logging
import sys import sys
# set default values to dev values for environment variables
env = environ.Env( env = environ.Env(
DEBUG=(bool, False) DEBUG=(bool, False), # MUST STAY FALSE FOR DEFAULT FOR SECURITY REASONS (e.g. if app can't access .env, prevent showing debug output)
DISABLE_SIGNUPS=(bool, False),
DISABLE_CACHE=(bool, False),
DJANGO_DATABASE_URL=(str, 'postgresql://postgres@db:5432/postgres?sslmode=disable'),
DJANGO_EMAIL_HOST=(str, ''),
DJANGO_EMAIL_PORT=(int, 587),
DJANGO_EMAIL_USER=(str, ''),
DJANGO_EMAIL_PASSWORD=(str, ''),
DJANGO_EMAIL_USE_TLS=(bool, True),
DJANGO_DEFAULT_FROM_EMAIL=(str, ''),
SECRET_KEY=(str, '0000000000000000000000000000000000000000000000000000000000000000'),
ALLOWED_HOSTS=(str, 'localhost,127.0.0.1'),
PUBLIC_HOST=(str, 'localhost'),
ACCOUNT_EMAIL_VERIFICATION=(str, 'none'),
DJANGO_SETTINGS_MODULE=(str, 'pkmntrade_club.django_project.settings'),
SCHEME=(str, 'http'),
REDIS_URL=(str, 'redis://redis:6379'),
CACHE_TIMEOUT=(int, 604800),
TIME_ZONE=(str, 'America/Los_Angeles'),
) )
LOGGING = { LOGGING = {
@ -59,6 +78,13 @@ BASE_DIR = Path(__file__).resolve().parent.parent
# Take environment variables from .env file # Take environment variables from .env file
environ.Env.read_env(os.path.join(BASE_DIR, '.env')) environ.Env.read_env(os.path.join(BASE_DIR, '.env'))
SCHEME = env('SCHEME')
PUBLIC_HOST = env('PUBLIC_HOST')
REDIS_URL = env('REDIS_URL')
CACHE_TIMEOUT = env('CACHE_TIMEOUT')
DISABLE_SIGNUPS = env('DISABLE_SIGNUPS')
DISABLE_CACHE = env('DISABLE_CACHE')
# Quick-start development settings - unsuitable for production # Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/dev/howto/deployment/checklist/ # See https://docs.djangoproject.com/en/dev/howto/deployment/checklist/
@ -66,11 +92,6 @@ environ.Env.read_env(os.path.join(BASE_DIR, '.env'))
# SECURITY WARNING: keep the secret key used in production secret! # SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = env('SECRET_KEY') SECRET_KEY = env('SECRET_KEY')
# Scaleway Secret Key
SCW_SECRET_KEY = env('SCW_SECRET_KEY')
DISABLE_SIGNUPS = env('DISABLE_SIGNUPS', default=False)
# https://docs.djangoproject.com/en/dev/ref/settings/#debug # https://docs.djangoproject.com/en/dev/ref/settings/#debug
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = env('DEBUG') DEBUG = env('DEBUG')
@ -85,9 +106,7 @@ try:
except Exception: except Exception:
logging.getLogger(__name__).info(f"Error determining server hostname for allowed hosts.") logging.getLogger(__name__).info(f"Error determining server hostname for allowed hosts.")
PUBLIC_HOST = env('PUBLIC_HOST') CSRF_TRUSTED_ORIGINS = [f"{SCHEME}://{PUBLIC_HOST}"]
CSRF_TRUSTED_ORIGINS = [f"https://{PUBLIC_HOST}"]
FIRST_PARTY_APPS = [ FIRST_PARTY_APPS = [
'pkmntrade_club.accounts', 'pkmntrade_club.accounts',
@ -140,9 +159,9 @@ if DEBUG:
TAILWIND_APP_NAME = 'theme' TAILWIND_APP_NAME = 'theme'
META_SITE_NAME = 'PKMN Trade Club' META_SITE_NAME = 'PKMN Trade Club'
META_SITE_PROTOCOL = 'https' META_SITE_PROTOCOL = SCHEME
META_USE_SITES = True META_USE_SITES = True
META_IMAGE_URL = f'https://{PUBLIC_HOST}/' META_IMAGE_URL = f'{SCHEME}://{PUBLIC_HOST}/'
# https://docs.djangoproject.com/en/dev/ref/settings/#middleware # https://docs.djangoproject.com/en/dev/ref/settings/#middleware
MIDDLEWARE = [ MIDDLEWARE = [
@ -228,7 +247,7 @@ AUTH_PASSWORD_VALIDATORS = [
LANGUAGE_CODE = "en-us" LANGUAGE_CODE = "en-us"
# https://docs.djangoproject.com/en/dev/ref/settings/#time-zone # https://docs.djangoproject.com/en/dev/ref/settings/#time-zone
TIME_ZONE = "UTC" TIME_ZONE = env('TIME_ZONE')
# https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-USE_I18N # https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-USE_I18N
USE_I18N = True USE_I18N = True
@ -281,19 +300,14 @@ CRISPY_TEMPLATE_PACK = "tailwind"
# https://docs.djangoproject.com/en/dev/ref/settings/#email-backend # https://docs.djangoproject.com/en/dev/ref/settings/#email-backend
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
# EMAIL_HOST = "smtp.resend.com" EMAIL_HOST = env('DJANGO_EMAIL_HOST')
# EMAIL_PORT = 587 EMAIL_PORT = env('DJANGO_EMAIL_PORT')
# EMAIL_HOST_USER = "resend" EMAIL_HOST_USER = env('DJANGO_EMAIL_USER')
# EMAIL_HOST_PASSWORD = RESEND_API_KEY EMAIL_HOST_PASSWORD = env('DJANGO_EMAIL_PASSWORD')
# EMAIL_USE_TLS = True EMAIL_USE_TLS = env('DJANGO_EMAIL_USE_TLS')
EMAIL_HOST = "smtp.tem.scaleway.com"
EMAIL_PORT = 587
EMAIL_HOST_USER = "dd2cd354-de6d-4fa4-bfe8-31c961cb4e90"
EMAIL_HOST_PASSWORD = SCW_SECRET_KEY
EMAIL_USE_TLS = True
# https://docs.djangoproject.com/en/dev/ref/settings/#default-from-email # https://docs.djangoproject.com/en/dev/ref/settings/#default-from-email
DEFAULT_FROM_EMAIL = "noreply@pkmntrade.club" DEFAULT_FROM_EMAIL = env('DJANGO_DEFAULT_FROM_EMAIL')
# django-debug-toolbar # django-debug-toolbar
# https://django-debug-toolbar.readthedocs.io/en/latest/installation.html # https://django-debug-toolbar.readthedocs.io/en/latest/installation.html
@ -302,7 +316,7 @@ INTERNAL_IPS = [
"127.0.0.1", "127.0.0.1",
] ]
# for docker + debug toolbar # for docker
hostname, _, ips = socket.gethostbyname_ex(socket.gethostname()) hostname, _, ips = socket.gethostbyname_ex(socket.gethostname())
for ip in ips: for ip in ips:
INTERNAL_IPS.append(ip) INTERNAL_IPS.append(ip)
@ -337,7 +351,7 @@ ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_EMAIL_VERIFICATION = env('ACCOUNT_EMAIL_VERIFICATION') ACCOUNT_EMAIL_VERIFICATION = env('ACCOUNT_EMAIL_VERIFICATION')
ACCOUNT_EMAIL_NOTIFICATIONS = True ACCOUNT_EMAIL_NOTIFICATIONS = True
ACCOUNT_EMAIL_UNKNOWN_ACCOUNTS = False ACCOUNT_EMAIL_UNKNOWN_ACCOUNTS = False
ACCOUNT_DEFAULT_HTTP_PROTOCOL = "https" ACCOUNT_DEFAULT_HTTP_PROTOCOL = SCHEME
ACCOUNT_LOGIN_ON_EMAIL_CONFIRMATION = True ACCOUNT_LOGIN_ON_EMAIL_CONFIRMATION = True
ACCOUNT_USERNAME_MIN_LENGTH = 2 ACCOUNT_USERNAME_MIN_LENGTH = 2
ACCOUNT_CHANGE_EMAIL = True ACCOUNT_CHANGE_EMAIL = True
@ -353,14 +367,9 @@ SOCIALACCOUNT_EMAIL_AUTHENTICATION = False
SOCIALACCOUNT_EMAIL_AUTHENTICATION_AUTO_CONNECT = False SOCIALACCOUNT_EMAIL_AUTHENTICATION_AUTO_CONNECT = False
SOCIALACCOUNT_ONLY = False SOCIALACCOUNT_ONLY = False
CACHE_TIMEOUT = 604800 # 1 week # auto-detection doesn't work properly sometimes, so we'll just use the DEBUG setting
DEBUG_TOOLBAR_CONFIG = {"SHOW_TOOLBAR_CALLBACK": lambda request: DEBUG} DEBUG_TOOLBAR_CONFIG = {"SHOW_TOOLBAR_CALLBACK": lambda request: DEBUG}
REDIS_URL = "redis://redis:6379"
DISABLE_CACHE = env('DISABLE_CACHE', default=DEBUG)
if DISABLE_CACHE: if DISABLE_CACHE:
CACHES = { CACHES = {
"default": { "default": {
@ -377,6 +386,6 @@ else:
CELERY_BROKER_URL = REDIS_URL CELERY_BROKER_URL = REDIS_URL
CELERY_RESULT_BACKEND = REDIS_URL CELERY_RESULT_BACKEND = REDIS_URL
CELERY_TIMEZONE = "America/Los_Angeles" CELERY_TIMEZONE = TIME_ZONE
CELERY_ENABLE_UTC = True CELERY_ENABLE_UTC = True
CELERY_BEAT_SCHEDULER = "django_celery_beat.schedulers:DatabaseScheduler" CELERY_BEAT_SCHEDULER = "django_celery_beat.schedulers:DatabaseScheduler"

74
uv.lock generated
View file

@ -104,14 +104,14 @@ wheels = [
[[package]] [[package]]
name = "click" name = "click"
version = "8.2.0" version = "8.2.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" }, { name = "colorama", marker = "sys_platform == 'win32'" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/cd/0f/62ca20172d4f87d93cf89665fbaedcd560ac48b465bd1d92bfc7ea6b0a41/click-8.2.0.tar.gz", hash = "sha256:f5452aeddd9988eefa20f90f05ab66f17fce1ee2a36907fd30b05bbb5953814d", size = 235857 } sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/a2/58/1f37bf81e3c689cc74ffa42102fa8915b59085f54a6e4a80bc6265c0f6bf/click-8.2.0-py3-none-any.whl", hash = "sha256:6b303f0b2aa85f1cb4e5303078fadcbcd4e476f114fab9b5007005711839325c", size = 102156 }, { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215 },
] ]
[[package]] [[package]]
@ -417,41 +417,41 @@ wheels = [
[[package]] [[package]]
name = "granian" name = "granian"
version = "2.2.5" version = "2.3.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "click" }, { name = "click" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/d8/59/064df25d63fbfc27c7ec48c1d0efe3fffe6b70b8d3d03c59f136f390cad7/granian-2.2.5.tar.gz", hash = "sha256:90b832270b6b03a41b1706051113a3ffcca307860d5c864dc1f47ea290fc4b58", size = 94178 } sdist = { url = "https://files.pythonhosted.org/packages/82/0f/04aacf7ec30ba04018c7be761e5a6964d73cf82da5969b35e912e8e4e662/granian-2.3.1.tar.gz", hash = "sha256:5e9bddf3580e8ffccfaa97196672a6351630c959c37eb2498772504759a9f1ba", size = 100302 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/7c/90/84dd92375dfb33876c82a05e1942c8800931b7c439299d5e1485ef7216c8/granian-2.2.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:2ff8916ba37490fef696a461c2a43498b8865b3dcfa73e3dbff9d72ea2f6fbb9", size = 2848961 }, { url = "https://files.pythonhosted.org/packages/26/cd/30917d357be84957b3e8a1a82ac45e14fdfeb8e1afcd82ffe50a94f759f1/granian-2.3.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9067454777e14b8d3d5ad2d3e5f11ee2cc1ae18c09455908d44e6b5a0d018207", size = 3047974 },
{ url = "https://files.pythonhosted.org/packages/2e/0d/e62d645ec01ac0b6dd3860949eda41de4e2ec1b014dc50b11a34989d4c4d/granian-2.2.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6cb56eccde7fe1c94ffb9ae60d516221c57b2e29224b6c6c2484ded044852320", size = 2592306 }, { url = "https://files.pythonhosted.org/packages/27/a5/c3752565733da327441e602e6dddafa79219e752c25ee70416b48df30321/granian-2.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4851094be97758f9d72a7d304021edeaf902818a5635b13ea1784092090098d8", size = 2722452 },
{ url = "https://files.pythonhosted.org/packages/b8/94/f955777a6d75c79198d8ca34132d04698dd0bf9b833833646e77c4fb164f/granian-2.2.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1883c7e3e1b1154ba49d1317e42a660b2c12a7bda8e4bc79b9279904db01d48b", size = 3114729 }, { url = "https://files.pythonhosted.org/packages/77/0a/38d6eb581c43cffb5b4a87a2bbd8b3f39e0874785d95db5d94c01f259809/granian-2.3.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7c6f805468151a756142d59fab8414c986bdbdeea26983178d5e3c00705aaba6", size = 3365275 },
{ url = "https://files.pythonhosted.org/packages/a4/9c/814f88c8bf4eb1b9758bacf38838c8d3de3deb9c51b8d7ecdf5dd524988a/granian-2.2.5-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84ce4b171e3dd10a8d1b0ddf6a09665faae257ca5e35394af0784d1682252903", size = 2877067 }, { url = "https://files.pythonhosted.org/packages/33/44/5fa5aab9a1cf27295bee81e22ecf758adef68c12244c0fd8d9d82da350e2/granian-2.3.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:300954af2a155d8827c6c7af45d8bff8c080303c23fac502e21c34cfb3f92de1", size = 3001384 },
{ url = "https://files.pythonhosted.org/packages/3b/e9/aa321896f8ce46e370a3b52dbd4104d3a145e47884cb885da1492bb20486/granian-2.2.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2cdded34f6a0f9f4bdbb26535c4b16312b38b7edb799b39e2282f57b605919ea", size = 3049879 }, { url = "https://files.pythonhosted.org/packages/21/25/df592394d957933dbe905510dc4ad35141ea3e49fd4e562bc529727a8f44/granian-2.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73659a68149d159cfadcf44fcf6cdb91b027aa4ebb7ad561a9febbfaaecc903b", size = 3215845 },
{ url = "https://files.pythonhosted.org/packages/e4/b0/14f73043a7762138681a07d7bf18125e7a7d7ba5e2b96406ccf281ad3251/granian-2.2.5-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:41f90d5123c1d772c24dfa552551831cd96742f72be26d550d3ac0bae733e870", size = 2960461 }, { url = "https://files.pythonhosted.org/packages/ef/67/9213bf996d0e687939924468615762d106fd38f8c098f34266648f465d2b/granian-2.3.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3fa062db9d6fe7e57aa9c0043e30d50e1ee9fcf4226768b1b13a4fddef81d761", size = 3163131 },
{ url = "https://files.pythonhosted.org/packages/f5/7d/c004df81422bfe1857f768a98550c8f1017140f00a6d9179e37ce29086dc/granian-2.2.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:fadb34659db0e4eaba19e3d3547eaa4f75a64a63f222a57df7badcc17d3842d9", size = 2865627 }, { url = "https://files.pythonhosted.org/packages/25/de/1829c71fd0cba459a9bfc998138ca3ff18f8b913c9ae3c3a3c8c675ceb0c/granian-2.3.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4d00d26d7d02436ca2d1a26b6390710bea6d98cd519927835b04991043777852", size = 3139498 },
{ url = "https://files.pythonhosted.org/packages/64/31/70bbfef65e5612f790d2b7140309ccde8736744203b78ef281288b6f201a/granian-2.2.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:bbeeeb96e7148cc36afa56f652da24f65098bd5e64a528ce894f60ab2db85ff7", size = 3128615 }, { url = "https://files.pythonhosted.org/packages/e7/8a/ce0adbeefcd7a78981d6a4f99709d82344a5c76d163d741547e9cc6f864e/granian-2.3.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:bd35d898c7be876924e73b13004ccee20b6bc071bf851c3d7eb029f01be22306", size = 3460123 },
{ url = "https://files.pythonhosted.org/packages/5e/5c/179a7f3f0c1c46ccaee8ef9b78dd679db304461c2538907e826c20a0025d/granian-2.2.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2ae5587061a95b936ecaaf337e1aff35d19e27c1872d09e1a86debf1010841f8", size = 2997155 }, { url = "https://files.pythonhosted.org/packages/58/f1/1918ff96843125ae12480eb7430692fa3243e42206b263918b087c34852c/granian-2.3.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5979218d189e8da8de50a690c7399e1f0b052419c0b10dd20210ec73dfe29f83", size = 3273879 },
{ url = "https://files.pythonhosted.org/packages/a6/99/331354780b32f1fc626e510856e43d094fe68e6ac101805cef9351e3078f/granian-2.2.5-cp312-cp312-win_amd64.whl", hash = "sha256:fc1fa600bf0be3e8a3e2a49fb013aa9edf740dbf1ab14a19cad75088bd44dae4", size = 2586303 }, { url = "https://files.pythonhosted.org/packages/3b/14/bc6f049852d26045839dc6bddfd06ca6efa8966b939fa4c747ad7c9ab9bf/granian-2.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:878dea8b920a52d7ab36ee8110f960a8a2dde1cb0d43331bf71e1815f1051628", size = 2766821 },
{ url = "https://files.pythonhosted.org/packages/b7/61/652d9817f6dff310950ab835b8838c44a370fa5c3ac8f997f4ec2738a403/granian-2.2.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:00d0d1797df1e0c3c9e438670e0f1f27500efef543ced42415d821e6162f884e", size = 2848540 }, { url = "https://files.pythonhosted.org/packages/e2/c1/71c4a64ca6e65e390ba82270c967318956ea67ed1467f68fc1bd236cc338/granian-2.3.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5e97505ba05f73d76669f732221d01c1c69b0ce01384db872d0b0c240cc422e4", size = 3047647 },
{ url = "https://files.pythonhosted.org/packages/f3/50/c63b8b7d4951be43ba5f1c9d3e67f9fde1ddddaca61164ab7ae70f3405c3/granian-2.2.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:05d0852938a40948ce48a14a0186c9757b738e2715bd817a7931cb5b65aff4cb", size = 2591960 }, { url = "https://files.pythonhosted.org/packages/1b/f7/e1135ee1f9b6188438ca0b406f081b040ebf5c8bcd290b8a2086c4e1cdf3/granian-2.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8f0add56a7188830e82ac76bc8c11ab758edaec889f6f744023d4cd0ac45a908", size = 2721628 },
{ url = "https://files.pythonhosted.org/packages/23/c5/631c10134ced73dfcf03f3ba1157aa02dffa1d30cd5ec3b85a5d469c7090/granian-2.2.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:19b6817f25f1c29858e06c2ded96d16734ebb5c7d0f2d29f71c0e6e3da240906", size = 3113616 }, { url = "https://files.pythonhosted.org/packages/ad/d8/73d953e94c7d62c60db7c0ae8448bed578fdbd85b6aa6d939f52541da266/granian-2.3.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:006fd310d23e904802ea293abdc909c1871762f955deeb4c32232f7ddec37a3f", size = 3364877 },
{ url = "https://files.pythonhosted.org/packages/64/d2/7015aa7b6faedccb1498acd0b2f838c1cf15b13faa4052077b3a82d7035c/granian-2.2.5-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a6a79710035d5c5964e3f4a528e0d9b74de5d73a69b1ea48142804469a4c635f", size = 2876933 }, { url = "https://files.pythonhosted.org/packages/a3/5e/a81c96fb365cee2395092250d97f600b6bc4b477702f98e1befbef27f937/granian-2.3.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:172ed6e87b588eb54dfaf3a39a486b06c82e0accbe3b86427333ea3a57c9b2c9", size = 3001067 },
{ url = "https://files.pythonhosted.org/packages/6b/fb/284b5fee9630f512c1ba9f54992f321a9f3b29e1d9c71199fb3cd700eb1a/granian-2.2.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a8b508b785e0a68593b111e598a199567c6fb98841cbd9cd1b5a10baa4cc13d", size = 3049641 }, { url = "https://files.pythonhosted.org/packages/40/b0/9270bf7d1b612923d14d9783dde0d065afec62146753a2d64f17ef49e955/granian-2.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:413617122d940bbcf793bd1a9ba6a0fabadd5ba75b03acf99c101f735030dc0e", size = 3215182 },
{ url = "https://files.pythonhosted.org/packages/71/c5/6e92f8d897017b53ac2e9608f268eccfa268433179dda5f7f6c6e87d71b6/granian-2.2.5-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:9c6f1f4351ccf9783db6d6da022f1ba83ef4c83c2d26f52ca5d30acf5fbac2df", size = 2960064 }, { url = "https://files.pythonhosted.org/packages/e3/27/a1166372c0f40fde0ea3778e2ddacbf752d4e1ce3a2ecb49b5e100c7fbaf/granian-2.3.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:a7a9b93e7dd2f63a1762343c6d396eef52163fb2cea044569102ae41fa3fd607", size = 3163178 },
{ url = "https://files.pythonhosted.org/packages/43/c7/86422d387da46eb956660d9a1fd12da07c165bd1033fc32badee854e4797/granian-2.2.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:ac26b7933e3302b526d481b7c61f017288e06eb56bf9168133f649097b2ce5ab", size = 2865506 }, { url = "https://files.pythonhosted.org/packages/57/cc/53d257346b9274001141421fca0436df67662634dfdd9f6ac5a595737804/granian-2.3.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:b6eea65f77527aeb5deb75186c90063e4368a94f7076aa9495067804f06d0345", size = 3138821 },
{ url = "https://files.pythonhosted.org/packages/e8/68/f6e5f9b087e1ede11fcd4dbb8d70bff8eed4f9b5ea691863035292ec9d39/granian-2.2.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:4950e77678378995df3b2be5544179ae5757a3ab6079272f14a161e14f0fe1eb", size = 3128304 }, { url = "https://files.pythonhosted.org/packages/9e/79/a1f50daf41dc1a50384a359dcd1f02a844c0a9b4f009a5d5c399c1893a9a/granian-2.3.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:d1ae09ae5424ca5e57dbea660088878aca858f176b2ddf26dc5bf685b939b567", size = 3460024 },
{ url = "https://files.pythonhosted.org/packages/40/31/65595f29a42fb7b6639ca4566d547219655796c45ad372cba7168dff2689/granian-2.2.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:928389b89ffe88af96fbd8120fc3cb64afe9a62797000f6e7e6ff88ff5612ccc", size = 2996875 }, { url = "https://files.pythonhosted.org/packages/e7/3a/78038ab237eda59707d0f0e0fae750ff21db543e7175dfb7ac3b87a87b7f/granian-2.3.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:8c10fdee7afee4d8b69a92f937552c94b979521e6f0169bb300c9612a85b9989", size = 3273042 },
{ url = "https://files.pythonhosted.org/packages/bd/76/6435e413702cc80963044627f96e03c49110bdf86e11a571e76560df5edc/granian-2.2.5-cp313-cp313-win_amd64.whl", hash = "sha256:db1f3c2ae4496e82803014587b822c702d5ea263c35a8edf1a2b098ee9459c9a", size = 2586059 }, { url = "https://files.pythonhosted.org/packages/62/2c/ab47958d0d808fda5659535a30214ed24622d89190f37fa00d2200e88fb5/granian-2.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:e58d2945ab1f99a5f2f804b9d552b337cccf77620dd17ddda94b0baaff0d78ef", size = 2766257 },
{ url = "https://files.pythonhosted.org/packages/a1/3e/fa02abd294ddf5e0e432c01727cc76a931d030e4f24141cfdcdfb078357a/granian-2.2.5-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:f0b688402347e646a2d2b95114eef2eb786ec4c9cb747157e7e892f809e0bb3f", size = 2697330 }, { url = "https://files.pythonhosted.org/packages/b2/58/5ef889557401cd01d9f4380dc4a23ee679d835b51f84956d06b97b4bcb8d/granian-2.3.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:f662331ff69802ffdc02160feadb1a347c652afe8192739d4adf34da6cd1bbff", size = 2999112 },
{ url = "https://files.pythonhosted.org/packages/e9/c8/a1dfaec4b6308e47a90c3e1920f681db36449829be445fef653e8ef7d3fa/granian-2.2.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e8fd8688c79fd7d9dec162b5217869288c1da64ce26518d9fbb69d8f8e97ac9", size = 2466298 }, { url = "https://files.pythonhosted.org/packages/13/70/4363eb6ac164063af7d322691be221477d127c6c6986a339688c32dbd1d1/granian-2.3.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3941e11bcb018cab31ed9a597c67458d278db1469242775e28711d5a3c0be481", size = 2667844 },
{ url = "https://files.pythonhosted.org/packages/c6/f8/ea86317f6582be1b3ac6b29781631ae69c5d4693e5da9467fd9fb18abe02/granian-2.2.5-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12fe0a5f37affa90d2d576b9a7c4e1bbe18ff4cce59f6cd05d33375e6d5b4b5a", size = 2816442 }, { url = "https://files.pythonhosted.org/packages/53/dc/bdcd9f18f7e070d17236c685cd56ec5834a088e8f885de671bd13c608176/granian-2.3.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d53338bb064586e929cece3611d1a49b398ac328f87604e56eda7754c2d0c55", size = 3076031 },
{ url = "https://files.pythonhosted.org/packages/dd/26/fd6d5d19ce5a4a149cc93c05d7522ce90ee6728c56b13035a2d5259404bc/granian-2.2.5-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:48a43bf791ec7ec8c75cf538f5957875aedc5b4653688c9292887911738d3f51", size = 2735825 }, { url = "https://files.pythonhosted.org/packages/74/18/9dbc3c4b7a14c3eeecac702a8d351e4c1220c89e99ffe7f0211e856f3c54/granian-2.3.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:16941818186789804da8b0837130117ca310f88d419856d8df2655ccae27f670", size = 3028926 },
{ url = "https://files.pythonhosted.org/packages/06/34/148a6f3918dbb71824845edbe2a6d8512a52ae2a8c323a9071002a68d6d1/granian-2.2.5-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:bb44abd371bf054626aa750ad25dfe6b17216a0dbf10faa4f6a61a2fea57eaf6", size = 2857911 }, { url = "https://files.pythonhosted.org/packages/9b/b5/7b72ada8a04203c4e9165a3ba7bf266568bf0507ea40c170f96566e0b390/granian-2.3.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:8265480fc80cf0546af509f9f9d27519af26038fbd586081cdd3641d4dd3f44e", size = 3130077 },
{ url = "https://files.pythonhosted.org/packages/28/1c/6c0c5aeae2a090ac046065944863fb76608c6b09c5249fda46148391b128/granian-2.2.5-cp313-cp313t-musllinux_1_1_armv7l.whl", hash = "sha256:4cdccad590be2183eed4e11f4aef7e62c5326df777e4aaefceecb23edea474ad", size = 3118748 }, { url = "https://files.pythonhosted.org/packages/79/34/2395be45fea5e818a5ba0b8871c0fb5776e23f664fdd05a9b00849757314/granian-2.3.1-cp313-cp313t-musllinux_1_1_armv7l.whl", hash = "sha256:0bf2ace1107950110e7da5cca88ecca1f97bb5ef7398c6bc9c95bd0787f0edac", size = 3450662 },
{ url = "https://files.pythonhosted.org/packages/1a/34/22ada66b585c9a3076c63777491dc6daf1773a86cb262a613cd9af3cb24f/granian-2.2.5-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:0ea5be0b02b78a024fc730cf2a271e58ec553aa39b0b43bdb11492c4c82024ba", size = 2989738 }, { url = "https://files.pythonhosted.org/packages/44/c3/9a037020e26ede18b8570f559254911722875ae129293337dc0170ec7c0e/granian-2.3.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:4a08c567a2472fdabd49c1564c560ffe1734e8acd1e0fc3027907296f36434fc", size = 3264479 },
{ url = "https://files.pythonhosted.org/packages/7f/6e/6063f3a44e20dcfa838467b5a3358b907e367edf3596056f86abed532085/granian-2.2.5-cp313-cp313t-win_amd64.whl", hash = "sha256:3e83de670fd6c75b405f28f62053d3477433650f102cb88e6582df6acced0a6c", size = 2510704 }, { url = "https://files.pythonhosted.org/packages/cd/32/660601986c5b5815d13399e9c3b6e84a119be010c974d32d505eb1ef4c7e/granian-2.3.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d828917559a53ff9581ac4a475a508d7b6de7abebb256a27543af8363eb7c844", size = 2792073 },
] ]
[[package]] [[package]]
@ -655,7 +655,7 @@ requires-dist = [
{ name = "django-tailwind-4", extras = ["reload"], specifier = "==0.1.4" }, { name = "django-tailwind-4", extras = ["reload"], specifier = "==0.1.4" },
{ name = "django-widget-tweaks", specifier = "==1.5.0" }, { name = "django-widget-tweaks", specifier = "==1.5.0" },
{ name = "gevent", specifier = "==25.4.1" }, { name = "gevent", specifier = "==25.4.1" },
{ name = "granian", specifier = "==2.2.5" }, { name = "granian", specifier = "==2.3.1" },
{ name = "gunicorn", specifier = "==23.0.0" }, { name = "gunicorn", specifier = "==23.0.0" },
{ name = "idna", specifier = "==3.4" }, { name = "idna", specifier = "==3.4" },
{ name = "oauthlib", specifier = "==3.2.2" }, { name = "oauthlib", specifier = "==3.2.2" },
@ -870,11 +870,11 @@ wheels = [
[[package]] [[package]]
name = "setuptools" name = "setuptools"
version = "80.7.1" version = "80.8.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/9e/8b/dc1773e8e5d07fd27c1632c45c1de856ac3dbf09c0147f782ca6d990cf15/setuptools-80.7.1.tar.gz", hash = "sha256:f6ffc5f0142b1bd8d0ca94ee91b30c0ca862ffd50826da1ea85258a06fd94552", size = 1319188 } sdist = { url = "https://files.pythonhosted.org/packages/8d/d2/ec1acaaff45caed5c2dedb33b67055ba9d4e96b091094df90762e60135fe/setuptools-80.8.0.tar.gz", hash = "sha256:49f7af965996f26d43c8ae34539c8d99c5042fbff34302ea151eaa9c207cd257", size = 1319720 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/a1/18/0e835c3a557dc5faffc8f91092f62fc337c1dab1066715842e7a4b318ec4/setuptools-80.7.1-py3-none-any.whl", hash = "sha256:ca5cc1069b85dc23070a6628e6bcecb3292acac802399c7f8edc0100619f9009", size = 1200776 }, { url = "https://files.pythonhosted.org/packages/58/29/93c53c098d301132196c3238c312825324740851d77a8500a2462c0fd888/setuptools-80.8.0-py3-none-any.whl", hash = "sha256:95a60484590d24103af13b686121328cc2736bee85de8936383111e421b9edc0", size = 1201470 },
] ]
[[package]] [[package]]