Major refactoring of build_deploy action, along with docker building and packaging improvements. Added no_signups and other .env improvements. There is no longer a separate .env.dev, both use .env now.

This commit is contained in:
badblocks 2025-05-18 11:27:59 -07:00
parent 76b2becc24
commit 6f57699c8d
28 changed files with 795 additions and 328 deletions

View file

@ -1,62 +0,0 @@
name: build-image-from-tag
on:
workflow_dispatch:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
# Don't build the image if the registry credentials are not set, the ref is not a tag or it doesn't contain '-v'
if: ${{ vars.REGISTRY_USER != '' && secrets.REGISTRY_PASS != '' && startsWith(github.ref, 'refs/tags/') && contains(github.ref, '-v') }}
runs-on: docker
container:
image: git.badblocks.dev/oci/pkmntrade-club_web:latest
# Mount the dind socket on the container at the default location
options: -v /dind/docker.sock:/var/run/docker.sock
steps:
- name: Extract image name and tag from git and get registry name from env
id: job_data
run: |
echo "::set-output name=img_name::${GITHUB_REF_NAME%%-v*}"
echo "::set-output name=img_tag::${GITHUB_REF_NAME##*-v}"
echo "::set-output name=registry::$(
echo "${{ github.server_url }}" | sed -e 's%https://%%'
)"
echo "::set-output name=oci_registry_prefix::$(
echo "${{ github.server_url }}/oci" | sed -e 's%https://%%'
)"
- name: Checkout the repo
uses: actions/checkout@v4
- name: Export build dir and Dockerfile
id: build_data
run: |
img="${{ steps.job_data.outputs.img_name }}"
build_dir="$(pwd)/${img}"
dockerfile="${build_dir}/Dockerfile"
if [ -f "$dockerfile" ]; then
echo "::set-output name=build_dir::$build_dir"
echo "::set-output name=dockerfile::$dockerfile"
else
echo "Couldn't find the Dockerfile for the '$img' image"
exit 1
fi
- name: Login to the Container Registry
uses: docker/login-action@v3
with:
registry: ${{ steps.job_data.outputs.registry }}
username: ${{ vars.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_PASS }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and Push
uses: docker/build-push-action@v6
with:
push: true
tags: |
${{ steps.job_data.outputs.oci_registry_prefix }}/${{ steps.job_data.outputs.img_name }}:${{ steps.job_data.outputs.img_tag }}
${{ steps.job_data.outputs.oci_registry_prefix }}/${{ steps.job_data.outputs.img_name }}:latest
context: ${{ steps.build_data.outputs.build_dir }}
file: ${{ steps.build_data.outputs.dockerfile }}
build-args: |
OCI_REGISTRY_PREFIX=${{ steps.job_data.outputs.oci_registry_prefix }}/

308
.github/workflows/build_deploy.yml vendored Normal file
View file

@ -0,0 +1,308 @@
name: Build & Deploy
on:
workflow_dispatch:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout the repo
uses: actions/checkout@v4
- name: Get full and partial repository name
id: meta
run: |
echo "GITHUB_REPOSITORY: ${{ github.repository }}"
if [[ "${{ github.repository }}" == *".git" ]]; then
if [[ "${{ github.repository }}" == "https://"* ]]; then
echo "GITHUB_REPOSITORY ends in .git and is a URL"
REPO=$(echo "${{ github.repository }}" | sed 's/\.git$//' | cut -d'/' -f4-5 | sed 's/[^a-zA-Z0-9\/-]/-/g')
else
echo "GITHUB_REPOSITORY ends in .git and is not a URL"
REPO=$(echo "${{ github.repository }}" | sed 's/\.git$//' | sed 's/[^a-zA-Z0-9\/-]/-/g')
fi
else
echo "GITHUB_REPOSITORY is not a URL"
REPO=$(echo "${{ github.repository }}" | sed 's/[^a-zA-Z0-9\/-]/-/g')
fi
echo "REPO=$REPO" >> $GITHUB_OUTPUT
REPO_NAME_ONLY=$(echo "$REPO" | cut -d'/' -f2)
echo "REPO_NAME_ONLY=$REPO_NAME_ONLY" >> $GITHUB_OUTPUT
REPO_PROJECT_PATH=/srv/$(echo "$REPO_NAME_ONLY")
echo "REPO_PROJECT_PATH=$REPO_PROJECT_PATH" >> $GITHUB_OUTPUT
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Set PROD environment variable
run: |
echo "✅ Exit script on any error"
set -eu -o pipefail
prod_value=""
echo "🔍 Check if PROD is set via vars; if not, determine from github.ref"
if [ -z "${{ vars.PROD }}" ]; then
prod_value="${{ startsWith(github.ref, 'refs/tags/v') && !endsWith(github.ref, '-prerelease') }}"
echo "📦 PROD mode unset, determined from github.ref (starts with v and does not end with -prerelease?): ${prod_value}"
else
prod_value="${{ vars.PROD }}"
echo "📦 PROD mode already set to: ${prod_value}"
fi
echo "🖊️ Writing determined values to GITHUB_ENV:"
echo "PROD=${prod_value}" >> $GITHUB_ENV
echo "PROD=${prod_value} -> GITHUB_ENV"
- name: Generate tags
id: generated_docker_tags
run: |
echo "✅ Exit script on any error"
set -eu -o pipefail
# echo current shell
echo "Current shell: $SHELL"
IMAGE_BASE_NAME="${{ steps.meta.outputs.REPO }}"
GIT_SHA="${{ github.sha }}"
GIT_REF="${{ github.ref }}"
echo "Inputs for tagging:"
echo "IMAGE_BASE_NAME: $IMAGE_BASE_NAME"
echo "GIT_SHA: $GIT_SHA"
echo "GIT_REF: $GIT_REF"
echo "PROD status for tagging: ${PROD}"
TAG_LIST=()
VERSION_TAG_LIST=()
if [[ -n "$GIT_SHA" ]]; then
SHORT_SHA=$(echo "$GIT_SHA" | cut -c1-7)
TAG_LIST+=("${IMAGE_BASE_NAME}:sha-${SHORT_SHA}")
TAG_LIST+=("${IMAGE_BASE_NAME}:sha-${GIT_SHA}")
else
echo "🔴 No Git SHA found, cannot generate tags. Aborting."
exit 1
fi
GIT_TAG_VERSION=""
# Extract version only if GIT_REF is a tag like refs/tags/vX.Y.Z or refs/tags/vX.Y.Z-prerelease
if [[ "$GIT_REF" == refs/tags/v* ]]; then
GIT_TAG_VERSION=$(echo "$GIT_REF" | sed 's%refs/tags/v%%' | sed 's%-prerelease$%%')
if [[ "$GIT_TAG_VERSION" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
echo "Detected Git tag: v$GIT_TAG_VERSION"
MAJOR=${BASH_REMATCH[1]}
MINOR=${BASH_REMATCH[2]}
PATCH=${BASH_REMATCH[3]}
echo "Parsed version: Major=$MAJOR, Minor=$MINOR, Patch=$PATCH from v$GIT_TAG_VERSION"
if [ "$MAJOR" -gt 0 ]; then
VERSION_TAG_LIST+=("${IMAGE_BASE_NAME}:v${MAJOR}")
else
echo " Major version is 0 (v$GIT_TAG_VERSION). Skipping MAJOR-only tag v0. Please reference by MAJOR.MINOR or MAJOR.MINOR.PATCH."
fi
VERSION_TAG_LIST+=("${IMAGE_BASE_NAME}:v${MAJOR}.${MINOR}")
VERSION_TAG_LIST+=("${IMAGE_BASE_NAME}:v${MAJOR}.${MINOR}.${PATCH}")
else
echo "⚠️ Git tag 'v$GIT_TAG_VERSION' is not a valid semantic version (x.y.z) but should be. Aborting."
exit 1
fi
fi
if [ "$PROD" = "true" ]; then
echo "📦 Generating PROD tags."
TAG_LIST+=("${IMAGE_BASE_NAME}:stable")
TAG_LIST+=("${IMAGE_BASE_NAME}:latest")
if [ ${#VERSION_TAG_LIST[@]} -gt 0 ]; then
TAG_LIST+=("${VERSION_TAG_LIST[@]}")
else
echo "🔴 PROD mode is true, but Git ref ($GIT_REF) is not a valid version tag. This is unexpected, aborting."
exit 1
fi
else # Non-PROD
echo "🔨 Generating STAGING tags."
TAG_LIST+=("${IMAGE_BASE_NAME}:staging")
TAG_LIST+=("${IMAGE_BASE_NAME}:latest-staging")
if [ ${#VERSION_TAG_LIST[@]} -gt 0 ]; then
echo "🔨 This is also a prerelease version, generating version tags with '-prerelease' suffix."
VERSION_TAG_LIST=("${VERSION_TAG_LIST[@]/%/-prerelease}")
TAG_LIST+=("${VERSION_TAG_LIST[@]}")
else
echo " Git ref ($GIT_REF) is not a valid version tag. Skipping versioned -prerelease tag generation."
fi
fi
echo "Final list of generated tags:"
printf "%s\n" "${TAG_LIST[@]}"
if [[ -z "$GIT_SHA" || ${#TAG_LIST[@]} -lt 4 ]]; then
echo "⚠️ No tags (or too few) were generated based on the logic. Need at least 4 tags: Git commit short and full length SHA tags, a latest/latest-staging tag, and a stable/staging tag. This is unexpected, aborting."
exit 1
fi
# Output the tags for the docker build action (output name is 'tag')
{
echo "tag<<EOF"
printf "%s\n" "${TAG_LIST[@]}"
echo "EOF"
} >> "$GITHUB_OUTPUT"
- name: Run prebuild tasks
run: |
echo "🔄 Chdir to src/pkmntrade_club/"
cd src/pkmntrade_club/
echo "🔄 Chdir to theme/static_src"
cd theme/static_src
echo "📦 Install npm dependencies"
npm install .
echo "🔨 Build the tailwind theme css"
npm run build
# - name: Cache Docker layers
# uses: actions/cache@v4
# with:
# path: ${{ runner.temp }}/.cache
# key: ${{ runner.os }}-${{ steps.meta.outputs.REPO_NAME_ONLY }}-${{ github.ref_name }}-${{ github.sha }}
# restore-keys: |
# ${{ runner.os }}-${{ steps.meta.outputs.REPO_NAME_ONLY }}-${{ github.ref_name }}-
# - name: Inject buildx-cache
# uses: reproducible-containers/buildkit-cache-dance@4b2444fec0c0fb9dbf175a96c094720a692ef810 # v2.1.4
# with:
# cache-source: ${{ runner.temp }}/.cache/buildx-cache
- name: Build container
uses: docker/build-push-action@v6
with:
outputs: type=docker,dest=${{ runner.temp }}/${{ steps.meta.outputs.REPO_NAME_ONLY }}-${{ github.ref_name }}_${{ github.sha }}.tar
tags: ${{ steps.generated_docker_tags.outputs.tag }}
build-args: CACHE_DIR=${{ runner.temp }}/.cache/dockerfile-cache
context: .
#cache-from: type=local,src=${{ runner.temp }}/.cache/buildx-cache
#cache-to: type=local,src=${{ runner.temp }}/.cache/buildx-cache-new,mode=max
# - name: Rotate cache # along with cache-from & cache-to: prevents cache from growing indefinitely
# run: |
# rm -rf ${{ runner.temp }}/.cache/buildx-cache
# mv ${{ runner.temp }}/.cache/buildx-cache-new ${{ runner.temp }}/.cache/buildx-cache
# - name: Upload container as artifact
# uses: actions/upload-artifact@v4
# with:
# name: ${{ steps.meta.outputs.REPO_NAME_ONLY }}-${{ github.ref_name }}_${{ github.sha }}.tar
# path: ${{ runner.temp }}/${{ steps.meta.outputs.REPO_NAME_ONLY }}-${{ github.ref_name }}_${{ github.sha }}.tar
# if-no-files-found: error
# compression-level: 0
- name: Get Deploy Secrets
uses: bitwarden/sm-action@v2
with:
access_token: ${{ secrets.BW_ACCESS_TOKEN }}
secrets: |
27a8da8d-5fb4-4f58-baaf-b2dd0032eca8 > DEPLOY_HOST
4cf9ab8d-1772-4ab0-9219-b2dd003315d4 > DEPLOY_KEY
9aefe34e-c2cf-442e-973c-b2dd0032b6cf > ENV_FILE_BASE64
d3bb47f8-bfc0-4a61-9cee-b2df0147a02a > CF_PEM_CERT
5f658ddf-aadd-4464-b501-b2df0147c338 > CF_PEM_CA
- name: Set up SSH
run: |
mkdir -p $HOME/.ssh
echo -e "${DEPLOY_KEY}" > $HOME/.ssh/deploy.key
chmod 700 $HOME/.ssh
chmod 600 $HOME/.ssh/deploy.key
cat >>$HOME/.ssh/config <<END
Host deploy
HostName ${DEPLOY_HOST}
Port ${{ vars.DEPLOY_PORT }}
User ${{ vars.DEPLOY_USER }}
IdentityFile $HOME/.ssh/deploy.key
UserKnownHostsFile /dev/null
StrictHostKeyChecking no
ControlMaster auto
ControlPath $HOME/.ssh/control-%C
ControlPersist yes
END
- name: Run Deploy Script
run: |
echo "✅ Exit script on any error"
set -eu -o pipefail
echo "⚙️ Set docker host to ssh://deploy so that all docker commands are run on the remote server"
export DOCKER_HOST=ssh://deploy
echo "🚀 Enable and start docker service"
ssh deploy "sudo systemctl enable --now docker.service"
echo "💾 Load the new docker image (${{ steps.meta.outputs.REPO_NAME_ONLY }}-${{ github.ref_name }}_${{ github.sha }}.tar)"
docker load -i "${{ runner.temp }}/${{ steps.meta.outputs.REPO_NAME_ONLY }}-${{ github.ref_name }}_${{ github.sha }}.tar"
echo "🧹 Remove the docker image artifact"
rm "${{ runner.temp }}/${{ steps.meta.outputs.REPO_NAME_ONLY }}-${{ github.ref_name }}_${{ github.sha }}.tar"
echo "🛑 Stop and remove containers before updating compose files"
#ssh deploy "cd ${{ steps.meta.outputs.REPO_PROJECT_PATH}} && docker compose -f docker-compose_core.yml down"
if [ "${PROD}" = true ]; then
ssh deploy "cd ${{ steps.meta.outputs.REPO_PROJECT_PATH}} && docker compose -f docker-compose_web.yml down"
else
ssh deploy "cd ${{ steps.meta.outputs.REPO_PROJECT_PATH}} && docker compose -f docker-compose_staging.yml down"
fi
echo "💾 Copy files to server"
ssh deploy "mkdir -p ${{ steps.meta.outputs.REPO_PROJECT_PATH}}"
scp -pr ./server/* deploy:${{ steps.meta.outputs.REPO_PROJECT_PATH}}/
echo "📝 Create .env file"
printf "%s" "${ENV_FILE_BASE64}" | base64 -d | ssh deploy "cat > ${{ steps.meta.outputs.REPO_PROJECT_PATH}}/.env && chmod 600 ${{ steps.meta.outputs.REPO_PROJECT_PATH}}/.env"
echo "🔑 Set up certificates"
ssh deploy "mkdir -p ${{ steps.meta.outputs.REPO_PROJECT_PATH}}/certs && chmod 550 ${{ steps.meta.outputs.REPO_PROJECT_PATH}}/certs && chown 99:root ${{ steps.meta.outputs.REPO_PROJECT_PATH}}/certs"
printf "%s" "$CF_PEM_CERT" | ssh deploy "cat > ${{ steps.meta.outputs.REPO_PROJECT_PATH}}/certs/crt.pem && chmod 440 ${{ steps.meta.outputs.REPO_PROJECT_PATH}}/certs/crt.pem && chown 99:root ${{ steps.meta.outputs.REPO_PROJECT_PATH}}/certs/crt.pem"
printf "%s" "$CF_PEM_CA" | ssh deploy "cat > ${{ steps.meta.outputs.REPO_PROJECT_PATH}}/certs/ca.pem && chmod 440 ${{ steps.meta.outputs.REPO_PROJECT_PATH}}/certs/ca.pem && chown 99:root ${{ steps.meta.outputs.REPO_PROJECT_PATH}}/certs/ca.pem"
echo "🚀 Start the new containers"
if [ "${PROD}" = true ]; then
ssh deploy "cd ${{ steps.meta.outputs.REPO_PROJECT_PATH }} && docker compose -f docker-compose_core.yml -f docker-compose_web.yml up -d --no-build"
else
ssh deploy "cd ${{ steps.meta.outputs.REPO_PROJECT_PATH }} && docker compose -f docker-compose_core.yml -f docker-compose_staging.yml up -d --no-build"
fi
# echo "🚀 Start the new containers"
# if [ "${PROD}" = true ]; then
# ssh deploy <<<END
# cd ${{ steps.meta.outputs.REPO_PROJECT_PATH}}
# old_container_id=$(docker compose -f docker-compose_web.yml ps -f name=web -q | tail -n1)
# docker compose -f docker-compose_web.yml up -d --no-build --no-recreate
# new_container_id=$(docker compose -f docker-compose_web.yml ps -f name=web -q | head -n1)
# # not needed, but might be useful at some point
# #new_container_ip=$(docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' $new_container_id)
# #new_container_name=$(docker inspect -f '{{.Name}}' $new_container_id | cut -c2-)
# sleep 100 # change to wait for healthcheck in the future
# #docker compose -f docker-compose_core.yml kill -s SIGUSR2 loba
# docker stop $old_container_id
# docker rm $old_container_id
# #docker compose -f docker-compose_core.yml kill -s SIGUSR2 loba
# END
# else
# ssh deploy <<<END
# cd ${{ steps.meta.outputs.REPO_PROJECT_PATH}}
# old_container_id=$(docker compose -f docker-compose_staging.yml ps -f name=web-staging -q | tail -n1)
# docker compose -f docker-compose_staging.yml up -d --no-build --no-recreate
# new_container_id=$(docker compose -f docker-compose_staging.yml ps -f name=web-staging -q | head -n1)
# # not needed, but might be useful at some point
# #new_container_ip=$(docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' $new_container_id)
# #new_container_name=$(docker inspect -f '{{.Name}}' $new_container_id | cut -c2-)
# sleep 100 # change to wait for healthcheck in the future
# #docker compose -f docker-compose_core.yml kill -s SIGUSR2 loba
# docker stop $old_container_id
# docker rm $old_container_id
# #docker compose -f docker-compose_core.yml kill -s SIGUSR2 loba
# END
# fi
echo "🧹 Prune all unused images"
docker system prune -f

View file

@ -1,20 +1,19 @@
name: Build, Test, and Deploy name: Test - Step 2
on: on:
push: workflow_dispatch:
branches: [main] # workflow_run:
pull_request: # workflows: ["Build - Step 1"]
branches: [main] # types:
# - completed
jobs: jobs:
test-step-2:
build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
max-parallel: 4 max-parallel: 4
matrix: matrix:
python-version: [ 3.13 ] python-version: [ 3.12 ]
database-name: database-name:
- test_db - test_db
database-password: database-password:
@ -41,8 +40,6 @@ jobs:
--health-interval 10s --health-interval 10s
--health-timeout 5s --health-timeout 5s
--health-retries 5 --health-retries 5
steps: steps:
- uses: actions/checkout@v2.4.0 - uses: actions/checkout@v2.4.0
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
@ -100,20 +97,7 @@ jobs:
python manage.py test python manage.py test
env: env:
DATABASE_URL: postgres://${{ matrix.database-user }}:${{ matrix.database-password }}@${{ matrix.database-host }}:${{ matrix.database-port }}/${{ matrix.database-name }} DATABASE_URL: postgres://${{ matrix.database-user }}:${{ matrix.database-password }}@${{ matrix.database-host }}:${{ matrix.database-port }}/${{ matrix.database-name }}
SECRET_KEY: test-secret-key
DEBUG: 1 DEBUG: 1
ALLOWED_HOSTS: localhost ALLOWED_HOSTS: localhost
GITHUB_WORKFLOW: True GITHUB_WORKFLOW: True
MODE: workflow MODE: workflow
- uses: actions/checkout@v2.4.0
- name: Build the images and start the containers
run: |
export GITHUB_WORKFLOW=True
export MODE="Test"
docker-compose -f docker-compose.yml build
docker-compose -f docker-compose.yml up -d
# run: docker-compose up -d --build
- name: Stop containers
if: always()
run: docker-compose -f "docker-compose.yml" down

1
.gitignore vendored
View file

@ -1,4 +1,5 @@
.env.production .env.production
.env
staticfiles/* staticfiles/*
!staticfiles/.gitkeep !staticfiles/.gitkeep
seed/0004_TestUsers.json seed/0004_TestUsers.json

4
.vscode/tasks.json vendored
View file

@ -10,13 +10,13 @@
{ {
"label": "Build & run app", "label": "Build & run app",
"type": "shell", "type": "shell",
"command": "docker compose up --build", "command": "./scripts/prebuild.sh && docker compose up --build",
"problemMatcher": [] "problemMatcher": []
}, },
{ {
"label": "Build & run app (no build cache)", "label": "Build & run app (no build cache)",
"type": "shell", "type": "shell",
"command": "docker compose up --build --no-cache", "command": "./scripts/prebuild.sh && docker compose build --no-cache && docker compose up",
"problemMatcher": [] "problemMatcher": []
}, },
{ {

View file

@ -3,6 +3,8 @@
### This should be a separate build container for better reuse. ### This should be a separate build container for better reuse.
FROM mcr.microsoft.com/playwright/python:v1.52.0-noble AS build FROM mcr.microsoft.com/playwright/python:v1.52.0-noble AS build
ARG CACHE_DIR=/root/.cache
# The following does not work in Podman unless you build in Docker # The following does not work in Podman unless you build in Docker
# compatibility mode: <https://github.com/containers/podman/issues/8477> # compatibility mode: <https://github.com/containers/podman/issues/8477>
# You can manually prepend every RUN script with `set -ex` too. # You can manually prepend every RUN script with `set -ex` too.
@ -30,7 +32,7 @@ ENV UV_LINK_MODE=copy \
# You can create `/app` using `uv venv` in a separate `RUN` # You can create `/app` using `uv venv` in a separate `RUN`
# step to have it cached, but with uv it's so fast, it's not worth # step to have it cached, but with uv it's so fast, it's not worth
# it, so we let `uv sync` create it for us automagically. # it, so we let `uv sync` create it for us automagically.
RUN --mount=type=cache,target=/root/.cache \ RUN --mount=type=cache,target=${CACHE_DIR} \
--mount=type=bind,source=uv.lock,target=uv.lock \ --mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \ --mount=type=bind,source=pyproject.toml,target=pyproject.toml \
uv sync \ uv sync \
@ -42,7 +44,7 @@ RUN --mount=type=cache,target=/root/.cache \
# `/src` will NOT be copied into the runtime container. # `/src` will NOT be copied into the runtime container.
COPY . /src COPY . /src
WORKDIR /src WORKDIR /src
RUN --mount=type=cache,target=/root/.cache \ RUN --mount=type=cache,target=${CACHE_DIR} \
uv sync \ uv sync \
--locked \ --locked \
--no-dev \ --no-dev \
@ -54,6 +56,8 @@ RUN --mount=type=cache,target=/root/.cache \
FROM mcr.microsoft.com/playwright/python:v1.52.0-noble FROM mcr.microsoft.com/playwright/python:v1.52.0-noble
SHELL ["sh", "-exc"] SHELL ["sh", "-exc"]
ARG CACHE_DIR=/root/.cache
ENV PATH=/app/bin:$PATH ENV PATH=/app/bin:$PATH
ENV PYTHONPATH=/app ENV PYTHONPATH=/app
ENV PYTHONUNBUFFERED=1 ENV PYTHONUNBUFFERED=1
@ -62,13 +66,13 @@ ENV HOME=/app
WORKDIR /app WORKDIR /app
# Don't run app as root # Don't run app as root
RUN <<EOT RUN --mount=type=cache,target=${CACHE_DIR} <<EOT
groupadd -r app -g 10003 groupadd -r app -g 10003
useradd -r -d /app -u 10003 -s /bin/bash -g app -N app useradd -r -d /app -u 10003 -s /bin/bash -g app -N app
EOT EOT
# Runtime dependencies # Runtime dependencies
RUN <<EOT RUN --mount=type=cache,target=${CACHE_DIR} <<EOT
apt-get update -qy apt-get update -qy
apt-get install -qyy \ apt-get install -qyy \
-o APT::Install-Recommends=false \ -o APT::Install-Recommends=false \
@ -85,15 +89,16 @@ COPY --from=build --chown=app:app /app /app
COPY --chown=app:app --chmod=700 /scripts/entrypoint.sh /entrypoint.sh COPY --chown=app:app --chmod=700 /scripts/entrypoint.sh /entrypoint.sh
COPY --chown=app:app --chmod=700 /scripts/deploy.sh /deploy.sh COPY --chown=app:app --chmod=700 /scripts/deploy.sh /deploy.sh
COPY --chown=app:app --chmod=700 /manage.py /app/manage.py
ENTRYPOINT ["/entrypoint.sh"] ENTRYPOINT ["/entrypoint.sh"]
RUN mkdir -p /app/.cursor-server && chown app:app /app /app/.cursor-server RUN --mount=type=cache,target=${CACHE_DIR} \
mkdir -p /app/.cursor-server && chown app:app /app /app/.cursor-server
USER app USER app
EXPOSE 8000 EXPOSE 8000
HEALTHCHECK CMD curl --fail http://localhost:8000/health/ || exit 1 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"
CMD ["granian", "--interface", "wsgi", "pkmntrade_club.django_project.wsgi", "--host", "0.0.0.0", "--port", "8000", "--workers", "1", "--respawn-failed-workers", "--workers-kill-timeout", "60"]

View file

@ -49,7 +49,7 @@ PKMN Trade Club is a Django-powered application built to connect Pokémon TCG Po
Copy the example environment file and update credentials as needed: Copy the example environment file and update credentials as needed:
```bash ```bash
cp .env .env.production cp .env.example .env
``` ```
4. **Apply migrations and seed initial data:** 4. **Apply migrations and seed initial data:**

View file

@ -1,30 +1,24 @@
services: services:
loba:
image: haproxy:3.1
stop_signal: SIGTERM
ports:
- 8000:8000
volumes:
- ./haproxy/haproxy.dev.cfg:/usr/local/etc/haproxy/haproxy.cfg
depends_on:
- web
web: web:
build: . build: .
command: ["django-admin", "runserver", "0.0.0.0:8000"]
ports:
- 8000:8000
restart: always
volumes: volumes:
- ./.env.dev:/.env:ro - ./seed:/seed:ro
env_file: env_file:
- .env.dev - .env
environment:
- DEBUG=true
- PUBLIC_HOST=localhost
- ALLOWED_HOSTS=127.0.0.1,localhost
depends_on: depends_on:
db: db:
condition: service_healthy condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
db: db:
image: postgres:16 image: postgres:16
restart: always
ports: ports:
- 5432:5432 - 5432:5432
volumes: volumes:

View file

@ -1,27 +0,0 @@
services:
loba:
image: haproxy:3.1
stop_signal: SIGTERM
ports:
- 443:443
entrypoint: ["/usr/local/bin/haproxy.entrypoint.sh"]
volumes:
- ./haproxy/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg
- ./scripts/haproxy.entrypoint.sh:/usr/local/bin/haproxy.entrypoint.sh
depends_on:
- web
web:
build: .
volumes:
- ./.env.production:/.env:ro
env_file:
- .env.production
deploy:
mode: replicated
replicas: 4
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s

View file

@ -1,22 +0,0 @@
defaults
mode http
timeout client 10s
timeout connect 5s
timeout server 10s
timeout http-request 10s
frontend cf
bind :443 ssl crt /certs/crt.pem verify required #ca-file /certs/ca.pem
default_backend django
backend django
balance leastconn
option httpchk
http-check send meth GET uri /health/
http-check expect string OK/HEALTHY
default-server check maxconn 10000 observe layer7
server django1 pkmntradeclub-web-1:8000
server django2 pkmntradeclub-web-2:8000
server django3 pkmntradeclub-web-3:8000
server django4 pkmntradeclub-web-4:8000

View file

@ -1,18 +0,0 @@
defaults
mode http
timeout client 10s
timeout connect 5s
timeout server 10s
timeout http-request 10s
frontend cf
bind :8000
default_backend django
backend django
option httpchk
http-check send meth GET uri /health/
http-check expect string OK/HEALTHY
default-server check maxconn 10000 observe layer7
server django web:8000

View file

@ -52,7 +52,7 @@ dependencies = [
"oauthlib==3.2.2", "oauthlib==3.2.2",
"packaging==23.1", "packaging==23.1",
"pillow>=11.2.1", "pillow>=11.2.1",
"playwright==1.51.0", "playwright==1.52.0",
"psycopg==3.2.3", "psycopg==3.2.3",
"psycopg-binary==3.2.3", "psycopg-binary==3.2.3",
"pycparser==2.21", "pycparser==2.21",

View file

@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
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 --check --noinput 2>&1 || exit 1 django-admin makemigrations --noinput --check 2>&1 || exit 1
echo "*** Running migrate to apply migrations..." echo "*** Running migrate to apply migrations..."
django-admin migrate --noinput 2>&1 django-admin migrate --noinput 2>&1

View file

@ -1,18 +1,6 @@
#!/bin/bash #!/bin/bash
set -ex set -ex
# Load environment variables from .env file if it exists in /code/
if [ -f /.env ]; then
echo "Loading environment variables from .env"
# Set allexport option to export all variables defined by sourcing the .env file.
set -a
source /.env
# Unset allexport option.
set +a
else
echo "Warning: Volume mount for /.env file not found. Make sure you are using env_file in docker-compose.yml or mounting it in the container."
fi
if [ "$1" == "" ]; then if [ "$1" == "" ]; then
echo "Startup command not set. Exiting" echo "Startup command not set. Exiting"
exit; exit;
@ -25,8 +13,8 @@ else
export DJANGO_SETTINGS_MODULE=$DJANGO_SETTINGS_MODULE export DJANGO_SETTINGS_MODULE=$DJANGO_SETTINGS_MODULE
fi fi
echo "Running deploy.sh..." echo "Running deploy.sh... (if you get a APP_REGISTRY_NOT_READY error, there's probably an error in settings.py)"
/deploy.sh /deploy.sh
echo "Enviroment is correct and deploy.sh has been run - executing command: '$@'" echo "Environment is correct and deploy.sh has been run - executing command: '$@'"
exec "$@" && exit 0 exec "$@" && exit 0

View file

@ -1,25 +0,0 @@
#!/bin/sh
CERT_PATH="/certs/crt.pem"
CA_PATH="/certs/ca.pem"
# Create the directory if it doesn't exist
mkdir -p "$(dirname "$CERT_PATH")" "$(dirname "$CA_PATH")"
if [ -n "$HAPROXY_PEM_CERT" ]; then
printf "%s" "$HAPROXY_PEM_CERT" > "$CERT_PATH"
chmod 600 "$CERT_PATH"
echo "HAProxy SSL certificate written to $CERT_PATH"
else
echo "Warning: HAPROXY_PEM_CERT environment variable is not set. SSL may not be configured."
fi
if [ -n "$HAPROXY_PEM_CA" ]; then
printf "%s" "$HAPROXY_PEM_CA" > "$CA_PATH"
chmod 600 "$CA_PATH" # Set restrictive permissions
echo "HAProxy SSL CA written to $CA_PATH"
else
echo "Warning: HAPROXY_PEM_CA environment variable is not set. SSL may not be configured."
fi
exec /usr/local/bin/docker-entrypoint.sh "$@"

View file

@ -9,4 +9,3 @@ fi
# Build the tailwind theme css # Build the tailwind theme css
cd src/pkmntrade_club/theme/static_src cd src/pkmntrade_club/theme/static_src
npm install . && npm run build npm install . && npm run build
cd ../../

View file

@ -3,18 +3,23 @@
set -e set -e
echo "Remaking migrations..." echo "Remaking migrations..."
docker compose up -d
find . -path "*/migrations/0*.py" -delete find . -path "*/migrations/0*.py" -delete
docker compose exec -it web bash -c "django-admin makemigrations --noinput" set -a
source .env
set +a
uv run manage.py makemigrations --noinput
echo "Resetting database... " echo "Resetting database... "
docker compose down \ docker compose down \
&& docker volume rm -f pkmntradeclub_postgres_data \ && docker volume rm -f pkmntradeclub_postgres_data \
&& docker compose up -d && ./scripts/rebuild-and-run.sh
# Wait for the database to be ready. # Wait for the database to be ready.
echo "Waiting for the database to be ready, and migrations to be autorun..." echo "Waiting 15 seconds for the database to be ready, and migrations to be autorun..."
sleep 10 sleep 15
echo "Creating cache table..."
docker compose exec -it web bash -c "django-admin createcachetable django_cache"
echo "Loading seed data..." echo "Loading seed data..."
docker compose exec -it web bash -c "django-admin loaddata /seed/0*" docker compose exec -it web bash -c "django-admin loaddata /seed/0*"

View file

@ -0,0 +1,76 @@
services:
db-healthcheck:
image: stephenc/postgresql-cli:latest
command:
- "sh"
- "-c"
- >-
apk --no-cache add curl;
sleep 30;
while true; do
pg_output=$$(pg_isready -d ${DJANGO_DATABASE_URL} 2>&1);
exit_code=$$?;
if [ $$exit_code -eq 0 ]; then
success="true";
error="";
else
success="false";
error="$$pg_output";
fi;
curl -s -f -X POST \
--connect-timeout 10 \
--max-time 15 \
--header "Authorization: Bearer ${GATUS_TOKEN}" \
http://health:8080/api/v1/endpoints/db_pg-isready/external?success=$$success&error=$$error;
if [ "$$success" = "true" ]; then
echo " Database is OK";
sleep 60;
else
echo "Database is not OK: $$pg_output";
exit 1;
fi;
done
env_file:
- .env
loba:
image: haproxy:3.1
stop_signal: SIGTERM
restart: always
ports:
- 443:443
env_file:
- .env
volumes:
- ./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg
- ./certs:/certs
feedback:
restart: always
image: getfider/fider:stable
env_file:
- .env
volumes:
- ./certs:/certs
cadvisor:
volumes:
- /:/rootfs:ro
- /var/run:/var/run:ro
- /sys:/sys:ro
- /var/lib/docker/:/var/lib/docker:ro
- /dev/disk/:/dev/disk:ro
privileged: true
devices:
- /dev/kmsg
image: gcr.io/cadvisor/cadvisor:v0.52.1
health:
image: twinproduction/gatus:latest
restart: always
env_file:
- .env
environment:
- GATUS_DELAY_START_SECONDS=30
volumes:
- ./gatus/config.yaml:/config/config.yaml
- ./certs:/certs
# secrets:
# env_file_base64:
# environment: ENV_FILE_BASE64

View file

@ -0,0 +1,20 @@
services:
web-staging:
image: badbl0cks/pkmntrade-club:edge
restart: always
env_file:
- .env
environment:
- DEBUG=True
- DISABLE_SIGNUPS=True
- PUBLIC_HOST=staging.pkmntrade.club
- ALLOWED_HOSTS=staging.pkmntrade.club,127.0.0.1
deploy:
mode: replicated
replicas: 2
# healthcheck:
# test: ["CMD", "curl", "-f", "http://127.0.0.1:8000"]
# interval: 30s
# timeout: 10s
# retries: 3
# start_period: 30s

View file

@ -0,0 +1,28 @@
services:
web:
image: ghcr.io/xe/x/httpdebug
entrypoint: ["/ko-app/httpdebug", "--bind", ":8000"]
#image: badbl0cks/pkmntrade-club:edge
#command: ["granian", "--interface", "wsgi", "pkmntrade_club.django_project.wsgi:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "1", "--workers-kill-timeout", "180", "--access-log"]
# env_file:
# - .env
# environment:
# - DEBUG=False
# - DISABLE_SIGNUPS=True
# - PUBLIC_HOST=pkmntrade.club
# - ALLOWED_HOSTS=pkmntrade.club,127.0.0.1
restart: always
deploy:
mode: replicated
replicas: 4
# healthcheck:
# test: ["CMD", "curl", "-f", "http://127.0.0.1:8000"]
# interval: 30s
# timeout: 10s
# retries: 3
# start_period: 30s
# secrets:
# - env_file_base64
# secrets:
# env_file_base64:
# environment: ENV_FILE_BASE64

136
server/gatus/config.yaml Normal file
View file

@ -0,0 +1,136 @@
storage:
type: postgres
path: "${GATUS_DATABASE_URL}"
web:
read-buffer-size: 32768
connectivity:
checker:
target: 1.1.1.1:53
interval: 60s
external-endpoints:
- name: pg_isready
group: db
token: "${GATUS_TOKEN}"
alerts:
- type: email
endpoints:
- name: Domain
group: expirations
url: "https://pkmntrade.club"
interval: 1h
conditions:
- "[DOMAIN_EXPIRATION] > 720h"
alerts:
- type: email
- name: Certificate
group: expirations
url: "https://pkmntrade.club"
interval: 1h
conditions:
- "[CERTIFICATE_EXPIRATION] > 240h"
alerts:
- type: email
- name: Cloudflare
group: dns
url: "1.1.1.1"
interval: 60s
dns:
query-name: "pkmntrade.club"
query-type: "A"
conditions:
- "[DNS_RCODE] == NOERROR"
alerts:
- type: email
- name: Google
group: dns
url: "8.8.8.8"
interval: 60s
dns:
query-name: "pkmntrade.club"
query-type: "A"
conditions:
- "[DNS_RCODE] == NOERROR"
alerts:
- type: email
- name: Quad9
group: dns
url: "9.9.9.9"
interval: 60s
dns:
query-name: "pkmntrade.club"
query-type: "A"
conditions:
- "[DNS_RCODE] == NOERROR"
alerts:
- type: email
- name: HAProxy
group: loadbalancer
url: "http://loba/"
interval: 60s
conditions:
- "[STATUS] == 200"
- "[BODY] == OK/HEALTHY"
alerts:
- type: email
- name: Feedback
group: backends
url: "http://feedback:3000/"
interval: 60s
conditions:
- "[STATUS] == 200"
alerts:
- type: email
- name: Web Worker 1
group: backends
url: "http://pkmntrade-club-web-1:8000/health/"
interval: 60s
conditions:
- "[STATUS] == 200"
#- "[BODY] == OK/HEALTHY"
#- [BODY].database == UP
# must return json like {"database": "UP"} first
alerts:
- type: email
- name: Web Worker 2
group: backends
url: "http://pkmntrade-club-web-2:8000/health/"
interval: 60s
conditions:
- "[STATUS] == 200"
#- "[BODY] == OK/HEALTHY"
alerts:
- type: email
- name: Web Worker 3
group: backends
url: "http://pkmntrade-club-web-3:8000/health/"
interval: 60s
conditions:
- "[STATUS] == 200"
#- "[BODY] == OK/HEALTHY"
alerts:
- type: email
- name: Web Worker 4
group: backends
url: "http://pkmntrade-club-web-4:8000/health/"
interval: 60s
conditions:
- "[STATUS] == 200"
#- "[BODY] == OK/HEALTHY"
alerts:
- type: email
# todo: add cadvisor checks via api https://github.com/google/cadvisor/blob/master/docs/api.md
alerting:
email:
from: noreply@pkmntrade.club
username: dd2cd354-de6d-4fa4-bfe8-31c961cb4e90
password: 1622e8a1-9a45-4a7f-8071-cccca29d8675
host: smtp.tem.scaleway.com
port: 465
to: rob@badblocks.email
client:
insecure: false
default-alert:
enabled: true
failure-threshold: 3
success-threshold: 2
send-on-resolved: true

50
server/haproxy.cfg Normal file
View file

@ -0,0 +1,50 @@
# https://docs.haproxy.org/3.1/configuration.html
global
log stdout format raw local0 # Send logs to Docker's stdout
master-worker
resolvers docker_resolver
nameserver docker_dns 127.0.0.11:53 # Docker's internal DNS
resolve_retries 3
timeout resolve 1s
timeout retry 1s
hold valid 10s
hold obsolete 30s
accepted_payload_size 8192 # Optional: Increase if you have many replicas
defaults
mode http
log global
timeout client 120s
timeout connect 120s
timeout server 120s
timeout http-request 120s
option httplog
frontend web_frontend
bind :443 ssl crt /certs/crt.pem verify required ca-file /certs/ca.pem
use_backend %[req.hdr(host),lower,word(1,:)] # strip out port from host
frontend checks
bind :80
default_backend basic_check
backend basic_check
http-request return status 200 content-type "text/plain" lf-string "OK/HEALTHY"
backend pkmntrade.club
balance leastconn
server-template web- 10 web:8000 check resolvers docker_resolver init-addr libc,none
backend staging.pkmntrade.club
balance leastconn
server-template web-staging- 10 web-staging:8000 check resolvers docker_resolver init-addr libc,none
backend feedback.pkmntrade.club
server feedback-1 feedback:3000
backend health.pkmntrade.club
server health-1 health:8080
#EOF - trailing newline required

View file

@ -0,0 +1,13 @@
from django.conf import settings
from allauth.account.adapter import DefaultAccountAdapter
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
class NoSignupAccountAdapter(DefaultAccountAdapter):
def is_open_for_signup(self, request):
return False
class NoSignupSocialAccountAdapter(DefaultSocialAccountAdapter):
def is_open_for_signup(self, request):
return False

View file

@ -1,4 +1,4 @@
# Generated by Django 5.1 on 2025-05-10 01:22 # Generated by Django 5.1 on 2025-05-17 02:07
import django.contrib.auth.models import django.contrib.auth.models
import django.contrib.auth.validators import django.contrib.auth.validators
@ -14,7 +14,7 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
('auth', '0012_alter_user_first_name_max_length'), ('auth', '0001_initial'),
] ]
operations = [ operations = [

View file

@ -1,20 +0,0 @@
from django.conf import settings
from django.contrib.auth import login
import time
import logging
class LogRequestsMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
if request.path == "/health/":
return self.get_response(request)
start = time.perf_counter()
response = self.get_response(request)
end = time.perf_counter()
self.log(request, response, start, end)
return response
def log(self, request, response, start, end):
logging.info(f"{request.method} {request.path_info} -> RESP {response.status_code}, took {end - start}s")

View file

@ -36,10 +36,19 @@ LOGGING = {
'level': 'INFO', 'level': 'INFO',
'propagate': False, 'propagate': False,
}, },
'granian.access': {
'handlers': ['console'],
'level': 'INFO',
'propagate': False,
},
'_granian': {
'handlers': ['console'],
'level': 'INFO',
'propagate': False,
},
'': { '': {
'handlers': ['console'], 'handlers': ['console'],
'level': 'INFO', 'level': 'INFO',
'propagate': True,
}, },
}, },
} }
@ -57,8 +66,10 @@ 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')
# Resend API Key # Scaleway Secret Key
RESEND_API_KEY = env('RESEND_API_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!
@ -67,7 +78,16 @@ DEBUG = env('DEBUG')
# https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts # https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts
ALLOWED_HOSTS = env('ALLOWED_HOSTS').split(',') ALLOWED_HOSTS = env('ALLOWED_HOSTS').split(',')
CSRF_TRUSTED_ORIGINS = env('CSRF_TRUSTED_ORIGINS').split(',') try:
current_web_worker_hostname = socket.gethostname()
ALLOWED_HOSTS.append(current_web_worker_hostname)
logging.getLogger(__name__).info(f"Added {current_web_worker_hostname} to allowed hosts.")
except Exception:
logging.getLogger(__name__).info(f"Error determining server hostname for allowed hosts.")
PUBLIC_HOST = env('PUBLIC_HOST')
CSRF_TRUSTED_ORIGINS = [f"https://{PUBLIC_HOST}"]
FIRST_PARTY_APPS = [ FIRST_PARTY_APPS = [
'pkmntrade_club.accounts', 'pkmntrade_club.accounts',
@ -101,15 +121,18 @@ INSTALLED_APPS = [
] + FIRST_PARTY_APPS ] + FIRST_PARTY_APPS
if DEBUG: if DEBUG:
INSTALLED_APPS.append("django_browser_reload") INSTALLED_APPS = [
INSTALLED_APPS.append("debug_toolbar") *INSTALLED_APPS,
"django_browser_reload",
"debug_toolbar",
]
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 = 'https'
META_USE_SITES = True META_USE_SITES = True
META_IMAGE_URL = 'https://pkmntrade.club/' META_IMAGE_URL = f'https://{PUBLIC_HOST}/'
# https://docs.djangoproject.com/en/dev/ref/settings/#middleware # https://docs.djangoproject.com/en/dev/ref/settings/#middleware
MIDDLEWARE = [ MIDDLEWARE = [
@ -117,18 +140,19 @@ MIDDLEWARE = [
"whitenoise.middleware.WhiteNoiseMiddleware", # WhiteNoise "whitenoise.middleware.WhiteNoiseMiddleware", # WhiteNoise
"django.contrib.sessions.middleware.SessionMiddleware", "django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware", "django.middleware.common.CommonMiddleware",
"debug_toolbar.middleware.DebugToolbarMiddleware",
"django.middleware.csrf.CsrfViewMiddleware", "django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware", "django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware",
"allauth.account.middleware.AccountMiddleware", # django-allauth "allauth.account.middleware.AccountMiddleware", # django-allauth
"pkmntrade_club.django_project.middleware.LogRequestsMiddleware",
] ]
if DEBUG: if DEBUG:
MIDDLEWARE.append( MIDDLEWARE = [
"django_browser_reload.middleware.BrowserReloadMiddleware") *MIDDLEWARE,
MIDDLEWARE.append("debug_toolbar.middleware.DebugToolbarMiddleware") "django_browser_reload.middleware.BrowserReloadMiddleware",
]
DAISY_SETTINGS = { DAISY_SETTINGS = {
'SITE_TITLE': 'PKMN Trade Club Admin', 'SITE_TITLE': 'PKMN Trade Club Admin',
@ -163,7 +187,7 @@ TEMPLATES = [
# https://docs.djangoproject.com/en/dev/ref/settings/#databases # https://docs.djangoproject.com/en/dev/ref/settings/#databases
DATABASES = { DATABASES = {
'default': env.db(), 'default': env.db(var="DJANGO_DATABASE_URL"),
} }
# Password validation # Password validation
@ -211,7 +235,10 @@ STATIC_ROOT = BASE_DIR / "staticfiles"
STATIC_URL = "/static/" STATIC_URL = "/static/"
# https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#std:setting-STATICFILES_DIRS # https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#std:setting-STATICFILES_DIRS
STATICFILES_DIRS = [BASE_DIR / "static"] STATICFILES_DIRS = [
BASE_DIR / "static", # For general static files
BASE_DIR / "theme" / "static", # For Tailwind generated CSS
]
# https://docs.djangoproject.com/en/dev/ref/settings/#media-root # https://docs.djangoproject.com/en/dev/ref/settings/#media-root
MEDIA_ROOT = BASE_DIR / "media" MEDIA_ROOT = BASE_DIR / "media"
@ -240,10 +267,15 @@ 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 = "smtp.resend.com"
# EMAIL_PORT = 587
# EMAIL_HOST_USER = "resend"
# EMAIL_HOST_PASSWORD = RESEND_API_KEY
# EMAIL_USE_TLS = True
EMAIL_HOST = "smtp.tem.scaleway.com"
EMAIL_PORT = 587 EMAIL_PORT = 587
EMAIL_HOST_USER = "resend" EMAIL_HOST_USER = "dd2cd354-de6d-4fa4-bfe8-31c961cb4e90"
EMAIL_HOST_PASSWORD = RESEND_API_KEY EMAIL_HOST_PASSWORD = SCW_SECRET_KEY
EMAIL_USE_TLS = True 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
@ -256,11 +288,11 @@ INTERNAL_IPS = [
"127.0.0.1", "127.0.0.1",
] ]
# for docker development # for docker + debug toolbar
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)
ALLOWED_HOSTS.append(ip) INTERNAL_IPS.append(".".join(ip.rsplit(".")[:-1])+ ".1")
# https://docs.djangoproject.com/en/dev/topics/auth/customizing/#substituting-a-custom-user-model # https://docs.djangoproject.com/en/dev/topics/auth/customizing/#substituting-a-custom-user-model
AUTH_USER_MODEL = "accounts.CustomUser" AUTH_USER_MODEL = "accounts.CustomUser"
@ -281,22 +313,27 @@ AUTHENTICATION_BACKENDS = (
"allauth.account.auth_backends.AuthenticationBackend", "allauth.account.auth_backends.AuthenticationBackend",
) )
# https://django-allauth.readthedocs.io/en/latest/configuration.html # https://django-allauth.readthedocs.io/en/latest/configuration.html
if DISABLE_SIGNUPS:
ACCOUNT_ADAPTER = 'pkmntrade_club.accounts.adapter.NoSignupAccountAdapter'
SOCIALACCOUNT_ADAPTER = 'pkmntrade_club.accounts.adapter.NoSignupSocialAccountAdapter' # always disable social account signups
ACCOUNT_SESSION_REMEMBER = True ACCOUNT_SESSION_REMEMBER = True
ACCOUNT_SIGNUP_PASSWORD_ENTER_TWICE = True ACCOUNT_SIGNUP_PASSWORD_ENTER_TWICE = True
ACCOUNT_AUTHENTICATION_METHOD = "username_email" ACCOUNT_AUTHENTICATION_METHOD = "username_email"
ACCOUNT_EMAIL_REQUIRED = True 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_DEFAULT_HTTP_PROTOCOL = "https" ACCOUNT_DEFAULT_HTTP_PROTOCOL = "https"
ACCOUNT_LOGIN_ON_EMAIL_CONFIRMATION = True ACCOUNT_LOGIN_ON_EMAIL_CONFIRMATION = True
ACCOUNT_USERNAME_MIN_LENGTH = 3 ACCOUNT_USERNAME_MIN_LENGTH = 2
ACCOUNT_CHANGE_EMAIL = True ACCOUNT_CHANGE_EMAIL = True
ACCOUNT_UNIQUE_EMAIL = True ACCOUNT_UNIQUE_EMAIL = True
ACCOUNT_LOGIN_BY_CODE_ENABLED = True ACCOUNT_LOGIN_BY_CODE_ENABLED = True
ACCOUNT_LOGIN_BY_CODE_REQUIRED = False
ACCOUNT_SIGNUP_FORM_HONEYPOT_FIELD = "website" ACCOUNT_SIGNUP_FORM_HONEYPOT_FIELD = "website"
ACCOUNT_USERNAME_REQUIRED = True ACCOUNT_USERNAME_REQUIRED = True
ACCOUNT_FORMS = { ACCOUNT_FORMS = {
"signup": "accounts.forms.CustomUserCreationForm", "signup": "pkmntrade_club.accounts.forms.CustomUserCreationForm",
} }
SOCIALACCOUNT_EMAIL_AUTHENTICATION = False SOCIALACCOUNT_EMAIL_AUTHENTICATION = False
SOCIALACCOUNT_EMAIL_AUTHENTICATION_AUTO_CONNECT = False SOCIALACCOUNT_EMAIL_AUTHENTICATION_AUTO_CONNECT = False
@ -304,7 +341,11 @@ SOCIALACCOUNT_ONLY = False
CACHE_TIMEOUT = 604800 # 1 week CACHE_TIMEOUT = 604800 # 1 week
if DEBUG: DEBUG_TOOLBAR_CONFIG = {"SHOW_TOOLBAR_CALLBACK": lambda request: DEBUG}
DISABLE_CACHE = env('DISABLE_CACHE', default=DEBUG)
if DISABLE_CACHE:
CACHES = { CACHES = {
"default": { "default": {
"BACKEND": "django.core.cache.backends.dummy.DummyCache", "BACKEND": "django.core.cache.backends.dummy.DummyCache",
@ -315,6 +356,5 @@ else:
"default": { "default": {
"BACKEND": "django.core.cache.backends.db.DatabaseCache", "BACKEND": "django.core.cache.backends.db.DatabaseCache",
"LOCATION": "django_cache", "LOCATION": "django_cache",
"TIMEOUT": 604800, # 1 week
} }
} }

View file

@ -1,6 +1,6 @@
from django.conf import settings
from django.contrib import admin from django.contrib import admin
from django.urls import path, include from django.urls import path, include
from debug_toolbar.toolbar import debug_toolbar_urls
urlpatterns = [ urlpatterns = [
path("admin/", admin.site.urls), path("admin/", admin.site.urls),
@ -10,11 +10,4 @@ urlpatterns = [
path('account/', include('pkmntrade_club.accounts.urls')), path('account/', include('pkmntrade_club.accounts.urls')),
path("trades/", include("pkmntrade_club.trades.urls")), path("trades/", include("pkmntrade_club.trades.urls")),
path("__reload__/", include("django_browser_reload.urls")), path("__reload__/", include("django_browser_reload.urls")),
] ] + debug_toolbar_urls()
if settings.DEBUG:
import debug_toolbar
urlpatterns = [
path("__debug__/", include(debug_toolbar.urls)),
] + urlpatterns

93
uv.lock generated
View file

@ -64,14 +64,14 @@ wheels = [
[[package]] [[package]]
name = "click" name = "click"
version = "8.1.8" version = "8.2.0"
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/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } sdist = { url = "https://files.pythonhosted.org/packages/cd/0f/62ca20172d4f87d93cf89665fbaedcd560ac48b465bd1d92bfc7ea6b0a41/click-8.2.0.tar.gz", hash = "sha256:f5452aeddd9988eefa20f90f05ab66f17fce1ee2a36907fd30b05bbb5953814d", size = 235857 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, { url = "https://files.pythonhosted.org/packages/a2/58/1f37bf81e3c689cc74ffa42102fa8915b59085f54a6e4a80bc6265c0f6bf/click-8.2.0-py3-none-any.whl", hash = "sha256:6b303f0b2aa85f1cb4e5303078fadcbcd4e476f114fab9b5007005711839325c", size = 102156 },
] ]
[[package]] [[package]]
@ -329,36 +329,36 @@ wheels = [
[[package]] [[package]]
name = "greenlet" name = "greenlet"
version = "3.2.1" version = "3.2.2"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/3f/74/907bb43af91782e0366b0960af62a8ce1f9398e4291cac7beaeffbee0c04/greenlet-3.2.1.tar.gz", hash = "sha256:9f4dd4b4946b14bb3bf038f81e1d2e535b7d94f1b2a59fdba1293cd9c1a0a4d7", size = 184475 } sdist = { url = "https://files.pythonhosted.org/packages/34/c1/a82edae11d46c0d83481aacaa1e578fea21d94a1ef400afd734d47ad95ad/greenlet-3.2.2.tar.gz", hash = "sha256:ad053d34421a2debba45aa3cc39acf454acbcd025b3fc1a9f8a0dee237abd485", size = 185797 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/f0/d1/e4777b188a04726f6cf69047830d37365b9191017f54caf2f7af336a6f18/greenlet-3.2.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:0ba2811509a30e5f943be048895a983a8daf0b9aa0ac0ead526dfb5d987d80ea", size = 270381 }, { url = "https://files.pythonhosted.org/packages/2c/a1/88fdc6ce0df6ad361a30ed78d24c86ea32acb2b563f33e39e927b1da9ea0/greenlet-3.2.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:df4d1509efd4977e6a844ac96d8be0b9e5aa5d5c77aa27ca9f4d3f92d3fcf330", size = 270413 },
{ url = "https://files.pythonhosted.org/packages/59/e7/b5b738f5679247ddfcf2179c38945519668dced60c3164c20d55c1a7bb4a/greenlet-3.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4245246e72352b150a1588d43ddc8ab5e306bef924c26571aafafa5d1aaae4e8", size = 637195 }, { url = "https://files.pythonhosted.org/packages/a6/2e/6c1caffd65490c68cd9bcec8cb7feb8ac7b27d38ba1fea121fdc1f2331dc/greenlet-3.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da956d534a6d1b9841f95ad0f18ace637668f680b1339ca4dcfb2c1837880a0b", size = 637242 },
{ url = "https://files.pythonhosted.org/packages/6c/9f/57968c88a5f6bc371364baf983a2e5549cca8f503bfef591b6dd81332cbc/greenlet-3.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7abc0545d8e880779f0c7ce665a1afc3f72f0ca0d5815e2b006cafc4c1cc5840", size = 651381 }, { url = "https://files.pythonhosted.org/packages/98/28/088af2cedf8823b6b7ab029a5626302af4ca1037cf8b998bed3a8d3cb9e2/greenlet-3.2.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9c7b15fb9b88d9ee07e076f5a683027bc3befd5bb5d25954bb633c385d8b737e", size = 651444 },
{ url = "https://files.pythonhosted.org/packages/40/81/1533c9a458e9f2ebccb3ae22f1463b2093b0eb448a88aac36182f1c2cd3d/greenlet-3.2.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6dcc6d604a6575c6225ac0da39df9335cc0c6ac50725063fa90f104f3dbdb2c9", size = 646110 }, { url = "https://files.pythonhosted.org/packages/4a/9f/0116ab876bb0bc7a81eadc21c3f02cd6100dcd25a1cf2a085a130a63a26a/greenlet-3.2.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:752f0e79785e11180ebd2e726c8a88109ded3e2301d40abced2543aa5d164275", size = 646067 },
{ url = "https://files.pythonhosted.org/packages/06/66/25f7e4b1468ebe4a520757f2e41c2a36a2f49a12e963431b82e9f98df2a0/greenlet-3.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2273586879affca2d1f414709bb1f61f0770adcabf9eda8ef48fd90b36f15d12", size = 648070 }, { url = "https://files.pythonhosted.org/packages/35/17/bb8f9c9580e28a94a9575da847c257953d5eb6e39ca888239183320c1c28/greenlet-3.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ae572c996ae4b5e122331e12bbb971ea49c08cc7c232d1bd43150800a2d6c65", size = 648153 },
{ url = "https://files.pythonhosted.org/packages/d7/4c/49d366565c4c4d29e6f666287b9e2f471a66c3a3d8d5066692e347f09e27/greenlet-3.2.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ff38c869ed30fff07f1452d9a204ece1ec6d3c0870e0ba6e478ce7c1515acf22", size = 603816 }, { url = "https://files.pythonhosted.org/packages/2c/ee/7f31b6f7021b8df6f7203b53b9cc741b939a2591dcc6d899d8042fcf66f2/greenlet-3.2.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02f5972ff02c9cf615357c17ab713737cccfd0eaf69b951084a9fd43f39833d3", size = 603865 },
{ url = "https://files.pythonhosted.org/packages/04/15/1612bb61506f44b6b8b6bebb6488702b1fe1432547e95dda57874303a1f5/greenlet-3.2.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e934591a7a4084fa10ee5ef50eb9d2ac8c4075d5c9cf91128116b5dca49d43b1", size = 1119572 }, { url = "https://files.pythonhosted.org/packages/b5/2d/759fa59323b521c6f223276a4fc3d3719475dc9ae4c44c2fe7fc750f8de0/greenlet-3.2.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4fefc7aa68b34b9224490dfda2e70ccf2131368493add64b4ef2d372955c207e", size = 1119575 },
{ url = "https://files.pythonhosted.org/packages/cc/2f/002b99dacd1610e825876f5cbbe7f86740aa2a6b76816e5eca41c8457e85/greenlet-3.2.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:063bcf7f8ee28eb91e7f7a8148c65a43b73fbdc0064ab693e024b5a940070145", size = 1147442 }, { url = "https://files.pythonhosted.org/packages/30/05/356813470060bce0e81c3df63ab8cd1967c1ff6f5189760c1a4734d405ba/greenlet-3.2.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a31ead8411a027c2c4759113cf2bd473690517494f3d6e4bf67064589afcd3c5", size = 1147460 },
{ url = "https://files.pythonhosted.org/packages/c0/ba/82a2c3b9868644ee6011da742156247070f30e952f4d33f33857458450f2/greenlet-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7132e024ebeeeabbe661cf8878aac5d2e643975c4feae833142592ec2f03263d", size = 296207 }, { url = "https://files.pythonhosted.org/packages/07/f4/b2a26a309a04fb844c7406a4501331b9400e1dd7dd64d3450472fd47d2e1/greenlet-3.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:b24c7844c0a0afc3ccbeb0b807adeefb7eff2b5599229ecedddcfeb0ef333bec", size = 296239 },
{ url = "https://files.pythonhosted.org/packages/77/2a/581b3808afec55b2db838742527c40b4ce68b9b64feedff0fd0123f4b19a/greenlet-3.2.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:e1967882f0c42eaf42282a87579685c8673c51153b845fde1ee81be720ae27ac", size = 269119 }, { url = "https://files.pythonhosted.org/packages/89/30/97b49779fff8601af20972a62cc4af0c497c1504dfbb3e93be218e093f21/greenlet-3.2.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:3ab7194ee290302ca15449f601036007873028712e92ca15fc76597a0aeb4c59", size = 269150 },
{ url = "https://files.pythonhosted.org/packages/b0/f3/1c4e27fbdc84e13f05afc2baf605e704668ffa26e73a43eca93e1120813e/greenlet-3.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e77ae69032a95640a5fe8c857ec7bee569a0997e809570f4c92048691ce4b437", size = 637314 }, { url = "https://files.pythonhosted.org/packages/21/30/877245def4220f684bc2e01df1c2e782c164e84b32e07373992f14a2d107/greenlet-3.2.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dc5c43bb65ec3669452af0ab10729e8fdc17f87a1f2ad7ec65d4aaaefabf6bf", size = 637381 },
{ url = "https://files.pythonhosted.org/packages/fc/1a/9fc43cb0044f425f7252da9847893b6de4e3b20c0a748bce7ab3f063d5bc/greenlet-3.2.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3227c6ec1149d4520bc99edac3b9bc8358d0034825f3ca7572165cb502d8f29a", size = 651421 }, { url = "https://files.pythonhosted.org/packages/8e/16/adf937908e1f913856b5371c1d8bdaef5f58f251d714085abeea73ecc471/greenlet-3.2.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:decb0658ec19e5c1f519faa9a160c0fc85a41a7e6654b3ce1b44b939f8bf1325", size = 651427 },
{ url = "https://files.pythonhosted.org/packages/8a/65/d47c03cdc62c6680206b7420c4a98363ee997e87a5e9da1e83bd7eeb57a8/greenlet-3.2.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ddda0197c5b46eedb5628d33dad034c455ae77708c7bf192686e760e26d6a0c", size = 645789 }, { url = "https://files.pythonhosted.org/packages/ad/49/6d79f58fa695b618654adac64e56aff2eeb13344dc28259af8f505662bb1/greenlet-3.2.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6fadd183186db360b61cb34e81117a096bff91c072929cd1b529eb20dd46e6c5", size = 645795 },
{ url = "https://files.pythonhosted.org/packages/2f/40/0faf8bee1b106c241780f377b9951dd4564ef0972de1942ef74687aa6bba/greenlet-3.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de62b542e5dcf0b6116c310dec17b82bb06ef2ceb696156ff7bf74a7a498d982", size = 648262 }, { url = "https://files.pythonhosted.org/packages/5a/e6/28ed5cb929c6b2f001e96b1d0698c622976cd8f1e41fe7ebc047fa7c6dd4/greenlet-3.2.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1919cbdc1c53ef739c94cf2985056bcc0838c1f217b57647cbf4578576c63825", size = 648398 },
{ url = "https://files.pythonhosted.org/packages/e0/a8/73305f713183c2cb08f3ddd32eaa20a6854ba9c37061d682192db9b021c3/greenlet-3.2.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c07a0c01010df42f1f058b3973decc69c4d82e036a951c3deaf89ab114054c07", size = 606770 }, { url = "https://files.pythonhosted.org/packages/9d/70/b200194e25ae86bc57077f695b6cc47ee3118becf54130c5514456cf8dac/greenlet-3.2.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3885f85b61798f4192d544aac7b25a04ece5fe2704670b4ab73c2d2c14ab740d", size = 606795 },
{ url = "https://files.pythonhosted.org/packages/c3/05/7d726e1fb7f8a6ac55ff212a54238a36c57db83446523c763e20cd30b837/greenlet-3.2.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:2530bfb0abcd451ea81068e6d0a1aac6dabf3f4c23c8bd8e2a8f579c2dd60d95", size = 1117960 }, { url = "https://files.pythonhosted.org/packages/f8/c8/ba1def67513a941154ed8f9477ae6e5a03f645be6b507d3930f72ed508d3/greenlet-3.2.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:85f3e248507125bf4af607a26fd6cb8578776197bd4b66e35229cdf5acf1dfbf", size = 1117976 },
{ url = "https://files.pythonhosted.org/packages/bf/9f/2b6cb1bd9f1537e7b08c08705c4a1d7bd4f64489c67d102225c4fd262bda/greenlet-3.2.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1c472adfca310f849903295c351d297559462067f618944ce2650a1878b84123", size = 1145500 }, { url = "https://files.pythonhosted.org/packages/c3/30/d0e88c1cfcc1b3331d63c2b54a0a3a4a950ef202fb8b92e772ca714a9221/greenlet-3.2.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1e76106b6fc55fa3d6fe1c527f95ee65e324a13b62e243f77b48317346559708", size = 1145509 },
{ url = "https://files.pythonhosted.org/packages/e4/f6/339c6e707062319546598eb9827d3ca8942a3eccc610d4a54c1da7b62527/greenlet-3.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:24a496479bc8bd01c39aa6516a43c717b4cee7196573c47b1f8e1011f7c12495", size = 295994 }, { url = "https://files.pythonhosted.org/packages/90/2e/59d6491834b6e289051b252cf4776d16da51c7c6ca6a87ff97e3a50aa0cd/greenlet-3.2.2-cp313-cp313-win_amd64.whl", hash = "sha256:fe46d4f8e94e637634d54477b0cfabcf93c53f29eedcbdeecaf2af32029b4421", size = 296023 },
{ url = "https://files.pythonhosted.org/packages/f1/72/2a251d74a596af7bb1717e891ad4275a3fd5ac06152319d7ad8c77f876af/greenlet-3.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:175d583f7d5ee57845591fc30d852b75b144eb44b05f38b67966ed6df05c8526", size = 629889 }, { url = "https://files.pythonhosted.org/packages/65/66/8a73aace5a5335a1cba56d0da71b7bd93e450f17d372c5b7c5fa547557e9/greenlet-3.2.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba30e88607fb6990544d84caf3c706c4b48f629e18853fc6a646f82db9629418", size = 629911 },
{ url = "https://files.pythonhosted.org/packages/29/2e/d7ed8bf97641bf704b6a43907c0e082cdf44d5bc026eb8e1b79283e7a719/greenlet-3.2.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3ecc9d33ca9428e4536ea53e79d781792cee114d2fa2695b173092bdbd8cd6d5", size = 635261 }, { url = "https://files.pythonhosted.org/packages/48/08/c8b8ebac4e0c95dcc68ec99198842e7db53eda4ab3fb0a4e785690883991/greenlet-3.2.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:055916fafad3e3388d27dd68517478933a97edc2fc54ae79d3bec827de2c64c4", size = 635251 },
{ url = "https://files.pythonhosted.org/packages/1e/75/802aa27848a6fcb5e566f69c64534f572e310f0f12d41e9201a81e741551/greenlet-3.2.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3f56382ac4df3860ebed8ed838f268f03ddf4e459b954415534130062b16bc32", size = 632523 }, { url = "https://files.pythonhosted.org/packages/37/26/7db30868f73e86b9125264d2959acabea132b444b88185ba5c462cb8e571/greenlet-3.2.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2593283bf81ca37d27d110956b79e8723f9aa50c4bcdc29d3c0543d4743d2763", size = 632620 },
{ url = "https://files.pythonhosted.org/packages/56/09/f7c1c3bab9b4c589ad356503dd71be00935e9c4db4db516ed88fc80f1187/greenlet-3.2.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc45a7189c91c0f89aaf9d69da428ce8301b0fd66c914a499199cfb0c28420fc", size = 628816 }, { url = "https://files.pythonhosted.org/packages/10/ec/718a3bd56249e729016b0b69bee4adea0dfccf6ca43d147ef3b21edbca16/greenlet-3.2.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89c69e9a10670eb7a66b8cef6354c24671ba241f46152dd3eed447f79c29fb5b", size = 628851 },
{ url = "https://files.pythonhosted.org/packages/79/e0/1bb90d30b5450eac2dffeaac6b692857c4bd642c21883b79faa8fa056cf2/greenlet-3.2.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51a2f49da08cff79ee42eb22f1658a2aed60c72792f0a0a95f5f0ca6d101b1fb", size = 593687 }, { url = "https://files.pythonhosted.org/packages/9b/9d/d1c79286a76bc62ccdc1387291464af16a4204ea717f24e77b0acd623b99/greenlet-3.2.2-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02a98600899ca1ca5d3a2590974c9e3ec259503b2d6ba6527605fcd74e08e207", size = 593718 },
{ url = "https://files.pythonhosted.org/packages/c5/b5/adbe03c8b4c178add20cc716021183ae6b0326d56ba8793d7828c94286f6/greenlet-3.2.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:0c68bbc639359493420282d2f34fa114e992a8724481d700da0b10d10a7611b8", size = 1105754 }, { url = "https://files.pythonhosted.org/packages/cd/41/96ba2bf948f67b245784cd294b84e3d17933597dffd3acdb367a210d1949/greenlet-3.2.2-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:b50a8c5c162469c3209e5ec92ee4f95c8231b11db6a04db09bbe338176723bb8", size = 1105752 },
{ url = "https://files.pythonhosted.org/packages/39/93/84582d7ef38dec009543ccadec6ab41079a6cbc2b8c0566bcd07bf1aaf6c/greenlet-3.2.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:e775176b5c203a1fa4be19f91da00fd3bff536868b77b237da3f4daa5971ae5d", size = 1125160 }, { url = "https://files.pythonhosted.org/packages/68/3b/3b97f9d33c1f2eb081759da62bd6162159db260f602f048bc2f36b4c453e/greenlet-3.2.2-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:45f9f4853fb4cc46783085261c9ec4706628f3b57de3e68bae03e8f8b3c0de51", size = 1125170 },
{ url = "https://files.pythonhosted.org/packages/01/e6/f9d759788518a6248684e3afeb3691f3ab0276d769b6217a1533362298c8/greenlet-3.2.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d6668caf15f181c1b82fb6406f3911696975cc4c37d782e19cb7ba499e556189", size = 269897 }, { url = "https://files.pythonhosted.org/packages/31/df/b7d17d66c8d0f578d2885a3d8f565e9e4725eacc9d3fdc946d0031c055c4/greenlet-3.2.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:9ea5231428af34226c05f927e16fc7f6fa5e39e3ad3cd24ffa48ba53a47f4240", size = 269899 },
] ]
[[package]] [[package]]
@ -512,7 +512,7 @@ requires-dist = [
{ name = "oauthlib", specifier = "==3.2.2" }, { name = "oauthlib", specifier = "==3.2.2" },
{ name = "packaging", specifier = "==23.1" }, { name = "packaging", specifier = "==23.1" },
{ name = "pillow", specifier = ">=11.2.1" }, { name = "pillow", specifier = ">=11.2.1" },
{ name = "playwright", specifier = "==1.51.0" }, { name = "playwright", specifier = "==1.52.0" },
{ name = "psycopg", specifier = "==3.2.3" }, { name = "psycopg", specifier = "==3.2.3" },
{ name = "psycopg-binary", specifier = "==3.2.3" }, { name = "psycopg-binary", specifier = "==3.2.3" },
{ name = "pycparser", specifier = "==2.21" }, { name = "pycparser", specifier = "==2.21" },
@ -528,20 +528,21 @@ requires-dist = [
[[package]] [[package]]
name = "playwright" name = "playwright"
version = "1.51.0" version = "1.52.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "greenlet" }, { name = "greenlet" },
{ name = "pyee" }, { name = "pyee" },
] ]
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/1b/e9/db98b5a8a41b3691be52dcc9b9d11b5db01bfc9b835e8e3ffe387b5c9266/playwright-1.51.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:bcaaa3d5d73bda659bfb9ff2a288b51e85a91bd89eda86eaf8186550973e416a", size = 39634776 }, { url = "https://files.pythonhosted.org/packages/1e/62/a20240605485ca99365a8b72ed95e0b4c5739a13fb986353f72d8d3f1d27/playwright-1.52.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:19b2cb9d4794062008a635a99bd135b03ebb782d460f96534a91cb583f549512", size = 39611246 },
{ url = "https://files.pythonhosted.org/packages/32/4a/5f2ff6866bdf88e86147930b0be86b227f3691f4eb01daad5198302a8cbe/playwright-1.51.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2e0ae6eb44297b24738e1a6d9c580ca4243b4e21b7e65cf936a71492c08dd0d4", size = 37986511 }, { url = "https://files.pythonhosted.org/packages/dc/23/57ff081663b3061a2a3f0e111713046f705da2595f2f384488a76e4db732/playwright-1.52.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:0797c0479cbdc99607412a3c486a3a2ec9ddc77ac461259fd2878c975bcbb94a", size = 37962977 },
{ url = "https://files.pythonhosted.org/packages/ba/b1/061c322319072225beba45e8c6695b7c1429f83bb97bdb5ed51ea3a009fc/playwright-1.51.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:ab4c0ff00bded52c946be60734868febc964c8a08a9b448d7c20cb3811c6521c", size = 39634776 }, { url = "https://files.pythonhosted.org/packages/a2/ff/eee8532cff4b3d768768152e8c4f30d3caa80f2969bf3143f4371d377b74/playwright-1.52.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:7223960b7dd7ddeec1ba378c302d1d09733b8dac438f492e9854c85d3ca7144f", size = 39611247 },
{ url = "https://files.pythonhosted.org/packages/7a/fd/bc60798803414ecab66456208eeff4308344d0c055ca0d294d2cdd692b60/playwright-1.51.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:d5c9f67bc6ef49094618991c78a1466c5bac5ed09157660d78b8510b77f92746", size = 45164868 }, { url = "https://files.pythonhosted.org/packages/73/c6/8e27af9798f81465b299741ef57064c6ec1a31128ed297406469907dc5a4/playwright-1.52.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:d010124d24a321e0489a8c0d38a3971a7ca7656becea7656c9376bfea7f916d4", size = 45141333 },
{ url = "https://files.pythonhosted.org/packages/0d/14/13db550d7b892aefe80f8581c6557a17cbfc2e084383cd09d25fdd488f6e/playwright-1.51.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:814e4ec2a1a0d6f6221f075622c06b31ceb2bdc6d622258cfefed900c01569ae", size = 44564157 }, { url = "https://files.pythonhosted.org/packages/4e/e9/0661d343ed55860bcfb8934ce10e9597fc953358773ece507b22b0f35c57/playwright-1.52.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4173e453c43180acc60fd77ffe1ebee8d0efbfd9986c03267007b9c3845415af", size = 44540623 },
{ url = "https://files.pythonhosted.org/packages/51/e4/4342f0bd51727df790deda95ee35db066ac05cf4593a73d0c42249fa39a6/playwright-1.51.0-py3-none-win32.whl", hash = "sha256:4cef804991867ea27f608b70fa288ee52a57651e22d02ab287f98f8620b9408c", size = 34862688 }, { url = "https://files.pythonhosted.org/packages/7a/81/a850dbc6bc2e1bd6cc87341e59c253269602352de83d34b00ea38cf410ee/playwright-1.52.0-py3-none-win32.whl", hash = "sha256:cd0bdf92df99db6237a99f828e80a6a50db6180ef8d5352fc9495df2c92f9971", size = 34839156 },
{ url = "https://files.pythonhosted.org/packages/20/0f/098488de02e3d52fc77e8d55c1467f6703701b6ea6788f40409bb8c00dd4/playwright-1.51.0-py3-none-win_amd64.whl", hash = "sha256:9ece9316c5d383aed1a207f079fc2d552fff92184f0ecf37cc596e912d00a8c3", size = 34862693 }, { url = "https://files.pythonhosted.org/packages/51/f3/cca2aa84eb28ea7d5b85d16caa92d62d18b6e83636e3d67957daca1ee4c7/playwright-1.52.0-py3-none-win_amd64.whl", hash = "sha256:dcbf75101eba3066b7521c6519de58721ea44379eb17a0dafa94f9f1b17f59e4", size = 34839164 },
{ url = "https://files.pythonhosted.org/packages/b5/4f/71a8a873e8c3c3e2d3ec03a578e546f6875be8a76214d90219f752f827cd/playwright-1.52.0-py3-none-win_arm64.whl", hash = "sha256:9d0085b8de513de5fb50669f8e6677f0252ef95a9a1d2d23ccee9638e71e65cb", size = 30688972 },
] ]
[[package]] [[package]]
@ -597,14 +598,14 @@ wheels = [
[[package]] [[package]]
name = "pyee" name = "pyee"
version = "12.1.1" version = "13.0.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "typing-extensions" }, { name = "typing-extensions" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/0a/37/8fb6e653597b2b67ef552ed49b438d5398ba3b85a9453f8ada0fd77d455c/pyee-12.1.1.tar.gz", hash = "sha256:bbc33c09e2ff827f74191e3e5bbc6be7da02f627b7ec30d86f5ce1a6fb2424a3", size = 30915 } sdist = { url = "https://files.pythonhosted.org/packages/95/03/1fd98d5841cd7964a27d729ccf2199602fe05eb7a405c1462eb7277945ed/pyee-13.0.0.tar.gz", hash = "sha256:b391e3c5a434d1f5118a25615001dbc8f669cf410ab67d04c4d4e07c55481c37", size = 31250 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/25/68/7e150cba9eeffdeb3c5cecdb6896d70c8edd46ce41c0491e12fb2b2256ff/pyee-12.1.1-py3-none-any.whl", hash = "sha256:18a19c650556bb6b32b406d7f017c8f513aceed1ef7ca618fb65de7bd2d347ef", size = 15527 }, { url = "https://files.pythonhosted.org/packages/9b/4d/b9add7c84060d4c1906abe9a7e5359f2a60f7a9a4f67268b2766673427d8/pyee-13.0.0-py3-none-any.whl", hash = "sha256:48195a3cddb3b1515ce0695ed76036b5ccc2ef3a9f963ff9f77aec0139845498", size = 15730 },
] ]
[[package]] [[package]]
@ -658,11 +659,11 @@ wheels = [
[[package]] [[package]]
name = "setuptools" name = "setuptools"
version = "80.3.1" version = "80.7.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/70/dc/3976b322de9d2e87ed0007cf04cc7553969b6c7b3f48a565d0333748fbcd/setuptools-80.3.1.tar.gz", hash = "sha256:31e2c58dbb67c99c289f51c16d899afedae292b978f8051efaf6262d8212f927", size = 1315082 } sdist = { url = "https://files.pythonhosted.org/packages/9e/8b/dc1773e8e5d07fd27c1632c45c1de856ac3dbf09c0147f782ca6d990cf15/setuptools-80.7.1.tar.gz", hash = "sha256:f6ffc5f0142b1bd8d0ca94ee91b30c0ca862ffd50826da1ea85258a06fd94552", size = 1319188 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/53/7e/5d8af3317ddbf9519b687bd1c39d8737fde07d97f54df65553faca5cffb1/setuptools-80.3.1-py3-none-any.whl", hash = "sha256:ea8e00d7992054c4c592aeb892f6ad51fe1b4d90cc6947cc45c45717c40ec537", size = 1201172 }, { url = "https://files.pythonhosted.org/packages/a1/18/0e835c3a557dc5faffc8f91092f62fc337c1dab1066715842e7a4b318ec4/setuptools-80.7.1-py3-none-any.whl", hash = "sha256:ca5cc1069b85dc23070a6628e6bcecb3292acac802399c7f8edc0100619f9009", size = 1200776 },
] ]
[[package]] [[package]]