diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..d1bcf0d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,33 @@ +# Ignore VCS +.git +.gitignore + +# Python artifacts +__pycache__/ +*.py[cod] +*.pyc +*.pyo +*.pyd +*.so + +# Local env & tooling +.env +.env.* +venv/ +.virtualenv/ +.cache/ +.mypy_cache/ +.pytest_cache/ +.coverage + +# Django/SQLite development DB (use real DB in production) +db.sqlite3 + +# Node / misc +node_modules/ + +# Ansible deployment artifacts (not needed in runtime image) +ansible/ + +# Other +*.log diff --git a/.env.example b/.env.example index c3a7c55..370501c 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,13 @@ -DJANGO_DEBUG=True +DJANGO_SECRET_KEY=change-me +DJANGO_DEBUG=false +DJANGO_SETTINGS_MODULE=config.settings +DJANGO_STATIC_ROOT=/app/static/ +PORT=8000 +GUNICORN_WORKERS=3 +IMAGE_NAME=histrio/idontneedit:latest +SITE_DOMAIN=idontneedit.org.ru +POSTGRES_DB=idontneedit +POSTGRES_USER=idontneedit +POSTGRES_PASSWORD=changeme +POSTGRES_HOST=postgres +POSTGRES_PORT=5432 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e749080..4bb2320 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,6 @@ jobs: - name: Install uv run: pip install uv - - name: Install project run: uv sync @@ -38,11 +37,40 @@ jobs: - name: Run tests run: uv run python manage.py test - deploy: + build-image: needs: build if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest + steps: + - name: checkout + uses: actions/checkout@v4 + + - name: set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + push: true + tags: | + histrio/idontneedit:latest + histrio/idontneedit:${{ github.sha }} + cache-from: type=registry,ref=histrio/idontneedit:buildcache + cache-to: type=registry,ref=histrio/idontneedit:buildcache,mode=max + + deploy: + needs: build-image + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + steps: - name: checkout uses: actions/checkout@v4 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d188fad..040ff9f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,6 +6,12 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - id: check-added-large-files + - id: forbid-new-submodules + - id: no-commit-to-branch + args: ['--branch', 'main', '--branch', 'master'] + - id: check-case-conflict + - id: check-merge-conflict + - id: detect-private-key - repo: https://github.com/psf/black rev: 25.11.0 hooks: @@ -20,3 +26,11 @@ repos: rev: v1.4.0 hooks: - id: detect-secrets + +- repo: local + hooks: + - id: forbid-env-files + name: Forbid .env files + entry: '.env files must not be committed' + language: fail + files: '(^|/)\.env($|\.(?!example$).*)' diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1a94355 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,48 @@ +ARG PYTHON_VERSION=3.13 +FROM python:${PYTHON_VERSION}-slim AS base + +ENV DEBIAN_FRONTEND=noninteractive \ + PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PIP_NO_CACHE_DIR=off \ + PIP_DISABLE_PIP_VERSION_CHECK=on \ + PIP_DEFAULT_TIMEOUT=100 + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + ca-certificates curl \ + && rm -rf /var/lib/apt/lists/* + +RUN useradd --uid 10001 --create-home --shell /usr/sbin/nologin app \ + && mkdir -p /app /app/static \ + && chown -R app:app /app + +WORKDIR /app + +FROM base AS runtime + +COPY requirements.txt ./ +RUN pip install --upgrade pip wheel \ + && pip install -r requirements.txt --no-cache-dir + +ENV DJANGO_SETTINGS_MODULE=config.settings \ + DJANGO_STATIC_ROOT=/app/static/ \ + GUNICORN_WORKERS=3 \ + GUNICORN_BIND=0.0.0.0:8000 \ + GUNICORN_MAX_REQUESTS=1000 \ + GUNICORN_MAX_REQUESTS_JITTER=100 \ + PORT=8000 + + +COPY . /app + +RUN chown -R app:app /app + +USER app + +EXPOSE 8000 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \ + CMD curl -fsS "http://127.0.0.1:${PORT}/" || exit 1 + +CMD ["/bin/sh", "-c", "exec gunicorn config.wsgi:application --bind 0.0.0.0:${PORT:-8000} --workers ${GUNICORN_WORKERS:-3} --access-logfile - --error-logfile -"] diff --git a/Makefile b/Makefile index c4d2fef..0cba1e5 100644 --- a/Makefile +++ b/Makefile @@ -15,3 +15,17 @@ requirements.txt: uv.lock @test -r .env \ && echo "Your .env is older than .env.example" \ || cp .env.example .env + +DOCKER?=docker +IMAGE_REPO:=histrio/idontneedit +IMAGE_TAG?=$(shell git rev-parse --short HEAD) +LATEST_TAG?=latest + +.PHONY: image.build +image.build: + @$(DOCKER) build -t $(IMAGE_REPO):$(IMAGE_TAG) -t $(IMAGE_REPO):$(LATEST_TAG) . + +.PHONY: image.push +image.push: + @$(DOCKER) push $(IMAGE_REPO):$(IMAGE_TAG) + @$(DOCKER) push $(IMAGE_REPO):$(LATEST_TAG) diff --git a/ansible/Caddyfile b/ansible/Caddyfile index 4bba58e..5cd7c29 100644 --- a/ansible/Caddyfile +++ b/ansible/Caddyfile @@ -1,15 +1,18 @@ -idontneedit.org.ru { +{$SITE_DOMAIN} { + encode zstd gzip - - handle_path /static/* { - root * /srv/idontneedit/static - file_server + header { + Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" + X-Content-Type-Options "nosniff" + X-Frame-Options "DENY" + Referrer-Policy "strict-origin-when-cross-origin" } - handle_path /media/* { - root * /srv/idontneedit/media + handle_path /static/* { + root * /app/static + header Cache-Control "public, max-age=31536000, immutable" file_server } - reverse_proxy unix//srv/idontneedit/run/gunicorn.sock + reverse_proxy app:8000 } diff --git a/ansible/ansible.cfg b/ansible/ansible.cfg new file mode 100644 index 0000000..94f1126 --- /dev/null +++ b/ansible/ansible.cfg @@ -0,0 +1,3 @@ +[ssh_connection] +ssh_args = -o ControlMaster=auto -o ControlPersist=60s +pipelining = True diff --git a/ansible/deploy.yml b/ansible/deploy.yml index da33ea3..4041775 100644 --- a/ansible/deploy.yml +++ b/ansible/deploy.yml @@ -3,7 +3,6 @@ vars: dest: /srv/idontneedit/app - venv: /srv/idontneedit/venv ssh_port: 22 vars_files: @@ -99,135 +98,124 @@ state: present update_cache: yes - - name: install systemd unit - copy: - src: idontneedit.service - dest: /etc/systemd/system/idontneedit.service - owner: root - group: root - mode: 644 + - name: ensure project dirs + ansible.builtin.file: + path: "{{ item }}" + state: directory + owner: deploy + group: www-data + mode: '0755' + loop: + - "{{ dest }}" + - "{{ dest }}/ansible" + + - name: copy docker-compose.yml + ansible.builtin.copy: + src: "{{ playbook_dir }}/../docker-compose.yml" + dest: "{{ dest }}/docker-compose.yml" + owner: deploy + group: www-data + mode: '0644' + + - name: copy Caddyfile for proxy + ansible.builtin.copy: + src: "{{ playbook_dir }}/Caddyfile" + dest: "{{ dest }}/ansible/Caddyfile" + owner: deploy + group: www-data + mode: '0644' - - name: upload env file + - name: upload env file for Docker Compose ansible.builtin.template: src: env.j2 - dest: /srv/idontneedit/env + dest: "{{ dest }}/.env" owner: deploy group: deploy mode: "0600" become: yes - - name: reload systemd - command: systemctl daemon-reload - - - name: enable service - systemd: - name: idontneedit - enabled: yes + - name: ensure prerequisites for Docker repo + ansible.builtin.package: + name: + - ca-certificates + - curl + - gnupg + state: present + update_cache: yes - - name: ensure dirs - file: - path: "{{ item }}" - state: directory - owner: deploy - group: www-data - mode: '0755' - loop: - - /srv/idontneedit/static - - /srv/idontneedit/media - - /srv/idontneedit/logs - - /srv/idontneedit/venv - - /srv/idontneedit/app - - /srv/idontneedit/run - - - name: copy application files - ansible.posix.synchronize: - src: "{{ playbook_dir }}/../" - dest: "{{ dest }}" - delete: yes - owner: no - group: no - rsync_opts: - - "--exclude=.git" - - "--exclude=__pycache__" - - "--exclude=*.pyc" - - "--exclude=.env" - - "--exclude=db.sqlite3" - - "--exclude=venv" - - "--exclude=.venv" - - "--exclude=ansible/" - - "--chown=deploy:www-data" - rsync_path: "rsync" - - - name: ensure database file exists with correct permissions + - name: ensure /etc/apt/keyrings exists ansible.builtin.file: - path: "{{ dest }}/db.sqlite3" - owner: deploy - group: www-data - mode: '0644' - state: touch - modification_time: preserve - access_time: preserve + path: /etc/apt/keyrings + state: directory + mode: "0755" + + - name: add Docker APT GPG key (Ubuntu) + ansible.builtin.shell: "curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg" + args: + creates: /etc/apt/keyrings/docker.gpg + + - name: add Docker APT repository (Ubuntu) + ansible.builtin.apt_repository: + repo: "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu {{ ansible_lsb.codename }} stable" + filename: docker + state: present - - name: ensure python3-venv is installed + - name: install Docker engine and plugins from Docker repo ansible.builtin.package: name: - - python3 - - python3-venv + - docker-ce + - docker-ce-cli + - containerd.io + - docker-buildx-plugin + - docker-compose-plugin state: present update_cache: yes - - name: create venv - ansible.builtin.command: python3 -m venv "{{ venv }}" + - name: add deploy user to docker group + ansible.builtin.user: + name: deploy + groups: docker + append: yes + + - name: start and enable docker service + ansible.builtin.systemd: + name: docker + enabled: yes + state: started + + - name: pull latest images via Docker Compose + ansible.builtin.command: docker compose pull args: - creates: "{{ venv }}/bin/activate" + chdir: "{{ dest }}" become_user: deploy - - name: install dependencies - become_user: deploy - ansible.builtin.command: "{{ venv }}/bin/pip install -r requirements.txt" + - name: bring up stack via Docker Compose (force recreate) + ansible.builtin.command: docker compose up -d --force-recreate args: chdir: "{{ dest }}" + become_user: deploy - - name: run migrations - ansible.builtin.command: "{{ venv }}/bin/python manage.py migrate --noinput" + - name: run database migrations + ansible.builtin.command: docker compose exec -T app python manage.py migrate --noinput args: chdir: "{{ dest }}" become_user: deploy - - name: collect static - become_user: deploy - ansible.builtin.command: "{{ venv }}/bin/python manage.py collectstatic --noinput" + - name: collect static files into shared volume + ansible.builtin.command: docker compose exec -T app python manage.py collectstatic --noinput args: chdir: "{{ dest }}" + become_user: deploy - - name: restart service - ansible.builtin.systemd: - name: idontneedit - state: restarted - - - name: install Caddy - apt: - name: caddy - state: present - update_cache: yes - - - name: install Caddyfile - copy: - src: Caddyfile - dest: /etc/caddy/Caddyfile - owner: root - group: root - mode: '0644' - backup: yes - - - name: enable and start caddy - ansible.builtin.systemd: - name: caddy - enabled: yes - state: restarted - - - name: Run deployment checks - ansible.builtin.command: "{{ venv }}/bin/python manage.py check --deploy --fail-level=WARNING" + - name: create superuser if it does not exist + ansible.builtin.command: docker compose exec -T app python manage.py createsuperuser --noinput args: chdir: "{{ dest }}" become_user: deploy + environment: + DJANGO_SUPERUSER_USERNAME: "{{ django_superuser_username | default('admin') }}" + DJANGO_SUPERUSER_EMAIL: "{{ django_superuser_email | default('admin@example.com') }}" + DJANGO_SUPERUSER_PASSWORD: "{{ django_superuser_password | default('admin') }}" + register: superuser_result + failed_when: superuser_result.rc != 0 and 'already exists' not in superuser_result.stderr + changed_when: superuser_result.rc == 0 diff --git a/ansible/env.j2 b/ansible/env.j2 index 71470fe..abc6ef2 100644 --- a/ansible/env.j2 +++ b/ansible/env.j2 @@ -1 +1,17 @@ -SECRET_KEY={{ secret_key }} +DJANGO_PRODUCTION=true +DJANGO_SECRET_KEY={{ secret_key }} +DJANGO_DEBUG={{ django_debug | default('False') }} +DJANGO_SETTINGS_MODULE={{ django_settings_module | default('config.settings') }} +DJANGO_STATIC_ROOT={{ django_static_root | default('/app/static/') }} +PORT={{ port | default('8000') }} +GUNICORN_WORKERS={{ gunicorn_workers | default('3') }} +IMAGE_NAME={{ image_name | default('histrio/idontneedit:latest') }} +SITE_DOMAIN={{ site_domain | default('idontneedit.org.ru') }} +POSTGRES_DB={{ postgres_db | default('idontneedit') }} +POSTGRES_USER={{ postgres_user | default('idontneedit') }} +POSTGRES_PASSWORD={{ postgres_password }} +POSTGRES_HOST=postgres +POSTGRES_PORT=5432 +DJANGO_SUPERUSER_USERNAME={{ django_superuser_username | default('admin') }} +DJANGO_SUPERUSER_EMAIL={{ django_superuser_email | default('admin@idontneedit.org.ru') }} +DJANGO_SUPERUSER_PASSWORD={{ django_superuser_password }} diff --git a/ansible/idontneedit.service b/ansible/idontneedit.service deleted file mode 100644 index 69914c0..0000000 --- a/ansible/idontneedit.service +++ /dev/null @@ -1,24 +0,0 @@ -[Unit] -Description=idontneedit web app -After=network.target - -[Service] -WorkingDirectory=/srv/idontneedit/app - -ExecStart=/srv/idontneedit/venv/bin/gunicorn config.wsgi:application --bind unix:/srv/idontneedit/run/gunicorn.sock --workers 3 - -User=deploy -Group=www-data - -RuntimeDirectory=idontneedit -RuntimeDirectoryMode=0755 - -Restart=always -RestartSec=3 - -Environment=DJANGO_SETTINGS_MODULE=config.settings -Environment=PYTHONUNBUFFERED=1 -EnvironmentFile=-/srv/idontneedit/env - -[Install] -WantedBy=multi-user.target diff --git a/ansible/secrets.yml b/ansible/secrets.yml index 02f848d..68180ef 100644 --- a/ansible/secrets.yml +++ b/ansible/secrets.yml @@ -1,9 +1,13 @@ $ANSIBLE_VAULT;1.1;AES256 -30613238653936646365643862313738353663356631303936323638366330386137626465346439 -3331626264333062636463393834386336616561326264610a323330333638663436356637393464 -61303731623831326539663433316562356634376165623932633364343833646561333964353434 -3033376130386239360a613362393532633838373763646163326239343238366331303230386634 -34376563643537303663623332643133616662616435373063306465376633333033393331306364 -64373466663531663830346136316365633730333436613139663630393739313365313338613638 -31623035646564326232363962633234376365323634323039666531303837376431613062333238 -62396634313766643734 +30333336633261626235393465623166376266313464663163343231636135383361306365653133 +6633626335633965383934336336346230336130363936340a343438306362373163396265386536 +62396539316138343961623935373534643733383065396139383732396464356531333434633862 +3230343862306461310a663333613138363566653162626237363535346165616436353838303562 +39396236366235643064393637373437623435613433356139663839303038363736333262633230 +39383863663835323938613438363134343162656563333932383661613139333839626231373538 +61643962323137333138363334666364353263663664326535383437336631653265643164343539 +38386234376239653931323963343736383036656330386363303033623134316466303834316561 +62376632353136316138663362373566643466373537366266663565336431366465393633383034 +65643637356361356361323934663463373635666562663335336161623564616534333232383230 +30656134306236356365656662363761636363313037343964326631653964613464303932633034 +39323834633631333930 diff --git a/config/settings.py b/config/settings.py index 25df8ea..32eed9b 100644 --- a/config/settings.py +++ b/config/settings.py @@ -11,20 +11,36 @@ """ from pathlib import Path -import os +import environ # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent +env = environ.Env( + DJANGO_PRODUCTION=(bool, False), + DJANGO_DEBUG=(bool, False), + DJANGO_SECRET_KEY=(str, "django-insecure-please-change-me-in-production"), + POSTGRES_DB=(str, "idontneedit"), + POSTGRES_USER=(str, "idontneedit"), + POSTGRES_PASSWORD=(str, ""), + POSTGRES_HOST=(str, "postgres"), + POSTGRES_PORT=(str, "5432"), + DJANGO_STATIC_ROOT=(str, "/app/static/"), +) + +PRODUCTION = env.bool("DJANGO_PRODUCTION") +if not PRODUCTION: + environ.Env.read_env(BASE_DIR / ".env") + +if PRODUCTION: + DEBUG = env.bool("DJANGO_DEBUG") +else: + DEBUG = True # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/dev/howto/deployment/checklist/ -SECRET_KEY = os.getenv( - "DJANGO_SECRET_KEY", "django-insecure-please-change-me-in-production" -) - -DEBUG = os.getenv("DJANGO_DEBUG", "False").lower() in {"1", "true", "yes"} +SECRET_KEY = env.str("DJANGO_SECRET_KEY") ALLOWED_HOSTS = [ "idontneedit.org.ru", @@ -78,12 +94,24 @@ # Database # https://docs.djangoproject.com/en/dev/ref/settings/#databases -DATABASES = { - "default": { - "ENGINE": "django.db.backends.sqlite3", - "NAME": BASE_DIR / "db.sqlite3", +if DEBUG: + DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "db.sqlite3", + } + } +else: + DATABASES = { + "default": { + "ENGINE": "django.db.backends.postgresql", + "NAME": env.str("POSTGRES_DB"), + "USER": env.str("POSTGRES_USER"), + "PASSWORD": env.str("POSTGRES_PASSWORD"), + "HOST": env.str("POSTGRES_HOST"), + "PORT": env.str("POSTGRES_PORT"), + } } -} # Password validation @@ -122,11 +150,12 @@ STATIC_URL = "static/" -STATIC_ROOT = os.getenv("DJANGO_STATIC_ROOT", "/srv/idontneedit/static/") +STATIC_ROOT = env.str("DJANGO_STATIC_ROOT") CSRF_TRUSTED_ORIGINS = ["https://idontneedit.org.ru"] if not DEBUG: + SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") SECURE_HSTS_SECONDS = 31536000 # 1 year SECURE_HSTS_INCLUDE_SUBDOMAINS = True SECURE_HSTS_PRELOAD = True diff --git a/docker-compose.override.yml b/docker-compose.override.yml new file mode 100644 index 0000000..6235b8b --- /dev/null +++ b/docker-compose.override.yml @@ -0,0 +1,14 @@ +services: + app: + build: + context: . + dockerfile: Dockerfile + image: idontneedit:dev + volumes: + - .:/app + - static-data:/app/static + - /app/.venv + - /app/__pycache__ + caddy: + environment: + SITE_DOMAIN: localhost diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..8a962bf --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,62 @@ +services: + postgres: + image: postgres:16-alpine + environment: + POSTGRES_DB: ${POSTGRES_DB:-idontneedit} + POSTGRES_USER: ${POSTGRES_USER:-idontneedit} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-changeme} + volumes: + - postgres-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-idontneedit}"] + interval: 10s + timeout: 5s + retries: 5 + restart: unless-stopped + + app: + image: ${IMAGE_NAME:-histrio/idontneedit:latest} + pull_policy: always + depends_on: + postgres: + condition: service_healthy + env_file: + - .env + environment: + DJANGO_PRODUCTION: "true" + volumes: + - static-data:/app/static + healthcheck: + test: ["CMD", "curl", "-fsSL", "http://127.0.0.1:8000/"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 20s + restart: unless-stopped + + caddy: + image: caddy:2-alpine + depends_on: + app: + condition: service_started + ports: + - "80:80" + - "443:443" + volumes: + - ./ansible/Caddyfile:/etc/caddy/Caddyfile:ro + - static-data:/app/static:ro + - caddy-data:/data + - caddy-config:/config + env_file: + - .env + restart: unless-stopped + +volumes: + static-data: + driver: local + postgres-data: + driver: local + caddy-data: + driver: local + caddy-config: + driver: local diff --git a/manage.py b/manage.py index 4cbac55..d28672e 100755 --- a/manage.py +++ b/manage.py @@ -3,13 +3,9 @@ import os import sys -import dotenv - def main(): """Run administrative tasks.""" - dotenv.read_dotenv() - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") try: from django.core.management import execute_from_command_line diff --git a/pyproject.toml b/pyproject.toml index e27ccc4..b09bfa6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,8 +6,9 @@ readme = "README.md" requires-python = ">=3.13" dependencies = [ "django==6.0rc1", - "django-dotenv>=1.4.2", + "django-environ>=0.11.2", "gunicorn>=23.0.0", + "psycopg[binary]>=3.2.3", ] [dependency-groups] diff --git a/requirements.txt b/requirements.txt index bdf1584..7289c64 100644 --- a/requirements.txt +++ b/requirements.txt @@ -35,9 +35,9 @@ django==6.0rc1 \ --hash=sha256:28d47cddbb7ef9c39ad7441c72e6c2d47a487397780ae9e75a4774fe20ac1a7d \ --hash=sha256:d37fc9cf38a30a20634ca7bc18580cb86351b01e51eda4e06dc66ab9ffe2e7d8 # via idontneedit -django-dotenv==1.4.2 \ - --hash=sha256:3812bb0f4876cf31f902aad140f0645e120e51ee30eb7c40c22050f58a0e4adb \ - --hash=sha256:a9b1b40a70bd321acd231926acedb9bd2c5e873e33a1873b34a7276d196a765e +django-environ==0.12.0 \ + --hash=sha256:227dc891453dd5bde769c3449cf4a74b6f2ee8f7ab2361c93a07068f4179041a \ + --hash=sha256:92fb346a158abda07ffe6eb23135ce92843af06ecf8753f43adf9d2366dcc0ca # via idontneedit filelock==3.20.0 \ --hash=sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2 \ @@ -78,6 +78,30 @@ platformdirs==4.5.0 \ pre-commit==4.5.0 \ --hash=sha256:25e2ce09595174d9c97860a95609f9f852c0614ba602de3561e267547f2335e1 \ --hash=sha256:dc5a065e932b19fc1d4c653c6939068fe54325af8e741e74e88db4d28a4dd66b +psycopg==3.2.13 \ + --hash=sha256:309adaeda61d44556046ec9a83a93f42bbe5310120b1995f3af49ab6d9f13c1d \ + --hash=sha256:a481374514f2da627157f767a9336705ebefe93ea7a0522a6cbacba165da179a + # via idontneedit +psycopg-binary==3.2.13 ; implementation_name != 'pypy' \ + --hash=sha256:13e2f8894d410678529ff9f1211f96c5a93ff142f992b302682b42d924428b61 \ + --hash=sha256:2f63868cc96bc18486cebec24445affbdd7f7debf28fac466ea935a8b5a4753b \ + --hash=sha256:4a6cafabdc0bfa37e11c6f365020fd5916b62d6296df581f4dceaa43a2ce680c \ + --hash=sha256:502a778c3e07c6b3aabfa56ee230e8c264d2debfab42d11535513a01bdfff0d6 \ + --hash=sha256:594dfbca3326e997ae738d3d339004e8416b1f7390f52ce8dc2d692393e8fa96 \ + --hash=sha256:596176ae3dfbf56fc61108870bfe17c7205d33ac28d524909feb5335201daa0a \ + --hash=sha256:5c77f156c7316529ed371b5f95a51139e531328ee39c37493a2afcbc1f79d5de \ + --hash=sha256:65df0d459ffba14082d8ca4bb2f6ffbb2f8d02968f7d34a747e1031934b76b23 \ + --hash=sha256:7561a71d764d6f74d66e8b7d844b0f27fa33de508f65c17b1d56a94c73644776 \ + --hash=sha256:8b843c00478739e95c46d6d3472b13123b634685f107831a9bfc41503a06ecbd \ + --hash=sha256:9caf14745a1930b4e03fe4072cd7154eaf6e1241d20c42130ed784408a26b24b \ + --hash=sha256:ac92d6bc1d4a41c7459953a9aa727b9966e937e94c9e072527317fd2a67d488b \ + --hash=sha256:c96cb5a27e68acac6d74b64fca38592a692de9c4b7827339190698d58027aa45 \ + --hash=sha256:cc3a0408435dfbb77eeca5e8050df4b19a6e9b7e5e5583edf524c4a83d6293b2 \ + --hash=sha256:dbae6ab1966e2b61d97e47220556c330c4608bb4cfb3a124aa0595c39995c068 \ + --hash=sha256:ea2fdbcc9142933a47c66970e0df8b363e3bd1ea4c5ce376f2f3d94a9aeec847 \ + --hash=sha256:f26f7009375cf1e92180e5c517c52da1054f7e690dde90e0ed00fa8b5736bcd4 \ + --hash=sha256:fae933e4564386199fc54845d85413eedb49760e0bcd2b621fde2dd1825b99b3 + # via psycopg pytokens==0.3.0 \ --hash=sha256:2f932b14ed08de5fcf0b391ace2642f858f1394c0857202959000b68ed7a458a \ --hash=sha256:95b2b5eaf832e469d141a378872480ede3f251a5a5041b8ec6e581d3ac71bbf3 @@ -120,7 +144,9 @@ sqlparse==0.5.4 \ tzdata==2025.2 ; sys_platform == 'win32' \ --hash=sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8 \ --hash=sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9 - # via django + # via + # django + # psycopg virtualenv==20.35.4 \ --hash=sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c \ --hash=sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b diff --git a/uv.lock b/uv.lock index b5e58eb..cd3e546 100644 --- a/uv.lock +++ b/uv.lock @@ -90,12 +90,12 @@ wheels = [ ] [[package]] -name = "django-dotenv" -version = "1.4.2" +name = "django-environ" +version = "0.12.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/18/b1d0bb605a5737e9a62c5135cf4bc234a5186abfbe1b06706ea68f235f32/django-dotenv-1.4.2.tar.gz", hash = "sha256:3812bb0f4876cf31f902aad140f0645e120e51ee30eb7c40c22050f58a0e4adb", size = 4295, upload-time = "2017-12-11T16:44:36.177Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/04/65d2521842c42f4716225f20d8443a50804920606aec018188bbee30a6b0/django_environ-0.12.0.tar.gz", hash = "sha256:227dc891453dd5bde769c3449cf4a74b6f2ee8f7ab2361c93a07068f4179041a", size = 56804, upload-time = "2025-01-13T17:03:37.74Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e4/af/d9175f2b40a9bcd700db2861610d6ed48c2795ffba46c1d6abf25f7c1dea/django_dotenv-1.4.2-py2.py3-none-any.whl", hash = "sha256:a9b1b40a70bd321acd231926acedb9bd2c5e873e33a1873b34a7276d196a765e", size = 3794, upload-time = "2017-12-11T16:44:32.658Z" }, + { url = "https://files.pythonhosted.org/packages/83/b3/0a3bec4ecbfee960f39b1842c2f91e4754251e0a6ed443db9fe3f666ba8f/django_environ-0.12.0-py2.py3-none-any.whl", hash = "sha256:92fb346a158abda07ffe6eb23135ce92843af06ecf8753f43adf9d2366dcc0ca", size = 19957, upload-time = "2025-01-13T17:03:32.918Z" }, ] [[package]] @@ -134,8 +134,9 @@ version = "0.1.0" source = { virtual = "." } dependencies = [ { name = "django" }, - { name = "django-dotenv" }, + { name = "django-environ" }, { name = "gunicorn" }, + { name = "psycopg", extra = ["binary"] }, ] [package.dev-dependencies] @@ -147,8 +148,9 @@ dev = [ [package.metadata] requires-dist = [ { name = "django", specifier = "==6.0rc1" }, - { name = "django-dotenv", specifier = ">=1.4.2" }, + { name = "django-environ", specifier = ">=0.11.2" }, { name = "gunicorn", specifier = ">=23.0.0" }, + { name = "psycopg", extras = ["binary"], specifier = ">=3.2.3" }, ] [package.metadata.requires-dev] @@ -218,6 +220,48 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5d/c4/b2d28e9d2edf4f1713eb3c29307f1a63f3d67cf09bdda29715a36a68921a/pre_commit-4.5.0-py2.py3-none-any.whl", hash = "sha256:25e2ce09595174d9c97860a95609f9f852c0614ba602de3561e267547f2335e1", size = 226429, upload-time = "2025-11-22T21:02:40.836Z" }, ] +[[package]] +name = "psycopg" +version = "3.2.13" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/44/05/d4a05988f15fcf90e0088c735b1f2fc04a30b7fc65461d6ec278f5f2f17a/psycopg-3.2.13.tar.gz", hash = "sha256:309adaeda61d44556046ec9a83a93f42bbe5310120b1995f3af49ab6d9f13c1d", size = 160626, upload-time = "2025-11-21T22:34:32.328Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/14/f2724bd1986158a348316e86fdd0837a838b14a711df3f00e47fba597447/psycopg-3.2.13-py3-none-any.whl", hash = "sha256:a481374514f2da627157f767a9336705ebefe93ea7a0522a6cbacba165da179a", size = 206797, upload-time = "2025-11-21T22:29:39.733Z" }, +] + +[package.optional-dependencies] +binary = [ + { name = "psycopg-binary", marker = "implementation_name != 'pypy'" }, +] + +[[package]] +name = "psycopg-binary" +version = "3.2.13" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/ec/ef37bb44dc02fcc6c0a3eeb93f4baaac13bcb228633fe38ad3fb5a3f6449/psycopg_binary-3.2.13-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dbae6ab1966e2b61d97e47220556c330c4608bb4cfb3a124aa0595c39995c068", size = 3995628, upload-time = "2025-11-21T22:31:45.921Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ad/4748f5f1a40248af16dba087dbec50bd335ee025cc1fb9bf64773378ceff/psycopg_binary-3.2.13-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fae933e4564386199fc54845d85413eedb49760e0bcd2b621fde2dd1825b99b3", size = 4069024, upload-time = "2025-11-21T22:31:50.202Z" }, + { url = "https://files.pythonhosted.org/packages/cf/c2/f02ec6bbc30c7fcd3b39823d2d624b42fae480edeb6e50eb3276281d5635/psycopg_binary-3.2.13-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:13e2f8894d410678529ff9f1211f96c5a93ff142f992b302682b42d924428b61", size = 4615127, upload-time = "2025-11-21T22:31:56.517Z" }, + { url = "https://files.pythonhosted.org/packages/f0/0d/a54fc2cdd672c84175d6869cc823d6ec2a8909318d491f3c24e6077983f2/psycopg_binary-3.2.13-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f26f7009375cf1e92180e5c517c52da1054f7e690dde90e0ed00fa8b5736bcd4", size = 4710267, upload-time = "2025-11-21T22:32:04.585Z" }, + { url = "https://files.pythonhosted.org/packages/9d/b7/067de1acaf3d312253351f3af4121f972584bd36cada6378d4b0cdcebd38/psycopg_binary-3.2.13-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ea2fdbcc9142933a47c66970e0df8b363e3bd1ea4c5ce376f2f3d94a9aeec847", size = 4400795, upload-time = "2025-11-21T22:32:08.883Z" }, + { url = "https://files.pythonhosted.org/packages/64/b5/030e6b1ebfc4d3a8fca03adc5fc827982643bad0b01a1268538d17c08ed3/psycopg_binary-3.2.13-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac92d6bc1d4a41c7459953a9aa727b9966e937e94c9e072527317fd2a67d488b", size = 3851239, upload-time = "2025-11-21T22:32:12.333Z" }, + { url = "https://files.pythonhosted.org/packages/79/6f/0541845364a7de9eae6807060da6a04b22a8eb2e803606d285d9250fbe93/psycopg_binary-3.2.13-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:8b843c00478739e95c46d6d3472b13123b634685f107831a9bfc41503a06ecbd", size = 3525084, upload-time = "2025-11-21T22:32:15.946Z" }, + { url = "https://files.pythonhosted.org/packages/83/ae/6507890dc30a4bbd9d938d4ff3a4079d009a5ad8170af51c7f762438fdbf/psycopg_binary-3.2.13-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2f63868cc96bc18486cebec24445affbdd7f7debf28fac466ea935a8b5a4753b", size = 3576787, upload-time = "2025-11-21T22:32:19.922Z" }, + { url = "https://files.pythonhosted.org/packages/9d/64/3d1c2f1fd09b60cdfbe68b9a810b357ba505eff6e4bdb1a2d9f6729da64c/psycopg_binary-3.2.13-cp313-cp313-win_amd64.whl", hash = "sha256:594dfbca3326e997ae738d3d339004e8416b1f7390f52ce8dc2d692393e8fa96", size = 2905584, upload-time = "2025-11-21T22:32:23.399Z" }, + { url = "https://files.pythonhosted.org/packages/d3/b4/7656b3d67bedff2b900c8c4671cb6eb5fb99c2fc36da33579cac89779c25/psycopg_binary-3.2.13-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:502a778c3e07c6b3aabfa56ee230e8c264d2debfab42d11535513a01bdfff0d6", size = 3997201, upload-time = "2025-11-21T22:32:28.185Z" }, + { url = "https://files.pythonhosted.org/packages/e0/2e/3b4afbd94d48df19c3931cedba464b109f89d81ac43178e6a3d654b4e8d5/psycopg_binary-3.2.13-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7561a71d764d6f74d66e8b7d844b0f27fa33de508f65c17b1d56a94c73644776", size = 4071631, upload-time = "2025-11-21T22:32:32.594Z" }, + { url = "https://files.pythonhosted.org/packages/5e/8b/107d06d55992e2f13157eb705ba5a47d06c4cf1bed077dff0c567b10c187/psycopg_binary-3.2.13-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9caf14745a1930b4e03fe4072cd7154eaf6e1241d20c42130ed784408a26b24b", size = 4620918, upload-time = "2025-11-21T22:32:37.357Z" }, + { url = "https://files.pythonhosted.org/packages/e1/47/a925620f261b115f31e813a5bfe640f316413b1864094a60162f4a6e4d67/psycopg_binary-3.2.13-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4a6cafabdc0bfa37e11c6f365020fd5916b62d6296df581f4dceaa43a2ce680c", size = 4714494, upload-time = "2025-11-21T22:32:42.138Z" }, + { url = "https://files.pythonhosted.org/packages/46/33/bed384665356bb9ba17dd8e104884d87cc2343d16dffdfd9aaa9a159bd4d/psycopg_binary-3.2.13-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c96cb5a27e68acac6d74b64fca38592a692de9c4b7827339190698d58027aa45", size = 4403046, upload-time = "2025-11-21T22:32:47.241Z" }, + { url = "https://files.pythonhosted.org/packages/41/88/749d8e8102fb5df502e2ecb053b79e78e3358af01af652b5dbeb96ab7905/psycopg_binary-3.2.13-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:596176ae3dfbf56fc61108870bfe17c7205d33ac28d524909feb5335201daa0a", size = 3859046, upload-time = "2025-11-21T22:32:51.481Z" }, + { url = "https://files.pythonhosted.org/packages/38/7c/f492e63b517d6dcd564e8c43bc15e11a4c712a848adf8938ce33bfd4c867/psycopg_binary-3.2.13-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:cc3a0408435dfbb77eeca5e8050df4b19a6e9b7e5e5583edf524c4a83d6293b2", size = 3531351, upload-time = "2025-11-21T22:32:55.571Z" }, + { url = "https://files.pythonhosted.org/packages/07/5a/d8743eb23944e5cf2a0bbfa92935c140b5beaacdb872be641065ed70ab2c/psycopg_binary-3.2.13-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:65df0d459ffba14082d8ca4bb2f6ffbb2f8d02968f7d34a747e1031934b76b23", size = 3581034, upload-time = "2025-11-21T22:33:01.648Z" }, + { url = "https://files.pythonhosted.org/packages/46/b2/411d4180252144f7eff024894d2d2ebb98c012c944a282fc20250870e461/psycopg_binary-3.2.13-cp314-cp314-win_amd64.whl", hash = "sha256:5c77f156c7316529ed371b5f95a51139e531328ee39c37493a2afcbc1f79d5de", size = 3000162, upload-time = "2025-11-21T22:33:07.378Z" }, +] + [[package]] name = "pytokens" version = "0.3.0"