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:
parent
76b2becc24
commit
6f57699c8d
28 changed files with 795 additions and 328 deletions
62
.github/workflows/build.yml
vendored
62
.github/workflows/build.yml
vendored
|
|
@ -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
308
.github/workflows/build_deploy.yml
vendored
Normal 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
|
||||
32
.github/workflows/test.yml
vendored
32
.github/workflows/test.yml
vendored
|
|
@ -1,20 +1,19 @@
|
|||
name: Build, Test, and Deploy
|
||||
name: Test - Step 2
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
# workflow_run:
|
||||
# workflows: ["Build - Step 1"]
|
||||
# types:
|
||||
# - completed
|
||||
|
||||
jobs:
|
||||
|
||||
build:
|
||||
|
||||
test-step-2:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
max-parallel: 4
|
||||
matrix:
|
||||
python-version: [ 3.13 ]
|
||||
python-version: [ 3.12 ]
|
||||
database-name:
|
||||
- test_db
|
||||
database-password:
|
||||
|
|
@ -41,8 +40,6 @@ jobs:
|
|||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2.4.0
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
|
|
@ -100,20 +97,7 @@ jobs:
|
|||
python manage.py test
|
||||
env:
|
||||
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
|
||||
ALLOWED_HOSTS: localhost
|
||||
GITHUB_WORKFLOW: True
|
||||
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
1
.gitignore
vendored
|
|
@ -1,4 +1,5 @@
|
|||
.env.production
|
||||
.env
|
||||
staticfiles/*
|
||||
!staticfiles/.gitkeep
|
||||
seed/0004_TestUsers.json
|
||||
|
|
|
|||
4
.vscode/tasks.json
vendored
4
.vscode/tasks.json
vendored
|
|
@ -10,13 +10,13 @@
|
|||
{
|
||||
"label": "Build & run app",
|
||||
"type": "shell",
|
||||
"command": "docker compose up --build",
|
||||
"command": "./scripts/prebuild.sh && docker compose up --build",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Build & run app (no build cache)",
|
||||
"type": "shell",
|
||||
"command": "docker compose up --build --no-cache",
|
||||
"command": "./scripts/prebuild.sh && docker compose build --no-cache && docker compose up",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
|
|
|
|||
21
Dockerfile
21
Dockerfile
|
|
@ -3,6 +3,8 @@
|
|||
### This should be a separate build container for better reuse.
|
||||
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
|
||||
# compatibility mode: <https://github.com/containers/podman/issues/8477>
|
||||
# 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`
|
||||
# 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.
|
||||
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=pyproject.toml,target=pyproject.toml \
|
||||
uv sync \
|
||||
|
|
@ -42,7 +44,7 @@ RUN --mount=type=cache,target=/root/.cache \
|
|||
# `/src` will NOT be copied into the runtime container.
|
||||
COPY . /src
|
||||
WORKDIR /src
|
||||
RUN --mount=type=cache,target=/root/.cache \
|
||||
RUN --mount=type=cache,target=${CACHE_DIR} \
|
||||
uv sync \
|
||||
--locked \
|
||||
--no-dev \
|
||||
|
|
@ -54,6 +56,8 @@ RUN --mount=type=cache,target=/root/.cache \
|
|||
FROM mcr.microsoft.com/playwright/python:v1.52.0-noble
|
||||
SHELL ["sh", "-exc"]
|
||||
|
||||
ARG CACHE_DIR=/root/.cache
|
||||
|
||||
ENV PATH=/app/bin:$PATH
|
||||
ENV PYTHONPATH=/app
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
|
@ -62,13 +66,13 @@ ENV HOME=/app
|
|||
WORKDIR /app
|
||||
|
||||
# Don't run app as root
|
||||
RUN <<EOT
|
||||
RUN --mount=type=cache,target=${CACHE_DIR} <<EOT
|
||||
groupadd -r app -g 10003
|
||||
useradd -r -d /app -u 10003 -s /bin/bash -g app -N app
|
||||
EOT
|
||||
|
||||
# Runtime dependencies
|
||||
RUN <<EOT
|
||||
RUN --mount=type=cache,target=${CACHE_DIR} <<EOT
|
||||
apt-get update -qy
|
||||
apt-get install -qyy \
|
||||
-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/deploy.sh /deploy.sh
|
||||
COPY --chown=app:app --chmod=700 /manage.py /app/manage.py
|
||||
|
||||
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
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
HEALTHCHECK CMD curl --fail http://localhost:8000/health/ || exit 1
|
||||
|
||||
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"]
|
||||
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"
|
||||
|
|
@ -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:
|
||||
|
||||
```bash
|
||||
cp .env .env.production
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
4. **Apply migrations and seed initial data:**
|
||||
|
|
|
|||
|
|
@ -1,30 +1,24 @@
|
|||
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:
|
||||
build: .
|
||||
command: ["django-admin", "runserver", "0.0.0.0:8000"]
|
||||
ports:
|
||||
- 8000:8000
|
||||
restart: always
|
||||
volumes:
|
||||
- ./.env.dev:/.env:ro
|
||||
- ./seed:/seed:ro
|
||||
env_file:
|
||||
- .env.dev
|
||||
- .env
|
||||
environment:
|
||||
- DEBUG=true
|
||||
- PUBLIC_HOST=localhost
|
||||
- ALLOWED_HOSTS=127.0.0.1,localhost
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8000"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
db:
|
||||
image: postgres:16
|
||||
restart: always
|
||||
ports:
|
||||
- 5432:5432
|
||||
volumes:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -52,7 +52,7 @@ dependencies = [
|
|||
"oauthlib==3.2.2",
|
||||
"packaging==23.1",
|
||||
"pillow>=11.2.1",
|
||||
"playwright==1.51.0",
|
||||
"playwright==1.52.0",
|
||||
"psycopg==3.2.3",
|
||||
"psycopg-binary==3.2.3",
|
||||
"pycparser==2.21",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
#!/bin/bash
|
||||
|
||||
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..."
|
||||
django-admin migrate --noinput 2>&1
|
||||
|
|
|
|||
|
|
@ -1,18 +1,6 @@
|
|||
#!/bin/bash
|
||||
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
|
||||
echo "Startup command not set. Exiting"
|
||||
exit;
|
||||
|
|
@ -25,8 +13,8 @@ else
|
|||
export DJANGO_SETTINGS_MODULE=$DJANGO_SETTINGS_MODULE
|
||||
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
|
||||
|
||||
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
|
||||
|
|
@ -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 "$@"
|
||||
|
|
@ -9,4 +9,3 @@ fi
|
|||
# Build the tailwind theme css
|
||||
cd src/pkmntrade_club/theme/static_src
|
||||
npm install . && npm run build
|
||||
cd ../../
|
||||
|
|
@ -3,18 +3,23 @@
|
|||
set -e
|
||||
|
||||
echo "Remaking migrations..."
|
||||
docker compose up -d
|
||||
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... "
|
||||
docker compose down \
|
||||
&& docker volume rm -f pkmntradeclub_postgres_data \
|
||||
&& docker compose up -d
|
||||
&& ./scripts/rebuild-and-run.sh
|
||||
|
||||
# Wait for the database to be ready.
|
||||
echo "Waiting for the database to be ready, and migrations to be autorun..."
|
||||
sleep 10
|
||||
echo "Waiting 15 seconds for the database to be ready, and migrations to be autorun..."
|
||||
sleep 15
|
||||
|
||||
echo "Creating cache table..."
|
||||
docker compose exec -it web bash -c "django-admin createcachetable django_cache"
|
||||
|
||||
echo "Loading seed data..."
|
||||
docker compose exec -it web bash -c "django-admin loaddata /seed/0*"
|
||||
|
|
|
|||
76
server/docker-compose_core.yml
Normal file
76
server/docker-compose_core.yml
Normal 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
|
||||
20
server/docker-compose_staging.yml
Normal file
20
server/docker-compose_staging.yml
Normal 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
|
||||
28
server/docker-compose_web.yml
Normal file
28
server/docker-compose_web.yml
Normal 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
136
server/gatus/config.yaml
Normal 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
50
server/haproxy.cfg
Normal 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
|
||||
|
||||
13
src/pkmntrade_club/accounts/adapter.py
Normal file
13
src/pkmntrade_club/accounts/adapter.py
Normal 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
|
||||
|
|
@ -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.validators
|
||||
|
|
@ -14,7 +14,7 @@ class Migration(migrations.Migration):
|
|||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('auth', '0012_alter_user_first_name_max_length'),
|
||||
('auth', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
|
@ -36,10 +36,19 @@ LOGGING = {
|
|||
'level': 'INFO',
|
||||
'propagate': False,
|
||||
},
|
||||
'granian.access': {
|
||||
'handlers': ['console'],
|
||||
'level': 'INFO',
|
||||
'propagate': False,
|
||||
},
|
||||
'_granian': {
|
||||
'handlers': ['console'],
|
||||
'level': 'INFO',
|
||||
'propagate': False,
|
||||
},
|
||||
'': {
|
||||
'handlers': ['console'],
|
||||
'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!
|
||||
SECRET_KEY = env('SECRET_KEY')
|
||||
|
||||
# Resend API Key
|
||||
RESEND_API_KEY = env('RESEND_API_KEY')
|
||||
# Scaleway Secret Key
|
||||
SCW_SECRET_KEY = env('SCW_SECRET_KEY')
|
||||
|
||||
DISABLE_SIGNUPS = env('DISABLE_SIGNUPS', default=False)
|
||||
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#debug
|
||||
# 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
|
||||
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 = [
|
||||
'pkmntrade_club.accounts',
|
||||
|
|
@ -101,15 +121,18 @@ INSTALLED_APPS = [
|
|||
] + FIRST_PARTY_APPS
|
||||
|
||||
if DEBUG:
|
||||
INSTALLED_APPS.append("django_browser_reload")
|
||||
INSTALLED_APPS.append("debug_toolbar")
|
||||
INSTALLED_APPS = [
|
||||
*INSTALLED_APPS,
|
||||
"django_browser_reload",
|
||||
"debug_toolbar",
|
||||
]
|
||||
|
||||
TAILWIND_APP_NAME = 'theme'
|
||||
|
||||
META_SITE_NAME = 'PKMN Trade Club'
|
||||
META_SITE_PROTOCOL = 'https'
|
||||
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
|
||||
MIDDLEWARE = [
|
||||
|
|
@ -117,18 +140,19 @@ MIDDLEWARE = [
|
|||
"whitenoise.middleware.WhiteNoiseMiddleware", # WhiteNoise
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
"debug_toolbar.middleware.DebugToolbarMiddleware",
|
||||
"django.middleware.csrf.CsrfViewMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
"allauth.account.middleware.AccountMiddleware", # django-allauth
|
||||
"pkmntrade_club.django_project.middleware.LogRequestsMiddleware",
|
||||
]
|
||||
|
||||
if DEBUG:
|
||||
MIDDLEWARE.append(
|
||||
"django_browser_reload.middleware.BrowserReloadMiddleware")
|
||||
MIDDLEWARE.append("debug_toolbar.middleware.DebugToolbarMiddleware")
|
||||
MIDDLEWARE = [
|
||||
*MIDDLEWARE,
|
||||
"django_browser_reload.middleware.BrowserReloadMiddleware",
|
||||
]
|
||||
|
||||
DAISY_SETTINGS = {
|
||||
'SITE_TITLE': 'PKMN Trade Club Admin',
|
||||
|
|
@ -163,7 +187,7 @@ TEMPLATES = [
|
|||
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#databases
|
||||
DATABASES = {
|
||||
'default': env.db(),
|
||||
'default': env.db(var="DJANGO_DATABASE_URL"),
|
||||
}
|
||||
|
||||
# Password validation
|
||||
|
|
@ -211,7 +235,10 @@ STATIC_ROOT = BASE_DIR / "staticfiles"
|
|||
STATIC_URL = "/static/"
|
||||
|
||||
# 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
|
||||
MEDIA_ROOT = BASE_DIR / "media"
|
||||
|
|
@ -240,10 +267,15 @@ CRISPY_TEMPLATE_PACK = "tailwind"
|
|||
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#email-backend
|
||||
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_HOST_USER = "resend"
|
||||
EMAIL_HOST_PASSWORD = RESEND_API_KEY
|
||||
EMAIL_HOST_USER = "dd2cd354-de6d-4fa4-bfe8-31c961cb4e90"
|
||||
EMAIL_HOST_PASSWORD = SCW_SECRET_KEY
|
||||
EMAIL_USE_TLS = True
|
||||
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#default-from-email
|
||||
|
|
@ -256,11 +288,11 @@ INTERNAL_IPS = [
|
|||
"127.0.0.1",
|
||||
]
|
||||
|
||||
# for docker development
|
||||
# for docker + debug toolbar
|
||||
hostname, _, ips = socket.gethostbyname_ex(socket.gethostname())
|
||||
for ip in ips:
|
||||
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
|
||||
AUTH_USER_MODEL = "accounts.CustomUser"
|
||||
|
|
@ -281,22 +313,27 @@ AUTHENTICATION_BACKENDS = (
|
|||
"allauth.account.auth_backends.AuthenticationBackend",
|
||||
)
|
||||
# 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_SIGNUP_PASSWORD_ENTER_TWICE = True
|
||||
ACCOUNT_AUTHENTICATION_METHOD = "username_email"
|
||||
ACCOUNT_EMAIL_REQUIRED = True
|
||||
ACCOUNT_EMAIL_VERIFICATION = env('ACCOUNT_EMAIL_VERIFICATION')
|
||||
ACCOUNT_EMAIL_NOTIFICATIONS = True
|
||||
ACCOUNT_EMAIL_UNKNOWN_ACCOUNTS = False
|
||||
ACCOUNT_DEFAULT_HTTP_PROTOCOL = "https"
|
||||
ACCOUNT_LOGIN_ON_EMAIL_CONFIRMATION = True
|
||||
ACCOUNT_USERNAME_MIN_LENGTH = 3
|
||||
ACCOUNT_USERNAME_MIN_LENGTH = 2
|
||||
ACCOUNT_CHANGE_EMAIL = True
|
||||
ACCOUNT_UNIQUE_EMAIL = True
|
||||
ACCOUNT_LOGIN_BY_CODE_ENABLED = True
|
||||
ACCOUNT_LOGIN_BY_CODE_REQUIRED = False
|
||||
ACCOUNT_SIGNUP_FORM_HONEYPOT_FIELD = "website"
|
||||
ACCOUNT_USERNAME_REQUIRED = True
|
||||
ACCOUNT_FORMS = {
|
||||
"signup": "accounts.forms.CustomUserCreationForm",
|
||||
"signup": "pkmntrade_club.accounts.forms.CustomUserCreationForm",
|
||||
}
|
||||
SOCIALACCOUNT_EMAIL_AUTHENTICATION = False
|
||||
SOCIALACCOUNT_EMAIL_AUTHENTICATION_AUTO_CONNECT = False
|
||||
|
|
@ -304,7 +341,11 @@ SOCIALACCOUNT_ONLY = False
|
|||
|
||||
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 = {
|
||||
"default": {
|
||||
"BACKEND": "django.core.cache.backends.dummy.DummyCache",
|
||||
|
|
@ -315,6 +356,5 @@ else:
|
|||
"default": {
|
||||
"BACKEND": "django.core.cache.backends.db.DatabaseCache",
|
||||
"LOCATION": "django_cache",
|
||||
"TIMEOUT": 604800, # 1 week
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
from django.conf import settings
|
||||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
from debug_toolbar.toolbar import debug_toolbar_urls
|
||||
|
||||
urlpatterns = [
|
||||
path("admin/", admin.site.urls),
|
||||
|
|
@ -10,11 +10,4 @@ urlpatterns = [
|
|||
path('account/', include('pkmntrade_club.accounts.urls')),
|
||||
path("trades/", include("pkmntrade_club.trades.urls")),
|
||||
path("__reload__/", include("django_browser_reload.urls")),
|
||||
]
|
||||
|
||||
if settings.DEBUG:
|
||||
import debug_toolbar
|
||||
|
||||
urlpatterns = [
|
||||
path("__debug__/", include(debug_toolbar.urls)),
|
||||
] + urlpatterns
|
||||
] + debug_toolbar_urls()
|
||||
|
|
|
|||
93
uv.lock
generated
93
uv.lock
generated
|
|
@ -64,14 +64,14 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.1.8"
|
||||
version = "8.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ 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 = [
|
||||
{ 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]]
|
||||
|
|
@ -329,36 +329,36 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "greenlet"
|
||||
version = "3.2.1"
|
||||
version = "3.2.2"
|
||||
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 = [
|
||||
{ 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/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/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/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/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/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/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/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/c0/ba/82a2c3b9868644ee6011da742156247070f30e952f4d33f33857458450f2/greenlet-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7132e024ebeeeabbe661cf8878aac5d2e643975c4feae833142592ec2f03263d", size = 296207 },
|
||||
{ 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/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/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/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/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/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/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/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/e4/f6/339c6e707062319546598eb9827d3ca8942a3eccc610d4a54c1da7b62527/greenlet-3.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:24a496479bc8bd01c39aa6516a43c717b4cee7196573c47b1f8e1011f7c12495", size = 295994 },
|
||||
{ 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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/07/f4/b2a26a309a04fb844c7406a4501331b9400e1dd7dd64d3450472fd47d2e1/greenlet-3.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:b24c7844c0a0afc3ccbeb0b807adeefb7eff2b5599229ecedddcfeb0ef333bec", size = 296239 },
|
||||
{ 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/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/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/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/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/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/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/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/90/2e/59d6491834b6e289051b252cf4776d16da51c7c6ca6a87ff97e3a50aa0cd/greenlet-3.2.2-cp313-cp313-win_amd64.whl", hash = "sha256:fe46d4f8e94e637634d54477b0cfabcf93c53f29eedcbdeecaf2af32029b4421", size = 296023 },
|
||||
{ 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/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/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/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/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/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/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/31/df/b7d17d66c8d0f578d2885a3d8f565e9e4725eacc9d3fdc946d0031c055c4/greenlet-3.2.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:9ea5231428af34226c05f927e16fc7f6fa5e39e3ad3cd24ffa48ba53a47f4240", size = 269899 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -512,7 +512,7 @@ requires-dist = [
|
|||
{ name = "oauthlib", specifier = "==3.2.2" },
|
||||
{ name = "packaging", specifier = "==23.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-binary", specifier = "==3.2.3" },
|
||||
{ name = "pycparser", specifier = "==2.21" },
|
||||
|
|
@ -528,20 +528,21 @@ requires-dist = [
|
|||
|
||||
[[package]]
|
||||
name = "playwright"
|
||||
version = "1.51.0"
|
||||
version = "1.52.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "greenlet" },
|
||||
{ name = "pyee" },
|
||||
]
|
||||
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/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/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/7a/fd/bc60798803414ecab66456208eeff4308344d0c055ca0d294d2cdd692b60/playwright-1.51.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:d5c9f67bc6ef49094618991c78a1466c5bac5ed09157660d78b8510b77f92746", size = 45164868 },
|
||||
{ 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/51/e4/4342f0bd51727df790deda95ee35db066ac05cf4593a73d0c42249fa39a6/playwright-1.51.0-py3-none-win32.whl", hash = "sha256:4cef804991867ea27f608b70fa288ee52a57651e22d02ab287f98f8620b9408c", size = 34862688 },
|
||||
{ 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/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/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/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/73/c6/8e27af9798f81465b299741ef57064c6ec1a31128ed297406469907dc5a4/playwright-1.52.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:d010124d24a321e0489a8c0d38a3971a7ca7656becea7656c9376bfea7f916d4", size = 45141333 },
|
||||
{ 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/7a/81/a850dbc6bc2e1bd6cc87341e59c253269602352de83d34b00ea38cf410ee/playwright-1.52.0-py3-none-win32.whl", hash = "sha256:cd0bdf92df99db6237a99f828e80a6a50db6180ef8d5352fc9495df2c92f9971", size = 34839156 },
|
||||
{ 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]]
|
||||
|
|
@ -597,14 +598,14 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "pyee"
|
||||
version = "12.1.1"
|
||||
version = "13.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ 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 = [
|
||||
{ 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]]
|
||||
|
|
@ -658,11 +659,11 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "setuptools"
|
||||
version = "80.3.1"
|
||||
version = "80.7.1"
|
||||
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 = [
|
||||
{ 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]]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue