diff --git a/heavenly-hostas/.gitattributes b/heavenly-hostas/.gitattributes new file mode 100644 index 00000000..541aa8a6 --- /dev/null +++ b/heavenly-hostas/.gitattributes @@ -0,0 +1,5 @@ +# Blender 3D models +*.blend binary +# Model binaries +*.glb binary +*.hdr binary diff --git a/heavenly-hostas/.github/workflows/data.yaml b/heavenly-hostas/.github/workflows/data.yaml new file mode 100644 index 00000000..7d79f454 --- /dev/null +++ b/heavenly-hostas/.github/workflows/data.yaml @@ -0,0 +1,98 @@ +name: Validate and auto-handle PR + +on: + pull_request_target: + types: [opened] + branches: + - data + +permissions: + pull-requests: write + contents: write + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - name: Set up GitHub CLI + run: | + sudo apt-get update + sudo apt-get install -y gh jq + gh --version + + - name: Check out PR + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Validate PR + id: validate + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + + PR_NUMBER=${{ github.event.pull_request.number }} + + # ensure a single commit + commit_count=$(gh pr view $PR_NUMBER --json commits -q '.commits | length') + if [ "$commit_count" -ne 1 ]; then + echo "::error::PR must have exactly one commit" + echo "valid=false" >> $GITHUB_OUTPUT + exit 0 + fi + + # ensure a single file + files=$(gh pr diff $PR_NUMBER --name-only) + file_count=$(echo "$files" | wc -l) + if [ "$file_count" -ne 1 ]; then + echo "::error::PR must modify exactly one file" + echo "valid=false" >> $GITHUB_OUTPUT + exit 0 + fi + + + commit_hash=${{ github.event.pull_request.head.sha }} + filename="$files" + + echo "Commit: $commit_hash" + echo "File: $filename" + + # ensure PR came from the backend + response=$(curl -s \ + "https://cj12.matiiss.com/api/verify_pr?filename=$filename&commit_hash=$commit_hash") + + echo "Endpoint response: $response" + + is_valid=$(echo "$response" | jq -r '.is_valid') + + if [ "$is_valid" != "true" ]; then + echo "::error::Validation endpoint returned is_valid=$is_valid" + echo "valid=false" >> $GITHUB_OUTPUT + exit 0 + fi + + echo "valid=true" >> $GITHUB_OUTPUT + echo "commit_hash=$commit_hash" >> $GITHUB_OUTPUT + echo "filename=$filename" >> $GITHUB_OUTPUT + + - name: Close PR if invalid + if: steps.validate.outputs.valid == 'false' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh pr close ${{ github.event.pull_request.number }} \ + --repo ${{ github.repository }} \ + --comment "Validation failed, closing the PR" + + - name: Merge PR if valid + if: steps.validate.outputs.valid == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh pr merge ${{ github.event.pull_request.number }} \ + --repo ${{ github.repository }} \ + --merge \ + --auto \ + --delete-branch \ + --body "Validation successful, merging the PR" diff --git a/heavenly-hostas/.github/workflows/lint.yaml b/heavenly-hostas/.github/workflows/lint.yaml new file mode 100644 index 00000000..7f67e803 --- /dev/null +++ b/heavenly-hostas/.github/workflows/lint.yaml @@ -0,0 +1,35 @@ +# GitHub Action workflow enforcing our code style. + +name: Lint + +# Trigger the workflow on both push (to the main repository, on the main branch) +# and pull requests (against the main repository, but from any repo, from any branch). +on: + push: + branches: + - main + pull_request: + +# Brand new concurrency setting! This ensures that not more than one run can be triggered for the same commit. +# It is useful for pull requests coming from the main repository since both triggers will match. +concurrency: lint-${{ github.sha }} + +jobs: + lint: + runs-on: ubuntu-latest + + env: + # The Python version your project uses. Feel free to change this if required. + PYTHON_VERSION: "3.12" + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python ${{ env.PYTHON_VERSION }} + uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Run pre-commit hooks + uses: pre-commit/action@v3.0.1 diff --git a/heavenly-hostas/.github/workflows/static-deploy.yaml b/heavenly-hostas/.github/workflows/static-deploy.yaml new file mode 100644 index 00000000..80c4f267 --- /dev/null +++ b/heavenly-hostas/.github/workflows/static-deploy.yaml @@ -0,0 +1,43 @@ +# Simple workflow for deploying static content to GitHub Pages +name: Deploy static content to Pages + +on: + # Runs on pushes targeting the default branch + push: + branches: ["main", "test-static-deploy"] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + # Single deploy job since we're just deploying + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Pages + uses: actions/configure-pages@v5 + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + # upload only the gallery + path: "./packages/gallery" + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/heavenly-hostas/.gitignore b/heavenly-hostas/.gitignore new file mode 100644 index 00000000..8789461f --- /dev/null +++ b/heavenly-hostas/.gitignore @@ -0,0 +1,40 @@ +# Files generated by the interpreter +__pycache__/ +*.py[cod] + +# Environment specific +.venv +venv +.env +env + +# Unittest reports +.coverage* + +# Logs +*.log + +# PyEnv version selector +.python-version + +# Built objects +*.so +dist/ +build/ + +# IDEs +# PyCharm +.idea/ +# VSCode +.vscode/ +# MacOS +.DS_Store + +# We're keeping .blend backup files out of the repo +*.blend1 + +# supabase volumes +volumes/ + +# secrets +*.pem diff --git a/heavenly-hostas/.pre-commit-config.yaml b/heavenly-hostas/.pre-commit-config.yaml new file mode 100644 index 00000000..c0a8de23 --- /dev/null +++ b/heavenly-hostas/.pre-commit-config.yaml @@ -0,0 +1,18 @@ +# Pre-commit configuration. +# See https://github.com/python-discord/code-jam-template/tree/main#pre-commit-run-linting-before-committing + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: check-toml + - id: check-yaml + - id: end-of-file-fixer + - id: trailing-whitespace + args: [--markdown-linebreak-ext=md] + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.12.2 + hooks: + - id: ruff-check + - id: ruff-format diff --git a/heavenly-hostas/Dockerfile.backend b/heavenly-hostas/Dockerfile.backend new file mode 100644 index 00000000..801cfdbd --- /dev/null +++ b/heavenly-hostas/Dockerfile.backend @@ -0,0 +1,21 @@ +FROM python:3.12-slim + +COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ + +WORKDIR /app + +RUN --mount=type=cache,target=/root/.cache/uv \ + --mount=type=bind,source=uv.lock,target=uv.lock \ + --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ + uv sync --frozen --no-install-workspace --package=backend + +# we'll rely on mounts for now +# ... +# err, nvm +ADD ./packages/backend /app + +# RUN --mount=type=cache,target=/root/.cache/uv \ +# uv sync --frozen + +# Run with uvicorn +CMD ["uv", "run", "uvicorn", "server:app", "--host", "0.0.0.0", "--port", "9000"] diff --git a/heavenly-hostas/LICENSE.txt b/heavenly-hostas/LICENSE.txt new file mode 100644 index 00000000..5a04926b --- /dev/null +++ b/heavenly-hostas/LICENSE.txt @@ -0,0 +1,7 @@ +Copyright 2021 Python Discord + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/heavenly-hostas/README.md b/heavenly-hostas/README.md new file mode 100644 index 00000000..e6f82560 --- /dev/null +++ b/heavenly-hostas/README.md @@ -0,0 +1,10 @@ +# Heavenly Hostas' Hosting + +This is our submission for the official Python Discord's code jam! + +The project is divided into three main components, please check their respective READMEs for details: +- [Image Editor](https://github.com/heavenly-hostas-hosting/HHH/tree/main/packages/editor) +- [Image Gallery](https://github.com/heavenly-hostas-hosting/HHH/tree/main/packages/gallery) +- [Global Image DB](https://github.com/heavenly-hostas-hosting/HHH/tree/main/packages/backend) + +You can also access a slides presentation on our project [here](https://docs.google.com/presentation/d/1ngL511CRSySNVy05QM7rpqqBGVEvdEtr4sJroFdb_BI/edit?usp=sharing). diff --git a/heavenly-hostas/docker-compose.yaml b/heavenly-hostas/docker-compose.yaml new file mode 100644 index 00000000..46489d13 --- /dev/null +++ b/heavenly-hostas/docker-compose.yaml @@ -0,0 +1,248 @@ +name: cj12 + +services: + cj12-backend: + build: + context: . + dockerfile: Dockerfile.backend + image: cj12-backend + container_name: cj12-backend + ports: + - "9000:9000" + # volumes: + # - ./packages/backend:/app + # - ./packages/backend/.env:/app/.env:ro + env_file: ./packages/backend/.env + networks: + - default + - shared-net + + cj12-editor: + image: zauberzeug/nicegui:latest + restart: always + ports: + - 9010:9010 + environment: + - PUID=1000 # change this to your user id + - PGID=1000 # change this to your group id + volumes: + - ./packages/editor:/app + networks: + - shared-net + + studio: + container_name: supabase-studio + image: supabase/studio:2025.06.30-sha-6f5982d + restart: unless-stopped + healthcheck: + test: + [ + "CMD", + "node", + "-e", + "fetch('http://studio:3000/api/platform/profile').then((r) => {if (r.status !== 200) throw new Error(r.status)})", + ] + timeout: 10s + interval: 5s + retries: 3 + environment: + STUDIO_PG_META_URL: http://meta:8080 + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + + DEFAULT_ORGANIZATION_NAME: ${STUDIO_DEFAULT_ORGANIZATION} + DEFAULT_PROJECT_NAME: ${STUDIO_DEFAULT_PROJECT} + + SUPABASE_URL: http://kong:8000 + SUPABASE_PUBLIC_URL: ${SUPABASE_PUBLIC_URL} + SUPABASE_ANON_KEY: ${ANON_KEY} + SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY} + AUTH_JWT_SECRET: ${JWT_SECRET} + + kong: + container_name: supabase-kong + image: kong:2.8.1 + restart: unless-stopped + # ports: + # - ${KONG_HTTP_PORT}:8000/tcp + # - ${KONG_HTTPS_PORT}:8443/tcp + volumes: + # https://github.com/supabase/supabase/issues/12661 + - ./volumes/api/kong.yml:/home/kong/temp.yml:ro,z + networks: + - default + - shared-net + environment: + KONG_DATABASE: "off" + KONG_DECLARATIVE_CONFIG: /home/kong/kong.yml + # https://github.com/supabase/cli/issues/14 + KONG_DNS_ORDER: LAST,A,CNAME + KONG_PLUGINS: request-transformer,cors,key-auth,acl,basic-auth + KONG_NGINX_PROXY_PROXY_BUFFER_SIZE: 160k + KONG_NGINX_PROXY_PROXY_BUFFERS: 64 160k + SUPABASE_ANON_KEY: ${ANON_KEY} + SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY} + DASHBOARD_USERNAME: ${DASHBOARD_USERNAME} + DASHBOARD_PASSWORD: ${DASHBOARD_PASSWORD} + # https://unix.stackexchange.com/a/294837 + entrypoint: bash -c 'eval "echo \"$$(cat ~/temp.yml)\"" > ~/kong.yml && /docker-entrypoint.sh kong docker-start' + + auth: + container_name: supabase-auth + image: supabase/gotrue:v2.177.0 + restart: unless-stopped + healthcheck: + test: + [ + "CMD", + "wget", + "--no-verbose", + "--tries=1", + "--spider", + "http://localhost:9999/health", + ] + timeout: 5s + interval: 5s + retries: 3 + depends_on: + db: + condition: service_healthy + environment: + GOTRUE_API_HOST: 0.0.0.0 + GOTRUE_API_PORT: 9999 + API_EXTERNAL_URL: ${API_EXTERNAL_URL} + + GOTRUE_DB_DRIVER: postgres + GOTRUE_DB_DATABASE_URL: postgres://supabase_auth_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB} + + GOTRUE_SITE_URL: ${SITE_URL} + GOTRUE_URI_ALLOW_LIST: ${ADDITIONAL_REDIRECT_URLS} + GOTRUE_DISABLE_SIGNUP: ${DISABLE_SIGNUP} + + GOTRUE_JWT_ADMIN_ROLES: service_role + GOTRUE_JWT_AUD: authenticated + GOTRUE_JWT_DEFAULT_GROUP_NAME: authenticated + GOTRUE_JWT_EXP: ${JWT_EXPIRY} + GOTRUE_JWT_SECRET: ${JWT_SECRET} + + GOTRUE_EXTERNAL_GITHUB_ENABLED: true + GOTRUE_EXTERNAL_GITHUB_CLIENT_ID: ${CLIENT_ID} + GOTRUE_EXTERNAL_GITHUB_SECRET: ${CLIENT_SECRET} + GOTRUE_EXTERNAL_GITHUB_REDIRECT_URI: ${GOTRUE_EXTERNAL_GITHUB_REDIRECT_URI} + + meta: + container_name: supabase-meta + image: supabase/postgres-meta:v0.91.0 + restart: unless-stopped + depends_on: + db: + condition: service_healthy + environment: + PG_META_PORT: 8080 + PG_META_DB_HOST: ${POSTGRES_HOST} + PG_META_DB_PORT: ${POSTGRES_PORT} + PG_META_DB_NAME: ${POSTGRES_DB} + PG_META_DB_USER: supabase_admin + PG_META_DB_PASSWORD: ${POSTGRES_PASSWORD} + + db: + container_name: supabase-db + image: supabase/postgres:15.8.1.060 + restart: unless-stopped + volumes: + - ./volumes/db/realtime.sql:/docker-entrypoint-initdb.d/migrations/99-realtime.sql:Z + # Must be superuser to create event trigger + - ./volumes/db/webhooks.sql:/docker-entrypoint-initdb.d/init-scripts/98-webhooks.sql:Z + # Must be superuser to alter reserved role + - ./volumes/db/roles.sql:/docker-entrypoint-initdb.d/init-scripts/99-roles.sql:Z + # Initialize the database settings with JWT_SECRET and JWT_EXP + - ./volumes/db/jwt.sql:/docker-entrypoint-initdb.d/init-scripts/99-jwt.sql:Z + # PGDATA directory is persisted between restarts + - ./volumes/db/data:/var/lib/postgresql/data:Z + # Changes required for internal supabase data such as _analytics + - ./volumes/db/_supabase.sql:/docker-entrypoint-initdb.d/migrations/97-_supabase.sql:Z + # Changes required for Analytics support + - ./volumes/db/logs.sql:/docker-entrypoint-initdb.d/migrations/99-logs.sql:Z + # Changes required for Pooler support + - ./volumes/db/pooler.sql:/docker-entrypoint-initdb.d/migrations/99-pooler.sql:Z + # Use named volume to persist pgsodium decryption key between restarts + - db-config:/etc/postgresql-custom + healthcheck: + test: ["CMD", "pg_isready", "-U", "postgres", "-h", "localhost"] + interval: 5s + timeout: 5s + retries: 10 + environment: + POSTGRES_HOST: /var/run/postgresql + PGPORT: ${POSTGRES_PORT} + POSTGRES_PORT: ${POSTGRES_PORT} + PGPASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + PGDATABASE: ${POSTGRES_DB} + POSTGRES_DB: ${POSTGRES_DB} + JWT_SECRET: ${JWT_SECRET} + JWT_EXP: ${JWT_EXPIRY} + command: [ + "postgres", + "-c", + "config_file=/etc/postgresql/postgresql.conf", + "-c", + "log_min_messages=fatal", # prevents Realtime polling queries from appearing in logs + ] + + supavisor: + container_name: supabase-pooler + image: supabase/supavisor:2.5.7 + restart: unless-stopped + ports: + - ${POSTGRES_PORT}:5432 + - ${POOLER_PROXY_PORT_TRANSACTION}:6543 + volumes: + - ./volumes/pooler/pooler.exs:/etc/pooler/pooler.exs:ro,z + healthcheck: + test: + [ + "CMD", + "curl", + "-sSfL", + "--head", + "-o", + "/dev/null", + "http://127.0.0.1:4000/api/health", + ] + interval: 10s + timeout: 5s + retries: 5 + depends_on: + db: + condition: service_healthy + environment: + PORT: 4000 + POSTGRES_PORT: ${POSTGRES_PORT} + POSTGRES_DB: ${POSTGRES_DB} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + DATABASE_URL: ecto://supabase_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/_supabase + CLUSTER_POSTGRES: true + SECRET_KEY_BASE: ${SECRET_KEY_BASE} + VAULT_ENC_KEY: ${VAULT_ENC_KEY} + API_JWT_SECRET: ${JWT_SECRET} + METRICS_JWT_SECRET: ${JWT_SECRET} + REGION: local + ERL_AFLAGS: -proto_dist inet_tcp + POOLER_TENANT_ID: ${POOLER_TENANT_ID} + POOLER_DEFAULT_POOL_SIZE: ${POOLER_DEFAULT_POOL_SIZE} + POOLER_MAX_CLIENT_CONN: ${POOLER_MAX_CLIENT_CONN} + POOLER_POOL_MODE: transaction + DB_POOL_SIZE: ${POOLER_DB_POOL_SIZE} + command: + [ + "/bin/sh", + "-c", + '/app/bin/migrate && /app/bin/supavisor eval "$$(cat /etc/pooler/pooler.exs)" && /app/bin/server', + ] + +volumes: + db-config: + +networks: + shared-net: + external: true diff --git a/heavenly-hostas/docs/backend/assets/actions_variables.png b/heavenly-hostas/docs/backend/assets/actions_variables.png new file mode 100644 index 00000000..062d4b9b Binary files /dev/null and b/heavenly-hostas/docs/backend/assets/actions_variables.png differ diff --git a/heavenly-hostas/docs/backend/assets/actions_variables_mgmt.png b/heavenly-hostas/docs/backend/assets/actions_variables_mgmt.png new file mode 100644 index 00000000..d267bcdb Binary files /dev/null and b/heavenly-hostas/docs/backend/assets/actions_variables_mgmt.png differ diff --git a/heavenly-hostas/docs/backend/assets/app_gen_key.png b/heavenly-hostas/docs/backend/assets/app_gen_key.png new file mode 100644 index 00000000..ce2c46ff Binary files /dev/null and b/heavenly-hostas/docs/backend/assets/app_gen_key.png differ diff --git a/heavenly-hostas/docs/backend/assets/app_gen_secrets.png b/heavenly-hostas/docs/backend/assets/app_gen_secrets.png new file mode 100644 index 00000000..c805809a Binary files /dev/null and b/heavenly-hostas/docs/backend/assets/app_gen_secrets.png differ diff --git a/heavenly-hostas/docs/backend/assets/app_name_site_callback.png b/heavenly-hostas/docs/backend/assets/app_name_site_callback.png new file mode 100644 index 00000000..d9aea733 Binary files /dev/null and b/heavenly-hostas/docs/backend/assets/app_name_site_callback.png differ diff --git a/heavenly-hostas/docs/backend/assets/app_no_active_hooks.png b/heavenly-hostas/docs/backend/assets/app_no_active_hooks.png new file mode 100644 index 00000000..ee050e35 Binary files /dev/null and b/heavenly-hostas/docs/backend/assets/app_no_active_hooks.png differ diff --git a/heavenly-hostas/docs/backend/assets/app_save_changes.png b/heavenly-hostas/docs/backend/assets/app_save_changes.png new file mode 100644 index 00000000..4b66d673 Binary files /dev/null and b/heavenly-hostas/docs/backend/assets/app_save_changes.png differ diff --git a/heavenly-hostas/docs/backend/assets/app_select_any_acc_and_create.png b/heavenly-hostas/docs/backend/assets/app_select_any_acc_and_create.png new file mode 100644 index 00000000..3cf976ee Binary files /dev/null and b/heavenly-hostas/docs/backend/assets/app_select_any_acc_and_create.png differ diff --git a/heavenly-hostas/docs/backend/assets/arrow_to_create_new.png b/heavenly-hostas/docs/backend/assets/arrow_to_create_new.png new file mode 100644 index 00000000..53d2d87d Binary files /dev/null and b/heavenly-hostas/docs/backend/assets/arrow_to_create_new.png differ diff --git a/heavenly-hostas/docs/backend/assets/arrow_to_developer_settings.png b/heavenly-hostas/docs/backend/assets/arrow_to_developer_settings.png new file mode 100644 index 00000000..5f5ac34d Binary files /dev/null and b/heavenly-hostas/docs/backend/assets/arrow_to_developer_settings.png differ diff --git a/heavenly-hostas/docs/backend/assets/arrow_to_edit_file.png b/heavenly-hostas/docs/backend/assets/arrow_to_edit_file.png new file mode 100644 index 00000000..cb7bad1e Binary files /dev/null and b/heavenly-hostas/docs/backend/assets/arrow_to_edit_file.png differ diff --git a/heavenly-hostas/docs/backend/assets/arrow_to_new_app.png b/heavenly-hostas/docs/backend/assets/arrow_to_new_app.png new file mode 100644 index 00000000..32687382 Binary files /dev/null and b/heavenly-hostas/docs/backend/assets/arrow_to_new_app.png differ diff --git a/heavenly-hostas/docs/backend/assets/arrow_to_new_repo.png b/heavenly-hostas/docs/backend/assets/arrow_to_new_repo.png new file mode 100644 index 00000000..79ce23ec Binary files /dev/null and b/heavenly-hostas/docs/backend/assets/arrow_to_new_repo.png differ diff --git a/heavenly-hostas/docs/backend/assets/arrow_to_pages_settings.png b/heavenly-hostas/docs/backend/assets/arrow_to_pages_settings.png new file mode 100644 index 00000000..bb3d1a81 Binary files /dev/null and b/heavenly-hostas/docs/backend/assets/arrow_to_pages_settings.png differ diff --git a/heavenly-hostas/docs/backend/assets/arrow_to_path_packages.png b/heavenly-hostas/docs/backend/assets/arrow_to_path_packages.png new file mode 100644 index 00000000..9f6b756b Binary files /dev/null and b/heavenly-hostas/docs/backend/assets/arrow_to_path_packages.png differ diff --git a/heavenly-hostas/docs/backend/assets/arrow_to_path_packages_gallery.png b/heavenly-hostas/docs/backend/assets/arrow_to_path_packages_gallery.png new file mode 100644 index 00000000..57b6c990 Binary files /dev/null and b/heavenly-hostas/docs/backend/assets/arrow_to_path_packages_gallery.png differ diff --git a/heavenly-hostas/docs/backend/assets/arrow_to_path_packages_gallery_assets.png b/heavenly-hostas/docs/backend/assets/arrow_to_path_packages_gallery_assets.png new file mode 100644 index 00000000..504e0e14 Binary files /dev/null and b/heavenly-hostas/docs/backend/assets/arrow_to_path_packages_gallery_assets.png differ diff --git a/heavenly-hostas/docs/backend/assets/arrow_to_path_packages_gallery_assets_editor-html.png b/heavenly-hostas/docs/backend/assets/arrow_to_path_packages_gallery_assets_editor-html.png new file mode 100644 index 00000000..abce2213 Binary files /dev/null and b/heavenly-hostas/docs/backend/assets/arrow_to_path_packages_gallery_assets_editor-html.png differ diff --git a/heavenly-hostas/docs/backend/assets/arrow_to_path_packages_gallery_main-py.png b/heavenly-hostas/docs/backend/assets/arrow_to_path_packages_gallery_main-py.png new file mode 100644 index 00000000..128af4a3 Binary files /dev/null and b/heavenly-hostas/docs/backend/assets/arrow_to_path_packages_gallery_main-py.png differ diff --git a/heavenly-hostas/docs/backend/assets/arrow_to_pfp.png b/heavenly-hostas/docs/backend/assets/arrow_to_pfp.png new file mode 100644 index 00000000..bf0bf0d9 Binary files /dev/null and b/heavenly-hostas/docs/backend/assets/arrow_to_pfp.png differ diff --git a/heavenly-hostas/docs/backend/assets/arrow_to_repo_settings.png b/heavenly-hostas/docs/backend/assets/arrow_to_repo_settings.png new file mode 100644 index 00000000..a4d97130 Binary files /dev/null and b/heavenly-hostas/docs/backend/assets/arrow_to_repo_settings.png differ diff --git a/heavenly-hostas/docs/backend/assets/arrow_to_settings.png b/heavenly-hostas/docs/backend/assets/arrow_to_settings.png new file mode 100644 index 00000000..cb3c7b28 Binary files /dev/null and b/heavenly-hostas/docs/backend/assets/arrow_to_settings.png differ diff --git a/heavenly-hostas/docs/backend/assets/code_button.png b/heavenly-hostas/docs/backend/assets/code_button.png new file mode 100644 index 00000000..39e3bedc Binary files /dev/null and b/heavenly-hostas/docs/backend/assets/code_button.png differ diff --git a/heavenly-hostas/docs/backend/assets/code_button_dropdown.png b/heavenly-hostas/docs/backend/assets/code_button_dropdown.png new file mode 100644 index 00000000..13e9ba8b Binary files /dev/null and b/heavenly-hostas/docs/backend/assets/code_button_dropdown.png differ diff --git a/heavenly-hostas/docs/backend/assets/commit_changes_1.png b/heavenly-hostas/docs/backend/assets/commit_changes_1.png new file mode 100644 index 00000000..6c4cb1bc Binary files /dev/null and b/heavenly-hostas/docs/backend/assets/commit_changes_1.png differ diff --git a/heavenly-hostas/docs/backend/assets/commit_changes_2.png b/heavenly-hostas/docs/backend/assets/commit_changes_2.png new file mode 100644 index 00000000..9b86a58c Binary files /dev/null and b/heavenly-hostas/docs/backend/assets/commit_changes_2.png differ diff --git a/heavenly-hostas/docs/backend/assets/forking-1.png b/heavenly-hostas/docs/backend/assets/forking-1.png new file mode 100644 index 00000000..204fa71e Binary files /dev/null and b/heavenly-hostas/docs/backend/assets/forking-1.png differ diff --git a/heavenly-hostas/docs/backend/assets/forking-2.png b/heavenly-hostas/docs/backend/assets/forking-2.png new file mode 100644 index 00000000..3dd53fcb Binary files /dev/null and b/heavenly-hostas/docs/backend/assets/forking-2.png differ diff --git a/heavenly-hostas/docs/backend/assets/gallery_editor_url_original.png b/heavenly-hostas/docs/backend/assets/gallery_editor_url_original.png new file mode 100644 index 00000000..b38babba Binary files /dev/null and b/heavenly-hostas/docs/backend/assets/gallery_editor_url_original.png differ diff --git a/heavenly-hostas/docs/backend/assets/gallery_editor_url_updated.png b/heavenly-hostas/docs/backend/assets/gallery_editor_url_updated.png new file mode 100644 index 00000000..fdb24d76 Binary files /dev/null and b/heavenly-hostas/docs/backend/assets/gallery_editor_url_updated.png differ diff --git a/heavenly-hostas/docs/backend/assets/gallery_repo_url_original.png b/heavenly-hostas/docs/backend/assets/gallery_repo_url_original.png new file mode 100644 index 00000000..88b8b867 Binary files /dev/null and b/heavenly-hostas/docs/backend/assets/gallery_repo_url_original.png differ diff --git a/heavenly-hostas/docs/backend/assets/gallery_repo_url_updated.png b/heavenly-hostas/docs/backend/assets/gallery_repo_url_updated.png new file mode 100644 index 00000000..5062e557 Binary files /dev/null and b/heavenly-hostas/docs/backend/assets/gallery_repo_url_updated.png differ diff --git a/heavenly-hostas/docs/backend/assets/gh_pages_settings.png b/heavenly-hostas/docs/backend/assets/gh_pages_settings.png new file mode 100644 index 00000000..eef0986c Binary files /dev/null and b/heavenly-hostas/docs/backend/assets/gh_pages_settings.png differ diff --git a/heavenly-hostas/docs/backend/assets/goto_existing_fork.png b/heavenly-hostas/docs/backend/assets/goto_existing_fork.png new file mode 100644 index 00000000..021c2afd Binary files /dev/null and b/heavenly-hostas/docs/backend/assets/goto_existing_fork.png differ diff --git a/heavenly-hostas/docs/backend/assets/new_repo_creation.png b/heavenly-hostas/docs/backend/assets/new_repo_creation.png new file mode 100644 index 00000000..233637c1 Binary files /dev/null and b/heavenly-hostas/docs/backend/assets/new_repo_creation.png differ diff --git a/heavenly-hostas/docs/backend/assets/new_repo_var.png b/heavenly-hostas/docs/backend/assets/new_repo_var.png new file mode 100644 index 00000000..d3c661fa Binary files /dev/null and b/heavenly-hostas/docs/backend/assets/new_repo_var.png differ diff --git a/heavenly-hostas/docs/backend/assets/perms_account.png b/heavenly-hostas/docs/backend/assets/perms_account.png new file mode 100644 index 00000000..1759d6cf Binary files /dev/null and b/heavenly-hostas/docs/backend/assets/perms_account.png differ diff --git a/heavenly-hostas/docs/backend/assets/perms_contents_rw.png b/heavenly-hostas/docs/backend/assets/perms_contents_rw.png new file mode 100644 index 00000000..ec8c2655 Binary files /dev/null and b/heavenly-hostas/docs/backend/assets/perms_contents_rw.png differ diff --git a/heavenly-hostas/docs/backend/assets/perms_email_ro.png b/heavenly-hostas/docs/backend/assets/perms_email_ro.png new file mode 100644 index 00000000..aaa9aaeb Binary files /dev/null and b/heavenly-hostas/docs/backend/assets/perms_email_ro.png differ diff --git a/heavenly-hostas/docs/backend/assets/perms_prs_rw.png b/heavenly-hostas/docs/backend/assets/perms_prs_rw.png new file mode 100644 index 00000000..df5ad563 Binary files /dev/null and b/heavenly-hostas/docs/backend/assets/perms_prs_rw.png differ diff --git a/heavenly-hostas/docs/backend/assets/perms_repo.png b/heavenly-hostas/docs/backend/assets/perms_repo.png new file mode 100644 index 00000000..c9c91198 Binary files /dev/null and b/heavenly-hostas/docs/backend/assets/perms_repo.png differ diff --git a/heavenly-hostas/docs/backend/assets/push_existing_local_repo_to_empty_repo.png b/heavenly-hostas/docs/backend/assets/push_existing_local_repo_to_empty_repo.png new file mode 100644 index 00000000..e5b2982f Binary files /dev/null and b/heavenly-hostas/docs/backend/assets/push_existing_local_repo_to_empty_repo.png differ diff --git a/heavenly-hostas/docs/backend/assets/pyfetch_localhost_api.png b/heavenly-hostas/docs/backend/assets/pyfetch_localhost_api.png new file mode 100644 index 00000000..2b5b7d19 Binary files /dev/null and b/heavenly-hostas/docs/backend/assets/pyfetch_localhost_api.png differ diff --git a/heavenly-hostas/docs/backend/assets/pyfetch_matiiss_api.png b/heavenly-hostas/docs/backend/assets/pyfetch_matiiss_api.png new file mode 100644 index 00000000..64905201 Binary files /dev/null and b/heavenly-hostas/docs/backend/assets/pyfetch_matiiss_api.png differ diff --git a/heavenly-hostas/packages/backend/.env.template b/heavenly-hostas/packages/backend/.env.template new file mode 100644 index 00000000..4960f636 --- /dev/null +++ b/heavenly-hostas/packages/backend/.env.template @@ -0,0 +1,64 @@ +# --- GitHub App Configuration --- + +CLIENT_ID="YOUR_CLIENT_ID" +CLIENT_SECRET="YOUR_CLIENT_SECRET" + +GIT_UPSTREAM_OWNER="heavenly-hostas-hosting" +GIT_UPSTREAM_REPO="HHH" +GIT_UPSTREAM_DATA_BRANCH="data" +GIT_UPSTREAM_DATA_BRANCH_FIRST_COMMIT_HASH="6fe3ed2dd48fbaa0bebaee5a1eb377a603feedca" +GIT_UPSTREAM_APP_INSTALLATION_ID="81340179" + + +# --- Supabase Configuration --- +# For more information visit https://supabase.com/docs/guides/self-hosting/docker +# These variables are "inspired" by the official Supabase configuration example: +# https://github.com/supabase/supabase/blob/master/docker/.env.example + +# Secrets +# YOU MUST CHANGE THESE BEFORE GOING INTO PRODUCTION +POSTGRES_PASSWORD="your-super-secret-and-long-postgres-password" +JWT_SECRET="your-super-secret-jwt-token-with-at-least-32-characters-long" +ANON_KEY="your-anon-jwt-token" +SERVICE_ROLE_KEY="your-service-jwt-token" +DASHBOARD_USERNAME="supabase" +DASHBOARD_PASSWORD="this_password_is_insecure_and_should_be_updated" +SECRET_KEY_BASE="UpNVntn3cDxHJpq99YMc1T1AQgQpc8kfYTuRgBiYa15BLrx8etQoXz3gZv1/u2oq" +VAULT_ENC_KEY="your-encryption-key-32-chars-min" + +# Database - You can change these to any PostgreSQL database that has logical replication enabled. +POSTGRES_HOST="db" +POSTGRES_DB="postgres" +POSTGRES_PORT="5432" +# default user is postgres + +# Supavisor -- Database pooler +POOLER_PROXY_PORT_TRANSACTION="6543" +POOLER_DEFAULT_POOL_SIZE="20" +POOLER_MAX_CLIENT_CONN="100" +POOLER_TENANT_ID="your-tenant-id" +POOLER_DB_POOL_SIZE="5" + +# API Proxy - Configuration for the Kong Reverse proxy. +KONG_HTTP_PORT="8000" +KONG_HTTPS_PORT="8443" + +# Auth +# This is the same as the callback URL you set in your GitHub app. +GOTRUE_EXTERNAL_GITHUB_REDIRECT_URI="http://localhost:8000/auth/v1/callback" +GITHUB_CALLBACK_REDIRECT_URI="http://localhost:9000/auth" +POST_AUTH_REDIRECT_URI="https://cj12.matiiss.com/editor" + +# General +SITE_URL=${POST_AUTH_REDIRECT_URI} +ADDITIONAL_REDIRECT_URLS=${GITHUB_CALLBACK_REDIRECT_URI} +JWT_EXPIRY="3600" +DISABLE_SIGNUP="false" +API_EXTERNAL_URL="http://localhost:8000" + +# Studio - Configuration for the Dashboard +STUDIO_DEFAULT_ORGANIZATION="Default Organization" +STUDIO_DEFAULT_PROJECT="Default Project" +STUDIO_PORT="3000" +SUPABASE_PUBLIC_URL="http://localhost:8000" +SUPABASE_INTERNAL_URL="http://kong:8000" diff --git a/heavenly-hostas/packages/backend/README.md b/heavenly-hostas/packages/backend/README.md new file mode 100644 index 00000000..68fe7325 --- /dev/null +++ b/heavenly-hostas/packages/backend/README.md @@ -0,0 +1,422 @@ +## Deployment +**So you want to deploy the entire stack yourself? You've come to the right place.** + +> [!NOTE] +> This guide is primarily written to facilitate deployment from Linux machines, but the vast majority of the things done here should translate 1-to-1 to Windows as well + +Now, there are some prerequisites you would want to get sorted out before we begin: +- `git` is installed and accessible from your terminal + - To download it, head over to https://git-scm.com/downloads +- Docker Engine is installed and you have access to `docker` and `docker compose` commands from your terminal + - You can see a detailed guide on how to install the Docker Engine here: https://docs.docker.com/engine/install/. + - Likewise, for `docker compose` you can see the guide here: https://docs.docker.com/compose/install/. +- Two [GitHub](https://github.com/) accounts + - This is important because part of the stack is entirely based on GitHub and the setup requires that you create a GitHub app and host a repository on GitHub. This might be one of the first of your clues on how this project fits the "wrong tool for the job" theme. + - The other account is necessary if you want to test the deployment with the publishing system because you can't fork your own repositories + - You must also set up either HTTPS or SSH authentication (I personally would suggest SSH) for GitHub: https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/about-authentication-to-github#authenticating-with-the-command-line + +Alright, got that sorted out? Great, let's move on to the next steps. + +### Creating a GitHub repository + +There are two approaches you can use to achieve this, the simplest and fastest way would be to simply fork this repository, however, if you choose this approach, you will need another account to test the "database" aspect of this project. The other approach is to clone it locally, create an empty repository on GitHub and push the local clone to this newly created GitHub repository. + +> [!IMPORTANT] +> If you choose to **fork**, you **must** have another (either personal or organizational) account if you want to test the deployment out, because you will need to create a fork of your fork to establish the "database" + +--- + +
+Creating a new, unassociated repository + +
+ 1. Click on the + button in the top right corner of the page anywhere on the GitHub site + Red arrow pointing to '+' icon +
+ +
+ 2. Click the New repository button + Red arrow pointing to 'New repository' button +
+ +
+ 3. Pick a name for the repository, leave all the settings to their defaults, click Create repository + GitHub new repository creation screen +
+ +
+ 4. Open your terminal, clone https://github.com/heavenly-hostas-hosting/HHH locally, and cd into the created directory +

+git clone https://github.com/heavenly-hostas-hosting/HHH
+cd ./HHH
+    
+
+ +
+ 5. While in the same terminal, remove the origin remote +

+git remote remove origin
+    
+ If you get an error along the lines of the remote not existing, first list all your remotes, then remove the one (should be only one) that you see +

+git remote  # for example, this outputs 'upstream', then you'd do the following
+git remote remove upstream
+    
+
+ +
+ 6. Go to your newly created repository from step 3, select HTTPS or SSH depending on which one you have set up for your account (personally I would suggest using SSH), copy the …or push an existing repository from the command line command and paste and run it in your terminal + GitHub empty repository instructions +
+ +
+ +--- + +
+Forking this repository + +There can exist two states of being for this step, you have either already forked this repository or you have not... Apparently (finding out as I'm writing this guide), GitHub does not let you create multiple forks of the same repository on the same account. So really your options in this case are either to pick another account to create the fork on if you want to separate this deployment from your potential artwork storage or you can use your already existing fork. + +--- + +
+Already have a fork of this repository and want to use it for deployment? Expand this section +Feels pretty empty, doesn't it? +
+Just head over to your forked repository, that's all :) +

+If you do still need more detailed instructions: + +
+ 1. Go to our GitHub repository: https://github.com/heavenly-hostas-hosting/HHH + You're probably already here :), but do head over to the main page (for example, by following the link given above) to be able to follow the next steps exactly. +
+ +
+ 2. Click the downwards facing triangle (it looks about like this: \/) next to the Fork button and then click on your forked repository + Red arrow pointing to Fork button +
+ +
+ +--- + +
+Haven't forked this repository yet or want to fork it with a different account? This is your dropdown +
+ 1. Go to our GitHub repository: https://github.com/heavenly-hostas-hosting/HHH + You're probably already here :), but do head over to the main page (for example, by following the link given above) to be able to follow the next steps exactly. +
+ +
+ 2. Click the Fork button at the top, it's right next to the Star button, and while you're at it, click that one as well :P + Red arrow pointing to Fork button +
+ +
+ 3. Pick a name for your fork (and an account if necessary) or leave it as the default and click on Create fork + Red arrow pointing to Fork button +
+
+ +--- + +Finally you're going to want to clone the forked repository locally: + +
+ 1. Go to your fork and click on <> Code + Red arrow pointing to Code button +
+ +
+ 2. Select either HTTPS or SSH tab depending on which method you have set up for authentication for your GitHub account (personally I would suggest using SSH), then copy the URL + Code button dropdown +
+ +
+ 3. In your terminal, clone the repository whose URL you just copied and cd into its directory +

+git clone <paste your url here> ./HHH  # <- this picks the destination directory of the repository, you can pick a different one if you want, but take that into account for the next command
+cd ./HHH
+    
+
+ +
+ +--- + +### Creating a GitHub App +For a more detailed overview take a loot at GitHub's own guide for Apps: https://docs.github.com/en/apps/overview + +
+ 1. Click on your profile picture in the top right corner of the page anywhere on the GitHub site + Red arrow pointing to GitHub profile icon +
+ +
+ 2. Go to Settings + Red arrow pointing to settings +
+ +
+ 3. Go to Developer Settings + Red arrow pointing to developer settings in user settings +
+ +
+ 4. Click on New GitHub App + Red arrow pointing to 'New GitHub App' button +
+ +
+ 5. Set GitHub App name to something like pydis-cj12-HHH-deploy-<your-github-username>, Homepage URL can just be your repository URL, set Callback URL to http://localhost/api/kong/auth/v1/callback and make sure to tick the OAuth box + +
+ +
+ 6. Disable webhooks + +
+ +
+ 7. Set Repository permissions to Contents: Read and write, Pull requests: Read and write, and Account permissions to Email addresses: Read-only +
+
+
+
+
+
+ +
+ 8. Allow app to be installed on any account and click Create GitHub App + +
+ +
+ 9. Generate a new client secret, store it somewhere safe, will be needed later on, then scroll down and Generate a private key, make sure to save it with the name pydis-cj12-heavenly-hostas-app.private-key.pem and store it inside the packages/backend directory +
+
+
+ +
+ 10. Make sure to Save changes + +
+ +
+ +Lastly go to your GitHub App's public URL and install it on your main deployment repository (it might seem like it fails because it can't find `localhost`, but it will install itself on the repository, do make sure to only install it on the deployment repo and not all of your repositories) + + +### Creating a `data` branch +In your terminal, go to your local repository and create a new branch and add the data workflow to it +``` +git switch --orphan data +git checkout main -- .github/workflows/data.yaml +``` +Now, if you're on Linux, you can use the following command to replace the string `cj12.matiiss.com` with the string `${{ vars.DATA_API_HOST }}`, otherwise you can just use any text editor you'd like (preferrably one with Find & Replace functionality) to do the same for the file `.github/workflows/data.yaml` +``` +sed 's/cj12.matiiss.com/\$\{\{ vars.DATA_API_HOST \}\}/g' .github/workflows/data.yaml > tmp.yaml && mv tmp.yaml .github/workflows/data.yaml +``` + +Next, just add, commit, and push the changes to GitHub and then switch back to the main branch +``` +git add .github/workflows/data.yaml +git commit -m "Change hard-coded URL to a workflow variable" +git push -u origin HEAD +git switch main +``` + +Now you need to learn how to set GitHub Actions variables + +
+ 1. Go to your repository's Settings + Red arrow pointing to 'Settings' +
+ +
+ 2. Go to Secrets and variables > Actions + Red arrow pointing to 'Settings' +
+ +
+ 3. Create a New repository variable + Red arrow pointing to 'Settings' +
+ +
+ 4. Name it DATA_API_HOST and add some filler value as its value for now + Red arrow pointing to 'Settings' +
+ + +### Set up and deploy GitHub Pages +For a more detailed overview of what GitHub Pages are see https://pages.github.com/ + +
+ 1. Go to your repository's Settings + Red arrow pointing to 'Settings' +
+ +
+ 2. Go to Pages + Red arrow pointing to 'Pages' settings +
+ +
+ 3. Change Source to deploy from GitHub Actions + GitHub Pages settings +
+ +
+ 4. Go back to your repository's main page and navigate to packages/gallery/main.py +
    +
  1. Arrow to directory
  2. +
  3. Arrow to directory
  4. +
  5. Arrow to file
  6. +
+
+ +
+ 5. Enter file edit mode for packages/gallery/main.py + Arrow to edit file button +
+ +
+ 6. Find where REPO_URL is defined and change heavenly-hostas-hosting/HHH to <your-username>/<your-repo-name> + For example, from: +
+ Text... +
+ to: +
+ Text... +
+ +
+ 7. Find cj12.matiiss.com and replace it with localhost + From: +
+ Text... +
+ to: +
+ Text... +
+ +
+ 8. Commit your changes +
    +
  1. Arrow to Commit changes button
  2. +
  3. Commit dialog close-up
  4. +
+
+ +
+ 9. Go back to your repository's main page and navigate to packages/gallery/assets/editor.html +
    +
  1. Arrow to directory
  2. +
  3. Arrow to directory
  4. +
  5. Arrow to directory
  6. +
  7. Arrow to file
  8. +
+
+ +
+ 10. Enter file edit mode for packages/gallery/assets/editor.html + Arrow to edit file button +
+ +
+ 11. Replace all instances of cj12.matiiss.com with localhost + From: +
+ Text... +
+ to: +
+ Text... +
+ +
+ 12. Commit your changes +
    +
  1. Arrow to Commit changes button
  2. +
  3. Commit dialog close-up
  4. +
+
+ + +### Local setup +You need to set up the initial volumes for supabase and to do that there's a convenience script, so just run that +``` +bash ./set_up_supabase_volumes.sh +``` + +Then afterwards you want to copy the `.env.template` from the backend and link it to project root +``` +cp packages/backend/.env.template packages/backend/.env +ln packages/backend/.env .env +``` + +Open up the `.env` file in a text editor and adjust the environment variables as necessary: +- `CLIENT_ID` is your GitHub App's client ID +- `CLIENT_SECRET` is the secret your generated for your GitHub App +- `GIT_UPSTREAM_*` refers to your deployment repository, the first commit hash you can get from GitHub or git for the `data` branch by looking at the history/logs. The app installation ID you can see in the URL in your browser when you visit your installation. +- Regarding supabase variables, they have a generator for those: https://supabase.com/docs/guides/self-hosting/docker#generate-api-keys +- `GOTRUE_EXTERNAL_GITHUB_REDIRECT_URI="https://localhost/api/kong/auth/v1/callback"` +- `GITHUB_CALLBACK_REDIRECT_URI="https://localhost/api/auth"` +- `POST_AUTH_REDIRECT_URI="https://localhost/editor"` +- `SUPABASE_PUBLIC_URL="https://localhost/api/kong"` + + + +### 3-2-1 Lift-off! +First, let's connect to the internet so that the GitHub workflow can access our API for PR verification, we'll do this using https://localhost.run/ which will provide us with a free, temporary, public domain name. +``` +ssh -R 80:localhost:80 localhost.run +``` +Navigate over to your GitHub repository and set that domain name (just the `.lhr.life` part without the protocol) as the value for the `DATA_API_HOST` repository variable as well. + +> [!IMPORTANT] +> Since this domain is temporary, it will get refreshed every once in a while, so keep the variable updated whenever that happens + +Before we run any `docker` container, we must first create a shared network for some of the `docker compose` services to communicate with `nginx` and vice versa +``` +docker network create shared-net +``` + +It's now time to spin up the entire stack +``` +docker compose up -d +``` + +Finally let's spin up `nginx` for traffic routing, just copy and paste the following in your terminal, then run it +```bash +NETWORK=shared-net TMP_DIR=$(mktemp -d) && \ +openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout "$TMP_DIR/key.pem" -out "$TMP_DIR/cert.pem" -subj "/CN=localhost" && \ +cat > "$TMP_DIR/nginx.conf" <<'EOF' +events {} +http { + server { listen 80; server_name localhost; return 301 https://$host$request_uri; } + server { + listen 443 ssl; server_name localhost; + ssl_certificate /etc/nginx/certs/cert.pem; ssl_certificate_key /etc/nginx/certs/key.pem; + location ~ ^/_next|monaco-editor/\$ { proxy_pass http://kong:8000; } + location /api/platform/ { proxy_pass http://kong:8000; } + location /api/kong/ { proxy_pass http://kong:8000/; } + location /api/ { proxy_pass http://cj12-backend:9000/; } + location /editor/ { proxy_pass http://cj12-editor:9010/; proxy_redirect / /editor/; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "Upgrade"; } + location /scripts/ { proxy_pass http://cj12-editor:9010; } + location /static/ { proxy_pass http://cj12-editor:9010; } + } + server { + listen 80; server_name *.lhr.life; + location = /api/verify_pr { proxy_pass http://cj12-backend:9000/verify_pr$is_args$args; } + location / { return 403; } + } +} +EOF +docker run --rm --name nginx -p 80:80 -p 443:443 --network "$NETWORK" -v "$TMP_DIR/nginx.conf":/etc/nginx/nginx.conf:ro -v "$TMP_DIR":/etc/nginx/certs:ro nginx +``` diff --git a/heavenly-hostas/packages/backend/pyproject.toml b/heavenly-hostas/packages/backend/pyproject.toml new file mode 100644 index 00000000..2b42d79e --- /dev/null +++ b/heavenly-hostas/packages/backend/pyproject.toml @@ -0,0 +1,16 @@ +[project] +name = "backend" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "cryptography>=45.0.6", + "fastapi>=0.116.1", + "httpx>=0.28.1", + "psycopg[binary,pool]>=3.2.9", + "pyjwt>=2.10.1", + "python-multipart>=0.0.20", + "supabase>=2.18.0", + "uvicorn>=0.35.0", +] diff --git a/heavenly-hostas/packages/backend/server/__init__.py b/heavenly-hostas/packages/backend/server/__init__.py new file mode 100644 index 00000000..ca95ba2c --- /dev/null +++ b/heavenly-hostas/packages/backend/server/__init__.py @@ -0,0 +1,254 @@ +import secrets +from datetime import datetime +from typing import Annotated + +from fastapi import FastAPI, HTTPException, Query, Request, Response, UploadFile +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import JSONResponse, RedirectResponse +from gotrue import CodeExchangeParams, SignInWithOAuthCredentials, SignInWithOAuthCredentialsOptions +from pydantic import BaseModel + +from . import env, gh, pg, sb + +app = FastAPI() +app.add_middleware( + CORSMiddleware, + allow_origins=["https://heavenly-hostas-hosting.github.io"], + allow_credentials=False, + allow_methods=["*"], + allow_headers=["*"], +) + + +@app.get("/login") +async def login() -> Response: + sb_client = await sb.create_public_client() + + gh_response = await sb_client.auth.sign_in_with_oauth( + SignInWithOAuthCredentials( + provider="github", + options=SignInWithOAuthCredentialsOptions(redirect_to=env.GITHUB_CALLBACK_REDIRECT_URI), + ), + ) + + response = RedirectResponse(gh_response.url) + response.set_cookie( + key=sb.CODE_VERIFIER_COOKIE_KEY, + value=await sb.get_code_verifier_from_client(sb_client), + httponly=True, + secure=True, + samesite="lax", + ) + + return response + + +@app.get("/logout") +async def logout(request: Request) -> Response: + sb_client = await sb.get_session(request) + await sb_client.auth.sign_out() + + response = RedirectResponse(env.POST_AUTH_REDIRECT_URI) + response.delete_cookie( + key=sb.ACCESS_TOKEN_COOKIE_KEY, + httponly=True, + secure=True, + samesite="lax", + ) + response.delete_cookie( + key=sb.REFRESH_TOKEN_COOKIE_KEY, + httponly=True, + secure=True, + samesite="lax", + ) + + return response + + +@app.get("/auth") +async def auth( + code: Annotated[str, Query()], + request: Request, +) -> RedirectResponse: + client = await sb.create_internal_client() + code_verifier = request.cookies.get(sb.CODE_VERIFIER_COOKIE_KEY) + if code_verifier is None: + raise HTTPException(status_code=401, detail="Code verifier not found in cookies") + + gh_response = await client.auth.exchange_code_for_session( + CodeExchangeParams( + code_verifier=code_verifier, + auth_code=code, + redirect_to="", + ), + ) + + if gh_response.session is None: + raise HTTPException(status_code=401, detail="Failed to exchange code for session") + + response = RedirectResponse(env.POST_AUTH_REDIRECT_URI) + response.delete_cookie( + key=sb.CODE_VERIFIER_COOKIE_KEY, + httponly=True, + secure=True, + samesite="lax", + ) + sb.set_response_token_cookies_( + response, + access_token=gh_response.session.access_token, + refresh_token=gh_response.session.refresh_token, + ) + + return response + + +@app.post("/publish") +async def publish( # noqa: C901 + image: UploadFile, + http_request: Request, +) -> Response: + client = await sb.get_session(http_request) + + client_session = await client.auth.get_session() + if client_session is None: + raise HTTPException(status_code=401, detail="User not authenticated") + + gh_identity = await sb.get_github_identity(client) + user_name = gh_identity.identity_data["user_name"] + app_token = gh.get_app_token() + + installation_id: int | None = None + for installation in await gh.get_app_installations(app_token): + if installation["account"]["login"] == user_name: + if installation_id is not None: + raise HTTPException(status_code=400, detail="Multiple GitHub App installations found for user") + + installation_id = installation["id"] + + if installation_id is None: + raise HTTPException(status_code=404, detail="No GitHub App installation found") + + app_installation_token = await gh.get_app_installation_token(installation_id, app_token) + installation_repositories = await gh.get_app_installation_repositories(app_installation_token) + + total_repo_count = installation_repositories["total_count"] + if total_repo_count == 0: + raise HTTPException(status_code=409, detail="GitHub App not installed on any repository") + elif total_repo_count > 1: + raise HTTPException(status_code=409, detail="GitHub App must be installed on a single repository") + + root_app_installation_token = await gh.get_app_installation_token(env.GIT_UPSTREAM_APP_INSTALLATION_ID, app_token) + all_fork_full_names = set( + repo["full_name"] for repo in await gh.get_app_installation_repository_forks(root_app_installation_token) + ) + repository = installation_repositories["repositories"][0] + + if not repository["fork"] or repository["full_name"] not in all_fork_full_names: + raise HTTPException( + status_code=409, detail="The installation repository must be a fork of the main repository" + ) + + now = datetime.now().strftime("%Y-%m-%dT%H-%M-%S") + random_sequence = secrets.token_hex(8) + file_stem = f"{now}_{random_sequence}" + file_name = f"{file_stem}.webp" + + commit_hash = await gh.commit_and_create_pull_request( + root_app_installation_token=root_app_installation_token, + app_installation_token=app_installation_token, + fork_owner=user_name, + fork_name=repository["name"], + new_branch=file_stem, + file_path=file_name, + file_content=await image.read(), + pr_title=f"Publish {file_name}", + ) + await pg.github_files_insert_row( + username=user_name, + filename=file_name, + commit_hash=commit_hash, + ) + + response = Response(content="Publish endpoint hit", status_code=200) + sb.set_response_token_cookies_( + response, + access_token=client_session.access_token, + refresh_token=client_session.refresh_token, + ) + + return response + + +class LoginStatusResponse(BaseModel): + username: str | None + logged_in: bool + + +@app.get("/status", response_model=LoginStatusResponse) +async def status(http_request: Request) -> JSONResponse: + try: + client = await sb.get_session(http_request) + client_session = await client.auth.get_session() + if client_session is None: + raise HTTPException(status_code=401, detail="User not authenticated") + + gh_identity = await sb.get_github_identity(client) + except HTTPException as e: + if e.status_code != 401: + raise + + return JSONResponse( + content=LoginStatusResponse( + username=None, + logged_in=False, + ).model_dump() + ) + + user_name = gh_identity.identity_data["user_name"] + response = JSONResponse( + content=LoginStatusResponse( + username=user_name, + logged_in=True, + ).model_dump() + ) + sb.set_response_token_cookies_( + response, + access_token=client_session.access_token, + refresh_token=client_session.refresh_token, + ) + + return response + + +class VerifyPRResponse(BaseModel): + is_valid: bool + + +@app.get("/verify_pr") +async def verify_pr( + filename: Annotated[str, Query()], + commit_hash: Annotated[str, Query()], +) -> VerifyPRResponse: + is_valid = await pg.github_files_check_exists( + filename=filename, + commit_hash=commit_hash, + ) + + return VerifyPRResponse(is_valid=is_valid) + + +class ArtworksResponse(BaseModel): + artworks: list[tuple[str, str]] + + +@app.get("/artworks") +async def artworks() -> ArtworksResponse: + works = await pg.github_files_get_all() + + return ArtworksResponse(artworks=works) + + +if __name__ == "__main__": + import uvicorn + + uvicorn.run("main:app", host="localhost", port=8000, workers=1) diff --git a/heavenly-hostas/packages/backend/server/env.py b/heavenly-hostas/packages/backend/server/env.py new file mode 100644 index 00000000..cca106e5 --- /dev/null +++ b/heavenly-hostas/packages/backend/server/env.py @@ -0,0 +1,22 @@ +from pathlib import Path + +from . import utils + +CLIENT_ID = utils.assure_get_env("CLIENT_ID") +CLIENT_SECRET = utils.assure_get_env("CLIENT_SECRET") +PRIVATE_KEY = Path("pydis-cj12-heavenly-hostas-app.private-key.pem").read_text().strip() + +SUPABASE_PUBLIC_URL = utils.assure_get_env("SUPABASE_PUBLIC_URL") +SUPABASE_INTERNAL_URL = utils.assure_get_env("SUPABASE_INTERNAL_URL") +SUPABASE_KEY = utils.assure_get_env("ANON_KEY") + +GITHUB_CALLBACK_REDIRECT_URI = utils.assure_get_env("GITHUB_CALLBACK_REDIRECT_URI") +POST_AUTH_REDIRECT_URI = utils.assure_get_env("POST_AUTH_REDIRECT_URI") + +JWT_SECRET = utils.assure_get_env("JWT_SECRET") + +GIT_UPSTREAM_OWNER = utils.assure_get_env("GIT_UPSTREAM_OWNER") +GIT_UPSTREAM_REPO = utils.assure_get_env("GIT_UPSTREAM_REPO") +GIT_UPSTREAM_DATA_BRANCH = utils.assure_get_env("GIT_UPSTREAM_DATA_BRANCH") +GIT_UPSTREAM_DATA_BRANCH_FIRST_COMMIT_HASH = utils.assure_get_env("GIT_UPSTREAM_DATA_BRANCH_FIRST_COMMIT_HASH") +GIT_UPSTREAM_APP_INSTALLATION_ID = int(utils.assure_get_env("GIT_UPSTREAM_APP_INSTALLATION_ID")) diff --git a/heavenly-hostas/packages/backend/server/gh.py b/heavenly-hostas/packages/backend/server/gh.py new file mode 100644 index 00000000..00c803ed --- /dev/null +++ b/heavenly-hostas/packages/backend/server/gh.py @@ -0,0 +1,140 @@ +import base64 # noqa: F401 +import time +from typing import Any + +import httpx +import jwt + +from . import env + + +def get_app_token() -> str: + """Generate a JWT token for the GitHub App.""" + now = int(time.time()) + payload = { + "iat": now, + "exp": now + (3 * 60), # JWT valid for 3 minutes + "iss": env.CLIENT_ID, # GitHub App ID + } + + return jwt.encode(payload, env.PRIVATE_KEY, algorithm="RS256") + + +async def get_app_installations(app_token: str) -> list[dict[str, Any]]: + """Get all installations of the GitHub App.""" + headers = { + "Authorization": f"Bearer {app_token}", + "Accept": "application/vnd.github+json", + } + async with httpx.AsyncClient() as client: + r = await client.get("https://api.github.com/app/installations", headers=headers) + r.raise_for_status() + return r.json() + + +async def get_app_installation_repositories(app_installation_token: str) -> dict[str, Any]: + """Get all repositories a GitHub App installation has access to.""" + headers = { + "Authorization": f"Bearer {app_installation_token}", + "Accept": "application/vnd.github+json", + } + async with httpx.AsyncClient() as client: + r = await client.get("https://api.github.com/installation/repositories", headers=headers) + r.raise_for_status() + return r.json() + + +async def get_app_installation_token(installation_id: int, app_token: str) -> str: + """Get an installation token for the GitHub App. + + This token is used to perform actions on behalf of the installation. + """ + headers = { + "Authorization": f"Bearer {app_token}", + "Accept": "application/vnd.github+json", + } + async with httpx.AsyncClient() as client: + r = await client.post( + f"https://api.github.com/app/installations/{installation_id}/access_tokens", + headers=headers, + ) + r.raise_for_status() + return r.json()["token"] + + +async def get_app_installation_repository_forks(app_installation_token: str) -> list[dict[str, Any]]: + headers = { + "Authorization": f"Bearer {app_installation_token}", + "Accept": "application/vnd.github+json", + } + async with httpx.AsyncClient() as client: + r = await client.get( + f"https://api.github.com/repos/{env.GIT_UPSTREAM_OWNER}/{env.GIT_UPSTREAM_REPO}/forks", + headers=headers, + ) + r.raise_for_status() + return r.json() + + +async def commit_and_create_pull_request( + root_app_installation_token: str, + app_installation_token: str, + fork_owner: str, + fork_name: str, + new_branch: str, + file_path: str, + file_content: bytes, + pr_title: str, +) -> str: + root_auth_headers = {"Authorization": f"token {root_app_installation_token}"} + auth_headers = {"Authorization": f"token {app_installation_token}"} + meta_headers = {"Accept": "application/vnd.github+json"} + + root_headers = root_auth_headers | meta_headers + headers = auth_headers | meta_headers + + async with httpx.AsyncClient() as client: + # Get SHA of the data branch to create a new branch off of in the fork + # r = await client.get( + # f"https://api.github.com/repos/{env.GIT_UPSTREAM_OWNER}/{env.GIT_UPSTREAM_REPO}/git/refs/heads/{env.GIT_UPSTREAM_DATA_BRANCH}", + # headers=headers, + # ) + # r.raise_for_status() + # base_sha = r.json()["object"]["sha"] + + # Create a new branch in the fork + r = await client.post( + f"https://api.github.com/repos/{fork_owner}/{fork_name}/git/refs", + headers=headers, + json={"ref": f"refs/heads/{new_branch}", "sha": env.GIT_UPSTREAM_DATA_BRANCH_FIRST_COMMIT_HASH}, + ) + r.raise_for_status() + + # Commit file contents to the new branch + r = await client.put( + f"https://api.github.com/repos/{fork_owner}/{fork_name}/contents/{file_path}", + headers=headers, + json={ + "message": f"Add {file_path}", + "content": base64.b64encode(file_content).decode("utf-8"), + "branch": new_branch, + }, + ) + r.raise_for_status() + commit_hash: str = r.json()["commit"]["sha"] + + # Open PR against upstream + r = await client.post( + f"https://api.github.com/repos/{env.GIT_UPSTREAM_OWNER}/{env.GIT_UPSTREAM_REPO}/pulls", + headers=root_headers, + json={ + "title": pr_title, + "head": f"{fork_owner}:{new_branch}", + "head_repo": fork_name, + "base": env.GIT_UPSTREAM_DATA_BRANCH, + "maintainer_can_modify": False, + }, + ) + r.raise_for_status() + + return commit_hash diff --git a/heavenly-hostas/packages/backend/server/pg.py b/heavenly-hostas/packages/backend/server/pg.py new file mode 100644 index 00000000..05a641f6 --- /dev/null +++ b/heavenly-hostas/packages/backend/server/pg.py @@ -0,0 +1,84 @@ +import os + +import psycopg +from psycopg.rows import tuple_row + + +async def get_connection() -> psycopg.AsyncConnection: + return await psycopg.AsyncConnection.connect( + dbname=os.getenv("POSTGRES_DB"), + user=os.getenv("POSTGRES_USER", "postgres"), + password=os.getenv("POSTGRES_PASSWORD"), + host=os.getenv("POSTGRES_HOST", "localhost"), + port=os.getenv("POSTGRES_PORT", "5432"), + row_factory=tuple_row, + ) + + +async def github_files_create_table() -> None: + async with await get_connection() as conn: + async with conn.cursor() as cur: + await cur.execute( + """ + CREATE TABLE IF NOT EXISTS github_files ( + id SERIAL PRIMARY KEY, + github_username VARCHAR(39) NOT NULL, + filename CHAR(42) NOT NULL, + commit_hash CHAR(40) NOT NULL + ); + """ + ) + await conn.commit() + + +async def github_files_insert_row(username: str, filename: str, commit_hash: str) -> None: + await github_files_create_table() + + async with await get_connection() as conn: + async with conn.cursor() as cur: + await cur.execute( + """ + INSERT INTO github_files (github_username, filename, commit_hash) + VALUES (%s, %s, %s); + """, + (username, filename, commit_hash), + ) + await conn.commit() + + +async def github_files_check_exists(filename: str, commit_hash: str) -> bool: + async with await get_connection() as conn: + async with conn.cursor() as cur: + await cur.execute( + """ + SELECT + * + FROM + github_files + WHERE + filename=%s + AND commit_hash=%s + """, + (filename, commit_hash), + ) + + rows = await cur.fetchall() + return len(rows) == 1 + + +async def github_files_get_all() -> list[tuple[str, str]]: + async with await get_connection() as conn: + async with conn.cursor() as cur: + await cur.execute( + """ + SELECT + github_username, + filename + FROM + github_files + ORDER BY + id ASC + """ + ) + rows = await cur.fetchall() + return rows diff --git a/heavenly-hostas/packages/backend/server/sb.py b/heavenly-hostas/packages/backend/server/sb.py new file mode 100644 index 00000000..2ed8eb79 --- /dev/null +++ b/heavenly-hostas/packages/backend/server/sb.py @@ -0,0 +1,86 @@ +from fastapi import HTTPException, Request, Response +from gotrue.constants import STORAGE_KEY +from gotrue.errors import AuthSessionMissingError +from gotrue.types import UserIdentity +from supabase import AsyncClient, AsyncClientOptions, create_async_client + +from . import env + +ACCESS_TOKEN_COOKIE_KEY = "sb_access_token" # noqa: S105 +REFRESH_TOKEN_COOKIE_KEY = "sb_refresh_token" # noqa: S105 +CODE_VERIFIER_COOKIE_KEY = "sb_code_verifier" + + +def set_response_token_cookies_(response: Response, access_token: str, refresh_token: str) -> None: + response.set_cookie( + key=ACCESS_TOKEN_COOKIE_KEY, + value=access_token, + httponly=True, + secure=True, + samesite="lax", + ) + response.set_cookie( + key=REFRESH_TOKEN_COOKIE_KEY, + value=refresh_token, + httponly=True, + secure=True, + samesite="lax", + ) + + +async def create_internal_client() -> AsyncClient: + """Create a Supabase client.""" + return await create_async_client( + supabase_url=env.SUPABASE_INTERNAL_URL, + supabase_key=env.SUPABASE_KEY, + options=AsyncClientOptions(flow_type="pkce"), + ) + + +async def create_public_client() -> AsyncClient: + """Create a Supabase client.""" + return await create_async_client( + supabase_url=env.SUPABASE_PUBLIC_URL, + supabase_key=env.SUPABASE_KEY, + options=AsyncClientOptions(flow_type="pkce"), + ) + + +async def get_code_verifier_from_client(client: AsyncClient) -> str: + """Get the code verifier from the client.""" + storage = client.auth._storage # noqa: SLF001 + code_verifier = await storage.get_item(f"{STORAGE_KEY}-code-verifier") + + if code_verifier is None: + raise HTTPException(status_code=401, detail="Code verifier not found in storage") + + return code_verifier + + +async def get_session(request: Request) -> AsyncClient: + """Get a Supabase client session.""" + access_token = request.cookies.get(ACCESS_TOKEN_COOKIE_KEY) + refresh_token = request.cookies.get(REFRESH_TOKEN_COOKIE_KEY) + + if access_token is None or refresh_token is None: + raise HTTPException(status_code=401, detail="No session tokens found") + + client = await create_internal_client() + await client.auth.set_session(access_token=access_token, refresh_token=refresh_token) + + return client + + +async def get_github_identity(client: AsyncClient) -> UserIdentity: + user_identities = await client.auth.get_user_identities() + if isinstance(user_identities, AuthSessionMissingError): + raise HTTPException(status_code=401, detail="User not authenticated") + + for identity in user_identities.identities: + if identity.provider == "github": + gh_identity = identity + break + else: + raise HTTPException(status_code=401, detail="GitHub identity not found... how did you get here?") + + return gh_identity diff --git a/heavenly-hostas/packages/backend/server/utils.py b/heavenly-hostas/packages/backend/server/utils.py new file mode 100644 index 00000000..f7d8379e --- /dev/null +++ b/heavenly-hostas/packages/backend/server/utils.py @@ -0,0 +1,10 @@ +import os + + +def assure_get_env(var: str) -> str: + """Get an environment variable or raise an error if it is not set.""" + value = os.getenv(var) + if value is None: + msg = f"Environment variable '{var}' is not set." + raise OSError(msg) + return value diff --git a/heavenly-hostas/packages/editor/README.md b/heavenly-hostas/packages/editor/README.md new file mode 100644 index 00000000..2360fb3e --- /dev/null +++ b/heavenly-hostas/packages/editor/README.md @@ -0,0 +1,52 @@ +## Dev Guide +### Intended Use +To run this package as intended, please see the [backend package README](https://github.com/heavenly-hostas-hosting/HHH/blob/main/packages/backend/README.md). +### Standalone Execution +If you wish to run this package as a standalone webapp, first install [`NiceGUI`](https://pypi.org/project/nicegui/). +Then, in `main.py` remove the `root_path` argument in `ui.run`, as shown below. + +From: +```py +if __name__ in {"__main__", "__mp_main__"}: + ui.run(port=9010, title="HHH Editor", root_path="/editor") +``` +To: +```py +if __name__ in {"__main__", "__mp_main__"}: + ui.run(port=9010, title="HHH Editor") +``` +Then run the file such as below. +``` +python3 packages/editor/main.py +``` + +## Concept + +This part of the project is an image editor, meant to be similar to apps such as Microsoft Paint. + +You can: +- Draw lines or pixels, depending on the mode you select. Changing modes will clear the canvas. Pixel mode is limited in features. [`p`] for pen mode. +- Erase lines/pixels. [`e`] for eraser mode. +- Smudge. [`s`] for smudge mode. +- Clip regions. [`c`] for clip mode. +- Draw circles, squares, triangles, stars, and the Python logo. +- Use a different colour via a slot machine-esque spinner. [`a`] to spin. +- Change line width. +- Add text. You can choose for the text to be bolded and/or italicised. You can also choose the font. +- Upload images. +- Undo/redo. [`ctrl` + `z`] and [`ctrl` + `shift` + `z`] respectively. +- Download your creations. +- Clear the canvas. +- Clipped regions, uploaded images, and text can all be resized or rotated. Using a scrollwheel or a similar device changes the size, and using [`Alt` + `Left Arrow`] or [`Alt` + `Right Arrow`] rotates the region +counterclockwise and clockwise respectively. Pressing [`backspace`] will remove the region. +- Open the help menu [`?`]. + +Keybinds are in the square brackets. Additional details are in a help menu. + +To publish your art to the gallery, click the `Register` button and follow the instructions. After logging in clicking the `Publish` button should add your piece +to the gallery. + +## Development + +We used [`Pyscript`](https://pyscript.com/) and [`NiceGUI`](https://nicegui.io/) for this part of the project. NiceGUI was used to create the UI, and Pyscript +was used to control the drawing features, via the [Canvas API](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API). diff --git a/heavenly-hostas/packages/editor/main.py b/heavenly-hostas/packages/editor/main.py new file mode 100644 index 00000000..a1a6fa6d --- /dev/null +++ b/heavenly-hostas/packages/editor/main.py @@ -0,0 +1,623 @@ +import asyncio +import base64 +import pathlib +import random + +from nicegui import app, ui +from nicegui.client import Client +from nicegui.events import UploadEventArguments, ValueChangeEventArguments + +SPIN_COUNT = 10 + +HEX = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"] + + +action_options = { + "pen": "🖊️", + "eraser": "🧽", + "smudge": "💨", + "clip": "📎", + "circle": "🟢", + "rectangle": "🟪", + "triangle": "🔺", + "star": "⭐", + "python": "🐍", +} +# I really don't want to do this but I don't know how else to achieve it +global_vars = { + "type_programatically_changed": False, +} + +app.add_static_files("/scripts", pathlib.Path(__file__).parent / "scripts") +app.add_static_files("/static", pathlib.Path(__file__).parent / "static") + + +@ui.page("/", response_timeout=10) +async def index(client: Client) -> None: # noqa: C901, PLR0915 All of the below lines need to be in this function for private viewing of the page + """Index page for the editor.""" + + def do_reset(*, mode_value: bool) -> None: + """Reset the canvas.""" + if mode_value: + ui.run_javascript(f""" + const event = new Event('change'); + const typeSelect = document.querySelector("#type-select"); + typeSelect.setAttribute("value", "{mode_value}"); + typeSelect.dispatchEvent(event); + """) + reset() + + def reset_confirmation(*, mode_value: bool = False) -> None: + """Prompt user to reset canvas.""" + with ui.dialog() as dialog, ui.card(): + ui.label("Are you sure you want to clear the canvas?") + with ui.row().style("display: flex; justify-content: space-between; width: 100%;"): + ui.button("Cancel", on_click=lambda: dialog.close()) + ui.button("Clear", on_click=lambda: (do_reset(mode_value=mode_value), dialog.close())).props( + "color='red'", + ) + dialog.open() + + def revert_type() -> None: + """Revert the type change when cancel is clicked.""" + global_vars["type_programatically_changed"] = True + type_toggle.set_visibility(False) + type_toggle.value = "smooth" if type_toggle.value == "pixel" else "pixel" + type_toggle.update() + type_toggle.set_visibility(True) + global_vars["type_programatically_changed"] = False + + def handle_type_change(dialog: ui.dialog, *, mode_value: bool) -> None: + """Handle type change.""" + dialog.close() + do_reset(mode_value=mode_value) + action_toggle.set_value("pen") + if type_toggle.value == "smooth": + width_input.enable() + width_slider.enable() + file_uploader.enable() + text_input.enable() + add_text_button.enable() + bold_checkbox.enable() + italics_checkbox.enable() + font_family.enable() + elif type_toggle.value == "pixel": + width_input.disable() + width_slider.disable() + file_uploader.disable() + text_input.disable() + add_text_button.disable() + bold_checkbox.disable() + italics_checkbox.disable() + font_family.disable() + + def change_type(*, mode_value: bool = False) -> None: + """Prompt user to reset canvas.""" + if global_vars["type_programatically_changed"]: + return + with ui.dialog() as dialog, ui.card(): + ui.label( + """ + Are you sure you want to change the drawing mode? This will clear the canvas. + You will not be able to undo this. + """, + ).style("text-align: center;") + with ui.row().style("display: flex; justify-content: space-between; width: 100%;"): + ui.button( + "Cancel", + on_click=lambda: ( + dialog.close(), + revert_type(), + ), + ) + ui.button( + "Change", + on_click=lambda: handle_type_change(dialog, mode_value=mode_value), + ).props( + "color='red'", + ) + dialog.open() + + def reset() -> None: + """Reset canvas.""" + ui.run_javascript(""" + const event = new Event('reset'); + document.body.dispatchEvent(event); + """) + + async def spin() -> None: + """Change RGB values.""" + hex_value = "" + for x in range(SPIN_COUNT): + hex_value = "" + for y in range(3): + text = random.choice(HEX) + random.choice(HEX) # noqa: S311 This isn't for cryptography + colour_values[y].text = text + hex_value += text + await asyncio.sleep(0.1) + ui.run_javascript(f""" + window.pen = window.pen || {{}}; + window.pen.colour = "#{hex_value}"; + const event = new Event('colourChange'); + document.body.dispatchEvent(event); + """) + + def upload_image(e: UploadEventArguments) -> None: + """Fire upload event.""" + ui.notify(f"Uploaded {e.name}") + content = base64.b64encode(e.content.read()).decode("utf-8") + ui.run_javascript(f""" + let event = new Event("change"); + const fileUpload = document.querySelector("#file-upload"); + fileUpload.src = "data:{e.type};base64,{content}"; + fileUpload.dispatchEvent(event); + """) + # e.sender is the file upload element which has a .reset() method + e.sender.reset() # type: ignore # noqa: PGH003 + + def switch_action(e: ValueChangeEventArguments) -> None: + """Fire switch action event.""" + if type_toggle.value == "pixel" and e.value not in ("pen", "eraser"): + action_toggle.value = "pen" + ui.notify("You can only select the pen or erase action while in pixel mode.", type="negative") + return + ui.run_javascript(f""" + const event = new Event('change'); + const actionSelect = document.querySelector("#action-select"); + actionSelect.setAttribute("value", "{e.value}"); + actionSelect.dispatchEvent(event); + """) + + def show_help_menu() -> None: + """Show help modal.""" + with ui.dialog() as dialog, ui.card(): + with ui.card_section(): + ui.markdown( + """ + There are keybinds for the editor actions. + """, + ) + with ui.list().props("dense separator"): + ui.item("p: Select pen (🖊️) mode.") + ui.item("e: Select eraser (🧽) mode.") + ui.item("s: Select smudge (💨) mode.") + ui.item("c: Select clip (📎) mode.") + ui.item("a: Spin a new colour.") + ui.item("ctrl+z: Undo.") + ui.item("ctrl+shift+z: Redo.") + ui.item("?: Show this help menu.") + ui.markdown( + """ + To add images to the canvas, upload one via the file upload, and then click where you want to add + it on the canvas. + """, + ) + ui.markdown( + """ + To add text to the canvas, type in the text input and click the `Add to canvas` button. You can + set the text to be bolded or italicised. You can also select the font from the dropdown. + """, + ) + ui.markdown( + """ + Clipped regions, images, and text can all be resized and rotated. They can be resized using the + scroll wheel or similar. They can be rotated by holding `Alt` and then pressing the + left or right arrow key. + """, + ) + ui.markdown( + """ + You can switch between the smooth (✍️) or pixel (👾) modes using the toggle below. + """, + ) + ui.button( + "Close", + on_click=dialog.close, + ) + dialog.open() + + def show_registration_menu() -> None: + with ui.dialog() as dialog, ui.card(): + with ui.card_section(): + with ui.column(): + ui.html( + """ +

To register you must have a GitHub account.

+

+ If you don't have a GitHub account yet, you can create one here. +

+

+ Already registered? Log In instead. +

+
+

Follow these steps to complete registration:

+ +
    +
  1. + Go to + + https://github.com/heavenly-hostas-hosting/HHH + and create a fork of the repository. +
  2. +
  3. + Head over to + + the app installation link + to + authorize and install our GitHub app, make sure to only select the repository you + forked. +
  4. +
  5. You can now Sign in with GitHub.
  6. +
+ """ + ).classes("registration-menu") + ui.space() + ui.separator().classes("w-full") + ui.space() + ui.label("Step By Step Reference Images") + with ui.expansion("Forking The Repository").classes("w-full"): + ui.image("/static/forking-1.png") + ui.image("/static/forking-2.png") + with ui.expansion("Installing The Application").classes("w-full"): + ui.image("/static/installing-app.png") + + ui.button( + "Close", + on_click=dialog.close, + ) + dialog.open() + + async def publish() -> None: + """Fetch the API and publish the canvas.""" + ui.notify("Publishing...") + try: + response_ok = await ui.run_javascript( + """ + const format = "image/webp"; + const quality = 0.7; // 70% + + const canvas = document.querySelector("#image-canvas"); + const blob = await new Promise((r) => canvas.toBlob(r, format, quality)); + + if (blob === null) { + return false; + } + + // Use FormData so FastAPI can read it as UploadFile + const form = new FormData(); + form.append("image", blob, "canvas.webp"); + + response = await fetch( + "/api/publish", + { + method: "POST", + body: form, + }, + ).catch((e) => console.error(e)); + + return response.ok; + """, + timeout=300, + ) + + if not response_ok: + ui.notify("Failed to publish!", type="negative") + return + + ui.notify("Artwork published successfully!", type="positive") + + except Exception as e: # noqa: BLE001 + ui.notify(f"An error occurred: {e}", type="negative") + + def show_publish_confirmation() -> None: + with ui.dialog() as dialog, ui.card(): + with ui.card_section(), ui.column(): + ui.label("Are you sure you want to publish your creation?").style("text-align: center;") + ui.label("You can only upload 5 images an hour.").style("text-align: center; margin: auto;") + ui.space() + with ui.row().style("display: flex; justify-content: space-between; width: 100%;"): + ui.button("Cancel", on_click=dialog.close) + + async def confirm_publish(): + dialog.close() + await publish() + + ui.button("Publish", on_click=confirm_publish) + + dialog.open() + + async def login() -> None: + """Fetch the API and login.""" + ui.notify("Logging in...") + try: + await ui.run_javascript( + """ + const redirectUrl = "/api/login"; + window.location.href = redirectUrl; + + sessionStorage.setItem("cj12-hhh-logged-in", "true"); + """, + timeout=60, + ) + + ui.notify("Logged in successfully!", type="positive") + + register_button.move(hidden_buttons) + login_button.move(hidden_buttons) + + publish_button.move(shown_buttons) + logout_button.move(shown_buttons) + + except Exception as e: + ui.notify(f"An error occurred: {e}", type="negative") + + async def logout() -> None: + """Fetch the API and logout.""" + ui.notify("Logging out...") + try: + await ui.run_javascript( + """ + const redirectUrl = "/api/logout"; + window.location.href = redirectUrl; + + sessionStorage.setItem("cj12-hhh-logged-in", "false"); + """, + timeout=60, + ) + + ui.notify("Logged out successfully!", type="positive") + + register_button.move(shown_buttons) + login_button.move(shown_buttons) + + publish_button.move(hidden_buttons) + logout_button.move(hidden_buttons) + + except Exception as e: + ui.notify(f"An error occurred: {e}", type="negative") + + async def check_login_status() -> None: + try: + response = await ui.run_javascript( + """ + response = await fetch( + "/api/status", + { method: "GET" }, + ).catch((e) => console.error(e)); + + response_json = response.json(); + + sessionStorage.setItem("cj12-hhh-logged-in", response_json['logged_in']); + + return response_json; + """, + timeout=60, + ) + + if response["logged_in"]: + username.set_text(response["username"]) + register_button.move(hidden_buttons) + login_button.move(hidden_buttons) + + publish_button.move(shown_buttons) + logout_button.move(shown_buttons) + else: + username.set_text("") + register_button.move(shown_buttons) + login_button.move(shown_buttons) + + publish_button.move(hidden_buttons) + logout_button.move(hidden_buttons) + + except Exception as e: + ui.notify(f"An error occurred: {e}", type="negative") + + ui.add_head_html(""" + + + + + """) + + ui.add_body_html(""" + +

Loading...

+
+ """) + + ui.element("img").props("id='file-upload'").style("display: none;") + + with ui.row().style("display: flex; width: 100%;"): + # Page controls + with ui.column().style("flex-grow: 1; flex-basis: 0;"): + username = ui.label("") + + ui.separator().classes("w-full") + + with ui.row(): + dark = ui.dark_mode() + ui.switch("Dark mode").bind_value(dark) + + ui.button(icon="help", on_click=lambda: show_help_menu()).props( + "class='keyboard-shortcuts' shortcut_data='btn,?'", + ) + + ui.button("Clear Canvas", on_click=reset_confirmation).props("color='red'") + + ui.button("Download").props("id='download-button'") + + file_uploader = ( + ui.upload( + label="Upload file", + auto_upload=True, + on_upload=upload_image, + on_rejected=lambda _: ui.notify("There was an issue with the upload."), + ) + .props("accept='image/*' id='file-input'") + .style("width: 100%;") + ) + + type_toggle = ui.toggle( + {"smooth": "✍️", "pixel": "👾"}, + value="smooth", + on_change=lambda e: change_type(mode_value=e.value), + ).props("id='type-select'") + + with ui.row().props("id='shown-buttons'") as shown_buttons: + register_button = ui.button("Register", on_click=show_registration_menu) + login_button = ui.button("Login", on_click=login) + + with ui.row().props("id='hidden-buttons'").style("display: none") as hidden_buttons: + publish_button = ui.button("Publish", on_click=show_publish_confirmation) + logout_button = ui.button("Logout", on_click=logout) + + ui.link("Visit the gallery", "https://heavenly-hostas-hosting.github.io/HHH/") + + with ui.element("div").style("position: relative;"): + ui.element("canvas").props("id='image-canvas'").style( + "border: 1px solid black; background-color: white;", + ) + ui.element("canvas").props("id='buffer-canvas'").style( + "pointer-events: none; position: absolute; top: 0; left: 0;", + ) + + # Canvas controls + with ui.column().style("flex-grow: 1; flex-basis: 0;"): + with ui.row(): + ui.button("Undo").props("id='undo-button' class='keyboard-shortcuts'") + ui.button("Redo").props("id='redo-button' class='keyboard-shortcuts'") + + action_toggle = ( + ui.toggle( + action_options, + value="pen", + on_change=switch_action, + ) + .props( + "id='action-select' class='keyboard-shortcuts' shortcut_data='toggle,p:🖊️,e:🧽,s:💨,c:📎'", + ) + .style("flex-wrap: wrap;") + ) + + ui.separator().classes("w-full") + + with ui.row(): + colour_values = [] + for colour in ["R", "G", "B"]: + with ui.column().style("align-items: center;"): + ui.label(colour) + colour_label = ui.label("00") + colour_values.append(colour_label) + + ui.button("Spin", on_click=spin).props("class='keyboard-shortcuts' shortcut_data='btn,a'") + + ui.separator().classes("w-full") + + width_input = ui.number(label="Line Width", min=1, max=100, step=1) + width_slider = ui.slider( + min=1, + max=100, + value=5, + on_change=lambda _: ui.run_javascript(""" + const event = new Event('change'); + document.querySelector(".width-input").dispatchEvent(event); + """), + ).classes("width-input") + width_input.bind_value(width_slider) + + ui.separator().classes("w-full") + + text_input = ui.input( + label="Text", + placeholder="Start typing", + ).props("id='text-input'") + + with ui.row(): + bold_checkbox = ui.checkbox("Bold").props("id='bold-text'") + italics_checkbox = ui.checkbox("Italics").props("id='italics-text'") + + with ui.row(): + font_family = ui.select( + [ + "Arial", + "Verdana", + "Tahoma", + "Trebuchet MS", + "Times New Roman", + "Georgia", + "Garamond", + "Courier New", + "Brush Script MT", + ], + value="Arial", + ).props("id='text-font-family'") + + add_text_button = ui.button( + "Add to canvas", + on_click=lambda: ( + ui.run_javascript(""" + const event = new Event("addText"); + document.querySelector("#text-input").dispatchEvent(event); + """), + text_input.set_value(""), + ), + ) + + ui.add_body_html(""" + + [[fetch]] + from = "/scripts/" + files = ["canvas_ctx.py", "editor.py", "shortcuts.py"] + + + + """) + + await client.connected() + drawing_mode = await ui.run_javascript("return localStorage.getItem('cj12-hhh-drawing-mode');") + if drawing_mode == "pixel": + revert_type() + width_input.disable() + width_slider.disable() + file_uploader.disable() + text_input.disable() + add_text_button.disable() + bold_checkbox.disable() + italics_checkbox.disable() + font_family.disable() + + logged_in = await ui.run_javascript("return sessionStorage.getItem('cj12-hhh-logged-in');") + if logged_in == "true": + register_button.move(hidden_buttons) + login_button.move(hidden_buttons) + + publish_button.move(shown_buttons) + logout_button.move(shown_buttons) + + ui.on("content_loaded", check_login_status) + ui.timer(5.0, check_login_status) + + +if __name__ in {"__main__", "__mp_main__"}: + ui.run(port=9010, title="HHH Editor", root_path="/editor") diff --git a/heavenly-hostas/packages/editor/pyproject.toml b/heavenly-hostas/packages/editor/pyproject.toml new file mode 100644 index 00000000..233e1071 --- /dev/null +++ b/heavenly-hostas/packages/editor/pyproject.toml @@ -0,0 +1,9 @@ +[project] +name = "editor" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "nicegui>=2.22.2", +] diff --git a/heavenly-hostas/packages/editor/scripts/__init__.py b/heavenly-hostas/packages/editor/scripts/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/heavenly-hostas/packages/editor/scripts/canvas_ctx.py b/heavenly-hostas/packages/editor/scripts/canvas_ctx.py new file mode 100644 index 00000000..e1b4f3e9 --- /dev/null +++ b/heavenly-hostas/packages/editor/scripts/canvas_ctx.py @@ -0,0 +1,486 @@ +# There are a lot of NOQAs in this file as these are typehints based on JS classes/methods. + +from typing import Any, Literal + +from js import ( # pyright: ignore[reportMissingImports] + ImageData, + MouseEvent, + document, +) +from pyodide.ffi import JsProxy # pyright: ignore[reportMissingImports] + + +class DOMRect: + """Bounding box typehint.""" + + bottom: float + height: float + left: float + right: float + top: float + width: float + x: float + y: float + + +class ImageBitmap: + """Image bitmap typehint.""" + + height: float + width: float + + +class TextMetrics: + """TextMetrics typehint.""" + + actualBoundingBoxAscent: float # noqa: N815 + actualBoundingBoxDescent: float # noqa: N815 + actualBoundingBoxLeft: float # noqa: N815 + actualBoundingBoxRight: float # noqa: N815 + alphabeticBaseline: float # noqa: N815 + emHeightAscent: float # noqa: N815 + emHeightDescent: float # noqa: N815 + fontBoundingBoxAscent: float # noqa: N815 + fontBoundingBoxDescent: float # noqa: N815 + hangingBaseline: float # noqa: N815 + ideographicBaseline: float # noqa: N815 + width: float + + +class CanvasSettings: + """`CanvasSettings` for `CanvasContext`.""" + + def __init__(self) -> None: + self.willReadFrequently = True + + +class CanvasContext: + """`CanvasContext` for a HTML5 Canvas element.""" + + # Custom attributes + scaled_by: float = 2 # Better resolution + drawing: bool = False + action: Literal["pen", "eraser", "smudge", "rectangle", "triangle", "star"] = "pen" + type: Literal["smooth", "pixel"] = "smooth" + current_img: Any + bounding_rect: Any + last_x: float + last_y: float + smudge_data: ImageData + prev_data: ImageData + moving_image: bool + writing_text: bool + text_value: str + prev_operation: Literal[ + "source-over", + "source-in", + "source-out", + "source-atop", + "destination-over", + "destination-in", + "destination-out", + "destination-atop", + "lighter", + "copy", + "xor", + "multiply", + "screen", + "overlay", + "darken", + "lighten", + "color-dodge", + "color-burn", + "hard-light", + "soft-light", + "difference", + "exclusion", + "hue", + "saturation", + "color", + "luminosity", + ] + text_settings: dict[str, str | bool | int] + history: list[Any] + history_index: int + text_placed: bool + clipping: bool + moving_clip: bool + start_coords: list[float] + prev_stroke_style: str + prev_line_width: int + size_change: int + rotation: float + is_rotating: bool + current_position: list[float] + drawing_shape: bool + + # Builtin attributes + canvas: Any + direction: Any + fillStyle: Any # noqa: N815 + filter: Any + font: Any + fontKerning: Any # noqa: N815 + fontStretch: Any # noqa: N815 + fontVariantCaps: Any # noqa: N815 + globalAlpha: Any # noqa: N815 + globalCompositeOperation: Literal[ # noqa: N815 + "source-over", + "source-in", + "source-out", + "source-atop", + "destination-over", + "destination-in", + "destination-out", + "destination-atop", + "lighter", + "copy", + "xor", + "multiply", + "screen", + "overlay", + "darken", + "lighten", + "color-dodge", + "color-burn", + "hard-light", + "soft-light", + "difference", + "exclusion", + "hue", + "saturation", + "color", + "luminosity", + ] = "source-over" + + imageSmoothingEnabled: bool # noqa: N815 + imageSmoothingQuality: Any # noqa: N815 + langExperimental: Any # noqa: N815 + letterSpacing: Any # noqa: N815 + lineCap: str = "round" # noqa: N815 + lineDashOffset: Any # noqa: N815 + lineJoin: str = "round" # noqa: N815 + lineWidth: float # noqa: N815 + miterLimit: Any # noqa: N815 + shadowBlur: Any # noqa: N815 + shadowColor: Any # noqa: N815 + shadowOffsetX: Any # noqa: N815 + shadowOffsetY: Any # noqa: N815 + strokeStyle: str # noqa: N815 + textAlign: Any # noqa: N815 + textBaseline: Any # noqa: N815 + textRendering: Any # noqa: N815 + wordSpacing: Any # noqa: N815 + + def __init__( + self, + settings: CanvasSettings, + ) -> None: + """Get the canvas context 2d.""" + self.canvas = document.getElementById("image-canvas") + self.ctx = self.canvas.getContext("2d", settings) + + ########################################################################### + # properties + ########################################################################### + @property + def rect_left(self) -> float: + """The left side of the bounding rect.""" + return self.getBoundingClientRect().left + + @property + def rect_right(self) -> float: + """The right side of the bounding rect.""" + return self.getBoundingClientRect().left + + @property + def rect_top(self) -> float: + """The top side of the bounding rect.""" + return self.getBoundingClientRect().left + + @property + def rect_bottom(self) -> float: + """The bottom side of the bounding rect.""" + return self.getBoundingClientRect().left + + ########################################################################### + # Cutstom Methods + ########################################################################### + def getBoundingClientRect(self) -> DOMRect: # noqa: N802 + """Get the canvas getBoundingClientRect.""" + return self.canvas.getBoundingClientRect() + + def get_canvas_coords(self, event: MouseEvent) -> tuple[float, float]: + """Give the canvas coordinates. + + Args: + event (MouseEvent): The mouse event + + Returns: + tuple[float, float]: The x and y coordinates + + """ + x = (event.pageX - self.rect_left) * self.scaled_by + y = (event.pageY - self.rect_top) * self.scaled_by + return (x, y) + + ########################################################################### + # Builtin Methods + ########################################################################### + def arc( # noqa: PLR0913 This method has this many arguments. + self, + x: float, + y: float, + radius: float, + startAngle: float, # noqa: N803 + endAngle: float, # noqa: N803 + *, + counterclockwise: bool = False, + ) -> None: + """Add arc.""" + self.ctx.arc(x, y, radius, startAngle, endAngle, counterclockwise) + + def arcTo(self) -> None: # noqa: N802 + """Add arcTo.""" + self.ctx.arcTo() + + def beginPath(self) -> None: # noqa: N802 + """Add beginPath.""" + self.ctx.beginPath() + + def bezierCurveTo(self, cp1x: float, cp1y: float, cp2x: float, cp2y: float, x: float, y: float) -> None: # noqa: N802 + """Add bezierCurveTo.""" + self.ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y) + + def clearRect(self, x: float, y: float, width: float, height: float) -> None: # noqa: N802 + """Add clearRect.""" + self.ctx.clearRect(x, y, width, height) + + def clip(self) -> None: + """Add clip.""" + self.ctx.clip() + + def closePath(self) -> None: # noqa: N802 + """Add closePath.""" + self.ctx.closePath() + + def createConicGradient(self) -> None: # noqa: N802 + """Add createConicGradient.""" + self.ctx.createConicGradient() + + def createImageData(self) -> None: # noqa: N802 + """Add createImageData.""" + self.ctx.createImageData() + + def createLinearGradient(self) -> None: # noqa: N802 + """Add createLinearGradient.""" + self.ctx.createLinearGradient() + + def createPattern(self) -> None: # noqa: N802 + """Add createPattern.""" + self.ctx.createPattern() + + def createRadialGradient(self) -> None: # noqa: N802 + """Add createRadialGradient.""" + self.ctx.createRadialGradient() + + def drawFocusIfNeeded(self) -> None: # noqa: N802 + """Add drawFocusIfNeeded.""" + self.ctx.drawFocusIfNeeded() + + def drawImage( # noqa: N802 + self, + image: JsProxy, + dx: float, + dy: float, + dWidth: float | None = None, # noqa: N803 + dHeight: float | None = None, # noqa: N803 + ) -> None: + """Add drawImage.""" + self.ctx.drawImage(image, dx, dy, dWidth, dHeight) + + def ellipse( # noqa: PLR0913 This method has this many arguments. + self, + x: float, + y: float, + radiusX: float, # noqa: N803 + radiusY: float, # noqa: N803 + rotation: float, + startAngle: float, # noqa: N803 + endAngle: float, # noqa: N803 + *, + counterclockwise: bool = False, + ) -> None: + """Add ellipse.""" + self.ctx.ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, counterclockwise) + + def fill(self) -> None: + """Add fill.""" + self.ctx.fill() + + def fillRect(self, x: float, y: float, width: float, height: float) -> None: # noqa: N802 + """Add fillRect.""" + self.ctx.fillRect(x, y, width, height) + + def fillText(self, text: str, x: float, y: float) -> None: # noqa: N802 + """Add fillText.""" + self.ctx.fillText(text, x, y) + + def getContextAttributes(self) -> None: # noqa: N802 + """Add getContextAttributes.""" + self.ctx.getContextAttributes() + + def getImageData(self, sx: float, sy: float, sw: float, sh: float) -> ImageData: # noqa: N802 + """Get the image data from the canvas.""" + return self.ctx.getImageData(sx, sy, sw, sh) + + def getLineDash(self) -> None: # noqa: N802 + """Add getLineDash.""" + self.ctx.getLineDash() + + def getTransform(self) -> None: # noqa: N802 + """Add getTransform.""" + self.ctx.getTransform() + + def isContextLost(self) -> None: # noqa: N802 + """Add isContextLost.""" + self.ctx.isContextLost() + + def isPointInPath(self) -> None: # noqa: N802 + """Add isPointInPath.""" + self.ctx.isPointInPath() + + def isPointInStroke(self) -> None: # noqa: N802 + """Add isPointInStroke.""" + self.ctx.isPointInStroke() + + def lineTo(self, x: float, y: float) -> None: # noqa: N802 + """Make a line to the x, y given.""" + self.ctx.lineTo(x, y) + + def measureText(self, text: str) -> TextMetrics: # noqa: N802 + """Add measureText.""" + return self.ctx.measureText(text) + + def moveTo(self, x: float, y: float) -> None: # noqa: N802 + """Move to the x, y given.""" + self.ctx.moveTo(x, y) + + def putImageData( # # noqa: N802, PLR0913 This method has this many arguments. + self, + imageData: ImageData, # noqa: N803 + dx: float, + dy: float, + dirtyX: float | None = None, # noqa: N803 + dirtyY: float | None = None, # noqa: N803 + dirtyWidth: float | None = None, # noqa: N803 + dirtyHeight: float | None = None, # noqa: N803 + ) -> None: + """Paint rectangle onto canvas. + + Paints data from the given ImageData object onto the canvas. If a dirty rectangle is provided, only the + pixels from that rectangle are painted. This method is not affected by the canvas transformation matrix. + + Parameters + ---------- + imageData + An ImageData object containing the array of pixel values. + + dx: float + Horizontal position (x coordinate) at which to place the image data + in the destination canvas. + + dy: float + Vertical position (y coordinate) at which to place the image data + in the destination canvas. + + dirtyX: float | None = None + Horizontal position (x coordinate) of the top-left corner from + which the image data will be extracted. Defaults to 0. + + dirtyY: float | None = None + Vertical position (y coordinate) of the top-left corner from which + the image data will be extracted. Defaults to 0. + + dirtyWidth: float | None = None + Width of the rectangle to be painted. Defaults to the width of the + image data. + + dirtyHeight: float | None = None + Height of the rectangle to be painted. Defaults to the height of + the image data. + + """ + self.ctx.putImageData( + imageData, + dx, + dy, + dirtyX, + dirtyY, + dirtyWidth, + dirtyHeight, + ) + + def quadraticCurveTo(self) -> None: # noqa: N802 + """Add quadraticCurveTo.""" + self.ctx.quadraticCurveTo() + + def rect(self, x: float, y: float, width: float, height: float) -> None: + """Set the rect.""" + self.ctx.rect(x, y, width, height) + + def reset(self) -> None: + """Add reset.""" + self.ctx.reset() + + def resetTransform(self) -> None: # noqa: N802 + """Add resetTransform.""" + self.ctx.resetTransform() + + def restore(self) -> None: + """Add restore.""" + self.ctx.restore() + + def rotate(self, angle: float) -> None: + """Add rotate.""" + self.ctx.rotate(angle) + + def roundRect(self, x: float, y: float, width: float, height: float, radii: list[float]) -> None: # noqa: N802 + """Add roundRect.""" + self.ctx.roundRect(x, y, width, height, radii) + + def save(self) -> None: + """Add save.""" + self.ctx.save() + + def scale(self, x: float, y: float) -> None: + """Add scale.""" + self.ctx.scale(x, y) + + def setLineDash(self, segments: list[float]) -> None: # noqa: N802 + """Add setLineDash.""" + self.ctx.setLineDash(segments) + + def setTransform(self) -> None: # noqa: N802 + """Add setTransform.""" + self.ctx.setTransform() + + def stroke(self) -> None: + """Add stroke.""" + self.ctx.stroke() + + def strokeRect(self, x: float, y: float, width: float, height: float) -> None: # noqa: N802 + """Add strokeRect.""" + self.ctx.strokeRect(x, y, width, height) + + def strokeText(self) -> None: # noqa: N802 + """Add strokeText.""" + self.ctx.strokeText() + + def transform(self) -> None: + """Add transform.""" + self.ctx.transform() + + def translate(self, x: float, y: float) -> None: + """Add translate.""" + self.ctx.translate(x, y) diff --git a/heavenly-hostas/packages/editor/scripts/editor.py b/heavenly-hostas/packages/editor/scripts/editor.py new file mode 100644 index 00000000..c839cfe4 --- /dev/null +++ b/heavenly-hostas/packages/editor/scripts/editor.py @@ -0,0 +1,1233 @@ +# This should be under the other imports but because it isn't imported in the traditional way, it's above them. +from math import cos, pi, sin +from typing import Literal + +from canvas_ctx import CanvasContext, ImageBitmap # pyright: ignore[reportMissingImports] + +# Following imports have the ignore flag as they are not pip installed +from js import ( # pyright: ignore[reportMissingImports] + Event, + Image, + ImageData, + KeyboardEvent, + Math, + MouseEvent, + Object, + createImageBitmap, + document, + localStorage, + window, +) +from pyodide.ffi import create_proxy # pyright: ignore[reportMissingImports] +from pyscript import when # pyright: ignore[reportMissingImports] + +canvas = document.getElementById("image-canvas") +buffer = document.getElementById("buffer-canvas") +text_input = document.getElementById("text-input") + +bold_input = document.getElementById("bold-text") +italics_input = document.getElementById("italics-text") +font_family_input = document.getElementById("text-font-family").querySelector("input") + +settings = Object() +settings.willReadFrequently = True + +ctx: CanvasContext = canvas.getContext("2d", settings) +buffer_ctx: CanvasContext = buffer.getContext("2d", settings) + +canvas.style.imageRendering = "pixelated" +canvas.style.imageRendering = "crisp-edges" + +buffer.style.imageRendering = "pixelated" +buffer.style.imageRendering = "crisp-edges" + +# Settings properties of the canvas. +display_height = window.innerHeight * 0.95 # 95vh +display_width = display_height * (2**0.5) # Same ratio as an A4 sheet of paper + +ctx.scaled_by = 2 # Better resolution + +canvas.style.height = f"{display_height}px" +canvas.style.width = f"{display_width}px" + +canvas.height = display_height * ctx.scaled_by +canvas.width = display_width * ctx.scaled_by + +buffer.style.height = f"{display_height}px" +buffer.style.width = f"{display_width}px" + +buffer.height = display_height * ctx.scaled_by +buffer.width = display_width * ctx.scaled_by + + +ctx.imageSmoothingEnabled = False +ctx.strokeStyle = "black" +ctx.lineWidth = 5 +ctx.lineCap = "round" +ctx.lineJoin = "round" +ctx.font = "50px Arial" + +# Custom attributes attached so we don't need to use global vars +ctx.drawing = False +ctx.action = "pen" +ctx.type = "smooth" +ctx.bounding_rect = canvas.getBoundingClientRect() +ctx.current_img = Image.new() +ctx.moving_image = False +ctx.writing_text = False +ctx.text_placed = True +ctx.prev_operation = "source-over" +ctx.text_settings = {"bold": False, "italics": False, "size": 50, "font-family": "Arial"} +ctx.clipping = False +ctx.moving_clip = False +ctx.drawing_shape = False +ctx.start_coords = [0, 0] +ctx.prev_stroke_style = "black" +ctx.prev_line_width = 5 +ctx.size_change = 0 +ctx.rotation = 0 +ctx.is_rotating = False +ctx.current_position = [0, 0] + + +buffer_ctx.imageSmoothingEnabled = False +buffer_ctx.strokeStyle = "black" +buffer_ctx.lineWidth = 5 +buffer_ctx.lineCap = "round" +buffer_ctx.lineJoin = "round" +buffer_ctx.font = f"{ctx.text_settings['size']}px {ctx.text_settings['font-family']}" + +ctx.history = [] +ctx.history_index = -1 +MAX_HISTORY = 50 +MIN_RESIZE_SIZE = 20 +MAX_RESIZE_SIZE = 200 +ROTATION_SPEED = 3 + +PIXEL_SIZE = 8 +SMUDGE_BLEND_FACTOR = 0.5 + + +def save_history() -> None: + """Save the historical data.""" + ctx.history = ctx.history[: ctx.history_index + 1] + if len(ctx.history) >= MAX_HISTORY: + ctx.history.pop(0) + ctx.history_index -= 1 + + ctx.history.append(ctx.getImageData(0, 0, canvas.width, canvas.height)) + ctx.history_index += 1 + save_change_to_browser() + + +def save_change_to_browser() -> None: + """Save change to browser storage.""" + localStorage.setItem("cj12-hhh-image-data", canvas.toDataURL("image/webp")) + localStorage.setItem("cj12-hhh-drawing-mode", ctx.type) + + +@when("click", "#undo-button") +def undo(_: Event) -> None: + """Undo history.""" + if ctx.history_index <= 0: + return + ctx.history_index -= 1 + + def place_history(img_bitmap: ImageBitmap) -> None: + ctx.clearRect(0, 0, canvas.width, canvas.height) + ctx.prev_operation = ctx.globalCompositeOperation + ctx.globalCompositeOperation = "source-over" + ctx.drawImage(img_bitmap, 0, 0, canvas.width, canvas.height) + ctx.globalCompositeOperation = ctx.prev_operation + localStorage.setItem("cj12-hhh-image-data", canvas.toDataURL("image/webp")) + localStorage.setItem("cj12-hhh-drawing-mode", ctx.type) + + createImageBitmap(ctx.history[ctx.history_index]).then(place_history) + + +@when("click", "#redo-button") +def redo(_: Event) -> None: + """Redo history.""" + if ctx.history_index >= len(ctx.history) - 1: + return + ctx.history_index += 1 + + def place_history(img_bitmap: ImageBitmap) -> None: + ctx.clearRect(0, 0, canvas.width, canvas.height) + ctx.prev_operation = ctx.globalCompositeOperation + ctx.globalCompositeOperation = "source-over" + ctx.drawImage(img_bitmap, 0, 0, canvas.width, canvas.height) + ctx.globalCompositeOperation = ctx.prev_operation + localStorage.setItem("cj12-hhh-image-data", canvas.toDataURL("image/webp")) + localStorage.setItem("cj12-hhh-drawing-mode", ctx.type) + + createImageBitmap(ctx.history[ctx.history_index]).then(place_history) + + +def draw_pixel(x: float, y: float) -> None: + """Draws the pixel on the canvas. + + Args: + x (float): X coordinate + y (float): Y coordinate + """ + ctx.fillStyle = ctx.strokeStyle + ctx.fillRect(x - PIXEL_SIZE // 2, y - PIXEL_SIZE // 2, PIXEL_SIZE, PIXEL_SIZE) + + +def show_action_icon(x: float, y: float) -> bool: + """Show icon to let user know what the action would look like. + + Args: + x (float): X coordinate + y (float): Y coordinate + + Returns: + bool: If True is returned mousemove doesn't do anything else + """ + + def draw_clip(img_bitmap: ImageBitmap) -> None: + buffer_ctx.clearRect(0, 0, canvas.width, canvas.height) + + # Canvas matrix to cursor coords first + buffer_ctx.translate( + x, + y, + ) + buffer_ctx.rotate(ctx.rotation) # Apply rotation + + # Ratio for scaling up/down + ratio = img_bitmap.width / img_bitmap.height + + if img_bitmap.width < img_bitmap.height: + buffer_ctx.strokeRect( + -(img_bitmap.width + ctx.size_change) / 2, # Shift the horizontal centre to be on the cursor + -(img_bitmap.height + ctx.size_change * ratio) / 2, # Shift the verical centre to be on the cursor + img_bitmap.width + ctx.size_change, + img_bitmap.height + ctx.size_change * ratio, + ) + buffer_ctx.drawImage( + img_bitmap, + -(img_bitmap.width + ctx.size_change) / 2, + -(img_bitmap.height + ctx.size_change * ratio) / 2, + img_bitmap.width + ctx.size_change, + img_bitmap.height + ctx.size_change * ratio, + ) + else: + buffer_ctx.strokeRect( + -(img_bitmap.width + ctx.size_change * ratio) / 2, + -(img_bitmap.height + ctx.size_change) / 2, + img_bitmap.width + ctx.size_change * ratio, + img_bitmap.height + ctx.size_change, + ) + buffer_ctx.drawImage( + img_bitmap, + -(img_bitmap.width + ctx.size_change * ratio) / 2, + -(img_bitmap.height + ctx.size_change) / 2, + img_bitmap.width + ctx.size_change * ratio, + img_bitmap.height + ctx.size_change, + ) + + buffer_ctx.rotate(-ctx.rotation) # Undo rotation + + # Move canvas matrix back to top left corner + buffer_ctx.translate( + -x, + -y, + ) + + if ctx.moving_clip: + createImageBitmap(ctx.prev_data).then(draw_clip) + return True + + buffer_ctx.clearRect(0, 0, canvas.width, canvas.height) + + if ctx.moving_image: + buffer_ctx.translate( + x, + y, + ) + buffer_ctx.rotate(ctx.rotation) + + ratio = ctx.current_img.width / ctx.current_img.height + + if ctx.current_img.width < ctx.current_img.height: + buffer_ctx.drawImage( + ctx.current_img, + -(ctx.current_img.width + ctx.size_change) / 2, + -(ctx.current_img.height + ctx.size_change * ratio) / 2, + ctx.current_img.width + ctx.size_change, + ctx.current_img.height + ctx.size_change * ratio, + ) + else: + buffer_ctx.drawImage( + ctx.current_img, + -(ctx.current_img.width + ctx.size_change * ratio) / 2, + -(ctx.current_img.height + ctx.size_change) / 2, + ctx.current_img.width + ctx.size_change * ratio, + ctx.current_img.height + ctx.size_change, + ) + + buffer_ctx.rotate(-ctx.rotation) + buffer_ctx.translate( + -x, + -y, + ) + + return True + if ctx.writing_text and not ctx.text_placed: + buffer_ctx.translate( + x, + y, + ) + buffer_ctx.rotate(ctx.rotation) + + text_dimensions = ctx.measureText(ctx.text_value) + buffer_ctx.fillText( + ctx.text_value, + -text_dimensions.width / 2, + (text_dimensions.actualBoundingBoxAscent + text_dimensions.actualBoundingBoxDescent) / 2, + ) + + buffer_ctx.rotate(-ctx.rotation) + buffer_ctx.translate( + -x, + -y, + ) + + return True + if ctx.clipping: + buffer_ctx.strokeRect( + ctx.start_coords[0], + ctx.start_coords[1], + x - ctx.start_coords[0], + y - ctx.start_coords[1], + ) + return True + + regular_icon_show(x, y) + return False + + +def regular_icon_show(x: float, y: float) -> None: + """Show preview for regular actions. + + Args: + x (float): X coordinate + y (float): Y coordinate + """ + if ctx.type == "smooth": + buffer_ctx.beginPath() + buffer_ctx.arc(x, y, ctx.lineWidth / 2, 0, 2 * Math.PI) # Put a dot here + if ctx.action == "pen": + buffer_ctx.fill() + elif ctx.action == "eraser": + prev_width = buffer_ctx.lineWidth + prev_fill = buffer_ctx.fillStyle + + buffer_ctx.lineWidth = ctx.scaled_by + buffer_ctx.fillStyle = "white" + buffer_ctx.fill() + buffer_ctx.arc(x, y, ctx.lineWidth / 2, 0, 2 * Math.PI) + buffer_ctx.stroke() + + buffer_ctx.lineWidth = prev_width + buffer_ctx.fillStyle = prev_fill + + +def get_smudge_data(x: float, y: float) -> ImageData: + """Get the smudge data around the xy for smudgeing.""" + smudge_size = ctx.lineWidth + + return ctx.getImageData( + x - (smudge_size // 2), + y - (smudge_size // 2), + smudge_size, + smudge_size, + ) + + +def put_smudge_data(x: float, y: float) -> None: + """Put the smudge data around the xy for smudgeing.""" + smudge_size = ctx.lineWidth + + ctx.putImageData( + ctx.smudge_data, + x - (smudge_size // 2), + y - (smudge_size // 2), + ) + + +def update_smudge_data(x: float, y: float) -> None: + """Update the smudge data around the xy for smudgeing.""" + ctx.smudge_data = get_smudge_data(x, y) + ctx.last_x = x + ctx.last_y = y + + +def draw_smudge(event: MouseEvent) -> None: + """Draws the smudge data on the canvas. + + Args: + event (MouseEvent): The javascript mouse event + """ + x, y = get_canvas_coords(event) + # draw the pevious smudge data at the current xy. + put_smudge_data(x, y) + + update_smudge_data(x, y) + + +def get_canvas_coords(event: MouseEvent) -> tuple[float, float]: + """Give the canvas coordinates. + + Args: + event (MouseEvent): The mouse event + + Returns: + tuple[float, float]: The x and y coordinates + """ + x = (event.pageX - ctx.bounding_rect.left) * ctx.scaled_by + y = (event.pageY - ctx.bounding_rect.top) * ctx.scaled_by + if ctx.type == "pixel": + x = (int(x) + 5) // 10 * 10 + y = (int(y) + 5) // 10 * 10 + return (x, y) + + +@when("mousedown", "#image-canvas") +def start_path(event: MouseEvent) -> None: + """Start drawing the path. + + Args: + event (MouseEvent): The mouse event + """ + if event.button != 0: + return + + if ctx.moving_image or ctx.drawing_shape: + return + + ctx.drawing = True + + x, y = get_canvas_coords(event) + if ctx.action == "smudge": + update_smudge_data(x, y) + elif ctx.type == "smooth": + x, y = get_canvas_coords(event) + ctx.beginPath() + ctx.moveTo(x, y) + + +def get_triangle_shape_points( + x: float | int, + y: float | int, + dx: float | int, + dy: float | int, +): + """Get the points in a triangle shape given the start coordinate x,y and + the size of the triangle dx, dy.""" + top_mid_point = (x + (dx / 2), y) + bot_left_point = (x, y + dy) + bot_right_point = (x + dx, y + dy) + return [top_mid_point, bot_left_point, bot_right_point] + + +def get_star_shape_points( + x: float | int, + y: float | int, + dx: float | int, + dy: float | int, +): + """Get the points in a star shape given the start coordinate x,y and the + size of the star dx, dy.""" + center_x = x + dx / 2 + center_y = y + dy / 2 + + outer_radius_x = dx / 2 + inner_radius_x = outer_radius_x / 2 + + outer_radius_y = dy / 2 + inner_radius_y = outer_radius_y / 2 + + NO_OF_STAR_POINTS = 5 + + points: list[tuple[float, float]] = [] + + for i in range(NO_OF_STAR_POINTS * 2): + angle = i * pi / NO_OF_STAR_POINTS # 10 points in the star including inner points. + + radius_x = outer_radius_x if i % 2 == 0 else inner_radius_x + radius_y = outer_radius_y if i % 2 == 0 else inner_radius_y + + px = center_x + cos(angle) * radius_x + py = center_y + sin(angle) * radius_y + + points.append((px, py)) + + return points + + +def draw_python_logo( + x: float | int, + y: float | int, +): + """Draw python logo.""" + buffer_ctx.save() + x0, y0 = ctx.start_coords + x1, y1 = x, y + + left = min(x0, x1) + top = min(y0, y1) + width = abs(x1 - x0) + height = abs(y1 - y0) + + buffer_ctx.translate(left, top) + + scale_x = width / 40 + scale_y = height / 40 + if x1 < x0: + buffer_ctx.translate(width, 0) + scale_x *= -1 + if y1 < y0: + buffer_ctx.translate(0, height) + scale_y *= -1 + + buffer_ctx.scale(scale_x, scale_y) + + # the two rounded rects forming a cross. + buffer_ctx.beginPath() + buffer_ctx.roundRect(10, 0, 20, 40, [10, 5]) + buffer_ctx.roundRect(0, 10, 40, 20, [5, 10]) + buffer_ctx.fill() + + # the two eyes for each of the snakey bois. O.o + buffer_ctx.beginPath() + buffer_ctx.arc(14.5, 5, 1.85, 0, 2 * pi) + buffer_ctx.arc(25.5, 35, 1.85, 0, 2 * pi) + buffer_ctx.fillStyle = "white" + buffer_ctx.fill() + + # the lines that make the mouth of the snakey bois. :) + buffer_ctx.lineWidth = 1 + buffer_ctx.beginPath() + buffer_ctx.moveTo(10, 9.5) + buffer_ctx.lineTo(20, 9.5) + buffer_ctx.moveTo(20, 30.5) + buffer_ctx.lineTo(30, 30.5) + buffer_ctx.strokeStyle = "white" + buffer_ctx.stroke() + + # The lines the seperates the snakey bois from one another. :) :) + buffer_ctx.beginPath() + buffer_ctx.moveTo(9.5, 30) + buffer_ctx.bezierCurveTo(9.5, 20, 12, 20, 19.5, 20) + buffer_ctx.bezierCurveTo(28, 20, 30.5, 20, 30.5, 10) + buffer_ctx.stroke() + + buffer_ctx.restore() + return + + +def draw_shape( + x: float | int, + y: float | int, + shape_type: Literal["rectangle", "triangle", "star"], +) -> None: + """Draw a shape to the buffer_ctx sized by the x,y given relative to the + start x,y when the canvas was initially clicked.""" + if not ctx.drawing_shape: + return + init_x, init_y = ctx.start_coords + dx = x - init_x + dy = y - init_y + match shape_type: + case "rectangle": + buffer_ctx.fillRect(init_x, init_y, dx, dy) + return + case "circle": + buffer_ctx.ellipse( + init_x + dx / 2, + init_y + dy / 2, + abs(dx), + abs(dy), + 0, + 0, + 2 * pi, + ) + buffer_ctx.fill() + return + case "python": + draw_python_logo(x, y) + return + + case "triangle": + points = get_triangle_shape_points(init_x, init_y, dx, dy) + + case "star": + points = get_star_shape_points(init_x, init_y, dx, dy) + + buffer_ctx.beginPath() + buffer_ctx.moveTo(points[0][0], points[0][1]) + for px, py in points[1:]: + buffer_ctx.lineTo(px, py) + + buffer_ctx.closePath() + buffer_ctx.fill() + buffer_ctx.stroke() + return + + +@when("mousemove", "#image-canvas") +def mouse_tracker(event: MouseEvent) -> None: + """Draw the path following the mouse. + + Args: + event (MouseEvent): The mouse event + """ + x, y = get_canvas_coords(event) + ctx.current_position = [x, y] + if show_action_icon(x, y): + return + if not ctx.text_placed: + text_dimensions = ctx.measureText(ctx.text_value) + + buffer_ctx.fillText( + ctx.text_value, + x - text_dimensions.width / 2, + y + (text_dimensions.actualBoundingBoxAscent + text_dimensions.actualBoundingBoxDescent) / 2, + ) + if ctx.writing_text: + return + if not ctx.drawing: + return + if ctx.drawing_shape: + draw_shape(x, y, ctx.action) # pyright: ignore[reportArgumentType] The action will be of the correct literal + return + draw_action(event, x, y) + + +def draw_action(event: MouseEvent, x: float, y: float) -> None: + """Draw the event on the screen. + + Args: + event (MouseEvent): Mouse event + x (float): X coordinate + y (float): Y coordinate + """ + match ctx.type: + case "smooth": + if ctx.action == "smudge": + draw_smudge(event) + elif ctx.action in ("pen", "eraser"): + ctx.lineTo(x, y) + ctx.stroke() + case "pixel": + if ctx.action == "pen": + draw_pixel(x, y) + elif ctx.action == "eraser": + ctx.clearRect(x - PIXEL_SIZE // 2, y - PIXEL_SIZE // 2, PIXEL_SIZE, PIXEL_SIZE) + + +@when("mouseup", "body") +def stop_path(_: MouseEvent) -> None: + """Stop drawing path. + + Args: + event (MouseEvent): The mouse event + """ + if ctx.drawing and not ctx.clipping and not ctx.drawing_shape: + ctx.drawing = False + save_history() + + +@when("mouseup", "body") +def drop_media(event: MouseEvent) -> None: + """Place text or clipping. + + Args: + event (MouseEvent): Mouse event + """ + x, y = get_canvas_coords(event) + if ctx.text_placed: + ctx.writing_text = False + if ctx.clipping: + ctx.clipping = False + ctx.moving_clip = True + + ctx.prev_data = ctx.getImageData( + ctx.start_coords[0], + ctx.start_coords[1], + x - ctx.start_coords[0], + y - ctx.start_coords[1], + ) + ctx.clearRect( + ctx.start_coords[0], + ctx.start_coords[1], + x - ctx.start_coords[0], + y - ctx.start_coords[1], + ) + if ctx.drawing_shape: + ctx.drawing_shape = False + ctx.drawing = False + ctx.drawImage(buffer, 0, 0) + save_history() + + +@when("mouseenter", "#image-canvas") +def start_reentry_path(event: MouseEvent) -> None: + """Start a new path from the edge upon canvas entry. + + Args: + event (MouseEvent): Mouse event + """ + if ctx.drawing: + x, y = get_canvas_coords(event) + ctx.beginPath() + ctx.moveTo(x, y) + + +@when("mouseout", "#image-canvas") +def leaves_canvas(event: MouseEvent) -> None: + """Handle mouse leaving canvas. + + Args: + event (MouseEvent): The mouse event + """ + if ( + not ctx.drawing + or ctx.clipping + or ctx.drawing_shape + or ctx.action in ("circle", "rectangle", "triangle", "star", "python") + ): + return + + if ctx.type == "smooth" and ctx.action != "smudge": # "pen" or "eraser" + x, y = get_canvas_coords(event) + ctx.lineTo(x, y) + ctx.stroke() + + +@when("mousedown", "#image-canvas") +def canvas_click(event: MouseEvent) -> None: + """Handle mouse clicking canvas. + + Args: + event (MouseEvent): The mouse event + """ + if event.button != 0: + return + x, y = get_canvas_coords(event) + if special_actions(x, y): + return + if ctx.type == "smooth": + if ctx.action == "clip" and not ctx.moving_clip: + ctx.clipping = True + ctx.start_coords = [x, y] + + ctx.setLineDash([2, 10]) + buffer_ctx.setLineDash([2, 10]) + + ctx.prev_stroke_style = ctx.strokeStyle + ctx.prev_line_width = ctx.lineWidth + + ctx.strokeStyle = "black" + ctx.lineWidth = 5 + + buffer_ctx.strokeStyle = "black" + buffer_ctx.lineWidth = 5 + + elif ctx.action in ("circle", "rectangle", "triangle", "star", "python"): + ctx.drawing_shape = True + ctx.start_coords = [x, y] + + else: + ctx.beginPath() + ctx.ellipse(x, y, ctx.lineWidth / 100, ctx.lineWidth / 100, 0, 0, 2 * Math.PI) # Put a dot here + if ctx.action in ("pen", "eraser"): + ctx.stroke() + elif ctx.type == "pixel": + if ctx.action == "pen": + draw_pixel(x, y) + elif ctx.action == "eraser": + ctx.clearRect(x - PIXEL_SIZE // 2, y - PIXEL_SIZE // 2, PIXEL_SIZE, PIXEL_SIZE) + + +def special_actions(x: float, y: float) -> bool: + """Draw special action on canvas. + + Args: + x (float): X coordinate + y (float): Y coordinate + + Returns: + bool: Whether to skip the regular drawing process or not + """ + if ctx.moving_image: + draw_image(x, y) + + return True + if ctx.writing_text: + ctx.text_placed = True + + buffer_ctx.clearRect(0, 0, canvas.width, canvas.height) + + ctx.translate( + x, + y, + ) + ctx.rotate(ctx.rotation) + + text_dimensions = ctx.measureText(ctx.text_value) + + ctx.fillText( + ctx.text_value, + -text_dimensions.width / 2, + (text_dimensions.actualBoundingBoxAscent + text_dimensions.actualBoundingBoxDescent) / 2, + ) + ctx.globalCompositeOperation = ctx.prev_operation + + ctx.rotate(-ctx.rotation) + ctx.translate( + -x, + -y, + ) + + ctx.rotation = 0 + return True + + if ctx.moving_clip: + ctx.moving_clip = False + + def draw_clip(img_bitmap: ImageBitmap) -> None: + buffer_ctx.clearRect(0, 0, canvas.width, canvas.height) + + ctx.translate( + x, + y, + ) + ctx.rotate(ctx.rotation) + + ratio = img_bitmap.width / img_bitmap.height + + if img_bitmap.width < img_bitmap.height: + ctx.drawImage( + img_bitmap, + -(img_bitmap.width + ctx.size_change) / 2, + -(img_bitmap.height + ctx.size_change * ratio) / 2, + img_bitmap.width + ctx.size_change, + img_bitmap.height + ctx.size_change * ratio, + ) + else: + ctx.drawImage( + img_bitmap, + -(img_bitmap.width + ctx.size_change * ratio) / 2, + -(img_bitmap.height + ctx.size_change) / 2, + img_bitmap.width + ctx.size_change * ratio, + img_bitmap.height + ctx.size_change, + ) + + ctx.size_change = 0 + + ctx.rotate(-ctx.rotation) + ctx.translate( + -x, + -y, + ) + ctx.rotation = 0 + + createImageBitmap(ctx.prev_data).then(draw_clip) + + ctx.setLineDash([]) + buffer_ctx.setLineDash([]) + + ctx.strokeStyle = ctx.prev_stroke_style + ctx.lineWidth = ctx.prev_line_width + + buffer_ctx.strokeStyle = ctx.prev_stroke_style + buffer_ctx.lineWidth = ctx.prev_line_width + + return True + return False + + +def draw_image(x: float, y: float) -> None: + """Draws the uploaded image to the canvas. + + Args: + x (float): X coordinate + y (float): Y coordinate + """ + ctx.moving_image = False + + buffer_ctx.clearRect(0, 0, canvas.width, canvas.height) + + ctx.translate( + x, + y, + ) + ctx.rotate(ctx.rotation) + + ratio = ctx.current_img.width / ctx.current_img.height + + if ctx.current_img.width < ctx.current_img.height: + ctx.drawImage( + ctx.current_img, + -(ctx.current_img.width + ctx.size_change) / 2, + -(ctx.current_img.height + ctx.size_change * ratio) / 2, + ctx.current_img.width + ctx.size_change, + ctx.current_img.height + ctx.size_change * ratio, + ) + else: + ctx.drawImage( + ctx.current_img, + -(ctx.current_img.width + ctx.size_change * ratio) / 2, + -(ctx.current_img.height + ctx.size_change) / 2, + ctx.current_img.width + ctx.size_change * ratio, + ctx.current_img.height + ctx.size_change, + ) + + ctx.rotate(-ctx.rotation) + ctx.translate( + -x, + -y, + ) + + ctx.globalCompositeOperation = ctx.prev_operation + ctx.size_change = 0 + ctx.rotation = 0 + save_history() + + +@when("colourChange", "body") +def colour_change(_: Event) -> None: + """Handle colour change. + + Args: + _ (Event): Change event + """ + ctx.strokeStyle = window.pen.colour + ctx.fillStyle = window.pen.colour + + buffer_ctx.strokeStyle = window.pen.colour + buffer_ctx.fillStyle = window.pen.colour + + +@when("change", ".width-input") +def width_change(event: Event) -> None: + """Handle colour change. + + Args: + event (Event): Change event + """ + ctx.lineWidth = int(event.target.getAttribute("aria-valuenow")) + buffer_ctx.lineWidth = ctx.lineWidth + + +@when("change", "#action-select") +def action_change(event: Event) -> None: + """Handle action change from `pen` to `eraser` or vice versa. + + Args: + event (Event): Change event + """ + ctx.action = event.target.getAttribute("value") + match ctx.action: + case "pen" | "smudge" | "clip": + ctx.globalCompositeOperation = "source-over" + case "eraser": + ctx.globalCompositeOperation = "destination-out" + + +@when("addText", "#text-input") +def add_text(_: Event) -> None: + """Add text to canvas. + + Args: + _ (Event): Add text event + """ + ctx.text_value = text_input.value + if ctx.text_value: + ctx.writing_text = True + ctx.text_placed = False + + ctx.prev_operation = ctx.globalCompositeOperation + ctx.globalCompositeOperation = "source-over" + + ctx.text_settings["bold"] = "bold" if bold_input.getAttribute("aria-checked") == "true" else "normal" + ctx.text_settings["italics"] = "italic" if italics_input.getAttribute("aria-checked") == "true" else "normal" + + ctx.text_settings["font-family"] = font_family_input.value + # I know it's too long but it doesn't work otherwise + ctx.font = f"{ctx.text_settings['italics']} {ctx.text_settings['bold']} {ctx.text_settings['size']}px {ctx.text_settings['font-family']}" # noqa: E501 + buffer_ctx.font = f"{ctx.text_settings['italics']} {ctx.text_settings['bold']} {ctx.text_settings['size']}px {ctx.text_settings['font-family']}" # noqa: E501 + + +@when("change", "#type-select") +def type_change(event: Event) -> None: + """Handle type change. + + Args: + event (Event): Change event + """ + ctx.type = event.target.getAttribute("value") + if ctx.type == "smooth": + ctx.imageSmoothingEnabled = True + ctx.scaled_by = 2 + elif ctx.type == "pixel": + ctx.imageSmoothingEnabled = False + ctx.scaled_by = 0.5 + buffer_ctx.clearRect(0, 0, canvas.width, canvas.height) + + resize(event, keep_content=False) + + # As far as I know there's no way to check when we change from pixel to smooth in the history so there's + # no way to switch the modes in the UI. Hence I've decided to just clear the history instead. + ctx.history.clear() + ctx.history_index = 0 + save_history() + + +@when("reset", "body") +def reset_board(_: Event) -> None: + """Reset the canvas. + + Args: + _ (Event): Reset event + """ + ctx.clearRect(0, 0, canvas.width, canvas.height) + save_history() + + +@when("click", "#download-button") +def download_image(_: Event) -> None: + """Download the canvas content as an image. + + Args: + _ (Event): Click event + """ + link = document.createElement("a") + link.download = "download.webp" + link.href = canvas.toDataURL() + link.click() + link.remove() + + +@when("change", "#file-upload") +def upload_image(e: Event) -> None: + """Handle image upload. + + Args: + e (Event): Upload event + """ + ctx.prev_operation = ctx.globalCompositeOperation + ctx.globalCompositeOperation = "source-over" + ctx.prev_data = ctx.getImageData(0, 0, canvas.width, canvas.height) + ctx.current_img.src = e.target.src + ctx.moving_image = True + + +@create_proxy +def resize(_: Event, keep_content: dict | bool = True) -> None: # noqa: FBT001, FBT002 keep_content has to be a positional arg + """Resize canvas according to window. + + Args: + _ (Event): Resize event + keep_content (bool): Flag to keep the existing content. It's technically not a dict. It's an Object, + but I can't type hint with it. + """ + data = ctx.getImageData(0, 0, canvas.width, canvas.height) + + line_width = ctx.lineWidth + stroke_style = ctx.strokeStyle + font = ctx.font + global_composite_operation = ctx.globalCompositeOperation + + display_height = window.innerHeight * 0.95 + display_width = display_height * (2**0.5) + + canvas.style.height = f"{display_height}px" + canvas.style.width = f"{display_width}px" + + canvas.height = display_height * ctx.scaled_by + canvas.width = display_width * ctx.scaled_by + ctx.bounding_rect = canvas.getBoundingClientRect() + + if isinstance(keep_content, bool): + if keep_content: + createImageBitmap(data).then( + lambda img_bitmap: ctx.drawImage(img_bitmap, 0, 0, canvas.width, canvas.height), + ) + # I don't know why but keep_content is an object sometimes + elif keep_content.keep_content: # pyright: ignore[reportAttributeAccessIssue] + createImageBitmap(data).then( + lambda img_bitmap: ctx.drawImage(img_bitmap, 0, 0, canvas.width, canvas.height), + ) + + ctx.lineWidth = line_width + ctx.strokeStyle = stroke_style + ctx.fillStyle = stroke_style + + ctx.imageSmoothingEnabled = False + ctx.lineCap = "round" + ctx.lineJoin = "round" + ctx.font = font + ctx.globalCompositeOperation = global_composite_operation + + buffer.style.height = f"{display_height}px" + buffer.style.width = f"{display_width}px" + + buffer.height = display_height * ctx.scaled_by + buffer.width = display_width * ctx.scaled_by + + buffer_ctx.imageSmoothingEnabled = False + buffer_ctx.strokeStyle = stroke_style + buffer_ctx.lineWidth = line_width + buffer_ctx.fillStyle = stroke_style + buffer_ctx.lineCap = "round" + buffer_ctx.lineJoin = "round" + buffer_ctx.font = font + + +@create_proxy +def handle_scroll(e: Event) -> None: + """Handle scrolling on the canvas. Used to increase/decrease the size of + images/text etc. + + Args: + e (Event): Scroll event + """ + e.preventDefault() + x, y = get_canvas_coords(e) + if ctx.writing_text: + # ctx.text_settings["size"] is an int + if e.deltaY > 0 and ctx.text_settings["size"] > MIN_RESIZE_SIZE: # pyright: ignore[reportOperatorIssue] + ctx.text_settings["size"] -= 5 # pyright: ignore[reportOperatorIssue] + elif e.deltaY < 0 and ctx.text_settings["size"] < MAX_RESIZE_SIZE: # pyright: ignore[reportOperatorIssue] + ctx.text_settings["size"] += 5 # pyright: ignore[reportOperatorIssue] + ctx.font = f"{ctx.text_settings['italics']} {ctx.text_settings['bold']} {ctx.text_settings['size']}px {ctx.text_settings['font-family']}" # noqa: E501 + buffer_ctx.font = f"{ctx.text_settings['italics']} {ctx.text_settings['bold']} {ctx.text_settings['size']}px {ctx.text_settings['font-family']}" # noqa: E501 + show_action_icon(x, y) + elif ctx.moving_image: + if ( + e.deltaY > 0 + and min(ctx.current_img.width + ctx.size_change, ctx.current_img.height + ctx.size_change) + > MIN_RESIZE_SIZE + ): + ctx.size_change -= 10 + elif ( + e.deltaY < 0 + and max(ctx.current_img.width + ctx.size_change, ctx.current_img.height + ctx.size_change) + < MAX_RESIZE_SIZE * 100 + ): + ctx.size_change += 10 + show_action_icon(x, y) + elif ctx.moving_clip: + if ( + e.deltaY > 0 + and min(ctx.prev_data.width + ctx.size_change, ctx.prev_data.height + ctx.size_change) > MIN_RESIZE_SIZE + ): + ctx.size_change -= 10 + elif ( + e.deltaY < 0 + and max(ctx.prev_data.width + ctx.size_change, ctx.prev_data.height + ctx.size_change) + < MAX_RESIZE_SIZE * 100 + ): + ctx.size_change += 10 + show_action_icon(x, y) + + +@create_proxy +def keydown_event(event: KeyboardEvent) -> None: + """Handle keydown events. + + Args: + event (KeyboardEvent): Keydown event + """ + if event.key == "Backspace": + if ctx.moving_image: + ctx.moving_image = False + + ctx.globalCompositeOperation = ctx.prev_operation + ctx.size_change = 0 + elif ctx.moving_clip: + ctx.moving_clip = False + + ctx.setLineDash([]) + ctx.strokeStyle = ctx.prev_stroke_style + ctx.lineWidth = ctx.prev_line_width + + buffer_ctx.setLineDash([]) + buffer_ctx.strokeStyle = ctx.prev_stroke_style + buffer_ctx.lineWidth = ctx.prev_line_width + elif ctx.writing_text: + ctx.writing_text = False + + ctx.text_placed = True + ctx.globalCompositeOperation = ctx.prev_operation + show_action_icon(ctx.current_position[0], ctx.current_position[1]) + elif event.key == "Alt": + ctx.is_rotating = True + elif event.key == "ArrowLeft" and ctx.is_rotating and (ctx.moving_image or ctx.moving_clip or ctx.writing_text): + ctx.rotation -= Math.PI / 180 * ROTATION_SPEED + show_action_icon(ctx.current_position[0], ctx.current_position[1]) + elif event.key == "ArrowRight" and ctx.is_rotating and (ctx.moving_image or ctx.moving_clip or ctx.writing_text): + ctx.rotation += Math.PI / 180 * ROTATION_SPEED + show_action_icon(ctx.current_position[0], ctx.current_position[1]) + + +@create_proxy +def keyup_event(event: KeyboardEvent) -> None: + """Handle keyup event. + + Args: + event (KeyboardEvent): Keyup event + """ + if event.key == "Alt": + ctx.is_rotating = False + + +@create_proxy +def load_image(event: Event = None) -> None: + """Load image from the browser storage.""" + data_url = localStorage.getItem("cj12-hhh-image-data") + drawing_mode = localStorage.getItem("cj12-hhh-drawing-mode") + + if data_url: + if drawing_mode == "pixel": + ctx.type = "pixel" + ctx.imageSmoothingEnabled = False + ctx.scaled_by = 0.5 + + saved_canvas_data = Image.new() + saved_canvas_data.src = data_url + saved_canvas_data.addEventListener( + "load", + create_proxy( + lambda _: ctx.drawImage(saved_canvas_data, 0, 0, canvas.width, canvas.height), + ), + ) + + if drawing_mode == "pixel": + resize(event) + save_history() + + +# Load image from storage +if document.readyState == "loading": + window.addEventListener("DOMContentLoaded", load_image) +else: + load_image() + + +window.addEventListener("resize", resize) + +ctx.current_img.addEventListener("load", resize) + +document.addEventListener("keydown", keydown_event) +document.addEventListener("keyup", keyup_event) + +# The wheel event is for most browsers. The mousewheel event is deprecated +# but the wheel event is not supported by Safari and Webviewer on iOS. +canvas.addEventListener("wheel", handle_scroll) +canvas.addEventListener("mousewheel", handle_scroll) diff --git a/heavenly-hostas/packages/editor/scripts/shortcuts.py b/heavenly-hostas/packages/editor/scripts/shortcuts.py new file mode 100644 index 00000000..26ccc790 --- /dev/null +++ b/heavenly-hostas/packages/editor/scripts/shortcuts.py @@ -0,0 +1,91 @@ +# Disable missing imports as Pyscript is loaded at runtime +from js import Element, KeyboardEvent # pyright: ignore[reportMissingImports] +from pyscript import document, when # pyright: ignore[reportMissingImports] + +shortcuts_dict = {} +text_input = document.getElementById("text-input") +holding_keys = {"control": False} +undo_button = document.getElementById("undo-button") +redo_button = document.getElementById("redo-button") + + +def handle_toggle(elem: Element, data: list[str]) -> None: + """Add elements to dictionary. + + Args: + elem (Element): Toggle element + data (list[str]): Keybind data + + """ + btn_dict = {btn.innerText: btn for btn in elem.children} + + for d in data: + key, value = d.split(":") + action = btn_dict[value].click + shortcuts_dict[key] = action + + +def handle_btn(elem: Element, data: list[str]) -> None: + """Add elements to dictionary. + + Args: + elem (Element): Button element + data (list[str]): Keybind data + + """ + action = elem.click + key = data[0] + + shortcuts_dict[key] = action + + +for elem in document.getElementsByClassName("keyboard-shortcuts"): + if elem.getAttribute("shortcut_data"): + data = elem.getAttribute("shortcut_data").split(",") + if not data: + continue + if data[0] == "toggle": + handle_toggle(elem, data[1:]) + continue + if data[0] == "btn": + handle_btn(elem, data[1:]) + continue + + +@when("keydown", "body") +def handle_keydown(event: KeyboardEvent) -> None: + """Switch action when keybind is pressed. + + Args: + event (KeyboardEvent): Keydown event + + """ + # Disable keybinds when writing text + if event.target == text_input: + return + if event.key == "Control": + holding_keys["control"] = True + if holding_keys["control"] and event.key == "z": + undo_button.click() + elif holding_keys["control"] and event.key == "Z": + redo_button.click() + if event.repeat: # runs only once when same key is pressed more than once or held down + return + action = shortcuts_dict.get(event.key, None) + if action: + action() + + +@when("keyup", "body") +def handle_up(event: KeyboardEvent) -> None: + """Switch action when keybind is released. + + Args: + event (KeyboardEvent): Keydown event + + """ + # Disable keybinds when writing text + if event.target == text_input: + return + if event.key == "Control": + holding_keys["control"] = False diff --git a/heavenly-hostas/packages/editor/static/forking-1.png b/heavenly-hostas/packages/editor/static/forking-1.png new file mode 100644 index 00000000..204fa71e Binary files /dev/null and b/heavenly-hostas/packages/editor/static/forking-1.png differ diff --git a/heavenly-hostas/packages/editor/static/forking-2.png b/heavenly-hostas/packages/editor/static/forking-2.png new file mode 100644 index 00000000..3dd53fcb Binary files /dev/null and b/heavenly-hostas/packages/editor/static/forking-2.png differ diff --git a/heavenly-hostas/packages/editor/static/installing-app.png b/heavenly-hostas/packages/editor/static/installing-app.png new file mode 100644 index 00000000..c3c59c27 Binary files /dev/null and b/heavenly-hostas/packages/editor/static/installing-app.png differ diff --git a/heavenly-hostas/packages/gallery/README.md b/heavenly-hostas/packages/gallery/README.md new file mode 100644 index 00000000..64a18941 --- /dev/null +++ b/heavenly-hostas/packages/gallery/README.md @@ -0,0 +1,22 @@ +This is the image gallery part of the project. The final product can be accessed [here!!](https://heavenly-hostas-hosting.github.io/HHH/) + +## Concept + +This is the section of the project that manages the Image Gallery. It's a 3D place that has on display every picture that every user has ever posted on the app. + +You are able to navigate the room in 3D from a first-person perspective, being able to fly around the place. You also have the ability to share its different artworks by generating a link that will place you on the exact spot to admire said piece in all of its glory. + +## Development + +We utilize the [*pyscript*](https://github.com/pyscript/pyscript) framework, which allows the execution of Python code inside the browser, including also some very useful interop with JavaScript. This last feature has been very important for the making of this section, as it allows us to have [three.js](https://github.com/mrdoob/three.js) bindings that enable fast 3D rendering in the web browser (the interface in question being similar to how you can use compiled *C* code through libraries like *numpy*). The use of said APIs along with some homemade assets built with Blender (all source files in the repo) have made this project possible. + +Only external asset used is the free [HDRi image 'Lebombo' by Greg Zaal (CC0)](https://polyhaven.com/a/lebombo), thank you Greg!! + +## Dev guide + +The easiest way to test the website locally is just to run a basic HTTP server. You can do that in Python by running the following in the directory that contains this file: + +``` +python3 -m http.server +``` +If you run the project locally you might also encounter issues with CORS permissions as the page is intended to access an external, global repository. To substitute said repo with a local placeholder for testing purposes, you can set the `USE_LOCALHOST` global to `True` on `./main.py`. diff --git a/heavenly-hostas/packages/gallery/assets/css/style.css b/heavenly-hostas/packages/gallery/assets/css/style.css new file mode 100644 index 00000000..44890bc4 --- /dev/null +++ b/heavenly-hostas/packages/gallery/assets/css/style.css @@ -0,0 +1,78 @@ + +#loading { + outline: none; + border: none; + background: transparent; +} + +#help-menu { + outline: 5px solid gray; + border: none; + background: white; + width: 50%; + box-shadow: 10cm; +} + +#instructions { + position: absolute; + top: 14%; + left: 50%; + transform: translate(-50%, -50%); /* perfectly center horizontally & vertically */ + max-width: 80%; /* optional, keeps text from stretching too wide */ + text-align: center; + color: black; + font-size: 28px; + font-family: sans-serif; + font-weight: bold; + background-color: white; /* highlight effect */ + padding: 12px 20px; /* padding inside box */ + border-radius: 8px; /* rounded corners */ + box-shadow: 0 4px 8px rgba(0,0,0,0.2); /* subtle shadow for depth */ + display: none; + cursor: pointer; +} + + +#editor { + position: absolute; + top: 20%; + left: 50%; + transform: translate(-50%, -50%); /* perfectly center horizontally & vertically */ + max-width: 80%; /* optional, keeps text from stretching too wide */ + text-align: center; + color: black; + font-size: 28px; + font-family: sans-serif; + font-weight: bold; + background-color: white; /* highlight effect */ + padding: 12px 20px; /* padding inside box */ + border-radius: 8px; /* rounded corners */ + box-shadow: 0 4px 8px rgba(0,0,0,0.2); /* subtle shadow for depth */ + display: none; +} + +li { + font-size: 24px; + line-height: 1.5; + margin-bottom: 8px; +} + +button { + font-size: 16px; + line-height: 1.5; + margin-bottom: 8px; + padding: 12px 20px; + border: none; + border-radius: 8px; + background-color: #007bff; + color: white; + cursor: pointer; +} + +#loading_tips { + font-size: 20px; +} + +body { + margin: 0; +} diff --git a/heavenly-hostas/packages/gallery/assets/editor.html b/heavenly-hostas/packages/gallery/assets/editor.html new file mode 100644 index 00000000..5e4ed029 --- /dev/null +++ b/heavenly-hostas/packages/gallery/assets/editor.html @@ -0,0 +1,17 @@ + + + + + + + + + Page Redirection + + + + If you are not redirected automatically, follow this link to https://cj12.matiiss.com/editor. + + diff --git a/heavenly-hostas/packages/gallery/assets/gallery.blend b/heavenly-hostas/packages/gallery/assets/gallery.blend new file mode 100644 index 00000000..535cef2c Binary files /dev/null and b/heavenly-hostas/packages/gallery/assets/gallery.blend differ diff --git a/heavenly-hostas/packages/gallery/assets/gallery_0.glb b/heavenly-hostas/packages/gallery/assets/gallery_0.glb new file mode 100644 index 00000000..5fdbd8f2 Binary files /dev/null and b/heavenly-hostas/packages/gallery/assets/gallery_0.glb differ diff --git a/heavenly-hostas/packages/gallery/assets/gallery_1.glb b/heavenly-hostas/packages/gallery/assets/gallery_1.glb new file mode 100644 index 00000000..ca1f3a04 Binary files /dev/null and b/heavenly-hostas/packages/gallery/assets/gallery_1.glb differ diff --git a/heavenly-hostas/packages/gallery/assets/gallery_2c.glb b/heavenly-hostas/packages/gallery/assets/gallery_2c.glb new file mode 100644 index 00000000..56a78926 Binary files /dev/null and b/heavenly-hostas/packages/gallery/assets/gallery_2c.glb differ diff --git a/heavenly-hostas/packages/gallery/assets/gallery_2s.glb b/heavenly-hostas/packages/gallery/assets/gallery_2s.glb new file mode 100644 index 00000000..ad5e2d38 Binary files /dev/null and b/heavenly-hostas/packages/gallery/assets/gallery_2s.glb differ diff --git a/heavenly-hostas/packages/gallery/assets/gallery_3.glb b/heavenly-hostas/packages/gallery/assets/gallery_3.glb new file mode 100644 index 00000000..c3249279 Binary files /dev/null and b/heavenly-hostas/packages/gallery/assets/gallery_3.glb differ diff --git a/heavenly-hostas/packages/gallery/assets/gallery_4.glb b/heavenly-hostas/packages/gallery/assets/gallery_4.glb new file mode 100644 index 00000000..7cd9a7fc Binary files /dev/null and b/heavenly-hostas/packages/gallery/assets/gallery_4.glb differ diff --git a/heavenly-hostas/packages/gallery/assets/images/test-image-nobg.png b/heavenly-hostas/packages/gallery/assets/images/test-image-nobg.png new file mode 100644 index 00000000..9d9f5cc5 Binary files /dev/null and b/heavenly-hostas/packages/gallery/assets/images/test-image-nobg.png differ diff --git a/heavenly-hostas/packages/gallery/assets/images/test-image.webp b/heavenly-hostas/packages/gallery/assets/images/test-image.webp new file mode 100644 index 00000000..ab22bb66 Binary files /dev/null and b/heavenly-hostas/packages/gallery/assets/images/test-image.webp differ diff --git a/heavenly-hostas/packages/gallery/assets/images/tree-test-image.avif b/heavenly-hostas/packages/gallery/assets/images/tree-test-image.avif new file mode 100644 index 00000000..f10ca8d7 Binary files /dev/null and b/heavenly-hostas/packages/gallery/assets/images/tree-test-image.avif differ diff --git a/heavenly-hostas/packages/gallery/assets/lebombo_1k.hdr b/heavenly-hostas/packages/gallery/assets/lebombo_1k.hdr new file mode 100644 index 00000000..3aee1937 Binary files /dev/null and b/heavenly-hostas/packages/gallery/assets/lebombo_1k.hdr differ diff --git a/heavenly-hostas/packages/gallery/assets/map.txt b/heavenly-hostas/packages/gallery/assets/map.txt new file mode 100644 index 00000000..c41ff1b4 --- /dev/null +++ b/heavenly-hostas/packages/gallery/assets/map.txt @@ -0,0 +1,15 @@ +x - x - x - x - x - x - x - x +| | | | +x - x x x x - x - x +| | | | | +x x x x - x - x +| | | | +x - x - x - x - x - x - x - x +| | | | +x - x - x x x - x - x +| | | +x - x x x +| | | | +x x x x x - x - x +| | | | | | +x - x - x - x - x - x - x - x diff --git a/heavenly-hostas/packages/gallery/assets/test-image-listing.json b/heavenly-hostas/packages/gallery/assets/test-image-listing.json new file mode 100644 index 00000000..79dc5e45 --- /dev/null +++ b/heavenly-hostas/packages/gallery/assets/test-image-listing.json @@ -0,0 +1,387 @@ +{"artworks":[ + ["a", "test-image.webp"], + ["a", "tree-test-image.avif"], + ["a", "test-image-nobg.png"], + ["a", "tree-test-image.avif"], + ["a", "test-image.webp"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "test-image.webp"], + ["a", "test-image.webp"], + ["a", "tree-test-image.avif"], + ["a", "test-image-nobg.png"], + ["a", "tree-test-image.avif"], + ["a", "test-image.webp"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "test-image.webp"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "test-image.webp"], + ["a", "test-image.webp"], + ["a", "tree-test-image.avif"], + ["a", "test-image-nobg.png"], + ["a", "tree-test-image.avif"], + ["a", "test-image.webp"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "test-image.webp"], + ["a", "test-image.webp"], + ["a", "tree-test-image.avif"], + ["a", "test-image-nobg.png"], + ["a", "tree-test-image.avif"], + ["a", "test-image.webp"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "test-image.webp"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "test-image.webp"], + ["a", "test-image.webp"], + ["a", "tree-test-image.avif"], + ["a", "test-image-nobg.png"], + ["a", "tree-test-image.avif"], + ["a", "test-image.webp"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "test-image.webp"], + ["a", "test-image.webp"], + ["a", "tree-test-image.avif"], + ["a", "test-image-nobg.png"], + ["a", "tree-test-image.avif"], + ["a", "test-image.webp"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "test-image.webp"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "test-image.webp"], + ["a", "test-image.webp"], + ["a", "tree-test-image.avif"], + ["a", "test-image-nobg.png"], + ["a", "tree-test-image.avif"], + ["a", "test-image.webp"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "test-image.webp"], + ["a", "test-image.webp"], + ["a", "tree-test-image.avif"], + ["a", "test-image-nobg.png"], + ["a", "tree-test-image.avif"], + ["a", "test-image.webp"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "test-image.webp"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "test-image.webp"], + ["a", "test-image.webp"], + ["a", "tree-test-image.avif"], + ["a", "test-image-nobg.png"], + ["a", "tree-test-image.avif"], + ["a", "test-image.webp"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "test-image.webp"], + ["a", "test-image.webp"], + ["a", "tree-test-image.avif"], + ["a", "test-image-nobg.png"], + ["a", "tree-test-image.avif"], + ["a", "test-image.webp"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "test-image.webp"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "test-image.webp"], + ["a", "test-image.webp"], + ["a", "tree-test-image.avif"], + ["a", "test-image-nobg.png"], + ["a", "tree-test-image.avif"], + ["a", "test-image.webp"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "test-image.webp"], + ["a", "test-image.webp"], + ["a", "tree-test-image.avif"], + ["a", "test-image-nobg.png"], + ["a", "tree-test-image.avif"], + ["a", "test-image.webp"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "test-image.webp"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "test-image.webp"], + ["a", "test-image.webp"], + ["a", "tree-test-image.avif"], + ["a", "test-image-nobg.png"], + ["a", "tree-test-image.avif"], + ["a", "test-image.webp"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "test-image.webp"], + ["a", "test-image.webp"], + ["a", "tree-test-image.avif"], + ["a", "test-image-nobg.png"], + ["a", "tree-test-image.avif"], + ["a", "test-image.webp"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "test-image.webp"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "test-image.webp"], + ["a", "test-image.webp"], + ["a", "tree-test-image.avif"], + ["a", "test-image-nobg.png"], + ["a", "tree-test-image.avif"], + ["a", "test-image.webp"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "test-image.webp"], + ["a", "test-image.webp"], + ["a", "tree-test-image.avif"], + ["a", "test-image-nobg.png"], + ["a", "tree-test-image.avif"], + ["a", "test-image.webp"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "test-image.webp"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "test-image.webp"], + ["a", "test-image.webp"], + ["a", "tree-test-image.avif"], + ["a", "test-image-nobg.png"], + ["a", "tree-test-image.avif"], + ["a", "test-image.webp"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "test-image.webp"], + ["a", "test-image.webp"], + ["a", "tree-test-image.avif"], + ["a", "test-image-nobg.png"], + ["a", "tree-test-image.avif"], + ["a", "test-image.webp"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "test-image.webp"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "test-image.webp"], + ["a", "test-image.webp"], + ["a", "tree-test-image.avif"], + ["a", "test-image-nobg.png"], + ["a", "tree-test-image.avif"], + ["a", "test-image.webp"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "test-image.webp"], + ["a", "test-image.webp"], + ["a", "tree-test-image.avif"], + ["a", "test-image-nobg.png"], + ["a", "tree-test-image.avif"], + ["a", "test-image.webp"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "test-image.webp"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "test-image.webp"], + ["a", "test-image.webp"], + ["a", "tree-test-image.avif"], + ["a", "test-image-nobg.png"], + ["a", "tree-test-image.avif"], + ["a", "test-image.webp"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "test-image.webp"], + ["a", "test-image.webp"], + ["a", "tree-test-image.avif"], + ["a", "test-image-nobg.png"], + ["a", "tree-test-image.avif"], + ["a", "test-image.webp"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "test-image.webp"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "tree-test-image.avif"], + ["a", "test-image.webp"] +]} diff --git a/heavenly-hostas/packages/gallery/assets/tips.txt b/heavenly-hostas/packages/gallery/assets/tips.txt new file mode 100644 index 00000000..abbe35d2 --- /dev/null +++ b/heavenly-hostas/packages/gallery/assets/tips.txt @@ -0,0 +1,4 @@ +This is just a list of "tips" we can display to the user with the loading scene + +1) "z" toggles run +2) "shift" is used to go down diff --git a/heavenly-hostas/packages/gallery/favicon.ico b/heavenly-hostas/packages/gallery/favicon.ico new file mode 100644 index 00000000..e69de29b diff --git a/heavenly-hostas/packages/gallery/index.html b/heavenly-hostas/packages/gallery/index.html new file mode 100644 index 00000000..5fb4fe0d --- /dev/null +++ b/heavenly-hostas/packages/gallery/index.html @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + Image Gallery + + + + + + +

Loading...

+

+
+ + +

+

HELP MENU

+
    +
  1. WASD or arrow keys to move forward, left, backward and right respectively.
  2. +
  3. Z and Ctrl toggle running
  4. +
  5. Space to up and Shift to go down
  6. +
  7. H to open help menu
  8. +
+

+ + +
+ +
Click here to look and move around!
+
Go to editor!
+ + + + + + diff --git a/heavenly-hostas/packages/gallery/main.py b/heavenly-hostas/packages/gallery/main.py new file mode 100644 index 00000000..7a54232d --- /dev/null +++ b/heavenly-hostas/packages/gallery/main.py @@ -0,0 +1,746 @@ +# -------------------------------------- IMPORTS -------------------------------------- +import asyncio +import json +import warnings +from collections import defaultdict + +# Typing +from collections.abc import Callable +from enum import Enum +from typing import Any + +from js import ( # pyright: ignore[reportMissingImports] + THREE, + GLTFLoader, + Math, + Object, + PointerLockControls, + RGBELoader, + console, +) + +# Local +from map_loader import MAP, ROOM_TYPES, get_gallery_room, get_map_layout +from pyodide.ffi import create_proxy, to_js # pyright: ignore[reportMissingImports] +from pyodide.http import pyfetch # pyright: ignore[reportMissingImports] +from pyscript import document, when, window # pyright: ignore[reportMissingImports] + +# -------------------------------------- GLOBAL VARIABLES -------------------------------------- +USE_LOCALHOST = False +print("GLOBAL VARIABLES") + +# Renderer set up +RENDERER = THREE.WebGLRenderer.new({"antialias": False}) +RENDERER.shadowMap.enabled = False +RENDERER.shadowMap.type = THREE.PCFSoftShadowMap +RENDERER.shadowMap.needsUpdate = True +RENDERER.setSize(window.innerWidth, window.innerHeight) +document.body.appendChild(RENDERER.domElement) + +# Scene setup +setcolor = "#8B8B8B" # Nicer than just black +SCENE = THREE.Scene.new() +SCENE.background = THREE.Color.new(setcolor) + +# Camera setup +CAMERA = THREE.PerspectiveCamera.new(53, window.innerWidth / window.innerHeight, 0.01, 500) +CAMERA.position.set(3, 1, 3.5) +CAMERA.rotation.set(0, 0.4, 0) +SCENE.add(CAMERA) + + +# Building blocks for the rooms, will be filled later +GALLERY_BLOCKS: dict[ROOM_TYPES, THREE.Group] = {} + + +# Picture group to know which paintings have been loaded +PICTURES: THREE.Group = THREE.Group.new() +PICTURES.name = "Picture_group" +SCENE.add(PICTURES) + + +# Other global variables +ROOMS: list[THREE.Group] = [] # a list of all rooms in the scene +PAINTINGS: list[THREE.Object3D] = [] # a list of all the paintings in the scene +LOADED_ROOMS: list[THREE.Group] = [] # a list of all the rooms that are currently loaded +IMAGES_LIST: list[str] = [] # a list of the names of the paintings that have to be loaded in order +LOADED_SLOTS: list[int] = [] # a list of all slots that have been loaded + +# Related to Moving +RUN_STATE: bool = False # to toggle running +CAN_MOVE: bool = False # so that the player cant move unless he is "locked in" + +# distance which we maintain from walls +OFFSET = 0.2 + +VELOCITY = THREE.Vector3.new() + +if USE_LOCALHOST: + REPO_URL = ( + r"https://cdn.jsdelivr.net/gh/" + r"Matiiss/pydis-cj12-heavenly-hostas@dev/" + r"packages/gallery/assets/images/" + ) +else: + REPO_URL = ( + r"https://cdn.jsdelivr.net/gh/" + r"heavenly-hostas-hosting/HHH@data/" + ) + + +# For Type Hinting +V3 = tuple[float, float, float] +V2 = tuple[float, float] + + +# -------------------------------------- HELPER FUNCTIONS -------------------------------------- + + +def tree_print(x, indent=0): + # Prints a 3D model's tree structure + qu = '"' + output = " " * 4 * indent + f"{x.name or qu * 2} ({x.type})" + for i in x.children: + output += "\n" + tree_print(i, indent=indent + 1) + return output + + +def convert_dict_to_js_object(my_dict: dict): + """Convert a Python dict to a JavaScript object.""" + return Object.fromEntries(to_js(my_dict)) + + +def mathRandom(num=1): + setNumber = -Math.random() * num + Math.random() * num + return setNumber + + +def get_painting_info(p: THREE.Mesh) -> tuple[V3, V3, V2]: + v_pos = THREE.Vector3.new() + p.getWorldPosition(v_pos) + position: V3 = v_pos.x, v_pos.y, v_pos.z + + # Beware possible Y-up/Z-up shenanigans + n_array = p.geometry.attributes.normal.array + world_matrix = p.matrixWorld + normal_matrix = THREE.Matrix3.new().getNormalMatrix(world_matrix) + world_normal = ( + THREE.Vector3.new( + -n_array[2], + n_array[1], + n_array[0], + ) + .applyMatrix3(normal_matrix) + .normalize() + ) + normal = world_normal.x, world_normal.y, world_normal.z + + bb = p.geometry.boundingBox + size_xyz = [abs(getattr(bb.max, i) - getattr(bb.min, i)) for i in ("x", "y", "z")] + # Height is Z + size_wh: V2 = (max(size_xyz[0], size_xyz[1]), (size_xyz[2])) + + return (position, normal, size_wh) + + +def get_player_chunk(room_apothem: float) -> tuple[int, int]: + x_coord = round((CAMERA.position.x) / (room_apothem * 2)) + z_coord = round((CAMERA.position.z) / (room_apothem * 2)) + + if x_coord < 0: + x_coord = 0 + if z_coord < 0: + z_coord = 0 + + return x_coord, z_coord + + +# -------------------------------------- MOVEMENT CONTROLS -------------------------------------- + +# Movement Controls +INPUTS = Enum("INPUTS", ["FORW", "LEFT", "RIGHT", "BACK", "UP", "DOWN", "RUN"]) +KEY_MAPPINGS: dict[INPUTS, set[str]] = { + INPUTS.FORW: {"KeyW", "ArrowUp"}, + INPUTS.LEFT: {"KeyA", "ArrowLeft"}, + INPUTS.BACK: {"KeyS", "ArrowDown"}, + INPUTS.RIGHT: {"KeyD", "ArrowRight"}, + INPUTS.UP: {"Space"}, + INPUTS.DOWN: {"ShiftLeft", "ShiftRight"}, + # + INPUTS.RUN: {"KeyZ", "ControlLeft", "ControlRight"}, +} +KEY_STATES: dict[str, bool] = defaultdict(bool) +document.addEventListener("keydown", create_proxy(lambda x: KEY_STATES.__setitem__(x.code, True))) +document.addEventListener("keyup", create_proxy(lambda x: KEY_STATES.__setitem__(x.code, False))) + + +def toggle_run(event): + global RUN_STATE + if event.code in KEY_MAPPINGS[INPUTS.RUN]: + RUN_STATE = not RUN_STATE + if event.key == "h": + openHelpMenu() + + +document.addEventListener("keydown", create_proxy(toggle_run)) + + +# Main move function +def move_character(delta_time: float) -> THREE.Vector3: # noqa: C901 + if not CAN_MOVE: + return THREE.Vector3.new(0, 0, 0) + pressed_keys = {k for k, v in KEY_MAPPINGS.items() if any(KEY_STATES[i] for i in v)} + damping = 8 + if RUN_STATE: + acceleration = 25 * 3 + max_speed = 50 * 3 + + CAMERA.fov = min(CAMERA.fov + 60 * delta_time, 60) + else: + acceleration = 10 * 3 + max_speed = 20 * 3 + + CAMERA.fov = max(CAMERA.fov - 60 * delta_time, 53) + CAMERA.updateProjectionMatrix() + + move = THREE.Vector3.new() + if INPUTS.FORW in pressed_keys: + move.z -= 1 + if INPUTS.BACK in pressed_keys: + move.z += 1 + + if INPUTS.LEFT in pressed_keys: + move.x -= 1 + if INPUTS.RIGHT in pressed_keys: + move.x += 1 + + if INPUTS.UP in pressed_keys: + move.y += 1 + if INPUTS.DOWN in pressed_keys: + move.y -= 1 + + if move.length() > 0: + q = CAMERA.quaternion + yaw = Math.atan2(2 * (q.w * q.y + q.x * q.z), 1 - 2 * (q.y * q.y + q.z * q.z)) + yaw_q = THREE.Quaternion.new() + yaw_q.setFromAxisAngle(THREE.Vector3.new(0, 1, 0), yaw) + + move.applyQuaternion(yaw_q).normalize() + VELOCITY.addScaledVector(move, acceleration * delta_time) + + if VELOCITY.length() > max_speed: + VELOCITY.setLength(max_speed) + + VELOCITY.multiplyScalar(1 - min(damping * delta_time, 1)) + return VELOCITY + + +# -------------------------------------- MOUSE CONTROLS -------------------------------------- + +MOUSE = THREE.Vector2.new() + + +# Mouse Lock Functions +def cam_lock(e): + global CAN_MOVE + setattr( + document.getElementById("instructions").style, + "display", + "none", + ) + setattr( + document.getElementById("editor").style, + "display", + "none", + ) + + CAN_MOVE = True + + +def cam_unlock(e): + global CAN_MOVE + setattr( + document.getElementById("instructions").style, + "display", + "block", + ) + setattr( + document.getElementById("editor").style, + "display", + "block", + ) + CAN_MOVE = False + + +# Mouse Lock +CONTROLS = PointerLockControls.new(CAMERA, document.body) +document.getElementById("instructions").addEventListener("click", create_proxy(CONTROLS.lock)) +CONTROLS.addEventListener( + "lock", + create_proxy(cam_lock), +) +CONTROLS.addEventListener("unlock", create_proxy(cam_unlock)) + + +# Mouse Controls +@when("mousemove", "body") +def onMouseMove(event): + event.preventDefault() + MOUSE.x = (event.clientX / window.innerWidth) * 2 - 1 + MOUSE.y = -(event.clientY / window.innerHeight) * 2 + 1 + + +# -------------------------------------- COLLISION DETECTION -------------------------------------- + + +async def check_collision(velocity: THREE.Vector3, delta_time: float) -> bool: + """ + Checks for collision with walls (cubes) and triggers + returns true if it is safe to move and false if movement should be stopped + """ + raycaster = THREE.Raycaster.new() + direction = velocity.clone().normalize() + raycaster.set(CAMERA.position, direction) + + await check_collision_with_trigger(velocity, delta_time, raycaster) + + return check_collision_with_wall(velocity, delta_time, raycaster) + + +def check_collision_with_wall(velocity: THREE.Vector3, delta_time: float, raycaster: THREE.Raycaster) -> bool: + cubes = [] + [cubes.extend(c.getObjectByName("Cubes").children) for c in ROOMS] + intersections = raycaster.intersectObjects(cubes, recursive=True) + if not intersections: + return True + return intersections[0].distance > velocity.length() * delta_time + OFFSET + + +async def check_collision_with_trigger(velocity: THREE.Vector3, delta_time: float, raycaster: THREE.Raycaster): + triggers = [] + [triggers.extend(c.getObjectByName("Triggers").children) for c in ROOMS] + + intersections = raycaster.intersectObjects(triggers, recursive=True) + if intersections and intersections[0].distance <= velocity.length() * delta_time: + room = intersections[0].object.parent.parent + asyncio.ensure_future(updated_loaded_rooms(room)) + + +# -------------------------------------- HELP MENU -------------------------------------- + + +def closeHelpMenu(e=None): + help_menu = document.getElementById("help-menu") + help_menu.close() + + instructions = document.getElementById("instructions") + instructions.style.display = "block" + + editor = document.getElementById("editor") + editor.style.display = "block" + + +def openHelpMenu(e=None): + CONTROLS.unlock() + + help_menu = document.getElementById("help-menu") + help_menu.showModal() + + instructions = document.getElementById("instructions") + instructions.style.display = "none" + + editor = document.getElementById("editor") + editor.style.display = "none" + + +document.getElementById("close-help-menu").addEventListener("click", create_proxy(closeHelpMenu)) + + +# -------------------------------------- ROOM CREATION -------------------------------------- + + +def room_objects_handling(room: THREE.Group) -> None: + assert room.children[0].name.startswith("Cube") + room.children[0].name = "Cubes" + for v in room.children[0].children: + v.material.side = THREE.FrontSide + + triggers = THREE.Group.new() + triggers.name = "Triggers" + + pictures = THREE.Group.new() + pictures.name = "Pictures" + + for v in room.children[1:]: + if v.name.startswith("trigger"): + room.remove(v) + triggers.add(v) + v.visible = False + + if v.name.startswith("pic"): + room.remove(v) + pictures.add(v) + v.visible = False + + room.add(triggers) + room.add(pictures) + + +def load_image(slot: int): + if slot >= len(PAINTINGS): + warnings.warn( + f"WARNING: slot to be accessed '{slot}' is greater than the maximum available " + f"one '{len(PAINTINGS) - 1}'. The image will not be loaded." + ) + + if slot >= len(IMAGES_LIST): + # this slot does not have a corresponding painting yet + return + + image_loc = IMAGES_LIST[slot] + textureLoader = THREE.TextureLoader.new() + + def inner_loader(loaded_obj): + # Put texture on a plane + perms = convert_dict_to_js_object( + { + "map": loaded_obj, + "transparent": True, + } + ) + geometry = THREE.PlaneGeometry.new(1, 1, 1) + material = THREE.MeshBasicMaterial.new(perms) + plane = THREE.Mesh.new(geometry, material) + plane.scale.x = 1.414 + + # Snap the plane to its slot + (x, y, z), (nx, ny, nz), (w, h) = get_painting_info(PAINTINGS[slot]) + plane.position.set(x, y, z) + + q = THREE.Quaternion.new() + q.setFromUnitVectors(THREE.Vector3.new(-1, 0, 0), THREE.Vector3.new(nx, ny, nz)) + plane.quaternion.copy(q) + + # Add the plane to the scene + plane.name = f"picture_{PAINTINGS[slot].parent.parent.name[5:]}_{slot:03d}" + PICTURES.add(plane) + LOADED_SLOTS.append(slot) + + try: + textureLoader.load( + REPO_URL + image_loc, + create_proxy(inner_loader), + None, + create_proxy(lambda _: None), + ) + except Exception as e: + console.error(e) + + +async def load_images_from_listing() -> int: + if USE_LOCALHOST: + r = await pyfetch("./assets/test-image-listing.json") + else: + r = await pyfetch("https://cj12.matiiss.com/api/artworks") + data = await r.text() + n_existing_images = len(IMAGES_LIST) + for username, img in json.loads(data)["artworks"][n_existing_images:]: + IMAGES_LIST.append(img) + + n_added_images = len(IMAGES_LIST) - n_existing_images + + return n_added_images + + +def create_room( + chunk_coords: tuple[int, int], + room_apothem: float, + room_type: ROOM_TYPES, + rotation: float = 0, +) -> None: + """ + chunk_coords represent the coordinates of the room + room_apothem is the perp distance from the center of the room to its edges + room_type represents the type of the room + rotation represents the rotation of the room, which is supposed to be multiples of pi/2 + """ + + room = GALLERY_BLOCKS[room_type].clone() + room.name = f"room_{chunk_coords[0]}_{chunk_coords[1]}" + ROOMS.append(room) + + position = (chunk_coords[0] * room_apothem * 2, 0, chunk_coords[1] * room_apothem * 2) + room.rotation.y = rotation + room.position.set(*position) + + # Add its children to a global list of paintings + for i in room.getObjectByName("Pictures").children: + i.name = f"pic_{len(PAINTINGS):03d}" + PAINTINGS.append(i) + + SCENE.add(room) + + +async def clone_rooms(chunks: list[tuple[int, int]], layout: MAP, apothem: float): + for x, y in chunks: + room, rotation = get_gallery_room(x, y, layout) + create_room((x, y), apothem, room, rotation) + + +# -------------------------------------- LAZY LOADING -------------------------------------- + + +async def load_room(room: THREE.Group) -> None: + """Loads a room and/or makes it visible.""" + paintings = room.getObjectByName("Pictures") + + if any(p.name.startswith(f"picture_{room.name[5:]}") for p in PICTURES.children): + # Checks whther the room has any "photoframes" in it, if yes then it has been previously loaded and we just + # need to set it to be visible + + for p in PICTURES.children: + if p.name.startswith(f"picture_{room.name[5:]}"): + p.visible = True + else: + # This is the first time we are loading this room so we need to load its paintings too + for p in paintings.children: + if p.name.startswith("pic_"): + slot = int(p.name.split("_")[1]) + if slot < len(IMAGES_LIST): + load_image(slot) + + +async def unload_room(room: THREE.Group) -> None: + """Makes the paintings invisible""" + for p in PICTURES.children: + if p.name.startswith(f"picture_{room.name[5:]}"): + p.visible = False + + +async def updated_loaded_rooms( + current_room: THREE.Group, + force_reload: bool = False, + r: int = 2, +) -> None: + """Loads all rooms which are at some r distance from the current room""" + + def get_chunk_coords(room): + return tuple(int(i) for i in room.name.split("_")[1:]) + + def calc(room): + return ( + sum( + abs(i - j) + for i, j in zip( + get_chunk_coords(current_room), + get_chunk_coords(room), + strict=True, + ) + ) + <= r + ) + + for room in ROOMS: + if room in LOADED_ROOMS: + if not calc(room): + await unload_room(room) + LOADED_ROOMS.remove(room) + elif force_reload: + await load_room(room) + else: + if calc(room): + await load_room(room) + LOADED_ROOMS.append(room) + + +# -------------------------------------- GALLERY LOADING -------------------------------------- + + +def generate_global_lights(): + # Global lighting + ambient_light = THREE.AmbientLight.new(0xFF_FF_FF, 0.5) + # Lighting for floors + hemi_light = THREE.HemisphereLight.new(0xFF_FF_FF, 0x44_44_44, 0.2) + hemi_light = THREE.HemisphereLight.new(0x0, 0xFF_FF_FF, 0.3) + # Adds some depth + main_light = THREE.DirectionalLight.new(0xFF_FF_FF, 1.2) + main_light.position.set(10, 20, 10) + main_light.castShadow = True + + SCENE.add(ambient_light) + SCENE.add(hemi_light) + SCENE.add(main_light) + + # Sexy reflections + loader = RGBELoader.new() + + def inner_loader(loaded_obj, *_): + pmrem = THREE.PMREMGenerator.new(RENDERER) + env_map = pmrem.fromEquirectangular(loaded_obj).texture + SCENE.environment = env_map + loaded_obj.dispose() + pmrem.dispose() + + loader.load( + "./assets/lebombo_1k.hdr", + create_proxy(inner_loader), + ) + + +async def load_gallery_blocks() -> None: + loader = GLTFLoader.new() + + # Needs to do it this way or python will reference the same 'i' + def inner_loader_factory(i: ROOM_TYPES) -> Callable[[Any], None]: + def inner_loader(loaded_obj): + room = loaded_obj.scene + # Backface culling, invisible objects, etc. + room_objects_handling(room) + GALLERY_BLOCKS[i] = room + + return inner_loader + + def inner_progress(xhr): + print(str(xhr.loaded) + " loaded") + + def inner_error(error): + print(f"error: {error}") + + inner_progress_proxy = create_proxy(inner_progress) + inner_error_proxy = create_proxy(inner_error) + + for i in ROOM_TYPES: + inner_loader = inner_loader_factory(i) + + loader.load( + f"./assets/gallery_{i.value}.glb", + create_proxy(inner_loader), + inner_progress_proxy, + inner_error_proxy, + ) + + # Ensure they are loaded + while True: + for i in ROOM_TYPES: + if i not in GALLERY_BLOCKS: + await asyncio.sleep(0.02) + break + else: + break + + +def get_room_apothem() -> float: + # Get the corner room, estimate distance from center + room = GALLERY_BLOCKS[ROOM_TYPES._2c] + assert room.children[0].name.startswith("Cube") + # Centers + cx, cz = room.position.x, room.position.z + + triggers: list[THREE.Group] = [i for i in room.getObjectByName("Triggers").children] + trigger_centers: list[tuple[float, float]] = [(i.position.x, i.position.z) for i in triggers] + + apothems = [max(abs(cx - ix), abs(cz - iz)) for ix, iz in trigger_centers] + output = sum(apothems) / len(apothems) + return output + + +async def load_gallery() -> None: + _, layout = await asyncio.gather( + load_gallery_blocks(), + get_map_layout(), + ) + apothem = get_room_apothem() + # Get all layout points, sorted by Hamiltonian distance from (0, 0) + layout_points = sorted( + [(x, y) for y in range(len(layout)) for x in range(len(layout)) if layout[y][x] is not None], + key=lambda p: abs(p[0]) + abs(p[1]), + ) + await clone_rooms(layout_points, layout, apothem) + + +async def image_query_loop(): + apothem = get_room_apothem() + while True: + await asyncio.sleep(15) + + # Might error out because of downtime, no big deal + try: + n_added_images = await load_images_from_listing() + if n_added_images: + chunk_x, chunk_z = get_player_chunk(apothem) + print(f"New images to be added: {n_added_images}") + await updated_loaded_rooms( + SCENE.getObjectByName(f"room_{chunk_x}_{chunk_z}"), + force_reload=True, + r=3, # A slightly bigger radius, just in case + ) + except Exception: + ... + + +def tp_to_slot(slot: int) -> None: + print(f"Going to image on index {slot}...") + try: + painting = PAINTINGS[slot] + except IndexError: + print("Invalid index to tp camera to") + return + (x, y, z), (nx, ny, nz), (w, h) = get_painting_info(painting) + pos = THREE.Vector3.new(x, y, z) + + CAMERA.position.copy(pos) + apothem = get_room_apothem() + chunk_x, chunk_z = get_player_chunk(apothem) + CAMERA.position.set(chunk_x * apothem * 2, CAMERA.position.y, chunk_z * apothem * 2) + + +def url_process() -> None: + params = window.URLSearchParams.new(window.location.search) + + idx_raw = params.get("idx") + picture = params.get("picture") + if idx_raw is not None: + idx = int(idx_raw) + elif picture is not None: + try: + idx = IMAGES_LIST.index(picture) + print(f"Image with name {picture} found") + except ValueError: + print(f"Image with name {picture} not found") + return + else: + return + + tp_to_slot(idx) + + +async def main(): + await load_images_from_listing() + + while not SCENE.getObjectByName("room_0_0"): + await asyncio.sleep(0.05) + + asyncio.ensure_future(updated_loaded_rooms(SCENE.getObjectByName("room_0_0"))) + + asyncio.ensure_future(image_query_loop()) + + # TP camera + url_process() + + clock = THREE.Clock.new() + while True: + delta = clock.getDelta() + velocity = move_character(delta) + if velocity == THREE.Vector3.new(0, 0, 0): + continue + if await check_collision(velocity, delta): + CAMERA.position.addScaledVector(velocity, delta) + + RENDERER.render(SCENE, CAMERA) + await asyncio.sleep(0.02) + + +if __name__ == "__main__": + asyncio.ensure_future(load_gallery()) + generate_global_lights() + asyncio.ensure_future(main()) diff --git a/heavenly-hostas/packages/gallery/map_loader.py b/heavenly-hostas/packages/gallery/map_loader.py new file mode 100644 index 00000000..bf62d212 --- /dev/null +++ b/heavenly-hostas/packages/gallery/map_loader.py @@ -0,0 +1,113 @@ +from enum import Enum +from math import pi + +from pyodide.http import pyfetch # pyright: ignore[reportMissingImports] + +__all__ = [ + "NODE", + "MAP", + "get_map_layout", + # + "ROOM_TYPES", + "get_gallery_room", +] + +NODE = tuple[bool, bool, bool, bool] +MAP = list[list[NODE | None]] + + +async def get_map_layout() -> MAP: + r = await pyfetch("./assets/map.txt") + text = await r.text() + data = [i for i in text.split("\n") if i] + + # (x, y) = (0, 0) is top left corner + output: MAP = [[] for _ in range(0, len(data), 2)] + + for y in range(0, len(data), 2): + for x in range(0, len(data[y]), 4): + if data[y][x] != "x": + output[y // 2].append(None) + continue + + north = False + if (y - 1 > 0) and (data[y - 1][x] == "|"): + north = True + + east = False + if (x + 2 < len(data[y])) and (data[y][x + 2] == "-"): + east = True + + south = False + if (y + 1 < len(data)) and (data[y + 1][x] == "|"): + south = True + + west = False + if (x - 2 > 0) and (data[y][x - 2] == "-"): + west = True + + output[y // 2].append((north, east, south, west)) + + return output + + +class ROOM_TYPES(Enum): + _1 = "1" + _2s = "2s" + _2c = "2c" + _3 = "3" + _4 = "4" + + +def get_gallery_room( # noqa: C901 + x: int, + y: int, + layout: MAP, +) -> tuple[ROOM_TYPES, float]: + node = layout[y][x] + assert node is not None + north, east, south, west = node + + match (north, east, south, west): + # 4 exits + case z if all(z): + return ROOM_TYPES._4, 0 + # no exit + case z if not any(z): + assert False + + # 1 exit + case z if sum(z) == 1: + for idx, i in enumerate(z): + if i: + break + else: + assert False + return ROOM_TYPES._1, (pi / 2 * ((-idx + 3) % 4)) + + # 2 exits + # straight + case (False, True, False, True): + return ROOM_TYPES._2s, 0 + case (True, False, True, False): + return ROOM_TYPES._2s, pi / 2 + # corner + case z if sum(z) == 2: + for idx, i in enumerate(z): + if i and z[idx - 1]: + break + else: + assert False + return ROOM_TYPES._2c, (pi / 2 * ((-idx + 0) % 4)) + + # 3 exits + case z if sum(z) == 3: + for idx, i in enumerate(z): + if not i: + break + else: + assert False + return ROOM_TYPES._3, (pi / 2 * ((-idx + 2) % 4)) + + case _: + assert False, "This one is serious" diff --git a/heavenly-hostas/packages/gallery/pyproject.toml b/heavenly-hostas/packages/gallery/pyproject.toml new file mode 100644 index 00000000..58768172 --- /dev/null +++ b/heavenly-hostas/packages/gallery/pyproject.toml @@ -0,0 +1,7 @@ +[project] +name = "gallery" +version = "0.1.0" +description = "An online Image Gallery rendered through Three.js and WebGL and programmed in PyScript." +readme = "README.md" +requires-python = ">=3.12" +dependencies = [] diff --git a/heavenly-hostas/packages/gallery/pyscript.toml b/heavenly-hostas/packages/gallery/pyscript.toml new file mode 100644 index 00000000..c9185a4e --- /dev/null +++ b/heavenly-hostas/packages/gallery/pyscript.toml @@ -0,0 +1,5 @@ +name = "Image Gallery" +description = "An online Image Gallery rendered through Three.js and WebGL and programmed in PyScript." +[[fetch]] +files = ["map_loader.py"] +from = '.' diff --git a/heavenly-hostas/pyproject.toml b/heavenly-hostas/pyproject.toml new file mode 100644 index 00000000..8d930fcc --- /dev/null +++ b/heavenly-hostas/pyproject.toml @@ -0,0 +1,62 @@ +[project] +name = "heavenly-hostas-hosting" +description = "A little app for the Python Discord Code Jam 12 from the Heavenly Hostas team." +authors = [ + { name = "Matiiss", email = "matiiss@matiiss.com" }, + { name = "PiLogic" }, + { name = "HiPeople21", email = "hipeople3.14159@gmail.com" }, + { name = "Sergio Díaz-Esparza" }, + { name = "Alan Manning" }, + { name = "Aadil Goyal", email = "aadilgoyal1@gmail.com" }, +] +version = "0.1.0" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [] + +[dependency-groups] +# This `dev` group contains all the development requirements for our linting toolchain. +dev = ["pre-commit~=4.2.0", "ruff~=0.12.2"] + +[tool.ruff] +line-length = 119 +target-version = "py312" # Target Python 3.12 +# Automatically fix auto-fixable issues. +fix = true +# The directory containing the source code. If you choose a different project layout +# you will need to update this value. +src = ["src"] # TODO: fix + +[tool.ruff.lint] +select = ["E", "F", "I", "C901"] + +# select = ["ALL"] +# Ignore some of the most obnoxious linting errors. +# ignore = [ +# # Missing docstrings. +# "D100", +# "D104", +# "D105", +# "D106", +# "D107", +# # Docstring whitespace. +# "D203", +# "D213", +# # Docstring punctuation. +# "D415", +# # Docstring quotes. +# "D301", +# # Builtins. +# "A", +# # Print statements. +# "T20", +# # TODOs. +# "TD002", +# "TD003", +# "FIX", +# # Namespace packages. +# "INP001", +# ] + +[tool.uv.workspace] +members = ["packages/backend", "packages/editor", "packages/gallery"] diff --git a/heavenly-hostas/set_up_supabase_volumes.sh b/heavenly-hostas/set_up_supabase_volumes.sh new file mode 100644 index 00000000..afac3cd4 --- /dev/null +++ b/heavenly-hostas/set_up_supabase_volumes.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +# inspired by https://supabase.com/docs/guides/self-hosting/docker#installing-and-running-supabase + +SB_DST=./supabase-repository +SHA=671aea0a4af7119131393e3bc2d187bc54c8604a + +git init $SB_DST +cd $SB_DST +git remote add origin https://github.com/supabase/supabase + +git sparse-checkout init --cone +git sparse-checkout set docker/volumes + +git fetch --filter=blob:none origin $SHA +git checkout $SHA + +cd .. + +cp -r $SB_DST/docker/volumes ./volumes +rm -rf $SB_DST diff --git a/heavenly-hostas/src/app/__init__.py b/heavenly-hostas/src/app/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/heavenly-hostas/uv.lock b/heavenly-hostas/uv.lock new file mode 100644 index 00000000..df307860 --- /dev/null +++ b/heavenly-hostas/uv.lock @@ -0,0 +1,1529 @@ +version = 1 +revision = 1 +requires-python = ">=3.12" + +[manifest] +members = [ + "backend", + "editor", + "gallery", + "heavenly-hostas-hosting", +] + +[[package]] +name = "aiofiles" +version = "24.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/03/a88171e277e8caa88a4c77808c20ebb04ba74cc4681bf1e9416c862de237/aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c", size = 30247 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/45/30bb92d442636f570cb5651bc661f52b610e2eec3f891a5dc3a4c3667db0/aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5", size = 15896 }, +] + +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265 }, +] + +[[package]] +name = "aiohttp" +version = "3.12.15" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/e7/d92a237d8802ca88483906c388f7c201bbe96cd80a165ffd0ac2f6a8d59f/aiohttp-3.12.15.tar.gz", hash = "sha256:4fc61385e9c98d72fcdf47e6dd81833f47b2f77c114c29cd64a361be57a763a2", size = 7823716 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/97/77cb2450d9b35f517d6cf506256bf4f5bda3f93a66b4ad64ba7fc917899c/aiohttp-3.12.15-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:802d3868f5776e28f7bf69d349c26fc0efadb81676d0afa88ed00d98a26340b7", size = 702333 }, + { url = "https://files.pythonhosted.org/packages/83/6d/0544e6b08b748682c30b9f65640d006e51f90763b41d7c546693bc22900d/aiohttp-3.12.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2800614cd560287be05e33a679638e586a2d7401f4ddf99e304d98878c29444", size = 476948 }, + { url = "https://files.pythonhosted.org/packages/3a/1d/c8c40e611e5094330284b1aea8a4b02ca0858f8458614fa35754cab42b9c/aiohttp-3.12.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8466151554b593909d30a0a125d638b4e5f3836e5aecde85b66b80ded1cb5b0d", size = 469787 }, + { url = "https://files.pythonhosted.org/packages/38/7d/b76438e70319796bfff717f325d97ce2e9310f752a267bfdf5192ac6082b/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e5a495cb1be69dae4b08f35a6c4579c539e9b5706f606632102c0f855bcba7c", size = 1716590 }, + { url = "https://files.pythonhosted.org/packages/79/b1/60370d70cdf8b269ee1444b390cbd72ce514f0d1cd1a715821c784d272c9/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6404dfc8cdde35c69aaa489bb3542fb86ef215fc70277c892be8af540e5e21c0", size = 1699241 }, + { url = "https://files.pythonhosted.org/packages/a3/2b/4968a7b8792437ebc12186db31523f541943e99bda8f30335c482bea6879/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3ead1c00f8521a5c9070fcb88f02967b1d8a0544e6d85c253f6968b785e1a2ab", size = 1754335 }, + { url = "https://files.pythonhosted.org/packages/fb/c1/49524ed553f9a0bec1a11fac09e790f49ff669bcd14164f9fab608831c4d/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6990ef617f14450bc6b34941dba4f12d5613cbf4e33805932f853fbd1cf18bfb", size = 1800491 }, + { url = "https://files.pythonhosted.org/packages/de/5e/3bf5acea47a96a28c121b167f5ef659cf71208b19e52a88cdfa5c37f1fcc/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd736ed420f4db2b8148b52b46b88ed038d0354255f9a73196b7bbce3ea97545", size = 1719929 }, + { url = "https://files.pythonhosted.org/packages/39/94/8ae30b806835bcd1cba799ba35347dee6961a11bd507db634516210e91d8/aiohttp-3.12.15-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c5092ce14361a73086b90c6efb3948ffa5be2f5b6fbcf52e8d8c8b8848bb97c", size = 1635733 }, + { url = "https://files.pythonhosted.org/packages/7a/46/06cdef71dd03acd9da7f51ab3a9107318aee12ad38d273f654e4f981583a/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:aaa2234bb60c4dbf82893e934d8ee8dea30446f0647e024074237a56a08c01bd", size = 1696790 }, + { url = "https://files.pythonhosted.org/packages/02/90/6b4cfaaf92ed98d0ec4d173e78b99b4b1a7551250be8937d9d67ecb356b4/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6d86a2fbdd14192e2f234a92d3b494dd4457e683ba07e5905a0b3ee25389ac9f", size = 1718245 }, + { url = "https://files.pythonhosted.org/packages/2e/e6/2593751670fa06f080a846f37f112cbe6f873ba510d070136a6ed46117c6/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a041e7e2612041a6ddf1c6a33b883be6a421247c7afd47e885969ee4cc58bd8d", size = 1658899 }, + { url = "https://files.pythonhosted.org/packages/8f/28/c15bacbdb8b8eb5bf39b10680d129ea7410b859e379b03190f02fa104ffd/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5015082477abeafad7203757ae44299a610e89ee82a1503e3d4184e6bafdd519", size = 1738459 }, + { url = "https://files.pythonhosted.org/packages/00/de/c269cbc4faa01fb10f143b1670633a8ddd5b2e1ffd0548f7aa49cb5c70e2/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:56822ff5ddfd1b745534e658faba944012346184fbfe732e0d6134b744516eea", size = 1766434 }, + { url = "https://files.pythonhosted.org/packages/52/b0/4ff3abd81aa7d929b27d2e1403722a65fc87b763e3a97b3a2a494bfc63bc/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b2acbbfff69019d9014508c4ba0401822e8bae5a5fdc3b6814285b71231b60f3", size = 1726045 }, + { url = "https://files.pythonhosted.org/packages/71/16/949225a6a2dd6efcbd855fbd90cf476052e648fb011aa538e3b15b89a57a/aiohttp-3.12.15-cp312-cp312-win32.whl", hash = "sha256:d849b0901b50f2185874b9a232f38e26b9b3d4810095a7572eacea939132d4e1", size = 423591 }, + { url = "https://files.pythonhosted.org/packages/2b/d8/fa65d2a349fe938b76d309db1a56a75c4fb8cc7b17a398b698488a939903/aiohttp-3.12.15-cp312-cp312-win_amd64.whl", hash = "sha256:b390ef5f62bb508a9d67cb3bba9b8356e23b3996da7062f1a57ce1a79d2b3d34", size = 450266 }, + { url = "https://files.pythonhosted.org/packages/f2/33/918091abcf102e39d15aba2476ad9e7bd35ddb190dcdd43a854000d3da0d/aiohttp-3.12.15-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9f922ffd05034d439dde1c77a20461cf4a1b0831e6caa26151fe7aa8aaebc315", size = 696741 }, + { url = "https://files.pythonhosted.org/packages/b5/2a/7495a81e39a998e400f3ecdd44a62107254803d1681d9189be5c2e4530cd/aiohttp-3.12.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2ee8a8ac39ce45f3e55663891d4b1d15598c157b4d494a4613e704c8b43112cd", size = 474407 }, + { url = "https://files.pythonhosted.org/packages/49/fc/a9576ab4be2dcbd0f73ee8675d16c707cfc12d5ee80ccf4015ba543480c9/aiohttp-3.12.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3eae49032c29d356b94eee45a3f39fdf4b0814b397638c2f718e96cfadf4c4e4", size = 466703 }, + { url = "https://files.pythonhosted.org/packages/09/2f/d4bcc8448cf536b2b54eed48f19682031ad182faa3a3fee54ebe5b156387/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b97752ff12cc12f46a9b20327104448042fce5c33a624f88c18f66f9368091c7", size = 1705532 }, + { url = "https://files.pythonhosted.org/packages/f1/f3/59406396083f8b489261e3c011aa8aee9df360a96ac8fa5c2e7e1b8f0466/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:894261472691d6fe76ebb7fcf2e5870a2ac284c7406ddc95823c8598a1390f0d", size = 1686794 }, + { url = "https://files.pythonhosted.org/packages/dc/71/164d194993a8d114ee5656c3b7ae9c12ceee7040d076bf7b32fb98a8c5c6/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5fa5d9eb82ce98959fc1031c28198b431b4d9396894f385cb63f1e2f3f20ca6b", size = 1738865 }, + { url = "https://files.pythonhosted.org/packages/1c/00/d198461b699188a93ead39cb458554d9f0f69879b95078dce416d3209b54/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0fa751efb11a541f57db59c1dd821bec09031e01452b2b6217319b3a1f34f3d", size = 1788238 }, + { url = "https://files.pythonhosted.org/packages/85/b8/9e7175e1fa0ac8e56baa83bf3c214823ce250d0028955dfb23f43d5e61fd/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5346b93e62ab51ee2a9d68e8f73c7cf96ffb73568a23e683f931e52450e4148d", size = 1710566 }, + { url = "https://files.pythonhosted.org/packages/59/e4/16a8eac9df39b48ae102ec030fa9f726d3570732e46ba0c592aeeb507b93/aiohttp-3.12.15-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:049ec0360f939cd164ecbfd2873eaa432613d5e77d6b04535e3d1fbae5a9e645", size = 1624270 }, + { url = "https://files.pythonhosted.org/packages/1f/f8/cd84dee7b6ace0740908fd0af170f9fab50c2a41ccbc3806aabcb1050141/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b52dcf013b57464b6d1e51b627adfd69a8053e84b7103a7cd49c030f9ca44461", size = 1677294 }, + { url = "https://files.pythonhosted.org/packages/ce/42/d0f1f85e50d401eccd12bf85c46ba84f947a84839c8a1c2c5f6e8ab1eb50/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9b2af240143dd2765e0fb661fd0361a1b469cab235039ea57663cda087250ea9", size = 1708958 }, + { url = "https://files.pythonhosted.org/packages/d5/6b/f6fa6c5790fb602538483aa5a1b86fcbad66244997e5230d88f9412ef24c/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ac77f709a2cde2cc71257ab2d8c74dd157c67a0558a0d2799d5d571b4c63d44d", size = 1651553 }, + { url = "https://files.pythonhosted.org/packages/04/36/a6d36ad545fa12e61d11d1932eef273928b0495e6a576eb2af04297fdd3c/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:47f6b962246f0a774fbd3b6b7be25d59b06fdb2f164cf2513097998fc6a29693", size = 1727688 }, + { url = "https://files.pythonhosted.org/packages/aa/c8/f195e5e06608a97a4e52c5d41c7927301bf757a8e8bb5bbf8cef6c314961/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:760fb7db442f284996e39cf9915a94492e1896baac44f06ae551974907922b64", size = 1761157 }, + { url = "https://files.pythonhosted.org/packages/05/6a/ea199e61b67f25ba688d3ce93f63b49b0a4e3b3d380f03971b4646412fc6/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad702e57dc385cae679c39d318def49aef754455f237499d5b99bea4ef582e51", size = 1710050 }, + { url = "https://files.pythonhosted.org/packages/b4/2e/ffeb7f6256b33635c29dbed29a22a723ff2dd7401fff42ea60cf2060abfb/aiohttp-3.12.15-cp313-cp313-win32.whl", hash = "sha256:f813c3e9032331024de2eb2e32a88d86afb69291fbc37a3a3ae81cc9917fb3d0", size = 422647 }, + { url = "https://files.pythonhosted.org/packages/1b/8e/78ee35774201f38d5e1ba079c9958f7629b1fd079459aea9467441dbfbf5/aiohttp-3.12.15-cp313-cp313-win_amd64.whl", hash = "sha256:1a649001580bdb37c6fdb1bebbd7e3bc688e8ec2b5c6f52edbb664662b17dc84", size = 449067 }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490 }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, +] + +[[package]] +name = "anyio" +version = "4.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/b4/636b3b65173d3ce9a38ef5f0522789614e590dab6a8d505340a4efe4c567/anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6", size = 213252 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/12/e5e0282d673bb9746bacfb6e2dba8719989d3660cdb2ea79aee9a9651afb/anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1", size = 107213 }, +] + +[[package]] +name = "attrs" +version = "25.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815 }, +] + +[[package]] +name = "backend" +version = "0.1.0" +source = { virtual = "packages/backend" } +dependencies = [ + { name = "cryptography" }, + { name = "fastapi" }, + { name = "httpx" }, + { name = "psycopg", extra = ["binary", "pool"] }, + { name = "pyjwt" }, + { name = "python-multipart" }, + { name = "supabase" }, + { name = "uvicorn" }, +] + +[package.metadata] +requires-dist = [ + { name = "cryptography", specifier = ">=45.0.6" }, + { name = "fastapi", specifier = ">=0.116.1" }, + { name = "httpx", specifier = ">=0.28.1" }, + { name = "psycopg", extras = ["binary", "pool"], specifier = ">=3.2.9" }, + { name = "pyjwt", specifier = ">=2.10.1" }, + { name = "python-multipart", specifier = ">=0.0.20" }, + { name = "supabase", specifier = ">=2.18.0" }, + { name = "uvicorn", specifier = ">=0.35.0" }, +] + +[[package]] +name = "bidict" +version = "0.23.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/6e/026678aa5a830e07cd9498a05d3e7e650a4f56a42f267a53d22bcda1bdc9/bidict-0.23.1.tar.gz", hash = "sha256:03069d763bc387bbd20e7d49914e75fc4132a41937fa3405417e1a5a2d006d71", size = 29093 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl", hash = "sha256:5dae8d4d79b552a71cbabc7deb25dfe8ce710b17ff41711e13010ead2abfc3e5", size = 32764 }, +] + +[[package]] +name = "certifi" +version = "2025.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216 }, +] + +[[package]] +name = "cffi" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178 }, + { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840 }, + { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803 }, + { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850 }, + { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729 }, + { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256 }, + { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424 }, + { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568 }, + { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736 }, + { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448 }, + { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976 }, + { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989 }, + { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802 }, + { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 }, + { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 }, + { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 }, + { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200 }, + { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 }, + { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 }, + { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 }, + { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 }, + { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 }, +] + +[[package]] +name = "cfgv" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249 }, +] + +[[package]] +name = "click" +version = "8.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "cryptography" +version = "45.0.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d6/0d/d13399c94234ee8f3df384819dc67e0c5ce215fb751d567a55a1f4b028c7/cryptography-45.0.6.tar.gz", hash = "sha256:5c966c732cf6e4a276ce83b6e4c729edda2df6929083a952cc7da973c539c719", size = 744949 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/29/2793d178d0eda1ca4a09a7c4e09a5185e75738cc6d526433e8663b460ea6/cryptography-45.0.6-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:048e7ad9e08cf4c0ab07ff7f36cc3115924e22e2266e034450a890d9e312dd74", size = 7042702 }, + { url = "https://files.pythonhosted.org/packages/b3/b6/cabd07410f222f32c8d55486c464f432808abaa1f12af9afcbe8f2f19030/cryptography-45.0.6-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:44647c5d796f5fc042bbc6d61307d04bf29bccb74d188f18051b635f20a9c75f", size = 4206483 }, + { url = "https://files.pythonhosted.org/packages/8b/9e/f9c7d36a38b1cfeb1cc74849aabe9bf817990f7603ff6eb485e0d70e0b27/cryptography-45.0.6-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e40b80ecf35ec265c452eea0ba94c9587ca763e739b8e559c128d23bff7ebbbf", size = 4429679 }, + { url = "https://files.pythonhosted.org/packages/9c/2a/4434c17eb32ef30b254b9e8b9830cee4e516f08b47fdd291c5b1255b8101/cryptography-45.0.6-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:00e8724bdad672d75e6f069b27970883179bd472cd24a63f6e620ca7e41cc0c5", size = 4210553 }, + { url = "https://files.pythonhosted.org/packages/ef/1d/09a5df8e0c4b7970f5d1f3aff1b640df6d4be28a64cae970d56c6cf1c772/cryptography-45.0.6-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a3085d1b319d35296176af31c90338eeb2ddac8104661df79f80e1d9787b8b2", size = 3894499 }, + { url = "https://files.pythonhosted.org/packages/79/62/120842ab20d9150a9d3a6bdc07fe2870384e82f5266d41c53b08a3a96b34/cryptography-45.0.6-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1b7fa6a1c1188c7ee32e47590d16a5a0646270921f8020efc9a511648e1b2e08", size = 4458484 }, + { url = "https://files.pythonhosted.org/packages/fd/80/1bc3634d45ddfed0871bfba52cf8f1ad724761662a0c792b97a951fb1b30/cryptography-45.0.6-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:275ba5cc0d9e320cd70f8e7b96d9e59903c815ca579ab96c1e37278d231fc402", size = 4210281 }, + { url = "https://files.pythonhosted.org/packages/7d/fe/ffb12c2d83d0ee625f124880a1f023b5878f79da92e64c37962bbbe35f3f/cryptography-45.0.6-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f4028f29a9f38a2025abedb2e409973709c660d44319c61762202206ed577c42", size = 4456890 }, + { url = "https://files.pythonhosted.org/packages/8c/8e/b3f3fe0dc82c77a0deb5f493b23311e09193f2268b77196ec0f7a36e3f3e/cryptography-45.0.6-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ee411a1b977f40bd075392c80c10b58025ee5c6b47a822a33c1198598a7a5f05", size = 4333247 }, + { url = "https://files.pythonhosted.org/packages/b3/a6/c3ef2ab9e334da27a1d7b56af4a2417d77e7806b2e0f90d6267ce120d2e4/cryptography-45.0.6-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e2a21a8eda2d86bb604934b6b37691585bd095c1f788530c1fcefc53a82b3453", size = 4565045 }, + { url = "https://files.pythonhosted.org/packages/31/c3/77722446b13fa71dddd820a5faab4ce6db49e7e0bf8312ef4192a3f78e2f/cryptography-45.0.6-cp311-abi3-win32.whl", hash = "sha256:d063341378d7ee9c91f9d23b431a3502fc8bfacd54ef0a27baa72a0843b29159", size = 2928923 }, + { url = "https://files.pythonhosted.org/packages/38/63/a025c3225188a811b82932a4dcc8457a26c3729d81578ccecbcce2cb784e/cryptography-45.0.6-cp311-abi3-win_amd64.whl", hash = "sha256:833dc32dfc1e39b7376a87b9a6a4288a10aae234631268486558920029b086ec", size = 3403805 }, + { url = "https://files.pythonhosted.org/packages/5b/af/bcfbea93a30809f126d51c074ee0fac5bd9d57d068edf56c2a73abedbea4/cryptography-45.0.6-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:3436128a60a5e5490603ab2adbabc8763613f638513ffa7d311c900a8349a2a0", size = 7020111 }, + { url = "https://files.pythonhosted.org/packages/98/c6/ea5173689e014f1a8470899cd5beeb358e22bb3cf5a876060f9d1ca78af4/cryptography-45.0.6-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0d9ef57b6768d9fa58e92f4947cea96ade1233c0e236db22ba44748ffedca394", size = 4198169 }, + { url = "https://files.pythonhosted.org/packages/ba/73/b12995edc0c7e2311ffb57ebd3b351f6b268fed37d93bfc6f9856e01c473/cryptography-45.0.6-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ea3c42f2016a5bbf71825537c2ad753f2870191134933196bee408aac397b3d9", size = 4421273 }, + { url = "https://files.pythonhosted.org/packages/f7/6e/286894f6f71926bc0da67408c853dd9ba953f662dcb70993a59fd499f111/cryptography-45.0.6-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:20ae4906a13716139d6d762ceb3e0e7e110f7955f3bc3876e3a07f5daadec5f3", size = 4199211 }, + { url = "https://files.pythonhosted.org/packages/de/34/a7f55e39b9623c5cb571d77a6a90387fe557908ffc44f6872f26ca8ae270/cryptography-45.0.6-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dac5ec199038b8e131365e2324c03d20e97fe214af051d20c49db129844e8b3", size = 3883732 }, + { url = "https://files.pythonhosted.org/packages/f9/b9/c6d32edbcba0cd9f5df90f29ed46a65c4631c4fbe11187feb9169c6ff506/cryptography-45.0.6-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:18f878a34b90d688982e43f4b700408b478102dd58b3e39de21b5ebf6509c301", size = 4450655 }, + { url = "https://files.pythonhosted.org/packages/77/2d/09b097adfdee0227cfd4c699b3375a842080f065bab9014248933497c3f9/cryptography-45.0.6-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5bd6020c80c5b2b2242d6c48487d7b85700f5e0038e67b29d706f98440d66eb5", size = 4198956 }, + { url = "https://files.pythonhosted.org/packages/55/66/061ec6689207d54effdff535bbdf85cc380d32dd5377173085812565cf38/cryptography-45.0.6-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:eccddbd986e43014263eda489abbddfbc287af5cddfd690477993dbb31e31016", size = 4449859 }, + { url = "https://files.pythonhosted.org/packages/41/ff/e7d5a2ad2d035e5a2af116e1a3adb4d8fcd0be92a18032917a089c6e5028/cryptography-45.0.6-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:550ae02148206beb722cfe4ef0933f9352bab26b087af00e48fdfb9ade35c5b3", size = 4320254 }, + { url = "https://files.pythonhosted.org/packages/82/27/092d311af22095d288f4db89fcaebadfb2f28944f3d790a4cf51fe5ddaeb/cryptography-45.0.6-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5b64e668fc3528e77efa51ca70fadcd6610e8ab231e3e06ae2bab3b31c2b8ed9", size = 4554815 }, + { url = "https://files.pythonhosted.org/packages/7e/01/aa2f4940262d588a8fdf4edabe4cda45854d00ebc6eaac12568b3a491a16/cryptography-45.0.6-cp37-abi3-win32.whl", hash = "sha256:780c40fb751c7d2b0c6786ceee6b6f871e86e8718a8ff4bc35073ac353c7cd02", size = 2912147 }, + { url = "https://files.pythonhosted.org/packages/0a/bc/16e0276078c2de3ceef6b5a34b965f4436215efac45313df90d55f0ba2d2/cryptography-45.0.6-cp37-abi3-win_amd64.whl", hash = "sha256:20d15aed3ee522faac1a39fbfdfee25d17b1284bafd808e1640a74846d7c4d1b", size = 3390459 }, +] + +[[package]] +name = "deprecation" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/d3/8ae2869247df154b64c1884d7346d412fed0c49df84db635aab2d1c40e62/deprecation-2.1.0.tar.gz", hash = "sha256:72b3bde64e5d778694b0cf68178aed03d15e15477116add3fb773e581f9518ff", size = 173788 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/c3/253a89ee03fc9b9682f1541728eb66db7db22148cd94f89ab22528cd1e1b/deprecation-2.1.0-py2.py3-none-any.whl", hash = "sha256:a10811591210e1fb0e768a8c25517cabeabcba6f0bf96564f8ff45189f90b14a", size = 11178 }, +] + +[[package]] +name = "distlib" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047 }, +] + +[[package]] +name = "docutils" +version = "0.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e9/86/5b41c32ecedcfdb4c77b28b6cb14234f252075f8cdb254531727a35547dd/docutils-0.22.tar.gz", hash = "sha256:ba9d57750e92331ebe7c08a1bbf7a7f8143b86c476acd51528b042216a6aad0f", size = 2277984 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/57/8db39bc5f98f042e0153b1de9fb88e1a409a33cda4dd7f723c2ed71e01f6/docutils-0.22-py3-none-any.whl", hash = "sha256:4ed966a0e96a0477d852f7af31bdcb3adc049fbb35ccba358c2ea8a03287615e", size = 630709 }, +] + +[[package]] +name = "editor" +version = "0.1.0" +source = { virtual = "packages/editor" } +dependencies = [ + { name = "nicegui" }, +] + +[package.metadata] +requires-dist = [{ name = "nicegui", specifier = ">=2.22.2" }] + +[[package]] +name = "fastapi" +version = "0.116.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/d7/6c8b3bfe33eeffa208183ec037fee0cce9f7f024089ab1c5d12ef04bd27c/fastapi-0.116.1.tar.gz", hash = "sha256:ed52cbf946abfd70c5a0dccb24673f0670deeb517a88b3544d03c2a6bf283143", size = 296485 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/47/d63c60f59a59467fda0f93f46335c9d18526d7071f025cb5b89d5353ea42/fastapi-0.116.1-py3-none-any.whl", hash = "sha256:c46ac7c312df840f0c9e220f7964bada936781bc4e2e6eb71f1c4d7553786565", size = 95631 }, +] + +[[package]] +name = "filelock" +version = "3.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215 }, +] + +[[package]] +name = "frozenlist" +version = "1.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/b1/b64018016eeb087db503b038296fd782586432b9c077fc5c7839e9cb6ef6/frozenlist-1.7.0.tar.gz", hash = "sha256:2e310d81923c2437ea8670467121cc3e9b0f76d3043cc1d2331d56c7fb7a3a8f", size = 45078 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a2/c8131383f1e66adad5f6ecfcce383d584ca94055a34d683bbb24ac5f2f1c/frozenlist-1.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3dbf9952c4bb0e90e98aec1bd992b3318685005702656bc6f67c1a32b76787f2", size = 81424 }, + { url = "https://files.pythonhosted.org/packages/4c/9d/02754159955088cb52567337d1113f945b9e444c4960771ea90eb73de8db/frozenlist-1.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1f5906d3359300b8a9bb194239491122e6cf1444c2efb88865426f170c262cdb", size = 47952 }, + { url = "https://files.pythonhosted.org/packages/01/7a/0046ef1bd6699b40acd2067ed6d6670b4db2f425c56980fa21c982c2a9db/frozenlist-1.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3dabd5a8f84573c8d10d8859a50ea2dec01eea372031929871368c09fa103478", size = 46688 }, + { url = "https://files.pythonhosted.org/packages/d6/a2/a910bafe29c86997363fb4c02069df4ff0b5bc39d33c5198b4e9dd42d8f8/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa57daa5917f1738064f302bf2626281a1cb01920c32f711fbc7bc36111058a8", size = 243084 }, + { url = "https://files.pythonhosted.org/packages/64/3e/5036af9d5031374c64c387469bfcc3af537fc0f5b1187d83a1cf6fab1639/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c193dda2b6d49f4c4398962810fa7d7c78f032bf45572b3e04dd5249dff27e08", size = 233524 }, + { url = "https://files.pythonhosted.org/packages/06/39/6a17b7c107a2887e781a48ecf20ad20f1c39d94b2a548c83615b5b879f28/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfe2b675cf0aaa6d61bf8fbffd3c274b3c9b7b1623beb3809df8a81399a4a9c4", size = 248493 }, + { url = "https://files.pythonhosted.org/packages/be/00/711d1337c7327d88c44d91dd0f556a1c47fb99afc060ae0ef66b4d24793d/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8fc5d5cda37f62b262405cf9652cf0856839c4be8ee41be0afe8858f17f4c94b", size = 244116 }, + { url = "https://files.pythonhosted.org/packages/24/fe/74e6ec0639c115df13d5850e75722750adabdc7de24e37e05a40527ca539/frozenlist-1.7.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0d5ce521d1dd7d620198829b87ea002956e4319002ef0bc8d3e6d045cb4646e", size = 224557 }, + { url = "https://files.pythonhosted.org/packages/8d/db/48421f62a6f77c553575201e89048e97198046b793f4a089c79a6e3268bd/frozenlist-1.7.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:488d0a7d6a0008ca0db273c542098a0fa9e7dfaa7e57f70acef43f32b3f69dca", size = 241820 }, + { url = "https://files.pythonhosted.org/packages/1d/fa/cb4a76bea23047c8462976ea7b7a2bf53997a0ca171302deae9d6dd12096/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:15a7eaba63983d22c54d255b854e8108e7e5f3e89f647fc854bd77a237e767df", size = 236542 }, + { url = "https://files.pythonhosted.org/packages/5d/32/476a4b5cfaa0ec94d3f808f193301debff2ea42288a099afe60757ef6282/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1eaa7e9c6d15df825bf255649e05bd8a74b04a4d2baa1ae46d9c2d00b2ca2cb5", size = 249350 }, + { url = "https://files.pythonhosted.org/packages/8d/ba/9a28042f84a6bf8ea5dbc81cfff8eaef18d78b2a1ad9d51c7bc5b029ad16/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e4389e06714cfa9d47ab87f784a7c5be91d3934cd6e9a7b85beef808297cc025", size = 225093 }, + { url = "https://files.pythonhosted.org/packages/bc/29/3a32959e68f9cf000b04e79ba574527c17e8842e38c91d68214a37455786/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:73bd45e1488c40b63fe5a7df892baf9e2a4d4bb6409a2b3b78ac1c6236178e01", size = 245482 }, + { url = "https://files.pythonhosted.org/packages/80/e8/edf2f9e00da553f07f5fa165325cfc302dead715cab6ac8336a5f3d0adc2/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99886d98e1643269760e5fe0df31e5ae7050788dd288947f7f007209b8c33f08", size = 249590 }, + { url = "https://files.pythonhosted.org/packages/1c/80/9a0eb48b944050f94cc51ee1c413eb14a39543cc4f760ed12657a5a3c45a/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:290a172aae5a4c278c6da8a96222e6337744cd9c77313efe33d5670b9f65fc43", size = 237785 }, + { url = "https://files.pythonhosted.org/packages/f3/74/87601e0fb0369b7a2baf404ea921769c53b7ae00dee7dcfe5162c8c6dbf0/frozenlist-1.7.0-cp312-cp312-win32.whl", hash = "sha256:426c7bc70e07cfebc178bc4c2bf2d861d720c4fff172181eeb4a4c41d4ca2ad3", size = 39487 }, + { url = "https://files.pythonhosted.org/packages/0b/15/c026e9a9fc17585a9d461f65d8593d281fedf55fbf7eb53f16c6df2392f9/frozenlist-1.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:563b72efe5da92e02eb68c59cb37205457c977aa7a449ed1b37e6939e5c47c6a", size = 43874 }, + { url = "https://files.pythonhosted.org/packages/24/90/6b2cebdabdbd50367273c20ff6b57a3dfa89bd0762de02c3a1eb42cb6462/frozenlist-1.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee80eeda5e2a4e660651370ebffd1286542b67e268aa1ac8d6dbe973120ef7ee", size = 79791 }, + { url = "https://files.pythonhosted.org/packages/83/2e/5b70b6a3325363293fe5fc3ae74cdcbc3e996c2a11dde2fd9f1fb0776d19/frozenlist-1.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d1a81c85417b914139e3a9b995d4a1c84559afc839a93cf2cb7f15e6e5f6ed2d", size = 47165 }, + { url = "https://files.pythonhosted.org/packages/f4/25/a0895c99270ca6966110f4ad98e87e5662eab416a17e7fd53c364bf8b954/frozenlist-1.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cbb65198a9132ebc334f237d7b0df163e4de83fb4f2bdfe46c1e654bdb0c5d43", size = 45881 }, + { url = "https://files.pythonhosted.org/packages/19/7c/71bb0bbe0832793c601fff68cd0cf6143753d0c667f9aec93d3c323f4b55/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dab46c723eeb2c255a64f9dc05b8dd601fde66d6b19cdb82b2e09cc6ff8d8b5d", size = 232409 }, + { url = "https://files.pythonhosted.org/packages/c0/45/ed2798718910fe6eb3ba574082aaceff4528e6323f9a8570be0f7028d8e9/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6aeac207a759d0dedd2e40745575ae32ab30926ff4fa49b1635def65806fddee", size = 225132 }, + { url = "https://files.pythonhosted.org/packages/ba/e2/8417ae0f8eacb1d071d4950f32f229aa6bf68ab69aab797b72a07ea68d4f/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bd8c4e58ad14b4fa7802b8be49d47993182fdd4023393899632c88fd8cd994eb", size = 237638 }, + { url = "https://files.pythonhosted.org/packages/f8/b7/2ace5450ce85f2af05a871b8c8719b341294775a0a6c5585d5e6170f2ce7/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04fb24d104f425da3540ed83cbfc31388a586a7696142004c577fa61c6298c3f", size = 233539 }, + { url = "https://files.pythonhosted.org/packages/46/b9/6989292c5539553dba63f3c83dc4598186ab2888f67c0dc1d917e6887db6/frozenlist-1.7.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a5c505156368e4ea6b53b5ac23c92d7edc864537ff911d2fb24c140bb175e60", size = 215646 }, + { url = "https://files.pythonhosted.org/packages/72/31/bc8c5c99c7818293458fe745dab4fd5730ff49697ccc82b554eb69f16a24/frozenlist-1.7.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8bd7eb96a675f18aa5c553eb7ddc24a43c8c18f22e1f9925528128c052cdbe00", size = 232233 }, + { url = "https://files.pythonhosted.org/packages/59/52/460db4d7ba0811b9ccb85af996019f5d70831f2f5f255f7cc61f86199795/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:05579bf020096fe05a764f1f84cd104a12f78eaab68842d036772dc6d4870b4b", size = 227996 }, + { url = "https://files.pythonhosted.org/packages/ba/c9/f4b39e904c03927b7ecf891804fd3b4df3db29b9e487c6418e37988d6e9d/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:376b6222d114e97eeec13d46c486facd41d4f43bab626b7c3f6a8b4e81a5192c", size = 242280 }, + { url = "https://files.pythonhosted.org/packages/b8/33/3f8d6ced42f162d743e3517781566b8481322be321b486d9d262adf70bfb/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0aa7e176ebe115379b5b1c95b4096fb1c17cce0847402e227e712c27bdb5a949", size = 217717 }, + { url = "https://files.pythonhosted.org/packages/3e/e8/ad683e75da6ccef50d0ab0c2b2324b32f84fc88ceee778ed79b8e2d2fe2e/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3fbba20e662b9c2130dc771e332a99eff5da078b2b2648153a40669a6d0e36ca", size = 236644 }, + { url = "https://files.pythonhosted.org/packages/b2/14/8d19ccdd3799310722195a72ac94ddc677541fb4bef4091d8e7775752360/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f3f4410a0a601d349dd406b5713fec59b4cee7e71678d5b17edda7f4655a940b", size = 238879 }, + { url = "https://files.pythonhosted.org/packages/ce/13/c12bf657494c2fd1079a48b2db49fa4196325909249a52d8f09bc9123fd7/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e2cdfaaec6a2f9327bf43c933c0319a7c429058e8537c508964a133dffee412e", size = 232502 }, + { url = "https://files.pythonhosted.org/packages/d7/8b/e7f9dfde869825489382bc0d512c15e96d3964180c9499efcec72e85db7e/frozenlist-1.7.0-cp313-cp313-win32.whl", hash = "sha256:5fc4df05a6591c7768459caba1b342d9ec23fa16195e744939ba5914596ae3e1", size = 39169 }, + { url = "https://files.pythonhosted.org/packages/35/89/a487a98d94205d85745080a37860ff5744b9820a2c9acbcdd9440bfddf98/frozenlist-1.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:52109052b9791a3e6b5d1b65f4b909703984b770694d3eb64fad124c835d7cba", size = 43219 }, + { url = "https://files.pythonhosted.org/packages/56/d5/5c4cf2319a49eddd9dd7145e66c4866bdc6f3dbc67ca3d59685149c11e0d/frozenlist-1.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a6f86e4193bb0e235ef6ce3dde5cbabed887e0b11f516ce8a0f4d3b33078ec2d", size = 84345 }, + { url = "https://files.pythonhosted.org/packages/a4/7d/ec2c1e1dc16b85bc9d526009961953df9cec8481b6886debb36ec9107799/frozenlist-1.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:82d664628865abeb32d90ae497fb93df398a69bb3434463d172b80fc25b0dd7d", size = 48880 }, + { url = "https://files.pythonhosted.org/packages/69/86/f9596807b03de126e11e7d42ac91e3d0b19a6599c714a1989a4e85eeefc4/frozenlist-1.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:912a7e8375a1c9a68325a902f3953191b7b292aa3c3fb0d71a216221deca460b", size = 48498 }, + { url = "https://files.pythonhosted.org/packages/5e/cb/df6de220f5036001005f2d726b789b2c0b65f2363b104bbc16f5be8084f8/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9537c2777167488d539bc5de2ad262efc44388230e5118868e172dd4a552b146", size = 292296 }, + { url = "https://files.pythonhosted.org/packages/83/1f/de84c642f17c8f851a2905cee2dae401e5e0daca9b5ef121e120e19aa825/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f34560fb1b4c3e30ba35fa9a13894ba39e5acfc5f60f57d8accde65f46cc5e74", size = 273103 }, + { url = "https://files.pythonhosted.org/packages/88/3c/c840bfa474ba3fa13c772b93070893c6e9d5c0350885760376cbe3b6c1b3/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:acd03d224b0175f5a850edc104ac19040d35419eddad04e7cf2d5986d98427f1", size = 292869 }, + { url = "https://files.pythonhosted.org/packages/a6/1c/3efa6e7d5a39a1d5ef0abeb51c48fb657765794a46cf124e5aca2c7a592c/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2038310bc582f3d6a09b3816ab01737d60bf7b1ec70f5356b09e84fb7408ab1", size = 291467 }, + { url = "https://files.pythonhosted.org/packages/4f/00/d5c5e09d4922c395e2f2f6b79b9a20dab4b67daaf78ab92e7729341f61f6/frozenlist-1.7.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8c05e4c8e5f36e5e088caa1bf78a687528f83c043706640a92cb76cd6999384", size = 266028 }, + { url = "https://files.pythonhosted.org/packages/4e/27/72765be905619dfde25a7f33813ac0341eb6b076abede17a2e3fbfade0cb/frozenlist-1.7.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:765bb588c86e47d0b68f23c1bee323d4b703218037765dcf3f25c838c6fecceb", size = 284294 }, + { url = "https://files.pythonhosted.org/packages/88/67/c94103a23001b17808eb7dd1200c156bb69fb68e63fcf0693dde4cd6228c/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:32dc2e08c67d86d0969714dd484fd60ff08ff81d1a1e40a77dd34a387e6ebc0c", size = 281898 }, + { url = "https://files.pythonhosted.org/packages/42/34/a3e2c00c00f9e2a9db5653bca3fec306349e71aff14ae45ecc6d0951dd24/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:c0303e597eb5a5321b4de9c68e9845ac8f290d2ab3f3e2c864437d3c5a30cd65", size = 290465 }, + { url = "https://files.pythonhosted.org/packages/bb/73/f89b7fbce8b0b0c095d82b008afd0590f71ccb3dee6eee41791cf8cd25fd/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:a47f2abb4e29b3a8d0b530f7c3598badc6b134562b1a5caee867f7c62fee51e3", size = 266385 }, + { url = "https://files.pythonhosted.org/packages/cd/45/e365fdb554159462ca12df54bc59bfa7a9a273ecc21e99e72e597564d1ae/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:3d688126c242a6fabbd92e02633414d40f50bb6002fa4cf995a1d18051525657", size = 288771 }, + { url = "https://files.pythonhosted.org/packages/00/11/47b6117002a0e904f004d70ec5194fe9144f117c33c851e3d51c765962d0/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:4e7e9652b3d367c7bd449a727dc79d5043f48b88d0cbfd4f9f1060cf2b414104", size = 288206 }, + { url = "https://files.pythonhosted.org/packages/40/37/5f9f3c3fd7f7746082ec67bcdc204db72dad081f4f83a503d33220a92973/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1a85e345b4c43db8b842cab1feb41be5cc0b10a1830e6295b69d7310f99becaf", size = 282620 }, + { url = "https://files.pythonhosted.org/packages/0b/31/8fbc5af2d183bff20f21aa743b4088eac4445d2bb1cdece449ae80e4e2d1/frozenlist-1.7.0-cp313-cp313t-win32.whl", hash = "sha256:3a14027124ddb70dfcee5148979998066897e79f89f64b13328595c4bdf77c81", size = 43059 }, + { url = "https://files.pythonhosted.org/packages/bb/ed/41956f52105b8dbc26e457c5705340c67c8cc2b79f394b79bffc09d0e938/frozenlist-1.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3bf8010d71d4507775f658e9823210b7427be36625b387221642725b515dcf3e", size = 47516 }, + { url = "https://files.pythonhosted.org/packages/ee/45/b82e3c16be2182bff01179db177fe144d58b5dc787a7d4492c6ed8b9317f/frozenlist-1.7.0-py3-none-any.whl", hash = "sha256:9a5af342e34f7e97caf8c995864c7a396418ae2859cc6fdf1b1073020d516a7e", size = 13106 }, +] + +[[package]] +name = "gallery" +version = "0.1.0" +source = { virtual = "packages/gallery" } + +[[package]] +name = "gotrue" +version = "2.12.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx", extra = ["http2"] }, + { name = "pydantic" }, + { name = "pyjwt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/67/ae47f68daae1bbb56a9fbf960dfb7d08b3dec52a6ad1e96f69c2ba5b3116/gotrue-2.12.3.tar.gz", hash = "sha256:f874cf9d0b2f0335bfbd0d6e29e3f7aff79998cd1c14d2ad814db8c06cee3852", size = 38323 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/fa/4165d298ef89254c9f742faa3f99a61fe6fd3552b4ba44df6924f8d307d7/gotrue-2.12.3-py3-none-any.whl", hash = "sha256:b1a3c6a5fe3f92e854a026c4c19de58706a96fd5fbdcc3d620b2802f6a46a26b", size = 44022 }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515 }, +] + +[[package]] +name = "h2" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "hpack" }, + { name = "hyperframe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1b/38/d7f80fd13e6582fb8e0df8c9a653dcc02b03ca34f4d72f34869298c5baf8/h2-4.2.0.tar.gz", hash = "sha256:c8a52129695e88b1a0578d8d2cc6842bbd79128ac685463b887ee278126ad01f", size = 2150682 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/9e/984486f2d0a0bd2b024bf4bc1c62688fcafa9e61991f041fb0e2def4a982/h2-4.2.0-py3-none-any.whl", hash = "sha256:479a53ad425bb29af087f3458a61d30780bc818e4ebcf01f0b536ba916462ed0", size = 60957 }, +] + +[[package]] +name = "heavenly-hostas-hosting" +version = "0.1.0" +source = { virtual = "." } + +[package.dev-dependencies] +dev = [ + { name = "pre-commit" }, + { name = "ruff" }, +] + +[package.metadata] + +[package.metadata.requires-dev] +dev = [ + { name = "pre-commit", specifier = "~=4.2.0" }, + { name = "ruff", specifier = "~=0.12.2" }, +] + +[[package]] +name = "hpack" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/48/71de9ed269fdae9c8057e5a4c0aa7402e8bb16f2c6e90b3aa53327b113f8/hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca", size = 51276 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/c6/80c95b1b2b94682a72cbdbfb85b81ae2daffa4291fbfa1b1464502ede10d/hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496", size = 34357 }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784 }, +] + +[[package]] +name = "httptools" +version = "0.6.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/9a/ce5e1f7e131522e6d3426e8e7a490b3a01f39a6696602e1c4f33f9e94277/httptools-0.6.4.tar.gz", hash = "sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c", size = 240639 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/0e/d0b71465c66b9185f90a091ab36389a7352985fe857e352801c39d6127c8/httptools-0.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:df017d6c780287d5c80601dafa31f17bddb170232d85c066604d8558683711a2", size = 200683 }, + { url = "https://files.pythonhosted.org/packages/e2/b8/412a9bb28d0a8988de3296e01efa0bd62068b33856cdda47fe1b5e890954/httptools-0.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:85071a1e8c2d051b507161f6c3e26155b5c790e4e28d7f236422dbacc2a9cc44", size = 104337 }, + { url = "https://files.pythonhosted.org/packages/9b/01/6fb20be3196ffdc8eeec4e653bc2a275eca7f36634c86302242c4fbb2760/httptools-0.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69422b7f458c5af875922cdb5bd586cc1f1033295aa9ff63ee196a87519ac8e1", size = 508796 }, + { url = "https://files.pythonhosted.org/packages/f7/d8/b644c44acc1368938317d76ac991c9bba1166311880bcc0ac297cb9d6bd7/httptools-0.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16e603a3bff50db08cd578d54f07032ca1631450ceb972c2f834c2b860c28ea2", size = 510837 }, + { url = "https://files.pythonhosted.org/packages/52/d8/254d16a31d543073a0e57f1c329ca7378d8924e7e292eda72d0064987486/httptools-0.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec4f178901fa1834d4a060320d2f3abc5c9e39766953d038f1458cb885f47e81", size = 485289 }, + { url = "https://files.pythonhosted.org/packages/5f/3c/4aee161b4b7a971660b8be71a92c24d6c64372c1ab3ae7f366b3680df20f/httptools-0.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f9eb89ecf8b290f2e293325c646a211ff1c2493222798bb80a530c5e7502494f", size = 489779 }, + { url = "https://files.pythonhosted.org/packages/12/b7/5cae71a8868e555f3f67a50ee7f673ce36eac970f029c0c5e9d584352961/httptools-0.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:db78cb9ca56b59b016e64b6031eda5653be0589dba2b1b43453f6e8b405a0970", size = 88634 }, + { url = "https://files.pythonhosted.org/packages/94/a3/9fe9ad23fd35f7de6b91eeb60848986058bd8b5a5c1e256f5860a160cc3e/httptools-0.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ade273d7e767d5fae13fa637f4d53b6e961fb7fd93c7797562663f0171c26660", size = 197214 }, + { url = "https://files.pythonhosted.org/packages/ea/d9/82d5e68bab783b632023f2fa31db20bebb4e89dfc4d2293945fd68484ee4/httptools-0.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:856f4bc0478ae143bad54a4242fccb1f3f86a6e1be5548fecfd4102061b3a083", size = 102431 }, + { url = "https://files.pythonhosted.org/packages/96/c1/cb499655cbdbfb57b577734fde02f6fa0bbc3fe9fb4d87b742b512908dff/httptools-0.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:322d20ea9cdd1fa98bd6a74b77e2ec5b818abdc3d36695ab402a0de8ef2865a3", size = 473121 }, + { url = "https://files.pythonhosted.org/packages/af/71/ee32fd358f8a3bb199b03261f10921716990808a675d8160b5383487a317/httptools-0.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d87b29bd4486c0093fc64dea80231f7c7f7eb4dc70ae394d70a495ab8436071", size = 473805 }, + { url = "https://files.pythonhosted.org/packages/8a/0a/0d4df132bfca1507114198b766f1737d57580c9ad1cf93c1ff673e3387be/httptools-0.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:342dd6946aa6bda4b8f18c734576106b8a31f2fe31492881a9a160ec84ff4bd5", size = 448858 }, + { url = "https://files.pythonhosted.org/packages/1e/6a/787004fdef2cabea27bad1073bf6a33f2437b4dbd3b6fb4a9d71172b1c7c/httptools-0.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b36913ba52008249223042dca46e69967985fb4051951f94357ea681e1f5dc0", size = 452042 }, + { url = "https://files.pythonhosted.org/packages/4d/dc/7decab5c404d1d2cdc1bb330b1bf70e83d6af0396fd4fc76fc60c0d522bf/httptools-0.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:28908df1b9bb8187393d5b5db91435ccc9c8e891657f9cbb42a2541b44c82fc8", size = 87682 }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, +] + +[package.optional-dependencies] +http2 = [ + { name = "h2" }, +] + +[[package]] +name = "hyperframe" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/e7/94f8232d4a74cc99514c13a9f995811485a6903d48e5d952771ef6322e30/hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08", size = 26566 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007 }, +] + +[[package]] +name = "identify" +version = "2.6.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ca/ffbabe3635bb839aa36b3a893c91a9b0d368cb4d8073e03a12896970af82/identify-2.6.13.tar.gz", hash = "sha256:da8d6c828e773620e13bfa86ea601c5a5310ba4bcd65edf378198b56a1f9fb32", size = 99243 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/ce/461b60a3ee109518c055953729bf9ed089a04db895d47e95444071dcdef2/identify-2.6.13-py2.py3-none-any.whl", hash = "sha256:60381139b3ae39447482ecc406944190f690d4a2997f2584062089848361b33b", size = 99153 }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, +] + +[[package]] +name = "ifaddr" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/ac/fb4c578f4a3256561548cd825646680edcadb9440f3f68add95ade1eb791/ifaddr-0.2.0.tar.gz", hash = "sha256:cc0cbfcaabf765d44595825fb96a99bb12c79716b73b44330ea38ee2b0c4aed4", size = 10485 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/1f/19ebc343cc71a7ffa78f17018535adc5cbdd87afb31d7c34874680148b32/ifaddr-0.2.0-py3-none-any.whl", hash = "sha256:085e0305cfe6f16ab12d72e2024030f5d52674afad6911bb1eee207177b8a748", size = 12314 }, +] + +[[package]] +name = "itsdangerous" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234 }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899 }, +] + +[[package]] +name = "markdown2" +version = "2.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/42/f8/b2ae8bf5f28f9b510ae097415e6e4cb63226bb28d7ee01aec03a755ba03b/markdown2-2.5.4.tar.gz", hash = "sha256:a09873f0b3c23dbfae589b0080587df52ad75bb09a5fa6559147554736676889", size = 145652 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/06/2697b5043c3ecb720ce0d243fc7cf5024c0b5b1e450506e9b21939019963/markdown2-2.5.4-py3-none-any.whl", hash = "sha256:3c4b2934e677be7fec0e6f2de4410e116681f4ad50ec8e5ba7557be506d3f439", size = 49954 }, +] + +[[package]] +name = "markupsafe" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274 }, + { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348 }, + { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149 }, + { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118 }, + { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993 }, + { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178 }, + { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319 }, + { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352 }, + { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097 }, + { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601 }, + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 }, +] + +[[package]] +name = "multidict" +version = "6.6.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/69/7f/0652e6ed47ab288e3756ea9c0df8b14950781184d4bd7883f4d87dd41245/multidict-6.6.4.tar.gz", hash = "sha256:d2d4e4787672911b48350df02ed3fa3fffdc2f2e8ca06dd6afdf34189b76a9dd", size = 101843 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/f6/512ffd8fd8b37fb2680e5ac35d788f1d71bbaf37789d21a820bdc441e565/multidict-6.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0ffb87be160942d56d7b87b0fdf098e81ed565add09eaa1294268c7f3caac4c8", size = 76516 }, + { url = "https://files.pythonhosted.org/packages/99/58/45c3e75deb8855c36bd66cc1658007589662ba584dbf423d01df478dd1c5/multidict-6.6.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d191de6cbab2aff5de6c5723101705fd044b3e4c7cfd587a1929b5028b9714b3", size = 45394 }, + { url = "https://files.pythonhosted.org/packages/fd/ca/e8c4472a93a26e4507c0b8e1f0762c0d8a32de1328ef72fd704ef9cc5447/multidict-6.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:38a0956dd92d918ad5feff3db8fcb4a5eb7dba114da917e1a88475619781b57b", size = 43591 }, + { url = "https://files.pythonhosted.org/packages/05/51/edf414f4df058574a7265034d04c935aa84a89e79ce90fcf4df211f47b16/multidict-6.6.4-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6865f6d3b7900ae020b495d599fcf3765653bc927951c1abb959017f81ae8287", size = 237215 }, + { url = "https://files.pythonhosted.org/packages/c8/45/8b3d6dbad8cf3252553cc41abea09ad527b33ce47a5e199072620b296902/multidict-6.6.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a2088c126b6f72db6c9212ad827d0ba088c01d951cee25e758c450da732c138", size = 258299 }, + { url = "https://files.pythonhosted.org/packages/3c/e8/8ca2e9a9f5a435fc6db40438a55730a4bf4956b554e487fa1b9ae920f825/multidict-6.6.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0f37bed7319b848097085d7d48116f545985db988e2256b2e6f00563a3416ee6", size = 242357 }, + { url = "https://files.pythonhosted.org/packages/0f/84/80c77c99df05a75c28490b2af8f7cba2a12621186e0a8b0865d8e745c104/multidict-6.6.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:01368e3c94032ba6ca0b78e7ccb099643466cf24f8dc8eefcfdc0571d56e58f9", size = 268369 }, + { url = "https://files.pythonhosted.org/packages/0d/e9/920bfa46c27b05fb3e1ad85121fd49f441492dca2449c5bcfe42e4565d8a/multidict-6.6.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fe323540c255db0bffee79ad7f048c909f2ab0edb87a597e1c17da6a54e493c", size = 269341 }, + { url = "https://files.pythonhosted.org/packages/af/65/753a2d8b05daf496f4a9c367fe844e90a1b2cac78e2be2c844200d10cc4c/multidict-6.6.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8eb3025f17b0a4c3cd08cda49acf312a19ad6e8a4edd9dbd591e6506d999402", size = 256100 }, + { url = "https://files.pythonhosted.org/packages/09/54/655be13ae324212bf0bc15d665a4e34844f34c206f78801be42f7a0a8aaa/multidict-6.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bbc14f0365534d35a06970d6a83478b249752e922d662dc24d489af1aa0d1be7", size = 253584 }, + { url = "https://files.pythonhosted.org/packages/5c/74/ab2039ecc05264b5cec73eb018ce417af3ebb384ae9c0e9ed42cb33f8151/multidict-6.6.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:75aa52fba2d96bf972e85451b99d8e19cc37ce26fd016f6d4aa60da9ab2b005f", size = 251018 }, + { url = "https://files.pythonhosted.org/packages/af/0a/ccbb244ac848e56c6427f2392741c06302bbfba49c0042f1eb3c5b606497/multidict-6.6.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4fefd4a815e362d4f011919d97d7b4a1e566f1dde83dc4ad8cfb5b41de1df68d", size = 251477 }, + { url = "https://files.pythonhosted.org/packages/0e/b0/0ed49bba775b135937f52fe13922bc64a7eaf0a3ead84a36e8e4e446e096/multidict-6.6.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:db9801fe021f59a5b375ab778973127ca0ac52429a26e2fd86aa9508f4d26eb7", size = 263575 }, + { url = "https://files.pythonhosted.org/packages/3e/d9/7fb85a85e14de2e44dfb6a24f03c41e2af8697a6df83daddb0e9b7569f73/multidict-6.6.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:a650629970fa21ac1fb06ba25dabfc5b8a2054fcbf6ae97c758aa956b8dba802", size = 259649 }, + { url = "https://files.pythonhosted.org/packages/03/9e/b3a459bcf9b6e74fa461a5222a10ff9b544cb1cd52fd482fb1b75ecda2a2/multidict-6.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:452ff5da78d4720d7516a3a2abd804957532dd69296cb77319c193e3ffb87e24", size = 251505 }, + { url = "https://files.pythonhosted.org/packages/86/a2/8022f78f041dfe6d71e364001a5cf987c30edfc83c8a5fb7a3f0974cff39/multidict-6.6.4-cp312-cp312-win32.whl", hash = "sha256:8c2fcb12136530ed19572bbba61b407f655e3953ba669b96a35036a11a485793", size = 41888 }, + { url = "https://files.pythonhosted.org/packages/c7/eb/d88b1780d43a56db2cba24289fa744a9d216c1a8546a0dc3956563fd53ea/multidict-6.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:047d9425860a8c9544fed1b9584f0c8bcd31bcde9568b047c5e567a1025ecd6e", size = 46072 }, + { url = "https://files.pythonhosted.org/packages/9f/16/b929320bf5750e2d9d4931835a4c638a19d2494a5b519caaaa7492ebe105/multidict-6.6.4-cp312-cp312-win_arm64.whl", hash = "sha256:14754eb72feaa1e8ae528468f24250dd997b8e2188c3d2f593f9eba259e4b364", size = 43222 }, + { url = "https://files.pythonhosted.org/packages/3a/5d/e1db626f64f60008320aab00fbe4f23fc3300d75892a3381275b3d284580/multidict-6.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f46a6e8597f9bd71b31cc708195d42b634c8527fecbcf93febf1052cacc1f16e", size = 75848 }, + { url = "https://files.pythonhosted.org/packages/4c/aa/8b6f548d839b6c13887253af4e29c939af22a18591bfb5d0ee6f1931dae8/multidict-6.6.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:22e38b2bc176c5eb9c0a0e379f9d188ae4cd8b28c0f53b52bce7ab0a9e534657", size = 45060 }, + { url = "https://files.pythonhosted.org/packages/eb/c6/f5e97e5d99a729bc2aa58eb3ebfa9f1e56a9b517cc38c60537c81834a73f/multidict-6.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5df8afd26f162da59e218ac0eefaa01b01b2e6cd606cffa46608f699539246da", size = 43269 }, + { url = "https://files.pythonhosted.org/packages/dc/31/d54eb0c62516776f36fe67f84a732f97e0b0e12f98d5685bebcc6d396910/multidict-6.6.4-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:49517449b58d043023720aa58e62b2f74ce9b28f740a0b5d33971149553d72aa", size = 237158 }, + { url = "https://files.pythonhosted.org/packages/c4/1c/8a10c1c25b23156e63b12165a929d8eb49a6ed769fdbefb06e6f07c1e50d/multidict-6.6.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae9408439537c5afdca05edd128a63f56a62680f4b3c234301055d7a2000220f", size = 257076 }, + { url = "https://files.pythonhosted.org/packages/ad/86/90e20b5771d6805a119e483fd3d1e8393e745a11511aebca41f0da38c3e2/multidict-6.6.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:87a32d20759dc52a9e850fe1061b6e41ab28e2998d44168a8a341b99ded1dba0", size = 240694 }, + { url = "https://files.pythonhosted.org/packages/e7/49/484d3e6b535bc0555b52a0a26ba86e4d8d03fd5587d4936dc59ba7583221/multidict-6.6.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:52e3c8d43cdfff587ceedce9deb25e6ae77daba560b626e97a56ddcad3756879", size = 266350 }, + { url = "https://files.pythonhosted.org/packages/bf/b4/aa4c5c379b11895083d50021e229e90c408d7d875471cb3abf721e4670d6/multidict-6.6.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ad8850921d3a8d8ff6fbef790e773cecfc260bbfa0566998980d3fa8f520bc4a", size = 267250 }, + { url = "https://files.pythonhosted.org/packages/80/e5/5e22c5bf96a64bdd43518b1834c6d95a4922cc2066b7d8e467dae9b6cee6/multidict-6.6.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:497a2954adc25c08daff36f795077f63ad33e13f19bfff7736e72c785391534f", size = 254900 }, + { url = "https://files.pythonhosted.org/packages/17/38/58b27fed927c07035abc02befacab42491e7388ca105e087e6e0215ead64/multidict-6.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:024ce601f92d780ca1617ad4be5ac15b501cc2414970ffa2bb2bbc2bd5a68fa5", size = 252355 }, + { url = "https://files.pythonhosted.org/packages/d0/a1/dad75d23a90c29c02b5d6f3d7c10ab36c3197613be5d07ec49c7791e186c/multidict-6.6.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a693fc5ed9bdd1c9e898013e0da4dcc640de7963a371c0bd458e50e046bf6438", size = 250061 }, + { url = "https://files.pythonhosted.org/packages/b8/1a/ac2216b61c7f116edab6dc3378cca6c70dc019c9a457ff0d754067c58b20/multidict-6.6.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:190766dac95aab54cae5b152a56520fd99298f32a1266d66d27fdd1b5ac00f4e", size = 249675 }, + { url = "https://files.pythonhosted.org/packages/d4/79/1916af833b800d13883e452e8e0977c065c4ee3ab7a26941fbfdebc11895/multidict-6.6.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:34d8f2a5ffdceab9dcd97c7a016deb2308531d5f0fced2bb0c9e1df45b3363d7", size = 261247 }, + { url = "https://files.pythonhosted.org/packages/c5/65/d1f84fe08ac44a5fc7391cbc20a7cedc433ea616b266284413fd86062f8c/multidict-6.6.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:59e8d40ab1f5a8597abcef00d04845155a5693b5da00d2c93dbe88f2050f2812", size = 257960 }, + { url = "https://files.pythonhosted.org/packages/13/b5/29ec78057d377b195ac2c5248c773703a6b602e132a763e20ec0457e7440/multidict-6.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:467fe64138cfac771f0e949b938c2e1ada2b5af22f39692aa9258715e9ea613a", size = 250078 }, + { url = "https://files.pythonhosted.org/packages/c4/0e/7e79d38f70a872cae32e29b0d77024bef7834b0afb406ddae6558d9e2414/multidict-6.6.4-cp313-cp313-win32.whl", hash = "sha256:14616a30fe6d0a48d0a48d1a633ab3b8bec4cf293aac65f32ed116f620adfd69", size = 41708 }, + { url = "https://files.pythonhosted.org/packages/9d/34/746696dffff742e97cd6a23da953e55d0ea51fa601fa2ff387b3edcfaa2c/multidict-6.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:40cd05eaeb39e2bc8939451f033e57feaa2ac99e07dbca8afe2be450a4a3b6cf", size = 45912 }, + { url = "https://files.pythonhosted.org/packages/c7/87/3bac136181e271e29170d8d71929cdeddeb77f3e8b6a0c08da3a8e9da114/multidict-6.6.4-cp313-cp313-win_arm64.whl", hash = "sha256:f6eb37d511bfae9e13e82cb4d1af36b91150466f24d9b2b8a9785816deb16605", size = 43076 }, + { url = "https://files.pythonhosted.org/packages/64/94/0a8e63e36c049b571c9ae41ee301ada29c3fee9643d9c2548d7d558a1d99/multidict-6.6.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:6c84378acd4f37d1b507dfa0d459b449e2321b3ba5f2338f9b085cf7a7ba95eb", size = 82812 }, + { url = "https://files.pythonhosted.org/packages/25/1a/be8e369dfcd260d2070a67e65dd3990dd635cbd735b98da31e00ea84cd4e/multidict-6.6.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0e0558693063c75f3d952abf645c78f3c5dfdd825a41d8c4d8156fc0b0da6e7e", size = 48313 }, + { url = "https://files.pythonhosted.org/packages/26/5a/dd4ade298674b2f9a7b06a32c94ffbc0497354df8285f27317c66433ce3b/multidict-6.6.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3f8e2384cb83ebd23fd07e9eada8ba64afc4c759cd94817433ab8c81ee4b403f", size = 46777 }, + { url = "https://files.pythonhosted.org/packages/89/db/98aa28bc7e071bfba611ac2ae803c24e96dd3a452b4118c587d3d872c64c/multidict-6.6.4-cp313-cp313t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:f996b87b420995a9174b2a7c1a8daf7db4750be6848b03eb5e639674f7963773", size = 229321 }, + { url = "https://files.pythonhosted.org/packages/c7/bc/01ddda2a73dd9d167bd85d0e8ef4293836a8f82b786c63fb1a429bc3e678/multidict-6.6.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc356250cffd6e78416cf5b40dc6a74f1edf3be8e834cf8862d9ed5265cf9b0e", size = 249954 }, + { url = "https://files.pythonhosted.org/packages/06/78/6b7c0f020f9aa0acf66d0ab4eb9f08375bac9a50ff5e3edb1c4ccd59eafc/multidict-6.6.4-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:dadf95aa862714ea468a49ad1e09fe00fcc9ec67d122f6596a8d40caf6cec7d0", size = 228612 }, + { url = "https://files.pythonhosted.org/packages/00/44/3faa416f89b2d5d76e9d447296a81521e1c832ad6e40b92f990697b43192/multidict-6.6.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7dd57515bebffd8ebd714d101d4c434063322e4fe24042e90ced41f18b6d3395", size = 257528 }, + { url = "https://files.pythonhosted.org/packages/05/5f/77c03b89af0fcb16f018f668207768191fb9dcfb5e3361a5e706a11db2c9/multidict-6.6.4-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:967af5f238ebc2eb1da4e77af5492219fbd9b4b812347da39a7b5f5c72c0fa45", size = 256329 }, + { url = "https://files.pythonhosted.org/packages/cf/e9/ed750a2a9afb4f8dc6f13dc5b67b514832101b95714f1211cd42e0aafc26/multidict-6.6.4-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a4c6875c37aae9794308ec43e3530e4aa0d36579ce38d89979bbf89582002bb", size = 247928 }, + { url = "https://files.pythonhosted.org/packages/1f/b5/e0571bc13cda277db7e6e8a532791d4403dacc9850006cb66d2556e649c0/multidict-6.6.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:7f683a551e92bdb7fac545b9c6f9fa2aebdeefa61d607510b3533286fcab67f5", size = 245228 }, + { url = "https://files.pythonhosted.org/packages/f3/a3/69a84b0eccb9824491f06368f5b86e72e4af54c3067c37c39099b6687109/multidict-6.6.4-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:3ba5aaf600edaf2a868a391779f7a85d93bed147854925f34edd24cc70a3e141", size = 235869 }, + { url = "https://files.pythonhosted.org/packages/a9/9d/28802e8f9121a6a0804fa009debf4e753d0a59969ea9f70be5f5fdfcb18f/multidict-6.6.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:580b643b7fd2c295d83cad90d78419081f53fd532d1f1eb67ceb7060f61cff0d", size = 243446 }, + { url = "https://files.pythonhosted.org/packages/38/ea/6c98add069b4878c1d66428a5f5149ddb6d32b1f9836a826ac764b9940be/multidict-6.6.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:37b7187197da6af3ee0b044dbc9625afd0c885f2800815b228a0e70f9a7f473d", size = 252299 }, + { url = "https://files.pythonhosted.org/packages/3a/09/8fe02d204473e14c0af3affd50af9078839dfca1742f025cca765435d6b4/multidict-6.6.4-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e1b93790ed0bc26feb72e2f08299691ceb6da5e9e14a0d13cc74f1869af327a0", size = 246926 }, + { url = "https://files.pythonhosted.org/packages/37/3d/7b1e10d774a6df5175ecd3c92bff069e77bed9ec2a927fdd4ff5fe182f67/multidict-6.6.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a506a77ddee1efcca81ecbeae27ade3e09cdf21a8ae854d766c2bb4f14053f92", size = 243383 }, + { url = "https://files.pythonhosted.org/packages/50/b0/a6fae46071b645ae98786ab738447de1ef53742eaad949f27e960864bb49/multidict-6.6.4-cp313-cp313t-win32.whl", hash = "sha256:f93b2b2279883d1d0a9e1bd01f312d6fc315c5e4c1f09e112e4736e2f650bc4e", size = 47775 }, + { url = "https://files.pythonhosted.org/packages/b2/0a/2436550b1520091af0600dff547913cb2d66fbac27a8c33bc1b1bccd8d98/multidict-6.6.4-cp313-cp313t-win_amd64.whl", hash = "sha256:6d46a180acdf6e87cc41dc15d8f5c2986e1e8739dc25dbb7dac826731ef381a4", size = 53100 }, + { url = "https://files.pythonhosted.org/packages/97/ea/43ac51faff934086db9c072a94d327d71b7d8b40cd5dcb47311330929ef0/multidict-6.6.4-cp313-cp313t-win_arm64.whl", hash = "sha256:756989334015e3335d087a27331659820d53ba432befdef6a718398b0a8493ad", size = 45501 }, + { url = "https://files.pythonhosted.org/packages/fd/69/b547032297c7e63ba2af494edba695d781af8a0c6e89e4d06cf848b21d80/multidict-6.6.4-py3-none-any.whl", hash = "sha256:27d8f8e125c07cb954e54d75d04905a9bba8a439c1d84aca94949d4d03d8601c", size = 12313 }, +] + +[[package]] +name = "nicegui" +version = "2.22.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiofiles" }, + { name = "aiohttp" }, + { name = "certifi" }, + { name = "docutils" }, + { name = "fastapi" }, + { name = "h11" }, + { name = "httpx" }, + { name = "ifaddr" }, + { name = "itsdangerous" }, + { name = "jinja2" }, + { name = "markdown2" }, + { name = "orjson", marker = "platform_machine != 'i386' and platform_machine != 'i686'" }, + { name = "pygments" }, + { name = "python-engineio" }, + { name = "python-multipart" }, + { name = "python-socketio", extra = ["asyncio-client"] }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "uvicorn", extra = ["standard"] }, + { name = "vbuild" }, + { name = "watchfiles" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cc/81/aaf1432abf54f25d62a3c346a087db39c4d7a38833795c3f1eb90feab5fd/nicegui-2.22.2.tar.gz", hash = "sha256:5c0aaf2d2365c665ae42955b17d6fa7ba19526d7aa9bd02ab547917e7bd1338c", size = 13107276 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/2b/141c85335df3982ca9a54da95d0c8954efe9756da8f2edaba2afbd262014/nicegui-2.22.2-py3-none-any.whl", hash = "sha256:a0e371918419af5bae7b22e6b55a29065a7a4222908af66584fd599e391b5b5f", size = 13494497 }, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, +] + +[[package]] +name = "orjson" +version = "3.11.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/1d/5e0ae38788bdf0721326695e65fdf41405ed535f633eb0df0f06f57552fa/orjson-3.11.2.tar.gz", hash = "sha256:91bdcf5e69a8fd8e8bdb3de32b31ff01d2bd60c1e8d5fe7d5afabdcf19920309", size = 5470739 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/02/46054ebe7996a8adee9640dcad7d39d76c2000dc0377efa38e55dc5cbf78/orjson-3.11.2-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:901d80d349d8452162b3aa1afb82cec5bee79a10550660bc21311cc61a4c5486", size = 226528 }, + { url = "https://files.pythonhosted.org/packages/e2/c6/6b6f0b4d8aea1137436546b990f71be2cd8bd870aa2f5aa14dba0fcc95dc/orjson-3.11.2-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:cf3bd3967a360e87ee14ed82cb258b7f18c710dacf3822fb0042a14313a673a1", size = 115931 }, + { url = "https://files.pythonhosted.org/packages/ae/05/4205cc97c30e82a293dd0d149b1a89b138ebe76afeca66fc129fa2aa4e6a/orjson-3.11.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26693dde66910078229a943e80eeb99fdce6cd2c26277dc80ead9f3ab97d2131", size = 111382 }, + { url = "https://files.pythonhosted.org/packages/50/c7/b8a951a93caa821f9272a7c917115d825ae2e4e8768f5ddf37968ec9de01/orjson-3.11.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4ad4c8acb50a28211c33fc7ef85ddf5cb18d4636a5205fd3fa2dce0411a0e30c", size = 116271 }, + { url = "https://files.pythonhosted.org/packages/17/03/1006c7f8782d5327439e26d9b0ec66500ea7b679d4bbb6b891d2834ab3ee/orjson-3.11.2-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:994181e7f1725bb5f2d481d7d228738e0743b16bf319ca85c29369c65913df14", size = 119086 }, + { url = "https://files.pythonhosted.org/packages/44/61/57d22bc31f36a93878a6f772aea76b2184102c6993dea897656a66d18c74/orjson-3.11.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dbb79a0476393c07656b69c8e763c3cc925fa8e1d9e9b7d1f626901bb5025448", size = 120724 }, + { url = "https://files.pythonhosted.org/packages/78/a9/4550e96b4c490c83aea697d5347b8f7eb188152cd7b5a38001055ca5b379/orjson-3.11.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:191ed27a1dddb305083d8716af413d7219f40ec1d4c9b0e977453b4db0d6fb6c", size = 123577 }, + { url = "https://files.pythonhosted.org/packages/3a/86/09b8cb3ebd513d708ef0c92d36ac3eebda814c65c72137b0a82d6d688fc4/orjson-3.11.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0afb89f16f07220183fd00f5f297328ed0a68d8722ad1b0c8dcd95b12bc82804", size = 121195 }, + { url = "https://files.pythonhosted.org/packages/37/68/7b40b39ac2c1c644d4644e706d0de6c9999764341cd85f2a9393cb387661/orjson-3.11.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6ab6e6b4e93b1573a026b6ec16fca9541354dd58e514b62c558b58554ae04307", size = 119234 }, + { url = "https://files.pythonhosted.org/packages/40/7c/bb6e7267cd80c19023d44d8cbc4ea4ed5429fcd4a7eb9950f50305697a28/orjson-3.11.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:9cb23527efb61fb75527df55d20ee47989c4ee34e01a9c98ee9ede232abf6219", size = 392250 }, + { url = "https://files.pythonhosted.org/packages/64/f2/6730ace05583dbca7c1b406d59f4266e48cd0d360566e71482420fb849fc/orjson-3.11.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a4dd1268e4035af21b8a09e4adf2e61f87ee7bf63b86d7bb0a237ac03fad5b45", size = 134572 }, + { url = "https://files.pythonhosted.org/packages/96/0f/7d3e03a30d5aac0432882b539a65b8c02cb6dd4221ddb893babf09c424cc/orjson-3.11.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ff8b155b145eaf5a9d94d2c476fbe18d6021de93cf36c2ae2c8c5b775763f14e", size = 123869 }, + { url = "https://files.pythonhosted.org/packages/45/80/1513265eba6d4a960f078f4b1d2bff94a571ab2d28c6f9835e03dfc65cc6/orjson-3.11.2-cp312-cp312-win32.whl", hash = "sha256:ae3bb10279d57872f9aba68c9931aa71ed3b295fa880f25e68da79e79453f46e", size = 124430 }, + { url = "https://files.pythonhosted.org/packages/fb/61/eadf057b68a332351eeb3d89a4cc538d14f31cd8b5ec1b31a280426ccca2/orjson-3.11.2-cp312-cp312-win_amd64.whl", hash = "sha256:d026e1967239ec11a2559b4146a61d13914504b396f74510a1c4d6b19dfd8732", size = 119598 }, + { url = "https://files.pythonhosted.org/packages/6b/3f/7f4b783402143d965ab7e9a2fc116fdb887fe53bdce7d3523271cd106098/orjson-3.11.2-cp312-cp312-win_arm64.whl", hash = "sha256:59f8d5ad08602711af9589375be98477d70e1d102645430b5a7985fdbf613b36", size = 114052 }, + { url = "https://files.pythonhosted.org/packages/c2/f3/0dd6b4750eb556ae4e2c6a9cb3e219ec642e9c6d95f8ebe5dc9020c67204/orjson-3.11.2-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a079fdba7062ab396380eeedb589afb81dc6683f07f528a03b6f7aae420a0219", size = 226419 }, + { url = "https://files.pythonhosted.org/packages/44/d5/e67f36277f78f2af8a4690e0c54da6b34169812f807fd1b4bfc4dbcf9558/orjson-3.11.2-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:6a5f62ebbc530bb8bb4b1ead103647b395ba523559149b91a6c545f7cd4110ad", size = 115803 }, + { url = "https://files.pythonhosted.org/packages/24/37/ff8bc86e0dacc48f07c2b6e20852f230bf4435611bab65e3feae2b61f0ae/orjson-3.11.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7df6c7b8b0931feb3420b72838c3e2ba98c228f7aa60d461bc050cf4ca5f7b2", size = 111337 }, + { url = "https://files.pythonhosted.org/packages/b9/25/37d4d3e8079ea9784ea1625029988e7f4594ce50d4738b0c1e2bf4a9e201/orjson-3.11.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6f59dfea7da1fced6e782bb3699718088b1036cb361f36c6e4dd843c5111aefe", size = 116222 }, + { url = "https://files.pythonhosted.org/packages/b7/32/a63fd9c07fce3b4193dcc1afced5dd4b0f3a24e27556604e9482b32189c9/orjson-3.11.2-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edf49146520fef308c31aa4c45b9925fd9c7584645caca7c0c4217d7900214ae", size = 119020 }, + { url = "https://files.pythonhosted.org/packages/b4/b6/400792b8adc3079a6b5d649264a3224d6342436d9fac9a0ed4abc9dc4596/orjson-3.11.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50995bbeb5d41a32ad15e023305807f561ac5dcd9bd41a12c8d8d1d2c83e44e6", size = 120721 }, + { url = "https://files.pythonhosted.org/packages/40/f3/31ab8f8c699eb9e65af8907889a0b7fef74c1d2b23832719a35da7bb0c58/orjson-3.11.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2cc42960515076eb639b705f105712b658c525863d89a1704d984b929b0577d1", size = 123574 }, + { url = "https://files.pythonhosted.org/packages/bd/a6/ce4287c412dff81878f38d06d2c80845709c60012ca8daf861cb064b4574/orjson-3.11.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c56777cab2a7b2a8ea687fedafb84b3d7fdafae382165c31a2adf88634c432fa", size = 121225 }, + { url = "https://files.pythonhosted.org/packages/69/b0/7a881b2aef4fed0287d2a4fbb029d01ed84fa52b4a68da82bdee5e50598e/orjson-3.11.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:07349e88025b9b5c783077bf7a9f401ffbfb07fd20e86ec6fc5b7432c28c2c5e", size = 119201 }, + { url = "https://files.pythonhosted.org/packages/cf/98/a325726b37f7512ed6338e5e65035c3c6505f4e628b09a5daf0419f054ea/orjson-3.11.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:45841fbb79c96441a8c58aa29ffef570c5df9af91f0f7a9572e5505e12412f15", size = 392193 }, + { url = "https://files.pythonhosted.org/packages/cb/4f/a7194f98b0ce1d28190e0c4caa6d091a3fc8d0107ad2209f75c8ba398984/orjson-3.11.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:13d8d8db6cd8d89d4d4e0f4161acbbb373a4d2a4929e862d1d2119de4aa324ac", size = 134548 }, + { url = "https://files.pythonhosted.org/packages/e8/5e/b84caa2986c3f472dc56343ddb0167797a708a8d5c3be043e1e2677b55df/orjson-3.11.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51da1ee2178ed09c00d09c1b953e45846bbc16b6420965eb7a913ba209f606d8", size = 123798 }, + { url = "https://files.pythonhosted.org/packages/9c/5b/e398449080ce6b4c8fcadad57e51fa16f65768e1b142ba90b23ac5d10801/orjson-3.11.2-cp313-cp313-win32.whl", hash = "sha256:51dc033df2e4a4c91c0ba4f43247de99b3cbf42ee7a42ee2b2b2f76c8b2f2cb5", size = 124402 }, + { url = "https://files.pythonhosted.org/packages/b3/66/429e4608e124debfc4790bfc37131f6958e59510ba3b542d5fc163be8e5f/orjson-3.11.2-cp313-cp313-win_amd64.whl", hash = "sha256:29d91d74942b7436f29b5d1ed9bcfc3f6ef2d4f7c4997616509004679936650d", size = 119498 }, + { url = "https://files.pythonhosted.org/packages/7b/04/f8b5f317cce7ad3580a9ad12d7e2df0714dfa8a83328ecddd367af802f5b/orjson-3.11.2-cp313-cp313-win_arm64.whl", hash = "sha256:4ca4fb5ac21cd1e48028d4f708b1bb13e39c42d45614befd2ead004a8bba8535", size = 114051 }, + { url = "https://files.pythonhosted.org/packages/74/83/2c363022b26c3c25b3708051a19d12f3374739bb81323f05b284392080c0/orjson-3.11.2-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:3dcba7101ea6a8d4ef060746c0f2e7aa8e2453a1012083e1ecce9726d7554cb7", size = 226406 }, + { url = "https://files.pythonhosted.org/packages/b0/a7/aa3c973de0b33fc93b4bd71691665ffdfeae589ea9d0625584ab10a7d0f5/orjson-3.11.2-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:15d17bdb76a142e1f55d91913e012e6e6769659daa6bfef3ef93f11083137e81", size = 115788 }, + { url = "https://files.pythonhosted.org/packages/ef/f2/e45f233dfd09fdbb052ec46352363dca3906618e1a2b264959c18f809d0b/orjson-3.11.2-cp314-cp314-manylinux_2_34_aarch64.whl", hash = "sha256:53c9e81768c69d4b66b8876ec3c8e431c6e13477186d0db1089d82622bccd19f", size = 111318 }, + { url = "https://files.pythonhosted.org/packages/3e/23/cf5a73c4da6987204cbbf93167f353ff0c5013f7c5e5ef845d4663a366da/orjson-3.11.2-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:d4f13af59a7b84c1ca6b8a7ab70d608f61f7c44f9740cd42409e6ae7b6c8d8b7", size = 121231 }, + { url = "https://files.pythonhosted.org/packages/40/1d/47468a398ae68a60cc21e599144e786e035bb12829cb587299ecebc088f1/orjson-3.11.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bde64aa469b5ee46cc960ed241fae3721d6a8801dacb2ca3466547a2535951e4", size = 119204 }, + { url = "https://files.pythonhosted.org/packages/4d/d9/f99433d89b288b5bc8836bffb32a643f805e673cf840ef8bab6e73ced0d1/orjson-3.11.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:b5ca86300aeb383c8fa759566aca065878d3d98c3389d769b43f0a2e84d52c5f", size = 392237 }, + { url = "https://files.pythonhosted.org/packages/d4/dc/1b9d80d40cebef603325623405136a29fb7d08c877a728c0943dd066c29a/orjson-3.11.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:24e32a558ebed73a6a71c8f1cbc163a7dd5132da5270ff3d8eeb727f4b6d1bc7", size = 134578 }, + { url = "https://files.pythonhosted.org/packages/45/b3/72e7a4c5b6485ef4e83ef6aba7f1dd041002bad3eb5d1d106ca5b0fc02c6/orjson-3.11.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e36319a5d15b97e4344110517450396845cc6789aed712b1fbf83c1bd95792f6", size = 123799 }, + { url = "https://files.pythonhosted.org/packages/c8/3e/a3d76b392e7acf9b34dc277171aad85efd6accc75089bb35b4c614990ea9/orjson-3.11.2-cp314-cp314-win32.whl", hash = "sha256:40193ada63fab25e35703454d65b6afc71dbc65f20041cb46c6d91709141ef7f", size = 124461 }, + { url = "https://files.pythonhosted.org/packages/fb/e3/75c6a596ff8df9e4a5894813ff56695f0a218e6ea99420b4a645c4f7795d/orjson-3.11.2-cp314-cp314-win_amd64.whl", hash = "sha256:7c8ac5f6b682d3494217085cf04dadae66efee45349ad4ee2a1da3c97e2305a8", size = 119494 }, + { url = "https://files.pythonhosted.org/packages/5b/3d/9e74742fc261c5ca473c96bb3344d03995869e1dc6402772c60afb97736a/orjson-3.11.2-cp314-cp314-win_arm64.whl", hash = "sha256:21cf261e8e79284242e4cb1e5924df16ae28255184aafeff19be1405f6d33f67", size = 114046 }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 }, +] + +[[package]] +name = "platformdirs" +version = "4.3.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567 }, +] + +[[package]] +name = "postgrest" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "deprecation" }, + { name = "httpx", extra = ["http2"] }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6e/3e/1b50568e1f5db0bdced4a82c7887e37326585faef7ca43ead86849cb4861/postgrest-1.1.1.tar.gz", hash = "sha256:f3bb3e8c4602775c75c844a31f565f5f3dd584df4d36d683f0b67d01a86be322", size = 15431 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/71/188a50ea64c17f73ff4df5196ec1553a8f1723421eb2d1069c73bab47d78/postgrest-1.1.1-py3-none-any.whl", hash = "sha256:98a6035ee1d14288484bfe36235942c5fb2d26af6d8120dfe3efbe007859251a", size = 22366 }, +] + +[[package]] +name = "pre-commit" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/08/39/679ca9b26c7bb2999ff122d50faa301e49af82ca9c066ec061cfbc0c6784/pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146", size = 193424 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd", size = 220707 }, +] + +[[package]] +name = "propcache" +version = "0.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/16/43264e4a779dd8588c21a70f0709665ee8f611211bdd2c87d952cfa7c776/propcache-0.3.2.tar.gz", hash = "sha256:20d7d62e4e7ef05f221e0db2856b979540686342e7dd9973b815599c7057e168", size = 44139 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/42/9ca01b0a6f48e81615dca4765a8f1dd2c057e0540f6116a27dc5ee01dfb6/propcache-0.3.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8de106b6c84506b31c27168582cd3cb3000a6412c16df14a8628e5871ff83c10", size = 73674 }, + { url = "https://files.pythonhosted.org/packages/af/6e/21293133beb550f9c901bbece755d582bfaf2176bee4774000bd4dd41884/propcache-0.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:28710b0d3975117239c76600ea351934ac7b5ff56e60953474342608dbbb6154", size = 43570 }, + { url = "https://files.pythonhosted.org/packages/0c/c8/0393a0a3a2b8760eb3bde3c147f62b20044f0ddac81e9d6ed7318ec0d852/propcache-0.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce26862344bdf836650ed2487c3d724b00fbfec4233a1013f597b78c1cb73615", size = 43094 }, + { url = "https://files.pythonhosted.org/packages/37/2c/489afe311a690399d04a3e03b069225670c1d489eb7b044a566511c1c498/propcache-0.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bca54bd347a253af2cf4544bbec232ab982f4868de0dd684246b67a51bc6b1db", size = 226958 }, + { url = "https://files.pythonhosted.org/packages/9d/ca/63b520d2f3d418c968bf596839ae26cf7f87bead026b6192d4da6a08c467/propcache-0.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55780d5e9a2ddc59711d727226bb1ba83a22dd32f64ee15594b9392b1f544eb1", size = 234894 }, + { url = "https://files.pythonhosted.org/packages/11/60/1d0ed6fff455a028d678df30cc28dcee7af77fa2b0e6962ce1df95c9a2a9/propcache-0.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:035e631be25d6975ed87ab23153db6a73426a48db688070d925aa27e996fe93c", size = 233672 }, + { url = "https://files.pythonhosted.org/packages/37/7c/54fd5301ef38505ab235d98827207176a5c9b2aa61939b10a460ca53e123/propcache-0.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee6f22b6eaa39297c751d0e80c0d3a454f112f5c6481214fcf4c092074cecd67", size = 224395 }, + { url = "https://files.pythonhosted.org/packages/ee/1a/89a40e0846f5de05fdc6779883bf46ba980e6df4d2ff8fb02643de126592/propcache-0.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ca3aee1aa955438c4dba34fc20a9f390e4c79967257d830f137bd5a8a32ed3b", size = 212510 }, + { url = "https://files.pythonhosted.org/packages/5e/33/ca98368586c9566a6b8d5ef66e30484f8da84c0aac3f2d9aec6d31a11bd5/propcache-0.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7a4f30862869fa2b68380d677cc1c5fcf1e0f2b9ea0cf665812895c75d0ca3b8", size = 222949 }, + { url = "https://files.pythonhosted.org/packages/ba/11/ace870d0aafe443b33b2f0b7efdb872b7c3abd505bfb4890716ad7865e9d/propcache-0.3.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b77ec3c257d7816d9f3700013639db7491a434644c906a2578a11daf13176251", size = 217258 }, + { url = "https://files.pythonhosted.org/packages/5b/d2/86fd6f7adffcfc74b42c10a6b7db721d1d9ca1055c45d39a1a8f2a740a21/propcache-0.3.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cab90ac9d3f14b2d5050928483d3d3b8fb6b4018893fc75710e6aa361ecb2474", size = 213036 }, + { url = "https://files.pythonhosted.org/packages/07/94/2d7d1e328f45ff34a0a284cf5a2847013701e24c2a53117e7c280a4316b3/propcache-0.3.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0b504d29f3c47cf6b9e936c1852246c83d450e8e063d50562115a6be6d3a2535", size = 227684 }, + { url = "https://files.pythonhosted.org/packages/b7/05/37ae63a0087677e90b1d14710e532ff104d44bc1efa3b3970fff99b891dc/propcache-0.3.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:ce2ac2675a6aa41ddb2a0c9cbff53780a617ac3d43e620f8fd77ba1c84dcfc06", size = 234562 }, + { url = "https://files.pythonhosted.org/packages/a4/7c/3f539fcae630408d0bd8bf3208b9a647ccad10976eda62402a80adf8fc34/propcache-0.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b4239611205294cc433845b914131b2a1f03500ff3c1ed093ed216b82621e1", size = 222142 }, + { url = "https://files.pythonhosted.org/packages/7c/d2/34b9eac8c35f79f8a962546b3e97e9d4b990c420ee66ac8255d5d9611648/propcache-0.3.2-cp312-cp312-win32.whl", hash = "sha256:df4a81b9b53449ebc90cc4deefb052c1dd934ba85012aa912c7ea7b7e38b60c1", size = 37711 }, + { url = "https://files.pythonhosted.org/packages/19/61/d582be5d226cf79071681d1b46b848d6cb03d7b70af7063e33a2787eaa03/propcache-0.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:7046e79b989d7fe457bb755844019e10f693752d169076138abf17f31380800c", size = 41479 }, + { url = "https://files.pythonhosted.org/packages/dc/d1/8c747fafa558c603c4ca19d8e20b288aa0c7cda74e9402f50f31eb65267e/propcache-0.3.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ca592ed634a73ca002967458187109265e980422116c0a107cf93d81f95af945", size = 71286 }, + { url = "https://files.pythonhosted.org/packages/61/99/d606cb7986b60d89c36de8a85d58764323b3a5ff07770a99d8e993b3fa73/propcache-0.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9ecb0aad4020e275652ba3975740f241bd12a61f1a784df044cf7477a02bc252", size = 42425 }, + { url = "https://files.pythonhosted.org/packages/8c/96/ef98f91bbb42b79e9bb82bdd348b255eb9d65f14dbbe3b1594644c4073f7/propcache-0.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7f08f1cc28bd2eade7a8a3d2954ccc673bb02062e3e7da09bc75d843386b342f", size = 41846 }, + { url = "https://files.pythonhosted.org/packages/5b/ad/3f0f9a705fb630d175146cd7b1d2bf5555c9beaed54e94132b21aac098a6/propcache-0.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1a342c834734edb4be5ecb1e9fb48cb64b1e2320fccbd8c54bf8da8f2a84c33", size = 208871 }, + { url = "https://files.pythonhosted.org/packages/3a/38/2085cda93d2c8b6ec3e92af2c89489a36a5886b712a34ab25de9fbca7992/propcache-0.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a544caaae1ac73f1fecfae70ded3e93728831affebd017d53449e3ac052ac1e", size = 215720 }, + { url = "https://files.pythonhosted.org/packages/61/c1/d72ea2dc83ac7f2c8e182786ab0fc2c7bd123a1ff9b7975bee671866fe5f/propcache-0.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:310d11aa44635298397db47a3ebce7db99a4cc4b9bbdfcf6c98a60c8d5261cf1", size = 215203 }, + { url = "https://files.pythonhosted.org/packages/af/81/b324c44ae60c56ef12007105f1460d5c304b0626ab0cc6b07c8f2a9aa0b8/propcache-0.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c1396592321ac83157ac03a2023aa6cc4a3cc3cfdecb71090054c09e5a7cce3", size = 206365 }, + { url = "https://files.pythonhosted.org/packages/09/73/88549128bb89e66d2aff242488f62869014ae092db63ccea53c1cc75a81d/propcache-0.3.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cabf5b5902272565e78197edb682017d21cf3b550ba0460ee473753f28d23c1", size = 196016 }, + { url = "https://files.pythonhosted.org/packages/b9/3f/3bdd14e737d145114a5eb83cb172903afba7242f67c5877f9909a20d948d/propcache-0.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0a2f2235ac46a7aa25bdeb03a9e7060f6ecbd213b1f9101c43b3090ffb971ef6", size = 205596 }, + { url = "https://files.pythonhosted.org/packages/0f/ca/2f4aa819c357d3107c3763d7ef42c03980f9ed5c48c82e01e25945d437c1/propcache-0.3.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:92b69e12e34869a6970fd2f3da91669899994b47c98f5d430b781c26f1d9f387", size = 200977 }, + { url = "https://files.pythonhosted.org/packages/cd/4a/e65276c7477533c59085251ae88505caf6831c0e85ff8b2e31ebcbb949b1/propcache-0.3.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:54e02207c79968ebbdffc169591009f4474dde3b4679e16634d34c9363ff56b4", size = 197220 }, + { url = "https://files.pythonhosted.org/packages/7c/54/fc7152e517cf5578278b242396ce4d4b36795423988ef39bb8cd5bf274c8/propcache-0.3.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4adfb44cb588001f68c5466579d3f1157ca07f7504fc91ec87862e2b8e556b88", size = 210642 }, + { url = "https://files.pythonhosted.org/packages/b9/80/abeb4a896d2767bf5f1ea7b92eb7be6a5330645bd7fb844049c0e4045d9d/propcache-0.3.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fd3e6019dc1261cd0291ee8919dd91fbab7b169bb76aeef6c716833a3f65d206", size = 212789 }, + { url = "https://files.pythonhosted.org/packages/b3/db/ea12a49aa7b2b6d68a5da8293dcf50068d48d088100ac016ad92a6a780e6/propcache-0.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4c181cad81158d71c41a2bce88edce078458e2dd5ffee7eddd6b05da85079f43", size = 205880 }, + { url = "https://files.pythonhosted.org/packages/d1/e5/9076a0bbbfb65d1198007059c65639dfd56266cf8e477a9707e4b1999ff4/propcache-0.3.2-cp313-cp313-win32.whl", hash = "sha256:8a08154613f2249519e549de2330cf8e2071c2887309a7b07fb56098f5170a02", size = 37220 }, + { url = "https://files.pythonhosted.org/packages/d3/f5/b369e026b09a26cd77aa88d8fffd69141d2ae00a2abaaf5380d2603f4b7f/propcache-0.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e41671f1594fc4ab0a6dec1351864713cb3a279910ae8b58f884a88a0a632c05", size = 40678 }, + { url = "https://files.pythonhosted.org/packages/a4/3a/6ece377b55544941a08d03581c7bc400a3c8cd3c2865900a68d5de79e21f/propcache-0.3.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:9a3cf035bbaf035f109987d9d55dc90e4b0e36e04bbbb95af3055ef17194057b", size = 76560 }, + { url = "https://files.pythonhosted.org/packages/0c/da/64a2bb16418740fa634b0e9c3d29edff1db07f56d3546ca2d86ddf0305e1/propcache-0.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:156c03d07dc1323d8dacaa221fbe028c5c70d16709cdd63502778e6c3ccca1b0", size = 44676 }, + { url = "https://files.pythonhosted.org/packages/36/7b/f025e06ea51cb72c52fb87e9b395cced02786610b60a3ed51da8af017170/propcache-0.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74413c0ba02ba86f55cf60d18daab219f7e531620c15f1e23d95563f505efe7e", size = 44701 }, + { url = "https://files.pythonhosted.org/packages/a4/00/faa1b1b7c3b74fc277f8642f32a4c72ba1d7b2de36d7cdfb676db7f4303e/propcache-0.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f066b437bb3fa39c58ff97ab2ca351db465157d68ed0440abecb21715eb24b28", size = 276934 }, + { url = "https://files.pythonhosted.org/packages/74/ab/935beb6f1756e0476a4d5938ff44bf0d13a055fed880caf93859b4f1baf4/propcache-0.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1304b085c83067914721e7e9d9917d41ad87696bf70f0bc7dee450e9c71ad0a", size = 278316 }, + { url = "https://files.pythonhosted.org/packages/f8/9d/994a5c1ce4389610838d1caec74bdf0e98b306c70314d46dbe4fcf21a3e2/propcache-0.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab50cef01b372763a13333b4e54021bdcb291fc9a8e2ccb9c2df98be51bcde6c", size = 282619 }, + { url = "https://files.pythonhosted.org/packages/2b/00/a10afce3d1ed0287cef2e09506d3be9822513f2c1e96457ee369adb9a6cd/propcache-0.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fad3b2a085ec259ad2c2842666b2a0a49dea8463579c606426128925af1ed725", size = 265896 }, + { url = "https://files.pythonhosted.org/packages/2e/a8/2aa6716ffa566ca57c749edb909ad27884680887d68517e4be41b02299f3/propcache-0.3.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:261fa020c1c14deafd54c76b014956e2f86991af198c51139faf41c4d5e83892", size = 252111 }, + { url = "https://files.pythonhosted.org/packages/36/4f/345ca9183b85ac29c8694b0941f7484bf419c7f0fea2d1e386b4f7893eed/propcache-0.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:46d7f8aa79c927e5f987ee3a80205c987717d3659f035c85cf0c3680526bdb44", size = 268334 }, + { url = "https://files.pythonhosted.org/packages/3e/ca/fcd54f78b59e3f97b3b9715501e3147f5340167733d27db423aa321e7148/propcache-0.3.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:6d8f3f0eebf73e3c0ff0e7853f68be638b4043c65a70517bb575eff54edd8dbe", size = 255026 }, + { url = "https://files.pythonhosted.org/packages/8b/95/8e6a6bbbd78ac89c30c225210a5c687790e532ba4088afb8c0445b77ef37/propcache-0.3.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:03c89c1b14a5452cf15403e291c0ccd7751d5b9736ecb2c5bab977ad6c5bcd81", size = 250724 }, + { url = "https://files.pythonhosted.org/packages/ee/b0/0dd03616142baba28e8b2d14ce5df6631b4673850a3d4f9c0f9dd714a404/propcache-0.3.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:0cc17efde71e12bbaad086d679ce575268d70bc123a5a71ea7ad76f70ba30bba", size = 268868 }, + { url = "https://files.pythonhosted.org/packages/c5/98/2c12407a7e4fbacd94ddd32f3b1e3d5231e77c30ef7162b12a60e2dd5ce3/propcache-0.3.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:acdf05d00696bc0447e278bb53cb04ca72354e562cf88ea6f9107df8e7fd9770", size = 271322 }, + { url = "https://files.pythonhosted.org/packages/35/91/9cb56efbb428b006bb85db28591e40b7736847b8331d43fe335acf95f6c8/propcache-0.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4445542398bd0b5d32df908031cb1b30d43ac848e20470a878b770ec2dcc6330", size = 265778 }, + { url = "https://files.pythonhosted.org/packages/9a/4c/b0fe775a2bdd01e176b14b574be679d84fc83958335790f7c9a686c1f468/propcache-0.3.2-cp313-cp313t-win32.whl", hash = "sha256:f86e5d7cd03afb3a1db8e9f9f6eff15794e79e791350ac48a8c924e6f439f394", size = 41175 }, + { url = "https://files.pythonhosted.org/packages/a4/ff/47f08595e3d9b5e149c150f88d9714574f1a7cbd89fe2817158a952674bf/propcache-0.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9704bedf6e7cbe3c65eca4379a9b53ee6a83749f047808cbb5044d40d7d72198", size = 44857 }, + { url = "https://files.pythonhosted.org/packages/cc/35/cc0aaecf278bb4575b8555f2b137de5ab821595ddae9da9d3cd1da4072c7/propcache-0.3.2-py3-none-any.whl", hash = "sha256:98f1ec44fb675f5052cccc8e609c46ed23a35a1cfd18545ad4e29002d858a43f", size = 12663 }, +] + +[[package]] +name = "pscript" +version = "0.7.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/59/68/f918702e270eddc5f7c54108f6a2f2afc2d299985820dbb0db9beb77d66d/pscript-0.7.7.tar.gz", hash = "sha256:8632f7a4483f235514aadee110edee82eb6d67336bf68744a7b18d76e50442f8", size = 176138 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/bc/980e2ebd442d2a8f1d22780f73db76f2a1df3bf79b3fb501b054b4b4dd03/pscript-0.7.7-py3-none-any.whl", hash = "sha256:b0fdac0df0393a4d7497153fea6a82e6429f32327c4c0a4817f1cd68adc08083", size = 126689 }, +] + +[[package]] +name = "psycopg" +version = "3.2.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/27/4a/93a6ab570a8d1a4ad171a1f4256e205ce48d828781312c0bbaff36380ecb/psycopg-3.2.9.tar.gz", hash = "sha256:2fbb46fcd17bc81f993f28c47f1ebea38d66ae97cc2dbc3cad73b37cefbff700", size = 158122 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/b0/a73c195a56eb6b92e937a5ca58521a5c3346fb233345adc80fd3e2f542e2/psycopg-3.2.9-py3-none-any.whl", hash = "sha256:01a8dadccdaac2123c916208c96e06631641c0566b22005493f09663c7a8d3b6", size = 202705 }, +] + +[package.optional-dependencies] +binary = [ + { name = "psycopg-binary", marker = "implementation_name != 'pypy'" }, +] +pool = [ + { name = "psycopg-pool" }, +] + +[[package]] +name = "psycopg-binary" +version = "3.2.9" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/6f/ec9957e37a606cd7564412e03f41f1b3c3637a5be018d0849914cb06e674/psycopg_binary-3.2.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:be7d650a434921a6b1ebe3fff324dbc2364393eb29d7672e638ce3e21076974e", size = 4022205 }, + { url = "https://files.pythonhosted.org/packages/6b/ba/497b8bea72b20a862ac95a94386967b745a472d9ddc88bc3f32d5d5f0d43/psycopg_binary-3.2.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6a76b4722a529390683c0304501f238b365a46b1e5fb6b7249dbc0ad6fea51a0", size = 4083795 }, + { url = "https://files.pythonhosted.org/packages/42/07/af9503e8e8bdad3911fd88e10e6a29240f9feaa99f57d6fac4a18b16f5a0/psycopg_binary-3.2.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96a551e4683f1c307cfc3d9a05fec62c00a7264f320c9962a67a543e3ce0d8ff", size = 4655043 }, + { url = "https://files.pythonhosted.org/packages/28/ed/aff8c9850df1648cc6a5cc7a381f11ee78d98a6b807edd4a5ae276ad60ad/psycopg_binary-3.2.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:61d0a6ceed8f08c75a395bc28cb648a81cf8dee75ba4650093ad1a24a51c8724", size = 4477972 }, + { url = "https://files.pythonhosted.org/packages/5c/bd/8e9d1b77ec1a632818fe2f457c3a65af83c68710c4c162d6866947d08cc5/psycopg_binary-3.2.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad280bbd409bf598683dda82232f5215cfc5f2b1bf0854e409b4d0c44a113b1d", size = 4737516 }, + { url = "https://files.pythonhosted.org/packages/46/ec/222238f774cd5a0881f3f3b18fb86daceae89cc410f91ef6a9fb4556f236/psycopg_binary-3.2.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76eddaf7fef1d0994e3d536ad48aa75034663d3a07f6f7e3e601105ae73aeff6", size = 4436160 }, + { url = "https://files.pythonhosted.org/packages/37/78/af5af2a1b296eeca54ea7592cd19284739a844974c9747e516707e7b3b39/psycopg_binary-3.2.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:52e239cd66c4158e412318fbe028cd94b0ef21b0707f56dcb4bdc250ee58fd40", size = 3753518 }, + { url = "https://files.pythonhosted.org/packages/ec/ac/8a3ed39ea069402e9e6e6a2f79d81a71879708b31cc3454283314994b1ae/psycopg_binary-3.2.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:08bf9d5eabba160dd4f6ad247cf12f229cc19d2458511cab2eb9647f42fa6795", size = 3313598 }, + { url = "https://files.pythonhosted.org/packages/da/43/26549af068347c808fbfe5f07d2fa8cef747cfff7c695136172991d2378b/psycopg_binary-3.2.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1b2cf018168cad87580e67bdde38ff5e51511112f1ce6ce9a8336871f465c19a", size = 3407289 }, + { url = "https://files.pythonhosted.org/packages/67/55/ea8d227c77df8e8aec880ded398316735add8fda5eb4ff5cc96fac11e964/psycopg_binary-3.2.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:14f64d1ac6942ff089fc7e926440f7a5ced062e2ed0949d7d2d680dc5c00e2d4", size = 3472493 }, + { url = "https://files.pythonhosted.org/packages/3c/02/6ff2a5bc53c3cd653d281666728e29121149179c73fddefb1e437024c192/psycopg_binary-3.2.9-cp312-cp312-win_amd64.whl", hash = "sha256:7a838852e5afb6b4126f93eb409516a8c02a49b788f4df8b6469a40c2157fa21", size = 2927400 }, + { url = "https://files.pythonhosted.org/packages/28/0b/f61ff4e9f23396aca674ed4d5c9a5b7323738021d5d72d36d8b865b3deaf/psycopg_binary-3.2.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:98bbe35b5ad24a782c7bf267596638d78aa0e87abc7837bdac5b2a2ab954179e", size = 4017127 }, + { url = "https://files.pythonhosted.org/packages/bc/00/7e181fb1179fbfc24493738b61efd0453d4b70a0c4b12728e2b82db355fd/psycopg_binary-3.2.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:72691a1615ebb42da8b636c5ca9f2b71f266be9e172f66209a361c175b7842c5", size = 4080322 }, + { url = "https://files.pythonhosted.org/packages/58/fd/94fc267c1d1392c4211e54ccb943be96ea4032e761573cf1047951887494/psycopg_binary-3.2.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25ab464bfba8c401f5536d5aa95f0ca1dd8257b5202eede04019b4415f491351", size = 4655097 }, + { url = "https://files.pythonhosted.org/packages/41/17/31b3acf43de0b2ba83eac5878ff0dea5a608ca2a5c5dd48067999503a9de/psycopg_binary-3.2.9-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e8aeefebe752f46e3c4b769e53f1d4ad71208fe1150975ef7662c22cca80fab", size = 4482114 }, + { url = "https://files.pythonhosted.org/packages/85/78/b4d75e5fd5a85e17f2beb977abbba3389d11a4536b116205846b0e1cf744/psycopg_binary-3.2.9-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7e4e4dd177a8665c9ce86bc9caae2ab3aa9360b7ce7ec01827ea1baea9ff748", size = 4737693 }, + { url = "https://files.pythonhosted.org/packages/3b/95/7325a8550e3388b00b5e54f4ced5e7346b531eb4573bf054c3dbbfdc14fe/psycopg_binary-3.2.9-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fc2915949e5c1ea27a851f7a472a7da7d0a40d679f0a31e42f1022f3c562e87", size = 4437423 }, + { url = "https://files.pythonhosted.org/packages/1a/db/cef77d08e59910d483df4ee6da8af51c03bb597f500f1fe818f0f3b925d3/psycopg_binary-3.2.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a1fa38a4687b14f517f049477178093c39c2a10fdcced21116f47c017516498f", size = 3758667 }, + { url = "https://files.pythonhosted.org/packages/95/3e/252fcbffb47189aa84d723b54682e1bb6d05c8875fa50ce1ada914ae6e28/psycopg_binary-3.2.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5be8292d07a3ab828dc95b5ee6b69ca0a5b2e579a577b39671f4f5b47116dfd2", size = 3320576 }, + { url = "https://files.pythonhosted.org/packages/1c/cd/9b5583936515d085a1bec32b45289ceb53b80d9ce1cea0fef4c782dc41a7/psycopg_binary-3.2.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:778588ca9897b6c6bab39b0d3034efff4c5438f5e3bd52fda3914175498202f9", size = 3411439 }, + { url = "https://files.pythonhosted.org/packages/45/6b/6f1164ea1634c87956cdb6db759e0b8c5827f989ee3cdff0f5c70e8331f2/psycopg_binary-3.2.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f0d5b3af045a187aedbd7ed5fc513bd933a97aaff78e61c3745b330792c4345b", size = 3477477 }, + { url = "https://files.pythonhosted.org/packages/7b/1d/bf54cfec79377929da600c16114f0da77a5f1670f45e0c3af9fcd36879bc/psycopg_binary-3.2.9-cp313-cp313-win_amd64.whl", hash = "sha256:2290bc146a1b6a9730350f695e8b670e1d1feb8446597bed0bbe7c3c30e0abcb", size = 2928009 }, +] + +[[package]] +name = "psycopg-pool" +version = "3.2.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cf/13/1e7850bb2c69a63267c3dbf37387d3f71a00fd0e2fa55c5db14d64ba1af4/psycopg_pool-3.2.6.tar.gz", hash = "sha256:0f92a7817719517212fbfe2fd58b8c35c1850cdd2a80d36b581ba2085d9148e5", size = 29770 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/fd/4feb52a55c1a4bd748f2acaed1903ab54a723c47f6d0242780f4d97104d4/psycopg_pool-3.2.6-py3-none-any.whl", hash = "sha256:5887318a9f6af906d041a0b1dc1c60f8f0dda8340c2572b74e10907b51ed5da7", size = 38252 }, +] + +[[package]] +name = "pycparser" +version = "2.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, +] + +[[package]] +name = "pydantic" +version = "2.11.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782 }, +] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000 }, + { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996 }, + { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957 }, + { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199 }, + { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296 }, + { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109 }, + { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028 }, + { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044 }, + { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881 }, + { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034 }, + { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187 }, + { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628 }, + { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866 }, + { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894 }, + { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688 }, + { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808 }, + { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580 }, + { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859 }, + { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810 }, + { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498 }, + { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611 }, + { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924 }, + { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196 }, + { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389 }, + { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223 }, + { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473 }, + { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269 }, + { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921 }, + { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162 }, + { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560 }, + { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777 }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217 }, +] + +[[package]] +name = "pyjwt" +version = "2.10.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997 }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, +] + +[[package]] +name = "python-dotenv" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556 }, +] + +[[package]] +name = "python-engineio" +version = "4.12.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "simple-websocket" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/0b/67295279b66835f9fa7a491650efcd78b20321c127036eef62c11a31e028/python_engineio-4.12.2.tar.gz", hash = "sha256:e7e712ffe1be1f6a05ee5f951e72d434854a32fcfc7f6e4d9d3cae24ec70defa", size = 91677 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/fa/df59acedf7bbb937f69174d00f921a7b93aa5a5f5c17d05296c814fff6fc/python_engineio-4.12.2-py3-none-any.whl", hash = "sha256:8218ab66950e179dfec4b4bbb30aecf3f5d86f5e58e6fc1aa7fde2c698b2804f", size = 59536 }, +] + +[[package]] +name = "python-multipart" +version = "0.0.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546 }, +] + +[[package]] +name = "python-socketio" +version = "5.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bidict" }, + { name = "python-engineio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/21/1a/396d50ccf06ee539fa758ce5623b59a9cb27637fc4b2dc07ed08bf495e77/python_socketio-5.13.0.tar.gz", hash = "sha256:ac4e19a0302ae812e23b712ec8b6427ca0521f7c582d6abb096e36e24a263029", size = 121125 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/32/b4fb8585d1be0f68bde7e110dffbcf354915f77ad8c778563f0ad9655c02/python_socketio-5.13.0-py3-none-any.whl", hash = "sha256:51f68d6499f2df8524668c24bcec13ba1414117cfb3a90115c559b601ab10caf", size = 77800 }, +] + +[package.optional-dependencies] +asyncio-client = [ + { name = "aiohttp" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, +] + +[[package]] +name = "realtime" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "typing-extensions" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d3/ca/e408fbdb6b344bf529c7e8bf020372d21114fe538392c72089462edd26e5/realtime-2.7.0.tar.gz", hash = "sha256:6b9434eeba8d756c8faf94fc0a32081d09f250d14d82b90341170602adbb019f", size = 18860 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/07/a5c7aef12f9a3497f5ad77157a37915645861e8b23b89b2ad4b0f11b48ad/realtime-2.7.0-py3-none-any.whl", hash = "sha256:d55a278803529a69d61c7174f16563a9cfa5bacc1664f656959694481903d99c", size = 22409 }, +] + +[[package]] +name = "ruff" +version = "0.12.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4b/da/5bd7565be729e86e1442dad2c9a364ceeff82227c2dece7c29697a9795eb/ruff-0.12.8.tar.gz", hash = "sha256:4cb3a45525176e1009b2b64126acf5f9444ea59066262791febf55e40493a033", size = 5242373 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/1e/c843bfa8ad1114fab3eb2b78235dda76acd66384c663a4e0415ecc13aa1e/ruff-0.12.8-py3-none-linux_armv6l.whl", hash = "sha256:63cb5a5e933fc913e5823a0dfdc3c99add73f52d139d6cd5cc8639d0e0465513", size = 11675315 }, + { url = "https://files.pythonhosted.org/packages/24/ee/af6e5c2a8ca3a81676d5480a1025494fd104b8896266502bb4de2a0e8388/ruff-0.12.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9a9bbe28f9f551accf84a24c366c1aa8774d6748438b47174f8e8565ab9dedbc", size = 12456653 }, + { url = "https://files.pythonhosted.org/packages/99/9d/e91f84dfe3866fa648c10512904991ecc326fd0b66578b324ee6ecb8f725/ruff-0.12.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2fae54e752a3150f7ee0e09bce2e133caf10ce9d971510a9b925392dc98d2fec", size = 11659690 }, + { url = "https://files.pythonhosted.org/packages/fe/ac/a363d25ec53040408ebdd4efcee929d48547665858ede0505d1d8041b2e5/ruff-0.12.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0acbcf01206df963d9331b5838fb31f3b44fa979ee7fa368b9b9057d89f4a53", size = 11896923 }, + { url = "https://files.pythonhosted.org/packages/58/9f/ea356cd87c395f6ade9bb81365bd909ff60860975ca1bc39f0e59de3da37/ruff-0.12.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ae3e7504666ad4c62f9ac8eedb52a93f9ebdeb34742b8b71cd3cccd24912719f", size = 11477612 }, + { url = "https://files.pythonhosted.org/packages/1a/46/92e8fa3c9dcfd49175225c09053916cb97bb7204f9f899c2f2baca69e450/ruff-0.12.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb82efb5d35d07497813a1c5647867390a7d83304562607f3579602fa3d7d46f", size = 13182745 }, + { url = "https://files.pythonhosted.org/packages/5e/c4/f2176a310f26e6160deaf661ef60db6c3bb62b7a35e57ae28f27a09a7d63/ruff-0.12.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:dbea798fc0065ad0b84a2947b0aff4233f0cb30f226f00a2c5850ca4393de609", size = 14206885 }, + { url = "https://files.pythonhosted.org/packages/87/9d/98e162f3eeeb6689acbedbae5050b4b3220754554526c50c292b611d3a63/ruff-0.12.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:49ebcaccc2bdad86fd51b7864e3d808aad404aab8df33d469b6e65584656263a", size = 13639381 }, + { url = "https://files.pythonhosted.org/packages/81/4e/1b7478b072fcde5161b48f64774d6edd59d6d198e4ba8918d9f4702b8043/ruff-0.12.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ac9c570634b98c71c88cb17badd90f13fc076a472ba6ef1d113d8ed3df109fb", size = 12613271 }, + { url = "https://files.pythonhosted.org/packages/e8/67/0c3c9179a3ad19791ef1b8f7138aa27d4578c78700551c60d9260b2c660d/ruff-0.12.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:560e0cd641e45591a3e42cb50ef61ce07162b9c233786663fdce2d8557d99818", size = 12847783 }, + { url = "https://files.pythonhosted.org/packages/4e/2a/0b6ac3dd045acf8aa229b12c9c17bb35508191b71a14904baf99573a21bd/ruff-0.12.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:71c83121512e7743fba5a8848c261dcc454cafb3ef2934a43f1b7a4eb5a447ea", size = 11702672 }, + { url = "https://files.pythonhosted.org/packages/9d/ee/f9fdc9f341b0430110de8b39a6ee5fa68c5706dc7c0aa940817947d6937e/ruff-0.12.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:de4429ef2ba091ecddedd300f4c3f24bca875d3d8b23340728c3cb0da81072c3", size = 11440626 }, + { url = "https://files.pythonhosted.org/packages/89/fb/b3aa2d482d05f44e4d197d1de5e3863feb13067b22c571b9561085c999dc/ruff-0.12.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a2cab5f60d5b65b50fba39a8950c8746df1627d54ba1197f970763917184b161", size = 12462162 }, + { url = "https://files.pythonhosted.org/packages/18/9f/5c5d93e1d00d854d5013c96e1a92c33b703a0332707a7cdbd0a4880a84fb/ruff-0.12.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:45c32487e14f60b88aad6be9fd5da5093dbefb0e3e1224131cb1d441d7cb7d46", size = 12913212 }, + { url = "https://files.pythonhosted.org/packages/71/13/ab9120add1c0e4604c71bfc2e4ef7d63bebece0cfe617013da289539cef8/ruff-0.12.8-py3-none-win32.whl", hash = "sha256:daf3475060a617fd5bc80638aeaf2f5937f10af3ec44464e280a9d2218e720d3", size = 11694382 }, + { url = "https://files.pythonhosted.org/packages/f6/dc/a2873b7c5001c62f46266685863bee2888caf469d1edac84bf3242074be2/ruff-0.12.8-py3-none-win_amd64.whl", hash = "sha256:7209531f1a1fcfbe8e46bcd7ab30e2f43604d8ba1c49029bb420b103d0b5f76e", size = 12740482 }, + { url = "https://files.pythonhosted.org/packages/cb/5c/799a1efb8b5abab56e8a9f2a0b72d12bd64bb55815e9476c7d0a2887d2f7/ruff-0.12.8-py3-none-win_arm64.whl", hash = "sha256:c90e1a334683ce41b0e7a04f41790c429bf5073b62c1ae701c9dc5b3d14f0749", size = 11884718 }, +] + +[[package]] +name = "simple-websocket" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wsproto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b0/d4/bfa032f961103eba93de583b161f0e6a5b63cebb8f2c7d0c6e6efe1e3d2e/simple_websocket-1.1.0.tar.gz", hash = "sha256:7939234e7aa067c534abdab3a9ed933ec9ce4691b0713c78acb195560aa52ae4", size = 17300 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl", hash = "sha256:4af6069630a38ed6c561010f0e11a5bc0d4ca569b36306eb257cd9a192497c8c", size = 13842 }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, +] + +[[package]] +name = "starlette" +version = "0.47.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/57/d062573f391d062710d4088fa1369428c38d51460ab6fedff920efef932e/starlette-0.47.2.tar.gz", hash = "sha256:6ae9aa5db235e4846decc1e7b79c4f346adf41e9777aebeb49dfd09bbd7023d8", size = 2583948 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/1f/b876b1f83aef204198a42dc101613fefccb32258e5428b5f9259677864b4/starlette-0.47.2-py3-none-any.whl", hash = "sha256:c5847e96134e5c5371ee9fac6fdf1a67336d5815e09eb2a01fdb57a351ef915b", size = 72984 }, +] + +[[package]] +name = "storage3" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "deprecation" }, + { name = "httpx", extra = ["http2"] }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/e2/280fe75f65e7a3ca680b7843acfc572a63aa41230e3d3c54c66568809c85/storage3-0.12.1.tar.gz", hash = "sha256:32ea8f5eb2f7185c2114a4f6ae66d577722e32503f0a30b56e7ed5c7f13e6b48", size = 10198 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3b/c5f8709fc5349928e591fee47592eeff78d29a7d75b097f96a4e01de028d/storage3-0.12.1-py3-none-any.whl", hash = "sha256:9da77fd4f406b019fdcba201e9916aefbf615ef87f551253ce427d8136459a34", size = 18420 }, +] + +[[package]] +name = "strenum" +version = "0.4.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/ad/430fb60d90e1d112a62ff57bdd1f286ec73a2a0331272febfddd21f330e1/StrEnum-0.4.15.tar.gz", hash = "sha256:878fb5ab705442070e4dd1929bb5e2249511c0bcf2b0eeacf3bcd80875c82eff", size = 23384 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/69/297302c5f5f59c862faa31e6cb9a4cd74721cd1e052b38e464c5b402df8b/StrEnum-0.4.15-py3-none-any.whl", hash = "sha256:a30cda4af7cc6b5bf52c8055bc4bf4b2b6b14a93b574626da33df53cf7740659", size = 8851 }, +] + +[[package]] +name = "supabase" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "gotrue" }, + { name = "httpx" }, + { name = "postgrest" }, + { name = "realtime" }, + { name = "storage3" }, + { name = "supafunc" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/e1/511411018596cfecfac99d9091630606e9a03cb07bd5dec2f15621d38b84/supabase-2.18.0.tar.gz", hash = "sha256:b1e98f0faff6e041e5347393a82524d9d5c2ddb87cb844e7557f967f23d8bc70", size = 11521 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/78/67238701e0253fdfef222a39df32f9883de2a8b046fa68d1ae47f3e52979/supabase-2.18.0-py3-none-any.whl", hash = "sha256:73774e6edd5ffd02653a99e2e98f92191c4ffb5089dcff3ed93f5d7c18ddaeb3", size = 18677 }, +] + +[[package]] +name = "supafunc" +version = "0.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx", extra = ["http2"] }, + { name = "strenum" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e8/4b/16f94bcae8a49f5e09544a4fb0e6ad1c2288038036cefdeedb72fcffd92c/supafunc-0.10.1.tar.gz", hash = "sha256:a5b33c8baecb6b5297d25da29a2503e2ec67ee6986f3d44c137e651b8a59a17d", size = 5036 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/4a/9cbea12d86a741d4e73a6e278c2b1d6479fb03d1002efb00e8e71aea76db/supafunc-0.10.1-py3-none-any.whl", hash = "sha256:26df9bd25ff2ef56cb5bfb8962de98f43331f7f8ff69572bac3ed9c3a9672040", size = 8028 }, +] + +[[package]] +name = "typing-extensions" +version = "4.14.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906 }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552 }, +] + +[[package]] +name = "tzdata" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839 }, +] + +[[package]] +name = "uvicorn" +version = "0.35.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/42/e0e305207bb88c6b8d3061399c6a961ffe5fbb7e2aa63c9234df7259e9cd/uvicorn-0.35.0.tar.gz", hash = "sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01", size = 78473 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/e2/dc81b1bd1dcfe91735810265e9d26bc8ec5da45b4c0f6237e286819194c3/uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a", size = 66406 }, +] + +[package.optional-dependencies] +standard = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "websockets" }, +] + +[[package]] +name = "uvloop" +version = "0.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/c0/854216d09d33c543f12a44b393c402e89a920b1a0a7dc634c42de91b9cf6/uvloop-0.21.0.tar.gz", hash = "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3", size = 2492741 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/4c/03f93178830dc7ce8b4cdee1d36770d2f5ebb6f3d37d354e061eefc73545/uvloop-0.21.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c", size = 1471284 }, + { url = "https://files.pythonhosted.org/packages/43/3e/92c03f4d05e50f09251bd8b2b2b584a2a7f8fe600008bcc4523337abe676/uvloop-0.21.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2", size = 821349 }, + { url = "https://files.pythonhosted.org/packages/a6/ef/a02ec5da49909dbbfb1fd205a9a1ac4e88ea92dcae885e7c961847cd51e2/uvloop-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d", size = 4580089 }, + { url = "https://files.pythonhosted.org/packages/06/a7/b4e6a19925c900be9f98bec0a75e6e8f79bb53bdeb891916609ab3958967/uvloop-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc", size = 4693770 }, + { url = "https://files.pythonhosted.org/packages/ce/0c/f07435a18a4b94ce6bd0677d8319cd3de61f3a9eeb1e5f8ab4e8b5edfcb3/uvloop-0.21.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb", size = 4451321 }, + { url = "https://files.pythonhosted.org/packages/8f/eb/f7032be105877bcf924709c97b1bf3b90255b4ec251f9340cef912559f28/uvloop-0.21.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f", size = 4659022 }, + { url = "https://files.pythonhosted.org/packages/3f/8d/2cbef610ca21539f0f36e2b34da49302029e7c9f09acef0b1c3b5839412b/uvloop-0.21.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281", size = 1468123 }, + { url = "https://files.pythonhosted.org/packages/93/0d/b0038d5a469f94ed8f2b2fce2434a18396d8fbfb5da85a0a9781ebbdec14/uvloop-0.21.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af", size = 819325 }, + { url = "https://files.pythonhosted.org/packages/50/94/0a687f39e78c4c1e02e3272c6b2ccdb4e0085fda3b8352fecd0410ccf915/uvloop-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6", size = 4582806 }, + { url = "https://files.pythonhosted.org/packages/d2/19/f5b78616566ea68edd42aacaf645adbf71fbd83fc52281fba555dc27e3f1/uvloop-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816", size = 4701068 }, + { url = "https://files.pythonhosted.org/packages/47/57/66f061ee118f413cd22a656de622925097170b9380b30091b78ea0c6ea75/uvloop-0.21.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc", size = 4454428 }, + { url = "https://files.pythonhosted.org/packages/63/9a/0962b05b308494e3202d3f794a6e85abe471fe3cafdbcf95c2e8c713aabd/uvloop-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553", size = 4660018 }, +] + +[[package]] +name = "vbuild" +version = "0.8.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pscript" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/be/f0c6204a36440bbcc086bfa25964d009b7391c5a3c74d6e73188efd47adb/vbuild-0.8.2.tar.gz", hash = "sha256:270cd9078349d907dfae6c0e6364a5a5e74cb86183bb5093613f12a18b435fa9", size = 8937 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/3d/7b22abbdb059d551507275a2815bc2b1974e3b9f6a13781c1eac9e858965/vbuild-0.8.2-py2.py3-none-any.whl", hash = "sha256:d76bcc976a1c53b6a5776ac947606f9e7786c25df33a587ebe33ed09dd8a1076", size = 9371 }, +] + +[[package]] +name = "virtualenv" +version = "20.33.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/60/4f20960df6c7b363a18a55ab034c8f2bcd5d9770d1f94f9370ec104c1855/virtualenv-20.33.1.tar.gz", hash = "sha256:1b44478d9e261b3fb8baa5e74a0ca3bc0e05f21aa36167bf9cbf850e542765b8", size = 6082160 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/ff/ded57ac5ff40a09e6e198550bab075d780941e0b0f83cbeabd087c59383a/virtualenv-20.33.1-py3-none-any.whl", hash = "sha256:07c19bc66c11acab6a5958b815cbcee30891cd1c2ccf53785a28651a0d8d8a67", size = 6060362 }, +] + +[[package]] +name = "watchfiles" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2a/9a/d451fcc97d029f5812e898fd30a53fd8c15c7bbd058fd75cfc6beb9bd761/watchfiles-1.1.0.tar.gz", hash = "sha256:693ed7ec72cbfcee399e92c895362b6e66d63dac6b91e2c11ae03d10d503e575", size = 94406 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/b8/858957045a38a4079203a33aaa7d23ea9269ca7761c8a074af3524fbb240/watchfiles-1.1.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9dc001c3e10de4725c749d4c2f2bdc6ae24de5a88a339c4bce32300a31ede179", size = 402339 }, + { url = "https://files.pythonhosted.org/packages/80/28/98b222cca751ba68e88521fabd79a4fab64005fc5976ea49b53fa205d1fa/watchfiles-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d9ba68ec283153dead62cbe81872d28e053745f12335d037de9cbd14bd1877f5", size = 394409 }, + { url = "https://files.pythonhosted.org/packages/86/50/dee79968566c03190677c26f7f47960aff738d32087087bdf63a5473e7df/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:130fc497b8ee68dce163e4254d9b0356411d1490e868bd8790028bc46c5cc297", size = 450939 }, + { url = "https://files.pythonhosted.org/packages/40/45/a7b56fb129700f3cfe2594a01aa38d033b92a33dddce86c8dfdfc1247b72/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:50a51a90610d0845a5931a780d8e51d7bd7f309ebc25132ba975aca016b576a0", size = 457270 }, + { url = "https://files.pythonhosted.org/packages/b5/c8/fa5ef9476b1d02dc6b5e258f515fcaaecf559037edf8b6feffcbc097c4b8/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc44678a72ac0910bac46fa6a0de6af9ba1355669b3dfaf1ce5f05ca7a74364e", size = 483370 }, + { url = "https://files.pythonhosted.org/packages/98/68/42cfcdd6533ec94f0a7aab83f759ec11280f70b11bfba0b0f885e298f9bd/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a543492513a93b001975ae283a51f4b67973662a375a403ae82f420d2c7205ee", size = 598654 }, + { url = "https://files.pythonhosted.org/packages/d3/74/b2a1544224118cc28df7e59008a929e711f9c68ce7d554e171b2dc531352/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ac164e20d17cc285f2b94dc31c384bc3aa3dd5e7490473b3db043dd70fbccfd", size = 478667 }, + { url = "https://files.pythonhosted.org/packages/8c/77/e3362fe308358dc9f8588102481e599c83e1b91c2ae843780a7ded939a35/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7590d5a455321e53857892ab8879dce62d1f4b04748769f5adf2e707afb9d4f", size = 452213 }, + { url = "https://files.pythonhosted.org/packages/6e/17/c8f1a36540c9a1558d4faf08e909399e8133599fa359bf52ec8fcee5be6f/watchfiles-1.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:37d3d3f7defb13f62ece99e9be912afe9dd8a0077b7c45ee5a57c74811d581a4", size = 626718 }, + { url = "https://files.pythonhosted.org/packages/26/45/fb599be38b4bd38032643783d7496a26a6f9ae05dea1a42e58229a20ac13/watchfiles-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:7080c4bb3efd70a07b1cc2df99a7aa51d98685be56be6038c3169199d0a1c69f", size = 623098 }, + { url = "https://files.pythonhosted.org/packages/a1/e7/fdf40e038475498e160cd167333c946e45d8563ae4dd65caf757e9ffe6b4/watchfiles-1.1.0-cp312-cp312-win32.whl", hash = "sha256:cbcf8630ef4afb05dc30107bfa17f16c0896bb30ee48fc24bf64c1f970f3b1fd", size = 279209 }, + { url = "https://files.pythonhosted.org/packages/3f/d3/3ae9d5124ec75143bdf088d436cba39812122edc47709cd2caafeac3266f/watchfiles-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:cbd949bdd87567b0ad183d7676feb98136cde5bb9025403794a4c0db28ed3a47", size = 292786 }, + { url = "https://files.pythonhosted.org/packages/26/2f/7dd4fc8b5f2b34b545e19629b4a018bfb1de23b3a496766a2c1165ca890d/watchfiles-1.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:0a7d40b77f07be87c6faa93d0951a0fcd8cbca1ddff60a1b65d741bac6f3a9f6", size = 284343 }, + { url = "https://files.pythonhosted.org/packages/d3/42/fae874df96595556a9089ade83be34a2e04f0f11eb53a8dbf8a8a5e562b4/watchfiles-1.1.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5007f860c7f1f8df471e4e04aaa8c43673429047d63205d1630880f7637bca30", size = 402004 }, + { url = "https://files.pythonhosted.org/packages/fa/55/a77e533e59c3003d9803c09c44c3651224067cbe7fb5d574ddbaa31e11ca/watchfiles-1.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:20ecc8abbd957046f1fe9562757903f5eaf57c3bce70929fda6c7711bb58074a", size = 393671 }, + { url = "https://files.pythonhosted.org/packages/05/68/b0afb3f79c8e832e6571022611adbdc36e35a44e14f129ba09709aa4bb7a/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2f0498b7d2a3c072766dba3274fe22a183dbea1f99d188f1c6c72209a1063dc", size = 449772 }, + { url = "https://files.pythonhosted.org/packages/ff/05/46dd1f6879bc40e1e74c6c39a1b9ab9e790bf1f5a2fe6c08b463d9a807f4/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:239736577e848678e13b201bba14e89718f5c2133dfd6b1f7846fa1b58a8532b", size = 456789 }, + { url = "https://files.pythonhosted.org/packages/8b/ca/0eeb2c06227ca7f12e50a47a3679df0cd1ba487ea19cf844a905920f8e95/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eff4b8d89f444f7e49136dc695599a591ff769300734446c0a86cba2eb2f9895", size = 482551 }, + { url = "https://files.pythonhosted.org/packages/31/47/2cecbd8694095647406645f822781008cc524320466ea393f55fe70eed3b/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12b0a02a91762c08f7264e2e79542f76870c3040bbc847fb67410ab81474932a", size = 597420 }, + { url = "https://files.pythonhosted.org/packages/d9/7e/82abc4240e0806846548559d70f0b1a6dfdca75c1b4f9fa62b504ae9b083/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:29e7bc2eee15cbb339c68445959108803dc14ee0c7b4eea556400131a8de462b", size = 477950 }, + { url = "https://files.pythonhosted.org/packages/25/0d/4d564798a49bf5482a4fa9416dea6b6c0733a3b5700cb8a5a503c4b15853/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9481174d3ed982e269c090f780122fb59cee6c3796f74efe74e70f7780ed94c", size = 451706 }, + { url = "https://files.pythonhosted.org/packages/81/b5/5516cf46b033192d544102ea07c65b6f770f10ed1d0a6d388f5d3874f6e4/watchfiles-1.1.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:80f811146831c8c86ab17b640801c25dc0a88c630e855e2bef3568f30434d52b", size = 625814 }, + { url = "https://files.pythonhosted.org/packages/0c/dd/7c1331f902f30669ac3e754680b6edb9a0dd06dea5438e61128111fadd2c/watchfiles-1.1.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:60022527e71d1d1fda67a33150ee42869042bce3d0fcc9cc49be009a9cded3fb", size = 622820 }, + { url = "https://files.pythonhosted.org/packages/1b/14/36d7a8e27cd128d7b1009e7715a7c02f6c131be9d4ce1e5c3b73d0e342d8/watchfiles-1.1.0-cp313-cp313-win32.whl", hash = "sha256:32d6d4e583593cb8576e129879ea0991660b935177c0f93c6681359b3654bfa9", size = 279194 }, + { url = "https://files.pythonhosted.org/packages/25/41/2dd88054b849aa546dbeef5696019c58f8e0774f4d1c42123273304cdb2e/watchfiles-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:f21af781a4a6fbad54f03c598ab620e3a77032c5878f3d780448421a6e1818c7", size = 292349 }, + { url = "https://files.pythonhosted.org/packages/c8/cf/421d659de88285eb13941cf11a81f875c176f76a6d99342599be88e08d03/watchfiles-1.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:5366164391873ed76bfdf618818c82084c9db7fac82b64a20c44d335eec9ced5", size = 283836 }, + { url = "https://files.pythonhosted.org/packages/45/10/6faf6858d527e3599cc50ec9fcae73590fbddc1420bd4fdccfebffeedbc6/watchfiles-1.1.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:17ab167cca6339c2b830b744eaf10803d2a5b6683be4d79d8475d88b4a8a4be1", size = 400343 }, + { url = "https://files.pythonhosted.org/packages/03/20/5cb7d3966f5e8c718006d0e97dfe379a82f16fecd3caa7810f634412047a/watchfiles-1.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:328dbc9bff7205c215a7807da7c18dce37da7da718e798356212d22696404339", size = 392916 }, + { url = "https://files.pythonhosted.org/packages/8c/07/d8f1176328fa9e9581b6f120b017e286d2a2d22ae3f554efd9515c8e1b49/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7208ab6e009c627b7557ce55c465c98967e8caa8b11833531fdf95799372633", size = 449582 }, + { url = "https://files.pythonhosted.org/packages/66/e8/80a14a453cf6038e81d072a86c05276692a1826471fef91df7537dba8b46/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a8f6f72974a19efead54195bc9bed4d850fc047bb7aa971268fd9a8387c89011", size = 456752 }, + { url = "https://files.pythonhosted.org/packages/5a/25/0853b3fe0e3c2f5af9ea60eb2e781eade939760239a72c2d38fc4cc335f6/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d181ef50923c29cf0450c3cd47e2f0557b62218c50b2ab8ce2ecaa02bd97e670", size = 481436 }, + { url = "https://files.pythonhosted.org/packages/fe/9e/4af0056c258b861fbb29dcb36258de1e2b857be4a9509e6298abcf31e5c9/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:adb4167043d3a78280d5d05ce0ba22055c266cf8655ce942f2fb881262ff3cdf", size = 596016 }, + { url = "https://files.pythonhosted.org/packages/c5/fa/95d604b58aa375e781daf350897aaaa089cff59d84147e9ccff2447c8294/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5701dc474b041e2934a26d31d39f90fac8a3dee2322b39f7729867f932b1d4", size = 476727 }, + { url = "https://files.pythonhosted.org/packages/65/95/fe479b2664f19be4cf5ceeb21be05afd491d95f142e72d26a42f41b7c4f8/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b067915e3c3936966a8607f6fe5487df0c9c4afb85226613b520890049deea20", size = 451864 }, + { url = "https://files.pythonhosted.org/packages/d3/8a/3c4af14b93a15ce55901cd7a92e1a4701910f1768c78fb30f61d2b79785b/watchfiles-1.1.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:9c733cda03b6d636b4219625a4acb5c6ffb10803338e437fb614fef9516825ef", size = 625626 }, + { url = "https://files.pythonhosted.org/packages/da/f5/cf6aa047d4d9e128f4b7cde615236a915673775ef171ff85971d698f3c2c/watchfiles-1.1.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:cc08ef8b90d78bfac66f0def80240b0197008e4852c9f285907377b2947ffdcb", size = 622744 }, + { url = "https://files.pythonhosted.org/packages/2c/00/70f75c47f05dea6fd30df90f047765f6fc2d6eb8b5a3921379b0b04defa2/watchfiles-1.1.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:9974d2f7dc561cce3bb88dfa8eb309dab64c729de85fba32e98d75cf24b66297", size = 402114 }, + { url = "https://files.pythonhosted.org/packages/53/03/acd69c48db4a1ed1de26b349d94077cca2238ff98fd64393f3e97484cae6/watchfiles-1.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c68e9f1fcb4d43798ad8814c4c1b61547b014b667216cb754e606bfade587018", size = 393879 }, + { url = "https://files.pythonhosted.org/packages/2f/c8/a9a2a6f9c8baa4eceae5887fecd421e1b7ce86802bcfc8b6a942e2add834/watchfiles-1.1.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95ab1594377effac17110e1352989bdd7bdfca9ff0e5eeccd8c69c5389b826d0", size = 450026 }, + { url = "https://files.pythonhosted.org/packages/fe/51/d572260d98388e6e2b967425c985e07d47ee6f62e6455cefb46a6e06eda5/watchfiles-1.1.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fba9b62da882c1be1280a7584ec4515d0a6006a94d6e5819730ec2eab60ffe12", size = 457917 }, + { url = "https://files.pythonhosted.org/packages/c6/2d/4258e52917bf9f12909b6ec314ff9636276f3542f9d3807d143f27309104/watchfiles-1.1.0-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3434e401f3ce0ed6b42569128b3d1e3af773d7ec18751b918b89cd49c14eaafb", size = 483602 }, + { url = "https://files.pythonhosted.org/packages/84/99/bee17a5f341a4345fe7b7972a475809af9e528deba056f8963d61ea49f75/watchfiles-1.1.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fa257a4d0d21fcbca5b5fcba9dca5a78011cb93c0323fb8855c6d2dfbc76eb77", size = 596758 }, + { url = "https://files.pythonhosted.org/packages/40/76/e4bec1d59b25b89d2b0716b41b461ed655a9a53c60dc78ad5771fda5b3e6/watchfiles-1.1.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7fd1b3879a578a8ec2076c7961076df540b9af317123f84569f5a9ddee64ce92", size = 477601 }, + { url = "https://files.pythonhosted.org/packages/1f/fa/a514292956f4a9ce3c567ec0c13cce427c158e9f272062685a8a727d08fc/watchfiles-1.1.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62cc7a30eeb0e20ecc5f4bd113cd69dcdb745a07c68c0370cea919f373f65d9e", size = 451936 }, + { url = "https://files.pythonhosted.org/packages/32/5d/c3bf927ec3bbeb4566984eba8dd7a8eb69569400f5509904545576741f88/watchfiles-1.1.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:891c69e027748b4a73847335d208e374ce54ca3c335907d381fde4e41661b13b", size = 626243 }, + { url = "https://files.pythonhosted.org/packages/e6/65/6e12c042f1a68c556802a84d54bb06d35577c81e29fba14019562479159c/watchfiles-1.1.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:12fe8eaffaf0faa7906895b4f8bb88264035b3f0243275e0bf24af0436b27259", size = 623073 }, + { url = "https://files.pythonhosted.org/packages/89/ab/7f79d9bf57329e7cbb0a6fd4c7bd7d0cee1e4a8ef0041459f5409da3506c/watchfiles-1.1.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:bfe3c517c283e484843cb2e357dd57ba009cff351edf45fb455b5fbd1f45b15f", size = 400872 }, + { url = "https://files.pythonhosted.org/packages/df/d5/3f7bf9912798e9e6c516094db6b8932df53b223660c781ee37607030b6d3/watchfiles-1.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a9ccbf1f129480ed3044f540c0fdbc4ee556f7175e5ab40fe077ff6baf286d4e", size = 392877 }, + { url = "https://files.pythonhosted.org/packages/0d/c5/54ec7601a2798604e01c75294770dbee8150e81c6e471445d7601610b495/watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba0e3255b0396cac3cc7bbace76404dd72b5438bf0d8e7cefa2f79a7f3649caa", size = 449645 }, + { url = "https://files.pythonhosted.org/packages/0a/04/c2f44afc3b2fce21ca0b7802cbd37ed90a29874f96069ed30a36dfe57c2b/watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4281cd9fce9fc0a9dbf0fc1217f39bf9cf2b4d315d9626ef1d4e87b84699e7e8", size = 457424 }, + { url = "https://files.pythonhosted.org/packages/9f/b0/eec32cb6c14d248095261a04f290636da3df3119d4040ef91a4a50b29fa5/watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6d2404af8db1329f9a3c9b79ff63e0ae7131986446901582067d9304ae8aaf7f", size = 481584 }, + { url = "https://files.pythonhosted.org/packages/d1/e2/ca4bb71c68a937d7145aa25709e4f5d68eb7698a25ce266e84b55d591bbd/watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e78b6ed8165996013165eeabd875c5dfc19d41b54f94b40e9fff0eb3193e5e8e", size = 596675 }, + { url = "https://files.pythonhosted.org/packages/a1/dd/b0e4b7fb5acf783816bc950180a6cd7c6c1d2cf7e9372c0ea634e722712b/watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:249590eb75ccc117f488e2fabd1bfa33c580e24b96f00658ad88e38844a040bb", size = 477363 }, + { url = "https://files.pythonhosted.org/packages/69/c4/088825b75489cb5b6a761a4542645718893d395d8c530b38734f19da44d2/watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d05686b5487cfa2e2c28ff1aa370ea3e6c5accfe6435944ddea1e10d93872147", size = 452240 }, + { url = "https://files.pythonhosted.org/packages/10/8c/22b074814970eeef43b7c44df98c3e9667c1f7bf5b83e0ff0201b0bd43f9/watchfiles-1.1.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:d0e10e6f8f6dc5762adee7dece33b722282e1f59aa6a55da5d493a97282fedd8", size = 625607 }, + { url = "https://files.pythonhosted.org/packages/32/fa/a4f5c2046385492b2273213ef815bf71a0d4c1943b784fb904e184e30201/watchfiles-1.1.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:af06c863f152005c7592df1d6a7009c836a247c9d8adb78fef8575a5a98699db", size = 623315 }, +] + +[[package]] +name = "websockets" +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437 }, + { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096 }, + { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332 }, + { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152 }, + { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096 }, + { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523 }, + { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790 }, + { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165 }, + { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160 }, + { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395 }, + { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841 }, + { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440 }, + { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098 }, + { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329 }, + { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111 }, + { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054 }, + { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496 }, + { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829 }, + { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217 }, + { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195 }, + { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393 }, + { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837 }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743 }, +] + +[[package]] +name = "wsproto" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/4a/44d3c295350d776427904d73c189e10aeae66d7f555bb2feee16d1e4ba5a/wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065", size = 53425 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/58/e860788190eba3bcce367f74d29c4675466ce8dddfba85f7827588416f01/wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736", size = 24226 }, +] + +[[package]] +name = "yarl" +version = "1.20.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3c/fb/efaa23fa4e45537b827620f04cf8f3cd658b76642205162e072703a5b963/yarl-1.20.1.tar.gz", hash = "sha256:d017a4997ee50c91fd5466cef416231bb82177b93b029906cefc542ce14c35ac", size = 186428 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/9a/cb7fad7d73c69f296eda6815e4a2c7ed53fc70c2f136479a91c8e5fbdb6d/yarl-1.20.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdcc4cd244e58593a4379fe60fdee5ac0331f8eb70320a24d591a3be197b94a9", size = 133667 }, + { url = "https://files.pythonhosted.org/packages/67/38/688577a1cb1e656e3971fb66a3492501c5a5df56d99722e57c98249e5b8a/yarl-1.20.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b29a2c385a5f5b9c7d9347e5812b6f7ab267193c62d282a540b4fc528c8a9d2a", size = 91025 }, + { url = "https://files.pythonhosted.org/packages/50/ec/72991ae51febeb11a42813fc259f0d4c8e0507f2b74b5514618d8b640365/yarl-1.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1112ae8154186dfe2de4732197f59c05a83dc814849a5ced892b708033f40dc2", size = 89709 }, + { url = "https://files.pythonhosted.org/packages/99/da/4d798025490e89426e9f976702e5f9482005c548c579bdae792a4c37769e/yarl-1.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90bbd29c4fe234233f7fa2b9b121fb63c321830e5d05b45153a2ca68f7d310ee", size = 352287 }, + { url = "https://files.pythonhosted.org/packages/1a/26/54a15c6a567aac1c61b18aa0f4b8aa2e285a52d547d1be8bf48abe2b3991/yarl-1.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:680e19c7ce3710ac4cd964e90dad99bf9b5029372ba0c7cbfcd55e54d90ea819", size = 345429 }, + { url = "https://files.pythonhosted.org/packages/d6/95/9dcf2386cb875b234353b93ec43e40219e14900e046bf6ac118f94b1e353/yarl-1.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a979218c1fdb4246a05efc2cc23859d47c89af463a90b99b7c56094daf25a16", size = 365429 }, + { url = "https://files.pythonhosted.org/packages/91/b2/33a8750f6a4bc224242a635f5f2cff6d6ad5ba651f6edcccf721992c21a0/yarl-1.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255b468adf57b4a7b65d8aad5b5138dce6a0752c139965711bdcb81bc370e1b6", size = 363862 }, + { url = "https://files.pythonhosted.org/packages/98/28/3ab7acc5b51f4434b181b0cee8f1f4b77a65919700a355fb3617f9488874/yarl-1.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a97d67108e79cfe22e2b430d80d7571ae57d19f17cda8bb967057ca8a7bf5bfd", size = 355616 }, + { url = "https://files.pythonhosted.org/packages/36/a3/f666894aa947a371724ec7cd2e5daa78ee8a777b21509b4252dd7bd15e29/yarl-1.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8570d998db4ddbfb9a590b185a0a33dbf8aafb831d07a5257b4ec9948df9cb0a", size = 339954 }, + { url = "https://files.pythonhosted.org/packages/f1/81/5f466427e09773c04219d3450d7a1256138a010b6c9f0af2d48565e9ad13/yarl-1.20.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:97c75596019baae7c71ccf1d8cc4738bc08134060d0adfcbe5642f778d1dca38", size = 365575 }, + { url = "https://files.pythonhosted.org/packages/2e/e3/e4b0ad8403e97e6c9972dd587388940a032f030ebec196ab81a3b8e94d31/yarl-1.20.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1c48912653e63aef91ff988c5432832692ac5a1d8f0fb8a33091520b5bbe19ef", size = 365061 }, + { url = "https://files.pythonhosted.org/packages/ac/99/b8a142e79eb86c926f9f06452eb13ecb1bb5713bd01dc0038faf5452e544/yarl-1.20.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4c3ae28f3ae1563c50f3d37f064ddb1511ecc1d5584e88c6b7c63cf7702a6d5f", size = 364142 }, + { url = "https://files.pythonhosted.org/packages/34/f2/08ed34a4a506d82a1a3e5bab99ccd930a040f9b6449e9fd050320e45845c/yarl-1.20.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c5e9642f27036283550f5f57dc6156c51084b458570b9d0d96100c8bebb186a8", size = 381894 }, + { url = "https://files.pythonhosted.org/packages/92/f8/9a3fbf0968eac704f681726eff595dce9b49c8a25cd92bf83df209668285/yarl-1.20.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2c26b0c49220d5799f7b22c6838409ee9bc58ee5c95361a4d7831f03cc225b5a", size = 383378 }, + { url = "https://files.pythonhosted.org/packages/af/85/9363f77bdfa1e4d690957cd39d192c4cacd1c58965df0470a4905253b54f/yarl-1.20.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:564ab3d517e3d01c408c67f2e5247aad4019dcf1969982aba3974b4093279004", size = 374069 }, + { url = "https://files.pythonhosted.org/packages/35/99/9918c8739ba271dcd935400cff8b32e3cd319eaf02fcd023d5dcd487a7c8/yarl-1.20.1-cp312-cp312-win32.whl", hash = "sha256:daea0d313868da1cf2fac6b2d3a25c6e3a9e879483244be38c8e6a41f1d876a5", size = 81249 }, + { url = "https://files.pythonhosted.org/packages/eb/83/5d9092950565481b413b31a23e75dd3418ff0a277d6e0abf3729d4d1ce25/yarl-1.20.1-cp312-cp312-win_amd64.whl", hash = "sha256:48ea7d7f9be0487339828a4de0360d7ce0efc06524a48e1810f945c45b813698", size = 86710 }, + { url = "https://files.pythonhosted.org/packages/8a/e1/2411b6d7f769a07687acee88a062af5833cf1966b7266f3d8dfb3d3dc7d3/yarl-1.20.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:0b5ff0fbb7c9f1b1b5ab53330acbfc5247893069e7716840c8e7d5bb7355038a", size = 131811 }, + { url = "https://files.pythonhosted.org/packages/b2/27/584394e1cb76fb771371770eccad35de400e7b434ce3142c2dd27392c968/yarl-1.20.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:14f326acd845c2b2e2eb38fb1346c94f7f3b01a4f5c788f8144f9b630bfff9a3", size = 90078 }, + { url = "https://files.pythonhosted.org/packages/bf/9a/3246ae92d4049099f52d9b0fe3486e3b500e29b7ea872d0f152966fc209d/yarl-1.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f60e4ad5db23f0b96e49c018596707c3ae89f5d0bd97f0ad3684bcbad899f1e7", size = 88748 }, + { url = "https://files.pythonhosted.org/packages/a3/25/35afe384e31115a1a801fbcf84012d7a066d89035befae7c5d4284df1e03/yarl-1.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:49bdd1b8e00ce57e68ba51916e4bb04461746e794e7c4d4bbc42ba2f18297691", size = 349595 }, + { url = "https://files.pythonhosted.org/packages/28/2d/8aca6cb2cabc8f12efcb82749b9cefecbccfc7b0384e56cd71058ccee433/yarl-1.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:66252d780b45189975abfed839616e8fd2dbacbdc262105ad7742c6ae58f3e31", size = 342616 }, + { url = "https://files.pythonhosted.org/packages/0b/e9/1312633d16b31acf0098d30440ca855e3492d66623dafb8e25b03d00c3da/yarl-1.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59174e7332f5d153d8f7452a102b103e2e74035ad085f404df2e40e663a22b28", size = 361324 }, + { url = "https://files.pythonhosted.org/packages/bc/a0/688cc99463f12f7669eec7c8acc71ef56a1521b99eab7cd3abb75af887b0/yarl-1.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e3968ec7d92a0c0f9ac34d5ecfd03869ec0cab0697c91a45db3fbbd95fe1b653", size = 359676 }, + { url = "https://files.pythonhosted.org/packages/af/44/46407d7f7a56e9a85a4c207724c9f2c545c060380718eea9088f222ba697/yarl-1.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1a4fbb50e14396ba3d375f68bfe02215d8e7bc3ec49da8341fe3157f59d2ff5", size = 352614 }, + { url = "https://files.pythonhosted.org/packages/b1/91/31163295e82b8d5485d31d9cf7754d973d41915cadce070491778d9c9825/yarl-1.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11a62c839c3a8eac2410e951301309426f368388ff2f33799052787035793b02", size = 336766 }, + { url = "https://files.pythonhosted.org/packages/b4/8e/c41a5bc482121f51c083c4c2bcd16b9e01e1cf8729e380273a952513a21f/yarl-1.20.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:041eaa14f73ff5a8986b4388ac6bb43a77f2ea09bf1913df7a35d4646db69e53", size = 364615 }, + { url = "https://files.pythonhosted.org/packages/e3/5b/61a3b054238d33d70ea06ebba7e58597891b71c699e247df35cc984ab393/yarl-1.20.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:377fae2fef158e8fd9d60b4c8751387b8d1fb121d3d0b8e9b0be07d1b41e83dc", size = 360982 }, + { url = "https://files.pythonhosted.org/packages/df/a3/6a72fb83f8d478cb201d14927bc8040af901811a88e0ff2da7842dd0ed19/yarl-1.20.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1c92f4390e407513f619d49319023664643d3339bd5e5a56a3bebe01bc67ec04", size = 369792 }, + { url = "https://files.pythonhosted.org/packages/7c/af/4cc3c36dfc7c077f8dedb561eb21f69e1e9f2456b91b593882b0b18c19dc/yarl-1.20.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d25ddcf954df1754ab0f86bb696af765c5bfaba39b74095f27eececa049ef9a4", size = 382049 }, + { url = "https://files.pythonhosted.org/packages/19/3a/e54e2c4752160115183a66dc9ee75a153f81f3ab2ba4bf79c3c53b33de34/yarl-1.20.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:909313577e9619dcff8c31a0ea2aa0a2a828341d92673015456b3ae492e7317b", size = 384774 }, + { url = "https://files.pythonhosted.org/packages/9c/20/200ae86dabfca89060ec6447649f219b4cbd94531e425e50d57e5f5ac330/yarl-1.20.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:793fd0580cb9664548c6b83c63b43c477212c0260891ddf86809e1c06c8b08f1", size = 374252 }, + { url = "https://files.pythonhosted.org/packages/83/75/11ee332f2f516b3d094e89448da73d557687f7d137d5a0f48c40ff211487/yarl-1.20.1-cp313-cp313-win32.whl", hash = "sha256:468f6e40285de5a5b3c44981ca3a319a4b208ccc07d526b20b12aeedcfa654b7", size = 81198 }, + { url = "https://files.pythonhosted.org/packages/ba/ba/39b1ecbf51620b40ab402b0fc817f0ff750f6d92712b44689c2c215be89d/yarl-1.20.1-cp313-cp313-win_amd64.whl", hash = "sha256:495b4ef2fea40596bfc0affe3837411d6aa3371abcf31aac0ccc4bdd64d4ef5c", size = 86346 }, + { url = "https://files.pythonhosted.org/packages/43/c7/669c52519dca4c95153c8ad96dd123c79f354a376346b198f438e56ffeb4/yarl-1.20.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:f60233b98423aab21d249a30eb27c389c14929f47be8430efa7dbd91493a729d", size = 138826 }, + { url = "https://files.pythonhosted.org/packages/6a/42/fc0053719b44f6ad04a75d7f05e0e9674d45ef62f2d9ad2c1163e5c05827/yarl-1.20.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6f3eff4cc3f03d650d8755c6eefc844edde99d641d0dcf4da3ab27141a5f8ddf", size = 93217 }, + { url = "https://files.pythonhosted.org/packages/4f/7f/fa59c4c27e2a076bba0d959386e26eba77eb52ea4a0aac48e3515c186b4c/yarl-1.20.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:69ff8439d8ba832d6bed88af2c2b3445977eba9a4588b787b32945871c2444e3", size = 92700 }, + { url = "https://files.pythonhosted.org/packages/2f/d4/062b2f48e7c93481e88eff97a6312dca15ea200e959f23e96d8ab898c5b8/yarl-1.20.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cf34efa60eb81dd2645a2e13e00bb98b76c35ab5061a3989c7a70f78c85006d", size = 347644 }, + { url = "https://files.pythonhosted.org/packages/89/47/78b7f40d13c8f62b499cc702fdf69e090455518ae544c00a3bf4afc9fc77/yarl-1.20.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8e0fe9364ad0fddab2688ce72cb7a8e61ea42eff3c7caeeb83874a5d479c896c", size = 323452 }, + { url = "https://files.pythonhosted.org/packages/eb/2b/490d3b2dc66f52987d4ee0d3090a147ea67732ce6b4d61e362c1846d0d32/yarl-1.20.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f64fbf81878ba914562c672024089e3401974a39767747691c65080a67b18c1", size = 346378 }, + { url = "https://files.pythonhosted.org/packages/66/ad/775da9c8a94ce925d1537f939a4f17d782efef1f973039d821cbe4bcc211/yarl-1.20.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6342d643bf9a1de97e512e45e4b9560a043347e779a173250824f8b254bd5ce", size = 353261 }, + { url = "https://files.pythonhosted.org/packages/4b/23/0ed0922b47a4f5c6eb9065d5ff1e459747226ddce5c6a4c111e728c9f701/yarl-1.20.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56dac5f452ed25eef0f6e3c6a066c6ab68971d96a9fb441791cad0efba6140d3", size = 335987 }, + { url = "https://files.pythonhosted.org/packages/3e/49/bc728a7fe7d0e9336e2b78f0958a2d6b288ba89f25a1762407a222bf53c3/yarl-1.20.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7d7f497126d65e2cad8dc5f97d34c27b19199b6414a40cb36b52f41b79014be", size = 329361 }, + { url = "https://files.pythonhosted.org/packages/93/8f/b811b9d1f617c83c907e7082a76e2b92b655400e61730cd61a1f67178393/yarl-1.20.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:67e708dfb8e78d8a19169818eeb5c7a80717562de9051bf2413aca8e3696bf16", size = 346460 }, + { url = "https://files.pythonhosted.org/packages/70/fd/af94f04f275f95da2c3b8b5e1d49e3e79f1ed8b6ceb0f1664cbd902773ff/yarl-1.20.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:595c07bc79af2494365cc96ddeb772f76272364ef7c80fb892ef9d0649586513", size = 334486 }, + { url = "https://files.pythonhosted.org/packages/84/65/04c62e82704e7dd0a9b3f61dbaa8447f8507655fd16c51da0637b39b2910/yarl-1.20.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7bdd2f80f4a7df852ab9ab49484a4dee8030023aa536df41f2d922fd57bf023f", size = 342219 }, + { url = "https://files.pythonhosted.org/packages/91/95/459ca62eb958381b342d94ab9a4b6aec1ddec1f7057c487e926f03c06d30/yarl-1.20.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c03bfebc4ae8d862f853a9757199677ab74ec25424d0ebd68a0027e9c639a390", size = 350693 }, + { url = "https://files.pythonhosted.org/packages/a6/00/d393e82dd955ad20617abc546a8f1aee40534d599ff555ea053d0ec9bf03/yarl-1.20.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:344d1103e9c1523f32a5ed704d576172d2cabed3122ea90b1d4e11fe17c66458", size = 355803 }, + { url = "https://files.pythonhosted.org/packages/9e/ed/c5fb04869b99b717985e244fd93029c7a8e8febdfcffa06093e32d7d44e7/yarl-1.20.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:88cab98aa4e13e1ade8c141daeedd300a4603b7132819c484841bb7af3edce9e", size = 341709 }, + { url = "https://files.pythonhosted.org/packages/24/fd/725b8e73ac2a50e78a4534ac43c6addf5c1c2d65380dd48a9169cc6739a9/yarl-1.20.1-cp313-cp313t-win32.whl", hash = "sha256:b121ff6a7cbd4abc28985b6028235491941b9fe8fe226e6fdc539c977ea1739d", size = 86591 }, + { url = "https://files.pythonhosted.org/packages/94/c3/b2e9f38bc3e11191981d57ea08cab2166e74ea770024a646617c9cddd9f6/yarl-1.20.1-cp313-cp313t-win_amd64.whl", hash = "sha256:541d050a355bbbc27e55d906bc91cb6fe42f96c01413dd0f4ed5a5240513874f", size = 93003 }, + { url = "https://files.pythonhosted.org/packages/b4/2d/2345fce04cfd4bee161bf1e7d9cdc702e3e16109021035dbb24db654a622/yarl-1.20.1-py3-none-any.whl", hash = "sha256:83b8eb083fe4683c6115795d9fc1cfaf2cbbefb19b3a1cb68f6527460f483a77", size = 46542 }, +]