From 6f57699c8d006e2fb8dbfc910a279931e9d4e3ae Mon Sep 17 00:00:00 2001 From: badbl0cks <4161747+badbl0cks@users.noreply.github.com> Date: Sun, 18 May 2025 11:27:59 -0700 Subject: [PATCH] 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. --- .github/workflows/build.yml | 62 ---- .github/workflows/build_deploy.yml | 308 ++++++++++++++++++ .github/workflows/test.yml | 32 +- .gitignore | 1 + .vscode/tasks.json | 4 +- Dockerfile | 21 +- README.md | 2 +- docker-compose.yml | 28 +- docker-compose_prod.yml | 27 -- haproxy/haproxy.cfg | 22 -- haproxy/haproxy.dev.cfg | 18 - pyproject.toml | 2 +- scripts/deploy.sh | 2 +- scripts/entrypoint.sh | 16 +- scripts/haproxy.entrypoint.sh | 25 -- scripts/prebuild.sh | 3 +- scripts/reset-db_make-migrations_seed-data.sh | 15 +- server/docker-compose_core.yml | 76 +++++ server/docker-compose_staging.yml | 20 ++ server/docker-compose_web.yml | 28 ++ server/gatus/config.yaml | 136 ++++++++ server/haproxy.cfg | 50 +++ src/pkmntrade_club/accounts/adapter.py | 13 + .../accounts/migrations/0001_initial.py | 4 +- .../django_project/middleware.py | 20 -- src/pkmntrade_club/django_project/settings.py | 84 +++-- src/pkmntrade_club/django_project/urls.py | 11 +- uv.lock | 93 +++--- 28 files changed, 795 insertions(+), 328 deletions(-) delete mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/build_deploy.yml delete mode 100644 docker-compose_prod.yml delete mode 100644 haproxy/haproxy.cfg delete mode 100644 haproxy/haproxy.dev.cfg delete mode 100644 scripts/haproxy.entrypoint.sh create mode 100644 server/docker-compose_core.yml create mode 100644 server/docker-compose_staging.yml create mode 100644 server/docker-compose_web.yml create mode 100644 server/gatus/config.yaml create mode 100644 server/haproxy.cfg create mode 100644 src/pkmntrade_club/accounts/adapter.py delete mode 100644 src/pkmntrade_club/django_project/middleware.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index 39c6e98..0000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -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 }}/ \ No newline at end of file diff --git a/.github/workflows/build_deploy.yml b/.github/workflows/build_deploy.yml new file mode 100644 index 0000000..be961aa --- /dev/null +++ b/.github/workflows/build_deploy.yml @@ -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<> "$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 < ${{ 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 << # 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 <=11.2.1", - "playwright==1.51.0", + "playwright==1.52.0", "psycopg==3.2.3", "psycopg-binary==3.2.3", "pycparser==2.21", diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 76e3d3e..4e97231 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -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 diff --git a/scripts/entrypoint.sh b/scripts/entrypoint.sh index 0ea62df..c63b65e 100755 --- a/scripts/entrypoint.sh +++ b/scripts/entrypoint.sh @@ -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 \ No newline at end of file diff --git a/scripts/haproxy.entrypoint.sh b/scripts/haproxy.entrypoint.sh deleted file mode 100644 index 2557fe6..0000000 --- a/scripts/haproxy.entrypoint.sh +++ /dev/null @@ -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 "$@" diff --git a/scripts/prebuild.sh b/scripts/prebuild.sh index e93a7a2..93e436c 100755 --- a/scripts/prebuild.sh +++ b/scripts/prebuild.sh @@ -8,5 +8,4 @@ fi # Build the tailwind theme css cd src/pkmntrade_club/theme/static_src -npm install . && npm run build -cd ../../ \ No newline at end of file +npm install . && npm run build \ No newline at end of file diff --git a/scripts/reset-db_make-migrations_seed-data.sh b/scripts/reset-db_make-migrations_seed-data.sh index 18630bd..406e185 100755 --- a/scripts/reset-db_make-migrations_seed-data.sh +++ b/scripts/reset-db_make-migrations_seed-data.sh @@ -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*" diff --git a/server/docker-compose_core.yml b/server/docker-compose_core.yml new file mode 100644 index 0000000..f40aef3 --- /dev/null +++ b/server/docker-compose_core.yml @@ -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 \ No newline at end of file diff --git a/server/docker-compose_staging.yml b/server/docker-compose_staging.yml new file mode 100644 index 0000000..70f2032 --- /dev/null +++ b/server/docker-compose_staging.yml @@ -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 \ No newline at end of file diff --git a/server/docker-compose_web.yml b/server/docker-compose_web.yml new file mode 100644 index 0000000..8018d4a --- /dev/null +++ b/server/docker-compose_web.yml @@ -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 \ No newline at end of file diff --git a/server/gatus/config.yaml b/server/gatus/config.yaml new file mode 100644 index 0000000..98ae997 --- /dev/null +++ b/server/gatus/config.yaml @@ -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 diff --git a/server/haproxy.cfg b/server/haproxy.cfg new file mode 100644 index 0000000..6c9bde8 --- /dev/null +++ b/server/haproxy.cfg @@ -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 + diff --git a/src/pkmntrade_club/accounts/adapter.py b/src/pkmntrade_club/accounts/adapter.py new file mode 100644 index 0000000..b41e761 --- /dev/null +++ b/src/pkmntrade_club/accounts/adapter.py @@ -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 \ No newline at end of file diff --git a/src/pkmntrade_club/accounts/migrations/0001_initial.py b/src/pkmntrade_club/accounts/migrations/0001_initial.py index d28133b..a706671 100644 --- a/src/pkmntrade_club/accounts/migrations/0001_initial.py +++ b/src/pkmntrade_club/accounts/migrations/0001_initial.py @@ -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 = [ diff --git a/src/pkmntrade_club/django_project/middleware.py b/src/pkmntrade_club/django_project/middleware.py deleted file mode 100644 index 36103fe..0000000 --- a/src/pkmntrade_club/django_project/middleware.py +++ /dev/null @@ -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") - diff --git a/src/pkmntrade_club/django_project/settings.py b/src/pkmntrade_club/django_project/settings.py index 73beb81..535f92d 100644 --- a/src/pkmntrade_club/django_project/settings.py +++ b/src/pkmntrade_club/django_project/settings.py @@ -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 } } diff --git a/src/pkmntrade_club/django_project/urls.py b/src/pkmntrade_club/django_project/urls.py index 2cf77c2..e01ec1f 100644 --- a/src/pkmntrade_club/django_project/urls.py +++ b/src/pkmntrade_club/django_project/urls.py @@ -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() diff --git a/uv.lock b/uv.lock index 5cf1c00..15e3af1 100644 --- a/uv.lock +++ b/uv.lock @@ -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]]