From 6e9432366ad1d0f954f514743476a291ea58e7c2 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Wed, 22 Apr 2026 17:45:07 -0700 Subject: [PATCH 01/16] add reflex hosting cli --- packages/reflex-hosting-cli/.coveragerc | 31 + .../actions/cli_deploy_reflex_app/action.yml | 110 + .../.github/pull_request_template.md | 5 + .../workflows/deploy_reflex_app_to_prod.yml | 68 + .../e2e_cli_deploy_reflex_web_to_dev.yml | 63 + .../.github/workflows/pre-commit.yml | 22 + .../.github/workflows/unit_tests.yml | 73 + packages/reflex-hosting-cli/.gitignore | 9 + packages/reflex-hosting-cli/LICENSE | 201 ++ packages/reflex-hosting-cli/README.md | 2 + packages/reflex-hosting-cli/pyproject.toml | 104 + .../reflex-hosting-cli/reflex_cli/__init__.py | 1 + packages/reflex-hosting-cli/reflex_cli/cli.py | 9 + .../reflex_cli/constants/__init__.py | 15 + .../reflex_cli/constants/base.py | 58 + .../reflex_cli/constants/compiler.py | 33 + .../reflex_cli/constants/hosting.py | 56 + .../reflex_cli/core/__init__.py | 1 + .../reflex_cli/core/config.py | 321 +++ .../reflex_cli/deployments.py | 242 ++ .../reflex_cli/utils/__init__.py | 25 + .../reflex_cli/utils/console.py | 226 ++ .../reflex_cli/utils/dependency.py | 170 ++ .../reflex_cli/utils/exceptions.py | 37 + .../reflex_cli/utils/hosting.py | 2210 +++++++++++++++++ .../reflex_cli/v2/__init__.py | 1 + .../reflex-hosting-cli/reflex_cli/v2/apps.py | 828 ++++++ .../reflex-hosting-cli/reflex_cli/v2/cli.py | 479 ++++ .../reflex_cli/v2/deployments.py | 137 + .../reflex_cli/v2/project.py | 533 ++++ .../reflex_cli/v2/secrets.py | 240 ++ .../reflex-hosting-cli/reflex_cli/v2/utils.py | 6 + .../reflex_cli/v2/vmtypes_regions.py | 199 ++ .../scripts/check_site_responsive.py | 110 + ...ex_hosting_cli_next_dev_release_version.py | 103 + packages/reflex-hosting-cli/tests/__init__.py | 0 .../tests/utils/__init__.py | 0 .../tests/utils/test_dependency.py | 37 + .../tests/utils/test_hosting.py | 170 ++ .../reflex-hosting-cli/tests/v2/__init__.py | 0 .../reflex-hosting-cli/tests/v2/test_apps.py | 1519 +++++++++++ .../reflex-hosting-cli/tests/v2/test_cli.py | 650 +++++ .../tests/v2/test_deployments.py | 70 + .../tests/v2/test_project.py | 838 +++++++ .../tests/v2/test_secrets.py | 275 ++ .../tests/v2/test_vmtypes_regions.py | 220 ++ packages/reflex-hosting-cli/uv.lock | 1537 ++++++++++++ pyproject.toml | 1 + uv.lock | 74 +- 49 files changed, 12113 insertions(+), 6 deletions(-) create mode 100644 packages/reflex-hosting-cli/.coveragerc create mode 100644 packages/reflex-hosting-cli/.github/actions/cli_deploy_reflex_app/action.yml create mode 100644 packages/reflex-hosting-cli/.github/pull_request_template.md create mode 100644 packages/reflex-hosting-cli/.github/workflows/deploy_reflex_app_to_prod.yml create mode 100644 packages/reflex-hosting-cli/.github/workflows/e2e_cli_deploy_reflex_web_to_dev.yml create mode 100644 packages/reflex-hosting-cli/.github/workflows/pre-commit.yml create mode 100644 packages/reflex-hosting-cli/.github/workflows/unit_tests.yml create mode 100644 packages/reflex-hosting-cli/.gitignore create mode 100644 packages/reflex-hosting-cli/LICENSE create mode 100644 packages/reflex-hosting-cli/README.md create mode 100644 packages/reflex-hosting-cli/pyproject.toml create mode 100644 packages/reflex-hosting-cli/reflex_cli/__init__.py create mode 100644 packages/reflex-hosting-cli/reflex_cli/cli.py create mode 100644 packages/reflex-hosting-cli/reflex_cli/constants/__init__.py create mode 100644 packages/reflex-hosting-cli/reflex_cli/constants/base.py create mode 100644 packages/reflex-hosting-cli/reflex_cli/constants/compiler.py create mode 100644 packages/reflex-hosting-cli/reflex_cli/constants/hosting.py create mode 100644 packages/reflex-hosting-cli/reflex_cli/core/__init__.py create mode 100644 packages/reflex-hosting-cli/reflex_cli/core/config.py create mode 100644 packages/reflex-hosting-cli/reflex_cli/deployments.py create mode 100644 packages/reflex-hosting-cli/reflex_cli/utils/__init__.py create mode 100644 packages/reflex-hosting-cli/reflex_cli/utils/console.py create mode 100644 packages/reflex-hosting-cli/reflex_cli/utils/dependency.py create mode 100644 packages/reflex-hosting-cli/reflex_cli/utils/exceptions.py create mode 100644 packages/reflex-hosting-cli/reflex_cli/utils/hosting.py create mode 100644 packages/reflex-hosting-cli/reflex_cli/v2/__init__.py create mode 100644 packages/reflex-hosting-cli/reflex_cli/v2/apps.py create mode 100644 packages/reflex-hosting-cli/reflex_cli/v2/cli.py create mode 100644 packages/reflex-hosting-cli/reflex_cli/v2/deployments.py create mode 100644 packages/reflex-hosting-cli/reflex_cli/v2/project.py create mode 100644 packages/reflex-hosting-cli/reflex_cli/v2/secrets.py create mode 100644 packages/reflex-hosting-cli/reflex_cli/v2/utils.py create mode 100644 packages/reflex-hosting-cli/reflex_cli/v2/vmtypes_regions.py create mode 100755 packages/reflex-hosting-cli/scripts/check_site_responsive.py create mode 100644 packages/reflex-hosting-cli/scripts/reflex_hosting_cli_next_dev_release_version.py create mode 100644 packages/reflex-hosting-cli/tests/__init__.py create mode 100644 packages/reflex-hosting-cli/tests/utils/__init__.py create mode 100644 packages/reflex-hosting-cli/tests/utils/test_dependency.py create mode 100644 packages/reflex-hosting-cli/tests/utils/test_hosting.py create mode 100644 packages/reflex-hosting-cli/tests/v2/__init__.py create mode 100644 packages/reflex-hosting-cli/tests/v2/test_apps.py create mode 100644 packages/reflex-hosting-cli/tests/v2/test_cli.py create mode 100644 packages/reflex-hosting-cli/tests/v2/test_deployments.py create mode 100644 packages/reflex-hosting-cli/tests/v2/test_project.py create mode 100644 packages/reflex-hosting-cli/tests/v2/test_secrets.py create mode 100644 packages/reflex-hosting-cli/tests/v2/test_vmtypes_regions.py create mode 100644 packages/reflex-hosting-cli/uv.lock diff --git a/packages/reflex-hosting-cli/.coveragerc b/packages/reflex-hosting-cli/.coveragerc new file mode 100644 index 00000000000..f0b2771d54e --- /dev/null +++ b/packages/reflex-hosting-cli/.coveragerc @@ -0,0 +1,31 @@ +[run] +source = reflex_cli +branch = true + +[report] +show_missing = true +# TODO bump back +fail_under = 50 +precision = 2 + +# Regexes for lines to exclude from consideration +exclude_also = + # Don't complain about missing debug-only code: + def __repr__ + if self\.debug + + # Don't complain if tests don't hit defensive assertion code: + raise AssertionError + raise NotImplementedError + + # Don't complain if non-runnable code isn't run: + if 0: + if __name__ == .__main__.: + + # Don't complain about abstract methods, they aren't run: + @(abc\.)?abstractmethod + +ignore_errors = True + +[html] +directory = coverage_html_report diff --git a/packages/reflex-hosting-cli/.github/actions/cli_deploy_reflex_app/action.yml b/packages/reflex-hosting-cli/.github/actions/cli_deploy_reflex_app/action.yml new file mode 100644 index 00000000000..781595875f1 --- /dev/null +++ b/packages/reflex-hosting-cli/.github/actions/cli_deploy_reflex_app/action.yml @@ -0,0 +1,110 @@ +name: Deploy-Reflex-Apps-using-Hosting-CLI +description: 'Deploy Reflex apps using Hosting CLI' + +inputs: + reflex-app-repo: + description: 'repository of the target app to deploy' + required: true + reflex-app-ref: + description: 'reflex app ref to deploy. e.g. git sha or `main`' + required: true + default: 'main' + reflex-pin: + description: 'Reflex pin (@, ==, all ok)' + cp-web-url: + description: 'Control Plane web URL' + cp-backend-url: + description: 'Control Plane backend URL' + cp-env: + description: 'Control Plane environment: dev/staging/prod' + deployment-key: + description: 'Instance key used for deploy' + required: true + user-email: + description: 'Email of the hosting service user deploying the app' + required: true + user-password: + description: 'Password of the hosting service user deploying the app' + required: true + openai-api-key: + description: 'If included, the openai api key will be used' + with-local-db: + description: 'Whether to upload the local db' + default: 'false' + +runs: + using: 'composite' + steps: + - name: Checkout the target app repo + uses: actions/checkout@v3 + with: + repository: ${{ inputs.reflex-app-repo }} + path: reflex-app + ref: ${{ inputs.reflex-app-ref }} + - name: Setup Python + uses: actions/setup-python@v3 + with: + python-version: 3.11 + - name: Install requirements + shell: bash + run: | + python -m venv venv + source venv/bin/activate + pip install --no-color --no-cache-dir -q . + if [ -n "${{ inputs.reflex-pin }}" ]; then + # override reflex pin + sed -i '/^reflex[>=]/d' reflex-app/requirements.txt + echo "" >> reflex-app/requirements.txt + echo "reflex${{ inputs.reflex-pin }}" >> reflex-app/requirements.txt + fi + # Note, below sed command only works on ubuntu, not MacOS + pip install "reflex${{ inputs.reflex-pin }}" --no-color --no-cache-dir -q -r reflex-app/requirements.txt + - name: Deploy bot log in to hosting service + shell: bash + env: + REFLEX_CLOUD_URL: ${{ inputs.cp-web-url }} + REFLEX_CLOUD_BACKEND_URL: ${{ inputs.cp-backend-url }} + run: | + source venv/bin/activate + access_token=$(curl "${REFLEX_CLOUD_BACKEND_URL}/authenticate/password" -u "${{ inputs.user-email }}:${{ inputs.user-password }}" -X POST) + if [ -z "$access_token" ]; then + echo "::error::Test user unable to log in" && exit 1 + fi + mkdir -p ~/.local/share/reflex/ + # Bypass the login process by manually stashing a token in the hosting config file. + echo "{\"access_token\": $access_token}" > ~/.local/share/reflex/hosting_v0.json + reflex login + - name: Deploy reflex-app using CLI + env: + REFLEX_CLOUD_URL: ${{ inputs.cp-web-url }} + REFLEX_CLOUD_BACKEND_URL: ${{ inputs.cp-backend-url }} + shell: bash + run: | + source venv/bin/activate + cd reflex-app/ + export TELEMETRY_ENABLED=false + reflex init + if [ -n "${{ inputs.openai-api-key }}" ]; then + export OPENAI_API_KEY=${{ inputs.openai-api-key }} + add_env_flags="--env OPENAI_API_KEY=${{ inputs.openai-api-key }}" + fi + if [ "${{ inputs.with-local-db }}" == 'true' ]; then + add_db_flags="--upload-db-file" + # Here we also need to do migration on the local sqlite db + reflex db init + reflex db migrate + fi + reflex deploy --no-interactive -k ${{ inputs.deployment-key }} -r sjc ${add_env_flags} ${add_db_flags} + listed="$(reflex deployments list | grep ${{ inputs.deployment-key }})" + if [[ -z "$listed" ]]; then + echo "::error::deploy was not successful" && exit 1 + fi + - name: Check deployed reflex app is responsive + env: + REFLEX_CLOUD_URL: ${{ inputs.cp-web-url }} + REFLEX_CLOUD_BACKEND_URL: ${{ inputs.cp-backend-url }} + CP_ENV: ${{ inputs.cp-env }} + shell: bash + run: | + source venv/bin/activate + python ./scripts/check_site_responsive.py --env ${CP_ENV} --deployment-key ${{ inputs.deployment-key }} --frontend-retries 30 --backend-retries 90 diff --git a/packages/reflex-hosting-cli/.github/pull_request_template.md b/packages/reflex-hosting-cli/.github/pull_request_template.md new file mode 100644 index 00000000000..d0c2a63641d --- /dev/null +++ b/packages/reflex-hosting-cli/.github/pull_request_template.md @@ -0,0 +1,5 @@ +## Summary +- TBD + +## Tests +- [ ] CI green diff --git a/packages/reflex-hosting-cli/.github/workflows/deploy_reflex_app_to_prod.yml b/packages/reflex-hosting-cli/.github/workflows/deploy_reflex_app_to_prod.yml new file mode 100644 index 00000000000..87197bf29c3 --- /dev/null +++ b/packages/reflex-hosting-cli/.github/workflows/deploy_reflex_app_to_prod.yml @@ -0,0 +1,68 @@ +name: deploy-reflex-app-to-prod +on: + workflow_dispatch: + inputs: + reflex-app-repo: + description: 'reflex app github repo to deploy. e.g. `reflex-dev/reflex-chat`' + required: true + reflex-app-ref: + description: 'reflex app ref to deploy. e.g. git sha or `main`' + reflex-pin: + description: 'Reflex pin (@, ==, all ok)' + deployment-key: + description: 'The key for the instance to be deployed. Must be a new app or already owned by the CI user.' + required: true + repository_dispatch: + +jobs: + deploy-reflex-app-to-prod: + runs-on: ubuntu-latest + timeout-minutes: 20 + env: + TELEMETRY_ENABLED: false + DEPLOYMENT_KEY: pcweb-${{ github.run_id }}-${{ github.run_attempt }} + steps: + - name: Check out reflex-hosting-cli repo + uses: actions/checkout@v3 + with: + repository: reflex-dev/reflex-hosting-cli + - name: Set reflex app envs + id: set-reflex-app-envs + shell: bash + run: | + if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then + echo "reflex-app-repo=${{ inputs.reflex-app-repo }}" >> "$GITHUB_OUTPUT" + echo "reflex-app-ref=${{ inputs.reflex-app-ref }}" >> "$GITHUB_OUTPUT" + echo "deployment-key=${{ inputs.deployment-key }}" >> "$GITHUB_OUTPUT" + else + echo "reflex-app-repo=${{ github.event.client_payload.repo }}" >> "$GITHUB_OUTPUT" + echo "reflex-app-ref=${{ github.event.client_payload.sha }}" >> "$GITHUB_OUTPUT" + # if the deployment-key is provided in the client payload, use it + # otherwise, use the default deployment key which is the repo name + if [ -z "${{ github.event.client_payload.deployment-key }}" ]; then + owner_and_repo="${{ github.event.client_payload.repo }}" + echo "deployment-key=${owner_and_repo##*/}" >> "$GITHUB_OUTPUT" + else + echo "deployment-key=${{ github.event.client_payload.deployment-key }}" >> "$GITHUB_OUTPUT" + fi + if [ 'true' == "${{ github.event.client_payload.with-openai }}" ]; then + echo "openai-api-key=${{ secrets.OPENAI_API_KEY }}" >> "$GITHUB_OUTPUT" + fi + if [ 'true' == "${{ github.event.client_payload.with-local-db }}" ]; then + echo "with-local-db='true'" >> "$GITHUB_OUTPUT" + fi + fi + - name: Deploy reflex app + uses: ./.github/actions/cli_deploy_reflex_app + with: + reflex-app-repo: ${{ steps.set-reflex-app-envs.outputs.reflex-app-repo }} + reflex-app-ref: ${{ steps.set-reflex-app-envs.outputs.reflex-app-ref }} + deployment-key: ${{ steps.set-reflex-app-envs.outputs.deployment-key }} + openai-api-key: ${{ steps.set-reflex-app-envs.outputs.openai-api-key }} + with-local-db: ${{ steps.set-reflex-app-envs.outputs.with-local-db }} + reflex-pin: ${{ inputs.reflex-pin }} + cp-web-url: ${{ secrets.PROD_CP_WEB_URL }} + cp-backend-url: ${{ secrets.PROD_CP_BACKEND_URL }} + cp-env: 'prod' + user-email: ${{ secrets.PROD_CI_TEST_USER_EMAIL }} + user-password: ${{ secrets.PROD_CI_TEST_USER_PASSWORD }} diff --git a/packages/reflex-hosting-cli/.github/workflows/e2e_cli_deploy_reflex_web_to_dev.yml b/packages/reflex-hosting-cli/.github/workflows/e2e_cli_deploy_reflex_web_to_dev.yml new file mode 100644 index 00000000000..22fca065cee --- /dev/null +++ b/packages/reflex-hosting-cli/.github/workflows/e2e_cli_deploy_reflex_web_to_dev.yml @@ -0,0 +1,63 @@ +name: e2e-cli-deploy-reflex-web-to-dev +on: + # pull_request: + # branches: [main] + # paths: + # - '.github/workflows/e2e_cli_deploy_reflex_web_to_dev.yml' + # - '.github/actions/cli_deploy_reflex_app/action.yml' + # - 'reflex_cli/**' + # - 'pyproject.yml' + # - 'poetry.lock' + # push: + # branches: [main] + # paths: + # - '.github/workflows/e2e_cli_deploy_reflex_web_to_dev.yml' + # - 'reflex_cli/**' + # - 'pyproject.yml' + # - 'poetry.lock' + workflow_dispatch: + inputs: + reflex-web-ref: + description: 'reflex-web ref to deploy' + required: true + default: 'main' + reflex-hosting-cli-ref: + description: 'reflex-hosting-cli ref to use' + required: true + default: 'main' + reflex-pin: + description: 'Reflex pin (@, ==, all ok)' + +jobs: + e2e-cli-deploy-reflex-web-to-dev: + runs-on: Ubuntu-8-32 + timeout-minutes: 20 + env: + TELEMETRY_ENABLED: false + DEPLOYMENT_KEY: pcweb-${{ github.run_id }}-${{ github.run_attempt }} + REFLEX_HOSTING_CLI_PATH: reflex-hosting-cli + steps: + - name: Check out reflex-hosting-cli repo + uses: actions/checkout@v3 + with: + repository: reflex-dev/reflex-hosting-cli + ref: ${{ inputs.reflex-hosting-cli-ref }} + - name: Deploy reflex-web + uses: ./.github/actions/cli_deploy_reflex_app + with: + reflex-app-repo: reflex-dev/reflex-web + deployment-key: ${DEPLOYMENT_KEY} + reflex-pin: '@git+https://github.com/reflex-dev/reflex@main' + cp-web-url: ${{ secrets.DEV_CP_WEB_URL }} + cp-backend-url: ${{ secrets.DEV_CP_BACKEND_URL }} + cp-env: 'dev' + user-email: ${{ secrets.DEV_CI_TEST_USER_EMAIL }} + user-password: ${{ secrets.DEV_CI_TEST_USER_PASSWORD }} + - name: Delete reflex-web deployment in dev + shell: bash + env: + REFLEX_CLOUD_URL: ${{ secrets.DEV_CP_WEB_URL }} + REFLEX_CLOUD_BACKEND_URL: ${{ secrets.DEV_CP_BACKEND_URL }} + run: | + source venv/bin/activate + reflex deployments delete ${DEPLOYMENT_KEY} diff --git a/packages/reflex-hosting-cli/.github/workflows/pre-commit.yml b/packages/reflex-hosting-cli/.github/workflows/pre-commit.yml new file mode 100644 index 00000000000..a95ae647e6e --- /dev/null +++ b/packages/reflex-hosting-cli/.github/workflows/pre-commit.yml @@ -0,0 +1,22 @@ +name: pre-commit + +on: + pull_request: + branches: [main] + push: + # Note even though this job is called "pre-commit" and runs "pre-commit", this job will run + # also POST-commit on main also! In case there are mishandled merge conflicts / bad auto-resolves + # when merging into main branch. + branches: [main] + +jobs: + pre-commit: + timeout-minutes: 30 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + python-version: 3.13 + - run: uv run pre-commit run --all-files diff --git a/packages/reflex-hosting-cli/.github/workflows/unit_tests.yml b/packages/reflex-hosting-cli/.github/workflows/unit_tests.yml new file mode 100644 index 00000000000..224a37dda32 --- /dev/null +++ b/packages/reflex-hosting-cli/.github/workflows/unit_tests.yml @@ -0,0 +1,73 @@ +name: unit-tests + +on: + push: + branches: ["main"] + paths: + - ".github/workflows/unit_tests.yml" + - ".github/actions/**" + - "reflex_cli/**" + - "tests/**" + - "pyproject.toml" + - "uv.lock" + - ".coveragerc" + pull_request: + branches: ["main"] + paths: + - ".github/workflows/unit_tests.yml" + - ".github/actions/**" + - "reflex_cli/**" + - "tests/**" + - "pyproject.toml" + - "uv.lock" + - ".coveragerc" + +permissions: + contents: read + +defaults: + run: + shell: bash + +jobs: + unit-tests: + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest] + python-version: ["3.10", "3.11", "3.12", "3.13"] + # Run windows only on 3.11 + exclude: + - os: windows-latest + python-version: "3.13" + - os: windows-latest + python-version: "3.12" + - os: windows-latest + python-version: "3.10" + runs-on: ${{ matrix.os }} + # Service containers to run with `runner-job` + services: + # Label used to access the service container + redis: + image: ${{ matrix.os == 'ubuntu-latest' && 'redis' || '' }} + # Set health checks to wait until redis has started + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + # Maps port 6379 on service container to the host + - 6379:6379 + steps: + - uses: actions/checkout@v4 + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Run unit tests + run: uv run pytest tests --cov --no-cov-on-fail --cov-report= + env: + PYTHONUNBUFFERED: 1 + - run: uv run coverage html diff --git a/packages/reflex-hosting-cli/.gitignore b/packages/reflex-hosting-cli/.gitignore new file mode 100644 index 00000000000..df43a9c49ce --- /dev/null +++ b/packages/reflex-hosting-cli/.gitignore @@ -0,0 +1,9 @@ +**/.DS_Store +**/*.pyc +dist/* +examples/ +.idea +.vscode +.coverage +.coverage.* +venv diff --git a/packages/reflex-hosting-cli/LICENSE b/packages/reflex-hosting-cli/LICENSE new file mode 100644 index 00000000000..7be89f167cb --- /dev/null +++ b/packages/reflex-hosting-cli/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + https://www.apache.org/licenses/LICENSE-2.0 + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2023, Pynecone, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/reflex-hosting-cli/README.md b/packages/reflex-hosting-cli/README.md new file mode 100644 index 00000000000..d02cd615bf6 --- /dev/null +++ b/packages/reflex-hosting-cli/README.md @@ -0,0 +1,2 @@ +# reflex-hosting-cli +Hosting CLI for Reflex. diff --git a/packages/reflex-hosting-cli/pyproject.toml b/packages/reflex-hosting-cli/pyproject.toml new file mode 100644 index 00000000000..29eda98e31e --- /dev/null +++ b/packages/reflex-hosting-cli/pyproject.toml @@ -0,0 +1,104 @@ +[project] +name = "reflex-hosting-cli" +version = "0.1.62" +description = "Reflex Hosting CLI" +license = "Apache-2.0" +authors = [ + { name = "Nikhil Rao", email = "nikhil@reflex.dev" }, + { name = "Alek Petuskey", email = "alek@reflex.dev" }, +] +maintainers = [ + { name = "Simon Young", email = "simon@reflex.dev" }, + { name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }, +] +readme = "README.md" +keywords = ["web", "framework"] +classifiers = ["Development Status :: 4 - Beta"] +requires-python = ">=3.10" +dependencies = [ + "click >=8.2", + "httpx >=0.25.1,<1.0", + "packaging >=24.2", + "platformdirs >=3.10.0,<5.0", + "rich >=13,<15", +] + +[project.urls] +homepage = "https://reflex.dev" +repository = "https://github.com/reflex-dev/reflex" +documentation = "https://reflex.dev/docs/getting-started/introduction" + +[dependency-groups] +dev = [ + "coverage", + "darglint", + "ruff", + "pre-commit", + "pyright", + "pytest", + "pytest-benchmark", + "pytest-cov", + "pytest-mock", + "typer", + "reflex", + "pyyaml", +] + + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build] +include = ["reflex_cli"] + +[tool.pyright] + +[tool.ruff] +output-format = "concise" +lint.isort.split-on-trailing-comma = false +lint.select = [ + "ANN001", + "B", + "C4", + "D", + "E", + "ERA", + "F", + "FURB", + "I", + "N", + "PERF", + "PGH", + "PTH", + "RUF", + "SIM", + "T", + "TRY", + "UP", + "W", +] +lint.ignore = [ + "B008", + "D205", + "E501", + "F403", + "SIM115", + "RUF006", + "RUF008", + "RUF012", + "TRY0", +] +lint.pydocstyle.convention = "google" +target-version = "py310" + + +[tool.ruff.lint.per-file-ignores] +"tests/*.py" = ["D100", "D103", "D104"] +"scripts/*.py" = ["T201"] + +[[tool.uv.index]] +name = "testpypi" +url = "https://test.pypi.org/simple/" +publish-url = "https://test.pypi.org/legacy/" +explicit = true diff --git a/packages/reflex-hosting-cli/reflex_cli/__init__.py b/packages/reflex-hosting-cli/reflex_cli/__init__.py new file mode 100644 index 00000000000..6c091d1f261 --- /dev/null +++ b/packages/reflex-hosting-cli/reflex_cli/__init__.py @@ -0,0 +1 @@ +"""CLI library for the hosting service.""" diff --git a/packages/reflex-hosting-cli/reflex_cli/cli.py b/packages/reflex-hosting-cli/reflex_cli/cli.py new file mode 100644 index 00000000000..1ec835b8979 --- /dev/null +++ b/packages/reflex-hosting-cli/reflex_cli/cli.py @@ -0,0 +1,9 @@ +"""Disabled V1 CLI for the hosting service.""" + +from __future__ import annotations + +from reflex_cli.utils import disabled_v1_hosting + +login = disabled_v1_hosting +logout = disabled_v1_hosting +deploy = disabled_v1_hosting diff --git a/packages/reflex-hosting-cli/reflex_cli/constants/__init__.py b/packages/reflex-hosting-cli/reflex_cli/constants/__init__.py new file mode 100644 index 00000000000..006935f8611 --- /dev/null +++ b/packages/reflex-hosting-cli/reflex_cli/constants/__init__.py @@ -0,0 +1,15 @@ +"""The constants package.""" + +from .base import Dirs, LogLevel, Reflex +from .compiler import ComponentName +from .hosting import Hosting, ReflexHostingCli, RequirementsTxt + +__ALL__ = [ + Hosting, + LogLevel, + Reflex, + ComponentName, + ReflexHostingCli, + RequirementsTxt, + Dirs, +] diff --git a/packages/reflex-hosting-cli/reflex_cli/constants/base.py b/packages/reflex-hosting-cli/reflex_cli/constants/base.py new file mode 100644 index 00000000000..d9c1996c6e9 --- /dev/null +++ b/packages/reflex-hosting-cli/reflex_cli/constants/base.py @@ -0,0 +1,58 @@ +"""Base file for constants that don't fit any other categories.""" + +from __future__ import annotations + +from enum import Enum +from types import SimpleNamespace + +from platformdirs import PlatformDirs + + +class Reflex(SimpleNamespace): + """Base constants concerning Reflex. This is duplicate of the same class in reflex main.""" + + # The name of the Reflex package. + MODULE_NAME = "reflex" + + # Files and directories used to init a new project. + # The directory to store reflex dependencies. + DIR = ( + # on windows, we use C:/Users//AppData/Local/reflex. + # on macOS, we use ~/Library/Application Support/reflex. + # on linux, we use ~/.local/share/reflex. + PlatformDirs(MODULE_NAME, False).user_data_dir + ) + + +# Log levels +class LogLevel(str, Enum): + """The log levels.""" + + DEBUG = "debug" + DEFAULT = "default" + INFO = "info" + WARNING = "warning" + ERROR = "error" + CRITICAL = "critical" + + def __le__(self, other: LogLevel | str) -> bool: + """Compare log levels. + + Args: + other: The other log level. + + Returns: + True if the log level is less than or equal to the other log level. + + """ + if isinstance(other, str): + other = LogLevel(other.lower().strip()) + levels = list(LogLevel) + return levels.index(self) <= levels.index(other) + + +class Dirs(SimpleNamespace): + """Various directories/paths used by the CLI.""" + + # The cloud.yaml file. + CLOUD_YAML = "cloud.yml" diff --git a/packages/reflex-hosting-cli/reflex_cli/constants/compiler.py b/packages/reflex-hosting-cli/reflex_cli/constants/compiler.py new file mode 100644 index 00000000000..8518cd0756b --- /dev/null +++ b/packages/reflex-hosting-cli/reflex_cli/constants/compiler.py @@ -0,0 +1,33 @@ +"""Compiler variables.""" + +from enum import Enum +from types import SimpleNamespace + + +class Ext(SimpleNamespace): + """Extension used in Reflex.""" + + # The extension for JS files. + JS = ".js" + # The extension for python files. + PY = ".py" + # The extension for css files. + CSS = ".css" + # The extension for zip files. + ZIP = ".zip" + + +class ComponentName(Enum): + """Component names.""" + + BACKEND = "Backend" + FRONTEND = "Frontend" + + def zip(self): + """Give the zip filename for the component. + + Returns: + The lower-case filename with zip extension. + + """ + return self.value.lower() + Ext.ZIP diff --git a/packages/reflex-hosting-cli/reflex_cli/constants/hosting.py b/packages/reflex-hosting-cli/reflex_cli/constants/hosting.py new file mode 100644 index 00000000000..3371acfbbda --- /dev/null +++ b/packages/reflex-hosting-cli/reflex_cli/constants/hosting.py @@ -0,0 +1,56 @@ +"""Constants related to hosting.""" + +import os +from pathlib import Path +from types import SimpleNamespace + +from packaging import version + +from reflex_cli.constants.base import Reflex + + +class ReflexHostingCli(SimpleNamespace): + """Constants related to reflex-hosting-cli.""" + + MODULE_NAME = "reflex-hosting-cli" + + MINIMUM_REFLEX_VERSION = version.parse("0.6.6.post1") + + RECOMMENDED_REFLEX_VERSION = version.parse("0.7.6") + + +class Hosting(SimpleNamespace): + """Constants related to hosting.""" + + # The hosting config json file + HOSTING_JSON = Path(Reflex.DIR) / "hosting_v1.json" + HOSTING_JSON_V0 = Path(Reflex.DIR) / "hosting_v0.json" + # The hosting service backend URL + HOSTING_SERVICE = os.environ.get( + "REFLEX_CLOUD_BACKEND_URL", + os.environ.get("CP_BACKEND_URL", "https://build.reflex.dev"), + ) + # The hosting service webpage URL + HOSTING_SERVICE_UI = os.environ.get( + "REFLEX_CLOUD_URL", os.environ.get("CP_WEB_URL", "https://build.reflex.dev") + ) + # The time to wait for HTTP requests to the backend + TIMEOUT = 10 + # The number of times to retry authentication + AUTH_RETRY_LIMIT = 5 + # How long to wait between retry attempts + AUTH_RETRY_SLEEP_DURATION = 5 + + # Aliases for compatibility with previous versions of Reflex + CP_BACKEND_URL = HOSTING_SERVICE + CP_WEB_URL = HOSTING_SERVICE_UI + + +class RequirementsTxt(SimpleNamespace): + """Requirements.txt constants.""" + + # The requirements.txt file. + FILE = "requirements.txt" + + # The pyproject.toml file. + PYPROJECT = "pyproject.toml" diff --git a/packages/reflex-hosting-cli/reflex_cli/core/__init__.py b/packages/reflex-hosting-cli/reflex_cli/core/__init__.py new file mode 100644 index 00000000000..7eabc703f0d --- /dev/null +++ b/packages/reflex-hosting-cli/reflex_cli/core/__init__.py @@ -0,0 +1 @@ +"""core module for reflex-cli.""" diff --git a/packages/reflex-hosting-cli/reflex_cli/core/config.py b/packages/reflex-hosting-cli/reflex_cli/core/config.py new file mode 100644 index 00000000000..f1cbb31b43c --- /dev/null +++ b/packages/reflex-hosting-cli/reflex_cli/core/config.py @@ -0,0 +1,321 @@ +"""Module for the Config class.""" + +from __future__ import annotations + +import dataclasses +import typing +from collections.abc import Sequence +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Literal, get_origin + +from reflex_cli import constants +from reflex_cli.utils.exceptions import ConfigError, ConfigInvalidFieldValueError + +RegionOption = Literal[ + "ams", + "arn", + "bog", + "cdg", + "dfw", + "ewr", + "fra", + "gru", + "iad", + "jnb", + "lax", + "lhr", + "nrt", + "ord", + "sjc", + "sin", + "syd", + "yyz", +] + +VmType = Literal[ + "c1m.5", + "c1m1", + "c1m2", + "c2m2", + "c2m4", + "c4m4", + "c4m8", + "pc1m2", + "pc2m4", + "pc2m8", + "pc4m8", +] + + +def _validate_type(value: Any, _type: type, key: str = ""): + if not isinstance(value, _type): + raise ( + ConfigInvalidFieldValueError( + f"Invalid value for {key}. Expected a {_type}, got {value} of type {type(value).__name__}." + ) + if key + else ConfigInvalidFieldValueError( + f"Invalid value. Expected a {_type}, got {value} of type {type(value).__name__}." + ) + ) + + +def _validate_literal(value: Any, key: str = "", valid_values: Sequence[str] = ()): + _validate_type(value, str, key) + if value not in valid_values: + raise ( + ConfigInvalidFieldValueError( + f"Invalid value for {key}. Expected one of {valid_values}, got {value}." + ) + if key + else ConfigInvalidFieldValueError( + f"Invalid value. Expected one of {valid_values}, got {value}." + ) + ) + + +def _validate_list(value: Any, item_type: type, key: str = ""): + _validate_type(value, list, key) + for item in value: + _validate_dispatch(item, item_type, key=f"{key} item" if key else "") + + +def _validate_dict(value: Any, key_type: type, value_type: type, key: str = ""): + _validate_type(value, dict, key) + for key, val in value.items(): + _validate_dispatch(key, key_type, key=f"{key} key" if key else "") + _validate_dispatch(val, value_type, key=f"{key} value" if key else "") + + +def _validate_optional(value: Any, non_optional_type: type, key: str = ""): + if value is not None: + _validate_dispatch(value, non_optional_type, key) + + +def _validate_dispatch( + value: Any, + _type: Any, + key: str = "", +): + if _type in [str, int, float, bool]: + _validate_type(value, _type, key) + origin = get_origin(_type) + if origin is typing.Union: + args = typing.get_args(_type) + if len(args) == 2 and type(None) in args: + non_optional_type = next(arg for arg in args if arg is not type(None)) + _validate_optional(value, non_optional_type, key) + return + if origin is list: + item_type = typing.get_args(_type)[0] + _validate_list(value, item_type, key) + if origin is typing.Literal: + _validate_literal(value, key, typing.get_args(_type)) + if origin is dict: + key_type, value_type = typing.get_args(_type) + _validate_dict(value, key_type, value_type, key) + + +@dataclass +class Config: + """Configuration class for the CLI.""" + + name: str | None = dataclasses.field(default=None) + description: str | None = dataclasses.field(default=None) + vmtype: VmType | None = dataclasses.field(default=None) + regions: dict[RegionOption, int] | None = dataclasses.field(default=None) + hostname: str | None = dataclasses.field(default=None) + envfile: str | None = dataclasses.field(default=None) + project: str | None = dataclasses.field(default=None) + packages: list[str] = dataclasses.field(default_factory=list) + appid: str | None = dataclasses.field(default=None) + strategy: str | None = dataclasses.field(default=None) + include_db: bool = dataclasses.field(default=False) + + _cloud_config_path: Path | None = dataclasses.field(default=None) + + def __post_init__(self): + """Post-initialization validation for the Config class. + + Raises: + ConfigInvalidFieldValueError: If any field value is invalid. + + # noqa: DAR401 + + """ + evaluated_type = typing.get_type_hints(Config) + for field in dataclasses.fields(self): + if field.name.startswith("_"): + continue + field_type = evaluated_type.get(field.name) + if field_type is None: + raise ConfigInvalidFieldValueError(f"Invalid field: {field}") + try: + _validate_dispatch( + getattr(self, field.name), field_type, key=field.name + ) + except ValueError as e: + if self._cloud_config_path: + raise ConfigInvalidFieldValueError( + f"Invalid {self._cloud_config_path.name}. " + str(e) + ).with_traceback(e.__traceback__) from None + + @classmethod + def _filter_dict( + cls, + data: dict[str, Any], + ) -> dict[str, Any]: + """Filters a dictionary to only include fields defined in the Config class. + + Args: + data: The dictionary to filter. + + Returns: + dict[str, Any]: A filtered dictionary containing only valid Config fields. + + """ + fields_keys = {field.name for field in dataclasses.fields(cls)} + return { + key: value + for key, value in data.items() + if key in fields_keys and value is not None + } + + @classmethod + def from_yaml( + cls, + yaml_path: Path = Path.cwd() / constants.Dirs.CLOUD_YAML, + env: str | None = None, + ) -> Config: + """Creates a Config instance from a YAML file. + + Args: + yaml_path: The path to the YAML file. Defaults to "cloud.yml" in the current directory. + env: The environment to load the config for. + + Returns: + Config: A Config instance with the values from the YAML file. + + Raises: + ConfigError: If the YAML file is not found. + + """ + if not yaml_path.exists(): + raise ConfigError(f"Config file not found at {yaml_path}.") + + try: + import yaml + except ImportError as e: + raise ConfigError( + "YAML support is not available. Please install PyYAML to use this feature." + ) from e + + with yaml_path.open() as file: + data = yaml.safe_load(file) + if env: + data = data.get("env", {}).get(env, {}) + data = cls._filter_dict(data) + return cls(_cloud_config_path=yaml_path, **data) + + @classmethod + def from_toml( + cls, + pyproject_path: Path = Path.cwd() / "pyproject.toml", + env: str | None = None, + ) -> Config: + """Creates a Config instance from a TOML file. + + Args: + pyproject_path: The path to the TOML file. Defaults to "pyproject.toml" in the current directory. + env: The environment to load the config for. + + Returns: + Config: A Config instance with the values from the TOML file. + + Raises: + ConfigError: If the TOML file is not found. + + """ + if not pyproject_path.exists(): + raise ConfigError(f"Config file not found at {pyproject_path}.") + + try: + import tomllib + except ImportError as e: + raise ConfigError( + "TOML support is not available. Please use Python 3.11 or later." + ) from e + + with pyproject_path.open("rb") as file: + pyproject_data = tomllib.load(file) + if not isinstance(pyproject_data, dict): + raise ConfigError( + f"Invalid TOML file format at {pyproject_path}. Expected a dictionary." + ) + tools = pyproject_data.get("tool", {}) + if not isinstance(tools, dict): + raise ConfigError( + f"Invalid TOML file format at {pyproject_path}. Expected 'tool' to be a dictionary." + ) + if "reflex-cloud" not in tools: + raise ConfigError( + f"Invalid TOML file format at {pyproject_path}. Expected 'tool.reflex-cloud' to be present." + ) + data = tools["reflex-cloud"] + if env: + data = data.get("env", {}).get(env, {}) + data = cls._filter_dict(data) + return cls(_cloud_config_path=pyproject_path, **data) + + @classmethod + def from_yaml_or_toml_or_default(cls) -> Config: + """Creates a Config instance from either a YAML or TOML file, or returns a default instance. + + Returns: + Config: A Config instance with values from the YAML or TOML file, or a default instance if neither file exists. + + """ + return cls.from_yaml_or_toml_or_none() or cls() + + @classmethod + def from_yaml_or_toml_or_none(cls, env: str | None = None) -> Config | None: + """Attempts to create a Config instance from either a YAML or TOML file. + + Args: + env: The environment to load the config for. If provided, it will look for environment + + Returns: + Config | None: A Config instance with values from the YAML or TOML file, or None if neither file exists or is valid. + + """ + try: + return cls.from_yaml(env=env) + + except ConfigError: + try: + return cls.from_toml(env=env) + except ConfigError: + return None + + def with_overrides(self, **kwargs: Any) -> Config: + """Creates a new Config instance with overrides. + + Args: + **kwargs: Key-value pairs of fields to override. The values take + precedence over the existing instance values. + + Returns: + Config: A new Config instance with updated values. + + """ + return dataclasses.replace(self, **kwargs) + + def exists(self) -> bool: + """Check if the config file exists. + + Returns: + bool: True if the config file exists, False otherwise. + + """ + return bool(self._cloud_config_path) and self._cloud_config_path.exists() diff --git a/packages/reflex-hosting-cli/reflex_cli/deployments.py b/packages/reflex-hosting-cli/reflex_cli/deployments.py new file mode 100644 index 00000000000..665d417592e --- /dev/null +++ b/packages/reflex-hosting-cli/reflex_cli/deployments.py @@ -0,0 +1,242 @@ +"""Disabled Hosting CLI deployments sub-commands.""" + +from __future__ import annotations + +from importlib.util import find_spec +from typing import TYPE_CHECKING + +import click + +from reflex_cli import constants +from reflex_cli.utils import disabled_v1_hosting + +if TYPE_CHECKING: + import typer + + +TIME_FORMAT_HELP = "Accepts ISO 8601 format, unix epoch or time relative to now. For time relative to now, use the format: . Valid units are d (day), h (hour), m (minute), s (second). For example, 1d for 1 day ago from now." +MIN_LOGS_LIMIT = 50 +MAX_LOGS_LIMIT = 1000 + + +@click.group() +def deployments_cli(): + """Commands for managing deployments.""" + disabled_v1_hosting() + + +@deployments_cli.command(name="list") +@click.option( + "--loglevel", + type=click.Choice([level.value for level in constants.LogLevel]), + default=constants.LogLevel.INFO.value, + help="The log level to use.", +) +@click.option( + "--json/--no-json", + "-j", + "as_json", + is_flag=True, + help="Whether to output the result in json format.", +) +def list_deployments(loglevel: str, as_json: bool): + """List all the hosted deployments of the authenticated user. + + Args: + loglevel: The log level to use. + as_json: Whether to output the result in json format. + + """ + + +@deployments_cli.command(name="delete") +@click.argument("key", required=True) +@click.option( + "--loglevel", + type=click.Choice([level.value for level in constants.LogLevel]), + default=constants.LogLevel.INFO.value, + help="The log level to use.", +) +def delete_deployment(key: str, loglevel: str): + """Delete a hosted instance. + + Args: + key: The name of the deployment. + loglevel: The log level to use. + + """ + + +@deployments_cli.command(name="status") +@click.argument("key", required=True) +@click.option( + "--loglevel", + type=click.Choice([level.value for level in constants.LogLevel]), + default=constants.LogLevel.INFO.value, + help="The log level to use.", +) +def get_deployment_status(key: str, loglevel: str): + """Check the status of a deployment. + + Args: + key: The name of the deployment. + loglevel: The log level to use. + + """ + + +@deployments_cli.command(name="logs") +@click.argument("key", required=True) +@click.option( + "--from", "from_timestamp", help=f"The start time of the logs. {TIME_FORMAT_HELP}" +) +@click.option( + "--to", "to_timestamp", help=f"The end time of the logs. {TIME_FORMAT_HELP}" +) +@click.option( + "--limit", + type=int, + help=f"The number of logs to return. The acceptable range is {MIN_LOGS_LIMIT}-{MAX_LOGS_LIMIT}.", +) +@click.option( + "--loglevel", + type=click.Choice([level.value for level in constants.LogLevel]), + default=constants.LogLevel.INFO.value, + help="The log level to use.", +) +def get_deployment_logs( + key: str, + from_timestamp: str | None, + to_timestamp: str | None, + limit: int | None, + loglevel: str, +): + """Get the logs for a deployment. + + Args: + key: The name of the deployment. + from_timestamp: The start time of the logs. + to_timestamp: The end time of the logs. + limit: The maximum number of logs to return. + loglevel: The log level to use. + + """ + + +@deployments_cli.command(name="build-logs") +@click.argument("key", required=True) +@click.option( + "--from", "from_timestamp", help=f"The start time of the logs. {TIME_FORMAT_HELP}" +) +@click.option( + "--to", "to_timestamp", help=f"The end time of the logs. {TIME_FORMAT_HELP}" +) +@click.option( + "--limit", + type=int, + help=f"The number of logs to return. The acceptable range is {MIN_LOGS_LIMIT}-{MAX_LOGS_LIMIT}.", +) +@click.option( + "--loglevel", + type=click.Choice([level.value for level in constants.LogLevel]), + default=constants.LogLevel.INFO.value, + help="The log level to use.", +) +def get_deployment_build_logs( + key: str, + from_timestamp: str | None, + to_timestamp: str | None, + limit: int | None, + loglevel: str, +): + """Get the build logs for a deployment. + + Args: + key: The name of the deployment. + from_timestamp: The start time of the logs. + to_timestamp: The end time of the logs. + limit: The maximum number of logs to return. + loglevel: The log level to use. + + """ + + +@deployments_cli.command(name="regions") +@click.option( + "--loglevel", + type=click.Choice([level.value for level in constants.LogLevel]), + default=constants.LogLevel.INFO.value, + help="The log level to use.", +) +@click.option( + "--json/--no-json", + "-j", + "as_json", + is_flag=True, + help="Whether to output the result in json format.", +) +def get_deployment_regions(loglevel: str, as_json: bool): + """List all the regions of the hosting service. + + Args: + loglevel: The log level to use. + as_json: Whether to output the result in json format. + + """ + + +@deployments_cli.command(name="share") +@click.option( + "--url", + help="The URL of the deployed app to share. If not provided, the latest deployment is shared.", +) +@click.option( + "--loglevel", + type=click.Choice([level.value for level in constants.LogLevel]), + default=constants.LogLevel.INFO.value, + help="The log level to use.", +) +def share_deployment(url: str | None, loglevel: str): + """Share a deployment. + + Args: + url: The URL of the deployed app to share. + loglevel: The log level to use. + + """ + + +def _patch_typer(click_instance: click.Command) -> typer.Typer: + import functools + + import typer + from typer.models import TyperInfo + + fake_typer_app = typer.Typer(add_completion=False) + + fake_typer_app.callback()(lambda: None) + + original_get_group_from_info = typer.main.get_group_from_info + + def get_group_from_info(group_info: TyperInfo, *args, **kwargs): + if group_info.typer_instance is fake_typer_app: + click_instance.name = group_info.name + return click_instance + return original_get_group_from_info(group_info, *args, **kwargs) + + functools.update_wrapper( + get_group_from_info, + original_get_group_from_info, + ) + + typer.main.get_group_from_info = get_group_from_info + + return fake_typer_app + + +if ( + find_spec("typer") is not None + and find_spec("typer.core") is not None + and find_spec("typer.models") is not None +): + deployments_cli = _patch_typer(deployments_cli) # pyright: ignore[reportAssignmentType] diff --git a/packages/reflex-hosting-cli/reflex_cli/utils/__init__.py b/packages/reflex-hosting-cli/reflex_cli/utils/__init__.py new file mode 100644 index 00000000000..02b3f38df09 --- /dev/null +++ b/packages/reflex-hosting-cli/reflex_cli/utils/__init__.py @@ -0,0 +1,25 @@ +"""Reflex utilities.""" + +import click + +from reflex_cli.constants.hosting import ReflexHostingCli + +from . import console + + +def disabled_v1_hosting(*args, **kwargs): + """Print error and exit when using v1 hosting commands. + + Args: + *args: ignored. + **kwargs: ignored. + + Raises: + Exit: Always. + + """ + console.error( + "The alpha hosting service has been decommissioned as of Dec 5, 2024. " + f"Please upgrade to reflex>={ReflexHostingCli.MINIMUM_REFLEX_VERSION} to use Reflex Cloud hosting." + ) + raise click.exceptions.Exit(1) diff --git a/packages/reflex-hosting-cli/reflex_cli/utils/console.py b/packages/reflex-hosting-cli/reflex_cli/utils/console.py new file mode 100644 index 00000000000..b0c3d0c4d9e --- /dev/null +++ b/packages/reflex-hosting-cli/reflex_cli/utils/console.py @@ -0,0 +1,226 @@ +"""Functions to communicate to the user via console.""" + +from __future__ import annotations + +from collections.abc import Sequence +from typing import overload + +from rich.console import Console + +from reflex_cli.constants import LogLevel + +# Console for pretty printing. +_console = Console() + +# The current log level. +_LOG_LEVEL = LogLevel.INFO + + +def set_log_level(log_level: LogLevel | str): + """Set the log level. + + Args: + log_level: The log level to set. + + """ + if isinstance(log_level, str): + try: + log_level = LogLevel(log_level) + except ValueError: + log_level = LogLevel.INFO + global _LOG_LEVEL + _LOG_LEVEL = log_level + + +def print(msg: str, **kwargs): + """Print a message. + + Args: + msg: The message to print. + kwargs: Keyword arguments to pass to the print function. + + """ + _console.print(msg, **kwargs) + + +def print_table( + tabular_data: list[list[str]], + headers: Sequence[str] = (), +) -> None: + """Print a table to the console. + + Args: + tabular_data: The data to print in tabular format. + headers: The headers for the table. + """ + from rich.table import Table + + table = Table() + + for column in headers: + table.add_column(column) + + for row in tabular_data: + table.add_row(*row) + + _console.print(table) + + +def debug(msg: str, **kwargs): + """Print a debug message. + + Args: + msg: The debug message. + kwargs: Keyword arguments to pass to the print function. + + """ + if _LOG_LEVEL <= LogLevel.DEBUG: + print(f"[blue]Debug: {msg}[/blue]", **kwargs) + + +def info(msg: str, **kwargs): + """Print an info message. + + Args: + msg: The info message. + kwargs: Keyword arguments to pass to the print function. + + """ + if _LOG_LEVEL <= LogLevel.INFO: + print(f"[cyan]Info: {msg}[/cyan]", **kwargs) + + +def success(msg: str, **kwargs): + """Print a success message. + + Args: + msg: The success message. + kwargs: Keyword arguments to pass to the print function. + + """ + if _LOG_LEVEL <= LogLevel.WARNING: + print(f"[green]Success: {msg}[/green]", **kwargs) + + +def log(msg: str, **kwargs): + """Takes a string and logs it to the console. + + Args: + msg: The message to log. + kwargs: Keyword arguments to pass to the print function. + + """ + if _LOG_LEVEL <= LogLevel.INFO: + _console.log(msg, **kwargs) + + +def rule(title: str, **kwargs): + """Prints a horizontal rule with a title. + + Args: + title: The title of the rule. + kwargs: Keyword arguments to pass to the print function. + + """ + _console.rule(title, **kwargs) + + +def warn(msg: str, **kwargs): + """Print a warning message. + + Args: + msg: The warning message. + kwargs: Keyword arguments to pass to the print function. + + """ + if _LOG_LEVEL <= LogLevel.WARNING: + print(f"[orange1]Warning: {msg}[/orange1]", **kwargs) + + +def error(msg: str, **kwargs): + """Print an error message. + + Args: + msg: The error message. + kwargs: Keyword arguments to pass to the print function. + + """ + if _LOG_LEVEL <= LogLevel.ERROR: + print(f"[red]{msg}[/red]", **kwargs) + + +@overload +def ask( + question: str, + *, + choices: list[str] | None = None, + show_choices: bool = True, +) -> str: ... + + +@overload +def ask( + question: str, + *, + default: str, + choices: list[str] | None = None, + show_choices: bool = True, +) -> str: ... + + +def ask( + question: str, + *, + choices: list[str] | None = None, + show_choices: bool = True, + default: str | None = None, +) -> str | None: + """Takes a prompt question and optionally a list of choices + and returns the user input. + + Args: + question: The question to ask the user. + choices: A list of choices to select from. + show_choices: Whether to show the choices. + default: The default option selected. + + Returns: + A string with the user input. + + """ + from rich.prompt import Prompt + + return Prompt.ask( + question, choices=choices, default=default, show_choices=show_choices + ) + + +def progress(): + """Create a new progress bar. + + + Returns: + A new progress bar. + + """ + from rich.progress import MofNCompleteColumn, Progress, TimeElapsedColumn + + return Progress( + *Progress.get_default_columns()[:-1], + MofNCompleteColumn(), + TimeElapsedColumn(), + ) + + +def status(*args, **kwargs): + """Create a status with a spinner. + + Args: + *args: Args to pass to the status. + **kwargs: Kwargs to pass to the status. + + Returns: + A new status. + + """ + return _console.status(*args, **kwargs) diff --git a/packages/reflex-hosting-cli/reflex_cli/utils/dependency.py b/packages/reflex-hosting-cli/reflex_cli/utils/dependency.py new file mode 100644 index 00000000000..0709d123a4d --- /dev/null +++ b/packages/reflex-hosting-cli/reflex_cli/utils/dependency.py @@ -0,0 +1,170 @@ +"""Building the app and initializing all prerequisites.""" + +from __future__ import annotations + +import importlib.metadata +import io +import re +import subprocess +import sys +from pathlib import Path +from urllib.parse import urlparse + +from reflex_cli import constants +from reflex_cli.utils import console + + +def detect_encoding(filename: Path) -> str | None: + """Detect the encoding of the given file. + + Args: + filename: The file to detect encoding for. + + Raises: + FileNotFoundError: If the file `filename` does not exist. + + Returns: + The encoding of the file if file exits and encoding is detected, otherwise None. + + """ + if not filename.exists(): + raise FileNotFoundError + + for encoding in [ + None if sys.version_info < (3, 10) else io.text_encoding(None), + "utf-8", + ]: + try: + filename.read_text(encoding) + except UnicodeDecodeError: # noqa: PERF203 + continue + except Exception: + return None + else: + return encoding + else: + return None + + +def does_requirements_file_or_pyproject_exist() -> bool: + """Check if requirements.txt or pyproject.toml exists. + + Returns: + True if either file exists, False otherwise. + """ + return ( + Path(constants.RequirementsTxt.FILE).exists() + or Path(constants.RequirementsTxt.PYPROJECT).exists() + ) + + +def check_requirements(): + """Check if the requirements.txt needs update based on current environment. + Throw warnings if too many installed or unused (based on imports) packages in + the local environment. + + Returns: + None + + Raises: + SystemExit: If no requirements.txt is found. + """ + if not does_requirements_file_or_pyproject_exist(): + console.warn("No requirements.txt or pyproject.toml found.") + return + + if not Path(constants.RequirementsTxt.FILE).exists(): + return + + # First check the encoding of requirements.txt if applicable. If unable to determine encoding + # will not proceed to check for requirement updates. + encoding = "utf-8" + if ( + Path(constants.RequirementsTxt.FILE).exists() + and (encoding := detect_encoding(Path(constants.RequirementsTxt.FILE))) is None + ): + return + + # Run the pipdeptree command and get the output + try: + result = subprocess.run( + [sys.executable, "-m", "pip", "freeze"], + capture_output=True, + text=True, + check=True, + ) + except subprocess.CalledProcessError as cpe: + console.debug(f"Unable to run pip freeze in subprocess: {cpe}") + console.warn( + "Unable to detect installed packages in your environment using pip freeze." + " Please make sure your requirements.txt is up to date." + ) + return + + # Filter the output lines using a regular expression + lines = result.stdout.split("\n") + new_requirements_lines: set[str] = set() + for line in lines: + if re.match(r"^\w+", line): + new_requirements_lines.add(f"{line}\n") + + current_requirements_lines: set[str] = set() + if Path(constants.RequirementsTxt.FILE).exists(): + with Path(constants.RequirementsTxt.FILE).open(encoding=encoding) as f: + current_requirements_lines = set(f) + console.debug("Current requirements.txt:") + console.debug("".join(current_requirements_lines)) + + diff = list(new_requirements_lines - current_requirements_lines) + + if not diff: + return + + if not current_requirements_lines: + console.warn("It seems like there's no requirements.txt in your project.") + raise SystemExit("No requirements.txt found.") + + console.warn("Detected difference in requirements.txt and python env.") + console.warn("The requirements.txt may need to be updated.") + console.ask("Do you wish to proceed? (ctl+c to cancel)") + return + + +def get_reflex_version() -> str: + """Get the version of the reflex package. + + Returns: + The version of the reflex package. + """ + return importlib.metadata.version(constants.Reflex.MODULE_NAME) + + +def is_valid_url(url: str) -> bool: + """Check if the given URL is valid. + + Args: + url: The URL to check. + + Returns: + True if the URL is valid, otherwise False. + + """ + try: + result = urlparse(url) + return all([result.scheme, result.netloc]) + except ValueError: + return False + + +def extract_domain(url: str) -> str: + """Extract the domain from the given URL. + + Args: + url: The URL to extract the domain from. + + Returns: + The domain part of the url. + + """ + parsed_url = urlparse(url) + return parsed_url.netloc diff --git a/packages/reflex-hosting-cli/reflex_cli/utils/exceptions.py b/packages/reflex-hosting-cli/reflex_cli/utils/exceptions.py new file mode 100644 index 00000000000..f40a3d493c7 --- /dev/null +++ b/packages/reflex-hosting-cli/reflex_cli/utils/exceptions.py @@ -0,0 +1,37 @@ +"""Custom Exceptions.""" + + +class ReflexHostingCliError(Exception): + """Base exception for all Reflex Hosting CLI exceptions.""" + + +class NotAuthenticatedError(ReflexHostingCliError): + """Raised when the user is not authenticated.""" + + +class GetAppError(ReflexHostingCliError): + """Raised when retrieving an app fails.""" + + +class ScaleAppError(ReflexHostingCliError): + """Raised when scaling an app fails.""" + + +class ResponseError(ReflexHostingCliError): + """Raised when a response is not as expected.""" + + +class ConfigError(ReflexHostingCliError): + """Raised when there is an error with the config.""" + + +class ConfigInvalidFieldValueError(ReflexHostingCliError): + """Raised when a field in the config has an invalid value.""" + + +class ScaleTypeError(ReflexHostingCliError): + """Raised when the scale type is invalid.""" + + +class ScaleParamError(ReflexHostingCliError): + """Raised when the scale parameter is invalid.""" diff --git a/packages/reflex-hosting-cli/reflex_cli/utils/hosting.py b/packages/reflex-hosting-cli/reflex_cli/utils/hosting.py new file mode 100644 index 00000000000..d521a9baa47 --- /dev/null +++ b/packages/reflex-hosting-cli/reflex_cli/utils/hosting.py @@ -0,0 +1,2210 @@ +"""Hosting service related utilities.""" + +from __future__ import annotations + +import contextlib +import dataclasses +import importlib.metadata +import json +import platform +import re +import subprocess +import sys +import time +import uuid +import webbrowser +from collections.abc import Mapping +from enum import Enum +from http import HTTPStatus +from pathlib import Path +from typing import Any, TypedDict +from urllib.parse import urljoin + +import click + +import reflex_cli.constants as constants +from reflex_cli.core.config import Config, RegionOption +from reflex_cli.utils import console, dependency +from reflex_cli.utils.dependency import is_valid_url +from reflex_cli.utils.exceptions import ( + GetAppError, + NotAuthenticatedError, + ResponseError, + ScaleAppError, + ScaleParamError, +) + + +class ScaleType(str, Enum): + """The scale type for an application.""" + + SIZE = "size" + REGION = "region" + + +@dataclasses.dataclass +class ScaleAppCliArgs: + """CLI arguments for scaling an application.""" + + type: ScaleType | None = None + regions: dict[str, int] | None = None + vm_type: str | None = None + + @classmethod + def create( + cls, + regions: list[str] | dict[str, int] | None = None, + vm_type: str | None = None, + scale_type: ScaleType | str | None = None, + ) -> ScaleAppCliArgs: + """Create a ScaleAppCliArgs object. + + Args: + regions: The regions to scale to. + vm_type: The VM size to scale to. + scale_type: The scale type. + + Returns: + An instance of ScaleAppCliArgs. + + Raises: + ScaleAppError: If both regions and vm_type are provided. + + """ + if isinstance(regions, list): + regions = dict.fromkeys(regions, 1) + + if vm_type is not None and regions: + raise ScaleAppError("Only one of --vmtype or --regions should be provided.") + return cls(ScaleType(scale_type) if scale_type else None, regions, vm_type) + + @property + def is_valid(self) -> bool: + """Check if the CLI arguments are valid. + + Returns: + bool: True if either vmtype or regions is set. + + """ + return bool(self.regions or self.vm_type) + + +class Region(TypedDict): + """Region for scaling an application.""" + + name: RegionOption + number_of_machines: int + + +@dataclasses.dataclass +class ScaleParams: + """Parameters for scaling an application.""" + + type: ScaleType | None = None + vm_type: str | None = None + regions: tuple[Region, ...] = () + + @classmethod + def create( + cls, + scale_type: ScaleType | None = None, + vm_type: str | None = None, + regions: list[RegionOption] | Mapping[RegionOption, int] | None = None, + ): + """Create a ScaleParams object. + + Args: + scale_type: The scale type. + vm_type: The VM type to scale to. + regions: The regions to scale to. + + Returns: + ScaleParams: The created ScaleParams object. + + """ + if isinstance(regions, list): + regions = dict.fromkeys(regions, 1) + return cls( + scale_type, + vm_type, + tuple( + Region(name=name, number_of_machines=number) + for name, number in regions.items() + ) + if regions + else (), + ) + + @classmethod + def from_config(cls, config: Config) -> ScaleParams: + """Create a ScaleParams object from a Config object. + + Args: + config: The Config object. + + Returns: + The created ScaleParams object. + + """ + return cls.create( + vm_type=config.vmtype, + regions={**config.regions} if config.regions else None, + ) + + def set_type(self, scale_type: ScaleType | str | None) -> ScaleParams: + """Set the scale type. + + Args: + scale_type: The scale type. + + Returns: + The ScaleParams object with the scale type set. + + """ + return ScaleParams( + ScaleType(scale_type) if scale_type else None, self.vm_type, self.regions + ) + + def set_type_from_cli_args(self, cli_args: ScaleAppCliArgs) -> ScaleParams: + """Set the scale type from CLI arguments. + + Args: + cli_args: The CLI arguments. + + Returns: + The ScaleParams object with the scale type set. + + Raises: + ScaleParamError: If the scale type is not provided when using cloud.yml or pyproject.toml. + + """ + scale_type = cli_args.type + + if scale_type is None and not cli_args.is_valid: + raise ScaleParamError( + "specify the type of scaling using --scale-type when using cloud.yml or pyproject.toml" + ) + + if scale_type is not None and cli_args.is_valid: + console.warn( + "using --scale-type with --regions or --vmtype will have no effect" + ) + + if not cli_args.is_valid: + if scale_type == ScaleType.SIZE and not cli_args.vm_type: + raise ScaleParamError( + f"'vmtype' should be provided in the {constants.Dirs.CLOUD_YAML} for size scaling" + ) + + if scale_type == ScaleType.REGION and not cli_args.regions: + raise ScaleParamError( + f"'regions' should be provided in the {constants.Dirs.CLOUD_YAML} for region scaling" + ) + + if cli_args.is_valid: + return self.set_type( + ScaleType(ScaleType.REGION) + if cli_args.regions + else ScaleType(ScaleType.SIZE) + ) + return self.set_type(ScaleType(scale_type) if scale_type else None) + + def as_json(self) -> dict[str, Any]: + """Convert the object to a dictionary. + + Returns: + dict: The object as a dictionary. + + """ + if self.type is None: + self.type = ScaleType.REGION + return ( + { + "type": str(self.type.value), + "size": self.vm_type, + } + if self.type == ScaleType.SIZE + else { + "type": str(self.type.value), + "regions": { + region["name"]: region["number_of_machines"] + for region in self.regions + }, + } + ) + + +@dataclasses.dataclass +class UnAuthenticatedClient: + """A client that is not authenticated.""" + + @staticmethod + def authenticate() -> AuthenticatedClient: + """Authenticate the client. + + Returns: + An authenticated client. + + """ + access_token, validated_info = authenticate_on_browser() + return AuthenticatedClient(access_token, validated_info) + + +@dataclasses.dataclass +class AuthenticatedClient: + """A client that is authenticated.""" + + token: str + validated_data: dict[str, Any] + + +def get_authentication_client( + token: str | None = None, +) -> AuthenticatedClient | UnAuthenticatedClient: + """Get an authentication client. + + Args: + token: The authentication token. + + Returns: + An authenticated client if the token is valid, otherwise an unauthenticated client. + + """ + access_token = token or get_existing_access_token() + if access_token: + validated_info = validate_token_with_retries(access_token) + if validated_info: + return AuthenticatedClient(access_token, validated_info) + return UnAuthenticatedClient() + + +def get_authenticated_client( + token: str | None = None, interactive: bool = True +) -> AuthenticatedClient: + """Get an authenticated client. + + Args: + token: The authentication token. + interactive: If running in interactive mode. + + Returns: + An authenticated client. + + Raises: + Exit: If no token is provided in non-interactive mode. + + """ + env_token = get_existing_access_token() if not token else "" + if not token and not env_token and not interactive: + console.error("Token is required for non-interactive mode.") + raise click.exceptions.Exit(1) + + client = get_authentication_client(token) + if isinstance(client, UnAuthenticatedClient): + return client.authenticate() + return client + + +class SilentBackgroundBrowser(webbrowser.BackgroundBrowser): + """A webbrowser.BackgroundBrowser that does not raise exceptions when it fails to open a browser.""" + + def open(self, url: str, new: int = 0, autoraise: bool = True): + """Open url in a new browser window. + + Args: + url: The URL to open. + new: Whether to open in a new window (2), tab (1), or the same tab (0). + autoraise: Whether to raise the window. + + Returns: + bool: True if the URL was opened successfully, False otherwise. + + """ + cmdline = [self.name] + [arg.replace("%s", url) for arg in self.args] + sys.audit("webbrowser.open", url) + try: + if sys.platform[:3] == "win": + p = subprocess.Popen( + cmdline, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL + ) + else: + p = subprocess.Popen( + cmdline, + close_fds=True, + start_new_session=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + return p.poll() is None + except OSError: + return False + + +webbrowser.BackgroundBrowser = SilentBackgroundBrowser + + +def get_existing_access_token() -> str: + """Fetch the access token from the existing config if applicable. + + Returns: + The access token. + If not found, return empty string for it instead. + + """ + import os + + console.debug("Fetching token from existing config...") + access_token = "" + try: + with constants.Hosting.HOSTING_JSON.open() as config_file: + hosting_config = json.load(config_file) + access_token = hosting_config.get("access_token", "") + except Exception as ex: + console.debug( + f"Unable to fetch token from {constants.Hosting.HOSTING_JSON} due to: {ex}" + ) + + if not access_token: + access_token = os.environ.get("REFLEX_ACCESS_TOKEN", "") + if access_token: + console.debug("Using REFLEX_ACCESS_TOKEN from environment") + + return access_token + + +def is_reflex_enterprise_installed() -> bool: + """Check if reflex-enterprise is installed. + + Returns: + True if reflex-enterprise is installed, False otherwise. + """ + import importlib.metadata + + try: + importlib.metadata.version("reflex-enterprise") + except importlib.metadata.PackageNotFoundError: + return False + except Exception: + return False + else: + return True + + +def validate_token(token: str) -> dict[str, Any]: + """Validate the token with the control plane. + + Args: + token: The access token to validate. + + Returns: + Information about the user associated with the token. + + Raises: + ValueError: if access denied. + Exception: if runs into timeout, failed requests, unexpected errors. These should be tried again. + + """ + import httpx + + try: + # Add reflex-enterprise detection flag as query parameter + params = { + "source": "reflex-enterprise" + if is_reflex_enterprise_installed() + else "reflex" + } + + response = httpx.post( + urljoin(constants.Hosting.HOSTING_SERVICE, "/api/v1/authenticate/me"), + headers=authorization_header(token), + params=params, + timeout=constants.Hosting.TIMEOUT, + ) + response.raise_for_status() + return response.json() + except httpx.RequestError as re: + console.debug(f"Request to auth server failed due to {re}") + raise Exception(str(re)) from re + except httpx.HTTPError as ex: + console.debug(f"Unable to validate the token due to: {ex}") + raise Exception("server error") from ex + except ValueError as ve: + console.debug(f"Access denied for {token}") + raise ValueError("access denied") from ve + except Exception as ex: + console.debug(f"Unexpected error: {ex}") + raise Exception("internal errors") from ex + + +def delete_token_from_config(): + """Delete the invalid token from the config file if applicable.""" + if constants.Hosting.HOSTING_JSON.exists(): + try: + with constants.Hosting.HOSTING_JSON.open("w") as config_file: + hosting_config = json.load(config_file) + del hosting_config["access_token"] + json.dump(hosting_config, config_file) + except Exception as ex: + # Best efforts removing invalid token is OK + console.debug( + f"Unable to delete the invalid token from config file, err: {ex}" + ) + # Delete the previous hosting service data if present. + if constants.Hosting.HOSTING_JSON_V0.exists(): + constants.Hosting.HOSTING_JSON_V0.unlink() + + +def save_token_to_config(token: str): + """Best efforts cache the token to the config file. + + Args: + token: The access token to save. + + """ + hosting_config: dict[str, str] = {"access_token": token} + + try: + if not Path(constants.Reflex.DIR).exists(): + Path(constants.Reflex.DIR).mkdir(parents=True, exist_ok=True) + with constants.Hosting.HOSTING_JSON.open("w") as config_file: + json.dump(hosting_config, config_file) + except Exception as ex: + console.warn( + f"Unable to save token to {constants.Hosting.HOSTING_JSON} due to: {ex}" + ) + + +def create_token( + name: str, + expiration: int, + client: AuthenticatedClient, +) -> str: + """Create a new access token. + + Args: + name: The name of the token. + expiration: The expiration time in seconds. If None, the token does not expire. + client: The authenticated client + + Returns: + The created access token. + + Raises: + NotAuthenticatedError: If the client is not authenticated. + Exception: If the token creation fails. + + """ + import httpx + + if not isinstance(client, AuthenticatedClient): + raise NotAuthenticatedError("not authenticated") + try: + response = httpx.post( + urljoin(constants.Hosting.HOSTING_SERVICE, "/api/v1/user/token"), + json={"name": name, "expiration": expiration}, + headers=authorization_header(client.token), + timeout=constants.Hosting.TIMEOUT, + ) + response.raise_for_status() + except httpx.HTTPStatusError as ex: + raise Exception(f"Failed to create token: {ex.response.text}") from ex + + return response.text + + +def requires_access_token() -> str: + """Fetch the access token from the existing config if applicable. + + Returns: + The access token. If not found, return empty string for it instead. + + """ + # Check if the user is authenticated + + access_token = get_existing_access_token() + if not access_token: + console.debug("No access token found from the existing config.") + + return access_token + + +def authenticated_token() -> tuple[str, dict[str, Any]]: + """Fetch the access token from the existing config if applicable and validate it. + + Returns: + The access token and validated user info. + If not found, return empty string and dict for it instead. + + """ + # Check if the user is authenticated + + validated_info = {} + access_token = get_existing_access_token() + if access_token and not ( + validated_info := validate_token_with_retries(access_token) + ): + access_token = "" + + return access_token, validated_info + + +def authorization_header(token: str) -> dict[str, str]: + """Construct an authorization header with the specified token. + + Args: + token: The access token to use. + + Returns: + The authorization header in dict format. + + """ + return {"X-API-TOKEN": token} + + +def requires_authenticated() -> str: + """Check if the user is authenticated. + + Returns: + The validated access token or empty string if not authenticated. + + """ + access_token, _ = authenticated_token() + if access_token: + return access_token + access_token, _ = authenticate_on_browser() + return access_token + + +def interactive_resolve_project_or_app_name_conflicts( + items: list[dict], + rows: list[list[str]], + headers: list[str], + conflict_warn_msg: str, + conflict_ask_msg: str, +) -> dict: + """Interactively resolve conflicts when multiple projects or apps are found. + + Args: + items: The list of items to choose from. + rows: The rows to display in the table. + headers: The headers of the table. + conflict_warn_msg: The warning message to display. + conflict_ask_msg: The question to ask the user. + + Returns: + The selected item as a dictionary + + """ + console.warn(conflict_warn_msg) + console.print_table(rows, headers=list(headers)) + option = console.ask( + conflict_ask_msg, + choices=[str(i) for i in range(len(rows))], + ) + return items[int(option)] + + +def search_app( + app_name: str, + client: AuthenticatedClient, + project_id: str | None, + interactive: bool = False, +) -> dict | None: + """Search for an application by name within a specific project. + + Args: + app_name: The name of the application to search for. + project_id: The ID of the project to search within. If None, searches across all projects. + client: The authenticated client + interactive: Whether to interactively resolve conflicts. + + Returns: + list[dict]: The search results as a list of dicts. + + Raises: + NotAuthenticatedError: If the token is not valid. + Exception: If the search request fails. + Exit: If multiple apps are found and interactive is False. + + """ + import httpx + + if not isinstance(client, AuthenticatedClient): + raise NotAuthenticatedError("not authenticated") + response = httpx.get( + urljoin( + constants.Hosting.HOSTING_SERVICE, + f"/api/v1/apps/search?app_name={app_name}" + + (f"&project_id={project_id}" if project_id else ""), + ), + headers=authorization_header(client.token), + timeout=constants.Hosting.TIMEOUT, + ) + try: + response.raise_for_status() + except httpx.HTTPStatusError as ex: + if response.status_code == HTTPStatus.NOT_FOUND: + return None + ex_details = ex.response.json().get("detail") + raise Exception(ex_details) from ex + + apps = response.json() + + if len(apps) > 1 and not interactive: + console.error( + f"Multiple apps with the name {app_name!r} found. Please provide a unique name." + ) + raise click.exceptions.Exit(1) + + elif len(apps) > 1 and interactive: + return interactive_resolve_project_or_app_name_conflicts( + apps, + rows=[ + [f"({i})", x["id"], x["name"], x["project"]["name"], x["project_id"]] + for i, x in enumerate(apps) + ], + headers=["", "App ID", "Name", "Project name", "Project ID"], + conflict_warn_msg="Found multiple apps with the same name. Select one to continue", + conflict_ask_msg="Which app would you like to use?", + ) + elif len(apps) == 1: + return apps[0] + else: + return None + + +def search_project( + project_name: str, client: AuthenticatedClient, interactive: bool = False +) -> dict | None: + """Search for a project by name. + + Args: + project_name: The name of the application to search for. + client: The authenticated client + interactive: Whether to interactively resolve conflicts. + + Returns: + list[dict]: The search results as a list of dict. + + Raises: + NotAuthenticatedError: If the token is not valid. + Exception: If the search request fails. + Exit: If multiple projects are found and interactive is False. + + """ + import httpx + + if not isinstance(client, AuthenticatedClient): + raise NotAuthenticatedError("not authenticated") + + response = httpx.get( + urljoin( + constants.Hosting.HOSTING_SERVICE, + f"/api/v1/project/search?project_name={project_name}", + ), + headers=authorization_header(client.token), + timeout=constants.Hosting.TIMEOUT, + ) + + try: + response.raise_for_status() + except httpx.HTTPStatusError as ex: + if response.status_code == HTTPStatus.NOT_FOUND: + return None + ex_details = ex.response.json().get("detail") + raise Exception(f"project search failed: {ex_details}") from ex + + projects = response.json() + + if len(projects) > 1 and not interactive: + console.error( + f"Multiple projects with the name {project_name!r} found. Please provide a unique name." + ) + raise click.exceptions.Exit(1) + + elif len(projects) > 1 and interactive: + return interactive_resolve_project_or_app_name_conflicts( + projects, + rows=[[f"({i})", x["id"], x["name"]] for i, x in enumerate(projects)], + headers=["", "Project ID", "Project name"], + conflict_warn_msg="Found multiple projects with the same name. Select one to continue", + conflict_ask_msg="Which project would you like to use?", + ) + elif len(projects) == 1: + return projects[0] + else: + return None + + +def get_app(app_id: str, client: AuthenticatedClient) -> dict: + """Retrieve details of a specific application by its ID. + + Args: + app_id: The ID of the application to retrieve. + client: The authenticated client + + Returns: + dict: The application details as a dictionary. + + Raises: + NotAuthenticatedError: If the token is not valid. + GetAppError: If the request to get the app fails. + ValueError: If the app_id is not valid. + + """ + import httpx + + if not isinstance(client, AuthenticatedClient): + raise NotAuthenticatedError("not authenticated") + if not isinstance(app_id, str) or not app_id: + raise ValueError("app_id should be a string") + response = httpx.get( + urljoin(constants.Hosting.HOSTING_SERVICE, f"/api/v1/apps/{app_id}"), + headers=authorization_header(client.token), + timeout=constants.Hosting.TIMEOUT, + ) + try: + response.raise_for_status() + except httpx.HTTPStatusError as ex: + try: + raise GetAppError(ex.response.json().get("detail")) from ex + except json.JSONDecodeError: + raise GetAppError(ex.response.text) from ex + return response.json() + + +def create_app( + app_name: str, + client: AuthenticatedClient, + description: str, + project_id: str | None, +): + """Create a new application. + + Args: + app_name: The name of the application. + description: The description of the application. + project_id: The ID of the project to associate the application with. + client: The authenticated client + + Returns: + dict: The created application details as a dictionary. + + Raises: + NotAuthenticatedError: If the token is not valid. + ValueError: If forbidden. + + """ + import httpx + + if not isinstance(app_name, str) or not app_name: + raise ValueError("app_name should be a string") + if not isinstance(client, AuthenticatedClient): + raise NotAuthenticatedError("not authenticated") + response = httpx.post( + urljoin(constants.Hosting.HOSTING_SERVICE, "/api/v1/apps/"), + json={"name": app_name, "description": description, "project": project_id}, + headers=authorization_header(client.token), + timeout=constants.Hosting.TIMEOUT, + ) + if response.status_code == HTTPStatus.FORBIDDEN: + console.debug(f"Server responded with 403: {response.text}") + raise ValueError(f"{response.text}") + response.raise_for_status() + response_json = response.json() + return response_json + + +def get_hostname( + app_id: str, app_name: str, client: AuthenticatedClient, hostname: str | None +) -> dict: + """Retrieve or reserve a hostname for a specific application. + + Args: + app_id: The ID of the application. + app_name: The name of the application. + hostname: The desired hostname. If None, a hostname will be generated. + client: The authenticated client + + Returns: + dict: The hostname details as a dictionary. + + Raises: + NotAuthenticatedError: If the token is not valid. + Exception: If deployment fails or the hostname is invalid. + + """ + import httpx + + if not isinstance(client, AuthenticatedClient): + raise NotAuthenticatedError("not authenticated") + + data = {"app_id": app_id, "app_name": app_name} + if hostname: + clean_hostname = extract_subdomain(hostname) + if clean_hostname is None: + raise Exception("bad hostname provided") + data["hostname"] = clean_hostname + response = httpx.post( + urljoin(constants.Hosting.HOSTING_SERVICE, "/api/v1/apps/reserve"), + headers=authorization_header(client.token), + json=data, + timeout=constants.Hosting.TIMEOUT, + ) + try: + response.raise_for_status() + except httpx.HTTPStatusError as ex: + if ex.response.status_code == 413: + raise Exception( + "deployment failed: the deployment payload is too large (over 100MB). " + "Please reduce the size of your project by removing large files or " + "adding them to your .gitignore file." + ) from ex + try: + ex_details = ex.response.json().get("detail") + if ex_details == "hostname taken": + return {"error": "hostname taken"} + raise Exception(f"deployment failed: {ex_details}") from ex + except (ValueError, AttributeError): + # Response is not valid JSON or missing detail field + raise Exception( + f"deployment failed: HTTP {ex.response.status_code} - {ex.response.text}" + ) from ex + response_json = response.json() + return response_json + + +def extract_subdomain(url: str): + """Extract the subdomain from a given URL. + + Args: + url: The URL to extract the subdomain from. + + Returns: + str | None: The extracted subdomain, or None if extraction fails. + + """ + from urllib.parse import urlparse + + if not url.startswith(("http://", "https://")): + url = "http://" + url + + parsed_url = urlparse(url) + netloc = parsed_url.netloc + + netloc = netloc.removeprefix("www.") + + parts = netloc.split(".") + + if len(parts) >= 2 or len(parts) == 1: + return parts[0] + + return None + + +def get_secrets(app_id: str, client: AuthenticatedClient) -> str: + """Retrieve secrets for a given application. + + Args: + app_id: The ID of the application. + client: The authenticated client + + Returns: + The secrets as a dictionary. + + Raises: + NotAuthenticatedError: If the token is not valid. + + """ + import httpx + + if not isinstance(client, AuthenticatedClient): + raise NotAuthenticatedError("not authenticated") + response = httpx.get( + urljoin(constants.Hosting.HOSTING_SERVICE, f"/api/v1/apps/{app_id}/secrets"), + headers=authorization_header(client.token), + timeout=constants.Hosting.TIMEOUT, + ) + try: + response.raise_for_status() + except httpx.HTTPStatusError as ex: + try: + return ex.response.json().get("detail") + except json.JSONDecodeError: + return ex.response.text + return response.json() + + +def update_secrets( + app_id: str, + secrets: dict, + client: AuthenticatedClient, + reboot: bool = False, +): + """Update secrets for a given application. + + Args: + app_id: The ID of the application. + secrets: The secrets to update. + reboot: Whether to reboot the application with the new secrets. + client: The authenticated client + + Returns: + The updated secrets as a dictionary. + + Raises: + NotAuthenticatedError: If the token is not valid. + + """ + import httpx + + if not isinstance(client, AuthenticatedClient): + raise NotAuthenticatedError("not authenticated") + response = httpx.post( + urljoin( + constants.Hosting.HOSTING_SERVICE, + f"/api/v1/apps/{app_id}/secrets?reboot={reboot}", + ), + headers=authorization_header(client.token), + json={"secrets": secrets}, + timeout=constants.Hosting.TIMEOUT, + ) + response.raise_for_status() + response_json = response.json() + return response_json + + +def delete_secret( + app_id: str, key: str, client: AuthenticatedClient, reboot: bool = False +) -> str: + """Delete a secret for a given application. + + Args: + app_id: The ID of the application. + key: The key of the secret to delete. + reboot: Whether to reboot the application with the updated secrets. + client: The authenticated client + + Returns: + The response from the delete operation as a dictionary. + + Raises: + NotAuthenticatedError: If the token is not valid. + + """ + import httpx + + if not isinstance(client, AuthenticatedClient): + raise NotAuthenticatedError("not authenticated") + response = httpx.delete( + urljoin( + constants.Hosting.HOSTING_SERVICE, + f"/api/v1/apps/{app_id}/secrets/{key}?reboot={reboot}", + ), + headers=authorization_header(client.token), + timeout=constants.Hosting.TIMEOUT, + ) + try: + response.raise_for_status() + except httpx.HTTPStatusError as ex: + try: + return ex.response.json().get("detail") + except json.JSONDecodeError: + return ex.response.text + return response.json() + + +def create_project(name: str, client: AuthenticatedClient) -> dict: + """Create a new project. + + Args: + name: The name of the project. + client: The authenticated client + + Returns: + dict: The created project details as a dictionary. + + Raises: + NotAuthenticatedError: If the token is not valid. + ValueError: If the request to create the project fails. + + """ + import httpx + + if not isinstance(client, AuthenticatedClient): + raise NotAuthenticatedError("not authenticated") + response = httpx.post( + urljoin(constants.Hosting.HOSTING_SERVICE, "/api/v1/project/create"), + json={"name": name}, + headers=authorization_header(client.token), + timeout=constants.Hosting.TIMEOUT, + ) + response_json = response.json() + if response.status_code == HTTPStatus.BAD_REQUEST: + console.debug(f"Server responded with 400: {response_json.get('detail')}") + raise ValueError(f"{response_json.get('detail', 'bad request')}") + if response.status_code == HTTPStatus.CONFLICT: + console.debug(f"Duplicate project name: {response_json.get('detail')}") + raise ValueError( + f"A project named '{name}' already exists. Please use a different name." + ) + response.raise_for_status() + return response_json + + +def select_project(project: str, token: str | None = None) -> str: + """Select a project by its ID. + + Args: + project: The ID of the project to select. + token: The authentication token. If None, attempts to authenticate. + + Returns: + None + + """ + try: + with constants.Hosting.HOSTING_JSON.open() as config_file: + hosting_config = json.load(config_file) + with constants.Hosting.HOSTING_JSON.open("w") as config_file: + hosting_config["project"] = project + json.dump(hosting_config, config_file) + except Exception as ex: + return ( + f"failed to fetch token from {constants.Hosting.HOSTING_JSON} due to: {ex}" + ) + return f"{project} is now selected." + + +def get_selected_project() -> str | None: + """Retrieve the currently selected project ID. + + Returns: + str | None: The ID of the selected project, or None if no project is selected. + + """ + try: + with constants.Hosting.HOSTING_JSON.open() as config_file: + hosting_config = json.load(config_file) + return hosting_config.get("project") + except Exception as ex: + console.debug( + f"Unable to fetch token from {constants.Hosting.HOSTING_JSON} due to: {ex}" + ) + return None + + +def get_projects(client: AuthenticatedClient) -> list[dict]: + """Retrieve a list of projects. + + Args: + client: The authenticated client. + + Returns: + The list of projects as a dictionary. + + Raises: + NotAuthenticatedError: If the token is not valid. + + """ + import httpx + + if not isinstance(client, AuthenticatedClient): + raise NotAuthenticatedError("not authenticated") + response = httpx.get( + urljoin(constants.Hosting.HOSTING_SERVICE, "/api/v1/project/"), + headers=authorization_header(client.token), + timeout=constants.Hosting.TIMEOUT, + ) + response.raise_for_status() + response_json = response.json() + return response_json + + +def get_project(project_id: str, client: AuthenticatedClient): + """Retrieve a single project given the project ID. + + Args: + project_id: The ID of the project. + client: The authenticated client + + Returns: + The project details as a dictionary. + + Raises: + NotAuthenticatedError: If the token is not valid. + + """ + import httpx + + if not isinstance(client, AuthenticatedClient): + raise NotAuthenticatedError("not authenticated") + response = httpx.get( + urljoin(constants.Hosting.HOSTING_SERVICE, f"/api/v1/project/{project_id}"), + headers=authorization_header(client.token), + timeout=constants.Hosting.TIMEOUT, + ) + response.raise_for_status() + response_json = response.json() + return response_json + + +def get_project_roles(project_id: str, client: AuthenticatedClient): + """Retrieve the roles for a project. + + Args: + project_id: The ID of the project. + client: The authenticated client + + Returns: + The roles as a dictionary. + + Raises: + NotAuthenticatedError: If the token is not valid. + + """ + import httpx + + if not isinstance(client, AuthenticatedClient): + raise NotAuthenticatedError("not authenticated") + response = httpx.get( + urljoin( + constants.Hosting.HOSTING_SERVICE, f"/api/v1/project/{project_id}/roles" + ), + headers=authorization_header(client.token), + timeout=constants.Hosting.TIMEOUT, + ) + response.raise_for_status() + response_json = response.json() + return response_json + + +def get_project_role_permissions( + project_id: str, role_id: str, client: AuthenticatedClient +): + """Retrieve the permissions for a specific role in a project. + + Args: + project_id: The ID of the project. + role_id: The ID of the role. + client: The authenticated client + + Returns: + The role permissions as a dictionary. + + Raises: + NotAuthenticatedError: If the token is not valid. + + """ + import httpx + + if not isinstance(client, AuthenticatedClient): + raise NotAuthenticatedError("not authenticated") + response = httpx.get( + urljoin( + constants.Hosting.HOSTING_SERVICE, + f"/api/v1/project/{project_id}/role/{role_id}", + ), + headers=authorization_header(client.token), + timeout=constants.Hosting.TIMEOUT, + ) + response.raise_for_status() + response_json = response.json() + return response_json + + +def get_project_role_users(project_id: str, client: AuthenticatedClient): + """Retrieve the users for a project. + + Args: + project_id: The ID of the project. + client: The authenticated client + + Returns: + The users as a dictionary. + + Raises: + NotAuthenticatedError: If the token is not valid. + + """ + import httpx + + if not isinstance(client, AuthenticatedClient): + raise NotAuthenticatedError("not authenticated") + response = httpx.get( + urljoin( + constants.Hosting.HOSTING_SERVICE, f"/api/v1/project/{project_id}/users" + ), + headers=authorization_header(client.token), + timeout=constants.Hosting.TIMEOUT, + ) + response.raise_for_status() + response_json = response.json() + return response_json + + +def invite_user_to_project( + role_id: str, user_id: str, client: AuthenticatedClient +) -> str: + """Invite a user to a project with a specific role. + + Args: + role_id: The ID of the role to assign to the user. + user_id: The ID of the user to invite. + client: The authenticated client + + Returns: + The response from the invite operation as a dictionary. + + Raises: + NotAuthenticatedError: If the token is not valid. + + """ + import httpx + + if not isinstance(client, AuthenticatedClient): + raise NotAuthenticatedError("not authenticated") + response = httpx.post( + urljoin(constants.Hosting.HOSTING_SERVICE, "/api/v1/project/users/invite"), + headers=authorization_header(client.token), + json={"user_id": user_id, "role_id": role_id}, + timeout=constants.Hosting.TIMEOUT, + ) + try: + response.raise_for_status() + except httpx.HTTPStatusError as ex: + try: + return ex.response.json().get("detail") + except json.JSONDecodeError: + return ex.response.text + return response.json() + + +def validate_deployment_args( + app_name: str, + app_id: str | None, + project_id: str | None, + regions: list[str] | None, + vmtype: str | None, + hostname: str | None, + client: AuthenticatedClient, +) -> str: + """Validate the deployment arguments. + + Args: + app_name: The name of the application. + app_id: The ID of the application. + project_id: The ID of the project to associate the deployment with. + regions: The list of regions for the deployment. + vmtype: The VM type for the deployment. + hostname: The hostname for the deployment. + client: The authenticated client. + + Returns: + The validation result as a string -- "success" if all checks pass. + + """ + import httpx + + if not isinstance(client, AuthenticatedClient): + return "not authenticated" + + param_data = { + "app_name": app_name or "", + "app_id": app_id or "", + "project_id": project_id or "", + "regions": json.dumps(regions or []), + "vmtype": vmtype or "", + "hostname": hostname or "", + } + response = httpx.get( + urljoin(constants.Hosting.HOSTING_SERVICE, "/api/v1/deployments/validate_cli"), + headers=authorization_header(client.token), + params=param_data, + timeout=15, + ) + try: + response.raise_for_status() + except httpx.HTTPStatusError as ex: + try: + ex_details = ex.response.json().get("detail") + except (httpx.RequestError, ValueError, KeyError): + return "deployment failed: internal server error" + else: + return f"deployment failed: {ex_details}" + + return "success" + + +def create_deployment( + zip_dir: Path, + client: AuthenticatedClient, + app_name: str | None, + project_id: str | None, + regions: list | None, + hostname: str | None, + vmtype: str | None, + secrets: dict | None, + packages: list | None, + strategy: str | None, + app_id: str | None, +) -> str: + """Create a new deployment for an application. + + Args: + app_name: The name of the application. + project_id: The ID of the project to associate the deployment with. + regions: The list of regions for the deployment. + zip_dir: The directory containing the zip files for the deployment. + hostname: The hostname for the deployment. + vmtype: The VM type for the deployment. + secrets: The secrets to use for the deployment. + client: The authenticated client + packages: The list of packages to install on the VM. + strategy: The deployment strategy to use. + app_id: The ID of the application. + + Returns: + The deployment id.git c + + Raises: + NotAuthenticatedError: If the token is not valid. + + """ + import httpx + + if not isinstance(client, AuthenticatedClient): + raise NotAuthenticatedError("not authenticated") + cli_version = importlib.metadata.version("reflex-hosting-cli") + zips = [ + ( + "files", + ( + "backend.zip", + (zip_dir / "backend.zip").open("rb"), + ), + ), + ( + "files", + ( + "frontend.zip", + (zip_dir / "frontend.zip").open("rb"), + ), + ), + ] + payload: dict[str, Any] = { + "app_id": app_id, + "app_name": app_name, + "reflex_hosting_cli_version": cli_version, + "reflex_version": dependency.get_reflex_version(), + "python_version": platform.python_version(), + } + if project_id: + payload["project_id"] = project_id + if regions: + regions = regions or [] + payload["regions"] = json.dumps(regions) + if hostname: + payload["hostname"] = hostname + if vmtype: + payload["vm_type"] = vmtype + if secrets: + payload["secrets"] = json.dumps(secrets) + if packages: + payload["packages"] = json.dumps(packages) + if strategy: + payload["deployment_strategy"] = strategy + + response = httpx.post( + urljoin(constants.Hosting.HOSTING_SERVICE, "/api/v1/deployments"), + data=payload, + files=zips, + headers=authorization_header(client.token), + timeout=55, + ) + try: + response.raise_for_status() + except httpx.HTTPStatusError as ex: + if ex.response.status_code == 413: + return ( + "deployment failed: the deployment payload is too large (over 100MB). " + "Please reduce the size of your project by removing large files or " + "adding them to your .gitignore file." + ) + try: + ex_details = ex.response.json().get("detail") + except (httpx.RequestError, ValueError, KeyError): + return "deployment failed: internal server error" + else: + return f"deployment failed: {ex_details}" + return response.json() + + +def stop_app(app_id: str, client: AuthenticatedClient): + """Stop a running application. + + Args: + app_id: The ID of the application. + client: The authenticated client + + Returns: + The response from the stop operation as a dictionary. + + Raises: + NotAuthenticatedError: If the token is not valid. + + """ + import httpx + + if not isinstance(client, AuthenticatedClient): + raise NotAuthenticatedError("not authenticated") + response = httpx.post( + urljoin(constants.Hosting.HOSTING_SERVICE, f"/api/v1/apps/{app_id}/stop"), + headers=authorization_header(client.token), + ) + try: + response.raise_for_status() + except httpx.HTTPStatusError as ex: + ex_details = ex.response.json().get("detail") + return f"stop app failed: {ex_details}" + return response.json() + + +def start_app(app_id: str, client: AuthenticatedClient): + """Start a stopped application. + + Args: + app_id: The ID of the application. + client: The authenticated client + + Returns: + The response from the start operation as a dictionary. + + Raises: + NotAuthenticatedError: If the token is not valid. + + """ + import httpx + + if not isinstance(client, AuthenticatedClient): + raise NotAuthenticatedError("not authenticated") + response = httpx.post( + urljoin(constants.Hosting.HOSTING_SERVICE, f"/api/v1/apps/{app_id}/start"), + headers=authorization_header(client.token), + ) + try: + response.raise_for_status() + except httpx.HTTPStatusError as ex: + ex_details = ex.response.json().get("detail") + return f"start app failed: {ex_details}" + return response.json() + + +def delete_app(app_id: str, client: AuthenticatedClient): + """Delete an application. + + Args: + app_id: The ID of the application. + client: The authenticated client + + Returns: + The response from the delete operation as a dictionary. + + Raises: + NotAuthenticatedError: If the token is not valid. + + """ + import httpx + + if not isinstance(client, AuthenticatedClient): + raise NotAuthenticatedError("not authenticated") + app = get_app(app_id=app_id, client=client) + if not app: + console.warn("no app with given id found") + return + response = httpx.delete( + urljoin(constants.Hosting.HOSTING_SERVICE, f"/api/v1/apps/{app['id']}/delete"), + headers=authorization_header(client.token), + ) + try: + response.raise_for_status() + except httpx.HTTPStatusError as ex: + ex_details = ex.response.json().get("detail") + return f"delete app failed: {ex_details}" + return response.json() + + +def get_app_logs( + app_id: str, + offset: int | None, + start: int | None, + end: int | None, + client: AuthenticatedClient, + cursor: str | None = None, +): + """Retrieve logs for a given application. + + Args: + app_id: The ID of the application. + offset: The offset in seconds from the current time. + start: The start time in Unix epoch format. + end: The end time in Unix epoch format. + client: The authenticated client + cursor: The cursor for pagination. + + Returns: + The logs as a dictionary. + + Raises: + NotAuthenticatedError: If the token is not valid. + + """ + import httpx + + if not isinstance(client, AuthenticatedClient): + raise NotAuthenticatedError("not authenticated") + try: + app = get_app(app_id=app_id, client=client) + except GetAppError: + console.warn(f"No application found with ID '{app_id}'") + return + if not app: + console.warn("no app with given id found") + return + params = f"?offset={offset}" if offset else f"?start={start}&end={end}" + if cursor: + params += f"&cursor={cursor}" + try: + with console.status("Fetching application logs..."): + response = httpx.get( + urljoin( + constants.Hosting.HOSTING_SERVICE, + f"/api/v1/apps/{app['id']}/logsv2{params}", + ), + headers=authorization_header(client.token), + ) + response.raise_for_status() + except httpx.RequestError: + return [] + except httpx.HTTPStatusError as ex: + try: + ex_details = ex.response.json().get("detail") + except json.JSONDecodeError: + return [] + else: + return f"get app logs failed: {ex_details}" + else: + try: + return response.json() + except json.JSONDecodeError: + return [] + + +def list_apps(client: AuthenticatedClient, project: str | None = None) -> list[dict]: + """List all the hosted deployments of the authenticated user. + + Args: + project: The project ID to filter deployments. + client: The authenticated client + + Returns: + List[dict]: A list of deployments as dictionaries. + + Raises: + NotAuthenticatedError: If the token is not valid. + Exception: when listing apps fails. + + """ + import httpx + + if not isinstance(client, AuthenticatedClient): + raise NotAuthenticatedError("not authenticated") + + url = urljoin( + constants.Hosting.HOSTING_SERVICE, + f"/api/v1/apps?project={project}" if project else "/api/v1/apps", + ) + + response = httpx.get(url, headers=authorization_header(client.token), timeout=5) + try: + response.raise_for_status() + except httpx.HTTPStatusError as ex: + ex_details = ex.response.json().get("detail") + raise Exception(f"list app failed: {ex_details}") from ex + return response.json() + + +def get_app_history(app_id: str, client: AuthenticatedClient) -> list: + """Retrieve the deployment history for a given application. + + Args: + app_id: The ID of the application. + client: The authenticated client + + Returns: + list: A list of deployment history entries as dictionaries. + + Raises: + NotAuthenticatedError: If the token is not valid. + + """ + import httpx + + if not isinstance(client, AuthenticatedClient): + raise NotAuthenticatedError("not authenticated") + response = httpx.get( + urljoin(constants.Hosting.HOSTING_SERVICE, f"/api/v1/apps/{app_id}/history"), + headers=authorization_header(client.token), + ) + + response.raise_for_status() + response_json = response.json() + result = [ + { + "id": deployment["id"], + "status": deployment["status"], + "hostname": deployment["hostname"], + "python version": deployment["python_version"], + "reflex version": deployment["reflex_version"], + "vm type": deployment["vm_type"], + "timestamp": deployment["timestamp"], + } + for deployment in response_json + ] + return result + + +def get_app_status(app_id: str, client: AuthenticatedClient) -> str: + """Retrieve the status of a specific app. + + Args: + app_id: The ID of the app. + client: The authenticated client + + Returns: + str: The status of the app. + + Raises: + NotAuthenticatedError: If the token is not valid. + + """ + import httpx + + if not isinstance(client, AuthenticatedClient): + raise NotAuthenticatedError("not authenticated") + try: + response = httpx.get( + urljoin( + constants.Hosting.HOSTING_SERVICE, + f"/api/v1/deployments/{app_id}/status", + ), + headers=authorization_header(client.token), + ) + except httpx.RequestError as e: + return "lost connection: trying again" + f"({e.__class__.__name__}: {e})" + + try: + response.raise_for_status() + except httpx.HTTPStatusError: + return f"error: bad response: {response.status_code}. received a bad response from cloud service." + return response.json() + + +def scale_app(app_id: str, scale_params: ScaleParams, client: AuthenticatedClient): + """Scale an application. + + Args: + app_id: The ID of the application. + scale_params: The scaling parameters. + client: The authenticated client + + Returns: + The response from the scale operation as a dictionary. + + Raises: + NotAuthenticatedError: If the token is not valid. + ResponseError: If the request to scale the app fails. + + """ + import httpx + + if not isinstance(client, AuthenticatedClient): + raise NotAuthenticatedError("not authenticated") + response = httpx.post( + urljoin(constants.Hosting.HOSTING_SERVICE, f"/api/v1/apps/{app_id}/scale"), + headers=authorization_header(client.token), + json=scale_params.as_json(), + timeout=constants.Hosting.TIMEOUT, + ) + try: + response.raise_for_status() + except httpx.HTTPStatusError as ex: + ex_details = ex.response.json().get("detail") + raise ResponseError(f"scale app failed: {ex_details}") from ex + return response.json() + + +def get_deployment_status(deployment_id: str, client: AuthenticatedClient) -> str: + """Retrieve the status of a specific deployment. + + Args: + deployment_id: The ID of the deployment. + client: The authenticated client + + Returns: + str: The status of the deployment. + + Raises: + NotAuthenticatedError: If the token is not valid. + + """ + import httpx + + if not isinstance(client, AuthenticatedClient): + raise NotAuthenticatedError("not authenticated") + response = httpx.get( + urljoin( + constants.Hosting.HOSTING_SERVICE, + f"/api/v1/deployments/{deployment_id}/status", + ), + headers=authorization_header(client.token), + ) + try: + response.raise_for_status() + except httpx.HTTPStatusError as ex: + ex_details = ex.response.json().get("detail") + return f"get status failed: {ex_details}" + return response.json() + + +def _get_deployment_status(deployment_id: str, token: str) -> str: + """Retrieve the status of a specific deployment with error handling. + + Args: + deployment_id: The ID of the deployment. + token: The authentication token. + + Returns: + str: The status of the deployment, or an error message if the request fails. + + """ + import httpx + + try: + response = httpx.get( + urljoin( + constants.Hosting.HOSTING_SERVICE, + f"/api/v1/deployments/{deployment_id}/status", + ), + headers=authorization_header(token), + ) + except httpx.RequestError as e: + return "lost connection: trying again" + f"({e.__class__.__name__}: {e})" + + try: + response.raise_for_status() + except httpx.HTTPStatusError: + return "bad response. received a bad response from cloud service." + return response.json() + + +def watch_deployment_status(deployment_id: str, client: AuthenticatedClient) -> bool: + """Continuously watch the status of a specific deployment. + + Args: + deployment_id: The ID of the deployment. + client: The authenticated client + + Returns: + True when the watching ends. + False when watching ends in fail. + + Raises: + NotAuthenticatedError: If the token is not valid. + + """ + if not isinstance(client, AuthenticatedClient): + raise NotAuthenticatedError("not authenticated") + with console.status("listening to status updates!"): + current_status = "" + while True: + status = _get_deployment_status( + deployment_id=deployment_id, token=client.token + ) + if "completed successfully" in status: + console.success(status) + break + if "build error" in status: + console.warn(status) + console.warn( + f"to see the build logs:\n reflex cloud apps build-logs {deployment_id}" + ) + return False + if "unable to find status for given id" in status: + console.error(status) + return False + if "error" in status: + console.warn(status) + return False + if "bad response" in status: + console.warn(status) + return True + if status == current_status: + continue + current_status = status + console.info(status) + time.sleep(0.5) + return True + + +def get_deployment_build_logs(deployment_id: str, client: AuthenticatedClient): + """Retrieve the build logs for a specific deployment. + + Args: + deployment_id: The ID of the deployment. + client: The authenticated client + + Returns: + dict: The build logs as a dictionary. + + Raises: + NotAuthenticatedError: If the token is not valid. + + """ + import httpx + + if not isinstance(client, AuthenticatedClient): + raise NotAuthenticatedError("not authenticated") + response = httpx.get( + urljoin( + constants.Hosting.HOSTING_SERVICE, + f"/api/v1/deployments/{deployment_id}/build/logs", + ), + headers=authorization_header(client.token), + ) + + response.raise_for_status() + return response.json() + + +def list_projects(): + """List all projects. + + This function is currently a placeholder and does not perform any operations. + + Returns: + None + + """ + return None + + +def fetch_token(request_id: str) -> str: + """Fetch the access token for the request_id from Control Plane. + + Args: + request_id: The request ID used when the user opens the browser for authentication. + + Returns: + The access token if it exists, empty strings otherwise. + + """ + import httpx + + token = "" + try: + resp = httpx.get( + urljoin( + constants.Hosting.HOSTING_SERVICE, + f"/api/v1/cli/token?request_id={request_id}", + ), + timeout=constants.Hosting.TIMEOUT, + ) + resp.raise_for_status() + token = (resp_json := resp.json()).get("token_id", "") + project_id = resp_json.get("user_id", "") + select_project(project=project_id) + except httpx.RequestError as re: + console.debug(f"Unable to fetch token due to request error: {re}") + except httpx.HTTPError as he: + console.debug(f"Unable to fetch token due to {he}") + except json.JSONDecodeError as jde: + console.debug(f"Server did not respond with valid json: {jde}") + except KeyError as ke: + console.debug(f"Server response format unexpected: {ke}") + except Exception as ex: + console.debug(f"Unexpected errors: {ex}") + + return token + + +def authenticate_on_browser() -> tuple[str, dict[str, Any]]: + """Open the browser to authenticate the user. + + Returns: + The access token if valid and user information dict otherwise ("", {}). + + Raises: + Exit: when the hosting service URL is invalid. + + """ + request_id = uuid.uuid4().hex + auth_url = urljoin( + constants.Hosting.HOSTING_SERVICE_UI, f"/cli/login?request_id={request_id}" + ) + + console.print(f"Opening {auth_url} ...") + + if not is_valid_url(constants.Hosting.HOSTING_SERVICE_UI): + console.error( + f"Invalid hosting URL: {constants.Hosting.HOSTING_SERVICE_UI}. Ensure the URL is in the correct format and includes a valid scheme" + ) + raise click.exceptions.Exit(1) + + if not webbrowser.open(auth_url): + console.warn( + f"Unable to automatically open the browser. Please go to {auth_url} to authenticate." + ) + validated_info = {} + access_token = "" + console.ask("please hit 'Enter' or 'Return' after login on website complete") + with console.status("Waiting for access token ..."): + for _ in range(constants.Hosting.AUTH_RETRY_LIMIT): + access_token = fetch_token(request_id) + if access_token: + break + else: + time.sleep(1) + + if access_token and (validated_info := validate_token_with_retries(access_token)): + save_token_to_config(access_token) + else: + access_token = "" + return access_token, validated_info + + +def get_default_project(authenticated_client: AuthenticatedClient) -> str | None: + """Get the default project ID for the authenticated user. + + Args: + authenticated_client: The authenticated client. + + Returns: + The default project ID if available, None otherwise. + """ + return authenticated_client.validated_data.get("user_id") + + +def validate_token_with_retries(access_token: str) -> dict[str, Any]: + """Validate the access token without retries. + + Args: + access_token: The access token to validate. + + Returns: + validated user info dict. + + """ + with console.status("Validating access token ..."): + try: + return validate_token(access_token) + except ValueError: + console.error("Access denied") + delete_token_from_config() + except Exception as ex: + console.debug(f"Unable to validate token due to: {ex}") + return {} + + +def process_envs(envs: list[str]) -> dict[str, str]: + """Process the environment variables. + + Args: + envs: The environment variables expected in key=value format. + + Raises: + SystemExit: If the envs are not in valid format. + + Returns: + dict[str, str]: The processed environment variables in a dictionary. + + Raises: + SystemExit: If invalid format. + + """ + processed_envs = {} + for env in envs: + kv = env.split("=", maxsplit=1) + if len(kv) != 2: + raise SystemExit("Invalid env format: should be =.") + + if not re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", kv[0]): + raise SystemExit( + "Invalid env name: should start with a letter or underscore, followed by letters, digits, or underscores." + ) + processed_envs[kv[0]] = kv[1] + return processed_envs + + +def read_config( + config_path: str | None = None, env: str | None = None +) -> Config | None: + """Read the config file. + + Args: + config_path: The path to the config file. If None, defaults to 'cloud.yml'. + env: The environment to read the config for. If None, reads the default config. + + Returns: + Config | None: The config file as a Config instance, or None if not found or invalid. + + """ + if config_path: + return Config.from_yaml(Path(config_path)) + return Config.from_yaml_or_toml_or_none() + + +def generate_config(interactive: bool = True, token: str | None = None): + """Generate the config file with app-based prefilling. + + Args: + interactive: Whether to use interactive mode for authentication and app selection. + token: An existing authentication token to use instead of interactive auth. + + Raises: + click.exceptions.Exit: If authentication fails or user cancels operation. + """ + try: + import yaml + except ImportError: + console.error("Please install PyYAML to use this command: pip install pyyaml") + return + + if Path("cloud.yml").exists(): + console.error("cloud.yml already exists.") + return + + try: + authenticated_client = get_authenticated_client( + token=token, interactive=interactive + ) + except click.exceptions.Exit: + console.error("Authentication required to generate prefilled config.") + raise + + current_dir_name = Path.cwd().name + + try: + app = search_app( + app_name=current_dir_name, + project_id=None, + client=authenticated_client, + interactive=interactive, + ) + except click.exceptions.Exit: + raise + except Exception as ex: + console.warn(f"Could not search for apps: {ex}") + app = None + + if app: + console.info(f"Found app '{app['name']}' - prefilling config with app data.") + default = {"name": app["name"]} + + if app.get("id"): + default["appid"] = app["id"] + if app.get("description"): + default["description"] = app["description"] + if app.get("project_id"): + default["project"] = app["project_id"] + else: + console.info( + f"No app found with name '{current_dir_name}' - creating config with minimal defaults." + ) + default = {"name": current_dir_name} + + with Path("cloud.yml").open("w") as config_file: + yaml.dump(default, config_file, default_flow_style=False, sort_keys=False) + console.success("cloud.yml created successfully.") + console.info( + "For more configuration options, see: https://reflex.dev/docs/hosting/config-file/" + ) + return + + +def log_out_on_browser(): + """Open the browser to log out the user.""" + with contextlib.suppress(Exception): + delete_token_from_config() + console.print(f"Opening {constants.Hosting.HOSTING_SERVICE_UI} ...") + if not webbrowser.open(constants.Hosting.HOSTING_SERVICE_UI): + console.warn( + f"Unable to open the browser automatically. Please go to {constants.Hosting.HOSTING_SERVICE_UI} to log out." + ) + + +def get_vm_types() -> list[dict]: + """Retrieve the available VM types. + + Returns: + list[dict]: A list of VM types as dictionaries. + + """ + import httpx + + try: + response = httpx.get( + urljoin(constants.Hosting.HOSTING_SERVICE, "/api/v1/deployments/vm_types"), + timeout=10, + ) + response.raise_for_status() + response_json = response.json() + if response_json is None or not isinstance(response_json, list): + console.error("Expect server to return a list ") + return [] + if ( + response_json + and response_json[0] is not None + and not isinstance(response_json[0], dict) + ): + console.error("Expect return values are dict's") + return [] + except Exception as ex: + console.error(f"Unable to get vmtypes due to {ex}.") + return [] + else: + return response_json + + +def get_regions() -> list[dict]: + """Get the supported regions from the hosting server. + + Returns: + list[dict]: A list of dict representation of the region information. + + """ + import httpx + + try: + response = httpx.get( + urljoin(constants.Hosting.HOSTING_SERVICE, "/api/v1/deployments/regions"), + timeout=10, + ) + response.raise_for_status() + response_json = response.json() + if response_json is None or not isinstance(response_json, list): + console.error("Expect server to return a list ") + return [] + if ( + response_json + and response_json[0] is not None + and not isinstance(response_json[0], dict) + ): + console.error("Expect return values are dict's") + return [] + return [ + {"name": region["name"], "code": region["code"]} for region in response_json + ] + except Exception as ex: + console.error(f"Unable to get regions due to {ex}.") + return [] diff --git a/packages/reflex-hosting-cli/reflex_cli/v2/__init__.py b/packages/reflex-hosting-cli/reflex_cli/v2/__init__.py new file mode 100644 index 00000000000..6c091d1f261 --- /dev/null +++ b/packages/reflex-hosting-cli/reflex_cli/v2/__init__.py @@ -0,0 +1 @@ +"""CLI library for the hosting service.""" diff --git a/packages/reflex-hosting-cli/reflex_cli/v2/apps.py b/packages/reflex-hosting-cli/reflex_cli/v2/apps.py new file mode 100644 index 00000000000..02f30c07091 --- /dev/null +++ b/packages/reflex-hosting-cli/reflex_cli/v2/apps.py @@ -0,0 +1,828 @@ +"""App commands for the Reflex Cloud CLI.""" + +from __future__ import annotations + +import json + +import click + +from reflex_cli import constants +from reflex_cli.core.config import Config +from reflex_cli.utils import console +from reflex_cli.utils.exceptions import ( + ConfigInvalidFieldValueError, + GetAppError, + NotAuthenticatedError, + ResponseError, + ScaleAppError, + ScaleParamError, + ScaleTypeError, +) + + +@click.group() +def apps_cli(): + """Commands for managing apps.""" + pass + + +@apps_cli.command(name="history") +@click.argument("app_id", required=False) +@click.option("--app-name", help="The name of the application.") +@click.option("--token", help="The authentication token.") +@click.option( + "--loglevel", + type=click.Choice([level.value for level in constants.LogLevel]), + default=constants.LogLevel.INFO.value, + help="The log level to use.", +) +@click.option( + "--json/--no-json", + "-j", + "as_json", + is_flag=True, + help="Whether to output the result in json format.", +) +@click.option( + "--interactive/--no-interactive", + "-i", + is_flag=True, + default=True, + help="Whether to use interactive mode.", +) +def app_history( + app_id: str | None, + app_name: str | None, + token: str | None, + loglevel: str, + as_json: bool, + interactive: bool, +): + """Retrieve the deployment history for a given application.""" + from reflex_cli.utils import hosting + + console.set_log_level(loglevel) + try: + authenticated_client = hosting.get_authenticated_client( + token=token, interactive=interactive + ) + + if not app_id: + config = hosting.read_config() + if config: + app_id = config.appid + if not isinstance(app_id, (str, type(None))): + console.error( + "app_id must be a string or None. Please check your config file." + ) + raise click.exceptions.Exit(1) + + if app_name is not None and app_id is None: + result = hosting.search_app( + app_name=app_name, + project_id=None, + client=authenticated_client, + interactive=interactive, + ) + app_id = result.get("id") if result else None + + if not app_id: + console.error("No valid app_id or app_name provided.") + raise click.exceptions.Exit(1) + + history = hosting.get_app_history(app_id=app_id, client=authenticated_client) + + if as_json: + console.print(json.dumps(history)) + return + if history: + headers = list(history[0].keys()) + table = [ + [str(value) for value in deployment.values()] for deployment in history + ] + console.print_table(table, headers=headers) + else: + console.print(str(history)) + except NotAuthenticatedError as err: + console.error("You are not authenticated. Run `reflex login` to authenticate.") + raise click.exceptions.Exit(1) from err + + +@apps_cli.command("build-logs") +@click.argument("deployment_id", required=True) +@click.option("--token", help="The authentication token.") +@click.option( + "--interactive/--no-interactive", + "-i", + is_flag=True, + default=True, + help="Whether to use interactive mode.", +) +def deployment_build_logs( + deployment_id: str, + token: str | None, + interactive: bool, +): + """Retrieve the build logs for a specific deployment.""" + from reflex_cli.utils import hosting + + try: + authenticated_client = hosting.get_authenticated_client( + token=token, interactive=interactive + ) + logs = hosting.get_deployment_build_logs( + deployment_id=deployment_id, client=authenticated_client + ) + console.print(logs) + except NotAuthenticatedError as err: + console.error("You are not authenticated. Run `reflex login` to authenticate.") + raise click.exceptions.Exit(1) from err + + +@apps_cli.command(name="status") +@click.argument("deployment_id", required=True) +@click.option( + "--watch/--no-watch", is_flag=True, help="Whether to continuously watch the status." +) +@click.option("--token", help="The authentication token.") +@click.option( + "--loglevel", + type=click.Choice([level.value for level in constants.LogLevel]), + default=constants.LogLevel.INFO.value, + help="The log level to use.", +) +@click.option( + "--interactive/--no-interactive", + "-i", + is_flag=True, + default=True, + help="Whether to use interactive mode.", +) +def deployment_status( + deployment_id: str, + watch: bool, + token: str | None, + loglevel: str, + interactive: bool, +): + """Retrieve the status of a specific deployment.""" + from reflex_cli.utils import hosting + + console.set_log_level(loglevel) + + try: + authenticated_client = hosting.get_authenticated_client( + token=token, interactive=interactive + ) + if watch: + status = hosting.watch_deployment_status( + deployment_id=deployment_id, client=authenticated_client + ) + if status is False: + raise click.exceptions.Exit(1) + else: + status = hosting.get_deployment_status( + deployment_id=deployment_id, client=authenticated_client + ) + console.error(status) if "failed" in status else console.print(status) + except NotAuthenticatedError as err: + console.error("You are not authenticated. Run `reflex login` to authenticate.") + raise click.exceptions.Exit(1) from err + + +@apps_cli.command(name="stop") +@click.argument("app_id", required=False) +@click.option("--app-name", help="The name of the application.") +@click.option("--token", help="The authentication token.") +@click.option( + "--loglevel", + type=click.Choice([level.value for level in constants.LogLevel]), + default=constants.LogLevel.INFO.value, + help="The log level to use.", +) +@click.option( + "--interactive/--no-interactive", + "-i", + is_flag=True, + default=True, + help="Whether to use interactive mode.", +) +def stop_app( + app_id: str | None, + app_name: str | None, + token: str | None, + loglevel: str, + interactive: bool, +): + """Stop a running application.""" + from reflex_cli.utils import hosting + + console.set_log_level(loglevel) + + try: + authenticated_client = hosting.get_authenticated_client( + token=token, interactive=interactive + ) + + if not app_id: + config = hosting.read_config() + if config: + app_id = config.appid + if not isinstance(app_id, (str, type(None))): + console.error( + "app_id must be a string or None. Please check your config file." + ) + raise click.exceptions.Exit(1) + + if app_name is not None and app_id is None: + app_result = hosting.search_app( + app_name=app_name, + project_id=None, + client=authenticated_client, + interactive=interactive, + ) + app_id = app_result.get("id") if app_result else None + + if not app_id: + console.error("No valid app_id or app_name provided.") + raise click.exceptions.Exit(1) + + result = hosting.stop_app(app_id=app_id, client=authenticated_client) + if result: + console.error(result) if "failed" in result else console.success(result) + except NotAuthenticatedError as err: + console.error("You are not authenticated. Run `reflex login` to authenticate.") + raise click.exceptions.Exit(1) from err + + +@apps_cli.command(name="start") +@click.argument("app_id", required=False) +@click.option("--app-name", help="The name of the application.") +@click.option("--token", help="The authentication token.") +@click.option( + "--loglevel", + type=click.Choice([level.value for level in constants.LogLevel]), + default=constants.LogLevel.INFO.value, + help="The log level to use.", +) +@click.option( + "--interactive/--no-interactive", + "-i", + is_flag=True, + default=True, + help="Whether to use interactive mode.", +) +def start_app( + app_id: str | None, + app_name: str | None, + token: str | None, + loglevel: str, + interactive: bool, +): + """Start a stopped application.""" + from reflex_cli.utils import hosting + + console.set_log_level(loglevel) + try: + authenticated_client = hosting.get_authenticated_client( + token=token, interactive=interactive + ) + + if not app_id: + config = hosting.read_config() + if config: + app_id = config.appid + if not isinstance(app_id, (str, type(None))): + console.error( + "app_id must be a string or None. Please check your config file." + ) + raise click.exceptions.Exit(1) + + if app_name is not None and app_id is None: + app_result = hosting.search_app( + app_name=app_name, + project_id=None, + client=authenticated_client, + interactive=interactive, + ) + app_id = app_result.get("id") if app_result else None + + if not app_id: + console.error("No valid app_id or app_name provided.") + raise click.exceptions.Exit(1) + + result = hosting.start_app(app_id=app_id, client=authenticated_client) + if result: + console.error(result) if "failed" in result else console.success(result) + except NotAuthenticatedError as err: + console.error("You are not authenticated. Run `reflex login` to authenticate.") + raise click.exceptions.Exit(1) from err + + +@apps_cli.command(name="delete") +@click.argument("app_id", required=False) +@click.option("--app-name", help="The name of the application.") +@click.option("--token", help="The authentication token.") +@click.option( + "--loglevel", + type=click.Choice([level.value for level in constants.LogLevel]), + default=constants.LogLevel.INFO.value, + help="The log level to use.", +) +@click.option( + "--interactive/--no-interactive", + "-i", + is_flag=True, + default=True, + help="Whether to use interactive mode.", +) +def delete_app( + app_id: str | None, + app_name: str | None, + token: str | None, + loglevel: str, + interactive: bool, +): + """Delete an application.""" + from reflex_cli.utils import hosting + + console.set_log_level(loglevel) + try: + authenticated_client = hosting.get_authenticated_client( + token=token, interactive=interactive + ) + + if not app_id: + config = hosting.read_config() + if config: + app_id = config.appid + if not isinstance(app_id, (str, type(None))): + console.error( + "app_id must be a string or None. Please check your config file." + ) + raise click.exceptions.Exit(1) + + app_name_from_search = None + if app_name is not None and app_id is None: + app_result = hosting.search_app( + app_name=app_name, + project_id=None, + client=authenticated_client, + interactive=interactive, + ) + if not app_result: + console.warn(f"App '{app_name}' not found.") + raise click.exceptions.Exit(1) + app_id = app_result.get("id") if app_result else None + app_name_from_search = app_result.get("name") if app_result else app_name + + if app_name_from_search is None and app_id: + try: + app_result = hosting.get_app( + client=authenticated_client, + app_id=app_id, + ) + except GetAppError: + console.warn(f"No application found with ID '{app_id}'") + return + if not app_result: + console.warn(f"App with ID '{app_id}' not found.") + raise click.exceptions.Exit(0) + + if not app_id: + console.error("No valid app_id or app_name provided.") + raise click.exceptions.Exit(1) + + if interactive: + app_name_display = "Unknown" + + if app_name_from_search is not None: + app_name_display = app_name_from_search + elif app_name is not None: + app_name_display = app_name + else: + try: + app_details = hosting.get_app( + app_id=app_id, client=authenticated_client + ) + app_name_display = app_details.get("name", "Unknown") + except Exception: + app_name_display = "Unknown" + + app_id_display = app_id + + if ( + console.ask( + f"Are you sure you want to delete app '{app_name_display}' (ID: {app_id_display})?", + choices=["y", "n"], + default="n", + ) + != "y" + ): + console.info("Deletion cancelled.") + return + + result = hosting.delete_app(app_id=app_id, client=authenticated_client) + if result: + console.warn(result) + except NotAuthenticatedError as err: + console.error("You are not authenticated. Run `reflex login` to authenticate.") + raise click.exceptions.Exit(1) from err + + +@apps_cli.command(name="logs") +@click.argument("app_id", required=False) +@click.option("--app-name", help="The name of the application.") +@click.option("--token", help="The authentication token.") +@click.option("--offset", type=int, help="The offset in seconds from the current time.") +@click.option("--start", type=int, help="The start time in Unix epoch format.") +@click.option("--end", type=int, help="The end time in Unix epoch format.") +@click.option( + "--loglevel", + type=click.Choice([level.value for level in constants.LogLevel]), + default=constants.LogLevel.INFO.value, + help="The log level to use.", +) +@click.option( + "--interactive/--no-interactive", + "-i", + is_flag=True, + default=True, + help="Whether to use interactive mode.", +) +@click.option("--cursor", type=str, help="The cursor for pagination.") +@click.option("--pretty", type=bool, help="Use pretty printing for logs.") +@click.option( + "--follow", type=bool, default=True, help="Asks to continue to query logs." +) +def app_logs( + app_id: str | None, + app_name: str | None, + token: str | None, + offset: int | None, + start: int | None, + end: int | None, + loglevel: str, + interactive: bool, + cursor: str | None = None, + pretty: bool = False, + follow: bool = True, +): + """Retrieve logs for a given application.""" + from reflex_cli.utils import hosting + + if pretty: + try: + import pprint + except ImportError: + console.error( + "pprint module is not available. Please install pprint to use pretty printing." + ) + raise click.exceptions.Exit(1) from ImportError + + authenticated_client = hosting.get_authenticated_client( + token=token, interactive=interactive + ) + + if not app_id: + config = hosting.read_config() + if config: + app_id = config.appid + if not isinstance(app_id, (str, type(None))): + console.error( + "app_id must be a string or None. Please check your config file." + ) + raise click.exceptions.Exit(1) + + if app_name is not None and app_id is None: + app_result = hosting.search_app( + app_name=app_name, + project_id=None, + client=authenticated_client, + interactive=interactive, + ) + app_id = app_result.get("id") if app_result else None + + if not app_id: + console.error("No valid app_id or app_name provided.") + raise click.exceptions.Exit(1) + + if offset is None and start is None and end is None: + offset = 3600 + if not offset and not (start and end): + console.error("must provide both start and end") + raise click.exceptions.Exit(1) + + console.set_log_level(loglevel) + + try: + console.debug(f"fetching logs with cursor: {cursor}") + result = hosting.get_app_logs( + app_id=app_id, + offset=offset, + start=start, + end=end, + client=authenticated_client, + cursor=cursor, + ) + if isinstance(result, list): + if len(result) == 2: + cursor = result[1] + result = result[0] + if not result: + console.warn("No logs found for the specified criteria.") + return + result.reverse() + for log in result: + if pretty: + log = pprint.pformat(log, indent=2) # type: ignore # noqa: PGH003 + console.info(log) + else: + console.warn("Unable to retrieve logs.") + return + if interactive and follow: + from rich.prompt import Prompt + + prompt = Prompt.ask( + "Press Enter to fetch next 100 logs or type 'exit' to quit", + default="", + show_default=False, + ) + if prompt.lower() == "exit": + console.info("Exiting log retrieval.") + raise click.exceptions.Exit(0) + else: + ctx = click.get_current_context() + ctx.invoke( + app_logs, + app_id=app_id, + app_name=None, # Don't pass app_name again since we have app_id + token=token, + offset=offset, + start=start, + end=end, + loglevel=loglevel, + interactive=interactive, + cursor=cursor, + pretty=pretty, + ) + except ResponseError as err: + console.error(f"Error retrieving logs: {err}") + raise click.exceptions.Exit(1) from err + except NotAuthenticatedError as err: + console.error("You are not authenticated. Run `reflex login` to authenticate.") + raise click.exceptions.Exit(1) from err + + +@apps_cli.command(name="list") +@click.option("--project", "project_id", help="The project ID to filter deployments.") +@click.option("--project-name", help="The name of the project.") +@click.option("--token", help="The authentication token.") +@click.option( + "--loglevel", + type=click.Choice([level.value for level in constants.LogLevel]), + default=constants.LogLevel.INFO.value, + help="The log level to use.", +) +@click.option( + "--json/--no-json", + "-j", + "as_json", + is_flag=True, + help="Whether to output the result in JSON format.", +) +@click.option( + "--interactive/--no-interactive", + is_flag=True, + default=True, + help="Whether to list configuration options and ask for confirmation.", +) +def list_apps( + project_id: str | None, + project_name: str | None, + token: str | None, + loglevel: str, + as_json: bool, + interactive: bool, +): + """List all the hosted deployments of the authenticated user. Will exit if unable to list deployments.""" + from reflex_cli.utils import hosting + + console.set_log_level(loglevel) + + authenticated_client = hosting.get_authenticated_client( + token=token, interactive=interactive + ) + + if project_name and not project_id: + result = hosting.search_project( + project_name, client=authenticated_client, interactive=interactive + ) + project_id = result.get("id") if result else None + + if project_id is None: + project_id = hosting.get_selected_project() + + if project_id is not None and not as_json: + try: + project = hosting.get_project(project_id, client=authenticated_client) + console.info(f"Listing apps for project '{project['name']}' ({project_id})") + except Exception: + pass + + try: + deployments = hosting.list_apps(project=project_id, client=authenticated_client) + except Exception as ex: + console.error("Unable to list deployments") + raise click.exceptions.Exit(1) from ex + + if as_json: + console.print(json.dumps(deployments)) + return + if deployments: + headers = list(deployments[0].keys()) + table = [ + [str(value) for value in deployment.values()] for deployment in deployments + ] + console.print_table(table, headers=headers) + else: + console.print(str(deployments)) + + +@apps_cli.command(name="scale") +@click.argument("app_id", required=False) +@click.option("--app-name", help="The name of the app.") +@click.option("--vmtype", help="The virtual machine type to scale to.") +@click.option("--regions", "-r", multiple=True, help="Region to scale the app to.") +@click.option("--token", help="The authentication token.") +@click.option( + "--loglevel", + type=click.Choice([level.value for level in constants.LogLevel]), + default=constants.LogLevel.INFO.value, + help="The log level to use.", +) +@click.option("--scale-type", help="The type of scaling.") +@click.option( + "--interactive/--no-interactive", + "-i", + is_flag=True, + default=True, + help="Whether to use interactive mode.", +) +def scale_app( + app_id: str | None, + app_name: str | None, + vmtype: str | None, + regions: tuple[str, ...], + token: str | None, + loglevel: str, + scale_type: str | None, + interactive: bool, +): + """Scale an application by changing the VM type or adding/removing regions.""" + from reflex_cli.utils import hosting + + console.set_log_level(loglevel) + try: + authenticated_client = hosting.get_authenticated_client( + token=token, interactive=interactive + ) + + if not app_id: + config = hosting.read_config() + if config: + app_id = config.appid + if not isinstance(app_id, (str, type(None))): + console.error( + "app_id must be a string or None. Please check your config file." + ) + raise click.exceptions.Exit(1) + + cli_args = hosting.ScaleAppCliArgs.create( + regions=list(regions), vm_type=vmtype, scale_type=scale_type + ) + config = Config.from_yaml_or_toml_or_default().with_overrides( + vmtype=cli_args.vm_type, + regions=cli_args.regions, + ) + + if not config.exists() and not cli_args.is_valid: + console.error( + "specify either --vmtype or --regions or add them to the cloud.yml or pyproject.toml file" + ) + raise click.exceptions.Exit(1) + + if config.exists() and cli_args.is_valid: + console.warn( + "CLI arguments will override the values in the cloud.yml or pyproject.toml file." + ) + scale_params = hosting.ScaleParams.from_config(config).set_type_from_cli_args( + cli_args + ) + + # If app_name is provided, find the app_id + if app_name is not None and app_id is None: + app_result = hosting.search_app( + app_name=app_name, + project_id=None, + client=authenticated_client, + interactive=interactive, + ) + app_id = app_result.get("id") if app_result else None + + if not app_id: + console.error("No valid app_id or app_name provided.") + raise click.exceptions.Exit(1) + + hosting.scale_app( + app_id=app_id, scale_params=scale_params, client=authenticated_client + ) + console.success("Successfully scaled the app.") + + except NotAuthenticatedError as err: + console.error("You are not authenticated. Run `reflex login` to authenticate.") + raise click.exceptions.Exit(1) from err + except ( + ScaleAppError, + ResponseError, + ConfigInvalidFieldValueError, + ScaleTypeError, + ScaleParamError, + ) as err: + console.error(err.args[0]) + raise click.exceptions.Exit(1) from err + + +@apps_cli.command(name="inspect") +@click.argument("app_id", required=False) +@click.option("--token", help="The authentication token.") +@click.option( + "--loglevel", + type=click.Choice([level.value for level in constants.LogLevel]), + default=constants.LogLevel.INFO.value, + help="The log level to use.", +) +@click.option( + "--json/--no-json", + "-j", + "as_json", + is_flag=True, + help="Whether to output the result in JSON format.", +) +@click.option( + "--interactive/--no-interactive", + "-i", + is_flag=True, + default=True, + help="Whether to use interactive mode.", +) +def inspect_app( + app_id: str | None, + token: str | None, + loglevel: str, + as_json: bool, + interactive: bool, +): + """Retrieve detailed information about a specific application.""" + from reflex_cli.utils import hosting + + console.set_log_level(loglevel) + try: + authenticated_client = hosting.get_authenticated_client( + token=token, interactive=interactive + ) + + if not app_id: + config = hosting.read_config() + if config: + app_id = config.appid + if not isinstance(app_id, (str, type(None))): + console.error( + "app_id must be a string or None. Please check your config file." + ) + raise click.exceptions.Exit(1) + + if not app_id: + console.error( + "No valid app_id provided or found in cloud.yml or pyproject.toml." + ) + raise click.exceptions.Exit(1) + + app_info = hosting.get_app(app_id=app_id, client=authenticated_client) + + if as_json: + console.print(json.dumps(app_info)) + return + + if app_info: + if isinstance(app_info, dict): + headers = list(app_info.keys()) + values = [[str(value) for value in app_info.values()]] + console.print_table(values, headers=headers) + else: + console.print(str(app_info)) + else: + console.print("No app information found.") + except NotAuthenticatedError as err: + console.error("You are not authenticated. Run `reflex login` to authenticate.") + raise click.exceptions.Exit(1) from err diff --git a/packages/reflex-hosting-cli/reflex_cli/v2/cli.py b/packages/reflex-hosting-cli/reflex_cli/v2/cli.py new file mode 100644 index 00000000000..106dcedf475 --- /dev/null +++ b/packages/reflex-hosting-cli/reflex_cli/v2/cli.py @@ -0,0 +1,479 @@ +"""CLI for the hosting service.""" + +from __future__ import annotations + +import dataclasses +import json +import os +import shutil +import tempfile +from collections.abc import Callable +from pathlib import Path +from typing import Any + +import click +from packaging import version + +from reflex_cli import constants +from reflex_cli.utils import console +from reflex_cli.utils.dependency import extract_domain + + +def login( + loglevel: constants.LogLevel = constants.LogLevel.INFO, +) -> dict[str, Any]: + """Authenticate with Reflex hosting service. + + Args: + loglevel: The log level to use. + + Returns: + Information about the logged in user. + + Raises: + SystemExit: If the command fails. + + """ + from reflex_cli.utils import hosting + + # Set the log level. + console.set_log_level(loglevel) + + access_token, validated_info = hosting.authenticated_token() + if access_token: + console.print("You already logged in.") + return validated_info + + # If not already logged in, open a browser window/tab to the login page. + access_token, validated_info = hosting.authenticate_on_browser() + + if not access_token: + console.error("Unable to authenticate. Please try again or contact support.") + raise SystemExit(1) + + console.print("Successfully logged in.") + return validated_info + + +def logout( + loglevel: constants.LogLevel = constants.LogLevel.INFO, +): + """Log out of access to Reflex hosting service. + + Args: + loglevel: The log level to use. + + """ + from reflex_cli.utils import hosting + + console.set_log_level(loglevel) + + console.debug("Deleting access token from config locally") + hosting.delete_token_from_config() + console.success("Successfully logged out.") + + +def deploy( + export_fn: Callable[[str, str, str, bool, bool, bool, bool], None] + | Callable[[str, str, str, bool, bool, bool], None], + app_name: str | None = None, + description: str | None = None, + regions: list[str] | None = None, + project: str | None = None, + envs: list[str] | None = None, + vmtype: str | None = None, + hostname: str | None = None, + interactive: bool = True, + envfile: str | None = None, + loglevel: constants.LogLevel = constants.LogLevel.INFO, + token: str | None = None, + config_path: str | None = None, + env: str | None = None, + project_name: str | None = None, + app_id: str | None = None, + **kwargs, +): + """Deploy the app to the Reflex hosting service. + + Args: + app_name: The name of the app. + export_fn: The function from the Reflex main framework to export the app. + description: The app's description. + regions: The regions to deploy to. + project: The project to deploy to. + envs: The environment variables to set. + vmtype: The VM type to allocate. + hostname: The hostname to use for the frontend. + interactive: Whether to use interactive mode. + envfile: The path to an env file to use. Will override any envs set manually. + loglevel: The log level to use. + token: The authentication token. + config_path: The path to the config file. + env: The environment to use for deployment. + project_name: The name of the project. + app_id: The ID of the app. + **kwargs: Additional keyword arguments. + + Raises: + Exit: If the command fails. + + """ + import httpx + + from reflex_cli.utils import hosting + + authenticated_client = hosting.get_authenticated_client( + token=token, interactive=interactive + ) + + # Set the log level. + console.set_log_level(loglevel) + project_id = project + config = {} + config_from_file = hosting.read_config(config_path, env=env) + if config_from_file: + config = dataclasses.asdict(config_from_file) + + packages = None + strategy = None + include_db = False + # If a config file is provided, use values from the file that are not provided as arguments. + if config: + if not regions: + regions = config.get("regions", None) + if not vmtype: + vmtype = config.get("vmtype", None) + if not hostname: + hostname = config.get("hostname", None) + if not envfile: + envfile = config.get("envfile", None) + if not project_id: + project_id = config.get("project", None) + if not project_name: + project_name = config.get("projectname", None) + if not app_id: + app_id = config.get("appid", None) + if not isinstance(app_id, (str, type(None))): + console.error( + "app_id must be a string or None. Please check your config file." + ) + raise SystemExit(1) + if not packages: + packages = config.get("packages", None) + if not include_db: + include_db = config.get("include_db", False) + if not strategy: + strategy = config.get("strategy", None) + app_name = config.get("name", app_name) + if not isinstance(app_name, (str, type(None))): + console.error( + "app_name must be a string or None. Please check your config file." + ) + raise SystemExit(1) + if app_name == "default": + # not sure if this is the best check? + console.error( + "Please set real config values in cloud.yml or pyproject.toml" + ) + raise SystemExit(1) + if not description: + description = config.get("description", None) + + # resolve the project id from the project name. + if project_name and not project_id: + result = hosting.search_project( + project_name, client=authenticated_client, interactive=interactive + ) + project_id = result.get("id") if result else None + + try: + # check if provided project exists. + if project_id: + hosting.get_project(project_id, client=authenticated_client) + else: + project_id = hosting.get_selected_project() + except httpx.HTTPStatusError as ex: + try: + console.error(ex.response.json().get("detail")) + except json.JSONDecodeError: + console.error(ex.response.text) + raise click.exceptions.Exit(1) from ex + + envs = envs or [] + + if not app_name and not app_id: + console.error( + "Please provide a valid app name or ID for the deployed instance." + ) + raise click.exceptions.Exit(1) + try: + if app_name and not app_id: + search_project_id = project_id + if not interactive and not project and not search_project_id: + search_project_id = hosting.get_selected_project() + elif interactive and not project: + search_project_id = None + + app = hosting.search_app( + app_name=app_name, + project_id=search_project_id, + client=authenticated_client, + interactive=interactive, + ) + else: + app = hosting.get_app(app_id or "", client=authenticated_client) + app_name = app.get("name") + except click.exceptions.Exit: + raise + except Exception as ex: + console.error(f"Deployment failed: {ex}") + raise click.exceptions.Exit(1) from ex + + if app and interactive and not project and not app_id: + default_project_id = hosting.get_selected_project() + app_project_id = app.get("project_id") + + if app_project_id and ( + not default_project_id or app_project_id != default_project_id + ): + app_project = hosting.get_project( + app_project_id, client=authenticated_client + ) + app_project_name = app_project.get("name", "Unknown") + if ( + console.ask( + f"Deploy to app '{app['name']}' in project '{app_project_name}'?", + choices=["y", "n"], + default="y", + ) + != "y" + ): + console.info("Deployment cancelled.") + raise click.exceptions.Exit(0) + + project_id = app_project_id + + if not app and interactive: + if ( + console.ask( + f"No app with {app_name or app_id} found. Do you want to create a new app to deploy?", + choices=["y", "n"], + default="y", + ) + == "y" + ): + # Check if we need confirmation for deploying to non-default project + if not project: + default_project_id = hosting.get_selected_project() + if not default_project_id: + try: + if project_id: + target_project = hosting.get_project( + project_id, client=authenticated_client + ) + project_name = target_project.get("name", "Unknown") + else: + token = hosting.get_existing_access_token() + default_project_id = hosting.get_default_project( + authenticated_client + ) + if default_project_id: + default_project = hosting.get_project( + default_project_id, client=authenticated_client + ) + project_name = default_project.get( + "name", "Default Project" + ) + else: + project_name = "Default Project" + except Exception: + project_name = "Unknown" + + if ( + console.ask( + f"Create and deploy app '{app_name}' in project '{project_name}'?", + choices=["y", "n"], + default="y", + ) + != "y" + ): + console.info("Deployment cancelled.") + raise click.exceptions.Exit(0) + elif project_id and project_id != default_project_id: + try: + target_project = hosting.get_project( + project_id, client=authenticated_client + ) + project_name = target_project.get("name", "Unknown") + except Exception: + project_name = "Unknown" + + if ( + console.ask( + f"Create and deploy app '{app_name}' in project '{project_name}'?", + choices=["y", "n"], + default="y", + ) + != "y" + ): + console.info("Deployment cancelled.") + raise click.exceptions.Exit(0) + + if description is None: + description = console.ask( + "App Description (Enter to skip)", + ) + app = hosting.create_app( + app_name=app_name or "", + description=description, + project_id=project_id, + client=authenticated_client, + ) + console.info(f"created app. \nName: {app['name']} \nId: {app['id']}") + else: + console.error("Please create an app to deploy.") + raise click.exceptions.Exit(1) + elif not app: + app = hosting.create_app( + app_name=app_name or "", + description=description or "", + project_id=project_id, + client=authenticated_client, + ) + console.info(f"created app. \nName: {app['name']} \nId: {app['id']}") + + urls = hosting.get_hostname( + app_id=app["id"], + app_name=app["name"], + hostname=hostname, + client=authenticated_client, + ) + if "error" in urls: + console.error(urls["error"]) + raise click.exceptions.Exit(1) + server_url = os.getenv("REFLEX_OVERRIDE_BACKEND_URL") or urls["server"] # backend + host_url = os.getenv("REFLEX_OVERRIDE_FRONTEND_URL") or urls["hostname"] # frontend + processed_envs = hosting.process_envs(envs) if envs else None + + if not app_name: + console.error("Please set an app name.") + raise click.exceptions.Exit(1) + + # at this point, if project_id is None, the App should have the correct project_id and + # we should use that going forward to pass validation checks. + project_id = project_id or app.get("project_id") + + validation_message = hosting.validate_deployment_args( + app_name=app_name, + app_id=app.get("id"), + project_id=project_id, + regions=regions, + vmtype=vmtype, + hostname=hostname, + client=authenticated_client, + ) + + if validation_message != "success": + console.error(validation_message) + raise click.exceptions.Exit(1) + + if envfile: + try: + from dotenv import dotenv_values # pyright: ignore[reportMissingImports] + + processed_envs = dotenv_values(envfile) + except ImportError: + console.error( + """The `python-dotenv` package is required to load environment variables from a file. Run `pip install "python-dotenv>=1.0.1"`.""" + ) + + # Compile the app in production mode: backend first then frontend. + temporary_dir = tempfile.TemporaryDirectory() + temporary_dir_path = Path(temporary_dir.name) + + import importlib.metadata + + rx_version = version.parse(importlib.metadata.version("reflex")) + breaking_version = version.parse("0.7.6") + # Try zipping backend first + try: + # Check if the reflex version is >= 0.7.6 + if rx_version <= breaking_version: + export_fn( + str(temporary_dir_path), + server_url, + host_url, + False, + True, + True, + ) # pyright: ignore[reportCallIssue] + else: + export_fn( + str(temporary_dir_path), + server_url, + host_url, + False, + True, + include_db, + True, # pyright: ignore[reportCallIssue] + ) + except Exception as ex: + console.error(f"Unable to export due to: {ex}") + if temporary_dir_path.exists(): + shutil.rmtree(temporary_dir_path) + raise click.exceptions.Exit(1) from ex + + # Zip frontend + try: + # Check if the reflex version is >= 0.7.6 + if rx_version <= breaking_version: + export_fn(str(temporary_dir_path), server_url, host_url, True, False, True) # pyright: ignore[reportCallIssue] + else: + export_fn( + str(temporary_dir_path), + server_url, + host_url, + True, + False, + include_db, + True, # pyright: ignore[reportCallIssue] + ) + except ImportError as ie: + console.error( + f"Encountered ImportError, did you install all the dependencies? {ie}" + ) + if temporary_dir_path.exists(): + shutil.rmtree(temporary_dir_path) + raise click.exceptions.Exit(1) from ie + except Exception as ex: + console.error(f"Unable to export due to: {ex}") + if temporary_dir_path.exists(): + shutil.rmtree(temporary_dir_path) + raise click.exceptions.Exit(1) from ex + + result = hosting.create_deployment( + app_id=app.get("id"), + app_name=app_name, + project_id=project_id, + regions=regions, + zip_dir=Path(temporary_dir_path), + hostname=extract_domain(host_url) if hostname else None, + vmtype=vmtype, + secrets=processed_envs, + client=authenticated_client, + packages=packages, + strategy=strategy, + ) + if "failed" in result: + console.error(result) + raise click.exceptions.Exit(1) + hosting_ui_url = f"{constants.Hosting.HOSTING_SERVICE_UI}/project/{app['project_id']}/app/{app['id']}/" + console.print( + f"deployment progress can now be viewed on the website: {hosting_ui_url}" + ) + console.print( + f"you are now safe to exit this command.\nfollow along with the deployment with the following command: \n reflex cloud apps status {result} --watch" + ) + status = hosting.watch_deployment_status(result, client=authenticated_client) + if status is False: + raise click.exceptions.Exit(1) diff --git a/packages/reflex-hosting-cli/reflex_cli/v2/deployments.py b/packages/reflex-hosting-cli/reflex_cli/v2/deployments.py new file mode 100644 index 00000000000..d6761fc56d6 --- /dev/null +++ b/packages/reflex-hosting-cli/reflex_cli/v2/deployments.py @@ -0,0 +1,137 @@ +"""The Hosting CLI deployments sub-commands.""" + +from __future__ import annotations + +import importlib.metadata +from importlib.util import find_spec +from typing import TYPE_CHECKING + +import click +from packaging import version + +from reflex_cli import constants +from reflex_cli.utils import console +from reflex_cli.v2.apps import apps_cli +from reflex_cli.v2.project import project_cli +from reflex_cli.v2.secrets import secrets_cli +from reflex_cli.v2.vmtypes_regions import vm_types_regions_cli + +if TYPE_CHECKING: + import typer + + +@click.group +@click.pass_context +def hosting_cli(ctx: click.Context) -> None: + """The Hosting CLI. + + This CLI is used to manage the Reflex cloud hosting service. + It provides commands for managing apps, projects, secrets, and VM types/regions. + + """ + if _reflex_version < constants.ReflexHostingCli.MINIMUM_REFLEX_VERSION: + ctx.fail( + f"Reflex version {_reflex_version} is not compatible with reflex-hosting-cli. " + f"Please upgrade Reflex to at least version {constants.ReflexHostingCli.MINIMUM_REFLEX_VERSION}." + ) + if _reflex_version < constants.ReflexHostingCli.RECOMMENDED_REFLEX_VERSION: + console.warn( + f"Support for Reflex version {_reflex_version} in reflex-hosting-cli is deprecated. " + f"Please upgrade Reflex to at least version {constants.ReflexHostingCli.RECOMMENDED_REFLEX_VERSION}." + ) + check_version() + + +_reflex_version = version.parse(importlib.metadata.version("reflex")) + + +hosting_cli.add_command( + apps_cli, + name="apps", +) +hosting_cli.add_command( + project_cli, + name="project", +) +hosting_cli.add_command( + secrets_cli, + name="secrets", +) +for name, command in vm_types_regions_cli.commands.items(): + # Add the command to the hosting CLI + hosting_cli.add_command(command, name=name) + + +def _patch_typer(click_instance: click.Command) -> typer.Typer: + import functools + + import typer + from typer.models import TyperInfo + + fake_typer_app = typer.Typer(add_completion=False) + + fake_typer_app.callback()(lambda: None) + + original_get_group_from_info = typer.main.get_group_from_info + + def get_group_from_info(group_info: TyperInfo, *args, **kwargs): + if group_info.typer_instance is fake_typer_app: + click_instance.name = group_info.name + return click_instance + return original_get_group_from_info(group_info, *args, **kwargs) + + functools.update_wrapper( + get_group_from_info, + original_get_group_from_info, + ) + + typer.main.get_group_from_info = get_group_from_info + + return fake_typer_app + + +if ( + find_spec("typer") is not None + and find_spec("typer.core") is not None + and find_spec("typer.models") is not None +): + hosting_cli = _patch_typer(hosting_cli) # pyright: ignore[reportAssignmentType] + +TIME_FORMAT_HELP = "Accepts ISO 8601 format, unix epoch or time relative to now. For time relative to now, use the format: . Valid units are d (day), h (hour), m (minute), s (second). For example, 1d for 1 day ago from now." +MIN_LOGS_LIMIT = 50 +MAX_LOGS_LIMIT = 1000 + + +def check_version(): + """Callback to be invoked for all hosting CLI commands. + + Checks if the installed version of the package is up-to-date with the latest version available on PyPI. + If a newer version is available, it prints a warning message and exits. + + Raises: + Exit: If a newer version is available, prompting the user to upgrade. + + """ + import httpx + + package_name = constants.ReflexHostingCli.MODULE_NAME + try: + installed_version = importlib.metadata.version(package_name) + response = httpx.get(f"https://pypi.org/pypi/{package_name}/json") + response.raise_for_status() + latest_version = response.json()["info"]["version"] + + if version.parse(installed_version) < version.parse(latest_version): + console.error( + f"Warning: You are using {package_name} version {installed_version}. " + f"A newer version {latest_version} is available. " + f"Upgrade using: pip install --upgrade {package_name}" + ) + raise click.exceptions.Exit(1) + except ( + importlib.metadata.PackageNotFoundError, + httpx.RequestError, + httpx.HTTPStatusError, + ): + # Silently pass if we can't check the version + pass diff --git a/packages/reflex-hosting-cli/reflex_cli/v2/project.py b/packages/reflex-hosting-cli/reflex_cli/v2/project.py new file mode 100644 index 00000000000..8a8a6ad81c9 --- /dev/null +++ b/packages/reflex-hosting-cli/reflex_cli/v2/project.py @@ -0,0 +1,533 @@ +"""Project commands for the Reflex Cloud CLI.""" + +import json + +import click + +from reflex_cli import constants +from reflex_cli.utils import console +from reflex_cli.utils.exceptions import NotAuthenticatedError + + +@click.group() +def project_cli(): + """Commands for managing projects.""" + pass + + +@project_cli.command(name="create") +@click.argument("name", required=True) +@click.option("--token", help="The authentication token.") +@click.option( + "--loglevel", + type=click.Choice([level.value for level in constants.LogLevel]), + default=constants.LogLevel.INFO.value, + help="The log level to use.", +) +@click.option( + "--json/--no-json", + "-j", + "as_json", + is_flag=True, + help="Whether to output the result in json format.", +) +@click.option( + "--interactive/--no-interactive", + "-i", + is_flag=True, + default=True, + help="Whether to use interactive mode.", +) +def create_project( + name: str, + token: str | None, + loglevel: str, + as_json: bool, + interactive: bool, +): + """Create a new project.""" + from reflex_cli.utils import hosting + + console.set_log_level(loglevel) + try: + authenticated_client = hosting.get_authenticated_client( + token=token, interactive=interactive + ) + project = hosting.create_project(name=name, client=authenticated_client) + except ValueError as err: + console.error(str(err)) + raise click.exceptions.Exit(1) from err + except NotAuthenticatedError as err: + console.error("You are not authenticated. Run `reflex login` to authenticate.") + raise click.exceptions.Exit(1) from err + + if as_json: + console.print(json.dumps(project)) + return + if project: + project = [project] + headers = list(project[0].keys()) + table = [ + [str(value) if value is not None else "" for value in p.values()] + for p in project + ] + console.print_table(table, headers=headers) + else: + console.print(str(project)) + + +@project_cli.command(name="invite") +@click.argument("role", required=True) +@click.argument("user", required=True) +@click.option("--token", help="The authentication token.") +@click.option( + "--loglevel", + type=click.Choice([level.value for level in constants.LogLevel]), + default=constants.LogLevel.INFO.value, + help="The log level to use.", +) +@click.option( + "--interactive/--no-interactive", + "-i", + is_flag=True, + default=True, + help="Whether to use interactive mode.", +) +def invite_user_to_project( + role: str, + user: str, + token: str | None, + loglevel: str, + interactive: bool, +): + """Invite a user to a project.""" + from reflex_cli.utils import hosting + + console.set_log_level(loglevel) + try: + authenticated_client = hosting.get_authenticated_client( + token=token, interactive=interactive + ) + result = hosting.invite_user_to_project( + role_id=role, user_id=user, client=authenticated_client + ) + except NotAuthenticatedError as err: + console.error("You are not authenticated. Run `reflex login` to authenticate.") + raise click.exceptions.Exit(1) from err + + if "failed" in result: + console.error(f"Unable to invite user to project: {result}") + raise click.exceptions.Exit(1) + console.success("Successfully invited user to project.") + + +@project_cli.command(name="select") +@click.argument("project_id", required=False) +@click.option("--project-name", help="The name of the project. ") +@click.option("--token", help="The authentication token.") +@click.option( + "--loglevel", + type=click.Choice([level.value for level in constants.LogLevel]), + default=constants.LogLevel.INFO.value, + help="The log level to use.", +) +@click.option( + "--interactive/--no-interactive", + is_flag=True, + default=True, + help="Whether to list configuration options and ask for confirmation.", +) +def select_project( + project_id: str | None, + project_name: str | None, + token: str | None, + loglevel: str, + interactive: bool, +): + """Select a project.""" + import httpx + + from reflex_cli.utils import hosting + + try: + authenticated_client = hosting.get_authenticated_client( + token=token, interactive=interactive + ) + # check if provided project exists. + if project_id: + hosting.get_project(project_id, client=authenticated_client) + except httpx.HTTPStatusError as ex: + try: + console.error(ex.response.json().get("detail")) + except json.JSONDecodeError: + console.error(ex.response.text) + raise click.exceptions.Exit(1) from ex + + if project_name and not project_id: + result = hosting.search_project( + project_name, interactive=interactive, client=authenticated_client + ) + project_id = result.get("id") if result else None + + if not project_id: + console.error("No project selected. Please provide a valid project ID or name.") + raise click.exceptions.Exit(1) + + console.set_log_level(loglevel) + result = hosting.select_project(project=project_id, token=token) + if "failed" in result: + console.error(result) + raise click.exceptions.Exit(1) + console.success(result) + + +@project_cli.command(name="selected") +@click.option( + "--loglevel", + type=click.Choice([level.value for level in constants.LogLevel]), + default=constants.LogLevel.INFO.value, + help="The log level to use.", +) +@click.option("--token", help="The authentication token.") +@click.option( + "--interactive/--no-interactive", + "-i", + is_flag=True, + default=True, + help="Whether to use interactive mode.", +) +def get_select_project( + loglevel: str, + token: str | None, + interactive: bool, +): + """Get the currently selected project.""" + from reflex_cli.utils import hosting + + console.set_log_level(loglevel) + project = hosting.get_selected_project() + if project: + try: + authenticated_client = hosting.get_authenticated_client( + token=token, interactive=interactive + ) + project_details = hosting.get_project( + project_id=project, client=authenticated_client + ) + console.print_table( + [[project, project_details["name"]]], + headers=["Selected Project ID", "Project Name"], + ) + except NotAuthenticatedError: + console.error( + "You are not authenticated. Run `reflex login` to authenticate." + ) + click.exceptions.Exit(1) + except Exception as e: + console.error(f"Unable to get the currently selected project: {e}") + else: + console.warn( + "no selected project. run `reflex cloud project select` to set one." + ) + + +@project_cli.command(name="list") +@click.option("--token", help="The authentication token.") +@click.option( + "--loglevel", + type=click.Choice([level.value for level in constants.LogLevel]), + default=constants.LogLevel.INFO.value, + help="The log level to use.", +) +@click.option( + "--json/--no-json", + "-j", + "as_json", + is_flag=True, + help="Whether to output the result in json format.", +) +@click.option( + "--interactive/--no-interactive", + "-i", + is_flag=True, + default=True, + help="Whether to use interactive mode.", +) +def get_projects( + token: str | None, + loglevel: str, + as_json: bool, + interactive: bool, +): + """Retrieve a list of projects.""" + from reflex_cli.utils import hosting + + console.set_log_level(loglevel) + + try: + authenticated_client = hosting.get_authenticated_client( + token=token, interactive=interactive + ) + projects = hosting.get_projects(client=authenticated_client) + if as_json: + console.print(json.dumps(projects)) + return + if projects: + headers = list(projects[0].keys()) + table = [] + for project in projects: + row = [] + for value in project.values(): + if isinstance(value, (dict, list)): + row.append(json.dumps(value)) + else: + row.append(str(value)) + table.append(row) + console.print_table(table, headers=headers) + else: + # If returned empty list, print the empty + console.print(str(projects)) + except NotAuthenticatedError: + console.error("You are not authenticated. Run `reflex login` to authenticate.") + click.exceptions.Exit(1) + except Exception as e: + console.error(f"Unable to get projects: {e}") + raise click.exceptions.Exit(1) from e + + +@project_cli.command(name="roles") +@click.option( + "--project-id", + help="The ID of the project. If not provided, the selected project will be used. If no project_id is provided or selected throws an error.", +) +@click.option("--project-name", help="The name of the project. ") +@click.option("--token", help="The authentication token.") +@click.option( + "--loglevel", + type=click.Choice([level.value for level in constants.LogLevel]), + default=constants.LogLevel.INFO.value, + help="The log level to use.", +) +@click.option( + "--json/--no-json", + "-j", + "as_json", + is_flag=True, + help="Whether to output the result in json format.", +) +@click.option( + "--interactive/--no-interactive", + is_flag=True, + default=True, + help="Whether to list configuration options and ask for confirmation.", +) +def get_project_roles( + project_id: str | None, + project_name: str | None, + token: str | None, + loglevel: str, + as_json: bool, + interactive: bool, +): + """Retrieve the roles for a project.""" + from reflex_cli.utils import hosting + + console.set_log_level(loglevel) + + try: + authenticated_client = hosting.get_authenticated_client( + token=token, interactive=interactive + ) + if project_name and not project_id: + result = hosting.search_project( + project_name, client=authenticated_client, interactive=interactive + ) + project_id = result.get("id") if result else None + if project_id is None: + project_id = hosting.get_selected_project() + if project_id is None: + console.error( + "no project_id provided or selected. Set it with `reflex cloud project roles --project-id \\[project_id]`" + ) + raise click.exceptions.Exit(1) + + roles = hosting.get_project_roles( + project_id=project_id, client=authenticated_client + ) + + if as_json: + console.print(json.dumps(roles)) + return + if roles: + headers = list(roles[0].keys()) + table = [ + [str(value) if value is not None else "" for value in role.values()] + for role in roles + ] + console.print_table(table, headers=headers) + else: + # If returned empty list, print the empty + console.print(str(roles)) + except NotAuthenticatedError as err: + console.error("You are not authenticated. Run `reflex login` to authenticate.") + raise click.exceptions.Exit(1) from err + + +@project_cli.command(name="role-permissions") +@click.argument("role_id", required=True) +@click.option( + "--project-id", + help="The ID of the project. If not provided, the selected project will be used. If no project is selected, it throws an error.", +) +@click.option("--project-name", help="The name of the project. ") +@click.option("--token", help="The authentication token.") +@click.option( + "--loglevel", + type=click.Choice([level.value for level in constants.LogLevel]), + default=constants.LogLevel.INFO.value, + help="The log level to use.", +) +@click.option( + "--json/--no-json", + "-j", + "as_json", + is_flag=True, + help="Whether to output the result in json format.", +) +@click.option( + "--interactive/--no-interactive", + is_flag=True, + default=True, + help="Whether to list configuration options and ask for confirmation.", +) +def get_project_role_permissions( + role_id: str, + project_id: str | None, + project_name: str | None, + token: str | None, + loglevel: str, + as_json: bool, + interactive: bool, +): + """Retrieve the permissions for a specific role in a project.""" + from reflex_cli.utils import hosting + + console.set_log_level(loglevel) + try: + authenticated_client = hosting.get_authenticated_client( + token=token, interactive=interactive + ) + if project_name and not project_id: + result = hosting.search_project( + project_name, client=authenticated_client, interactive=interactive + ) + project_id = result.get("id") if result else None + if project_id is None: + project_id = hosting.get_selected_project() + if project_id is None: + console.error( + "no project_id provided or selected. Set it with `reflex cloud project role-permissions --project-id \\[project_id]`." + ) + raise click.exceptions.Exit(1) + + permissions = hosting.get_project_role_permissions( + project_id=project_id, role_id=role_id, client=authenticated_client + ) + + if as_json: + console.print(json.dumps(permissions)) + return + if permissions: + headers = list(permissions[0].keys()) + table = [ + [ + str(value) if value is not None else "" + for value in permission.values() + ] + for permission in permissions + ] + console.print_table(table, headers=headers) + else: + # If returned empty list, print the empty + console.print(str(permissions)) + except NotAuthenticatedError as err: + console.error("You are not authenticated. Run `reflex login` to authenticate.") + raise click.exceptions.Exit(1) from err + + +@project_cli.command(name="users") +@click.option( + "--project-id", + help="The ID of the project. If not provided, the selected project will be used. If no project is selected, it throws an error.", +) +@click.option("--project-name", help="The name of the project. ") +@click.option("--token", help="The authentication token.") +@click.option( + "--loglevel", + type=click.Choice([level.value for level in constants.LogLevel]), + default=constants.LogLevel.INFO.value, + help="The log level to use.", +) +@click.option( + "--json/--no-json", + "-j", + "as_json", + is_flag=True, + help="Whether to output the result in json format.", +) +@click.option( + "--interactive/--no-interactive", + is_flag=True, + default=True, + help="Whether to list configuration options and ask for confirmation.", +) +def get_project_role_users( + project_id: str | None, + project_name: str | None, + token: str | None, + loglevel: str, + as_json: bool, + interactive: bool, +): + """Retrieve the users for a project.""" + from reflex_cli.utils import hosting + + console.set_log_level(loglevel) + + try: + authenticated_client = hosting.get_authenticated_client( + token=token, interactive=interactive + ) + if project_name and not project_id: + result = hosting.search_project( + project_name, client=authenticated_client, interactive=interactive + ) + project_id = result.get("id") if result else None + if project_id is None: + project_id = hosting.get_selected_project() + if project_id is None: + console.error( + "no project_id provided or selected. Set it with `reflex cloud project users --project-id \\[project_id]`" + ) + raise click.exceptions.Exit(1) + + users = hosting.get_project_role_users( + project_id=project_id, client=authenticated_client + ) + + if as_json: + console.print(json.dumps(users)) + return + if users: + headers = list(users[0].keys()) + table = [ + [str(value) if value is not None else "" for value in user.values()] + for user in users + ] + console.print_table(table, headers=headers) + else: + # If returned empty list, print the empty + console.print(str(users)) + except NotAuthenticatedError as err: + console.error("You are not authenticated. Run `reflex login` to authenticate.") + raise click.exceptions.Exit(1) from err diff --git a/packages/reflex-hosting-cli/reflex_cli/v2/secrets.py b/packages/reflex-hosting-cli/reflex_cli/v2/secrets.py new file mode 100644 index 00000000000..7a49dc770c2 --- /dev/null +++ b/packages/reflex-hosting-cli/reflex_cli/v2/secrets.py @@ -0,0 +1,240 @@ +"""Secrets commands for the Reflex Cloud CLI.""" + +from __future__ import annotations + +import click + +from reflex_cli import constants +from reflex_cli.utils import console +from reflex_cli.utils.exceptions import NotAuthenticatedError + + +@click.group() +def secrets_cli(): + """Commands for managing secrets.""" + pass + + +@secrets_cli.command(name="list") +@click.argument("app_id", required=False) +@click.option("--token", help="The authentication token.") +@click.option( + "--loglevel", + type=click.Choice([level.value for level in constants.LogLevel]), + default=constants.LogLevel.INFO.value, + help="The log level to use.", +) +@click.option( + "--json/--no-json", + "-j", + "as_json", + is_flag=True, + help="Whether to output the result in JSON format.", +) +@click.option( + "--interactive/--no-interactive", + "-i", + is_flag=True, + default=True, + help="Whether to use interactive mode.", +) +def get_secrets( + app_id: str | None, + token: str | None, + loglevel: str, + as_json: bool, + interactive: bool, +): + """Retrieve secrets for a given application.""" + from reflex_cli.utils import hosting + + console.set_log_level(loglevel) + + try: + authenticated_client = hosting.get_authenticated_client( + token=token, interactive=interactive + ) + + if not app_id: + config = hosting.read_config() + if config: + app_id = config.appid + if not isinstance(app_id, (str, type(None))): + console.error( + "app_id must be a string or None. Please check your config file." + ) + raise click.exceptions.Exit(1) + + if not app_id: + console.error("No valid app_id provided.") + raise click.exceptions.Exit(1) + + secrets = hosting.get_secrets(app_id=app_id, client=authenticated_client) + if "failed" in secrets: + console.error(secrets) + raise click.exceptions.Exit(1) + if as_json: + console.print(secrets) + return + if secrets: + headers = ["Keys"] + table = [[key] for key in secrets] + console.print_table(table, headers=headers) + else: + console.print(str(secrets)) + except NotAuthenticatedError as err: + console.error("You are not authenticated. Run `reflex login` to authenticate.") + raise click.exceptions.Exit(1) from err + + +@secrets_cli.command(name="update") +@click.argument("app_id", required=False) +@click.option( + "--envfile", + help="The path to an env file to use. Will override any envs set manually.", +) +@click.option( + "--env", + "envs", + multiple=True, + help="The environment variables to set: =. Required if envfile is not specified. For multiple envs, repeat this option, e.g. --env k1=v2 --env k2=v2.", +) +@click.option( + "--reboot/--no-reboot", + is_flag=True, + help="Automatically reboot your site with the new secrets", +) +@click.option("--token", help="The authentication token.") +@click.option( + "--loglevel", + type=click.Choice([level.value for level in constants.LogLevel]), + default=constants.LogLevel.INFO.value, + help="The log level to use.", +) +@click.option( + "--interactive/--no-interactive", + "-i", + is_flag=True, + default=True, + help="Whether to use interactive mode.", +) +def update_secrets( + app_id: str | None, + envfile: str | None, + envs: tuple[str, ...], + reboot: bool, + token: str | None, + loglevel: str, + interactive: bool, +): + """Update secrets for a given application.""" + from reflex_cli.utils import hosting + + console.set_log_level(loglevel) + authenticated_client = hosting.get_authenticated_client( + token=token, interactive=interactive + ) + + if not app_id: + config = hosting.read_config() + if config: + app_id = config.appid + if not isinstance(app_id, (str, type(None))): + console.error( + "app_id must be a string or None. Please check your config file." + ) + raise click.exceptions.Exit(1) + + if not app_id: + console.error("No valid app_id provided.") + raise click.exceptions.Exit(1) + + if envfile is None and not envs: + console.error("--envfile or --env must be provided") + raise click.exceptions.Exit(1) + + if envfile and envs: + console.warn("--envfile is set; ignoring --env") + + secrets = {} + + if envfile: + try: + from dotenv import dotenv_values # pyright: ignore[reportMissingImports] + + secrets = dotenv_values(envfile) + except ImportError: + console.error( + """The `python-dotenv` package is required to load environment variables from a file. Run `pip install "python-dotenv>=1.0.1"`.""" + ) + + else: + secrets = hosting.process_envs(list(envs)) + hosting.update_secrets( + app_id=app_id, secrets=secrets, reboot=reboot, client=authenticated_client + ) + + +@secrets_cli.command(name="delete") +@click.argument("app_id", required=False) +@click.argument("key", required=True) +@click.option("--token", help="The authentication token.") +@click.option( + "--reboot/--no-reboot", + is_flag=True, + help="Automatically reboot your site with the new secrets", +) +@click.option( + "--loglevel", + type=click.Choice([level.value for level in constants.LogLevel]), + default=constants.LogLevel.INFO.value, + help="The log level to use.", +) +@click.option( + "--interactive/--no-interactive", + "-i", + is_flag=True, + default=True, + help="Whether to use interactive mode.", +) +def delete_secret( + app_id: str | None, + key: str, + token: str | None, + reboot: bool, + loglevel: str, + interactive: bool, +): + """Delete a secret for a given application.""" + from reflex_cli.utils import hosting + + console.set_log_level(loglevel) + try: + authenticated_client = hosting.get_authenticated_client( + token=token, interactive=interactive + ) + + if not app_id: + config = hosting.read_config() + if config: + app_id = config.appid + if not isinstance(app_id, (str, type(None))): + console.error( + "app_id must be a string or None. Please check your config file." + ) + raise click.exceptions.Exit(1) + + if not app_id: + console.error("No valid app_id provided.") + raise click.exceptions.Exit(1) + + result = hosting.delete_secret( + app_id=app_id, key=key, reboot=reboot, client=authenticated_client + ) + if "failed" in result: + console.error(result) + raise click.exceptions.Exit(1) + console.success("Successfully deleted secret.") + except NotAuthenticatedError as err: + console.error("You are not authenticated. Run `reflex login` to authenticate.") + raise click.exceptions.Exit(1) from err diff --git a/packages/reflex-hosting-cli/reflex_cli/v2/utils.py b/packages/reflex-hosting-cli/reflex_cli/v2/utils.py new file mode 100644 index 00000000000..6f7aabf665c --- /dev/null +++ b/packages/reflex-hosting-cli/reflex_cli/v2/utils.py @@ -0,0 +1,6 @@ +"""Compat module for reflex 0.6.5 -> 0.6.6.""" + +from reflex_cli.utils import dependency as dependency +from reflex_cli.utils import hosting as hosting + +# Do not add more stuff to this module, put it in reflex_cli.utils instead. diff --git a/packages/reflex-hosting-cli/reflex_cli/v2/vmtypes_regions.py b/packages/reflex-hosting-cli/reflex_cli/v2/vmtypes_regions.py new file mode 100644 index 00000000000..210a6b55493 --- /dev/null +++ b/packages/reflex-hosting-cli/reflex_cli/v2/vmtypes_regions.py @@ -0,0 +1,199 @@ +"""VMTypes and Regions commands for the Reflex Cloud CLI.""" + +import json + +import click + +from reflex_cli import constants +from reflex_cli.utils import console + + +@click.group() +def vm_types_regions_cli(): + """Commands for VM types and regions.""" + pass + + +@vm_types_regions_cli.command("create-token") +@click.argument("name", required=True) +@click.option( + "--duration", + type=click.IntRange(min=1, max=90), + default=90, + help="Duration in days for the token to be valid. Default is 90 days.", +) +@click.option("--token", help="An existing authentication token.") +@click.option( + "--loglevel", + type=click.Choice([level.value for level in constants.LogLevel]), + default=constants.LogLevel.INFO.value, + help="The log level to use.", +) +@click.option( + "--interactive/--no-interactive", + "-i", + is_flag=True, + default=True, + help="Whether to use interactive mode.", +) +def create_token( + name: str, + token: str | None, + interactive: bool, + duration: int, + loglevel: constants.LogLevel = constants.LogLevel.INFO, +): + """Create a new authentication token for the hosting service.""" + from reflex_cli.utils import hosting + + console.set_log_level(loglevel) + authenticated_client = hosting.get_authenticated_client( + token=token, interactive=interactive + ) + + if duration is None: + duration = 90 # Default duration is 90 days + console.info("No duration specified. Using default duration of 90 days.") + + token = hosting.create_token( + name=name, expiration=duration, client=authenticated_client + ) + console.success(f"Token: {token}") + + +@vm_types_regions_cli.command("vmtypes") +@click.option("--token", help="The authentication token.") +@click.option( + "--loglevel", + type=click.Choice([level.value for level in constants.LogLevel]), + default=constants.LogLevel.INFO.value, + help="The log level to use.", +) +@click.option( + "--json/--no-json", + "-j", + "as_json", + is_flag=True, + help="Whether to output the result in json format.", +) +def get_vm_types( + token: str | None, + loglevel: str, + as_json: bool, +): + """Retrieve the available VM types.""" + from reflex_cli.utils import hosting + + console.set_log_level(loglevel) + + vmtypes = hosting.get_vm_types() + if as_json: + console.print(json.dumps(vmtypes)) + return + if vmtypes: + ordered_vmtpes: list[list[str | float]] = [ + [ + value + for key in ["id", "name", "cpu", "ram"] + if (value := vmtype.get(key)) is not None + ] + for vmtype in vmtypes + ] + headers = ["id", "name", "cpu (cores)", "ram (gb)"] + table = [list(map(str, vmtype)) for vmtype in ordered_vmtpes] + console.print_table(table, headers=headers) + else: + console.print(str(vmtypes)) + + +@vm_types_regions_cli.command(name="regions") +@click.option( + "--loglevel", + type=click.Choice([level.value for level in constants.LogLevel]), + default=constants.LogLevel.INFO.value, + help="The log level to use.", +) +@click.option( + "--json/--no-json", + "-j", + "as_json", + is_flag=True, + help="Whether to output the result in json format.", +) +def get_deployment_regions( + loglevel: str, + as_json: bool, +): + """List all the regions of the hosting service. + Areas available for deployment are: + ams Amsterdam, Netherlands + arn Stockholm, Sweden + atl Atlanta, Georgia (US) + bog Bogotá, Colombia + bom Mumbai, India + bos Boston, Massachusetts (US) + cdg Paris, France + den Denver, Colorado (US) + dfw Dallas, Texas (US) + ewr Secaucus, NJ (US) + eze Ezeiza, Argentina + fra Frankfurt, Germany + gdl Guadalajara, Mexico + gig Rio de Janeiro, Brazil + gru Sao Paulo, Brazil + hkg Hong Kong, Hong Kong + iad Ashburn, Virginia (US) + jnb Johannesburg, South Africa + lax Los Angeles, California (US) + lhr London, United Kingdom + mad Madrid, Spain + mia Miami, Florida (US) + nrt Tokyo, Japan + ord Chicago, Illinois (US) + otp Bucharest, Romania + phx Phoenix, Arizona (US) + qro Querétaro, Mexico + scl Santiago, Chile + sea Seattle, Washington (US) + sin Singapore, Singapore + sjc San Jose, California (US) + syd Sydney, Australia + waw Warsaw, Poland + yul Montreal, Canada + yyz Toronto, Canada. + """ + from reflex_cli.utils import hosting + + console.set_log_level(loglevel) + + list_regions_info = hosting.get_regions() + if as_json: + console.print(json.dumps(list_regions_info)) + return + if list_regions_info: + headers = list(list_regions_info[0].keys()) + table = [ + [str(value) if value is not None else "" for value in deployment.values()] + for deployment in list_regions_info + ] + console.print_table(table, headers=headers) + + +@vm_types_regions_cli.command(name="config") +@click.option("--token", help="An existing authentication token.") +@click.option( + "--interactive/--no-interactive", + "-i", + is_flag=True, + default=True, + help="Whether to use interactive mode.", +) +def generate_cloud_config( + token: str | None = None, + interactive: bool = True, +): + """Generate a configuration file for the cloud deployment.""" + from reflex_cli.utils import hosting + + hosting.generate_config(interactive=interactive, token=token) + console.print("Configuration file generated.") diff --git a/packages/reflex-hosting-cli/scripts/check_site_responsive.py b/packages/reflex-hosting-cli/scripts/check_site_responsive.py new file mode 100755 index 00000000000..778a6e56ca1 --- /dev/null +++ b/packages/reflex-hosting-cli/scripts/check_site_responsive.py @@ -0,0 +1,110 @@ +"""Script to check if the backend and frontend of a deployed reflex app are responsive.""" + +from __future__ import annotations + +import argparse +import time +from concurrent.futures import ThreadPoolExecutor, as_completed + + +def _ping_site_with_retries(url: str, retries: int) -> tuple[bool, str]: + import httpx + + print(f"Checking {url} for responsiveness in {retries} retries.") + for i in range(retries): + try: + response = httpx.get(url) + response.raise_for_status() + except httpx.HTTPError: # noqa: PERF203 + time.sleep(1) + continue + else: + return True, f"{url} is responsive after {i} retries" + return False, f"{url} not responsive after {retries} retries." + + +ENV_TO_RUN_DOMAIN = { + "dev": "dev.reflexcorp.run", + "staging": "staging.reflexcorp.run", + "prod": "reflex.run", +} + + +def get_frontend_url(key: str, env: str = "dev") -> str: + """Return the frontend url for a deployment. + + Args: + key: the deployment key + env: dev/staging/prod. + + Returns: + The frontend URL by the given deployment key + + """ + return f"https://{key}.{ENV_TO_RUN_DOMAIN[env]}" + + +ENV_TO_APP_PREFIX = { + "dev": "rxh-dev-", + "staging": "rxh-staging-", + "prod": "rxh-prod-", +} + + +def get_backend_url(key: str, env: str = "dev"): + """Return the expected API URL for a deployment based on its key. + + Args: + key: the deployment key + env: dev/staging/prod + + Returns: + The backend URL by the given deployment key + + """ + return f"https://{ENV_TO_APP_PREFIX[env]}{key}.fly.dev" + + +def main(): + """Wait for ports to start listening.""" + parser = argparse.ArgumentParser( + description="Check the backend and frontend are responsive." + ) + parser.add_argument("--deployment-key", type=str, required=True) + parser.add_argument("--frontend-retries", type=int, default=30) + parser.add_argument("--backend-retries", type=int, default=90) + parser.add_argument("--env", type=str, required=True) + args = parser.parse_args() + + executor = ThreadPoolExecutor(max_workers=2) + futures = [] + print(f"Checking frontend and backend for: {args.deployment_key}") + frontend_url = get_frontend_url(key=args.deployment_key, env=args.env) + backend_url = get_backend_url(key=args.deployment_key, env=args.env) + + futures.append( + executor.submit(_ping_site_with_retries, frontend_url, args.frontend_retries) + ) + futures.append( + executor.submit( + _ping_site_with_retries, + f"{backend_url}/ping", # Note here, we need the ping endpoint instead of `/` + args.backend_retries, + ) + ) + + all_successful = True + for f in as_completed(futures): + ok, msg = f.result() + if ok: + print(f"OK: {msg}") + else: + print(f"FAIL: {msg}") + all_successful = False + + if not all_successful: + exit(1) + + +if __name__ == "__main__": + main() diff --git a/packages/reflex-hosting-cli/scripts/reflex_hosting_cli_next_dev_release_version.py b/packages/reflex-hosting-cli/scripts/reflex_hosting_cli_next_dev_release_version.py new file mode 100644 index 00000000000..6c57700e16f --- /dev/null +++ b/packages/reflex-hosting-cli/scripts/reflex_hosting_cli_next_dev_release_version.py @@ -0,0 +1,103 @@ +"""Script to check the versions of reflex-hosting-cli on PyPI and return the next pre-release version.""" + +import json +import sys + +from packaging import version + +MODULE_NAME = "reflex-hosting-cli" + + +def get_latest_package_version_info( + package_name: str, index_url: str, dev: bool = True +) -> version.Version: + """Check if the latest version of the package on the index. + + Args: + package_name: The name of the package. + index_url: The index URL to check, e.g. https://pypi.org or https://test.pypi.org + dev: Whether to include dev releases only + + Raises: + httpx.HTTPError: If the request fails. + + Returns: + The latest version of the package published on the specified index + + """ + import httpx + + try: + # Get the latest version from specified index + url = f"{index_url}/pypi/{package_name}/json" + response = httpx.get(url) + response.raise_for_status() + response_json = response.json() + if not dev: + # Stable release + return version.parse(response_json["info"]["version"]) + else: + # All the releases including pre-releases + releases = response_json["releases"] + parsed_versions = [version.parse(v) for v in releases] + dev_releases = [v for v in parsed_versions if v.dev is not None] + return max(dev_releases) + except httpx.HTTPError as he: + sys.stderr.write(f"Failed to fetch package info from PyPI: {he}") + raise + except ( + json.JSONDecodeError, + KeyError, + AttributeError, + ValueError, + version.InvalidVersion, + ) as ex: + sys.stderr.write(f"Unexpected response format from PyPI: {ex}") + raise + + +def get_next_dev_release_version( + latest_stable_version: version.Version, latest_dev_version: version.Version +) -> str: + """Get the next dev release version. + + Args: + latest_stable_version: The latest stable version of the package on PyPI. + latest_dev_version: The latest dev version of the package on Test PyPI. + + Raises: + ValueError: if the dev version is ill-formed. + + Returns: + The next dev release version in string. + + """ + if latest_stable_version > latest_dev_version: + # we bump the micro version + return f"{latest_stable_version.major}.{latest_stable_version.minor}.{latest_stable_version.micro + 1}dev0" + else: + dev = latest_dev_version.dev + if dev is None: + raise ValueError( + f"The latest dev version not in expected format: got {latest_dev_version}" + ) + # we bump the dev build version + return f"{latest_dev_version.major}.{latest_dev_version.minor}.{latest_dev_version.micro}dev{dev + 1}" + + +def main(): + """Return the latest version of reflex-hosting-cli on PyPI or Test PyPI.""" + latest_stable_version = get_latest_package_version_info( + MODULE_NAME, "https://pypi.org", dev=False + ) + latest_dev_version = get_latest_package_version_info( + MODULE_NAME, "https://test.pypi.org", dev=True + ) + next_dev_version = get_next_dev_release_version( + latest_stable_version, latest_dev_version + ) + sys.stdout.write(str(next_dev_version)) + + +if __name__ == "__main__": + main() diff --git a/packages/reflex-hosting-cli/tests/__init__.py b/packages/reflex-hosting-cli/tests/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/reflex-hosting-cli/tests/utils/__init__.py b/packages/reflex-hosting-cli/tests/utils/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/reflex-hosting-cli/tests/utils/test_dependency.py b/packages/reflex-hosting-cli/tests/utils/test_dependency.py new file mode 100644 index 00000000000..175860d4be0 --- /dev/null +++ b/packages/reflex-hosting-cli/tests/utils/test_dependency.py @@ -0,0 +1,37 @@ +from pathlib import Path + +import pytest +from pytest_mock import MockFixture + +from reflex_cli.utils.dependency import detect_encoding, is_valid_url + + +def test_detect_encoding_file_not_found(mocker: MockFixture): + filename = "non_existent_file.txt" + + mocker.patch("pathlib.Path.exists", return_value=False) + + with pytest.raises(FileNotFoundError): + detect_encoding(Path(filename)) + + +@pytest.mark.parametrize( + "url,expected", + [ + ("https://www.example.com", True), + ("http://example.com", True), + ("https://subdomain.example.com", True), + ("http://example.com:8080", True), + ("https://example.com/path?query=1&lang=en", True), + ("https://example.com/#fragment", True), + ("invalid-url", False), + ("www.example.com", False), + ("http://", False), + ("", False), + (None, False), + ("https://", False), + ("ftp://", False), + ], +) +def test_is_valid_url(url: str, expected: bool): + assert is_valid_url(url) == expected diff --git a/packages/reflex-hosting-cli/tests/utils/test_hosting.py b/packages/reflex-hosting-cli/tests/utils/test_hosting.py new file mode 100644 index 00000000000..c51c8e7bd28 --- /dev/null +++ b/packages/reflex-hosting-cli/tests/utils/test_hosting.py @@ -0,0 +1,170 @@ +from __future__ import annotations + +import json +from unittest.mock import mock_open + +import click +import pytest +from pytest_mock import MockerFixture, MockFixture + +from reflex_cli.utils.hosting import ( + authenticated_token, + delete_token_from_config, + get_authenticated_client, + get_existing_access_token, + save_token_to_config, +) + + +@pytest.mark.parametrize( + "config_content, expected_token", + [ + ('{"access_token": "valid_token"}', "valid_token"), + ("{}", ""), + (None, ""), + ], +) +def test_get_existing_access_token( + mocker: MockerFixture, config_content: str | None, expected_token: str +): + mocker.patch("os.environ.get", return_value="") + mocker.patch("pathlib.Path.open", mock_open(read_data=config_content)) + assert get_existing_access_token() == expected_token + + mocker.patch("pathlib.Path.open", side_effect=FileNotFoundError("Test exception")) + assert get_existing_access_token() == "" + + +@pytest.mark.parametrize( + "file_exists, config_content, expected_deleted", + [ + (True, '{"access_token": "valid_token"}', True), + (True, '{"another_key": "value"}', False), + (False, "", False), + ], +) +def test_delete_token_from_config( + mocker: MockerFixture, + file_exists: bool, + config_content: str, + expected_deleted: bool, +): + mocker.patch("pathlib.Path.exists", return_value=file_exists) + mock_os_remove = mocker.patch("pathlib.Path.unlink") + + mocked_open = mock_open(read_data=config_content) + mocker.patch("pathlib.Path.open", mocked_open) + mock_json_load = mocker.patch( + "json.load", return_value=json.loads(config_content or "{}") + ) + mock_json_dump = mocker.patch("json.dump") + + delete_token_from_config() + + if file_exists: + mocked_open.assert_called_once() + if expected_deleted: + mock_json_load.assert_called_once() + mock_json_dump.assert_called_once() + else: + mock_json_dump.assert_not_called() + mock_os_remove.assert_called_once() + else: + mocked_open.assert_not_called() + mock_os_remove.assert_not_called() + + +def test_save_token_to_config(mocker: MockFixture): + mocker.patch("pathlib.Path.exists", return_value=False) + mock_makedirs = mocker.patch("pathlib.Path.mkdir") + save_token_to_config("test_token") + mock_makedirs.assert_called_once() + + mocker.patch("pathlib.Path.exists", return_value=True) + mock_json_dump = mocker.patch("json.dump") + mocker.patch("pathlib.Path.open", mock_open()) + save_token_to_config("test_token") + mock_json_dump.assert_called_once() + + +def test_authenticated_token_found_and_valid(mocker: MockFixture): + mocker.patch( + "reflex_cli.utils.hosting.get_existing_access_token", + return_value="valid_token", + ) + mocker.patch( + "reflex_cli.utils.hosting.validate_token", return_value={"user_info": True} + ) + + token = authenticated_token() + + assert token == ("valid_token", {"user_info": True}) + + +def test_authenticated_token_not_found(mocker: MockFixture): + mocker.patch("reflex_cli.utils.hosting.get_existing_access_token", return_value="") + + token = authenticated_token() + assert token == ("", {}) + + +def test_authenticated_token_found_but_invalid(mocker: MockFixture): + mocker.patch( + "reflex_cli.utils.hosting.get_existing_access_token", + return_value="invalid_token", + ) + mocker.patch( + "reflex_cli.utils.hosting.validate_token", + side_effect=ValueError("access denied"), + ) + mocker.patch( + "reflex_cli.constants.hosting.Hosting.AUTH_RETRY_LIMIT", return_value=1 + ) + + token = authenticated_token() + assert token == ("", {}) + + +def test_authenticated_token_found_but_validation_fails(mocker: MockFixture): + mocker.patch( + "reflex_cli.utils.hosting.get_existing_access_token", + return_value="invalid_token", + ) + mocker.patch( + "reflex_cli.utils.hosting.validate_token", + side_effect=ValueError("server error"), + ) + mocker.patch( + "reflex_cli.utils.hosting.authenticate_on_browser", + return_value="new_valid_token", + ) + mock_delete_token = mocker.patch( + "reflex_cli.utils.hosting.delete_token_from_config" + ) + + token = authenticated_token() + + assert token == ("", {}) + mock_delete_token.assert_called_once() + + +def test_authenticate_without_token_in_non_interactive_mode(mocker: MockerFixture): + mocker.patch("reflex_cli.utils.hosting.get_existing_access_token", return_value="") + with pytest.raises(click.exceptions.Exit): + get_authenticated_client(token=None, interactive=False) + + +def test_authenticate_with_env_token_in_non_interactive_mode(mocker: MockerFixture): + mocker.patch( + "reflex_cli.utils.hosting.get_existing_access_token", return_value="env_token" + ) + mock_get_auth_client = mocker.patch( + "reflex_cli.utils.hosting.get_authentication_client" + ) + mock_authenticated_client = mocker.MagicMock() + mock_get_auth_client.return_value = mock_authenticated_client + + result = get_authenticated_client(token=None, interactive=False) + + assert result == mock_authenticated_client + mock_get_auth_client.assert_called_once_with(None) diff --git a/packages/reflex-hosting-cli/tests/v2/__init__.py b/packages/reflex-hosting-cli/tests/v2/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/reflex-hosting-cli/tests/v2/test_apps.py b/packages/reflex-hosting-cli/tests/v2/test_apps.py new file mode 100644 index 00000000000..ee31ad22449 --- /dev/null +++ b/packages/reflex-hosting-cli/tests/v2/test_apps.py @@ -0,0 +1,1519 @@ +from __future__ import annotations + +import json +from unittest import mock + +import httpx +import pytest +from click.testing import CliRunner +from pytest_mock import MockerFixture, MockFixture +from typer.main import Typer, get_command + +from reflex_cli.core.config import Config +from reflex_cli.utils import hosting +from reflex_cli.utils.exceptions import GetAppError +from reflex_cli.v2.deployments import hosting_cli + +hosting_cli = ( + get_command(hosting_cli) if isinstance(hosting_cli, Typer) else hosting_cli +) + +runner = CliRunner() + + +def test_app_history_success(mocker: MockFixture): + """Test retrieving deployment history successfully.""" + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_get_app_history = mocker.patch( + "reflex_cli.utils.hosting.get_app_history", + return_value=[ + { + "id": "deployment1", + "status": "success", + "hostname": "example.com", + "python version": "3.10", + "reflex version": "1.2.3", + "vm type": "small", + "timestamp": "2024-11-29T12:00:00Z", + }, + { + "id": "deployment2", + "status": "failure", + "hostname": "example.org", + "python version": "3.11", + "reflex version": "1.1.0", + "vm type": "medium", + "timestamp": "2024-11-28T10:00:00Z", + }, + ], + ) + mock_console_print_table = mocker.patch("reflex_cli.utils.console.print_table") + + result = runner.invoke(hosting_cli, ["apps", "history", "test_app_id"]) + + assert result.exit_code == 0, result.output + mock_get_app_history.assert_called_once_with( + app_id="test_app_id", + client=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_console_print_table.assert_called_once() + + +def test_app_history_as_json(mocker: MockFixture): + """Test retrieving deployment history with JSON output.""" + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_get_app_history = mocker.patch( + "reflex_cli.utils.hosting.get_app_history", + return_value=[ + { + "id": "deployment1", + "status": "success", + "hostname": "example.com", + "python version": "3.10", + "reflex version": "1.2.3", + "vm type": "small", + "timestamp": "2024-11-29T12:00:00Z", + } + ], + ) + mock_console_print = mocker.patch("reflex_cli.utils.console.print") + + result = runner.invoke( + hosting_cli, + ["apps", "history", "test_app_id", "--json"], + ) + + assert result.exit_code == 0, result.output + mock_get_app_history.assert_called_once_with( + app_id="test_app_id", + client=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_console_print.assert_called_once_with( + json.dumps( + [ + { + "id": "deployment1", + "status": "success", + "hostname": "example.com", + "python version": "3.10", + "reflex version": "1.2.3", + "vm type": "small", + "timestamp": "2024-11-29T12:00:00Z", + } + ] + ) + ) + + +def test_app_history_no_deployments(mocker: MockFixture): + """Test retrieving deployment history when there are no deployments.""" + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_get_app_history = mocker.patch( + "reflex_cli.utils.hosting.get_app_history", + return_value=[], + ) + mock_console_print = mocker.patch("reflex_cli.utils.console.print") + + result = runner.invoke(hosting_cli, ["apps", "history", "test_app_id"]) + + assert result.exit_code == 0, result.output + mock_get_app_history.assert_called_once_with( + app_id="test_app_id", + client=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_console_print.assert_called_once_with("[]") + + +def test_app_history_http_error(mocker: MockFixture): + """Test retrieving deployment history when an HTTP error occurs.""" + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_get_app_history = mocker.patch( + "reflex_cli.utils.hosting.get_app_history", + side_effect=Exception("HTTP request failed"), + ) + mocker.patch("reflex_cli.utils.console.error") + + result = runner.invoke(hosting_cli, ["apps", "history", "test_app_id"]) + + assert result.exit_code == 1 + mock_get_app_history.assert_called_once_with( + app_id="test_app_id", + client=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + + +def test_deployment_build_logs_success(mocker: MockFixture): + """Test successful retrieval of build logs.""" + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_get_deployment_build_logs = mocker.patch( + "reflex_cli.utils.hosting.get_deployment_build_logs", + return_value={"log": "Build completed successfully."}, + ) + mock_console_print = mocker.patch("reflex_cli.utils.console.print") + + result = runner.invoke(hosting_cli, ["apps", "build-logs", "test_deployment_id"]) + + assert result.exit_code == 0, result.output + mock_get_deployment_build_logs.assert_called_once_with( + deployment_id="test_deployment_id", + client=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_console_print.assert_called_once_with({"log": "Build completed successfully."}) + + +def test_deployment_build_logs_with_token(mocker: MockFixture): + """Test retrieval of build logs with a provided token.""" + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_get_deployment_build_logs = mocker.patch( + "reflex_cli.utils.hosting.get_deployment_build_logs", + return_value={"log": "Build completed successfully."}, + ) + mock_console_print = mocker.patch("reflex_cli.utils.console.print") + + result = runner.invoke( + hosting_cli, + ["apps", "build-logs", "test_deployment_id", "--token", "fake-token"], + ) + + assert result.exit_code == 0, result.output + mock_get_deployment_build_logs.assert_called_once_with( + deployment_id="test_deployment_id", + client=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_console_print.assert_called_once_with({"log": "Build completed successfully."}) + + +def test_deployment_build_logs_not_authenticated(mocker: MockFixture): + """Test retrieval of build logs when not authenticated.""" + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_get_deployment_build_logs = mocker.patch( + "reflex_cli.utils.hosting.get_deployment_build_logs", + side_effect=Exception("not authenticated"), + ) + mock_console_print = mocker.patch("reflex_cli.utils.console.print") + + result = runner.invoke(hosting_cli, ["apps", "build-logs", "test_deployment_id"]) + + assert result.exit_code == 1 # Command should fail due to exception + mock_get_deployment_build_logs.assert_called_once_with( + deployment_id="test_deployment_id", + client=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_console_print.assert_not_called() + + +def test_deployment_build_logs_http_error(mocker: MockFixture): + """Test retrieval of build logs when an HTTP error occurs.""" + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_get_deployment_build_logs = mocker.patch( + "reflex_cli.utils.hosting.get_deployment_build_logs", + side_effect=Exception("HTTP error: bad response from server"), + ) + mock_console_print = mocker.patch("reflex_cli.utils.console.print") + + result = runner.invoke(hosting_cli, ["apps", "build-logs", "test_deployment_id"]) + + assert result.exit_code == 1 + mock_get_deployment_build_logs.assert_called_once_with( + deployment_id="test_deployment_id", + client=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_console_print.assert_not_called() + + +def test_deployment_status_success(mocker: MockFixture): + """Test successful retrieval of a deployment's status.""" + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_get_status = mocker.patch( + "reflex_cli.utils.hosting.get_deployment_status", + return_value="Deployment is running smoothly.", + ) + mock_print = mocker.patch("reflex_cli.utils.console.print") + + result = runner.invoke(hosting_cli, ["apps", "status", "12345"]) + + assert result.exit_code == 0, result.output + mock_get_status.assert_called_once_with( + deployment_id="12345", + client=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_print.assert_called_once_with("Deployment is running smoothly.") + + +def test_deployment_status_watch_success(mocker: MockFixture): + """Test continuous status watching for a deployment.""" + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_watch_status = mocker.patch( + "reflex_cli.utils.hosting.watch_deployment_status", + return_value=None, + ) + + result = runner.invoke(hosting_cli, ["apps", "status", "12345", "--watch"]) + + assert result.exit_code == 0, result.output + mock_watch_status.assert_called_once_with( + deployment_id="12345", + client=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + + +def test_deployment_status_http_error(mocker: MockFixture): + """Test HTTP error during status retrieval.""" + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_get = mocker.patch("httpx.get") + mock_response = mocker.Mock() + mock_response.raise_for_status.side_effect = httpx.HTTPStatusError( + "HTTP Error", + request=mocker.Mock(), + response=mocker.Mock(json=lambda: {"detail": "Invalid token"}), + ) + mock_get.return_value = mock_response + mock_error = mocker.patch("reflex_cli.utils.console.error") + mocker.patch( + "reflex_cli.utils.hosting.requires_authenticated", return_value="fake_token" + ) + mocker.patch("reflex_cli.utils.hosting.get_app", return_value={"id": "fake_app_id"}) + mocker.patch( + "reflex_cli.utils.hosting.authorization_header", + return_value={"X-API-TOKEN": "fake_token"}, + ) + + result = runner.invoke(hosting_cli, ["apps", "status", "12345"]) + + assert result.exit_code == 0, result.output + mock_error.assert_called_once_with("get status failed: Invalid token") + + +def test_stop_app_success(mocker: MockFixture): + """Test successful stopping of an app.""" + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_stop_app = mocker.patch( + "reflex_cli.utils.hosting.stop_app", + return_value="App stopped successfully", + ) + mock_success = mocker.patch("reflex_cli.utils.console.success") + + result = runner.invoke(hosting_cli, ["apps", "stop", "app123"]) + + assert result.exit_code == 0, result.output + mock_stop_app.assert_called_once_with( + app_id="app123", + client=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_success.assert_called_once_with("App stopped successfully") + + +def test_stop_app_failure(mocker: MockFixture): + """Test failure during app stop operation.""" + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_stop_app = mocker.patch( + "reflex_cli.utils.hosting.stop_app", + return_value="stop app failed: Unable to stop app due to server error", + ) + mock_error = mocker.patch("reflex_cli.utils.console.error") + + result = runner.invoke(hosting_cli, ["apps", "stop", "app123"]) + + assert result.exit_code == 0, result.output + mock_stop_app.assert_called_once_with( + app_id="app123", + client=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_error.assert_called_once_with( + "stop app failed: Unable to stop app due to server error" + ) + + +def test_stop_app_http_error(mocker: MockFixture): + """Test HTTP error during app stop operation.""" + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_post = mocker.patch("httpx.post") + mock_response = mocker.Mock() + mock_response.raise_for_status.side_effect = httpx.HTTPStatusError( + "HTTP Error", + request=mocker.Mock(), + response=mocker.Mock(json=lambda: {"detail": "Invalid token"}), + ) + mock_post.return_value = mock_response + mock_error = mocker.patch("reflex_cli.utils.console.error") + mocker.patch( + "reflex_cli.utils.hosting.requires_authenticated", return_value="fake_token" + ) + mocker.patch("reflex_cli.utils.hosting.get_app", return_value={"id": "fake_app_id"}) + mocker.patch( + "reflex_cli.utils.hosting.authorization_header", + return_value={"X-API-TOKEN": "fake_token"}, + ) + + result = runner.invoke(hosting_cli, ["apps", "stop", "app123"]) + + assert result.exit_code == 0, result.output + mock_error.assert_called_once_with("stop app failed: Invalid token") + + +def test_start_app_success(mocker: MockFixture): + """Test successful start of an app.""" + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_start_app = mocker.patch( + "reflex_cli.utils.hosting.start_app", + return_value={"status": "success", "message": "App started successfully"}, + ) + mock_success = mocker.patch("reflex_cli.utils.console.success") + + result = runner.invoke(hosting_cli, ["apps", "start", "app123"]) + + assert result.exit_code == 0, result.output + mock_start_app.assert_called_once_with( + app_id="app123", + client=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_success.assert_called_once_with( + {"status": "success", "message": "App started successfully"} + ) + + +def test_start_app_failure(mocker: MockFixture): + """Test failure during app start operation.""" + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_start_app = mocker.patch( + "reflex_cli.utils.hosting.start_app", + return_value="start app failed: Unable to start app due to server error", + ) + mock_error = mocker.patch("reflex_cli.utils.console.error") + + result = runner.invoke(hosting_cli, ["apps", "start", "app123"]) + + assert result.exit_code == 0, result.output + mock_start_app.assert_called_once_with( + app_id="app123", + client=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_error.assert_called_once_with( + "start app failed: Unable to start app due to server error" + ) + + +def test_start_app_http_error(mocker: MockFixture): + """Test HTTP error during app start operation.""" + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_post = mocker.patch("httpx.post") + mock_response = mocker.Mock() + mock_response.raise_for_status.side_effect = httpx.HTTPStatusError( + "HTTP Error", + request=mocker.Mock(), + response=mocker.Mock(json=lambda: {"detail": "Invalid token"}), + ) + mock_post.return_value = mock_response + mock_error = mocker.patch("reflex_cli.utils.console.error") + mocker.patch( + "reflex_cli.utils.hosting.requires_authenticated", return_value="fake_token" + ) + mocker.patch("reflex_cli.utils.hosting.get_app", return_value={"id": "fake_app_id"}) + mocker.patch( + "reflex_cli.utils.hosting.authorization_header", + return_value={"X-API-TOKEN": "fake_token"}, + ) + + result = runner.invoke(hosting_cli, ["apps", "start", "app123"]) + + assert result.exit_code == 0, result.output + mock_error.assert_called_once_with("start app failed: Invalid token") + + +def test_delete_app_success(mocker: MockFixture): + """Test successful deletion of an app with confirmation.""" + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_delete_app = mocker.patch( + "reflex_cli.utils.hosting.delete_app", + return_value={"status": "success", "message": "App deleted successfully"}, + ) + mock_get_app = mocker.patch( + "reflex_cli.utils.hosting.get_app", + return_value={"id": "app123", "name": "test-app"}, + ) + mock_warn = mocker.patch("reflex_cli.utils.console.warn") + mock_ask = mocker.patch("reflex_cli.utils.console.ask", return_value="y") + + result = runner.invoke(hosting_cli, ["apps", "delete", "app123"]) + + assert result.exit_code == 0, result.output + assert mock_get_app.call_count == 2 + mock_get_app.assert_has_calls( + [ + mock.call( + client=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + app_id="app123", + ), + mock.call( + app_id="app123", + client=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ), + ], + any_order=True, + ) + mock_ask.assert_called_once_with( + "Are you sure you want to delete app 'test-app' (ID: app123)?", + choices=["y", "n"], + default="n", + ) + mock_delete_app.assert_called_once_with( + app_id="app123", + client=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_warn.assert_called_once_with( + {"status": "success", "message": "App deleted successfully"} + ) + + +def test_delete_app_failure(mocker: MockFixture): + """Test failure during app deletion.""" + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_delete_app = mocker.patch( + "reflex_cli.utils.hosting.delete_app", + return_value="delete app failed: Unable to delete app due to server error", + ) + mock_get_app = mocker.patch( + "reflex_cli.utils.hosting.get_app", + return_value={"id": "app123", "name": "test-app"}, + ) + mock_warn = mocker.patch("reflex_cli.utils.console.warn") + mock_ask = mocker.patch("reflex_cli.utils.console.ask", return_value="y") + + result = runner.invoke(hosting_cli, ["apps", "delete", "app123"]) + + assert result.exit_code == 0, result.output + assert mock_get_app.call_count == 2 + mock_get_app.assert_has_calls( + [ + mock.call( + client=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + app_id="app123", + ), + mock.call( + app_id="app123", + client=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ), + ], + any_order=True, + ) + mock_ask.assert_called_once_with( + "Are you sure you want to delete app 'test-app' (ID: app123)?", + choices=["y", "n"], + default="n", + ) + mock_delete_app.assert_called_once_with( + app_id="app123", + client=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_warn.assert_called_once_with( + "delete app failed: Unable to delete app due to server error" + ) + + +def test_delete_app_no_app_id(mocker: MockFixture): + """Test case when no app_id is provided.""" + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_error = mocker.patch("reflex_cli.utils.console.error") + result = runner.invoke(hosting_cli, ["apps", "delete", ""]) + + assert result.exit_code == 1 + mock_error.assert_called_once_with("No valid app_id or app_name provided.") + + +def test_delete_app_http_error(mocker: MockFixture): + """Test HTTP error during app deletion.""" + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_delete = mocker.patch("httpx.delete") + mock_response = mocker.Mock() + mock_response.raise_for_status.side_effect = httpx.HTTPStatusError( + "HTTP Error", + request=mocker.Mock(), + response=mocker.Mock(json=lambda: {"detail": "Invalid token"}), + ) + mock_delete.return_value = mock_response + + mock_get_app = mocker.patch( + "reflex_cli.utils.hosting.get_app", + return_value={"id": "app123", "name": "test-app"}, + ) + mock_warn = mocker.patch("reflex_cli.utils.console.warn") + mock_ask = mocker.patch("reflex_cli.utils.console.ask", return_value="y") + mocker.patch( + "reflex_cli.utils.hosting.requires_authenticated", return_value="fake_token" + ) + mocker.patch( + "reflex_cli.utils.hosting.authorization_header", + return_value={"X-API-TOKEN": "fake_token"}, + ) + + result = runner.invoke(hosting_cli, ["apps", "delete", "app123"]) + + assert result.exit_code == 0, result.output + assert mock_get_app.call_count >= 1 + mock_ask.assert_called_once_with( + "Are you sure you want to delete app 'test-app' (ID: app123)?", + choices=["y", "n"], + default="n", + ) + mock_warn.assert_called_once_with("delete app failed: Invalid token") + + +def test_delete_app_confirmation_cancelled(mocker: MockFixture): + """Test deletion cancelled when user responds 'n' to confirmation.""" + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_delete_app = mocker.patch("reflex_cli.utils.hosting.delete_app") + mock_get_app = mocker.patch( + "reflex_cli.utils.hosting.get_app", + return_value={"id": "app123", "name": "test-app"}, + ) + mock_ask = mocker.patch("reflex_cli.utils.console.ask", return_value="n") + mock_info = mocker.patch("reflex_cli.utils.console.info") + + result = runner.invoke(hosting_cli, ["apps", "delete", "app123"]) + + assert result.exit_code == 0, result.output + assert mock_get_app.call_count == 2 + mock_get_app.assert_has_calls( + [ + mock.call( + client=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + app_id="app123", + ), + mock.call( + app_id="app123", + client=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ), + ], + any_order=True, + ) + mock_ask.assert_called_once_with( + "Are you sure you want to delete app 'test-app' (ID: app123)?", + choices=["y", "n"], + default="n", + ) + mock_info.assert_called_once_with("Deletion cancelled.") + mock_delete_app.assert_not_called() + + +def test_delete_app_non_interactive_skips_confirmation(mocker: MockFixture): + """Test deletion proceeds without confirmation when --no-interactive flag is used.""" + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_delete_app = mocker.patch( + "reflex_cli.utils.hosting.delete_app", + return_value={"status": "success", "message": "App deleted successfully"}, + ) + mock_get_app = mocker.patch("reflex_cli.utils.hosting.get_app") + mock_warn = mocker.patch("reflex_cli.utils.console.warn") + mock_ask = mocker.patch("reflex_cli.utils.console.ask") + + result = runner.invoke( + hosting_cli, ["apps", "delete", "app123", "--no-interactive"] + ) + + assert result.exit_code == 0, result.output + mock_ask.assert_not_called() + assert mock_get_app.call_count == 1 + mock_delete_app.assert_called_once_with( + app_id="app123", + client=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_warn.assert_called_once_with( + {"status": "success", "message": "App deleted successfully"} + ) + + +def test_delete_app_get_app_fails_fallback_to_unknown(mocker: MockFixture): + """Test deletion shows 'Unknown' when get_app fails.""" + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_delete_app = mocker.patch( + "reflex_cli.utils.hosting.delete_app", + return_value={"status": "success", "message": "App deleted successfully"}, + ) + mock_get_app = mocker.patch( + "reflex_cli.utils.hosting.get_app", + side_effect=[ + GetAppError("Failed to fetch app"), + {"id": "app123", "name": "Unknown"}, + ], + ) + mock_warn = mocker.patch("reflex_cli.utils.console.warn") + mock_ask = mocker.patch("reflex_cli.utils.console.ask", return_value="y") + + result = runner.invoke(hosting_cli, ["apps", "delete", "app123"]) + + assert result.exit_code == 0, result.output + assert mock_get_app.call_count == 1 + mock_ask.assert_not_called() + mock_delete_app.assert_not_called() + mock_warn.assert_called_once_with("No application found with ID 'app123'") + + +def test_delete_app_with_app_name_confirmation(mocker: MockFixture): + """Test deletion with app name shows proper app name in confirmation.""" + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_search_app = mocker.patch( + "reflex_cli.utils.hosting.search_app", + return_value={"id": "app123", "name": "my-test-app"}, + ) + mock_delete_app = mocker.patch( + "reflex_cli.utils.hosting.delete_app", + return_value={"status": "success", "message": "App deleted successfully"}, + ) + mock_warn = mocker.patch("reflex_cli.utils.console.warn") + mock_ask = mocker.patch("reflex_cli.utils.console.ask", return_value="y") + + result = runner.invoke(hosting_cli, ["apps", "delete", "--app-name", "my-test-app"]) + + assert result.exit_code == 0, result.output + mock_search_app.assert_called_once() + mock_ask.assert_called_once_with( + "Are you sure you want to delete app 'my-test-app' (ID: app123)?", + choices=["y", "n"], + default="n", + ) + mock_delete_app.assert_called_once_with( + app_id="app123", + client=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_warn.assert_called_once_with( + {"status": "success", "message": "App deleted successfully"} + ) + + +def test_delete_app_not_found_early_exit(mocker: MockFixture): + """Test early exit with warning when app is not found during search.""" + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_search_app = mocker.patch( + "reflex_cli.utils.hosting.search_app", + return_value=None, + ) + mock_warn = mocker.patch("reflex_cli.utils.console.warn") + mock_delete_app = mocker.patch("reflex_cli.utils.hosting.delete_app") + mock_ask = mocker.patch("reflex_cli.utils.console.ask") + + result = runner.invoke( + hosting_cli, ["apps", "delete", "--app-name", "nonexistent-app"] + ) + + assert result.exit_code == 1, result.output + mock_search_app.assert_called_once() + mock_warn.assert_called_once_with("App 'nonexistent-app' not found.") + mock_ask.assert_not_called() + mock_delete_app.assert_not_called() + + +def test_app_logs_no_app_id(mocker: MockFixture): + """Test case when no app_id is provided.""" + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_error = mocker.patch("reflex_cli.utils.console.error") + result = runner.invoke(hosting_cli, ["apps", "logs", ""]) + + assert result.exit_code == 1 + mock_error.assert_called_once_with("No valid app_id or app_name provided.") + + +def test_app_logs_invalid_time_range(mocker: MockFixture): + """Test case when offset is provided without start and end.""" + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_error = mocker.patch("reflex_cli.utils.console.error") + result = runner.invoke( + hosting_cli, + [ + "apps", + "logs", + "app123", + "--start", + "423453423", + ], + ) + + assert result.exit_code == 1 + mock_error.assert_called_once_with("must provide both start and end") + + +def test_app_logs_success(mocker: MockFixture): + """Test case for successful log retrieval.""" + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_get_app_logs = mocker.patch( + "reflex_cli.utils.hosting.get_app_logs", + return_value=["log1", "log2", "log3"], + ) + mock_info = mocker.patch("reflex_cli.utils.console.info") + + result = runner.invoke(hosting_cli, ["apps", "logs", "app123", "--follow", "false"]) + + assert result.exit_code == 0, result.output + mock_get_app_logs.assert_called_once_with( + app_id="app123", + offset=3600, + start=None, + end=None, + client=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + cursor=None, + ) + mock_info.assert_any_call("log3") + mock_info.assert_any_call("log2") + mock_info.assert_any_call("log1") + + +def test_app_logs_failure(mocker: MockFixture): + """Test case when log retrieval fails.""" + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_get_app_logs = mocker.patch( + "reflex_cli.utils.hosting.get_app_logs", + return_value="get app logs failed: Unable to retrieve logs", + ) + mock_warn = mocker.patch("reflex_cli.utils.console.warn") + + result = runner.invoke(hosting_cli, ["apps", "logs", "app123", "--follow", "false"]) + + assert result.exit_code == 0, result.output + mock_get_app_logs.assert_called_once_with( + app_id="app123", + offset=3600, + start=None, + end=None, + client=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + cursor=None, + ) + mock_warn.assert_called_once_with("Unable to retrieve logs.") + + +def test_app_logs_http_error(mocker: MockFixture): + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_get = mocker.patch("httpx.get") + mock_response = mocker.Mock() + mock_response.raise_for_status.side_effect = httpx.HTTPStatusError( + "HTTP Error", + request=mocker.Mock(), + response=mocker.Mock(json=lambda: {"detail": "Invalid token"}), + ) + mock_get.return_value = mock_response + mock_warn = mocker.patch("reflex_cli.v2.deployments.console.warn") + mocker.patch( + "reflex_cli.utils.hosting.requires_authenticated", return_value="fake_token" + ) + mocker.patch("reflex_cli.utils.hosting.get_app", return_value={"id": "fake_app_id"}) + mocker.patch( + "reflex_cli.utils.hosting.authorization_header", + return_value={"X-API-TOKEN": "fake_token"}, + ) + + result = runner.invoke( + hosting_cli, + ["apps", "logs", "fake_app_id", "--token", "fake_token", "--follow", "false"], + ) + + assert result.exit_code == 0, result.output + mock_warn.assert_called_once_with("Unable to retrieve logs.") + + +def test_list_apps_no_project(mocker: MockFixture): + """Test case when no project is provided.""" + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_get_selected_project = mocker.patch( + "reflex_cli.utils.hosting.get_selected_project", + return_value="default_project", + ) + mock_list_apps = mocker.patch( + "reflex_cli.utils.hosting.list_apps", + return_value=[{"id": "1", "name": "App1"}, {"id": "2", "name": "App2"}], + ) + mock_print_table = mocker.patch("reflex_cli.utils.console.print_table") + + result = runner.invoke(hosting_cli, ["apps", "list"]) + + assert result.exit_code == 0, result.output + mock_get_selected_project.assert_called_once() + mock_list_apps.assert_called_once_with( + project="default_project", + client=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_print_table.assert_called_once_with( + [["1", "App1"], ["2", "App2"]], + headers=["id", "name"], + ) + + +def test_list_apps_with_project(mocker: MockFixture): + """Test case when a project is provided.""" + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_list_apps = mocker.patch( + "reflex_cli.utils.hosting.list_apps", + return_value=[{"id": "1", "name": "App1"}], + ) + mock_print_table = mocker.patch("reflex_cli.utils.console.print_table") + + result = runner.invoke(hosting_cli, ["apps", "list", "--project", "project123"]) + + assert result.exit_code == 0, result.output + mock_list_apps.assert_called_once_with( + project="project123", + client=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_print_table.assert_called_once_with( + [["1", "App1"]], + headers=["id", "name"], + ) + + +def test_list_apps_json_output(mocker: MockFixture): + """Test case for JSON output.""" + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_list_apps = mocker.patch( + "reflex_cli.utils.hosting.list_apps", + return_value=[{"id": "1", "name": "App1"}], + ) + mock_print = mocker.patch("reflex_cli.utils.console.print") + + result = runner.invoke(hosting_cli, ["apps", "list", "--json"]) + + assert result.exit_code == 0, result.output + mock_list_apps.assert_called_once_with( + project=None, + client=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_print.assert_called_once_with(json.dumps([{"id": "1", "name": "App1"}])) + + +def test_list_apps_error(mocker: MockFixture): + """Test case when an error occurs while listing deployments.""" + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_list_apps = mocker.patch( + "reflex_cli.utils.hosting.list_apps", + side_effect=Exception("Unable to list deployments"), + ) + mock_error = mocker.patch("reflex_cli.utils.console.error") + + result = runner.invoke(hosting_cli, ["apps", "list"]) + + assert result.exit_code == 1 + mock_list_apps.assert_called_once_with( + project=None, + client=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_error.assert_called_once_with("Unable to list deployments") + + +def test_list_apps_empty_response(mocker: MockFixture): + """Test case when no deployments are found.""" + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_list_apps = mocker.patch("reflex_cli.utils.hosting.list_apps", return_value=[]) + mock_print = mocker.patch("reflex_cli.utils.console.print") + + result = runner.invoke(hosting_cli, ["apps", "list"]) + + assert result.exit_code == 0, result.output + mock_list_apps.assert_called_once_with( + project=None, + client=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_print.assert_called_once_with("[]") + + +def test_scale_no_args_or_config(mocker: MockFixture): + """Test error when neither args nor config file exists.""" + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mocker.patch( + "reflex_cli.core.config.Config.from_yaml_or_toml_or_default", + return_value=Config(), + ) + mocker.patch("reflex_cli.core.config.Config.exists", return_value=False) + mock_error = mocker.patch("reflex_cli.utils.console.error") + + result = runner.invoke(hosting_cli, ["apps", "scale", "--app-name", "random"]) + + assert result.exit_code == 1 + mock_error.assert_called_with( + "specify either --vmtype or --regions or add them to the cloud.yml or pyproject.toml file" + ) + + +def test_scale_both_vmtype_and_regions(mocker: MockFixture): + """Test error when both --vmtype and --regions are provided.""" + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_error = mocker.patch("reflex_cli.utils.console.error") + + result = runner.invoke( + hosting_cli, ["apps", "scale", "--vmtype", "c1m1", "--regions", "sjc"] + ) + + assert result.exit_code == 1 + mock_error.assert_called_with( + "Only one of --vmtype or --regions should be provided." + ) + + +def test_scale_args_override_config(mocker: MockFixture): + """Test warning when both args and config are provided.""" + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mocker.patch( + "reflex_cli.utils.hosting.search_app", + return_value={ + "name": "fake-app", + "id": "fake-id", + "project_id": "fake-project", + }, + ) + mocker.patch( + "reflex_cli.utils.hosting.scale_app", + ) + mocker.patch( + "reflex_cli.utils.hosting.ScaleParams.from_config", + return_value=hosting.ScaleParams( + type=hosting.ScaleType(hosting.ScaleType.SIZE), vm_type="c1m1" + ), + ) + mocker.patch( + "reflex_cli.core.config.Config.from_yaml_or_toml_or_default", + return_value=Config(regions={"ams": 1}, vmtype="c1m2"), + ) + mocker.patch("reflex_cli.core.config.Config.exists", return_value=True) + mock_warn = mocker.patch("reflex_cli.v2.deployments.console.warn") + + result = runner.invoke( + hosting_cli, ["apps", "scale", "--app-name", "random", "--vmtype", "c1m1"] + ) + + assert result.exit_code == 0, result.output + mock_warn.assert_called_with( + "CLI arguments will override the values in the cloud.yml or pyproject.toml file." + ) + + +def test_scale_warn_cli_args_with_scale_type(mocker: MockFixture): + """Test error when scaletype is set to size but vmtype is missing from config.""" + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", + validated_data={"foo": "bar"}, + ), + ) + mocker.patch( + "reflex_cli.utils.hosting.scale_app", + ) + mocker.patch( + "reflex_cli.utils.hosting.ScaleParams.from_config", + return_value=hosting.ScaleParams( + type=hosting.ScaleType(hosting.ScaleType.SIZE), vm_type="c1m1" + ), + ) + mocker.patch( + "reflex_cli.utils.hosting.search_app", + return_value={ + "name": "fake-app", + "id": "fake-id", + "project_id": "fake-project", + }, + ) + + mocker.patch("reflex_cli.core.config.Config.exists", return_value=True) + mocker.patch( + "reflex_cli.core.config.Config.from_yaml_or_toml_or_default", + return_value=Config(regions={"ams": 1}, vmtype=None), + ) + mock_warn = mocker.patch("reflex_cli.utils.console.warn") + + result = runner.invoke( + hosting_cli, + [ + "apps", + "scale", + "--app-name", + "random", + "--regions", + "ams", + "--scale-type", + "size", + ], + ) + + assert result.exit_code == 0, result.output + mock_warn.assert_called_with( + "using --scale-type with --regions or --vmtype will have no effect" + ) + + +def test_scale_regions_via_config_no_scaletype(mocker: MockFixture): + """Test error when scaletype is set to regions but regions is missing from config.""" + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", + validated_data={"foo": "bar"}, + ), + ) + + mocker.patch("reflex_cli.core.config.Config.exists", return_value=True) + mocker.patch( + "reflex_cli.core.config.Config.from_yaml_or_toml_or_default", + return_value=Config(regions=None, vmtype="c1m2"), + ) + mock_error = mocker.patch("reflex_cli.utils.console.error") + + result = runner.invoke(hosting_cli, ["apps", "scale", "--app-name", "random"]) + + assert result.exit_code == 1 + mock_error.assert_called_with( + "specify the type of scaling using --scale-type when using cloud.yml or pyproject.toml" + ) + + +def test_scale_regions_via_config_without_regions(mocker: MockFixture): + """Test error when scaletype is set to regions but regions is missing from config.""" + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", + validated_data={"foo": "bar"}, + ), + ) + + mocker.patch("reflex_cli.core.config.Config.exists", return_value=True) + mocker.patch( + "reflex_cli.core.config.Config.from_yaml_or_toml_or_default", + return_value=Config(regions=None, vmtype="c1m2"), + ) + mock_error = mocker.patch("reflex_cli.utils.console.error") + + result = runner.invoke( + hosting_cli, ["apps", "scale", "--app-name", "random", "--scale-type", "region"] + ) + + assert result.exit_code == 1 + mock_error.assert_called_with( + "'regions' should be provided in the cloud.yml for region scaling" + ) + + +def test_scale_size_via_config_without_vmtype(mocker: MockFixture): + """Test error when scaletype is set to size but vmtype is missing from config.""" + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", + validated_data={"foo": "bar"}, + ), + ) + + mocker.patch("reflex_cli.core.config.Config.exists", return_value=True) + mocker.patch( + "reflex_cli.core.config.Config.from_yaml_or_toml_or_default", + return_value=Config(regions=None, vmtype=None), + ) + mock_error = mocker.patch("reflex_cli.utils.console.error") + + result = runner.invoke( + hosting_cli, ["apps", "scale", "--app-name", "random", "--scale-type", "size"] + ) + + assert result.exit_code == 1 + mock_error.assert_called_with( + "'vmtype' should be provided in the cloud.yml for size scaling" + ) + + +@pytest.mark.parametrize( + ("config", "scale_params", "command_args"), + [ + ( + Config(vmtype="c1m1"), + hosting.ScaleParams( + type=hosting.ScaleType(hosting.ScaleType.SIZE), + vm_type="c1m1", + ), + ["--vmtype", "c1m1"], + ), + ( + Config(vmtype=None, regions={"ams": 1}), + hosting.ScaleParams( + type=hosting.ScaleType.REGION, + vm_type=None, + regions=(hosting.Region(name="ams", number_of_machines=1),), + ), + ["--regions", "ams"], + ), + ( + Config(vmtype=None, regions={"ams": 1, "sjc": 1}), + hosting.ScaleParams( + type=hosting.ScaleType.REGION, + vm_type=None, + regions=( + hosting.Region(name="ams", number_of_machines=1), + hosting.Region(name="sjc", number_of_machines=1), + ), + ), + ["--regions", "ams", "--regions", "sjc"], + ), + ], +) +def test_scale_correct_post_request_cli_args( + mocker: MockerFixture, + config: Config, + scale_params: hosting.ScaleParams, + command_args: list[str], +): + """Test the correct POST request is made with appropriate parameters.""" + mocker.patch("reflex_cli.core.config.Config.exists", return_value=False) + mocker.patch( + "reflex_cli.utils.hosting.search_app", + return_value={ + "name": "fake-app", + "id": "fake-id", + "project_id": "fake-project", + }, + ) + mock_authenticated_client = mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=mock_authenticated_client, + ) + mocker.patch( + "reflex_cli.core.config.Config.from_yaml_or_toml_or_default", + return_value=config, + ) + mock_post = mocker.patch("reflex_cli.utils.hosting.scale_app") + + result = runner.invoke( + hosting_cli, ["apps", "scale", "--app-name", "random", *command_args] + ) + + assert result.exit_code == 0, result.output + mock_post.assert_called_with( + app_id="fake-id", scale_params=scale_params, client=mock_authenticated_client + ) + + +@pytest.mark.parametrize( + ("config", "scale_params", "command_args"), + [ + ( + Config(vmtype="c1m1", regions=None), + hosting.ScaleParams( + type=hosting.ScaleType(hosting.ScaleType.SIZE), + vm_type="c1m1", + ), + ["--vmtype", "c1m1"], + ), + ( + Config(vmtype=None, regions={"ams": 1}), + hosting.ScaleParams( + type=hosting.ScaleType.REGION, + vm_type=None, + regions=(hosting.Region(name="ams", number_of_machines=1),), + ), + ["--regions", "ams"], + ), + ], +) +def test_scale_correct_post_request_config( + mocker: MockerFixture, + config: Config, + scale_params: hosting.ScaleParams, + command_args: list[str], +): + """Test the correct POST request is made with appropriate parameters from config.""" + mocker.patch("reflex_cli.core.config.Config.exists", return_value=True) + mocker.patch( + "reflex_cli.utils.hosting.search_app", + return_value={ + "name": "fake-app", + "id": "fake-id", + "project_id": "fake-project", + }, + ) + mock_authenticated_client = mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=mock_authenticated_client, + ) + mocker.patch( + "reflex_cli.core.config.Config.from_yaml_or_toml_or_default", + return_value=config, + ) + mock_post = mocker.patch("reflex_cli.utils.hosting.scale_app") + mocker.patch( + "reflex_cli.utils.hosting.ScaleParams.from_config", return_value=scale_params + ) + mock_scale_params = mocker.patch( + "reflex_cli.utils.hosting.ScaleParams.set_type_from_cli_args" + ) + + result = runner.invoke( + hosting_cli, ["apps", "scale", "--app-name", "random", *command_args] + ) + + assert result.exit_code == 0, result.output + mock_post.assert_called_with( + app_id="fake-id", + scale_params=mock_scale_params.return_value, + client=mock_authenticated_client, + ) diff --git a/packages/reflex-hosting-cli/tests/v2/test_cli.py b/packages/reflex-hosting-cli/tests/v2/test_cli.py new file mode 100644 index 00000000000..3d24b1ea4f2 --- /dev/null +++ b/packages/reflex-hosting-cli/tests/v2/test_cli.py @@ -0,0 +1,650 @@ +from __future__ import annotations + +import importlib.metadata +from collections.abc import Callable +from unittest.mock import MagicMock + +import click +import httpx +import pytest +from packaging import version +from pytest_mock import MockerFixture, MockFixture + +from reflex_cli.utils import hosting +from reflex_cli.v2 import cli + + +def test_login_success_existing_token(mocker: MockFixture): + mocker.patch( + "reflex_cli.utils.hosting.authenticated_token", + return_value=("fake-code", {}), + ) + mock_authenticate_on_browser = mocker.patch( + "reflex_cli.utils.hosting.authenticate_on_browser", + return_value=("fake-token", {}), + ) + cli.login() + mock_authenticate_on_browser.assert_not_called() + + +def test_login_success_on_browser(mocker: MockFixture): + mocker.patch( + "reflex_cli.utils.hosting.authenticated_token", + side_effect=[("", {}), ("fake-token", {})], + ) + + mock_authenticate_on_browser = mocker.patch( + "reflex_cli.utils.hosting.authenticate_on_browser", + return_value=("fake-token", {}), + ) + cli.login() + mock_authenticate_on_browser.assert_called_once() + + +def test_login_failure(mocker: MockFixture): + mocker.patch( + "reflex_cli.utils.hosting.authenticated_token", + return_value=("", {}), + ) + mock_authenticate_on_browser = mocker.patch( + "reflex_cli.utils.hosting.authenticate_on_browser", return_value=("", {}) + ) + with pytest.raises(SystemExit): + cli.login() + mock_authenticate_on_browser.assert_called_once() + + +def test_logout(mocker: MockFixture): + mock_delete_token = mocker.patch( + "reflex_cli.utils.hosting.delete_token_from_config", + ) + mock_success = mocker.patch( + "reflex_cli.utils.console.success", + ) + + cli.logout() + mock_delete_token.assert_called_once() + mock_success.assert_called_once_with("Successfully logged out.") + + +@pytest.fixture +def mock_export_fn(): + rx_version = version.parse(importlib.metadata.version("reflex")) + breaking_version = version.parse("0.7.6") + _mock_export_fn = ( + (lambda arg1, arg2, arg3, arg4, arg5, arg6: ...) + if rx_version <= breaking_version + else (lambda arg1, arg2, arg3, arg4, arg5, arg6, arg7: ...) + ) + + return MagicMock(side_effect=_mock_export_fn) + + +@pytest.fixture +def mock_export_import_error_fn(): + def _mock_export_fn( + arg1: str, arg2: str, arg3: str, arg4: bool, arg5: bool, arg6: bool + ) -> None: + raise ImportError + + return MagicMock(side_effect=_mock_export_fn) + + +def test_deploy_non_interactive_with_invalid_app_name(mocker: MockFixture): + mocker.patch( + "reflex_cli.utils.hosting.get_selected_project", + return_value="fake-project", + ) + mocker.patch( + "reflex_cli.utils.hosting.get_project", + ) + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + + with pytest.raises(click.exceptions.Exit): + cli.deploy(app_name="", export_fn=MagicMock(), interactive=False) + + +@pytest.mark.parametrize( + "hostname", + [{"error": "fake-error"}, {"hostname": "fake-hostname", "server": "fake-server"}], +) +def test_deploy_non_interactive_app_not_found( + mocker: MockerFixture, + mock_export_fn: Callable[[str, str, str, bool, bool, bool, bool], None], + hostname: dict[str, str], +): + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mocker.patch( + "reflex_cli.utils.hosting.validate_deployment_args", + return_value="success", + ) + mocker.patch( + "reflex_cli.utils.hosting.get_selected_project", + return_value="fake-project", + ) + mocker.patch( + "reflex_cli.utils.hosting.search_app", + return_value=None, + ) + create_app = mocker.patch( + "reflex_cli.utils.hosting.create_app", + return_value={ + "name": "fake-app", + "id": "fake-id", + "project_id": "fake-project", + }, + ) + + mocker.patch( + "reflex_cli.utils.hosting.get_hostname", + return_value=hostname, + ) + create_deployment = mocker.patch( + "reflex_cli.utils.hosting.create_deployment", + return_value={"error": "fake-error"}, + ) + watch_deployment = mocker.patch( + "reflex_cli.utils.hosting.watch_deployment_status", + return_value={"error": "fake-error"}, + ) + mocker.patch( + "reflex_cli.utils.hosting.get_project", + ) + + if "error" in hostname: + with pytest.raises(click.exceptions.Exit): + cli.deploy(app_name="fake-app", export_fn=mock_export_fn, interactive=False) + create_app.assert_called_once() + return + + cli.deploy(app_name="fake-app", export_fn=mock_export_fn, interactive=False) + create_app.assert_called_once() + create_deployment.assert_called_once() + watch_deployment.assert_called_once() + + +def test_deploy_create_deployment_failure( + mocker: MockerFixture, + mock_export_fn: Callable[[str, str, str, bool, bool, bool, bool], None], +): + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mocker.patch( + "reflex_cli.utils.hosting.validate_deployment_args", + return_value="success", + ) + mocker.patch( + "reflex_cli.utils.hosting.get_selected_project", + return_value="fake-project", + ) + mocker.patch( + "reflex_cli.utils.hosting.search_app", + return_value={"name": "fake-app", "id": "fake-id"}, + ) + mocker.patch( + "reflex_cli.utils.hosting.get_hostname", + return_value={"hostname": "fake-hostname", "server": "fake-server"}, + ) + create_deployment = mocker.patch( + "reflex_cli.utils.hosting.create_deployment", + return_value="deployment failed", + ) + watch_deployment = mocker.patch( + "reflex_cli.utils.hosting.watch_deployment_status", + ) + mocker.patch( + "reflex_cli.utils.hosting.get_project", + ) + + with pytest.raises(click.exceptions.Exit): + cli.deploy(app_name="fake-app", export_fn=mock_export_fn, interactive=False) + create_deployment.assert_called_once() + watch_deployment.assert_not_called() + + +def test_deploy_non_interactive_project_name( + mocker: MockerFixture, + mock_export_fn: Callable[[str, str, str, bool, bool, bool, bool], None], +): + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mocker.patch( + "reflex_cli.utils.hosting.get_project", + return_value={"name": "fake-project"}, + ) + mocker.patch( + "reflex_cli.utils.hosting.get_selected_project", + return_value="fake-project", + ) + mocker.patch( + "reflex_cli.utils.hosting.search_app", + return_value={ + "name": "fake-app", + "id": "fake-id", + "project_id": "fake-project", + }, + ) + search_project = mocker.patch( + "reflex_cli.utils.hosting.search_project", + return_value={"name": "fake-project", "id": "fake-project-id"}, + ) + mocker.patch( + "reflex_cli.utils.hosting.get_hostname", + return_value={"hostname": "fake-hostname", "server": "fake-server"}, + ) + mocker.patch( + "reflex_cli.utils.hosting.read_config", + return_value={}, + ) + create_deployment = mocker.patch( + "reflex_cli.utils.hosting.create_deployment", + return_value={"deployment_id": "fake-deployment-id"}, + ) + watch_deployment = mocker.patch( + "reflex_cli.utils.hosting.watch_deployment_status", + return_value={"status": "ready"}, + ) + mocker.patch( + "reflex_cli.utils.hosting.validate_deployment_args", + return_value="success", + ) + + cli.deploy( + app_name="fake-app", + export_fn=mock_export_fn, + interactive=False, + project_name="fake-project", + ) + search_project.assert_called_once() + create_deployment.assert_called_once() + watch_deployment.assert_called_once() + + +def test_deploy_non_interactive_project_name_multiple_values( + mocker: MockerFixture, + mock_export_fn: Callable[[str, str, str, bool, bool, bool, bool], None], +): + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mocker.patch( + "reflex_cli.utils.hosting.get_project", + return_value={"name": "fake-project"}, + ) + mocker.patch( + "reflex_cli.utils.hosting.get_selected_project", + return_value="fake-project", + ) + mocker.patch( + "reflex_cli.utils.hosting.search_app", + return_value={ + "name": "fake-app", + "id": "fake-id", + "project_id": "fake-project", + }, + ) + mock_response = MagicMock() + mock_response.json.return_value = [ + { + "name": "fake-project", + "id": "fake-id", + }, + { + "name": "fake-project", + "id": "another-fake-id", + }, + ] + mocker.patch("httpx.get", return_value=mock_response) + + mocker.patch( + "reflex_cli.utils.hosting.get_hostname", + return_value={"hostname": "fake-hostname", "server": "fake-server"}, + ) + mocker.patch( + "reflex_cli.utils.hosting.read_config", + return_value={}, + ) + mocker.patch( + "reflex_cli.utils.hosting.requires_authenticated", return_value="fake_token" + ) + console_error = mocker.patch("reflex_cli.utils.console.error") + + with pytest.raises(click.exceptions.Exit): + cli.deploy( + app_name="fake-app", + export_fn=mock_export_fn, + interactive=False, + project_name="fake-project", + ) + console_error.assert_called_once_with( + "Multiple projects with the name 'fake-project' found. Please provide a unique name." + ) + + +def test_deploy_interactive_project_name_multiple_values( + mocker: MockerFixture, + mock_export_fn: Callable[[str, str, str, bool, bool, bool, bool], None], +): + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + get_project = mocker.patch( + "reflex_cli.utils.hosting.get_project", + return_value={"name": "fake-project"}, + ) + mocker.patch( + "reflex_cli.utils.hosting.get_selected_project", + return_value="fake-project", + ) + mocker.patch( + "reflex_cli.utils.hosting.search_app", + return_value={ + "name": "fake-app", + "id": "fake-id", + "project_id": "fake-project", + }, + ) + mock_response = MagicMock() + mock_response.json.return_value = [ + { + "name": "fake-project", + "id": "fake-id", + }, + { + "name": "fake-project", + "id": "another-fake-id", + }, + ] + mocker.patch("httpx.get", return_value=mock_response) + + mocker.patch( + "reflex_cli.utils.hosting.get_hostname", + return_value={"hostname": "fake-hostname", "server": "fake-server"}, + ) + mocker.patch( + "reflex_cli.utils.hosting.read_config", + return_value={}, + ) + mocker.patch( + "reflex_cli.utils.hosting.requires_authenticated", return_value="fake_token" + ) + mocker.patch( + "reflex_cli.utils.hosting.create_deployment", + return_value={"deployment_id": "fake-deployment-id"}, + ) + mocker.patch( + "reflex_cli.utils.hosting.watch_deployment_status", + return_value={"status": "ready"}, + ) + mocker.patch( + "reflex_cli.utils.hosting.validate_deployment_args", + return_value="success", + ) + console_ask = mocker.patch("reflex_cli.utils.console.ask", return_value="0") + + cli.deploy( + app_name="fake-app", export_fn=mock_export_fn, project_name="fake-project" + ) + console_ask.assert_called_once() + get_project.assert_called_once_with( + "fake-id", + client=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + + +@pytest.mark.parametrize( + "app_name, app_id", + [ + (None, None), + ("", ""), + ], +) +def test_deploy_non_interactive_no_app_name_and_id( + mocker: MockerFixture, app_name: str | None, app_id: str | None +): + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mocker.patch( + "reflex_cli.utils.hosting.get_selected_project", + return_value="fake-project", + ) + mocker.patch( + "reflex_cli.utils.hosting.get_project", + ) + console_error = mocker.patch("reflex_cli.utils.console.error") + + with pytest.raises(click.exceptions.Exit): + cli.deploy( + app_name=app_name, app_id=app_id, export_fn=MagicMock(), interactive=False + ) + + console_error.assert_called_once_with( + "Please provide a valid app name or ID for the deployed instance." + ) + + +def test_deploy_non_interactive_export_failure( + mocker: MockerFixture, mock_export_import_error_fn: MagicMock +): + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mocker.patch( + "reflex_cli.utils.hosting.validate_deployment_args", + return_value="success", + ) + mocker.patch( + "reflex_cli.utils.hosting.get_selected_project", + return_value="fake-project", + ) + mocker.patch( + "reflex_cli.utils.hosting.search_app", + return_value={"name": "fake-app", "id": "fake-id"}, + ) + mocker.patch( + "reflex_cli.utils.hosting.get_hostname", + return_value={"hostname": "fake-hostname", "server": "fake-server"}, + ) + create_deployment = mocker.patch( + "reflex_cli.utils.hosting.create_deployment", + return_value={"deployment_id": "fake-deployment-id"}, + ) + watch_deployment = mocker.patch( + "reflex_cli.utils.hosting.watch_deployment_status", + return_value={"status": "ready"}, + ) + mocker.patch( + "reflex_cli.utils.hosting.get_project", + ) + + with pytest.raises(click.exceptions.Exit): + cli.deploy( + app_name="fake-app", + export_fn=mock_export_import_error_fn, + interactive=False, + ) + + create_deployment.assert_not_called() + watch_deployment.assert_not_called() + + +def test_deploy_non_interactive_with_invalid_project(mocker: MockFixture): + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mocker.patch( + "reflex_cli.utils.hosting.get_project", + side_effect=httpx.HTTPStatusError( + "HTTP Error", + request=mocker.Mock(), + response=mocker.Mock(json=lambda: {"detail": "project does not exist"}), + ), + ) + mock_error = mocker.patch( + "reflex_cli.utils.console.error", + ) + with pytest.raises(click.exceptions.Exit): + cli.deploy( + app_name="app-name", + export_fn=MagicMock(), + project="fake-project", + interactive=False, + ) + + mock_error.assert_called_with("project does not exist") + + +def test_deploy_create_deployment_multiple_apps_non_interactive( + mocker: MockerFixture, + mock_export_fn: Callable[[str, str, str, bool, bool, bool, bool], None], +): + mocker.patch( + "reflex_cli.utils.hosting.get_selected_project", + return_value="fake-project", + ) + mock_response = MagicMock() + mock_response.json.return_value = [ + {"name": "fake-app", "id": "fake-id"}, + {"name": "fake-app", "id": "another-fake-id"}, + ] + mocker.patch("httpx.get", return_value=mock_response) + + mocker.patch( + "reflex_cli.utils.hosting.get_hostname", + return_value={"hostname": "fake-hostname", "server": "fake-server"}, + ) + mocker.patch( + "reflex_cli.utils.hosting.get_project", + ) + console_error = mocker.patch("reflex_cli.utils.console.error") + mocker.patch( + "reflex_cli.utils.hosting.authenticated_token", + return_value=("fake-code", {}), + ) + mocker.patch( + "reflex_cli.utils.hosting.authenticate_on_browser", + return_value=("fake-token", {}), + ) + + with pytest.raises(click.exceptions.Exit): + cli.deploy( + app_name="fake-app", + export_fn=mock_export_fn, + interactive=False, + token="fake-token", + ) + console_error.assert_called_once_with( + "Multiple apps with the name 'fake-app' found. Please provide a unique name." + ) + + +def test_deploy_create_deployment_multiple_apps_interactive( + mocker: MockerFixture, + mock_export_fn: Callable[[str, str, str, bool, bool, bool, bool], None], +): + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mocker.patch( + "reflex_cli.utils.hosting.get_selected_project", + return_value="fake-project", + ) + mocker.patch( + "reflex_cli.utils.hosting.validate_deployment_args", + return_value="success", + ) + mock_response = MagicMock() + mock_response.json.return_value = [ + { + "name": "fake-app", + "id": "fake-id", + "project_id": "fake-project", + "project": {"name": "fake-project", "id": "fake-project-id"}, + }, + { + "name": "fake-app", + "id": "another-fake-id", + "project_id": "another-fake-project", + "project": { + "name": "another-fake-project", + "id": "another-fake-project-id", + }, + }, + ] + mocker.patch("httpx.get", return_value=mock_response) + + get_host_name = mocker.patch( + "reflex_cli.utils.hosting.get_hostname", + return_value={"hostname": "fake-hostname", "server": "fake-server"}, + ) + mocker.patch( + "reflex_cli.utils.hosting.get_project", + ) + mocker.patch( + "reflex_cli.utils.hosting.create_deployment", + return_value={"deployment_id": "fake-deployment-id"}, + ) + mocker.patch( + "reflex_cli.utils.hosting.watch_deployment_status", + return_value={"status": "ready"}, + ) + mocker.patch( + "reflex_cli.utils.hosting.authenticated_token", + return_value=("fake-code", {}), + ) + mocker.patch( + "reflex_cli.utils.hosting.authenticate_on_browser", + return_value=("fake-token", {}), + ) + mocker.patch("reflex_cli.utils.console.print") + console_ask = mocker.patch("reflex_cli.utils.console.ask", return_value="0") + + cli.deploy(app_name="fake-app", export_fn=mock_export_fn, interactive=True) + console_ask.assert_called_once() + get_host_name.assert_called_once_with( + app_id="fake-id", + app_name="fake-app", + hostname=None, + client=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) diff --git a/packages/reflex-hosting-cli/tests/v2/test_deployments.py b/packages/reflex-hosting-cli/tests/v2/test_deployments.py new file mode 100644 index 00000000000..c697ee5a17b --- /dev/null +++ b/packages/reflex-hosting-cli/tests/v2/test_deployments.py @@ -0,0 +1,70 @@ +import importlib.metadata +from unittest.mock import MagicMock + +import click +import httpx +import pytest +from pytest_mock import MockerFixture, MockFixture + +from reflex_cli.v2.deployments import check_version + + +@pytest.mark.parametrize( + "installed_version, latest_version, should_exit", + [ + ("1.0.0", "1.0.0", False), + ("1.0.0", "2.0.0", True), + ], +) +def test_check_version( + mocker: MockerFixture, + installed_version: str, + latest_version: str, + should_exit: bool, +): + mocker.patch( + "importlib.metadata.version", + return_value=installed_version, + ) + mock_response = MagicMock() + mock_response.json.return_value = {"info": {"version": latest_version}} + mocker.patch("httpx.get", return_value=mock_response) + mock_console_error = mocker.patch("reflex_cli.utils.console.error") + + if should_exit: + with pytest.raises(click.exceptions.Exit) as excinfo: + check_version() + assert excinfo.value.exit_code == 1 + mock_console_error.assert_called_once_with( + "Warning: You are using reflex-hosting-cli version 1.0.0. A newer version 2.0.0 is available. Upgrade using: pip install --upgrade reflex-hosting-cli" + ) + else: + check_version() + mock_console_error.assert_not_called() + + +def test_check_version_distribution_not_found(mocker: MockFixture): + mocker.patch( + "importlib.metadata.version", + side_effect=importlib.metadata.PackageNotFoundError, + ) + mock_httpx_get = mocker.patch("httpx.get") + + check_version() + mock_httpx_get.assert_not_called() + + +def test_check_version_request_exception(mocker: MockFixture): + mocker.patch("importlib.metadata.version", return_value=MagicMock(version="1.0.0")) + mocker.patch("httpx.get", side_effect=httpx.RequestError("Request failed")) + check_version() + + +def test_check_version_http_status_error(mocker: MockFixture): + mocker.patch("importlib.metadata.version", return_value=MagicMock(version="1.0.0")) + mock_response = MagicMock() + mock_response.raise_for_status.side_effect = httpx.HTTPStatusError( + "HTTP error", request=MagicMock(), response=MagicMock() + ) + mocker.patch("httpx.get", return_value=mock_response) + check_version() diff --git a/packages/reflex-hosting-cli/tests/v2/test_project.py b/packages/reflex-hosting-cli/tests/v2/test_project.py new file mode 100644 index 00000000000..620ca5c5b59 --- /dev/null +++ b/packages/reflex-hosting-cli/tests/v2/test_project.py @@ -0,0 +1,838 @@ +import json + +import httpx +from click.testing import CliRunner +from pytest_mock import MockFixture +from typer import Typer +from typer.main import get_command + +from reflex_cli.utils import hosting +from reflex_cli.utils.exceptions import NotAuthenticatedError +from reflex_cli.v2.deployments import hosting_cli + +hosting_cli = ( + get_command(hosting_cli) if isinstance(hosting_cli, Typer) else hosting_cli +) + +runner = CliRunner() + + +def test_create_project_with_valid_token(mocker: MockFixture): + mock_create_project = mocker.patch( + "reflex_cli.utils.hosting.create_project", + return_value={"name": "test_project", "id": 1}, + ) + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="valid_token", validated_data={"foo": "bar"} + ), + ) + mock_print_table = mocker.patch("reflex_cli.utils.console.print_table") + + project_name = "test_project" + token = "valid_token" + + args = ["project", "create", project_name, "--token", token] + + result = runner.invoke(hosting_cli, args) + + mock_create_project.assert_called_once_with( + name=project_name, + client=hosting.AuthenticatedClient( + token="valid_token", validated_data={"foo": "bar"} + ), + ) + + headers = list({"name": "test_project", "id": 1}.keys()) + table = [ + [ + str(value) if value is not None else "" + for value in {"name": "test_project", "id": 1}.values() + ] + ] + mock_print_table.assert_called_once_with(table, headers=headers) + + # Asserting the result's exit code is 0 (indicating success) + assert result.exit_code == 0, result.output + + +def test_create_project_with_json_output(mocker: MockFixture): + mock_create_project = mocker.patch( + "reflex_cli.utils.hosting.create_project", + return_value={"name": "test_project", "id": 1}, + ) + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="valid_token", validated_data={"foo": "bar"} + ), + ) + mock_print = mocker.patch("reflex_cli.utils.console.print") + + project_name = "test_project" + token = "valid_token" + + args = ["project", "create", project_name, "--token", token, "--json"] + + result = runner.invoke(hosting_cli, args) + + mock_create_project.assert_called_once_with( + name=project_name, + client=hosting.AuthenticatedClient( + token="valid_token", validated_data={"foo": "bar"} + ), + ) + + mock_print.assert_called_once_with(json.dumps({"name": "test_project", "id": 1})) + + assert result.exit_code == 0, result.output + + +def test_create_project_without_token(mocker: MockFixture): + mock_create_project = mocker.patch( + "reflex_cli.utils.hosting.create_project", return_value=None + ) + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_print = mocker.patch("reflex_cli.utils.console.print") + + project_name = "test_project" + + args = ["project", "create", project_name] + + result = runner.invoke(hosting_cli, args) + + mock_create_project.assert_called_once_with( + name=project_name, + client=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_print.assert_called_once_with(str(None)) + assert result.exit_code == 0, result.output + + +def test_invite_user_to_project_success(mocker: MockFixture): + mock_invite = mocker.patch( + "reflex_cli.utils.hosting.invite_user_to_project", return_value="success" + ) + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="valid_token", validated_data={"foo": "bar"} + ), + ) + mock_success = mocker.patch("reflex_cli.utils.console.success") + mock_error = mocker.patch("reflex_cli.utils.console.error") + + role = "admin" + user = "user123" + token = "valid_token" + + args = ["project", "invite", role, user, "--token", token] + + result = runner.invoke(hosting_cli, args) + + mock_invite.assert_called_once_with( + role_id=role, + user_id=user, + client=hosting.AuthenticatedClient( + token="valid_token", validated_data={"foo": "bar"} + ), + ) + + mock_success.assert_called_once_with("Successfully invited user to project.") + mock_error.assert_not_called() + + assert result.exit_code == 0, result.output + + +def test_invite_user_to_project_failure(mocker: MockFixture): + mock_invite = mocker.patch( + "reflex_cli.utils.hosting.invite_user_to_project", + return_value="user invite failed: Unauthorized", + ) + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_success = mocker.patch("reflex_cli.utils.console.success") + mock_error = mocker.patch("reflex_cli.utils.console.error") + + role = "admin" + user = "user123" + + args = ["project", "invite", role, user] + + result = runner.invoke(hosting_cli, args) + + mock_invite.assert_called_once_with( + role_id=role, + user_id=user, + client=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + + mock_error.assert_called_once_with( + "Unable to invite user to project: user invite failed: Unauthorized" + ) + mock_success.assert_not_called() + + assert result.exit_code == 1 + + +def test_invite_user_to_project_missing_token(mocker: MockFixture): + mock_invite = mocker.patch( + "reflex_cli.utils.hosting.invite_user_to_project", + ) + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_success = mocker.patch("reflex_cli.utils.console.success") + mock_error = mocker.patch("reflex_cli.utils.console.error") + + role = "admin" + user = "user123" + + args = ["project", "invite", role, user] + + result = runner.invoke(hosting_cli, args) + + mock_invite.assert_called_once_with( + role_id=role, + user_id=user, + client=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_success.assert_called_once_with("Successfully invited user to project.") + mock_error.assert_not_called() + assert result.exit_code == 0, result.output + + +def test_select_project_success(mocker: MockFixture): + """Test successful project selection.""" + mock_select_project = mocker.patch( + "reflex_cli.utils.hosting.select_project", + return_value="TestProject is now selected.", + ) + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mocker.patch("reflex_cli.utils.hosting.get_project") + mock_success = mocker.patch("reflex_cli.utils.console.success") + mock_error = mocker.patch("reflex_cli.utils.console.error") + + project = "TestProject" + + args = ["project", "select", project] + + result = runner.invoke(hosting_cli, args) + + mock_select_project.assert_called_once_with(project=project, token=None) + + mock_success.assert_called_once_with("TestProject is now selected.") + mock_error.assert_not_called() + + assert result.exit_code == 0, result.output + + +def test_select_project_failure(mocker: MockFixture): + """Test failure during project selection.""" + mock_select_project = mocker.patch( + "reflex_cli.utils.hosting.select_project", + return_value="failed to select project.", + ) + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_success = mocker.patch("reflex_cli.utils.console.success") + mock_error = mocker.patch("reflex_cli.utils.console.error") + get_project = mocker.patch("reflex_cli.utils.hosting.get_project") + mock_response = mocker.Mock() + mock_response.raise_for_status.side_effect = httpx.HTTPStatusError( + "HTTP Error", + request=mocker.Mock(), + response=mocker.Mock( + json=lambda: { + "detail": "no project with given id found that user has access to." + } + ), + ) + get_project.return_value = mock_response + + project = "InvalidProject" + + args = ["project", "select", project] + + result = runner.invoke(hosting_cli, args) + + mock_select_project.assert_called_once_with(project=project, token=None) + + mock_error.assert_called_once_with("failed to select project.") + mock_success.assert_not_called() + + assert result.exit_code == 1 + + +def test_select_project_valid_project_name(mocker: MockFixture): + """Test successful project selection using project name.""" + mock_select_project = mocker.patch( + "reflex_cli.utils.hosting.select_project", + return_value="test_project_id is now selected.", + ) + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mocker.patch( + "reflex_cli.utils.hosting.search_project", + return_value={"id": "test_project_id"}, + ) + get_project = mocker.patch( + "reflex_cli.utils.hosting.get_project", + ) + mock_success = mocker.patch("reflex_cli.utils.console.success") + mock_error = mocker.patch("reflex_cli.utils.console.error") + mocker.patch( + "reflex_cli.utils.hosting.requires_authenticated", return_value="fake_token" + ) + token = "test_token" + + args = ["project", "select", "--project-name", "TestProject", "--token", token] + + result = runner.invoke(hosting_cli, args) + + mock_select_project.assert_called_once_with(project="test_project_id", token=token) + + mock_success.assert_called_once_with("test_project_id is now selected.") + mock_error.assert_not_called() + get_project.assert_not_called() + + assert result.exit_code == 0, result.output + + +def test_select_project_invalid_id(mocker: MockFixture): + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_get = mocker.patch("httpx.get") + mock_response = mocker.Mock() + mock_response.raise_for_status.side_effect = httpx.HTTPStatusError( + "HTTP Error", + request=mocker.Mock(), + response=mocker.Mock( + json=lambda: { + "detail": "no project with given id found that user has access to." + } + ), + ) + mock_get.return_value = mock_response + mocker.patch( + "reflex_cli.utils.hosting.requires_authenticated", return_value="fake_token" + ) + mocker.patch( + "reflex_cli.utils.hosting.authorization_header", + return_value={"X-API-TOKEN": "fake_token"}, + ) + + mock_error = mocker.patch("reflex_cli.utils.console.error") + + project = "InvalidProject" + + args = ["project", "select", project] + + result = runner.invoke(hosting_cli, args) + + mock_error.assert_called_once_with( + "no project with given id found that user has access to." + ) + assert result.exit_code == 1 + + +def test_select_project_invalid_project_name(mocker: MockFixture): + mocker.patch( + "reflex_cli.utils.hosting.requires_authenticated", return_value="fake_token" + ) + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mocker.patch( + "reflex_cli.utils.hosting.authorization_header", + return_value={"X-API-TOKEN": "fake_token"}, + ) + mocker.patch( + "reflex_cli.utils.hosting.search_project", + return_value=None, + ) + + mock_error = mocker.patch("reflex_cli.utils.console.error") + + args = [ + "project", + "select", + "--project-name", + "invalid_project", + "--token", + "test_token", + ] + + result = runner.invoke(hosting_cli, args) + + mock_error.assert_called_once_with( + "No project selected. Please provide a valid project ID or name." + ) + assert result.exit_code == 1 + + +def test_get_project_roles_with_project_id(mocker: MockFixture): + """Test retrieving project roles with a provided project ID.""" + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_get_project_roles = mocker.patch( + "reflex_cli.utils.hosting.get_project_roles", + return_value=[ + {"role": "admin", "user": "user1@example.com"}, + {"role": "viewer", "user": "user2@example.com"}, + ], + ) + mock_console_print_table = mocker.patch("reflex_cli.utils.console.print_table") + + result = runner.invoke( + hosting_cli, + ["project", "roles", "--project-id", "test_project_id"], + ) + + assert result.exit_code == 0, result.output + mock_get_project_roles.assert_called_once_with( + project_id="test_project_id", + client=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_console_print_table.assert_called_once() + + +def test_get_project_roles_no_project_selected(mocker: MockFixture): + """Test retrieving project roles when no project ID is provided or selected.""" + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_get_selected_project = mocker.patch( + "reflex_cli.utils.hosting.get_selected_project", return_value=None + ) + mock_console_error = mocker.patch("reflex_cli.utils.console.error") + + result = runner.invoke(hosting_cli, ["project", "roles"]) + + assert result.exit_code == 1 + mock_get_selected_project.assert_called_once() + mock_console_error.assert_called_once_with( + "no project_id provided or selected. Set it with `reflex cloud project roles --project-id \\[project_id]`" + ) + + +def test_get_project_roles_as_json(mocker: MockFixture): + """Test retrieving project roles with JSON output.""" + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_get_project_roles = mocker.patch( + "reflex_cli.utils.hosting.get_project_roles", + return_value=[ + {"role": "admin", "user": "user1@example.com"}, + {"role": "viewer", "user": "user2@example.com"}, + ], + ) + mock_console_print = mocker.patch("reflex_cli.utils.console.print") + + result = runner.invoke( + hosting_cli, + ["project", "roles", "--project-id", "test_project_id", "--json"], + ) + + assert result.exit_code == 0, result.output + mock_get_project_roles.assert_called_once_with( + project_id="test_project_id", + client=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_console_print.assert_called_once_with( + json.dumps( + [ + {"role": "admin", "user": "user1@example.com"}, + {"role": "viewer", "user": "user2@example.com"}, + ] + ) + ) + + +def test_get_project_roles_empty_roles(mocker: MockFixture): + """Test retrieving project roles when the result is an empty list.""" + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_get_project_roles = mocker.patch( + "reflex_cli.utils.hosting.get_project_roles", + return_value=[], + ) + mock_console_print = mocker.patch("reflex_cli.utils.console.print") + + result = runner.invoke( + hosting_cli, + ["project", "roles", "--project-id", "test_project_id"], + ) + + assert result.exit_code == 0, result.output + mock_get_project_roles.assert_called_once_with( + project_id="test_project_id", + client=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_console_print.assert_called_once_with("[]") + + +def test_get_project_role_permissions_with_role_and_project_id(mocker: MockFixture): + """Test retrieving role permissions with provided role_id and project_id.""" + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_get_project_role_permissions = mocker.patch( + "reflex_cli.utils.hosting.get_project_role_permissions", + return_value=[ + {"permission": "read", "resource": "resource1"}, + {"permission": "write", "resource": "resource2"}, + ], + ) + mock_console_print_table = mocker.patch("reflex_cli.utils.console.print_table") + + result = runner.invoke( + hosting_cli, + [ + "project", + "role-permissions", + "test_role_id", + "--project-id", + "test_project_id", + ], + ) + + assert result.exit_code == 0, result.output + mock_get_project_role_permissions.assert_called_once_with( + project_id="test_project_id", + role_id="test_role_id", + client=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_console_print_table.assert_called_once() + + +def test_get_project_role_permissions_no_project_selected(mocker: MockFixture): + """Test retrieving role permissions when no project_id is provided or selected.""" + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_get_selected_project = mocker.patch( + "reflex_cli.utils.hosting.get_selected_project", return_value=None + ) + mock_console_error = mocker.patch("reflex_cli.utils.console.error") + + result = runner.invoke(hosting_cli, ["project", "role-permissions", "test_role_id"]) + + assert result.exit_code == 1 + mock_get_selected_project.assert_called_once() + mock_console_error.assert_called_once_with( + "no project_id provided or selected. Set it with `reflex cloud project role-permissions --project-id \\[project_id]`." + ) + + +def test_get_project_role_permissions_not_authenticated(mocker: MockFixture): + """Test retrieving role permissions when the user is not authenticated.""" + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_get_project_role_permissions = mocker.patch( + "reflex_cli.utils.hosting.get_project_role_permissions", + side_effect=NotAuthenticatedError("not authenticated"), + ) + mock_get_selected_project = mocker.patch( + "reflex_cli.utils.hosting.get_selected_project", + return_value="test_project_id", + ) + mock_console_error = mocker.patch("reflex_cli.utils.console.error") + + result = runner.invoke(hosting_cli, ["project", "role-permissions", "test_role_id"]) + + assert result.exit_code == 1 + mock_get_selected_project.assert_called_once() + mock_get_project_role_permissions.assert_called_once_with( + project_id="test_project_id", + role_id="test_role_id", + client=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_console_error.assert_called_once_with( + "You are not authenticated. Run `reflex login` to authenticate." + ) + + +def test_get_project_role_permissions_as_json(mocker: MockFixture): + """Test retrieving role permissions with JSON output.""" + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_get_project_role_permissions = mocker.patch( + "reflex_cli.utils.hosting.get_project_role_permissions", + return_value=[ + {"permission": "read", "resource": "resource1"}, + {"permission": "write", "resource": "resource2"}, + ], + ) + mock_console_print = mocker.patch("reflex_cli.utils.console.print") + + result = runner.invoke( + hosting_cli, + [ + "project", + "role-permissions", + "test_role_id", + "--project-id", + "test_project_id", + "--json", + ], + ) + + assert result.exit_code == 0, result.output + mock_get_project_role_permissions.assert_called_once_with( + project_id="test_project_id", + role_id="test_role_id", + client=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_console_print.assert_called_once_with( + json.dumps( + [ + {"permission": "read", "resource": "resource1"}, + {"permission": "write", "resource": "resource2"}, + ] + ) + ) + + +def test_get_project_role_permissions_empty_permissions(mocker: MockFixture): + """Test retrieving role permissions when the result is an empty list.""" + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_get_project_role_permissions = mocker.patch( + "reflex_cli.utils.hosting.get_project_role_permissions", + return_value=[], + ) + mock_console_print = mocker.patch("reflex_cli.utils.console.print") + + result = runner.invoke( + hosting_cli, + [ + "project", + "role-permissions", + "test_role_id", + "--project-id", + "test_project_id", + ], + ) + + assert result.exit_code == 0, result.output + mock_get_project_role_permissions.assert_called_once_with( + project_id="test_project_id", + role_id="test_role_id", + client=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_console_print.assert_called_once_with("[]") + + +def test_get_project_role_users_with_project_id(mocker: MockFixture): + """Test retrieving users with a provided project_id.""" + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_get_project_role_users = mocker.patch( + "reflex_cli.utils.hosting.get_project_role_users", + return_value=[ + {"user_id": "user1", "role": "admin"}, + {"user_id": "user2", "role": "developer"}, + ], + ) + mock_console_print_table = mocker.patch("reflex_cli.utils.console.print_table") + + result = runner.invoke( + hosting_cli, + ["project", "users", "--project-id", "test_project_id"], + ) + + assert result.exit_code == 0, result.output + mock_get_project_role_users.assert_called_once_with( + project_id="test_project_id", + client=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_console_print_table.assert_called_once() + + +def test_get_project_role_users_no_project_selected(mocker: MockFixture): + """Test retrieving users when no project_id is provided or selected.""" + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_get_selected_project = mocker.patch( + "reflex_cli.utils.hosting.get_selected_project", return_value=None + ) + mock_console_error = mocker.patch("reflex_cli.utils.console.error") + + result = runner.invoke(hosting_cli, ["project", "users"]) + + assert result.exit_code == 1 + mock_get_selected_project.assert_called_once() + mock_console_error.assert_called_once_with( + "no project_id provided or selected. Set it with `reflex cloud project users --project-id \\[project_id]`" + ) + + +def test_get_project_role_users_as_json(mocker: MockFixture): + """Test retrieving users with JSON output.""" + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_get_project_role_users = mocker.patch( + "reflex_cli.utils.hosting.get_project_role_users", + return_value=[ + {"user_id": "user1", "role": "admin"}, + {"user_id": "user2", "role": "developer"}, + ], + ) + mock_console_print = mocker.patch("reflex_cli.utils.console.print") + + result = runner.invoke( + hosting_cli, + [ + "project", + "users", + "--project-id", + "test_project_id", + "--json", + ], + ) + + assert result.exit_code == 0, result.output + mock_get_project_role_users.assert_called_once_with( + project_id="test_project_id", + client=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_console_print.assert_called_once_with( + json.dumps( + [ + {"user_id": "user1", "role": "admin"}, + {"user_id": "user2", "role": "developer"}, + ] + ) + ) + + +def test_get_project_role_users_empty_users(mocker: MockFixture): + """Test retrieving users when the result is an empty list.""" + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_get_project_role_users = mocker.patch( + "reflex_cli.utils.hosting.get_project_role_users", + return_value=[], + ) + mock_console_print = mocker.patch("reflex_cli.utils.console.print") + + result = runner.invoke( + hosting_cli, + ["project", "users", "--project-id", "test_project_id"], + ) + + assert result.exit_code == 0, result.output + mock_get_project_role_users.assert_called_once_with( + project_id="test_project_id", + client=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_console_print.assert_called_once_with("[]") diff --git a/packages/reflex-hosting-cli/tests/v2/test_secrets.py b/packages/reflex-hosting-cli/tests/v2/test_secrets.py new file mode 100644 index 00000000000..94f654c7b05 --- /dev/null +++ b/packages/reflex-hosting-cli/tests/v2/test_secrets.py @@ -0,0 +1,275 @@ +import tempfile +from pathlib import Path + +from click.testing import CliRunner +from pytest_mock import MockFixture +from typer import Typer +from typer.main import get_command + +from reflex_cli.utils import hosting +from reflex_cli.v2.deployments import hosting_cli + +hosting_cli = ( + get_command(hosting_cli) if isinstance(hosting_cli, Typer) else hosting_cli +) + +runner = CliRunner() + + +def test_get_secrets_success(mocker: MockFixture): + """Test successful retrieval of secrets.""" + mock_get_secrets = mocker.patch( + "reflex_cli.utils.hosting.get_secrets", + return_value={"secret_key_1": "value1", "secret_key_2": "value2"}, + ) + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_console_print_table = mocker.patch("reflex_cli.utils.console.print_table") + + app_id = "app_id" + + args = ["secrets", "list", app_id] + + result = runner.invoke(hosting_cli, args) + + mock_get_secrets.assert_called_once_with( + app_id=app_id, + client=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + + mock_console_print_table.assert_called_once_with( + [ + ["secret_key_1"], + ["secret_key_2"], + ], + headers=["Keys"], + ) + + assert result.exit_code == 0, result.output + + +def test_get_secrets_error(mocker: MockFixture): + """Test failure to retrieve secrets.""" + mock_get_secrets = mocker.patch( + "reflex_cli.utils.hosting.get_secrets", + return_value="failed to retrieve secrets.", + ) + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_console_error = mocker.patch("reflex_cli.utils.console.error") + + app_id = "app_id" + + args = ["secrets", "list", app_id] + result = runner.invoke(hosting_cli, args) + + mock_get_secrets.assert_called_once_with( + app_id=app_id, + client=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + + mock_console_error.assert_called_once_with("failed to retrieve secrets.") + + assert result.exit_code == 1 + + +def test_get_secrets_json_output(mocker: MockFixture): + """Test JSON output for secrets.""" + mock_get_secrets = mocker.patch( + "reflex_cli.utils.hosting.get_secrets", + return_value={"secret_key_1": "value1", "secret_key_2": "value2"}, + ) + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_console_print = mocker.patch("reflex_cli.utils.console.print") + + app_id = "app_id" + + args = ["secrets", "list", app_id, "--json"] + + result = runner.invoke(hosting_cli, args) + + mock_get_secrets.assert_called_once_with( + app_id=app_id, + client=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_console_print.assert_called_once_with( + {"secret_key_1": "value1", "secret_key_2": "value2"} + ) + assert result.exit_code == 0, result.output + + +def test_delete_secret_success(mocker: MockFixture): + """Test successful deletion of a secret.""" + mock_delete_secret = mocker.patch( + "reflex_cli.utils.hosting.delete_secret", + return_value="Successfully deleted secret.", + ) + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_console_success = mocker.patch("reflex_cli.utils.console.success") + + result = runner.invoke( + hosting_cli, + ["secrets", "delete", "app_id", "key", "--reboot"], + ) + + assert result.exit_code == 0, result.output + mock_delete_secret.assert_called_once_with( + app_id="app_id", + key="key", + reboot=True, + client=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_console_success.assert_called_once_with("Successfully deleted secret.") + + +def test_delete_secret_failure(mocker: MockFixture): + """Test failure to delete a secret.""" + mock_delete_secret = mocker.patch( + "reflex_cli.utils.hosting.delete_secret", + return_value="failed to delete secret.", + ) + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_console_error = mocker.patch("reflex_cli.utils.console.error") + + result = runner.invoke( + hosting_cli, + ["secrets", "delete", "app_id", "key", "--reboot"], + ) + + assert result.exit_code == 1 + mock_delete_secret.assert_called_once_with( + app_id="app_id", + key="key", + reboot=True, + client=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_console_error.assert_called_once_with("failed to delete secret.") + + +def test_update_secrets_with_envfile(mocker: MockFixture): + """Test updating secrets with an envfile.""" + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + + with tempfile.TemporaryDirectory() as tmpdir: + env_path = Path(tmpdir) / ".env" + env_content = "key1=value1\nkey2=value2" + env_path.write_text(env_content) + + mocker.patch("reflex_cli.utils.hosting.update_secrets") + mocker.patch("reflex_cli.utils.console.warn") + + result = runner.invoke( + hosting_cli, + [ + "secrets", + "update", + "app_id", + "--envfile", + str(env_path), + "--env", + "key3=value3", + ], + ) + + assert result.exit_code == 0, result.output + + +def test_update_secrets_with_envs(mocker: MockFixture): + """Test updating secrets with --env arguments.""" + mock_process_envs = mocker.patch( + "reflex_cli.utils.hosting.process_envs", + return_value={"key1": "value1", "key2": "value2"}, + ) + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_update_secrets = mocker.patch("reflex_cli.utils.hosting.update_secrets") + + result = runner.invoke( + hosting_cli, + ["secrets", "update", "app_id", "--env", "key1=value1", "--env", "key2=value2"], + ) + + assert result.exit_code == 0, result.output + mock_process_envs.assert_called_once_with(["key1=value1", "key2=value2"]) + mock_update_secrets.assert_called_once_with( + app_id="app_id", + secrets={"key1": "value1", "key2": "value2"}, + reboot=False, + client=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + + +def test_update_secrets_missing_arguments(mocker: MockFixture): + """Test updating secrets with neither --envfile nor --env.""" + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + mock_console_error = mocker.patch("reflex_cli.utils.console.error") + + result = runner.invoke(hosting_cli, ["secrets", "update", "app_id"]) + + assert result.exit_code == 1 + mock_console_error.assert_called_once_with("--envfile or --env must be provided") + + +def test_update_secrets_invalid_env_format(mocker: MockFixture): + """Test invalid format for --env.""" + mocker.patch( + "reflex_cli.utils.hosting.get_authenticated_client", + return_value=hosting.AuthenticatedClient( + token="fake-token", validated_data={"foo": "bar"} + ), + ) + result = runner.invoke( + hosting_cli, ["secrets", "update", "app_id", "--env", "invalid_env"] + ) + + assert result.exit_code == 1 + assert "Invalid env format: should be =." in result.stdout diff --git a/packages/reflex-hosting-cli/tests/v2/test_vmtypes_regions.py b/packages/reflex-hosting-cli/tests/v2/test_vmtypes_regions.py new file mode 100644 index 00000000000..75fd7d0dd16 --- /dev/null +++ b/packages/reflex-hosting-cli/tests/v2/test_vmtypes_regions.py @@ -0,0 +1,220 @@ +import json + +import httpx +import pytest +from click.testing import CliRunner +from pytest_mock import MockerFixture, MockFixture +from typer import Typer +from typer.main import get_command + +from reflex_cli.v2.deployments import hosting_cli + +hosting_cli = ( + get_command(hosting_cli) if isinstance(hosting_cli, Typer) else hosting_cli +) + + +runner = CliRunner() + + +@pytest.fixture +def mock_console(mocker: MockFixture): + """Fixture to mock console.print and console.error.""" + mock_print = mocker.patch("reflex_cli.utils.console.print") + mock_error = mocker.patch("reflex_cli.utils.console.error") + return mock_print, mock_error + + +def test_get_vm_types_success(mocker: MockFixture): + """Test successful retrieval of VM types.""" + mock_get_vm_types = mocker.patch( + "reflex_cli.utils.hosting.get_vm_types", + return_value=[ + {"id": "1", "name": "Small", "cpu": 2, "ram": 4}, + {"id": "2", "name": "Medium", "cpu": 4, "ram": 8}, + ], + ) + mock_console_print_table = mocker.patch("reflex_cli.utils.console.print_table") + + result = runner.invoke(hosting_cli, ["vmtypes"]) + + assert result.exit_code == 0, result.output + mock_get_vm_types.assert_called_once() + mock_console_print_table.assert_called_once_with( + [ + ["1", "Small", "2", "4"], + ["2", "Medium", "4", "8"], + ], + headers=["id", "name", "cpu (cores)", "ram (gb)"], + ) + + +def test_get_vm_types_as_json(mocker: MockFixture): + """Test retrieval of VM types with JSON output.""" + mock_get_vm_types = mocker.patch( + "reflex_cli.utils.hosting.get_vm_types", + return_value=[ + {"id": "1", "name": "Small", "cpu": 2, "ram": 4}, + {"id": "2", "name": "Medium", "cpu": 4, "ram": 8}, + ], + ) + mock_console_print = mocker.patch("reflex_cli.utils.console.print") + + result = runner.invoke(hosting_cli, ["vmtypes", "--json"]) + + assert result.exit_code == 0, result.output + mock_get_vm_types.assert_called_once() + mock_console_print.assert_called_once_with( + '[{"id": "1", "name": "Small", "cpu": 2, "ram": 4}, {"id": "2", "name": "Medium", "cpu": 4, "ram": 8}]' + ) + + +def test_get_vm_types_empty(mocker: MockFixture): + """Test retrieval when no VM types are available.""" + mock_get_vm_types = mocker.patch( + "reflex_cli.utils.hosting.get_vm_types", return_value=[] + ) + mock_console_print = mocker.patch("reflex_cli.utils.console.print") + + result = runner.invoke(hosting_cli, ["vmtypes"]) + + assert result.exit_code == 0, result.output + mock_get_vm_types.assert_called_once() + mock_console_print.assert_called_once_with("[]") + + +def test_get_vm_types_invalid_response(mocker: MockFixture): + """Test handling of invalid server response.""" + mock_get_vm_types = mocker.patch( + "reflex_cli.utils.hosting.get_vm_types", + return_value=[{"invalid_key": "value"}], + ) + mock_console_print_table = mocker.patch("reflex_cli.utils.console.print_table") + + result = runner.invoke(hosting_cli, ["vmtypes"]) + + assert result.exit_code == 0, result.output + mock_get_vm_types.assert_called_once() + # Expect the invalid key will not match the displayed table + mock_console_print_table.assert_called_once_with( + [[]], headers=["id", "name", "cpu (cores)", "ram (gb)"] + ) + + +def test_get_vm_types_http_error(mocker: MockFixture): + """Test handling of an HTTP error.""" + mock_get = mocker.patch("httpx.get") + mock_response = mocker.Mock() + mock_response.raise_for_status.side_effect = httpx.HTTPStatusError( + "HTTP Error", + request=mocker.Mock(), + response=mocker.Mock(json=lambda: {"detail": "Invalid token"}), + ) + mock_get.return_value = mock_response + mocker.patch("reflex_cli.utils.console.error") + mocker.patch( + "reflex_cli.utils.hosting.requires_authenticated", return_value="fake_token" + ) + mocker.patch("reflex_cli.utils.hosting.get_app", return_value={"id": "fake_app_id"}) + mocker.patch( + "reflex_cli.utils.hosting.authorization_header", + return_value={"X-API-TOKEN": "fake_token"}, + ) + + mock_console_error = mocker.patch("reflex_cli.utils.console.error") + mock_console_print = mocker.patch("reflex_cli.utils.console.print") + result = runner.invoke(hosting_cli, ["vmtypes"]) + + assert result.exit_code == 0, result.output + mock_console_error.assert_called_once_with( + "Unable to get vmtypes due to HTTP Error." + ) + mock_console_print.assert_called_once_with("[]") + + +def test_get_deployment_regions_success(mocker: MockerFixture): + """Test successful retrieval of regions with table output.""" + mock_get_regions = mocker.patch( + "reflex_cli.utils.hosting.get_regions", + return_value=[ + {"name": "Amsterdam, Netherlands", "code": "ams"}, + {"name": "Stockholm, Sweden", "code": "arn"}, + ], + ) + mock_print_table = mocker.patch("reflex_cli.utils.console.print_table") + + result = runner.invoke(hosting_cli, ["regions"]) + + assert result.exit_code == 0, result.output + mock_get_regions.assert_called_once() + mock_print_table.assert_called_once_with( + [["Amsterdam, Netherlands", "ams"], ["Stockholm, Sweden", "arn"]], + headers=["name", "code"], + ) + + +def test_get_deployment_regions_as_json(mocker: MockFixture): + """Test successful retrieval of regions with JSON output.""" + mock_get_regions = mocker.patch( + "reflex_cli.utils.hosting.get_regions", + return_value=[ + {"name": "Amsterdam, Netherlands", "code": "ams"}, + {"name": "Stockholm, Sweden", "code": "arn"}, + ], + ) + mock_print = mocker.patch("reflex_cli.utils.console.print") + + result = runner.invoke(hosting_cli, ["regions", "--json"]) + + assert result.exit_code == 0, result.output + mock_get_regions.assert_called_once() + mock_print.assert_called_once_with( + json.dumps( + [ + {"name": "Amsterdam, Netherlands", "code": "ams"}, + {"name": "Stockholm, Sweden", "code": "arn"}, + ] + ) + ) + + +def test_get_deployment_regions_empty(mocker: MockFixture): + """Test retrieval when no regions are available.""" + mock_get_regions = mocker.patch( + "reflex_cli.utils.hosting.get_regions", + return_value=[], + ) + mocker.patch("reflex_cli.utils.console.print") + + result = runner.invoke(hosting_cli, ["regions"]) + + assert result.exit_code == 0, result.output + mock_get_regions.assert_called_once() + + +def test_get_deployment_regions_http_error(mocker: MockerFixture): + """Test handling of an HTTP error.""" + mock_get = mocker.patch("httpx.get") + mock_response = mocker.Mock() + mock_response.raise_for_status.side_effect = httpx.HTTPStatusError( + "HTTP Error", + request=mocker.Mock(), + response=mocker.Mock(json=lambda: {"detail": "Invalid token"}), + ) + mock_get.return_value = mock_response + mock_error = mocker.patch("reflex_cli.utils.console.error") + mocker.patch( + "reflex_cli.utils.hosting.requires_authenticated", return_value="fake_token" + ) + mocker.patch("reflex_cli.utils.hosting.get_app", return_value={"id": "fake_app_id"}) + mocker.patch( + "reflex_cli.utils.hosting.authorization_header", + return_value={"X-API-TOKEN": "fake_token"}, + ) + + mock_error = mocker.patch("reflex_cli.utils.console.error") + + result = runner.invoke(hosting_cli, ["regions"]) + + assert result.exit_code == 0, result.output + mock_error.assert_called_once_with("Unable to get regions due to HTTP Error.") diff --git a/packages/reflex-hosting-cli/uv.lock b/packages/reflex-hosting-cli/uv.lock new file mode 100644 index 00000000000..2c152873769 --- /dev/null +++ b/packages/reflex-hosting-cli/uv.lock @@ -0,0 +1,1537 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" + +[[package]] +name = "alembic" +version = "1.18.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mako" }, + { name = "sqlalchemy" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/94/13/8b084e0f2efb0275a1d534838844926f798bd766566b1375174e2448cd31/alembic-1.18.4.tar.gz", hash = "sha256:cb6e1fd84b6174ab8dbb2329f86d631ba9559dd78df550b57804d607672cedbc", size = 2056725, upload-time = "2026-02-10T16:00:47.195Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/29/6533c317b74f707ea28f8d633734dbda2119bbadfc61b2f3640ba835d0f7/alembic-1.18.4-py3-none-any.whl", hash = "sha256:a5ed4adcf6d8a4cb575f3d759f071b03cd6e5c7618eb796cb52497be25bfe19a", size = 263893, upload-time = "2026-02-10T16:00:49.997Z" }, +] + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[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, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] + +[[package]] +name = "async-timeout" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274, upload-time = "2024-11-06T16:41:39.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" }, +] + +[[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, upload-time = "2024-02-18T19:09:05.748Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl", hash = "sha256:5dae8d4d79b552a71cbabc7deb25dfe8ce710b17ff41711e13010ead2abfc3e5", size = 32764, upload-time = "2024-02-18T19:09:04.156Z" }, +] + +[[package]] +name = "certifi" +version = "2026.2.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, +] + +[[package]] +name = "cfgv" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[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, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.13.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/56/95b7e30fa389756cb56630faa728da46a27b8c6eb46f9d557c68fff12b65/coverage-7.13.4.tar.gz", hash = "sha256:e5c8f6ed1e61a8b2dcdf31eb0b9bbf0130750ca79c1c49eb898e2ad86f5ccc91", size = 827239, upload-time = "2026-02-09T12:59:03.86Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/d4/7827d9ffa34d5d4d752eec907022aa417120936282fc488306f5da08c292/coverage-7.13.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fc31c787a84f8cd6027eba44010517020e0d18487064cd3d8968941856d1415", size = 219152, upload-time = "2026-02-09T12:56:11.974Z" }, + { url = "https://files.pythonhosted.org/packages/35/b0/d69df26607c64043292644dbb9dc54b0856fabaa2cbb1eeee3331cc9e280/coverage-7.13.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a32ebc02a1805adf637fc8dec324b5cdacd2e493515424f70ee33799573d661b", size = 219667, upload-time = "2026-02-09T12:56:13.33Z" }, + { url = "https://files.pythonhosted.org/packages/82/a4/c1523f7c9e47b2271dbf8c2a097e7a1f89ef0d66f5840bb59b7e8814157b/coverage-7.13.4-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e24f9156097ff9dc286f2f913df3a7f63c0e333dcafa3c196f2c18b4175ca09a", size = 246425, upload-time = "2026-02-09T12:56:14.552Z" }, + { url = "https://files.pythonhosted.org/packages/f8/02/aa7ec01d1a5023c4b680ab7257f9bfde9defe8fdddfe40be096ac19e8177/coverage-7.13.4-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8041b6c5bfdc03257666e9881d33b1abc88daccaf73f7b6340fb7946655cd10f", size = 248229, upload-time = "2026-02-09T12:56:16.31Z" }, + { url = "https://files.pythonhosted.org/packages/35/98/85aba0aed5126d896162087ef3f0e789a225697245256fc6181b95f47207/coverage-7.13.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a09cfa6a5862bc2fc6ca7c3def5b2926194a56b8ab78ffcf617d28911123012", size = 250106, upload-time = "2026-02-09T12:56:18.024Z" }, + { url = "https://files.pythonhosted.org/packages/96/72/1db59bd67494bc162e3e4cd5fbc7edba2c7026b22f7c8ef1496d58c2b94c/coverage-7.13.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:296f8b0af861d3970c2a4d8c91d48eb4dd4771bcef9baedec6a9b515d7de3def", size = 252021, upload-time = "2026-02-09T12:56:19.272Z" }, + { url = "https://files.pythonhosted.org/packages/9d/97/72899c59c7066961de6e3daa142d459d47d104956db43e057e034f015c8a/coverage-7.13.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e101609bcbbfb04605ea1027b10dc3735c094d12d40826a60f897b98b1c30256", size = 247114, upload-time = "2026-02-09T12:56:21.051Z" }, + { url = "https://files.pythonhosted.org/packages/39/1f/f1885573b5970235e908da4389176936c8933e86cb316b9620aab1585fa2/coverage-7.13.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:aa3feb8db2e87ff5e6d00d7e1480ae241876286691265657b500886c98f38bda", size = 248143, upload-time = "2026-02-09T12:56:22.585Z" }, + { url = "https://files.pythonhosted.org/packages/a8/cf/e80390c5b7480b722fa3e994f8202807799b85bc562aa4f1dde209fbb7be/coverage-7.13.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4fc7fa81bbaf5a02801b65346c8b3e657f1d93763e58c0abdf7c992addd81a92", size = 246152, upload-time = "2026-02-09T12:56:23.748Z" }, + { url = "https://files.pythonhosted.org/packages/44/bf/f89a8350d85572f95412debb0fb9bb4795b1d5b5232bd652923c759e787b/coverage-7.13.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:33901f604424145c6e9c2398684b92e176c0b12df77d52db81c20abd48c3794c", size = 249959, upload-time = "2026-02-09T12:56:25.209Z" }, + { url = "https://files.pythonhosted.org/packages/f7/6e/612a02aece8178c818df273e8d1642190c4875402ca2ba74514394b27aba/coverage-7.13.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:bb28c0f2cf2782508a40cec377935829d5fcc3ad9a3681375af4e84eb34b6b58", size = 246416, upload-time = "2026-02-09T12:56:26.475Z" }, + { url = "https://files.pythonhosted.org/packages/cb/98/b5afc39af67c2fa6786b03c3a7091fc300947387ce8914b096db8a73d67a/coverage-7.13.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9d107aff57a83222ddbd8d9ee705ede2af2cc926608b57abed8ef96b50b7e8f9", size = 247025, upload-time = "2026-02-09T12:56:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/51/30/2bba8ef0682d5bd210c38fe497e12a06c9f8d663f7025e9f5c2c31ce847d/coverage-7.13.4-cp310-cp310-win32.whl", hash = "sha256:a6f94a7d00eb18f1b6d403c91a88fd58cfc92d4b16080dfdb774afc8294469bf", size = 221758, upload-time = "2026-02-09T12:56:29.051Z" }, + { url = "https://files.pythonhosted.org/packages/78/13/331f94934cf6c092b8ea59ff868eb587bc8fe0893f02c55bc6c0183a192e/coverage-7.13.4-cp310-cp310-win_amd64.whl", hash = "sha256:2cb0f1e000ebc419632bbe04366a8990b6e32c4e0b51543a6484ffe15eaeda95", size = 222693, upload-time = "2026-02-09T12:56:30.366Z" }, + { url = "https://files.pythonhosted.org/packages/b4/ad/b59e5b451cf7172b8d1043dc0fa718f23aab379bc1521ee13d4bd9bfa960/coverage-7.13.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d490ba50c3f35dd7c17953c68f3270e7ccd1c6642e2d2afe2d8e720b98f5a053", size = 219278, upload-time = "2026-02-09T12:56:31.673Z" }, + { url = "https://files.pythonhosted.org/packages/f1/17/0cb7ca3de72e5f4ef2ec2fa0089beafbcaaaead1844e8b8a63d35173d77d/coverage-7.13.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:19bc3c88078789f8ef36acb014d7241961dbf883fd2533d18cb1e7a5b4e28b11", size = 219783, upload-time = "2026-02-09T12:56:33.104Z" }, + { url = "https://files.pythonhosted.org/packages/ab/63/325d8e5b11e0eaf6d0f6a44fad444ae58820929a9b0de943fa377fe73e85/coverage-7.13.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3998e5a32e62fdf410c0dbd3115df86297995d6e3429af80b8798aad894ca7aa", size = 250200, upload-time = "2026-02-09T12:56:34.474Z" }, + { url = "https://files.pythonhosted.org/packages/76/53/c16972708cbb79f2942922571a687c52bd109a7bd51175aeb7558dff2236/coverage-7.13.4-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e264226ec98e01a8e1054314af91ee6cde0eacac4f465cc93b03dbe0bce2fd7", size = 252114, upload-time = "2026-02-09T12:56:35.749Z" }, + { url = "https://files.pythonhosted.org/packages/eb/c2/7ab36d8b8cc412bec9ea2d07c83c48930eb4ba649634ba00cb7e4e0f9017/coverage-7.13.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3aa4e7b9e416774b21797365b358a6e827ffadaaca81b69ee02946852449f00", size = 254220, upload-time = "2026-02-09T12:56:37.796Z" }, + { url = "https://files.pythonhosted.org/packages/d6/4d/cf52c9a3322c89a0e6febdfbc83bb45c0ed3c64ad14081b9503adee702e7/coverage-7.13.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:71ca20079dd8f27fcf808817e281e90220475cd75115162218d0e27549f95fef", size = 256164, upload-time = "2026-02-09T12:56:39.016Z" }, + { url = "https://files.pythonhosted.org/packages/78/e9/eb1dd17bd6de8289df3580e967e78294f352a5df8a57ff4671ee5fc3dcd0/coverage-7.13.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e2f25215f1a359ab17320b47bcdaca3e6e6356652e8256f2441e4ef972052903", size = 250325, upload-time = "2026-02-09T12:56:40.668Z" }, + { url = "https://files.pythonhosted.org/packages/71/07/8c1542aa873728f72267c07278c5cc0ec91356daf974df21335ccdb46368/coverage-7.13.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d65b2d373032411e86960604dc4edac91fdfb5dca539461cf2cbe78327d1e64f", size = 251913, upload-time = "2026-02-09T12:56:41.97Z" }, + { url = "https://files.pythonhosted.org/packages/74/d7/c62e2c5e4483a748e27868e4c32ad3daa9bdddbba58e1bc7a15e252baa74/coverage-7.13.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94eb63f9b363180aff17de3e7c8760c3ba94664ea2695c52f10111244d16a299", size = 249974, upload-time = "2026-02-09T12:56:43.323Z" }, + { url = "https://files.pythonhosted.org/packages/98/9f/4c5c015a6e98ced54efd0f5cf8d31b88e5504ecb6857585fc0161bb1e600/coverage-7.13.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e856bf6616714c3a9fbc270ab54103f4e685ba236fa98c054e8f87f266c93505", size = 253741, upload-time = "2026-02-09T12:56:45.155Z" }, + { url = "https://files.pythonhosted.org/packages/bd/59/0f4eef89b9f0fcd9633b5d350016f54126ab49426a70ff4c4e87446cabdc/coverage-7.13.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:65dfcbe305c3dfe658492df2d85259e0d79ead4177f9ae724b6fb245198f55d6", size = 249695, upload-time = "2026-02-09T12:56:46.636Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2c/b7476f938deb07166f3eb281a385c262675d688ff4659ad56c6c6b8e2e70/coverage-7.13.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b507778ae8a4c915436ed5c2e05b4a6cecfa70f734e19c22a005152a11c7b6a9", size = 250599, upload-time = "2026-02-09T12:56:48.13Z" }, + { url = "https://files.pythonhosted.org/packages/b8/34/c3420709d9846ee3785b9f2831b4d94f276f38884032dca1457fa83f7476/coverage-7.13.4-cp311-cp311-win32.whl", hash = "sha256:784fc3cf8be001197b652d51d3fd259b1e2262888693a4636e18879f613a62a9", size = 221780, upload-time = "2026-02-09T12:56:50.479Z" }, + { url = "https://files.pythonhosted.org/packages/61/08/3d9c8613079d2b11c185b865de9a4c1a68850cfda2b357fae365cf609f29/coverage-7.13.4-cp311-cp311-win_amd64.whl", hash = "sha256:2421d591f8ca05b308cf0092807308b2facbefe54af7c02ac22548b88b95c98f", size = 222715, upload-time = "2026-02-09T12:56:51.815Z" }, + { url = "https://files.pythonhosted.org/packages/18/1a/54c3c80b2f056164cc0a6cdcb040733760c7c4be9d780fe655f356f433e4/coverage-7.13.4-cp311-cp311-win_arm64.whl", hash = "sha256:79e73a76b854d9c6088fe5d8b2ebe745f8681c55f7397c3c0a016192d681045f", size = 221385, upload-time = "2026-02-09T12:56:53.194Z" }, + { url = "https://files.pythonhosted.org/packages/d1/81/4ce2fdd909c5a0ed1f6dedb88aa57ab79b6d1fbd9b588c1ac7ef45659566/coverage-7.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02231499b08dabbe2b96612993e5fc34217cdae907a51b906ac7fca8027a4459", size = 219449, upload-time = "2026-02-09T12:56:54.889Z" }, + { url = "https://files.pythonhosted.org/packages/5d/96/5238b1efc5922ddbdc9b0db9243152c09777804fb7c02ad1741eb18a11c0/coverage-7.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40aa8808140e55dc022b15d8aa7f651b6b3d68b365ea0398f1441e0b04d859c3", size = 219810, upload-time = "2026-02-09T12:56:56.33Z" }, + { url = "https://files.pythonhosted.org/packages/78/72/2f372b726d433c9c35e56377cf1d513b4c16fe51841060d826b95caacec1/coverage-7.13.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5b856a8ccf749480024ff3bd7310adaef57bf31fd17e1bfc404b7940b6986634", size = 251308, upload-time = "2026-02-09T12:56:57.858Z" }, + { url = "https://files.pythonhosted.org/packages/5d/a0/2ea570925524ef4e00bb6c82649f5682a77fac5ab910a65c9284de422600/coverage-7.13.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c048ea43875fbf8b45d476ad79f179809c590ec7b79e2035c662e7afa3192e3", size = 254052, upload-time = "2026-02-09T12:56:59.754Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ac/45dc2e19a1939098d783c846e130b8f862fbb50d09e0af663988f2f21973/coverage-7.13.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7b38448866e83176e28086674fe7368ab8590e4610fb662b44e345b86d63ffa", size = 255165, upload-time = "2026-02-09T12:57:01.287Z" }, + { url = "https://files.pythonhosted.org/packages/2d/4d/26d236ff35abc3b5e63540d3386e4c3b192168c1d96da5cb2f43c640970f/coverage-7.13.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de6defc1c9badbf8b9e67ae90fd00519186d6ab64e5cc5f3d21359c2a9b2c1d3", size = 257432, upload-time = "2026-02-09T12:57:02.637Z" }, + { url = "https://files.pythonhosted.org/packages/ec/55/14a966c757d1348b2e19caf699415a2a4c4f7feaa4bbc6326a51f5c7dd1b/coverage-7.13.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7eda778067ad7ffccd23ecffce537dface96212576a07924cbf0d8799d2ded5a", size = 251716, upload-time = "2026-02-09T12:57:04.056Z" }, + { url = "https://files.pythonhosted.org/packages/77/33/50116647905837c66d28b2af1321b845d5f5d19be9655cb84d4a0ea806b4/coverage-7.13.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e87f6c587c3f34356c3759f0420693e35e7eb0e2e41e4c011cb6ec6ecbbf1db7", size = 253089, upload-time = "2026-02-09T12:57:05.503Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b4/8efb11a46e3665d92635a56e4f2d4529de6d33f2cb38afd47d779d15fc99/coverage-7.13.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8248977c2e33aecb2ced42fef99f2d319e9904a36e55a8a68b69207fb7e43edc", size = 251232, upload-time = "2026-02-09T12:57:06.879Z" }, + { url = "https://files.pythonhosted.org/packages/51/24/8cd73dd399b812cc76bb0ac260e671c4163093441847ffe058ac9fda1e32/coverage-7.13.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:25381386e80ae727608e662474db537d4df1ecd42379b5ba33c84633a2b36d47", size = 255299, upload-time = "2026-02-09T12:57:08.245Z" }, + { url = "https://files.pythonhosted.org/packages/03/94/0a4b12f1d0e029ce1ccc1c800944a9984cbe7d678e470bb6d3c6bc38a0da/coverage-7.13.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ee756f00726693e5ba94d6df2bdfd64d4852d23b09bb0bc700e3b30e6f333985", size = 250796, upload-time = "2026-02-09T12:57:10.142Z" }, + { url = "https://files.pythonhosted.org/packages/73/44/6002fbf88f6698ca034360ce474c406be6d5a985b3fdb3401128031eef6b/coverage-7.13.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fdfc1e28e7c7cdce44985b3043bc13bbd9c747520f94a4d7164af8260b3d91f0", size = 252673, upload-time = "2026-02-09T12:57:12.197Z" }, + { url = "https://files.pythonhosted.org/packages/de/c6/a0279f7c00e786be75a749a5674e6fa267bcbd8209cd10c9a450c655dfa7/coverage-7.13.4-cp312-cp312-win32.whl", hash = "sha256:01d4cbc3c283a17fc1e42d614a119f7f438eabb593391283adca8dc86eff1246", size = 221990, upload-time = "2026-02-09T12:57:14.085Z" }, + { url = "https://files.pythonhosted.org/packages/77/4e/c0a25a425fcf5557d9abd18419c95b63922e897bc86c1f327f155ef234a9/coverage-7.13.4-cp312-cp312-win_amd64.whl", hash = "sha256:9401ebc7ef522f01d01d45532c68c5ac40fb27113019b6b7d8b208f6e9baa126", size = 222800, upload-time = "2026-02-09T12:57:15.944Z" }, + { url = "https://files.pythonhosted.org/packages/47/ac/92da44ad9a6f4e3a7debd178949d6f3769bedca33830ce9b1dcdab589a37/coverage-7.13.4-cp312-cp312-win_arm64.whl", hash = "sha256:b1ec7b6b6e93255f952e27ab58fbc68dcc468844b16ecbee881aeb29b6ab4d8d", size = 221415, upload-time = "2026-02-09T12:57:17.497Z" }, + { url = "https://files.pythonhosted.org/packages/db/23/aad45061a31677d68e47499197a131eea55da4875d16c1f42021ab963503/coverage-7.13.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b66a2da594b6068b48b2692f043f35d4d3693fb639d5ea8b39533c2ad9ac3ab9", size = 219474, upload-time = "2026-02-09T12:57:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/a5/70/9b8b67a0945f3dfec1fd896c5cefb7c19d5a3a6d74630b99a895170999ae/coverage-7.13.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3599eb3992d814d23b35c536c28df1a882caa950f8f507cef23d1cbf334995ac", size = 219844, upload-time = "2026-02-09T12:57:20.66Z" }, + { url = "https://files.pythonhosted.org/packages/97/fd/7e859f8fab324cef6c4ad7cff156ca7c489fef9179d5749b0c8d321281c2/coverage-7.13.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:93550784d9281e374fb5a12bf1324cc8a963fd63b2d2f223503ef0fd4aa339ea", size = 250832, upload-time = "2026-02-09T12:57:22.007Z" }, + { url = "https://files.pythonhosted.org/packages/e4/dc/b2442d10020c2f52617828862d8b6ee337859cd8f3a1f13d607dddda9cf7/coverage-7.13.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b720ce6a88a2755f7c697c23268ddc47a571b88052e6b155224347389fdf6a3b", size = 253434, upload-time = "2026-02-09T12:57:23.339Z" }, + { url = "https://files.pythonhosted.org/packages/5a/88/6728a7ad17428b18d836540630487231f5470fb82454871149502f5e5aa2/coverage-7.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b322db1284a2ed3aa28ffd8ebe3db91c929b7a333c0820abec3d838ef5b3525", size = 254676, upload-time = "2026-02-09T12:57:24.774Z" }, + { url = "https://files.pythonhosted.org/packages/7c/bc/21244b1b8cedf0dff0a2b53b208015fe798d5f2a8d5348dbfece04224fff/coverage-7.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4594c67d8a7c89cf922d9df0438c7c7bb022ad506eddb0fdb2863359ff78242", size = 256807, upload-time = "2026-02-09T12:57:26.125Z" }, + { url = "https://files.pythonhosted.org/packages/97/a0/ddba7ed3251cff51006737a727d84e05b61517d1784a9988a846ba508877/coverage-7.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:53d133df809c743eb8bce33b24bcababb371f4441340578cd406e084d94a6148", size = 251058, upload-time = "2026-02-09T12:57:27.614Z" }, + { url = "https://files.pythonhosted.org/packages/9b/55/e289addf7ff54d3a540526f33751951bf0878f3809b47f6dfb3def69c6f7/coverage-7.13.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76451d1978b95ba6507a039090ba076105c87cc76fc3efd5d35d72093964d49a", size = 252805, upload-time = "2026-02-09T12:57:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/13/4e/cc276b1fa4a59be56d96f1dabddbdc30f4ba22e3b1cd42504c37b3313255/coverage-7.13.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7f57b33491e281e962021de110b451ab8a24182589be17e12a22c79047935e23", size = 250766, upload-time = "2026-02-09T12:57:30.522Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/1093b8f93018f8b41a8cf29636c9292502f05e4a113d4d107d14a3acd044/coverage-7.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1731dc33dc276dafc410a885cbf5992f1ff171393e48a21453b78727d090de80", size = 254923, upload-time = "2026-02-09T12:57:31.946Z" }, + { url = "https://files.pythonhosted.org/packages/8b/55/ea2796da2d42257f37dbea1aab239ba9263b31bd91d5527cdd6db5efe174/coverage-7.13.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:bd60d4fe2f6fa7dff9223ca1bbc9f05d2b6697bc5961072e5d3b952d46e1b1ea", size = 250591, upload-time = "2026-02-09T12:57:33.842Z" }, + { url = "https://files.pythonhosted.org/packages/d4/fa/7c4bb72aacf8af5020675aa633e59c1fbe296d22aed191b6a5b711eb2bc7/coverage-7.13.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9181a3ccead280b828fae232df12b16652702b49d41e99d657f46cc7b1f6ec7a", size = 252364, upload-time = "2026-02-09T12:57:35.743Z" }, + { url = "https://files.pythonhosted.org/packages/5c/38/a8d2ec0146479c20bbaa7181b5b455a0c41101eed57f10dd19a78ab44c80/coverage-7.13.4-cp313-cp313-win32.whl", hash = "sha256:f53d492307962561ac7de4cd1de3e363589b000ab69617c6156a16ba7237998d", size = 222010, upload-time = "2026-02-09T12:57:37.25Z" }, + { url = "https://files.pythonhosted.org/packages/e2/0c/dbfafbe90a185943dcfbc766fe0e1909f658811492d79b741523a414a6cc/coverage-7.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:e6f70dec1cc557e52df5306d051ef56003f74d56e9c4dd7ddb07e07ef32a84dd", size = 222818, upload-time = "2026-02-09T12:57:38.734Z" }, + { url = "https://files.pythonhosted.org/packages/04/d1/934918a138c932c90d78301f45f677fb05c39a3112b96fd2c8e60503cdc7/coverage-7.13.4-cp313-cp313-win_arm64.whl", hash = "sha256:fb07dc5da7e849e2ad31a5d74e9bece81f30ecf5a42909d0a695f8bd1874d6af", size = 221438, upload-time = "2026-02-09T12:57:40.223Z" }, + { url = "https://files.pythonhosted.org/packages/52/57/ee93ced533bcb3e6df961c0c6e42da2fc6addae53fb95b94a89b1e33ebd7/coverage-7.13.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40d74da8e6c4b9ac18b15331c4b5ebc35a17069410cad462ad4f40dcd2d50c0d", size = 220165, upload-time = "2026-02-09T12:57:41.639Z" }, + { url = "https://files.pythonhosted.org/packages/c5/e0/969fc285a6fbdda49d91af278488d904dcd7651b2693872f0ff94e40e84a/coverage-7.13.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4223b4230a376138939a9173f1bdd6521994f2aff8047fae100d6d94d50c5a12", size = 220516, upload-time = "2026-02-09T12:57:44.215Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b8/9531944e16267e2735a30a9641ff49671f07e8138ecf1ca13db9fd2560c7/coverage-7.13.4-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1d4be36a5114c499f9f1f9195e95ebf979460dbe2d88e6816ea202010ba1c34b", size = 261804, upload-time = "2026-02-09T12:57:45.989Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f3/e63df6d500314a2a60390d1989240d5f27318a7a68fa30ad3806e2a9323e/coverage-7.13.4-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:200dea7d1e8095cc6e98cdabe3fd1d21ab17d3cee6dab00cadbb2fe35d9c15b9", size = 263885, upload-time = "2026-02-09T12:57:47.42Z" }, + { url = "https://files.pythonhosted.org/packages/f3/67/7654810de580e14b37670b60a09c599fa348e48312db5b216d730857ffe6/coverage-7.13.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8eb931ee8e6d8243e253e5ed7336deea6904369d2fd8ae6e43f68abbf167092", size = 266308, upload-time = "2026-02-09T12:57:49.345Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/39d41eca0eab3cc82115953ad41c4e77935286c930e8fad15eaed1389d83/coverage-7.13.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75eab1ebe4f2f64d9509b984f9314d4aa788540368218b858dad56dc8f3e5eb9", size = 267452, upload-time = "2026-02-09T12:57:50.811Z" }, + { url = "https://files.pythonhosted.org/packages/50/6d/39c0fbb8fc5cd4d2090811e553c2108cf5112e882f82505ee7495349a6bf/coverage-7.13.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c35eb28c1d085eb7d8c9b3296567a1bebe03ce72962e932431b9a61f28facf26", size = 261057, upload-time = "2026-02-09T12:57:52.447Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a2/60010c669df5fa603bb5a97fb75407e191a846510da70ac657eb696b7fce/coverage-7.13.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb88b316ec33760714a4720feb2816a3a59180fd58c1985012054fa7aebee4c2", size = 263875, upload-time = "2026-02-09T12:57:53.938Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d9/63b22a6bdbd17f1f96e9ed58604c2a6b0e72a9133e37d663bef185877cf6/coverage-7.13.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7d41eead3cc673cbd38a4417deb7fd0b4ca26954ff7dc6078e33f6ff97bed940", size = 261500, upload-time = "2026-02-09T12:57:56.012Z" }, + { url = "https://files.pythonhosted.org/packages/70/bf/69f86ba1ad85bc3ad240e4c0e57a2e620fbc0e1645a47b5c62f0e941ad7f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:fb26a934946a6afe0e326aebe0730cdff393a8bc0bbb65a2f41e30feddca399c", size = 265212, upload-time = "2026-02-09T12:57:57.5Z" }, + { url = "https://files.pythonhosted.org/packages/ae/f2/5f65a278a8c2148731831574c73e42f57204243d33bedaaf18fa79c5958f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:dae88bc0fc77edaa65c14be099bd57ee140cf507e6bfdeea7938457ab387efb0", size = 260398, upload-time = "2026-02-09T12:57:59.027Z" }, + { url = "https://files.pythonhosted.org/packages/ef/80/6e8280a350ee9fea92f14b8357448a242dcaa243cb2c72ab0ca591f66c8c/coverage-7.13.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:845f352911777a8e722bfce168958214951e07e47e5d5d9744109fa5fe77f79b", size = 262584, upload-time = "2026-02-09T12:58:01.129Z" }, + { url = "https://files.pythonhosted.org/packages/22/63/01ff182fc95f260b539590fb12c11ad3e21332c15f9799cb5e2386f71d9f/coverage-7.13.4-cp313-cp313t-win32.whl", hash = "sha256:2fa8d5f8de70688a28240de9e139fa16b153cc3cbb01c5f16d88d6505ebdadf9", size = 222688, upload-time = "2026-02-09T12:58:02.736Z" }, + { url = "https://files.pythonhosted.org/packages/a9/43/89de4ef5d3cd53b886afa114065f7e9d3707bdb3e5efae13535b46ae483d/coverage-7.13.4-cp313-cp313t-win_amd64.whl", hash = "sha256:9351229c8c8407645840edcc277f4a2d44814d1bc34a2128c11c2a031d45a5dd", size = 223746, upload-time = "2026-02-09T12:58:05.362Z" }, + { url = "https://files.pythonhosted.org/packages/35/39/7cf0aa9a10d470a5309b38b289b9bb07ddeac5d61af9b664fe9775a4cb3e/coverage-7.13.4-cp313-cp313t-win_arm64.whl", hash = "sha256:30b8d0512f2dc8c8747557e8fb459d6176a2c9e5731e2b74d311c03b78451997", size = 222003, upload-time = "2026-02-09T12:58:06.952Z" }, + { url = "https://files.pythonhosted.org/packages/92/11/a9cf762bb83386467737d32187756a42094927150c3e107df4cb078e8590/coverage-7.13.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:300deaee342f90696ed186e3a00c71b5b3d27bffe9e827677954f4ee56969601", size = 219522, upload-time = "2026-02-09T12:58:08.623Z" }, + { url = "https://files.pythonhosted.org/packages/d3/28/56e6d892b7b052236d67c95f1936b6a7cf7c3e2634bf27610b8cbd7f9c60/coverage-7.13.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29e3220258d682b6226a9b0925bc563ed9a1ebcff3cad30f043eceea7eaf2689", size = 219855, upload-time = "2026-02-09T12:58:10.176Z" }, + { url = "https://files.pythonhosted.org/packages/e5/69/233459ee9eb0c0d10fcc2fe425a029b3fa5ce0f040c966ebce851d030c70/coverage-7.13.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:391ee8f19bef69210978363ca930f7328081c6a0152f1166c91f0b5fdd2a773c", size = 250887, upload-time = "2026-02-09T12:58:12.503Z" }, + { url = "https://files.pythonhosted.org/packages/06/90/2cdab0974b9b5bbc1623f7876b73603aecac11b8d95b85b5b86b32de5eab/coverage-7.13.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0dd7ab8278f0d58a0128ba2fca25824321f05d059c1441800e934ff2efa52129", size = 253396, upload-time = "2026-02-09T12:58:14.615Z" }, + { url = "https://files.pythonhosted.org/packages/ac/15/ea4da0f85bf7d7b27635039e649e99deb8173fe551096ea15017f7053537/coverage-7.13.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78cdf0d578b15148b009ccf18c686aa4f719d887e76e6b40c38ffb61d264a552", size = 254745, upload-time = "2026-02-09T12:58:16.162Z" }, + { url = "https://files.pythonhosted.org/packages/99/11/bb356e86920c655ca4d61daee4e2bbc7258f0a37de0be32d233b561134ff/coverage-7.13.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:48685fee12c2eb3b27c62f2658e7ea21e9c3239cba5a8a242801a0a3f6a8c62a", size = 257055, upload-time = "2026-02-09T12:58:17.892Z" }, + { url = "https://files.pythonhosted.org/packages/c9/0f/9ae1f8cb17029e09da06ca4e28c9e1d5c1c0a511c7074592e37e0836c915/coverage-7.13.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4e83efc079eb39480e6346a15a1bcb3e9b04759c5202d157e1dd4303cd619356", size = 250911, upload-time = "2026-02-09T12:58:19.495Z" }, + { url = "https://files.pythonhosted.org/packages/89/3a/adfb68558fa815cbc29747b553bc833d2150228f251b127f1ce97e48547c/coverage-7.13.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecae9737b72408d6a950f7e525f30aca12d4bd8dd95e37342e5beb3a2a8c4f71", size = 252754, upload-time = "2026-02-09T12:58:21.064Z" }, + { url = "https://files.pythonhosted.org/packages/32/b1/540d0c27c4e748bd3cd0bd001076ee416eda993c2bae47a73b7cc9357931/coverage-7.13.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ae4578f8528569d3cf303fef2ea569c7f4c4059a38c8667ccef15c6e1f118aa5", size = 250720, upload-time = "2026-02-09T12:58:22.622Z" }, + { url = "https://files.pythonhosted.org/packages/c7/95/383609462b3ffb1fe133014a7c84fc0dd01ed55ac6140fa1093b5af7ebb1/coverage-7.13.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6fdef321fdfbb30a197efa02d48fcd9981f0d8ad2ae8903ac318adc653f5df98", size = 254994, upload-time = "2026-02-09T12:58:24.548Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ba/1761138e86c81680bfc3c49579d66312865457f9fe405b033184e5793cb3/coverage-7.13.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b0f6ccf3dbe577170bebfce1318707d0e8c3650003cb4b3a9dd744575daa8b5", size = 250531, upload-time = "2026-02-09T12:58:26.271Z" }, + { url = "https://files.pythonhosted.org/packages/f8/8e/05900df797a9c11837ab59c4d6fe94094e029582aab75c3309a93e6fb4e3/coverage-7.13.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75fcd519f2a5765db3f0e391eb3b7d150cce1a771bf4c9f861aeab86c767a3c0", size = 252189, upload-time = "2026-02-09T12:58:27.807Z" }, + { url = "https://files.pythonhosted.org/packages/00/bd/29c9f2db9ea4ed2738b8a9508c35626eb205d51af4ab7bf56a21a2e49926/coverage-7.13.4-cp314-cp314-win32.whl", hash = "sha256:8e798c266c378da2bd819b0677df41ab46d78065fb2a399558f3f6cae78b2fbb", size = 222258, upload-time = "2026-02-09T12:58:29.441Z" }, + { url = "https://files.pythonhosted.org/packages/a7/4d/1f8e723f6829977410efeb88f73673d794075091c8c7c18848d273dc9d73/coverage-7.13.4-cp314-cp314-win_amd64.whl", hash = "sha256:245e37f664d89861cf2329c9afa2c1fe9e6d4e1a09d872c947e70718aeeac505", size = 223073, upload-time = "2026-02-09T12:58:31.026Z" }, + { url = "https://files.pythonhosted.org/packages/51/5b/84100025be913b44e082ea32abcf1afbf4e872f5120b7a1cab1d331b1e13/coverage-7.13.4-cp314-cp314-win_arm64.whl", hash = "sha256:ad27098a189e5838900ce4c2a99f2fe42a0bf0c2093c17c69b45a71579e8d4a2", size = 221638, upload-time = "2026-02-09T12:58:32.599Z" }, + { url = "https://files.pythonhosted.org/packages/a7/e4/c884a405d6ead1370433dad1e3720216b4f9fd8ef5b64bfd984a2a60a11a/coverage-7.13.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:85480adfb35ffc32d40918aad81b89c69c9cc5661a9b8a81476d3e645321a056", size = 220246, upload-time = "2026-02-09T12:58:34.181Z" }, + { url = "https://files.pythonhosted.org/packages/81/5c/4d7ed8b23b233b0fffbc9dfec53c232be2e695468523242ea9fd30f97ad2/coverage-7.13.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:79be69cf7f3bf9b0deeeb062eab7ac7f36cd4cc4c4dd694bd28921ba4d8596cc", size = 220514, upload-time = "2026-02-09T12:58:35.704Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6f/3284d4203fd2f28edd73034968398cd2d4cb04ab192abc8cff007ea35679/coverage-7.13.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:caa421e2684e382c5d8973ac55e4f36bed6821a9bad5c953494de960c74595c9", size = 261877, upload-time = "2026-02-09T12:58:37.864Z" }, + { url = "https://files.pythonhosted.org/packages/09/aa/b672a647bbe1556a85337dc95bfd40d146e9965ead9cc2fe81bde1e5cbce/coverage-7.13.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14375934243ee05f56c45393fe2ce81fe5cc503c07cee2bdf1725fb8bef3ffaf", size = 264004, upload-time = "2026-02-09T12:58:39.492Z" }, + { url = "https://files.pythonhosted.org/packages/79/a1/aa384dbe9181f98bba87dd23dda436f0c6cf2e148aecbb4e50fc51c1a656/coverage-7.13.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25a41c3104d08edb094d9db0d905ca54d0cd41c928bb6be3c4c799a54753af55", size = 266408, upload-time = "2026-02-09T12:58:41.852Z" }, + { url = "https://files.pythonhosted.org/packages/53/5e/5150bf17b4019bc600799f376bb9606941e55bd5a775dc1e096b6ffea952/coverage-7.13.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f01afcff62bf9a08fb32b2c1d6e924236c0383c02c790732b6537269e466a72", size = 267544, upload-time = "2026-02-09T12:58:44.093Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ed/f1de5c675987a4a7a672250d2c5c9d73d289dbf13410f00ed7181d8017dd/coverage-7.13.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eb9078108fbf0bcdde37c3f4779303673c2fa1fe8f7956e68d447d0dd426d38a", size = 260980, upload-time = "2026-02-09T12:58:45.721Z" }, + { url = "https://files.pythonhosted.org/packages/b3/e3/fe758d01850aa172419a6743fe76ba8b92c29d181d4f676ffe2dae2ba631/coverage-7.13.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e086334e8537ddd17e5f16a344777c1ab8194986ec533711cbe6c41cde841b6", size = 263871, upload-time = "2026-02-09T12:58:47.334Z" }, + { url = "https://files.pythonhosted.org/packages/b6/76/b829869d464115e22499541def9796b25312b8cf235d3bb00b39f1675395/coverage-7.13.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:725d985c5ab621268b2edb8e50dfe57633dc69bda071abc470fed55a14935fd3", size = 261472, upload-time = "2026-02-09T12:58:48.995Z" }, + { url = "https://files.pythonhosted.org/packages/14/9e/caedb1679e73e2f6ad240173f55218488bfe043e38da577c4ec977489915/coverage-7.13.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3c06f0f1337c667b971ca2f975523347e63ec5e500b9aa5882d91931cd3ef750", size = 265210, upload-time = "2026-02-09T12:58:51.178Z" }, + { url = "https://files.pythonhosted.org/packages/3a/10/0dd02cb009b16ede425b49ec344aba13a6ae1dc39600840ea6abcb085ac4/coverage-7.13.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:590c0ed4bf8e85f745e6b805b2e1c457b2e33d5255dd9729743165253bc9ad39", size = 260319, upload-time = "2026-02-09T12:58:53.081Z" }, + { url = "https://files.pythonhosted.org/packages/92/8e/234d2c927af27c6d7a5ffad5bd2cf31634c46a477b4c7adfbfa66baf7ebb/coverage-7.13.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eb30bf180de3f632cd043322dad5751390e5385108b2807368997d1a92a509d0", size = 262638, upload-time = "2026-02-09T12:58:55.258Z" }, + { url = "https://files.pythonhosted.org/packages/2f/64/e5547c8ff6964e5965c35a480855911b61509cce544f4d442caa759a0702/coverage-7.13.4-cp314-cp314t-win32.whl", hash = "sha256:c4240e7eded42d131a2d2c4dec70374b781b043ddc79a9de4d55ca71f8e98aea", size = 223040, upload-time = "2026-02-09T12:58:56.936Z" }, + { url = "https://files.pythonhosted.org/packages/c7/96/38086d58a181aac86d503dfa9c47eb20715a79c3e3acbdf786e92e5c09a8/coverage-7.13.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4c7d3cc01e7350f2f0f6f7036caaf5673fb56b6998889ccfe9e1c1fe75a9c932", size = 224148, upload-time = "2026-02-09T12:58:58.645Z" }, + { url = "https://files.pythonhosted.org/packages/ce/72/8d10abd3740a0beb98c305e0c3faf454366221c0f37a8bcf8f60020bb65a/coverage-7.13.4-cp314-cp314t-win_arm64.whl", hash = "sha256:23e3f687cf945070d1c90f85db66d11e3025665d8dafa831301a0e0038f3db9b", size = 222172, upload-time = "2026-02-09T12:59:00.396Z" }, + { url = "https://files.pythonhosted.org/packages/0d/4a/331fe2caf6799d591109bb9c08083080f6de90a823695d412a935622abb2/coverage-7.13.4-py3-none-any.whl", hash = "sha256:1af1641e57cf7ba1bd67d677c9abdbcd6cc2ab7da3bca7fa1e2b7e50e65f2ad0", size = 211242, upload-time = "2026-02-09T12:59:02.032Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "darglint" +version = "1.8.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/2c/86e8549e349388c18ca8a4ff8661bb5347da550f598656d32a98eaaf91cc/darglint-1.8.1.tar.gz", hash = "sha256:080d5106df149b199822e7ee7deb9c012b49891538f14a11be681044f0bb20da", size = 74435, upload-time = "2021-10-18T03:40:37.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/28/85d1e0396d64422c5218d68e5cdcc53153aa8a2c83c7dbc3ee1502adf3a1/darglint-1.8.1-py3-none-any.whl", hash = "sha256:5ae11c259c17b0701618a20c3da343a3eb98b3bc4b5a83d31cdd94f5ebdced8d", size = 120767, upload-time = "2021-10-18T03:40:35.034Z" }, +] + +[[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, upload-time = "2025-07-17T16:52:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "filelock" +version = "3.25.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/8b/4c32ecde6bea6486a2a5d05340e695174351ff6b06cf651a74c005f9df00/filelock-3.25.1.tar.gz", hash = "sha256:b9a2e977f794ef94d77cdf7d27129ac648a61f585bff3ca24630c1629f701aa9", size = 40319, upload-time = "2026-03-09T19:38:47.309Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/b8/2f664b56a3b4b32d28d3d106c71783073f712ba43ff6d34b9ea0ce36dc7b/filelock-3.25.1-py3-none-any.whl", hash = "sha256:18972df45473c4aa2c7921b609ee9ca4925910cc3a0fb226c96b92fc224ef7bf", size = 26720, upload-time = "2026-03-09T19:38:45.718Z" }, +] + +[[package]] +name = "granian" +version = "2.7.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/19/d4ea523715ba8dd2ed295932cc3dda6bb197060f78aada6e886ff08587b2/granian-2.7.2.tar.gz", hash = "sha256:cdae2f3a26fa998d41fefad58f1d1c84a0b035a6cc9377addd81b51ba82f927f", size = 128969, upload-time = "2026-02-24T23:04:23.314Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/d4/25d4315e1bb07bb7ad9ce4558773ceaac70b023450e672586dac48675241/granian-2.7.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:c3a2e5734fc71d3b8c64ecb614570836b2c37a7df293d78de981a10f756464b0", size = 6522657, upload-time = "2026-02-24T23:01:57.987Z" }, + { url = "https://files.pythonhosted.org/packages/da/ed/dc91f663d3681ba480ff028cb96fefde0b9db5318d1508b6a69691d4a78e/granian-2.7.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ae8786a13e8054c0dda96e9f22e27c5d94dcad72fc89de191ae5ea83e2d6b023", size = 6136126, upload-time = "2026-02-24T23:01:59.646Z" }, + { url = "https://files.pythonhosted.org/packages/cf/d1/df30d998fab7317b4c9f81f76955c230d9b7e7a16d730388e7013b4c820d/granian-2.7.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3f4287275dd360935ed3d95da67da104d96ad9a19d4b9fde176dc7c3becf3553", size = 7138802, upload-time = "2026-02-24T23:02:01.173Z" }, + { url = "https://files.pythonhosted.org/packages/27/8f/6b691e419d6ec78724351233a521c3c00131fc279b864ac9d374191bfcbe/granian-2.7.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:24925bc301707c8bb21dd87a668e806a972a818a1929caa47f7af5f3108816fb", size = 6467262, upload-time = "2026-02-24T23:02:03.319Z" }, + { url = "https://files.pythonhosted.org/packages/8d/28/7eb0ea0632b119ccd03f7ed6ca3a7bf6642fb7c680a7a5813a2a22b9dd17/granian-2.7.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2edda56ab8f15a83503b9e753785b0bcb01e7476418cb2326e683b3f2ca26202", size = 6869805, upload-time = "2026-02-24T23:02:05.357Z" }, + { url = "https://files.pythonhosted.org/packages/68/6a/91022ee4da3bd616ac2426aed2154c97dac8ef3cd50ac3d971d07ed168d6/granian-2.7.2-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:2c7e1303e5b913298e3d7ada3a4a72c4ce75589574eee744ef30bc123a836a55", size = 7035362, upload-time = "2026-02-24T23:02:07.749Z" }, + { url = "https://files.pythonhosted.org/packages/c3/9f/c3bbfb9f1c7327b5deb68b69c03bc1f7cf55e723307d5ddc6e339f604aeb/granian-2.7.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ec6027a53d88eca16620f0c617f4efa9961bfea74a48166619a291545bd9fc38", size = 7122757, upload-time = "2026-02-24T23:02:09.505Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e8/bf8f20535b1e830c7839439b61221885edd9449ab2261d5cbec9a84e20bb/granian-2.7.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:4b8b409cfe743130dad1f59edee9f729e6fe5e86bcba1cea633e8cc045beb6f5", size = 7302495, upload-time = "2026-02-24T23:02:10.912Z" }, + { url = "https://files.pythonhosted.org/packages/2e/42/a767291821f3ac4aa57c1aa2fff729f9d547f256fc91a2e946dc66acf585/granian-2.7.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bd2d0438d07a1f5daba035825a86abdafc44285ad4062c2a867dec107f0742c5", size = 7010685, upload-time = "2026-02-24T23:02:12.836Z" }, + { url = "https://files.pythonhosted.org/packages/fe/8a/c18c2faf5dd7aacb016ffb513fa2b2b72ac7f8c9deb29e322595c030ab5a/granian-2.7.2-cp310-cp310-win_amd64.whl", hash = "sha256:3627a82c87a066f37ec15dae6f63372e748fc6a0d722c165700dfd4a466baa37", size = 4158971, upload-time = "2026-02-24T23:02:14.344Z" }, + { url = "https://files.pythonhosted.org/packages/f8/58/dcf0e8a54b9a7f8b7482ed617bca08503a47eb6b702aea73cda9efd2c81c/granian-2.7.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3a0d33ada95a1421e5a22d447d918e5615ff0aa37f12de5b84455afe89970875", size = 6522860, upload-time = "2026-02-24T23:02:15.901Z" }, + { url = "https://files.pythonhosted.org/packages/2b/dd/398de0f273fdcf0e96bd70d8cd97364625176990e67457f11e23f95772bd/granian-2.7.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ee26f0258cc1b6ccf87c7bdcee6d1f90710505522fc9880ec02b299fb15679ad", size = 6135934, upload-time = "2026-02-24T23:02:18.52Z" }, + { url = "https://files.pythonhosted.org/packages/67/b7/7bf635bbdfb88dfc6591fa2ce5c3837ab9535e57e197a780c4a338363de7/granian-2.7.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f52338cfab08b8cdaadaa5b93665e0be5b4c4f718fbd132d76ceacacb9ff864e", size = 7138393, upload-time = "2026-02-24T23:02:19.911Z" }, + { url = "https://files.pythonhosted.org/packages/0a/90/e424fd8a703add1e8922390503be8d057882b35b42ba51796157aabd659b/granian-2.7.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e377d03a638fecb6949ab05c8fd4a76f892993aed17c602d179bfd56aebc2de", size = 6467189, upload-time = "2026-02-24T23:02:21.896Z" }, + { url = "https://files.pythonhosted.org/packages/65/9a/5de24d7e2dba1aa9fbac6f0a80dace975cfac1b7c7624ece21da75a38987/granian-2.7.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f742f3ca1797a746fae4a9337fe5d966460c957fa8efeaccf464b872e158d3d", size = 6870813, upload-time = "2026-02-24T23:02:23.972Z" }, + { url = "https://files.pythonhosted.org/packages/ac/cd/a604e38237857f4ad4262eadc409f94fe08fed3e86fa0b8734479cc5bfb1/granian-2.7.2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:ca4402e8f28a958f0c0f6ebff94cd0b04ca79690aded785648a438bc3c875ba3", size = 7046583, upload-time = "2026-02-24T23:02:25.94Z" }, + { url = "https://files.pythonhosted.org/packages/cc/ad/79eaae0cddd90c4e191b37674cedd8f4863b44465cb435b10396d0f12c82/granian-2.7.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1f9a899123b0d084783626e5225608094f1d2f6fc81b3a7c77ab8daac33ab74a", size = 7121958, upload-time = "2026-02-24T23:02:27.641Z" }, + { url = "https://files.pythonhosted.org/packages/ca/51/e5c923b1baa003f5b4b7fc148be6f8d2e3cabe55d41040fe8139da52e31b/granian-2.7.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:56ba4bef79d0ae3736328038deed2b5d281b11672bc0b08ffc8ce6210e406ef8", size = 7303047, upload-time = "2026-02-24T23:02:30.863Z" }, + { url = "https://files.pythonhosted.org/packages/06/c0/ebd68144a3ce9ead1a3192ac02e1c26e4874df1257435ce6137adf92fedb/granian-2.7.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ea46e3f43d94715aa89d1f2f5754753d46e6b653d561b82b0291e62a31bdfb35", size = 7011349, upload-time = "2026-02-24T23:02:32.887Z" }, + { url = "https://files.pythonhosted.org/packages/53/92/db2978c55dee2330868615526797a503f5d68f05dac05cb2e3a277e2b17c/granian-2.7.2-cp311-cp311-win_amd64.whl", hash = "sha256:7f8d5d16a1cd277cc6244f13f97d924b4cc0ea24506a71ca21b0546541d57ab0", size = 4157888, upload-time = "2026-02-24T23:02:34.429Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ed/37f5d7d887ec9159dd8f5b1c9c38cee711d51016d203959f2d51c536a33b/granian-2.7.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a836f3f8ebfe61cb25d9afb655f2e5d3851154fd2ad97d47bb4fb202817212fc", size = 6451593, upload-time = "2026-02-24T23:02:36.203Z" }, + { url = "https://files.pythonhosted.org/packages/1e/06/84ee67a68504836a52c48ec3b4b2b406cbd927c9b43aae89d82db8d097a0/granian-2.7.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:09b1c543ba30886dea515a156baf6d857bbb8b57dbfd8b012c578b93c80ef0c3", size = 6101239, upload-time = "2026-02-24T23:02:37.636Z" }, + { url = "https://files.pythonhosted.org/packages/ed/50/ece7dc8efe144542cd626b88b1475b649e2eaa3eb5f7541ca57390151b05/granian-2.7.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6d334d4fbefb97001e78aa8067deafb107b867c102ba2120b4b2ec989fa58a89", size = 7079443, upload-time = "2026-02-24T23:02:39.651Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e8/0f37b531d3cc96b8538cca2dc86eda92102e0ee345b30aa689354194a4cb/granian-2.7.2-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c86081d8c87989db69650e9d0e50ed925b8cd5dad21e0a86aa72d7a45f45925", size = 6428683, upload-time = "2026-02-24T23:02:41.827Z" }, + { url = "https://files.pythonhosted.org/packages/47/09/228626706554b389407270e2a6b19b7dee06d6890e8c01a39c6a785827fd/granian-2.7.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9eda33dca2c8bc6471bb6e9e25863077bca3877a1bba4069cd5e0ee2de41765", size = 6959520, upload-time = "2026-02-24T23:02:43.488Z" }, + { url = "https://files.pythonhosted.org/packages/61/c0/a639ceabd59b8acae2d71b5c918fcb2d42f8ef98994eedcf9a8b6813731d/granian-2.7.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:9cf69aaff6f632074ffbe7c1ee214e50f64be36101b7cb8253eeec1d460f2dba", size = 6991548, upload-time = "2026-02-24T23:02:44.954Z" }, + { url = "https://files.pythonhosted.org/packages/b1/99/a35ed838a3095dcad02ae3944d19ebafe1d5a98cdc72bb61835fb5faf933/granian-2.7.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f761a748cc7f3843b430422d2539da679daf5d3ef0259a101b90d5e55a0aafa7", size = 7121475, upload-time = "2026-02-24T23:02:46.991Z" }, + { url = "https://files.pythonhosted.org/packages/ce/24/3952c464432b904ec1cf537d2bd80d2dfde85524fa428ab9db2b5afe653c/granian-2.7.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:41c7b8390b78647fe34662ed7296e1465dad4a5112af9b0ecf8e367083d6c76a", size = 7243647, upload-time = "2026-02-24T23:02:49.165Z" }, + { url = "https://files.pythonhosted.org/packages/c9/fa/ab39e39c6b78eab6b42cf5bb36f56badde2aaafc3807f03f781d00e7861a/granian-2.7.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a052ed466da5922cb443435a95a0c751566943278a6f22cef3d2e19d4e7ecdea", size = 7048915, upload-time = "2026-02-24T23:02:50.773Z" }, + { url = "https://files.pythonhosted.org/packages/39/64/4502918f7d92a7e668d9e2fba83e2decbbf44c8ea896bacd8551d64f1d29/granian-2.7.2-cp312-cp312-win_amd64.whl", hash = "sha256:1e438096c36ed6aa4f6c0c8dde22bebe08ac008d08257517b15182c262a08cfa", size = 4150398, upload-time = "2026-02-24T23:02:52.199Z" }, + { url = "https://files.pythonhosted.org/packages/ab/bc/cf0bc29f583096a842cf0f26ae2fe40c72ed5286d4548be99ecfcdbb17e2/granian-2.7.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:76b840ff13dde8838fd33cd096f2e7cadf2c21a499a67f695f53de57deab6ff8", size = 6440868, upload-time = "2026-02-24T23:02:53.619Z" }, + { url = "https://files.pythonhosted.org/packages/2f/0d/bae1dcd2182ba5d9a5df33eb50b56dc5bbe67e31033d822e079aa8c1ff30/granian-2.7.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:00ccc8d7284bc7360f310179d0b4d17e5ca3077bbe24427e9e9310df397e3831", size = 6097336, upload-time = "2026-02-24T23:02:55.185Z" }, + { url = "https://files.pythonhosted.org/packages/65/7d/3e0a7f32b0ad5faa1d847c51191391552fa239821c95fc7c022688985df2/granian-2.7.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:675987c1b321dc8af593db8639e00c25277449b32e8c1b2ddd46b35f28d9fac4", size = 7098742, upload-time = "2026-02-24T23:02:57.898Z" }, + { url = "https://files.pythonhosted.org/packages/89/41/3b44386d636ac6467f0f13f45474c71fc3b90a4f0ba8b536de91b2845a09/granian-2.7.2-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:681c6fbe3354aaa6251e6191ec89f5174ac3b9fbc4b4db606fea456d01969fcb", size = 6430667, upload-time = "2026-02-24T23:02:59.789Z" }, + { url = "https://files.pythonhosted.org/packages/52/70/7b24e187aed3fb7ac2b29d2480a045559a509ef9fec54cffb8694a2d94af/granian-2.7.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e5c9ae65af5e572dca27d8ca0da4c5180b08473ac47e6f5329699e9455a5cc3", size = 6948424, upload-time = "2026-02-24T23:03:01.406Z" }, + { url = "https://files.pythonhosted.org/packages/fa/4c/cb74c367f9efb874f2c8433fe9bf3e824f05cf719f2251d40e29e07f08c0/granian-2.7.2-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:e37fab2be919ceb195db00d7f49ec220444b1ecaa07c03f7c1c874cacff9de83", size = 7000407, upload-time = "2026-02-24T23:03:03.214Z" }, + { url = "https://files.pythonhosted.org/packages/58/98/dfed3966ed7fbd3aae56e123598f90dc206484092b8373d0a71e2d8b82a8/granian-2.7.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:8ec167ab30f5396b5caaff16820a39f4e91986d2fe5bdc02992a03c2b2b2b313", size = 7121626, upload-time = "2026-02-24T23:03:05.349Z" }, + { url = "https://files.pythonhosted.org/packages/39/82/acec732a345cd03b2f6e48ac04b66b7b8b61f5c50eb08d7421fc8c56591a/granian-2.7.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:63f426d793f2116d23be265dd826bec1e623680baf94cc270fe08923113a86ba", size = 7253447, upload-time = "2026-02-24T23:03:06.986Z" }, + { url = "https://files.pythonhosted.org/packages/c5/2b/64779e69b08c1ff1bfc09a4ede904ab761ff63f936c275710886057c52f7/granian-2.7.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1617cbb4efe3112f07fb6762cf81d2d9fe4bdb78971d1fd0a310f8b132f6a51e", size = 7053005, upload-time = "2026-02-24T23:03:09.021Z" }, + { url = "https://files.pythonhosted.org/packages/04/c9/83e546d5f6b0447a4b9ee48ce15c29e43bb3f6b5e1040d33ac61fc9e3b6f/granian-2.7.2-cp313-cp313-win_amd64.whl", hash = "sha256:7a4bd347694ace7a48cd784b911f2d519c2a22154e0d1ed59f5b4864914a8cfe", size = 4145886, upload-time = "2026-02-24T23:03:10.829Z" }, + { url = "https://files.pythonhosted.org/packages/4c/49/9eb88875d709db7e7844e1c681546448dab5ff5651cd1c1d80ac4b1de4e3/granian-2.7.2-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:016c5857c8baedeab7eb065f98417f5ea26bb72b0f7e0544fe76071efc5ab255", size = 6401748, upload-time = "2026-02-24T23:03:12.802Z" }, + { url = "https://files.pythonhosted.org/packages/e3/80/85726ad9999ed89cb6a32f7f57eb50ce7261459d9c30c3b194ae4c5aa2c5/granian-2.7.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dcbe01fa141adf3f90964e86a959e250754aa7c6dad8fa7a855e6fd382de4c13", size = 6101265, upload-time = "2026-02-24T23:03:14.435Z" }, + { url = "https://files.pythonhosted.org/packages/07/82/0df56a42b9f4c327d0e0b052f43369127e1b565b9e66bf2c9488f1c8d759/granian-2.7.2-cp313-cp313t-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:283ba23817a685784b66f45423d2f25715fdc076c8ffb43c49a807ee56a0ffc0", size = 6249488, upload-time = "2026-02-24T23:03:16.387Z" }, + { url = "https://files.pythonhosted.org/packages/ef/cc/d83a351560a3d6377672636129c52f06f8393f5831c5ee0f06f274883ea6/granian-2.7.2-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3258419c741897273ce155568b5a9cbacb7700a00516e87119a90f7d520d6783", size = 7104734, upload-time = "2026-02-24T23:03:17.993Z" }, + { url = "https://files.pythonhosted.org/packages/84/d1/539907ee96d0ee2bcceabb4a6a9643b75378d6dfea09b7a9e4fd22cdf977/granian-2.7.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a196125c4837491c139c9cc83541b48c408c92b9cfbbf004fd28717f9c02ad21", size = 6785504, upload-time = "2026-02-24T23:03:19.763Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/4b6f45882f8341e7c6cb824d693deb94c306be6525b483c76fb373d1e749/granian-2.7.2-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:746555ac8a2dcd9257bfe7ad58f1d7a60892bc4613df6a7d8f736692b3bb3b88", size = 6902790, upload-time = "2026-02-24T23:03:22.215Z" }, + { url = "https://files.pythonhosted.org/packages/44/b8/832970d2d4b144b87be39f5b9dfd31fdb17f298dc238a0b2100c95002cf8/granian-2.7.2-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:5ac1843c6084933a54a07d9dcae643365f1d83aaff3fd4f2676ea301185e4e8b", size = 7082682, upload-time = "2026-02-24T23:03:23.875Z" }, + { url = "https://files.pythonhosted.org/packages/38/bc/1521dbf026d1c9d2465cd54e016efd8ff6e1e72eff521071dab20dd61c44/granian-2.7.2-cp313-cp313t-musllinux_1_1_armv7l.whl", hash = "sha256:3612eb6a3f4351dd2c4df246ed0d21056c0556a6b1ed772dd865310aa55a9ba9", size = 7264742, upload-time = "2026-02-24T23:03:25.562Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/00884ab77045a2f54db90932f9d1ca522201e2a6b2cf2a9b38840db0fd54/granian-2.7.2-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:34708b145e31b4538e0556704a07454a76d6776c55c5bc3a1335e80ef6b3bae3", size = 7062571, upload-time = "2026-02-24T23:03:27.278Z" }, + { url = "https://files.pythonhosted.org/packages/ee/0e/4321e361bccb9681e1045c75e783476de5be7aa47cf05066907530772eba/granian-2.7.2-cp313-cp313t-win_amd64.whl", hash = "sha256:841c48608e55daa2fa434392397cc24175abd48bc5bcefa1e4f74b7243e36c72", size = 4098734, upload-time = "2026-02-24T23:03:28.973Z" }, + { url = "https://files.pythonhosted.org/packages/69/4a/8ce622f4f7d58e035d121b9957dd5a8929028dc99cfc5d2bf7f2aa28912c/granian-2.7.2-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:592806c28c491f9c1d1501bac706ecf5e72b73969f20f912678d53308786d658", size = 6442041, upload-time = "2026-02-24T23:03:30.986Z" }, + { url = "https://files.pythonhosted.org/packages/27/62/7d36ed38a40a68c2856b6d2a6fedd40833e7f82eb90ba0d03f2d69ffadf5/granian-2.7.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c9dcde3968b921654bde999468e97d03031f28668bc1fc145c81d8bedb0fb2a4", size = 6100793, upload-time = "2026-02-24T23:03:32.734Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c5/17fea68f4cb280c217cbd65534664722c9c9b0138c2754e20c235d70b5f4/granian-2.7.2-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6d4d78408283ec51f0fb00557856b4593947ad5b48287c04e1c22764a0ac28a5", size = 7119810, upload-time = "2026-02-24T23:03:34.807Z" }, + { url = "https://files.pythonhosted.org/packages/0a/76/35e240d107e0f158662652fd61191de4fb0c2c080e3786ca8f16c71547b7/granian-2.7.2-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66d28b078e8087f794b83822055f95caf93d83b23f47f4efcd5e2f0f7a5d8a81", size = 6450789, upload-time = "2026-02-24T23:03:36.81Z" }, + { url = "https://files.pythonhosted.org/packages/4c/55/a6d08cfecc808149a910e51c57883ab26fad69d922dc2e76fb2d87469e2d/granian-2.7.2-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ff7a93123ab339ba6cad51cc7141f8880ec47b152ce2491595bb08edda20106", size = 6902672, upload-time = "2026-02-24T23:03:38.655Z" }, + { url = "https://files.pythonhosted.org/packages/98/2e/c86d95f324248fcc5dcaf034c9f688b32f7a488f0b2a4a25e6673776107f/granian-2.7.2-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:a52effb9889f0944f0353afd6ce5a9d9aa83826d44bbf3c8013e978a3d6ef7b7", size = 6964399, upload-time = "2026-02-24T23:03:40.459Z" }, + { url = "https://files.pythonhosted.org/packages/37/4b/44fde33fe10245a3fba76bf843c387fad2d548244345115b9d87e1c40994/granian-2.7.2-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:76c987c3ca78bf7666ab053c3ed7e3af405af91b2e5ce2f1cf92634c1581e238", size = 7034929, upload-time = "2026-02-24T23:03:42.149Z" }, + { url = "https://files.pythonhosted.org/packages/90/76/38d205cb527046241a9ee4f51048bf44101c626ad4d2af16dd9d14dc1db6/granian-2.7.2-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:6590f8092c2bb6614e561ba771f084cbf72ecbc38dbf9849762ac38718085c29", size = 7259609, upload-time = "2026-02-24T23:03:43.852Z" }, + { url = "https://files.pythonhosted.org/packages/00/37/04245c7259e65f1083ce193875c6c44da4c98604d3b00a264a74dd4f042b/granian-2.7.2-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:7c1ce9b0c9446b680e9545e7fc95a75f0c53a25dedcf924b1750c3e5ba5bf908", size = 7073161, upload-time = "2026-02-24T23:03:45.655Z" }, + { url = "https://files.pythonhosted.org/packages/23/e4/28097a852d8f93f8e3be2014a81f03aa914b8a2c12ca761fac5ae1344b8b/granian-2.7.2-cp314-cp314-win_amd64.whl", hash = "sha256:a69cafb6518c630c84a9285674d45ea6f7342a6279dc25c6bd933b6fad5c55ab", size = 4121462, upload-time = "2026-02-24T23:03:47.322Z" }, + { url = "https://files.pythonhosted.org/packages/cc/07/0e56fb4f178e14b4c1fa1f6f00586ca81761ccbe2d8803f2c12b6b17a7d6/granian-2.7.2-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:a698d9b662d5648c8ae3dc01ad01688e1a8afc3525e431e7cddb841c53e5e291", size = 6415279, upload-time = "2026-02-24T23:03:48.932Z" }, + { url = "https://files.pythonhosted.org/packages/27/bc/3e69305bf34806cd852f4683deec844a2cb9a4d8888d7f172b507f6080a8/granian-2.7.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:17516095b520b3c039ddbe41a6beb2c59d554b668cc229d36d82c93154a799af", size = 6090528, upload-time = "2026-02-24T23:03:50.52Z" }, + { url = "https://files.pythonhosted.org/packages/ec/10/7d58a922b44417a6207c0a3230b0841cd7385a36fc518ac15fed16ebf6f7/granian-2.7.2-cp314-cp314t-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:96b0fd9eac60f939b3cbe44c8f32a42fdb7c1a1a9e07ca89e7795cdc7a606beb", size = 6252291, upload-time = "2026-02-24T23:03:52.248Z" }, + { url = "https://files.pythonhosted.org/packages/54/56/65776c6d759dcef9cce15bc11bdea2c64fe668088faf35d87916bd88f595/granian-2.7.2-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e50fb13e053384b8bd3823d4967606c6fd89f2b0d20e64de3ae212b85ffdfed2", size = 7106748, upload-time = "2026-02-24T23:03:53.994Z" }, + { url = "https://files.pythonhosted.org/packages/81/ee/d9ed836316607401f158ac264a3f770469d1b1edbf119402777a9eff1833/granian-2.7.2-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9bb1ef13125bc05ab2e18869ed311beaeb085a4c4c195d55d0865f5753a4c0b4", size = 6778883, upload-time = "2026-02-24T23:03:55.574Z" }, + { url = "https://files.pythonhosted.org/packages/a1/46/eabab80e07a14527c336dec6d902329399f3ba2b82dc94b6435651021359/granian-2.7.2-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b1c77189335070c6ba6b8d158518fde4c50f892753620f0b22a7552ad4347143", size = 6903426, upload-time = "2026-02-24T23:03:57.296Z" }, + { url = "https://files.pythonhosted.org/packages/24/8a/8ce186826066f6d453316229383a5be3b0b8a4130146c21f321ee64fe2cb/granian-2.7.2-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:1777166c3c853eed4440adb3cbbf34bba2b77d595bfc143a5826904a80b22f34", size = 7083877, upload-time = "2026-02-24T23:03:59.425Z" }, + { url = "https://files.pythonhosted.org/packages/cf/eb/91ed4646ce1c920ad39db0bcddb6f4755e1823002b14fb026104e3eb8bce/granian-2.7.2-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:0ffac19208ae548f3647c849579b803beaed2b50dfb0f3790ad26daac0033484", size = 7267282, upload-time = "2026-02-24T23:04:01.218Z" }, + { url = "https://files.pythonhosted.org/packages/49/2f/58cba479254530ab09132e150e4ab55362f6e875d9e82b6790477843e0aa/granian-2.7.2-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:82f34e78c1297bf5a1b6a5097e30428db98b59fce60a7387977b794855c0c3bc", size = 7054941, upload-time = "2026-02-24T23:04:03.211Z" }, + { url = "https://files.pythonhosted.org/packages/29/b3/fd13123ac936a4f79f1ba20ad67328a8d09d586262b8f28cc1cfaa555213/granian-2.7.2-cp314-cp314t-win_amd64.whl", hash = "sha256:e8b87d7ada696eec7e9023974665c83cec978cb83c205eae8fe377de20622f25", size = 4101983, upload-time = "2026-02-24T23:04:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/59/71/f21b26c7dc7a8bc9d8288552c9c12128e73f1c3f04799b6e28a0a269b9b0/granian-2.7.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5613ee8c1233a79e56e1735e19c8c70af22a8c6b5808d7c1423dc5387bee4c05", size = 6504773, upload-time = "2026-02-24T23:04:06.498Z" }, + { url = "https://files.pythonhosted.org/packages/6e/68/282fbf5418f9348f657f505dc744cdca70ac850d39a805b21395211bf099/granian-2.7.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0cd6fee79f585de2e1a90b6a311f62b3768c7cda649bc0e02908157ffa2553cc", size = 6138096, upload-time = "2026-02-24T23:04:09.138Z" }, + { url = "https://files.pythonhosted.org/packages/e7/e0/b578709020f84c07ad2ca88f77ac67fd2c62e6b16f93ff8c8d65b7d99296/granian-2.7.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e94c825f8b327114f7062d158c502a540ef5819f809e10158f0edddddaf41bb9", size = 6900043, upload-time = "2026-02-24T23:04:11.015Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2f/a2671cc160f29ccf8e605eb8fa113c01051b0d7947048c5b29eb4e603384/granian-2.7.2-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a6adea5fb8a537d18f3f2b848023151063bc45896415fdebfeb0bf0663d5a03b", size = 7040211, upload-time = "2026-02-24T23:04:13.31Z" }, + { url = "https://files.pythonhosted.org/packages/36/ce/df9bba3b211cda2d47535bb21bc040007e021e8c8adc20ce36619f903bc4/granian-2.7.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2392ab03cb92b1b2d4363f450b2d875177e10f0e22d67a4423052e6885e430f2", size = 7118085, upload-time = "2026-02-24T23:04:15.05Z" }, + { url = "https://files.pythonhosted.org/packages/a9/87/37124b2ee0cddce6ba438b0ff879ddae094ae2c92b24b28ffbe35110931f/granian-2.7.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:406c0bb1f5bf55c72cfbfdfd2ccec21299eb3f7b311d85c4889dde357fd36f33", size = 7314667, upload-time = "2026-02-24T23:04:16.783Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ac/8b142ed352bc525e3c97440aab312928beebc735927b0cf979692bfcda3b/granian-2.7.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:362a6001daa2ce62532a49df407fe545076052ef29289a76d5760064d820f48b", size = 7004934, upload-time = "2026-02-24T23:04:19.059Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a8/d9ba7a0d05303f57279ca8b17364945d5090ed90b32b92df219129039f50/granian-2.7.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:001bab0862857d3f029eb2819e88754e746547f4cd8cc28ae229090fc929fbaf", size = 4158219, upload-time = "2026-02-24T23:04:21.072Z" }, +] + +[package.optional-dependencies] +reload = [ + { name = "watchfiles" }, +] + +[[package]] +name = "greenlet" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/51/1664f6b78fc6ebbd98019a1fd730e83fa78f2db7058f72b1463d3612b8db/greenlet-3.3.2.tar.gz", hash = "sha256:2eaf067fc6d886931c7962e8c6bede15d2f01965560f3359b27c80bde2d151f2", size = 188267, upload-time = "2026-02-20T20:54:15.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/3f/9859f655d11901e7b2996c6e3d33e0caa9a1d4572c3bc61ed0faa64b2f4c/greenlet-3.3.2-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9bc885b89709d901859cf95179ec9f6bb67a3d2bb1f0e88456461bd4b7f8fd0d", size = 277747, upload-time = "2026-02-20T20:16:21.325Z" }, + { url = "https://files.pythonhosted.org/packages/fb/07/cb284a8b5c6498dbd7cba35d31380bb123d7dceaa7907f606c8ff5993cbf/greenlet-3.3.2-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b568183cf65b94919be4438dc28416b234b678c608cafac8874dfeeb2a9bbe13", size = 579202, upload-time = "2026-02-20T20:47:28.955Z" }, + { url = "https://files.pythonhosted.org/packages/ed/45/67922992b3a152f726163b19f890a85129a992f39607a2a53155de3448b8/greenlet-3.3.2-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:527fec58dc9f90efd594b9b700662ed3fb2493c2122067ac9c740d98080a620e", size = 590620, upload-time = "2026-02-20T20:55:55.581Z" }, + { url = "https://files.pythonhosted.org/packages/ad/55/9f1ebb5a825215fadcc0f7d5073f6e79e3007e3282b14b22d6aba7ca6cb8/greenlet-3.3.2-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ad0c8917dd42a819fe77e6bdfcb84e3379c0de956469301d9fd36427a1ca501f", size = 591729, upload-time = "2026-02-20T20:20:58.395Z" }, + { url = "https://files.pythonhosted.org/packages/24/b4/21f5455773d37f94b866eb3cf5caed88d6cea6dd2c6e1f9c34f463cba3ec/greenlet-3.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:97245cc10e5515dbc8c3104b2928f7f02b6813002770cfaffaf9a6e0fc2b94ef", size = 1551946, upload-time = "2026-02-20T20:49:31.102Z" }, + { url = "https://files.pythonhosted.org/packages/00/68/91f061a926abead128fe1a87f0b453ccf07368666bd59ffa46016627a930/greenlet-3.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8c1fdd7d1b309ff0da81d60a9688a8bd044ac4e18b250320a96fc68d31c209ca", size = 1618494, upload-time = "2026-02-20T20:21:06.541Z" }, + { url = "https://files.pythonhosted.org/packages/ac/78/f93e840cbaef8becaf6adafbaf1319682a6c2d8c1c20224267a5c6c8c891/greenlet-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:5d0e35379f93a6d0222de929a25ab47b5eb35b5ef4721c2b9cbcc4036129ff1f", size = 230092, upload-time = "2026-02-20T20:17:09.379Z" }, + { url = "https://files.pythonhosted.org/packages/f3/47/16400cb42d18d7a6bb46f0626852c1718612e35dcb0dffa16bbaffdf5dd2/greenlet-3.3.2-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:c56692189a7d1c7606cb794be0a8381470d95c57ce5be03fb3d0ef57c7853b86", size = 278890, upload-time = "2026-02-20T20:19:39.263Z" }, + { url = "https://files.pythonhosted.org/packages/a3/90/42762b77a5b6aa96cd8c0e80612663d39211e8ae8a6cd47c7f1249a66262/greenlet-3.3.2-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ebd458fa8285960f382841da585e02201b53a5ec2bac6b156fc623b5ce4499f", size = 581120, upload-time = "2026-02-20T20:47:30.161Z" }, + { url = "https://files.pythonhosted.org/packages/bf/6f/f3d64f4fa0a9c7b5c5b3c810ff1df614540d5aa7d519261b53fba55d4df9/greenlet-3.3.2-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a443358b33c4ec7b05b79a7c8b466f5d275025e750298be7340f8fc63dff2a55", size = 594363, upload-time = "2026-02-20T20:55:56.965Z" }, + { url = "https://files.pythonhosted.org/packages/72/83/3e06a52aca8128bdd4dcd67e932b809e76a96ab8c232a8b025b2850264c5/greenlet-3.3.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e2cd90d413acbf5e77ae41e5d3c9b3ac1d011a756d7284d7f3f2b806bbd6358", size = 594156, upload-time = "2026-02-20T20:20:59.955Z" }, + { url = "https://files.pythonhosted.org/packages/70/79/0de5e62b873e08fe3cef7dbe84e5c4bc0e8ed0c7ff131bccb8405cd107c8/greenlet-3.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:442b6057453c8cb29b4fb36a2ac689382fc71112273726e2423f7f17dc73bf99", size = 1554649, upload-time = "2026-02-20T20:49:32.293Z" }, + { url = "https://files.pythonhosted.org/packages/5a/00/32d30dee8389dc36d42170a9c66217757289e2afb0de59a3565260f38373/greenlet-3.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:45abe8eb6339518180d5a7fa47fa01945414d7cca5ecb745346fc6a87d2750be", size = 1619472, upload-time = "2026-02-20T20:21:07.966Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3a/efb2cf697fbccdf75b24e2c18025e7dfa54c4f31fab75c51d0fe79942cef/greenlet-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e692b2dae4cc7077cbb11b47d258533b48c8fde69a33d0d8a82e2fe8d8531d5", size = 230389, upload-time = "2026-02-20T20:17:18.772Z" }, + { url = "https://files.pythonhosted.org/packages/e1/a1/65bbc059a43a7e2143ec4fc1f9e3f673e04f9c7b371a494a101422ac4fd5/greenlet-3.3.2-cp311-cp311-win_arm64.whl", hash = "sha256:02b0a8682aecd4d3c6c18edf52bc8e51eacdd75c8eac52a790a210b06aa295fd", size = 229645, upload-time = "2026-02-20T20:18:18.695Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ab/1608e5a7578e62113506740b88066bf09888322a311cff602105e619bd87/greenlet-3.3.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ac8d61d4343b799d1e526db579833d72f23759c71e07181c2d2944e429eb09cd", size = 280358, upload-time = "2026-02-20T20:17:43.971Z" }, + { url = "https://files.pythonhosted.org/packages/a5/23/0eae412a4ade4e6623ff7626e38998cb9b11e9ff1ebacaa021e4e108ec15/greenlet-3.3.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ceec72030dae6ac0c8ed7591b96b70410a8be370b6a477b1dbc072856ad02bd", size = 601217, upload-time = "2026-02-20T20:47:31.462Z" }, + { url = "https://files.pythonhosted.org/packages/f8/16/5b1678a9c07098ecb9ab2dd159fafaf12e963293e61ee8d10ecb55273e5e/greenlet-3.3.2-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2a5be83a45ce6188c045bcc44b0ee037d6a518978de9a5d97438548b953a1ac", size = 611792, upload-time = "2026-02-20T20:55:58.423Z" }, + { url = "https://files.pythonhosted.org/packages/50/1f/5155f55bd71cabd03765a4aac9ac446be129895271f73872c36ebd4b04b6/greenlet-3.3.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43e99d1749147ac21dde49b99c9abffcbc1e2d55c67501465ef0930d6e78e070", size = 613875, upload-time = "2026-02-20T20:21:01.102Z" }, + { url = "https://files.pythonhosted.org/packages/fc/dd/845f249c3fcd69e32df80cdab059b4be8b766ef5830a3d0aa9d6cad55beb/greenlet-3.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c956a19350e2c37f2c48b336a3afb4bff120b36076d9d7fb68cb44e05d95b79", size = 1571467, upload-time = "2026-02-20T20:49:33.495Z" }, + { url = "https://files.pythonhosted.org/packages/2a/50/2649fe21fcc2b56659a452868e695634722a6655ba245d9f77f5656010bf/greenlet-3.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6c6f8ba97d17a1e7d664151284cb3315fc5f8353e75221ed4324f84eb162b395", size = 1640001, upload-time = "2026-02-20T20:21:09.154Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/cc802e067d02af8b60b6771cea7d57e21ef5e6659912814babb42b864713/greenlet-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:34308836d8370bddadb41f5a7ce96879b72e2fdfb4e87729330c6ab52376409f", size = 231081, upload-time = "2026-02-20T20:17:28.121Z" }, + { url = "https://files.pythonhosted.org/packages/58/2e/fe7f36ff1982d6b10a60d5e0740c759259a7d6d2e1dc41da6d96de32fff6/greenlet-3.3.2-cp312-cp312-win_arm64.whl", hash = "sha256:d3a62fa76a32b462a97198e4c9e99afb9ab375115e74e9a83ce180e7a496f643", size = 230331, upload-time = "2026-02-20T20:17:23.34Z" }, + { url = "https://files.pythonhosted.org/packages/ac/48/f8b875fa7dea7dd9b33245e37f065af59df6a25af2f9561efa8d822fde51/greenlet-3.3.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:aa6ac98bdfd716a749b84d4034486863fd81c3abde9aa3cf8eff9127981a4ae4", size = 279120, upload-time = "2026-02-20T20:19:01.9Z" }, + { url = "https://files.pythonhosted.org/packages/49/8d/9771d03e7a8b1ee456511961e1b97a6d77ae1dea4a34a5b98eee706689d3/greenlet-3.3.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab0c7e7901a00bc0a7284907273dc165b32e0d109a6713babd04471327ff7986", size = 603238, upload-time = "2026-02-20T20:47:32.873Z" }, + { url = "https://files.pythonhosted.org/packages/59/0e/4223c2bbb63cd5c97f28ffb2a8aee71bdfb30b323c35d409450f51b91e3e/greenlet-3.3.2-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d248d8c23c67d2291ffd47af766e2a3aa9fa1c6703155c099feb11f526c63a92", size = 614219, upload-time = "2026-02-20T20:55:59.817Z" }, + { url = "https://files.pythonhosted.org/packages/7a/34/259b28ea7a2a0c904b11cd36c79b8cef8019b26ee5dbe24e73b469dea347/greenlet-3.3.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6997d360a4e6a4e936c0f9625b1c20416b8a0ea18a8e19cabbefc712e7397ab", size = 616774, upload-time = "2026-02-20T20:21:02.454Z" }, + { url = "https://files.pythonhosted.org/packages/0a/03/996c2d1689d486a6e199cb0f1cf9e4aa940c500e01bdf201299d7d61fa69/greenlet-3.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64970c33a50551c7c50491671265d8954046cb6e8e2999aacdd60e439b70418a", size = 1571277, upload-time = "2026-02-20T20:49:34.795Z" }, + { url = "https://files.pythonhosted.org/packages/d9/c4/2570fc07f34a39f2caf0bf9f24b0a1a0a47bc2e8e465b2c2424821389dfc/greenlet-3.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1a9172f5bf6bd88e6ba5a84e0a68afeac9dc7b6b412b245dd64f52d83c81e55b", size = 1640455, upload-time = "2026-02-20T20:21:10.261Z" }, + { url = "https://files.pythonhosted.org/packages/91/39/5ef5aa23bc545aa0d31e1b9b55822b32c8da93ba657295840b6b34124009/greenlet-3.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:a7945dd0eab63ded0a48e4dcade82939783c172290a7903ebde9e184333ca124", size = 230961, upload-time = "2026-02-20T20:16:58.461Z" }, + { url = "https://files.pythonhosted.org/packages/62/6b/a89f8456dcb06becff288f563618e9f20deed8dd29beea14f9a168aef64b/greenlet-3.3.2-cp313-cp313-win_arm64.whl", hash = "sha256:394ead29063ee3515b4e775216cb756b2e3b4a7e55ae8fd884f17fa579e6b327", size = 230221, upload-time = "2026-02-20T20:17:37.152Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ae/8bffcbd373b57a5992cd077cbe8858fff39110480a9d50697091faea6f39/greenlet-3.3.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8d1658d7291f9859beed69a776c10822a0a799bc4bfe1bd4272bb60e62507dab", size = 279650, upload-time = "2026-02-20T20:18:00.783Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c0/45f93f348fa49abf32ac8439938726c480bd96b2a3c6f4d949ec0124b69f/greenlet-3.3.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18cb1b7337bca281915b3c5d5ae19f4e76d35e1df80f4ad3c1a7be91fadf1082", size = 650295, upload-time = "2026-02-20T20:47:34.036Z" }, + { url = "https://files.pythonhosted.org/packages/b3/de/dd7589b3f2b8372069ab3e4763ea5329940fc7ad9dcd3e272a37516d7c9b/greenlet-3.3.2-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2e47408e8ce1c6f1ceea0dffcdf6ebb85cc09e55c7af407c99f1112016e45e9", size = 662163, upload-time = "2026-02-20T20:56:01.295Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d8/09bfa816572a4d83bccd6750df1926f79158b1c36c5f73786e26dbe4ee38/greenlet-3.3.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63d10328839d1973e5ba35e98cccbca71b232b14051fd957b6f8b6e8e80d0506", size = 664160, upload-time = "2026-02-20T20:21:04.015Z" }, + { url = "https://files.pythonhosted.org/packages/48/cf/56832f0c8255d27f6c35d41b5ec91168d74ec721d85f01a12131eec6b93c/greenlet-3.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e4ab3cfb02993c8cc248ea73d7dae6cec0253e9afa311c9b37e603ca9fad2ce", size = 1619181, upload-time = "2026-02-20T20:49:36.052Z" }, + { url = "https://files.pythonhosted.org/packages/0a/23/b90b60a4aabb4cec0796e55f25ffbfb579a907c3898cd2905c8918acaa16/greenlet-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94ad81f0fd3c0c0681a018a976e5c2bd2ca2d9d94895f23e7bb1af4e8af4e2d5", size = 1687713, upload-time = "2026-02-20T20:21:11.684Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ca/2101ca3d9223a1dc125140dbc063644dca76df6ff356531eb27bc267b446/greenlet-3.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:8c4dd0f3997cf2512f7601563cc90dfb8957c0cff1e3a1b23991d4ea1776c492", size = 232034, upload-time = "2026-02-20T20:20:08.186Z" }, + { url = "https://files.pythonhosted.org/packages/f6/4a/ecf894e962a59dea60f04877eea0fd5724618da89f1867b28ee8b91e811f/greenlet-3.3.2-cp314-cp314-win_arm64.whl", hash = "sha256:cd6f9e2bbd46321ba3bbb4c8a15794d32960e3b0ae2cc4d49a1a53d314805d71", size = 231437, upload-time = "2026-02-20T20:18:59.722Z" }, + { url = "https://files.pythonhosted.org/packages/98/6d/8f2ef704e614bcf58ed43cfb8d87afa1c285e98194ab2cfad351bf04f81e/greenlet-3.3.2-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:e26e72bec7ab387ac80caa7496e0f908ff954f31065b0ffc1f8ecb1338b11b54", size = 286617, upload-time = "2026-02-20T20:19:29.856Z" }, + { url = "https://files.pythonhosted.org/packages/5e/0d/93894161d307c6ea237a43988f27eba0947b360b99ac5239ad3fe09f0b47/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b466dff7a4ffda6ca975979bab80bdadde979e29fc947ac3be4451428d8b0e4", size = 655189, upload-time = "2026-02-20T20:47:35.742Z" }, + { url = "https://files.pythonhosted.org/packages/f5/2c/d2d506ebd8abcb57386ec4f7ba20f4030cbe56eae541bc6fd6ef399c0b41/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8bddc5b73c9720bea487b3bffdb1840fe4e3656fba3bd40aa1489e9f37877ff", size = 658225, upload-time = "2026-02-20T20:56:02.527Z" }, + { url = "https://files.pythonhosted.org/packages/8e/30/3a09155fbf728673a1dea713572d2d31159f824a37c22da82127056c44e4/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b26b0f4428b871a751968285a1ac9648944cea09807177ac639b030bddebcea4", size = 657907, upload-time = "2026-02-20T20:21:05.259Z" }, + { url = "https://files.pythonhosted.org/packages/f3/fd/d05a4b7acd0154ed758797f0a43b4c0962a843bedfe980115e842c5b2d08/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1fb39a11ee2e4d94be9a76671482be9398560955c9e568550de0224e41104727", size = 1618857, upload-time = "2026-02-20T20:49:37.309Z" }, + { url = "https://files.pythonhosted.org/packages/6f/e1/50ee92a5db521de8f35075b5eff060dd43d39ebd46c2181a2042f7070385/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:20154044d9085151bc309e7689d6f7ba10027f8f5a8c0676ad398b951913d89e", size = 1680010, upload-time = "2026-02-20T20:21:13.427Z" }, + { url = "https://files.pythonhosted.org/packages/29/4b/45d90626aef8e65336bed690106d1382f7a43665e2249017e9527df8823b/greenlet-3.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c04c5e06ec3e022cbfe2cd4a846e1d4e50087444f875ff6d2c2ad8445495cf1a", size = 237086, upload-time = "2026-02-20T20:20:45.786Z" }, +] + +[[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, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[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, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[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, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "identify" +version = "2.6.17" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/84/376a3b96e5a8d33a7aa2c5b3b31a4b3c364117184bf0b17418055f6ace66/identify-2.6.17.tar.gz", hash = "sha256:f816b0b596b204c9fdf076ded172322f2723cf958d02f9c3587504834c8ff04d", size = 99579, upload-time = "2026-03-01T20:04:12.702Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/66/71c1227dff78aaeb942fed29dd5651f2aec166cc7c9aeea3e8b26a539b7d/identify-2.6.17-py2.py3-none-any.whl", hash = "sha256:be5f8412d5ed4b20f2bd41a65f920990bdccaa6a4a18a08f1eefdcd0bdd885f0", size = 99382, upload-time = "2026-03-01T20:04:11.439Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "mako" +version = "1.3.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/38/bd5b78a920a64d708fe6bc8e0a2c075e1389d53bef8413725c63ba041535/mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28", size = 392474, upload-time = "2025-04-10T12:44:31.16Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/fb/99f81ac72ae23375f22b7afdb7642aba97c00a713c217124420147681a2f/mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59", size = 78509, upload-time = "2025-04-10T12:50:53.297Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, + { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, + { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.9.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/56/8d4c30c8a1d07013911a8fdbd8f89440ef9f08d07a1b50ab8ca8be5a20f9/platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934", size = 28737, upload-time = "2026-03-05T18:34:13.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868", size = 21216, upload-time = "2026-03-05T18:34:12.172Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pre-commit" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/f1/6d86a29246dfd2e9b6237f0b5823717f60cad94d47ddc26afa916d21f525/pre_commit-4.5.1.tar.gz", hash = "sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61", size = 198232, upload-time = "2025-12-16T21:14:33.552Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl", hash = "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77", size = 226437, upload-time = "2025-12-16T21:14:32.409Z" }, +] + +[[package]] +name = "psutil" +version = "7.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, + { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, + { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, +] + +[[package]] +name = "py-cpuinfo" +version = "9.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/37/a8/d832f7293ebb21690860d2e01d8115e5ff6f2ae8bbdc953f0eb0fa4bd2c7/py-cpuinfo-9.0.0.tar.gz", hash = "sha256:3cdbbf3fac90dc6f118bfd64384f309edeadd902d7c8fb17f02ffa1fc3f49690", size = 104716, upload-time = "2022-10-25T20:38:06.303Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/a9/023730ba63db1e494a271cb018dcd361bd2c917ba7004c3e49d5daf795a2/py_cpuinfo-9.0.0-py3-none-any.whl", hash = "sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5", size = 22335, upload-time = "2022-10-25T20:38:27.636Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +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/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" }, + { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" }, + { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" }, + { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" }, + { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" }, + { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" }, + { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" }, + { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" }, + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" }, + { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" }, + { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" }, + { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" }, + { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" }, + { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" }, + { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, +] + +[[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, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pyright" +version = "1.1.408" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodeenv" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/b2/5db700e52554b8f025faa9c3c624c59f1f6c8841ba81ab97641b54322f16/pyright-1.1.408.tar.gz", hash = "sha256:f28f2321f96852fa50b5829ea492f6adb0e6954568d1caa3f3af3a5f555eb684", size = 4400578, upload-time = "2026-01-08T08:07:38.795Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/82/a2c93e32800940d9573fb28c346772a14778b84ba7524e691b324620ab89/pyright-1.1.408-py3-none-any.whl", hash = "sha256:090b32865f4fdb1e0e6cd82bf5618480d48eecd2eb2e70f960982a3d9a4c17c1", size = 6399144, upload-time = "2026-01-08T08:07:37.082Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "pytest-benchmark" +version = "5.2.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "py-cpuinfo" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/24/34/9f732b76456d64faffbef6232f1f9dbec7a7c4999ff46282fa418bd1af66/pytest_benchmark-5.2.3.tar.gz", hash = "sha256:deb7317998a23c650fd4ff76e1230066a76cb45dcece0aca5607143c619e7779", size = 341340, upload-time = "2025-11-09T18:48:43.215Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/29/e756e715a48959f1c0045342088d7ca9762a2f509b945f362a316e9412b7/pytest_benchmark-5.2.3-py3-none-any.whl", hash = "sha256:bc839726ad20e99aaa0d11a127445457b4219bdb9e80a1afc4b51da7f96b0803", size = 45255, upload-time = "2025-11-09T18:48:39.765Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, +] + +[[package]] +name = "pytest-mock" +version = "3.15.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036, upload-time = "2025-09-16T16:37:27.081Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" }, +] + +[[package]] +name = "python-discovery" +version = "1.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/16/6f3f5e9258f0733aaca19aa18e298cb3a629ae49363573e78d241abeef59/python_discovery-1.1.2.tar.gz", hash = "sha256:c500bd2153e3afc5f48a61d33ff570b6f3e710d36ceaaf882fa9bbe5cc2cec49", size = 56928, upload-time = "2026-03-09T20:02:28.402Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/48/8bdfaec240edb1a79b79201eff38b737fc3c29ce59e2e71271bdd8bafdda/python_discovery-1.1.2-py3-none-any.whl", hash = "sha256:d18edd61b382d62f8bcd004a71ebaabc87df31dbefb30aeed59f4fc6afa005be", size = 31486, upload-time = "2026-03-09T20:02:27.277Z" }, +] + +[[package]] +name = "python-engineio" +version = "4.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "simple-websocket" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/34/12/bdef9dbeedbe2cdeba2a2056ad27b1fb081557d34b69a97f574843462cae/python_engineio-4.13.1.tar.gz", hash = "sha256:0a853fcef52f5b345425d8c2b921ac85023a04dfcf75d7b74696c61e940fd066", size = 92348, upload-time = "2026-02-06T23:38:06.12Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl", hash = "sha256:f32ad10589859c11053ad7d9bb3c9695cdf862113bfb0d20bc4d890198287399", size = 59847, upload-time = "2026-02-06T23:38:04.861Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" }, +] + +[[package]] +name = "python-socketio" +version = "5.16.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bidict" }, + { name = "python-engineio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/59/81/cf8284f45e32efa18d3848ed82cdd4dcc1b657b082458fbe01ad3e1f2f8d/python_socketio-5.16.1.tar.gz", hash = "sha256:f863f98eacce81ceea2e742f6388e10ca3cdd0764be21d30d5196470edf5ea89", size = 128508, upload-time = "2026-02-06T23:42:07Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/c7/deb8c5e604404dbf10a3808a858946ca3547692ff6316b698945bb72177e/python_socketio-5.16.1-py3-none-any.whl", hash = "sha256:a3eb1702e92aa2f2b5d3ba00261b61f062cce51f1cfb6900bf3ab4d1934d2d35", size = 82054, upload-time = "2026-02-06T23:42:05.772Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "redis" +version = "7.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "async-timeout", marker = "python_full_version < '3.11.3'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/82/4d1a5279f6c1251d3d2a603a798a1137c657de9b12cfc1fba4858232c4d2/redis-7.3.0.tar.gz", hash = "sha256:4d1b768aafcf41b01022410b3cc4f15a07d9b3d6fe0c66fc967da2c88e551034", size = 4928081, upload-time = "2026-03-06T18:18:16.287Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/28/84e57fce7819e81ec5aa1bd31c42b89607241f4fb1a3ea5b0d2dbeaea26c/redis-7.3.0-py3-none-any.whl", hash = "sha256:9d4fcb002a12a5e3c3fbe005d59c48a2cc231f87fbb2f6b70c2d89bb64fec364", size = 404379, upload-time = "2026-03-06T18:18:14.583Z" }, +] + +[[package]] +name = "reflex" +version = "0.8.27" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "alembic" }, + { name = "click" }, + { name = "granian", extra = ["reload"] }, + { name = "httpx" }, + { name = "packaging" }, + { name = "platformdirs" }, + { name = "psutil", marker = "sys_platform == 'win32'" }, + { name = "pydantic" }, + { name = "python-multipart" }, + { name = "python-socketio" }, + { name = "redis" }, + { name = "reflex-hosting-cli" }, + { name = "rich" }, + { name = "sqlmodel" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e3/51/f6dd657f04a097aa7b02fb6ab08b24f3a4fe676378af5b9e6dff2d92c64b/reflex-0.8.27.tar.gz", hash = "sha256:c90cca51d77955e0162baf4384b843e4350c75afa656589e484470222b48368f", size = 572762, upload-time = "2026-02-17T19:23:37.574Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/03/abdfa493299dd63ecb3f046755a71748a259c7535f58c8d9203eced73262/reflex-0.8.27-py3-none-any.whl", hash = "sha256:1ddd492542d39735f2d1a002549025698022a130b6fa2af1cea33af516058ac6", size = 815640, upload-time = "2026-02-17T19:23:35.897Z" }, +] + +[[package]] +name = "reflex-hosting-cli" +version = "0.1.62" +source = { editable = "." } +dependencies = [ + { name = "click" }, + { name = "httpx" }, + { name = "packaging" }, + { name = "platformdirs" }, + { name = "rich" }, +] + +[package.dev-dependencies] +dev = [ + { name = "coverage" }, + { name = "darglint" }, + { name = "pre-commit" }, + { name = "pyright" }, + { name = "pytest" }, + { name = "pytest-benchmark" }, + { name = "pytest-cov" }, + { name = "pytest-mock" }, + { name = "pyyaml" }, + { name = "reflex" }, + { name = "ruff" }, + { name = "typer" }, +] + +[package.metadata] +requires-dist = [ + { name = "click", specifier = ">=8.2" }, + { name = "httpx", specifier = ">=0.25.1,<1.0" }, + { name = "packaging", specifier = ">=24.2" }, + { name = "platformdirs", specifier = ">=3.10.0,<5.0" }, + { name = "rich", specifier = ">=13,<15" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "coverage" }, + { name = "darglint" }, + { name = "pre-commit" }, + { name = "pyright" }, + { name = "pytest" }, + { name = "pytest-benchmark" }, + { name = "pytest-cov" }, + { name = "pytest-mock" }, + { name = "pyyaml" }, + { name = "reflex" }, + { name = "ruff" }, + { name = "typer" }, +] + +[[package]] +name = "rich" +version = "14.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/77/9b/840e0039e65fcf12758adf684d2289024d6140cde9268cc59887dc55189c/ruff-0.15.5.tar.gz", hash = "sha256:7c3601d3b6d76dce18c5c824fc8d06f4eef33d6df0c21ec7799510cde0f159a2", size = 4574214, upload-time = "2026-03-05T20:06:34.946Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/20/5369c3ce21588c708bcbe517a8fbe1a8dfdb5dfd5137e14790b1da71612c/ruff-0.15.5-py3-none-linux_armv6l.whl", hash = "sha256:4ae44c42281f42e3b06b988e442d344a5b9b72450ff3c892e30d11b29a96a57c", size = 10478185, upload-time = "2026-03-05T20:06:29.093Z" }, + { url = "https://files.pythonhosted.org/packages/44/ed/e81dd668547da281e5dce710cf0bc60193f8d3d43833e8241d006720e42b/ruff-0.15.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6edd3792d408ebcf61adabc01822da687579a1a023f297618ac27a5b51ef0080", size = 10859201, upload-time = "2026-03-05T20:06:32.632Z" }, + { url = "https://files.pythonhosted.org/packages/c4/8f/533075f00aaf19b07c5cd6aa6e5d89424b06b3b3f4583bfa9c640a079059/ruff-0.15.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:89f463f7c8205a9f8dea9d658d59eff49db05f88f89cc3047fb1a02d9f344010", size = 10184752, upload-time = "2026-03-05T20:06:40.312Z" }, + { url = "https://files.pythonhosted.org/packages/66/0e/ba49e2c3fa0395b3152bad634c7432f7edfc509c133b8f4529053ff024fb/ruff-0.15.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba786a8295c6574c1116704cf0b9e6563de3432ac888d8f83685654fe528fd65", size = 10534857, upload-time = "2026-03-05T20:06:19.581Z" }, + { url = "https://files.pythonhosted.org/packages/59/71/39234440f27a226475a0659561adb0d784b4d247dfe7f43ffc12dd02e288/ruff-0.15.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd4b801e57955fe9f02b31d20375ab3a5c4415f2e5105b79fb94cf2642c91440", size = 10309120, upload-time = "2026-03-05T20:06:00.435Z" }, + { url = "https://files.pythonhosted.org/packages/f5/87/4140aa86a93df032156982b726f4952aaec4a883bb98cb6ef73c347da253/ruff-0.15.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:391f7c73388f3d8c11b794dbbc2959a5b5afe66642c142a6effa90b45f6f5204", size = 11047428, upload-time = "2026-03-05T20:05:51.867Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f7/4953e7e3287676f78fbe85e3a0ca414c5ca81237b7575bdadc00229ac240/ruff-0.15.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8dc18f30302e379fe1e998548b0f5e9f4dff907f52f73ad6da419ea9c19d66c8", size = 11914251, upload-time = "2026-03-05T20:06:22.887Z" }, + { url = "https://files.pythonhosted.org/packages/77/46/0f7c865c10cf896ccf5a939c3e84e1cfaeed608ff5249584799a74d33835/ruff-0.15.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1cc6e7f90087e2d27f98dc34ed1b3ab7c8f0d273cc5431415454e22c0bd2a681", size = 11333801, upload-time = "2026-03-05T20:05:57.168Z" }, + { url = "https://files.pythonhosted.org/packages/d3/01/a10fe54b653061585e655f5286c2662ebddb68831ed3eaebfb0eb08c0a16/ruff-0.15.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1cb7169f53c1ddb06e71a9aebd7e98fc0fea936b39afb36d8e86d36ecc2636a", size = 11206821, upload-time = "2026-03-05T20:06:03.441Z" }, + { url = "https://files.pythonhosted.org/packages/7a/0d/2132ceaf20c5e8699aa83da2706ecb5c5dcdf78b453f77edca7fb70f8a93/ruff-0.15.5-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9b037924500a31ee17389b5c8c4d88874cc6ea8e42f12e9c61a3d754ff72f1ca", size = 11133326, upload-time = "2026-03-05T20:06:25.655Z" }, + { url = "https://files.pythonhosted.org/packages/72/cb/2e5259a7eb2a0f87c08c0fe5bf5825a1e4b90883a52685524596bfc93072/ruff-0.15.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:65bb414e5b4eadd95a8c1e4804f6772bbe8995889f203a01f77ddf2d790929dd", size = 10510820, upload-time = "2026-03-05T20:06:37.79Z" }, + { url = "https://files.pythonhosted.org/packages/ff/20/b67ce78f9e6c59ffbdb5b4503d0090e749b5f2d31b599b554698a80d861c/ruff-0.15.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d20aa469ae3b57033519c559e9bc9cd9e782842e39be05b50e852c7c981fa01d", size = 10302395, upload-time = "2026-03-05T20:05:54.504Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e5/719f1acccd31b720d477751558ed74e9c88134adcc377e5e886af89d3072/ruff-0.15.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:15388dd28c9161cdb8eda68993533acc870aa4e646a0a277aa166de9ad5a8752", size = 10754069, upload-time = "2026-03-05T20:06:06.422Z" }, + { url = "https://files.pythonhosted.org/packages/c3/9c/d1db14469e32d98f3ca27079dbd30b7b44dbb5317d06ab36718dee3baf03/ruff-0.15.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b30da330cbd03bed0c21420b6b953158f60c74c54c5f4c1dabbdf3a57bf355d2", size = 11304315, upload-time = "2026-03-05T20:06:10.867Z" }, + { url = "https://files.pythonhosted.org/packages/28/3a/950367aee7c69027f4f422059227b290ed780366b6aecee5de5039d50fa8/ruff-0.15.5-py3-none-win32.whl", hash = "sha256:732e5ee1f98ba5b3679029989a06ca39a950cced52143a0ea82a2102cb592b74", size = 10551676, upload-time = "2026-03-05T20:06:13.705Z" }, + { url = "https://files.pythonhosted.org/packages/b8/00/bf077a505b4e649bdd3c47ff8ec967735ce2544c8e4a43aba42ee9bf935d/ruff-0.15.5-py3-none-win_amd64.whl", hash = "sha256:821d41c5fa9e19117616c35eaa3f4b75046ec76c65e7ae20a333e9a8696bc7fe", size = 11678972, upload-time = "2026-03-05T20:06:45.379Z" }, + { url = "https://files.pythonhosted.org/packages/fe/4e/cd76eca6db6115604b7626668e891c9dd03330384082e33662fb0f113614/ruff-0.15.5-py3-none-win_arm64.whl", hash = "sha256:b498d1c60d2fe5c10c45ec3f698901065772730b411f164ae270bb6bfcc4740b", size = 10965572, upload-time = "2026-03-05T20:06:16.984Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[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, upload-time = "2024-10-10T22:39:31.412Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl", hash = "sha256:4af6069630a38ed6c561010f0e11a5bc0d4ca569b36306eb257cd9a192497c8c", size = 13842, upload-time = "2024-10-10T22:39:29.645Z" }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.48" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/73/b4a9737255583b5fa858e0bb8e116eb94b88c910164ed2ed719147bde3de/sqlalchemy-2.0.48.tar.gz", hash = "sha256:5ca74f37f3369b45e1f6b7b06afb182af1fd5dde009e4ffd831830d98cbe5fe7", size = 9886075, upload-time = "2026-03-02T15:28:51.474Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/67/1235676e93dd3b742a4a8eddfae49eea46c85e3eed29f0da446a8dd57500/sqlalchemy-2.0.48-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7001dc9d5f6bb4deb756d5928eaefe1930f6f4179da3924cbd95ee0e9f4dce89", size = 2157384, upload-time = "2026-03-02T15:38:26.781Z" }, + { url = "https://files.pythonhosted.org/packages/4d/d7/fa728b856daa18c10e1390e76f26f64ac890c947008284387451d56ca3d0/sqlalchemy-2.0.48-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1a89ce07ad2d4b8cfc30bd5889ec40613e028ed80ef47da7d9dd2ce969ad30e0", size = 3236981, upload-time = "2026-03-02T15:58:53.53Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ad/6c4395649a212a6c603a72c5b9ab5dce3135a1546cfdffa3c427e71fd535/sqlalchemy-2.0.48-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10853a53a4a00417a00913d270dddda75815fcb80675874285f41051c094d7dd", size = 3235232, upload-time = "2026-03-02T15:52:25.654Z" }, + { url = "https://files.pythonhosted.org/packages/01/f4/58f845e511ac0509765a6f85eb24924c1ef0d54fb50de9d15b28c3601458/sqlalchemy-2.0.48-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fac0fa4e4f55f118fd87177dacb1c6522fe39c28d498d259014020fec9164c29", size = 3188106, upload-time = "2026-03-02T15:58:55.193Z" }, + { url = "https://files.pythonhosted.org/packages/3f/f9/6dcc7bfa5f5794c3a095e78cd1de8269dfb5584dfd4c2c00a50d3c1ade44/sqlalchemy-2.0.48-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3713e21ea67bca727eecd4a24bf68bcd414c403faae4989442be60994301ded0", size = 3209522, upload-time = "2026-03-02T15:52:27.407Z" }, + { url = "https://files.pythonhosted.org/packages/d7/5a/b632875ab35874d42657f079529f0745410604645c269a8c21fb4272ff7a/sqlalchemy-2.0.48-cp310-cp310-win32.whl", hash = "sha256:d404dc897ce10e565d647795861762aa2d06ca3f4a728c5e9a835096c7059018", size = 2117695, upload-time = "2026-03-02T15:46:51.389Z" }, + { url = "https://files.pythonhosted.org/packages/de/03/9752eb2a41afdd8568e41ac3c3128e32a0a73eada5ab80483083604a56d1/sqlalchemy-2.0.48-cp310-cp310-win_amd64.whl", hash = "sha256:841a94c66577661c1f088ac958cd767d7c9bf507698f45afffe7a4017049de76", size = 2140928, upload-time = "2026-03-02T15:46:52.992Z" }, + { url = "https://files.pythonhosted.org/packages/d7/6d/b8b78b5b80f3c3ab3f7fa90faa195ec3401f6d884b60221260fd4d51864c/sqlalchemy-2.0.48-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b4c575df7368b3b13e0cebf01d4679f9a28ed2ae6c1cd0b1d5beffb6b2007dc", size = 2157184, upload-time = "2026-03-02T15:38:28.161Z" }, + { url = "https://files.pythonhosted.org/packages/21/4b/4f3d4a43743ab58b95b9ddf5580a265b593d017693df9e08bd55780af5bb/sqlalchemy-2.0.48-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e83e3f959aaa1c9df95c22c528096d94848a1bc819f5d0ebf7ee3df0ca63db6c", size = 3313555, upload-time = "2026-03-02T15:58:57.21Z" }, + { url = "https://files.pythonhosted.org/packages/21/dd/3b7c53f1dbbf736fd27041aee68f8ac52226b610f914085b1652c2323442/sqlalchemy-2.0.48-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f7b7243850edd0b8b97043f04748f31de50cf426e939def5c16bedb540698f7", size = 3313057, upload-time = "2026-03-02T15:52:29.366Z" }, + { url = "https://files.pythonhosted.org/packages/d9/cc/3e600a90ae64047f33313d7d32e5ad025417f09d2ded487e8284b5e21a15/sqlalchemy-2.0.48-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:82745b03b4043e04600a6b665cb98697c4339b24e34d74b0a2ac0a2488b6f94d", size = 3265431, upload-time = "2026-03-02T15:58:59.096Z" }, + { url = "https://files.pythonhosted.org/packages/8b/19/780138dacfe3f5024f4cf96e4005e91edf6653d53d3673be4844578faf1d/sqlalchemy-2.0.48-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e5e088bf43f6ee6fec7dbf1ef7ff7774a616c236b5c0cb3e00662dd71a56b571", size = 3287646, upload-time = "2026-03-02T15:52:31.569Z" }, + { url = "https://files.pythonhosted.org/packages/40/fd/f32ced124f01a23151f4777e4c705f3a470adc7bd241d9f36a7c941a33bf/sqlalchemy-2.0.48-cp311-cp311-win32.whl", hash = "sha256:9c7d0a77e36b5f4b01ca398482230ab792061d243d715299b44a0b55c89fe617", size = 2116956, upload-time = "2026-03-02T15:46:54.535Z" }, + { url = "https://files.pythonhosted.org/packages/58/d5/dd767277f6feef12d05651538f280277e661698f617fa4d086cce6055416/sqlalchemy-2.0.48-cp311-cp311-win_amd64.whl", hash = "sha256:583849c743e0e3c9bb7446f5b5addeacedc168d657a69b418063dfdb2d90081c", size = 2141627, upload-time = "2026-03-02T15:46:55.849Z" }, + { url = "https://files.pythonhosted.org/packages/ef/91/a42ae716f8925e9659df2da21ba941f158686856107a61cc97a95e7647a3/sqlalchemy-2.0.48-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:348174f228b99f33ca1f773e85510e08927620caa59ffe7803b37170df30332b", size = 2155737, upload-time = "2026-03-02T15:49:13.207Z" }, + { url = "https://files.pythonhosted.org/packages/b9/52/f75f516a1f3888f027c1cfb5d22d4376f4b46236f2e8669dcb0cddc60275/sqlalchemy-2.0.48-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53667b5f668991e279d21f94ccfa6e45b4e3f4500e7591ae59a8012d0f010dcb", size = 3337020, upload-time = "2026-03-02T15:50:34.547Z" }, + { url = "https://files.pythonhosted.org/packages/37/9a/0c28b6371e0cdcb14f8f1930778cb3123acfcbd2c95bb9cf6b4a2ba0cce3/sqlalchemy-2.0.48-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34634e196f620c7a61d18d5cf7dc841ca6daa7961aed75d532b7e58b309ac894", size = 3349983, upload-time = "2026-03-02T15:53:25.542Z" }, + { url = "https://files.pythonhosted.org/packages/1c/46/0aee8f3ff20b1dcbceb46ca2d87fcc3d48b407925a383ff668218509d132/sqlalchemy-2.0.48-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:546572a1793cc35857a2ffa1fe0e58571af1779bcc1ffa7c9fb0839885ed69a9", size = 3279690, upload-time = "2026-03-02T15:50:36.277Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8c/a957bc91293b49181350bfd55e6dfc6e30b7f7d83dc6792d72043274a390/sqlalchemy-2.0.48-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:07edba08061bc277bfdc772dd2a1a43978f5a45994dd3ede26391b405c15221e", size = 3314738, upload-time = "2026-03-02T15:53:27.519Z" }, + { url = "https://files.pythonhosted.org/packages/4b/44/1d257d9f9556661e7bdc83667cc414ba210acfc110c82938cb3611eea58f/sqlalchemy-2.0.48-cp312-cp312-win32.whl", hash = "sha256:908a3fa6908716f803b86896a09a2c4dde5f5ce2bb07aacc71ffebb57986ce99", size = 2115546, upload-time = "2026-03-02T15:54:31.591Z" }, + { url = "https://files.pythonhosted.org/packages/f2/af/c3c7e1f3a2b383155a16454df62ae8c62a30dd238e42e68c24cebebbfae6/sqlalchemy-2.0.48-cp312-cp312-win_amd64.whl", hash = "sha256:68549c403f79a8e25984376480959975212a670405e3913830614432b5daa07a", size = 2142484, upload-time = "2026-03-02T15:54:34.072Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c6/569dc8bf3cd375abc5907e82235923e986799f301cd79a903f784b996fca/sqlalchemy-2.0.48-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e3070c03701037aa418b55d36532ecb8f8446ed0135acb71c678dbdf12f5b6e4", size = 2152599, upload-time = "2026-03-02T15:49:14.41Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ff/f4e04a4bd5a24304f38cb0d4aa2ad4c0fb34999f8b884c656535e1b2b74c/sqlalchemy-2.0.48-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2645b7d8a738763b664a12a1542c89c940daa55196e8d73e55b169cc5c99f65f", size = 3278825, upload-time = "2026-03-02T15:50:38.269Z" }, + { url = "https://files.pythonhosted.org/packages/fe/88/cb59509e4668d8001818d7355d9995be90c321313078c912420603a7cb95/sqlalchemy-2.0.48-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b19151e76620a412c2ac1c6f977ab1b9fa7ad43140178345136456d5265b32ed", size = 3295200, upload-time = "2026-03-02T15:53:29.366Z" }, + { url = "https://files.pythonhosted.org/packages/87/dc/1609a4442aefd750ea2f32629559394ec92e89ac1d621a7f462b70f736ff/sqlalchemy-2.0.48-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5b193a7e29fd9fa56e502920dca47dffe60f97c863494946bd698c6058a55658", size = 3226876, upload-time = "2026-03-02T15:50:39.802Z" }, + { url = "https://files.pythonhosted.org/packages/37/c3/6ae2ab5ea2fa989fbac4e674de01224b7a9d744becaf59bb967d62e99bed/sqlalchemy-2.0.48-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:36ac4ddc3d33e852da9cb00ffb08cea62ca05c39711dc67062ca2bb1fae35fd8", size = 3265045, upload-time = "2026-03-02T15:53:31.421Z" }, + { url = "https://files.pythonhosted.org/packages/6f/82/ea4665d1bb98c50c19666e672f21b81356bd6077c4574e3d2bbb84541f53/sqlalchemy-2.0.48-cp313-cp313-win32.whl", hash = "sha256:389b984139278f97757ea9b08993e7b9d1142912e046ab7d82b3fbaeb0209131", size = 2113700, upload-time = "2026-03-02T15:54:35.825Z" }, + { url = "https://files.pythonhosted.org/packages/b7/2b/b9040bec58c58225f073f5b0c1870defe1940835549dafec680cbd58c3c3/sqlalchemy-2.0.48-cp313-cp313-win_amd64.whl", hash = "sha256:d612c976cbc2d17edfcc4c006874b764e85e990c29ce9bd411f926bbfb02b9a2", size = 2139487, upload-time = "2026-03-02T15:54:37.079Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/7b17bd50244b78a49d22cc63c969d71dc4de54567dc152a9b46f6fae40ce/sqlalchemy-2.0.48-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69f5bc24904d3bc3640961cddd2523e361257ef68585d6e364166dfbe8c78fae", size = 3558851, upload-time = "2026-03-02T15:57:48.607Z" }, + { url = "https://files.pythonhosted.org/packages/20/0d/213668e9aca61d370f7d2a6449ea4ec699747fac67d4bda1bb3d129025be/sqlalchemy-2.0.48-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd08b90d211c086181caed76931ecfa2bdfc83eea3cfccdb0f82abc6c4b876cb", size = 3525525, upload-time = "2026-03-02T16:04:38.058Z" }, + { url = "https://files.pythonhosted.org/packages/85/d7/a84edf412979e7d59c69b89a5871f90a49228360594680e667cb2c46a828/sqlalchemy-2.0.48-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1ccd42229aaac2df431562117ac7e667d702e8e44afdb6cf0e50fa3f18160f0b", size = 3466611, upload-time = "2026-03-02T15:57:50.759Z" }, + { url = "https://files.pythonhosted.org/packages/86/55/42404ce5770f6be26a2b0607e7866c31b9a4176c819e9a7a5e0a055770be/sqlalchemy-2.0.48-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f0dcbc588cd5b725162c076eb9119342f6579c7f7f55057bb7e3c6ff27e13121", size = 3475812, upload-time = "2026-03-02T16:04:40.092Z" }, + { url = "https://files.pythonhosted.org/packages/ae/ae/29b87775fadc43e627cf582fe3bda4d02e300f6b8f2747c764950d13784c/sqlalchemy-2.0.48-cp313-cp313t-win32.whl", hash = "sha256:9764014ef5e58aab76220c5664abb5d47d5bc858d9debf821e55cfdd0f128485", size = 2141335, upload-time = "2026-03-02T15:52:51.518Z" }, + { url = "https://files.pythonhosted.org/packages/91/44/f39d063c90f2443e5b46ec4819abd3d8de653893aae92df42a5c4f5843de/sqlalchemy-2.0.48-cp313-cp313t-win_amd64.whl", hash = "sha256:e2f35b4cccd9ed286ad62e0a3c3ac21e06c02abc60e20aa51a3e305a30f5fa79", size = 2173095, upload-time = "2026-03-02T15:52:52.79Z" }, + { url = "https://files.pythonhosted.org/packages/f7/b3/f437eaa1cf028bb3c927172c7272366393e73ccd104dcf5b6963f4ab5318/sqlalchemy-2.0.48-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e2d0d88686e3d35a76f3e15a34e8c12d73fc94c1dea1cd55782e695cc14086dd", size = 2154401, upload-time = "2026-03-02T15:49:17.24Z" }, + { url = "https://files.pythonhosted.org/packages/6c/1c/b3abdf0f402aa3f60f0df6ea53d92a162b458fca2321d8f1f00278506402/sqlalchemy-2.0.48-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49b7bddc1eebf011ea5ab722fdbe67a401caa34a350d278cc7733c0e88fecb1f", size = 3274528, upload-time = "2026-03-02T15:50:41.489Z" }, + { url = "https://files.pythonhosted.org/packages/f2/5e/327428a034407651a048f5e624361adf3f9fbac9d0fa98e981e9c6ff2f5e/sqlalchemy-2.0.48-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:426c5ca86415d9b8945c7073597e10de9644802e2ff502b8e1f11a7a2642856b", size = 3279523, upload-time = "2026-03-02T15:53:32.962Z" }, + { url = "https://files.pythonhosted.org/packages/2a/ca/ece73c81a918add0965b76b868b7b5359e068380b90ef1656ee995940c02/sqlalchemy-2.0.48-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:288937433bd44e3990e7da2402fabc44a3c6c25d3704da066b85b89a85474ae0", size = 3224312, upload-time = "2026-03-02T15:50:42.996Z" }, + { url = "https://files.pythonhosted.org/packages/88/11/fbaf1ae91fa4ee43f4fe79661cead6358644824419c26adb004941bdce7c/sqlalchemy-2.0.48-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8183dc57ae7d9edc1346e007e840a9f3d6aa7b7f165203a99e16f447150140d2", size = 3246304, upload-time = "2026-03-02T15:53:34.937Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5fb0deb13930b4f2f698c5541ae076c18981173e27dd00376dbaea7a9c82/sqlalchemy-2.0.48-cp314-cp314-win32.whl", hash = "sha256:1182437cb2d97988cfea04cf6cdc0b0bb9c74f4d56ec3d08b81e23d621a28cc6", size = 2116565, upload-time = "2026-03-02T15:54:38.321Z" }, + { url = "https://files.pythonhosted.org/packages/95/7e/e83615cb63f80047f18e61e31e8e32257d39458426c23006deeaf48f463b/sqlalchemy-2.0.48-cp314-cp314-win_amd64.whl", hash = "sha256:144921da96c08feb9e2b052c5c5c1d0d151a292c6135623c6b2c041f2a45f9e0", size = 2142205, upload-time = "2026-03-02T15:54:39.831Z" }, + { url = "https://files.pythonhosted.org/packages/83/e3/69d8711b3f2c5135e9cde5f063bc1605860f0b2c53086d40c04017eb1f77/sqlalchemy-2.0.48-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5aee45fd2c6c0f2b9cdddf48c48535e7471e42d6fb81adfde801da0bd5b93241", size = 3563519, upload-time = "2026-03-02T15:57:52.387Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4f/a7cce98facca73c149ea4578981594aaa5fd841e956834931de503359336/sqlalchemy-2.0.48-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7cddca31edf8b0653090cbb54562ca027c421c58ddde2c0685f49ff56a1690e0", size = 3528611, upload-time = "2026-03-02T16:04:42.097Z" }, + { url = "https://files.pythonhosted.org/packages/cd/7d/5936c7a03a0b0cb0fa0cc425998821c6029756b0855a8f7ee70fba1de955/sqlalchemy-2.0.48-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7a936f1bb23d370b7c8cc079d5fce4c7d18da87a33c6744e51a93b0f9e97e9b3", size = 3472326, upload-time = "2026-03-02T15:57:54.423Z" }, + { url = "https://files.pythonhosted.org/packages/f4/33/cea7dfc31b52904efe3dcdc169eb4514078887dff1f5ae28a7f4c5d54b3c/sqlalchemy-2.0.48-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e004aa9248e8cb0a5f9b96d003ca7c1c0a5da8decd1066e7b53f59eb8ce7c62b", size = 3478453, upload-time = "2026-03-02T16:04:44.584Z" }, + { url = "https://files.pythonhosted.org/packages/c8/95/32107c4d13be077a9cae61e9ae49966a35dc4bf442a8852dd871db31f62e/sqlalchemy-2.0.48-cp314-cp314t-win32.whl", hash = "sha256:b8438ec5594980d405251451c5b7ea9aa58dda38eb7ac35fb7e4c696712ee24f", size = 2147209, upload-time = "2026-03-02T15:52:54.274Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d7/1e073da7a4bc645eb83c76067284a0374e643bc4be57f14cc6414656f92c/sqlalchemy-2.0.48-cp314-cp314t-win_amd64.whl", hash = "sha256:d854b3970067297f3a7fbd7a4683587134aa9b3877ee15aa29eea478dc68f933", size = 2182198, upload-time = "2026-03-02T15:52:55.606Z" }, + { url = "https://files.pythonhosted.org/packages/46/2c/9664130905f03db57961b8980b05cab624afd114bf2be2576628a9f22da4/sqlalchemy-2.0.48-py3-none-any.whl", hash = "sha256:a66fe406437dd65cacd96a72689a3aaaecaebbcd62d81c5ac1c0fdbeac835096", size = 1940202, upload-time = "2026-03-02T15:52:43.285Z" }, +] + +[[package]] +name = "sqlmodel" +version = "0.0.37" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "sqlalchemy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/26/1d2faa0fd5a765267f49751de533adac6b9ff9366c7c6e7692df4f32230f/sqlmodel-0.0.37.tar.gz", hash = "sha256:d2c19327175794faf50b1ee31cc966764f55b1dedefc046450bc5741a3d68352", size = 85527, upload-time = "2026-02-21T16:39:47.038Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/e1/7c8d18e737433f3b5bbe27b56a9072a9fcb36342b48f1bef34b6da1d61f2/sqlmodel-0.0.37-py3-none-any.whl", hash = "sha256:2137a4045ef3fd66a917a7717ada959a1ceb3630d95e1f6aaab39dd2c0aef278", size = 27224, upload-time = "2026-02-21T16:39:47.781Z" }, +] + +[[package]] +name = "starlette" +version = "0.52.1" +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/c4/68/79977123bb7be889ad680d79a40f339082c1978b5cfcf62c2d8d196873ac/starlette-0.52.1.tar.gz", hash = "sha256:834edd1b0a23167694292e94f597773bc3f89f362be6effee198165a35d62933", size = 2653702, upload-time = "2026-01-18T13:34:11.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74", size = 74272, upload-time = "2026-01-18T13:34:09.188Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, + { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, + { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, + { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, + { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, + { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, + { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, + { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, + { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, + { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, + { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, + { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, + { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, + { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, + { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, + { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, + { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, + { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, + { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, + { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, + { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, + { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, + { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, + { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, + { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, + { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, +] + +[[package]] +name = "typer" +version = "0.24.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/24/cb09efec5cc954f7f9b930bf8279447d24618bb6758d4f6adf2574c41780/typer-0.24.1.tar.gz", hash = "sha256:e39b4732d65fbdcde189ae76cf7cd48aeae72919dea1fdfc16593be016256b45", size = 118613, upload-time = "2026-02-21T16:54:40.609Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl", hash = "sha256:112c1f0ce578bfb4cab9ffdabc68f031416ebcc216536611ba21f04e9aa84c9e", size = 56085, upload-time = "2026-02-21T16:54:41.616Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "virtualenv" +version = "21.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, + { name = "python-discovery" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/92/58199fe10049f9703c2666e809c4f686c54ef0a68b0f6afccf518c0b1eb9/virtualenv-21.2.0.tar.gz", hash = "sha256:1720dc3a62ef5b443092e3f499228599045d7fea4c79199770499df8becf9098", size = 5840618, upload-time = "2026-03-09T17:24:38.013Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/59/7d02447a55b2e55755011a647479041bc92a82e143f96a8195cb33bd0a1c/virtualenv-21.2.0-py3-none-any.whl", hash = "sha256:1bd755b504931164a5a496d217c014d098426cddc79363ad66ac78125f9d908f", size = 5825084, upload-time = "2026-03-09T17:24:35.378Z" }, +] + +[[package]] +name = "watchfiles" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/1a/206e8cf2dd86fddf939165a57b4df61607a1e0add2785f170a3f616b7d9f/watchfiles-1.1.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:eef58232d32daf2ac67f42dea51a2c80f0d03379075d44a587051e63cc2e368c", size = 407318, upload-time = "2025-10-14T15:04:18.753Z" }, + { url = "https://files.pythonhosted.org/packages/b3/0f/abaf5262b9c496b5dad4ed3c0e799cbecb1f8ea512ecb6ddd46646a9fca3/watchfiles-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:03fa0f5237118a0c5e496185cafa92878568b652a2e9a9382a5151b1a0380a43", size = 394478, upload-time = "2025-10-14T15:04:20.297Z" }, + { url = "https://files.pythonhosted.org/packages/b1/04/9cc0ba88697b34b755371f5ace8d3a4d9a15719c07bdc7bd13d7d8c6a341/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca65483439f9c791897f7db49202301deb6e15fe9f8fe2fed555bf986d10c31", size = 449894, upload-time = "2025-10-14T15:04:21.527Z" }, + { url = "https://files.pythonhosted.org/packages/d2/9c/eda4615863cd8621e89aed4df680d8c3ec3da6a4cf1da113c17decd87c7f/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f0ab1c1af0cb38e3f598244c17919fb1a84d1629cc08355b0074b6d7f53138ac", size = 459065, upload-time = "2025-10-14T15:04:22.795Z" }, + { url = "https://files.pythonhosted.org/packages/84/13/f28b3f340157d03cbc8197629bc109d1098764abe1e60874622a0be5c112/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bc570d6c01c206c46deb6e935a260be44f186a2f05179f52f7fcd2be086a94d", size = 488377, upload-time = "2025-10-14T15:04:24.138Z" }, + { url = "https://files.pythonhosted.org/packages/86/93/cfa597fa9389e122488f7ffdbd6db505b3b915ca7435ecd7542e855898c2/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e84087b432b6ac94778de547e08611266f1f8ffad28c0ee4c82e028b0fc5966d", size = 595837, upload-time = "2025-10-14T15:04:25.057Z" }, + { url = "https://files.pythonhosted.org/packages/57/1e/68c1ed5652b48d89fc24d6af905d88ee4f82fa8bc491e2666004e307ded1/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:620bae625f4cb18427b1bb1a2d9426dc0dd5a5ba74c7c2cdb9de405f7b129863", size = 473456, upload-time = "2025-10-14T15:04:26.497Z" }, + { url = "https://files.pythonhosted.org/packages/d5/dc/1a680b7458ffa3b14bb64878112aefc8f2e4f73c5af763cbf0bd43100658/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:544364b2b51a9b0c7000a4b4b02f90e9423d97fbbf7e06689236443ebcad81ab", size = 455614, upload-time = "2025-10-14T15:04:27.539Z" }, + { url = "https://files.pythonhosted.org/packages/61/a5/3d782a666512e01eaa6541a72ebac1d3aae191ff4a31274a66b8dd85760c/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bbe1ef33d45bc71cf21364df962af171f96ecaeca06bd9e3d0b583efb12aec82", size = 630690, upload-time = "2025-10-14T15:04:28.495Z" }, + { url = "https://files.pythonhosted.org/packages/9b/73/bb5f38590e34687b2a9c47a244aa4dd50c56a825969c92c9c5fc7387cea1/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1a0bb430adb19ef49389e1ad368450193a90038b5b752f4ac089ec6942c4dff4", size = 622459, upload-time = "2025-10-14T15:04:29.491Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ac/c9bb0ec696e07a20bd58af5399aeadaef195fb2c73d26baf55180fe4a942/watchfiles-1.1.1-cp310-cp310-win32.whl", hash = "sha256:3f6d37644155fb5beca5378feb8c1708d5783145f2a0f1c4d5a061a210254844", size = 272663, upload-time = "2025-10-14T15:04:30.435Z" }, + { url = "https://files.pythonhosted.org/packages/11/a0/a60c5a7c2ec59fa062d9a9c61d02e3b6abd94d32aac2d8344c4bdd033326/watchfiles-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a36d8efe0f290835fd0f33da35042a1bb5dc0e83cbc092dcf69bce442579e88e", size = 287453, upload-time = "2025-10-14T15:04:31.53Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f8/2c5f479fb531ce2f0564eda479faecf253d886b1ab3630a39b7bf7362d46/watchfiles-1.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5", size = 406529, upload-time = "2025-10-14T15:04:32.899Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cd/f515660b1f32f65df671ddf6f85bfaca621aee177712874dc30a97397977/watchfiles-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741", size = 394384, upload-time = "2025-10-14T15:04:33.761Z" }, + { url = "https://files.pythonhosted.org/packages/7b/c3/28b7dc99733eab43fca2d10f55c86e03bd6ab11ca31b802abac26b23d161/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6", size = 448789, upload-time = "2025-10-14T15:04:34.679Z" }, + { url = "https://files.pythonhosted.org/packages/4a/24/33e71113b320030011c8e4316ccca04194bf0cbbaeee207f00cbc7d6b9f5/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b", size = 460521, upload-time = "2025-10-14T15:04:35.963Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c3/3c9a55f255aa57b91579ae9e98c88704955fa9dac3e5614fb378291155df/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2cd9e04277e756a2e2d2543d65d1e2166d6fd4c9b183f8808634fda23f17b14", size = 488722, upload-time = "2025-10-14T15:04:37.091Z" }, + { url = "https://files.pythonhosted.org/packages/49/36/506447b73eb46c120169dc1717fe2eff07c234bb3232a7200b5f5bd816e9/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d", size = 596088, upload-time = "2025-10-14T15:04:38.39Z" }, + { url = "https://files.pythonhosted.org/packages/82/ab/5f39e752a9838ec4d52e9b87c1e80f1ee3ccdbe92e183c15b6577ab9de16/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff", size = 472923, upload-time = "2025-10-14T15:04:39.666Z" }, + { url = "https://files.pythonhosted.org/packages/af/b9/a419292f05e302dea372fa7e6fda5178a92998411f8581b9830d28fb9edb/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606", size = 456080, upload-time = "2025-10-14T15:04:40.643Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c3/d5932fd62bde1a30c36e10c409dc5d54506726f08cb3e1d8d0ba5e2bc8db/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701", size = 629432, upload-time = "2025-10-14T15:04:41.789Z" }, + { url = "https://files.pythonhosted.org/packages/f7/77/16bddd9779fafb795f1a94319dc965209c5641db5bf1edbbccace6d1b3c0/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10", size = 623046, upload-time = "2025-10-14T15:04:42.718Z" }, + { url = "https://files.pythonhosted.org/packages/46/ef/f2ecb9a0f342b4bfad13a2787155c6ee7ce792140eac63a34676a2feeef2/watchfiles-1.1.1-cp311-cp311-win32.whl", hash = "sha256:de6da501c883f58ad50db3a32ad397b09ad29865b5f26f64c24d3e3281685849", size = 271473, upload-time = "2025-10-14T15:04:43.624Z" }, + { url = "https://files.pythonhosted.org/packages/94/bc/f42d71125f19731ea435c3948cad148d31a64fccde3867e5ba4edee901f9/watchfiles-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:35c53bd62a0b885bf653ebf6b700d1bf05debb78ad9292cf2a942b23513dc4c4", size = 287598, upload-time = "2025-10-14T15:04:44.516Z" }, + { url = "https://files.pythonhosted.org/packages/57/c9/a30f897351f95bbbfb6abcadafbaca711ce1162f4db95fc908c98a9165f3/watchfiles-1.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:57ca5281a8b5e27593cb7d82c2ac927ad88a96ed406aa446f6344e4328208e9e", size = 277210, upload-time = "2025-10-14T15:04:45.883Z" }, + { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" }, + { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" }, + { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" }, + { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" }, + { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" }, + { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" }, + { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" }, + { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" }, + { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" }, + { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" }, + { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" }, + { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" }, + { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" }, + { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" }, + { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" }, + { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" }, + { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" }, + { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" }, + { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" }, + { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" }, + { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" }, + { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" }, + { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" }, + { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" }, + { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" }, + { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" }, + { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" }, + { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" }, + { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" }, + { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078, upload-time = "2025-10-14T15:05:37.63Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664, upload-time = "2025-10-14T15:05:38.95Z" }, + { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154, upload-time = "2025-10-14T15:05:39.954Z" }, + { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" }, + { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" }, + { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" }, + { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" }, + { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" }, + { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" }, + { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, + { url = "https://files.pythonhosted.org/packages/ba/4c/a888c91e2e326872fa4705095d64acd8aa2fb9c1f7b9bd0588f33850516c/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:17ef139237dfced9da49fb7f2232c86ca9421f666d78c264c7ffca6601d154c3", size = 409611, upload-time = "2025-10-14T15:06:05.809Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c7/5420d1943c8e3ce1a21c0a9330bcf7edafb6aa65d26b21dbb3267c9e8112/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:672b8adf25b1a0d35c96b5888b7b18699d27d4194bac8beeae75be4b7a3fc9b2", size = 396889, upload-time = "2025-10-14T15:06:07.035Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e5/0072cef3804ce8d3aaddbfe7788aadff6b3d3f98a286fdbee9fd74ca59a7/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77a13aea58bc2b90173bc69f2a90de8e282648939a00a602e1dc4ee23e26b66d", size = 451616, upload-time = "2025-10-14T15:06:08.072Z" }, + { url = "https://files.pythonhosted.org/packages/83/4e/b87b71cbdfad81ad7e83358b3e447fedd281b880a03d64a760fe0a11fc2e/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b495de0bb386df6a12b18335a0285dda90260f51bdb505503c02bcd1ce27a8b", size = 458413, upload-time = "2025-10-14T15:06:09.209Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8e/e500f8b0b77be4ff753ac94dc06b33d8f0d839377fee1b78e8c8d8f031bf/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88", size = 408250, upload-time = "2025-10-14T15:06:10.264Z" }, + { url = "https://files.pythonhosted.org/packages/bd/95/615e72cd27b85b61eec764a5ca51bd94d40b5adea5ff47567d9ebc4d275a/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336", size = 396117, upload-time = "2025-10-14T15:06:11.28Z" }, + { url = "https://files.pythonhosted.org/packages/c9/81/e7fe958ce8a7fb5c73cc9fb07f5aeaf755e6aa72498c57d760af760c91f8/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24", size = 450493, upload-time = "2025-10-14T15:06:12.321Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546, upload-time = "2025-10-14T15:06:13.372Z" }, +] + +[[package]] +name = "wrapt" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/64/925f213fdcbb9baeb1530449ac71a4d57fc361c053d06bf78d0c5c7cd80c/wrapt-2.1.2.tar.gz", hash = "sha256:3996a67eecc2c68fd47b4e3c564405a5777367adfd9b8abb58387b63ee83b21e", size = 81678, upload-time = "2026-03-06T02:53:25.134Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/d2/387594fb592d027366645f3d7cc9b4d7ca7be93845fbaba6d835a912ef3c/wrapt-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4b7a86d99a14f76facb269dc148590c01aaf47584071809a70da30555228158c", size = 60669, upload-time = "2026-03-06T02:52:40.671Z" }, + { url = "https://files.pythonhosted.org/packages/c9/18/3f373935bc5509e7ac444c8026a56762e50c1183e7061797437ca96c12ce/wrapt-2.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a819e39017f95bf7aede768f75915635aa8f671f2993c036991b8d3bfe8dbb6f", size = 61603, upload-time = "2026-03-06T02:54:21.032Z" }, + { url = "https://files.pythonhosted.org/packages/c2/7a/32758ca2853b07a887a4574b74e28843919103194bb47001a304e24af62f/wrapt-2.1.2-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5681123e60aed0e64c7d44f72bbf8b4ce45f79d81467e2c4c728629f5baf06eb", size = 113632, upload-time = "2026-03-06T02:53:54.121Z" }, + { url = "https://files.pythonhosted.org/packages/1d/d5/eeaa38f670d462e97d978b3b0d9ce06d5b91e54bebac6fbed867809216e7/wrapt-2.1.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b8b28e97a44d21836259739ae76284e180b18abbb4dcfdff07a415cf1016c3e", size = 115644, upload-time = "2026-03-06T02:54:53.33Z" }, + { url = "https://files.pythonhosted.org/packages/e3/09/2a41506cb17affb0bdf9d5e2129c8c19e192b388c4c01d05e1b14db23c00/wrapt-2.1.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cef91c95a50596fcdc31397eb6955476f82ae8a3f5a8eabdc13611b60ee380ba", size = 112016, upload-time = "2026-03-06T02:54:43.274Z" }, + { url = "https://files.pythonhosted.org/packages/64/15/0e6c3f5e87caadc43db279724ee36979246d5194fa32fed489c73643ba59/wrapt-2.1.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dad63212b168de8569b1c512f4eac4b57f2c6934b30df32d6ee9534a79f1493f", size = 114823, upload-time = "2026-03-06T02:54:29.392Z" }, + { url = "https://files.pythonhosted.org/packages/56/b2/0ad17c8248f4e57bedf44938c26ec3ee194715f812d2dbbd9d7ff4be6c06/wrapt-2.1.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d307aa6888d5efab2c1cde09843d48c843990be13069003184b67d426d145394", size = 111244, upload-time = "2026-03-06T02:54:02.149Z" }, + { url = "https://files.pythonhosted.org/packages/ff/04/bcdba98c26f2c6522c7c09a726d5d9229120163493620205b2f76bd13c01/wrapt-2.1.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c87cf3f0c85e27b3ac7d9ad95da166bf8739ca215a8b171e8404a2d739897a45", size = 113307, upload-time = "2026-03-06T02:54:12.428Z" }, + { url = "https://files.pythonhosted.org/packages/0e/1b/5e2883c6bc14143924e465a6fc5a92d09eeabe35310842a481fb0581f832/wrapt-2.1.2-cp310-cp310-win32.whl", hash = "sha256:d1c5fea4f9fe3762e2b905fdd67df51e4be7a73b7674957af2d2ade71a5c075d", size = 57986, upload-time = "2026-03-06T02:54:26.823Z" }, + { url = "https://files.pythonhosted.org/packages/42/5a/4efc997bccadd3af5749c250b49412793bc41e13a83a486b2b54a33e240c/wrapt-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:d8f7740e1af13dff2684e4d56fe604a7e04d6c94e737a60568d8d4238b9a0c71", size = 60336, upload-time = "2026-03-06T02:54:18Z" }, + { url = "https://files.pythonhosted.org/packages/c1/f5/a2bb833e20181b937e87c242645ed5d5aa9c373006b0467bfe1a35c727d0/wrapt-2.1.2-cp310-cp310-win_arm64.whl", hash = "sha256:1c6cc827c00dc839350155f316f1f8b4b0c370f52b6a19e782e2bda89600c7dc", size = 58757, upload-time = "2026-03-06T02:53:51.545Z" }, + { url = "https://files.pythonhosted.org/packages/c7/81/60c4471fce95afa5922ca09b88a25f03c93343f759aae0f31fb4412a85c7/wrapt-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:96159a0ee2b0277d44201c3b5be479a9979cf154e8c82fa5df49586a8e7679bb", size = 60666, upload-time = "2026-03-06T02:52:58.934Z" }, + { url = "https://files.pythonhosted.org/packages/6b/be/80e80e39e7cb90b006a0eaf11c73ac3a62bbfb3068469aec15cc0bc795de/wrapt-2.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:98ba61833a77b747901e9012072f038795de7fc77849f1faa965464f3f87ff2d", size = 61601, upload-time = "2026-03-06T02:53:00.487Z" }, + { url = "https://files.pythonhosted.org/packages/b0/be/d7c88cd9293c859fc74b232abdc65a229bb953997995d6912fc85af18323/wrapt-2.1.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:767c0dbbe76cae2a60dd2b235ac0c87c9cccf4898aef8062e57bead46b5f6894", size = 114057, upload-time = "2026-03-06T02:52:44.08Z" }, + { url = "https://files.pythonhosted.org/packages/ea/25/36c04602831a4d685d45a93b3abea61eca7fe35dab6c842d6f5d570ef94a/wrapt-2.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c691a6bc752c0cc4711cc0c00896fcd0f116abc253609ef64ef930032821842", size = 116099, upload-time = "2026-03-06T02:54:56.74Z" }, + { url = "https://files.pythonhosted.org/packages/5c/4e/98a6eb417ef551dc277bec1253d5246b25003cf36fdf3913b65cb7657a56/wrapt-2.1.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f3b7d73012ea75aee5844de58c88f44cf62d0d62711e39da5a82824a7c4626a8", size = 112457, upload-time = "2026-03-06T02:53:52.842Z" }, + { url = "https://files.pythonhosted.org/packages/cb/a6/a6f7186a5297cad8ec53fd7578533b28f795fdf5372368c74bd7e6e9841c/wrapt-2.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:577dff354e7acd9d411eaf4bfe76b724c89c89c8fc9b7e127ee28c5f7bcb25b6", size = 115351, upload-time = "2026-03-06T02:53:32.684Z" }, + { url = "https://files.pythonhosted.org/packages/97/6f/06e66189e721dbebd5cf20e138acc4d1150288ce118462f2fcbff92d38db/wrapt-2.1.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3d7b6fd105f8b24e5bd23ccf41cb1d1099796524bcc6f7fbb8fe576c44befbc9", size = 111748, upload-time = "2026-03-06T02:53:08.455Z" }, + { url = "https://files.pythonhosted.org/packages/ef/43/4808b86f499a51370fbdbdfa6cb91e9b9169e762716456471b619fca7a70/wrapt-2.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:866abdbf4612e0b34764922ef8b1c5668867610a718d3053d59e24a5e5fcfc15", size = 113783, upload-time = "2026-03-06T02:53:02.02Z" }, + { url = "https://files.pythonhosted.org/packages/91/2c/a3f28b8fa7ac2cefa01cfcaca3471f9b0460608d012b693998cd61ef43df/wrapt-2.1.2-cp311-cp311-win32.whl", hash = "sha256:5a0a0a3a882393095573344075189eb2d566e0fd205a2b6414e9997b1b800a8b", size = 57977, upload-time = "2026-03-06T02:53:27.844Z" }, + { url = "https://files.pythonhosted.org/packages/3f/c3/2b1c7bd07a27b1db885a2fab469b707bdd35bddf30a113b4917a7e2139d2/wrapt-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:64a07a71d2730ba56f11d1a4b91f7817dc79bc134c11516b75d1921a7c6fcda1", size = 60336, upload-time = "2026-03-06T02:54:28.104Z" }, + { url = "https://files.pythonhosted.org/packages/ec/5c/76ece7b401b088daa6503d6264dd80f9a727df3e6042802de9a223084ea2/wrapt-2.1.2-cp311-cp311-win_arm64.whl", hash = "sha256:b89f095fe98bc12107f82a9f7d570dc83a0870291aeb6b1d7a7d35575f55d98a", size = 58756, upload-time = "2026-03-06T02:53:16.319Z" }, + { url = "https://files.pythonhosted.org/packages/4c/b6/1db817582c49c7fcbb7df6809d0f515af29d7c2fbf57eb44c36e98fb1492/wrapt-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ff2aad9c4cda28a8f0653fc2d487596458c2a3f475e56ba02909e950a9efa6a9", size = 61255, upload-time = "2026-03-06T02:52:45.663Z" }, + { url = "https://files.pythonhosted.org/packages/a2/16/9b02a6b99c09227c93cd4b73acc3678114154ec38da53043c0ddc1fba0dc/wrapt-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6433ea84e1cfacf32021d2a4ee909554ade7fd392caa6f7c13f1f4bf7b8e8748", size = 61848, upload-time = "2026-03-06T02:53:48.728Z" }, + { url = "https://files.pythonhosted.org/packages/af/aa/ead46a88f9ec3a432a4832dfedb84092fc35af2d0ba40cd04aea3889f247/wrapt-2.1.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c20b757c268d30d6215916a5fa8461048d023865d888e437fab451139cad6c8e", size = 121433, upload-time = "2026-03-06T02:54:40.328Z" }, + { url = "https://files.pythonhosted.org/packages/3a/9f/742c7c7cdf58b59085a1ee4b6c37b013f66ac33673a7ef4aaed5e992bc33/wrapt-2.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79847b83eb38e70d93dc392c7c5b587efe65b3e7afcc167aa8abd5d60e8761c8", size = 123013, upload-time = "2026-03-06T02:53:26.58Z" }, + { url = "https://files.pythonhosted.org/packages/e8/44/2c3dd45d53236b7ed7c646fcf212251dc19e48e599debd3926b52310fafb/wrapt-2.1.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f8fba1bae256186a83d1875b2b1f4e2d1242e8fac0f58ec0d7e41b26967b965c", size = 117326, upload-time = "2026-03-06T02:53:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/74/e2/b17d66abc26bd96f89dec0ecd0ef03da4a1286e6ff793839ec431b9fae57/wrapt-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e3d3b35eedcf5f7d022291ecd7533321c4775f7b9cd0050a31a68499ba45757c", size = 121444, upload-time = "2026-03-06T02:54:09.5Z" }, + { url = "https://files.pythonhosted.org/packages/3c/62/e2977843fdf9f03daf1586a0ff49060b1b2fc7ff85a7ea82b6217c1ae36e/wrapt-2.1.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:6f2c5390460de57fa9582bc8a1b7a6c86e1a41dfad74c5225fc07044c15cc8d1", size = 116237, upload-time = "2026-03-06T02:54:03.884Z" }, + { url = "https://files.pythonhosted.org/packages/88/dd/27fc67914e68d740bce512f11734aec08696e6b17641fef8867c00c949fc/wrapt-2.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7dfa9f2cf65d027b951d05c662cc99ee3bd01f6e4691ed39848a7a5fffc902b2", size = 120563, upload-time = "2026-03-06T02:53:20.412Z" }, + { url = "https://files.pythonhosted.org/packages/ec/9f/b750b3692ed2ef4705cb305bd68858e73010492b80e43d2a4faa5573cbe7/wrapt-2.1.2-cp312-cp312-win32.whl", hash = "sha256:eba8155747eb2cae4a0b913d9ebd12a1db4d860fc4c829d7578c7b989bd3f2f0", size = 58198, upload-time = "2026-03-06T02:53:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/8e/b2/feecfe29f28483d888d76a48f03c4c4d8afea944dbee2b0cd3380f9df032/wrapt-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1c51c738d7d9faa0b3601708e7e2eda9bf779e1b601dce6c77411f2a1b324a63", size = 60441, upload-time = "2026-03-06T02:52:47.138Z" }, + { url = "https://files.pythonhosted.org/packages/44/e1/e328f605d6e208547ea9fd120804fcdec68536ac748987a68c47c606eea8/wrapt-2.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:c8e46ae8e4032792eb2f677dbd0d557170a8e5524d22acc55199f43efedd39bf", size = 58836, upload-time = "2026-03-06T02:53:22.053Z" }, + { url = "https://files.pythonhosted.org/packages/4c/7a/d936840735c828b38d26a854e85d5338894cda544cb7a85a9d5b8b9c4df7/wrapt-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787fd6f4d67befa6fe2abdffcbd3de2d82dfc6fb8a6d850407c53332709d030b", size = 61259, upload-time = "2026-03-06T02:53:41.922Z" }, + { url = "https://files.pythonhosted.org/packages/5e/88/9a9b9a90ac8ca11c2fdb6a286cb3a1fc7dd774c00ed70929a6434f6bc634/wrapt-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4bdf26e03e6d0da3f0e9422fd36bcebf7bc0eeb55fdf9c727a09abc6b9fe472e", size = 61851, upload-time = "2026-03-06T02:52:48.672Z" }, + { url = "https://files.pythonhosted.org/packages/03/a9/5b7d6a16fd6533fed2756900fc8fc923f678179aea62ada6d65c92718c00/wrapt-2.1.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bbac24d879aa22998e87f6b3f481a5216311e7d53c7db87f189a7a0266dafffb", size = 121446, upload-time = "2026-03-06T02:54:14.013Z" }, + { url = "https://files.pythonhosted.org/packages/45/bb/34c443690c847835cfe9f892be78c533d4f32366ad2888972c094a897e39/wrapt-2.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:16997dfb9d67addc2e3f41b62a104341e80cac52f91110dece393923c0ebd5ca", size = 123056, upload-time = "2026-03-06T02:54:10.829Z" }, + { url = "https://files.pythonhosted.org/packages/93/b9/ff205f391cb708f67f41ea148545f2b53ff543a7ac293b30d178af4d2271/wrapt-2.1.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:162e4e2ba7542da9027821cb6e7c5e068d64f9a10b5f15512ea28e954893a267", size = 117359, upload-time = "2026-03-06T02:53:03.623Z" }, + { url = "https://files.pythonhosted.org/packages/1f/3d/1ea04d7747825119c3c9a5e0874a40b33594ada92e5649347c457d982805/wrapt-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f29c827a8d9936ac320746747a016c4bc66ef639f5cd0d32df24f5eacbf9c69f", size = 121479, upload-time = "2026-03-06T02:53:45.844Z" }, + { url = "https://files.pythonhosted.org/packages/78/cc/ee3a011920c7a023b25e8df26f306b2484a531ab84ca5c96260a73de76c0/wrapt-2.1.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:a9dd9813825f7ecb018c17fd147a01845eb330254dff86d3b5816f20f4d6aaf8", size = 116271, upload-time = "2026-03-06T02:54:46.356Z" }, + { url = "https://files.pythonhosted.org/packages/98/fd/e5ff7ded41b76d802cf1191288473e850d24ba2e39a6ec540f21ae3b57cb/wrapt-2.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6f8dbdd3719e534860d6a78526aafc220e0241f981367018c2875178cf83a413", size = 120573, upload-time = "2026-03-06T02:52:50.163Z" }, + { url = "https://files.pythonhosted.org/packages/47/c5/242cae3b5b080cd09bacef0591691ba1879739050cc7c801ff35c8886b66/wrapt-2.1.2-cp313-cp313-win32.whl", hash = "sha256:5c35b5d82b16a3bc6e0a04349b606a0582bc29f573786aebe98e0c159bc48db6", size = 58205, upload-time = "2026-03-06T02:53:47.494Z" }, + { url = "https://files.pythonhosted.org/packages/12/69/c358c61e7a50f290958809b3c61ebe8b3838ea3e070d7aac9814f95a0528/wrapt-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:f8bc1c264d8d1cf5b3560a87bbdd31131573eb25f9f9447bb6252b8d4c44a3a1", size = 60452, upload-time = "2026-03-06T02:53:30.038Z" }, + { url = "https://files.pythonhosted.org/packages/8e/66/c8a6fcfe321295fd8c0ab1bd685b5a01462a9b3aa2f597254462fc2bc975/wrapt-2.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:3beb22f674550d5634642c645aba4c72a2c66fb185ae1aebe1e955fae5a13baf", size = 58842, upload-time = "2026-03-06T02:52:52.114Z" }, + { url = "https://files.pythonhosted.org/packages/da/55/9c7052c349106e0b3f17ae8db4b23a691a963c334de7f9dbd60f8f74a831/wrapt-2.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0fc04bc8664a8bc4c8e00b37b5355cffca2535209fba1abb09ae2b7c76ddf82b", size = 63075, upload-time = "2026-03-06T02:53:19.108Z" }, + { url = "https://files.pythonhosted.org/packages/09/a8/ce7b4006f7218248dd71b7b2b732d0710845a0e49213b18faef64811ffef/wrapt-2.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a9b9d50c9af998875a1482a038eb05755dfd6fe303a313f6a940bb53a83c3f18", size = 63719, upload-time = "2026-03-06T02:54:33.452Z" }, + { url = "https://files.pythonhosted.org/packages/e4/e5/2ca472e80b9e2b7a17f106bb8f9df1db11e62101652ce210f66935c6af67/wrapt-2.1.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2d3ff4f0024dd224290c0eabf0240f1bfc1f26363431505fb1b0283d3b08f11d", size = 152643, upload-time = "2026-03-06T02:52:42.721Z" }, + { url = "https://files.pythonhosted.org/packages/36/42/30f0f2cefca9d9cbf6835f544d825064570203c3e70aa873d8ae12e23791/wrapt-2.1.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3278c471f4468ad544a691b31bb856374fbdefb7fee1a152153e64019379f015", size = 158805, upload-time = "2026-03-06T02:54:25.441Z" }, + { url = "https://files.pythonhosted.org/packages/bb/67/d08672f801f604889dcf58f1a0b424fe3808860ede9e03affc1876b295af/wrapt-2.1.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a8914c754d3134a3032601c6984db1c576e6abaf3fc68094bb8ab1379d75ff92", size = 145990, upload-time = "2026-03-06T02:53:57.456Z" }, + { url = "https://files.pythonhosted.org/packages/68/a7/fd371b02e73babec1de6ade596e8cd9691051058cfdadbfd62a5898f3295/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ff95d4264e55839be37bafe1536db2ab2de19da6b65f9244f01f332b5286cfbf", size = 155670, upload-time = "2026-03-06T02:54:55.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/2d/9fe0095dfdb621009f40117dcebf41d7396c2c22dca6eac779f4c007b86c/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:76405518ca4e1b76fbb1b9f686cff93aebae03920cc55ceeec48ff9f719c5f67", size = 144357, upload-time = "2026-03-06T02:54:24.092Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b6/ec7b4a254abbe4cde9fa15c5d2cca4518f6b07d0f1b77d4ee9655e30280e/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c0be8b5a74c5824e9359b53e7e58bef71a729bacc82e16587db1c4ebc91f7c5a", size = 150269, upload-time = "2026-03-06T02:53:31.268Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6b/2fabe8ebf148f4ee3c782aae86a795cc68ffe7d432ef550f234025ce0cfa/wrapt-2.1.2-cp313-cp313t-win32.whl", hash = "sha256:f01277d9a5fc1862f26f7626da9cf443bebc0abd2f303f41c5e995b15887dabd", size = 59894, upload-time = "2026-03-06T02:54:15.391Z" }, + { url = "https://files.pythonhosted.org/packages/ca/fb/9ba66fc2dedc936de5f8073c0217b5d4484e966d87723415cc8262c5d9c2/wrapt-2.1.2-cp313-cp313t-win_amd64.whl", hash = "sha256:84ce8f1c2104d2f6daa912b1b5b039f331febfeee74f8042ad4e04992bd95c8f", size = 63197, upload-time = "2026-03-06T02:54:41.943Z" }, + { url = "https://files.pythonhosted.org/packages/c0/1c/012d7423c95d0e337117723eb8ecf73c622ce15a97847e84cf3f8f26cd7e/wrapt-2.1.2-cp313-cp313t-win_arm64.whl", hash = "sha256:a93cd767e37faeddbe07d8fc4212d5cba660af59bdb0f6372c93faaa13e6e679", size = 60363, upload-time = "2026-03-06T02:54:48.093Z" }, + { url = "https://files.pythonhosted.org/packages/39/25/e7ea0b417db02bb796182a5316398a75792cd9a22528783d868755e1f669/wrapt-2.1.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1370e516598854e5b4366e09ce81e08bfe94d42b0fd569b88ec46cc56d9164a9", size = 61418, upload-time = "2026-03-06T02:53:55.706Z" }, + { url = "https://files.pythonhosted.org/packages/ec/0f/fa539e2f6a770249907757eaeb9a5ff4deb41c026f8466c1c6d799088a9b/wrapt-2.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6de1a3851c27e0bd6a04ca993ea6f80fc53e6c742ee1601f486c08e9f9b900a9", size = 61914, upload-time = "2026-03-06T02:52:53.37Z" }, + { url = "https://files.pythonhosted.org/packages/53/37/02af1867f5b1441aaeda9c82deed061b7cd1372572ddcd717f6df90b5e93/wrapt-2.1.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:de9f1a2bbc5ac7f6012ec24525bdd444765a2ff64b5985ac6e0692144838542e", size = 120417, upload-time = "2026-03-06T02:54:30.74Z" }, + { url = "https://files.pythonhosted.org/packages/c3/b7/0138a6238c8ba7476c77cf786a807f871672b37f37a422970342308276e7/wrapt-2.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:970d57ed83fa040d8b20c52fe74a6ae7e3775ae8cff5efd6a81e06b19078484c", size = 122797, upload-time = "2026-03-06T02:54:51.539Z" }, + { url = "https://files.pythonhosted.org/packages/e1/ad/819ae558036d6a15b7ed290d5b14e209ca795dd4da9c58e50c067d5927b0/wrapt-2.1.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3969c56e4563c375861c8df14fa55146e81ac11c8db49ea6fb7f2ba58bc1ff9a", size = 117350, upload-time = "2026-03-06T02:54:37.651Z" }, + { url = "https://files.pythonhosted.org/packages/8b/2d/afc18dc57a4600a6e594f77a9ae09db54f55ba455440a54886694a84c71b/wrapt-2.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:57d7c0c980abdc5f1d98b11a2aa3bb159790add80258c717fa49a99921456d90", size = 121223, upload-time = "2026-03-06T02:54:35.221Z" }, + { url = "https://files.pythonhosted.org/packages/b9/5b/5ec189b22205697bc56eb3b62aed87a1e0423e9c8285d0781c7a83170d15/wrapt-2.1.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:776867878e83130c7a04237010463372e877c1c994d449ca6aaafeab6aab2586", size = 116287, upload-time = "2026-03-06T02:54:19.654Z" }, + { url = "https://files.pythonhosted.org/packages/f7/2d/f84939a7c9b5e6cdd8a8d0f6a26cabf36a0f7e468b967720e8b0cd2bdf69/wrapt-2.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:fab036efe5464ec3291411fabb80a7a39e2dd80bae9bcbeeca5087fdfa891e19", size = 119593, upload-time = "2026-03-06T02:54:16.697Z" }, + { url = "https://files.pythonhosted.org/packages/0b/fe/ccd22a1263159c4ac811ab9374c061bcb4a702773f6e06e38de5f81a1bdc/wrapt-2.1.2-cp314-cp314-win32.whl", hash = "sha256:e6ed62c82ddf58d001096ae84ce7f833db97ae2263bff31c9b336ba8cfe3f508", size = 58631, upload-time = "2026-03-06T02:53:06.498Z" }, + { url = "https://files.pythonhosted.org/packages/65/0a/6bd83be7bff2e7efaac7b4ac9748da9d75a34634bbbbc8ad077d527146df/wrapt-2.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:467e7c76315390331c67073073d00662015bb730c566820c9ca9b54e4d67fd04", size = 60875, upload-time = "2026-03-06T02:53:50.252Z" }, + { url = "https://files.pythonhosted.org/packages/6c/c0/0b3056397fe02ff80e5a5d72d627c11eb885d1ca78e71b1a5c1e8c7d45de/wrapt-2.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:da1f00a557c66225d53b095a97eace0fc5349e3bfda28fa34ffae238978ee575", size = 59164, upload-time = "2026-03-06T02:53:59.128Z" }, + { url = "https://files.pythonhosted.org/packages/71/ed/5d89c798741993b2371396eb9d4634f009ff1ad8a6c78d366fe2883ea7a6/wrapt-2.1.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:62503ffbc2d3a69891cf29beeaccdb4d5e0a126e2b6a851688d4777e01428dbb", size = 63163, upload-time = "2026-03-06T02:52:54.873Z" }, + { url = "https://files.pythonhosted.org/packages/c6/8c/05d277d182bf36b0a13d6bd393ed1dec3468a25b59d01fba2dd70fe4d6ae/wrapt-2.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c7e6cd120ef837d5b6f860a6ea3745f8763805c418bb2f12eeb1fa6e25f22d22", size = 63723, upload-time = "2026-03-06T02:52:56.374Z" }, + { url = "https://files.pythonhosted.org/packages/f4/27/6c51ec1eff4413c57e72d6106bb8dec6f0c7cdba6503d78f0fa98767bcc9/wrapt-2.1.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3769a77df8e756d65fbc050333f423c01ae012b4f6731aaf70cf2bef61b34596", size = 152652, upload-time = "2026-03-06T02:53:23.79Z" }, + { url = "https://files.pythonhosted.org/packages/db/4c/d7dd662d6963fc7335bfe29d512b02b71cdfa23eeca7ab3ac74a67505deb/wrapt-2.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a76d61a2e851996150ba0f80582dd92a870643fa481f3b3846f229de88caf044", size = 158807, upload-time = "2026-03-06T02:53:35.742Z" }, + { url = "https://files.pythonhosted.org/packages/b4/4d/1e5eea1a78d539d346765727422976676615814029522c76b87a95f6bcdd/wrapt-2.1.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6f97edc9842cf215312b75fe737ee7c8adda75a89979f8e11558dfff6343cc4b", size = 146061, upload-time = "2026-03-06T02:52:57.574Z" }, + { url = "https://files.pythonhosted.org/packages/89/bc/62cabea7695cd12a288023251eeefdcb8465056ddaab6227cb78a2de005b/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4006c351de6d5007aa33a551f600404ba44228a89e833d2fadc5caa5de8edfbf", size = 155667, upload-time = "2026-03-06T02:53:39.422Z" }, + { url = "https://files.pythonhosted.org/packages/e9/99/6f2888cd68588f24df3a76572c69c2de28287acb9e1972bf0c83ce97dbc1/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a9372fc3639a878c8e7d87e1556fa209091b0a66e912c611e3f833e2c4202be2", size = 144392, upload-time = "2026-03-06T02:54:22.41Z" }, + { url = "https://files.pythonhosted.org/packages/40/51/1dfc783a6c57971614c48e361a82ca3b6da9055879952587bc99fe1a7171/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3144b027ff30cbd2fca07c0a87e67011adb717eb5f5bd8496325c17e454257a3", size = 150296, upload-time = "2026-03-06T02:54:07.848Z" }, + { url = "https://files.pythonhosted.org/packages/6c/38/cbb8b933a0201076c1f64fc42883b0023002bdc14a4964219154e6ff3350/wrapt-2.1.2-cp314-cp314t-win32.whl", hash = "sha256:3b8d15e52e195813efe5db8cec156eebe339aaf84222f4f4f051a6c01f237ed7", size = 60539, upload-time = "2026-03-06T02:54:00.594Z" }, + { url = "https://files.pythonhosted.org/packages/82/dd/e5176e4b241c9f528402cebb238a36785a628179d7d8b71091154b3e4c9e/wrapt-2.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:08ffa54146a7559f5b8df4b289b46d963a8e74ed16ba3687f99896101a3990c5", size = 63969, upload-time = "2026-03-06T02:54:39Z" }, + { url = "https://files.pythonhosted.org/packages/5c/99/79f17046cf67e4a95b9987ea129632ba8bcec0bc81f3fb3d19bdb0bd60cd/wrapt-2.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:72aaa9d0d8e4ed0e2e98019cea47a21f823c9dd4b43c7b77bba6679ffcca6a00", size = 60554, upload-time = "2026-03-06T02:53:14.132Z" }, + { url = "https://files.pythonhosted.org/packages/1a/c7/8528ac2dfa2c1e6708f647df7ae144ead13f0a31146f43c7264b4942bf12/wrapt-2.1.2-py3-none-any.whl", hash = "sha256:b8fd6fa2b2c4e7621808f8c62e8317f4aae56e59721ad933bac5239d913cf0e8", size = 43993, upload-time = "2026-03-06T02:53:12.905Z" }, +] + +[[package]] +name = "wsproto" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/79/12135bdf8b9c9367b8701c2c19a14c913c120b882d50b014ca0d38083c2c/wsproto-1.3.2.tar.gz", hash = "sha256:b86885dcf294e15204919950f666e06ffc6c7c114ca900b060d6e16293528294", size = 50116, upload-time = "2025-11-20T18:18:01.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl", hash = "sha256:61eea322cdf56e8cc904bd3ad7573359a242ba65688716b0710a5eb12beab584", size = 24405, upload-time = "2025-11-20T18:18:00.454Z" }, +] diff --git a/pyproject.toml b/pyproject.toml index a97ce9d3d8e..7ab7574cee5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -320,6 +320,7 @@ reflex-components-react-player.workspace = true reflex-components-recharts.workspace = true reflex-components-sonner.workspace = true reflex-docgen.workspace = true +reflex-hosting-cli.workspace = true reflex-components-internal.workspace = true reflex-site-shared.workspace = true reflex-integrations-docs.workspace = true diff --git a/uv.lock b/uv.lock index 819f50d25b4..ef75a4ffc59 100644 --- a/uv.lock +++ b/uv.lock @@ -31,6 +31,7 @@ members = [ "reflex-components-sonner", "reflex-docgen", "reflex-docs-app", + "reflex-hosting-cli", "reflex-integrations-docs", "reflex-site-shared", ] @@ -3516,7 +3517,7 @@ requires-dist = [ { name = "reflex-components-react-player", editable = "packages/reflex-components-react-player" }, { name = "reflex-components-recharts", editable = "packages/reflex-components-recharts" }, { name = "reflex-components-sonner", editable = "packages/reflex-components-sonner" }, - { name = "reflex-hosting-cli", specifier = ">=0.1.61" }, + { name = "reflex-hosting-cli", editable = "packages/reflex-hosting-cli" }, { name = "rich", specifier = ">=13,<15" }, { name = "sqlmodel", marker = "extra == 'db'", specifier = ">=0.0.24,<0.1" }, { name = "starlette", specifier = ">=0.47.0" }, @@ -3795,7 +3796,7 @@ requires-dist = [ { name = "reflex-components-internal", editable = "packages/reflex-components-internal" }, { name = "reflex-docgen", editable = "packages/reflex-docgen" }, { name = "reflex-enterprise" }, - { name = "reflex-hosting-cli" }, + { name = "reflex-hosting-cli", editable = "packages/reflex-hosting-cli" }, { name = "reflex-integrations-docs", editable = "packages/integrations-docs" }, { name = "reflex-pyplot" }, { name = "reflex-site-shared", editable = "packages/reflex-site-shared" }, @@ -3835,7 +3836,7 @@ wheels = [ [[package]] name = "reflex-hosting-cli" version = "0.1.62" -source = { registry = "https://pypi.org/simple" } +source = { editable = "packages/reflex-hosting-cli" } dependencies = [ { name = "click" }, { name = "httpx" }, @@ -3843,9 +3844,46 @@ dependencies = [ { name = "platformdirs" }, { name = "rich" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/83/2f/6d9acd7afd3971da8f0cdef4824551b836ec66c598088fc1af3c16d6f7a4/reflex_hosting_cli-0.1.62.tar.gz", hash = "sha256:7a3ab872218a7ebdfa2ea186b83440322e658fa90cf7ea270aec1bafe1eb0d98", size = 35506, upload-time = "2026-03-10T01:12:14.131Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/67/415e1f7fe09e77027e9e0063f39053d74f1299e671e837c9a7745453676d/reflex_hosting_cli-0.1.62-py3-none-any.whl", hash = "sha256:73d517fa827b1d52dcb81ba9024671acfd4889015a436ed223d2eda3c07eab89", size = 45049, upload-time = "2026-03-10T01:12:15.033Z" }, + +[package.dev-dependencies] +dev = [ + { name = "coverage" }, + { name = "darglint" }, + { name = "pre-commit" }, + { name = "pyright" }, + { name = "pytest" }, + { name = "pytest-benchmark" }, + { name = "pytest-cov" }, + { name = "pytest-mock" }, + { name = "pyyaml" }, + { name = "reflex" }, + { name = "ruff" }, + { name = "typer" }, +] + +[package.metadata] +requires-dist = [ + { name = "click", specifier = ">=8.2" }, + { name = "httpx", specifier = ">=0.25.1,<1.0" }, + { name = "packaging", specifier = ">=24.2" }, + { name = "platformdirs", specifier = ">=3.10.0,<5.0" }, + { name = "rich", specifier = ">=13,<15" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "coverage" }, + { name = "darglint" }, + { name = "pre-commit" }, + { name = "pyright" }, + { name = "pytest" }, + { name = "pytest-benchmark" }, + { name = "pytest-cov" }, + { name = "pytest-mock" }, + { name = "pyyaml" }, + { name = "reflex", editable = "." }, + { name = "ruff" }, + { name = "typer" }, ] [[package]] @@ -4132,6 +4170,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/82/c7/0c55fbb0275fc368676ea50514ce7d7839d799a8b3ff8425f380186c7626/selenium-4.43.0-py3-none-any.whl", hash = "sha256:4f97639055dcfa9eadf8ccf549ba7b0e49c655d4e2bde19b9a44e916b754e769", size = 9573091, upload-time = "2026-04-10T06:47:01.134Z" }, ] +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + [[package]] name = "simple-websocket" version = "1.1.0" @@ -4446,6 +4493,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bb/4a/2e5583e544bc437d5e8e54b47db87430df9031b29b48d17f26d129fa60c0/trove_classifiers-2026.1.14.14-py3-none-any.whl", hash = "sha256:1f9553927f18d0513d8e5ff80ab8980b8202ce37ecae0e3274ed2ef11880e74d", size = 14197, upload-time = "2026-01-14T14:54:49.067Z" }, ] +[[package]] +name = "typer" +version = "0.24.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/b8/9ebb531b6c2d377af08ac6746a5df3425b21853a5d2260876919b58a2a4a/typer-0.24.2.tar.gz", hash = "sha256:ec070dcfca1408e85ee203c6365001e818c3b7fffe686fd07ff2d68095ca0480", size = 119849, upload-time = "2026-04-22T17:45:34.413Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/d1/9484b497e0a0410b901c12b8251c3e746e1e863f7d28419ffe06f7892fda/typer-0.24.2-py3-none-any.whl", hash = "sha256:b618bc3d721f9a8d30f3e05565be26416d06e9bcc29d49bc491dc26aba674fa8", size = 55977, upload-time = "2026-04-22T17:45:33.055Z" }, +] + [[package]] name = "typesense" version = "2.0.0" From 94e3a0f03af5fac22c22eb9abfc9ae0c92706a8e Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Wed, 22 Apr 2026 17:55:35 -0700 Subject: [PATCH 02/16] update pyproject --- packages/hatch-reflex-pyi/pyproject.toml | 1 + packages/integrations-docs/pyproject.toml | 1 + packages/reflex-base/pyproject.toml | 1 + .../reflex-components-code/pyproject.toml | 1 + .../reflex-components-core/pyproject.toml | 1 + .../pyproject.toml | 1 + .../reflex-components-gridjs/pyproject.toml | 1 + .../reflex-components-internal/pyproject.toml | 2 + .../reflex-components-lucide/pyproject.toml | 1 + .../reflex-components-markdown/pyproject.toml | 1 + .../reflex-components-moment/pyproject.toml | 1 + .../reflex-components-plotly/pyproject.toml | 1 + .../reflex-components-radix/pyproject.toml | 1 + .../pyproject.toml | 1 + .../reflex-components-recharts/pyproject.toml | 1 + .../reflex-components-sonner/pyproject.toml | 1 + packages/reflex-docgen/pyproject.toml | 1 + packages/reflex-hosting-cli/LICENSE | 201 ------------------ packages/reflex-hosting-cli/pyproject.toml | 90 +------- packages/reflex-site-shared/pyproject.toml | 2 + pyproject.toml | 2 +- uv.lock | 57 ----- 22 files changed, 32 insertions(+), 338 deletions(-) delete mode 100644 packages/reflex-hosting-cli/LICENSE diff --git a/packages/hatch-reflex-pyi/pyproject.toml b/packages/hatch-reflex-pyi/pyproject.toml index a2754ad8a0c..49dbb448edc 100644 --- a/packages/hatch-reflex-pyi/pyproject.toml +++ b/packages/hatch-reflex-pyi/pyproject.toml @@ -2,6 +2,7 @@ name = "hatch-reflex-pyi" dynamic = ["version"] description = "Hatch build hook that generates .pyi stubs for Reflex component packages." +license.text = "Apache-2.0" readme = "README.md" authors = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] maintainers = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] diff --git a/packages/integrations-docs/pyproject.toml b/packages/integrations-docs/pyproject.toml index 8c1f21066f1..4224611b1fd 100644 --- a/packages/integrations-docs/pyproject.toml +++ b/packages/integrations-docs/pyproject.toml @@ -2,6 +2,7 @@ name = "reflex-integrations-docs" dynamic = ["version"] description = "Reflex Integrations Docs." +license.text = "Apache-2.0" readme = "README.md" authors = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] maintainers = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] diff --git a/packages/reflex-base/pyproject.toml b/packages/reflex-base/pyproject.toml index dac1ac47f52..1c267d80d3c 100644 --- a/packages/reflex-base/pyproject.toml +++ b/packages/reflex-base/pyproject.toml @@ -2,6 +2,7 @@ name = "reflex-base" dynamic = ["version"] description = "Core types for the Reflex framework." +license.text = "Apache-2.0" readme = "README.md" authors = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] maintainers = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] diff --git a/packages/reflex-components-code/pyproject.toml b/packages/reflex-components-code/pyproject.toml index 763f4b94712..ef6439bd439 100644 --- a/packages/reflex-components-code/pyproject.toml +++ b/packages/reflex-components-code/pyproject.toml @@ -2,6 +2,7 @@ name = "reflex-components-code" dynamic = ["version"] description = "Reflex code display components." +license.text = "Apache-2.0" readme = "README.md" authors = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] maintainers = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] diff --git a/packages/reflex-components-core/pyproject.toml b/packages/reflex-components-core/pyproject.toml index 0fb3e2dccca..4620ece472a 100644 --- a/packages/reflex-components-core/pyproject.toml +++ b/packages/reflex-components-core/pyproject.toml @@ -2,6 +2,7 @@ name = "reflex-components-core" dynamic = ["version"] description = "UI components for Reflex." +license.text = "Apache-2.0" readme = "README.md" authors = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] maintainers = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] diff --git a/packages/reflex-components-dataeditor/pyproject.toml b/packages/reflex-components-dataeditor/pyproject.toml index 40ad5717189..4e9d6f61b14 100644 --- a/packages/reflex-components-dataeditor/pyproject.toml +++ b/packages/reflex-components-dataeditor/pyproject.toml @@ -2,6 +2,7 @@ name = "reflex-components-dataeditor" dynamic = ["version"] description = "Reflex dataeditor components." +license.text = "Apache-2.0" readme = "README.md" authors = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] maintainers = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] diff --git a/packages/reflex-components-gridjs/pyproject.toml b/packages/reflex-components-gridjs/pyproject.toml index 1f6dc004ce9..6af602a3e89 100644 --- a/packages/reflex-components-gridjs/pyproject.toml +++ b/packages/reflex-components-gridjs/pyproject.toml @@ -2,6 +2,7 @@ name = "reflex-components-gridjs" dynamic = ["version"] description = "Reflex gridjs components." +license.text = "Apache-2.0" readme = "README.md" authors = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] maintainers = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] diff --git a/packages/reflex-components-internal/pyproject.toml b/packages/reflex-components-internal/pyproject.toml index 7a07ae2881c..c35de3244e2 100644 --- a/packages/reflex-components-internal/pyproject.toml +++ b/packages/reflex-components-internal/pyproject.toml @@ -2,6 +2,7 @@ name = "reflex-components-internal" dynamic = ["version"] description = "Reflex internal components." +license.text = "Apache-2.0" readme = "README.md" authors = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] maintainers = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] @@ -36,6 +37,7 @@ dependencies = [ "reflex-components-react-player", "reflex-components-recharts", "reflex-components-sonner", + "reflex-hosting-cli", ] [build-system] diff --git a/packages/reflex-components-lucide/pyproject.toml b/packages/reflex-components-lucide/pyproject.toml index b13287b210a..f61f75482c7 100644 --- a/packages/reflex-components-lucide/pyproject.toml +++ b/packages/reflex-components-lucide/pyproject.toml @@ -2,6 +2,7 @@ name = "reflex-components-lucide" dynamic = ["version"] description = "Reflex lucide components." +license.text = "Apache-2.0" readme = "README.md" authors = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] maintainers = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] diff --git a/packages/reflex-components-markdown/pyproject.toml b/packages/reflex-components-markdown/pyproject.toml index 322d2600316..95e347e5e30 100644 --- a/packages/reflex-components-markdown/pyproject.toml +++ b/packages/reflex-components-markdown/pyproject.toml @@ -2,6 +2,7 @@ name = "reflex-components-markdown" dynamic = ["version"] description = "Reflex markdown components." +license.text = "Apache-2.0" readme = "README.md" authors = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] maintainers = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] diff --git a/packages/reflex-components-moment/pyproject.toml b/packages/reflex-components-moment/pyproject.toml index 9e11d129ba1..8dc4dc89e76 100644 --- a/packages/reflex-components-moment/pyproject.toml +++ b/packages/reflex-components-moment/pyproject.toml @@ -2,6 +2,7 @@ name = "reflex-components-moment" dynamic = ["version"] description = "Reflex moment components." +license.text = "Apache-2.0" readme = "README.md" authors = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] maintainers = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] diff --git a/packages/reflex-components-plotly/pyproject.toml b/packages/reflex-components-plotly/pyproject.toml index 4e1a6866153..1df3dbbf3f9 100644 --- a/packages/reflex-components-plotly/pyproject.toml +++ b/packages/reflex-components-plotly/pyproject.toml @@ -2,6 +2,7 @@ name = "reflex-components-plotly" dynamic = ["version"] description = "Reflex plotly components." +license.text = "Apache-2.0" readme = "README.md" authors = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] maintainers = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] diff --git a/packages/reflex-components-radix/pyproject.toml b/packages/reflex-components-radix/pyproject.toml index 5e5aa750549..52288918209 100644 --- a/packages/reflex-components-radix/pyproject.toml +++ b/packages/reflex-components-radix/pyproject.toml @@ -2,6 +2,7 @@ name = "reflex-components-radix" dynamic = ["version"] description = "Reflex radix components." +license.text = "Apache-2.0" readme = "README.md" authors = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] maintainers = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] diff --git a/packages/reflex-components-react-player/pyproject.toml b/packages/reflex-components-react-player/pyproject.toml index b198bee33fc..96ea69612be 100644 --- a/packages/reflex-components-react-player/pyproject.toml +++ b/packages/reflex-components-react-player/pyproject.toml @@ -2,6 +2,7 @@ name = "reflex-components-react-player" dynamic = ["version"] description = "Reflex react-player components." +license.text = "Apache-2.0" readme = "README.md" authors = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] maintainers = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] diff --git a/packages/reflex-components-recharts/pyproject.toml b/packages/reflex-components-recharts/pyproject.toml index 8ed8f072bec..46ca97c1ab2 100644 --- a/packages/reflex-components-recharts/pyproject.toml +++ b/packages/reflex-components-recharts/pyproject.toml @@ -2,6 +2,7 @@ name = "reflex-components-recharts" dynamic = ["version"] description = "Reflex recharts components." +license.text = "Apache-2.0" readme = "README.md" authors = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] maintainers = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] diff --git a/packages/reflex-components-sonner/pyproject.toml b/packages/reflex-components-sonner/pyproject.toml index 837431fe861..e378463d2d6 100644 --- a/packages/reflex-components-sonner/pyproject.toml +++ b/packages/reflex-components-sonner/pyproject.toml @@ -2,6 +2,7 @@ name = "reflex-components-sonner" dynamic = ["version"] description = "Reflex sonner components." +license.text = "Apache-2.0" readme = "README.md" authors = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] maintainers = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] diff --git a/packages/reflex-docgen/pyproject.toml b/packages/reflex-docgen/pyproject.toml index fcd2e920d7e..fa180d5fb40 100644 --- a/packages/reflex-docgen/pyproject.toml +++ b/packages/reflex-docgen/pyproject.toml @@ -2,6 +2,7 @@ name = "reflex-docgen" dynamic = ["version"] description = "Generate documentation for Reflex components." +license.text = "Apache-2.0" readme = "README.md" authors = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] maintainers = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] diff --git a/packages/reflex-hosting-cli/LICENSE b/packages/reflex-hosting-cli/LICENSE deleted file mode 100644 index 7be89f167cb..00000000000 --- a/packages/reflex-hosting-cli/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - https://www.apache.org/licenses/LICENSE-2.0 - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2023, Pynecone, Inc. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/packages/reflex-hosting-cli/pyproject.toml b/packages/reflex-hosting-cli/pyproject.toml index 29eda98e31e..b21e2ce4365 100644 --- a/packages/reflex-hosting-cli/pyproject.toml +++ b/packages/reflex-hosting-cli/pyproject.toml @@ -1,8 +1,9 @@ [project] name = "reflex-hosting-cli" -version = "0.1.62" +dynamic = ["version"] description = "Reflex Hosting CLI" -license = "Apache-2.0" +license.text = "Apache-2.0" +readme = "README.md" authors = [ { name = "Nikhil Rao", email = "nikhil@reflex.dev" }, { name = "Alek Petuskey", email = "alek@reflex.dev" }, @@ -11,9 +12,6 @@ maintainers = [ { name = "Simon Young", email = "simon@reflex.dev" }, { name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }, ] -readme = "README.md" -keywords = ["web", "framework"] -classifiers = ["Development Status :: 4 - Beta"] requires-python = ">=3.10" dependencies = [ "click >=8.2", @@ -23,82 +21,16 @@ dependencies = [ "rich >=13,<15", ] -[project.urls] -homepage = "https://reflex.dev" -repository = "https://github.com/reflex-dev/reflex" -documentation = "https://reflex.dev/docs/getting-started/introduction" +[tool.hatch.version] +source = "uv-dynamic-versioning" -[dependency-groups] -dev = [ - "coverage", - "darglint", - "ruff", - "pre-commit", - "pyright", - "pytest", - "pytest-benchmark", - "pytest-cov", - "pytest-mock", - "typer", - "reflex", - "pyyaml", -] +[tool.uv-dynamic-versioning] +pattern-prefix = "reflex-hosting-cli-" +fallback-version = "0.0.0dev0" +[tool.hatch.build.targets.wheel] +packages = ["reflex_cli"] [build-system] -requires = ["hatchling"] +requires = ["hatchling", "uv-dynamic-versioning"] build-backend = "hatchling.build" - -[tool.hatch.build] -include = ["reflex_cli"] - -[tool.pyright] - -[tool.ruff] -output-format = "concise" -lint.isort.split-on-trailing-comma = false -lint.select = [ - "ANN001", - "B", - "C4", - "D", - "E", - "ERA", - "F", - "FURB", - "I", - "N", - "PERF", - "PGH", - "PTH", - "RUF", - "SIM", - "T", - "TRY", - "UP", - "W", -] -lint.ignore = [ - "B008", - "D205", - "E501", - "F403", - "SIM115", - "RUF006", - "RUF008", - "RUF012", - "TRY0", -] -lint.pydocstyle.convention = "google" -target-version = "py310" - - -[tool.ruff.lint.per-file-ignores] -"tests/*.py" = ["D100", "D103", "D104"] -"scripts/*.py" = ["T201"] - -[[tool.uv.index]] -name = "testpypi" -url = "https://test.pypi.org/simple/" -publish-url = "https://test.pypi.org/legacy/" -explicit = true diff --git a/packages/reflex-site-shared/pyproject.toml b/packages/reflex-site-shared/pyproject.toml index 4fdadce6044..3f859ebf1ea 100644 --- a/packages/reflex-site-shared/pyproject.toml +++ b/packages/reflex-site-shared/pyproject.toml @@ -2,6 +2,7 @@ name = "reflex-site-shared" dynamic = ["version"] description = "Reflex Site Shared." +license.text = "Apache-2.0" readme = "README.md" authors = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] maintainers = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] @@ -46,6 +47,7 @@ dependencies = [ "reflex-components-react-player", "reflex-components-recharts", "reflex-components-sonner", + "reflex-hosting-cli", "reflex", "ruff-format", "ruff", diff --git a/pyproject.toml b/pyproject.toml index 7ab7574cee5..ebea453b602 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ dependencies = [ "python-multipart >=0.0.20,<1.0", "python-socketio >=5.12.0,<6.0", "redis >=5.2.1,<8.0", - "reflex-hosting-cli >=0.1.61", + "reflex-hosting-cli", "rich >=13,<15", "starlette >=0.47.0", "typing_extensions >=4.13.0", diff --git a/uv.lock b/uv.lock index ef75a4ffc59..a914de738e5 100644 --- a/uv.lock +++ b/uv.lock @@ -3835,7 +3835,6 @@ wheels = [ [[package]] name = "reflex-hosting-cli" -version = "0.1.62" source = { editable = "packages/reflex-hosting-cli" } dependencies = [ { name = "click" }, @@ -3845,22 +3844,6 @@ dependencies = [ { name = "rich" }, ] -[package.dev-dependencies] -dev = [ - { name = "coverage" }, - { name = "darglint" }, - { name = "pre-commit" }, - { name = "pyright" }, - { name = "pytest" }, - { name = "pytest-benchmark" }, - { name = "pytest-cov" }, - { name = "pytest-mock" }, - { name = "pyyaml" }, - { name = "reflex" }, - { name = "ruff" }, - { name = "typer" }, -] - [package.metadata] requires-dist = [ { name = "click", specifier = ">=8.2" }, @@ -3870,22 +3853,6 @@ requires-dist = [ { name = "rich", specifier = ">=13,<15" }, ] -[package.metadata.requires-dev] -dev = [ - { name = "coverage" }, - { name = "darglint" }, - { name = "pre-commit" }, - { name = "pyright" }, - { name = "pytest" }, - { name = "pytest-benchmark" }, - { name = "pytest-cov" }, - { name = "pytest-mock" }, - { name = "pyyaml" }, - { name = "reflex", editable = "." }, - { name = "ruff" }, - { name = "typer" }, -] - [[package]] name = "reflex-integrations-docs" source = { editable = "packages/integrations-docs" } @@ -4170,15 +4137,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/82/c7/0c55fbb0275fc368676ea50514ce7d7839d799a8b3ff8425f380186c7626/selenium-4.43.0-py3-none-any.whl", hash = "sha256:4f97639055dcfa9eadf8ccf549ba7b0e49c655d4e2bde19b9a44e916b754e769", size = 9573091, upload-time = "2026-04-10T06:47:01.134Z" }, ] -[[package]] -name = "shellingham" -version = "1.5.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, -] - [[package]] name = "simple-websocket" version = "1.1.0" @@ -4493,21 +4451,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bb/4a/2e5583e544bc437d5e8e54b47db87430df9031b29b48d17f26d129fa60c0/trove_classifiers-2026.1.14.14-py3-none-any.whl", hash = "sha256:1f9553927f18d0513d8e5ff80ab8980b8202ce37ecae0e3274ed2ef11880e74d", size = 14197, upload-time = "2026-01-14T14:54:49.067Z" }, ] -[[package]] -name = "typer" -version = "0.24.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "annotated-doc" }, - { name = "click" }, - { name = "rich" }, - { name = "shellingham" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/83/b8/9ebb531b6c2d377af08ac6746a5df3425b21853a5d2260876919b58a2a4a/typer-0.24.2.tar.gz", hash = "sha256:ec070dcfca1408e85ee203c6365001e818c3b7fffe686fd07ff2d68095ca0480", size = 119849, upload-time = "2026-04-22T17:45:34.413Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/39/d1/9484b497e0a0410b901c12b8251c3e746e1e863f7d28419ffe06f7892fda/typer-0.24.2-py3-none-any.whl", hash = "sha256:b618bc3d721f9a8d30f3e05565be26416d06e9bcc29d49bc491dc26aba674fa8", size = 55977, upload-time = "2026-04-22T17:45:33.055Z" }, -] - [[package]] name = "typesense" version = "2.0.0" From b8f3d62877cf6f24bbb4d1b0c77cdabf39776a73 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Wed, 22 Apr 2026 18:02:59 -0700 Subject: [PATCH 03/16] make pre commit pass --- .../reflex_cli/utils/hosting.py | 25 ++++----- .../reflex-hosting-cli/reflex_cli/v2/apps.py | 30 +++++----- .../reflex_cli/v2/project.py | 1 - .../reflex_cli/v2/secrets.py | 1 - .../reflex_cli/v2/vmtypes_regions.py | 1 - ...ex_hosting_cli_next_dev_release_version.py | 26 ++++----- .../tests/utils/test_dependency.py | 1 - .../tests/utils/test_hosting.py | 1 - .../reflex-hosting-cli/tests/v2/test_apps.py | 55 ++++++++++--------- .../reflex-hosting-cli/tests/v2/test_cli.py | 1 - .../tests/v2/test_deployments.py | 1 - .../tests/v2/test_project.py | 35 +++++------- .../tests/v2/test_secrets.py | 12 ++-- .../tests/v2/test_vmtypes_regions.py | 13 ++--- pyproject.toml | 24 ++++++++ uv.lock | 26 +++++++++ 16 files changed, 140 insertions(+), 113 deletions(-) diff --git a/packages/reflex-hosting-cli/reflex_cli/utils/hosting.py b/packages/reflex-hosting-cli/reflex_cli/utils/hosting.py index d521a9baa47..a95dcea98a5 100644 --- a/packages/reflex-hosting-cli/reflex_cli/utils/hosting.py +++ b/packages/reflex-hosting-cli/reflex_cli/utils/hosting.py @@ -656,7 +656,7 @@ def search_app( ) raise click.exceptions.Exit(1) - elif len(apps) > 1 and interactive: + if len(apps) > 1 and interactive: return interactive_resolve_project_or_app_name_conflicts( apps, rows=[ @@ -667,10 +667,9 @@ def search_app( conflict_warn_msg="Found multiple apps with the same name. Select one to continue", conflict_ask_msg="Which app would you like to use?", ) - elif len(apps) == 1: + if len(apps) == 1: return apps[0] - else: - return None + return None def search_project( @@ -722,7 +721,7 @@ def search_project( ) raise click.exceptions.Exit(1) - elif len(projects) > 1 and interactive: + if len(projects) > 1 and interactive: return interactive_resolve_project_or_app_name_conflicts( projects, rows=[[f"({i})", x["id"], x["name"]] for i, x in enumerate(projects)], @@ -730,10 +729,9 @@ def search_project( conflict_warn_msg="Found multiple projects with the same name. Select one to continue", conflict_ask_msg="Which project would you like to use?", ) - elif len(projects) == 1: + if len(projects) == 1: return projects[0] - else: - return None + return None def get_app(app_id: str, client: AuthenticatedClient) -> dict: @@ -1521,7 +1519,7 @@ def delete_app(app_id: str, client: AuthenticatedClient): app = get_app(app_id=app_id, client=client) if not app: console.warn("no app with given id found") - return + return None response = httpx.delete( urljoin(constants.Hosting.HOSTING_SERVICE, f"/api/v1/apps/{app['id']}/delete"), headers=authorization_header(client.token), @@ -1567,10 +1565,10 @@ def get_app_logs( app = get_app(app_id=app_id, client=client) except GetAppError: console.warn(f"No application found with ID '{app_id}'") - return + return None if not app: console.warn("no app with given id found") - return + return None params = f"?offset={offset}" if offset else f"?start={start}&end={end}" if cursor: params += f"&cursor={cursor}" @@ -1896,7 +1894,7 @@ def list_projects(): None """ - return None + return def fetch_token(request_id: str) -> str: @@ -1973,8 +1971,7 @@ def authenticate_on_browser() -> tuple[str, dict[str, Any]]: access_token = fetch_token(request_id) if access_token: break - else: - time.sleep(1) + time.sleep(1) if access_token and (validated_info := validate_token_with_retries(access_token)): save_token_to_config(access_token) diff --git a/packages/reflex-hosting-cli/reflex_cli/v2/apps.py b/packages/reflex-hosting-cli/reflex_cli/v2/apps.py index 02f30c07091..9b465eb0514 100644 --- a/packages/reflex-hosting-cli/reflex_cli/v2/apps.py +++ b/packages/reflex-hosting-cli/reflex_cli/v2/apps.py @@ -23,7 +23,6 @@ @click.group() def apps_cli(): """Commands for managing apps.""" - pass @apps_cli.command(name="history") @@ -551,21 +550,20 @@ def app_logs( if prompt.lower() == "exit": console.info("Exiting log retrieval.") raise click.exceptions.Exit(0) - else: - ctx = click.get_current_context() - ctx.invoke( - app_logs, - app_id=app_id, - app_name=None, # Don't pass app_name again since we have app_id - token=token, - offset=offset, - start=start, - end=end, - loglevel=loglevel, - interactive=interactive, - cursor=cursor, - pretty=pretty, - ) + ctx = click.get_current_context() + ctx.invoke( + app_logs, + app_id=app_id, + app_name=None, # Don't pass app_name again since we have app_id + token=token, + offset=offset, + start=start, + end=end, + loglevel=loglevel, + interactive=interactive, + cursor=cursor, + pretty=pretty, + ) except ResponseError as err: console.error(f"Error retrieving logs: {err}") raise click.exceptions.Exit(1) from err diff --git a/packages/reflex-hosting-cli/reflex_cli/v2/project.py b/packages/reflex-hosting-cli/reflex_cli/v2/project.py index 8a8a6ad81c9..7522b1046d8 100644 --- a/packages/reflex-hosting-cli/reflex_cli/v2/project.py +++ b/packages/reflex-hosting-cli/reflex_cli/v2/project.py @@ -12,7 +12,6 @@ @click.group() def project_cli(): """Commands for managing projects.""" - pass @project_cli.command(name="create") diff --git a/packages/reflex-hosting-cli/reflex_cli/v2/secrets.py b/packages/reflex-hosting-cli/reflex_cli/v2/secrets.py index 7a49dc770c2..fa2fe0b2b3f 100644 --- a/packages/reflex-hosting-cli/reflex_cli/v2/secrets.py +++ b/packages/reflex-hosting-cli/reflex_cli/v2/secrets.py @@ -12,7 +12,6 @@ @click.group() def secrets_cli(): """Commands for managing secrets.""" - pass @secrets_cli.command(name="list") diff --git a/packages/reflex-hosting-cli/reflex_cli/v2/vmtypes_regions.py b/packages/reflex-hosting-cli/reflex_cli/v2/vmtypes_regions.py index 210a6b55493..d8049ffd4cc 100644 --- a/packages/reflex-hosting-cli/reflex_cli/v2/vmtypes_regions.py +++ b/packages/reflex-hosting-cli/reflex_cli/v2/vmtypes_regions.py @@ -11,7 +11,6 @@ @click.group() def vm_types_regions_cli(): """Commands for VM types and regions.""" - pass @vm_types_regions_cli.command("create-token") diff --git a/packages/reflex-hosting-cli/scripts/reflex_hosting_cli_next_dev_release_version.py b/packages/reflex-hosting-cli/scripts/reflex_hosting_cli_next_dev_release_version.py index 6c57700e16f..3a793294dd2 100644 --- a/packages/reflex-hosting-cli/scripts/reflex_hosting_cli_next_dev_release_version.py +++ b/packages/reflex-hosting-cli/scripts/reflex_hosting_cli_next_dev_release_version.py @@ -36,12 +36,11 @@ def get_latest_package_version_info( if not dev: # Stable release return version.parse(response_json["info"]["version"]) - else: - # All the releases including pre-releases - releases = response_json["releases"] - parsed_versions = [version.parse(v) for v in releases] - dev_releases = [v for v in parsed_versions if v.dev is not None] - return max(dev_releases) + # All the releases including pre-releases + releases = response_json["releases"] + parsed_versions = [version.parse(v) for v in releases] + dev_releases = [v for v in parsed_versions if v.dev is not None] + return max(dev_releases) except httpx.HTTPError as he: sys.stderr.write(f"Failed to fetch package info from PyPI: {he}") raise @@ -75,14 +74,13 @@ def get_next_dev_release_version( if latest_stable_version > latest_dev_version: # we bump the micro version return f"{latest_stable_version.major}.{latest_stable_version.minor}.{latest_stable_version.micro + 1}dev0" - else: - dev = latest_dev_version.dev - if dev is None: - raise ValueError( - f"The latest dev version not in expected format: got {latest_dev_version}" - ) - # we bump the dev build version - return f"{latest_dev_version.major}.{latest_dev_version.minor}.{latest_dev_version.micro}dev{dev + 1}" + dev = latest_dev_version.dev + if dev is None: + raise ValueError( + f"The latest dev version not in expected format: got {latest_dev_version}" + ) + # we bump the dev build version + return f"{latest_dev_version.major}.{latest_dev_version.minor}.{latest_dev_version.micro}dev{dev + 1}" def main(): diff --git a/packages/reflex-hosting-cli/tests/utils/test_dependency.py b/packages/reflex-hosting-cli/tests/utils/test_dependency.py index 175860d4be0..285007582d3 100644 --- a/packages/reflex-hosting-cli/tests/utils/test_dependency.py +++ b/packages/reflex-hosting-cli/tests/utils/test_dependency.py @@ -2,7 +2,6 @@ import pytest from pytest_mock import MockFixture - from reflex_cli.utils.dependency import detect_encoding, is_valid_url diff --git a/packages/reflex-hosting-cli/tests/utils/test_hosting.py b/packages/reflex-hosting-cli/tests/utils/test_hosting.py index c51c8e7bd28..c861d9a028e 100644 --- a/packages/reflex-hosting-cli/tests/utils/test_hosting.py +++ b/packages/reflex-hosting-cli/tests/utils/test_hosting.py @@ -6,7 +6,6 @@ import click import pytest from pytest_mock import MockerFixture, MockFixture - from reflex_cli.utils.hosting import ( authenticated_token, delete_token_from_config, diff --git a/packages/reflex-hosting-cli/tests/v2/test_apps.py b/packages/reflex-hosting-cli/tests/v2/test_apps.py index ee31ad22449..218068dc8f0 100644 --- a/packages/reflex-hosting-cli/tests/v2/test_apps.py +++ b/packages/reflex-hosting-cli/tests/v2/test_apps.py @@ -7,12 +7,11 @@ import pytest from click.testing import CliRunner from pytest_mock import MockerFixture, MockFixture -from typer.main import Typer, get_command - from reflex_cli.core.config import Config from reflex_cli.utils import hosting from reflex_cli.utils.exceptions import GetAppError from reflex_cli.v2.deployments import hosting_cli +from typer.main import Typer, get_command hosting_cli = ( get_command(hosting_cli) if isinstance(hosting_cli, Typer) else hosting_cli @@ -103,19 +102,17 @@ def test_app_history_as_json(mocker: MockFixture): ), ) mock_console_print.assert_called_once_with( - json.dumps( - [ - { - "id": "deployment1", - "status": "success", - "hostname": "example.com", - "python version": "3.10", - "reflex version": "1.2.3", - "vm type": "small", - "timestamp": "2024-11-29T12:00:00Z", - } - ] - ) + json.dumps([ + { + "id": "deployment1", + "status": "success", + "hostname": "example.com", + "python version": "3.10", + "reflex version": "1.2.3", + "vm type": "small", + "timestamp": "2024-11-29T12:00:00Z", + } + ]) ) @@ -468,9 +465,10 @@ def test_start_app_success(mocker: MockFixture): token="fake-token", validated_data={"foo": "bar"} ), ) - mock_success.assert_called_once_with( - {"status": "success", "message": "App started successfully"} - ) + mock_success.assert_called_once_with({ + "status": "success", + "message": "App started successfully", + }) def test_start_app_failure(mocker: MockFixture): @@ -584,9 +582,10 @@ def test_delete_app_success(mocker: MockFixture): token="fake-token", validated_data={"foo": "bar"} ), ) - mock_warn.assert_called_once_with( - {"status": "success", "message": "App deleted successfully"} - ) + mock_warn.assert_called_once_with({ + "status": "success", + "message": "App deleted successfully", + }) def test_delete_app_failure(mocker: MockFixture): @@ -778,9 +777,10 @@ def test_delete_app_non_interactive_skips_confirmation(mocker: MockFixture): token="fake-token", validated_data={"foo": "bar"} ), ) - mock_warn.assert_called_once_with( - {"status": "success", "message": "App deleted successfully"} - ) + mock_warn.assert_called_once_with({ + "status": "success", + "message": "App deleted successfully", + }) def test_delete_app_get_app_fails_fallback_to_unknown(mocker: MockFixture): @@ -848,9 +848,10 @@ def test_delete_app_with_app_name_confirmation(mocker: MockFixture): token="fake-token", validated_data={"foo": "bar"} ), ) - mock_warn.assert_called_once_with( - {"status": "success", "message": "App deleted successfully"} - ) + mock_warn.assert_called_once_with({ + "status": "success", + "message": "App deleted successfully", + }) def test_delete_app_not_found_early_exit(mocker: MockFixture): diff --git a/packages/reflex-hosting-cli/tests/v2/test_cli.py b/packages/reflex-hosting-cli/tests/v2/test_cli.py index 3d24b1ea4f2..fbeba06841d 100644 --- a/packages/reflex-hosting-cli/tests/v2/test_cli.py +++ b/packages/reflex-hosting-cli/tests/v2/test_cli.py @@ -9,7 +9,6 @@ import pytest from packaging import version from pytest_mock import MockerFixture, MockFixture - from reflex_cli.utils import hosting from reflex_cli.v2 import cli diff --git a/packages/reflex-hosting-cli/tests/v2/test_deployments.py b/packages/reflex-hosting-cli/tests/v2/test_deployments.py index c697ee5a17b..48d0a544b89 100644 --- a/packages/reflex-hosting-cli/tests/v2/test_deployments.py +++ b/packages/reflex-hosting-cli/tests/v2/test_deployments.py @@ -5,7 +5,6 @@ import httpx import pytest from pytest_mock import MockerFixture, MockFixture - from reflex_cli.v2.deployments import check_version diff --git a/packages/reflex-hosting-cli/tests/v2/test_project.py b/packages/reflex-hosting-cli/tests/v2/test_project.py index 620ca5c5b59..81e494cade0 100644 --- a/packages/reflex-hosting-cli/tests/v2/test_project.py +++ b/packages/reflex-hosting-cli/tests/v2/test_project.py @@ -3,12 +3,11 @@ import httpx from click.testing import CliRunner from pytest_mock import MockFixture -from typer import Typer -from typer.main import get_command - from reflex_cli.utils import hosting from reflex_cli.utils.exceptions import NotAuthenticatedError from reflex_cli.v2.deployments import hosting_cli +from typer import Typer +from typer.main import get_command hosting_cli = ( get_command(hosting_cli) if isinstance(hosting_cli, Typer) else hosting_cli @@ -494,12 +493,10 @@ def test_get_project_roles_as_json(mocker: MockFixture): ), ) mock_console_print.assert_called_once_with( - json.dumps( - [ - {"role": "admin", "user": "user1@example.com"}, - {"role": "viewer", "user": "user2@example.com"}, - ] - ) + json.dumps([ + {"role": "admin", "user": "user1@example.com"}, + {"role": "viewer", "user": "user2@example.com"}, + ]) ) @@ -665,12 +662,10 @@ def test_get_project_role_permissions_as_json(mocker: MockFixture): ), ) mock_console_print.assert_called_once_with( - json.dumps( - [ - {"permission": "read", "resource": "resource1"}, - {"permission": "write", "resource": "resource2"}, - ] - ) + json.dumps([ + {"permission": "read", "resource": "resource1"}, + {"permission": "write", "resource": "resource2"}, + ]) ) @@ -800,12 +795,10 @@ def test_get_project_role_users_as_json(mocker: MockFixture): ), ) mock_console_print.assert_called_once_with( - json.dumps( - [ - {"user_id": "user1", "role": "admin"}, - {"user_id": "user2", "role": "developer"}, - ] - ) + json.dumps([ + {"user_id": "user1", "role": "admin"}, + {"user_id": "user2", "role": "developer"}, + ]) ) diff --git a/packages/reflex-hosting-cli/tests/v2/test_secrets.py b/packages/reflex-hosting-cli/tests/v2/test_secrets.py index 94f654c7b05..0ee199b698f 100644 --- a/packages/reflex-hosting-cli/tests/v2/test_secrets.py +++ b/packages/reflex-hosting-cli/tests/v2/test_secrets.py @@ -3,11 +3,10 @@ from click.testing import CliRunner from pytest_mock import MockFixture -from typer import Typer -from typer.main import get_command - from reflex_cli.utils import hosting from reflex_cli.v2.deployments import hosting_cli +from typer import Typer +from typer.main import get_command hosting_cli = ( get_command(hosting_cli) if isinstance(hosting_cli, Typer) else hosting_cli @@ -111,9 +110,10 @@ def test_get_secrets_json_output(mocker: MockFixture): token="fake-token", validated_data={"foo": "bar"} ), ) - mock_console_print.assert_called_once_with( - {"secret_key_1": "value1", "secret_key_2": "value2"} - ) + mock_console_print.assert_called_once_with({ + "secret_key_1": "value1", + "secret_key_2": "value2", + }) assert result.exit_code == 0, result.output diff --git a/packages/reflex-hosting-cli/tests/v2/test_vmtypes_regions.py b/packages/reflex-hosting-cli/tests/v2/test_vmtypes_regions.py index 75fd7d0dd16..b3231cc84ef 100644 --- a/packages/reflex-hosting-cli/tests/v2/test_vmtypes_regions.py +++ b/packages/reflex-hosting-cli/tests/v2/test_vmtypes_regions.py @@ -4,11 +4,10 @@ import pytest from click.testing import CliRunner from pytest_mock import MockerFixture, MockFixture +from reflex_cli.v2.deployments import hosting_cli from typer import Typer from typer.main import get_command -from reflex_cli.v2.deployments import hosting_cli - hosting_cli = ( get_command(hosting_cli) if isinstance(hosting_cli, Typer) else hosting_cli ) @@ -169,12 +168,10 @@ def test_get_deployment_regions_as_json(mocker: MockFixture): assert result.exit_code == 0, result.output mock_get_regions.assert_called_once() mock_print.assert_called_once_with( - json.dumps( - [ - {"name": "Amsterdam, Netherlands", "code": "ams"}, - {"name": "Stockholm, Sweden", "code": "arn"}, - ] - ) + json.dumps([ + {"name": "Amsterdam, Netherlands", "code": "ams"}, + {"name": "Stockholm, Sweden", "code": "arn"}, + ]) ) diff --git a/pyproject.toml b/pyproject.toml index ebea453b602..f2d6daed92d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -110,6 +110,7 @@ dev = [ "sqlmodel", "starlette-admin", "toml", + "typer", "uvicorn", ] @@ -231,6 +232,29 @@ lint.flake8-bugbear.extend-immutable-calls = [ "*.pyi" = ["D301", "D415", "D417", "D418", "E742", "N", "PGH"] "pyi_generator.py" = ["N802"] "packages/reflex-base/src/reflex_base/constants/*.py" = ["N"] +"packages/reflex-hosting-cli/reflex_cli/**/*.py" = [ + "B008", + "D420", + "EM101", + "EM102", + "RET504", +] +"packages/reflex-hosting-cli/scripts/**/*.py" = [ + "D420", + "EM102", + "EXE002", + "FURB113", + "INP001", + "T201", +] +"packages/reflex-hosting-cli/tests/**/*.py" = [ + "D100", + "D103", + "D104", + "DOC201", + "PT006", + "RUF052", +] "*/.templates/apps/blank/code/*" = ["INP001"] "*/blank.py" = ["I001"] diff --git a/uv.lock b/uv.lock index a914de738e5..7cc4a581380 100644 --- a/uv.lock +++ b/uv.lock @@ -3489,6 +3489,7 @@ dev = [ { name = "sqlmodel" }, { name = "starlette-admin" }, { name = "toml" }, + { name = "typer" }, { name = "uvicorn" }, ] @@ -3562,6 +3563,7 @@ dev = [ { name = "sqlmodel" }, { name = "starlette-admin" }, { name = "toml" }, + { name = "typer" }, { name = "uvicorn" }, ] @@ -4137,6 +4139,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/82/c7/0c55fbb0275fc368676ea50514ce7d7839d799a8b3ff8425f380186c7626/selenium-4.43.0-py3-none-any.whl", hash = "sha256:4f97639055dcfa9eadf8ccf549ba7b0e49c655d4e2bde19b9a44e916b754e769", size = 9573091, upload-time = "2026-04-10T06:47:01.134Z" }, ] +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + [[package]] name = "simple-websocket" version = "1.1.0" @@ -4451,6 +4462,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bb/4a/2e5583e544bc437d5e8e54b47db87430df9031b29b48d17f26d129fa60c0/trove_classifiers-2026.1.14.14-py3-none-any.whl", hash = "sha256:1f9553927f18d0513d8e5ff80ab8980b8202ce37ecae0e3274ed2ef11880e74d", size = 14197, upload-time = "2026-01-14T14:54:49.067Z" }, ] +[[package]] +name = "typer" +version = "0.24.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/b8/9ebb531b6c2d377af08ac6746a5df3425b21853a5d2260876919b58a2a4a/typer-0.24.2.tar.gz", hash = "sha256:ec070dcfca1408e85ee203c6365001e818c3b7fffe686fd07ff2d68095ca0480", size = 119849, upload-time = "2026-04-22T17:45:34.413Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/d1/9484b497e0a0410b901c12b8251c3e746e1e863f7d28419ffe06f7892fda/typer-0.24.2-py3-none-any.whl", hash = "sha256:b618bc3d721f9a8d30f3e05565be26416d06e9bcc29d49bc491dc26aba674fa8", size = 55977, upload-time = "2026-04-22T17:45:33.055Z" }, +] + [[package]] name = "typesense" version = "2.0.0" From 978a030192b49d328c6ddc5ea6aa66df7658ed89 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Wed, 22 Apr 2026 18:06:57 -0700 Subject: [PATCH 04/16] move the files to similar structure --- packages/reflex-hosting-cli/{ => src}/reflex_cli/__init__.py | 0 packages/reflex-hosting-cli/{ => src}/reflex_cli/cli.py | 0 .../reflex-hosting-cli/{ => src}/reflex_cli/constants/__init__.py | 0 .../reflex-hosting-cli/{ => src}/reflex_cli/constants/base.py | 0 .../reflex-hosting-cli/{ => src}/reflex_cli/constants/compiler.py | 0 .../reflex-hosting-cli/{ => src}/reflex_cli/constants/hosting.py | 0 packages/reflex-hosting-cli/{ => src}/reflex_cli/core/__init__.py | 0 packages/reflex-hosting-cli/{ => src}/reflex_cli/core/config.py | 0 packages/reflex-hosting-cli/{ => src}/reflex_cli/deployments.py | 0 .../reflex-hosting-cli/{ => src}/reflex_cli/utils/__init__.py | 0 packages/reflex-hosting-cli/{ => src}/reflex_cli/utils/console.py | 0 .../reflex-hosting-cli/{ => src}/reflex_cli/utils/dependency.py | 0 .../reflex-hosting-cli/{ => src}/reflex_cli/utils/exceptions.py | 0 packages/reflex-hosting-cli/{ => src}/reflex_cli/utils/hosting.py | 0 packages/reflex-hosting-cli/{ => src}/reflex_cli/v2/__init__.py | 0 packages/reflex-hosting-cli/{ => src}/reflex_cli/v2/apps.py | 0 packages/reflex-hosting-cli/{ => src}/reflex_cli/v2/cli.py | 0 .../reflex-hosting-cli/{ => src}/reflex_cli/v2/deployments.py | 0 packages/reflex-hosting-cli/{ => src}/reflex_cli/v2/project.py | 0 packages/reflex-hosting-cli/{ => src}/reflex_cli/v2/secrets.py | 0 packages/reflex-hosting-cli/{ => src}/reflex_cli/v2/utils.py | 0 .../reflex-hosting-cli/{ => src}/reflex_cli/v2/vmtypes_regions.py | 0 22 files changed, 0 insertions(+), 0 deletions(-) rename packages/reflex-hosting-cli/{ => src}/reflex_cli/__init__.py (100%) rename packages/reflex-hosting-cli/{ => src}/reflex_cli/cli.py (100%) rename packages/reflex-hosting-cli/{ => src}/reflex_cli/constants/__init__.py (100%) rename packages/reflex-hosting-cli/{ => src}/reflex_cli/constants/base.py (100%) rename packages/reflex-hosting-cli/{ => src}/reflex_cli/constants/compiler.py (100%) rename packages/reflex-hosting-cli/{ => src}/reflex_cli/constants/hosting.py (100%) rename packages/reflex-hosting-cli/{ => src}/reflex_cli/core/__init__.py (100%) rename packages/reflex-hosting-cli/{ => src}/reflex_cli/core/config.py (100%) rename packages/reflex-hosting-cli/{ => src}/reflex_cli/deployments.py (100%) rename packages/reflex-hosting-cli/{ => src}/reflex_cli/utils/__init__.py (100%) rename packages/reflex-hosting-cli/{ => src}/reflex_cli/utils/console.py (100%) rename packages/reflex-hosting-cli/{ => src}/reflex_cli/utils/dependency.py (100%) rename packages/reflex-hosting-cli/{ => src}/reflex_cli/utils/exceptions.py (100%) rename packages/reflex-hosting-cli/{ => src}/reflex_cli/utils/hosting.py (100%) rename packages/reflex-hosting-cli/{ => src}/reflex_cli/v2/__init__.py (100%) rename packages/reflex-hosting-cli/{ => src}/reflex_cli/v2/apps.py (100%) rename packages/reflex-hosting-cli/{ => src}/reflex_cli/v2/cli.py (100%) rename packages/reflex-hosting-cli/{ => src}/reflex_cli/v2/deployments.py (100%) rename packages/reflex-hosting-cli/{ => src}/reflex_cli/v2/project.py (100%) rename packages/reflex-hosting-cli/{ => src}/reflex_cli/v2/secrets.py (100%) rename packages/reflex-hosting-cli/{ => src}/reflex_cli/v2/utils.py (100%) rename packages/reflex-hosting-cli/{ => src}/reflex_cli/v2/vmtypes_regions.py (100%) diff --git a/packages/reflex-hosting-cli/reflex_cli/__init__.py b/packages/reflex-hosting-cli/src/reflex_cli/__init__.py similarity index 100% rename from packages/reflex-hosting-cli/reflex_cli/__init__.py rename to packages/reflex-hosting-cli/src/reflex_cli/__init__.py diff --git a/packages/reflex-hosting-cli/reflex_cli/cli.py b/packages/reflex-hosting-cli/src/reflex_cli/cli.py similarity index 100% rename from packages/reflex-hosting-cli/reflex_cli/cli.py rename to packages/reflex-hosting-cli/src/reflex_cli/cli.py diff --git a/packages/reflex-hosting-cli/reflex_cli/constants/__init__.py b/packages/reflex-hosting-cli/src/reflex_cli/constants/__init__.py similarity index 100% rename from packages/reflex-hosting-cli/reflex_cli/constants/__init__.py rename to packages/reflex-hosting-cli/src/reflex_cli/constants/__init__.py diff --git a/packages/reflex-hosting-cli/reflex_cli/constants/base.py b/packages/reflex-hosting-cli/src/reflex_cli/constants/base.py similarity index 100% rename from packages/reflex-hosting-cli/reflex_cli/constants/base.py rename to packages/reflex-hosting-cli/src/reflex_cli/constants/base.py diff --git a/packages/reflex-hosting-cli/reflex_cli/constants/compiler.py b/packages/reflex-hosting-cli/src/reflex_cli/constants/compiler.py similarity index 100% rename from packages/reflex-hosting-cli/reflex_cli/constants/compiler.py rename to packages/reflex-hosting-cli/src/reflex_cli/constants/compiler.py diff --git a/packages/reflex-hosting-cli/reflex_cli/constants/hosting.py b/packages/reflex-hosting-cli/src/reflex_cli/constants/hosting.py similarity index 100% rename from packages/reflex-hosting-cli/reflex_cli/constants/hosting.py rename to packages/reflex-hosting-cli/src/reflex_cli/constants/hosting.py diff --git a/packages/reflex-hosting-cli/reflex_cli/core/__init__.py b/packages/reflex-hosting-cli/src/reflex_cli/core/__init__.py similarity index 100% rename from packages/reflex-hosting-cli/reflex_cli/core/__init__.py rename to packages/reflex-hosting-cli/src/reflex_cli/core/__init__.py diff --git a/packages/reflex-hosting-cli/reflex_cli/core/config.py b/packages/reflex-hosting-cli/src/reflex_cli/core/config.py similarity index 100% rename from packages/reflex-hosting-cli/reflex_cli/core/config.py rename to packages/reflex-hosting-cli/src/reflex_cli/core/config.py diff --git a/packages/reflex-hosting-cli/reflex_cli/deployments.py b/packages/reflex-hosting-cli/src/reflex_cli/deployments.py similarity index 100% rename from packages/reflex-hosting-cli/reflex_cli/deployments.py rename to packages/reflex-hosting-cli/src/reflex_cli/deployments.py diff --git a/packages/reflex-hosting-cli/reflex_cli/utils/__init__.py b/packages/reflex-hosting-cli/src/reflex_cli/utils/__init__.py similarity index 100% rename from packages/reflex-hosting-cli/reflex_cli/utils/__init__.py rename to packages/reflex-hosting-cli/src/reflex_cli/utils/__init__.py diff --git a/packages/reflex-hosting-cli/reflex_cli/utils/console.py b/packages/reflex-hosting-cli/src/reflex_cli/utils/console.py similarity index 100% rename from packages/reflex-hosting-cli/reflex_cli/utils/console.py rename to packages/reflex-hosting-cli/src/reflex_cli/utils/console.py diff --git a/packages/reflex-hosting-cli/reflex_cli/utils/dependency.py b/packages/reflex-hosting-cli/src/reflex_cli/utils/dependency.py similarity index 100% rename from packages/reflex-hosting-cli/reflex_cli/utils/dependency.py rename to packages/reflex-hosting-cli/src/reflex_cli/utils/dependency.py diff --git a/packages/reflex-hosting-cli/reflex_cli/utils/exceptions.py b/packages/reflex-hosting-cli/src/reflex_cli/utils/exceptions.py similarity index 100% rename from packages/reflex-hosting-cli/reflex_cli/utils/exceptions.py rename to packages/reflex-hosting-cli/src/reflex_cli/utils/exceptions.py diff --git a/packages/reflex-hosting-cli/reflex_cli/utils/hosting.py b/packages/reflex-hosting-cli/src/reflex_cli/utils/hosting.py similarity index 100% rename from packages/reflex-hosting-cli/reflex_cli/utils/hosting.py rename to packages/reflex-hosting-cli/src/reflex_cli/utils/hosting.py diff --git a/packages/reflex-hosting-cli/reflex_cli/v2/__init__.py b/packages/reflex-hosting-cli/src/reflex_cli/v2/__init__.py similarity index 100% rename from packages/reflex-hosting-cli/reflex_cli/v2/__init__.py rename to packages/reflex-hosting-cli/src/reflex_cli/v2/__init__.py diff --git a/packages/reflex-hosting-cli/reflex_cli/v2/apps.py b/packages/reflex-hosting-cli/src/reflex_cli/v2/apps.py similarity index 100% rename from packages/reflex-hosting-cli/reflex_cli/v2/apps.py rename to packages/reflex-hosting-cli/src/reflex_cli/v2/apps.py diff --git a/packages/reflex-hosting-cli/reflex_cli/v2/cli.py b/packages/reflex-hosting-cli/src/reflex_cli/v2/cli.py similarity index 100% rename from packages/reflex-hosting-cli/reflex_cli/v2/cli.py rename to packages/reflex-hosting-cli/src/reflex_cli/v2/cli.py diff --git a/packages/reflex-hosting-cli/reflex_cli/v2/deployments.py b/packages/reflex-hosting-cli/src/reflex_cli/v2/deployments.py similarity index 100% rename from packages/reflex-hosting-cli/reflex_cli/v2/deployments.py rename to packages/reflex-hosting-cli/src/reflex_cli/v2/deployments.py diff --git a/packages/reflex-hosting-cli/reflex_cli/v2/project.py b/packages/reflex-hosting-cli/src/reflex_cli/v2/project.py similarity index 100% rename from packages/reflex-hosting-cli/reflex_cli/v2/project.py rename to packages/reflex-hosting-cli/src/reflex_cli/v2/project.py diff --git a/packages/reflex-hosting-cli/reflex_cli/v2/secrets.py b/packages/reflex-hosting-cli/src/reflex_cli/v2/secrets.py similarity index 100% rename from packages/reflex-hosting-cli/reflex_cli/v2/secrets.py rename to packages/reflex-hosting-cli/src/reflex_cli/v2/secrets.py diff --git a/packages/reflex-hosting-cli/reflex_cli/v2/utils.py b/packages/reflex-hosting-cli/src/reflex_cli/v2/utils.py similarity index 100% rename from packages/reflex-hosting-cli/reflex_cli/v2/utils.py rename to packages/reflex-hosting-cli/src/reflex_cli/v2/utils.py diff --git a/packages/reflex-hosting-cli/reflex_cli/v2/vmtypes_regions.py b/packages/reflex-hosting-cli/src/reflex_cli/v2/vmtypes_regions.py similarity index 100% rename from packages/reflex-hosting-cli/reflex_cli/v2/vmtypes_regions.py rename to packages/reflex-hosting-cli/src/reflex_cli/v2/vmtypes_regions.py From 2d12d68c1026feedcbb858c1e9d81a79728b80b1 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Wed, 22 Apr 2026 18:07:14 -0700 Subject: [PATCH 05/16] fix links to directory names --- packages/reflex-hosting-cli/pyproject.toml | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/reflex-hosting-cli/pyproject.toml b/packages/reflex-hosting-cli/pyproject.toml index b21e2ce4365..c3dfca357db 100644 --- a/packages/reflex-hosting-cli/pyproject.toml +++ b/packages/reflex-hosting-cli/pyproject.toml @@ -29,7 +29,7 @@ pattern-prefix = "reflex-hosting-cli-" fallback-version = "0.0.0dev0" [tool.hatch.build.targets.wheel] -packages = ["reflex_cli"] +packages = ["src/reflex_cli"] [build-system] requires = ["hatchling", "uv-dynamic-versioning"] diff --git a/pyproject.toml b/pyproject.toml index f2d6daed92d..ea7a5fa27c7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -232,7 +232,7 @@ lint.flake8-bugbear.extend-immutable-calls = [ "*.pyi" = ["D301", "D415", "D417", "D418", "E742", "N", "PGH"] "pyi_generator.py" = ["N802"] "packages/reflex-base/src/reflex_base/constants/*.py" = ["N"] -"packages/reflex-hosting-cli/reflex_cli/**/*.py" = [ +"packages/reflex-hosting-cli/src/reflex_cli/**/*.py" = [ "B008", "D420", "EM101", From 5b36ac1a9b195a920853b96c8787eaec90a81fa0 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Wed, 22 Apr 2026 18:09:11 -0700 Subject: [PATCH 06/16] delete scripts dir --- .../scripts/check_site_responsive.py | 110 ------------------ ...ex_hosting_cli_next_dev_release_version.py | 101 ---------------- pyproject.toml | 8 -- 3 files changed, 219 deletions(-) delete mode 100755 packages/reflex-hosting-cli/scripts/check_site_responsive.py delete mode 100644 packages/reflex-hosting-cli/scripts/reflex_hosting_cli_next_dev_release_version.py diff --git a/packages/reflex-hosting-cli/scripts/check_site_responsive.py b/packages/reflex-hosting-cli/scripts/check_site_responsive.py deleted file mode 100755 index 778a6e56ca1..00000000000 --- a/packages/reflex-hosting-cli/scripts/check_site_responsive.py +++ /dev/null @@ -1,110 +0,0 @@ -"""Script to check if the backend and frontend of a deployed reflex app are responsive.""" - -from __future__ import annotations - -import argparse -import time -from concurrent.futures import ThreadPoolExecutor, as_completed - - -def _ping_site_with_retries(url: str, retries: int) -> tuple[bool, str]: - import httpx - - print(f"Checking {url} for responsiveness in {retries} retries.") - for i in range(retries): - try: - response = httpx.get(url) - response.raise_for_status() - except httpx.HTTPError: # noqa: PERF203 - time.sleep(1) - continue - else: - return True, f"{url} is responsive after {i} retries" - return False, f"{url} not responsive after {retries} retries." - - -ENV_TO_RUN_DOMAIN = { - "dev": "dev.reflexcorp.run", - "staging": "staging.reflexcorp.run", - "prod": "reflex.run", -} - - -def get_frontend_url(key: str, env: str = "dev") -> str: - """Return the frontend url for a deployment. - - Args: - key: the deployment key - env: dev/staging/prod. - - Returns: - The frontend URL by the given deployment key - - """ - return f"https://{key}.{ENV_TO_RUN_DOMAIN[env]}" - - -ENV_TO_APP_PREFIX = { - "dev": "rxh-dev-", - "staging": "rxh-staging-", - "prod": "rxh-prod-", -} - - -def get_backend_url(key: str, env: str = "dev"): - """Return the expected API URL for a deployment based on its key. - - Args: - key: the deployment key - env: dev/staging/prod - - Returns: - The backend URL by the given deployment key - - """ - return f"https://{ENV_TO_APP_PREFIX[env]}{key}.fly.dev" - - -def main(): - """Wait for ports to start listening.""" - parser = argparse.ArgumentParser( - description="Check the backend and frontend are responsive." - ) - parser.add_argument("--deployment-key", type=str, required=True) - parser.add_argument("--frontend-retries", type=int, default=30) - parser.add_argument("--backend-retries", type=int, default=90) - parser.add_argument("--env", type=str, required=True) - args = parser.parse_args() - - executor = ThreadPoolExecutor(max_workers=2) - futures = [] - print(f"Checking frontend and backend for: {args.deployment_key}") - frontend_url = get_frontend_url(key=args.deployment_key, env=args.env) - backend_url = get_backend_url(key=args.deployment_key, env=args.env) - - futures.append( - executor.submit(_ping_site_with_retries, frontend_url, args.frontend_retries) - ) - futures.append( - executor.submit( - _ping_site_with_retries, - f"{backend_url}/ping", # Note here, we need the ping endpoint instead of `/` - args.backend_retries, - ) - ) - - all_successful = True - for f in as_completed(futures): - ok, msg = f.result() - if ok: - print(f"OK: {msg}") - else: - print(f"FAIL: {msg}") - all_successful = False - - if not all_successful: - exit(1) - - -if __name__ == "__main__": - main() diff --git a/packages/reflex-hosting-cli/scripts/reflex_hosting_cli_next_dev_release_version.py b/packages/reflex-hosting-cli/scripts/reflex_hosting_cli_next_dev_release_version.py deleted file mode 100644 index 3a793294dd2..00000000000 --- a/packages/reflex-hosting-cli/scripts/reflex_hosting_cli_next_dev_release_version.py +++ /dev/null @@ -1,101 +0,0 @@ -"""Script to check the versions of reflex-hosting-cli on PyPI and return the next pre-release version.""" - -import json -import sys - -from packaging import version - -MODULE_NAME = "reflex-hosting-cli" - - -def get_latest_package_version_info( - package_name: str, index_url: str, dev: bool = True -) -> version.Version: - """Check if the latest version of the package on the index. - - Args: - package_name: The name of the package. - index_url: The index URL to check, e.g. https://pypi.org or https://test.pypi.org - dev: Whether to include dev releases only - - Raises: - httpx.HTTPError: If the request fails. - - Returns: - The latest version of the package published on the specified index - - """ - import httpx - - try: - # Get the latest version from specified index - url = f"{index_url}/pypi/{package_name}/json" - response = httpx.get(url) - response.raise_for_status() - response_json = response.json() - if not dev: - # Stable release - return version.parse(response_json["info"]["version"]) - # All the releases including pre-releases - releases = response_json["releases"] - parsed_versions = [version.parse(v) for v in releases] - dev_releases = [v for v in parsed_versions if v.dev is not None] - return max(dev_releases) - except httpx.HTTPError as he: - sys.stderr.write(f"Failed to fetch package info from PyPI: {he}") - raise - except ( - json.JSONDecodeError, - KeyError, - AttributeError, - ValueError, - version.InvalidVersion, - ) as ex: - sys.stderr.write(f"Unexpected response format from PyPI: {ex}") - raise - - -def get_next_dev_release_version( - latest_stable_version: version.Version, latest_dev_version: version.Version -) -> str: - """Get the next dev release version. - - Args: - latest_stable_version: The latest stable version of the package on PyPI. - latest_dev_version: The latest dev version of the package on Test PyPI. - - Raises: - ValueError: if the dev version is ill-formed. - - Returns: - The next dev release version in string. - - """ - if latest_stable_version > latest_dev_version: - # we bump the micro version - return f"{latest_stable_version.major}.{latest_stable_version.minor}.{latest_stable_version.micro + 1}dev0" - dev = latest_dev_version.dev - if dev is None: - raise ValueError( - f"The latest dev version not in expected format: got {latest_dev_version}" - ) - # we bump the dev build version - return f"{latest_dev_version.major}.{latest_dev_version.minor}.{latest_dev_version.micro}dev{dev + 1}" - - -def main(): - """Return the latest version of reflex-hosting-cli on PyPI or Test PyPI.""" - latest_stable_version = get_latest_package_version_info( - MODULE_NAME, "https://pypi.org", dev=False - ) - latest_dev_version = get_latest_package_version_info( - MODULE_NAME, "https://test.pypi.org", dev=True - ) - next_dev_version = get_next_dev_release_version( - latest_stable_version, latest_dev_version - ) - sys.stdout.write(str(next_dev_version)) - - -if __name__ == "__main__": - main() diff --git a/pyproject.toml b/pyproject.toml index ea7a5fa27c7..05d2829e2f5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -239,14 +239,6 @@ lint.flake8-bugbear.extend-immutable-calls = [ "EM102", "RET504", ] -"packages/reflex-hosting-cli/scripts/**/*.py" = [ - "D420", - "EM102", - "EXE002", - "FURB113", - "INP001", - "T201", -] "packages/reflex-hosting-cli/tests/**/*.py" = [ "D100", "D103", From 12ee0628387f52d6a0e2dc0b025c722fa2368f62 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Wed, 22 Apr 2026 18:11:28 -0700 Subject: [PATCH 07/16] move the tests --- .../tests => tests/units/reflex_cli}/__init__.py | 0 .../tests => tests/units/reflex_cli}/utils/__init__.py | 0 .../tests => tests/units/reflex_cli}/utils/test_dependency.py | 0 .../tests => tests/units/reflex_cli}/utils/test_hosting.py | 0 .../tests => tests/units/reflex_cli}/v2/__init__.py | 0 .../tests => tests/units/reflex_cli}/v2/test_apps.py | 0 .../tests => tests/units/reflex_cli}/v2/test_cli.py | 0 .../tests => tests/units/reflex_cli}/v2/test_deployments.py | 0 .../tests => tests/units/reflex_cli}/v2/test_project.py | 0 .../tests => tests/units/reflex_cli}/v2/test_secrets.py | 0 .../tests => tests/units/reflex_cli}/v2/test_vmtypes_regions.py | 0 11 files changed, 0 insertions(+), 0 deletions(-) rename {packages/reflex-hosting-cli/tests => tests/units/reflex_cli}/__init__.py (100%) rename {packages/reflex-hosting-cli/tests => tests/units/reflex_cli}/utils/__init__.py (100%) rename {packages/reflex-hosting-cli/tests => tests/units/reflex_cli}/utils/test_dependency.py (100%) rename {packages/reflex-hosting-cli/tests => tests/units/reflex_cli}/utils/test_hosting.py (100%) rename {packages/reflex-hosting-cli/tests => tests/units/reflex_cli}/v2/__init__.py (100%) rename {packages/reflex-hosting-cli/tests => tests/units/reflex_cli}/v2/test_apps.py (100%) rename {packages/reflex-hosting-cli/tests => tests/units/reflex_cli}/v2/test_cli.py (100%) rename {packages/reflex-hosting-cli/tests => tests/units/reflex_cli}/v2/test_deployments.py (100%) rename {packages/reflex-hosting-cli/tests => tests/units/reflex_cli}/v2/test_project.py (100%) rename {packages/reflex-hosting-cli/tests => tests/units/reflex_cli}/v2/test_secrets.py (100%) rename {packages/reflex-hosting-cli/tests => tests/units/reflex_cli}/v2/test_vmtypes_regions.py (100%) diff --git a/packages/reflex-hosting-cli/tests/__init__.py b/tests/units/reflex_cli/__init__.py similarity index 100% rename from packages/reflex-hosting-cli/tests/__init__.py rename to tests/units/reflex_cli/__init__.py diff --git a/packages/reflex-hosting-cli/tests/utils/__init__.py b/tests/units/reflex_cli/utils/__init__.py similarity index 100% rename from packages/reflex-hosting-cli/tests/utils/__init__.py rename to tests/units/reflex_cli/utils/__init__.py diff --git a/packages/reflex-hosting-cli/tests/utils/test_dependency.py b/tests/units/reflex_cli/utils/test_dependency.py similarity index 100% rename from packages/reflex-hosting-cli/tests/utils/test_dependency.py rename to tests/units/reflex_cli/utils/test_dependency.py diff --git a/packages/reflex-hosting-cli/tests/utils/test_hosting.py b/tests/units/reflex_cli/utils/test_hosting.py similarity index 100% rename from packages/reflex-hosting-cli/tests/utils/test_hosting.py rename to tests/units/reflex_cli/utils/test_hosting.py diff --git a/packages/reflex-hosting-cli/tests/v2/__init__.py b/tests/units/reflex_cli/v2/__init__.py similarity index 100% rename from packages/reflex-hosting-cli/tests/v2/__init__.py rename to tests/units/reflex_cli/v2/__init__.py diff --git a/packages/reflex-hosting-cli/tests/v2/test_apps.py b/tests/units/reflex_cli/v2/test_apps.py similarity index 100% rename from packages/reflex-hosting-cli/tests/v2/test_apps.py rename to tests/units/reflex_cli/v2/test_apps.py diff --git a/packages/reflex-hosting-cli/tests/v2/test_cli.py b/tests/units/reflex_cli/v2/test_cli.py similarity index 100% rename from packages/reflex-hosting-cli/tests/v2/test_cli.py rename to tests/units/reflex_cli/v2/test_cli.py diff --git a/packages/reflex-hosting-cli/tests/v2/test_deployments.py b/tests/units/reflex_cli/v2/test_deployments.py similarity index 100% rename from packages/reflex-hosting-cli/tests/v2/test_deployments.py rename to tests/units/reflex_cli/v2/test_deployments.py diff --git a/packages/reflex-hosting-cli/tests/v2/test_project.py b/tests/units/reflex_cli/v2/test_project.py similarity index 100% rename from packages/reflex-hosting-cli/tests/v2/test_project.py rename to tests/units/reflex_cli/v2/test_project.py diff --git a/packages/reflex-hosting-cli/tests/v2/test_secrets.py b/tests/units/reflex_cli/v2/test_secrets.py similarity index 100% rename from packages/reflex-hosting-cli/tests/v2/test_secrets.py rename to tests/units/reflex_cli/v2/test_secrets.py diff --git a/packages/reflex-hosting-cli/tests/v2/test_vmtypes_regions.py b/tests/units/reflex_cli/v2/test_vmtypes_regions.py similarity index 100% rename from packages/reflex-hosting-cli/tests/v2/test_vmtypes_regions.py rename to tests/units/reflex_cli/v2/test_vmtypes_regions.py From e2ecc9ba8c4c8153947242188f95c6a580134c12 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Wed, 22 Apr 2026 18:22:27 -0700 Subject: [PATCH 08/16] make tests pass --- pyproject.toml | 9 +-------- tests/units/reflex_cli/conftest.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 8 deletions(-) create mode 100644 tests/units/reflex_cli/conftest.py diff --git a/pyproject.toml b/pyproject.toml index 05d2829e2f5..79f7bc40acd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -239,14 +239,7 @@ lint.flake8-bugbear.extend-immutable-calls = [ "EM102", "RET504", ] -"packages/reflex-hosting-cli/tests/**/*.py" = [ - "D100", - "D103", - "D104", - "DOC201", - "PT006", - "RUF052", -] +"tests/units/reflex_cli/**/*.py" = ["DOC201", "PT006", "RUF052"] "*/.templates/apps/blank/code/*" = ["INP001"] "*/blank.py" = ["I001"] diff --git a/tests/units/reflex_cli/conftest.py b/tests/units/reflex_cli/conftest.py new file mode 100644 index 00000000000..f01f3f74a1f --- /dev/null +++ b/tests/units/reflex_cli/conftest.py @@ -0,0 +1,14 @@ +"""Shared fixtures for reflex_cli tests.""" + +import pytest +from pytest_mock import MockFixture + + +@pytest.fixture(autouse=True) +def mock_check_version(mocker: MockFixture) -> None: + """Bypass the hosting-cli PyPI version check during tests. + + The workspace build reports a dev version older than the published one, + causing `check_version` to emit a warning and exit(1). + """ + mocker.patch("reflex_cli.v2.deployments.check_version") From 1292f4fefcd82bd222b8fa4b3ea50a0420b0dac3 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Wed, 22 Apr 2026 18:23:36 -0700 Subject: [PATCH 09/16] delet config files --- packages/reflex-hosting-cli/.coveragerc | 31 - packages/reflex-hosting-cli/.gitignore | 9 - packages/reflex-hosting-cli/uv.lock | 1537 ----------------------- 3 files changed, 1577 deletions(-) delete mode 100644 packages/reflex-hosting-cli/.coveragerc delete mode 100644 packages/reflex-hosting-cli/.gitignore delete mode 100644 packages/reflex-hosting-cli/uv.lock diff --git a/packages/reflex-hosting-cli/.coveragerc b/packages/reflex-hosting-cli/.coveragerc deleted file mode 100644 index f0b2771d54e..00000000000 --- a/packages/reflex-hosting-cli/.coveragerc +++ /dev/null @@ -1,31 +0,0 @@ -[run] -source = reflex_cli -branch = true - -[report] -show_missing = true -# TODO bump back -fail_under = 50 -precision = 2 - -# Regexes for lines to exclude from consideration -exclude_also = - # Don't complain about missing debug-only code: - def __repr__ - if self\.debug - - # Don't complain if tests don't hit defensive assertion code: - raise AssertionError - raise NotImplementedError - - # Don't complain if non-runnable code isn't run: - if 0: - if __name__ == .__main__.: - - # Don't complain about abstract methods, they aren't run: - @(abc\.)?abstractmethod - -ignore_errors = True - -[html] -directory = coverage_html_report diff --git a/packages/reflex-hosting-cli/.gitignore b/packages/reflex-hosting-cli/.gitignore deleted file mode 100644 index df43a9c49ce..00000000000 --- a/packages/reflex-hosting-cli/.gitignore +++ /dev/null @@ -1,9 +0,0 @@ -**/.DS_Store -**/*.pyc -dist/* -examples/ -.idea -.vscode -.coverage -.coverage.* -venv diff --git a/packages/reflex-hosting-cli/uv.lock b/packages/reflex-hosting-cli/uv.lock deleted file mode 100644 index 2c152873769..00000000000 --- a/packages/reflex-hosting-cli/uv.lock +++ /dev/null @@ -1,1537 +0,0 @@ -version = 1 -revision = 3 -requires-python = ">=3.10" - -[[package]] -name = "alembic" -version = "1.18.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mako" }, - { name = "sqlalchemy" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/94/13/8b084e0f2efb0275a1d534838844926f798bd766566b1375174e2448cd31/alembic-1.18.4.tar.gz", hash = "sha256:cb6e1fd84b6174ab8dbb2329f86d631ba9559dd78df550b57804d607672cedbc", size = 2056725, upload-time = "2026-02-10T16:00:47.195Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/29/6533c317b74f707ea28f8d633734dbda2119bbadfc61b2f3640ba835d0f7/alembic-1.18.4-py3-none-any.whl", hash = "sha256:a5ed4adcf6d8a4cb575f3d759f071b03cd6e5c7618eb796cb52497be25bfe19a", size = 263893, upload-time = "2026-02-10T16:00:49.997Z" }, -] - -[[package]] -name = "annotated-doc" -version = "0.0.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, -] - -[[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, upload-time = "2024-05-20T21:33:25.928Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, -] - -[[package]] -name = "anyio" -version = "4.12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, - { name = "idna" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, -] - -[[package]] -name = "async-timeout" -version = "5.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274, upload-time = "2024-11-06T16:41:39.6Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" }, -] - -[[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, upload-time = "2024-02-18T19:09:05.748Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl", hash = "sha256:5dae8d4d79b552a71cbabc7deb25dfe8ce710b17ff41711e13010ead2abfc3e5", size = 32764, upload-time = "2024-02-18T19:09:04.156Z" }, -] - -[[package]] -name = "certifi" -version = "2026.2.25" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, -] - -[[package]] -name = "cfgv" -version = "3.5.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, -] - -[[package]] -name = "click" -version = "8.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, -] - -[[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, upload-time = "2022-10-25T02:36:22.414Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, -] - -[[package]] -name = "coverage" -version = "7.13.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/24/56/95b7e30fa389756cb56630faa728da46a27b8c6eb46f9d557c68fff12b65/coverage-7.13.4.tar.gz", hash = "sha256:e5c8f6ed1e61a8b2dcdf31eb0b9bbf0130750ca79c1c49eb898e2ad86f5ccc91", size = 827239, upload-time = "2026-02-09T12:59:03.86Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/44/d4/7827d9ffa34d5d4d752eec907022aa417120936282fc488306f5da08c292/coverage-7.13.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fc31c787a84f8cd6027eba44010517020e0d18487064cd3d8968941856d1415", size = 219152, upload-time = "2026-02-09T12:56:11.974Z" }, - { url = "https://files.pythonhosted.org/packages/35/b0/d69df26607c64043292644dbb9dc54b0856fabaa2cbb1eeee3331cc9e280/coverage-7.13.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a32ebc02a1805adf637fc8dec324b5cdacd2e493515424f70ee33799573d661b", size = 219667, upload-time = "2026-02-09T12:56:13.33Z" }, - { url = "https://files.pythonhosted.org/packages/82/a4/c1523f7c9e47b2271dbf8c2a097e7a1f89ef0d66f5840bb59b7e8814157b/coverage-7.13.4-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e24f9156097ff9dc286f2f913df3a7f63c0e333dcafa3c196f2c18b4175ca09a", size = 246425, upload-time = "2026-02-09T12:56:14.552Z" }, - { url = "https://files.pythonhosted.org/packages/f8/02/aa7ec01d1a5023c4b680ab7257f9bfde9defe8fdddfe40be096ac19e8177/coverage-7.13.4-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8041b6c5bfdc03257666e9881d33b1abc88daccaf73f7b6340fb7946655cd10f", size = 248229, upload-time = "2026-02-09T12:56:16.31Z" }, - { url = "https://files.pythonhosted.org/packages/35/98/85aba0aed5126d896162087ef3f0e789a225697245256fc6181b95f47207/coverage-7.13.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a09cfa6a5862bc2fc6ca7c3def5b2926194a56b8ab78ffcf617d28911123012", size = 250106, upload-time = "2026-02-09T12:56:18.024Z" }, - { url = "https://files.pythonhosted.org/packages/96/72/1db59bd67494bc162e3e4cd5fbc7edba2c7026b22f7c8ef1496d58c2b94c/coverage-7.13.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:296f8b0af861d3970c2a4d8c91d48eb4dd4771bcef9baedec6a9b515d7de3def", size = 252021, upload-time = "2026-02-09T12:56:19.272Z" }, - { url = "https://files.pythonhosted.org/packages/9d/97/72899c59c7066961de6e3daa142d459d47d104956db43e057e034f015c8a/coverage-7.13.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e101609bcbbfb04605ea1027b10dc3735c094d12d40826a60f897b98b1c30256", size = 247114, upload-time = "2026-02-09T12:56:21.051Z" }, - { url = "https://files.pythonhosted.org/packages/39/1f/f1885573b5970235e908da4389176936c8933e86cb316b9620aab1585fa2/coverage-7.13.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:aa3feb8db2e87ff5e6d00d7e1480ae241876286691265657b500886c98f38bda", size = 248143, upload-time = "2026-02-09T12:56:22.585Z" }, - { url = "https://files.pythonhosted.org/packages/a8/cf/e80390c5b7480b722fa3e994f8202807799b85bc562aa4f1dde209fbb7be/coverage-7.13.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4fc7fa81bbaf5a02801b65346c8b3e657f1d93763e58c0abdf7c992addd81a92", size = 246152, upload-time = "2026-02-09T12:56:23.748Z" }, - { url = "https://files.pythonhosted.org/packages/44/bf/f89a8350d85572f95412debb0fb9bb4795b1d5b5232bd652923c759e787b/coverage-7.13.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:33901f604424145c6e9c2398684b92e176c0b12df77d52db81c20abd48c3794c", size = 249959, upload-time = "2026-02-09T12:56:25.209Z" }, - { url = "https://files.pythonhosted.org/packages/f7/6e/612a02aece8178c818df273e8d1642190c4875402ca2ba74514394b27aba/coverage-7.13.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:bb28c0f2cf2782508a40cec377935829d5fcc3ad9a3681375af4e84eb34b6b58", size = 246416, upload-time = "2026-02-09T12:56:26.475Z" }, - { url = "https://files.pythonhosted.org/packages/cb/98/b5afc39af67c2fa6786b03c3a7091fc300947387ce8914b096db8a73d67a/coverage-7.13.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9d107aff57a83222ddbd8d9ee705ede2af2cc926608b57abed8ef96b50b7e8f9", size = 247025, upload-time = "2026-02-09T12:56:27.727Z" }, - { url = "https://files.pythonhosted.org/packages/51/30/2bba8ef0682d5bd210c38fe497e12a06c9f8d663f7025e9f5c2c31ce847d/coverage-7.13.4-cp310-cp310-win32.whl", hash = "sha256:a6f94a7d00eb18f1b6d403c91a88fd58cfc92d4b16080dfdb774afc8294469bf", size = 221758, upload-time = "2026-02-09T12:56:29.051Z" }, - { url = "https://files.pythonhosted.org/packages/78/13/331f94934cf6c092b8ea59ff868eb587bc8fe0893f02c55bc6c0183a192e/coverage-7.13.4-cp310-cp310-win_amd64.whl", hash = "sha256:2cb0f1e000ebc419632bbe04366a8990b6e32c4e0b51543a6484ffe15eaeda95", size = 222693, upload-time = "2026-02-09T12:56:30.366Z" }, - { url = "https://files.pythonhosted.org/packages/b4/ad/b59e5b451cf7172b8d1043dc0fa718f23aab379bc1521ee13d4bd9bfa960/coverage-7.13.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d490ba50c3f35dd7c17953c68f3270e7ccd1c6642e2d2afe2d8e720b98f5a053", size = 219278, upload-time = "2026-02-09T12:56:31.673Z" }, - { url = "https://files.pythonhosted.org/packages/f1/17/0cb7ca3de72e5f4ef2ec2fa0089beafbcaaaead1844e8b8a63d35173d77d/coverage-7.13.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:19bc3c88078789f8ef36acb014d7241961dbf883fd2533d18cb1e7a5b4e28b11", size = 219783, upload-time = "2026-02-09T12:56:33.104Z" }, - { url = "https://files.pythonhosted.org/packages/ab/63/325d8e5b11e0eaf6d0f6a44fad444ae58820929a9b0de943fa377fe73e85/coverage-7.13.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3998e5a32e62fdf410c0dbd3115df86297995d6e3429af80b8798aad894ca7aa", size = 250200, upload-time = "2026-02-09T12:56:34.474Z" }, - { url = "https://files.pythonhosted.org/packages/76/53/c16972708cbb79f2942922571a687c52bd109a7bd51175aeb7558dff2236/coverage-7.13.4-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e264226ec98e01a8e1054314af91ee6cde0eacac4f465cc93b03dbe0bce2fd7", size = 252114, upload-time = "2026-02-09T12:56:35.749Z" }, - { url = "https://files.pythonhosted.org/packages/eb/c2/7ab36d8b8cc412bec9ea2d07c83c48930eb4ba649634ba00cb7e4e0f9017/coverage-7.13.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3aa4e7b9e416774b21797365b358a6e827ffadaaca81b69ee02946852449f00", size = 254220, upload-time = "2026-02-09T12:56:37.796Z" }, - { url = "https://files.pythonhosted.org/packages/d6/4d/cf52c9a3322c89a0e6febdfbc83bb45c0ed3c64ad14081b9503adee702e7/coverage-7.13.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:71ca20079dd8f27fcf808817e281e90220475cd75115162218d0e27549f95fef", size = 256164, upload-time = "2026-02-09T12:56:39.016Z" }, - { url = "https://files.pythonhosted.org/packages/78/e9/eb1dd17bd6de8289df3580e967e78294f352a5df8a57ff4671ee5fc3dcd0/coverage-7.13.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e2f25215f1a359ab17320b47bcdaca3e6e6356652e8256f2441e4ef972052903", size = 250325, upload-time = "2026-02-09T12:56:40.668Z" }, - { url = "https://files.pythonhosted.org/packages/71/07/8c1542aa873728f72267c07278c5cc0ec91356daf974df21335ccdb46368/coverage-7.13.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d65b2d373032411e86960604dc4edac91fdfb5dca539461cf2cbe78327d1e64f", size = 251913, upload-time = "2026-02-09T12:56:41.97Z" }, - { url = "https://files.pythonhosted.org/packages/74/d7/c62e2c5e4483a748e27868e4c32ad3daa9bdddbba58e1bc7a15e252baa74/coverage-7.13.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94eb63f9b363180aff17de3e7c8760c3ba94664ea2695c52f10111244d16a299", size = 249974, upload-time = "2026-02-09T12:56:43.323Z" }, - { url = "https://files.pythonhosted.org/packages/98/9f/4c5c015a6e98ced54efd0f5cf8d31b88e5504ecb6857585fc0161bb1e600/coverage-7.13.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e856bf6616714c3a9fbc270ab54103f4e685ba236fa98c054e8f87f266c93505", size = 253741, upload-time = "2026-02-09T12:56:45.155Z" }, - { url = "https://files.pythonhosted.org/packages/bd/59/0f4eef89b9f0fcd9633b5d350016f54126ab49426a70ff4c4e87446cabdc/coverage-7.13.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:65dfcbe305c3dfe658492df2d85259e0d79ead4177f9ae724b6fb245198f55d6", size = 249695, upload-time = "2026-02-09T12:56:46.636Z" }, - { url = "https://files.pythonhosted.org/packages/b5/2c/b7476f938deb07166f3eb281a385c262675d688ff4659ad56c6c6b8e2e70/coverage-7.13.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b507778ae8a4c915436ed5c2e05b4a6cecfa70f734e19c22a005152a11c7b6a9", size = 250599, upload-time = "2026-02-09T12:56:48.13Z" }, - { url = "https://files.pythonhosted.org/packages/b8/34/c3420709d9846ee3785b9f2831b4d94f276f38884032dca1457fa83f7476/coverage-7.13.4-cp311-cp311-win32.whl", hash = "sha256:784fc3cf8be001197b652d51d3fd259b1e2262888693a4636e18879f613a62a9", size = 221780, upload-time = "2026-02-09T12:56:50.479Z" }, - { url = "https://files.pythonhosted.org/packages/61/08/3d9c8613079d2b11c185b865de9a4c1a68850cfda2b357fae365cf609f29/coverage-7.13.4-cp311-cp311-win_amd64.whl", hash = "sha256:2421d591f8ca05b308cf0092807308b2facbefe54af7c02ac22548b88b95c98f", size = 222715, upload-time = "2026-02-09T12:56:51.815Z" }, - { url = "https://files.pythonhosted.org/packages/18/1a/54c3c80b2f056164cc0a6cdcb040733760c7c4be9d780fe655f356f433e4/coverage-7.13.4-cp311-cp311-win_arm64.whl", hash = "sha256:79e73a76b854d9c6088fe5d8b2ebe745f8681c55f7397c3c0a016192d681045f", size = 221385, upload-time = "2026-02-09T12:56:53.194Z" }, - { url = "https://files.pythonhosted.org/packages/d1/81/4ce2fdd909c5a0ed1f6dedb88aa57ab79b6d1fbd9b588c1ac7ef45659566/coverage-7.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02231499b08dabbe2b96612993e5fc34217cdae907a51b906ac7fca8027a4459", size = 219449, upload-time = "2026-02-09T12:56:54.889Z" }, - { url = "https://files.pythonhosted.org/packages/5d/96/5238b1efc5922ddbdc9b0db9243152c09777804fb7c02ad1741eb18a11c0/coverage-7.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40aa8808140e55dc022b15d8aa7f651b6b3d68b365ea0398f1441e0b04d859c3", size = 219810, upload-time = "2026-02-09T12:56:56.33Z" }, - { url = "https://files.pythonhosted.org/packages/78/72/2f372b726d433c9c35e56377cf1d513b4c16fe51841060d826b95caacec1/coverage-7.13.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5b856a8ccf749480024ff3bd7310adaef57bf31fd17e1bfc404b7940b6986634", size = 251308, upload-time = "2026-02-09T12:56:57.858Z" }, - { url = "https://files.pythonhosted.org/packages/5d/a0/2ea570925524ef4e00bb6c82649f5682a77fac5ab910a65c9284de422600/coverage-7.13.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c048ea43875fbf8b45d476ad79f179809c590ec7b79e2035c662e7afa3192e3", size = 254052, upload-time = "2026-02-09T12:56:59.754Z" }, - { url = "https://files.pythonhosted.org/packages/e8/ac/45dc2e19a1939098d783c846e130b8f862fbb50d09e0af663988f2f21973/coverage-7.13.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7b38448866e83176e28086674fe7368ab8590e4610fb662b44e345b86d63ffa", size = 255165, upload-time = "2026-02-09T12:57:01.287Z" }, - { url = "https://files.pythonhosted.org/packages/2d/4d/26d236ff35abc3b5e63540d3386e4c3b192168c1d96da5cb2f43c640970f/coverage-7.13.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de6defc1c9badbf8b9e67ae90fd00519186d6ab64e5cc5f3d21359c2a9b2c1d3", size = 257432, upload-time = "2026-02-09T12:57:02.637Z" }, - { url = "https://files.pythonhosted.org/packages/ec/55/14a966c757d1348b2e19caf699415a2a4c4f7feaa4bbc6326a51f5c7dd1b/coverage-7.13.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7eda778067ad7ffccd23ecffce537dface96212576a07924cbf0d8799d2ded5a", size = 251716, upload-time = "2026-02-09T12:57:04.056Z" }, - { url = "https://files.pythonhosted.org/packages/77/33/50116647905837c66d28b2af1321b845d5f5d19be9655cb84d4a0ea806b4/coverage-7.13.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e87f6c587c3f34356c3759f0420693e35e7eb0e2e41e4c011cb6ec6ecbbf1db7", size = 253089, upload-time = "2026-02-09T12:57:05.503Z" }, - { url = "https://files.pythonhosted.org/packages/c2/b4/8efb11a46e3665d92635a56e4f2d4529de6d33f2cb38afd47d779d15fc99/coverage-7.13.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8248977c2e33aecb2ced42fef99f2d319e9904a36e55a8a68b69207fb7e43edc", size = 251232, upload-time = "2026-02-09T12:57:06.879Z" }, - { url = "https://files.pythonhosted.org/packages/51/24/8cd73dd399b812cc76bb0ac260e671c4163093441847ffe058ac9fda1e32/coverage-7.13.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:25381386e80ae727608e662474db537d4df1ecd42379b5ba33c84633a2b36d47", size = 255299, upload-time = "2026-02-09T12:57:08.245Z" }, - { url = "https://files.pythonhosted.org/packages/03/94/0a4b12f1d0e029ce1ccc1c800944a9984cbe7d678e470bb6d3c6bc38a0da/coverage-7.13.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ee756f00726693e5ba94d6df2bdfd64d4852d23b09bb0bc700e3b30e6f333985", size = 250796, upload-time = "2026-02-09T12:57:10.142Z" }, - { url = "https://files.pythonhosted.org/packages/73/44/6002fbf88f6698ca034360ce474c406be6d5a985b3fdb3401128031eef6b/coverage-7.13.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fdfc1e28e7c7cdce44985b3043bc13bbd9c747520f94a4d7164af8260b3d91f0", size = 252673, upload-time = "2026-02-09T12:57:12.197Z" }, - { url = "https://files.pythonhosted.org/packages/de/c6/a0279f7c00e786be75a749a5674e6fa267bcbd8209cd10c9a450c655dfa7/coverage-7.13.4-cp312-cp312-win32.whl", hash = "sha256:01d4cbc3c283a17fc1e42d614a119f7f438eabb593391283adca8dc86eff1246", size = 221990, upload-time = "2026-02-09T12:57:14.085Z" }, - { url = "https://files.pythonhosted.org/packages/77/4e/c0a25a425fcf5557d9abd18419c95b63922e897bc86c1f327f155ef234a9/coverage-7.13.4-cp312-cp312-win_amd64.whl", hash = "sha256:9401ebc7ef522f01d01d45532c68c5ac40fb27113019b6b7d8b208f6e9baa126", size = 222800, upload-time = "2026-02-09T12:57:15.944Z" }, - { url = "https://files.pythonhosted.org/packages/47/ac/92da44ad9a6f4e3a7debd178949d6f3769bedca33830ce9b1dcdab589a37/coverage-7.13.4-cp312-cp312-win_arm64.whl", hash = "sha256:b1ec7b6b6e93255f952e27ab58fbc68dcc468844b16ecbee881aeb29b6ab4d8d", size = 221415, upload-time = "2026-02-09T12:57:17.497Z" }, - { url = "https://files.pythonhosted.org/packages/db/23/aad45061a31677d68e47499197a131eea55da4875d16c1f42021ab963503/coverage-7.13.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b66a2da594b6068b48b2692f043f35d4d3693fb639d5ea8b39533c2ad9ac3ab9", size = 219474, upload-time = "2026-02-09T12:57:19.332Z" }, - { url = "https://files.pythonhosted.org/packages/a5/70/9b8b67a0945f3dfec1fd896c5cefb7c19d5a3a6d74630b99a895170999ae/coverage-7.13.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3599eb3992d814d23b35c536c28df1a882caa950f8f507cef23d1cbf334995ac", size = 219844, upload-time = "2026-02-09T12:57:20.66Z" }, - { url = "https://files.pythonhosted.org/packages/97/fd/7e859f8fab324cef6c4ad7cff156ca7c489fef9179d5749b0c8d321281c2/coverage-7.13.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:93550784d9281e374fb5a12bf1324cc8a963fd63b2d2f223503ef0fd4aa339ea", size = 250832, upload-time = "2026-02-09T12:57:22.007Z" }, - { url = "https://files.pythonhosted.org/packages/e4/dc/b2442d10020c2f52617828862d8b6ee337859cd8f3a1f13d607dddda9cf7/coverage-7.13.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b720ce6a88a2755f7c697c23268ddc47a571b88052e6b155224347389fdf6a3b", size = 253434, upload-time = "2026-02-09T12:57:23.339Z" }, - { url = "https://files.pythonhosted.org/packages/5a/88/6728a7ad17428b18d836540630487231f5470fb82454871149502f5e5aa2/coverage-7.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b322db1284a2ed3aa28ffd8ebe3db91c929b7a333c0820abec3d838ef5b3525", size = 254676, upload-time = "2026-02-09T12:57:24.774Z" }, - { url = "https://files.pythonhosted.org/packages/7c/bc/21244b1b8cedf0dff0a2b53b208015fe798d5f2a8d5348dbfece04224fff/coverage-7.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4594c67d8a7c89cf922d9df0438c7c7bb022ad506eddb0fdb2863359ff78242", size = 256807, upload-time = "2026-02-09T12:57:26.125Z" }, - { url = "https://files.pythonhosted.org/packages/97/a0/ddba7ed3251cff51006737a727d84e05b61517d1784a9988a846ba508877/coverage-7.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:53d133df809c743eb8bce33b24bcababb371f4441340578cd406e084d94a6148", size = 251058, upload-time = "2026-02-09T12:57:27.614Z" }, - { url = "https://files.pythonhosted.org/packages/9b/55/e289addf7ff54d3a540526f33751951bf0878f3809b47f6dfb3def69c6f7/coverage-7.13.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76451d1978b95ba6507a039090ba076105c87cc76fc3efd5d35d72093964d49a", size = 252805, upload-time = "2026-02-09T12:57:29.066Z" }, - { url = "https://files.pythonhosted.org/packages/13/4e/cc276b1fa4a59be56d96f1dabddbdc30f4ba22e3b1cd42504c37b3313255/coverage-7.13.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7f57b33491e281e962021de110b451ab8a24182589be17e12a22c79047935e23", size = 250766, upload-time = "2026-02-09T12:57:30.522Z" }, - { url = "https://files.pythonhosted.org/packages/94/44/1093b8f93018f8b41a8cf29636c9292502f05e4a113d4d107d14a3acd044/coverage-7.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1731dc33dc276dafc410a885cbf5992f1ff171393e48a21453b78727d090de80", size = 254923, upload-time = "2026-02-09T12:57:31.946Z" }, - { url = "https://files.pythonhosted.org/packages/8b/55/ea2796da2d42257f37dbea1aab239ba9263b31bd91d5527cdd6db5efe174/coverage-7.13.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:bd60d4fe2f6fa7dff9223ca1bbc9f05d2b6697bc5961072e5d3b952d46e1b1ea", size = 250591, upload-time = "2026-02-09T12:57:33.842Z" }, - { url = "https://files.pythonhosted.org/packages/d4/fa/7c4bb72aacf8af5020675aa633e59c1fbe296d22aed191b6a5b711eb2bc7/coverage-7.13.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9181a3ccead280b828fae232df12b16652702b49d41e99d657f46cc7b1f6ec7a", size = 252364, upload-time = "2026-02-09T12:57:35.743Z" }, - { url = "https://files.pythonhosted.org/packages/5c/38/a8d2ec0146479c20bbaa7181b5b455a0c41101eed57f10dd19a78ab44c80/coverage-7.13.4-cp313-cp313-win32.whl", hash = "sha256:f53d492307962561ac7de4cd1de3e363589b000ab69617c6156a16ba7237998d", size = 222010, upload-time = "2026-02-09T12:57:37.25Z" }, - { url = "https://files.pythonhosted.org/packages/e2/0c/dbfafbe90a185943dcfbc766fe0e1909f658811492d79b741523a414a6cc/coverage-7.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:e6f70dec1cc557e52df5306d051ef56003f74d56e9c4dd7ddb07e07ef32a84dd", size = 222818, upload-time = "2026-02-09T12:57:38.734Z" }, - { url = "https://files.pythonhosted.org/packages/04/d1/934918a138c932c90d78301f45f677fb05c39a3112b96fd2c8e60503cdc7/coverage-7.13.4-cp313-cp313-win_arm64.whl", hash = "sha256:fb07dc5da7e849e2ad31a5d74e9bece81f30ecf5a42909d0a695f8bd1874d6af", size = 221438, upload-time = "2026-02-09T12:57:40.223Z" }, - { url = "https://files.pythonhosted.org/packages/52/57/ee93ced533bcb3e6df961c0c6e42da2fc6addae53fb95b94a89b1e33ebd7/coverage-7.13.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40d74da8e6c4b9ac18b15331c4b5ebc35a17069410cad462ad4f40dcd2d50c0d", size = 220165, upload-time = "2026-02-09T12:57:41.639Z" }, - { url = "https://files.pythonhosted.org/packages/c5/e0/969fc285a6fbdda49d91af278488d904dcd7651b2693872f0ff94e40e84a/coverage-7.13.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4223b4230a376138939a9173f1bdd6521994f2aff8047fae100d6d94d50c5a12", size = 220516, upload-time = "2026-02-09T12:57:44.215Z" }, - { url = "https://files.pythonhosted.org/packages/b1/b8/9531944e16267e2735a30a9641ff49671f07e8138ecf1ca13db9fd2560c7/coverage-7.13.4-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1d4be36a5114c499f9f1f9195e95ebf979460dbe2d88e6816ea202010ba1c34b", size = 261804, upload-time = "2026-02-09T12:57:45.989Z" }, - { url = "https://files.pythonhosted.org/packages/8a/f3/e63df6d500314a2a60390d1989240d5f27318a7a68fa30ad3806e2a9323e/coverage-7.13.4-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:200dea7d1e8095cc6e98cdabe3fd1d21ab17d3cee6dab00cadbb2fe35d9c15b9", size = 263885, upload-time = "2026-02-09T12:57:47.42Z" }, - { url = "https://files.pythonhosted.org/packages/f3/67/7654810de580e14b37670b60a09c599fa348e48312db5b216d730857ffe6/coverage-7.13.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8eb931ee8e6d8243e253e5ed7336deea6904369d2fd8ae6e43f68abbf167092", size = 266308, upload-time = "2026-02-09T12:57:49.345Z" }, - { url = "https://files.pythonhosted.org/packages/37/6f/39d41eca0eab3cc82115953ad41c4e77935286c930e8fad15eaed1389d83/coverage-7.13.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75eab1ebe4f2f64d9509b984f9314d4aa788540368218b858dad56dc8f3e5eb9", size = 267452, upload-time = "2026-02-09T12:57:50.811Z" }, - { url = "https://files.pythonhosted.org/packages/50/6d/39c0fbb8fc5cd4d2090811e553c2108cf5112e882f82505ee7495349a6bf/coverage-7.13.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c35eb28c1d085eb7d8c9b3296567a1bebe03ce72962e932431b9a61f28facf26", size = 261057, upload-time = "2026-02-09T12:57:52.447Z" }, - { url = "https://files.pythonhosted.org/packages/a4/a2/60010c669df5fa603bb5a97fb75407e191a846510da70ac657eb696b7fce/coverage-7.13.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb88b316ec33760714a4720feb2816a3a59180fd58c1985012054fa7aebee4c2", size = 263875, upload-time = "2026-02-09T12:57:53.938Z" }, - { url = "https://files.pythonhosted.org/packages/3e/d9/63b22a6bdbd17f1f96e9ed58604c2a6b0e72a9133e37d663bef185877cf6/coverage-7.13.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7d41eead3cc673cbd38a4417deb7fd0b4ca26954ff7dc6078e33f6ff97bed940", size = 261500, upload-time = "2026-02-09T12:57:56.012Z" }, - { url = "https://files.pythonhosted.org/packages/70/bf/69f86ba1ad85bc3ad240e4c0e57a2e620fbc0e1645a47b5c62f0e941ad7f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:fb26a934946a6afe0e326aebe0730cdff393a8bc0bbb65a2f41e30feddca399c", size = 265212, upload-time = "2026-02-09T12:57:57.5Z" }, - { url = "https://files.pythonhosted.org/packages/ae/f2/5f65a278a8c2148731831574c73e42f57204243d33bedaaf18fa79c5958f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:dae88bc0fc77edaa65c14be099bd57ee140cf507e6bfdeea7938457ab387efb0", size = 260398, upload-time = "2026-02-09T12:57:59.027Z" }, - { url = "https://files.pythonhosted.org/packages/ef/80/6e8280a350ee9fea92f14b8357448a242dcaa243cb2c72ab0ca591f66c8c/coverage-7.13.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:845f352911777a8e722bfce168958214951e07e47e5d5d9744109fa5fe77f79b", size = 262584, upload-time = "2026-02-09T12:58:01.129Z" }, - { url = "https://files.pythonhosted.org/packages/22/63/01ff182fc95f260b539590fb12c11ad3e21332c15f9799cb5e2386f71d9f/coverage-7.13.4-cp313-cp313t-win32.whl", hash = "sha256:2fa8d5f8de70688a28240de9e139fa16b153cc3cbb01c5f16d88d6505ebdadf9", size = 222688, upload-time = "2026-02-09T12:58:02.736Z" }, - { url = "https://files.pythonhosted.org/packages/a9/43/89de4ef5d3cd53b886afa114065f7e9d3707bdb3e5efae13535b46ae483d/coverage-7.13.4-cp313-cp313t-win_amd64.whl", hash = "sha256:9351229c8c8407645840edcc277f4a2d44814d1bc34a2128c11c2a031d45a5dd", size = 223746, upload-time = "2026-02-09T12:58:05.362Z" }, - { url = "https://files.pythonhosted.org/packages/35/39/7cf0aa9a10d470a5309b38b289b9bb07ddeac5d61af9b664fe9775a4cb3e/coverage-7.13.4-cp313-cp313t-win_arm64.whl", hash = "sha256:30b8d0512f2dc8c8747557e8fb459d6176a2c9e5731e2b74d311c03b78451997", size = 222003, upload-time = "2026-02-09T12:58:06.952Z" }, - { url = "https://files.pythonhosted.org/packages/92/11/a9cf762bb83386467737d32187756a42094927150c3e107df4cb078e8590/coverage-7.13.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:300deaee342f90696ed186e3a00c71b5b3d27bffe9e827677954f4ee56969601", size = 219522, upload-time = "2026-02-09T12:58:08.623Z" }, - { url = "https://files.pythonhosted.org/packages/d3/28/56e6d892b7b052236d67c95f1936b6a7cf7c3e2634bf27610b8cbd7f9c60/coverage-7.13.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29e3220258d682b6226a9b0925bc563ed9a1ebcff3cad30f043eceea7eaf2689", size = 219855, upload-time = "2026-02-09T12:58:10.176Z" }, - { url = "https://files.pythonhosted.org/packages/e5/69/233459ee9eb0c0d10fcc2fe425a029b3fa5ce0f040c966ebce851d030c70/coverage-7.13.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:391ee8f19bef69210978363ca930f7328081c6a0152f1166c91f0b5fdd2a773c", size = 250887, upload-time = "2026-02-09T12:58:12.503Z" }, - { url = "https://files.pythonhosted.org/packages/06/90/2cdab0974b9b5bbc1623f7876b73603aecac11b8d95b85b5b86b32de5eab/coverage-7.13.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0dd7ab8278f0d58a0128ba2fca25824321f05d059c1441800e934ff2efa52129", size = 253396, upload-time = "2026-02-09T12:58:14.615Z" }, - { url = "https://files.pythonhosted.org/packages/ac/15/ea4da0f85bf7d7b27635039e649e99deb8173fe551096ea15017f7053537/coverage-7.13.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78cdf0d578b15148b009ccf18c686aa4f719d887e76e6b40c38ffb61d264a552", size = 254745, upload-time = "2026-02-09T12:58:16.162Z" }, - { url = "https://files.pythonhosted.org/packages/99/11/bb356e86920c655ca4d61daee4e2bbc7258f0a37de0be32d233b561134ff/coverage-7.13.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:48685fee12c2eb3b27c62f2658e7ea21e9c3239cba5a8a242801a0a3f6a8c62a", size = 257055, upload-time = "2026-02-09T12:58:17.892Z" }, - { url = "https://files.pythonhosted.org/packages/c9/0f/9ae1f8cb17029e09da06ca4e28c9e1d5c1c0a511c7074592e37e0836c915/coverage-7.13.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4e83efc079eb39480e6346a15a1bcb3e9b04759c5202d157e1dd4303cd619356", size = 250911, upload-time = "2026-02-09T12:58:19.495Z" }, - { url = "https://files.pythonhosted.org/packages/89/3a/adfb68558fa815cbc29747b553bc833d2150228f251b127f1ce97e48547c/coverage-7.13.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecae9737b72408d6a950f7e525f30aca12d4bd8dd95e37342e5beb3a2a8c4f71", size = 252754, upload-time = "2026-02-09T12:58:21.064Z" }, - { url = "https://files.pythonhosted.org/packages/32/b1/540d0c27c4e748bd3cd0bd001076ee416eda993c2bae47a73b7cc9357931/coverage-7.13.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ae4578f8528569d3cf303fef2ea569c7f4c4059a38c8667ccef15c6e1f118aa5", size = 250720, upload-time = "2026-02-09T12:58:22.622Z" }, - { url = "https://files.pythonhosted.org/packages/c7/95/383609462b3ffb1fe133014a7c84fc0dd01ed55ac6140fa1093b5af7ebb1/coverage-7.13.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6fdef321fdfbb30a197efa02d48fcd9981f0d8ad2ae8903ac318adc653f5df98", size = 254994, upload-time = "2026-02-09T12:58:24.548Z" }, - { url = "https://files.pythonhosted.org/packages/f7/ba/1761138e86c81680bfc3c49579d66312865457f9fe405b033184e5793cb3/coverage-7.13.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b0f6ccf3dbe577170bebfce1318707d0e8c3650003cb4b3a9dd744575daa8b5", size = 250531, upload-time = "2026-02-09T12:58:26.271Z" }, - { url = "https://files.pythonhosted.org/packages/f8/8e/05900df797a9c11837ab59c4d6fe94094e029582aab75c3309a93e6fb4e3/coverage-7.13.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75fcd519f2a5765db3f0e391eb3b7d150cce1a771bf4c9f861aeab86c767a3c0", size = 252189, upload-time = "2026-02-09T12:58:27.807Z" }, - { url = "https://files.pythonhosted.org/packages/00/bd/29c9f2db9ea4ed2738b8a9508c35626eb205d51af4ab7bf56a21a2e49926/coverage-7.13.4-cp314-cp314-win32.whl", hash = "sha256:8e798c266c378da2bd819b0677df41ab46d78065fb2a399558f3f6cae78b2fbb", size = 222258, upload-time = "2026-02-09T12:58:29.441Z" }, - { url = "https://files.pythonhosted.org/packages/a7/4d/1f8e723f6829977410efeb88f73673d794075091c8c7c18848d273dc9d73/coverage-7.13.4-cp314-cp314-win_amd64.whl", hash = "sha256:245e37f664d89861cf2329c9afa2c1fe9e6d4e1a09d872c947e70718aeeac505", size = 223073, upload-time = "2026-02-09T12:58:31.026Z" }, - { url = "https://files.pythonhosted.org/packages/51/5b/84100025be913b44e082ea32abcf1afbf4e872f5120b7a1cab1d331b1e13/coverage-7.13.4-cp314-cp314-win_arm64.whl", hash = "sha256:ad27098a189e5838900ce4c2a99f2fe42a0bf0c2093c17c69b45a71579e8d4a2", size = 221638, upload-time = "2026-02-09T12:58:32.599Z" }, - { url = "https://files.pythonhosted.org/packages/a7/e4/c884a405d6ead1370433dad1e3720216b4f9fd8ef5b64bfd984a2a60a11a/coverage-7.13.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:85480adfb35ffc32d40918aad81b89c69c9cc5661a9b8a81476d3e645321a056", size = 220246, upload-time = "2026-02-09T12:58:34.181Z" }, - { url = "https://files.pythonhosted.org/packages/81/5c/4d7ed8b23b233b0fffbc9dfec53c232be2e695468523242ea9fd30f97ad2/coverage-7.13.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:79be69cf7f3bf9b0deeeb062eab7ac7f36cd4cc4c4dd694bd28921ba4d8596cc", size = 220514, upload-time = "2026-02-09T12:58:35.704Z" }, - { url = "https://files.pythonhosted.org/packages/2f/6f/3284d4203fd2f28edd73034968398cd2d4cb04ab192abc8cff007ea35679/coverage-7.13.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:caa421e2684e382c5d8973ac55e4f36bed6821a9bad5c953494de960c74595c9", size = 261877, upload-time = "2026-02-09T12:58:37.864Z" }, - { url = "https://files.pythonhosted.org/packages/09/aa/b672a647bbe1556a85337dc95bfd40d146e9965ead9cc2fe81bde1e5cbce/coverage-7.13.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14375934243ee05f56c45393fe2ce81fe5cc503c07cee2bdf1725fb8bef3ffaf", size = 264004, upload-time = "2026-02-09T12:58:39.492Z" }, - { url = "https://files.pythonhosted.org/packages/79/a1/aa384dbe9181f98bba87dd23dda436f0c6cf2e148aecbb4e50fc51c1a656/coverage-7.13.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25a41c3104d08edb094d9db0d905ca54d0cd41c928bb6be3c4c799a54753af55", size = 266408, upload-time = "2026-02-09T12:58:41.852Z" }, - { url = "https://files.pythonhosted.org/packages/53/5e/5150bf17b4019bc600799f376bb9606941e55bd5a775dc1e096b6ffea952/coverage-7.13.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f01afcff62bf9a08fb32b2c1d6e924236c0383c02c790732b6537269e466a72", size = 267544, upload-time = "2026-02-09T12:58:44.093Z" }, - { url = "https://files.pythonhosted.org/packages/e0/ed/f1de5c675987a4a7a672250d2c5c9d73d289dbf13410f00ed7181d8017dd/coverage-7.13.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eb9078108fbf0bcdde37c3f4779303673c2fa1fe8f7956e68d447d0dd426d38a", size = 260980, upload-time = "2026-02-09T12:58:45.721Z" }, - { url = "https://files.pythonhosted.org/packages/b3/e3/fe758d01850aa172419a6743fe76ba8b92c29d181d4f676ffe2dae2ba631/coverage-7.13.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e086334e8537ddd17e5f16a344777c1ab8194986ec533711cbe6c41cde841b6", size = 263871, upload-time = "2026-02-09T12:58:47.334Z" }, - { url = "https://files.pythonhosted.org/packages/b6/76/b829869d464115e22499541def9796b25312b8cf235d3bb00b39f1675395/coverage-7.13.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:725d985c5ab621268b2edb8e50dfe57633dc69bda071abc470fed55a14935fd3", size = 261472, upload-time = "2026-02-09T12:58:48.995Z" }, - { url = "https://files.pythonhosted.org/packages/14/9e/caedb1679e73e2f6ad240173f55218488bfe043e38da577c4ec977489915/coverage-7.13.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3c06f0f1337c667b971ca2f975523347e63ec5e500b9aa5882d91931cd3ef750", size = 265210, upload-time = "2026-02-09T12:58:51.178Z" }, - { url = "https://files.pythonhosted.org/packages/3a/10/0dd02cb009b16ede425b49ec344aba13a6ae1dc39600840ea6abcb085ac4/coverage-7.13.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:590c0ed4bf8e85f745e6b805b2e1c457b2e33d5255dd9729743165253bc9ad39", size = 260319, upload-time = "2026-02-09T12:58:53.081Z" }, - { url = "https://files.pythonhosted.org/packages/92/8e/234d2c927af27c6d7a5ffad5bd2cf31634c46a477b4c7adfbfa66baf7ebb/coverage-7.13.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eb30bf180de3f632cd043322dad5751390e5385108b2807368997d1a92a509d0", size = 262638, upload-time = "2026-02-09T12:58:55.258Z" }, - { url = "https://files.pythonhosted.org/packages/2f/64/e5547c8ff6964e5965c35a480855911b61509cce544f4d442caa759a0702/coverage-7.13.4-cp314-cp314t-win32.whl", hash = "sha256:c4240e7eded42d131a2d2c4dec70374b781b043ddc79a9de4d55ca71f8e98aea", size = 223040, upload-time = "2026-02-09T12:58:56.936Z" }, - { url = "https://files.pythonhosted.org/packages/c7/96/38086d58a181aac86d503dfa9c47eb20715a79c3e3acbdf786e92e5c09a8/coverage-7.13.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4c7d3cc01e7350f2f0f6f7036caaf5673fb56b6998889ccfe9e1c1fe75a9c932", size = 224148, upload-time = "2026-02-09T12:58:58.645Z" }, - { url = "https://files.pythonhosted.org/packages/ce/72/8d10abd3740a0beb98c305e0c3faf454366221c0f37a8bcf8f60020bb65a/coverage-7.13.4-cp314-cp314t-win_arm64.whl", hash = "sha256:23e3f687cf945070d1c90f85db66d11e3025665d8dafa831301a0e0038f3db9b", size = 222172, upload-time = "2026-02-09T12:59:00.396Z" }, - { url = "https://files.pythonhosted.org/packages/0d/4a/331fe2caf6799d591109bb9c08083080f6de90a823695d412a935622abb2/coverage-7.13.4-py3-none-any.whl", hash = "sha256:1af1641e57cf7ba1bd67d677c9abdbcd6cc2ab7da3bca7fa1e2b7e50e65f2ad0", size = 211242, upload-time = "2026-02-09T12:59:02.032Z" }, -] - -[package.optional-dependencies] -toml = [ - { name = "tomli", marker = "python_full_version <= '3.11'" }, -] - -[[package]] -name = "darglint" -version = "1.8.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d4/2c/86e8549e349388c18ca8a4ff8661bb5347da550f598656d32a98eaaf91cc/darglint-1.8.1.tar.gz", hash = "sha256:080d5106df149b199822e7ee7deb9c012b49891538f14a11be681044f0bb20da", size = 74435, upload-time = "2021-10-18T03:40:37.283Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/69/28/85d1e0396d64422c5218d68e5cdcc53153aa8a2c83c7dbc3ee1502adf3a1/darglint-1.8.1-py3-none-any.whl", hash = "sha256:5ae11c259c17b0701618a20c3da343a3eb98b3bc4b5a83d31cdd94f5ebdced8d", size = 120767, upload-time = "2021-10-18T03:40:35.034Z" }, -] - -[[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, upload-time = "2025-07-17T16:52:00.465Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, -] - -[[package]] -name = "exceptiongroup" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, -] - -[[package]] -name = "filelock" -version = "3.25.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b3/8b/4c32ecde6bea6486a2a5d05340e695174351ff6b06cf651a74c005f9df00/filelock-3.25.1.tar.gz", hash = "sha256:b9a2e977f794ef94d77cdf7d27129ac648a61f585bff3ca24630c1629f701aa9", size = 40319, upload-time = "2026-03-09T19:38:47.309Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a9/b8/2f664b56a3b4b32d28d3d106c71783073f712ba43ff6d34b9ea0ce36dc7b/filelock-3.25.1-py3-none-any.whl", hash = "sha256:18972df45473c4aa2c7921b609ee9ca4925910cc3a0fb226c96b92fc224ef7bf", size = 26720, upload-time = "2026-03-09T19:38:45.718Z" }, -] - -[[package]] -name = "granian" -version = "2.7.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/57/19/d4ea523715ba8dd2ed295932cc3dda6bb197060f78aada6e886ff08587b2/granian-2.7.2.tar.gz", hash = "sha256:cdae2f3a26fa998d41fefad58f1d1c84a0b035a6cc9377addd81b51ba82f927f", size = 128969, upload-time = "2026-02-24T23:04:23.314Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/15/d4/25d4315e1bb07bb7ad9ce4558773ceaac70b023450e672586dac48675241/granian-2.7.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:c3a2e5734fc71d3b8c64ecb614570836b2c37a7df293d78de981a10f756464b0", size = 6522657, upload-time = "2026-02-24T23:01:57.987Z" }, - { url = "https://files.pythonhosted.org/packages/da/ed/dc91f663d3681ba480ff028cb96fefde0b9db5318d1508b6a69691d4a78e/granian-2.7.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ae8786a13e8054c0dda96e9f22e27c5d94dcad72fc89de191ae5ea83e2d6b023", size = 6136126, upload-time = "2026-02-24T23:01:59.646Z" }, - { url = "https://files.pythonhosted.org/packages/cf/d1/df30d998fab7317b4c9f81f76955c230d9b7e7a16d730388e7013b4c820d/granian-2.7.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3f4287275dd360935ed3d95da67da104d96ad9a19d4b9fde176dc7c3becf3553", size = 7138802, upload-time = "2026-02-24T23:02:01.173Z" }, - { url = "https://files.pythonhosted.org/packages/27/8f/6b691e419d6ec78724351233a521c3c00131fc279b864ac9d374191bfcbe/granian-2.7.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:24925bc301707c8bb21dd87a668e806a972a818a1929caa47f7af5f3108816fb", size = 6467262, upload-time = "2026-02-24T23:02:03.319Z" }, - { url = "https://files.pythonhosted.org/packages/8d/28/7eb0ea0632b119ccd03f7ed6ca3a7bf6642fb7c680a7a5813a2a22b9dd17/granian-2.7.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2edda56ab8f15a83503b9e753785b0bcb01e7476418cb2326e683b3f2ca26202", size = 6869805, upload-time = "2026-02-24T23:02:05.357Z" }, - { url = "https://files.pythonhosted.org/packages/68/6a/91022ee4da3bd616ac2426aed2154c97dac8ef3cd50ac3d971d07ed168d6/granian-2.7.2-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:2c7e1303e5b913298e3d7ada3a4a72c4ce75589574eee744ef30bc123a836a55", size = 7035362, upload-time = "2026-02-24T23:02:07.749Z" }, - { url = "https://files.pythonhosted.org/packages/c3/9f/c3bbfb9f1c7327b5deb68b69c03bc1f7cf55e723307d5ddc6e339f604aeb/granian-2.7.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ec6027a53d88eca16620f0c617f4efa9961bfea74a48166619a291545bd9fc38", size = 7122757, upload-time = "2026-02-24T23:02:09.505Z" }, - { url = "https://files.pythonhosted.org/packages/bc/e8/bf8f20535b1e830c7839439b61221885edd9449ab2261d5cbec9a84e20bb/granian-2.7.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:4b8b409cfe743130dad1f59edee9f729e6fe5e86bcba1cea633e8cc045beb6f5", size = 7302495, upload-time = "2026-02-24T23:02:10.912Z" }, - { url = "https://files.pythonhosted.org/packages/2e/42/a767291821f3ac4aa57c1aa2fff729f9d547f256fc91a2e946dc66acf585/granian-2.7.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bd2d0438d07a1f5daba035825a86abdafc44285ad4062c2a867dec107f0742c5", size = 7010685, upload-time = "2026-02-24T23:02:12.836Z" }, - { url = "https://files.pythonhosted.org/packages/fe/8a/c18c2faf5dd7aacb016ffb513fa2b2b72ac7f8c9deb29e322595c030ab5a/granian-2.7.2-cp310-cp310-win_amd64.whl", hash = "sha256:3627a82c87a066f37ec15dae6f63372e748fc6a0d722c165700dfd4a466baa37", size = 4158971, upload-time = "2026-02-24T23:02:14.344Z" }, - { url = "https://files.pythonhosted.org/packages/f8/58/dcf0e8a54b9a7f8b7482ed617bca08503a47eb6b702aea73cda9efd2c81c/granian-2.7.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3a0d33ada95a1421e5a22d447d918e5615ff0aa37f12de5b84455afe89970875", size = 6522860, upload-time = "2026-02-24T23:02:15.901Z" }, - { url = "https://files.pythonhosted.org/packages/2b/dd/398de0f273fdcf0e96bd70d8cd97364625176990e67457f11e23f95772bd/granian-2.7.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ee26f0258cc1b6ccf87c7bdcee6d1f90710505522fc9880ec02b299fb15679ad", size = 6135934, upload-time = "2026-02-24T23:02:18.52Z" }, - { url = "https://files.pythonhosted.org/packages/67/b7/7bf635bbdfb88dfc6591fa2ce5c3837ab9535e57e197a780c4a338363de7/granian-2.7.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f52338cfab08b8cdaadaa5b93665e0be5b4c4f718fbd132d76ceacacb9ff864e", size = 7138393, upload-time = "2026-02-24T23:02:19.911Z" }, - { url = "https://files.pythonhosted.org/packages/0a/90/e424fd8a703add1e8922390503be8d057882b35b42ba51796157aabd659b/granian-2.7.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e377d03a638fecb6949ab05c8fd4a76f892993aed17c602d179bfd56aebc2de", size = 6467189, upload-time = "2026-02-24T23:02:21.896Z" }, - { url = "https://files.pythonhosted.org/packages/65/9a/5de24d7e2dba1aa9fbac6f0a80dace975cfac1b7c7624ece21da75a38987/granian-2.7.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f742f3ca1797a746fae4a9337fe5d966460c957fa8efeaccf464b872e158d3d", size = 6870813, upload-time = "2026-02-24T23:02:23.972Z" }, - { url = "https://files.pythonhosted.org/packages/ac/cd/a604e38237857f4ad4262eadc409f94fe08fed3e86fa0b8734479cc5bfb1/granian-2.7.2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:ca4402e8f28a958f0c0f6ebff94cd0b04ca79690aded785648a438bc3c875ba3", size = 7046583, upload-time = "2026-02-24T23:02:25.94Z" }, - { url = "https://files.pythonhosted.org/packages/cc/ad/79eaae0cddd90c4e191b37674cedd8f4863b44465cb435b10396d0f12c82/granian-2.7.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1f9a899123b0d084783626e5225608094f1d2f6fc81b3a7c77ab8daac33ab74a", size = 7121958, upload-time = "2026-02-24T23:02:27.641Z" }, - { url = "https://files.pythonhosted.org/packages/ca/51/e5c923b1baa003f5b4b7fc148be6f8d2e3cabe55d41040fe8139da52e31b/granian-2.7.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:56ba4bef79d0ae3736328038deed2b5d281b11672bc0b08ffc8ce6210e406ef8", size = 7303047, upload-time = "2026-02-24T23:02:30.863Z" }, - { url = "https://files.pythonhosted.org/packages/06/c0/ebd68144a3ce9ead1a3192ac02e1c26e4874df1257435ce6137adf92fedb/granian-2.7.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ea46e3f43d94715aa89d1f2f5754753d46e6b653d561b82b0291e62a31bdfb35", size = 7011349, upload-time = "2026-02-24T23:02:32.887Z" }, - { url = "https://files.pythonhosted.org/packages/53/92/db2978c55dee2330868615526797a503f5d68f05dac05cb2e3a277e2b17c/granian-2.7.2-cp311-cp311-win_amd64.whl", hash = "sha256:7f8d5d16a1cd277cc6244f13f97d924b4cc0ea24506a71ca21b0546541d57ab0", size = 4157888, upload-time = "2026-02-24T23:02:34.429Z" }, - { url = "https://files.pythonhosted.org/packages/ec/ed/37f5d7d887ec9159dd8f5b1c9c38cee711d51016d203959f2d51c536a33b/granian-2.7.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a836f3f8ebfe61cb25d9afb655f2e5d3851154fd2ad97d47bb4fb202817212fc", size = 6451593, upload-time = "2026-02-24T23:02:36.203Z" }, - { url = "https://files.pythonhosted.org/packages/1e/06/84ee67a68504836a52c48ec3b4b2b406cbd927c9b43aae89d82db8d097a0/granian-2.7.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:09b1c543ba30886dea515a156baf6d857bbb8b57dbfd8b012c578b93c80ef0c3", size = 6101239, upload-time = "2026-02-24T23:02:37.636Z" }, - { url = "https://files.pythonhosted.org/packages/ed/50/ece7dc8efe144542cd626b88b1475b649e2eaa3eb5f7541ca57390151b05/granian-2.7.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6d334d4fbefb97001e78aa8067deafb107b867c102ba2120b4b2ec989fa58a89", size = 7079443, upload-time = "2026-02-24T23:02:39.651Z" }, - { url = "https://files.pythonhosted.org/packages/7e/e8/0f37b531d3cc96b8538cca2dc86eda92102e0ee345b30aa689354194a4cb/granian-2.7.2-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c86081d8c87989db69650e9d0e50ed925b8cd5dad21e0a86aa72d7a45f45925", size = 6428683, upload-time = "2026-02-24T23:02:41.827Z" }, - { url = "https://files.pythonhosted.org/packages/47/09/228626706554b389407270e2a6b19b7dee06d6890e8c01a39c6a785827fd/granian-2.7.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9eda33dca2c8bc6471bb6e9e25863077bca3877a1bba4069cd5e0ee2de41765", size = 6959520, upload-time = "2026-02-24T23:02:43.488Z" }, - { url = "https://files.pythonhosted.org/packages/61/c0/a639ceabd59b8acae2d71b5c918fcb2d42f8ef98994eedcf9a8b6813731d/granian-2.7.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:9cf69aaff6f632074ffbe7c1ee214e50f64be36101b7cb8253eeec1d460f2dba", size = 6991548, upload-time = "2026-02-24T23:02:44.954Z" }, - { url = "https://files.pythonhosted.org/packages/b1/99/a35ed838a3095dcad02ae3944d19ebafe1d5a98cdc72bb61835fb5faf933/granian-2.7.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f761a748cc7f3843b430422d2539da679daf5d3ef0259a101b90d5e55a0aafa7", size = 7121475, upload-time = "2026-02-24T23:02:46.991Z" }, - { url = "https://files.pythonhosted.org/packages/ce/24/3952c464432b904ec1cf537d2bd80d2dfde85524fa428ab9db2b5afe653c/granian-2.7.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:41c7b8390b78647fe34662ed7296e1465dad4a5112af9b0ecf8e367083d6c76a", size = 7243647, upload-time = "2026-02-24T23:02:49.165Z" }, - { url = "https://files.pythonhosted.org/packages/c9/fa/ab39e39c6b78eab6b42cf5bb36f56badde2aaafc3807f03f781d00e7861a/granian-2.7.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a052ed466da5922cb443435a95a0c751566943278a6f22cef3d2e19d4e7ecdea", size = 7048915, upload-time = "2026-02-24T23:02:50.773Z" }, - { url = "https://files.pythonhosted.org/packages/39/64/4502918f7d92a7e668d9e2fba83e2decbbf44c8ea896bacd8551d64f1d29/granian-2.7.2-cp312-cp312-win_amd64.whl", hash = "sha256:1e438096c36ed6aa4f6c0c8dde22bebe08ac008d08257517b15182c262a08cfa", size = 4150398, upload-time = "2026-02-24T23:02:52.199Z" }, - { url = "https://files.pythonhosted.org/packages/ab/bc/cf0bc29f583096a842cf0f26ae2fe40c72ed5286d4548be99ecfcdbb17e2/granian-2.7.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:76b840ff13dde8838fd33cd096f2e7cadf2c21a499a67f695f53de57deab6ff8", size = 6440868, upload-time = "2026-02-24T23:02:53.619Z" }, - { url = "https://files.pythonhosted.org/packages/2f/0d/bae1dcd2182ba5d9a5df33eb50b56dc5bbe67e31033d822e079aa8c1ff30/granian-2.7.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:00ccc8d7284bc7360f310179d0b4d17e5ca3077bbe24427e9e9310df397e3831", size = 6097336, upload-time = "2026-02-24T23:02:55.185Z" }, - { url = "https://files.pythonhosted.org/packages/65/7d/3e0a7f32b0ad5faa1d847c51191391552fa239821c95fc7c022688985df2/granian-2.7.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:675987c1b321dc8af593db8639e00c25277449b32e8c1b2ddd46b35f28d9fac4", size = 7098742, upload-time = "2026-02-24T23:02:57.898Z" }, - { url = "https://files.pythonhosted.org/packages/89/41/3b44386d636ac6467f0f13f45474c71fc3b90a4f0ba8b536de91b2845a09/granian-2.7.2-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:681c6fbe3354aaa6251e6191ec89f5174ac3b9fbc4b4db606fea456d01969fcb", size = 6430667, upload-time = "2026-02-24T23:02:59.789Z" }, - { url = "https://files.pythonhosted.org/packages/52/70/7b24e187aed3fb7ac2b29d2480a045559a509ef9fec54cffb8694a2d94af/granian-2.7.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e5c9ae65af5e572dca27d8ca0da4c5180b08473ac47e6f5329699e9455a5cc3", size = 6948424, upload-time = "2026-02-24T23:03:01.406Z" }, - { url = "https://files.pythonhosted.org/packages/fa/4c/cb74c367f9efb874f2c8433fe9bf3e824f05cf719f2251d40e29e07f08c0/granian-2.7.2-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:e37fab2be919ceb195db00d7f49ec220444b1ecaa07c03f7c1c874cacff9de83", size = 7000407, upload-time = "2026-02-24T23:03:03.214Z" }, - { url = "https://files.pythonhosted.org/packages/58/98/dfed3966ed7fbd3aae56e123598f90dc206484092b8373d0a71e2d8b82a8/granian-2.7.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:8ec167ab30f5396b5caaff16820a39f4e91986d2fe5bdc02992a03c2b2b2b313", size = 7121626, upload-time = "2026-02-24T23:03:05.349Z" }, - { url = "https://files.pythonhosted.org/packages/39/82/acec732a345cd03b2f6e48ac04b66b7b8b61f5c50eb08d7421fc8c56591a/granian-2.7.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:63f426d793f2116d23be265dd826bec1e623680baf94cc270fe08923113a86ba", size = 7253447, upload-time = "2026-02-24T23:03:06.986Z" }, - { url = "https://files.pythonhosted.org/packages/c5/2b/64779e69b08c1ff1bfc09a4ede904ab761ff63f936c275710886057c52f7/granian-2.7.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1617cbb4efe3112f07fb6762cf81d2d9fe4bdb78971d1fd0a310f8b132f6a51e", size = 7053005, upload-time = "2026-02-24T23:03:09.021Z" }, - { url = "https://files.pythonhosted.org/packages/04/c9/83e546d5f6b0447a4b9ee48ce15c29e43bb3f6b5e1040d33ac61fc9e3b6f/granian-2.7.2-cp313-cp313-win_amd64.whl", hash = "sha256:7a4bd347694ace7a48cd784b911f2d519c2a22154e0d1ed59f5b4864914a8cfe", size = 4145886, upload-time = "2026-02-24T23:03:10.829Z" }, - { url = "https://files.pythonhosted.org/packages/4c/49/9eb88875d709db7e7844e1c681546448dab5ff5651cd1c1d80ac4b1de4e3/granian-2.7.2-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:016c5857c8baedeab7eb065f98417f5ea26bb72b0f7e0544fe76071efc5ab255", size = 6401748, upload-time = "2026-02-24T23:03:12.802Z" }, - { url = "https://files.pythonhosted.org/packages/e3/80/85726ad9999ed89cb6a32f7f57eb50ce7261459d9c30c3b194ae4c5aa2c5/granian-2.7.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dcbe01fa141adf3f90964e86a959e250754aa7c6dad8fa7a855e6fd382de4c13", size = 6101265, upload-time = "2026-02-24T23:03:14.435Z" }, - { url = "https://files.pythonhosted.org/packages/07/82/0df56a42b9f4c327d0e0b052f43369127e1b565b9e66bf2c9488f1c8d759/granian-2.7.2-cp313-cp313t-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:283ba23817a685784b66f45423d2f25715fdc076c8ffb43c49a807ee56a0ffc0", size = 6249488, upload-time = "2026-02-24T23:03:16.387Z" }, - { url = "https://files.pythonhosted.org/packages/ef/cc/d83a351560a3d6377672636129c52f06f8393f5831c5ee0f06f274883ea6/granian-2.7.2-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3258419c741897273ce155568b5a9cbacb7700a00516e87119a90f7d520d6783", size = 7104734, upload-time = "2026-02-24T23:03:17.993Z" }, - { url = "https://files.pythonhosted.org/packages/84/d1/539907ee96d0ee2bcceabb4a6a9643b75378d6dfea09b7a9e4fd22cdf977/granian-2.7.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a196125c4837491c139c9cc83541b48c408c92b9cfbbf004fd28717f9c02ad21", size = 6785504, upload-time = "2026-02-24T23:03:19.763Z" }, - { url = "https://files.pythonhosted.org/packages/86/bf/4b6f45882f8341e7c6cb824d693deb94c306be6525b483c76fb373d1e749/granian-2.7.2-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:746555ac8a2dcd9257bfe7ad58f1d7a60892bc4613df6a7d8f736692b3bb3b88", size = 6902790, upload-time = "2026-02-24T23:03:22.215Z" }, - { url = "https://files.pythonhosted.org/packages/44/b8/832970d2d4b144b87be39f5b9dfd31fdb17f298dc238a0b2100c95002cf8/granian-2.7.2-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:5ac1843c6084933a54a07d9dcae643365f1d83aaff3fd4f2676ea301185e4e8b", size = 7082682, upload-time = "2026-02-24T23:03:23.875Z" }, - { url = "https://files.pythonhosted.org/packages/38/bc/1521dbf026d1c9d2465cd54e016efd8ff6e1e72eff521071dab20dd61c44/granian-2.7.2-cp313-cp313t-musllinux_1_1_armv7l.whl", hash = "sha256:3612eb6a3f4351dd2c4df246ed0d21056c0556a6b1ed772dd865310aa55a9ba9", size = 7264742, upload-time = "2026-02-24T23:03:25.562Z" }, - { url = "https://files.pythonhosted.org/packages/19/ae/00884ab77045a2f54db90932f9d1ca522201e2a6b2cf2a9b38840db0fd54/granian-2.7.2-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:34708b145e31b4538e0556704a07454a76d6776c55c5bc3a1335e80ef6b3bae3", size = 7062571, upload-time = "2026-02-24T23:03:27.278Z" }, - { url = "https://files.pythonhosted.org/packages/ee/0e/4321e361bccb9681e1045c75e783476de5be7aa47cf05066907530772eba/granian-2.7.2-cp313-cp313t-win_amd64.whl", hash = "sha256:841c48608e55daa2fa434392397cc24175abd48bc5bcefa1e4f74b7243e36c72", size = 4098734, upload-time = "2026-02-24T23:03:28.973Z" }, - { url = "https://files.pythonhosted.org/packages/69/4a/8ce622f4f7d58e035d121b9957dd5a8929028dc99cfc5d2bf7f2aa28912c/granian-2.7.2-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:592806c28c491f9c1d1501bac706ecf5e72b73969f20f912678d53308786d658", size = 6442041, upload-time = "2026-02-24T23:03:30.986Z" }, - { url = "https://files.pythonhosted.org/packages/27/62/7d36ed38a40a68c2856b6d2a6fedd40833e7f82eb90ba0d03f2d69ffadf5/granian-2.7.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c9dcde3968b921654bde999468e97d03031f28668bc1fc145c81d8bedb0fb2a4", size = 6100793, upload-time = "2026-02-24T23:03:32.734Z" }, - { url = "https://files.pythonhosted.org/packages/b4/c5/17fea68f4cb280c217cbd65534664722c9c9b0138c2754e20c235d70b5f4/granian-2.7.2-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6d4d78408283ec51f0fb00557856b4593947ad5b48287c04e1c22764a0ac28a5", size = 7119810, upload-time = "2026-02-24T23:03:34.807Z" }, - { url = "https://files.pythonhosted.org/packages/0a/76/35e240d107e0f158662652fd61191de4fb0c2c080e3786ca8f16c71547b7/granian-2.7.2-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66d28b078e8087f794b83822055f95caf93d83b23f47f4efcd5e2f0f7a5d8a81", size = 6450789, upload-time = "2026-02-24T23:03:36.81Z" }, - { url = "https://files.pythonhosted.org/packages/4c/55/a6d08cfecc808149a910e51c57883ab26fad69d922dc2e76fb2d87469e2d/granian-2.7.2-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ff7a93123ab339ba6cad51cc7141f8880ec47b152ce2491595bb08edda20106", size = 6902672, upload-time = "2026-02-24T23:03:38.655Z" }, - { url = "https://files.pythonhosted.org/packages/98/2e/c86d95f324248fcc5dcaf034c9f688b32f7a488f0b2a4a25e6673776107f/granian-2.7.2-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:a52effb9889f0944f0353afd6ce5a9d9aa83826d44bbf3c8013e978a3d6ef7b7", size = 6964399, upload-time = "2026-02-24T23:03:40.459Z" }, - { url = "https://files.pythonhosted.org/packages/37/4b/44fde33fe10245a3fba76bf843c387fad2d548244345115b9d87e1c40994/granian-2.7.2-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:76c987c3ca78bf7666ab053c3ed7e3af405af91b2e5ce2f1cf92634c1581e238", size = 7034929, upload-time = "2026-02-24T23:03:42.149Z" }, - { url = "https://files.pythonhosted.org/packages/90/76/38d205cb527046241a9ee4f51048bf44101c626ad4d2af16dd9d14dc1db6/granian-2.7.2-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:6590f8092c2bb6614e561ba771f084cbf72ecbc38dbf9849762ac38718085c29", size = 7259609, upload-time = "2026-02-24T23:03:43.852Z" }, - { url = "https://files.pythonhosted.org/packages/00/37/04245c7259e65f1083ce193875c6c44da4c98604d3b00a264a74dd4f042b/granian-2.7.2-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:7c1ce9b0c9446b680e9545e7fc95a75f0c53a25dedcf924b1750c3e5ba5bf908", size = 7073161, upload-time = "2026-02-24T23:03:45.655Z" }, - { url = "https://files.pythonhosted.org/packages/23/e4/28097a852d8f93f8e3be2014a81f03aa914b8a2c12ca761fac5ae1344b8b/granian-2.7.2-cp314-cp314-win_amd64.whl", hash = "sha256:a69cafb6518c630c84a9285674d45ea6f7342a6279dc25c6bd933b6fad5c55ab", size = 4121462, upload-time = "2026-02-24T23:03:47.322Z" }, - { url = "https://files.pythonhosted.org/packages/cc/07/0e56fb4f178e14b4c1fa1f6f00586ca81761ccbe2d8803f2c12b6b17a7d6/granian-2.7.2-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:a698d9b662d5648c8ae3dc01ad01688e1a8afc3525e431e7cddb841c53e5e291", size = 6415279, upload-time = "2026-02-24T23:03:48.932Z" }, - { url = "https://files.pythonhosted.org/packages/27/bc/3e69305bf34806cd852f4683deec844a2cb9a4d8888d7f172b507f6080a8/granian-2.7.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:17516095b520b3c039ddbe41a6beb2c59d554b668cc229d36d82c93154a799af", size = 6090528, upload-time = "2026-02-24T23:03:50.52Z" }, - { url = "https://files.pythonhosted.org/packages/ec/10/7d58a922b44417a6207c0a3230b0841cd7385a36fc518ac15fed16ebf6f7/granian-2.7.2-cp314-cp314t-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:96b0fd9eac60f939b3cbe44c8f32a42fdb7c1a1a9e07ca89e7795cdc7a606beb", size = 6252291, upload-time = "2026-02-24T23:03:52.248Z" }, - { url = "https://files.pythonhosted.org/packages/54/56/65776c6d759dcef9cce15bc11bdea2c64fe668088faf35d87916bd88f595/granian-2.7.2-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e50fb13e053384b8bd3823d4967606c6fd89f2b0d20e64de3ae212b85ffdfed2", size = 7106748, upload-time = "2026-02-24T23:03:53.994Z" }, - { url = "https://files.pythonhosted.org/packages/81/ee/d9ed836316607401f158ac264a3f770469d1b1edbf119402777a9eff1833/granian-2.7.2-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9bb1ef13125bc05ab2e18869ed311beaeb085a4c4c195d55d0865f5753a4c0b4", size = 6778883, upload-time = "2026-02-24T23:03:55.574Z" }, - { url = "https://files.pythonhosted.org/packages/a1/46/eabab80e07a14527c336dec6d902329399f3ba2b82dc94b6435651021359/granian-2.7.2-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b1c77189335070c6ba6b8d158518fde4c50f892753620f0b22a7552ad4347143", size = 6903426, upload-time = "2026-02-24T23:03:57.296Z" }, - { url = "https://files.pythonhosted.org/packages/24/8a/8ce186826066f6d453316229383a5be3b0b8a4130146c21f321ee64fe2cb/granian-2.7.2-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:1777166c3c853eed4440adb3cbbf34bba2b77d595bfc143a5826904a80b22f34", size = 7083877, upload-time = "2026-02-24T23:03:59.425Z" }, - { url = "https://files.pythonhosted.org/packages/cf/eb/91ed4646ce1c920ad39db0bcddb6f4755e1823002b14fb026104e3eb8bce/granian-2.7.2-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:0ffac19208ae548f3647c849579b803beaed2b50dfb0f3790ad26daac0033484", size = 7267282, upload-time = "2026-02-24T23:04:01.218Z" }, - { url = "https://files.pythonhosted.org/packages/49/2f/58cba479254530ab09132e150e4ab55362f6e875d9e82b6790477843e0aa/granian-2.7.2-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:82f34e78c1297bf5a1b6a5097e30428db98b59fce60a7387977b794855c0c3bc", size = 7054941, upload-time = "2026-02-24T23:04:03.211Z" }, - { url = "https://files.pythonhosted.org/packages/29/b3/fd13123ac936a4f79f1ba20ad67328a8d09d586262b8f28cc1cfaa555213/granian-2.7.2-cp314-cp314t-win_amd64.whl", hash = "sha256:e8b87d7ada696eec7e9023974665c83cec978cb83c205eae8fe377de20622f25", size = 4101983, upload-time = "2026-02-24T23:04:04.792Z" }, - { url = "https://files.pythonhosted.org/packages/59/71/f21b26c7dc7a8bc9d8288552c9c12128e73f1c3f04799b6e28a0a269b9b0/granian-2.7.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5613ee8c1233a79e56e1735e19c8c70af22a8c6b5808d7c1423dc5387bee4c05", size = 6504773, upload-time = "2026-02-24T23:04:06.498Z" }, - { url = "https://files.pythonhosted.org/packages/6e/68/282fbf5418f9348f657f505dc744cdca70ac850d39a805b21395211bf099/granian-2.7.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0cd6fee79f585de2e1a90b6a311f62b3768c7cda649bc0e02908157ffa2553cc", size = 6138096, upload-time = "2026-02-24T23:04:09.138Z" }, - { url = "https://files.pythonhosted.org/packages/e7/e0/b578709020f84c07ad2ca88f77ac67fd2c62e6b16f93ff8c8d65b7d99296/granian-2.7.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e94c825f8b327114f7062d158c502a540ef5819f809e10158f0edddddaf41bb9", size = 6900043, upload-time = "2026-02-24T23:04:11.015Z" }, - { url = "https://files.pythonhosted.org/packages/c7/2f/a2671cc160f29ccf8e605eb8fa113c01051b0d7947048c5b29eb4e603384/granian-2.7.2-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a6adea5fb8a537d18f3f2b848023151063bc45896415fdebfeb0bf0663d5a03b", size = 7040211, upload-time = "2026-02-24T23:04:13.31Z" }, - { url = "https://files.pythonhosted.org/packages/36/ce/df9bba3b211cda2d47535bb21bc040007e021e8c8adc20ce36619f903bc4/granian-2.7.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2392ab03cb92b1b2d4363f450b2d875177e10f0e22d67a4423052e6885e430f2", size = 7118085, upload-time = "2026-02-24T23:04:15.05Z" }, - { url = "https://files.pythonhosted.org/packages/a9/87/37124b2ee0cddce6ba438b0ff879ddae094ae2c92b24b28ffbe35110931f/granian-2.7.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:406c0bb1f5bf55c72cfbfdfd2ccec21299eb3f7b311d85c4889dde357fd36f33", size = 7314667, upload-time = "2026-02-24T23:04:16.783Z" }, - { url = "https://files.pythonhosted.org/packages/8c/ac/8b142ed352bc525e3c97440aab312928beebc735927b0cf979692bfcda3b/granian-2.7.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:362a6001daa2ce62532a49df407fe545076052ef29289a76d5760064d820f48b", size = 7004934, upload-time = "2026-02-24T23:04:19.059Z" }, - { url = "https://files.pythonhosted.org/packages/ab/a8/d9ba7a0d05303f57279ca8b17364945d5090ed90b32b92df219129039f50/granian-2.7.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:001bab0862857d3f029eb2819e88754e746547f4cd8cc28ae229090fc929fbaf", size = 4158219, upload-time = "2026-02-24T23:04:21.072Z" }, -] - -[package.optional-dependencies] -reload = [ - { name = "watchfiles" }, -] - -[[package]] -name = "greenlet" -version = "3.3.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a3/51/1664f6b78fc6ebbd98019a1fd730e83fa78f2db7058f72b1463d3612b8db/greenlet-3.3.2.tar.gz", hash = "sha256:2eaf067fc6d886931c7962e8c6bede15d2f01965560f3359b27c80bde2d151f2", size = 188267, upload-time = "2026-02-20T20:54:15.531Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/38/3f/9859f655d11901e7b2996c6e3d33e0caa9a1d4572c3bc61ed0faa64b2f4c/greenlet-3.3.2-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9bc885b89709d901859cf95179ec9f6bb67a3d2bb1f0e88456461bd4b7f8fd0d", size = 277747, upload-time = "2026-02-20T20:16:21.325Z" }, - { url = "https://files.pythonhosted.org/packages/fb/07/cb284a8b5c6498dbd7cba35d31380bb123d7dceaa7907f606c8ff5993cbf/greenlet-3.3.2-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b568183cf65b94919be4438dc28416b234b678c608cafac8874dfeeb2a9bbe13", size = 579202, upload-time = "2026-02-20T20:47:28.955Z" }, - { url = "https://files.pythonhosted.org/packages/ed/45/67922992b3a152f726163b19f890a85129a992f39607a2a53155de3448b8/greenlet-3.3.2-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:527fec58dc9f90efd594b9b700662ed3fb2493c2122067ac9c740d98080a620e", size = 590620, upload-time = "2026-02-20T20:55:55.581Z" }, - { url = "https://files.pythonhosted.org/packages/ad/55/9f1ebb5a825215fadcc0f7d5073f6e79e3007e3282b14b22d6aba7ca6cb8/greenlet-3.3.2-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ad0c8917dd42a819fe77e6bdfcb84e3379c0de956469301d9fd36427a1ca501f", size = 591729, upload-time = "2026-02-20T20:20:58.395Z" }, - { url = "https://files.pythonhosted.org/packages/24/b4/21f5455773d37f94b866eb3cf5caed88d6cea6dd2c6e1f9c34f463cba3ec/greenlet-3.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:97245cc10e5515dbc8c3104b2928f7f02b6813002770cfaffaf9a6e0fc2b94ef", size = 1551946, upload-time = "2026-02-20T20:49:31.102Z" }, - { url = "https://files.pythonhosted.org/packages/00/68/91f061a926abead128fe1a87f0b453ccf07368666bd59ffa46016627a930/greenlet-3.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8c1fdd7d1b309ff0da81d60a9688a8bd044ac4e18b250320a96fc68d31c209ca", size = 1618494, upload-time = "2026-02-20T20:21:06.541Z" }, - { url = "https://files.pythonhosted.org/packages/ac/78/f93e840cbaef8becaf6adafbaf1319682a6c2d8c1c20224267a5c6c8c891/greenlet-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:5d0e35379f93a6d0222de929a25ab47b5eb35b5ef4721c2b9cbcc4036129ff1f", size = 230092, upload-time = "2026-02-20T20:17:09.379Z" }, - { url = "https://files.pythonhosted.org/packages/f3/47/16400cb42d18d7a6bb46f0626852c1718612e35dcb0dffa16bbaffdf5dd2/greenlet-3.3.2-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:c56692189a7d1c7606cb794be0a8381470d95c57ce5be03fb3d0ef57c7853b86", size = 278890, upload-time = "2026-02-20T20:19:39.263Z" }, - { url = "https://files.pythonhosted.org/packages/a3/90/42762b77a5b6aa96cd8c0e80612663d39211e8ae8a6cd47c7f1249a66262/greenlet-3.3.2-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ebd458fa8285960f382841da585e02201b53a5ec2bac6b156fc623b5ce4499f", size = 581120, upload-time = "2026-02-20T20:47:30.161Z" }, - { url = "https://files.pythonhosted.org/packages/bf/6f/f3d64f4fa0a9c7b5c5b3c810ff1df614540d5aa7d519261b53fba55d4df9/greenlet-3.3.2-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a443358b33c4ec7b05b79a7c8b466f5d275025e750298be7340f8fc63dff2a55", size = 594363, upload-time = "2026-02-20T20:55:56.965Z" }, - { url = "https://files.pythonhosted.org/packages/72/83/3e06a52aca8128bdd4dcd67e932b809e76a96ab8c232a8b025b2850264c5/greenlet-3.3.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e2cd90d413acbf5e77ae41e5d3c9b3ac1d011a756d7284d7f3f2b806bbd6358", size = 594156, upload-time = "2026-02-20T20:20:59.955Z" }, - { url = "https://files.pythonhosted.org/packages/70/79/0de5e62b873e08fe3cef7dbe84e5c4bc0e8ed0c7ff131bccb8405cd107c8/greenlet-3.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:442b6057453c8cb29b4fb36a2ac689382fc71112273726e2423f7f17dc73bf99", size = 1554649, upload-time = "2026-02-20T20:49:32.293Z" }, - { url = "https://files.pythonhosted.org/packages/5a/00/32d30dee8389dc36d42170a9c66217757289e2afb0de59a3565260f38373/greenlet-3.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:45abe8eb6339518180d5a7fa47fa01945414d7cca5ecb745346fc6a87d2750be", size = 1619472, upload-time = "2026-02-20T20:21:07.966Z" }, - { url = "https://files.pythonhosted.org/packages/f1/3a/efb2cf697fbccdf75b24e2c18025e7dfa54c4f31fab75c51d0fe79942cef/greenlet-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e692b2dae4cc7077cbb11b47d258533b48c8fde69a33d0d8a82e2fe8d8531d5", size = 230389, upload-time = "2026-02-20T20:17:18.772Z" }, - { url = "https://files.pythonhosted.org/packages/e1/a1/65bbc059a43a7e2143ec4fc1f9e3f673e04f9c7b371a494a101422ac4fd5/greenlet-3.3.2-cp311-cp311-win_arm64.whl", hash = "sha256:02b0a8682aecd4d3c6c18edf52bc8e51eacdd75c8eac52a790a210b06aa295fd", size = 229645, upload-time = "2026-02-20T20:18:18.695Z" }, - { url = "https://files.pythonhosted.org/packages/ea/ab/1608e5a7578e62113506740b88066bf09888322a311cff602105e619bd87/greenlet-3.3.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ac8d61d4343b799d1e526db579833d72f23759c71e07181c2d2944e429eb09cd", size = 280358, upload-time = "2026-02-20T20:17:43.971Z" }, - { url = "https://files.pythonhosted.org/packages/a5/23/0eae412a4ade4e6623ff7626e38998cb9b11e9ff1ebacaa021e4e108ec15/greenlet-3.3.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ceec72030dae6ac0c8ed7591b96b70410a8be370b6a477b1dbc072856ad02bd", size = 601217, upload-time = "2026-02-20T20:47:31.462Z" }, - { url = "https://files.pythonhosted.org/packages/f8/16/5b1678a9c07098ecb9ab2dd159fafaf12e963293e61ee8d10ecb55273e5e/greenlet-3.3.2-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2a5be83a45ce6188c045bcc44b0ee037d6a518978de9a5d97438548b953a1ac", size = 611792, upload-time = "2026-02-20T20:55:58.423Z" }, - { url = "https://files.pythonhosted.org/packages/50/1f/5155f55bd71cabd03765a4aac9ac446be129895271f73872c36ebd4b04b6/greenlet-3.3.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43e99d1749147ac21dde49b99c9abffcbc1e2d55c67501465ef0930d6e78e070", size = 613875, upload-time = "2026-02-20T20:21:01.102Z" }, - { url = "https://files.pythonhosted.org/packages/fc/dd/845f249c3fcd69e32df80cdab059b4be8b766ef5830a3d0aa9d6cad55beb/greenlet-3.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c956a19350e2c37f2c48b336a3afb4bff120b36076d9d7fb68cb44e05d95b79", size = 1571467, upload-time = "2026-02-20T20:49:33.495Z" }, - { url = "https://files.pythonhosted.org/packages/2a/50/2649fe21fcc2b56659a452868e695634722a6655ba245d9f77f5656010bf/greenlet-3.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6c6f8ba97d17a1e7d664151284cb3315fc5f8353e75221ed4324f84eb162b395", size = 1640001, upload-time = "2026-02-20T20:21:09.154Z" }, - { url = "https://files.pythonhosted.org/packages/9b/40/cc802e067d02af8b60b6771cea7d57e21ef5e6659912814babb42b864713/greenlet-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:34308836d8370bddadb41f5a7ce96879b72e2fdfb4e87729330c6ab52376409f", size = 231081, upload-time = "2026-02-20T20:17:28.121Z" }, - { url = "https://files.pythonhosted.org/packages/58/2e/fe7f36ff1982d6b10a60d5e0740c759259a7d6d2e1dc41da6d96de32fff6/greenlet-3.3.2-cp312-cp312-win_arm64.whl", hash = "sha256:d3a62fa76a32b462a97198e4c9e99afb9ab375115e74e9a83ce180e7a496f643", size = 230331, upload-time = "2026-02-20T20:17:23.34Z" }, - { url = "https://files.pythonhosted.org/packages/ac/48/f8b875fa7dea7dd9b33245e37f065af59df6a25af2f9561efa8d822fde51/greenlet-3.3.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:aa6ac98bdfd716a749b84d4034486863fd81c3abde9aa3cf8eff9127981a4ae4", size = 279120, upload-time = "2026-02-20T20:19:01.9Z" }, - { url = "https://files.pythonhosted.org/packages/49/8d/9771d03e7a8b1ee456511961e1b97a6d77ae1dea4a34a5b98eee706689d3/greenlet-3.3.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab0c7e7901a00bc0a7284907273dc165b32e0d109a6713babd04471327ff7986", size = 603238, upload-time = "2026-02-20T20:47:32.873Z" }, - { url = "https://files.pythonhosted.org/packages/59/0e/4223c2bbb63cd5c97f28ffb2a8aee71bdfb30b323c35d409450f51b91e3e/greenlet-3.3.2-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d248d8c23c67d2291ffd47af766e2a3aa9fa1c6703155c099feb11f526c63a92", size = 614219, upload-time = "2026-02-20T20:55:59.817Z" }, - { url = "https://files.pythonhosted.org/packages/7a/34/259b28ea7a2a0c904b11cd36c79b8cef8019b26ee5dbe24e73b469dea347/greenlet-3.3.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6997d360a4e6a4e936c0f9625b1c20416b8a0ea18a8e19cabbefc712e7397ab", size = 616774, upload-time = "2026-02-20T20:21:02.454Z" }, - { url = "https://files.pythonhosted.org/packages/0a/03/996c2d1689d486a6e199cb0f1cf9e4aa940c500e01bdf201299d7d61fa69/greenlet-3.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64970c33a50551c7c50491671265d8954046cb6e8e2999aacdd60e439b70418a", size = 1571277, upload-time = "2026-02-20T20:49:34.795Z" }, - { url = "https://files.pythonhosted.org/packages/d9/c4/2570fc07f34a39f2caf0bf9f24b0a1a0a47bc2e8e465b2c2424821389dfc/greenlet-3.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1a9172f5bf6bd88e6ba5a84e0a68afeac9dc7b6b412b245dd64f52d83c81e55b", size = 1640455, upload-time = "2026-02-20T20:21:10.261Z" }, - { url = "https://files.pythonhosted.org/packages/91/39/5ef5aa23bc545aa0d31e1b9b55822b32c8da93ba657295840b6b34124009/greenlet-3.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:a7945dd0eab63ded0a48e4dcade82939783c172290a7903ebde9e184333ca124", size = 230961, upload-time = "2026-02-20T20:16:58.461Z" }, - { url = "https://files.pythonhosted.org/packages/62/6b/a89f8456dcb06becff288f563618e9f20deed8dd29beea14f9a168aef64b/greenlet-3.3.2-cp313-cp313-win_arm64.whl", hash = "sha256:394ead29063ee3515b4e775216cb756b2e3b4a7e55ae8fd884f17fa579e6b327", size = 230221, upload-time = "2026-02-20T20:17:37.152Z" }, - { url = "https://files.pythonhosted.org/packages/3f/ae/8bffcbd373b57a5992cd077cbe8858fff39110480a9d50697091faea6f39/greenlet-3.3.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8d1658d7291f9859beed69a776c10822a0a799bc4bfe1bd4272bb60e62507dab", size = 279650, upload-time = "2026-02-20T20:18:00.783Z" }, - { url = "https://files.pythonhosted.org/packages/d1/c0/45f93f348fa49abf32ac8439938726c480bd96b2a3c6f4d949ec0124b69f/greenlet-3.3.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18cb1b7337bca281915b3c5d5ae19f4e76d35e1df80f4ad3c1a7be91fadf1082", size = 650295, upload-time = "2026-02-20T20:47:34.036Z" }, - { url = "https://files.pythonhosted.org/packages/b3/de/dd7589b3f2b8372069ab3e4763ea5329940fc7ad9dcd3e272a37516d7c9b/greenlet-3.3.2-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2e47408e8ce1c6f1ceea0dffcdf6ebb85cc09e55c7af407c99f1112016e45e9", size = 662163, upload-time = "2026-02-20T20:56:01.295Z" }, - { url = "https://files.pythonhosted.org/packages/d2/d8/09bfa816572a4d83bccd6750df1926f79158b1c36c5f73786e26dbe4ee38/greenlet-3.3.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63d10328839d1973e5ba35e98cccbca71b232b14051fd957b6f8b6e8e80d0506", size = 664160, upload-time = "2026-02-20T20:21:04.015Z" }, - { url = "https://files.pythonhosted.org/packages/48/cf/56832f0c8255d27f6c35d41b5ec91168d74ec721d85f01a12131eec6b93c/greenlet-3.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e4ab3cfb02993c8cc248ea73d7dae6cec0253e9afa311c9b37e603ca9fad2ce", size = 1619181, upload-time = "2026-02-20T20:49:36.052Z" }, - { url = "https://files.pythonhosted.org/packages/0a/23/b90b60a4aabb4cec0796e55f25ffbfb579a907c3898cd2905c8918acaa16/greenlet-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94ad81f0fd3c0c0681a018a976e5c2bd2ca2d9d94895f23e7bb1af4e8af4e2d5", size = 1687713, upload-time = "2026-02-20T20:21:11.684Z" }, - { url = "https://files.pythonhosted.org/packages/f3/ca/2101ca3d9223a1dc125140dbc063644dca76df6ff356531eb27bc267b446/greenlet-3.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:8c4dd0f3997cf2512f7601563cc90dfb8957c0cff1e3a1b23991d4ea1776c492", size = 232034, upload-time = "2026-02-20T20:20:08.186Z" }, - { url = "https://files.pythonhosted.org/packages/f6/4a/ecf894e962a59dea60f04877eea0fd5724618da89f1867b28ee8b91e811f/greenlet-3.3.2-cp314-cp314-win_arm64.whl", hash = "sha256:cd6f9e2bbd46321ba3bbb4c8a15794d32960e3b0ae2cc4d49a1a53d314805d71", size = 231437, upload-time = "2026-02-20T20:18:59.722Z" }, - { url = "https://files.pythonhosted.org/packages/98/6d/8f2ef704e614bcf58ed43cfb8d87afa1c285e98194ab2cfad351bf04f81e/greenlet-3.3.2-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:e26e72bec7ab387ac80caa7496e0f908ff954f31065b0ffc1f8ecb1338b11b54", size = 286617, upload-time = "2026-02-20T20:19:29.856Z" }, - { url = "https://files.pythonhosted.org/packages/5e/0d/93894161d307c6ea237a43988f27eba0947b360b99ac5239ad3fe09f0b47/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b466dff7a4ffda6ca975979bab80bdadde979e29fc947ac3be4451428d8b0e4", size = 655189, upload-time = "2026-02-20T20:47:35.742Z" }, - { url = "https://files.pythonhosted.org/packages/f5/2c/d2d506ebd8abcb57386ec4f7ba20f4030cbe56eae541bc6fd6ef399c0b41/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8bddc5b73c9720bea487b3bffdb1840fe4e3656fba3bd40aa1489e9f37877ff", size = 658225, upload-time = "2026-02-20T20:56:02.527Z" }, - { url = "https://files.pythonhosted.org/packages/8e/30/3a09155fbf728673a1dea713572d2d31159f824a37c22da82127056c44e4/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b26b0f4428b871a751968285a1ac9648944cea09807177ac639b030bddebcea4", size = 657907, upload-time = "2026-02-20T20:21:05.259Z" }, - { url = "https://files.pythonhosted.org/packages/f3/fd/d05a4b7acd0154ed758797f0a43b4c0962a843bedfe980115e842c5b2d08/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1fb39a11ee2e4d94be9a76671482be9398560955c9e568550de0224e41104727", size = 1618857, upload-time = "2026-02-20T20:49:37.309Z" }, - { url = "https://files.pythonhosted.org/packages/6f/e1/50ee92a5db521de8f35075b5eff060dd43d39ebd46c2181a2042f7070385/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:20154044d9085151bc309e7689d6f7ba10027f8f5a8c0676ad398b951913d89e", size = 1680010, upload-time = "2026-02-20T20:21:13.427Z" }, - { url = "https://files.pythonhosted.org/packages/29/4b/45d90626aef8e65336bed690106d1382f7a43665e2249017e9527df8823b/greenlet-3.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c04c5e06ec3e022cbfe2cd4a846e1d4e50087444f875ff6d2c2ad8445495cf1a", size = 237086, upload-time = "2026-02-20T20:20:45.786Z" }, -] - -[[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, upload-time = "2025-04-24T03:35:25.427Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, -] - -[[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, upload-time = "2025-04-24T22:06:22.219Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, -] - -[[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, upload-time = "2024-12-06T15:37:23.222Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, -] - -[[package]] -name = "identify" -version = "2.6.17" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/57/84/376a3b96e5a8d33a7aa2c5b3b31a4b3c364117184bf0b17418055f6ace66/identify-2.6.17.tar.gz", hash = "sha256:f816b0b596b204c9fdf076ded172322f2723cf958d02f9c3587504834c8ff04d", size = 99579, upload-time = "2026-03-01T20:04:12.702Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/40/66/71c1227dff78aaeb942fed29dd5651f2aec166cc7c9aeea3e8b26a539b7d/identify-2.6.17-py2.py3-none-any.whl", hash = "sha256:be5f8412d5ed4b20f2bd41a65f920990bdccaa6a4a18a08f1eefdcd0bdd885f0", size = 99382, upload-time = "2026-03-01T20:04:11.439Z" }, -] - -[[package]] -name = "idna" -version = "3.11" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, -] - -[[package]] -name = "iniconfig" -version = "2.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, -] - -[[package]] -name = "mako" -version = "1.3.10" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markupsafe" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9e/38/bd5b78a920a64d708fe6bc8e0a2c075e1389d53bef8413725c63ba041535/mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28", size = 392474, upload-time = "2025-04-10T12:44:31.16Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/87/fb/99f81ac72ae23375f22b7afdb7642aba97c00a713c217124420147681a2f/mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59", size = 78509, upload-time = "2025-04-10T12:50:53.297Z" }, -] - -[[package]] -name = "markdown-it-py" -version = "4.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mdurl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, -] - -[[package]] -name = "markupsafe" -version = "3.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, - { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, - { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, - { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, - { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, - { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, - { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, - { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, - { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, - { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, - { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, - { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, - { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, - { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, - { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, - { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, - { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, - { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, - { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, - { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, - { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, - { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, - { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, - { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, - { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, - { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, - { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, - { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, - { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, - { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, - { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, - { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, - { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, - { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, - { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, - { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, - { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, - { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, - { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, - { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, - { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, - { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, - { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, - { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, - { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, - { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, - { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, - { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, - { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, - { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, - { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, - { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, - { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, - { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, - { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, - { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, - { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, - { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, - { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, - { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, - { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, - { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, - { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, - { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, - { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, - { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, - { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, - { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, - { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, - { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, - { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, - { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, - { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, - { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, - { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, - { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, - { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, -] - -[[package]] -name = "mdurl" -version = "0.1.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, -] - -[[package]] -name = "nodeenv" -version = "1.10.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, -] - -[[package]] -name = "packaging" -version = "26.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, -] - -[[package]] -name = "platformdirs" -version = "4.9.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/19/56/8d4c30c8a1d07013911a8fdbd8f89440ef9f08d07a1b50ab8ca8be5a20f9/platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934", size = 28737, upload-time = "2026-03-05T18:34:13.271Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868", size = 21216, upload-time = "2026-03-05T18:34:12.172Z" }, -] - -[[package]] -name = "pluggy" -version = "1.6.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, -] - -[[package]] -name = "pre-commit" -version = "4.5.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cfgv" }, - { name = "identify" }, - { name = "nodeenv" }, - { name = "pyyaml" }, - { name = "virtualenv" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/40/f1/6d86a29246dfd2e9b6237f0b5823717f60cad94d47ddc26afa916d21f525/pre_commit-4.5.1.tar.gz", hash = "sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61", size = 198232, upload-time = "2025-12-16T21:14:33.552Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl", hash = "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77", size = 226437, upload-time = "2025-12-16T21:14:32.409Z" }, -] - -[[package]] -name = "psutil" -version = "7.2.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, - { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, - { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, - { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, - { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, - { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, -] - -[[package]] -name = "py-cpuinfo" -version = "9.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/37/a8/d832f7293ebb21690860d2e01d8115e5ff6f2ae8bbdc953f0eb0fa4bd2c7/py-cpuinfo-9.0.0.tar.gz", hash = "sha256:3cdbbf3fac90dc6f118bfd64384f309edeadd902d7c8fb17f02ffa1fc3f49690", size = 104716, upload-time = "2022-10-25T20:38:06.303Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/a9/023730ba63db1e494a271cb018dcd361bd2c917ba7004c3e49d5daf795a2/py_cpuinfo-9.0.0-py3-none-any.whl", hash = "sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5", size = 22335, upload-time = "2022-10-25T20:38:27.636Z" }, -] - -[[package]] -name = "pydantic" -version = "2.12.5" -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/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, -] - -[[package]] -name = "pydantic-core" -version = "2.41.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" }, - { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" }, - { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" }, - { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" }, - { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" }, - { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" }, - { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" }, - { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" }, - { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" }, - { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" }, - { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" }, - { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" }, - { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" }, - { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, - { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, - { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, - { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, - { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, - { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, - { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, - { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, - { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, - { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, - { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, - { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, - { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, - { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, - { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, - { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, - { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, - { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, - { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, - { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, - { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, - { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, - { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, - { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, - { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, - { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, - { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, - { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, - { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, - { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, - { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, - { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, - { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, - { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, - { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, - { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, - { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, - { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, - { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, - { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, - { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, - { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, - { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, - { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, - { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, - { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, - { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, - { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, - { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, - { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, - { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, - { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, - { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, - { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, - { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, - { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, - { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, - { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, - { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, - { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, - { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, - { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, - { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, - { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, - { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, - { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, - { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, - { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, - { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, - { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, - { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, - { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, - { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, - { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, - { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, - { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, - { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, - { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, - { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" }, - { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" }, - { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" }, - { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" }, - { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" }, - { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" }, - { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" }, - { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" }, - { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, - { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, - { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, - { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, - { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, - { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, - { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, -] - -[[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, upload-time = "2025-06-21T13:39:12.283Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, -] - -[[package]] -name = "pyright" -version = "1.1.408" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "nodeenv" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/74/b2/5db700e52554b8f025faa9c3c624c59f1f6c8841ba81ab97641b54322f16/pyright-1.1.408.tar.gz", hash = "sha256:f28f2321f96852fa50b5829ea492f6adb0e6954568d1caa3f3af3a5f555eb684", size = 4400578, upload-time = "2026-01-08T08:07:38.795Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/82/a2c93e32800940d9573fb28c346772a14778b84ba7524e691b324620ab89/pyright-1.1.408-py3-none-any.whl", hash = "sha256:090b32865f4fdb1e0e6cd82bf5618480d48eecd2eb2e70f960982a3d9a4c17c1", size = 6399144, upload-time = "2026-01-08T08:07:37.082Z" }, -] - -[[package]] -name = "pytest" -version = "9.0.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, - { name = "iniconfig" }, - { name = "packaging" }, - { name = "pluggy" }, - { name = "pygments" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, -] - -[[package]] -name = "pytest-benchmark" -version = "5.2.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "py-cpuinfo" }, - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/24/34/9f732b76456d64faffbef6232f1f9dbec7a7c4999ff46282fa418bd1af66/pytest_benchmark-5.2.3.tar.gz", hash = "sha256:deb7317998a23c650fd4ff76e1230066a76cb45dcece0aca5607143c619e7779", size = 341340, upload-time = "2025-11-09T18:48:43.215Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/33/29/e756e715a48959f1c0045342088d7ca9762a2f509b945f362a316e9412b7/pytest_benchmark-5.2.3-py3-none-any.whl", hash = "sha256:bc839726ad20e99aaa0d11a127445457b4219bdb9e80a1afc4b51da7f96b0803", size = 45255, upload-time = "2025-11-09T18:48:39.765Z" }, -] - -[[package]] -name = "pytest-cov" -version = "7.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "coverage", extra = ["toml"] }, - { name = "pluggy" }, - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, -] - -[[package]] -name = "pytest-mock" -version = "3.15.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036, upload-time = "2025-09-16T16:37:27.081Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" }, -] - -[[package]] -name = "python-discovery" -version = "1.1.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "filelock" }, - { name = "platformdirs" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/7a/16/6f3f5e9258f0733aaca19aa18e298cb3a629ae49363573e78d241abeef59/python_discovery-1.1.2.tar.gz", hash = "sha256:c500bd2153e3afc5f48a61d33ff570b6f3e710d36ceaaf882fa9bbe5cc2cec49", size = 56928, upload-time = "2026-03-09T20:02:28.402Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/03/48/8bdfaec240edb1a79b79201eff38b737fc3c29ce59e2e71271bdd8bafdda/python_discovery-1.1.2-py3-none-any.whl", hash = "sha256:d18edd61b382d62f8bcd004a71ebaabc87df31dbefb30aeed59f4fc6afa005be", size = 31486, upload-time = "2026-03-09T20:02:27.277Z" }, -] - -[[package]] -name = "python-engineio" -version = "4.13.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "simple-websocket" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/34/12/bdef9dbeedbe2cdeba2a2056ad27b1fb081557d34b69a97f574843462cae/python_engineio-4.13.1.tar.gz", hash = "sha256:0a853fcef52f5b345425d8c2b921ac85023a04dfcf75d7b74696c61e940fd066", size = 92348, upload-time = "2026-02-06T23:38:06.12Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl", hash = "sha256:f32ad10589859c11053ad7d9bb3c9695cdf862113bfb0d20bc4d890198287399", size = 59847, upload-time = "2026-02-06T23:38:04.861Z" }, -] - -[[package]] -name = "python-multipart" -version = "0.0.22" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" }, -] - -[[package]] -name = "python-socketio" -version = "5.16.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "bidict" }, - { name = "python-engineio" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/59/81/cf8284f45e32efa18d3848ed82cdd4dcc1b657b082458fbe01ad3e1f2f8d/python_socketio-5.16.1.tar.gz", hash = "sha256:f863f98eacce81ceea2e742f6388e10ca3cdd0764be21d30d5196470edf5ea89", size = 128508, upload-time = "2026-02-06T23:42:07Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/07/c7/deb8c5e604404dbf10a3808a858946ca3547692ff6316b698945bb72177e/python_socketio-5.16.1-py3-none-any.whl", hash = "sha256:a3eb1702e92aa2f2b5d3ba00261b61f062cce51f1cfb6900bf3ab4d1934d2d35", size = 82054, upload-time = "2026-02-06T23:42:05.772Z" }, -] - -[[package]] -name = "pyyaml" -version = "6.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, - { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, - { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, - { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, - { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, - { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, - { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, - { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, - { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, - { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, - { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, - { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, - { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, - { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, - { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, - { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, - { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, - { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, - { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, - { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, - { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, - { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, - { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, - { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, - { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, - { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, - { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, - { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, - { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, - { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, - { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, - { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, - { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, - { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, - { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, - { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, - { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, - { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, - { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, - { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, - { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, - { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, - { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, - { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, - { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, - { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, - { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, - { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, - { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, - { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, - { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, - { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, - { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, - { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, - { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, -] - -[[package]] -name = "redis" -version = "7.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "async-timeout", marker = "python_full_version < '3.11.3'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/da/82/4d1a5279f6c1251d3d2a603a798a1137c657de9b12cfc1fba4858232c4d2/redis-7.3.0.tar.gz", hash = "sha256:4d1b768aafcf41b01022410b3cc4f15a07d9b3d6fe0c66fc967da2c88e551034", size = 4928081, upload-time = "2026-03-06T18:18:16.287Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f0/28/84e57fce7819e81ec5aa1bd31c42b89607241f4fb1a3ea5b0d2dbeaea26c/redis-7.3.0-py3-none-any.whl", hash = "sha256:9d4fcb002a12a5e3c3fbe005d59c48a2cc231f87fbb2f6b70c2d89bb64fec364", size = 404379, upload-time = "2026-03-06T18:18:14.583Z" }, -] - -[[package]] -name = "reflex" -version = "0.8.27" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "alembic" }, - { name = "click" }, - { name = "granian", extra = ["reload"] }, - { name = "httpx" }, - { name = "packaging" }, - { name = "platformdirs" }, - { name = "psutil", marker = "sys_platform == 'win32'" }, - { name = "pydantic" }, - { name = "python-multipart" }, - { name = "python-socketio" }, - { name = "redis" }, - { name = "reflex-hosting-cli" }, - { name = "rich" }, - { name = "sqlmodel" }, - { name = "starlette" }, - { name = "typing-extensions" }, - { name = "wrapt" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e3/51/f6dd657f04a097aa7b02fb6ab08b24f3a4fe676378af5b9e6dff2d92c64b/reflex-0.8.27.tar.gz", hash = "sha256:c90cca51d77955e0162baf4384b843e4350c75afa656589e484470222b48368f", size = 572762, upload-time = "2026-02-17T19:23:37.574Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/03/abdfa493299dd63ecb3f046755a71748a259c7535f58c8d9203eced73262/reflex-0.8.27-py3-none-any.whl", hash = "sha256:1ddd492542d39735f2d1a002549025698022a130b6fa2af1cea33af516058ac6", size = 815640, upload-time = "2026-02-17T19:23:35.897Z" }, -] - -[[package]] -name = "reflex-hosting-cli" -version = "0.1.62" -source = { editable = "." } -dependencies = [ - { name = "click" }, - { name = "httpx" }, - { name = "packaging" }, - { name = "platformdirs" }, - { name = "rich" }, -] - -[package.dev-dependencies] -dev = [ - { name = "coverage" }, - { name = "darglint" }, - { name = "pre-commit" }, - { name = "pyright" }, - { name = "pytest" }, - { name = "pytest-benchmark" }, - { name = "pytest-cov" }, - { name = "pytest-mock" }, - { name = "pyyaml" }, - { name = "reflex" }, - { name = "ruff" }, - { name = "typer" }, -] - -[package.metadata] -requires-dist = [ - { name = "click", specifier = ">=8.2" }, - { name = "httpx", specifier = ">=0.25.1,<1.0" }, - { name = "packaging", specifier = ">=24.2" }, - { name = "platformdirs", specifier = ">=3.10.0,<5.0" }, - { name = "rich", specifier = ">=13,<15" }, -] - -[package.metadata.requires-dev] -dev = [ - { name = "coverage" }, - { name = "darglint" }, - { name = "pre-commit" }, - { name = "pyright" }, - { name = "pytest" }, - { name = "pytest-benchmark" }, - { name = "pytest-cov" }, - { name = "pytest-mock" }, - { name = "pyyaml" }, - { name = "reflex" }, - { name = "ruff" }, - { name = "typer" }, -] - -[[package]] -name = "rich" -version = "14.3.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markdown-it-py" }, - { name = "pygments" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" }, -] - -[[package]] -name = "ruff" -version = "0.15.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/77/9b/840e0039e65fcf12758adf684d2289024d6140cde9268cc59887dc55189c/ruff-0.15.5.tar.gz", hash = "sha256:7c3601d3b6d76dce18c5c824fc8d06f4eef33d6df0c21ec7799510cde0f159a2", size = 4574214, upload-time = "2026-03-05T20:06:34.946Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/47/20/5369c3ce21588c708bcbe517a8fbe1a8dfdb5dfd5137e14790b1da71612c/ruff-0.15.5-py3-none-linux_armv6l.whl", hash = "sha256:4ae44c42281f42e3b06b988e442d344a5b9b72450ff3c892e30d11b29a96a57c", size = 10478185, upload-time = "2026-03-05T20:06:29.093Z" }, - { url = "https://files.pythonhosted.org/packages/44/ed/e81dd668547da281e5dce710cf0bc60193f8d3d43833e8241d006720e42b/ruff-0.15.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6edd3792d408ebcf61adabc01822da687579a1a023f297618ac27a5b51ef0080", size = 10859201, upload-time = "2026-03-05T20:06:32.632Z" }, - { url = "https://files.pythonhosted.org/packages/c4/8f/533075f00aaf19b07c5cd6aa6e5d89424b06b3b3f4583bfa9c640a079059/ruff-0.15.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:89f463f7c8205a9f8dea9d658d59eff49db05f88f89cc3047fb1a02d9f344010", size = 10184752, upload-time = "2026-03-05T20:06:40.312Z" }, - { url = "https://files.pythonhosted.org/packages/66/0e/ba49e2c3fa0395b3152bad634c7432f7edfc509c133b8f4529053ff024fb/ruff-0.15.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba786a8295c6574c1116704cf0b9e6563de3432ac888d8f83685654fe528fd65", size = 10534857, upload-time = "2026-03-05T20:06:19.581Z" }, - { url = "https://files.pythonhosted.org/packages/59/71/39234440f27a226475a0659561adb0d784b4d247dfe7f43ffc12dd02e288/ruff-0.15.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd4b801e57955fe9f02b31d20375ab3a5c4415f2e5105b79fb94cf2642c91440", size = 10309120, upload-time = "2026-03-05T20:06:00.435Z" }, - { url = "https://files.pythonhosted.org/packages/f5/87/4140aa86a93df032156982b726f4952aaec4a883bb98cb6ef73c347da253/ruff-0.15.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:391f7c73388f3d8c11b794dbbc2959a5b5afe66642c142a6effa90b45f6f5204", size = 11047428, upload-time = "2026-03-05T20:05:51.867Z" }, - { url = "https://files.pythonhosted.org/packages/5a/f7/4953e7e3287676f78fbe85e3a0ca414c5ca81237b7575bdadc00229ac240/ruff-0.15.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8dc18f30302e379fe1e998548b0f5e9f4dff907f52f73ad6da419ea9c19d66c8", size = 11914251, upload-time = "2026-03-05T20:06:22.887Z" }, - { url = "https://files.pythonhosted.org/packages/77/46/0f7c865c10cf896ccf5a939c3e84e1cfaeed608ff5249584799a74d33835/ruff-0.15.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1cc6e7f90087e2d27f98dc34ed1b3ab7c8f0d273cc5431415454e22c0bd2a681", size = 11333801, upload-time = "2026-03-05T20:05:57.168Z" }, - { url = "https://files.pythonhosted.org/packages/d3/01/a10fe54b653061585e655f5286c2662ebddb68831ed3eaebfb0eb08c0a16/ruff-0.15.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1cb7169f53c1ddb06e71a9aebd7e98fc0fea936b39afb36d8e86d36ecc2636a", size = 11206821, upload-time = "2026-03-05T20:06:03.441Z" }, - { url = "https://files.pythonhosted.org/packages/7a/0d/2132ceaf20c5e8699aa83da2706ecb5c5dcdf78b453f77edca7fb70f8a93/ruff-0.15.5-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9b037924500a31ee17389b5c8c4d88874cc6ea8e42f12e9c61a3d754ff72f1ca", size = 11133326, upload-time = "2026-03-05T20:06:25.655Z" }, - { url = "https://files.pythonhosted.org/packages/72/cb/2e5259a7eb2a0f87c08c0fe5bf5825a1e4b90883a52685524596bfc93072/ruff-0.15.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:65bb414e5b4eadd95a8c1e4804f6772bbe8995889f203a01f77ddf2d790929dd", size = 10510820, upload-time = "2026-03-05T20:06:37.79Z" }, - { url = "https://files.pythonhosted.org/packages/ff/20/b67ce78f9e6c59ffbdb5b4503d0090e749b5f2d31b599b554698a80d861c/ruff-0.15.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d20aa469ae3b57033519c559e9bc9cd9e782842e39be05b50e852c7c981fa01d", size = 10302395, upload-time = "2026-03-05T20:05:54.504Z" }, - { url = "https://files.pythonhosted.org/packages/5f/e5/719f1acccd31b720d477751558ed74e9c88134adcc377e5e886af89d3072/ruff-0.15.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:15388dd28c9161cdb8eda68993533acc870aa4e646a0a277aa166de9ad5a8752", size = 10754069, upload-time = "2026-03-05T20:06:06.422Z" }, - { url = "https://files.pythonhosted.org/packages/c3/9c/d1db14469e32d98f3ca27079dbd30b7b44dbb5317d06ab36718dee3baf03/ruff-0.15.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b30da330cbd03bed0c21420b6b953158f60c74c54c5f4c1dabbdf3a57bf355d2", size = 11304315, upload-time = "2026-03-05T20:06:10.867Z" }, - { url = "https://files.pythonhosted.org/packages/28/3a/950367aee7c69027f4f422059227b290ed780366b6aecee5de5039d50fa8/ruff-0.15.5-py3-none-win32.whl", hash = "sha256:732e5ee1f98ba5b3679029989a06ca39a950cced52143a0ea82a2102cb592b74", size = 10551676, upload-time = "2026-03-05T20:06:13.705Z" }, - { url = "https://files.pythonhosted.org/packages/b8/00/bf077a505b4e649bdd3c47ff8ec967735ce2544c8e4a43aba42ee9bf935d/ruff-0.15.5-py3-none-win_amd64.whl", hash = "sha256:821d41c5fa9e19117616c35eaa3f4b75046ec76c65e7ae20a333e9a8696bc7fe", size = 11678972, upload-time = "2026-03-05T20:06:45.379Z" }, - { url = "https://files.pythonhosted.org/packages/fe/4e/cd76eca6db6115604b7626668e891c9dd03330384082e33662fb0f113614/ruff-0.15.5-py3-none-win_arm64.whl", hash = "sha256:b498d1c60d2fe5c10c45ec3f698901065772730b411f164ae270bb6bfcc4740b", size = 10965572, upload-time = "2026-03-05T20:06:16.984Z" }, -] - -[[package]] -name = "shellingham" -version = "1.5.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, -] - -[[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, upload-time = "2024-10-10T22:39:31.412Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl", hash = "sha256:4af6069630a38ed6c561010f0e11a5bc0d4ca569b36306eb257cd9a192497c8c", size = 13842, upload-time = "2024-10-10T22:39:29.645Z" }, -] - -[[package]] -name = "sqlalchemy" -version = "2.0.48" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/1f/73/b4a9737255583b5fa858e0bb8e116eb94b88c910164ed2ed719147bde3de/sqlalchemy-2.0.48.tar.gz", hash = "sha256:5ca74f37f3369b45e1f6b7b06afb182af1fd5dde009e4ffd831830d98cbe5fe7", size = 9886075, upload-time = "2026-03-02T15:28:51.474Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/67/1235676e93dd3b742a4a8eddfae49eea46c85e3eed29f0da446a8dd57500/sqlalchemy-2.0.48-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7001dc9d5f6bb4deb756d5928eaefe1930f6f4179da3924cbd95ee0e9f4dce89", size = 2157384, upload-time = "2026-03-02T15:38:26.781Z" }, - { url = "https://files.pythonhosted.org/packages/4d/d7/fa728b856daa18c10e1390e76f26f64ac890c947008284387451d56ca3d0/sqlalchemy-2.0.48-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1a89ce07ad2d4b8cfc30bd5889ec40613e028ed80ef47da7d9dd2ce969ad30e0", size = 3236981, upload-time = "2026-03-02T15:58:53.53Z" }, - { url = "https://files.pythonhosted.org/packages/5c/ad/6c4395649a212a6c603a72c5b9ab5dce3135a1546cfdffa3c427e71fd535/sqlalchemy-2.0.48-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10853a53a4a00417a00913d270dddda75815fcb80675874285f41051c094d7dd", size = 3235232, upload-time = "2026-03-02T15:52:25.654Z" }, - { url = "https://files.pythonhosted.org/packages/01/f4/58f845e511ac0509765a6f85eb24924c1ef0d54fb50de9d15b28c3601458/sqlalchemy-2.0.48-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fac0fa4e4f55f118fd87177dacb1c6522fe39c28d498d259014020fec9164c29", size = 3188106, upload-time = "2026-03-02T15:58:55.193Z" }, - { url = "https://files.pythonhosted.org/packages/3f/f9/6dcc7bfa5f5794c3a095e78cd1de8269dfb5584dfd4c2c00a50d3c1ade44/sqlalchemy-2.0.48-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3713e21ea67bca727eecd4a24bf68bcd414c403faae4989442be60994301ded0", size = 3209522, upload-time = "2026-03-02T15:52:27.407Z" }, - { url = "https://files.pythonhosted.org/packages/d7/5a/b632875ab35874d42657f079529f0745410604645c269a8c21fb4272ff7a/sqlalchemy-2.0.48-cp310-cp310-win32.whl", hash = "sha256:d404dc897ce10e565d647795861762aa2d06ca3f4a728c5e9a835096c7059018", size = 2117695, upload-time = "2026-03-02T15:46:51.389Z" }, - { url = "https://files.pythonhosted.org/packages/de/03/9752eb2a41afdd8568e41ac3c3128e32a0a73eada5ab80483083604a56d1/sqlalchemy-2.0.48-cp310-cp310-win_amd64.whl", hash = "sha256:841a94c66577661c1f088ac958cd767d7c9bf507698f45afffe7a4017049de76", size = 2140928, upload-time = "2026-03-02T15:46:52.992Z" }, - { url = "https://files.pythonhosted.org/packages/d7/6d/b8b78b5b80f3c3ab3f7fa90faa195ec3401f6d884b60221260fd4d51864c/sqlalchemy-2.0.48-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b4c575df7368b3b13e0cebf01d4679f9a28ed2ae6c1cd0b1d5beffb6b2007dc", size = 2157184, upload-time = "2026-03-02T15:38:28.161Z" }, - { url = "https://files.pythonhosted.org/packages/21/4b/4f3d4a43743ab58b95b9ddf5580a265b593d017693df9e08bd55780af5bb/sqlalchemy-2.0.48-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e83e3f959aaa1c9df95c22c528096d94848a1bc819f5d0ebf7ee3df0ca63db6c", size = 3313555, upload-time = "2026-03-02T15:58:57.21Z" }, - { url = "https://files.pythonhosted.org/packages/21/dd/3b7c53f1dbbf736fd27041aee68f8ac52226b610f914085b1652c2323442/sqlalchemy-2.0.48-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f7b7243850edd0b8b97043f04748f31de50cf426e939def5c16bedb540698f7", size = 3313057, upload-time = "2026-03-02T15:52:29.366Z" }, - { url = "https://files.pythonhosted.org/packages/d9/cc/3e600a90ae64047f33313d7d32e5ad025417f09d2ded487e8284b5e21a15/sqlalchemy-2.0.48-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:82745b03b4043e04600a6b665cb98697c4339b24e34d74b0a2ac0a2488b6f94d", size = 3265431, upload-time = "2026-03-02T15:58:59.096Z" }, - { url = "https://files.pythonhosted.org/packages/8b/19/780138dacfe3f5024f4cf96e4005e91edf6653d53d3673be4844578faf1d/sqlalchemy-2.0.48-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e5e088bf43f6ee6fec7dbf1ef7ff7774a616c236b5c0cb3e00662dd71a56b571", size = 3287646, upload-time = "2026-03-02T15:52:31.569Z" }, - { url = "https://files.pythonhosted.org/packages/40/fd/f32ced124f01a23151f4777e4c705f3a470adc7bd241d9f36a7c941a33bf/sqlalchemy-2.0.48-cp311-cp311-win32.whl", hash = "sha256:9c7d0a77e36b5f4b01ca398482230ab792061d243d715299b44a0b55c89fe617", size = 2116956, upload-time = "2026-03-02T15:46:54.535Z" }, - { url = "https://files.pythonhosted.org/packages/58/d5/dd767277f6feef12d05651538f280277e661698f617fa4d086cce6055416/sqlalchemy-2.0.48-cp311-cp311-win_amd64.whl", hash = "sha256:583849c743e0e3c9bb7446f5b5addeacedc168d657a69b418063dfdb2d90081c", size = 2141627, upload-time = "2026-03-02T15:46:55.849Z" }, - { url = "https://files.pythonhosted.org/packages/ef/91/a42ae716f8925e9659df2da21ba941f158686856107a61cc97a95e7647a3/sqlalchemy-2.0.48-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:348174f228b99f33ca1f773e85510e08927620caa59ffe7803b37170df30332b", size = 2155737, upload-time = "2026-03-02T15:49:13.207Z" }, - { url = "https://files.pythonhosted.org/packages/b9/52/f75f516a1f3888f027c1cfb5d22d4376f4b46236f2e8669dcb0cddc60275/sqlalchemy-2.0.48-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53667b5f668991e279d21f94ccfa6e45b4e3f4500e7591ae59a8012d0f010dcb", size = 3337020, upload-time = "2026-03-02T15:50:34.547Z" }, - { url = "https://files.pythonhosted.org/packages/37/9a/0c28b6371e0cdcb14f8f1930778cb3123acfcbd2c95bb9cf6b4a2ba0cce3/sqlalchemy-2.0.48-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34634e196f620c7a61d18d5cf7dc841ca6daa7961aed75d532b7e58b309ac894", size = 3349983, upload-time = "2026-03-02T15:53:25.542Z" }, - { url = "https://files.pythonhosted.org/packages/1c/46/0aee8f3ff20b1dcbceb46ca2d87fcc3d48b407925a383ff668218509d132/sqlalchemy-2.0.48-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:546572a1793cc35857a2ffa1fe0e58571af1779bcc1ffa7c9fb0839885ed69a9", size = 3279690, upload-time = "2026-03-02T15:50:36.277Z" }, - { url = "https://files.pythonhosted.org/packages/ce/8c/a957bc91293b49181350bfd55e6dfc6e30b7f7d83dc6792d72043274a390/sqlalchemy-2.0.48-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:07edba08061bc277bfdc772dd2a1a43978f5a45994dd3ede26391b405c15221e", size = 3314738, upload-time = "2026-03-02T15:53:27.519Z" }, - { url = "https://files.pythonhosted.org/packages/4b/44/1d257d9f9556661e7bdc83667cc414ba210acfc110c82938cb3611eea58f/sqlalchemy-2.0.48-cp312-cp312-win32.whl", hash = "sha256:908a3fa6908716f803b86896a09a2c4dde5f5ce2bb07aacc71ffebb57986ce99", size = 2115546, upload-time = "2026-03-02T15:54:31.591Z" }, - { url = "https://files.pythonhosted.org/packages/f2/af/c3c7e1f3a2b383155a16454df62ae8c62a30dd238e42e68c24cebebbfae6/sqlalchemy-2.0.48-cp312-cp312-win_amd64.whl", hash = "sha256:68549c403f79a8e25984376480959975212a670405e3913830614432b5daa07a", size = 2142484, upload-time = "2026-03-02T15:54:34.072Z" }, - { url = "https://files.pythonhosted.org/packages/d1/c6/569dc8bf3cd375abc5907e82235923e986799f301cd79a903f784b996fca/sqlalchemy-2.0.48-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e3070c03701037aa418b55d36532ecb8f8446ed0135acb71c678dbdf12f5b6e4", size = 2152599, upload-time = "2026-03-02T15:49:14.41Z" }, - { url = "https://files.pythonhosted.org/packages/6d/ff/f4e04a4bd5a24304f38cb0d4aa2ad4c0fb34999f8b884c656535e1b2b74c/sqlalchemy-2.0.48-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2645b7d8a738763b664a12a1542c89c940daa55196e8d73e55b169cc5c99f65f", size = 3278825, upload-time = "2026-03-02T15:50:38.269Z" }, - { url = "https://files.pythonhosted.org/packages/fe/88/cb59509e4668d8001818d7355d9995be90c321313078c912420603a7cb95/sqlalchemy-2.0.48-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b19151e76620a412c2ac1c6f977ab1b9fa7ad43140178345136456d5265b32ed", size = 3295200, upload-time = "2026-03-02T15:53:29.366Z" }, - { url = "https://files.pythonhosted.org/packages/87/dc/1609a4442aefd750ea2f32629559394ec92e89ac1d621a7f462b70f736ff/sqlalchemy-2.0.48-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5b193a7e29fd9fa56e502920dca47dffe60f97c863494946bd698c6058a55658", size = 3226876, upload-time = "2026-03-02T15:50:39.802Z" }, - { url = "https://files.pythonhosted.org/packages/37/c3/6ae2ab5ea2fa989fbac4e674de01224b7a9d744becaf59bb967d62e99bed/sqlalchemy-2.0.48-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:36ac4ddc3d33e852da9cb00ffb08cea62ca05c39711dc67062ca2bb1fae35fd8", size = 3265045, upload-time = "2026-03-02T15:53:31.421Z" }, - { url = "https://files.pythonhosted.org/packages/6f/82/ea4665d1bb98c50c19666e672f21b81356bd6077c4574e3d2bbb84541f53/sqlalchemy-2.0.48-cp313-cp313-win32.whl", hash = "sha256:389b984139278f97757ea9b08993e7b9d1142912e046ab7d82b3fbaeb0209131", size = 2113700, upload-time = "2026-03-02T15:54:35.825Z" }, - { url = "https://files.pythonhosted.org/packages/b7/2b/b9040bec58c58225f073f5b0c1870defe1940835549dafec680cbd58c3c3/sqlalchemy-2.0.48-cp313-cp313-win_amd64.whl", hash = "sha256:d612c976cbc2d17edfcc4c006874b764e85e990c29ce9bd411f926bbfb02b9a2", size = 2139487, upload-time = "2026-03-02T15:54:37.079Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f4/7b17bd50244b78a49d22cc63c969d71dc4de54567dc152a9b46f6fae40ce/sqlalchemy-2.0.48-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69f5bc24904d3bc3640961cddd2523e361257ef68585d6e364166dfbe8c78fae", size = 3558851, upload-time = "2026-03-02T15:57:48.607Z" }, - { url = "https://files.pythonhosted.org/packages/20/0d/213668e9aca61d370f7d2a6449ea4ec699747fac67d4bda1bb3d129025be/sqlalchemy-2.0.48-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd08b90d211c086181caed76931ecfa2bdfc83eea3cfccdb0f82abc6c4b876cb", size = 3525525, upload-time = "2026-03-02T16:04:38.058Z" }, - { url = "https://files.pythonhosted.org/packages/85/d7/a84edf412979e7d59c69b89a5871f90a49228360594680e667cb2c46a828/sqlalchemy-2.0.48-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1ccd42229aaac2df431562117ac7e667d702e8e44afdb6cf0e50fa3f18160f0b", size = 3466611, upload-time = "2026-03-02T15:57:50.759Z" }, - { url = "https://files.pythonhosted.org/packages/86/55/42404ce5770f6be26a2b0607e7866c31b9a4176c819e9a7a5e0a055770be/sqlalchemy-2.0.48-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f0dcbc588cd5b725162c076eb9119342f6579c7f7f55057bb7e3c6ff27e13121", size = 3475812, upload-time = "2026-03-02T16:04:40.092Z" }, - { url = "https://files.pythonhosted.org/packages/ae/ae/29b87775fadc43e627cf582fe3bda4d02e300f6b8f2747c764950d13784c/sqlalchemy-2.0.48-cp313-cp313t-win32.whl", hash = "sha256:9764014ef5e58aab76220c5664abb5d47d5bc858d9debf821e55cfdd0f128485", size = 2141335, upload-time = "2026-03-02T15:52:51.518Z" }, - { url = "https://files.pythonhosted.org/packages/91/44/f39d063c90f2443e5b46ec4819abd3d8de653893aae92df42a5c4f5843de/sqlalchemy-2.0.48-cp313-cp313t-win_amd64.whl", hash = "sha256:e2f35b4cccd9ed286ad62e0a3c3ac21e06c02abc60e20aa51a3e305a30f5fa79", size = 2173095, upload-time = "2026-03-02T15:52:52.79Z" }, - { url = "https://files.pythonhosted.org/packages/f7/b3/f437eaa1cf028bb3c927172c7272366393e73ccd104dcf5b6963f4ab5318/sqlalchemy-2.0.48-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e2d0d88686e3d35a76f3e15a34e8c12d73fc94c1dea1cd55782e695cc14086dd", size = 2154401, upload-time = "2026-03-02T15:49:17.24Z" }, - { url = "https://files.pythonhosted.org/packages/6c/1c/b3abdf0f402aa3f60f0df6ea53d92a162b458fca2321d8f1f00278506402/sqlalchemy-2.0.48-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49b7bddc1eebf011ea5ab722fdbe67a401caa34a350d278cc7733c0e88fecb1f", size = 3274528, upload-time = "2026-03-02T15:50:41.489Z" }, - { url = "https://files.pythonhosted.org/packages/f2/5e/327428a034407651a048f5e624361adf3f9fbac9d0fa98e981e9c6ff2f5e/sqlalchemy-2.0.48-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:426c5ca86415d9b8945c7073597e10de9644802e2ff502b8e1f11a7a2642856b", size = 3279523, upload-time = "2026-03-02T15:53:32.962Z" }, - { url = "https://files.pythonhosted.org/packages/2a/ca/ece73c81a918add0965b76b868b7b5359e068380b90ef1656ee995940c02/sqlalchemy-2.0.48-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:288937433bd44e3990e7da2402fabc44a3c6c25d3704da066b85b89a85474ae0", size = 3224312, upload-time = "2026-03-02T15:50:42.996Z" }, - { url = "https://files.pythonhosted.org/packages/88/11/fbaf1ae91fa4ee43f4fe79661cead6358644824419c26adb004941bdce7c/sqlalchemy-2.0.48-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8183dc57ae7d9edc1346e007e840a9f3d6aa7b7f165203a99e16f447150140d2", size = 3246304, upload-time = "2026-03-02T15:53:34.937Z" }, - { url = "https://files.pythonhosted.org/packages/fa/a8/5fb0deb13930b4f2f698c5541ae076c18981173e27dd00376dbaea7a9c82/sqlalchemy-2.0.48-cp314-cp314-win32.whl", hash = "sha256:1182437cb2d97988cfea04cf6cdc0b0bb9c74f4d56ec3d08b81e23d621a28cc6", size = 2116565, upload-time = "2026-03-02T15:54:38.321Z" }, - { url = "https://files.pythonhosted.org/packages/95/7e/e83615cb63f80047f18e61e31e8e32257d39458426c23006deeaf48f463b/sqlalchemy-2.0.48-cp314-cp314-win_amd64.whl", hash = "sha256:144921da96c08feb9e2b052c5c5c1d0d151a292c6135623c6b2c041f2a45f9e0", size = 2142205, upload-time = "2026-03-02T15:54:39.831Z" }, - { url = "https://files.pythonhosted.org/packages/83/e3/69d8711b3f2c5135e9cde5f063bc1605860f0b2c53086d40c04017eb1f77/sqlalchemy-2.0.48-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5aee45fd2c6c0f2b9cdddf48c48535e7471e42d6fb81adfde801da0bd5b93241", size = 3563519, upload-time = "2026-03-02T15:57:52.387Z" }, - { url = "https://files.pythonhosted.org/packages/f8/4f/a7cce98facca73c149ea4578981594aaa5fd841e956834931de503359336/sqlalchemy-2.0.48-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7cddca31edf8b0653090cbb54562ca027c421c58ddde2c0685f49ff56a1690e0", size = 3528611, upload-time = "2026-03-02T16:04:42.097Z" }, - { url = "https://files.pythonhosted.org/packages/cd/7d/5936c7a03a0b0cb0fa0cc425998821c6029756b0855a8f7ee70fba1de955/sqlalchemy-2.0.48-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7a936f1bb23d370b7c8cc079d5fce4c7d18da87a33c6744e51a93b0f9e97e9b3", size = 3472326, upload-time = "2026-03-02T15:57:54.423Z" }, - { url = "https://files.pythonhosted.org/packages/f4/33/cea7dfc31b52904efe3dcdc169eb4514078887dff1f5ae28a7f4c5d54b3c/sqlalchemy-2.0.48-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e004aa9248e8cb0a5f9b96d003ca7c1c0a5da8decd1066e7b53f59eb8ce7c62b", size = 3478453, upload-time = "2026-03-02T16:04:44.584Z" }, - { url = "https://files.pythonhosted.org/packages/c8/95/32107c4d13be077a9cae61e9ae49966a35dc4bf442a8852dd871db31f62e/sqlalchemy-2.0.48-cp314-cp314t-win32.whl", hash = "sha256:b8438ec5594980d405251451c5b7ea9aa58dda38eb7ac35fb7e4c696712ee24f", size = 2147209, upload-time = "2026-03-02T15:52:54.274Z" }, - { url = "https://files.pythonhosted.org/packages/d2/d7/1e073da7a4bc645eb83c76067284a0374e643bc4be57f14cc6414656f92c/sqlalchemy-2.0.48-cp314-cp314t-win_amd64.whl", hash = "sha256:d854b3970067297f3a7fbd7a4683587134aa9b3877ee15aa29eea478dc68f933", size = 2182198, upload-time = "2026-03-02T15:52:55.606Z" }, - { url = "https://files.pythonhosted.org/packages/46/2c/9664130905f03db57961b8980b05cab624afd114bf2be2576628a9f22da4/sqlalchemy-2.0.48-py3-none-any.whl", hash = "sha256:a66fe406437dd65cacd96a72689a3aaaecaebbcd62d81c5ac1c0fdbeac835096", size = 1940202, upload-time = "2026-03-02T15:52:43.285Z" }, -] - -[[package]] -name = "sqlmodel" -version = "0.0.37" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pydantic" }, - { name = "sqlalchemy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fb/26/1d2faa0fd5a765267f49751de533adac6b9ff9366c7c6e7692df4f32230f/sqlmodel-0.0.37.tar.gz", hash = "sha256:d2c19327175794faf50b1ee31cc966764f55b1dedefc046450bc5741a3d68352", size = 85527, upload-time = "2026-02-21T16:39:47.038Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b1/e1/7c8d18e737433f3b5bbe27b56a9072a9fcb36342b48f1bef34b6da1d61f2/sqlmodel-0.0.37-py3-none-any.whl", hash = "sha256:2137a4045ef3fd66a917a7717ada959a1ceb3630d95e1f6aaab39dd2c0aef278", size = 27224, upload-time = "2026-02-21T16:39:47.781Z" }, -] - -[[package]] -name = "starlette" -version = "0.52.1" -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/c4/68/79977123bb7be889ad680d79a40f339082c1978b5cfcf62c2d8d196873ac/starlette-0.52.1.tar.gz", hash = "sha256:834edd1b0a23167694292e94f597773bc3f89f362be6effee198165a35d62933", size = 2653702, upload-time = "2026-01-18T13:34:11.062Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74", size = 74272, upload-time = "2026-01-18T13:34:09.188Z" }, -] - -[[package]] -name = "tomli" -version = "2.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, - { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, - { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, - { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, - { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, - { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, - { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, - { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, - { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, - { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, - { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, - { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, - { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, - { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, - { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, - { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, - { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, - { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, - { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, - { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, - { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, - { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, - { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, - { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, - { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, - { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, - { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, - { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, - { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, - { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, - { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, - { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, - { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, - { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, - { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, - { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, - { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, - { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, - { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, - { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, - { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, - { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, - { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, - { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, - { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, - { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, -] - -[[package]] -name = "typer" -version = "0.24.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "annotated-doc" }, - { name = "click" }, - { name = "rich" }, - { name = "shellingham" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f5/24/cb09efec5cc954f7f9b930bf8279447d24618bb6758d4f6adf2574c41780/typer-0.24.1.tar.gz", hash = "sha256:e39b4732d65fbdcde189ae76cf7cd48aeae72919dea1fdfc16593be016256b45", size = 118613, upload-time = "2026-02-21T16:54:40.609Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl", hash = "sha256:112c1f0ce578bfb4cab9ffdabc68f031416ebcc216536611ba21f04e9aa84c9e", size = 56085, upload-time = "2026-02-21T16:54:41.616Z" }, -] - -[[package]] -name = "typing-extensions" -version = "4.15.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, -] - -[[package]] -name = "typing-inspection" -version = "0.4.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, -] - -[[package]] -name = "virtualenv" -version = "21.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "distlib" }, - { name = "filelock" }, - { name = "platformdirs" }, - { name = "python-discovery" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/aa/92/58199fe10049f9703c2666e809c4f686c54ef0a68b0f6afccf518c0b1eb9/virtualenv-21.2.0.tar.gz", hash = "sha256:1720dc3a62ef5b443092e3f499228599045d7fea4c79199770499df8becf9098", size = 5840618, upload-time = "2026-03-09T17:24:38.013Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/59/7d02447a55b2e55755011a647479041bc92a82e143f96a8195cb33bd0a1c/virtualenv-21.2.0-py3-none-any.whl", hash = "sha256:1bd755b504931164a5a496d217c014d098426cddc79363ad66ac78125f9d908f", size = 5825084, upload-time = "2026-03-09T17:24:35.378Z" }, -] - -[[package]] -name = "watchfiles" -version = "1.1.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/1a/206e8cf2dd86fddf939165a57b4df61607a1e0add2785f170a3f616b7d9f/watchfiles-1.1.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:eef58232d32daf2ac67f42dea51a2c80f0d03379075d44a587051e63cc2e368c", size = 407318, upload-time = "2025-10-14T15:04:18.753Z" }, - { url = "https://files.pythonhosted.org/packages/b3/0f/abaf5262b9c496b5dad4ed3c0e799cbecb1f8ea512ecb6ddd46646a9fca3/watchfiles-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:03fa0f5237118a0c5e496185cafa92878568b652a2e9a9382a5151b1a0380a43", size = 394478, upload-time = "2025-10-14T15:04:20.297Z" }, - { url = "https://files.pythonhosted.org/packages/b1/04/9cc0ba88697b34b755371f5ace8d3a4d9a15719c07bdc7bd13d7d8c6a341/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca65483439f9c791897f7db49202301deb6e15fe9f8fe2fed555bf986d10c31", size = 449894, upload-time = "2025-10-14T15:04:21.527Z" }, - { url = "https://files.pythonhosted.org/packages/d2/9c/eda4615863cd8621e89aed4df680d8c3ec3da6a4cf1da113c17decd87c7f/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f0ab1c1af0cb38e3f598244c17919fb1a84d1629cc08355b0074b6d7f53138ac", size = 459065, upload-time = "2025-10-14T15:04:22.795Z" }, - { url = "https://files.pythonhosted.org/packages/84/13/f28b3f340157d03cbc8197629bc109d1098764abe1e60874622a0be5c112/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bc570d6c01c206c46deb6e935a260be44f186a2f05179f52f7fcd2be086a94d", size = 488377, upload-time = "2025-10-14T15:04:24.138Z" }, - { url = "https://files.pythonhosted.org/packages/86/93/cfa597fa9389e122488f7ffdbd6db505b3b915ca7435ecd7542e855898c2/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e84087b432b6ac94778de547e08611266f1f8ffad28c0ee4c82e028b0fc5966d", size = 595837, upload-time = "2025-10-14T15:04:25.057Z" }, - { url = "https://files.pythonhosted.org/packages/57/1e/68c1ed5652b48d89fc24d6af905d88ee4f82fa8bc491e2666004e307ded1/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:620bae625f4cb18427b1bb1a2d9426dc0dd5a5ba74c7c2cdb9de405f7b129863", size = 473456, upload-time = "2025-10-14T15:04:26.497Z" }, - { url = "https://files.pythonhosted.org/packages/d5/dc/1a680b7458ffa3b14bb64878112aefc8f2e4f73c5af763cbf0bd43100658/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:544364b2b51a9b0c7000a4b4b02f90e9423d97fbbf7e06689236443ebcad81ab", size = 455614, upload-time = "2025-10-14T15:04:27.539Z" }, - { url = "https://files.pythonhosted.org/packages/61/a5/3d782a666512e01eaa6541a72ebac1d3aae191ff4a31274a66b8dd85760c/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bbe1ef33d45bc71cf21364df962af171f96ecaeca06bd9e3d0b583efb12aec82", size = 630690, upload-time = "2025-10-14T15:04:28.495Z" }, - { url = "https://files.pythonhosted.org/packages/9b/73/bb5f38590e34687b2a9c47a244aa4dd50c56a825969c92c9c5fc7387cea1/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1a0bb430adb19ef49389e1ad368450193a90038b5b752f4ac089ec6942c4dff4", size = 622459, upload-time = "2025-10-14T15:04:29.491Z" }, - { url = "https://files.pythonhosted.org/packages/f1/ac/c9bb0ec696e07a20bd58af5399aeadaef195fb2c73d26baf55180fe4a942/watchfiles-1.1.1-cp310-cp310-win32.whl", hash = "sha256:3f6d37644155fb5beca5378feb8c1708d5783145f2a0f1c4d5a061a210254844", size = 272663, upload-time = "2025-10-14T15:04:30.435Z" }, - { url = "https://files.pythonhosted.org/packages/11/a0/a60c5a7c2ec59fa062d9a9c61d02e3b6abd94d32aac2d8344c4bdd033326/watchfiles-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a36d8efe0f290835fd0f33da35042a1bb5dc0e83cbc092dcf69bce442579e88e", size = 287453, upload-time = "2025-10-14T15:04:31.53Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f8/2c5f479fb531ce2f0564eda479faecf253d886b1ab3630a39b7bf7362d46/watchfiles-1.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5", size = 406529, upload-time = "2025-10-14T15:04:32.899Z" }, - { url = "https://files.pythonhosted.org/packages/fe/cd/f515660b1f32f65df671ddf6f85bfaca621aee177712874dc30a97397977/watchfiles-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741", size = 394384, upload-time = "2025-10-14T15:04:33.761Z" }, - { url = "https://files.pythonhosted.org/packages/7b/c3/28b7dc99733eab43fca2d10f55c86e03bd6ab11ca31b802abac26b23d161/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6", size = 448789, upload-time = "2025-10-14T15:04:34.679Z" }, - { url = "https://files.pythonhosted.org/packages/4a/24/33e71113b320030011c8e4316ccca04194bf0cbbaeee207f00cbc7d6b9f5/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b", size = 460521, upload-time = "2025-10-14T15:04:35.963Z" }, - { url = "https://files.pythonhosted.org/packages/f4/c3/3c9a55f255aa57b91579ae9e98c88704955fa9dac3e5614fb378291155df/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2cd9e04277e756a2e2d2543d65d1e2166d6fd4c9b183f8808634fda23f17b14", size = 488722, upload-time = "2025-10-14T15:04:37.091Z" }, - { url = "https://files.pythonhosted.org/packages/49/36/506447b73eb46c120169dc1717fe2eff07c234bb3232a7200b5f5bd816e9/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d", size = 596088, upload-time = "2025-10-14T15:04:38.39Z" }, - { url = "https://files.pythonhosted.org/packages/82/ab/5f39e752a9838ec4d52e9b87c1e80f1ee3ccdbe92e183c15b6577ab9de16/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff", size = 472923, upload-time = "2025-10-14T15:04:39.666Z" }, - { url = "https://files.pythonhosted.org/packages/af/b9/a419292f05e302dea372fa7e6fda5178a92998411f8581b9830d28fb9edb/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606", size = 456080, upload-time = "2025-10-14T15:04:40.643Z" }, - { url = "https://files.pythonhosted.org/packages/b0/c3/d5932fd62bde1a30c36e10c409dc5d54506726f08cb3e1d8d0ba5e2bc8db/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701", size = 629432, upload-time = "2025-10-14T15:04:41.789Z" }, - { url = "https://files.pythonhosted.org/packages/f7/77/16bddd9779fafb795f1a94319dc965209c5641db5bf1edbbccace6d1b3c0/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10", size = 623046, upload-time = "2025-10-14T15:04:42.718Z" }, - { url = "https://files.pythonhosted.org/packages/46/ef/f2ecb9a0f342b4bfad13a2787155c6ee7ce792140eac63a34676a2feeef2/watchfiles-1.1.1-cp311-cp311-win32.whl", hash = "sha256:de6da501c883f58ad50db3a32ad397b09ad29865b5f26f64c24d3e3281685849", size = 271473, upload-time = "2025-10-14T15:04:43.624Z" }, - { url = "https://files.pythonhosted.org/packages/94/bc/f42d71125f19731ea435c3948cad148d31a64fccde3867e5ba4edee901f9/watchfiles-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:35c53bd62a0b885bf653ebf6b700d1bf05debb78ad9292cf2a942b23513dc4c4", size = 287598, upload-time = "2025-10-14T15:04:44.516Z" }, - { url = "https://files.pythonhosted.org/packages/57/c9/a30f897351f95bbbfb6abcadafbaca711ce1162f4db95fc908c98a9165f3/watchfiles-1.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:57ca5281a8b5e27593cb7d82c2ac927ad88a96ed406aa446f6344e4328208e9e", size = 277210, upload-time = "2025-10-14T15:04:45.883Z" }, - { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" }, - { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" }, - { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" }, - { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" }, - { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" }, - { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" }, - { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" }, - { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" }, - { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" }, - { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" }, - { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" }, - { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" }, - { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" }, - { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" }, - { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" }, - { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" }, - { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" }, - { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" }, - { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" }, - { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" }, - { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" }, - { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" }, - { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" }, - { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" }, - { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" }, - { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" }, - { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" }, - { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" }, - { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" }, - { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" }, - { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" }, - { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" }, - { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" }, - { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" }, - { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" }, - { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" }, - { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" }, - { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" }, - { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" }, - { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" }, - { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" }, - { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" }, - { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" }, - { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" }, - { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" }, - { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078, upload-time = "2025-10-14T15:05:37.63Z" }, - { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664, upload-time = "2025-10-14T15:05:38.95Z" }, - { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154, upload-time = "2025-10-14T15:05:39.954Z" }, - { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" }, - { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" }, - { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" }, - { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" }, - { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" }, - { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" }, - { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" }, - { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" }, - { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" }, - { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, - { url = "https://files.pythonhosted.org/packages/ba/4c/a888c91e2e326872fa4705095d64acd8aa2fb9c1f7b9bd0588f33850516c/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:17ef139237dfced9da49fb7f2232c86ca9421f666d78c264c7ffca6601d154c3", size = 409611, upload-time = "2025-10-14T15:06:05.809Z" }, - { url = "https://files.pythonhosted.org/packages/1e/c7/5420d1943c8e3ce1a21c0a9330bcf7edafb6aa65d26b21dbb3267c9e8112/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:672b8adf25b1a0d35c96b5888b7b18699d27d4194bac8beeae75be4b7a3fc9b2", size = 396889, upload-time = "2025-10-14T15:06:07.035Z" }, - { url = "https://files.pythonhosted.org/packages/0c/e5/0072cef3804ce8d3aaddbfe7788aadff6b3d3f98a286fdbee9fd74ca59a7/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77a13aea58bc2b90173bc69f2a90de8e282648939a00a602e1dc4ee23e26b66d", size = 451616, upload-time = "2025-10-14T15:06:08.072Z" }, - { url = "https://files.pythonhosted.org/packages/83/4e/b87b71cbdfad81ad7e83358b3e447fedd281b880a03d64a760fe0a11fc2e/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b495de0bb386df6a12b18335a0285dda90260f51bdb505503c02bcd1ce27a8b", size = 458413, upload-time = "2025-10-14T15:06:09.209Z" }, - { url = "https://files.pythonhosted.org/packages/d3/8e/e500f8b0b77be4ff753ac94dc06b33d8f0d839377fee1b78e8c8d8f031bf/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88", size = 408250, upload-time = "2025-10-14T15:06:10.264Z" }, - { url = "https://files.pythonhosted.org/packages/bd/95/615e72cd27b85b61eec764a5ca51bd94d40b5adea5ff47567d9ebc4d275a/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336", size = 396117, upload-time = "2025-10-14T15:06:11.28Z" }, - { url = "https://files.pythonhosted.org/packages/c9/81/e7fe958ce8a7fb5c73cc9fb07f5aeaf755e6aa72498c57d760af760c91f8/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24", size = 450493, upload-time = "2025-10-14T15:06:12.321Z" }, - { url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546, upload-time = "2025-10-14T15:06:13.372Z" }, -] - -[[package]] -name = "wrapt" -version = "2.1.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2e/64/925f213fdcbb9baeb1530449ac71a4d57fc361c053d06bf78d0c5c7cd80c/wrapt-2.1.2.tar.gz", hash = "sha256:3996a67eecc2c68fd47b4e3c564405a5777367adfd9b8abb58387b63ee83b21e", size = 81678, upload-time = "2026-03-06T02:53:25.134Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/da/d2/387594fb592d027366645f3d7cc9b4d7ca7be93845fbaba6d835a912ef3c/wrapt-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4b7a86d99a14f76facb269dc148590c01aaf47584071809a70da30555228158c", size = 60669, upload-time = "2026-03-06T02:52:40.671Z" }, - { url = "https://files.pythonhosted.org/packages/c9/18/3f373935bc5509e7ac444c8026a56762e50c1183e7061797437ca96c12ce/wrapt-2.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a819e39017f95bf7aede768f75915635aa8f671f2993c036991b8d3bfe8dbb6f", size = 61603, upload-time = "2026-03-06T02:54:21.032Z" }, - { url = "https://files.pythonhosted.org/packages/c2/7a/32758ca2853b07a887a4574b74e28843919103194bb47001a304e24af62f/wrapt-2.1.2-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5681123e60aed0e64c7d44f72bbf8b4ce45f79d81467e2c4c728629f5baf06eb", size = 113632, upload-time = "2026-03-06T02:53:54.121Z" }, - { url = "https://files.pythonhosted.org/packages/1d/d5/eeaa38f670d462e97d978b3b0d9ce06d5b91e54bebac6fbed867809216e7/wrapt-2.1.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b8b28e97a44d21836259739ae76284e180b18abbb4dcfdff07a415cf1016c3e", size = 115644, upload-time = "2026-03-06T02:54:53.33Z" }, - { url = "https://files.pythonhosted.org/packages/e3/09/2a41506cb17affb0bdf9d5e2129c8c19e192b388c4c01d05e1b14db23c00/wrapt-2.1.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cef91c95a50596fcdc31397eb6955476f82ae8a3f5a8eabdc13611b60ee380ba", size = 112016, upload-time = "2026-03-06T02:54:43.274Z" }, - { url = "https://files.pythonhosted.org/packages/64/15/0e6c3f5e87caadc43db279724ee36979246d5194fa32fed489c73643ba59/wrapt-2.1.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dad63212b168de8569b1c512f4eac4b57f2c6934b30df32d6ee9534a79f1493f", size = 114823, upload-time = "2026-03-06T02:54:29.392Z" }, - { url = "https://files.pythonhosted.org/packages/56/b2/0ad17c8248f4e57bedf44938c26ec3ee194715f812d2dbbd9d7ff4be6c06/wrapt-2.1.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d307aa6888d5efab2c1cde09843d48c843990be13069003184b67d426d145394", size = 111244, upload-time = "2026-03-06T02:54:02.149Z" }, - { url = "https://files.pythonhosted.org/packages/ff/04/bcdba98c26f2c6522c7c09a726d5d9229120163493620205b2f76bd13c01/wrapt-2.1.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c87cf3f0c85e27b3ac7d9ad95da166bf8739ca215a8b171e8404a2d739897a45", size = 113307, upload-time = "2026-03-06T02:54:12.428Z" }, - { url = "https://files.pythonhosted.org/packages/0e/1b/5e2883c6bc14143924e465a6fc5a92d09eeabe35310842a481fb0581f832/wrapt-2.1.2-cp310-cp310-win32.whl", hash = "sha256:d1c5fea4f9fe3762e2b905fdd67df51e4be7a73b7674957af2d2ade71a5c075d", size = 57986, upload-time = "2026-03-06T02:54:26.823Z" }, - { url = "https://files.pythonhosted.org/packages/42/5a/4efc997bccadd3af5749c250b49412793bc41e13a83a486b2b54a33e240c/wrapt-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:d8f7740e1af13dff2684e4d56fe604a7e04d6c94e737a60568d8d4238b9a0c71", size = 60336, upload-time = "2026-03-06T02:54:18Z" }, - { url = "https://files.pythonhosted.org/packages/c1/f5/a2bb833e20181b937e87c242645ed5d5aa9c373006b0467bfe1a35c727d0/wrapt-2.1.2-cp310-cp310-win_arm64.whl", hash = "sha256:1c6cc827c00dc839350155f316f1f8b4b0c370f52b6a19e782e2bda89600c7dc", size = 58757, upload-time = "2026-03-06T02:53:51.545Z" }, - { url = "https://files.pythonhosted.org/packages/c7/81/60c4471fce95afa5922ca09b88a25f03c93343f759aae0f31fb4412a85c7/wrapt-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:96159a0ee2b0277d44201c3b5be479a9979cf154e8c82fa5df49586a8e7679bb", size = 60666, upload-time = "2026-03-06T02:52:58.934Z" }, - { url = "https://files.pythonhosted.org/packages/6b/be/80e80e39e7cb90b006a0eaf11c73ac3a62bbfb3068469aec15cc0bc795de/wrapt-2.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:98ba61833a77b747901e9012072f038795de7fc77849f1faa965464f3f87ff2d", size = 61601, upload-time = "2026-03-06T02:53:00.487Z" }, - { url = "https://files.pythonhosted.org/packages/b0/be/d7c88cd9293c859fc74b232abdc65a229bb953997995d6912fc85af18323/wrapt-2.1.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:767c0dbbe76cae2a60dd2b235ac0c87c9cccf4898aef8062e57bead46b5f6894", size = 114057, upload-time = "2026-03-06T02:52:44.08Z" }, - { url = "https://files.pythonhosted.org/packages/ea/25/36c04602831a4d685d45a93b3abea61eca7fe35dab6c842d6f5d570ef94a/wrapt-2.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c691a6bc752c0cc4711cc0c00896fcd0f116abc253609ef64ef930032821842", size = 116099, upload-time = "2026-03-06T02:54:56.74Z" }, - { url = "https://files.pythonhosted.org/packages/5c/4e/98a6eb417ef551dc277bec1253d5246b25003cf36fdf3913b65cb7657a56/wrapt-2.1.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f3b7d73012ea75aee5844de58c88f44cf62d0d62711e39da5a82824a7c4626a8", size = 112457, upload-time = "2026-03-06T02:53:52.842Z" }, - { url = "https://files.pythonhosted.org/packages/cb/a6/a6f7186a5297cad8ec53fd7578533b28f795fdf5372368c74bd7e6e9841c/wrapt-2.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:577dff354e7acd9d411eaf4bfe76b724c89c89c8fc9b7e127ee28c5f7bcb25b6", size = 115351, upload-time = "2026-03-06T02:53:32.684Z" }, - { url = "https://files.pythonhosted.org/packages/97/6f/06e66189e721dbebd5cf20e138acc4d1150288ce118462f2fcbff92d38db/wrapt-2.1.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3d7b6fd105f8b24e5bd23ccf41cb1d1099796524bcc6f7fbb8fe576c44befbc9", size = 111748, upload-time = "2026-03-06T02:53:08.455Z" }, - { url = "https://files.pythonhosted.org/packages/ef/43/4808b86f499a51370fbdbdfa6cb91e9b9169e762716456471b619fca7a70/wrapt-2.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:866abdbf4612e0b34764922ef8b1c5668867610a718d3053d59e24a5e5fcfc15", size = 113783, upload-time = "2026-03-06T02:53:02.02Z" }, - { url = "https://files.pythonhosted.org/packages/91/2c/a3f28b8fa7ac2cefa01cfcaca3471f9b0460608d012b693998cd61ef43df/wrapt-2.1.2-cp311-cp311-win32.whl", hash = "sha256:5a0a0a3a882393095573344075189eb2d566e0fd205a2b6414e9997b1b800a8b", size = 57977, upload-time = "2026-03-06T02:53:27.844Z" }, - { url = "https://files.pythonhosted.org/packages/3f/c3/2b1c7bd07a27b1db885a2fab469b707bdd35bddf30a113b4917a7e2139d2/wrapt-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:64a07a71d2730ba56f11d1a4b91f7817dc79bc134c11516b75d1921a7c6fcda1", size = 60336, upload-time = "2026-03-06T02:54:28.104Z" }, - { url = "https://files.pythonhosted.org/packages/ec/5c/76ece7b401b088daa6503d6264dd80f9a727df3e6042802de9a223084ea2/wrapt-2.1.2-cp311-cp311-win_arm64.whl", hash = "sha256:b89f095fe98bc12107f82a9f7d570dc83a0870291aeb6b1d7a7d35575f55d98a", size = 58756, upload-time = "2026-03-06T02:53:16.319Z" }, - { url = "https://files.pythonhosted.org/packages/4c/b6/1db817582c49c7fcbb7df6809d0f515af29d7c2fbf57eb44c36e98fb1492/wrapt-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ff2aad9c4cda28a8f0653fc2d487596458c2a3f475e56ba02909e950a9efa6a9", size = 61255, upload-time = "2026-03-06T02:52:45.663Z" }, - { url = "https://files.pythonhosted.org/packages/a2/16/9b02a6b99c09227c93cd4b73acc3678114154ec38da53043c0ddc1fba0dc/wrapt-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6433ea84e1cfacf32021d2a4ee909554ade7fd392caa6f7c13f1f4bf7b8e8748", size = 61848, upload-time = "2026-03-06T02:53:48.728Z" }, - { url = "https://files.pythonhosted.org/packages/af/aa/ead46a88f9ec3a432a4832dfedb84092fc35af2d0ba40cd04aea3889f247/wrapt-2.1.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c20b757c268d30d6215916a5fa8461048d023865d888e437fab451139cad6c8e", size = 121433, upload-time = "2026-03-06T02:54:40.328Z" }, - { url = "https://files.pythonhosted.org/packages/3a/9f/742c7c7cdf58b59085a1ee4b6c37b013f66ac33673a7ef4aaed5e992bc33/wrapt-2.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79847b83eb38e70d93dc392c7c5b587efe65b3e7afcc167aa8abd5d60e8761c8", size = 123013, upload-time = "2026-03-06T02:53:26.58Z" }, - { url = "https://files.pythonhosted.org/packages/e8/44/2c3dd45d53236b7ed7c646fcf212251dc19e48e599debd3926b52310fafb/wrapt-2.1.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f8fba1bae256186a83d1875b2b1f4e2d1242e8fac0f58ec0d7e41b26967b965c", size = 117326, upload-time = "2026-03-06T02:53:11.547Z" }, - { url = "https://files.pythonhosted.org/packages/74/e2/b17d66abc26bd96f89dec0ecd0ef03da4a1286e6ff793839ec431b9fae57/wrapt-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e3d3b35eedcf5f7d022291ecd7533321c4775f7b9cd0050a31a68499ba45757c", size = 121444, upload-time = "2026-03-06T02:54:09.5Z" }, - { url = "https://files.pythonhosted.org/packages/3c/62/e2977843fdf9f03daf1586a0ff49060b1b2fc7ff85a7ea82b6217c1ae36e/wrapt-2.1.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:6f2c5390460de57fa9582bc8a1b7a6c86e1a41dfad74c5225fc07044c15cc8d1", size = 116237, upload-time = "2026-03-06T02:54:03.884Z" }, - { url = "https://files.pythonhosted.org/packages/88/dd/27fc67914e68d740bce512f11734aec08696e6b17641fef8867c00c949fc/wrapt-2.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7dfa9f2cf65d027b951d05c662cc99ee3bd01f6e4691ed39848a7a5fffc902b2", size = 120563, upload-time = "2026-03-06T02:53:20.412Z" }, - { url = "https://files.pythonhosted.org/packages/ec/9f/b750b3692ed2ef4705cb305bd68858e73010492b80e43d2a4faa5573cbe7/wrapt-2.1.2-cp312-cp312-win32.whl", hash = "sha256:eba8155747eb2cae4a0b913d9ebd12a1db4d860fc4c829d7578c7b989bd3f2f0", size = 58198, upload-time = "2026-03-06T02:53:37.732Z" }, - { url = "https://files.pythonhosted.org/packages/8e/b2/feecfe29f28483d888d76a48f03c4c4d8afea944dbee2b0cd3380f9df032/wrapt-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1c51c738d7d9faa0b3601708e7e2eda9bf779e1b601dce6c77411f2a1b324a63", size = 60441, upload-time = "2026-03-06T02:52:47.138Z" }, - { url = "https://files.pythonhosted.org/packages/44/e1/e328f605d6e208547ea9fd120804fcdec68536ac748987a68c47c606eea8/wrapt-2.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:c8e46ae8e4032792eb2f677dbd0d557170a8e5524d22acc55199f43efedd39bf", size = 58836, upload-time = "2026-03-06T02:53:22.053Z" }, - { url = "https://files.pythonhosted.org/packages/4c/7a/d936840735c828b38d26a854e85d5338894cda544cb7a85a9d5b8b9c4df7/wrapt-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787fd6f4d67befa6fe2abdffcbd3de2d82dfc6fb8a6d850407c53332709d030b", size = 61259, upload-time = "2026-03-06T02:53:41.922Z" }, - { url = "https://files.pythonhosted.org/packages/5e/88/9a9b9a90ac8ca11c2fdb6a286cb3a1fc7dd774c00ed70929a6434f6bc634/wrapt-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4bdf26e03e6d0da3f0e9422fd36bcebf7bc0eeb55fdf9c727a09abc6b9fe472e", size = 61851, upload-time = "2026-03-06T02:52:48.672Z" }, - { url = "https://files.pythonhosted.org/packages/03/a9/5b7d6a16fd6533fed2756900fc8fc923f678179aea62ada6d65c92718c00/wrapt-2.1.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bbac24d879aa22998e87f6b3f481a5216311e7d53c7db87f189a7a0266dafffb", size = 121446, upload-time = "2026-03-06T02:54:14.013Z" }, - { url = "https://files.pythonhosted.org/packages/45/bb/34c443690c847835cfe9f892be78c533d4f32366ad2888972c094a897e39/wrapt-2.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:16997dfb9d67addc2e3f41b62a104341e80cac52f91110dece393923c0ebd5ca", size = 123056, upload-time = "2026-03-06T02:54:10.829Z" }, - { url = "https://files.pythonhosted.org/packages/93/b9/ff205f391cb708f67f41ea148545f2b53ff543a7ac293b30d178af4d2271/wrapt-2.1.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:162e4e2ba7542da9027821cb6e7c5e068d64f9a10b5f15512ea28e954893a267", size = 117359, upload-time = "2026-03-06T02:53:03.623Z" }, - { url = "https://files.pythonhosted.org/packages/1f/3d/1ea04d7747825119c3c9a5e0874a40b33594ada92e5649347c457d982805/wrapt-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f29c827a8d9936ac320746747a016c4bc66ef639f5cd0d32df24f5eacbf9c69f", size = 121479, upload-time = "2026-03-06T02:53:45.844Z" }, - { url = "https://files.pythonhosted.org/packages/78/cc/ee3a011920c7a023b25e8df26f306b2484a531ab84ca5c96260a73de76c0/wrapt-2.1.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:a9dd9813825f7ecb018c17fd147a01845eb330254dff86d3b5816f20f4d6aaf8", size = 116271, upload-time = "2026-03-06T02:54:46.356Z" }, - { url = "https://files.pythonhosted.org/packages/98/fd/e5ff7ded41b76d802cf1191288473e850d24ba2e39a6ec540f21ae3b57cb/wrapt-2.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6f8dbdd3719e534860d6a78526aafc220e0241f981367018c2875178cf83a413", size = 120573, upload-time = "2026-03-06T02:52:50.163Z" }, - { url = "https://files.pythonhosted.org/packages/47/c5/242cae3b5b080cd09bacef0591691ba1879739050cc7c801ff35c8886b66/wrapt-2.1.2-cp313-cp313-win32.whl", hash = "sha256:5c35b5d82b16a3bc6e0a04349b606a0582bc29f573786aebe98e0c159bc48db6", size = 58205, upload-time = "2026-03-06T02:53:47.494Z" }, - { url = "https://files.pythonhosted.org/packages/12/69/c358c61e7a50f290958809b3c61ebe8b3838ea3e070d7aac9814f95a0528/wrapt-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:f8bc1c264d8d1cf5b3560a87bbdd31131573eb25f9f9447bb6252b8d4c44a3a1", size = 60452, upload-time = "2026-03-06T02:53:30.038Z" }, - { url = "https://files.pythonhosted.org/packages/8e/66/c8a6fcfe321295fd8c0ab1bd685b5a01462a9b3aa2f597254462fc2bc975/wrapt-2.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:3beb22f674550d5634642c645aba4c72a2c66fb185ae1aebe1e955fae5a13baf", size = 58842, upload-time = "2026-03-06T02:52:52.114Z" }, - { url = "https://files.pythonhosted.org/packages/da/55/9c7052c349106e0b3f17ae8db4b23a691a963c334de7f9dbd60f8f74a831/wrapt-2.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0fc04bc8664a8bc4c8e00b37b5355cffca2535209fba1abb09ae2b7c76ddf82b", size = 63075, upload-time = "2026-03-06T02:53:19.108Z" }, - { url = "https://files.pythonhosted.org/packages/09/a8/ce7b4006f7218248dd71b7b2b732d0710845a0e49213b18faef64811ffef/wrapt-2.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a9b9d50c9af998875a1482a038eb05755dfd6fe303a313f6a940bb53a83c3f18", size = 63719, upload-time = "2026-03-06T02:54:33.452Z" }, - { url = "https://files.pythonhosted.org/packages/e4/e5/2ca472e80b9e2b7a17f106bb8f9df1db11e62101652ce210f66935c6af67/wrapt-2.1.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2d3ff4f0024dd224290c0eabf0240f1bfc1f26363431505fb1b0283d3b08f11d", size = 152643, upload-time = "2026-03-06T02:52:42.721Z" }, - { url = "https://files.pythonhosted.org/packages/36/42/30f0f2cefca9d9cbf6835f544d825064570203c3e70aa873d8ae12e23791/wrapt-2.1.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3278c471f4468ad544a691b31bb856374fbdefb7fee1a152153e64019379f015", size = 158805, upload-time = "2026-03-06T02:54:25.441Z" }, - { url = "https://files.pythonhosted.org/packages/bb/67/d08672f801f604889dcf58f1a0b424fe3808860ede9e03affc1876b295af/wrapt-2.1.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a8914c754d3134a3032601c6984db1c576e6abaf3fc68094bb8ab1379d75ff92", size = 145990, upload-time = "2026-03-06T02:53:57.456Z" }, - { url = "https://files.pythonhosted.org/packages/68/a7/fd371b02e73babec1de6ade596e8cd9691051058cfdadbfd62a5898f3295/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ff95d4264e55839be37bafe1536db2ab2de19da6b65f9244f01f332b5286cfbf", size = 155670, upload-time = "2026-03-06T02:54:55.309Z" }, - { url = "https://files.pythonhosted.org/packages/86/2d/9fe0095dfdb621009f40117dcebf41d7396c2c22dca6eac779f4c007b86c/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:76405518ca4e1b76fbb1b9f686cff93aebae03920cc55ceeec48ff9f719c5f67", size = 144357, upload-time = "2026-03-06T02:54:24.092Z" }, - { url = "https://files.pythonhosted.org/packages/0e/b6/ec7b4a254abbe4cde9fa15c5d2cca4518f6b07d0f1b77d4ee9655e30280e/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c0be8b5a74c5824e9359b53e7e58bef71a729bacc82e16587db1c4ebc91f7c5a", size = 150269, upload-time = "2026-03-06T02:53:31.268Z" }, - { url = "https://files.pythonhosted.org/packages/6e/6b/2fabe8ebf148f4ee3c782aae86a795cc68ffe7d432ef550f234025ce0cfa/wrapt-2.1.2-cp313-cp313t-win32.whl", hash = "sha256:f01277d9a5fc1862f26f7626da9cf443bebc0abd2f303f41c5e995b15887dabd", size = 59894, upload-time = "2026-03-06T02:54:15.391Z" }, - { url = "https://files.pythonhosted.org/packages/ca/fb/9ba66fc2dedc936de5f8073c0217b5d4484e966d87723415cc8262c5d9c2/wrapt-2.1.2-cp313-cp313t-win_amd64.whl", hash = "sha256:84ce8f1c2104d2f6daa912b1b5b039f331febfeee74f8042ad4e04992bd95c8f", size = 63197, upload-time = "2026-03-06T02:54:41.943Z" }, - { url = "https://files.pythonhosted.org/packages/c0/1c/012d7423c95d0e337117723eb8ecf73c622ce15a97847e84cf3f8f26cd7e/wrapt-2.1.2-cp313-cp313t-win_arm64.whl", hash = "sha256:a93cd767e37faeddbe07d8fc4212d5cba660af59bdb0f6372c93faaa13e6e679", size = 60363, upload-time = "2026-03-06T02:54:48.093Z" }, - { url = "https://files.pythonhosted.org/packages/39/25/e7ea0b417db02bb796182a5316398a75792cd9a22528783d868755e1f669/wrapt-2.1.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1370e516598854e5b4366e09ce81e08bfe94d42b0fd569b88ec46cc56d9164a9", size = 61418, upload-time = "2026-03-06T02:53:55.706Z" }, - { url = "https://files.pythonhosted.org/packages/ec/0f/fa539e2f6a770249907757eaeb9a5ff4deb41c026f8466c1c6d799088a9b/wrapt-2.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6de1a3851c27e0bd6a04ca993ea6f80fc53e6c742ee1601f486c08e9f9b900a9", size = 61914, upload-time = "2026-03-06T02:52:53.37Z" }, - { url = "https://files.pythonhosted.org/packages/53/37/02af1867f5b1441aaeda9c82deed061b7cd1372572ddcd717f6df90b5e93/wrapt-2.1.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:de9f1a2bbc5ac7f6012ec24525bdd444765a2ff64b5985ac6e0692144838542e", size = 120417, upload-time = "2026-03-06T02:54:30.74Z" }, - { url = "https://files.pythonhosted.org/packages/c3/b7/0138a6238c8ba7476c77cf786a807f871672b37f37a422970342308276e7/wrapt-2.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:970d57ed83fa040d8b20c52fe74a6ae7e3775ae8cff5efd6a81e06b19078484c", size = 122797, upload-time = "2026-03-06T02:54:51.539Z" }, - { url = "https://files.pythonhosted.org/packages/e1/ad/819ae558036d6a15b7ed290d5b14e209ca795dd4da9c58e50c067d5927b0/wrapt-2.1.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3969c56e4563c375861c8df14fa55146e81ac11c8db49ea6fb7f2ba58bc1ff9a", size = 117350, upload-time = "2026-03-06T02:54:37.651Z" }, - { url = "https://files.pythonhosted.org/packages/8b/2d/afc18dc57a4600a6e594f77a9ae09db54f55ba455440a54886694a84c71b/wrapt-2.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:57d7c0c980abdc5f1d98b11a2aa3bb159790add80258c717fa49a99921456d90", size = 121223, upload-time = "2026-03-06T02:54:35.221Z" }, - { url = "https://files.pythonhosted.org/packages/b9/5b/5ec189b22205697bc56eb3b62aed87a1e0423e9c8285d0781c7a83170d15/wrapt-2.1.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:776867878e83130c7a04237010463372e877c1c994d449ca6aaafeab6aab2586", size = 116287, upload-time = "2026-03-06T02:54:19.654Z" }, - { url = "https://files.pythonhosted.org/packages/f7/2d/f84939a7c9b5e6cdd8a8d0f6a26cabf36a0f7e468b967720e8b0cd2bdf69/wrapt-2.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:fab036efe5464ec3291411fabb80a7a39e2dd80bae9bcbeeca5087fdfa891e19", size = 119593, upload-time = "2026-03-06T02:54:16.697Z" }, - { url = "https://files.pythonhosted.org/packages/0b/fe/ccd22a1263159c4ac811ab9374c061bcb4a702773f6e06e38de5f81a1bdc/wrapt-2.1.2-cp314-cp314-win32.whl", hash = "sha256:e6ed62c82ddf58d001096ae84ce7f833db97ae2263bff31c9b336ba8cfe3f508", size = 58631, upload-time = "2026-03-06T02:53:06.498Z" }, - { url = "https://files.pythonhosted.org/packages/65/0a/6bd83be7bff2e7efaac7b4ac9748da9d75a34634bbbbc8ad077d527146df/wrapt-2.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:467e7c76315390331c67073073d00662015bb730c566820c9ca9b54e4d67fd04", size = 60875, upload-time = "2026-03-06T02:53:50.252Z" }, - { url = "https://files.pythonhosted.org/packages/6c/c0/0b3056397fe02ff80e5a5d72d627c11eb885d1ca78e71b1a5c1e8c7d45de/wrapt-2.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:da1f00a557c66225d53b095a97eace0fc5349e3bfda28fa34ffae238978ee575", size = 59164, upload-time = "2026-03-06T02:53:59.128Z" }, - { url = "https://files.pythonhosted.org/packages/71/ed/5d89c798741993b2371396eb9d4634f009ff1ad8a6c78d366fe2883ea7a6/wrapt-2.1.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:62503ffbc2d3a69891cf29beeaccdb4d5e0a126e2b6a851688d4777e01428dbb", size = 63163, upload-time = "2026-03-06T02:52:54.873Z" }, - { url = "https://files.pythonhosted.org/packages/c6/8c/05d277d182bf36b0a13d6bd393ed1dec3468a25b59d01fba2dd70fe4d6ae/wrapt-2.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c7e6cd120ef837d5b6f860a6ea3745f8763805c418bb2f12eeb1fa6e25f22d22", size = 63723, upload-time = "2026-03-06T02:52:56.374Z" }, - { url = "https://files.pythonhosted.org/packages/f4/27/6c51ec1eff4413c57e72d6106bb8dec6f0c7cdba6503d78f0fa98767bcc9/wrapt-2.1.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3769a77df8e756d65fbc050333f423c01ae012b4f6731aaf70cf2bef61b34596", size = 152652, upload-time = "2026-03-06T02:53:23.79Z" }, - { url = "https://files.pythonhosted.org/packages/db/4c/d7dd662d6963fc7335bfe29d512b02b71cdfa23eeca7ab3ac74a67505deb/wrapt-2.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a76d61a2e851996150ba0f80582dd92a870643fa481f3b3846f229de88caf044", size = 158807, upload-time = "2026-03-06T02:53:35.742Z" }, - { url = "https://files.pythonhosted.org/packages/b4/4d/1e5eea1a78d539d346765727422976676615814029522c76b87a95f6bcdd/wrapt-2.1.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6f97edc9842cf215312b75fe737ee7c8adda75a89979f8e11558dfff6343cc4b", size = 146061, upload-time = "2026-03-06T02:52:57.574Z" }, - { url = "https://files.pythonhosted.org/packages/89/bc/62cabea7695cd12a288023251eeefdcb8465056ddaab6227cb78a2de005b/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4006c351de6d5007aa33a551f600404ba44228a89e833d2fadc5caa5de8edfbf", size = 155667, upload-time = "2026-03-06T02:53:39.422Z" }, - { url = "https://files.pythonhosted.org/packages/e9/99/6f2888cd68588f24df3a76572c69c2de28287acb9e1972bf0c83ce97dbc1/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a9372fc3639a878c8e7d87e1556fa209091b0a66e912c611e3f833e2c4202be2", size = 144392, upload-time = "2026-03-06T02:54:22.41Z" }, - { url = "https://files.pythonhosted.org/packages/40/51/1dfc783a6c57971614c48e361a82ca3b6da9055879952587bc99fe1a7171/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3144b027ff30cbd2fca07c0a87e67011adb717eb5f5bd8496325c17e454257a3", size = 150296, upload-time = "2026-03-06T02:54:07.848Z" }, - { url = "https://files.pythonhosted.org/packages/6c/38/cbb8b933a0201076c1f64fc42883b0023002bdc14a4964219154e6ff3350/wrapt-2.1.2-cp314-cp314t-win32.whl", hash = "sha256:3b8d15e52e195813efe5db8cec156eebe339aaf84222f4f4f051a6c01f237ed7", size = 60539, upload-time = "2026-03-06T02:54:00.594Z" }, - { url = "https://files.pythonhosted.org/packages/82/dd/e5176e4b241c9f528402cebb238a36785a628179d7d8b71091154b3e4c9e/wrapt-2.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:08ffa54146a7559f5b8df4b289b46d963a8e74ed16ba3687f99896101a3990c5", size = 63969, upload-time = "2026-03-06T02:54:39Z" }, - { url = "https://files.pythonhosted.org/packages/5c/99/79f17046cf67e4a95b9987ea129632ba8bcec0bc81f3fb3d19bdb0bd60cd/wrapt-2.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:72aaa9d0d8e4ed0e2e98019cea47a21f823c9dd4b43c7b77bba6679ffcca6a00", size = 60554, upload-time = "2026-03-06T02:53:14.132Z" }, - { url = "https://files.pythonhosted.org/packages/1a/c7/8528ac2dfa2c1e6708f647df7ae144ead13f0a31146f43c7264b4942bf12/wrapt-2.1.2-py3-none-any.whl", hash = "sha256:b8fd6fa2b2c4e7621808f8c62e8317f4aae56e59721ad933bac5239d913cf0e8", size = 43993, upload-time = "2026-03-06T02:53:12.905Z" }, -] - -[[package]] -name = "wsproto" -version = "1.3.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "h11" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c7/79/12135bdf8b9c9367b8701c2c19a14c913c120b882d50b014ca0d38083c2c/wsproto-1.3.2.tar.gz", hash = "sha256:b86885dcf294e15204919950f666e06ffc6c7c114ca900b060d6e16293528294", size = 50116, upload-time = "2025-11-20T18:18:01.871Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl", hash = "sha256:61eea322cdf56e8cc904bd3ad7573359a242ba65688716b0710a5eb12beab584", size = 24405, upload-time = "2025-11-20T18:18:00.454Z" }, -] From 3dc62feac4098aeb7d0695d8d3098b2972a3e862 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Wed, 22 Apr 2026 18:26:57 -0700 Subject: [PATCH 10/16] delete actions --- .../actions/cli_deploy_reflex_app/action.yml | 110 ------------------ .../.github/pull_request_template.md | 5 - .../workflows/deploy_reflex_app_to_prod.yml | 68 ----------- .../e2e_cli_deploy_reflex_web_to_dev.yml | 63 ---------- .../.github/workflows/pre-commit.yml | 22 ---- .../.github/workflows/unit_tests.yml | 73 ------------ 6 files changed, 341 deletions(-) delete mode 100644 packages/reflex-hosting-cli/.github/actions/cli_deploy_reflex_app/action.yml delete mode 100644 packages/reflex-hosting-cli/.github/pull_request_template.md delete mode 100644 packages/reflex-hosting-cli/.github/workflows/deploy_reflex_app_to_prod.yml delete mode 100644 packages/reflex-hosting-cli/.github/workflows/e2e_cli_deploy_reflex_web_to_dev.yml delete mode 100644 packages/reflex-hosting-cli/.github/workflows/pre-commit.yml delete mode 100644 packages/reflex-hosting-cli/.github/workflows/unit_tests.yml diff --git a/packages/reflex-hosting-cli/.github/actions/cli_deploy_reflex_app/action.yml b/packages/reflex-hosting-cli/.github/actions/cli_deploy_reflex_app/action.yml deleted file mode 100644 index 781595875f1..00000000000 --- a/packages/reflex-hosting-cli/.github/actions/cli_deploy_reflex_app/action.yml +++ /dev/null @@ -1,110 +0,0 @@ -name: Deploy-Reflex-Apps-using-Hosting-CLI -description: 'Deploy Reflex apps using Hosting CLI' - -inputs: - reflex-app-repo: - description: 'repository of the target app to deploy' - required: true - reflex-app-ref: - description: 'reflex app ref to deploy. e.g. git sha or `main`' - required: true - default: 'main' - reflex-pin: - description: 'Reflex pin (@, ==, all ok)' - cp-web-url: - description: 'Control Plane web URL' - cp-backend-url: - description: 'Control Plane backend URL' - cp-env: - description: 'Control Plane environment: dev/staging/prod' - deployment-key: - description: 'Instance key used for deploy' - required: true - user-email: - description: 'Email of the hosting service user deploying the app' - required: true - user-password: - description: 'Password of the hosting service user deploying the app' - required: true - openai-api-key: - description: 'If included, the openai api key will be used' - with-local-db: - description: 'Whether to upload the local db' - default: 'false' - -runs: - using: 'composite' - steps: - - name: Checkout the target app repo - uses: actions/checkout@v3 - with: - repository: ${{ inputs.reflex-app-repo }} - path: reflex-app - ref: ${{ inputs.reflex-app-ref }} - - name: Setup Python - uses: actions/setup-python@v3 - with: - python-version: 3.11 - - name: Install requirements - shell: bash - run: | - python -m venv venv - source venv/bin/activate - pip install --no-color --no-cache-dir -q . - if [ -n "${{ inputs.reflex-pin }}" ]; then - # override reflex pin - sed -i '/^reflex[>=]/d' reflex-app/requirements.txt - echo "" >> reflex-app/requirements.txt - echo "reflex${{ inputs.reflex-pin }}" >> reflex-app/requirements.txt - fi - # Note, below sed command only works on ubuntu, not MacOS - pip install "reflex${{ inputs.reflex-pin }}" --no-color --no-cache-dir -q -r reflex-app/requirements.txt - - name: Deploy bot log in to hosting service - shell: bash - env: - REFLEX_CLOUD_URL: ${{ inputs.cp-web-url }} - REFLEX_CLOUD_BACKEND_URL: ${{ inputs.cp-backend-url }} - run: | - source venv/bin/activate - access_token=$(curl "${REFLEX_CLOUD_BACKEND_URL}/authenticate/password" -u "${{ inputs.user-email }}:${{ inputs.user-password }}" -X POST) - if [ -z "$access_token" ]; then - echo "::error::Test user unable to log in" && exit 1 - fi - mkdir -p ~/.local/share/reflex/ - # Bypass the login process by manually stashing a token in the hosting config file. - echo "{\"access_token\": $access_token}" > ~/.local/share/reflex/hosting_v0.json - reflex login - - name: Deploy reflex-app using CLI - env: - REFLEX_CLOUD_URL: ${{ inputs.cp-web-url }} - REFLEX_CLOUD_BACKEND_URL: ${{ inputs.cp-backend-url }} - shell: bash - run: | - source venv/bin/activate - cd reflex-app/ - export TELEMETRY_ENABLED=false - reflex init - if [ -n "${{ inputs.openai-api-key }}" ]; then - export OPENAI_API_KEY=${{ inputs.openai-api-key }} - add_env_flags="--env OPENAI_API_KEY=${{ inputs.openai-api-key }}" - fi - if [ "${{ inputs.with-local-db }}" == 'true' ]; then - add_db_flags="--upload-db-file" - # Here we also need to do migration on the local sqlite db - reflex db init - reflex db migrate - fi - reflex deploy --no-interactive -k ${{ inputs.deployment-key }} -r sjc ${add_env_flags} ${add_db_flags} - listed="$(reflex deployments list | grep ${{ inputs.deployment-key }})" - if [[ -z "$listed" ]]; then - echo "::error::deploy was not successful" && exit 1 - fi - - name: Check deployed reflex app is responsive - env: - REFLEX_CLOUD_URL: ${{ inputs.cp-web-url }} - REFLEX_CLOUD_BACKEND_URL: ${{ inputs.cp-backend-url }} - CP_ENV: ${{ inputs.cp-env }} - shell: bash - run: | - source venv/bin/activate - python ./scripts/check_site_responsive.py --env ${CP_ENV} --deployment-key ${{ inputs.deployment-key }} --frontend-retries 30 --backend-retries 90 diff --git a/packages/reflex-hosting-cli/.github/pull_request_template.md b/packages/reflex-hosting-cli/.github/pull_request_template.md deleted file mode 100644 index d0c2a63641d..00000000000 --- a/packages/reflex-hosting-cli/.github/pull_request_template.md +++ /dev/null @@ -1,5 +0,0 @@ -## Summary -- TBD - -## Tests -- [ ] CI green diff --git a/packages/reflex-hosting-cli/.github/workflows/deploy_reflex_app_to_prod.yml b/packages/reflex-hosting-cli/.github/workflows/deploy_reflex_app_to_prod.yml deleted file mode 100644 index 87197bf29c3..00000000000 --- a/packages/reflex-hosting-cli/.github/workflows/deploy_reflex_app_to_prod.yml +++ /dev/null @@ -1,68 +0,0 @@ -name: deploy-reflex-app-to-prod -on: - workflow_dispatch: - inputs: - reflex-app-repo: - description: 'reflex app github repo to deploy. e.g. `reflex-dev/reflex-chat`' - required: true - reflex-app-ref: - description: 'reflex app ref to deploy. e.g. git sha or `main`' - reflex-pin: - description: 'Reflex pin (@, ==, all ok)' - deployment-key: - description: 'The key for the instance to be deployed. Must be a new app or already owned by the CI user.' - required: true - repository_dispatch: - -jobs: - deploy-reflex-app-to-prod: - runs-on: ubuntu-latest - timeout-minutes: 20 - env: - TELEMETRY_ENABLED: false - DEPLOYMENT_KEY: pcweb-${{ github.run_id }}-${{ github.run_attempt }} - steps: - - name: Check out reflex-hosting-cli repo - uses: actions/checkout@v3 - with: - repository: reflex-dev/reflex-hosting-cli - - name: Set reflex app envs - id: set-reflex-app-envs - shell: bash - run: | - if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then - echo "reflex-app-repo=${{ inputs.reflex-app-repo }}" >> "$GITHUB_OUTPUT" - echo "reflex-app-ref=${{ inputs.reflex-app-ref }}" >> "$GITHUB_OUTPUT" - echo "deployment-key=${{ inputs.deployment-key }}" >> "$GITHUB_OUTPUT" - else - echo "reflex-app-repo=${{ github.event.client_payload.repo }}" >> "$GITHUB_OUTPUT" - echo "reflex-app-ref=${{ github.event.client_payload.sha }}" >> "$GITHUB_OUTPUT" - # if the deployment-key is provided in the client payload, use it - # otherwise, use the default deployment key which is the repo name - if [ -z "${{ github.event.client_payload.deployment-key }}" ]; then - owner_and_repo="${{ github.event.client_payload.repo }}" - echo "deployment-key=${owner_and_repo##*/}" >> "$GITHUB_OUTPUT" - else - echo "deployment-key=${{ github.event.client_payload.deployment-key }}" >> "$GITHUB_OUTPUT" - fi - if [ 'true' == "${{ github.event.client_payload.with-openai }}" ]; then - echo "openai-api-key=${{ secrets.OPENAI_API_KEY }}" >> "$GITHUB_OUTPUT" - fi - if [ 'true' == "${{ github.event.client_payload.with-local-db }}" ]; then - echo "with-local-db='true'" >> "$GITHUB_OUTPUT" - fi - fi - - name: Deploy reflex app - uses: ./.github/actions/cli_deploy_reflex_app - with: - reflex-app-repo: ${{ steps.set-reflex-app-envs.outputs.reflex-app-repo }} - reflex-app-ref: ${{ steps.set-reflex-app-envs.outputs.reflex-app-ref }} - deployment-key: ${{ steps.set-reflex-app-envs.outputs.deployment-key }} - openai-api-key: ${{ steps.set-reflex-app-envs.outputs.openai-api-key }} - with-local-db: ${{ steps.set-reflex-app-envs.outputs.with-local-db }} - reflex-pin: ${{ inputs.reflex-pin }} - cp-web-url: ${{ secrets.PROD_CP_WEB_URL }} - cp-backend-url: ${{ secrets.PROD_CP_BACKEND_URL }} - cp-env: 'prod' - user-email: ${{ secrets.PROD_CI_TEST_USER_EMAIL }} - user-password: ${{ secrets.PROD_CI_TEST_USER_PASSWORD }} diff --git a/packages/reflex-hosting-cli/.github/workflows/e2e_cli_deploy_reflex_web_to_dev.yml b/packages/reflex-hosting-cli/.github/workflows/e2e_cli_deploy_reflex_web_to_dev.yml deleted file mode 100644 index 22fca065cee..00000000000 --- a/packages/reflex-hosting-cli/.github/workflows/e2e_cli_deploy_reflex_web_to_dev.yml +++ /dev/null @@ -1,63 +0,0 @@ -name: e2e-cli-deploy-reflex-web-to-dev -on: - # pull_request: - # branches: [main] - # paths: - # - '.github/workflows/e2e_cli_deploy_reflex_web_to_dev.yml' - # - '.github/actions/cli_deploy_reflex_app/action.yml' - # - 'reflex_cli/**' - # - 'pyproject.yml' - # - 'poetry.lock' - # push: - # branches: [main] - # paths: - # - '.github/workflows/e2e_cli_deploy_reflex_web_to_dev.yml' - # - 'reflex_cli/**' - # - 'pyproject.yml' - # - 'poetry.lock' - workflow_dispatch: - inputs: - reflex-web-ref: - description: 'reflex-web ref to deploy' - required: true - default: 'main' - reflex-hosting-cli-ref: - description: 'reflex-hosting-cli ref to use' - required: true - default: 'main' - reflex-pin: - description: 'Reflex pin (@, ==, all ok)' - -jobs: - e2e-cli-deploy-reflex-web-to-dev: - runs-on: Ubuntu-8-32 - timeout-minutes: 20 - env: - TELEMETRY_ENABLED: false - DEPLOYMENT_KEY: pcweb-${{ github.run_id }}-${{ github.run_attempt }} - REFLEX_HOSTING_CLI_PATH: reflex-hosting-cli - steps: - - name: Check out reflex-hosting-cli repo - uses: actions/checkout@v3 - with: - repository: reflex-dev/reflex-hosting-cli - ref: ${{ inputs.reflex-hosting-cli-ref }} - - name: Deploy reflex-web - uses: ./.github/actions/cli_deploy_reflex_app - with: - reflex-app-repo: reflex-dev/reflex-web - deployment-key: ${DEPLOYMENT_KEY} - reflex-pin: '@git+https://github.com/reflex-dev/reflex@main' - cp-web-url: ${{ secrets.DEV_CP_WEB_URL }} - cp-backend-url: ${{ secrets.DEV_CP_BACKEND_URL }} - cp-env: 'dev' - user-email: ${{ secrets.DEV_CI_TEST_USER_EMAIL }} - user-password: ${{ secrets.DEV_CI_TEST_USER_PASSWORD }} - - name: Delete reflex-web deployment in dev - shell: bash - env: - REFLEX_CLOUD_URL: ${{ secrets.DEV_CP_WEB_URL }} - REFLEX_CLOUD_BACKEND_URL: ${{ secrets.DEV_CP_BACKEND_URL }} - run: | - source venv/bin/activate - reflex deployments delete ${DEPLOYMENT_KEY} diff --git a/packages/reflex-hosting-cli/.github/workflows/pre-commit.yml b/packages/reflex-hosting-cli/.github/workflows/pre-commit.yml deleted file mode 100644 index a95ae647e6e..00000000000 --- a/packages/reflex-hosting-cli/.github/workflows/pre-commit.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: pre-commit - -on: - pull_request: - branches: [main] - push: - # Note even though this job is called "pre-commit" and runs "pre-commit", this job will run - # also POST-commit on main also! In case there are mishandled merge conflicts / bad auto-resolves - # when merging into main branch. - branches: [main] - -jobs: - pre-commit: - timeout-minutes: 30 - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Install uv - uses: astral-sh/setup-uv@v5 - with: - python-version: 3.13 - - run: uv run pre-commit run --all-files diff --git a/packages/reflex-hosting-cli/.github/workflows/unit_tests.yml b/packages/reflex-hosting-cli/.github/workflows/unit_tests.yml deleted file mode 100644 index 224a37dda32..00000000000 --- a/packages/reflex-hosting-cli/.github/workflows/unit_tests.yml +++ /dev/null @@ -1,73 +0,0 @@ -name: unit-tests - -on: - push: - branches: ["main"] - paths: - - ".github/workflows/unit_tests.yml" - - ".github/actions/**" - - "reflex_cli/**" - - "tests/**" - - "pyproject.toml" - - "uv.lock" - - ".coveragerc" - pull_request: - branches: ["main"] - paths: - - ".github/workflows/unit_tests.yml" - - ".github/actions/**" - - "reflex_cli/**" - - "tests/**" - - "pyproject.toml" - - "uv.lock" - - ".coveragerc" - -permissions: - contents: read - -defaults: - run: - shell: bash - -jobs: - unit-tests: - timeout-minutes: 30 - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, windows-latest] - python-version: ["3.10", "3.11", "3.12", "3.13"] - # Run windows only on 3.11 - exclude: - - os: windows-latest - python-version: "3.13" - - os: windows-latest - python-version: "3.12" - - os: windows-latest - python-version: "3.10" - runs-on: ${{ matrix.os }} - # Service containers to run with `runner-job` - services: - # Label used to access the service container - redis: - image: ${{ matrix.os == 'ubuntu-latest' && 'redis' || '' }} - # Set health checks to wait until redis has started - options: >- - --health-cmd "redis-cli ping" - --health-interval 10s - --health-timeout 5s - --health-retries 5 - ports: - # Maps port 6379 on service container to the host - - 6379:6379 - steps: - - uses: actions/checkout@v4 - - name: Install uv - uses: astral-sh/setup-uv@v5 - with: - python-version: ${{ matrix.python-version }} - - name: Run unit tests - run: uv run pytest tests --cov --no-cov-on-fail --cov-report= - env: - PYTHONUNBUFFERED: 1 - - run: uv run coverage html From c9419e9deeddcba4d9fdc65b52261761be84954b Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Wed, 22 Apr 2026 18:38:44 -0700 Subject: [PATCH 11/16] fix greptile complaints --- .../src/reflex_cli/utils/hosting.py | 23 +++++++++---------- .../src/reflex_cli/v2/project.py | 4 ++-- tests/units/reflex_cli/utils/test_hosting.py | 19 +++++++-------- 3 files changed, 21 insertions(+), 25 deletions(-) diff --git a/packages/reflex-hosting-cli/src/reflex_cli/utils/hosting.py b/packages/reflex-hosting-cli/src/reflex_cli/utils/hosting.py index a95dcea98a5..ef133b42e9d 100644 --- a/packages/reflex-hosting-cli/src/reflex_cli/utils/hosting.py +++ b/packages/reflex-hosting-cli/src/reflex_cli/utils/hosting.py @@ -429,7 +429,7 @@ def validate_token(token: str) -> dict[str, Any]: console.debug(f"Unable to validate the token due to: {ex}") raise Exception("server error") from ex except ValueError as ve: - console.debug(f"Access denied for {token}") + console.debug("Access denied") raise ValueError("access denied") from ve except Exception as ex: console.debug(f"Unexpected error: {ex}") @@ -440,9 +440,10 @@ def delete_token_from_config(): """Delete the invalid token from the config file if applicable.""" if constants.Hosting.HOSTING_JSON.exists(): try: - with constants.Hosting.HOSTING_JSON.open("w") as config_file: + with constants.Hosting.HOSTING_JSON.open("r") as config_file: hosting_config = json.load(config_file) - del hosting_config["access_token"] + hosting_config.pop("access_token", None) + with constants.Hosting.HOSTING_JSON.open("w") as config_file: json.dump(hosting_config, config_file) except Exception as ex: # Best efforts removing invalid token is OK @@ -631,12 +632,12 @@ def search_app( if not isinstance(client, AuthenticatedClient): raise NotAuthenticatedError("not authenticated") + params: dict[str, str] = {"app_name": app_name} + if project_id: + params["project_id"] = project_id response = httpx.get( - urljoin( - constants.Hosting.HOSTING_SERVICE, - f"/api/v1/apps/search?app_name={app_name}" - + (f"&project_id={project_id}" if project_id else ""), - ), + urljoin(constants.Hosting.HOSTING_SERVICE, "/api/v1/apps/search"), + params=params, headers=authorization_header(client.token), timeout=constants.Hosting.TIMEOUT, ) @@ -697,10 +698,8 @@ def search_project( raise NotAuthenticatedError("not authenticated") response = httpx.get( - urljoin( - constants.Hosting.HOSTING_SERVICE, - f"/api/v1/project/search?project_name={project_name}", - ), + urljoin(constants.Hosting.HOSTING_SERVICE, "/api/v1/project/search"), + params={"project_name": project_name}, headers=authorization_header(client.token), timeout=constants.Hosting.TIMEOUT, ) diff --git a/packages/reflex-hosting-cli/src/reflex_cli/v2/project.py b/packages/reflex-hosting-cli/src/reflex_cli/v2/project.py index 7522b1046d8..dd889b92be7 100644 --- a/packages/reflex-hosting-cli/src/reflex_cli/v2/project.py +++ b/packages/reflex-hosting-cli/src/reflex_cli/v2/project.py @@ -221,7 +221,7 @@ def get_select_project( console.error( "You are not authenticated. Run `reflex login` to authenticate." ) - click.exceptions.Exit(1) + raise click.exceptions.Exit(1) from None except Exception as e: console.error(f"Unable to get the currently selected project: {e}") else: @@ -288,7 +288,7 @@ def get_projects( console.print(str(projects)) except NotAuthenticatedError: console.error("You are not authenticated. Run `reflex login` to authenticate.") - click.exceptions.Exit(1) + raise click.exceptions.Exit(1) from None except Exception as e: console.error(f"Unable to get projects: {e}") raise click.exceptions.Exit(1) from e diff --git a/tests/units/reflex_cli/utils/test_hosting.py b/tests/units/reflex_cli/utils/test_hosting.py index c861d9a028e..312a12e3f09 100644 --- a/tests/units/reflex_cli/utils/test_hosting.py +++ b/tests/units/reflex_cli/utils/test_hosting.py @@ -35,18 +35,17 @@ def test_get_existing_access_token( @pytest.mark.parametrize( - "file_exists, config_content, expected_deleted", + "file_exists, config_content", [ - (True, '{"access_token": "valid_token"}', True), - (True, '{"another_key": "value"}', False), - (False, "", False), + (True, '{"access_token": "valid_token"}'), + (True, '{"another_key": "value"}'), + (False, ""), ], ) def test_delete_token_from_config( mocker: MockerFixture, file_exists: bool, config_content: str, - expected_deleted: bool, ): mocker.patch("pathlib.Path.exists", return_value=file_exists) mock_os_remove = mocker.patch("pathlib.Path.unlink") @@ -61,12 +60,10 @@ def test_delete_token_from_config( delete_token_from_config() if file_exists: - mocked_open.assert_called_once() - if expected_deleted: - mock_json_load.assert_called_once() - mock_json_dump.assert_called_once() - else: - mock_json_dump.assert_not_called() + assert mocked_open.call_count == 2 + mock_json_load.assert_called_once() + mock_json_dump.assert_called_once() + assert "access_token" not in mock_json_dump.call_args.args[0] mock_os_remove.assert_called_once() else: mocked_open.assert_not_called() From 8813b86e8c48a468cdaad0f0c7889256d1b6e38d Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Wed, 22 Apr 2026 18:51:01 -0700 Subject: [PATCH 12/16] fix greptile complains again --- .../reflex-hosting-cli/src/reflex_cli/utils/hosting.py | 10 ++++++++-- .../src/reflex_cli/v2/deployments.py | 9 ++++++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/reflex-hosting-cli/src/reflex_cli/utils/hosting.py b/packages/reflex-hosting-cli/src/reflex_cli/utils/hosting.py index ef133b42e9d..dfd1dfc47bd 100644 --- a/packages/reflex-hosting-cli/src/reflex_cli/utils/hosting.py +++ b/packages/reflex-hosting-cli/src/reflex_cli/utils/hosting.py @@ -462,11 +462,17 @@ def save_token_to_config(token: str): token: The access token to save. """ - hosting_config: dict[str, str] = {"access_token": token} - try: if not Path(constants.Reflex.DIR).exists(): Path(constants.Reflex.DIR).mkdir(parents=True, exist_ok=True) + hosting_config: dict[str, str] = {} + if constants.Hosting.HOSTING_JSON.exists(): + try: + with constants.Hosting.HOSTING_JSON.open("r") as config_file: + hosting_config = json.load(config_file) + except (OSError, ValueError): + hosting_config = {} + hosting_config["access_token"] = token with constants.Hosting.HOSTING_JSON.open("w") as config_file: json.dump(hosting_config, config_file) except Exception as ex: diff --git a/packages/reflex-hosting-cli/src/reflex_cli/v2/deployments.py b/packages/reflex-hosting-cli/src/reflex_cli/v2/deployments.py index d6761fc56d6..8042bf8f6c5 100644 --- a/packages/reflex-hosting-cli/src/reflex_cli/v2/deployments.py +++ b/packages/reflex-hosting-cli/src/reflex_cli/v2/deployments.py @@ -29,6 +29,8 @@ def hosting_cli(ctx: click.Context) -> None: It provides commands for managing apps, projects, secrets, and VM types/regions. """ + if _reflex_version is None: + ctx.fail("Reflex is not installed. Install it with `pip install reflex`.") if _reflex_version < constants.ReflexHostingCli.MINIMUM_REFLEX_VERSION: ctx.fail( f"Reflex version {_reflex_version} is not compatible with reflex-hosting-cli. " @@ -42,7 +44,12 @@ def hosting_cli(ctx: click.Context) -> None: check_version() -_reflex_version = version.parse(importlib.metadata.version("reflex")) +try: + _reflex_version: version.Version | None = version.parse( + importlib.metadata.version("reflex") + ) +except importlib.metadata.PackageNotFoundError: + _reflex_version = None hosting_cli.add_command( From bc843366aca79f270764267fb5c1d0d66b45de16 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Wed, 22 Apr 2026 18:59:54 -0700 Subject: [PATCH 13/16] fix greptile comments --- .../reflex-hosting-cli/src/reflex_cli/utils/hosting.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/reflex-hosting-cli/src/reflex_cli/utils/hosting.py b/packages/reflex-hosting-cli/src/reflex_cli/utils/hosting.py index dfd1dfc47bd..347a528a501 100644 --- a/packages/reflex-hosting-cli/src/reflex_cli/utils/hosting.py +++ b/packages/reflex-hosting-cli/src/reflex_cli/utils/hosting.py @@ -1574,17 +1574,21 @@ def get_app_logs( if not app: console.warn("no app with given id found") return None - params = f"?offset={offset}" if offset else f"?start={start}&end={end}" + params: dict[str, str | int | None] = ( + {"offset": offset} if offset else {"start": start, "end": end} + ) if cursor: - params += f"&cursor={cursor}" + params["cursor"] = cursor try: with console.status("Fetching application logs..."): response = httpx.get( urljoin( constants.Hosting.HOSTING_SERVICE, - f"/api/v1/apps/{app['id']}/logsv2{params}", + f"/api/v1/apps/{app['id']}/logsv2", ), + params=params, headers=authorization_header(client.token), + timeout=constants.Hosting.TIMEOUT, ) response.raise_for_status() except httpx.RequestError: From 0af000f8cf4c65121a45cf0fd85ae3ec152e50bd Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Wed, 22 Apr 2026 21:30:14 -0700 Subject: [PATCH 14/16] respond to more greptile comments --- .../src/reflex_cli/utils/hosting.py | 13 ++-- .../src/reflex_cli/v2/secrets.py | 75 ++++++++++--------- 2 files changed, 47 insertions(+), 41 deletions(-) diff --git a/packages/reflex-hosting-cli/src/reflex_cli/utils/hosting.py b/packages/reflex-hosting-cli/src/reflex_cli/utils/hosting.py index 347a528a501..d5dd13f07bd 100644 --- a/packages/reflex-hosting-cli/src/reflex_cli/utils/hosting.py +++ b/packages/reflex-hosting-cli/src/reflex_cli/utils/hosting.py @@ -1627,12 +1627,15 @@ def list_apps(client: AuthenticatedClient, project: str | None = None) -> list[d if not isinstance(client, AuthenticatedClient): raise NotAuthenticatedError("not authenticated") - url = urljoin( - constants.Hosting.HOSTING_SERVICE, - f"/api/v1/apps?project={project}" if project else "/api/v1/apps", - ) + url = urljoin(constants.Hosting.HOSTING_SERVICE, "/api/v1/apps") + params = {"project": project} if project else None - response = httpx.get(url, headers=authorization_header(client.token), timeout=5) + response = httpx.get( + url, + params=params, + headers=authorization_header(client.token), + timeout=constants.Hosting.TIMEOUT, + ) try: response.raise_for_status() except httpx.HTTPStatusError as ex: diff --git a/packages/reflex-hosting-cli/src/reflex_cli/v2/secrets.py b/packages/reflex-hosting-cli/src/reflex_cli/v2/secrets.py index fa2fe0b2b3f..3f1b28f66a6 100644 --- a/packages/reflex-hosting-cli/src/reflex_cli/v2/secrets.py +++ b/packages/reflex-hosting-cli/src/reflex_cli/v2/secrets.py @@ -130,48 +130,51 @@ def update_secrets( from reflex_cli.utils import hosting console.set_log_level(loglevel) - authenticated_client = hosting.get_authenticated_client( - token=token, interactive=interactive - ) - - if not app_id: - config = hosting.read_config() - if config: - app_id = config.appid - if not isinstance(app_id, (str, type(None))): - console.error( - "app_id must be a string or None. Please check your config file." - ) - raise click.exceptions.Exit(1) - - if not app_id: - console.error("No valid app_id provided.") - raise click.exceptions.Exit(1) + try: + authenticated_client = hosting.get_authenticated_client( + token=token, interactive=interactive + ) - if envfile is None and not envs: - console.error("--envfile or --env must be provided") - raise click.exceptions.Exit(1) + if not app_id: + config = hosting.read_config() + if config: + app_id = config.appid + if not isinstance(app_id, (str, type(None))): + console.error( + "app_id must be a string or None. Please check your config file." + ) + raise click.exceptions.Exit(1) - if envfile and envs: - console.warn("--envfile is set; ignoring --env") + if not app_id: + console.error("No valid app_id provided.") + raise click.exceptions.Exit(1) - secrets = {} + if envfile is None and not envs: + console.error("--envfile or --env must be provided") + raise click.exceptions.Exit(1) - if envfile: - try: - from dotenv import dotenv_values # pyright: ignore[reportMissingImports] + if envfile and envs: + console.warn("--envfile is set; ignoring --env") + if envfile: + try: + from dotenv import ( # pyright: ignore[reportMissingImports] + dotenv_values, + ) + except ImportError: + console.error( + """The `python-dotenv` package is required to load environment variables from a file. Run `pip install "python-dotenv>=1.0.1"`.""" + ) + raise click.exceptions.Exit(1) from None secrets = dotenv_values(envfile) - except ImportError: - console.error( - """The `python-dotenv` package is required to load environment variables from a file. Run `pip install "python-dotenv>=1.0.1"`.""" - ) - - else: - secrets = hosting.process_envs(list(envs)) - hosting.update_secrets( - app_id=app_id, secrets=secrets, reboot=reboot, client=authenticated_client - ) + else: + secrets = hosting.process_envs(list(envs)) + hosting.update_secrets( + app_id=app_id, secrets=secrets, reboot=reboot, client=authenticated_client + ) + except NotAuthenticatedError as err: + console.error("You are not authenticated. Run `reflex login` to authenticate.") + raise click.exceptions.Exit(1) from err @secrets_cli.command(name="delete") From 78b5de5873206688f69e0c8c769ca8dac306d6a4 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Wed, 22 Apr 2026 22:43:28 -0700 Subject: [PATCH 15/16] fix infinite recursion --- .../src/reflex_cli/v2/apps.py | 122 ++++++++---------- 1 file changed, 52 insertions(+), 70 deletions(-) diff --git a/packages/reflex-hosting-cli/src/reflex_cli/v2/apps.py b/packages/reflex-hosting-cli/src/reflex_cli/v2/apps.py index 9b465eb0514..0b56a9d2fdf 100644 --- a/packages/reflex-hosting-cli/src/reflex_cli/v2/apps.py +++ b/packages/reflex-hosting-cli/src/reflex_cli/v2/apps.py @@ -468,78 +468,74 @@ def app_logs( follow: bool = True, ): """Retrieve logs for a given application.""" - from reflex_cli.utils import hosting - - if pretty: - try: - import pprint - except ImportError: - console.error( - "pprint module is not available. Please install pprint to use pretty printing." - ) - raise click.exceptions.Exit(1) from ImportError + import pprint - authenticated_client = hosting.get_authenticated_client( - token=token, interactive=interactive - ) + from reflex_cli.utils import hosting - if not app_id: - config = hosting.read_config() - if config: - app_id = config.appid - if not isinstance(app_id, (str, type(None))): - console.error( - "app_id must be a string or None. Please check your config file." - ) - raise click.exceptions.Exit(1) + console.set_log_level(loglevel) - if app_name is not None and app_id is None: - app_result = hosting.search_app( - app_name=app_name, - project_id=None, - client=authenticated_client, - interactive=interactive, + try: + authenticated_client = hosting.get_authenticated_client( + token=token, interactive=interactive ) - app_id = app_result.get("id") if app_result else None - if not app_id: - console.error("No valid app_id or app_name provided.") - raise click.exceptions.Exit(1) + if not app_id: + config = hosting.read_config() + if config: + app_id = config.appid + if not isinstance(app_id, (str, type(None))): + console.error( + "app_id must be a string or None. Please check your config file." + ) + raise click.exceptions.Exit(1) - if offset is None and start is None and end is None: - offset = 3600 - if not offset and not (start and end): - console.error("must provide both start and end") - raise click.exceptions.Exit(1) + if app_name is not None and app_id is None: + app_result = hosting.search_app( + app_name=app_name, + project_id=None, + client=authenticated_client, + interactive=interactive, + ) + app_id = app_result.get("id") if app_result else None - console.set_log_level(loglevel) + if not app_id: + console.error("No valid app_id or app_name provided.") + raise click.exceptions.Exit(1) - try: - console.debug(f"fetching logs with cursor: {cursor}") - result = hosting.get_app_logs( - app_id=app_id, - offset=offset, - start=start, - end=end, - client=authenticated_client, - cursor=cursor, - ) - if isinstance(result, list): - if len(result) == 2: + if offset is None and start is None and end is None: + offset = 3600 + if not offset and not (start and end): + console.error("must provide both start and end") + raise click.exceptions.Exit(1) + + while True: + console.debug(f"fetching logs with cursor: {cursor}") + result = hosting.get_app_logs( + app_id=app_id, + offset=offset, + start=start, + end=end, + client=authenticated_client, + cursor=cursor, + ) + if not isinstance(result, list): + console.warn("Unable to retrieve logs.") + return + if len(result) == 2 and isinstance(result[1], str): cursor = result[1] result = result[0] + else: + cursor = None if not result: console.warn("No logs found for the specified criteria.") return result.reverse() for log in result: if pretty: - log = pprint.pformat(log, indent=2) # type: ignore # noqa: PGH003 + log = pprint.pformat(log, indent=2) console.info(log) - else: - console.warn("Unable to retrieve logs.") - return - if interactive and follow: + if not (interactive and follow): + return from rich.prompt import Prompt prompt = Prompt.ask( @@ -549,21 +545,7 @@ def app_logs( ) if prompt.lower() == "exit": console.info("Exiting log retrieval.") - raise click.exceptions.Exit(0) - ctx = click.get_current_context() - ctx.invoke( - app_logs, - app_id=app_id, - app_name=None, # Don't pass app_name again since we have app_id - token=token, - offset=offset, - start=start, - end=end, - loglevel=loglevel, - interactive=interactive, - cursor=cursor, - pretty=pretty, - ) + return except ResponseError as err: console.error(f"Error retrieving logs: {err}") raise click.exceptions.Exit(1) from err From 1ac5a95a133fcd8d66e58db2bbfc5b47e3c94d13 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Thu, 23 Apr 2026 10:34:52 -0700 Subject: [PATCH 16/16] add any missing timeouts and guard against not authenticated --- .../src/reflex_cli/utils/hosting.py | 8 ++++ .../src/reflex_cli/v2/apps.py | 39 +++++++++++-------- .../src/reflex_cli/v2/project.py | 3 ++ 3 files changed, 33 insertions(+), 17 deletions(-) diff --git a/packages/reflex-hosting-cli/src/reflex_cli/utils/hosting.py b/packages/reflex-hosting-cli/src/reflex_cli/utils/hosting.py index d5dd13f07bd..010aa1b2823 100644 --- a/packages/reflex-hosting-cli/src/reflex_cli/utils/hosting.py +++ b/packages/reflex-hosting-cli/src/reflex_cli/utils/hosting.py @@ -1464,6 +1464,7 @@ def stop_app(app_id: str, client: AuthenticatedClient): response = httpx.post( urljoin(constants.Hosting.HOSTING_SERVICE, f"/api/v1/apps/{app_id}/stop"), headers=authorization_header(client.token), + timeout=constants.Hosting.TIMEOUT, ) try: response.raise_for_status() @@ -1494,6 +1495,7 @@ def start_app(app_id: str, client: AuthenticatedClient): response = httpx.post( urljoin(constants.Hosting.HOSTING_SERVICE, f"/api/v1/apps/{app_id}/start"), headers=authorization_header(client.token), + timeout=constants.Hosting.TIMEOUT, ) try: response.raise_for_status() @@ -1528,6 +1530,7 @@ def delete_app(app_id: str, client: AuthenticatedClient): response = httpx.delete( urljoin(constants.Hosting.HOSTING_SERVICE, f"/api/v1/apps/{app['id']}/delete"), headers=authorization_header(client.token), + timeout=constants.Hosting.TIMEOUT, ) try: response.raise_for_status() @@ -1665,6 +1668,7 @@ def get_app_history(app_id: str, client: AuthenticatedClient) -> list: response = httpx.get( urljoin(constants.Hosting.HOSTING_SERVICE, f"/api/v1/apps/{app_id}/history"), headers=authorization_header(client.token), + timeout=constants.Hosting.TIMEOUT, ) response.raise_for_status() @@ -1709,6 +1713,7 @@ def get_app_status(app_id: str, client: AuthenticatedClient) -> str: f"/api/v1/deployments/{app_id}/status", ), headers=authorization_header(client.token), + timeout=constants.Hosting.TIMEOUT, ) except httpx.RequestError as e: return "lost connection: trying again" + f"({e.__class__.__name__}: {e})" @@ -1778,6 +1783,7 @@ def get_deployment_status(deployment_id: str, client: AuthenticatedClient) -> st f"/api/v1/deployments/{deployment_id}/status", ), headers=authorization_header(client.token), + timeout=constants.Hosting.TIMEOUT, ) try: response.raise_for_status() @@ -1807,6 +1813,7 @@ def _get_deployment_status(deployment_id: str, token: str) -> str: f"/api/v1/deployments/{deployment_id}/status", ), headers=authorization_header(token), + timeout=constants.Hosting.TIMEOUT, ) except httpx.RequestError as e: return "lost connection: trying again" + f"({e.__class__.__name__}: {e})" @@ -1891,6 +1898,7 @@ def get_deployment_build_logs(deployment_id: str, client: AuthenticatedClient): f"/api/v1/deployments/{deployment_id}/build/logs", ), headers=authorization_header(client.token), + timeout=constants.Hosting.TIMEOUT, ) response.raise_for_status() diff --git a/packages/reflex-hosting-cli/src/reflex_cli/v2/apps.py b/packages/reflex-hosting-cli/src/reflex_cli/v2/apps.py index 0b56a9d2fdf..a8745353885 100644 --- a/packages/reflex-hosting-cli/src/reflex_cli/v2/apps.py +++ b/packages/reflex-hosting-cli/src/reflex_cli/v2/apps.py @@ -590,28 +590,33 @@ def list_apps( console.set_log_level(loglevel) - authenticated_client = hosting.get_authenticated_client( - token=token, interactive=interactive - ) - - if project_name and not project_id: - result = hosting.search_project( - project_name, client=authenticated_client, interactive=interactive + try: + authenticated_client = hosting.get_authenticated_client( + token=token, interactive=interactive ) - project_id = result.get("id") if result else None - if project_id is None: - project_id = hosting.get_selected_project() + if project_name and not project_id: + result = hosting.search_project( + project_name, client=authenticated_client, interactive=interactive + ) + project_id = result.get("id") if result else None - if project_id is not None and not as_json: - try: - project = hosting.get_project(project_id, client=authenticated_client) - console.info(f"Listing apps for project '{project['name']}' ({project_id})") - except Exception: - pass + if project_id is None: + project_id = hosting.get_selected_project() + + if project_id is not None and not as_json: + try: + project = hosting.get_project(project_id, client=authenticated_client) + console.info( + f"Listing apps for project '{project['name']}' ({project_id})" + ) + except Exception: + pass - try: deployments = hosting.list_apps(project=project_id, client=authenticated_client) + except NotAuthenticatedError as err: + console.error("You are not authenticated. Run `reflex login` to authenticate.") + raise click.exceptions.Exit(1) from err except Exception as ex: console.error("Unable to list deployments") raise click.exceptions.Exit(1) from ex diff --git a/packages/reflex-hosting-cli/src/reflex_cli/v2/project.py b/packages/reflex-hosting-cli/src/reflex_cli/v2/project.py index dd889b92be7..271dbe77884 100644 --- a/packages/reflex-hosting-cli/src/reflex_cli/v2/project.py +++ b/packages/reflex-hosting-cli/src/reflex_cli/v2/project.py @@ -155,6 +155,9 @@ def select_project( # check if provided project exists. if project_id: hosting.get_project(project_id, client=authenticated_client) + except NotAuthenticatedError as err: + console.error("You are not authenticated. Run `reflex login` to authenticate.") + raise click.exceptions.Exit(1) from err except httpx.HTTPStatusError as ex: try: console.error(ex.response.json().get("detail"))