diff --git a/.github/DISCUSSION_TEMPLATE/questions.yml b/.github/DISCUSSION_TEMPLATE/questions.yml index 97902a658e..524d5c2d08 100644 --- a/.github/DISCUSSION_TEMPLATE/questions.yml +++ b/.github/DISCUSSION_TEMPLATE/questions.yml @@ -64,16 +64,14 @@ body: If I (or someone) can copy it, run it, and see it right away, there's a much higher chance I (or someone) will be able to help you. placeholder: | - from typing import Optional - from sqlmodel import Field, Session, SQLModel, create_engine class Hero(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) + id: int | None = Field(default=None, primary_key=True) name: str secret_name: str - age: Optional[int] = None + age: int | None = None hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") diff --git a/.github/dependabot.yml b/.github/dependabot.yml index cd972a0ba4..959ec970fb 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -8,7 +8,7 @@ updates: commit-message: prefix: ⬆ # Python - - package-ecosystem: "pip" + - package-ecosystem: "uv" directory: "/" schedule: interval: "daily" diff --git a/.github/labeler.yml b/.github/labeler.yml index 1af3d5e408..fbbe9f28ef 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -16,8 +16,8 @@ internal: - scripts/** - .gitignore - .pre-commit-config.yaml - - pdm_build.py - requirements*.txt + - uv.lock - all-globs-to-all-files: - '!docs/**' - '!sqlmodel/**' diff --git a/.github/workflows/add-to-project.yml b/.github/workflows/add-to-project.yml index dccea83f35..0308d7a07f 100644 --- a/.github/workflows/add-to-project.yml +++ b/.github/workflows/add-to-project.yml @@ -12,7 +12,7 @@ jobs: name: Add to project runs-on: ubuntu-latest steps: - - uses: actions/add-to-project@v1.0.2 + - uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2 with: project-url: https://github.com/orgs/fastapi/projects/2 github-token: ${{ secrets.PROJECTS_TOKEN }} diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 00adbfbc5e..dd531666eb 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -7,6 +7,7 @@ on: types: - opened - synchronize + jobs: changes: runs-on: ubuntu-latest @@ -17,9 +18,9 @@ jobs: outputs: docs: ${{ steps.filter.outputs.docs }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 # For pull requests it's not necessary to checkout the code but for the main branch it is - - uses: dorny/paths-filter@v3 + - uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1 id: filter with: filters: | @@ -27,15 +28,13 @@ jobs: - README.md - docs/** - docs_src/** - - requirements-docs.txt - - requirements-docs-insiders.txt - pyproject.toml + - uv.lock - mkdocs.yml - - mkdocs.insiders.yml - - mkdocs.maybe-insiders.yml - - mkdocs.no-insiders.yml + - mkdocs.env.yml - .github/workflows/build-docs.yml - .github/workflows/deploy-docs.yml + - data/** build-docs: needs: @@ -47,33 +46,27 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v4 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: - python-version: "3.11" - - uses: actions/cache@v4 - id: cache + python-version-file: ".python-version" + - name: Setup uv + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 with: - path: ${{ env.pythonLocation }} - key: ${{ runner.os }}-python-docs-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml', 'requirements-docs.txt', 'requirements-docs-insiders.txt', 'requirements-docs-tests.txt') }}-v02 + enable-cache: true + cache-dependency-glob: | + pyproject.toml + uv.lock - name: Install docs extras - if: steps.cache.outputs.cache-hit != 'true' - run: pip install -r requirements-docs.txt - - name: Install Material for MkDocs Insiders - if: ( github.event_name != 'pull_request' || github.secret_source == 'Actions' ) && steps.cache.outputs.cache-hit != 'true' - run: pip install -r requirements-docs-insiders.txt - env: - TOKEN: ${{ secrets.SQLMODEL_MKDOCS_MATERIAL_INSIDERS }} - - uses: actions/cache@v4 + run: uv sync --locked --no-dev --group docs + - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: key: mkdocs-cards-${{ github.ref }} path: .cache - - name: Verify README - run: python ./scripts/docs.py verify-readme - name: Build Docs - run: python ./scripts/docs.py build - - uses: actions/upload-artifact@v4 + run: uv run ./scripts/docs.py build + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: docs-site path: ./site/** @@ -87,7 +80,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Decide whether the needed jobs succeeded or failed - uses: re-actors/alls-green@release/v1 + uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # v1.2.2 with: jobs: ${{ toJSON(needs) }} allowed-skips: build-docs diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index ca43310691..31c3d73fdc 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -20,31 +20,32 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v4 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: - python-version: "3.11" - - uses: actions/cache@v4 - id: cache + python-version-file: ".python-version" + - name: Setup uv + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 with: - path: ${{ env.pythonLocation }} - key: ${{ runner.os }}-python-github-actions-${{ env.pythonLocation }}-${{ hashFiles('requirements-github-actions.txt') }}-v01 + enable-cache: true + cache-dependency-glob: | + pyproject.toml + uv.lock - name: Install GitHub Actions dependencies - if: steps.cache.outputs.cache-hit != 'true' - run: pip install -r requirements-github-actions.txt + run: uv sync --locked --no-dev --group github-actions - name: Deploy Docs Status Pending - run: python ./scripts/deploy_docs_status.py + run: uv run ./scripts/deploy_docs_status.py env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} COMMIT_SHA: ${{ github.event.workflow_run.head_sha }} RUN_ID: ${{ github.run_id }} - + STATE: "pending" - name: Clean site run: | rm -rf ./site mkdir ./site - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: path: ./site/ pattern: docs-site @@ -55,19 +56,27 @@ jobs: # hashFiles returns an empty string if there are no files if: hashFiles('./site/*') id: deploy - uses: cloudflare/pages-action@v1 + env: + PROJECT_NAME: sqlmodel + BRANCH: ${{ ( github.event.workflow_run.head_repository.full_name == github.repository && github.event.workflow_run.head_branch == 'main' && 'main' ) || ( github.event.workflow_run.head_sha ) }} + uses: cloudflare/wrangler-action@9acf94ace14e7dc412b076f2c5c20b8ce93c79cd # v3.15.0 with: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - projectName: sqlmodel - directory: './site' - gitHubToken: ${{ secrets.GITHUB_TOKEN }} - branch: ${{ ( github.event.workflow_run.head_repository.full_name == github.repository && github.event.workflow_run.head_branch == 'main' && 'main' ) || ( github.event.workflow_run.head_sha ) }} + command: pages deploy ./site --project-name=${{ env.PROJECT_NAME }} --branch=${{ env.BRANCH }} + - name: Deploy Docs Status Error + if: failure() + run: uv run ./scripts/deploy_docs_status.py + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + COMMIT_SHA: ${{ github.event.workflow_run.head_sha }} + RUN_ID: ${{ github.run_id }} + STATE: "error" - name: Comment Deploy - run: python ./scripts/deploy_docs_status.py + run: uv run ./scripts/deploy_docs_status.py env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - DEPLOY_URL: ${{ steps.deploy.outputs.url }} + DEPLOY_URL: ${{ steps.deploy.outputs.deployment-url }} COMMIT_SHA: ${{ github.event.workflow_run.head_sha }} RUN_ID: ${{ github.run_id }} - IS_DONE: "true" + STATE: "success" diff --git a/.github/workflows/detect-conflicts.yml b/.github/workflows/detect-conflicts.yml new file mode 100644 index 0000000000..3ac6f65e2f --- /dev/null +++ b/.github/workflows/detect-conflicts.yml @@ -0,0 +1,19 @@ +name: "Conflict detector" +on: + push: + pull_request_target: + types: [synchronize] + +jobs: + main: + permissions: + contents: read + pull-requests: write + runs-on: ubuntu-latest + steps: + - name: Check if PRs have merge conflicts + uses: eps1lon/actions-label-merge-conflict@1df065ebe6e3310545d4f4c4e862e43bdca146f0 # v3.0.3 + with: + dirtyLabel: "conflicts" + repoToken: "${{ secrets.GITHUB_TOKEN }}" + commentOnDirty: "This pull request has a merge conflict that needs to be resolved." diff --git a/.github/workflows/issue-manager.yml b/.github/workflows/issue-manager.yml index 038491f828..065ee4ceb9 100644 --- a/.github/workflows/issue-manager.yml +++ b/.github/workflows/issue-manager.yml @@ -2,7 +2,7 @@ name: Issue Manager on: schedule: - - cron: "11 4 * * *" + - cron: "13 18 * * *" issue_comment: types: - created @@ -16,6 +16,7 @@ on: permissions: issues: write + pull-requests: write jobs: issue-manager: @@ -26,7 +27,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: tiangolo/issue-manager@0.5.0 + - uses: tiangolo/issue-manager@2fb3484ec9279485df8659e8ec73de262431737d # 0.6.0 with: token: ${{ secrets.GITHUB_TOKEN }} config: > @@ -35,8 +36,20 @@ jobs: "delay": 864000, "message": "Assuming the original need was handled, this will be automatically closed now. But feel free to add more comments or create new issues or PRs." }, - "changes-requested": { + "waiting": { "delay": 2628000, - "message": "As this PR had requested changes to be applied but has been inactive for a while, it's now going to be closed. But if there's anyone interested, feel free to create a new PR." + "message": "As this PR has been waiting for the original user for a while but seems to be inactive, it's now going to be closed. But if there's anyone interested, feel free to create a new PR.", + "reminder": { + "before": "P3D", + "message": "Heads-up: this will be closed in 3 days unless there's new activity." + } + }, + "invalid": { + "delay": 0, + "message": "This was marked as invalid and will be closed now. If this is an error, please provide additional details." + }, + "maybe-ai": { + "delay": 0, + "message": "This was marked as potentially AI generated and will be closed now. If this is an error, please provide additional details, make sure to read the docs about contributing and AI." } } diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index c3bb83f9a5..6ba567399b 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -16,7 +16,9 @@ jobs: pull-requests: write runs-on: ubuntu-latest steps: - - uses: actions/labeler@v5 + - uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1 + if: ${{ github.event.action != 'labeled' && github.event.action != 'unlabeled' }} + - run: echo "Done adding labels" # Run this after labeler applied labels check-labels: needs: @@ -25,7 +27,7 @@ jobs: pull-requests: read runs-on: ubuntu-latest steps: - - uses: docker://agilepathway/pull-request-label-checker:latest + - uses: agilepathway/label-checker@c3d16ad512e7cea5961df85ff2486bb774caf3c5 # v1.6.65 with: one_of: breaking,security,feature,bug,refactor,upgrade,docs,lang-all,internal repo_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/latest-changes.yml b/.github/workflows/latest-changes.yml index 77a3b3eec6..1325a6813f 100644 --- a/.github/workflows/latest-changes.yml +++ b/.github/workflows/latest-changes.yml @@ -20,17 +20,17 @@ jobs: latest-changes: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: # To allow latest-changes to commit to the main branch token: ${{ secrets.SQLMODEL_LATEST_CHANGES }} # Allow debugging with tmate - name: Setup tmate session - uses: mxschmitt/action-tmate@v3 + uses: mxschmitt/action-tmate@c0afd6f790e3a5564914980036ebf83216678101 # v3.23 if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled == 'true' }} with: limit-access-to-actor: true - - uses: tiangolo/latest-changes@0.3.1 + - uses: tiangolo/latest-changes@c9d329cb147f0ddf4fb631214e3f838ff17ccbbd # 0.4.1 with: token: ${{ secrets.GITHUB_TOKEN }} latest_changes_file: docs/release-notes.md diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 0000000000..e5e9c5740e --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,90 @@ +name: pre-commit + +on: + pull_request: + types: + - opened + - synchronize + +env: + # Forks and Dependabot don't have access to secrets + HAS_SECRETS: ${{ secrets.PRE_COMMIT != '' }} + +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + name: Checkout PR for own repo + if: env.HAS_SECRETS == 'true' + with: + # To be able to commit it needs to fetch the head of the branch, not the + # merge commit + ref: ${{ github.head_ref }} + # And it needs the full history to be able to compute diffs + fetch-depth: 0 + # A token other than the default GITHUB_TOKEN is needed to be able to trigger CI + token: ${{ secrets.PRE_COMMIT }} + # pre-commit lite ci needs the default checkout configs to work + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + name: Checkout PR for fork + if: env.HAS_SECRETS == 'false' + with: + # To be able to commit it needs the head branch of the PR, the remote one + ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 0 + - name: Set up Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version-file: ".python-version" + - name: Setup uv + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 + with: + cache-dependency-glob: | + pyproject.toml + uv.lock + - name: Install Dependencies + run: uv sync --locked + - name: Run prek - pre-commit + id: precommit + run: uvx prek run --from-ref origin/${GITHUB_BASE_REF} --to-ref HEAD --show-diff-on-failure + continue-on-error: true + - name: Commit and push changes + if: env.HAS_SECRETS == 'true' + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add -A + if git diff --staged --quiet; then + echo "No changes to commit" + else + git commit -m "🎨 Auto format" + git push + fi + - uses: pre-commit-ci/lite-action@5d6cc0eb514c891a40562a58a8e71576c5c7fb43 # v1.1.0 + if: env.HAS_SECRETS == 'false' + with: + msg: 🎨 Auto format + - name: Error out on pre-commit errors + if: steps.precommit.outcome == 'failure' + run: exit 1 + + # https://github.com/marketplace/actions/alls-green#why + pre-commit-alls-green: # This job does nothing and is only used for the branch protection + if: always() + needs: + - pre-commit + runs-on: ubuntu-latest + steps: + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" + - name: Decide whether the needed jobs succeeded or failed + uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # v1.2.2 + with: + jobs: ${{ toJSON(needs) }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 10be182e6b..6180501bbf 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -14,24 +14,22 @@ on: jobs: publish: runs-on: ubuntu-latest - strategy: - matrix: - package: - - sqlmodel - - sqlmodel-slim permissions: id-token: write + contents: read steps: - - uses: actions/checkout@v4 + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: - python-version: "3.11" - - name: Install build dependencies - run: pip install build + python-version-file: ".python-version" + - name: Install uv + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 - name: Build distribution - env: - TIANGOLO_BUILD_PACKAGE: ${{ matrix.package }} - run: python -m build + run: uv build - name: Publish - uses: pypa/gh-action-pypi-publish@v1.9.0 + run: uv publish diff --git a/.github/workflows/smokeshow.yml b/.github/workflows/smokeshow.yml index bc37a92e78..919054ebe3 100644 --- a/.github/workflows/smokeshow.yml +++ b/.github/workflows/smokeshow.yml @@ -10,27 +10,39 @@ permissions: jobs: smokeshow: - if: ${{ github.event.workflow_run.conclusion == 'success' }} runs-on: ubuntu-latest - steps: - - uses: actions/setup-python@v5 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: - python-version: '3.9' - - - run: pip install smokeshow - - - uses: actions/download-artifact@v4 + python-version-file: ".python-version" + - name: Setup uv + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 + with: + cache-dependency-glob: | + pyproject.toml + uv.lock + - run: uv sync --locked --no-dev --group github-actions + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: coverage-html path: htmlcov github-token: ${{ secrets.GITHUB_TOKEN }} run-id: ${{ github.event.workflow_run.id }} - - - run: smokeshow upload htmlcov + # Try 5 times to upload coverage to smokeshow + - name: Upload coverage to Smokeshow + run: | + for i in 1 2 3 4 5; do + if uv run smokeshow upload htmlcov; then + echo "Smokeshow upload success!" + break + fi + echo "Smokeshow upload error, sleep 1 sec and try again." + sleep 1 + done env: SMOKESHOW_GITHUB_STATUS_DESCRIPTION: Coverage {coverage-percentage} - SMOKESHOW_GITHUB_COVERAGE_THRESHOLD: 95 + SMOKESHOW_GITHUB_COVERAGE_THRESHOLD: 99 SMOKESHOW_GITHUB_CONTEXT: coverage SMOKESHOW_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SMOKESHOW_GITHUB_PR_HEAD_SHA: ${{ github.event.workflow_run.head_sha }} diff --git a/.github/workflows/test-redistribute.yml b/.github/workflows/test-redistribute.yml index 45e22bc4f3..88c24e58b1 100644 --- a/.github/workflows/test-redistribute.yml +++ b/.github/workflows/test-redistribute.yml @@ -12,26 +12,19 @@ on: jobs: test-redistribute: runs-on: ubuntu-latest - strategy: - matrix: - package: - - sqlmodel - - sqlmodel-slim steps: - name: Dump GitHub context env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v4 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: - python-version: "3.10" + python-version-file: ".python-version" - name: Install build dependencies run: pip install build - name: Build source distribution - env: - TIANGOLO_BUILD_PACKAGE: ${{ matrix.package }} run: python -m build --sdist - name: Decompress source distribution run: | @@ -40,9 +33,7 @@ jobs: - name: Install test dependencies run: | cd dist/sqlmodel*/ - pip install -r requirements-tests.txt - env: - TIANGOLO_BUILD_PACKAGE: ${{ matrix.package }} + pip install --group tests --editable . - name: Run source distribution tests run: | cd dist/sqlmodel*/ @@ -60,6 +51,6 @@ jobs: runs-on: ubuntu-latest steps: - name: Decide whether the needed jobs succeeded or failed - uses: re-actors/alls-green@release/v1 + uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # v1.2.2 with: jobs: ${{ toJSON(needs) }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fe2b2c025b..8cce657cec 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,63 +18,69 @@ on: # cron every week on monday - cron: "0 0 * * 1" +env: + UV_NO_SYNC: true + jobs: test: - runs-on: ubuntu-latest strategy: matrix: - python-version: - - "3.7" - - "3.8" - - "3.9" - - "3.10" - - "3.11" - - "3.12" - pydantic-version: - - pydantic-v1 - - pydantic-v2 + os: [ ubuntu-latest, windows-latest, macos-latest ] + python-version: [ "3.14" ] + uv-resolution: + - highest + include: + - os: ubuntu-latest + python-version: "3.10" + uv-resolution: lowest-direct + - os: macos-latest + python-version: "3.11" + uv-resolution: highest + - os: windows-latest + python-version: "3.12" + uv-resolution: lowest-direct + - os: ubuntu-latest + python-version: "3.13" + uv-resolution: highest + - os: macos-latest + python-version: "3.13" + uv-resolution: highest fail-fast: false - + runs-on: ${{ matrix.os }} + env: + UV_PYTHON: ${{ matrix.python-version }} + UV_RESOLUTION: ${{ matrix.uv-resolution }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: ${{ matrix.python-version }} + - name: Setup uv + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 + with: + enable-cache: true + cache-dependency-glob: | + pyproject.toml + uv.lock # Allow debugging with tmate - name: Setup tmate session - uses: mxschmitt/action-tmate@v3 + uses: mxschmitt/action-tmate@c0afd6f790e3a5564914980036ebf83216678101 # v3.23 if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled == 'true' }} with: limit-access-to-actor: true - - uses: actions/cache@v4 - id: cache - with: - path: ${{ env.pythonLocation }} - key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml', 'requirements-tests.txt') }}-v01 - name: Install Dependencies - if: steps.cache.outputs.cache-hit != 'true' - run: pip install -r requirements-tests.txt - - name: Install Pydantic v1 - if: matrix.pydantic-version == 'pydantic-v1' - run: pip install --upgrade "pydantic>=1.10.0,<2.0.0" - - name: Install Pydantic v2 - if: matrix.pydantic-version == 'pydantic-v2' - run: pip install --upgrade "pydantic>=2.0.2,<3.0.0" "typing-extensions==4.6.1" - - name: Lint - # Do not run on Python 3.7 as mypy behaves differently - if: matrix.python-version != '3.7' && matrix.pydantic-version == 'pydantic-v2' - run: bash scripts/lint.sh + run: uv sync --no-dev --group tests - run: mkdir coverage - name: Test - run: bash scripts/test.sh + run: uv run bash scripts/test.sh env: - COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }} + COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }}-${{ matrix.pydantic-version }} CONTEXT: ${{ runner.os }}-py${{ matrix.python-version }} - name: Store coverage files - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: coverage-${{ matrix.python-version }}-${{ matrix.pydantic-version }} + name: coverage-${{ runner.os }}-${{ matrix.python-version }}-${{ matrix.pydantic-version }} path: coverage include-hidden-files: true @@ -82,34 +88,36 @@ jobs: needs: - test runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-python@v5 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: - python-version: '3.12' - + python-version-file: ".python-version" + - name: Setup uv + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 + with: + enable-cache: true + cache-dependency-glob: | + pyproject.toml + uv.lock - name: Get coverage files - uses: actions/download-artifact@v4 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: pattern: coverage-* path: coverage merge-multiple: true - - - run: pip install coverage[toml] - + - name: Install Dependencies + run: uv sync --locked --no-dev --group tests - run: ls -la coverage - - run: coverage combine coverage - - run: coverage report - - run: coverage html --title "Coverage for ${{ github.sha }}" - + - run: uv run coverage combine coverage + - run: uv run coverage html --title "Coverage for ${{ github.sha }}" - name: Store coverage HTML - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: coverage-html path: htmlcov include-hidden-files: true + - run: uv run coverage report --fail-under=99 # https://github.com/marketplace/actions/alls-green#why alls-green: # This job does nothing and is only used for the branch protection @@ -119,6 +127,6 @@ jobs: runs-on: ubuntu-latest steps: - name: Decide whether the needed jobs succeeded or failed - uses: re-actors/alls-green@release/v1 + uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # v1.2.2 with: jobs: ${{ toJSON(needs) }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3175140622..f9e13ea775 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,25 +1,56 @@ # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks -default_language_version: - python: python3.10 repos: -- repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: 3e8a8703264a2f4a69428a0aa4dcb512790b2c8c # v6.0.0 hooks: - - id: check-added-large-files - - id: check-toml - - id: check-yaml + - id: check-added-large-files + - id: check-toml + - id: check-yaml args: - - --unsafe - - id: end-of-file-fixer - - id: trailing-whitespace -- repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.6.2 + - --unsafe + - id: end-of-file-fixer + - id: trailing-whitespace + + - repo: local hooks: - - id: ruff - args: - - --fix - - id: ruff-format -ci: - autofix_commit_msg: 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks - autoupdate_commit_msg: ⬆ [pre-commit.ci] pre-commit autoupdate + - id: local-ruff-check + name: ruff check + entry: uv run ruff check --force-exclude --fix --exit-non-zero-on-fix + require_serial: true + language: unsupported + types: [python] + + - id: local-ruff-format + name: ruff format + entry: uv run ruff format --force-exclude --exit-non-zero-on-format + require_serial: true + language: unsupported + types: [python] + + - id: local-ty + name: ty check + entry: uv run ty check sqlmodel tests/test_select_typing.py + require_serial: true + language: unsupported + pass_filenames: false + + - id: generate-select + language: unsupported + name: generate-select + entry: uv run ./scripts/generate_select.py + files: ^scripts/generate_select\.py|sqlmodel/sql/_expression_select_gen\.py\.jinja2$ + + - id: add-release-date + language: unsupported + name: add date to latest release header + entry: uv run python scripts/add_latest_release_date.py + files: ^docs/release-notes\.md$ + pass_filenames: false + + - id: generate-readme + language: unsupported + name: generate README.md from index.md + entry: uv run ./scripts/docs.py generate-readme + files: ^docs/index\.md|scripts/docs\.py$ + pass_filenames: false diff --git a/.python-version b/.python-version new file mode 100644 index 0000000000..c8cfe39591 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.10 diff --git a/README.md b/README.md index fc38789b7c..712167ffd6 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,11 @@ SQLModel, SQL databases in Python, designed for simplicity, compatibility, and robustness.
-
-
+
+
-
+
@@ -105,16 +105,14 @@ And you want it to have this data:
Then you could create a **SQLModel** model like this:
```Python
-from typing import Optional
-
from sqlmodel import Field, SQLModel
class Hero(SQLModel, table=True):
- id: Optional[int] = Field(default=None, primary_key=True)
+ id: int | None = Field(default=None, primary_key=True)
name: str
secret_name: str
- age: Optional[int] = None
+ age: int | None = None
```
That class `Hero` is a **SQLModel** model, the equivalent of a SQL table in Python code.
@@ -149,17 +147,15 @@ And **inline errors**:
You can learn a lot more about **SQLModel** by quickly following the **tutorial**, but if you need a taste right now of how to put all that together and save to the database, you can do this:
-```Python hl_lines="18 21 23-27"
-from typing import Optional
-
+```Python hl_lines="16 19 21-25"
from sqlmodel import Field, Session, SQLModel, create_engine
class Hero(SQLModel, table=True):
- id: Optional[int] = Field(default=None, primary_key=True)
+ id: int | None = Field(default=None, primary_key=True)
name: str
secret_name: str
- age: Optional[int] = None
+ age: int | None = None
hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson")
@@ -185,17 +181,15 @@ That will save a **SQLite** database with the 3 heroes.
Then you could write queries to select from that same database, for example with:
-```Python hl_lines="15-18"
-from typing import Optional
-
+```Python hl_lines="13-17"
from sqlmodel import Field, Session, SQLModel, create_engine, select
class Hero(SQLModel, table=True):
- id: Optional[int] = Field(default=None, primary_key=True)
+ id: int | None = Field(default=None, primary_key=True)
name: str
secret_name: str
- age: Optional[int] = None
+ age: int | None = None
engine = create_engine("sqlite:///database.db")
diff --git a/data/members.yml b/data/members.yml
index 4dba986672..7a51cbc9ac 100644
--- a/data/members.yml
+++ b/data/members.yml
@@ -1,4 +1,3 @@
members:
- login: tiangolo
-- login: estebanx64
- login: alejsdev
diff --git a/docs/advanced/decimal.md b/docs/advanced/decimal.md
index 26994eccf8..ce971b201b 100644
--- a/docs/advanced/decimal.md
+++ b/docs/advanced/decimal.md
@@ -33,45 +33,7 @@ For the database, **SQLModel** will use `UUID` types.
@@ -132,49 +94,7 @@ As `uuid.uuid4` will be called when creating the model instance, even before sen
And that **same ID (a UUID)** will be saved in the database.
-//// tab | Python 3.10+
-
-```Python hl_lines="5 7 9 14"
-# Code above omitted 👆
-
-{!./docs_src/advanced/uuid/tutorial001_py310.py[ln:23-34]!}
-
-# Code below omitted 👇
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python hl_lines="5 7 9 14"
-# Code above omitted 👆
-
-{!./docs_src/advanced/uuid/tutorial001.py[ln:24-35]!}
-
-# Code below omitted 👇
-```
-
-////
-
-/// details | 👀 Full file preview
-
-//// tab | Python 3.10+
-
-```Python
-{!./docs_src/advanced/uuid/tutorial001_py310.py!}
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python
-{!./docs_src/advanced/uuid/tutorial001.py!}
-```
-
-////
-
-///
+{* ./docs_src/advanced/uuid/tutorial001_py310.py ln[23:34] hl[25,27,29,34] *}
### Select a Hero
@@ -182,49 +102,7 @@ We can do the same operations we could do with other fields.
For example we can **select a hero by ID**:
-//// tab | Python 3.10+
-
-```Python hl_lines="15"
-# Code above omitted 👆
-
-{!./docs_src/advanced/uuid/tutorial001_py310.py[ln:37-54]!}
-
-# Code below omitted 👇
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python hl_lines="15"
-# Code above omitted 👆
-
-{!./docs_src/advanced/uuid/tutorial001.py[ln:38-55]!}
-
-# Code below omitted 👇
-```
-
-////
-
-/// details | 👀 Full file preview
-
-//// tab | Python 3.10+
-
-```Python
-{!./docs_src/advanced/uuid/tutorial001_py310.py!}
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python
-{!./docs_src/advanced/uuid/tutorial001.py!}
-```
-
-////
-
-///
+{* ./docs_src/advanced/uuid/tutorial001_py310.py ln[37:54] hl[49] *}
/// tip
@@ -238,49 +116,7 @@ SQLModel (actually SQLAlchemy) will take care of making it work. ✨
We could also select by ID with `session.get()`:
-//// tab | Python 3.10+
-
-```Python hl_lines="15"
-# Code above omitted 👆
-
-{!./docs_src/advanced/uuid/tutorial002_py310.py[ln:37-54]!}
-
-# Code below omitted 👇
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python hl_lines="15"
-# Code above omitted 👆
-
-{!./docs_src/advanced/uuid/tutorial002.py[ln:38-55]!}
-
-# Code below omitted 👇
-```
-
-////
-
-/// details | 👀 Full file preview
-
-//// tab | Python 3.10+
-
-```Python
-{!./docs_src/advanced/uuid/tutorial002_py310.py!}
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python
-{!./docs_src/advanced/uuid/tutorial002.py!}
-```
-
-////
-
-///
+{* ./docs_src/advanced/uuid/tutorial002_py310.py ln[37:53] hl[49] *}
The same way as with other fields, we could update, delete, etc. 🚀
diff --git a/docs/contributing.md b/docs/contributing.md
index e3693c5a8d..5da90a9f57 100644
--- a/docs/contributing.md
+++ b/docs/contributing.md
@@ -6,18 +6,14 @@ First, you might want to see the basic ways to [help SQLModel and get help](help
If you already cloned the sqlmodel repository and you want to deep dive in the code, here are some guidelines to set up your environment.
-### Virtual Environment
+### Install Requirements Using `uv`
-Follow the instructions to create and activate a [virtual environment](virtual-environments.md){.internal-link target=_blank} for the internal code of `sqlmodel`.
-
-### Install Requirements Using `pip`
-
-After activating the environment, install the required packages:
+Create a virtual environment and install the required packages in one command:
+With great+ +Avoid inadvertently doing harm. + +You have amazing tools at hand, use them wisely to help effectively. diff --git a/docs/css/custom.css b/docs/css/custom.css index 200ac45cd6..a1267e60bd 100644 --- a/docs/css/custom.css +++ b/docs/css/custom.css @@ -1,3 +1,18 @@ +/* Fira Code, including characters used by Rich output, like the "heavy right-pointing angle bracket ornament", not included in Google Fonts */ +@import url(https://cdn.jsdelivr.net/npm/firacode@6.2.0/distr/fira_code.css); +/* Noto Color Emoji for emoji support with the same font everywhere */ +@import url(https://fonts.googleapis.com/css2?family=Noto+Color+Emoji&display=swap); + +/* Override default code font in Material for MkDocs to Fira Code */ +:root { + --md-code-font: "Fira Code", monospace, "Noto Color Emoji"; +} + +/* Override default regular font in Material for MkDocs to include Noto Color Emoji */ +:root { + --md-text-font: "Roboto", "Noto Color Emoji"; +} + .termynal-comment { color: #4a968f; font-style: italic; @@ -12,18 +27,70 @@ display: none; } -a.external-link::after { - /* \00A0 is a non-breaking space - to make the mark be on the same line as the link - */ - content: "\00A0[↪]"; +/* External links: detected by JS comparing origin to site origin + JS sets data-external-link on links pointing outside the site + Skip image links, .no-link-icon, and .announce-link */ +a[data-external-link]:not(:has(img)):not(.no-link-icon):not(.announce-link) { + /* For right to left languages */ + direction: ltr; + display: inline-block; +} + +a[data-external-link]:not(:has(img)):not(.no-link-icon):not(.announce-link)::after { + content: ""; + display: inline-block; + width: 0.75em; + height: 0.75em; + margin-left: 0.25em; + vertical-align: middle; + opacity: 0.55; + background: currentColor; + -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6'/%3E%3Cpolyline points='15 3 21 3 21 9'/%3E%3Cline x1='10' y1='14' x2='21' y2='3'/%3E%3C/svg%3E"); + -webkit-mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6'/%3E%3Cpolyline points='15 3 21 3 21 9'/%3E%3Cline x1='10' y1='14' x2='21' y2='3'/%3E%3C/svg%3E"); + mask-size: contain; + mask-repeat: no-repeat; +} + +a[data-external-link]:not(:has(img)):not(.no-link-icon):not(.announce-link):hover::after { + opacity: 0.85; +} + +/* Internal links opening in new tab: same-origin links with target=_blank + JS sets data-internal-link on links pointing to the same site origin + Skip image links, .no-link-icon, and .announce-link */ +a[data-internal-link][target="_blank"]:not(:has(img)):not(.no-link-icon):not(.announce-link) { + /* For right to left languages */ + direction: ltr; + display: inline-block; +} + +a[data-internal-link][target="_blank"]:not(:has(img)):not(.no-link-icon):not(.announce-link)::after { + content: ""; + display: inline-block; + width: 0.75em; + height: 0.75em; + margin-left: 0.25em; + vertical-align: middle; + opacity: 0.55; + background: currentColor; + -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='7' width='14' height='14' rx='2'/%3E%3Cpath d='M7 3h14v14'/%3E%3C/svg%3E"); + -webkit-mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='7' width='14' height='14' rx='2'/%3E%3Cpath d='M7 3h14v14'/%3E%3C/svg%3E"); + mask-size: contain; + mask-repeat: no-repeat; +} + +a[data-internal-link][target="_blank"]:not(:has(img)):not(.no-link-icon):not(.announce-link):hover::after { + opacity: 0.85; } -a.internal-link::after { - /* \00A0 is a non-breaking space - to make the mark be on the same line as the link - */ - content: "\00A0↪"; +/* Disable link icons in footer and header nav */ +.md-footer a::after, +.md-header a::after { + content: none !important; } .shadow { diff --git a/docs/css/termynal.css b/docs/css/termynal.css index 8534f91021..a2564e2860 100644 --- a/docs/css/termynal.css +++ b/docs/css/termynal.css @@ -20,7 +20,7 @@ /* font-size: 18px; */ font-size: 15px; /* font-family: 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier, monospace; */ - font-family: 'Roboto Mono', 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier, monospace; + font-family: var(--md-code-font-family), 'Roboto Mono', 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier, monospace; border-radius: 4px; padding: 75px 45px 35px; position: relative; diff --git a/docs/databases.md b/docs/databases.md index e874767268..de25ebb4cb 100644 --- a/docs/databases.md +++ b/docs/databases.md @@ -66,9 +66,9 @@ There are many databases of many types. ### A single file database -A database could be a single file called `heroes.db`, managed with code in a very efficient way. An example would be SQLite, more about that on a bit. +A database could be a single file called `heroes.db`, managed with code in a very efficient way. An example would be SQLite, more about that in a bit. - + ### A server database @@ -80,11 +80,11 @@ In this case, your code would talk to this server application instead of reading The database could be located in a different server/machine: - + Or the database could be located in the same server/machine: - + The most important aspect of these types of databases is that **your code doesn't read or modify** the files containing the data directly. @@ -98,7 +98,7 @@ In some cases, the database could even be a group of server applications running In this case, your code would talk to one or more of these server applications running on different machines. - + Most of the databases that work as server applications also support multiple servers in one way or another. @@ -257,7 +257,7 @@ For example, the table for the teams has the ID `1` for the team `Preventers` an As these **primary key** IDs can uniquely identify each row on the table for teams, we can now go to the table for heroes and refer to those IDs in the table for teams. -powertools comes great responsibility. +
-
+{class="shadow"}
## ORMs and SQL
@@ -253,10 +252,10 @@ For example this class is part of that **Object** Oriented Programming:
```Python
class Hero(SQLModel):
- id: Optional[int] = Field(default=None, primary_key=True)
+ id: int | None = Field(default=None, primary_key=True)
name: str
secret_name: str
- age: Optional[int] = None
+ age: int | None = None
```
* **Relational**: refers to the **SQL Databases**. Remember that they are also called **Relational Databases**, because each of those tables is also called a "**relation**"? That's where the "**Relational**" comes from.
@@ -280,7 +279,7 @@ For example this **Relation** or table:
* **Mapper**: this comes from Math, when there's something that can convert from some set of things to another, that's called a "**mapping function**". That's where the **Mapper** comes from.
-
+
We could also write a **mapping function** in Python that converts from the *set of lowercase letters* to the *set of uppercase letters*, like this:
diff --git a/docs/features.md b/docs/features.md
index f0d56925c7..2f2e873105 100644
--- a/docs/features.md
+++ b/docs/features.md
@@ -76,7 +76,7 @@ Underneath, ✨ a **SQLModel** model is also a **SQLAlchemy** model. ✨
There was **a lot** of research and effort dedicated to make it that way. In particular, there was a lot of effort and experimentation in making a single model be **both a SQLAlchemy model and a Pydantic** model at the same time.
-That means that you get all the power, robustness, and certainty of SQLAlchemy, the most widely used database library in Python.
+That means that you get all the power, robustness, and certainty of SQLAlchemy, the most widely used database library in Python.
**SQLModel** provides its own utilities to improve the developer experience, but underneath, it uses all of SQLAlchemy.
diff --git a/docs/img/databases/external-server.drawio b/docs/img/databases/external-server.drawio
deleted file mode 100644
index 7631d02662..0000000000
--- a/docs/img/databases/external-server.drawio
+++ /dev/null
@@ -1,93 +0,0 @@
-
-
-
+
+
-
+
@@ -118,16 +118,14 @@ And you want it to have this data:
Then you could create a **SQLModel** model like this:
```Python
-from typing import Optional
-
from sqlmodel import Field, SQLModel
class Hero(SQLModel, table=True):
- id: Optional[int] = Field(default=None, primary_key=True)
+ id: int | None = Field(default=None, primary_key=True)
name: str
secret_name: str
- age: Optional[int] = None
+ age: int | None = None
```
That class `Hero` is a **SQLModel** model, the equivalent of a SQL table in Python code.
@@ -162,17 +160,15 @@ And **inline errors**:
You can learn a lot more about **SQLModel** by quickly following the **tutorial**, but if you need a taste right now of how to put all that together and save to the database, you can do this:
-```Python hl_lines="18 21 23-27"
-from typing import Optional
-
+```Python hl_lines="16 19 21-25"
from sqlmodel import Field, Session, SQLModel, create_engine
class Hero(SQLModel, table=True):
- id: Optional[int] = Field(default=None, primary_key=True)
+ id: int | None = Field(default=None, primary_key=True)
name: str
secret_name: str
- age: Optional[int] = None
+ age: int | None = None
hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson")
@@ -198,17 +194,15 @@ That will save a **SQLite** database with the 3 heroes.
Then you could write queries to select from that same database, for example with:
-```Python hl_lines="15-18"
-from typing import Optional
-
+```Python hl_lines="13-17"
from sqlmodel import Field, Session, SQLModel, create_engine, select
class Hero(SQLModel, table=True):
- id: Optional[int] = Field(default=None, primary_key=True)
+ id: int | None = Field(default=None, primary_key=True)
name: str
secret_name: str
- age: Optional[int] = None
+ age: int | None = None
engine = create_engine("sqlite:///database.db")
diff --git a/docs/install.md b/docs/install.md
index 8ac2e04c21..129df3512b 100644
--- a/docs/install.md
+++ b/docs/install.md
@@ -16,11 +16,11 @@ As **SQLModel** is built on top of PostgreSQL (which is also free). But for now we'll stick to SQLite.
diff --git a/docs/js/custom.js b/docs/js/custom.js
index ef64c612a9..29f2bf8524 100644
--- a/docs/js/custom.js
+++ b/docs/js/custom.js
@@ -81,8 +81,11 @@ function setupTermynal() {
}
}
saveBuffer();
+ const inputCommands = useLines.filter(line => line.type === "input").map(line => line.value).join("\n");
+ node.textContent = inputCommands;
const div = document.createElement("div");
- node.replaceWith(div);
+ node.style.display = "none";
+ node.after(div);
const termynal = new Termynal(div, {
lineData: useLines,
noInit: true,
@@ -106,8 +109,36 @@ function setupTermynal() {
loadVisibleTermynals();
}
+function openLinksInNewTab() {
+ const siteUrl = document.querySelector("link[rel='canonical']")?.href
+ || window.location.origin;
+ const siteOrigin = new URL(siteUrl).origin;
+ document.querySelectorAll(".md-content a[href]").forEach(a => {
+ if (a.getAttribute("target") === "_self") return;
+ const href = a.getAttribute("href");
+ if (!href) return;
+ try {
+ const url = new URL(href, window.location.href);
+ // Skip same-page anchor links (only the hash differs)
+ if (url.origin === window.location.origin
+ && url.pathname === window.location.pathname
+ && url.search === window.location.search) return;
+ if (!a.hasAttribute("target")) {
+ a.setAttribute("target", "_blank");
+ a.setAttribute("rel", "noopener");
+ }
+ if (url.origin !== siteOrigin) {
+ a.dataset.externalLink = "";
+ } else {
+ a.dataset.internalLink = "";
+ }
+ } catch (_) {}
+ });
+}
+
async function main() {
- setupTermynal()
+ setupTermynal();
+ openLinksInNewTab();
}
document$.subscribe(() => {
diff --git a/docs/learn/index.md b/docs/learn/index.md
index 5a8253c4f6..bcf8a0b0f5 100644
--- a/docs/learn/index.md
+++ b/docs/learn/index.md
@@ -4,4 +4,4 @@ Learn how to use **SQLModel** here.
This includes an introduction to **databases**, **SQL**, how to interact with databases from **code** and more.
-You could consider this a **book**, a **course**, the **official** and recommended way to learn **SQLModel**. 😎
+You could consider this a **book**, a **course**, and the **official** recommended way to learn **SQLModel**. 😎
diff --git a/docs/management-tasks.md b/docs/management-tasks.md
index f8deb992f0..a1eb0b1863 100644
--- a/docs/management-tasks.md
+++ b/docs/management-tasks.md
@@ -68,7 +68,7 @@ Make sure you use a supported label from the `Questions` that are `Unanswered`.
diff --git a/docs/release-notes.md b/docs/release-notes.md
index 722b5b9b83..347b215edc 100644
--- a/docs/release-notes.md
+++ b/docs/release-notes.md
@@ -2,20 +2,467 @@
## Latest Changes
+### Internal
+
+* ⬆ Bump cloudflare/wrangler-action from 3.14.1 to 3.15.0. PR [#1868](https://github.com/fastapi/sqlmodel/pull/1868) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump ty from 0.0.30 to 0.0.31. PR [#1862](https://github.com/fastapi/sqlmodel/pull/1862) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump actions/cache from 5.0.4 to 5.0.5. PR [#1865](https://github.com/fastapi/sqlmodel/pull/1865) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump actions/upload-artifact from 7.0.0 to 7.0.1. PR [#1867](https://github.com/fastapi/sqlmodel/pull/1867) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump astral-sh/setup-uv from 7.6.0 to 8.1.0. PR [#1866](https://github.com/fastapi/sqlmodel/pull/1866) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump fastapi from 0.135.3 to 0.136.0. PR [#1869](https://github.com/fastapi/sqlmodel/pull/1869) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump ruff from 0.15.10 to 0.15.11. PR [#1870](https://github.com/fastapi/sqlmodel/pull/1870) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* 🔒 Pin GitHub actions by commit SHA. PR [#1840](https://github.com/fastapi/sqlmodel/pull/1840) by [@YuriiMotov](https://github.com/YuriiMotov).
+* ⬆ Bump ty from 0.0.29 to 0.0.30. PR [#1861](https://github.com/fastapi/sqlmodel/pull/1861) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump prek from 0.3.8 to 0.3.9. PR [#1859](https://github.com/fastapi/sqlmodel/pull/1859) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump ruff from 0.15.9 to 0.15.10. PR [#1856](https://github.com/fastapi/sqlmodel/pull/1856) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump pytest from 9.0.2 to 9.0.3. PR [#1855](https://github.com/fastapi/sqlmodel/pull/1855) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump ty from 0.0.28 to 0.0.29. PR [#1854](https://github.com/fastapi/sqlmodel/pull/1854) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* 🔥 Remove outdated `needs_py310` marker from tests. PR [#1852](https://github.com/fastapi/sqlmodel/pull/1852) by [@stickm4n](https://github.com/stickm4n).
+* ⬆ Bump sqlalchemy from 2.0.48 to 2.0.49. PR [#1850](https://github.com/fastapi/sqlmodel/pull/1850) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump ty from 0.0.26 to 0.0.28. PR [#1849](https://github.com/fastapi/sqlmodel/pull/1849) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* 👷 Replace `mypy` with `ty` in precommit. PR [#1806](https://github.com/fastapi/sqlmodel/pull/1806) by [@svlandeg](https://github.com/svlandeg).
+* ⬆ Bump ruff from 0.15.7 to 0.15.9. PR [#1847](https://github.com/fastapi/sqlmodel/pull/1847) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump mypy from 1.19.1 to 1.20.0. PR [#1843](https://github.com/fastapi/sqlmodel/pull/1843) by [@dependabot[bot]](https://github.com/apps/dependabot).
+
+## 0.0.38 (2026-04-02)
+
+### Fixes
+
+* 🐛 Fix type annotation in `SQLModel.__new__`, avoid explicitly returning `Any`. PR [#1846](https://github.com/fastapi/sqlmodel/pull/1846) by [@carljm](https://github.com/carljm).
+* 🐛 Fix `tuple_` return type annotation. PR [#1639](https://github.com/fastapi/sqlmodel/pull/1639) by [@kakeruzoku](https://github.com/kakeruzoku).
+
+### Docs
+
+* ✏️ Fix typos in `contributing.md`. PR [#1842](https://github.com/fastapi/sqlmodel/pull/1842) by [@GopalGB](https://github.com/GopalGB).
+* 🔥 Remove outdated Python 3.9 tutorial file. PR [#1822](https://github.com/fastapi/sqlmodel/pull/1822) by [@svlandeg](https://github.com/svlandeg).
+* 📝 Fix ambiguous phrasing regarding `HeroPublicWithTeam` model. PR [#1678](https://github.com/fastapi/sqlmodel/pull/1678) by [@berkaykrc](https://github.com/berkaykrc).
+* 🔨 Handle external links `target=_blank` and CSS automatically in JS and CSS. PR [#1799](https://github.com/fastapi/sqlmodel/pull/1799) by [@tiangolo](https://github.com/tiangolo).
+* 📝 Document `.in_()` method. PR [#619](https://github.com/fastapi/sqlmodel/pull/619) by [@masylum](https://github.com/masylum).
+* 📝 Fix small typos in the documentation. PR [#1641](https://github.com/fastapi/sqlmodel/pull/1641) by [@svlandeg](https://github.com/svlandeg).
+
+### Internal
+
+* 🔨 Add pre-commit hook to ensure latest release header has date. PR [#1786](https://github.com/fastapi/sqlmodel/pull/1786) by [@YuriiMotov](https://github.com/YuriiMotov).
+* ⬆ Bump pillow from 12.1.1 to 12.2.0. PR [#1845](https://github.com/fastapi/sqlmodel/pull/1845) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump fastapi from 0.135.2 to 0.135.3. PR [#1844](https://github.com/fastapi/sqlmodel/pull/1844) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump prek from 0.3.6 to 0.3.8. PR [#1829](https://github.com/fastapi/sqlmodel/pull/1829) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump pygithub from 2.8.1 to 2.9.0. PR [#1827](https://github.com/fastapi/sqlmodel/pull/1827) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump fastapi from 0.135.1 to 0.135.2. PR [#1828](https://github.com/fastapi/sqlmodel/pull/1828) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ➕ Add a direct dependency on `typing-extensions`. PR [#1815](https://github.com/fastapi/sqlmodel/pull/1815) by [@musicinmybrain](https://github.com/musicinmybrain).
+* ⬆ Bump mkdocs-material from 9.7.5 to 9.7.6. PR [#1825](https://github.com/fastapi/sqlmodel/pull/1825) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump ruff from 0.15.6 to 0.15.7. PR [#1826](https://github.com/fastapi/sqlmodel/pull/1826) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump prek from 0.3.5 to 0.3.6. PR [#1817](https://github.com/fastapi/sqlmodel/pull/1817) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* 📌 Update internal dependency limits. PR [#1809](https://github.com/fastapi/sqlmodel/pull/1809) by [@svlandeg](https://github.com/svlandeg).
+* ⬆ Bump ruff from 0.15.5 to 0.15.6. PR [#1814](https://github.com/fastapi/sqlmodel/pull/1814) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump cairosvg from 2.8.2 to 2.9.0. PR [#1813](https://github.com/fastapi/sqlmodel/pull/1813) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump dorny/paths-filter from 3 to 4. PR [#1812](https://github.com/fastapi/sqlmodel/pull/1812) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump black from 26.3.0 to 26.3.1. PR [#1811](https://github.com/fastapi/sqlmodel/pull/1811) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump mkdocs-material from 9.7.4 to 9.7.5. PR [#1808](https://github.com/fastapi/sqlmodel/pull/1808) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump prek from 0.3.4 to 0.3.5. PR [#1807](https://github.com/fastapi/sqlmodel/pull/1807) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump black from 26.1.0 to 26.3.0. PR [#1803](https://github.com/fastapi/sqlmodel/pull/1803) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump ruff from 0.15.4 to 0.15.5. PR [#1801](https://github.com/fastapi/sqlmodel/pull/1801) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump mkdocs-material from 9.7.3 to 9.7.4. PR [#1797](https://github.com/fastapi/sqlmodel/pull/1797) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump sqlalchemy from 2.0.47 to 2.0.48. PR [#1793](https://github.com/fastapi/sqlmodel/pull/1793) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump prek from 0.3.3 to 0.3.4. PR [#1794](https://github.com/fastapi/sqlmodel/pull/1794) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump fastapi from 0.134.0 to 0.135.1. PR [#1795](https://github.com/fastapi/sqlmodel/pull/1795) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump fastapi from 0.133.1 to 0.134.0. PR [#1792](https://github.com/fastapi/sqlmodel/pull/1792) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump ruff from 0.15.2 to 0.15.4. PR [#1790](https://github.com/fastapi/sqlmodel/pull/1790) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump actions/download-artifact from 7 to 8. PR [#1789](https://github.com/fastapi/sqlmodel/pull/1789) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump actions/upload-artifact from 6 to 7. PR [#1788](https://github.com/fastapi/sqlmodel/pull/1788) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump fastapi from 0.133.0 to 0.133.1. PR [#1787](https://github.com/fastapi/sqlmodel/pull/1787) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump mkdocs-material from 9.7.2 to 9.7.3. PR [#1782](https://github.com/fastapi/sqlmodel/pull/1782) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump fastapi from 0.132.0 to 0.133.0. PR [#1781](https://github.com/fastapi/sqlmodel/pull/1781) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump sqlalchemy from 2.0.46 to 2.0.47. PR [#1783](https://github.com/fastapi/sqlmodel/pull/1783) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump pytest from 8.4.2 to 9.0.2. PR [#1780](https://github.com/fastapi/sqlmodel/pull/1780) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump mkdocstrings[python] from 0.30.1 to 1.0.3. PR [#1776](https://github.com/fastapi/sqlmodel/pull/1776) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump typer from 0.23.2 to 0.24.1. PR [#1777](https://github.com/fastapi/sqlmodel/pull/1777) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump ruff from 0.15.1 to 0.15.2. PR [#1779](https://github.com/fastapi/sqlmodel/pull/1779) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump fastapi from 0.129.0 to 0.132.0. PR [#1778](https://github.com/fastapi/sqlmodel/pull/1778) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Update all dependencies to use `griffelib` instead of `griffe`. PR [#1775](https://github.com/fastapi/sqlmodel/pull/1775) by [@svlandeg](https://github.com/svlandeg).
+* ⬆ Bump mkdocs-material from 9.7.1 to 9.7.2. PR [#1764](https://github.com/fastapi/sqlmodel/pull/1764) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump pillow from 11.3.0 to 12.1.1. PR [#1770](https://github.com/fastapi/sqlmodel/pull/1770) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump pydantic-settings from 2.12.0 to 2.13.1. PR [#1771](https://github.com/fastapi/sqlmodel/pull/1771) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump fastapi from 0.128.8 to 0.129.0. PR [#1769](https://github.com/fastapi/sqlmodel/pull/1769) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump black from 25.12.0 to 26.1.0. PR [#1768](https://github.com/fastapi/sqlmodel/pull/1768) by [@dependabot[bot]](https://github.com/apps/dependabot).
+
+## 0.0.37 (2026-02-21)
+
+### Internal
+
+* 👷 Fix build CI to not attempt to build `sqlmodel-slim`. PR [#1773](https://github.com/fastapi/sqlmodel/pull/1773) by [@tiangolo](https://github.com/tiangolo).
+
+## 0.0.36 (2026-02-21)
+
+### Internal
+
+* ➖ Drop support for `sqlmodel-slim`, no more versions will be released, use only `sqmodel`. PR [#1772](https://github.com/fastapi/sqlmodel/pull/1772) by [@tiangolo](https://github.com/tiangolo).
+
+## 0.0.35 (2026-02-20)
+
+### Breaking Changes
+
+* ➖ Drop support for Python 3.9. PR [#1766](https://github.com/fastapi/sqlmodel/pull/1766) by [@tiangolo](https://github.com/tiangolo).
+
+### Internal
+
+* 🔨 Add script to remove Python 3.9 files, migrate to Python 3.10. PR [#1767](https://github.com/fastapi/sqlmodel/pull/1767) by [@tiangolo](https://github.com/tiangolo).
+* ⬆ Bump prek from 0.3.2 to 0.3.3. PR [#1761](https://github.com/fastapi/sqlmodel/pull/1761) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump typer from 0.23.1 to 0.23.2. PR [#1760](https://github.com/fastapi/sqlmodel/pull/1760) by [@dependabot[bot]](https://github.com/apps/dependabot).
+
+## 0.0.34 (2026-02-16)
+
+### Internal
+
+* 👷 Enable tests with lower and upper bound versions. PR [#1735](https://github.com/fastapi/sqlmodel/pull/1735) by [@tiangolo](https://github.com/tiangolo).
+* ⬆ Bump ruff from 0.15.0 to 0.15.1. PR [#1758](https://github.com/fastapi/sqlmodel/pull/1758) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump typer from 0.23.0 to 0.23.1. PR [#1759](https://github.com/fastapi/sqlmodel/pull/1759) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump typer from 0.21.2 to 0.23.0. PR [#1756](https://github.com/fastapi/sqlmodel/pull/1756) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump fastapi from 0.128.7 to 0.128.8. PR [#1755](https://github.com/fastapi/sqlmodel/pull/1755) by [@dependabot[bot]](https://github.com/apps/dependabot).
+
+## 0.0.33 (2026-02-11)
+
+### Docs
+
+* ✏️ Fix typos in inline comment in `expression.py`. PR [#1747](https://github.com/fastapi/sqlmodel/pull/1747) by [@veeceey](https://github.com/veeceey).
+* 📝 Fix async example in `session.execute()` docstring & deprecation message. PR [#1643](https://github.com/fastapi/sqlmodel/pull/1643) by [@DanielLeviLucas](https://github.com/DanielLeviLucas).
+* 📝 Update `management-tasks.md` to be in line with `management-tasks.md` in FastAPI repo. PR [#1743](https://github.com/fastapi/sqlmodel/pull/1743) by [@YuriiMotov](https://github.com/YuriiMotov).
+
+### Internal
+
+* 🔨 Update build script for `sqlmodel-slim`. PR [#1754](https://github.com/fastapi/sqlmodel/pull/1754) by [@tiangolo](https://github.com/tiangolo).
+* ⬆ Bump fastapi from 0.128.6 to 0.128.7. PR [#1751](https://github.com/fastapi/sqlmodel/pull/1751) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump typer from 0.21.1 to 0.21.2. PR [#1752](https://github.com/fastapi/sqlmodel/pull/1752) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* 👷 Update build setup for `sqlmodel-slim`, deprecate it, and make it only depend on `sqlmodel`. PR [#1753](https://github.com/fastapi/sqlmodel/pull/1753) by [@tiangolo](https://github.com/tiangolo).
+* ⬆ Bump fastapi from 0.128.3 to 0.128.6. PR [#1750](https://github.com/fastapi/sqlmodel/pull/1750) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump fastapi from 0.128.1 to 0.128.3. PR [#1746](https://github.com/fastapi/sqlmodel/pull/1746) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump prek from 0.3.1 to 0.3.2. PR [#1745](https://github.com/fastapi/sqlmodel/pull/1745) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump fastapi from 0.128.0 to 0.128.1. PR [#1742](https://github.com/fastapi/sqlmodel/pull/1742) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ✅ Update `ValidationError` schema in FastAPI-related tests to be compatible with FastAPI 0.128.1+. PR [#1744](https://github.com/fastapi/sqlmodel/pull/1744) by [@YuriiMotov](https://github.com/YuriiMotov).
+* ⬆ Bump ruff from 0.14.14 to 0.15.0. PR [#1740](https://github.com/fastapi/sqlmodel/pull/1740) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* 👷 Add generate-readme to pre-commit. PR [#1741](https://github.com/fastapi/sqlmodel/pull/1741) by [@tiangolo](https://github.com/tiangolo).
+* 👷 Run mypy by pre-commit. PR [#1738](https://github.com/fastapi/sqlmodel/pull/1738) by [@YuriiMotov](https://github.com/YuriiMotov).
+* ⬆ Bump prek from 0.3.0 to 0.3.1. PR [#1739](https://github.com/fastapi/sqlmodel/pull/1739) by [@dependabot[bot]](https://github.com/apps/dependabot).
+
+## 0.0.32 (2026-02-01)
+
+### Fixes
+
+* 🐛 Fix support for `Annotated` fields with Pydantic 2.12+. PR [#1607](https://github.com/fastapi/sqlmodel/pull/1607) by [@vimota](https://github.com/vimota).
+
+### Refactors
+
+* ♻️ Import `Literal` from the `typing` module directly. PR [#1699](https://github.com/fastapi/sqlmodel/pull/1699) by [@svlandeg](https://github.com/svlandeg).
+
+### Docs
+
+* 📝 Add contribution instructions about LLM generated code and comments and automated tools for PRs. PR [#1712](https://github.com/fastapi/sqlmodel/pull/1712) by [@alejsdev](https://github.com/alejsdev).
+* 🐛 Fix copy button in `custom.js`. PR [#1711](https://github.com/fastapi/sqlmodel/pull/1711) by [@alejsdev](https://github.com/alejsdev).
+* 📝 Remove duplicated word in `read-relationships.md`. PR [#1705](https://github.com/fastapi/sqlmodel/pull/1705) by [@stefmolin](https://github.com/stefmolin).
+
+### Internal
+
+* ⬆ Bump ruff from 0.14.13 to 0.14.14. PR [#1721](https://github.com/fastapi/sqlmodel/pull/1721) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump prek from 0.2.30 to 0.3.0. PR [#1720](https://github.com/fastapi/sqlmodel/pull/1720) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* 🔧 Ensure that an edit to `uv.lock` gets the `internal` label. PR [#1719](https://github.com/fastapi/sqlmodel/pull/1719) by [@svlandeg](https://github.com/svlandeg).
+* ⬆ Bump sqlalchemy from 2.0.45 to 2.0.46. PR [#1717](https://github.com/fastapi/sqlmodel/pull/1717) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump typer from 0.21.0 to 0.21.1. PR [#1715](https://github.com/fastapi/sqlmodel/pull/1715) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump ruff from 0.14.10 to 0.14.13. PR [#1714](https://github.com/fastapi/sqlmodel/pull/1714) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump prek from 0.2.25 to 0.2.30. PR [#1716](https://github.com/fastapi/sqlmodel/pull/1716) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆️ Update FastAPI version pin to `>=0.103.2` in tests. PR [#1709](https://github.com/fastapi/sqlmodel/pull/1709) by [@YuriiMotov](https://github.com/YuriiMotov).
+* 📌 Pin development Python version to 3.10, for `deploy_docs_status.py`. PR [#1707](https://github.com/fastapi/sqlmodel/pull/1707) by [@tiangolo](https://github.com/tiangolo).
+* ⬆️ Migrate to uv. PR [#1688](https://github.com/fastapi/sqlmodel/pull/1688) by [@DoctorJohn](https://github.com/DoctorJohn).
+* ⬆ Update fastapi requirement from >=0.103.2,<0.126.0 to >=0.103.2,<0.129.0. PR [#1703](https://github.com/fastapi/sqlmodel/pull/1703) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ✅ Update tests, remove conditionals for Pydantic v1. PR [#1702](https://github.com/fastapi/sqlmodel/pull/1702) by [@tiangolo](https://github.com/tiangolo).
+
+## 0.0.31 (2025-12-28)
+
+### Breaking Changes
+
+* ➖ Drop support for Pydantic v1. PR [#1701](https://github.com/fastapi/sqlmodel/pull/1701) by [@tiangolo](https://github.com/tiangolo).
+
+### Internal
+
+* ⬆ Bump dirty-equals from 0.9.0 to 0.11. PR [#1649](https://github.com/fastapi/sqlmodel/pull/1649) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump mkdocs-material from 9.7.0 to 9.7.1. PR [#1690](https://github.com/fastapi/sqlmodel/pull/1690) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump typer from 0.20.1 to 0.21.0. PR [#1694](https://github.com/fastapi/sqlmodel/pull/1694) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* 📌 Relax `prek` version pin to `>=0.2.24,<1.0.0`. PR [#1698](https://github.com/fastapi/sqlmodel/pull/1698) by [@YuriiMotov](https://github.com/YuriiMotov).
+
+## 0.0.30 (2025-12-26)
+
+### Breaking Changes
+
+* ➖ Drop support for Python 3.8. PR [#1696](https://github.com/fastapi/sqlmodel/pull/1696) by [@tiangolo](https://github.com/tiangolo).
+
+### Docs
+
+* ➖ Drop support for Python 3.8 in CI and docs. PR [#1695](https://github.com/fastapi/sqlmodel/pull/1695) by [@YuriiMotov](https://github.com/YuriiMotov) and [@tiangolo](https://github.com/tiangolo).
+
+### Internal
+
+* 🔧 Update pre-commit, generate select on pre-commit, use local Ruff. PR [#1697](https://github.com/fastapi/sqlmodel/pull/1697) by [@tiangolo](https://github.com/tiangolo).
+* ⬆ Bump actions/checkout from 5 to 6. PR [#1692](https://github.com/fastapi/sqlmodel/pull/1692) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* 👷 Add pre-commit workflow. PR [#1684](https://github.com/fastapi/sqlmodel/pull/1684) by [@YuriiMotov](https://github.com/YuriiMotov).
+* ✅ Simplify tests for code examples, one test file for multiple variants. PR [#1664](https://github.com/fastapi/sqlmodel/pull/1664) by [@YuriiMotov](https://github.com/YuriiMotov).
+* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1677](https://github.com/fastapi/sqlmodel/pull/1677) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
+* ⬆ Bump actions/download-artifact from 6 to 7. PR [#1676](https://github.com/fastapi/sqlmodel/pull/1676) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump actions/cache from 4 to 5. PR [#1673](https://github.com/fastapi/sqlmodel/pull/1673) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump markdown-include-variants from 0.0.5 to 0.0.8. PR [#1674](https://github.com/fastapi/sqlmodel/pull/1674) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump actions/upload-artifact from 5 to 6. PR [#1675](https://github.com/fastapi/sqlmodel/pull/1675) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump mypy from 1.18.2 to 1.19.1. PR [#1679](https://github.com/fastapi/sqlmodel/pull/1679) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump typer from 0.20.0 to 0.20.1. PR [#1685](https://github.com/fastapi/sqlmodel/pull/1685) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump ruff from 0.14.8 to 0.14.10. PR [#1681](https://github.com/fastapi/sqlmodel/pull/1681) by [@dependabot[bot]](https://github.com/apps/dependabot).
+
+## 0.0.29 (2025-12-23)
+
+### Fixes
+
+* 🐛 Fix `alias` support for Pydantic v2. PR [#1577](https://github.com/fastapi/sqlmodel/pull/1577) by [@ravishan16](https://github.com/ravishan16).
+
+## 0.0.28 (2025-12-23)
+
+### Fixes
+
+* 🐛 Fix `RuntimeError: dictionary changed size during iteration` in `sqlmodel_update()`. PR [#997](https://github.com/fastapi/sqlmodel/pull/997) by [@BartSchuurmans](https://github.com/BartSchuurmans).
+
+### Docs
+
+* 💅 Update CSS to explicitly use emoji font. PR [#1658](https://github.com/fastapi/sqlmodel/pull/1658) by [@tiangolo](https://github.com/tiangolo).
+* 📝 Update link to JetBrains Python survey in `features.md`. PR [#1627](https://github.com/fastapi/sqlmodel/pull/1627) by [@sparkiegeek](https://github.com/sparkiegeek).
+* 📝 Fix broken links in docs. PR [#1601](https://github.com/fastapi/sqlmodel/pull/1601) by [@YuriiMotov](https://github.com/YuriiMotov).
+
+### Internal
+
+* 📌 Pin FastAPI in tests to 0.125.0 while dropping support for Python 3.8. PR [#1689](https://github.com/fastapi/sqlmodel/pull/1689) by [@tiangolo](https://github.com/tiangolo).
+* 👷 Configure coverage, error on main tests, don't wait for Smokeshow. PR [#1683](https://github.com/fastapi/sqlmodel/pull/1683) by [@YuriiMotov](https://github.com/YuriiMotov).
+* 👷 Run Smokeshow always, even on test failures. PR [#1682](https://github.com/fastapi/sqlmodel/pull/1682) by [@YuriiMotov](https://github.com/YuriiMotov).
+* ⬆ Bump ruff from 0.14.6 to 0.14.8. PR [#1667](https://github.com/fastapi/sqlmodel/pull/1667) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1662](https://github.com/fastapi/sqlmodel/pull/1662) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
+* ⬆ Bump actions/checkout from 5 to 6. PR [#1656](https://github.com/fastapi/sqlmodel/pull/1656) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump ruff from 0.14.5 to 0.14.6. PR [#1652](https://github.com/fastapi/sqlmodel/pull/1652) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1655](https://github.com/fastapi/sqlmodel/pull/1655) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
+* ⬆ Bump actions/checkout from 5 to 6. PR [#1651](https://github.com/fastapi/sqlmodel/pull/1651) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* 💄 Use font Fira Code to fix display of Rich panels in docs in Windows. PR [#1653](https://github.com/fastapi/sqlmodel/pull/1653) by [@tiangolo](https://github.com/tiangolo).
+* 👷 Upgrade `latest-changes` GitHub Action and pin `actions/checkout@v5`. PR [#1654](https://github.com/fastapi/sqlmodel/pull/1654) by [@svlandeg](https://github.com/svlandeg).
+* 🔧 Upgrade Material for MkDocs and remove insiders. PR [#1650](https://github.com/fastapi/sqlmodel/pull/1650) by [@tiangolo](https://github.com/tiangolo).
+* ⬆ Bump mkdocs-material from 9.6.23 to 9.7.0. PR [#1645](https://github.com/fastapi/sqlmodel/pull/1645) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump mkdocs-macros-plugin from 1.4.1 to 1.5.0. PR [#1647](https://github.com/fastapi/sqlmodel/pull/1647) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump ruff from 0.14.4 to 0.14.5. PR [#1646](https://github.com/fastapi/sqlmodel/pull/1646) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1648](https://github.com/fastapi/sqlmodel/pull/1648) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
+* ⬆ Bump ruff from 0.14.3 to 0.14.4. PR [#1640](https://github.com/fastapi/sqlmodel/pull/1640) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1642](https://github.com/fastapi/sqlmodel/pull/1642) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
+* ⬆ Bump mkdocs-material from 9.6.22 to 9.6.23. PR [#1637](https://github.com/fastapi/sqlmodel/pull/1637) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump ruff from 0.14.2 to 0.14.3. PR [#1633](https://github.com/fastapi/sqlmodel/pull/1633) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1636](https://github.com/fastapi/sqlmodel/pull/1636) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
+* ⬆ Bump mkdocs-macros-plugin from 1.4.0 to 1.4.1. PR [#1626](https://github.com/fastapi/sqlmodel/pull/1626) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump ruff from 0.14.1 to 0.14.2. PR [#1616](https://github.com/fastapi/sqlmodel/pull/1616) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1625](https://github.com/fastapi/sqlmodel/pull/1625) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
+* 🔧 Add PEP-639 license metadata. PR [#1624](https://github.com/fastapi/sqlmodel/pull/1624) by [@svlandeg](https://github.com/svlandeg).
+* ⬆ Bump griffe-typingdoc from 0.2.9 to 0.3.0. PR [#1615](https://github.com/fastapi/sqlmodel/pull/1615) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump actions/upload-artifact from 4 to 5. PR [#1620](https://github.com/fastapi/sqlmodel/pull/1620) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump actions/download-artifact from 5 to 6. PR [#1621](https://github.com/fastapi/sqlmodel/pull/1621) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump ruff from 0.14.0 to 0.14.1. PR [#1614](https://github.com/fastapi/sqlmodel/pull/1614) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump ruff from 0.13.2 to 0.14.0. PR [#1592](https://github.com/fastapi/sqlmodel/pull/1592) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1605](https://github.com/fastapi/sqlmodel/pull/1605) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
+* ⬆ Bump astral-sh/setup-uv from 6 to 7. PR [#1593](https://github.com/fastapi/sqlmodel/pull/1593) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump mkdocs-material from 9.6.21 to 9.6.22. PR [#1608](https://github.com/fastapi/sqlmodel/pull/1608) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* 🔧 Configure reminder for `waiting` label in `issue-manager`. PR [#1609](https://github.com/fastapi/sqlmodel/pull/1609) by [@YuriiMotov](https://github.com/YuriiMotov).
+* ⬆ Bump typer from 0.19.2 to 0.20.0. PR [#1612](https://github.com/fastapi/sqlmodel/pull/1612) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ✅ Remove unused type ignores since SQLAlchemy 2.0.44. PR [#1613](https://github.com/fastapi/sqlmodel/pull/1613) by [@svlandeg](https://github.com/svlandeg).
+
+## 0.0.27 (2025-10-08)
+
+### Upgrades
+
+* ⬆️ Add support for Python 3.14. PR [#1578](https://github.com/fastapi/sqlmodel/pull/1578) by [@svlandeg](https://github.com/svlandeg).
+
+## 0.0.26 (2025-10-08)
+
+### Fixes
+
+* 🐛 Fix attribute handling in `model_dump` for compatibility with the latest Pydantic versions. PR [#1595](https://github.com/fastapi/sqlmodel/pull/1595) by [@spazm](https://github.com/spazm).
+
+### Docs
+
+* 📝 Fix typo in `docs/tutorial/fastapi/simple-hero-api.md`. PR [#1583](https://github.com/fastapi/sqlmodel/pull/1583) by [@kofi-kusi](https://github.com/kofi-kusi).
+
+### Internal
+
+* ⬆ Bump mypy from 1.4.1 to 1.18.2. PR [#1560](https://github.com/fastapi/sqlmodel/pull/1560) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ✅ Add test that runs select with 3 or 4 arguments. PR [#1590](https://github.com/fastapi/sqlmodel/pull/1590) by [@svlandeg](https://github.com/svlandeg).
+* ⬆ Bump mkdocs-macros-plugin from 1.3.9 to 1.4.0. PR [#1581](https://github.com/fastapi/sqlmodel/pull/1581) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump mkdocs-material from 9.6.20 to 9.6.21. PR [#1588](https://github.com/fastapi/sqlmodel/pull/1588) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1584](https://github.com/fastapi/sqlmodel/pull/1584) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
+* ⬆ Bump tiangolo/issue-manager from 0.5.1 to 0.6.0. PR [#1589](https://github.com/fastapi/sqlmodel/pull/1589) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* 👷 Update docs previews comment, single comment, add failure status. PR [#1586](https://github.com/fastapi/sqlmodel/pull/1586) by [@tiangolo](https://github.com/tiangolo).
+* ⬆ Bump markdown-include-variants from 0.0.4 to 0.0.5. PR [#1582](https://github.com/fastapi/sqlmodel/pull/1582) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump typing-extensions from 4.13.2 to 4.15.0 for Python 3.9+. PR [#1580](https://github.com/fastapi/sqlmodel/pull/1580) by [@svlandeg](https://github.com/svlandeg).
+* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1571](https://github.com/fastapi/sqlmodel/pull/1571) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
+* ⬆ Bump typer from 0.17.4 to 0.19.2. PR [#1573](https://github.com/fastapi/sqlmodel/pull/1573) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump ruff from 0.13.0 to 0.13.2. PR [#1576](https://github.com/fastapi/sqlmodel/pull/1576) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* 💚 Fix CI test suite for Windows and MacOS. PR [#1307](https://github.com/fastapi/sqlmodel/pull/1307) by [@svlandeg](https://github.com/svlandeg).
+
+## 0.0.25 (2025-09-17)
+
+### Features
+
+* ✨ Add overload for `exec` method to support `insert`, `update`, `delete` statements. PR [#1342](https://github.com/fastapi/sqlmodel/pull/1342) by [@seriaati](https://github.com/seriaati).
+
+### Upgrades
+
+* ⬆️ Drop support for Python 3.7, require Python 3.8 or above. PR [#1316](https://github.com/fastapi/sqlmodel/pull/1316) by [@svlandeg](https://github.com/svlandeg).
+
+### Docs
+
+* ✏️ Fix typos in `docs/tutorial/relationship-attributes/cascade-delete-relationships.md`. PR [#1543](https://github.com/fastapi/sqlmodel/pull/1543) by [@YuriiMotov](https://github.com/YuriiMotov).
+* 🍱 Update SVG files, a single file per diagram, sans-serif fonts. PR [#1373](https://github.com/fastapi/sqlmodel/pull/1373) by [@tiangolo](https://github.com/tiangolo).
+* 📝 Grammar tweak in `docs/tutorial/insert.md`. PR [#1368](https://github.com/fastapi/sqlmodel/pull/1368) by [@brettcannon](https://github.com/brettcannon).
+* 📝 Update `docs/tutorial/fastapi/relationships.md`. PR [#1365](https://github.com/fastapi/sqlmodel/pull/1365) by [@Foxerine](https://github.com/Foxerine).
+* ✏️ Tweak the grammar in `docs/learn/index.md`. PR [#1363](https://github.com/fastapi/sqlmodel/pull/1363) by [@brettcannon](https://github.com/brettcannon).
+* 📝 Update all docs references to `Optional` to use the new syntax in Python 3.10, e.g. `int | None`. PR [#1351](https://github.com/fastapi/sqlmodel/pull/1351) by [@tiangolo](https://github.com/tiangolo).
+* 📝 Update install and usage with FastAPI CLI in FastAPI tutorial. PR [#1350](https://github.com/fastapi/sqlmodel/pull/1350) by [@tiangolo](https://github.com/tiangolo).
+* 📝 Update FastAPI tutorial docs to use the new `model.sqlmodel_update()` instead of old `setattr()`. PR [#1117](https://github.com/fastapi/sqlmodel/pull/1117) by [@jpizquierdo](https://github.com/jpizquierdo).
+* ✏️ Update `docs/virtual-environments.md`. PR [#1321](https://github.com/fastapi/sqlmodel/pull/1321) by [@sylvainHellin](https://github.com/sylvainHellin).
+
+### Internal
+
+* ⬆ Bump griffe-typingdoc from 0.2.8 to 0.2.9. PR [#1553](https://github.com/fastapi/sqlmodel/pull/1553) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump mkdocs-material from 9.6.17 to 9.6.20. PR [#1565](https://github.com/fastapi/sqlmodel/pull/1565) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump actions/setup-python from 5 to 6. PR [#1551](https://github.com/fastapi/sqlmodel/pull/1551) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump ruff from 0.12.12 to 0.13.0. PR [#1559](https://github.com/fastapi/sqlmodel/pull/1559) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1564](https://github.com/fastapi/sqlmodel/pull/1564) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
+* ⬆ Bump actions/labeler from 5 to 6. PR [#1549](https://github.com/fastapi/sqlmodel/pull/1549) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1556](https://github.com/fastapi/sqlmodel/pull/1556) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
+* ⬆ Bump typer from 0.17.3 to 0.17.4. PR [#1554](https://github.com/fastapi/sqlmodel/pull/1554) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1546](https://github.com/fastapi/sqlmodel/pull/1546) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
+* ⬆ Bump ruff from 0.12.10 to 0.12.12. PR [#1548](https://github.com/fastapi/sqlmodel/pull/1548) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump typer from 0.16.1 to 0.17.3. PR [#1547](https://github.com/fastapi/sqlmodel/pull/1547) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump pypa/gh-action-pypi-publish from 1.12.4 to 1.13.0. PR [#1550](https://github.com/fastapi/sqlmodel/pull/1550) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* 👷 Detect and label merge conflicts on PRs automatically. PR [#1552](https://github.com/fastapi/sqlmodel/pull/1552) by [@svlandeg](https://github.com/svlandeg).
+* ⬆ Bump ruff from 0.12.9 to 0.12.10. PR [#1532](https://github.com/fastapi/sqlmodel/pull/1532) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1534](https://github.com/fastapi/sqlmodel/pull/1534) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
+* ⬆ Bump typer from 0.16.0 to 0.16.1. PR [#1531](https://github.com/fastapi/sqlmodel/pull/1531) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump actions/download-artifact from 4 to 5. PR [#1451](https://github.com/fastapi/sqlmodel/pull/1451) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump actions/checkout from 4 to 5. PR [#1488](https://github.com/fastapi/sqlmodel/pull/1488) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1479](https://github.com/fastapi/sqlmodel/pull/1479) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
+* ⬆ Bump mkdocs-macros-plugin from 1.3.7 to 1.3.9. PR [#1507](https://github.com/fastapi/sqlmodel/pull/1507) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump ruff from 0.12.7 to 0.12.9. PR [#1521](https://github.com/fastapi/sqlmodel/pull/1521) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump mkdocs-material from 9.6.16 to 9.6.17. PR [#1528](https://github.com/fastapi/sqlmodel/pull/1528) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1444](https://github.com/fastapi/sqlmodel/pull/1444) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
+* ⬆ Bump mkdocs-material from 9.6.15 to 9.6.16. PR [#1446](https://github.com/fastapi/sqlmodel/pull/1446) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump ruff from 0.12.4 to 0.12.7. PR [#1447](https://github.com/fastapi/sqlmodel/pull/1447) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump tiangolo/latest-changes from 0.3.2 to 0.4.0. PR [#1448](https://github.com/fastapi/sqlmodel/pull/1448) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1437](https://github.com/fastapi/sqlmodel/pull/1437) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
+* ⬆ Bump ruff from 0.12.3 to 0.12.4. PR [#1436](https://github.com/fastapi/sqlmodel/pull/1436) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1428](https://github.com/fastapi/sqlmodel/pull/1428) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
+* ⬆ Bump ruff from 0.12.2 to 0.12.3. PR [#1432](https://github.com/fastapi/sqlmodel/pull/1432) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1418](https://github.com/fastapi/sqlmodel/pull/1418) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
+* ⬆ Bump pillow from 11.2.1 to 11.3.0. PR [#1423](https://github.com/fastapi/sqlmodel/pull/1423) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump mkdocs-material from 9.6.14 to 9.6.15. PR [#1424](https://github.com/fastapi/sqlmodel/pull/1424) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump ruff from 0.12.0 to 0.12.2. PR [#1425](https://github.com/fastapi/sqlmodel/pull/1425) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1374](https://github.com/fastapi/sqlmodel/pull/1374) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
+* ⬆ Bump ruff from 0.11.13 to 0.12.0. PR [#1403](https://github.com/fastapi/sqlmodel/pull/1403) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ✅ Simplify tests for `tests/test_tutorial/test_code_structure/test_tutorial001.py`, one test file for multiple variants. PR [#1408](https://github.com/fastapi/sqlmodel/pull/1408) by [@tiangolo](https://github.com/tiangolo).
+* ✅ Simplify tests setup, one test file for multiple source variants. PR [#1407](https://github.com/fastapi/sqlmodel/pull/1407) by [@tiangolo](https://github.com/tiangolo).
+* ✅ Refactor tests to use autouse `clear_sqlmodel`. PR [#1406](https://github.com/fastapi/sqlmodel/pull/1406) by [@tiangolo](https://github.com/tiangolo).
+* ⬆ Bump mkdocs-material from 9.5.18 to 9.6.14. PR [#1378](https://github.com/fastapi/sqlmodel/pull/1378) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump typer from 0.15.3 to 0.16.0. PR [#1393](https://github.com/fastapi/sqlmodel/pull/1393) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump cairosvg from 2.7.1 to 2.8.2. PR [#1383](https://github.com/fastapi/sqlmodel/pull/1383) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump ruff from 0.11.7 to 0.11.13. PR [#1397](https://github.com/fastapi/sqlmodel/pull/1397) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* 🔧 Remove Google Analytics. PR [#1386](https://github.com/fastapi/sqlmodel/pull/1386) by [@tiangolo](https://github.com/tiangolo).
+* ⬆ Bump mkdocs-macros-plugin from 1.0.5 to 1.3.7. PR [#1354](https://github.com/fastapi/sqlmodel/pull/1354) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump griffe-typingdoc from 0.2.5 to 0.2.8. PR [#1359](https://github.com/fastapi/sqlmodel/pull/1359) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Update pre-commit requirement from <4.0.0,>=2.17.0 to >=2.17.0,<5.0.0. PR [#1360](https://github.com/fastapi/sqlmodel/pull/1360) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump pillow from 11.0.0 to 11.2.1. PR [#1361](https://github.com/fastapi/sqlmodel/pull/1361) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1367](https://github.com/fastapi/sqlmodel/pull/1367) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
+* ⬆ Bump ruff from 0.9.6 to 0.11.7. PR [#1355](https://github.com/fastapi/sqlmodel/pull/1355) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1353](https://github.com/fastapi/sqlmodel/pull/1353) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
+* ⬆ Bump typing-extensions from 4.12.2 to 4.13.2. PR [#1356](https://github.com/fastapi/sqlmodel/pull/1356) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump typer from 0.15.2 to 0.15.3. PR [#1357](https://github.com/fastapi/sqlmodel/pull/1357) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1339](https://github.com/fastapi/sqlmodel/pull/1339) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
+* ⬆ Bump typer from 0.12.3 to 0.15.2. PR [#1325](https://github.com/fastapi/sqlmodel/pull/1325) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump httpx from 0.24.1 to 0.28.1. PR [#1238](https://github.com/fastapi/sqlmodel/pull/1238) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump astral-sh/setup-uv from 5 to 6. PR [#1348](https://github.com/fastapi/sqlmodel/pull/1348) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Update pytest requirement from <8.0.0,>=7.0.1 to >=7.0.1,<9.0.0. PR [#1022](https://github.com/fastapi/sqlmodel/pull/1022) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ♻️ Update `tests/test_select_gen.py`, pass environment variables, needed for NixOS nixpkgs. PR [#969](https://github.com/fastapi/sqlmodel/pull/969) by [@pbsds](https://github.com/pbsds).
+* 💚 Fix linting in CI. PR [#1340](https://github.com/fastapi/sqlmodel/pull/1340) by [@svlandeg](https://github.com/svlandeg).
+* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1327](https://github.com/fastapi/sqlmodel/pull/1327) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
+* ⬆ Bump jinja2 from 3.1.4 to 3.1.6. PR [#1317](https://github.com/fastapi/sqlmodel/pull/1317) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1319](https://github.com/fastapi/sqlmodel/pull/1319) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
+
+## 0.0.24 (2025-03-07)
+
+### Upgrades
+
+* ⬆️ Add support for Python 3.13. PR [#1289](https://github.com/fastapi/sqlmodel/pull/1289) by [@svlandeg](https://github.com/svlandeg).
+
+### Internal
+
+* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1114](https://github.com/fastapi/sqlmodel/pull/1114) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
+* ⬆ Bump ruff from 0.6.2 to 0.9.6. PR [#1294](https://github.com/fastapi/sqlmodel/pull/1294) by [@dependabot[bot]](https://github.com/apps/dependabot).
+
+## 0.0.23 (2025-02-28)
+
+### Fixes
+
+* 🐛 Fix type annotation in `Field` constructor. PR [#1304](https://github.com/fastapi/sqlmodel/pull/1304) by [@AlanBogarin](https://github.com/AlanBogarin).
+* 🐛 Fix Pydantic version check for version 2.10.x onwards. PR [#1255](https://github.com/fastapi/sqlmodel/pull/1255) by [@asiunov](https://github.com/asiunov).
+
+### Refactors
+
+* 🚨 Fix types for new Pydantic. PR [#1131](https://github.com/fastapi/sqlmodel/pull/1131) by [@tiangolo](https://github.com/tiangolo).
+
### Docs
+* 🩺 Take the GH badge only from pushes to the `main` branch. PR [#1291](https://github.com/fastapi/sqlmodel/pull/1291) by [@svlandeg](https://github.com/svlandeg).
+* 📝 Update documentation to refer to `list` instead of `List`. PR [#1147](https://github.com/fastapi/sqlmodel/pull/1147) by [@bubbletroubles](https://github.com/bubbletroubles).
+* ✏️ Fix typo in `databases.md`. PR [#1113](https://github.com/fastapi/sqlmodel/pull/1113) by [@radi-dev](https://github.com/radi-dev).
+* ✏️ Fix typo in `docs/tutorial/create-db-and-table.md`. PR [#1252](https://github.com/fastapi/sqlmodel/pull/1252) by [@ArianHamdi](https://github.com/ArianHamdi).
+* ✏️ Fix typo in `insert.md`. PR [#1256](https://github.com/fastapi/sqlmodel/pull/1256) by [@Noushadaliam](https://github.com/Noushadaliam).
+* 📝 Update markdown includes format. PR [#1254](https://github.com/fastapi/sqlmodel/pull/1254) by [@tiangolo](https://github.com/tiangolo).
+* 📝 Update fenced code in Decimal docs for consistency. PR [#1251](https://github.com/fastapi/sqlmodel/pull/1251) by [@tiangolo](https://github.com/tiangolo).
+* ✏️ Fix typo in the release notes of v0.0.22. PR [#1195](https://github.com/fastapi/sqlmodel/pull/1195) by [@PipeKnight](https://github.com/PipeKnight).
+* 📝 Update includes for `docs/advanced/uuid.md`. PR [#1151](https://github.com/fastapi/sqlmodel/pull/1151) by [@tiangolo](https://github.com/tiangolo).
+* 📝 Update includes for `docs/tutorial/create-db-and-table.md`. PR [#1149](https://github.com/fastapi/sqlmodel/pull/1149) by [@tiangolo](https://github.com/tiangolo).
+* 📝 Fix internal links in docs. PR [#1148](https://github.com/fastapi/sqlmodel/pull/1148) by [@tiangolo](https://github.com/tiangolo).
+* ✏️ Fix typo in documentation. PR [#1106](https://github.com/fastapi/sqlmodel/pull/1106) by [@Solipsistmonkey](https://github.com/Solipsistmonkey).
* 📝 Remove highlights in `indexes.md` . PR [#1100](https://github.com/fastapi/sqlmodel/pull/1100) by [@alejsdev](https://github.com/alejsdev).
### Internal
+* ⬆ Bump pypa/gh-action-pypi-publish from 1.12.3 to 1.12.4. PR [#1277](https://github.com/fastapi/sqlmodel/pull/1277) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* 💚 Fix CI test suite for Python 3.7. PR [#1309](https://github.com/fastapi/sqlmodel/pull/1309) by [@svlandeg](https://github.com/svlandeg).
+* 👷 Revert "Add Codecov to CI, Smokeshow/Cloudflare has been flaky lately (#1303)". PR [#1306](https://github.com/fastapi/sqlmodel/pull/1306) by [@svlandeg](https://github.com/svlandeg).
+* 👷 Add Codecov to CI, Smokeshow/Cloudflare has been flaky lately. PR [#1303](https://github.com/fastapi/sqlmodel/pull/1303) by [@tiangolo](https://github.com/tiangolo).
+* 👷 Add retries to Smokeshow. PR [#1302](https://github.com/fastapi/sqlmodel/pull/1302) by [@svlandeg](https://github.com/svlandeg).
+* ⬆ Bump astral-sh/setup-uv from 4 to 5. PR [#1249](https://github.com/fastapi/sqlmodel/pull/1249) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump pillow from 10.3.0 to 11.0.0. PR [#1139](https://github.com/fastapi/sqlmodel/pull/1139) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump pypa/gh-action-pypi-publish from 1.9.0 to 1.12.3. PR [#1240](https://github.com/fastapi/sqlmodel/pull/1240) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump astral-sh/setup-uv from 3 to 4. PR [#1225](https://github.com/fastapi/sqlmodel/pull/1225) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump tiangolo/latest-changes from 0.3.1 to 0.3.2. PR [#1207](https://github.com/fastapi/sqlmodel/pull/1207) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* 🔨 Update docs previews script. PR [#1236](https://github.com/fastapi/sqlmodel/pull/1236) by [@tiangolo](https://github.com/tiangolo).
+* 🔧 Update build-docs filter paths. PR [#1235](https://github.com/fastapi/sqlmodel/pull/1235) by [@tiangolo](https://github.com/tiangolo).
+* 🔧 Update team members. PR [#1234](https://github.com/fastapi/sqlmodel/pull/1234) by [@tiangolo](https://github.com/tiangolo).
+* ⬆️ Upgrade markdown-include-variants to version 0.0.3. PR [#1152](https://github.com/fastapi/sqlmodel/pull/1152) by [@tiangolo](https://github.com/tiangolo).
+* 👷 Update issue manager workflow. PR [#1137](https://github.com/fastapi/sqlmodel/pull/1137) by [@alejsdev](https://github.com/alejsdev).
+* 👷 Fix smokeshow, checkout files on CI. PR [#1136](https://github.com/fastapi/sqlmodel/pull/1136) by [@tiangolo](https://github.com/tiangolo).
+* 👷 Use uv in CI. PR [#1135](https://github.com/fastapi/sqlmodel/pull/1135) by [@tiangolo](https://github.com/tiangolo).
+* ➕ Add docs dependency markdown-include-variants. PR [#1129](https://github.com/fastapi/sqlmodel/pull/1129) by [@tiangolo](https://github.com/tiangolo).
+* 🔨 Update script to standardize format. PR [#1130](https://github.com/fastapi/sqlmodel/pull/1130) by [@tiangolo](https://github.com/tiangolo).
+* 👷 Update `labeler.yml`. PR [#1128](https://github.com/fastapi/sqlmodel/pull/1128) by [@tiangolo](https://github.com/tiangolo).
+* 👷 Update worfkow deploy-docs-notify URL. PR [#1126](https://github.com/fastapi/sqlmodel/pull/1126) by [@tiangolo](https://github.com/tiangolo).
+* 👷 Upgrade Cloudflare GitHub Action. PR [#1124](https://github.com/fastapi/sqlmodel/pull/1124) by [@tiangolo](https://github.com/tiangolo).
+* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1097](https://github.com/fastapi/sqlmodel/pull/1097) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
+* ⬆ Bump tiangolo/issue-manager from 0.5.0 to 0.5.1. PR [#1107](https://github.com/fastapi/sqlmodel/pull/1107) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* 👷 Update `issue-manager.yml`. PR [#1103](https://github.com/fastapi/sqlmodel/pull/1103) by [@tiangolo](https://github.com/tiangolo).
+* 👷 Fix coverage processing in CI, one name per matrix run. PR [#1104](https://github.com/fastapi/sqlmodel/pull/1104) by [@tiangolo](https://github.com/tiangolo).
* 💚 Set `include-hidden-files` to `True` when using the `upload-artifact` GH action. PR [#1098](https://github.com/fastapi/sqlmodel/pull/1098) by [@svlandeg](https://github.com/svlandeg).
* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1088](https://github.com/fastapi/sqlmodel/pull/1088) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
-## 0.0.22
+## 0.0.22 (2024-08-31)
### Fixes
-* 🐛 Fix support for types with `Optional[Annoated[x, f()]]`, e.g. `id: Optional[pydantic.UUID4]`. PR [#1093](https://github.com/fastapi/sqlmodel/pull/1093) by [@tiangolo](https://github.com/tiangolo).
+* 🐛 Fix support for types with `Optional[Annotated[x, f()]]`, e.g. `id: Optional[pydantic.UUID4]`. PR [#1093](https://github.com/fastapi/sqlmodel/pull/1093) by [@tiangolo](https://github.com/tiangolo).
### Docs
@@ -57,7 +504,7 @@
* 👷 Update issue-manager.yml GitHub Action permissions. PR [#1040](https://github.com/tiangolo/sqlmodel/pull/1040) by [@tiangolo](https://github.com/tiangolo).
* ♻️ Refactor Deploy Docs GitHub Action to be a script and update token preparing for org. PR [#1039](https://github.com/tiangolo/sqlmodel/pull/1039) by [@tiangolo](https://github.com/tiangolo).
-## 0.0.21
+## 0.0.21 (2024-07-21)
### Features
@@ -77,7 +524,7 @@
* ⬆ Bump mkdocstrings[python] from 0.23.0 to 0.25.1. PR [#927](https://github.com/tiangolo/sqlmodel/pull/927) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump dorny/paths-filter from 2 to 3. PR [#972](https://github.com/tiangolo/sqlmodel/pull/972) by [@dependabot[bot]](https://github.com/apps/dependabot).
-## 0.0.20
+## 0.0.20 (2024-07-17)
### Features
@@ -98,7 +545,7 @@
* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#979](https://github.com/tiangolo/sqlmodel/pull/979) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
* 🔨 Update docs Termynal scripts to not include line nums for local dev. PR [#1018](https://github.com/tiangolo/sqlmodel/pull/1018) by [@tiangolo](https://github.com/tiangolo).
-## 0.0.19
+## 0.0.19 (2024-06-04)
### Fixes
@@ -125,7 +572,7 @@
* 👷 Update GitHub Actions to download and upload artifacts. PR [#936](https://github.com/tiangolo/sqlmodel/pull/936) by [@tiangolo](https://github.com/tiangolo).
* 👷 Tweak CI for test-redistribute, add needed env vars for slim. PR [#929](https://github.com/tiangolo/sqlmodel/pull/929) by [@tiangolo](https://github.com/tiangolo).
-## 0.0.18
+## 0.0.18 (2024-04-30)
### Internal
@@ -135,7 +582,7 @@ In the future SQLModel will include the standard default recommended packages, a
* 🔧 Re-enable MkDocs Material Social plugin. PR [#915](https://github.com/tiangolo/sqlmodel/pull/915) by [@tiangolo](https://github.com/tiangolo).
-## 0.0.17
+## 0.0.17 (2024-04-29)
### Refactors
@@ -156,7 +603,7 @@ In the future SQLModel will include the standard default recommended packages, a
* ⬆️ Upgrade Ruff version and configs. PR [#859](https://github.com/tiangolo/sqlmodel/pull/859) by [@tiangolo](https://github.com/tiangolo).
* 🔥 Remove Jina QA Bot as it has been discontinued. PR [#840](https://github.com/tiangolo/sqlmodel/pull/840) by [@tiangolo](https://github.com/tiangolo).
-## 0.0.16
+## 0.0.16 (2024-02-17)
### Features
@@ -164,7 +611,7 @@ In the future SQLModel will include the standard default recommended packages, a
* Updated docs: [Update Data with FastAPI](https://sqlmodel.tiangolo.com/tutorial/fastapi/update/).
* New docs: [Update with Extra Data (Hashed Passwords) with FastAPI](https://sqlmodel.tiangolo.com/tutorial/fastapi/update-extra-data/).
-## 0.0.15
+## 0.0.15 (2024-02-17)
### Fixes
@@ -175,13 +622,13 @@ In the future SQLModel will include the standard default recommended packages, a
* ⬆ Bump tiangolo/issue-manager from 0.4.0 to 0.4.1. PR [#775](https://github.com/tiangolo/sqlmodel/pull/775) by [@dependabot[bot]](https://github.com/apps/dependabot).
* 👷 Fix GitHub Actions build docs filter paths for GitHub workflows. PR [#738](https://github.com/tiangolo/sqlmodel/pull/738) by [@tiangolo](https://github.com/tiangolo).
-## 0.0.14
+## 0.0.14 (2023-12-04)
### Features
* ✨ Add support for Pydantic v2 (while keeping support for v1 if v2 is not available). PR [#722](https://github.com/tiangolo/sqlmodel/pull/722) by [@tiangolo](https://github.com/tiangolo) including initial work in PR [#699](https://github.com/tiangolo/sqlmodel/pull/699) by [@AntonDeMeester](https://github.com/AntonDeMeester).
-## 0.0.13
+## 0.0.13 (2023-12-04)
### Fixes
@@ -211,7 +658,7 @@ In the future SQLModel will include the standard default recommended packages, a
* 🔧 Show line numbers in docs during local development. PR [#714](https://github.com/tiangolo/sqlmodel/pull/714) by [@tiangolo](https://github.com/tiangolo).
* 📝 Update details syntax with new pymdown extensions format. PR [#713](https://github.com/tiangolo/sqlmodel/pull/713) by [@tiangolo](https://github.com/tiangolo).
-## 0.0.12
+## 0.0.12 (2023-11-18)
### Features
@@ -222,7 +669,7 @@ In the future SQLModel will include the standard default recommended packages, a
* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#686](https://github.com/tiangolo/sqlmodel/pull/686) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
* 👷 Upgrade latest-changes GitHub Action. PR [#693](https://github.com/tiangolo/sqlmodel/pull/693) by [@tiangolo](https://github.com/tiangolo).
-## 0.0.11
+## 0.0.11 (2023-10-29)
### Features
@@ -242,7 +689,7 @@ In the future SQLModel will include the standard default recommended packages, a
* ⬆ Update black requirement from ^22.10.0 to >=22.10,<24.0. PR [#664](https://github.com/tiangolo/sqlmodel/pull/664) by [@dependabot[bot]](https://github.com/apps/dependabot).
* 👷 Update CI to build MkDocs Insiders only when the secrets are available, for Dependabot. PR [#683](https://github.com/tiangolo/sqlmodel/pull/683) by [@tiangolo](https://github.com/tiangolo).
-## 0.0.10
+## 0.0.10 (2023-10-26)
### Features
@@ -252,7 +699,7 @@ In the future SQLModel will include the standard default recommended packages, a
* 🔧 Adopt Ruff for formatting. PR [#679](https://github.com/tiangolo/sqlmodel/pull/679) by [@tiangolo](https://github.com/tiangolo).
-## 0.0.9
+## 0.0.9 (2023-10-23)
### Breaking Changes
@@ -330,7 +777,7 @@ In the future SQLModel will include the standard default recommended packages, a
* ⬆ Bump dawidd6/action-download-artifact from 2.9.0 to 2.24.0. PR [#470](https://github.com/tiangolo/sqlmodel/pull/470) by [@dependabot[bot]](https://github.com/apps/dependabot).
* 👷 Update Dependabot config. PR [#484](https://github.com/tiangolo/sqlmodel/pull/484) by [@tiangolo](https://github.com/tiangolo).
-## 0.0.8
+## 0.0.8 (2022-08-30)
### Fixes
@@ -342,7 +789,7 @@ In the future SQLModel will include the standard default recommended packages, a
* 📝 Adjust and clarify docs for `docs/tutorial/create-db-and-table.md`. PR [#426](https://github.com/tiangolo/sqlmodel/pull/426) by [@tiangolo](https://github.com/tiangolo).
* ✏ Fix typo in `docs/tutorial/connect/remove-data-connections.md`. PR [#421](https://github.com/tiangolo/sqlmodel/pull/421) by [@VerdantFox](https://github.com/VerdantFox).
-## 0.0.7
+## 0.0.7 (2022-08-28)
### Features
@@ -406,7 +853,7 @@ In the future SQLModel will include the standard default recommended packages, a
* 🔧 Upgrade MkDocs Material and update configs. PR [#217](https://github.com/tiangolo/sqlmodel/pull/217) by [@tiangolo](https://github.com/tiangolo).
* ⬆ Upgrade mypy, fix type annotations. PR [#218](https://github.com/tiangolo/sqlmodel/pull/218) by [@tiangolo](https://github.com/tiangolo).
-## 0.0.6
+## 0.0.6 (2021-12-28)
### Breaking Changes
@@ -477,7 +924,7 @@ Here's the new, extensive documentation explaining indexes and how to use them:
* ✏ Fix typo in `docs/tutorial/automatic-id-none-refresh.md`. PR [#14](https://github.com/tiangolo/sqlmodel/pull/14) by [@leynier](https://github.com/leynier).
* ✏ Fix typos in `docs/tutorial/index.md` and `docs/databases.md`. PR [#5](https://github.com/tiangolo/sqlmodel/pull/5) by [@sebastianmarines](https://github.com/sebastianmarines).
-## 0.0.5
+## 0.0.5 (2021-12-13)
### Features
@@ -495,19 +942,19 @@ Here's the new, extensive documentation explaining indexes and how to use them:
* 🔧 Add MkDocs Material social cards. PR [#90](https://github.com/tiangolo/sqlmodel/pull/90) by [@tiangolo](https://github.com/tiangolo).
* ✨ Update type annotations and upgrade mypy. PR [#173](https://github.com/tiangolo/sqlmodel/pull/173) by [@tiangolo](https://github.com/tiangolo).
-## 0.0.4
+## 0.0.4 (2021-08-25)
* 🎨 Fix type detection of select results in PyCharm. PR [#15](https://github.com/tiangolo/sqlmodel/pull/15) by [@tiangolo](https://github.com/tiangolo).
-## 0.0.3
+## 0.0.3 (2021-08-24)
* ⬆️ Update and relax specification range for `sqlalchemy-stubs`. PR [#4](https://github.com/tiangolo/sqlmodel/pull/4) by [@tiangolo](https://github.com/tiangolo).
-## 0.0.2
+## 0.0.2 (2021-08-24)
* This includes several small bug fixes detected during the first CI runs.
* 💚 Fix CI installs and tests. PR [#2](https://github.com/tiangolo/sqlmodel/pull/2) by [@tiangolo](https://github.com/tiangolo).
-## 0.0.1
+## 0.0.1 (2021-08-24)
* First release. 🎉
diff --git a/docs/tutorial/automatic-id-none-refresh.md b/docs/tutorial/automatic-id-none-refresh.md
index 1963492359..c55951f4e4 100644
--- a/docs/tutorial/automatic-id-none-refresh.md
+++ b/docs/tutorial/automatic-id-none-refresh.md
@@ -4,51 +4,9 @@ In the previous chapter, we saw how to add rows to the database using **SQLModel
Now let's talk a bit about why the `id` field **can't be `NULL`** on the database because it's a **primary key**, and we declare it using `Field(primary_key=True)`.
-But the same `id` field actually **can be `None`** in the Python code, so we declare the type with `int | None (or Optional[int])`, and set the default value to `Field(default=None)`:
+But the same `id` field actually **can be `None`** in the Python code, so we declare the type with `int | None`, and set the default value to `Field(default=None)`:
-//// tab | Python 3.10+
-
-```Python hl_lines="4"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/automatic_id_none_refresh/tutorial001_py310.py[ln:4-8]!}
-
-# Code below omitted 👇
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python hl_lines="4"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/automatic_id_none_refresh/tutorial001.py[ln:6-10]!}
-
-# Code below omitted 👇
-```
-
-////
-
-/// details | 👀 Full file preview
-
-//// tab | Python 3.10+
-
-```Python
-{!./docs_src/tutorial/automatic_id_none_refresh/tutorial001_py310.py!}
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python
-{!./docs_src/tutorial/automatic_id_none_refresh/tutorial001.py!}
-```
-
-////
-
-///
+{* ./docs_src/tutorial/automatic_id_none_refresh/tutorial001_py310.py ln[4:8] hl[5] *}
Next, I'll show you a bit more about the synchronization of data between the database and the Python code.
@@ -58,59 +16,17 @@ When do we get an actual `int` from the database in that `id` field? Let's see a
When we create a new `Hero` instance, we don't set the `id`:
-//// tab | Python 3.10+
-
-```Python hl_lines="3-6"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/automatic_id_none_refresh/tutorial001_py310.py[ln:21-24]!}
-
-# Code below omitted 👇
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python hl_lines="3-6"
-# Code above omitted 👆
+{* ./docs_src/tutorial/automatic_id_none_refresh/tutorial001_py310.py ln[21:24] hl[21:24] *}
-{!./docs_src/tutorial/automatic_id_none_refresh/tutorial001.py[ln:23-26]!}
-
-# Code below omitted 👇
-```
-
-////
-
-/// details | 👀 Full file preview
-
-//// tab | Python 3.10+
-
-```Python
-{!./docs_src/tutorial/automatic_id_none_refresh/tutorial001_py310.py!}
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python
-{!./docs_src/tutorial/automatic_id_none_refresh/tutorial001.py!}
-```
-
-////
-
-///
-
-### How `Optional` Helps
+### How `int | None` Helps
Because we don't set the `id`, it takes the Python's default value of `None` that we set in `Field(default=None)`.
-This is the only reason why we define it with `Optional` and with a default value of `None`.
+This is the only reason why we define it with `int | None` and with a default value of `None`.
Because at this point in the code, **before interacting with the database**, the Python value could actually be `None`.
-If we assumed that the `id` was *always* an `int` and added the type annotation without `Optional`, we could end up writing broken code, like:
+If we assumed that the `id` was *always* an `int` and added the type annotation without `int | None`, we could end up writing broken code, like:
```Python
next_hero_id = hero_1.id + 1
@@ -122,55 +38,13 @@ If we ran this code before saving the hero to the database and the `hero_1.id` w
TypeError: unsupported operand type(s) for +: 'NoneType' and 'int'
```
-But by declaring it with `Optional[int]`, the editor will help us to avoid writing broken code by showing us a warning telling us that the code could be invalid if `hero_1.id` is `None`. 🔍
+But by declaring it with `int | None`, the editor will help us to avoid writing broken code by showing us a warning telling us that the code could be invalid if `hero_1.id` is `None`. 🔍
## Print the Default `id` Values
We can confirm that by printing our heroes before adding them to the database:
-//// tab | Python 3.10+
-
-```Python hl_lines="9-11"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/automatic_id_none_refresh/tutorial001_py310.py[ln:21-29]!}
-
-# Code below omitted 👇
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python hl_lines="9-11"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/automatic_id_none_refresh/tutorial001.py[ln:23-31]!}
-
-# Code below omitted 👇
-```
-
-////
-
-/// details | 👀 Full file preview
-
-//// tab | Python 3.10+
-
-```Python
-{!./docs_src/tutorial/automatic_id_none_refresh/tutorial001_py310.py!}
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python
-{!./docs_src/tutorial/automatic_id_none_refresh/tutorial001.py!}
-```
-
-////
-
-///
+{* ./docs_src/tutorial/automatic_id_none_refresh/tutorial001_py310.py ln[21:29] hl[27:29] *}
That will output:
@@ -201,49 +75,7 @@ After we add the `Hero` instance objects to the **session**, the IDs are *still*
We can verify by creating a session using a `with` block and adding the objects. And then printing them again:
-//// tab | Python 3.10+
-
-```Python hl_lines="19-21"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/automatic_id_none_refresh/tutorial001_py310.py[ln:21-39]!}
-
-# Code below omitted 👇
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python hl_lines="19-21"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/automatic_id_none_refresh/tutorial001.py[ln:23-41]!}
-
-# Code below omitted 👇
-```
-
-////
-
-/// details | 👀 Full file preview
-
-//// tab | Python 3.10+
-
-```Python
-{!./docs_src/tutorial/automatic_id_none_refresh/tutorial001_py310.py!}
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python
-{!./docs_src/tutorial/automatic_id_none_refresh/tutorial001.py!}
-```
-
-////
-
-///
+{* ./docs_src/tutorial/automatic_id_none_refresh/tutorial001_py310.py ln[21:39] hl[37:39] *}
This will, again, output the `id`s of the objects as `None`:
@@ -268,49 +100,7 @@ As we saw before, the **session** is smart and doesn't talk to the database ever
Then we can `commit` the changes in the session, and print again:
-//// tab | Python 3.10+
-
-```Python hl_lines="13 16-18"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/automatic_id_none_refresh/tutorial001_py310.py[ln:31-46]!}
-
-# Code below omitted 👇
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python hl_lines="13 16-18"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/automatic_id_none_refresh/tutorial001.py[ln:33-48]!}
-
-# Code below omitted 👇
-```
-
-////
-
-/// details | 👀 Full file preview
-
-//// tab | Python 3.10+
-
-```Python
-{!./docs_src/tutorial/automatic_id_none_refresh/tutorial001_py310.py!}
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python
-{!./docs_src/tutorial/automatic_id_none_refresh/tutorial001.py!}
-```
-
-////
-
-///
+{* ./docs_src/tutorial/automatic_id_none_refresh/tutorial001_py310.py ln[31:46] hl[41,44:46] *}
And now, something unexpected happens, look at the output, it seems as if the `Hero` instance objects had no data at all:
@@ -368,49 +158,7 @@ We didn't access the object's attributes, like `hero.name`. We only accessed the
To confirm and understand how this **automatic expiration and refresh** of data when accessing attributes work, we can print some individual fields (instance attributes):
-//// tab | Python 3.10+
-
-```Python hl_lines="21-23 26-28"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/automatic_id_none_refresh/tutorial001_py310.py[ln:31-56]!}
-
-# Code below omitted 👇
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python hl_lines="21-23 26-28"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/automatic_id_none_refresh/tutorial001.py[ln:33-58]!}
-
-# Code below omitted 👇
-```
-
-////
-
-/// details | 👀 Full file preview
-
-//// tab | Python 3.10+
-
-```Python
-{!./docs_src/tutorial/automatic_id_none_refresh/tutorial001_py310.py!}
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python
-{!./docs_src/tutorial/automatic_id_none_refresh/tutorial001.py!}
-```
-
-////
-
-///
+{* ./docs_src/tutorial/automatic_id_none_refresh/tutorial001_py310.py ln[31:56] hl[49:51,54:56] *}
Now we are actually accessing the attributes, because instead of printing the whole object `hero_1`:
@@ -491,49 +239,7 @@ But what if you want to **explicitly refresh** the data?
You can do that too with `session.refresh(object)`:
-//// tab | Python 3.10+
-
-```Python hl_lines="30-32 35-37"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/automatic_id_none_refresh/tutorial001_py310.py[ln:31-65]!}
-
-# Code below omitted 👇
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python hl_lines="30-32 35-37"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/automatic_id_none_refresh/tutorial001.py[ln:33-67]!}
-
-# Code below omitted 👇
-```
-
-////
-
-/// details | 👀 Full file preview
-
-//// tab | Python 3.10+
-
-```Python
-{!./docs_src/tutorial/automatic_id_none_refresh/tutorial001_py310.py!}
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python
-{!./docs_src/tutorial/automatic_id_none_refresh/tutorial001.py!}
-```
-
-////
-
-///
+{* ./docs_src/tutorial/automatic_id_none_refresh/tutorial001_py310.py ln[31:65] hl[58:60,63:65] *}
When Python executes this code:
@@ -591,49 +297,7 @@ Now, as a final experiment, we can also print data after the **session** is clos
There are no surprises here, it still works:
-//// tab | Python 3.10+
-
-```Python hl_lines="40-42"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/automatic_id_none_refresh/tutorial001_py310.py[ln:31-70]!}
-
-# Code below omitted 👇
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python hl_lines="40-42"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/automatic_id_none_refresh/tutorial001.py[ln:33-72]!}
-
-# Code below omitted 👇
-```
-
-////
-
-/// details | 👀 Full file preview
-
-//// tab | Python 3.10+
-
-```Python
-{!./docs_src/tutorial/automatic_id_none_refresh/tutorial001_py310.py!}
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python
-{!./docs_src/tutorial/automatic_id_none_refresh/tutorial001.py!}
-```
-
-////
-
-///
+{* ./docs_src/tutorial/automatic_id_none_refresh/tutorial001_py310.py ln[31:70] hl[68:70] *}
And the output shows again the same data:
@@ -678,16 +342,6 @@ And as we created the **engine** with `echo=True`, we can see the SQL statements
////
-//// tab | Python 3.7+
-
-```Python
-{!./docs_src/tutorial/automatic_id_none_refresh/tutorial002.py!}
-```
-
-{!./docs_src/tutorial/automatic_id_none_refresh/annotations/en/tutorial002.md!}
-
-////
-
And here's all the output generated by running this program, all together:
-A dialog should show up. Go to the [project directory you created](./index.md#create-a-project){.internal-link target=_blank} and save the file with a name of `database.db`.
+A dialog should show up. Go to the [project directory you created](../virtual-environments.md#create-a-project){.internal-link target=_blank} and save the file with a name of `database.db`.
/// tip
diff --git a/docs/tutorial/create-db-and-table.md b/docs/tutorial/create-db-and-table.md
index de820ab760..bbd5532bae 100644
--- a/docs/tutorial/create-db-and-table.md
+++ b/docs/tutorial/create-db-and-table.md
@@ -2,7 +2,7 @@
Now let's get to the code. 👩💻
-Make sure you are inside of your project directory and with your virtual environment activated as [explained in the previous chapter](index.md){.internal-link target=_blank}.
+Make sure you are inside of your project directory and with your virtual environment activated as explained in [Virtual Environments](../virtual-environments.md#create-a-project){.internal-link target=_blank}.
We will:
@@ -41,45 +41,7 @@ That's why this package is called `SQLModel`. Because it's mainly used to create
For that, we will import `SQLModel` (plus other things we will also use) and create a class `Hero` that inherits from `SQLModel` and represents the **table model** for our heroes:
-//// tab | Python 3.10+
-
-```Python hl_lines="1 4"
-{!./docs_src/tutorial/create_db_and_table/tutorial001_py310.py[ln:1-8]!}
-
-# More code here later 👇
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python hl_lines="3 6"
-{!./docs_src/tutorial/create_db_and_table/tutorial001.py[ln:1-10]!}
-
-# More code here later 👇
-```
-
-////
-
-/// details | 👀 Full file preview
-
-//// tab | Python 3.10+
-
-```Python
-{!./docs_src/tutorial/create_db_and_table/tutorial001_py310.py!}
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python
-{!./docs_src/tutorial/create_db_and_table/tutorial001.py!}
-```
-
-////
-
-///
+{* ./docs_src/tutorial/create_db_and_table/tutorial001_py310.py ln[1:8] hl[1,4] *}
This class `Hero` **represents the table** for our heroes. And each instance we create later will **represent a row** in the table.
@@ -101,115 +63,39 @@ The name of each of these variables will be the name of the column in the table.
And the type of each of them will also be the type of table column:
-//// tab | Python 3.10+
-
-```Python hl_lines="1 5-8"
-{!./docs_src/tutorial/create_db_and_table/tutorial001_py310.py[ln:1-8]!}
-
-# More code here later 👇
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python hl_lines="1 3 7-10"
-{!./docs_src/tutorial/create_db_and_table/tutorial001.py[ln:1-10]!}
-
-# More code here later 👇
-```
-
-////
-
-/// details | 👀 Full file preview
-
-//// tab | Python 3.10+
-
-```Python
-{!./docs_src/tutorial/create_db_and_table/tutorial001_py310.py!}
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python
-{!./docs_src/tutorial/create_db_and_table/tutorial001.py!}
-```
-
-////
-
-///
+{* ./docs_src/tutorial/create_db_and_table/tutorial001_py310.py ln[1:8] hl[1,5:8] *}
Let's now see with more detail these field/column declarations.
-### Optional Fields, Nullable Columns
+### `None` Fields, Nullable Columns
-Let's start with `age`, notice that it has a type of `int | None (or Optional[int])`.
-
-And we import that `Optional` from the `typing` standard module.
+Let's start with `age`, notice that it has a type of `int | None`.
That is the standard way to declare that something "could be an `int` or `None`" in Python.
And we also set the default value of `age` to `None`.
-//// tab | Python 3.10+
-
-```Python hl_lines="8"
-{!./docs_src/tutorial/create_db_and_table/tutorial001_py310.py[ln:1-8]!}
-
-# More code here later 👇
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python hl_lines="1 10"
-{!./docs_src/tutorial/create_db_and_table/tutorial001.py[ln:1-10]!}
-
-# More code here later 👇
-```
-
-////
-
-/// details | 👀 Full file preview
-
-//// tab | Python 3.10+
-
-```Python
-{!./docs_src/tutorial/create_db_and_table/tutorial001_py310.py!}
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python
-{!./docs_src/tutorial/create_db_and_table/tutorial001.py!}
-```
-
-////
-
-///
+{* ./docs_src/tutorial/create_db_and_table/tutorial001_py310.py ln[1:8] hl[8] *}
/// tip
-We also define `id` with `Optional`. But we will talk about `id` below.
+We also define `id` with `int | None`. But we will talk about `id` below.
///
-This way, we tell **SQLModel** that `age` is not required when validating data and that it has a default value of `None`.
+Because the type is `int | None`:
-And we also tell it that, in the SQL database, the default value of `age` is `NULL` (the SQL equivalent to Python's `None`).
+* When validating data, `None` will be an allowed value for `age`.
+* In the database, the column for `age` will be allowed to have `NULL` (the SQL equivalent to Python's `None`).
-So, this column is "nullable" (can be set to `NULL`).
+And because there's a default value `= None`:
-/// info
+* When validating data, this `age` field won't be required, it will be `None` by default.
+* When saving to the database, the `age` column will have a `NULL` value by default.
-In terms of **Pydantic**, `age` is an **optional field**.
+/// tip
-In terms of **SQLAlchemy**, `age` is a **nullable column**.
+The default value could have been something else, like `= 42`.
///
@@ -221,49 +107,11 @@ So, we need to mark `id` as the **primary key**.
To do that, we use the special `Field` function from `sqlmodel` and set the argument `primary_key=True`:
-//// tab | Python 3.10+
-
-```Python hl_lines="1 5"
-{!./docs_src/tutorial/create_db_and_table/tutorial001_py310.py[ln:1-8]!}
-
-# More code here later 👇
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python hl_lines="3 7"
-{!./docs_src/tutorial/create_db_and_table/tutorial001.py[ln:1-10]!}
-
-# More code here later 👇
-```
-
-////
-
-/// details | 👀 Full file preview
-
-//// tab | Python 3.10+
-
-```Python
-{!./docs_src/tutorial/create_db_and_table/tutorial001_py310.py!}
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python
-{!./docs_src/tutorial/create_db_and_table/tutorial001.py!}
-```
-
-////
-
-///
+{* ./docs_src/tutorial/create_db_and_table/tutorial001_py310.py ln[1:8] hl[1,5] *}
That way, we tell **SQLModel** that this `id` field/column is the primary key of the table.
-But inside the SQL database, it is **always required** and can't be `NULL`. Why should we declare it with `Optional`?
+But inside the SQL database, it is **always required** and can't be `NULL`. Why should we declare it with `int | None`?
The `id` will be required in the database, but it will be *generated by the database*, not by our code.
@@ -280,7 +128,7 @@ somehow_save_in_db(my_hero)
do_something(my_hero.id) # Now my_hero.id has a value generated in DB 🎉
```
-So, because in *our code* (not in the database) the value of `id` *could be* `None`, we use `Optional`. This way **the editor will be able to help us**, for example, if we try to access the `id` of an object that we haven't saved in the database yet and would still be `None`.
+So, because in *our code* (not in the database) the value of `id` *could be* `None`, we use `int | None`. This way **the editor will be able to help us**, for example, if we try to access the `id` of an object that we haven't saved in the database yet and would still be `None`.
@@ -302,45 +150,7 @@ If you have a server database (for example PostgreSQL or MySQL), the **engine**
Creating the **engine** is very simple, just call `create_engine()` with a URL for the database to use:
-//// tab | Python 3.10+
-
-```Python hl_lines="1 14"
-{!./docs_src/tutorial/create_db_and_table/tutorial001_py310.py[ln:1-16]!}
-
-# More code here later 👇
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python hl_lines="3 16"
-{!./docs_src/tutorial/create_db_and_table/tutorial001.py[ln:1-18]!}
-
-# More code here later 👇
-```
-
-////
-
-/// details | 👀 Full file preview
-
-//// tab | Python 3.10+
-
-```Python
-{!./docs_src/tutorial/create_db_and_table/tutorial001_py310.py!}
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python
-{!./docs_src/tutorial/create_db_and_table/tutorial001.py!}
-```
-
-////
-
-///
+{* ./docs_src/tutorial/create_db_and_table/tutorial001_py310.py ln[1:16] hl[1,14] *}
You should normally have a single **engine** object for your whole application and re-use it everywhere.
@@ -364,45 +174,7 @@ SQLite supports a special database that lives all *in memory*. Hence, it's very
* `sqlite://`
-//// tab | Python 3.10+
-
-```Python hl_lines="11-12 14"
-{!./docs_src/tutorial/create_db_and_table/tutorial001_py310.py[ln:1-16]!}
-
-# More code here later 👇
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python hl_lines="13-14 16"
-{!./docs_src/tutorial/create_db_and_table/tutorial001.py[ln:1-18]!}
-
-# More code here later 👇
-```
-
-////
-
-/// details | 👀 Full file preview
-
-//// tab | Python 3.10+
-
-```Python
-{!./docs_src/tutorial/create_db_and_table/tutorial001_py310.py!}
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python
-{!./docs_src/tutorial/create_db_and_table/tutorial001.py!}
-```
-
-////
-
-///
+{* ./docs_src/tutorial/create_db_and_table/tutorial001_py310.py ln[1:16] hl[11:12,14] *}
You can read a lot more about all the databases supported by **SQLAlchemy** (and that way supported by **SQLModel**) in the SQLAlchemy documentation.
@@ -414,45 +186,7 @@ It will make the engine print all the SQL statements it executes, which can help
It is particularly useful for **learning** and **debugging**:
-//// tab | Python 3.10+
-
-```Python hl_lines="14"
-{!./docs_src/tutorial/create_db_and_table/tutorial001_py310.py[ln:1-16]!}
-
-# More code here later 👇
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python hl_lines="16"
-{!./docs_src/tutorial/create_db_and_table/tutorial001.py[ln:1-18]!}
-
-# More code here later 👇
-```
-
-////
-
-/// details | 👀 Full file preview
-
-//// tab | Python 3.10+
-
-```Python
-{!./docs_src/tutorial/create_db_and_table/tutorial001_py310.py!}
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python
-{!./docs_src/tutorial/create_db_and_table/tutorial001.py!}
-```
-
-////
-
-///
+{* ./docs_src/tutorial/create_db_and_table/tutorial001_py310.py ln[1:16] hl[14] *}
But in production, you would probably want to remove `echo=True`:
@@ -478,21 +212,7 @@ And SQLModel's version of `create_engine()` is type annotated internally, so you
Now everything is in place to finally create the database and table:
-//// tab | Python 3.10+
-
-```Python hl_lines="16"
-{!./docs_src/tutorial/create_db_and_table/tutorial001_py310.py!}
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python hl_lines="18"
-{!./docs_src/tutorial/create_db_and_table/tutorial001.py!}
-```
-
-////
+{* ./docs_src/tutorial/create_db_and_table/tutorial001_py310.py hl[16] *}
/// tip
@@ -601,31 +321,13 @@ But you will learn about migrations later in the Advanced User Guide.
Let's run the program to see it all working.
-Put the code it in a file `app.py` if you haven't already.
-
-/// details | 👀 Full file preview
-
-//// tab | Python 3.10+
-
-```Python
-{!./docs_src/tutorial/create_db_and_table/tutorial001_py310.py!}
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python
-{!./docs_src/tutorial/create_db_and_table/tutorial001.py!}
-```
-
-////
+Put the code in a file `app.py` if you haven't already.
-///
+{* ./docs_src/tutorial/create_db_and_table/tutorial001_py310.py *}
/// tip
-Remember to [activate the virtual environment](./index.md#create-a-python-virtual-environment){.internal-link target=_blank} before running it.
+Remember to [activate the virtual environment](../virtual-environments.md#create-a-virtual-environment){.internal-link target=_blank} before running it.
///
@@ -726,45 +428,7 @@ In this example it's just the `SQLModel.metadata.create_all(engine)`.
Let's put it in a function `create_db_and_tables()`:
-//// tab | Python 3.10+
-
-```Python hl_lines="17-18"
-{!./docs_src/tutorial/create_db_and_table/tutorial002_py310.py[ln:1-18]!}
-
-# More code here later 👇
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python hl_lines="19-20"
-{!./docs_src/tutorial/create_db_and_table/tutorial002.py[ln:1-20]!}
-
-# More code here later 👇
-```
-
-////
-
-/// details | 👀 Full file preview
-
-//// tab | Python 3.10+
-
-```Python
-{!./docs_src/tutorial/create_db_and_table/tutorial002_py310.py!}
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python
-{!./docs_src/tutorial/create_db_and_table/tutorial002.py!}
-```
-
-////
-
-///
+{* ./docs_src/tutorial/create_db_and_table/tutorial002_py310.py ln[1:18] hl[17:18] *}
If `SQLModel.metadata.create_all(engine)` was not in a function and we tried to import something from this module (from this file) in another, it would try to create the database and table **every time** we executed that other file that imported this module.
@@ -782,7 +446,7 @@ Now we would be able to, for example, import the `Hero` class in some other file
We prevented the side effects when importing something from your `app.py` file.
-But we still want it to **create the database and table** when we call it with Python directly as an independent script from the terminal, just as as above.
+But we still want it to **create the database and table** when we call it with Python directly as an independent script from the terminal, just as above.
/// tip
@@ -794,21 +458,7 @@ The word **script** often implies that the code could be run independently and e
For that we can use the special variable `__name__` in an `if` block:
-//// tab | Python 3.10+
-
-```Python hl_lines="21-22"
-{!./docs_src/tutorial/create_db_and_table/tutorial002_py310.py!}
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python hl_lines="23-24"
-{!./docs_src/tutorial/create_db_and_table/tutorial002.py!}
-```
-
-////
+{* ./docs_src/tutorial/create_db_and_table/tutorial002_py310.py hl[21:22] *}
### About `__name__ == "__main__"`
@@ -912,16 +562,6 @@ Now, let's give the code a final look:
////
-//// tab | Python 3.7+
-
-```{.python .annotate}
-{!./docs_src/tutorial/create_db_and_table/tutorial003.py!}
-```
-
-{!./docs_src/tutorial/create_db_and_table/annotations/en/tutorial003.md!}
-
-////
-
/// tip
Review what each line does by clicking each number bubble in the code. 👆
diff --git a/docs/tutorial/delete.md b/docs/tutorial/delete.md
index 9cb6748b7c..f6807fddab 100644
--- a/docs/tutorial/delete.md
+++ b/docs/tutorial/delete.md
@@ -6,25 +6,7 @@ Now let's delete some data using **SQLModel**.
As before, we'll continue from where we left off with the previous code.
-/// details | 👀 Full file preview
-
-//// tab | Python 3.10+
-
-```Python
-{!./docs_src/tutorial/update/tutorial003_py310.py!}
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python
-{!./docs_src/tutorial/update/tutorial003.py!}
-```
-
-////
-
-///
+{* ./docs_src/tutorial/update/tutorial003_py310.py ln[0] *}
Remember to remove the `database.db` file before running the examples to get the same results.
@@ -74,91 +56,11 @@ To get the same results, delete the `database.db` file before running the exampl
We'll start by selecting the hero `"Spider-Youngster"` that we updated in the previous chapter, this is the one we will delete:
-//// tab | Python 3.10+
-
-```Python hl_lines="5"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/delete/tutorial001_py310.py[ln:70-75]!}
-
-# Code below omitted 👇
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python hl_lines="5"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/delete/tutorial001.py[ln:72-77]!}
-
-# Code below omitted 👇
-```
-
-////
-
-/// details | 👀 Full file preview
-
-//// tab | Python 3.10+
-
-```Python
-{!./docs_src/tutorial/delete/tutorial001_py310.py!}
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python
-{!./docs_src/tutorial/delete/tutorial001.py!}
-```
-
-////
-
-///
+{* ./docs_src/tutorial/delete/tutorial001_py310.py ln[70:75] hl[72] *}
As this is a new function `delete_heroes()`, we'll also add it to the `main()` function so that we call it when executing the program from the command line:
-//// tab | Python 3.10+
-
-```Python hl_lines="7"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/delete/tutorial001_py310.py[ln:90-98]!}
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python hl_lines="7"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/delete/tutorial001.py[ln:92-100]!}
-```
-
-////
-
-/// details | 👀 Full file preview
-
-//// tab | Python 3.10+
-
-```Python
-{!./docs_src/tutorial/delete/tutorial001_py310.py!}
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python
-{!./docs_src/tutorial/delete/tutorial001.py!}
-```
-
-////
-
-///
+{* ./docs_src/tutorial/delete/tutorial001_py310.py ln[90:98] hl[94] *}
That will print the same existing hero **Spider-Youngster**:
@@ -186,49 +88,7 @@ Hero: name='Spider-Youngster' secret_name='Pedro Parqueador' age=16 id=2
Now, very similar to how we used `session.add()` to add or update new heroes, we can use `session.delete()` to delete the hero from the session:
-//// tab | Python 3.10+
-
-```Python hl_lines="10"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/delete/tutorial001_py310.py[ln:70-77]!}
-
-# Code below omitted 👇
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python hl_lines="10"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/delete/tutorial001.py[ln:72-79]!}
-
-# Code below omitted 👇
-```
-
-////
-
-/// details | 👀 Full file preview
-
-//// tab | Python 3.10+
-
-```Python
-{!./docs_src/tutorial/delete/tutorial001_py310.py!}
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python
-{!./docs_src/tutorial/delete/tutorial001.py!}
-```
-
-////
-
-///
+{* ./docs_src/tutorial/delete/tutorial001_py310.py ln[70:77] hl[77] *}
## Commit the Session
@@ -236,49 +96,7 @@ To save the current changes in the session, **commit** it.
This will save all the changes stored in the **session**, like the deleted hero:
-//// tab | Python 3.10+
-
-```Python hl_lines="11"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/delete/tutorial001_py310.py[ln:70-78]!}
-
-# Code below omitted 👇
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python hl_lines="11"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/delete/tutorial001.py[ln:72-80]!}
-
-# Code below omitted 👇
-```
-
-////
-
-/// details | 👀 Full file preview
-
-//// tab | Python 3.10+
-
-```Python
-{!./docs_src/tutorial/delete/tutorial001_py310.py!}
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python
-{!./docs_src/tutorial/delete/tutorial001.py!}
-```
-
-////
-
-///
+{* ./docs_src/tutorial/delete/tutorial001_py310.py ln[70:78] hl[78] *}
The same as we have seen before, `.commit()` will also save anything else that was added to the session. Including updates, or created heroes.
@@ -313,49 +131,7 @@ As the object is not connected to the session, it is not marked as "expired", th
Because of that, the object still contains its attributes with the data in it, so we can print it:
-//// tab | Python 3.10+
-
-```Python hl_lines="13"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/delete/tutorial001_py310.py[ln:70-80]!}
-
-# Code below omitted 👇
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python hl_lines="13"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/delete/tutorial001.py[ln:72-82]!}
-
-# Code below omitted 👇
-```
-
-////
-
-/// details | 👀 Full file preview
-
-//// tab | Python 3.10+
-
-```Python
-{!./docs_src/tutorial/delete/tutorial001_py310.py!}
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python
-{!./docs_src/tutorial/delete/tutorial001.py!}
-```
-
-////
-
-///
+{* ./docs_src/tutorial/delete/tutorial001_py310.py ln[70:80] hl[80] *}
This will output:
@@ -378,49 +154,7 @@ Deleted hero: name='Spider-Youngster' secret_name='Pedro Parqueador' age=16 id=2
To confirm if it was deleted, now let's query the database again, with the same `"Spider-Youngster"` name:
-//// tab | Python 3.10+
-
-```Python hl_lines="15-17"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/delete/tutorial001_py310.py[ln:70-84]!}
-
-# Code below omitted 👇
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python hl_lines="15-17"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/delete/tutorial001.py[ln:72-86]!}
-
-# Code below omitted 👇
-```
-
-////
-
-/// details | 👀 Full file preview
-
-//// tab | Python 3.10+
-
-```Python
-{!./docs_src/tutorial/delete/tutorial001_py310.py!}
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python
-{!./docs_src/tutorial/delete/tutorial001.py!}
-```
-
-////
-
-///
+{* ./docs_src/tutorial/delete/tutorial001_py310.py ln[70:84] hl[82:84] *}
Here we are using `results.first()` to get the first object found (in case it found multiple) or `None`, if it didn't find anything.
@@ -457,49 +191,7 @@ Now let's just confirm that, indeed, no hero was found in the database with that
We'll do it by checking that the "first" item in the `results` is `None`:
-//// tab | Python 3.10+
-
-```Python hl_lines="19-20"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/delete/tutorial001_py310.py[ln:70-87]!}
-
-# Code below omitted 👇
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python hl_lines="19-20"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/delete/tutorial001.py[ln:72-89]!}
-
-# Code below omitted 👇
-```
-
-////
-
-/// details | 👀 Full file preview
-
-//// tab | Python 3.10+
-
-```Python
-{!./docs_src/tutorial/delete/tutorial001_py310.py!}
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python
-{!./docs_src/tutorial/delete/tutorial001.py!}
-```
-
-////
-
-///
+{* ./docs_src/tutorial/delete/tutorial001_py310.py ln[70:87] hl[86:87] *}
This will output:
@@ -535,16 +227,6 @@ Now let's review all that code:
////
-//// tab | Python 3.7+
-
-```{ .python .annotate hl_lines="72-90" }
-{!./docs_src/tutorial/delete/tutorial002.py!}
-```
-
-{!./docs_src/tutorial/delete/annotations/en/tutorial002.md!}
-
-////
-
/// tip
Check out the number bubbles to see what is done by each line of code.
diff --git a/docs/tutorial/fastapi/delete.md b/docs/tutorial/fastapi/delete.md
index 87144cc08d..c7339ef384 100644
--- a/docs/tutorial/fastapi/delete.md
+++ b/docs/tutorial/fastapi/delete.md
@@ -12,69 +12,7 @@ We get a `hero_id` from the path parameter and verify if it exists, just as we d
And if we actually find a hero, we just delete it with the **session**.
-//// tab | Python 3.10+
-
-```Python hl_lines="3-11"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/fastapi/delete/tutorial001_py310.py[ln:89-97]!}
-
-# Code below omitted 👇
-```
-
-////
-
-//// tab | Python 3.9+
-
-```Python hl_lines="3-11"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/fastapi/delete/tutorial001_py39.py[ln:91-99]!}
-
-# Code below omitted 👇
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python hl_lines="3-11"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/fastapi/delete/tutorial001.py[ln:91-99]!}
-
-# Code below omitted 👇
-```
-
-////
-
-/// details | 👀 Full file preview
-
-//// tab | Python 3.10+
-
-```Python
-{!./docs_src/tutorial/fastapi/delete/tutorial001_py310.py!}
-```
-
-////
-
-//// tab | Python 3.9+
-
-```Python
-{!./docs_src/tutorial/fastapi/delete/tutorial001_py39.py!}
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python
-{!./docs_src/tutorial/fastapi/delete/tutorial001.py!}
-```
-
-////
-
-///
+{* ./docs_src/tutorial/fastapi/delete/tutorial001_py310.py ln[89:97] hl[89:97] *}
After deleting it successfully, we just return a response of:
diff --git a/docs/tutorial/fastapi/limit-and-offset.md b/docs/tutorial/fastapi/limit-and-offset.md
index 61d282f60c..1a464a664f 100644
--- a/docs/tutorial/fastapi/limit-and-offset.md
+++ b/docs/tutorial/fastapi/limit-and-offset.md
@@ -22,75 +22,7 @@ By default, we will return the first results from the database, so `offset` will
And by default, we will return a maximum of `100` heroes, so `limit` will have a default value of `100`.
-//// tab | Python 3.10+
-
-```Python hl_lines="1 7 9"
-{!./docs_src/tutorial/fastapi/limit_and_offset/tutorial001_py310.py[ln:1-2]!}
-
-# Code here omitted 👈
-
-{!./docs_src/tutorial/fastapi/limit_and_offset/tutorial001_py310.py[ln:52-56]!}
-
-# Code below omitted 👇
-```
-
-////
-
-//// tab | Python 3.9+
-
-```Python hl_lines="3 9 11"
-{!./docs_src/tutorial/fastapi/limit_and_offset/tutorial001_py39.py[ln:1-4]!}
-
-# Code here omitted 👈
-
-{!./docs_src/tutorial/fastapi/limit_and_offset/tutorial001_py39.py[ln:54-58]!}
-
-# Code below omitted 👇
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python hl_lines="3 9 11"
-{!./docs_src/tutorial/fastapi/limit_and_offset/tutorial001.py[ln:1-4]!}
-
-# Code here omitted 👈
-
-{!./docs_src/tutorial/fastapi/limit_and_offset/tutorial001.py[ln:54-58]!}
-
-# Code below omitted 👇
-```
-
-////
-
-/// details | 👀 Full file preview
-
-//// tab | Python 3.10+
-
-```Python
-{!./docs_src/tutorial/fastapi/limit_and_offset/tutorial001_py310.py!}
-```
-
-////
-
-//// tab | Python 3.9+
-
-```Python
-{!./docs_src/tutorial/fastapi/limit_and_offset/tutorial001_py39.py!}
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python
-{!./docs_src/tutorial/fastapi/limit_and_offset/tutorial001.py!}
-```
-
-////
-
-///
+{* ./docs_src/tutorial/fastapi/limit_and_offset/tutorial001_py310.py ln[1:2,52:56] hl[1,53,55] *}
We want to allow clients to set different `offset` and `limit` values.
diff --git a/docs/tutorial/fastapi/multiple-models.md b/docs/tutorial/fastapi/multiple-models.md
index 41c1ac6498..1bc045612b 100644
--- a/docs/tutorial/fastapi/multiple-models.md
+++ b/docs/tutorial/fastapi/multiple-models.md
@@ -16,7 +16,7 @@ For input, we have:
If we pay attention, it shows that the client *could* send an `id` in the JSON body of the request.
-This means that the client could try to use the same ID that already exists in the database for another hero.
+This means that the client could try to use the same ID that already exists in the database to create another hero.
That's not what we want.
@@ -51,7 +51,7 @@ The `age` is optional, we don't have to return it, or it could be `None` (or `nu
Here's the weird thing, the `id` currently seems also "optional". 🤔
-This is because in our **SQLModel** class we declare the `id` with `Optional[int]`, because it could be `None` in memory until we save it in the database and we finally get the actual ID.
+This is because in our **SQLModel** class we declare the `id` with a default value of `= None`, because it could be `None` in memory until we save it in the database and we finally get the actual ID.
But in the responses, we always send a model from the database, so it **always has an ID**. So the `id` in the responses can be declared as required.
@@ -71,7 +71,7 @@ And in most of the cases, the developer of the client for that API **will also b
### So Why is it Important to Have Required IDs
-Now, what's the matter with having one **`id` field marked as "optional"** in a response when in reality it is always required?
+Now, what's the matter with having one **`id` field marked as "optional"** in a response when in reality it is always available (required)?
For example, **automatically generated clients** in other languages (or also in Python) would have some declaration that this field `id` is optional.
@@ -98,7 +98,7 @@ But we also want to have a `HeroCreate` for the data we want to receive when **c
* `secret_name`, required
* `age`, optional
-And we want to have a `HeroPublic` with the `id` field, but this time annotated with `id: int`, instead of `id: Optional[int]`, to make it clear that it is required in responses **read** from the clients:
+And we want to have a `HeroPublic` with the `id` field, but this time with a type of `id: int`, instead of `id: int | None`, to make it clear that it will always have an `int` in responses **read** from the clients:
* `id`, required
* `name`, required
@@ -109,75 +109,7 @@ And we want to have a `HeroPublic` with the `id` field, but this time annotated
The simplest way to solve it could be to create **multiple models**, each one with all the corresponding fields:
-//// tab | Python 3.10+
-
-```Python hl_lines="5-9 12-15 18-22"
-# This would work, but there's a better option below 🚨
-
-# Code above omitted 👆
-
-{!./docs_src/tutorial/fastapi/multiple_models/tutorial001_py310.py[ln:5-22]!}
-
-# Code below omitted 👇
-```
-
-////
-
-//// tab | Python 3.9+
-
-```Python hl_lines="5-9 12-15 18-22"
-# This would work, but there's a better option below 🚨
-
-# Code above omitted 👆
-
-{!./docs_src/tutorial/fastapi/multiple_models/tutorial001_py39.py[ln:7-24]!}
-
-# Code below omitted 👇
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python hl_lines="5-9 12-15 18-22"
-# This would work, but there's a better option below 🚨
-
-# Code above omitted 👆
-
-{!./docs_src/tutorial/fastapi/multiple_models/tutorial001.py[ln:7-24]!}
-
-# Code below omitted 👇
-```
-
-////
-
-/// details | 👀 Full file preview
-
-//// tab | Python 3.10+
-
-```Python
-{!./docs_src/tutorial/fastapi/multiple_models/tutorial001_py310.py!}
-```
-
-////
-
-//// tab | Python 3.9+
-
-```Python
-{!./docs_src/tutorial/fastapi/multiple_models/tutorial001_py39.py!}
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python
-{!./docs_src/tutorial/fastapi/multiple_models/tutorial001.py!}
-```
-
-////
-
-///
+{* ./docs_src/tutorial/fastapi/multiple_models/tutorial001_py310.py ln[5:22] hl[5:9,12:15,18:22] *}
Here's the important detail, and probably the most important feature of **SQLModel**: only `Hero` is declared with `table = True`.
@@ -199,109 +131,13 @@ Let's now see how to use these new models in the FastAPI application.
Let's first check how is the process to create a hero now:
-//// tab | Python 3.10+
-
-```Python hl_lines="3-4 6"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/fastapi/multiple_models/tutorial001_py310.py[ln:44-51]!}
-
-# Code below omitted 👇
-```
-
-////
-
-//// tab | Python 3.9+
-
-```Python hl_lines="3-4 6"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/fastapi/multiple_models/tutorial001_py39.py[ln:46-53]!}
-
-# Code below omitted 👇
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python hl_lines="3-4 6"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/fastapi/multiple_models/tutorial001.py[ln:46-53]!}
-
-# Code below omitted 👇
-```
-
-////
-
-/// details | 👀 Full file preview
-
-//// tab | Python 3.10+
-
-```Python
-{!./docs_src/tutorial/fastapi/multiple_models/tutorial001_py310.py!}
-```
-
-////
-
-//// tab | Python 3.9+
-
-```Python
-{!./docs_src/tutorial/fastapi/multiple_models/tutorial001_py39.py!}
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python
-{!./docs_src/tutorial/fastapi/multiple_models/tutorial001.py!}
-```
-
-////
-
-///
+{* ./docs_src/tutorial/fastapi/multiple_models/tutorial001_py310.py ln[44:51] hl[44:45,47] *}
Let's check that in detail.
Now we use the type annotation `HeroCreate` for the request JSON data in the `hero` parameter of the **path operation function**.
-//// tab | Python 3.10+
-
-```Python hl_lines="3"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/fastapi/multiple_models/tutorial001_py310.py[ln:45]!}
-
-# Code below omitted 👇
-```
-
-////
-
-//// tab | Python 3.9+
-
-```Python hl_lines="3"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/fastapi/multiple_models/tutorial001_py39.py[ln:47]!}
-
-# Code below omitted 👇
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python hl_lines="3"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/fastapi/multiple_models/tutorial001.py[ln:47]!}
-
-# Code below omitted 👇
-```
-
-////
+{* ./docs_src/tutorial/fastapi/multiple_models/tutorial001_py310.py ln[45] hl[45] *}
Then we create a new `Hero` (this is the actual **table** model that saves things to the database) using `Hero.model_validate()`.
@@ -315,41 +151,7 @@ In versions of **SQLModel** before `0.0.14` you would use the method `.from_orm(
We can now create a new `Hero` instance (the one for the database) and put it in the variable `db_hero` from the data in the `hero` variable that is the `HeroCreate` instance we received from the request.
-//// tab | Python 3.10+
-
-```Python hl_lines="3"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/fastapi/multiple_models/tutorial001_py310.py[ln:47]!}
-
-# Code below omitted 👇
-```
-
-////
-
-//// tab | Python 3.9+
-
-```Python hl_lines="3"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/fastapi/multiple_models/tutorial001_py39.py[ln:49]!}
-
-# Code below omitted 👇
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python hl_lines="3"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/fastapi/multiple_models/tutorial001.py[ln:49]!}
-
-# Code below omitted 👇
-```
-
-////
+{* ./docs_src/tutorial/fastapi/multiple_models/tutorial001_py310.py ln[47] hl[47] *}
Then we just `add` it to the **session**, `commit`, and `refresh` it, and finally, we return the same `db_hero` variable that has the just refreshed `Hero` instance.
@@ -357,41 +159,7 @@ Because it is just refreshed, it has the `id` field set with a new ID taken from
And now that we return it, FastAPI will validate the data with the `response_model`, which is a `HeroPublic`:
-//// tab | Python 3.10+
-
-```Python hl_lines="3"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/fastapi/multiple_models/tutorial001_py310.py[ln:44]!}
-
-# Code below omitted 👇
-```
-
-////
-
-//// tab | Python 3.9+
-
-```Python hl_lines="3"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/fastapi/multiple_models/tutorial001_py39.py[ln:46]!}
-
-# Code below omitted 👇
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python hl_lines="3"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/fastapi/multiple_models/tutorial001.py[ln:46]!}
-
-# Code below omitted 👇
-```
-
-////
+{* ./docs_src/tutorial/fastapi/multiple_models/tutorial001_py310.py ln[44] hl[44] *}
This will validate that all the data that we promised is there and will remove any data we didn't declare.
@@ -443,69 +211,7 @@ We can see from above that they all share some **base** fields:
So let's create a **base** model `HeroBase` that the others can inherit from:
-//// tab | Python 3.10+
-
-```Python hl_lines="3-6"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/fastapi/multiple_models/tutorial002_py310.py[ln:5-8]!}
-
-# Code below omitted 👇
-```
-
-////
-
-//// tab | Python 3.9+
-
-```Python hl_lines="3-6"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/fastapi/multiple_models/tutorial002_py39.py[ln:7-10]!}
-
-# Code below omitted 👇
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python hl_lines="3-6"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/fastapi/multiple_models/tutorial002.py[ln:7-10]!}
-
-# Code below omitted 👇
-```
-
-////
-
-/// details | 👀 Full file preview
-
-//// tab | Python 3.10+
-
-```Python
-{!./docs_src/tutorial/fastapi/multiple_models/tutorial002_py310.py!}
-```
-
-////
-
-//// tab | Python 3.9+
-
-```Python
-{!./docs_src/tutorial/fastapi/multiple_models/tutorial002_py39.py!}
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python
-{!./docs_src/tutorial/fastapi/multiple_models/tutorial002.py!}
-```
-
-////
-
-///
+{* ./docs_src/tutorial/fastapi/multiple_models/tutorial002_py310.py ln[5:8] hl[5:8] *}
As you can see, this is *not* a **table model**, it doesn't have the `table = True` config.
@@ -515,73 +221,11 @@ But now we can create the **other models inheriting from it**, they will all sha
Let's start with the only **table model**, the `Hero`:
-//// tab | Python 3.10+
-
-```Python hl_lines="9-10"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/fastapi/multiple_models/tutorial002_py310.py[ln:5-12]!}
-
-# Code below omitted 👇
-```
-
-////
-
-//// tab | Python 3.9+
-
-```Python hl_lines="9-10"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/fastapi/multiple_models/tutorial002_py39.py[ln:7-14]!}
-
-# Code below omitted 👇
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python hl_lines="9-10"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/fastapi/multiple_models/tutorial002.py[ln:7-14]!}
-
-# Code below omitted 👇
-```
-
-////
-
-/// details | 👀 Full file preview
-
-//// tab | Python 3.10+
-
-```Python
-{!./docs_src/tutorial/fastapi/multiple_models/tutorial002_py310.py!}
-```
-
-////
-
-//// tab | Python 3.9+
-
-```Python
-{!./docs_src/tutorial/fastapi/multiple_models/tutorial002_py39.py!}
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python
-{!./docs_src/tutorial/fastapi/multiple_models/tutorial002.py!}
-```
-
-////
-
-///
+{* ./docs_src/tutorial/fastapi/multiple_models/tutorial002_py310.py ln[5:12] hl[11:12] *}
Notice that `Hero` now doesn't inherit from `SQLModel`, but from `HeroBase`.
-And now we only declare one single field directly, the `id`, that here is `Optional[int]`, and is a `primary_key`.
+And now we only declare one single field directly, the `id`, that here is `int | None`, and is a `primary_key`.
And even though we don't declare the other fields **explicitly**, because they are inherited, they are also part of this `Hero` model.
@@ -593,69 +237,7 @@ And those inherited fields will also be in the **autocompletion** and **inline e
Notice that the parent model `HeroBase` is not a **table model**, but still, we can declare `name` and `age` using `Field(index=True)`.
-//// tab | Python 3.10+
-
-```Python hl_lines="4 6 9"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/fastapi/multiple_models/tutorial002_py310.py[ln:5-12]!}
-
-# Code below omitted 👇
-```
-
-////
-
-//// tab | Python 3.9+
-
-```Python hl_lines="4 6 9"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/fastapi/multiple_models/tutorial002_py39.py[ln:7-14]!}
-
-# Code below omitted 👇
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python hl_lines="4 6 9"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/fastapi/multiple_models/tutorial002.py[ln:7-14]!}
-
-# Code below omitted 👇
-```
-
-////
-
-/// details | 👀 Full file preview
-
-//// tab | Python 3.10+
-
-```Python
-{!./docs_src/tutorial/fastapi/multiple_models/tutorial002_py310.py!}
-```
-
-////
-
-//// tab | Python 3.9+
-
-```Python
-{!./docs_src/tutorial/fastapi/multiple_models/tutorial002_py39.py!}
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python
-{!./docs_src/tutorial/fastapi/multiple_models/tutorial002.py!}
-```
-
-////
-
-///
+{* ./docs_src/tutorial/fastapi/multiple_models/tutorial002_py310.py ln[5:12] hl[6,8,11] *}
This won't affect this parent **data model** `HeroBase`.
@@ -667,69 +249,7 @@ Now let's see the `HeroCreate` model that will be used to define the data that w
This is a fun one:
-//// tab | Python 3.10+
-
-```Python hl_lines="13-14"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/fastapi/multiple_models/tutorial002_py310.py[ln:5-16]!}
-
-# Code below omitted 👇
-```
-
-////
-
-//// tab | Python 3.9+
-
-```Python hl_lines="13-14"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/fastapi/multiple_models/tutorial002_py39.py[ln:7-18]!}
-
-# Code below omitted 👇
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python hl_lines="13-14"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/fastapi/multiple_models/tutorial002.py[ln:7-18]!}
-
-# Code below omitted 👇
-```
-
-////
-
-/// details | 👀 Full file preview
-
-//// tab | Python 3.10+
-
-```Python
-{!./docs_src/tutorial/fastapi/multiple_models/tutorial002_py310.py!}
-```
-
-////
-
-//// tab | Python 3.9+
-
-```Python
-{!./docs_src/tutorial/fastapi/multiple_models/tutorial002_py39.py!}
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python
-{!./docs_src/tutorial/fastapi/multiple_models/tutorial002.py!}
-```
-
-////
-
-///
+{* ./docs_src/tutorial/fastapi/multiple_models/tutorial002_py310.py ln[5:16] hl[15:16] *}
What's happening here?
@@ -749,69 +269,7 @@ Now let's check the `HeroPublic` model.
This one just declares that the `id` field is required when reading a hero from the API, because a hero read from the API will come from the database, and in the database it will always have an ID.
-//// tab | Python 3.10+
-
-```Python hl_lines="17-18"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/fastapi/multiple_models/tutorial002_py310.py[ln:5-20]!}
-
-# Code below omitted 👇
-```
-
-////
-
-//// tab | Python 3.9+
-
-```Python hl_lines="17-18"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/fastapi/multiple_models/tutorial002_py39.py[ln:7-22]!}
-
-# Code below omitted 👇
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python hl_lines="17-18"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/fastapi/multiple_models/tutorial002.py[ln:7-22]!}
-
-# Code below omitted 👇
-```
-
-////
-
-/// details | 👀 Full file preview
-
-//// tab | Python 3.10+
-
-```Python
-{!./docs_src/tutorial/fastapi/multiple_models/tutorial002_py310.py!}
-```
-
-////
-
-//// tab | Python 3.9+
-
-```Python
-{!./docs_src/tutorial/fastapi/multiple_models/tutorial002_py39.py!}
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python
-{!./docs_src/tutorial/fastapi/multiple_models/tutorial002.py!}
-```
-
-////
-
-///
+{* ./docs_src/tutorial/fastapi/multiple_models/tutorial002_py310.py ln[5:20] hl[19:20] *}
## Review the Updated Docs UI
diff --git a/docs/tutorial/fastapi/read-one.md b/docs/tutorial/fastapi/read-one.md
index 0976961e01..03a65a0a2d 100644
--- a/docs/tutorial/fastapi/read-one.md
+++ b/docs/tutorial/fastapi/read-one.md
@@ -14,69 +14,7 @@ If you need to refresh how *path parameters* work, including their data validati
///
-//// tab | Python 3.10+
-
-```Python hl_lines="6"
-{!./docs_src/tutorial/fastapi/read_one/tutorial001_py310.py[ln:1-2]!}
-
-# Code here omitted 👈
-
-{!./docs_src/tutorial/fastapi/read_one/tutorial001_py310.py[ln:59-65]!}
-```
-
-////
-
-//// tab | Python 3.9+
-
-```Python hl_lines="8"
-{!./docs_src/tutorial/fastapi/read_one/tutorial001_py39.py[ln:1-4]!}
-
-# Code here omitted 👈
-
-{!./docs_src/tutorial/fastapi/read_one/tutorial001_py39.py[ln:61-67]!}
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python hl_lines="8"
-{!./docs_src/tutorial/fastapi/read_one/tutorial001.py[ln:1-4]!}
-
-# Code here omitted 👈
-
-{!./docs_src/tutorial/fastapi/read_one/tutorial001.py[ln:61-67]!}
-```
-
-////
-
-/// details | 👀 Full file preview
-
-//// tab | Python 3.10+
-
-```Python
-{!./docs_src/tutorial/fastapi/read_one/tutorial001_py310.py!}
-```
-
-////
-
-//// tab | Python 3.9+
-
-```Python
-{!./docs_src/tutorial/fastapi/read_one/tutorial001_py39.py!}
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python
-{!./docs_src/tutorial/fastapi/read_one/tutorial001.py!}
-```
-
-////
-
-///
+{* ./docs_src/tutorial/fastapi/read_one/tutorial001_py310.py ln[1:2,59:65] hl[59] *}
For example, to get the hero with ID `2` we would send a `GET` request to:
@@ -96,69 +34,7 @@ And to use it, we first import `HTTPException` from `fastapi`.
This will let the client know that they probably made a mistake on their side and requested a hero that doesn't exist in the database.
-//// tab | Python 3.10+
-
-```Python hl_lines="1 9-11"
-{!./docs_src/tutorial/fastapi/read_one/tutorial001_py310.py[ln:1-2]!}
-
-# Code here omitted 👈
-
-{!./docs_src/tutorial/fastapi/read_one/tutorial001_py310.py[ln:59-65]!}
-```
-
-////
-
-//// tab | Python 3.9+
-
-```Python hl_lines="3 11-13"
-{!./docs_src/tutorial/fastapi/read_one/tutorial001_py39.py[ln:1-4]!}
-
-# Code here omitted 👈
-
-{!./docs_src/tutorial/fastapi/read_one/tutorial001_py39.py[ln:61-67]!}
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python hl_lines="3 11-13"
-{!./docs_src/tutorial/fastapi/read_one/tutorial001.py[ln:1-4]!}
-
-# Code here omitted 👈
-
-{!./docs_src/tutorial/fastapi/read_one/tutorial001.py[ln:61-67]!}
-```
-
-////
-
-/// details | 👀 Full file preview
-
-//// tab | Python 3.10+
-
-```Python
-{!./docs_src/tutorial/fastapi/read_one/tutorial001_py310.py!}
-```
-
-////
-
-//// tab | Python 3.9+
-
-```Python
-{!./docs_src/tutorial/fastapi/read_one/tutorial001_py39.py!}
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python
-{!./docs_src/tutorial/fastapi/read_one/tutorial001.py!}
-```
-
-////
-
-///
+{* ./docs_src/tutorial/fastapi/read_one/tutorial001_py310.py ln[1:2,59:65] hl[1,62:64] *}
## Return the Hero
@@ -166,69 +42,7 @@ Then, if the hero exists, we return it.
And because we are using the `response_model` with `HeroPublic`, it will be validated, documented, etc.
-//// tab | Python 3.10+
-
-```Python hl_lines="6 12"
-{!./docs_src/tutorial/fastapi/read_one/tutorial001_py310.py[ln:1-2]!}
-
-# Code here omitted 👈
-
-{!./docs_src/tutorial/fastapi/read_one/tutorial001_py310.py[ln:59-65]!}
-```
-
-////
-
-//// tab | Python 3.9+
-
-```Python hl_lines="8 14"
-{!./docs_src/tutorial/fastapi/read_one/tutorial001_py39.py[ln:1-4]!}
-
-# Code here omitted 👈
-
-{!./docs_src/tutorial/fastapi/read_one/tutorial001_py39.py[ln:61-67]!}
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python hl_lines="8 14"
-{!./docs_src/tutorial/fastapi/read_one/tutorial001.py[ln:1-4]!}
-
-# Code here omitted 👈
-
-{!./docs_src/tutorial/fastapi/read_one/tutorial001.py[ln:61-67]!}
-```
-
-////
-
-/// details | 👀 Full file preview
-
-//// tab | Python 3.10+
-
-```Python
-{!./docs_src/tutorial/fastapi/read_one/tutorial001_py310.py!}
-```
-
-////
-
-//// tab | Python 3.9+
-
-```Python
-{!./docs_src/tutorial/fastapi/read_one/tutorial001_py39.py!}
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python
-{!./docs_src/tutorial/fastapi/read_one/tutorial001.py!}
-```
-
-////
-
-///
+{* ./docs_src/tutorial/fastapi/read_one/tutorial001_py310.py ln[1:2,59:65] hl[59,65] *}
## Check the Docs UI
diff --git a/docs/tutorial/fastapi/relationships.md b/docs/tutorial/fastapi/relationships.md
index 4087dfca10..8c69b51776 100644
--- a/docs/tutorial/fastapi/relationships.md
+++ b/docs/tutorial/fastapi/relationships.md
@@ -44,185 +44,13 @@ It's because we declared the `HeroPublic` with only the same base fields of the
And the same way, we declared the `TeamPublic` with only the same base fields of the `TeamBase` plus the `id`. But it doesn't include a field `heroes` for the **relationship attribute**.
-//// tab | Python 3.10+
-
-```Python hl_lines="3-5 9-10 14-19 23-24"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/fastapi/teams/tutorial001_py310.py[ln:5-7]!}
-
-# Code here omitted 👈
-
-{!./docs_src/tutorial/fastapi/teams/tutorial001_py310.py[ln:20-21]!}
-
-# Code here omitted 👈
-
-{!./docs_src/tutorial/fastapi/teams/tutorial001_py310.py[ln:29-34]!}
-
-# Code here omitted 👈
-
-{!./docs_src/tutorial/fastapi/teams/tutorial001_py310.py[ln:43-44]!}
-
-# Code below omitted 👇
-```
-
-////
-
-//// tab | Python 3.9+
-
-```Python hl_lines="3-5 9-10 14-19 23-24"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/fastapi/teams/tutorial001_py39.py[ln:7-9]!}
-
-# Code here omitted 👈
-
-{!./docs_src/tutorial/fastapi/teams/tutorial001_py39.py[ln:22-23]!}
-
-# Code here omitted 👈
-
-{!./docs_src/tutorial/fastapi/teams/tutorial001_py39.py[ln:31-36]!}
-
-# Code here omitted 👈
-
-{!./docs_src/tutorial/fastapi/teams/tutorial001_py39.py[ln:45-46]!}
-
-# Code below omitted 👇
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python hl_lines="3-5 9-10 14-19 23-24"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:7-9]!}
-
-# Code here omitted 👈
-
-{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:22-23]!}
-
-# Code here omitted 👈
-
-{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:31-36]!}
-
-# Code here omitted 👈
-
-{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:45-46]!}
-
-# Code below omitted 👇
-```
-
-////
-
-/// details | 👀 Full file preview
-
-//// tab | Python 3.10+
-
-```Python
-{!./docs_src/tutorial/fastapi/teams/tutorial001_py310.py!}
-```
-
-////
-
-//// tab | Python 3.9+
-
-```Python
-{!./docs_src/tutorial/fastapi/teams/tutorial001_py39.py!}
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python
-{!./docs_src/tutorial/fastapi/teams/tutorial001.py!}
-```
-
-////
-
-///
+{* ./docs_src/tutorial/fastapi/teams/tutorial001_py310.py ln[5:7,20:21,29:34,43:44] hl[5:7,20:21,29:34,43:44] *}
Now, remember that FastAPI uses the `response_model` to validate and **filter** the response data?
In this case, we used `response_model=TeamPublic` and `response_model=HeroPublic`, so FastAPI will use them to filter the response data, even if we return a **table model** that includes **relationship attributes**:
-//// tab | Python 3.10+
-
-```Python hl_lines="3 8 12 17"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/fastapi/teams/tutorial001_py310.py[ln:102-107]!}
-
-# Code here omitted 👈
-
-{!./docs_src/tutorial/fastapi/teams/tutorial001_py310.py[ln:156-161]!}
-
-# Code below omitted 👇
-```
-
-////
-
-//// tab | Python 3.9+
-
-```Python hl_lines="3 8 12 17"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/fastapi/teams/tutorial001_py39.py[ln:104-109]!}
-
-# Code here omitted 👈
-
-{!./docs_src/tutorial/fastapi/teams/tutorial001_py39.py[ln:158-163]!}
-
-# Code below omitted 👇
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python hl_lines="3 8 12 17"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:104-109]!}
-
-# Code here omitted 👈
-
-{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:158-163]!}
-
-# Code below omitted 👇
-```
-
-////
-
-/// details | 👀 Full file preview
-
-//// tab | Python 3.10+
-
-```Python
-{!./docs_src/tutorial/fastapi/teams/tutorial001_py310.py!}
-```
-
-////
-
-//// tab | Python 3.9+
-
-```Python
-{!./docs_src/tutorial/fastapi/teams/tutorial001_py39.py!}
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python
-{!./docs_src/tutorial/fastapi/teams/tutorial001.py!}
-```
-
-////
-
-///
+{* ./docs_src/tutorial/fastapi/teams/tutorial001_py310.py ln[102:107,155:160] hl[102,107,155,160] *}
## Don't Include All the Data
@@ -304,69 +132,7 @@ Let's add the models `HeroPublicWithTeam` and `TeamPublicWithHeroes`.
We'll add them **after** the other models so that we can easily reference the previous models.
-//// tab | Python 3.10+
-
-```Python hl_lines="3-4 7-8"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/fastapi/relationships/tutorial001_py310.py[ln:59-64]!}
-
-# Code below omitted 👇
-```
-
-////
-
-//// tab | Python 3.9+
-
-```Python hl_lines="3-4 7-8"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/fastapi/relationships/tutorial001_py39.py[ln:61-66]!}
-
-# Code below omitted 👇
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python hl_lines="3-4 7-8"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/fastapi/relationships/tutorial001.py[ln:61-66]!}
-
-# Code below omitted 👇
-```
-
-////
-
-/// details | 👀 Full file preview
-
-//// tab | Python 3.10+
-
-```Python
-{!./docs_src/tutorial/fastapi/relationships/tutorial001_py310.py!}
-```
-
-////
-
-//// tab | Python 3.9+
-
-```Python
-{!./docs_src/tutorial/fastapi/relationships/tutorial001_py39.py!}
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python
-{!./docs_src/tutorial/fastapi/relationships/tutorial001.py!}
-```
-
-////
-
-///
+{* ./docs_src/tutorial/fastapi/relationships/tutorial001_py310.py ln[59:64] hl[59:60,63:64] *}
These two models are very **simple in code**, but there's a lot happening here. Let's check it out.
@@ -380,13 +146,13 @@ Then we do the same for the `TeamPublicWithHeroes`, it **inherits** from `TeamPu
### Data Models Without Relationship Attributes
-Now, notice that these new fields `team` and `heroes` are not declared with `Relationship()`, because these are not **table models**, they cannot have **relationship attributes** with the magic access to get that data from the database.
+Now, notice that these new fields `team` and `heroes` are not declared with `Relationship()`, because `HeroPublicWithTeam` and `TeamPublicWithHeroes` are not **table models**, they cannot have **relationship attributes** with the magic access to get that data from the database.
Instead, here these are only **data models** that will tell FastAPI **which attributes** to get data from and **which data** to get from them.
### Reference to Other Models
-Also, notice that the field `team` is not declared with this new `TeamPublicWithHeroes`, because that would again create that infinite recursion of data. Instead, we declare it with the normal `TeamPublic` model.
+Also, notice that in the `HeroPublicWithTeam` model, the field `team` is not declared with this new `TeamPublicWithHeroes`, because that would again create that infinite recursion of data. Instead, we declare it with the normal `TeamPublic` model.
And the same for `TeamPublicWithHeroes`, the model used for the new field `heroes` uses `HeroPublic` to get only each hero's data.
@@ -400,81 +166,7 @@ This will tell **FastAPI** to take the object that we return from the *path oper
In the case of the hero, this tells FastAPI to extract the `team` too. And in the case of the team, to extract the list of `heroes` too.
-//// tab | Python 3.10+
-
-```Python hl_lines="3 8 12 17"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/fastapi/relationships/tutorial001_py310.py[ln:111-116]!}
-
-# Code here omitted 👈
-
-{!./docs_src/tutorial/fastapi/relationships/tutorial001_py310.py[ln:165-170]!}
-
-# Code below omitted 👇
-```
-
-////
-
-//// tab | Python 3.9+
-
-```Python hl_lines="3 8 12 17"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/fastapi/relationships/tutorial001_py39.py[ln:113-118]!}
-
-# Code here omitted 👈
-
-{!./docs_src/tutorial/fastapi/relationships/tutorial001_py39.py[ln:167-172]!}
-
-# Code below omitted 👇
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python hl_lines="3 8 12 17"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/fastapi/relationships/tutorial001.py[ln:113-118]!}
-
-# Code here omitted 👈
-
-{!./docs_src/tutorial/fastapi/relationships/tutorial001.py[ln:167-172]!}
-
-# Code below omitted 👇
-```
-
-////
-
-/// details | 👀 Full file preview
-
-//// tab | Python 3.10+
-
-```Python
-{!./docs_src/tutorial/fastapi/relationships/tutorial001_py310.py!}
-```
-
-////
-
-//// tab | Python 3.9+
-
-```Python
-{!./docs_src/tutorial/fastapi/relationships/tutorial001_py39.py!}
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python
-{!./docs_src/tutorial/fastapi/relationships/tutorial001.py!}
-```
-
-////
-
-///
+{* ./docs_src/tutorial/fastapi/relationships/tutorial001_py310.py ln[111:116,164:169] hl[111,116,164,169] *}
## Check It Out in the Docs UI
diff --git a/docs/tutorial/fastapi/response-model.md b/docs/tutorial/fastapi/response-model.md
index b333d58b1a..f9214332c6 100644
--- a/docs/tutorial/fastapi/response-model.md
+++ b/docs/tutorial/fastapi/response-model.md
@@ -32,143 +32,15 @@ We can use `response_model` to tell FastAPI the schema of the data we want to se
For example, we can pass the same `Hero` **SQLModel** class (because it is also a Pydantic model):
-//// tab | Python 3.10+
-
-```Python hl_lines="3"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/fastapi/response_model/tutorial001_py310.py[ln:31-37]!}
-
-# Code below omitted 👇
-```
-
-////
-
-//// tab | Python 3.9+
-
-```Python hl_lines="3"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/fastapi/response_model/tutorial001_py39.py[ln:33-39]!}
-
-# Code below omitted 👇
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python hl_lines="3"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/fastapi/response_model/tutorial001.py[ln:33-39]!}
-
-# Code below omitted 👇
-```
-
-////
-
-/// details | 👀 Full file preview
-
-//// tab | Python 3.10+
-
-```Python
-{!./docs_src/tutorial/fastapi/response_model/tutorial001_py310.py!}
-```
-
-////
-
-//// tab | Python 3.9+
-
-```Python
-{!./docs_src/tutorial/fastapi/response_model/tutorial001_py39.py!}
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python
-{!./docs_src/tutorial/fastapi/response_model/tutorial001.py!}
-```
-
-////
-
-///
+{* ./docs_src/tutorial/fastapi/response_model/tutorial001_py310.py ln[31:37] hl[31] *}
## List of Heroes in `response_model`
We can also use other type annotations, the same way we can use with Pydantic fields. For example, we can pass a list of `Hero`s.
-First, we import `List` from `typing` and then we declare the `response_model` with `List[Hero]`:
-
-//// tab | Python 3.10+
-
-```Python hl_lines="3"
+To do so, we declare the `response_model` with `list[Hero]`:
-# Code here omitted 👈
-
-{!./docs_src/tutorial/fastapi/response_model/tutorial001_py310.py[ln:40-44]!}
-
-# Code below omitted 👇
-```
-
-////
-
-//// tab | Python 3.9+
-
-```Python hl_lines="3"
-
-# Code here omitted 👈
-
-{!./docs_src/tutorial/fastapi/response_model/tutorial001_py39.py[ln:42-46]!}
-
-# Code below omitted 👇
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python hl_lines="1 5"
-{!./docs_src/tutorial/fastapi/response_model/tutorial001.py[ln:1]!}
-
-# Code here omitted 👈
-
-{!./docs_src/tutorial/fastapi/response_model/tutorial001.py[ln:42-46]!}
-
-# Code below omitted 👇
-```
-
-////
-
-/// details | 👀 Full file preview
-
-//// tab | Python 3.10+
-
-```Python
-{!./docs_src/tutorial/fastapi/response_model/tutorial001_py310.py!}
-```
-
-////
-
-//// tab | Python 3.9+
-
-```Python
-{!./docs_src/tutorial/fastapi/response_model/tutorial001_py39.py!}
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python
-{!./docs_src/tutorial/fastapi/response_model/tutorial001.py!}
-```
-
-////
-
-///
+{* ./docs_src/tutorial/fastapi/response_model/tutorial001_py310.py ln[40:44] hl[40] *}
## FastAPI and Response Model
diff --git a/docs/tutorial/fastapi/session-with-dependency.md b/docs/tutorial/fastapi/session-with-dependency.md
index e148452dcb..e81e9e6745 100644
--- a/docs/tutorial/fastapi/session-with-dependency.md
+++ b/docs/tutorial/fastapi/session-with-dependency.md
@@ -6,69 +6,7 @@ Before we keep adding things, let's change a bit how we get the session for each
Up to now, we have been creating a session in each *path operation*, in a `with` block.
-//// tab | Python 3.10+
-
-```Python hl_lines="5"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/fastapi/delete/tutorial001_py310.py[ln:48-55]!}
-
-# Code below omitted 👇
-```
-
-////
-
-//// tab | Python 3.9+
-
-```Python hl_lines="5"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/fastapi/delete/tutorial001_py39.py[ln:50-57]!}
-
-# Code below omitted 👇
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python hl_lines="5"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/fastapi/delete/tutorial001.py[ln:50-57]!}
-
-# Code below omitted 👇
-```
-
-////
-
-/// details | 👀 Full file preview
-
-//// tab | Python 3.10+
-
-```Python
-{!./docs_src/tutorial/fastapi/delete/tutorial001_py310.py!}
-```
-
-////
-
-//// tab | Python 3.9+
-
-```Python
-{!./docs_src/tutorial/fastapi/delete/tutorial001_py39.py!}
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python
-{!./docs_src/tutorial/fastapi/delete/tutorial001.py!}
-```
-
-////
-
-///
+{* ./docs_src/tutorial/fastapi/delete/tutorial001_py310.py ln[48:55] hl[50] *}
That's perfectly fine, but in many use cases we would want to use FastAPI Dependencies, for example to **verify** that the client is **logged in** and get the **current user** before executing any other code in the *path operation*.
@@ -82,69 +20,7 @@ A **FastAPI** dependency is very simple, it's just a function that returns a val
It could use `yield` instead of `return`, and in that case **FastAPI** will make sure it executes all the code **after** the `yield`, once it is done with the request.
-//// tab | Python 3.10+
-
-```Python hl_lines="3-5"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py[ln:40-42]!}
-
-# Code below omitted 👇
-```
-
-////
-
-//// tab | Python 3.9+
-
-```Python hl_lines="3-5"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py[ln:42-44]!}
-
-# Code below omitted 👇
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python hl_lines="3-5"
-# Code above omitted 👆
-
-{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py[ln:42-44]!}
-
-# Code below omitted 👇
-```
-
-////
-
-/// details | 👀 Full file preview
-
-//// tab | Python 3.10+
-
-```Python
-{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py!}
-```
-
-////
-
-//// tab | Python 3.9+
-
-```Python
-{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py!}
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python
-{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py!}
-```
-
-////
-
-///
+{* ./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py ln[40:42] hl[40:42] *}
## Use the Dependency
@@ -152,87 +28,7 @@ Now let's make FastAPI execute a dependency and get its value in the *path opera
We import `Depends()` from `fastapi`. Then we use it in the *path operation function* in a **parameter**, the same way we declared parameters to get JSON bodies, path parameters, etc.
-//// tab | Python 3.10+
-
-```Python hl_lines="1 13"
-{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py[ln:1-2]!}
-
-# Code here omitted 👈
-
-{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py[ln:40-42]!}
-
-# Code here omitted 👈
-
-{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py[ln:53-59]!}
-
-# Code below omitted 👇
-```
-
-////
-
-//// tab | Python 3.9+
-
-```Python hl_lines="3 15"
-{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py[ln:1-4]!}
-
-# Code here omitted 👈
-
-{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py[ln:42-44]!}
-
-# Code here omitted 👈
-
-{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py[ln:55-61]!}
-
-# Code below omitted 👇
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python hl_lines="3 15"
-{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py[ln:1-4]!}
-
-# Code here omitted 👈
-
-{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py[ln:42-44]!}
-
-# Code here omitted 👈
-
-{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py[ln:55-61]!}
-
-# Code below omitted 👇
-```
-
-////
-
-/// details | 👀 Full file preview
-
-//// tab | Python 3.10+
-
-```Python
-{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py!}
-```
-
-////
-
-//// tab | Python 3.9+
-
-```Python
-{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py!}
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python
-{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py!}
-```
-
-////
-
-///
+{* ./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py ln[1:2,40:42,53:59] hl[1,54] *}
/// tip
@@ -260,173 +56,13 @@ And because dependencies can use `yield`, FastAPI will make sure to run the code
This means that in the main code of the *path operation function*, it will work equivalently to the previous version with the explicit `with` block.
-//// tab | Python 3.10+
-
-```Python hl_lines="14-18"
-{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py[ln:1-2]!}
-
-# Code here omitted 👈
-
-{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py[ln:40-42]!}
-
-# Code here omitted 👈
-
-{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py[ln:53-59]!}
-
-# Code below omitted 👇
-```
-
-////
-
-//// tab | Python 3.9+
-
-```Python hl_lines="16-20"
-{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py[ln:1-4]!}
-
-# Code here omitted 👈
-
-{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py[ln:42-44]!}
-
-# Code here omitted 👈
-
-{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py[ln:55-61]!}
-
-# Code below omitted 👇
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python hl_lines="16-20"
-{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py[ln:1-4]!}
-
-# Code here omitted 👈
-
-{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py[ln:42-44]!}
-
-# Code here omitted 👈
-
-{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py[ln:55-61]!}
-
-# Code below omitted 👇
-```
-
-////
-
-/// details | 👀 Full file preview
-
-//// tab | Python 3.10+
-
-```Python
-{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py!}
-```
-
-////
-
-//// tab | Python 3.9+
-
-```Python
-{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py!}
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python
-{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py!}
-```
-
-////
-
-///
+{* ./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py ln[1:2,40:42,53:59] hl[55:59] *}
In fact, you could think that all that block of code inside of the `create_hero()` function is still inside a `with` block for the **session**, because this is more or less what's happening behind the scenes.
But now, the `with` block is not explicitly in the function, but in the dependency above:
-//// tab | Python 3.10+
-
-```Python hl_lines="7-8"
-{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py[ln:1-2]!}
-
-# Code here omitted 👈
-
-{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py[ln:40-42]!}
-
-# Code here omitted 👈
-
-{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py[ln:53-59]!}
-
-# Code below omitted 👇
-```
-
-////
-
-//// tab | Python 3.9+
-
-```Python hl_lines="9-10"
-{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py[ln:1-4]!}
-
-# Code here omitted 👈
-
-{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py[ln:42-44]!}
-
-# Code here omitted 👈
-
-{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py[ln:55-61]!}
-
-# Code below omitted 👇
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python hl_lines="9-10"
-{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py[ln:1-4]!}
-
-# Code here omitted 👈
-
-{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py[ln:42-44]!}
-
-# Code here omitted 👈
-
-{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py[ln:55-61]!}
-
-# Code below omitted 👇
-```
-
-////
-
-/// details | 👀 Full file preview
-
-//// tab | Python 3.10+
-
-```Python
-{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py!}
-```
-
-////
-
-//// tab | Python 3.9+
-
-```Python
-{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py!}
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python
-{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py!}
-```
-
-////
-
-///
+{* ./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py ln[1:2,40:42,53:59] hl[41:42] *}
We will see how this is very useful when testing the code later. ✅
@@ -442,81 +78,7 @@ session: Session = Depends(get_session)
And then we remove the previous `with` block with the old **session**.
-//// tab | Python 3.10+
-
-```Python hl_lines="13 24 33 42 57"
-{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py[ln:1-2]!}
-
-# Code here omitted 👈
-
-{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py[ln:40-42]!}
-
-# Code here omitted 👈
-
-{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py[ln:53-104]!}
-```
-
-////
-
-//// tab | Python 3.9+
-
-```Python hl_lines="15 26 35 44 59"
-{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py[ln:1-4]!}
-
-# Code here omitted 👈
-
-{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py[ln:42-44]!}
-
-# Code here omitted 👈
-
-{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py[ln:55-106]!}
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python hl_lines="15 26 35 44 59"
-{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py[ln:1-4]!}
-
-# Code here omitted 👈
-
-{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py[ln:42-44]!}
-
-# Code here omitted 👈
-
-{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py[ln:55-106]!}
-```
-
-////
-
-/// details | 👀 Full file preview
-
-//// tab | Python 3.10+
-
-```Python
-{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py!}
-```
-
-////
-
-//// tab | Python 3.9+
-
-```Python
-{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py!}
-```
-
-////
-
-//// tab | Python 3.7+
-
-```Python
-{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py!}
-```
-
-////
-
-///
+{* ./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py ln[1:2,40:42,53:104] hl[54,65,74,83,98] *}
## Recap
diff --git a/docs/tutorial/fastapi/simple-hero-api.md b/docs/tutorial/fastapi/simple-hero-api.md
index fc85f2a008..79cf075e1b 100644
--- a/docs/tutorial/fastapi/simple-hero-api.md
+++ b/docs/tutorial/fastapi/simple-hero-api.md
@@ -8,10 +8,6 @@ The first step is to install FastAPI.
FastAPI is the framework to create the **web API**.
-But we also need another type of program to run it, it is called a "**server**". We will use **Uvicorn** for that. And we will install Uvicorn with its *standard* dependencies.
-
-Then install FastAPI.
-
Make sure you create a [virtual environment](../../virtual-environments.md){.internal-link target=_blank}, activate it, and then install them, for example with:
+ SQLModel, SQL databases in Python, designed for simplicity, compatibility, and robustness. +
+ + +--- + +**Documentation**: https://sqlmodel.tiangolo.com + +**Source Code**: https://github.com/fastapi/sqlmodel + +--- + +SQLModel is a library for interacting with SQL databases from Python code, with Python objects. It is designed to be intuitive, easy to use, highly compatible, and robust. + +**SQLModel** is based on Python type annotations, and powered by Pydantic and SQLAlchemy. + +## `sqlmodel-slim` + +⚠️ Do not install this package. ⚠️ + +This package, `sqlmodel-slim`, does nothing other than depend on `sqlmodel`. + +You **should not** install this package. + +Install instead: + +```bash +pip install sqlmodel +``` + +This package is deprecated and will stop receiving any updates and published versions. + +## License + +This project is licensed under the terms of the MIT license. diff --git a/sqlmodel/__init__.py b/sqlmodel/__init__.py index f62988f4ac..8e3cdaae3b 100644 --- a/sqlmodel/__init__.py +++ b/sqlmodel/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.0.22" +__version__ = "0.0.38" # Re-export from SQLAlchemy from sqlalchemy.engine import create_engine as create_engine diff --git a/sqlmodel/_compat.py b/sqlmodel/_compat.py index 4e80cdc374..a220b193f1 100644 --- a/sqlmodel/_compat.py +++ b/sqlmodel/_compat.py @@ -1,31 +1,35 @@ +import sys import types +from collections.abc import Generator from contextlib import contextmanager from contextvars import ContextVar from dataclasses import dataclass from typing import ( TYPE_CHECKING, - AbstractSet, + Annotated, Any, - Callable, - Dict, ForwardRef, - Generator, - Mapping, - Optional, - Set, - Type, + TypeAlias, TypeVar, Union, + get_args, + get_origin, ) +from annotated_types import MaxLen from pydantic import VERSION as P_VERSION from pydantic import BaseModel +from pydantic import ConfigDict as ConfigDict +from pydantic._internal._fields import PydanticMetadata +from pydantic._internal._model_construction import ModelMetaclass as ModelMetaclass +from pydantic._internal._repr import Representation as Representation from pydantic.fields import FieldInfo -from typing_extensions import Annotated, get_args, get_origin +from pydantic_core import PydanticUndefined as Undefined +from pydantic_core import PydanticUndefinedType as PydanticUndefinedType -# Reassign variable to make it reexported for mypy -PYDANTIC_VERSION = P_VERSION -IS_PYDANTIC_V2 = PYDANTIC_VERSION.startswith("2.") +BaseConfig = ConfigDict +UndefinedType = PydanticUndefinedType +PYDANTIC_MINOR_VERSION = tuple(int(i) for i in P_VERSION.split(".")[:2]) if TYPE_CHECKING: @@ -34,20 +38,20 @@ UnionType = getattr(types, "UnionType", Union) NoneType = type(None) T = TypeVar("T") -InstanceOrType = Union[T, Type[T]] +InstanceOrType: TypeAlias = T | type[T] _TSQLModel = TypeVar("_TSQLModel", bound="SQLModel") class FakeMetadata: - max_length: Optional[int] = None - max_digits: Optional[int] = None - decimal_places: Optional[int] = None + max_length: int | None = None + max_digits: int | None = None + decimal_places: int | None = None @dataclass class ObjectWithUpdateWrapper: obj: Any - update: Dict[str, Any] + update: dict[str, Any] def __getattribute__(self, __name: str) -> Any: update = super().__getattribute__("update") @@ -71,505 +75,271 @@ def partial_init() -> Generator[None, None, None]: finish_init.reset(token) -if IS_PYDANTIC_V2: - from annotated_types import MaxLen - from pydantic import ConfigDict as BaseConfig - from pydantic._internal._fields import PydanticMetadata - from pydantic._internal._model_construction import ModelMetaclass - from pydantic._internal._repr import Representation as Representation - from pydantic_core import PydanticUndefined as Undefined - from pydantic_core import PydanticUndefinedType as UndefinedType - - # Dummy for types, to make it importable - class ModelField: - pass - - class SQLModelConfig(BaseConfig, total=False): - table: Optional[bool] - registry: Optional[Any] - - def get_config_value( - *, model: InstanceOrType["SQLModel"], parameter: str, default: Any = None - ) -> Any: - return model.model_config.get(parameter, default) - - def set_config_value( - *, - model: InstanceOrType["SQLModel"], - parameter: str, - value: Any, - ) -> None: - model.model_config[parameter] = value # type: ignore[literal-required] - - def get_model_fields(model: InstanceOrType[BaseModel]) -> Dict[str, "FieldInfo"]: - return model.model_fields - - def get_fields_set( - object: InstanceOrType["SQLModel"], - ) -> Union[Set[str], Callable[[BaseModel], Set[str]]]: - return object.model_fields_set - - def init_pydantic_private_attrs(new_object: InstanceOrType["SQLModel"]) -> None: - object.__setattr__(new_object, "__pydantic_fields_set__", set()) - object.__setattr__(new_object, "__pydantic_extra__", None) - object.__setattr__(new_object, "__pydantic_private__", None) - - def get_annotations(class_dict: Dict[str, Any]) -> Dict[str, Any]: - return class_dict.get("__annotations__", {}) - - def is_table_model_class(cls: Type[Any]) -> bool: - config = getattr(cls, "model_config", {}) - if config: - return config.get("table", False) or False - return False +class SQLModelConfig(BaseConfig, total=False): + table: bool | None + registry: Any | None - def get_relationship_to( - name: str, - rel_info: "RelationshipInfo", - annotation: Any, - ) -> Any: - origin = get_origin(annotation) - use_annotation = annotation - # Direct relationships (e.g. 'Team' or Team) have None as an origin - if origin is None: - if isinstance(use_annotation, ForwardRef): - use_annotation = use_annotation.__forward_arg__ - else: - return use_annotation - # If Union (e.g. Optional), get the real field - elif _is_union_type(origin): - use_annotation = get_args(annotation) - if len(use_annotation) > 2: - raise ValueError( - "Cannot have a (non-optional) union as a SQLAlchemy field" - ) - arg1, arg2 = use_annotation - if arg1 is NoneType and arg2 is not NoneType: - use_annotation = arg2 - elif arg2 is NoneType and arg1 is not NoneType: - use_annotation = arg1 - else: - raise ValueError( - "Cannot have a Union of None and None as a SQLAlchemy field" - ) - - # If a list, then also get the real field - elif origin is list: - use_annotation = get_args(annotation)[0] - - return get_relationship_to( - name=name, rel_info=rel_info, annotation=use_annotation - ) - def is_field_noneable(field: "FieldInfo") -> bool: - if getattr(field, "nullable", Undefined) is not Undefined: - return field.nullable # type: ignore - origin = get_origin(field.annotation) - if origin is not None and _is_union_type(origin): - args = get_args(field.annotation) - if any(arg is NoneType for arg in args): - return True - if not field.is_required(): - if field.default is Undefined: - return False - if field.annotation is None or field.annotation is NoneType: # type: ignore[comparison-overlap] - return True - return False - return False +def get_model_fields(model: InstanceOrType[BaseModel]) -> dict[str, "FieldInfo"]: + # TODO: refactor the usage of this function to always pass the class + # not the instance, and then remove this extra check + # this is for compatibility with Pydantic v3 + if isinstance(model, type): + use_model = model + else: + use_model = model.__class__ + return use_model.model_fields - def get_sa_type_from_type_annotation(annotation: Any) -> Any: - # Resolve Optional fields - if annotation is None: - raise ValueError("Missing field type") - origin = get_origin(annotation) - if origin is None: - return annotation - elif origin is Annotated: - return get_sa_type_from_type_annotation(get_args(annotation)[0]) - if _is_union_type(origin): - bases = get_args(annotation) - if len(bases) > 2: - raise ValueError( - "Cannot have a (non-optional) union as a SQLAlchemy field" - ) - # Non optional unions are not allowed - if bases[0] is not NoneType and bases[1] is not NoneType: - raise ValueError( - "Cannot have a (non-optional) union as a SQLAlchemy field" - ) - # Optional unions are allowed - use_type = bases[0] if bases[0] is not NoneType else bases[1] - return get_sa_type_from_type_annotation(use_type) - return origin - - def get_sa_type_from_field(field: Any) -> Any: - type_: Any = field.annotation - return get_sa_type_from_type_annotation(type_) - - def get_field_metadata(field: Any) -> Any: - for meta in field.metadata: - if isinstance(meta, (PydanticMetadata, MaxLen)): - return meta - return FakeMetadata() - - def post_init_field_info(field_info: FieldInfo) -> None: - return None - - # Dummy to make it importable - def _calculate_keys( - self: "SQLModel", - include: Optional[Mapping[Union[int, str], Any]], - exclude: Optional[Mapping[Union[int, str], Any]], - exclude_unset: bool, - update: Optional[Dict[str, Any]] = None, - ) -> Optional[AbstractSet[str]]: # pragma: no cover - return None - - def sqlmodel_table_construct( - *, - self_instance: _TSQLModel, - values: Dict[str, Any], - _fields_set: Union[Set[str], None] = None, - ) -> _TSQLModel: - # Copy from Pydantic's BaseModel.construct() - # Ref: https://github.com/pydantic/pydantic/blob/v2.5.2/pydantic/main.py#L198 - # Modified to not include everything, only the model fields, and to - # set relationships - # SQLModel override to get class SQLAlchemy __dict__ attributes and - # set them back in after creating the object - # new_obj = cls.__new__(cls) - cls = type(self_instance) - old_dict = self_instance.__dict__.copy() - # End SQLModel override - - fields_values: Dict[str, Any] = {} - defaults: Dict[ - str, Any - ] = {} # keeping this separate from `fields_values` helps us compute `_fields_set` - for name, field in cls.model_fields.items(): - if field.alias and field.alias in values: - fields_values[name] = values.pop(field.alias) - elif name in values: - fields_values[name] = values.pop(name) - elif not field.is_required(): - defaults[name] = field.get_default(call_default_factory=True) - if _fields_set is None: - _fields_set = set(fields_values.keys()) - fields_values.update(defaults) - - _extra: Union[Dict[str, Any], None] = None - if cls.model_config.get("extra") == "allow": - _extra = {} - for k, v in values.items(): - _extra[k] = v - # SQLModel override, do not include everything, only the model fields - # else: - # fields_values.update(values) - # End SQLModel override - # SQLModel override - # Do not set __dict__, instead use setattr to trigger SQLAlchemy - # object.__setattr__(new_obj, "__dict__", fields_values) - # instrumentation - for key, value in {**old_dict, **fields_values}.items(): - setattr(self_instance, key, value) - # End SQLModel override - object.__setattr__(self_instance, "__pydantic_fields_set__", _fields_set) - if not cls.__pydantic_root_model__: - object.__setattr__(self_instance, "__pydantic_extra__", _extra) - - if cls.__pydantic_post_init__: - self_instance.model_post_init(None) - elif not cls.__pydantic_root_model__: - # Note: if there are any private attributes, cls.__pydantic_post_init__ would exist - # Since it doesn't, that means that `__pydantic_private__` should be set to None - object.__setattr__(self_instance, "__pydantic_private__", None) - # SQLModel override, set relationships - # Get and set any relationship objects - for key in self_instance.__sqlmodel_relationships__: - value = values.get(key, Undefined) - if value is not Undefined: - setattr(self_instance, key, value) - # End SQLModel override - return self_instance - - def sqlmodel_validate( - cls: Type[_TSQLModel], - obj: Any, - *, - strict: Union[bool, None] = None, - from_attributes: Union[bool, None] = None, - context: Union[Dict[str, Any], None] = None, - update: Union[Dict[str, Any], None] = None, - ) -> _TSQLModel: - if not is_table_model_class(cls): - new_obj: _TSQLModel = cls.__new__(cls) - else: - # If table, create the new instance normally to make SQLAlchemy create - # the _sa_instance_state attribute - # The wrapper of this function should use with _partial_init() - with partial_init(): - new_obj = cls() - # SQLModel Override to get class SQLAlchemy __dict__ attributes and - # set them back in after creating the object - old_dict = new_obj.__dict__.copy() - use_obj = obj - if isinstance(obj, dict) and update: - use_obj = {**obj, **update} - elif update: - use_obj = ObjectWithUpdateWrapper(obj=obj, update=update) - cls.__pydantic_validator__.validate_python( - use_obj, - strict=strict, - from_attributes=from_attributes, - context=context, - self_instance=new_obj, + +def init_pydantic_private_attrs(new_object: InstanceOrType["SQLModel"]) -> None: + object.__setattr__(new_object, "__pydantic_fields_set__", set()) + object.__setattr__(new_object, "__pydantic_extra__", None) + object.__setattr__(new_object, "__pydantic_private__", None) + + +def get_annotations(class_dict: dict[str, Any]) -> dict[str, Any]: + raw_annotations: dict[str, Any] = class_dict.get("__annotations__", {}) + if sys.version_info >= (3, 14) and "__annotations__" not in class_dict: + # See https://github.com/pydantic/pydantic/pull/11991 + from annotationlib import ( + Format, + call_annotate_function, + get_annotate_from_class_namespace, ) - # Capture fields set to restore it later - fields_set = new_obj.__pydantic_fields_set__.copy() - if not is_table_model_class(cls): - # If not table, normal Pydantic code, set __dict__ - new_obj.__dict__ = {**old_dict, **new_obj.__dict__} + + if annotate := get_annotate_from_class_namespace(class_dict): + raw_annotations = call_annotate_function(annotate, format=Format.FORWARDREF) + return raw_annotations + + +def is_table_model_class(cls: type[Any]) -> bool: + config = getattr(cls, "model_config", {}) + if config: + return config.get("table", False) or False + return False + + +def get_relationship_to( + name: str, + rel_info: "RelationshipInfo", + annotation: Any, +) -> Any: + origin = get_origin(annotation) + use_annotation = annotation + # Direct relationships (e.g. 'Team' or Team) have None as an origin + if origin is None: + if isinstance(use_annotation, ForwardRef): + use_annotation = use_annotation.__forward_arg__ else: - # Do not set __dict__, instead use setattr to trigger SQLAlchemy - # instrumentation - for key, value in {**old_dict, **new_obj.__dict__}.items(): - setattr(new_obj, key, value) - # Restore fields set - object.__setattr__(new_obj, "__pydantic_fields_set__", fields_set) - # Get and set any relationship objects - if is_table_model_class(cls): - for key in new_obj.__sqlmodel_relationships__: - value = getattr(use_obj, key, Undefined) - if value is not Undefined: - setattr(new_obj, key, value) - return new_obj - - def sqlmodel_init(*, self: "SQLModel", data: Dict[str, Any]) -> None: - old_dict = self.__dict__.copy() - if not is_table_model_class(self.__class__): - self.__pydantic_validator__.validate_python( - data, - self_instance=self, - ) + return use_annotation + # If Union (e.g. Optional), get the real field + elif _is_union_type(origin): + use_annotation = get_args(annotation) + if len(use_annotation) > 2: + raise ValueError("Cannot have a (non-optional) union as a SQLAlchemy field") + arg1, arg2 = use_annotation + if arg1 is NoneType and arg2 is not NoneType: + use_annotation = arg2 + elif arg2 is NoneType and arg1 is not NoneType: + use_annotation = arg1 else: - sqlmodel_table_construct( - self_instance=self, - values=data, + raise ValueError( + "Cannot have a Union of None and None as a SQLAlchemy field" ) - object.__setattr__( - self, - "__dict__", - {**old_dict, **self.__dict__}, - ) -else: - from pydantic import BaseConfig as BaseConfig # type: ignore[assignment] - from pydantic.errors import ConfigError - from pydantic.fields import ( # type: ignore[attr-defined, no-redef] - SHAPE_SINGLETON, - ModelField, - ) - from pydantic.fields import ( # type: ignore[attr-defined, no-redef] - Undefined as Undefined, # noqa - ) - from pydantic.fields import ( # type: ignore[attr-defined, no-redef] - UndefinedType as UndefinedType, - ) - from pydantic.main import ( # type: ignore[no-redef] - ModelMetaclass as ModelMetaclass, - ) - from pydantic.main import validate_model - from pydantic.typing import resolve_annotations - from pydantic.utils import ROOT_KEY, ValueItems - from pydantic.utils import ( # type: ignore[no-redef] - Representation as Representation, - ) + # If a list, then also get the real field + elif origin is list: + use_annotation = get_args(annotation)[0] + + return get_relationship_to(name=name, rel_info=rel_info, annotation=use_annotation) - class SQLModelConfig(BaseConfig): # type: ignore[no-redef] - table: Optional[bool] = None # type: ignore[misc] - registry: Optional[Any] = None # type: ignore[misc] - - def get_config_value( - *, model: InstanceOrType["SQLModel"], parameter: str, default: Any = None - ) -> Any: - return getattr(model.__config__, parameter, default) # type: ignore[union-attr] - - def set_config_value( - *, - model: InstanceOrType["SQLModel"], - parameter: str, - value: Any, - ) -> None: - setattr(model.__config__, parameter, value) # type: ignore - - def get_model_fields(model: InstanceOrType[BaseModel]) -> Dict[str, "FieldInfo"]: - return model.__fields__ # type: ignore - - def get_fields_set( - object: InstanceOrType["SQLModel"], - ) -> Union[Set[str], Callable[[BaseModel], Set[str]]]: - return object.__fields_set__ - - def init_pydantic_private_attrs(new_object: InstanceOrType["SQLModel"]) -> None: - object.__setattr__(new_object, "__fields_set__", set()) - - def get_annotations(class_dict: Dict[str, Any]) -> Dict[str, Any]: - return resolve_annotations( # type: ignore[no-any-return] - class_dict.get("__annotations__", {}), - class_dict.get("__module__", None), - ) - def is_table_model_class(cls: Type[Any]) -> bool: - config = getattr(cls, "__config__", None) - if config: - return getattr(config, "table", False) +def is_field_noneable(field: "FieldInfo") -> bool: + if getattr(field, "nullable", Undefined) is not Undefined: + return field.nullable # type: ignore + origin = get_origin(field.annotation) + if origin is not None and _is_union_type(origin): + args = get_args(field.annotation) + if any(arg is NoneType for arg in args): + return True + if not field.is_required(): + if field.default is Undefined: + return False + if field.annotation is None or field.annotation is NoneType: + return True return False + return False + + +def get_sa_type_from_type_annotation(annotation: Any) -> Any: + # Resolve Optional fields + if annotation is None: + raise ValueError("Missing field type") + origin = get_origin(annotation) + if origin is None: + return annotation + elif origin is Annotated: + return get_sa_type_from_type_annotation(get_args(annotation)[0]) + if _is_union_type(origin): + bases = get_args(annotation) + if len(bases) > 2: + raise ValueError("Cannot have a (non-optional) union as a SQLAlchemy field") + # Non optional unions are not allowed + if bases[0] is not NoneType and bases[1] is not NoneType: + raise ValueError("Cannot have a (non-optional) union as a SQLAlchemy field") + # Optional unions are allowed + use_type = bases[0] if bases[0] is not NoneType else bases[1] + return get_sa_type_from_type_annotation(use_type) + return origin + + +def get_sa_type_from_field(field: Any) -> Any: + type_: Any = field.annotation + return get_sa_type_from_type_annotation(type_) + + +def get_field_metadata(field: Any) -> Any: + for meta in field.metadata: + if isinstance(meta, (PydanticMetadata, MaxLen)): + return meta + return FakeMetadata() + + +def sqlmodel_table_construct( + *, + self_instance: _TSQLModel, + values: dict[str, Any], + _fields_set: set[str] | None = None, +) -> _TSQLModel: + # Copy from Pydantic's BaseModel.construct() + # Ref: https://github.com/pydantic/pydantic/blob/v2.5.2/pydantic/main.py#L198 + # Modified to not include everything, only the model fields, and to + # set relationships + # SQLModel override to get class SQLAlchemy __dict__ attributes and + # set them back in after creating the object + # new_obj = cls.__new__(cls) + cls = type(self_instance) + old_dict = self_instance.__dict__.copy() + # End SQLModel override + + fields_values: dict[str, Any] = {} + defaults: dict[ + str, Any + ] = {} # keeping this separate from `fields_values` helps us compute `_fields_set` + for name, field in cls.model_fields.items(): + if field.alias and field.alias in values: + fields_values[name] = values.pop(field.alias) + elif name in values: + fields_values[name] = values.pop(name) + elif not field.is_required(): + defaults[name] = field.get_default(call_default_factory=True) + if _fields_set is None: + _fields_set = set(fields_values.keys()) + fields_values.update(defaults) + + _extra: dict[str, Any] | None = None + if cls.model_config.get("extra") == "allow": + _extra = {} + for k, v in values.items(): + _extra[k] = v + # SQLModel override, do not include everything, only the model fields + # else: + # fields_values.update(values) + # End SQLModel override + # SQLModel override + # Do not set __dict__, instead use setattr to trigger SQLAlchemy + # object.__setattr__(new_obj, "__dict__", fields_values) + # instrumentation + for key, value in {**old_dict, **fields_values}.items(): + setattr(self_instance, key, value) + # End SQLModel override + object.__setattr__(self_instance, "__pydantic_fields_set__", _fields_set) + if not cls.__pydantic_root_model__: + object.__setattr__(self_instance, "__pydantic_extra__", _extra) + + if cls.__pydantic_post_init__: + self_instance.model_post_init(None) + elif not cls.__pydantic_root_model__: + # Note: if there are any private attributes, cls.__pydantic_post_init__ would exist + # Since it doesn't, that means that `__pydantic_private__` should be set to None + object.__setattr__(self_instance, "__pydantic_private__", None) + # SQLModel override, set relationships + # Get and set any relationship objects + for key in self_instance.__sqlmodel_relationships__: + value = values.get(key, Undefined) + if value is not Undefined: + setattr(self_instance, key, value) + # End SQLModel override + return self_instance + + +def sqlmodel_validate( + cls: type[_TSQLModel], + obj: Any, + *, + strict: bool | None = None, + from_attributes: bool | None = None, + context: dict[str, Any] | None = None, + update: dict[str, Any] | None = None, +) -> _TSQLModel: + if not is_table_model_class(cls): + new_obj: _TSQLModel = cls.__new__(cls) + else: + # If table, create the new instance normally to make SQLAlchemy create + # the _sa_instance_state attribute + # The wrapper of this function should use with _partial_init() + with partial_init(): + new_obj = cls() + # SQLModel Override to get class SQLAlchemy __dict__ attributes and + # set them back in after creating the object + old_dict = new_obj.__dict__.copy() + use_obj = obj + if isinstance(obj, dict) and update: + use_obj = {**obj, **update} + elif update: + use_obj = ObjectWithUpdateWrapper(obj=obj, update=update) + cls.__pydantic_validator__.validate_python( + use_obj, + strict=strict, + from_attributes=from_attributes, + context=context, + self_instance=new_obj, + ) + # Capture fields set to restore it later + fields_set = new_obj.__pydantic_fields_set__.copy() + if not is_table_model_class(cls): + # If not table, normal Pydantic code, set __dict__ + new_obj.__dict__ = {**old_dict, **new_obj.__dict__} + else: + # Do not set __dict__, instead use setattr to trigger SQLAlchemy + # instrumentation + for key, value in {**old_dict, **new_obj.__dict__}.items(): + setattr(new_obj, key, value) + # Restore fields set + object.__setattr__(new_obj, "__pydantic_fields_set__", fields_set) + # Get and set any relationship objects + if is_table_model_class(cls): + for key in new_obj.__sqlmodel_relationships__: + value = getattr(use_obj, key, Undefined) + if value is not Undefined: + setattr(new_obj, key, value) + return new_obj - def get_relationship_to( - name: str, - rel_info: "RelationshipInfo", - annotation: Any, - ) -> Any: - temp_field = ModelField.infer( # type: ignore[attr-defined] - name=name, - value=rel_info, - annotation=annotation, - class_validators=None, - config=SQLModelConfig, + +def sqlmodel_init(*, self: "SQLModel", data: dict[str, Any]) -> None: + old_dict = self.__dict__.copy() + if not is_table_model_class(self.__class__): + self.__pydantic_validator__.validate_python( + data, + self_instance=self, ) - relationship_to = temp_field.type_ - if isinstance(temp_field.type_, ForwardRef): - relationship_to = temp_field.type_.__forward_arg__ - return relationship_to - - def is_field_noneable(field: "FieldInfo") -> bool: - if not field.required: # type: ignore[attr-defined] - # Taken from [Pydantic](https://github.com/samuelcolvin/pydantic/blob/v1.8.2/pydantic/fields.py#L946-L947) - return field.allow_none and ( # type: ignore[attr-defined] - field.shape != SHAPE_SINGLETON or not field.sub_fields # type: ignore[attr-defined] - ) - return field.allow_none # type: ignore[no-any-return, attr-defined] - - def get_sa_type_from_field(field: Any) -> Any: - if isinstance(field.type_, type) and field.shape == SHAPE_SINGLETON: - return field.type_ - raise ValueError(f"The field {field.name} has no matching SQLAlchemy type") - - def get_field_metadata(field: Any) -> Any: - metadata = FakeMetadata() - metadata.max_length = field.field_info.max_length - metadata.max_digits = getattr(field.type_, "max_digits", None) - metadata.decimal_places = getattr(field.type_, "decimal_places", None) - return metadata - - def post_init_field_info(field_info: FieldInfo) -> None: - field_info._validate() # type: ignore[attr-defined] - - def _calculate_keys( - self: "SQLModel", - include: Optional[Mapping[Union[int, str], Any]], - exclude: Optional[Mapping[Union[int, str], Any]], - exclude_unset: bool, - update: Optional[Dict[str, Any]] = None, - ) -> Optional[AbstractSet[str]]: - if include is None and exclude is None and not exclude_unset: - # Original in Pydantic: - # return None - # Updated to not return SQLAlchemy attributes - # Do not include relationships as that would easily lead to infinite - # recursion, or traversing the whole database - return ( - self.__fields__.keys() # noqa - ) # | self.__sqlmodel_relationships__.keys() - - keys: AbstractSet[str] - if exclude_unset: - keys = self.__fields_set__.copy() # noqa - else: - # Original in Pydantic: - # keys = self.__dict__.keys() - # Updated to not return SQLAlchemy attributes - # Do not include relationships as that would easily lead to infinite - # recursion, or traversing the whole database - keys = ( - self.__fields__.keys() # noqa - ) # | self.__sqlmodel_relationships__.keys() - if include is not None: - keys &= include.keys() - - if update: - keys -= update.keys() - - if exclude: - keys -= {k for k, v in exclude.items() if ValueItems.is_true(v)} - - return keys - - def sqlmodel_validate( - cls: Type[_TSQLModel], - obj: Any, - *, - strict: Union[bool, None] = None, - from_attributes: Union[bool, None] = None, - context: Union[Dict[str, Any], None] = None, - update: Union[Dict[str, Any], None] = None, - ) -> _TSQLModel: - # This was SQLModel's original from_orm() for Pydantic v1 - # Duplicated from Pydantic - if not cls.__config__.orm_mode: # type: ignore[attr-defined] # noqa - raise ConfigError( - "You must have the config attribute orm_mode=True to use from_orm" - ) - if not isinstance(obj, Mapping): - obj = ( - {ROOT_KEY: obj} - if cls.__custom_root_type__ # type: ignore[attr-defined] # noqa - else cls._decompose_class(obj) # type: ignore[attr-defined] # noqa - ) - # SQLModel, support update dict - if update is not None: - obj = {**obj, **update} - # End SQLModel support dict - if not getattr(cls.__config__, "table", False): # noqa - # If not table, normal Pydantic code - m: _TSQLModel = cls.__new__(cls) - else: - # If table, create the new instance normally to make SQLAlchemy create - # the _sa_instance_state attribute - m = cls() - values, fields_set, validation_error = validate_model(cls, obj) - if validation_error: - raise validation_error - # Updated to trigger SQLAlchemy internal handling - if not getattr(cls.__config__, "table", False): # noqa - object.__setattr__(m, "__dict__", values) - else: - for key, value in values.items(): - setattr(m, key, value) - # Continue with standard Pydantic logic - object.__setattr__(m, "__fields_set__", fields_set) - m._init_private_attributes() # type: ignore[attr-defined] # noqa - return m - - def sqlmodel_init(*, self: "SQLModel", data: Dict[str, Any]) -> None: - values, fields_set, validation_error = validate_model(self.__class__, data) - # Only raise errors if not a SQLModel model - if ( - not is_table_model_class(self.__class__) # noqa - and validation_error - ): - raise validation_error - if not is_table_model_class(self.__class__): - object.__setattr__(self, "__dict__", values) - else: - # Do not set values as in Pydantic, pass them through setattr, so - # SQLAlchemy can handle them - for key, value in values.items(): - setattr(self, key, value) - object.__setattr__(self, "__fields_set__", fields_set) - non_pydantic_keys = data.keys() - values.keys() - - if is_table_model_class(self.__class__): - for key in non_pydantic_keys: - if key in self.__sqlmodel_relationships__: - setattr(self, key, data[key]) + else: + sqlmodel_table_construct( + self_instance=self, + values=data, + ) + object.__setattr__( + self, + "__dict__", + {**old_dict, **self.__dict__}, + ) diff --git a/sqlmodel/ext/asyncio/session.py b/sqlmodel/ext/asyncio/session.py index 467d0bd84e..f2b989e2fa 100644 --- a/sqlmodel/ext/asyncio/session.py +++ b/sqlmodel/ext/asyncio/session.py @@ -1,17 +1,13 @@ +from collections.abc import Mapping, Sequence from typing import ( Any, - Dict, - Mapping, - Optional, - Sequence, - Type, TypeVar, - Union, cast, overload, ) from sqlalchemy import util +from sqlalchemy.engine.cursor import CursorResult from sqlalchemy.engine.interfaces import _CoreAnyExecuteParams from sqlalchemy.engine.result import Result, ScalarResult, TupleResult from sqlalchemy.ext.asyncio import AsyncSession as _AsyncSession @@ -19,6 +15,7 @@ from sqlalchemy.ext.asyncio.session import _EXECUTE_OPTIONS from sqlalchemy.orm._typing import OrmExecuteOptionsParameter from sqlalchemy.sql.base import Executable as _Executable +from sqlalchemy.sql.dml import UpdateBase from sqlalchemy.util.concurrency import greenlet_spawn from typing_extensions import deprecated @@ -30,7 +27,7 @@ class AsyncSession(_AsyncSession): - sync_session_class: Type[Session] = Session + sync_session_class: type[Session] = Session sync_session: Session @overload @@ -38,11 +35,11 @@ async def exec( self, statement: Select[_TSelectParam], *, - params: Optional[Union[Mapping[str, Any], Sequence[Mapping[str, Any]]]] = None, + params: Mapping[str, Any] | Sequence[Mapping[str, Any]] | None = None, execution_options: Mapping[str, Any] = util.EMPTY_DICT, - bind_arguments: Optional[Dict[str, Any]] = None, - _parent_execute_state: Optional[Any] = None, - _add_event: Optional[Any] = None, + bind_arguments: dict[str, Any] | None = None, + _parent_execute_state: Any | None = None, + _add_event: Any | None = None, ) -> TupleResult[_TSelectParam]: ... @overload @@ -50,27 +47,38 @@ async def exec( self, statement: SelectOfScalar[_TSelectParam], *, - params: Optional[Union[Mapping[str, Any], Sequence[Mapping[str, Any]]]] = None, + params: Mapping[str, Any] | Sequence[Mapping[str, Any]] | None = None, execution_options: Mapping[str, Any] = util.EMPTY_DICT, - bind_arguments: Optional[Dict[str, Any]] = None, - _parent_execute_state: Optional[Any] = None, - _add_event: Optional[Any] = None, + bind_arguments: dict[str, Any] | None = None, + _parent_execute_state: Any | None = None, + _add_event: Any | None = None, ) -> ScalarResult[_TSelectParam]: ... + @overload + async def exec( + self, + statement: UpdateBase, + *, + params: Mapping[str, Any] | Sequence[Mapping[str, Any]] | None = None, + execution_options: Mapping[str, Any] = util.EMPTY_DICT, + bind_arguments: dict[str, Any] | None = None, + _parent_execute_state: Any | None = None, + _add_event: Any | None = None, + ) -> CursorResult[Any]: ... + async def exec( self, - statement: Union[ - Select[_TSelectParam], - SelectOfScalar[_TSelectParam], - Executable[_TSelectParam], - ], + statement: Select[_TSelectParam] + | SelectOfScalar[_TSelectParam] + | Executable[_TSelectParam] + | UpdateBase, *, - params: Optional[Union[Mapping[str, Any], Sequence[Mapping[str, Any]]]] = None, + params: Mapping[str, Any] | Sequence[Mapping[str, Any]] | None = None, execution_options: Mapping[str, Any] = util.EMPTY_DICT, - bind_arguments: Optional[Dict[str, Any]] = None, - _parent_execute_state: Optional[Any] = None, - _add_event: Optional[Any] = None, - ) -> Union[TupleResult[_TSelectParam], ScalarResult[_TSelectParam]]: + bind_arguments: dict[str, Any] | None = None, + _parent_execute_state: Any | None = None, + _add_event: Any | None = None, + ) -> TupleResult[_TSelectParam] | ScalarResult[_TSelectParam] | CursorResult[Any]: if execution_options: execution_options = util.immutabledict(execution_options).union( _EXECUTE_OPTIONS @@ -102,25 +110,27 @@ async def exec( For example: ```Python - heroes = await session.execute(select(Hero)).scalars().all() + result = await session.execute(select(Hero)) + heroes = result.scalars().all() ``` instead you could use `exec()`: ```Python - heroes = await session.exec(select(Hero)).all() + result = await session.exec(select(Hero)) + heroes = result.all() ``` """ ) - async def execute( # type: ignore + async def execute( self, statement: _Executable, - params: Optional[_CoreAnyExecuteParams] = None, + params: _CoreAnyExecuteParams | None = None, *, execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT, - bind_arguments: Optional[Dict[str, Any]] = None, - _parent_execute_state: Optional[Any] = None, - _add_event: Optional[Any] = None, + bind_arguments: dict[str, Any] | None = None, + _parent_execute_state: Any | None = None, + _add_event: Any | None = None, ) -> Result[Any]: """ 🚨 You probably want to use `session.exec()` instead of `session.execute()`. @@ -131,13 +141,15 @@ async def execute( # type: ignore For example: ```Python - heroes = await session.execute(select(Hero)).scalars().all() + result = await session.execute(select(Hero)) + heroes = result.scalars().all() ``` instead you could use `exec()`: ```Python - heroes = await session.exec(select(Hero)).all() + result = await session.exec(select(Hero)) + heroes = result.all() ``` """ return await super().execute( diff --git a/sqlmodel/main.py b/sqlmodel/main.py index 1597e4e04f..9a1a676775 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -1,27 +1,24 @@ +from __future__ import annotations + +import builtins import ipaddress import uuid -import weakref +from collections.abc import Callable, Mapping, Sequence, Set +from dataclasses import dataclass from datetime import date, datetime, time, timedelta from decimal import Decimal from enum import Enum from pathlib import Path from typing import ( TYPE_CHECKING, - AbstractSet, Any, - Callable, ClassVar, - Dict, - List, - Mapping, - Optional, - Sequence, - Set, - Tuple, - Type, + Literal, + TypeAlias, TypeVar, Union, cast, + get_origin, overload, ) @@ -52,22 +49,18 @@ from sqlalchemy.orm.instrumentation import is_instrumented from sqlalchemy.sql.schema import MetaData from sqlalchemy.sql.sqltypes import LargeBinary, Time, Uuid -from typing_extensions import Literal, deprecated, get_origin +from typing_extensions import deprecated -from ._compat import ( # type: ignore[attr-defined] - IS_PYDANTIC_V2, - PYDANTIC_VERSION, +from ._compat import ( + PYDANTIC_MINOR_VERSION, BaseConfig, - ModelField, ModelMetaclass, Representation, SQLModelConfig, Undefined, UndefinedType, - _calculate_keys, finish_init, get_annotations, - get_config_value, get_field_metadata, get_model_fields, get_relationship_to, @@ -75,8 +68,6 @@ init_pydantic_private_attrs, is_field_noneable, is_table_model_class, - post_init_field_info, - set_config_value, sqlmodel_init, sqlmodel_validate, ) @@ -90,7 +81,12 @@ _T = TypeVar("_T") NoArgAnyCallable = Callable[[], Any] -IncEx = Union[Set[int], Set[str], Dict[int, Any], Dict[str, Any], None] +IncEx: TypeAlias = ( + set[int] + | set[str] + | Mapping[int, Union["IncEx", bool]] + | Mapping[str, Union["IncEx", bool]] +) OnDeleteType = Literal["CASCADE", "SET NULL", "RESTRICT"] @@ -99,12 +95,13 @@ def __dataclass_transform__( eq_default: bool = True, order_default: bool = False, kw_only_default: bool = False, - field_descriptors: Tuple[Union[type, Callable[..., Any]], ...] = (()), + field_descriptors: tuple[type | Callable[..., Any], ...] = (()), ) -> Callable[[_T], _T]: return lambda a: a -class FieldInfo(PydanticFieldInfo): +class FieldInfo(PydanticFieldInfo): # ty: ignore[subclass-of-final-class] + # mypy - ignore that PydanticFieldInfo is @final def __init__(self, default: Any = Undefined, **kwargs: Any) -> None: primary_key = kwargs.pop("primary_key", False) nullable = kwargs.pop("nullable", Undefined) @@ -129,8 +126,7 @@ def __init__(self, default: Any = Undefined, **kwargs: Any) -> None: ) if primary_key is not Undefined: raise RuntimeError( - "Passing primary_key is not supported when " - "also passing a sa_column" + "Passing primary_key is not supported when also passing a sa_column" ) if nullable is not Undefined: raise RuntimeError( @@ -138,8 +134,7 @@ def __init__(self, default: Any = Undefined, **kwargs: Any) -> None: ) if foreign_key is not Undefined: raise RuntimeError( - "Passing foreign_key is not supported when " - "also passing a sa_column" + "Passing foreign_key is not supported when also passing a sa_column" ) if ondelete is not Undefined: raise RuntimeError( @@ -177,13 +172,13 @@ class RelationshipInfo(Representation): def __init__( self, *, - back_populates: Optional[str] = None, - cascade_delete: Optional[bool] = False, - passive_deletes: Optional[Union[bool, Literal["all"]]] = False, - link_model: Optional[Any] = None, - sa_relationship: Optional[RelationshipProperty] = None, # type: ignore - sa_relationship_args: Optional[Sequence[Any]] = None, - sa_relationship_kwargs: Optional[Mapping[str, Any]] = None, + back_populates: str | None = None, + cascade_delete: bool | None = False, + passive_deletes: bool | Literal["all"] | None = False, + link_model: Any | None = None, + sa_relationship: RelationshipProperty | None = None, + sa_relationship_args: Sequence[Any] | None = None, + sa_relationship_kwargs: Mapping[str, Any] | None = None, ) -> None: if sa_relationship is not None: if sa_relationship_args is not None: @@ -205,47 +200,77 @@ def __init__( self.sa_relationship_kwargs = sa_relationship_kwargs +@dataclass +class FieldInfoMetadata: + primary_key: bool | UndefinedType = Undefined + nullable: bool | UndefinedType = Undefined + foreign_key: Any = Undefined + ondelete: OnDeleteType | UndefinedType = Undefined + unique: bool | UndefinedType = Undefined + index: bool | UndefinedType = Undefined + sa_type: type[Any] | UndefinedType = Undefined + sa_column: Column[Any] | UndefinedType = Undefined + sa_column_args: Sequence[Any] | UndefinedType = Undefined + sa_column_kwargs: Mapping[str, Any] | UndefinedType = Undefined + + +def _get_sqlmodel_field_metadata(field_info: Any) -> FieldInfoMetadata | None: + metadata_items = getattr(field_info, "metadata", None) + if metadata_items: + for meta in metadata_items: + if isinstance(meta, FieldInfoMetadata): + return meta + return None + + +def _get_sqlmodel_field_value( + field_info: Any, attribute: str, default: Any = Undefined +) -> Any: + metadata = _get_sqlmodel_field_metadata(field_info) + if metadata is not None and hasattr(metadata, attribute): + return getattr(metadata, attribute) + return getattr(field_info, attribute, default) + + # include sa_type, sa_column_args, sa_column_kwargs @overload def Field( default: Any = Undefined, *, - default_factory: Optional[NoArgAnyCallable] = None, - alias: Optional[str] = None, - title: Optional[str] = None, - description: Optional[str] = None, - exclude: Union[ - AbstractSet[Union[int, str]], Mapping[Union[int, str], Any], Any - ] = None, - include: Union[ - AbstractSet[Union[int, str]], Mapping[Union[int, str], Any], Any - ] = None, - const: Optional[bool] = None, - gt: Optional[float] = None, - ge: Optional[float] = None, - lt: Optional[float] = None, - le: Optional[float] = None, - multiple_of: Optional[float] = None, - max_digits: Optional[int] = None, - decimal_places: Optional[int] = None, - min_items: Optional[int] = None, - max_items: Optional[int] = None, - unique_items: Optional[bool] = None, - min_length: Optional[int] = None, - max_length: Optional[int] = None, + default_factory: NoArgAnyCallable | None = None, + alias: str | None = None, + validation_alias: str | None = None, + serialization_alias: str | None = None, + title: str | None = None, + description: str | None = None, + exclude: Set[int | str] | Mapping[int | str, Any] | Any = None, + include: Set[int | str] | Mapping[int | str, Any] | Any = None, + const: bool | None = None, + gt: float | None = None, + ge: float | None = None, + lt: float | None = None, + le: float | None = None, + multiple_of: float | None = None, + max_digits: int | None = None, + decimal_places: int | None = None, + min_items: int | None = None, + max_items: int | None = None, + unique_items: bool | None = None, + min_length: int | None = None, + max_length: int | None = None, allow_mutation: bool = True, - regex: Optional[str] = None, - discriminator: Optional[str] = None, + regex: str | None = None, + discriminator: str | None = None, repr: bool = True, - primary_key: Union[bool, UndefinedType] = Undefined, + primary_key: bool | UndefinedType = Undefined, foreign_key: Any = Undefined, - unique: Union[bool, UndefinedType] = Undefined, - nullable: Union[bool, UndefinedType] = Undefined, - index: Union[bool, UndefinedType] = Undefined, - sa_type: Union[Type[Any], UndefinedType] = Undefined, - sa_column_args: Union[Sequence[Any], UndefinedType] = Undefined, - sa_column_kwargs: Union[Mapping[str, Any], UndefinedType] = Undefined, - schema_extra: Optional[Dict[str, Any]] = None, + unique: bool | UndefinedType = Undefined, + nullable: bool | UndefinedType = Undefined, + index: bool | UndefinedType = Undefined, + sa_type: type[Any] | UndefinedType = Undefined, + sa_column_args: Sequence[Any] | UndefinedType = Undefined, + sa_column_kwargs: Mapping[str, Any] | UndefinedType = Undefined, + schema_extra: dict[str, Any] | None = None, ) -> Any: ... @@ -255,43 +280,41 @@ def Field( def Field( default: Any = Undefined, *, - default_factory: Optional[NoArgAnyCallable] = None, - alias: Optional[str] = None, - title: Optional[str] = None, - description: Optional[str] = None, - exclude: Union[ - AbstractSet[Union[int, str]], Mapping[Union[int, str], Any], Any - ] = None, - include: Union[ - AbstractSet[Union[int, str]], Mapping[Union[int, str], Any], Any - ] = None, - const: Optional[bool] = None, - gt: Optional[float] = None, - ge: Optional[float] = None, - lt: Optional[float] = None, - le: Optional[float] = None, - multiple_of: Optional[float] = None, - max_digits: Optional[int] = None, - decimal_places: Optional[int] = None, - min_items: Optional[int] = None, - max_items: Optional[int] = None, - unique_items: Optional[bool] = None, - min_length: Optional[int] = None, - max_length: Optional[int] = None, + default_factory: NoArgAnyCallable | None = None, + alias: str | None = None, + validation_alias: str | None = None, + serialization_alias: str | None = None, + title: str | None = None, + description: str | None = None, + exclude: Set[int | str] | Mapping[int | str, Any] | Any = None, + include: Set[int | str] | Mapping[int | str, Any] | Any = None, + const: bool | None = None, + gt: float | None = None, + ge: float | None = None, + lt: float | None = None, + le: float | None = None, + multiple_of: float | None = None, + max_digits: int | None = None, + decimal_places: int | None = None, + min_items: int | None = None, + max_items: int | None = None, + unique_items: bool | None = None, + min_length: int | None = None, + max_length: int | None = None, allow_mutation: bool = True, - regex: Optional[str] = None, - discriminator: Optional[str] = None, + regex: str | None = None, + discriminator: str | None = None, repr: bool = True, - primary_key: Union[bool, UndefinedType] = Undefined, + primary_key: bool | UndefinedType = Undefined, foreign_key: str, - ondelete: Union[OnDeleteType, UndefinedType] = Undefined, - unique: Union[bool, UndefinedType] = Undefined, - nullable: Union[bool, UndefinedType] = Undefined, - index: Union[bool, UndefinedType] = Undefined, - sa_type: Union[Type[Any], UndefinedType] = Undefined, - sa_column_args: Union[Sequence[Any], UndefinedType] = Undefined, - sa_column_kwargs: Union[Mapping[str, Any], UndefinedType] = Undefined, - schema_extra: Optional[Dict[str, Any]] = None, + ondelete: OnDeleteType | UndefinedType = Undefined, + unique: bool | UndefinedType = Undefined, + nullable: bool | UndefinedType = Undefined, + index: bool | UndefinedType = Undefined, + sa_type: type[Any] | UndefinedType = Undefined, + sa_column_args: Sequence[Any] | UndefinedType = Undefined, + sa_column_kwargs: Mapping[str, Any] | UndefinedType = Undefined, + schema_extra: dict[str, Any] | None = None, ) -> Any: ... @@ -309,154 +332,178 @@ def Field( def Field( default: Any = Undefined, *, - default_factory: Optional[NoArgAnyCallable] = None, - alias: Optional[str] = None, - title: Optional[str] = None, - description: Optional[str] = None, - exclude: Union[ - AbstractSet[Union[int, str]], Mapping[Union[int, str], Any], Any - ] = None, - include: Union[ - AbstractSet[Union[int, str]], Mapping[Union[int, str], Any], Any - ] = None, - const: Optional[bool] = None, - gt: Optional[float] = None, - ge: Optional[float] = None, - lt: Optional[float] = None, - le: Optional[float] = None, - multiple_of: Optional[float] = None, - max_digits: Optional[int] = None, - decimal_places: Optional[int] = None, - min_items: Optional[int] = None, - max_items: Optional[int] = None, - unique_items: Optional[bool] = None, - min_length: Optional[int] = None, - max_length: Optional[int] = None, + default_factory: NoArgAnyCallable | None = None, + alias: str | None = None, + validation_alias: str | None = None, + serialization_alias: str | None = None, + title: str | None = None, + description: str | None = None, + exclude: Set[int | str] | Mapping[int | str, Any] | Any = None, + include: Set[int | str] | Mapping[int | str, Any] | Any = None, + const: bool | None = None, + gt: float | None = None, + ge: float | None = None, + lt: float | None = None, + le: float | None = None, + multiple_of: float | None = None, + max_digits: int | None = None, + decimal_places: int | None = None, + min_items: int | None = None, + max_items: int | None = None, + unique_items: bool | None = None, + min_length: int | None = None, + max_length: int | None = None, allow_mutation: bool = True, - regex: Optional[str] = None, - discriminator: Optional[str] = None, + regex: str | None = None, + discriminator: str | None = None, repr: bool = True, - sa_column: Union[Column, UndefinedType] = Undefined, # type: ignore - schema_extra: Optional[Dict[str, Any]] = None, + sa_column: Column[Any] | UndefinedType = Undefined, + schema_extra: dict[str, Any] | None = None, ) -> Any: ... def Field( default: Any = Undefined, *, - default_factory: Optional[NoArgAnyCallable] = None, - alias: Optional[str] = None, - title: Optional[str] = None, - description: Optional[str] = None, - exclude: Union[ - AbstractSet[Union[int, str]], Mapping[Union[int, str], Any], Any - ] = None, - include: Union[ - AbstractSet[Union[int, str]], Mapping[Union[int, str], Any], Any - ] = None, - const: Optional[bool] = None, - gt: Optional[float] = None, - ge: Optional[float] = None, - lt: Optional[float] = None, - le: Optional[float] = None, - multiple_of: Optional[float] = None, - max_digits: Optional[int] = None, - decimal_places: Optional[int] = None, - min_items: Optional[int] = None, - max_items: Optional[int] = None, - unique_items: Optional[bool] = None, - min_length: Optional[int] = None, - max_length: Optional[int] = None, + default_factory: NoArgAnyCallable | None = None, + alias: str | None = None, + validation_alias: str | None = None, + serialization_alias: str | None = None, + title: str | None = None, + description: str | None = None, + exclude: Set[int | str] | Mapping[int | str, Any] | Any = None, + include: Set[int | str] | Mapping[int | str, Any] | Any = None, + const: bool | None = None, + gt: float | None = None, + ge: float | None = None, + lt: float | None = None, + le: float | None = None, + multiple_of: float | None = None, + max_digits: int | None = None, + decimal_places: int | None = None, + min_items: int | None = None, + max_items: int | None = None, + unique_items: bool | None = None, + min_length: int | None = None, + max_length: int | None = None, allow_mutation: bool = True, - regex: Optional[str] = None, - discriminator: Optional[str] = None, + regex: str | None = None, + discriminator: str | None = None, repr: bool = True, - primary_key: Union[bool, UndefinedType] = Undefined, + primary_key: bool | UndefinedType = Undefined, foreign_key: Any = Undefined, - ondelete: Union[OnDeleteType, UndefinedType] = Undefined, - unique: Union[bool, UndefinedType] = Undefined, - nullable: Union[bool, UndefinedType] = Undefined, - index: Union[bool, UndefinedType] = Undefined, - sa_type: Union[Type[Any], UndefinedType] = Undefined, - sa_column: Union[Column, UndefinedType] = Undefined, # type: ignore - sa_column_args: Union[Sequence[Any], UndefinedType] = Undefined, - sa_column_kwargs: Union[Mapping[str, Any], UndefinedType] = Undefined, - schema_extra: Optional[Dict[str, Any]] = None, + ondelete: OnDeleteType | UndefinedType = Undefined, + unique: bool | UndefinedType = Undefined, + nullable: bool | UndefinedType = Undefined, + index: bool | UndefinedType = Undefined, + sa_type: type[Any] | UndefinedType = Undefined, + sa_column: Column | UndefinedType = Undefined, + sa_column_args: Sequence[Any] | UndefinedType = Undefined, + sa_column_kwargs: Mapping[str, Any] | UndefinedType = Undefined, + schema_extra: dict[str, Any] | None = None, ) -> Any: current_schema_extra = schema_extra or {} + # Extract possible alias settings from schema_extra so we can control precedence + schema_validation_alias = current_schema_extra.pop("validation_alias", None) + schema_serialization_alias = current_schema_extra.pop("serialization_alias", None) + field_info_kwargs = { + "alias": alias, + "title": title, + "description": description, + "exclude": exclude, + "include": include, + "const": const, + "gt": gt, + "ge": ge, + "lt": lt, + "le": le, + "multiple_of": multiple_of, + "max_digits": max_digits, + "decimal_places": decimal_places, + "min_items": min_items, + "max_items": max_items, + "unique_items": unique_items, + "min_length": min_length, + "max_length": max_length, + "allow_mutation": allow_mutation, + "regex": regex, + "discriminator": discriminator, + "repr": repr, + "primary_key": primary_key, + "foreign_key": foreign_key, + "ondelete": ondelete, + "unique": unique, + "nullable": nullable, + "index": index, + "sa_type": sa_type, + "sa_column": sa_column, + "sa_column_args": sa_column_args, + "sa_column_kwargs": sa_column_kwargs, + **current_schema_extra, + } + + # explicit params > schema_extra > alias propagation + field_info_kwargs["validation_alias"] = ( + validation_alias or schema_validation_alias or alias + ) + field_info_kwargs["serialization_alias"] = ( + serialization_alias or schema_serialization_alias or alias + ) + field_info = FieldInfo( default, default_factory=default_factory, - alias=alias, - title=title, - description=description, - exclude=exclude, - include=include, - const=const, - gt=gt, - ge=ge, - lt=lt, - le=le, - multiple_of=multiple_of, - max_digits=max_digits, - decimal_places=decimal_places, - min_items=min_items, - max_items=max_items, - unique_items=unique_items, - min_length=min_length, - max_length=max_length, - allow_mutation=allow_mutation, - regex=regex, - discriminator=discriminator, - repr=repr, + **field_info_kwargs, + ) + field_metadata = FieldInfoMetadata( primary_key=primary_key, + nullable=nullable, foreign_key=foreign_key, ondelete=ondelete, unique=unique, - nullable=nullable, index=index, sa_type=sa_type, sa_column=sa_column, sa_column_args=sa_column_args, sa_column_kwargs=sa_column_kwargs, - **current_schema_extra, ) - post_init_field_info(field_info) + if hasattr(field_info, "metadata"): + field_info.metadata.append(field_metadata) return field_info @overload def Relationship( *, - back_populates: Optional[str] = None, - cascade_delete: Optional[bool] = False, - passive_deletes: Optional[Union[bool, Literal["all"]]] = False, - link_model: Optional[Any] = None, - sa_relationship_args: Optional[Sequence[Any]] = None, - sa_relationship_kwargs: Optional[Mapping[str, Any]] = None, + back_populates: str | None = None, + cascade_delete: bool | None = False, + passive_deletes: bool | Literal["all"] | None = False, + link_model: Any | None = None, + sa_relationship_args: Sequence[Any] | None = None, + sa_relationship_kwargs: Mapping[str, Any] | None = None, ) -> Any: ... @overload def Relationship( *, - back_populates: Optional[str] = None, - cascade_delete: Optional[bool] = False, - passive_deletes: Optional[Union[bool, Literal["all"]]] = False, - link_model: Optional[Any] = None, - sa_relationship: Optional[RelationshipProperty[Any]] = None, + back_populates: str | None = None, + cascade_delete: bool | None = False, + passive_deletes: bool | Literal["all"] | None = False, + link_model: Any | None = None, + sa_relationship: RelationshipProperty[Any] | None = None, ) -> Any: ... def Relationship( *, - back_populates: Optional[str] = None, - cascade_delete: Optional[bool] = False, - passive_deletes: Optional[Union[bool, Literal["all"]]] = False, - link_model: Optional[Any] = None, - sa_relationship: Optional[RelationshipProperty[Any]] = None, - sa_relationship_args: Optional[Sequence[Any]] = None, - sa_relationship_kwargs: Optional[Mapping[str, Any]] = None, + back_populates: str | None = None, + cascade_delete: bool | None = False, + passive_deletes: bool | Literal["all"] | None = False, + link_model: Any | None = None, + sa_relationship: RelationshipProperty[Any] | None = None, + sa_relationship_args: Sequence[Any] | None = None, + sa_relationship_kwargs: Mapping[str, Any] | None = None, ) -> Any: relationship_info = RelationshipInfo( back_populates=back_populates, @@ -472,20 +519,18 @@ def Relationship( @__dataclass_transform__(kw_only_default=True, field_descriptors=(Field, FieldInfo)) class SQLModelMetaclass(ModelMetaclass, DeclarativeMeta): - __sqlmodel_relationships__: Dict[str, RelationshipInfo] + __sqlmodel_relationships__: dict[str, RelationshipInfo] model_config: SQLModelConfig - model_fields: Dict[str, FieldInfo] - __config__: Type[SQLModelConfig] - __fields__: Dict[str, ModelField] # type: ignore[assignment] + model_fields: ClassVar[dict[str, FieldInfo]] # Replicate SQLAlchemy - def __setattr__(cls, name: str, value: Any) -> None: + def __setattr__(cls, name: str, value: Any) -> None: # ty: ignore[invalid-method-override] if is_table_model_class(cls): DeclarativeMeta.__setattr__(cls, name, value) else: super().__setattr__(name, value) - def __delattr__(cls, name: str) -> None: + def __delattr__(cls, name: str) -> None: # ty: ignore[invalid-method-override] if is_table_model_class(cls): DeclarativeMeta.__delattr__(cls, name) else: @@ -495,11 +540,11 @@ def __delattr__(cls, name: str) -> None: def __new__( cls, name: str, - bases: Tuple[Type[Any], ...], - class_dict: Dict[str, Any], + bases: tuple[type[Any], ...], + class_dict: dict[str, Any], **kwargs: Any, ) -> Any: - relationships: Dict[str, RelationshipInfo] = {} + relationships: dict[str, RelationshipInfo] = {} dict_for_pydantic = {} original_annotations = get_annotations(class_dict) pydantic_annotations = {} @@ -523,7 +568,7 @@ def __new__( # Duplicate logic from Pydantic to filter config kwargs because if they are # passed directly including the registry Pydantic will pass them over to the # superclass causing an error - allowed_config_kwargs: Set[str] = { + allowed_config_kwargs: set[str] = { key for key in dir(BaseConfig) if not ( @@ -533,7 +578,9 @@ def __new__( config_kwargs = { key: kwargs[key] for key in kwargs.keys() & allowed_config_kwargs } - new_cls = super().__new__(cls, name, bases, dict_used, **config_kwargs) + new_cls = cast( + "SQLModel", super().__new__(cls, name, bases, dict_used, **config_kwargs) + ) new_cls.__annotations__ = { **relationship_annotations, **pydantic_annotations, @@ -541,9 +588,7 @@ def __new__( } def get_config(name: str) -> Any: - config_class_value = get_config_value( - model=new_cls, parameter=name, default=Undefined - ) + config_class_value = new_cls.model_config.get(name, Undefined) if config_class_value is not Undefined: return config_class_value kwarg_value = kwargs.get(name, Undefined) @@ -554,7 +599,7 @@ def get_config(name: str) -> Any: config_table = get_config("table") if config_table is True: # If it was passed by kwargs, ensure it's also set in config - set_config_value(model=new_cls, parameter="table", value=config_table) + new_cls.model_config["table"] = config_table for k, v in get_model_fields(new_cls).items(): col = get_column_from_field(v) setattr(new_cls, k, col) @@ -563,18 +608,16 @@ def get_config(name: str) -> Any: # This could be done by reading new_cls.model_config['table'] in FastAPI, but # that's very specific about SQLModel, so let's have another config that # other future tools based on Pydantic can use. - set_config_value( - model=new_cls, parameter="read_from_attributes", value=True - ) + new_cls.model_config["read_from_attributes"] = True # ty: ignore[invalid-key] # For compatibility with older versions # TODO: remove this in the future - set_config_value(model=new_cls, parameter="read_with_orm_mode", value=True) + new_cls.model_config["read_with_orm_mode"] = True # ty: ignore[invalid-key] config_registry = get_config("registry") if config_registry is not Undefined: config_registry = cast(registry, config_registry) # If it was passed by kwargs, ensure it's also set in config - set_config_value(model=new_cls, parameter="registry", value=config_table) + new_cls.model_config["registry"] = config_table setattr(new_cls, "_sa_registry", config_registry) # noqa: B010 setattr(new_cls, "metadata", config_registry.metadata) # noqa: B010 setattr(new_cls, "__abstract__", True) # noqa: B010 @@ -582,7 +625,7 @@ def get_config(name: str) -> Any: # Override SQLAlchemy, allow both SQLAlchemy and plain Pydantic models def __init__( - cls, classname: str, bases: Tuple[type, ...], dict_: Dict[str, Any], **kw: Any + cls, classname: str, bases: tuple[type, ...], dict_: dict[str, Any], **kw: Any ) -> None: # Only one of the base classes (or the current one) should be a table model # this allows FastAPI cloning a SQLModel for the response_model without @@ -597,7 +640,7 @@ def __init__( setattr(cls, rel_name, rel_info.sa_relationship) # Fix #315 continue raw_ann = cls.__annotations__[rel_name] - origin = get_origin(raw_ann) + origin: Any = get_origin(raw_ann) if origin is Mapped: ann = raw_ann.__args__[0] else: @@ -605,11 +648,11 @@ def __init__( # Plain forward references, for models not yet defined, are not # handled well by SQLAlchemy without Mapped, so, wrap the # annotations in Mapped here - cls.__annotations__[rel_name] = Mapped[ann] # type: ignore[valid-type] + cls.__annotations__[rel_name] = Mapped[ann] relationship_to = get_relationship_to( name=rel_name, rel_info=rel_info, annotation=ann ) - rel_kwargs: Dict[str, Any] = {} + rel_kwargs: dict[str, Any] = {} if rel_info.back_populates: rel_kwargs["back_populates"] = rel_info.back_populates if rel_info.cascade_delete: @@ -625,7 +668,7 @@ def __init__( f"model {rel_info.link_model}" ) rel_kwargs["secondary"] = local_table - rel_args: List[Any] = [] + rel_args: list[Any] = [] if rel_info.sa_relationship_args: rel_args.extend(rel_info.sa_relationship_args) if rel_info.sa_relationship_kwargs: @@ -641,11 +684,8 @@ def __init__( def get_sqlalchemy_type(field: Any) -> Any: - if IS_PYDANTIC_V2: - field_info = field - else: - field_info = field.field_info - sa_type = getattr(field_info, "sa_type", Undefined) # noqa: B009 + field_info = field + sa_type = _get_sqlmodel_field_value(field_info, "sa_type", Undefined) # noqa: B009 if sa_type is not Undefined: return sa_type @@ -697,45 +737,42 @@ def get_sqlalchemy_type(field: Any) -> Any: raise ValueError(f"{type_} has no matching SQLAlchemy type") -def get_column_from_field(field: Any) -> Column: # type: ignore - if IS_PYDANTIC_V2: - field_info = field - else: - field_info = field.field_info - sa_column = getattr(field_info, "sa_column", Undefined) +def get_column_from_field(field: Any) -> Column: + field_info = field + sa_column = _get_sqlmodel_field_value(field_info, "sa_column", Undefined) if isinstance(sa_column, Column): return sa_column sa_type = get_sqlalchemy_type(field) - primary_key = getattr(field_info, "primary_key", Undefined) + primary_key = _get_sqlmodel_field_value(field_info, "primary_key", Undefined) if primary_key is Undefined: primary_key = False - index = getattr(field_info, "index", Undefined) + index = _get_sqlmodel_field_value(field_info, "index", Undefined) if index is Undefined: index = False nullable = not primary_key and is_field_noneable(field) # Override derived nullability if the nullable property is set explicitly # on the field - field_nullable = getattr(field_info, "nullable", Undefined) # noqa: B009 + field_nullable = _get_sqlmodel_field_value(field_info, "nullable", Undefined) if field_nullable is not Undefined: assert not isinstance(field_nullable, UndefinedType) nullable = field_nullable args = [] - foreign_key = getattr(field_info, "foreign_key", Undefined) + foreign_key = _get_sqlmodel_field_value(field_info, "foreign_key", Undefined) if foreign_key is Undefined: foreign_key = None - unique = getattr(field_info, "unique", Undefined) + unique = _get_sqlmodel_field_value(field_info, "unique", Undefined) if unique is Undefined: unique = False if foreign_key: - if field_info.ondelete == "SET NULL" and not nullable: + ondelete_value = _get_sqlmodel_field_value(field_info, "ondelete", Undefined) + if ondelete_value is Undefined: + ondelete_value = None + if ondelete_value == "SET NULL" and not nullable: raise RuntimeError('ondelete="SET NULL" requires nullable=True') assert isinstance(foreign_key, str) - ondelete = getattr(field_info, "ondelete", Undefined) - if ondelete is Undefined: - ondelete = None - assert isinstance(ondelete, (str, type(None))) # for typing - args.append(ForeignKey(foreign_key, ondelete=ondelete)) - kwargs = { + assert isinstance(ondelete_value, (str, type(None))) # for typing + args.append(ForeignKey(foreign_key, ondelete=ondelete_value)) + kwargs: dict[str, Any] = { "primary_key": primary_key, "nullable": nullable, "index": index, @@ -748,17 +785,17 @@ def get_column_from_field(field: Any) -> Column: # type: ignore sa_default = field_info.default if sa_default is not Undefined: kwargs["default"] = sa_default - sa_column_args = getattr(field_info, "sa_column_args", Undefined) + sa_column_args = _get_sqlmodel_field_value(field_info, "sa_column_args", Undefined) if sa_column_args is not Undefined: args.extend(list(cast(Sequence[Any], sa_column_args))) - sa_column_kwargs = getattr(field_info, "sa_column_kwargs", Undefined) + sa_column_kwargs = _get_sqlmodel_field_value( + field_info, "sa_column_kwargs", Undefined + ) if sa_column_kwargs is not Undefined: - kwargs.update(cast(Dict[Any, Any], sa_column_kwargs)) - return Column(sa_type, *args, **kwargs) # type: ignore + kwargs.update(cast(dict[Any, Any], sa_column_kwargs)) + return Column(sa_type, *args, **kwargs) -class_registry = weakref.WeakValueDictionary() # type: ignore - default_registry = registry() _TSQLModel = TypeVar("_TSQLModel", bound="SQLModel") @@ -767,20 +804,16 @@ def get_column_from_field(field: Any) -> Column: # type: ignore class SQLModel(BaseModel, metaclass=SQLModelMetaclass, registry=default_registry): # SQLAlchemy needs to set weakref(s), Pydantic will set the other slots values __slots__ = ("__weakref__",) - __tablename__: ClassVar[Union[str, Callable[..., str]]] - __sqlmodel_relationships__: ClassVar[Dict[str, RelationshipProperty[Any]]] + __tablename__: ClassVar[str | Callable[..., str]] + __sqlmodel_relationships__: ClassVar[builtins.dict[str, RelationshipProperty[Any]]] __name__: ClassVar[str] metadata: ClassVar[MetaData] __allow_unmapped__ = True # https://docs.sqlalchemy.org/en/20/changelog/migration_20.html#migration-20-step-six + model_config = SQLModelConfig(from_attributes=True) - if IS_PYDANTIC_V2: - model_config = SQLModelConfig(from_attributes=True) - else: - - class Config: - orm_mode = True - - def __new__(cls, *args: Any, **kwargs: Any) -> Any: + # Typing spec says `__new__` returning `Any` overrides normal constructor + # behavior, but a missing annotation does not: + def __new__(cls, *args: Any, **kwargs: Any): # type: ignore[no-untyped-def] new_object = super().__new__(cls) # SQLAlchemy doesn't call __init__ on the base class when querying from DB # Ref: https://docs.sqlalchemy.org/en/14/orm/constructors.html @@ -816,14 +849,14 @@ def __setattr__(self, name: str, value: Any) -> None: return else: # Set in SQLAlchemy, before Pydantic to trigger events and updates - if is_table_model_class(self.__class__) and is_instrumented(self, name): # type: ignore[no-untyped-call] + if is_table_model_class(self.__class__) and is_instrumented(self, name): set_attribute(self, name, value) # Set in Pydantic model to trigger possible validation changes, only for # non relationship values if name not in self.__sqlmodel_relationships__: super().__setattr__(name, value) - def __repr_args__(self) -> Sequence[Tuple[Optional[str], Any]]: + def __repr_args__(self) -> Sequence[tuple[str | None, Any]]: # Don't show SQLAlchemy private attributes return [ (k, v) @@ -836,14 +869,14 @@ def __tablename__(cls) -> str: return cls.__name__.lower() @classmethod - def model_validate( - cls: Type[_TSQLModel], + def model_validate( # ty: ignore[invalid-method-override] + cls: type[_TSQLModel], obj: Any, *, - strict: Union[bool, None] = None, - from_attributes: Union[bool, None] = None, - context: Union[Dict[str, Any], None] = None, - update: Union[Dict[str, Any], None] = None, + strict: bool | None = None, + from_attributes: bool | None = None, + context: builtins.dict[str, Any] | None = None, + update: builtins.dict[str, Any] | None = None, ) -> _TSQLModel: return sqlmodel_validate( cls=cls, @@ -857,47 +890,41 @@ def model_validate( def model_dump( self, *, - mode: Union[Literal["json", "python"], str] = "python", - include: IncEx = None, - exclude: IncEx = None, - context: Union[Dict[str, Any], None] = None, - by_alias: bool = False, + mode: Literal["json", "python"] | str = "python", + include: IncEx | None = None, + exclude: IncEx | None = None, + context: Any | None = None, # v2.7 + by_alias: bool | None = None, exclude_unset: bool = False, exclude_defaults: bool = False, exclude_none: bool = False, + exclude_computed_fields: bool = False, # v2.12 round_trip: bool = False, - warnings: Union[bool, Literal["none", "warn", "error"]] = True, - serialize_as_any: bool = False, - ) -> Dict[str, Any]: - if PYDANTIC_VERSION >= "2.7.0": - extra_kwargs: Dict[str, Any] = { - "context": context, - "serialize_as_any": serialize_as_any, - } - else: - extra_kwargs = {} - if IS_PYDANTIC_V2: - return super().model_dump( - mode=mode, - include=include, - exclude=exclude, - by_alias=by_alias, - exclude_unset=exclude_unset, - exclude_defaults=exclude_defaults, - exclude_none=exclude_none, - round_trip=round_trip, - warnings=warnings, - **extra_kwargs, - ) - else: - return super().dict( - include=include, - exclude=exclude, - by_alias=by_alias, - exclude_unset=exclude_unset, - exclude_defaults=exclude_defaults, - exclude_none=exclude_none, - ) + warnings: bool | Literal["none", "warn", "error"] = True, + fallback: Callable[[Any], Any] | None = None, # v2.11 + serialize_as_any: bool = False, # v2.7 + ) -> builtins.dict[str, Any]: + if PYDANTIC_MINOR_VERSION < (2, 11): + by_alias = by_alias or False + extra_kwargs: dict[str, Any] = {} + extra_kwargs["context"] = context + extra_kwargs["serialize_as_any"] = serialize_as_any + if PYDANTIC_MINOR_VERSION >= (2, 11): + extra_kwargs["fallback"] = fallback + if PYDANTIC_MINOR_VERSION >= (2, 12): + extra_kwargs["exclude_computed_fields"] = exclude_computed_fields + return super().model_dump( + mode=mode, + include=include, + exclude=exclude, + by_alias=by_alias, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + round_trip=round_trip, + warnings=warnings, + **extra_kwargs, + ) @deprecated( """ @@ -908,13 +935,13 @@ def model_dump( def dict( self, *, - include: IncEx = None, - exclude: IncEx = None, + include: IncEx | None = None, + exclude: IncEx | None = None, by_alias: bool = False, exclude_unset: bool = False, exclude_defaults: bool = False, exclude_none: bool = False, - ) -> Dict[str, Any]: + ) -> builtins.dict[str, Any]: return self.model_dump( include=include, exclude=exclude, @@ -932,7 +959,9 @@ def dict( """ ) def from_orm( - cls: Type[_TSQLModel], obj: Any, update: Optional[Dict[str, Any]] = None + cls: type[_TSQLModel], + obj: Any, + update: builtins.dict[str, Any] | None = None, ) -> _TSQLModel: return cls.model_validate(obj, update=update) @@ -944,42 +973,17 @@ def from_orm( """ ) def parse_obj( - cls: Type[_TSQLModel], obj: Any, update: Optional[Dict[str, Any]] = None + cls: type[_TSQLModel], + obj: Any, + update: builtins.dict[str, Any] | None = None, ) -> _TSQLModel: - if not IS_PYDANTIC_V2: - obj = cls._enforce_dict_if_root(obj) # type: ignore[attr-defined] # noqa return cls.model_validate(obj, update=update) - # From Pydantic, override to only show keys from fields, omit SQLAlchemy attributes - @deprecated( - """ - 🚨 You should not access `obj._calculate_keys()` directly. - - It is only useful for Pydantic v1.X, you should probably upgrade to - Pydantic v2.X. - """, - category=None, - ) - def _calculate_keys( - self, - include: Optional[Mapping[Union[int, str], Any]], - exclude: Optional[Mapping[Union[int, str], Any]], - exclude_unset: bool, - update: Optional[Dict[str, Any]] = None, - ) -> Optional[AbstractSet[str]]: - return _calculate_keys( - self, - include=include, - exclude=exclude, - exclude_unset=exclude_unset, - update=update, - ) - def sqlmodel_update( self: _TSQLModel, - obj: Union[Dict[str, Any], BaseModel], + obj: builtins.dict[str, Any] | BaseModel, *, - update: Union[Dict[str, Any], None] = None, + update: builtins.dict[str, Any] | None = None, ) -> _TSQLModel: use_update = (update or {}).copy() if isinstance(obj, dict): @@ -993,9 +997,8 @@ def sqlmodel_update( else: value = getattr(obj, key) setattr(self, key, value) - for remaining_key in use_update: + for remaining_key, value in use_update.items(): if remaining_key in get_model_fields(self): - value = use_update.pop(remaining_key) setattr(self, remaining_key, value) else: raise ValueError( diff --git a/sqlmodel/orm/session.py b/sqlmodel/orm/session.py index b60875095b..5c721ae0da 100644 --- a/sqlmodel/orm/session.py +++ b/sqlmodel/orm/session.py @@ -1,15 +1,12 @@ +from collections.abc import Mapping, Sequence from typing import ( Any, - Dict, - Mapping, - Optional, - Sequence, TypeVar, - Union, overload, ) from sqlalchemy import util +from sqlalchemy.engine.cursor import CursorResult from sqlalchemy.engine.interfaces import _CoreAnyExecuteParams from sqlalchemy.engine.result import Result, ScalarResult, TupleResult from sqlalchemy.orm import Query as _Query @@ -17,6 +14,7 @@ from sqlalchemy.orm._typing import OrmExecuteOptionsParameter from sqlalchemy.sql._typing import _ColumnsClauseArgument from sqlalchemy.sql.base import Executable as _Executable +from sqlalchemy.sql.dml import UpdateBase from sqlmodel.sql.base import Executable from sqlmodel.sql.expression import Select, SelectOfScalar from typing_extensions import deprecated @@ -30,11 +28,11 @@ def exec( self, statement: Select[_TSelectParam], *, - params: Optional[Union[Mapping[str, Any], Sequence[Mapping[str, Any]]]] = None, + params: Mapping[str, Any] | Sequence[Mapping[str, Any]] | None = None, execution_options: Mapping[str, Any] = util.EMPTY_DICT, - bind_arguments: Optional[Dict[str, Any]] = None, - _parent_execute_state: Optional[Any] = None, - _add_event: Optional[Any] = None, + bind_arguments: dict[str, Any] | None = None, + _parent_execute_state: Any | None = None, + _add_event: Any | None = None, ) -> TupleResult[_TSelectParam]: ... @overload @@ -42,27 +40,38 @@ def exec( self, statement: SelectOfScalar[_TSelectParam], *, - params: Optional[Union[Mapping[str, Any], Sequence[Mapping[str, Any]]]] = None, + params: Mapping[str, Any] | Sequence[Mapping[str, Any]] | None = None, execution_options: Mapping[str, Any] = util.EMPTY_DICT, - bind_arguments: Optional[Dict[str, Any]] = None, - _parent_execute_state: Optional[Any] = None, - _add_event: Optional[Any] = None, + bind_arguments: dict[str, Any] | None = None, + _parent_execute_state: Any | None = None, + _add_event: Any | None = None, ) -> ScalarResult[_TSelectParam]: ... + @overload + def exec( + self, + statement: UpdateBase, + *, + params: Mapping[str, Any] | Sequence[Mapping[str, Any]] | None = None, + execution_options: Mapping[str, Any] = util.EMPTY_DICT, + bind_arguments: dict[str, Any] | None = None, + _parent_execute_state: Any | None = None, + _add_event: Any | None = None, + ) -> CursorResult[Any]: ... + def exec( self, - statement: Union[ - Select[_TSelectParam], - SelectOfScalar[_TSelectParam], - Executable[_TSelectParam], - ], + statement: Select[_TSelectParam] + | SelectOfScalar[_TSelectParam] + | Executable[_TSelectParam] + | UpdateBase, *, - params: Optional[Union[Mapping[str, Any], Sequence[Mapping[str, Any]]]] = None, + params: Mapping[str, Any] | Sequence[Mapping[str, Any]] | None = None, execution_options: Mapping[str, Any] = util.EMPTY_DICT, - bind_arguments: Optional[Dict[str, Any]] = None, - _parent_execute_state: Optional[Any] = None, - _add_event: Optional[Any] = None, - ) -> Union[TupleResult[_TSelectParam], ScalarResult[_TSelectParam]]: + bind_arguments: dict[str, Any] | None = None, + _parent_execute_state: Any | None = None, + _add_event: Any | None = None, + ) -> TupleResult[_TSelectParam] | ScalarResult[_TSelectParam] | CursorResult[Any]: results = super().execute( statement, params=params, @@ -96,15 +105,15 @@ def exec( """, category=None, ) - def execute( # type: ignore + def execute( self, statement: _Executable, - params: Optional[_CoreAnyExecuteParams] = None, + params: _CoreAnyExecuteParams | None = None, *, execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT, - bind_arguments: Optional[Dict[str, Any]] = None, - _parent_execute_state: Optional[Any] = None, - _add_event: Optional[Any] = None, + bind_arguments: dict[str, Any] | None = None, + _parent_execute_state: Any | None = None, + _add_event: Any | None = None, ) -> Result[Any]: """ 🚨 You probably want to use `session.exec()` instead of `session.execute()`. diff --git a/sqlmodel/sql/_expression_select_cls.py b/sqlmodel/sql/_expression_select_cls.py index 9fd8609956..124929d2c3 100644 --- a/sqlmodel/sql/_expression_select_cls.py +++ b/sqlmodel/sql/_expression_select_cls.py @@ -1,7 +1,5 @@ from typing import ( - Tuple, TypeVar, - Union, ) from sqlalchemy.sql._typing import ( @@ -15,20 +13,20 @@ # Separate this class in SelectBase, Select, and SelectOfScalar so that they can share # where and having without having type overlap incompatibility in session.exec(). -class SelectBase(_Select[Tuple[_T]]): +class SelectBase(_Select[tuple[_T]]): inherit_cache = True - def where(self, *whereclause: Union[_ColumnExpressionArgument[bool], bool]) -> Self: + def where(self, *whereclause: _ColumnExpressionArgument[bool] | bool) -> Self: """Return a new `Select` construct with the given expression added to its `WHERE` clause, joined to the existing clause via `AND`, if any. """ - return super().where(*whereclause) # type: ignore[arg-type] + return super().where(*whereclause) - def having(self, *having: Union[_ColumnExpressionArgument[bool], bool]) -> Self: + def having(self, *having: _ColumnExpressionArgument[bool] | bool) -> Self: """Return a new `Select` construct with the given expression added to its `HAVING` clause, joined to the existing clause via `AND`, if any. """ - return super().having(*having) # type: ignore[arg-type] + return super().having(*having) class Select(SelectBase[_T]): diff --git a/sqlmodel/sql/_expression_select_gen.py b/sqlmodel/sql/_expression_select_gen.py index 08aa59ad61..a1c85c71c9 100644 --- a/sqlmodel/sql/_expression_select_gen.py +++ b/sqlmodel/sql/_expression_select_gen.py @@ -1,14 +1,10 @@ # WARNING: do not modify this code, it is generated by _expression_select_gen.py.jinja2 +from collections.abc import Mapping, Sequence from datetime import datetime from typing import ( Any, - Mapping, - Sequence, - Tuple, - Type, TypeVar, - Union, overload, ) from uuid import UUID @@ -26,20 +22,16 @@ _T = TypeVar("_T") -_TCCA = Union[ - TypedColumnsClauseRole[_T], - SQLCoreOperations[_T], - Type[_T], -] +_TCCA = TypedColumnsClauseRole[_T] | SQLCoreOperations[_T] | type[_T] # Generated TypeVars start _TScalar_0 = TypeVar( "_TScalar_0", - Column, # type: ignore - Sequence, # type: ignore - Mapping, # type: ignore + Column, + Sequence, + Mapping, UUID, datetime, float, @@ -55,9 +47,9 @@ _TScalar_1 = TypeVar( "_TScalar_1", - Column, # type: ignore - Sequence, # type: ignore - Mapping, # type: ignore + Column, + Sequence, + Mapping, UUID, datetime, float, @@ -73,9 +65,9 @@ _TScalar_2 = TypeVar( "_TScalar_2", - Column, # type: ignore - Sequence, # type: ignore - Mapping, # type: ignore + Column, + Sequence, + Mapping, UUID, datetime, float, @@ -91,9 +83,9 @@ _TScalar_3 = TypeVar( "_TScalar_3", - Column, # type: ignore - Sequence, # type: ignore - Mapping, # type: ignore + Column, + Sequence, + Mapping, UUID, datetime, float, @@ -111,257 +103,284 @@ @overload -def select(__ent0: _TCCA[_T0]) -> SelectOfScalar[_T0]: ... +def select(ent0: _TCCA[_T0], /) -> SelectOfScalar[_T0]: ... @overload -def select(__ent0: _TScalar_0) -> SelectOfScalar[_TScalar_0]: # type: ignore - ... +def select(ent0: _TScalar_0, /) -> SelectOfScalar[_TScalar_0]: ... # Generated overloads start @overload -def select( # type: ignore - __ent0: _TCCA[_T0], - __ent1: _TCCA[_T1], -) -> Select[Tuple[_T0, _T1]]: ... +def select( + ent0: _TCCA[_T0], + ent1: _TCCA[_T1], + /, +) -> Select[tuple[_T0, _T1]]: ... @overload -def select( # type: ignore - __ent0: _TCCA[_T0], +def select( + ent0: _TCCA[_T0], entity_1: _TScalar_1, -) -> Select[Tuple[_T0, _TScalar_1]]: ... + /, +) -> Select[tuple[_T0, _TScalar_1]]: ... @overload -def select( # type: ignore +def select( entity_0: _TScalar_0, - __ent1: _TCCA[_T1], -) -> Select[Tuple[_TScalar_0, _T1]]: ... + ent1: _TCCA[_T1], + /, +) -> Select[tuple[_TScalar_0, _T1]]: ... @overload -def select( # type: ignore +def select( entity_0: _TScalar_0, entity_1: _TScalar_1, -) -> Select[Tuple[_TScalar_0, _TScalar_1]]: ... + /, +) -> Select[tuple[_TScalar_0, _TScalar_1]]: ... @overload -def select( # type: ignore - __ent0: _TCCA[_T0], - __ent1: _TCCA[_T1], - __ent2: _TCCA[_T2], -) -> Select[Tuple[_T0, _T1, _T2]]: ... +def select( + ent0: _TCCA[_T0], + ent1: _TCCA[_T1], + ent2: _TCCA[_T2], + /, +) -> Select[tuple[_T0, _T1, _T2]]: ... @overload -def select( # type: ignore - __ent0: _TCCA[_T0], - __ent1: _TCCA[_T1], +def select( + ent0: _TCCA[_T0], + ent1: _TCCA[_T1], entity_2: _TScalar_2, -) -> Select[Tuple[_T0, _T1, _TScalar_2]]: ... + /, +) -> Select[tuple[_T0, _T1, _TScalar_2]]: ... @overload -def select( # type: ignore - __ent0: _TCCA[_T0], +def select( + ent0: _TCCA[_T0], entity_1: _TScalar_1, - __ent2: _TCCA[_T2], -) -> Select[Tuple[_T0, _TScalar_1, _T2]]: ... + ent2: _TCCA[_T2], + /, +) -> Select[tuple[_T0, _TScalar_1, _T2]]: ... @overload -def select( # type: ignore - __ent0: _TCCA[_T0], +def select( + ent0: _TCCA[_T0], entity_1: _TScalar_1, entity_2: _TScalar_2, -) -> Select[Tuple[_T0, _TScalar_1, _TScalar_2]]: ... + /, +) -> Select[tuple[_T0, _TScalar_1, _TScalar_2]]: ... @overload -def select( # type: ignore +def select( entity_0: _TScalar_0, - __ent1: _TCCA[_T1], - __ent2: _TCCA[_T2], -) -> Select[Tuple[_TScalar_0, _T1, _T2]]: ... + ent1: _TCCA[_T1], + ent2: _TCCA[_T2], + /, +) -> Select[tuple[_TScalar_0, _T1, _T2]]: ... @overload -def select( # type: ignore +def select( entity_0: _TScalar_0, - __ent1: _TCCA[_T1], + ent1: _TCCA[_T1], entity_2: _TScalar_2, -) -> Select[Tuple[_TScalar_0, _T1, _TScalar_2]]: ... + /, +) -> Select[tuple[_TScalar_0, _T1, _TScalar_2]]: ... @overload -def select( # type: ignore +def select( entity_0: _TScalar_0, entity_1: _TScalar_1, - __ent2: _TCCA[_T2], -) -> Select[Tuple[_TScalar_0, _TScalar_1, _T2]]: ... + ent2: _TCCA[_T2], + /, +) -> Select[tuple[_TScalar_0, _TScalar_1, _T2]]: ... @overload -def select( # type: ignore +def select( entity_0: _TScalar_0, entity_1: _TScalar_1, entity_2: _TScalar_2, -) -> Select[Tuple[_TScalar_0, _TScalar_1, _TScalar_2]]: ... + /, +) -> Select[tuple[_TScalar_0, _TScalar_1, _TScalar_2]]: ... @overload -def select( # type: ignore - __ent0: _TCCA[_T0], - __ent1: _TCCA[_T1], - __ent2: _TCCA[_T2], - __ent3: _TCCA[_T3], -) -> Select[Tuple[_T0, _T1, _T2, _T3]]: ... +def select( + ent0: _TCCA[_T0], + ent1: _TCCA[_T1], + ent2: _TCCA[_T2], + ent3: _TCCA[_T3], + /, +) -> Select[tuple[_T0, _T1, _T2, _T3]]: ... @overload -def select( # type: ignore - __ent0: _TCCA[_T0], - __ent1: _TCCA[_T1], - __ent2: _TCCA[_T2], +def select( + ent0: _TCCA[_T0], + ent1: _TCCA[_T1], + ent2: _TCCA[_T2], entity_3: _TScalar_3, -) -> Select[Tuple[_T0, _T1, _T2, _TScalar_3]]: ... + /, +) -> Select[tuple[_T0, _T1, _T2, _TScalar_3]]: ... @overload -def select( # type: ignore - __ent0: _TCCA[_T0], - __ent1: _TCCA[_T1], +def select( + ent0: _TCCA[_T0], + ent1: _TCCA[_T1], entity_2: _TScalar_2, - __ent3: _TCCA[_T3], -) -> Select[Tuple[_T0, _T1, _TScalar_2, _T3]]: ... + ent3: _TCCA[_T3], + /, +) -> Select[tuple[_T0, _T1, _TScalar_2, _T3]]: ... @overload -def select( # type: ignore - __ent0: _TCCA[_T0], - __ent1: _TCCA[_T1], +def select( + ent0: _TCCA[_T0], + ent1: _TCCA[_T1], entity_2: _TScalar_2, entity_3: _TScalar_3, -) -> Select[Tuple[_T0, _T1, _TScalar_2, _TScalar_3]]: ... + /, +) -> Select[tuple[_T0, _T1, _TScalar_2, _TScalar_3]]: ... @overload -def select( # type: ignore - __ent0: _TCCA[_T0], +def select( + ent0: _TCCA[_T0], entity_1: _TScalar_1, - __ent2: _TCCA[_T2], - __ent3: _TCCA[_T3], -) -> Select[Tuple[_T0, _TScalar_1, _T2, _T3]]: ... + ent2: _TCCA[_T2], + ent3: _TCCA[_T3], + /, +) -> Select[tuple[_T0, _TScalar_1, _T2, _T3]]: ... @overload -def select( # type: ignore - __ent0: _TCCA[_T0], +def select( + ent0: _TCCA[_T0], entity_1: _TScalar_1, - __ent2: _TCCA[_T2], + ent2: _TCCA[_T2], entity_3: _TScalar_3, -) -> Select[Tuple[_T0, _TScalar_1, _T2, _TScalar_3]]: ... + /, +) -> Select[tuple[_T0, _TScalar_1, _T2, _TScalar_3]]: ... @overload -def select( # type: ignore - __ent0: _TCCA[_T0], +def select( + ent0: _TCCA[_T0], entity_1: _TScalar_1, entity_2: _TScalar_2, - __ent3: _TCCA[_T3], -) -> Select[Tuple[_T0, _TScalar_1, _TScalar_2, _T3]]: ... + ent3: _TCCA[_T3], + /, +) -> Select[tuple[_T0, _TScalar_1, _TScalar_2, _T3]]: ... @overload -def select( # type: ignore - __ent0: _TCCA[_T0], +def select( + ent0: _TCCA[_T0], entity_1: _TScalar_1, entity_2: _TScalar_2, entity_3: _TScalar_3, -) -> Select[Tuple[_T0, _TScalar_1, _TScalar_2, _TScalar_3]]: ... + /, +) -> Select[tuple[_T0, _TScalar_1, _TScalar_2, _TScalar_3]]: ... @overload -def select( # type: ignore +def select( entity_0: _TScalar_0, - __ent1: _TCCA[_T1], - __ent2: _TCCA[_T2], - __ent3: _TCCA[_T3], -) -> Select[Tuple[_TScalar_0, _T1, _T2, _T3]]: ... + ent1: _TCCA[_T1], + ent2: _TCCA[_T2], + ent3: _TCCA[_T3], + /, +) -> Select[tuple[_TScalar_0, _T1, _T2, _T3]]: ... @overload -def select( # type: ignore +def select( entity_0: _TScalar_0, - __ent1: _TCCA[_T1], - __ent2: _TCCA[_T2], + ent1: _TCCA[_T1], + ent2: _TCCA[_T2], entity_3: _TScalar_3, -) -> Select[Tuple[_TScalar_0, _T1, _T2, _TScalar_3]]: ... + /, +) -> Select[tuple[_TScalar_0, _T1, _T2, _TScalar_3]]: ... @overload -def select( # type: ignore +def select( entity_0: _TScalar_0, - __ent1: _TCCA[_T1], + ent1: _TCCA[_T1], entity_2: _TScalar_2, - __ent3: _TCCA[_T3], -) -> Select[Tuple[_TScalar_0, _T1, _TScalar_2, _T3]]: ... + ent3: _TCCA[_T3], + /, +) -> Select[tuple[_TScalar_0, _T1, _TScalar_2, _T3]]: ... @overload -def select( # type: ignore +def select( entity_0: _TScalar_0, - __ent1: _TCCA[_T1], + ent1: _TCCA[_T1], entity_2: _TScalar_2, entity_3: _TScalar_3, -) -> Select[Tuple[_TScalar_0, _T1, _TScalar_2, _TScalar_3]]: ... + /, +) -> Select[tuple[_TScalar_0, _T1, _TScalar_2, _TScalar_3]]: ... @overload -def select( # type: ignore +def select( entity_0: _TScalar_0, entity_1: _TScalar_1, - __ent2: _TCCA[_T2], - __ent3: _TCCA[_T3], -) -> Select[Tuple[_TScalar_0, _TScalar_1, _T2, _T3]]: ... + ent2: _TCCA[_T2], + ent3: _TCCA[_T3], + /, +) -> Select[tuple[_TScalar_0, _TScalar_1, _T2, _T3]]: ... @overload -def select( # type: ignore +def select( entity_0: _TScalar_0, entity_1: _TScalar_1, - __ent2: _TCCA[_T2], + ent2: _TCCA[_T2], entity_3: _TScalar_3, -) -> Select[Tuple[_TScalar_0, _TScalar_1, _T2, _TScalar_3]]: ... + /, +) -> Select[tuple[_TScalar_0, _TScalar_1, _T2, _TScalar_3]]: ... @overload -def select( # type: ignore +def select( entity_0: _TScalar_0, entity_1: _TScalar_1, entity_2: _TScalar_2, - __ent3: _TCCA[_T3], -) -> Select[Tuple[_TScalar_0, _TScalar_1, _TScalar_2, _T3]]: ... + ent3: _TCCA[_T3], + /, +) -> Select[tuple[_TScalar_0, _TScalar_1, _TScalar_2, _T3]]: ... @overload -def select( # type: ignore +def select( entity_0: _TScalar_0, entity_1: _TScalar_1, entity_2: _TScalar_2, entity_3: _TScalar_3, -) -> Select[Tuple[_TScalar_0, _TScalar_1, _TScalar_2, _TScalar_3]]: ... + /, +) -> Select[tuple[_TScalar_0, _TScalar_1, _TScalar_2, _TScalar_3]]: ... # Generated overloads end -def select(*entities: Any) -> Union[Select, SelectOfScalar]: # type: ignore +def select(*entities: Any) -> Select | SelectOfScalar: if len(entities) == 1: return SelectOfScalar(*entities) return Select(*entities) diff --git a/sqlmodel/sql/_expression_select_gen.py.jinja2 b/sqlmodel/sql/_expression_select_gen.py.jinja2 index ef838e4168..648785dec1 100644 --- a/sqlmodel/sql/_expression_select_gen.py.jinja2 +++ b/sqlmodel/sql/_expression_select_gen.py.jinja2 @@ -1,12 +1,8 @@ +from collections.abc import Mapping, Sequence from datetime import datetime from typing import ( Any, - Mapping, - Sequence, - Tuple, - Type, TypeVar, - Union, overload, ) from uuid import UUID @@ -24,11 +20,7 @@ from ._expression_select_cls import Select, SelectOfScalar _T = TypeVar("_T") -_TCCA = Union[ - TypedColumnsClauseRole[_T], - SQLCoreOperations[_T], - Type[_T], -] +_TCCA = TypedColumnsClauseRole[_T] | SQLCoreOperations[_T] | type[_T] # Generated TypeVars start @@ -36,9 +28,9 @@ _TCCA = Union[ {% for i in range(number_of_types) %} _TScalar_{{ i }} = TypeVar( "_TScalar_{{ i }}", - Column, # type: ignore - Sequence, # type: ignore - Mapping, # type: ignore + Column, + Sequence, + Mapping, UUID, datetime, float, @@ -56,12 +48,11 @@ _T{{ i }} = TypeVar("_T{{ i }}") # Generated TypeVars end @overload -def select(__ent0: _TCCA[_T0]) -> SelectOfScalar[_T0]: ... +def select(ent0: _TCCA[_T0], /) -> SelectOfScalar[_T0]: ... @overload -def select(__ent0: _TScalar_0) -> SelectOfScalar[_TScalar_0]: # type: ignore - ... +def select(ent0: _TScalar_0, /) -> SelectOfScalar[_TScalar_0]: ... # Generated overloads start @@ -69,16 +60,16 @@ def select(__ent0: _TScalar_0) -> SelectOfScalar[_TScalar_0]: # type: ignore {% for signature in signatures %} @overload -def select( # type: ignore - {% for arg in signature[0] %}{{ arg.name }}: {{ arg.annotation }}, {% endfor %} - ) -> Select[Tuple[{%for ret in signature[1] %}{{ ret }} {% if not loop.last %}, {% endif %}{% endfor %}]]: ... +def select( + {% for arg in signature[0] %}{{ arg.name }}: {{ arg.annotation }}, {% endfor %}/, + ) -> Select[tuple[{%for ret in signature[1] %}{{ ret }} {% if not loop.last %}, {% endif %}{% endfor %}]]: ... {% endfor %} # Generated overloads end -def select(*entities: Any) -> Union[Select, SelectOfScalar]: # type: ignore +def select(*entities: Any) -> Select | SelectOfScalar: if len(entities) == 1: return SelectOfScalar(*entities) return Select(*entities) diff --git a/sqlmodel/sql/expression.py b/sqlmodel/sql/expression.py index f431747670..92d772a883 100644 --- a/sqlmodel/sql/expression.py +++ b/sqlmodel/sql/expression.py @@ -1,13 +1,9 @@ +from collections.abc import Iterable, Mapping, Sequence from typing import ( Any, - Iterable, - Mapping, + Literal, Optional, - Sequence, - Tuple, - Type, TypeVar, - Union, ) import sqlalchemy @@ -38,7 +34,6 @@ UnaryExpression, ) from sqlalchemy.sql.type_api import TypeEngine -from typing_extensions import Literal from ._expression_select_cls import Select as Select from ._expression_select_cls import SelectOfScalar as SelectOfScalar @@ -46,41 +41,41 @@ _T = TypeVar("_T") -_TypeEngineArgument = Union[Type[TypeEngine[_T]], TypeEngine[_T]] +_TypeEngineArgument = type[TypeEngine[_T]] | TypeEngine[_T] -# Redefine operatos that would only take a column expresion to also take the (virtual) +# Redefine operators that would only take a column expression to also take the (virtual) # types of Pydantic models, e.g. str instead of only Mapped[str]. -def all_(expr: Union[_ColumnExpressionArgument[_T], _T]) -> CollectionAggregate[bool]: - return sqlalchemy.all_(expr) # type: ignore[arg-type] +def all_(expr: _ColumnExpressionArgument[_T] | _T) -> CollectionAggregate[bool]: + return sqlalchemy.all_(expr) # ty: ignore[invalid-argument-type] def and_( - initial_clause: Union[Literal[True], _ColumnExpressionArgument[bool], bool], - *clauses: Union[_ColumnExpressionArgument[bool], bool], + initial_clause: Literal[True] | _ColumnExpressionArgument[bool] | bool, + *clauses: _ColumnExpressionArgument[bool] | bool, ) -> ColumnElement[bool]: - return sqlalchemy.and_(initial_clause, *clauses) # type: ignore[arg-type] + return sqlalchemy.and_(initial_clause, *clauses) # ty: ignore[invalid-argument-type] -def any_(expr: Union[_ColumnExpressionArgument[_T], _T]) -> CollectionAggregate[bool]: - return sqlalchemy.any_(expr) # type: ignore[arg-type] +def any_(expr: _ColumnExpressionArgument[_T] | _T) -> CollectionAggregate[bool]: + return sqlalchemy.any_(expr) # ty: ignore[invalid-argument-type] def asc( - column: Union[_ColumnExpressionOrStrLabelArgument[_T], _T], + column: _ColumnExpressionOrStrLabelArgument[_T] | _T, ) -> UnaryExpression[_T]: - return sqlalchemy.asc(column) # type: ignore[arg-type] + return sqlalchemy.asc(column) # ty: ignore[invalid-argument-type] def collate( - expression: Union[_ColumnExpressionArgument[str], str], collation: str + expression: _ColumnExpressionArgument[str] | str, collation: str ) -> BinaryExpression[str]: - return sqlalchemy.collate(expression, collation) # type: ignore[arg-type] + return sqlalchemy.collate(expression, collation) # ty: ignore[invalid-argument-type] def between( - expr: Union[_ColumnExpressionOrLiteralArgument[_T], _T], + expr: _ColumnExpressionOrLiteralArgument[_T] | _T, lower_bound: Any, upper_bound: Any, symmetric: bool = False, @@ -88,101 +83,93 @@ def between( return sqlalchemy.between(expr, lower_bound, upper_bound, symmetric=symmetric) -def not_(clause: Union[_ColumnExpressionArgument[_T], _T]) -> ColumnElement[_T]: - return sqlalchemy.not_(clause) # type: ignore[arg-type] +def not_(clause: _ColumnExpressionArgument[_T] | _T) -> ColumnElement[_T]: + return sqlalchemy.not_(clause) # ty: ignore[no-matching-overload] def case( - *whens: Union[ - Tuple[Union[_ColumnExpressionArgument[bool], bool], Any], Mapping[Any, Any] - ], - value: Optional[Any] = None, - else_: Optional[Any] = None, + *whens: tuple[_ColumnExpressionArgument[bool] | bool, Any] | Mapping[Any, Any], + value: Any | None = None, + else_: Any | None = None, ) -> Case[Any]: - return sqlalchemy.case(*whens, value=value, else_=else_) # type: ignore[arg-type] + return sqlalchemy.case(*whens, value=value, else_=else_) # ty: ignore[invalid-argument-type] def cast( - expression: Union[_ColumnExpressionOrLiteralArgument[Any], Any], + expression: _ColumnExpressionOrLiteralArgument[Any] | Any, type_: "_TypeEngineArgument[_T]", ) -> Cast[_T]: return sqlalchemy.cast(expression, type_) def try_cast( - expression: Union[_ColumnExpressionOrLiteralArgument[Any], Any], + expression: _ColumnExpressionOrLiteralArgument[Any] | Any, type_: "_TypeEngineArgument[_T]", ) -> TryCast[_T]: return sqlalchemy.try_cast(expression, type_) def desc( - column: Union[_ColumnExpressionOrStrLabelArgument[_T], _T], + column: _ColumnExpressionOrStrLabelArgument[_T] | _T, ) -> UnaryExpression[_T]: - return sqlalchemy.desc(column) # type: ignore[arg-type] + return sqlalchemy.desc(column) # ty: ignore[invalid-argument-type] -def distinct(expr: Union[_ColumnExpressionArgument[_T], _T]) -> UnaryExpression[_T]: - return sqlalchemy.distinct(expr) # type: ignore[arg-type] +def distinct(expr: _ColumnExpressionArgument[_T] | _T) -> UnaryExpression[_T]: + return sqlalchemy.distinct(expr) # ty: ignore[invalid-argument-type] -def bitwise_not(expr: Union[_ColumnExpressionArgument[_T], _T]) -> UnaryExpression[_T]: - return sqlalchemy.bitwise_not(expr) # type: ignore[arg-type] +def bitwise_not(expr: _ColumnExpressionArgument[_T] | _T) -> UnaryExpression[_T]: + return sqlalchemy.bitwise_not(expr) # ty: ignore[invalid-argument-type] -def extract(field: str, expr: Union[_ColumnExpressionArgument[Any], Any]) -> Extract: +def extract(field: str, expr: _ColumnExpressionArgument[Any] | Any) -> Extract: return sqlalchemy.extract(field, expr) def funcfilter( - func: FunctionElement[_T], *criterion: Union[_ColumnExpressionArgument[bool], bool] + func: FunctionElement[_T], *criterion: _ColumnExpressionArgument[bool] | bool ) -> FunctionFilter[_T]: - return sqlalchemy.funcfilter(func, *criterion) # type: ignore[arg-type] + return sqlalchemy.funcfilter(func, *criterion) # ty: ignore[invalid-argument-type] def label( name: str, - element: Union[_ColumnExpressionArgument[_T], _T], + element: _ColumnExpressionArgument[_T] | _T, type_: Optional["_TypeEngineArgument[_T]"] = None, ) -> Label[_T]: - return sqlalchemy.label(name, element, type_=type_) # type: ignore[arg-type] + return sqlalchemy.label(name, element, type_=type_) # ty: ignore[invalid-argument-type] def nulls_first( - column: Union[_ColumnExpressionArgument[_T], _T], + column: _ColumnExpressionArgument[_T] | _T, ) -> UnaryExpression[_T]: - return sqlalchemy.nulls_first(column) # type: ignore[arg-type] + return sqlalchemy.nulls_first(column) # ty: ignore[invalid-argument-type] -def nulls_last(column: Union[_ColumnExpressionArgument[_T], _T]) -> UnaryExpression[_T]: - return sqlalchemy.nulls_last(column) # type: ignore[arg-type] +def nulls_last(column: _ColumnExpressionArgument[_T] | _T) -> UnaryExpression[_T]: + return sqlalchemy.nulls_last(column) # ty: ignore[invalid-argument-type] def or_( - initial_clause: Union[Literal[False], _ColumnExpressionArgument[bool], bool], - *clauses: Union[_ColumnExpressionArgument[bool], bool], + initial_clause: Literal[False] | _ColumnExpressionArgument[bool] | bool, + *clauses: _ColumnExpressionArgument[bool] | bool, ) -> ColumnElement[bool]: - return sqlalchemy.or_(initial_clause, *clauses) # type: ignore[arg-type] + return sqlalchemy.or_(initial_clause, *clauses) # ty: ignore[invalid-argument-type] def over( element: FunctionElement[_T], - partition_by: Optional[ - Union[ - Iterable[Union[_ColumnExpressionArgument[Any], Any]], - _ColumnExpressionArgument[Any], - Any, - ] - ] = None, - order_by: Optional[ - Union[ - Iterable[Union[_ColumnExpressionArgument[Any], Any]], - _ColumnExpressionArgument[Any], - Any, - ] - ] = None, - range_: Optional[Tuple[Optional[int], Optional[int]]] = None, - rows: Optional[Tuple[Optional[int], Optional[int]]] = None, + partition_by: Iterable[_ColumnExpressionArgument[Any] | Any] + | _ColumnExpressionArgument[Any] + | Any + | None = None, + order_by: Iterable[_ColumnExpressionArgument[Any] | Any] + | _ColumnExpressionArgument[Any] + | Any + | None = None, + range_: tuple[int | None, int | None] | None = None, + rows: tuple[int | None, int | None] | None = None, ) -> Over[_T]: return sqlalchemy.over( element, partition_by=partition_by, order_by=order_by, range_=range_, rows=rows @@ -190,21 +177,21 @@ def over( def tuple_( - *clauses: Union[_ColumnExpressionArgument[Any], Any], - types: Optional[Sequence["_TypeEngineArgument[Any]"]] = None, -) -> Tuple[Any, ...]: - return sqlalchemy.tuple_(*clauses, types=types) # type: ignore[return-value] + *clauses: _ColumnExpressionArgument[Any] | Any, + types: Sequence["_TypeEngineArgument[Any]"] | None = None, +) -> sqlalchemy.Tuple: + return sqlalchemy.tuple_(*clauses, types=types) def type_coerce( - expression: Union[_ColumnExpressionOrLiteralArgument[Any], Any], + expression: _ColumnExpressionOrLiteralArgument[Any] | Any, type_: "_TypeEngineArgument[_T]", ) -> TypeCoerce[_T]: return sqlalchemy.type_coerce(expression, type_) def within_group( - element: FunctionElement[_T], *order_by: Union[_ColumnExpressionArgument[Any], Any] + element: FunctionElement[_T], *order_by: _ColumnExpressionArgument[Any] | Any ) -> WithinGroup[_T]: return sqlalchemy.within_group(element, *order_by) diff --git a/sqlmodel/sql/sqltypes.py b/sqlmodel/sql/sqltypes.py index 512daacbab..3119b1b624 100644 --- a/sqlmodel/sql/sqltypes.py +++ b/sqlmodel/sql/sqltypes.py @@ -4,7 +4,7 @@ from sqlalchemy.engine.interfaces import Dialect -class AutoString(types.TypeDecorator): # type: ignore +class AutoString(types.TypeDecorator): impl = types.String cache_ok = True mysql_default_length = 255 diff --git a/tests/conftest.py b/tests/conftest.py index a95eb3279f..9e241eb51c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,21 +1,22 @@ import shutil import subprocess -import sys +from collections.abc import Callable, Generator +from dataclasses import dataclass, field from pathlib import Path -from typing import Any, Callable, Dict, List, Union +from typing import Any +from unittest.mock import patch import pytest from pydantic import BaseModel from sqlmodel import SQLModel -from sqlmodel._compat import IS_PYDANTIC_V2 from sqlmodel.main import default_registry top_level_path = Path(__file__).resolve().parent.parent docs_src_path = top_level_path / "docs_src" -@pytest.fixture() -def clear_sqlmodel(): +@pytest.fixture(autouse=True) +def clear_sqlmodel() -> Any: # Clear the tables in the metadata for the default base model SQLModel.metadata.clear() # Clear the Models associated with the registry, to avoid warnings @@ -26,14 +27,14 @@ def clear_sqlmodel(): @pytest.fixture() -def cov_tmp_path(tmp_path: Path): +def cov_tmp_path(tmp_path: Path) -> Generator[Path, None, None]: yield tmp_path for coverage_path in tmp_path.glob(".coverage*"): - coverage_destiny_path = top_level_path / coverage_path.name + coverage_destiny_path = top_level_path / "coverage" / coverage_path.name shutil.copy(coverage_path, coverage_destiny_path) -def coverage_run(*, module: str, cwd: Union[str, Path]) -> subprocess.CompletedProcess: +def coverage_run(*, module: str, cwd: str | Path) -> subprocess.CompletedProcess: result = subprocess.run( [ "coverage", @@ -51,10 +52,10 @@ def coverage_run(*, module: str, cwd: Union[str, Path]) -> subprocess.CompletedP def get_testing_print_function( - calls: List[List[Union[str, Dict[str, Any]]]], + calls: list[list[str | dict[str, Any]]], ) -> Callable[..., Any]: - def new_print(*args): - data = [] + def new_print(*args: Any) -> None: + data: list[Any] = [] for arg in args: if isinstance(arg, BaseModel): data.append(arg.model_dump()) @@ -71,10 +72,14 @@ def new_print(*args): return new_print -needs_pydanticv2 = pytest.mark.skipif(not IS_PYDANTIC_V2, reason="requires Pydantic v2") -needs_pydanticv1 = pytest.mark.skipif(IS_PYDANTIC_V2, reason="requires Pydantic v1") +@dataclass +class PrintMock: + calls: list[Any] = field(default_factory=list) + -needs_py39 = pytest.mark.skipif(sys.version_info < (3, 9), reason="requires python3.9+") -needs_py310 = pytest.mark.skipif( - sys.version_info < (3, 10), reason="requires python3.10+" -) +@pytest.fixture(name="print_mock") +def print_mock_fixture() -> Generator[PrintMock, None, None]: + print_mock = PrintMock() + new_print = get_testing_print_function(print_mock.calls) + with patch("builtins.print", new=new_print): + yield print_mock diff --git a/tests/test_advanced/test_decimal/test_tutorial001.py b/tests/test_advanced/test_decimal/test_tutorial001.py index 1dafdfb322..0350190003 100644 --- a/tests/test_advanced/test_decimal/test_tutorial001.py +++ b/tests/test_advanced/test_decimal/test_tutorial001.py @@ -1,9 +1,25 @@ +import importlib from decimal import Decimal -from unittest.mock import patch +from types import ModuleType +import pytest from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock + + +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial001_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.advanced.decimal.{request.param}") + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + return mod + expected_calls = [ [ @@ -30,15 +46,6 @@ ] -def test_tutorial(clear_sqlmodel): - from docs_src.advanced.decimal import tutorial001 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() + assert print_mock.calls == expected_calls diff --git a/tests/test_advanced/test_decimal/test_tutorial001_py310.py b/tests/test_advanced/test_decimal/test_tutorial001_py310.py deleted file mode 100644 index f58ea11a7c..0000000000 --- a/tests/test_advanced/test_decimal/test_tutorial001_py310.py +++ /dev/null @@ -1,45 +0,0 @@ -from decimal import Decimal -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - -expected_calls = [ - [ - "Hero 1:", - { - "name": "Deadpond", - "age": None, - "id": 1, - "secret_name": "Dive Wilson", - "money": Decimal("1.100"), - }, - ], - [ - "Hero 2:", - { - "name": "Rusty-Man", - "age": 48, - "id": 3, - "secret_name": "Tommy Sharp", - "money": Decimal("2.200"), - }, - ], - ["Total money: 3.300"], -] - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.advanced.decimal import tutorial001_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls diff --git a/tests/test_advanced/test_uuid/test_tutorial001.py b/tests/test_advanced/test_uuid/test_tutorial001.py index 405195f8e9..ce0a330f21 100644 --- a/tests/test_advanced/test_uuid/test_tutorial001.py +++ b/tests/test_advanced/test_uuid/test_tutorial001.py @@ -1,31 +1,37 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from dirty_equals import IsUUID from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock -def test_tutorial(clear_sqlmodel) -> None: - from docs_src.advanced.uuid import tutorial001 as mod - +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial001_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.advanced.uuid.{request.param}") mod.sqlite_url = "sqlite://" mod.engine = create_engine(mod.sqlite_url) - calls = [] + return mod - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() - first_uuid = calls[1][0]["id"] +def test_tutorial(print_mock: PrintMock, mod: ModuleType) -> None: + mod.main() + first_uuid = print_mock.calls[1][0]["id"] assert first_uuid == IsUUID(4) - second_uuid = calls[7][0]["id"] + second_uuid = print_mock.calls[7][0]["id"] assert second_uuid == IsUUID(4) assert first_uuid != second_uuid - assert calls == [ + assert print_mock.calls == [ ["The hero before saving in the DB"], [ { diff --git a/tests/test_advanced/test_uuid/test_tutorial001_py310.py b/tests/test_advanced/test_uuid/test_tutorial001_py310.py deleted file mode 100644 index ee8cb085df..0000000000 --- a/tests/test_advanced/test_uuid/test_tutorial001_py310.py +++ /dev/null @@ -1,72 +0,0 @@ -from unittest.mock import patch - -from dirty_equals import IsUUID -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel) -> None: - from docs_src.advanced.uuid import tutorial001_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - first_uuid = calls[1][0]["id"] - assert first_uuid == IsUUID(4) - - second_uuid = calls[7][0]["id"] - assert second_uuid == IsUUID(4) - - assert first_uuid != second_uuid - - assert calls == [ - ["The hero before saving in the DB"], - [ - { - "name": "Deadpond", - "secret_name": "Dive Wilson", - "id": first_uuid, - "age": None, - } - ], - ["The hero ID was already set"], - [first_uuid], - ["After saving in the DB"], - [ - { - "name": "Deadpond", - "secret_name": "Dive Wilson", - "age": None, - "id": first_uuid, - } - ], - ["Created hero:"], - [ - { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "age": None, - "id": second_uuid, - } - ], - ["Created hero ID:"], - [second_uuid], - ["Selected hero:"], - [ - { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "age": None, - "id": second_uuid, - } - ], - ["Selected hero ID:"], - [second_uuid], - ] diff --git a/tests/test_advanced/test_uuid/test_tutorial002.py b/tests/test_advanced/test_uuid/test_tutorial002.py index cefd95ba49..1035771502 100644 --- a/tests/test_advanced/test_uuid/test_tutorial002.py +++ b/tests/test_advanced/test_uuid/test_tutorial002.py @@ -1,31 +1,37 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from dirty_equals import IsUUID from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock -def test_tutorial(clear_sqlmodel) -> None: - from docs_src.advanced.uuid import tutorial002 as mod - +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial002_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.advanced.uuid.{request.param}") mod.sqlite_url = "sqlite://" mod.engine = create_engine(mod.sqlite_url) - calls = [] + return mod - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() - first_uuid = calls[1][0]["id"] +def test_tutorial(print_mock: PrintMock, mod: ModuleType) -> None: + mod.main() + first_uuid = print_mock.calls[1][0]["id"] assert first_uuid == IsUUID(4) - second_uuid = calls[7][0]["id"] + second_uuid = print_mock.calls[7][0]["id"] assert second_uuid == IsUUID(4) assert first_uuid != second_uuid - assert calls == [ + assert print_mock.calls == [ ["The hero before saving in the DB"], [ { diff --git a/tests/test_advanced/test_uuid/test_tutorial002_py310.py b/tests/test_advanced/test_uuid/test_tutorial002_py310.py deleted file mode 100644 index 96f85c5333..0000000000 --- a/tests/test_advanced/test_uuid/test_tutorial002_py310.py +++ /dev/null @@ -1,72 +0,0 @@ -from unittest.mock import patch - -from dirty_equals import IsUUID -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel) -> None: - from docs_src.advanced.uuid import tutorial002_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - first_uuid = calls[1][0]["id"] - assert first_uuid == IsUUID(4) - - second_uuid = calls[7][0]["id"] - assert second_uuid == IsUUID(4) - - assert first_uuid != second_uuid - - assert calls == [ - ["The hero before saving in the DB"], - [ - { - "name": "Deadpond", - "secret_name": "Dive Wilson", - "id": first_uuid, - "age": None, - } - ], - ["The hero ID was already set"], - [first_uuid], - ["After saving in the DB"], - [ - { - "name": "Deadpond", - "secret_name": "Dive Wilson", - "age": None, - "id": first_uuid, - } - ], - ["Created hero:"], - [ - { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "age": None, - "id": second_uuid, - } - ], - ["Created hero ID:"], - [second_uuid], - ["Selected hero:"], - [ - { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "age": None, - "id": second_uuid, - } - ], - ["Selected hero ID:"], - [second_uuid], - ] diff --git a/tests/test_aliases.py b/tests/test_aliases.py new file mode 100644 index 0000000000..f0ebf747b4 --- /dev/null +++ b/tests/test_aliases.py @@ -0,0 +1,181 @@ +import pytest +from pydantic import BaseModel, ValidationError +from pydantic import Field as PField +from sqlmodel import Field, SQLModel + +""" +Alias tests for SQLModel and Pydantic compatibility +""" + + +class PydanticUser(BaseModel): + full_name: str = PField(alias="fullName") + + +class SQLModelUser(SQLModel): + full_name: str = Field(alias="fullName") + + +# Models with config (validate_by_name=True) +class PydanticUserWithConfig(PydanticUser): + model_config = {"validate_by_name": True} + + +class SQLModelUserWithConfig(SQLModelUser): + model_config = {"validate_by_name": True} + + +@pytest.mark.parametrize("model", [PydanticUser, SQLModelUser]) +def test_create_with_field_name(model: type[PydanticUser] | type[SQLModelUser]): + with pytest.raises(ValidationError): + model(full_name="Alice") + + +@pytest.mark.parametrize("model", [PydanticUserWithConfig, SQLModelUserWithConfig]) +def test_create_with_field_name_with_config( + model: type[PydanticUserWithConfig] | type[SQLModelUserWithConfig], +): + user = model(full_name="Alice") + assert user.full_name == "Alice" + + +@pytest.mark.parametrize( + "model", + [PydanticUser, SQLModelUser, PydanticUserWithConfig, SQLModelUserWithConfig], +) +def test_create_with_alias( + model: type[PydanticUser] + | type[SQLModelUser] + | type[PydanticUserWithConfig] + | type[SQLModelUserWithConfig], +): + user = model(fullName="Bob") # using alias + assert user.full_name == "Bob" + + +@pytest.mark.parametrize("model", [PydanticUserWithConfig, SQLModelUserWithConfig]) +def test_create_with_both_prefers_alias( + model: type[PydanticUserWithConfig] | type[SQLModelUserWithConfig], +): + user = model(full_name="IGNORED", fullName="Charlie") + assert user.full_name == "Charlie" # alias should take precedence + + +@pytest.mark.parametrize("model", [PydanticUser, SQLModelUser]) +def test_dict_default_uses_field_names( + model: type[PydanticUser] | type[SQLModelUser], +): + user = model(fullName="Dana") + data = user.model_dump() + assert "full_name" in data + assert "fullName" not in data + assert data["full_name"] == "Dana" + + +@pytest.mark.parametrize("model", [PydanticUser, SQLModelUser]) +def test_dict_by_alias_uses_aliases( + model: type[PydanticUser] | type[SQLModelUser], +): + user = model(fullName="Dana") + data = user.model_dump(by_alias=True) + assert "fullName" in data + assert "full_name" not in data + assert data["fullName"] == "Dana" + + +@pytest.mark.parametrize("model", [PydanticUser, SQLModelUser]) +def test_json_by_alias( + model: type[PydanticUser] | type[SQLModelUser], +): + user = model(fullName="Frank") + json_data = user.model_dump_json(by_alias=True) + assert ('"fullName":"Frank"' in json_data) or ('"fullName": "Frank"' in json_data) + assert "full_name" not in json_data + + +class PydanticUserV2(BaseModel): + first_name: str = PField(validation_alias="firstName", serialization_alias="f_name") + + +class SQLModelUserV2(SQLModel): + first_name: str = Field(validation_alias="firstName", serialization_alias="f_name") + + +@pytest.mark.parametrize("model", [PydanticUserV2, SQLModelUserV2]) +def test_create_with_validation_alias( + model: type[PydanticUserV2] | type[SQLModelUserV2], +): + user = model(firstName="John") + assert user.first_name == "John" + + +@pytest.mark.parametrize("model", [PydanticUserV2, SQLModelUserV2]) +def test_serialize_with_serialization_alias( + model: type[PydanticUserV2] | type[SQLModelUserV2], +): + user = model(firstName="Jane") + data = user.model_dump(by_alias=True) + assert "f_name" in data + assert "firstName" not in data + assert "first_name" not in data + assert data["f_name"] == "Jane" + + +def test_schema_extra_validation_alias_sqlmodel_v2(): + class M(SQLModel): + f: str = Field(schema_extra={"validation_alias": "f_alias"}) + + m = M.model_validate({"f_alias": "asd"}) + assert m.f == "asd" + + +def test_schema_extra_serialization_alias_sqlmodel_v2(): + class M(SQLModel): + f: str = Field(schema_extra={"serialization_alias": "f_out"}) + + m = M(f="x") + data = m.model_dump(by_alias=True) + assert "f_out" in data + assert "f" not in data + assert data["f_out"] == "x" + + +def test_alias_plus_validation_alias_prefers_validation_alias_sqlmodel_v2(): + class M(SQLModel): + first_name: str = Field(alias="fullName", validation_alias="v_name") + + m = M.model_validate({"fullName": "A", "v_name": "B"}) + assert m.first_name == "B" + + +def test_alias_plus_serialization_alias_prefers_serialization_alias_sqlmodel_v2(): + class M(SQLModel): + first_name: str = Field(alias="fullName", serialization_alias="f_name") + + m = M(fullName="Z") + data = m.model_dump(by_alias=True) + assert "f_name" in data + assert "fullName" not in data + assert data["f_name"] == "Z" + + +def test_alias_generator_works_sqlmodel_v2(): + class M(SQLModel): + model_config = {"alias_generator": lambda s: "gen_" + s} + f: str = Field() + + m = M.model_validate({"gen_f": "ok"}) + assert m.f == "ok" + data = m.model_dump(by_alias=True) + assert "gen_f" in data and data["gen_f"] == "ok" + + +def test_alias_generator_with_explicit_alias_prefers_field_alias_sqlmodel_v2(): + class M(SQLModel): + model_config = {"alias_generator": lambda s: "gen_" + s} + f: str = Field(alias="custom") + + m = M.model_validate({"custom": "ok"}) + assert m.f == "ok" + data = m.model_dump(by_alias=True) + assert "custom" in data and "gen_f" not in data diff --git a/tests/test_annotated_uuid.py b/tests/test_annotated_uuid.py index b0e25ab099..44d5ced780 100644 --- a/tests/test_annotated_uuid.py +++ b/tests/test_annotated_uuid.py @@ -1,18 +1,14 @@ import uuid -from typing import Optional from sqlmodel import Field, Session, SQLModel, create_engine, select -from tests.conftest import needs_pydanticv2 - -@needs_pydanticv2 def test_annotated_optional_types(clear_sqlmodel) -> None: from pydantic import UUID4 class Hero(SQLModel, table=True): # Pydantic UUID4 is: Annotated[UUID, UuidVersion(4)] - id: Optional[UUID4] = Field(default_factory=uuid.uuid4, primary_key=True) + id: UUID4 | None = Field(default_factory=uuid.uuid4, primary_key=True) engine = create_engine("sqlite:///:memory:") SQLModel.metadata.create_all(engine) diff --git a/tests/test_enums.py b/tests/test_enums.py index 2808f3f9a9..933a24e99e 100644 --- a/tests/test_enums.py +++ b/tests/test_enums.py @@ -6,7 +6,6 @@ from sqlmodel import SQLModel from . import test_enums_models -from .conftest import needs_pydanticv1, needs_pydanticv2 """ Tests related to Enums @@ -55,49 +54,6 @@ def test_sqlite_ddl_sql(clear_sqlmodel, capsys: pytest.CaptureFixture[str]): assert "CREATE TYPE" not in captured.out -@needs_pydanticv1 -def test_json_schema_flat_model_pydantic_v1(): - assert test_enums_models.FlatModel.schema() == { - "title": "FlatModel", - "type": "object", - "properties": { - "id": {"title": "Id", "type": "string", "format": "uuid"}, - "enum_field": {"$ref": "#/definitions/MyEnum1"}, - }, - "required": ["id", "enum_field"], - "definitions": { - "MyEnum1": { - "title": "MyEnum1", - "description": "An enumeration.", - "enum": ["A", "B"], - "type": "string", - } - }, - } - - -@needs_pydanticv1 -def test_json_schema_inherit_model_pydantic_v1(): - assert test_enums_models.InheritModel.schema() == { - "title": "InheritModel", - "type": "object", - "properties": { - "id": {"title": "Id", "type": "string", "format": "uuid"}, - "enum_field": {"$ref": "#/definitions/MyEnum2"}, - }, - "required": ["id", "enum_field"], - "definitions": { - "MyEnum2": { - "title": "MyEnum2", - "description": "An enumeration.", - "enum": ["C", "D"], - "type": "string", - } - }, - } - - -@needs_pydanticv2 def test_json_schema_flat_model_pydantic_v2(): assert test_enums_models.FlatModel.model_json_schema() == { "title": "FlatModel", @@ -113,7 +69,6 @@ def test_json_schema_flat_model_pydantic_v2(): } -@needs_pydanticv2 def test_json_schema_inherit_model_pydantic_v2(): assert test_enums_models.InheritModel.model_json_schema() == { "title": "InheritModel", diff --git a/tests/test_field_sa_args_kwargs.py b/tests/test_field_sa_args_kwargs.py index 94a1a13483..4a67e8e826 100644 --- a/tests/test_field_sa_args_kwargs.py +++ b/tests/test_field_sa_args_kwargs.py @@ -1,17 +1,15 @@ -from typing import Optional - from sqlalchemy import ForeignKey from sqlmodel import Field, SQLModel, create_engine def test_sa_column_args(clear_sqlmodel, caplog) -> None: class Team(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) + id: int | None = Field(default=None, primary_key=True) name: str class Hero(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - team_id: Optional[int] = Field( + id: int | None = Field(default=None, primary_key=True) + team_id: int | None = Field( default=None, sa_column_args=[ForeignKey("team.id")], ) @@ -26,7 +24,7 @@ class Hero(SQLModel, table=True): def test_sa_column_kargs(clear_sqlmodel, caplog) -> None: class Item(SQLModel, table=True): - id: Optional[int] = Field( + id: int | None = Field( default=None, sa_column_kwargs={"primary_key": True}, ) diff --git a/tests/test_field_sa_column.py b/tests/test_field_sa_column.py index e2ccc6d7ef..1bfca79503 100644 --- a/tests/test_field_sa_column.py +++ b/tests/test_field_sa_column.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Annotated import pytest from sqlalchemy import Column, Integer, String @@ -7,7 +7,7 @@ def test_sa_column_takes_precedence() -> None: class Item(SQLModel, table=True): - id: Optional[int] = Field( + id: int | None = Field( default=None, sa_column=Column(String, primary_key=True, nullable=False), ) @@ -17,11 +17,22 @@ class Item(SQLModel, table=True): assert isinstance(Item.id.type, String) # type: ignore +def test_sa_column_with_annotated_metadata() -> None: + class Item(SQLModel, table=True): + id: Annotated[int | None, "meta"] = Field( + default=None, + sa_column=Column(String, primary_key=True, nullable=False), + ) + + assert Item.id.nullable is False # type: ignore + assert isinstance(Item.id.type, String) # type: ignore + + def test_sa_column_no_sa_args() -> None: with pytest.raises(RuntimeError): class Item(SQLModel, table=True): - id: Optional[int] = Field( + id: int | None = Field( default=None, sa_column_args=[Integer], sa_column=Column(Integer, primary_key=True), @@ -32,7 +43,7 @@ def test_sa_column_no_sa_kargs() -> None: with pytest.raises(RuntimeError): class Item(SQLModel, table=True): - id: Optional[int] = Field( + id: int | None = Field( default=None, sa_column_kwargs={"primary_key": True}, sa_column=Column(Integer, primary_key=True), @@ -43,7 +54,7 @@ def test_sa_column_no_type() -> None: with pytest.raises(RuntimeError): class Item(SQLModel, table=True): - id: Optional[int] = Field( + id: int | None = Field( default=None, sa_type=Integer, sa_column=Column(Integer, primary_key=True), @@ -54,7 +65,7 @@ def test_sa_column_no_primary_key() -> None: with pytest.raises(RuntimeError): class Item(SQLModel, table=True): - id: Optional[int] = Field( + id: int | None = Field( default=None, primary_key=True, sa_column=Column(Integer, primary_key=True), @@ -65,7 +76,7 @@ def test_sa_column_no_nullable() -> None: with pytest.raises(RuntimeError): class Item(SQLModel, table=True): - id: Optional[int] = Field( + id: int | None = Field( default=None, nullable=True, sa_column=Column(Integer, primary_key=True), @@ -76,12 +87,12 @@ def test_sa_column_no_foreign_key() -> None: with pytest.raises(RuntimeError): class Team(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) + id: int | None = Field(default=None, primary_key=True) name: str class Hero(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - team_id: Optional[int] = Field( + id: int | None = Field(default=None, primary_key=True) + team_id: int | None = Field( default=None, foreign_key="team.id", sa_column=Column(Integer, primary_key=True), @@ -92,7 +103,7 @@ def test_sa_column_no_unique() -> None: with pytest.raises(RuntimeError): class Item(SQLModel, table=True): - id: Optional[int] = Field( + id: int | None = Field( default=None, unique=True, sa_column=Column(Integer, primary_key=True), @@ -103,7 +114,7 @@ def test_sa_column_no_index() -> None: with pytest.raises(RuntimeError): class Item(SQLModel, table=True): - id: Optional[int] = Field( + id: int | None = Field( default=None, index=True, sa_column=Column(Integer, primary_key=True), @@ -114,7 +125,7 @@ def test_sa_column_no_ondelete() -> None: with pytest.raises(RuntimeError): class Item(SQLModel, table=True): - id: Optional[int] = Field( + id: int | None = Field( default=None, sa_column=Column(Integer, primary_key=True), ondelete="CASCADE", diff --git a/tests/test_field_sa_relationship.py b/tests/test_field_sa_relationship.py index 022a100a78..55b0334df9 100644 --- a/tests/test_field_sa_relationship.py +++ b/tests/test_field_sa_relationship.py @@ -1,5 +1,3 @@ -from typing import List, Optional - import pytest from sqlalchemy.orm import relationship from sqlmodel import Field, Relationship, SQLModel @@ -9,45 +7,45 @@ def test_sa_relationship_no_args() -> None: with pytest.raises(RuntimeError): # pragma: no cover class Team(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) + id: int | None = Field(default=None, primary_key=True) name: str = Field(index=True) headquarters: str - heroes: List["Hero"] = Relationship( + heroes: list["Hero"] = Relationship( back_populates="team", sa_relationship_args=["Hero"], sa_relationship=relationship("Hero", back_populates="team"), ) class Hero(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) + id: int | None = Field(default=None, primary_key=True) name: str = Field(index=True) secret_name: str - age: Optional[int] = Field(default=None, index=True) + age: int | None = Field(default=None, index=True) - team_id: Optional[int] = Field(default=None, foreign_key="team.id") - team: Optional[Team] = Relationship(back_populates="heroes") + team_id: int | None = Field(default=None, foreign_key="team.id") + team: Team | None = Relationship(back_populates="heroes") def test_sa_relationship_no_kwargs() -> None: with pytest.raises(RuntimeError): # pragma: no cover class Team(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) + id: int | None = Field(default=None, primary_key=True) name: str = Field(index=True) headquarters: str - heroes: List["Hero"] = Relationship( + heroes: list["Hero"] = Relationship( back_populates="team", sa_relationship_kwargs={"lazy": "selectin"}, sa_relationship=relationship("Hero", back_populates="team"), ) class Hero(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) + id: int | None = Field(default=None, primary_key=True) name: str = Field(index=True) secret_name: str - age: Optional[int] = Field(default=None, index=True) + age: int | None = Field(default=None, index=True) - team_id: Optional[int] = Field(default=None, foreign_key="team.id") - team: Optional[Team] = Relationship(back_populates="heroes") + team_id: int | None = Field(default=None, foreign_key="team.id") + team: Team | None = Relationship(back_populates="heroes") diff --git a/tests/test_fields_set.py b/tests/test_fields_set.py index e0bd8cba76..0574e5a200 100644 --- a/tests/test_fields_set.py +++ b/tests/test_fields_set.py @@ -1,7 +1,6 @@ from datetime import datetime, timedelta from sqlmodel import Field, SQLModel -from sqlmodel._compat import get_fields_set def test_fields_set(): @@ -11,12 +10,12 @@ class User(SQLModel): last_updated: datetime = Field(default_factory=datetime.now) user = User(username="bob") - assert get_fields_set(user) == {"username"} + assert user.model_fields_set == {"username"} user = User(username="bob", email="bob@test.com") - assert get_fields_set(user) == {"username", "email"} + assert user.model_fields_set == {"username", "email"} user = User( username="bob", email="bob@test.com", last_updated=datetime.now() - timedelta(days=1), ) - assert get_fields_set(user) == {"username", "email", "last_updated"} + assert user.model_fields_set == {"username", "email", "last_updated"} diff --git a/tests/test_future_annotations.py b/tests/test_future_annotations.py new file mode 100644 index 0000000000..93f39a33fe --- /dev/null +++ b/tests/test_future_annotations.py @@ -0,0 +1,63 @@ +from __future__ import annotations + +from typing import Annotated + +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +def test_model_with_future_annotations(clear_sqlmodel): + class Hero(SQLModel, table=True): + id: Annotated[int | None, Field(primary_key=True)] = None + name: str + secret_name: str + age: int | None = None + + hero = Hero(name="Deadpond", secret_name="Dive Wilson", age=25) + + engine = create_engine("sqlite://") + SQLModel.metadata.create_all(engine) + + with Session(engine) as session: + session.add(hero) + session.commit() + session.refresh(hero) + + assert hero.id is not None + assert hero.name == "Deadpond" + assert hero.secret_name == "Dive Wilson" + assert hero.age == 25 + + with Session(engine) as session: + heroes = session.exec(select(Hero)).all() + assert len(heroes) == 1 + assert heroes[0].name == "Deadpond" + + +def test_model_with_string_annotations(clear_sqlmodel): + class Team(SQLModel, table=True): + id: Annotated[int | None, Field(primary_key=True)] = None + name: str + + class Player(SQLModel, table=True): + id: Annotated[int | None, Field(primary_key=True)] = None + name: str + team_id: Annotated[int | None, Field(foreign_key="team.id")] = None + + engine = create_engine("sqlite://") + SQLModel.metadata.create_all(engine) + + team = Team(name="Champions") + player = Player(name="Alice", team_id=None) + + with Session(engine) as session: + session.add(team) + session.commit() + session.refresh(team) + + player.team_id = team.id + session.add(player) + session.commit() + session.refresh(player) + + assert team.id is not None + assert player.team_id == team.id diff --git a/tests/test_instance_no_args.py b/tests/test_instance_no_args.py index 5c8ad77531..72680dfff9 100644 --- a/tests/test_instance_no_args.py +++ b/tests/test_instance_no_args.py @@ -1,5 +1,3 @@ -from typing import Optional - import pytest from pydantic import ValidationError from sqlmodel import Field, Session, SQLModel, create_engine, select @@ -7,9 +5,9 @@ def test_allow_instantiation_without_arguments(clear_sqlmodel): class Item(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) + id: int | None = Field(default=None, primary_key=True) name: str - description: Optional[str] = None + description: str | None = None engine = create_engine("sqlite:///:memory:") SQLModel.metadata.create_all(engine) @@ -27,9 +25,9 @@ class Item(SQLModel, table=True): def test_not_allow_instantiation_without_arguments_if_not_table(): class Item(SQLModel): - id: Optional[int] = Field(default=None, primary_key=True) + id: int | None = Field(default=None, primary_key=True) name: str - description: Optional[str] = None + description: str | None = None with pytest.raises(ValidationError): Item() diff --git a/tests/test_main.py b/tests/test_main.py index 60d5c40ebb..fa40b71853 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import Annotated import pytest from sqlalchemy.exc import IntegrityError @@ -8,10 +8,10 @@ def test_should_allow_duplicate_row_if_unique_constraint_is_not_passed(clear_sqlmodel): class Hero(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) + id: int | None = Field(default=None, primary_key=True) name: str secret_name: str - age: Optional[int] = None + age: int | None = None hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") hero_2 = Hero(name="Deadpond", secret_name="Dive Wilson") @@ -38,10 +38,10 @@ class Hero(SQLModel, table=True): def test_should_allow_duplicate_row_if_unique_constraint_is_false(clear_sqlmodel): class Hero(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) + id: int | None = Field(default=None, primary_key=True) name: str secret_name: str = Field(unique=False) - age: Optional[int] = None + age: int | None = None hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") hero_2 = Hero(name="Deadpond", secret_name="Dive Wilson") @@ -70,10 +70,10 @@ def test_should_raise_exception_when_try_to_duplicate_row_if_unique_constraint_i clear_sqlmodel, ): class Hero(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) + id: int | None = Field(default=None, primary_key=True) name: str secret_name: str = Field(unique=True) - age: Optional[int] = None + age: int | None = None hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") hero_2 = Hero(name="Deadpond", secret_name="Dive Wilson") @@ -97,17 +97,17 @@ def test_sa_relationship_property(clear_sqlmodel): """Test https://github.com/tiangolo/sqlmodel/issues/315#issuecomment-1272122306""" class Team(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) + id: int | None = Field(default=None, primary_key=True) name: str = Field(unique=True) - heroes: List["Hero"] = Relationship( # noqa: F821 + heroes: list["Hero"] = Relationship( # noqa: F821 sa_relationship=RelationshipProperty("Hero", back_populates="team") ) class Hero(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) + id: int | None = Field(default=None, primary_key=True) name: str = Field(unique=True) - team_id: Optional[int] = Field(default=None, foreign_key="team.id") - team: Optional[Team] = Relationship( + team_id: int | None = Field(default=None, foreign_key="team.id") + team: Team | None = Relationship( sa_relationship=RelationshipProperty("Team", back_populates="heroes") ) @@ -125,3 +125,94 @@ class Hero(SQLModel, table=True): # The next statement should not raise an AttributeError assert hero_rusty_man.team assert hero_rusty_man.team.name == "Preventers" + + +def test_composite_primary_key(clear_sqlmodel): + class UserPermission(SQLModel, table=True): + user_id: int = Field(primary_key=True) + resource_id: int = Field(primary_key=True) + permission: str + + engine = create_engine("sqlite://") + SQLModel.metadata.create_all(engine) + + pk_column_names = {column.name for column in UserPermission.__table__.primary_key} + assert pk_column_names == {"user_id", "resource_id"} + + with Session(engine) as session: + perm1 = UserPermission(user_id=1, resource_id=1, permission="read") + perm2 = UserPermission(user_id=1, resource_id=2, permission="write") + session.add(perm1) + session.add(perm2) + session.commit() + + with pytest.raises(IntegrityError): + with Session(engine) as session: + perm3 = UserPermission(user_id=1, resource_id=1, permission="admin") + session.add(perm3) + session.commit() + + +def test_composite_primary_key_and_validator(clear_sqlmodel): + from pydantic import AfterValidator + + def validate_resource_id(value: int) -> int: + if value < 1: + raise ValueError("Resource ID must be positive") + return value + + class UserPermission(SQLModel, table=True): + user_id: int = Field(primary_key=True) + resource_id: Annotated[int, AfterValidator(validate_resource_id)] = Field( + primary_key=True + ) + permission: str + + engine = create_engine("sqlite://") + SQLModel.metadata.create_all(engine) + + pk_column_names = {column.name for column in UserPermission.__table__.primary_key} + assert pk_column_names == {"user_id", "resource_id"} + + with Session(engine) as session: + perm1 = UserPermission(user_id=1, resource_id=1, permission="read") + perm2 = UserPermission(user_id=1, resource_id=2, permission="write") + session.add(perm1) + session.add(perm2) + session.commit() + + with pytest.raises(IntegrityError): + with Session(engine) as session: + perm3 = UserPermission(user_id=1, resource_id=1, permission="admin") + session.add(perm3) + session.commit() + + +def test_foreign_key_ondelete_with_annotated(clear_sqlmodel): + from pydantic import AfterValidator + + def ensure_positive(value: int) -> int: + if value < 0: + raise ValueError("Team ID must be positive") + return value + + class Team(SQLModel, table=True): + id: int = Field(primary_key=True) + name: str + + class Hero(SQLModel, table=True): + id: int = Field(primary_key=True) + team_id: Annotated[int, AfterValidator(ensure_positive)] = Field( + foreign_key="team.id", + ondelete="CASCADE", + ) + name: str + + engine = create_engine("sqlite://") + SQLModel.metadata.create_all(engine) + + team_id_column = Hero.__table__.c.team_id # type: ignore[attr-defined] + foreign_keys = list(team_id_column.foreign_keys) + assert len(foreign_keys) == 1 + assert foreign_keys[0].ondelete == "CASCADE" + assert team_id_column.nullable is False diff --git a/tests/test_missing_type.py b/tests/test_missing_type.py index ac4aa42e05..9005390d4f 100644 --- a/tests/test_missing_type.py +++ b/tests/test_missing_type.py @@ -1,5 +1,3 @@ -from typing import Optional - import pytest from pydantic import BaseModel from sqlmodel import Field, SQLModel @@ -18,5 +16,5 @@ def validate(cls, v): # pragma: no cover with pytest.raises(ValueError): class Item(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) + id: int | None = Field(default=None, primary_key=True) item: CustomType diff --git a/tests/test_nullable.py b/tests/test_nullable.py index a40bb5b5f0..db028304ab 100644 --- a/tests/test_nullable.py +++ b/tests/test_nullable.py @@ -1,5 +1,3 @@ -from typing import Optional - import pytest from sqlalchemy.exc import IntegrityError from sqlmodel import Field, Session, SQLModel, create_engine @@ -7,41 +5,41 @@ def test_nullable_fields(clear_sqlmodel, caplog): class Hero(SQLModel, table=True): - primary_key: Optional[int] = Field( + primary_key: int | None = Field( default=None, primary_key=True, ) required_value: str - optional_default_ellipsis: Optional[str] = Field(default=...) - optional_default_none: Optional[str] = Field(default=None) - optional_non_nullable: Optional[str] = Field( + optional_default_ellipsis: str | None = Field(default=...) + optional_default_none: str | None = Field(default=None) + optional_non_nullable: str | None = Field( nullable=False, ) - optional_nullable: Optional[str] = Field( + optional_nullable: str | None = Field( nullable=True, ) - optional_default_ellipses_non_nullable: Optional[str] = Field( + optional_default_ellipses_non_nullable: str | None = Field( default=..., nullable=False, ) - optional_default_ellipses_nullable: Optional[str] = Field( + optional_default_ellipses_nullable: str | None = Field( default=..., nullable=True, ) - optional_default_none_non_nullable: Optional[str] = Field( + optional_default_none_non_nullable: str | None = Field( default=None, nullable=False, ) - optional_default_none_nullable: Optional[str] = Field( + optional_default_none_nullable: str | None = Field( default=None, nullable=True, ) default_ellipses_non_nullable: str = Field(default=..., nullable=False) - optional_default_str: Optional[str] = "default" - optional_default_str_non_nullable: Optional[str] = Field( + optional_default_str: str | None = "default" + optional_default_str_non_nullable: str | None = Field( default="default", nullable=False ) - optional_default_str_nullable: Optional[str] = Field( + optional_default_str_nullable: str | None = Field( default="default", nullable=True ) str_default_str: str = "default" @@ -82,12 +80,12 @@ class Hero(SQLModel, table=True): # Test for regression in https://github.com/tiangolo/sqlmodel/issues/420 def test_non_nullable_optional_field_with_no_default_set(clear_sqlmodel, caplog): class Hero(SQLModel, table=True): - primary_key: Optional[int] = Field( + primary_key: int | None = Field( default=None, primary_key=True, ) - optional_non_nullable_no_default: Optional[str] = Field(nullable=False) + optional_non_nullable_no_default: str | None = Field(nullable=False) engine = create_engine("sqlite://", echo=True) SQLModel.metadata.create_all(engine) @@ -110,7 +108,7 @@ class Hero(SQLModel, table=True): def test_nullable_primary_key(clear_sqlmodel, caplog): # Probably the weirdest corner case, it shouldn't happen anywhere, but let's test it class Hero(SQLModel, table=True): - nullable_integer_primary_key: Optional[int] = Field( + nullable_integer_primary_key: int | None = Field( default=None, primary_key=True, nullable=True, diff --git a/tests/test_ondelete_raises.py b/tests/test_ondelete_raises.py index cbcab4ca41..c6bc05c06a 100644 --- a/tests/test_ondelete_raises.py +++ b/tests/test_ondelete_raises.py @@ -1,4 +1,4 @@ -from typing import Any, List, Union +from typing import Any import pytest from sqlmodel import Field, Relationship, SQLModel @@ -8,17 +8,17 @@ def test_ondelete_requires_nullable(clear_sqlmodel: Any) -> None: with pytest.raises(RuntimeError) as exc: class Team(SQLModel, table=True): - id: Union[int, None] = Field(default=None, primary_key=True) + id: int | None = Field(default=None, primary_key=True) - heroes: List["Hero"] = Relationship( + heroes: list["Hero"] = Relationship( back_populates="team", passive_deletes="all" ) class Hero(SQLModel, table=True): - id: Union[int, None] = Field(default=None, primary_key=True) + id: int | None = Field(default=None, primary_key=True) name: str = Field(index=True) secret_name: str - age: Union[int, None] = Field(default=None, index=True) + age: int | None = Field(default=None, index=True) team_id: int = Field(foreign_key="team.id", ondelete="SET NULL") team: Team = Relationship(back_populates="heroes") @@ -30,7 +30,7 @@ def test_ondelete_requires_foreign_key(clear_sqlmodel: Any) -> None: with pytest.raises(RuntimeError) as exc: class Team(SQLModel, table=True): - id: Union[int, None] = Field(default=None, primary_key=True) + id: int | None = Field(default=None, primary_key=True) age: int = Field(ondelete="CASCADE") diff --git a/tests/test_pydantic/test_field.py b/tests/test_pydantic/test_field.py index 9d7bc77625..11f4150d98 100644 --- a/tests/test_pydantic/test_field.py +++ b/tests/test_pydantic/test_field.py @@ -1,10 +1,9 @@ from decimal import Decimal -from typing import Optional, Union +from typing import Literal import pytest from pydantic import ValidationError from sqlmodel import Field, SQLModel -from typing_extensions import Literal def test_decimal(): @@ -39,7 +38,7 @@ class Lizard(SQLModel): scales: bool class Model(SQLModel): - pet: Union[Cat, Dog, Lizard] = Field(..., discriminator="pet_type") + pet: Cat | Dog | Lizard = Field(..., discriminator="pet_type") n: int Model(pet={"pet_type": "dog", "barks": 3.14}, n=1) # type: ignore[arg-type] @@ -50,7 +49,7 @@ class Model(SQLModel): def test_repr(): class Model(SQLModel): - id: Optional[int] = Field(primary_key=True) + id: int | None = Field(primary_key=True) foo: str = Field(repr=False) instance = Model(id=123, foo="bar") diff --git a/tests/test_query.py b/tests/test_query.py index 88517b92fe..26d35b3c2b 100644 --- a/tests/test_query.py +++ b/tests/test_query.py @@ -1,15 +1,13 @@ -from typing import Optional - import pytest from sqlmodel import Field, Session, SQLModel, create_engine def test_query(clear_sqlmodel): class Hero(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) + id: int | None = Field(default=None, primary_key=True) name: str secret_name: str - age: Optional[int] = None + age: int | None = None hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") diff --git a/tests/test_select_gen.py b/tests/test_select_gen.py index 6d578f7708..94ed340e94 100644 --- a/tests/test_select_gen.py +++ b/tests/test_select_gen.py @@ -1,17 +1,17 @@ +import os import subprocess import sys from pathlib import Path -from .conftest import needs_py39 - root_path = Path(__file__).parent.parent -@needs_py39 def test_select_gen() -> None: + env = os.environ.copy() + env["CHECK_JINJA"] = "1" result = subprocess.run( - [sys.executable, "scripts/generate_select.py"], - env={"CHECK_JINJA": "1"}, + [sys.executable, Path("scripts") / "generate_select.py"], + env=env, check=True, cwd=root_path, capture_output=True, diff --git a/tests/test_select_typing.py b/tests/test_select_typing.py new file mode 100644 index 0000000000..315f0efec7 --- /dev/null +++ b/tests/test_select_typing.py @@ -0,0 +1,62 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select +from sqlmodel.pool import StaticPool + + +def test_fields() -> None: + class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str + secret_name: str + age: int | None = None + food: str | None = None + + engine = create_engine( + "sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool + ) + + SQLModel.metadata.create_all(engine) + + with Session(engine) as session: + session.add(Hero(name="Deadpond", secret_name="Dive Wilson")) + session.add( + Hero(name="Spider-Boy", secret_name="Pedro Parqueador", food="pizza") + ) + session.add(Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48)) + + session.commit() + + # check typing of select with 3 fields + with Session(engine) as session: + statement_3 = select(Hero.id, Hero.name, Hero.secret_name) + results_3 = session.exec(statement_3) + for hero_3 in results_3: + assert len(hero_3) == 3 + name_3: str = hero_3[1] + assert type(name_3) is str + assert type(hero_3[0]) is int + assert type(hero_3[2]) is str + + # check typing of select with 4 fields + with Session(engine) as session: + statement_4 = select(Hero.id, Hero.name, Hero.secret_name, Hero.age) + results_4 = session.exec(statement_4) + for hero_4 in results_4: + assert len(hero_4) == 4 + name_4: str = hero_4[1] + assert type(name_4) is str + assert type(hero_4[0]) is int + assert type(hero_4[2]) is str + assert type(hero_4[3]) in [int, type(None)] + + # check typing of select with 5 fields: currently runs but doesn't pass mypy + # with Session(engine) as session: + # statement_5 = select(Hero.id, Hero.name, Hero.secret_name, Hero.age, Hero.food) + # results_5 = session.exec(statement_5) + # for hero_5 in results_5: + # assert len(hero_5) == 5 + # name_5: str = hero_5[1] + # assert type(name_5) is str + # assert type(hero_5[0]) is int + # assert type(hero_5[2]) is str + # assert type(hero_5[3]) in [int, type(None)] + # assert type(hero_5[4]) in [str, type(None)] diff --git a/tests/test_sqlalchemy_type_errors.py b/tests/test_sqlalchemy_type_errors.py index e211c46a34..5a40f9c172 100644 --- a/tests/test_sqlalchemy_type_errors.py +++ b/tests/test_sqlalchemy_type_errors.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, List, Optional, Union +from typing import Any import pytest from sqlmodel import Field, SQLModel @@ -8,21 +8,21 @@ def test_type_list_breaks() -> None: with pytest.raises(ValueError): class Hero(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - tags: List[str] + id: int | None = Field(default=None, primary_key=True) + tags: list[str] def test_type_dict_breaks() -> None: with pytest.raises(ValueError): class Hero(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - tags: Dict[str, Any] + id: int | None = Field(default=None, primary_key=True) + tags: dict[str, Any] def test_type_union_breaks() -> None: with pytest.raises(ValueError): class Hero(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - tags: Union[int, str] + id: int | None = Field(default=None, primary_key=True) + tags: int | str diff --git a/tests/test_tutorial/test_automatic_id_none_refresh/test_tutorial001_py310_tutorial002_py310.py b/tests/test_tutorial/test_automatic_id_none_refresh/test_tutorial001_py310_tutorial002_py310.py deleted file mode 100644 index 9ffcd8ae33..0000000000 --- a/tests/test_tutorial/test_automatic_id_none_refresh/test_tutorial001_py310_tutorial002_py310.py +++ /dev/null @@ -1,163 +0,0 @@ -from typing import Any, Dict, List, Union -from unittest.mock import patch - -from sqlmodel import create_engine - -from tests.conftest import get_testing_print_function, needs_py310 - - -def check_calls(calls: List[List[Union[str, Dict[str, Any]]]]): - assert calls[0] == ["Before interacting with the database"] - assert calls[1] == [ - "Hero 1:", - { - "id": None, - "name": "Deadpond", - "secret_name": "Dive Wilson", - "age": None, - }, - ] - assert calls[2] == [ - "Hero 2:", - { - "id": None, - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "age": None, - }, - ] - assert calls[3] == [ - "Hero 3:", - { - "id": None, - "name": "Rusty-Man", - "secret_name": "Tommy Sharp", - "age": 48, - }, - ] - assert calls[4] == ["After adding to the session"] - assert calls[5] == [ - "Hero 1:", - { - "id": None, - "name": "Deadpond", - "secret_name": "Dive Wilson", - "age": None, - }, - ] - assert calls[6] == [ - "Hero 2:", - { - "id": None, - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "age": None, - }, - ] - assert calls[7] == [ - "Hero 3:", - { - "id": None, - "name": "Rusty-Man", - "secret_name": "Tommy Sharp", - "age": 48, - }, - ] - assert calls[8] == ["After committing the session"] - assert calls[9] == ["Hero 1:", {}] - assert calls[10] == ["Hero 2:", {}] - assert calls[11] == ["Hero 3:", {}] - assert calls[12] == ["After committing the session, show IDs"] - assert calls[13] == ["Hero 1 ID:", 1] - assert calls[14] == ["Hero 2 ID:", 2] - assert calls[15] == ["Hero 3 ID:", 3] - assert calls[16] == ["After committing the session, show names"] - assert calls[17] == ["Hero 1 name:", "Deadpond"] - assert calls[18] == ["Hero 2 name:", "Spider-Boy"] - assert calls[19] == ["Hero 3 name:", "Rusty-Man"] - assert calls[20] == ["After refreshing the heroes"] - assert calls[21] == [ - "Hero 1:", - { - "id": 1, - "name": "Deadpond", - "secret_name": "Dive Wilson", - "age": None, - }, - ] - assert calls[22] == [ - "Hero 2:", - { - "id": 2, - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "age": None, - }, - ] - assert calls[23] == [ - "Hero 3:", - { - "id": 3, - "name": "Rusty-Man", - "secret_name": "Tommy Sharp", - "age": 48, - }, - ] - assert calls[24] == ["After the session closes"] - assert calls[21] == [ - "Hero 1:", - { - "id": 1, - "name": "Deadpond", - "secret_name": "Dive Wilson", - "age": None, - }, - ] - assert calls[22] == [ - "Hero 2:", - { - "id": 2, - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "age": None, - }, - ] - assert calls[23] == [ - "Hero 3:", - { - "id": 3, - "name": "Rusty-Man", - "secret_name": "Tommy Sharp", - "age": 48, - }, - ] - - -@needs_py310 -def test_tutorial_001(clear_sqlmodel): - from docs_src.tutorial.automatic_id_none_refresh import tutorial001_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - check_calls(calls) - - -@needs_py310 -def test_tutorial_002(clear_sqlmodel): - from docs_src.tutorial.automatic_id_none_refresh import tutorial002_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - check_calls(calls) diff --git a/tests/test_tutorial/test_automatic_id_none_refresh/test_tutorial001_tutorial002.py b/tests/test_tutorial/test_automatic_id_none_refresh/test_tutorial001_tutorial002.py index 399f431b49..ad7615ab2c 100644 --- a/tests/test_tutorial/test_automatic_id_none_refresh/test_tutorial001_tutorial002.py +++ b/tests/test_tutorial/test_automatic_id_none_refresh/test_tutorial001_tutorial002.py @@ -1,12 +1,14 @@ -from typing import Any, Dict, List, Union -from unittest.mock import patch +import importlib +from types import ModuleType +from typing import Any +import pytest from sqlmodel import create_engine -from tests.conftest import get_testing_print_function +from tests.conftest import PrintMock -def check_calls(calls: List[List[Union[str, Dict[str, Any]]]]): +def check_calls(calls: list[list[str | dict[str, Any]]]) -> None: assert calls[0] == ["Before interacting with the database"] assert calls[1] == [ "Hero 1:", @@ -133,29 +135,23 @@ def check_calls(calls: List[List[Union[str, Dict[str, Any]]]]): ] -def test_tutorial_001(clear_sqlmodel): - from docs_src.tutorial.automatic_id_none_refresh import tutorial001 as mod +@pytest.fixture( + name="module", + params=[ + pytest.param("tutorial001_py310"), + pytest.param("tutorial002_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + module = importlib.import_module( + f"docs_src.tutorial.automatic_id_none_refresh.{request.param}" + ) + module.sqlite_url = "sqlite://" + module.engine = create_engine(module.sqlite_url) - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] + return module - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() - check_calls(calls) - - -def test_tutorial_002(clear_sqlmodel): - from docs_src.tutorial.automatic_id_none_refresh import tutorial002 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - check_calls(calls) +def test_tutorial_001_tutorial_002(print_mock: PrintMock, module: ModuleType) -> None: + module.main() + check_calls(print_mock.calls) diff --git a/tests/test_tutorial/test_code_structure/test_tutorial001.py b/tests/test_tutorial/test_code_structure/test_tutorial001.py index c6e3158360..e4a4509aa5 100644 --- a/tests/test_tutorial/test_code_structure/test_tutorial001.py +++ b/tests/test_tutorial/test_code_structure/test_tutorial001.py @@ -1,8 +1,11 @@ -from unittest.mock import patch +import importlib +from dataclasses import dataclass +from types import ModuleType +import pytest from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from tests.conftest import PrintMock expected_calls = [ [ @@ -22,16 +25,32 @@ ] -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.code_structure.tutorial001 import app, database +@dataclass +class Modules: + app: ModuleType + database: ModuleType - database.sqlite_url = "sqlite://" - database.engine = create_engine(database.sqlite_url) - app.engine = database.engine - calls = [] - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - app.main() - assert calls == expected_calls +@pytest.fixture( + name="modules", + params=[ + pytest.param("tutorial001_py310"), + ], +) +def get_modules(request: pytest.FixtureRequest) -> Modules: + app_module = importlib.import_module( + f"docs_src.tutorial.code_structure.{request.param}.app" + ) + database_module = importlib.import_module( + f"docs_src.tutorial.code_structure.{request.param}.database" + ) + database_module.sqlite_url = "sqlite://" + database_module.engine = create_engine(database_module.sqlite_url) + app_module.engine = database_module.engine + + return Modules(app=app_module, database=database_module) + + +def test_tutorial(print_mock: PrintMock, modules: Modules): + modules.app.main() + assert print_mock.calls == expected_calls diff --git a/tests/test_tutorial/test_code_structure/test_tutorial001_py310.py b/tests/test_tutorial/test_code_structure/test_tutorial001_py310.py deleted file mode 100644 index 44d9d920fa..0000000000 --- a/tests/test_tutorial/test_code_structure/test_tutorial001_py310.py +++ /dev/null @@ -1,38 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - -expected_calls = [ - [ - "Created hero:", - { - "id": 1, - "name": "Deadpond", - "age": None, - "secret_name": "Dive Wilson", - "team_id": 1, - }, - ], - [ - "Hero's team:", - {"name": "Z-Force", "headquarters": "Sister Margaret's Bar", "id": 1}, - ], -] - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.code_structure.tutorial001_py310 import app, database - - database.sqlite_url = "sqlite://" - database.engine = create_engine(database.sqlite_url) - app.engine = database.engine - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - app.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_code_structure/test_tutorial001_py39.py b/tests/test_tutorial/test_code_structure/test_tutorial001_py39.py deleted file mode 100644 index b17917cff2..0000000000 --- a/tests/test_tutorial/test_code_structure/test_tutorial001_py39.py +++ /dev/null @@ -1,38 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py39 - -expected_calls = [ - [ - "Created hero:", - { - "id": 1, - "name": "Deadpond", - "age": None, - "secret_name": "Dive Wilson", - "team_id": 1, - }, - ], - [ - "Hero's team:", - {"name": "Z-Force", "headquarters": "Sister Margaret's Bar", "id": 1}, - ], -] - - -@needs_py39 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.code_structure.tutorial001_py39 import app, database - - database.sqlite_url = "sqlite://" - database.engine = create_engine(database.sqlite_url) - app.engine = database.engine - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - app.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_code_structure/test_tutorial002.py b/tests/test_tutorial/test_code_structure/test_tutorial002.py index 8e7ac8f173..cb3f049d48 100644 --- a/tests/test_tutorial/test_code_structure/test_tutorial002.py +++ b/tests/test_tutorial/test_code_structure/test_tutorial002.py @@ -1,8 +1,11 @@ -from unittest.mock import patch +import importlib +from dataclasses import dataclass +from types import ModuleType +import pytest from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock expected_calls = [ [ @@ -22,16 +25,32 @@ ] -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.code_structure.tutorial002 import app, database +@dataclass +class Modules: + app: ModuleType + database: ModuleType - database.sqlite_url = "sqlite://" - database.engine = create_engine(database.sqlite_url) - app.engine = database.engine - calls = [] - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - app.main() - assert calls == expected_calls +@pytest.fixture( + name="modules", + params=[ + pytest.param("tutorial002_py310"), + ], +) +def get_modules(request: pytest.FixtureRequest) -> Modules: + app_module = importlib.import_module( + f"docs_src.tutorial.code_structure.{request.param}.app" + ) + database_module = importlib.import_module( + f"docs_src.tutorial.code_structure.{request.param}.database" + ) + database_module.sqlite_url = "sqlite://" + database_module.engine = create_engine(database_module.sqlite_url) + app_module.engine = database_module.engine + + return Modules(app=app_module, database=database_module) + + +def test_tutorial(print_mock: PrintMock, modules: Modules): + modules.app.main() + assert print_mock.calls == expected_calls diff --git a/tests/test_tutorial/test_code_structure/test_tutorial002_py310.py b/tests/test_tutorial/test_code_structure/test_tutorial002_py310.py deleted file mode 100644 index 3eafdee831..0000000000 --- a/tests/test_tutorial/test_code_structure/test_tutorial002_py310.py +++ /dev/null @@ -1,38 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - -expected_calls = [ - [ - "Created hero:", - { - "id": 1, - "name": "Deadpond", - "age": None, - "secret_name": "Dive Wilson", - "team_id": 1, - }, - ], - [ - "Hero's team:", - {"name": "Z-Force", "headquarters": "Sister Margaret's Bar", "id": 1}, - ], -] - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.code_structure.tutorial002_py310 import app, database - - database.sqlite_url = "sqlite://" - database.engine = create_engine(database.sqlite_url) - app.engine = database.engine - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - app.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_code_structure/test_tutorial002_py39.py b/tests/test_tutorial/test_code_structure/test_tutorial002_py39.py deleted file mode 100644 index 9b5eb670c2..0000000000 --- a/tests/test_tutorial/test_code_structure/test_tutorial002_py39.py +++ /dev/null @@ -1,38 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py39 - -expected_calls = [ - [ - "Created hero:", - { - "id": 1, - "name": "Deadpond", - "age": None, - "secret_name": "Dive Wilson", - "team_id": 1, - }, - ], - [ - "Hero's team:", - {"name": "Z-Force", "headquarters": "Sister Margaret's Bar", "id": 1}, - ], -] - - -@needs_py39 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.code_structure.tutorial002_py39 import app, database - - database.sqlite_url = "sqlite://" - database.engine = create_engine(database.sqlite_url) - app.engine = database.engine - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - app.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_connect/test_create_connected_tables/test_tutorial001.py b/tests/test_tutorial/test_connect/test_create_connected_tables/test_tutorial001.py index e3e0799246..55e9bda9e9 100644 --- a/tests/test_tutorial/test_connect/test_create_connected_tables/test_tutorial001.py +++ b/tests/test_tutorial/test_connect/test_create_connected_tables/test_tutorial001.py @@ -1,14 +1,29 @@ +import importlib +from types import ModuleType + +import pytest from sqlalchemy import inspect from sqlalchemy.engine.reflection import Inspector from sqlmodel import create_engine -def test_tutorial001(clear_sqlmodel): - from docs_src.tutorial.connect.create_tables import tutorial001 as mod +@pytest.fixture( + name="module", + params=[ + pytest.param("tutorial001_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + module = importlib.import_module( + f"docs_src.tutorial.connect.create_tables.{request.param}" + ) + return module + - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - mod.main() - insp: Inspector = inspect(mod.engine) - assert insp.has_table(str(mod.Hero.__tablename__)) - assert insp.has_table(str(mod.Team.__tablename__)) +def test_tutorial001(module: ModuleType): + module.sqlite_url = "sqlite://" + module.engine = create_engine(module.sqlite_url) + module.main() + insp: Inspector = inspect(module.engine) + assert insp.has_table(str(module.Hero.__tablename__)) + assert insp.has_table(str(module.Team.__tablename__)) diff --git a/tests/test_tutorial/test_connect/test_create_connected_tables/test_tutorial001_py310.py b/tests/test_tutorial/test_connect/test_create_connected_tables/test_tutorial001_py310.py deleted file mode 100644 index ec2990ebfb..0000000000 --- a/tests/test_tutorial/test_connect/test_create_connected_tables/test_tutorial001_py310.py +++ /dev/null @@ -1,17 +0,0 @@ -from sqlalchemy import inspect -from sqlalchemy.engine.reflection import Inspector -from sqlmodel import create_engine - -from ....conftest import needs_py310 - - -@needs_py310 -def test_tutorial001(clear_sqlmodel): - from docs_src.tutorial.connect.create_tables import tutorial001_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - mod.main() - insp: Inspector = inspect(mod.engine) - assert insp.has_table(str(mod.Hero.__tablename__)) - assert insp.has_table(str(mod.Team.__tablename__)) diff --git a/tests/test_tutorial/test_connect/test_delete/test_tutorial001.py b/tests/test_tutorial/test_connect/test_delete/test_tutorial001.py index a5db3867e4..11e3ceeb01 100644 --- a/tests/test_tutorial/test_connect/test_delete/test_tutorial001.py +++ b/tests/test_tutorial/test_connect/test_delete/test_tutorial001.py @@ -1,8 +1,10 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ....conftest import get_testing_print_function +from ....conftest import PrintMock expected_calls = [ [ @@ -58,15 +60,21 @@ ] -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.connect.delete import tutorial001 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] +@pytest.fixture( + name="module", + params=[ + pytest.param("tutorial001_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + module = importlib.import_module( + f"docs_src.tutorial.connect.delete.{request.param}" + ) + module.sqlite_url = "sqlite://" + module.engine = create_engine(module.sqlite_url) + return module - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls +def test_tutorial(print_mock: PrintMock, module: ModuleType): + module.main() + assert print_mock.calls == expected_calls diff --git a/tests/test_tutorial/test_connect/test_delete/test_tutorial001_py310.py b/tests/test_tutorial/test_connect/test_delete/test_tutorial001_py310.py deleted file mode 100644 index edc70b8a3d..0000000000 --- a/tests/test_tutorial/test_connect/test_delete/test_tutorial001_py310.py +++ /dev/null @@ -1,73 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ....conftest import get_testing_print_function, needs_py310 - -expected_calls = [ - [ - "Created hero:", - { - "age": None, - "id": 1, - "secret_name": "Dive Wilson", - "team_id": 2, - "name": "Deadpond", - }, - ], - [ - "Created hero:", - { - "age": 48, - "id": 2, - "secret_name": "Tommy Sharp", - "team_id": 1, - "name": "Rusty-Man", - }, - ], - [ - "Created hero:", - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": None, - "name": "Spider-Boy", - }, - ], - [ - "Updated hero:", - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": 1, - "name": "Spider-Boy", - }, - ], - [ - "No longer Preventer:", - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": None, - "name": "Spider-Boy", - }, - ], -] - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.connect.delete import tutorial001_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_connect/test_insert/test_tutorial001.py b/tests/test_tutorial/test_connect/test_insert/test_tutorial001.py index 8c8a303a21..675011554f 100644 --- a/tests/test_tutorial/test_connect/test_insert/test_tutorial001.py +++ b/tests/test_tutorial/test_connect/test_insert/test_tutorial001.py @@ -1,8 +1,10 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ....conftest import get_testing_print_function +from ....conftest import PrintMock expected_calls = [ [ @@ -38,15 +40,21 @@ ] -def test_tutorial001(clear_sqlmodel): - from docs_src.tutorial.connect.insert import tutorial001 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls +@pytest.fixture( + name="module", + params=[ + pytest.param("tutorial001_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + module = importlib.import_module( + f"docs_src.tutorial.connect.insert.{request.param}" + ) + module.sqlite_url = "sqlite://" + module.engine = create_engine(module.sqlite_url) + return module + + +def test_tutorial001(print_mock: PrintMock, module: ModuleType): + module.main() + assert print_mock.calls == expected_calls diff --git a/tests/test_tutorial/test_connect/test_insert/test_tutorial001_py310.py b/tests/test_tutorial/test_connect/test_insert/test_tutorial001_py310.py deleted file mode 100644 index 854c0068ab..0000000000 --- a/tests/test_tutorial/test_connect/test_insert/test_tutorial001_py310.py +++ /dev/null @@ -1,53 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ....conftest import get_testing_print_function, needs_py310 - -expected_calls = [ - [ - "Created hero:", - { - "age": None, - "id": 1, - "secret_name": "Dive Wilson", - "team_id": 2, - "name": "Deadpond", - }, - ], - [ - "Created hero:", - { - "age": 48, - "id": 2, - "secret_name": "Tommy Sharp", - "team_id": 1, - "name": "Rusty-Man", - }, - ], - [ - "Created hero:", - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": None, - "name": "Spider-Boy", - }, - ], -] - - -@needs_py310 -def test_tutorial001(clear_sqlmodel): - from docs_src.tutorial.connect.insert import tutorial001_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_connect/test_select/test_tutorial001_py310_tutorial002_py310.py b/tests/test_tutorial/test_connect/test_select/test_tutorial001_py310_tutorial002_py310.py deleted file mode 100644 index d3bab7f669..0000000000 --- a/tests/test_tutorial/test_connect/test_select/test_tutorial001_py310_tutorial002_py310.py +++ /dev/null @@ -1,92 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ....conftest import get_testing_print_function, needs_py310 - -expected_calls = [ - [ - "Created hero:", - { - "age": None, - "id": 1, - "secret_name": "Dive Wilson", - "team_id": 2, - "name": "Deadpond", - }, - ], - [ - "Created hero:", - { - "age": 48, - "id": 2, - "secret_name": "Tommy Sharp", - "team_id": 1, - "name": "Rusty-Man", - }, - ], - [ - "Created hero:", - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": None, - "name": "Spider-Boy", - }, - ], - [ - "Hero:", - { - "age": None, - "id": 1, - "secret_name": "Dive Wilson", - "team_id": 2, - "name": "Deadpond", - }, - "Team:", - {"id": 2, "name": "Z-Force", "headquarters": "Sister Margaret's Bar"}, - ], - [ - "Hero:", - { - "age": 48, - "id": 2, - "secret_name": "Tommy Sharp", - "team_id": 1, - "name": "Rusty-Man", - }, - "Team:", - {"id": 1, "name": "Preventers", "headquarters": "Sharp Tower"}, - ], -] - - -@needs_py310 -def test_tutorial001(clear_sqlmodel): - from docs_src.tutorial.connect.select import tutorial001_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls - - -@needs_py310 -def test_tutorial002(clear_sqlmodel): - from docs_src.tutorial.connect.select import tutorial002_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_connect/test_select/test_tutorial001_tutorial002.py b/tests/test_tutorial/test_connect/test_select/test_tutorial001_tutorial002.py index 541a8ee00f..1a8d07ccd7 100644 --- a/tests/test_tutorial/test_connect/test_select/test_tutorial001_tutorial002.py +++ b/tests/test_tutorial/test_connect/test_select/test_tutorial001_tutorial002.py @@ -1,8 +1,10 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ....conftest import get_testing_print_function +from ....conftest import PrintMock expected_calls = [ [ @@ -62,29 +64,35 @@ ] -def test_tutorial001(clear_sqlmodel): - from docs_src.tutorial.connect.select import tutorial001 as mod +@pytest.fixture(name="module") +def get_module(request: pytest.FixtureRequest) -> ModuleType: + module = importlib.import_module( + f"docs_src.tutorial.connect.select.{request.param}" + ) + module.sqlite_url = "sqlite://" + module.engine = create_engine(module.sqlite_url) + return module - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls - - -def test_tutorial002(clear_sqlmodel): - from docs_src.tutorial.connect.select import tutorial002 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] +@pytest.mark.parametrize( + "module", + [ + pytest.param("tutorial001_py310"), + ], + indirect=True, +) +def test_tutorial001(print_mock: PrintMock, module: ModuleType): + module.main() + assert print_mock.calls == expected_calls - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls +@pytest.mark.parametrize( + "module", + [ + pytest.param("tutorial002_py310"), + ], + indirect=True, +) +def test_tutorial002(print_mock: PrintMock, module: ModuleType): + module.main() + assert print_mock.calls == expected_calls diff --git a/tests/test_tutorial/test_connect/test_select/test_tutorial003.py b/tests/test_tutorial/test_connect/test_select/test_tutorial003.py index 2eab135add..0442617f39 100644 --- a/tests/test_tutorial/test_connect/test_select/test_tutorial003.py +++ b/tests/test_tutorial/test_connect/test_select/test_tutorial003.py @@ -1,8 +1,10 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ....conftest import get_testing_print_function +from ....conftest import PrintMock expected_calls = [ [ @@ -74,15 +76,21 @@ ] -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.connect.select import tutorial003 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] +@pytest.fixture( + name="module", + params=[ + pytest.param("tutorial003_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + module = importlib.import_module( + f"docs_src.tutorial.connect.select.{request.param}" + ) + module.sqlite_url = "sqlite://" + module.engine = create_engine(module.sqlite_url) + return module - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls +def test_tutorial(print_mock: PrintMock, module: ModuleType): + module.main() + assert print_mock.calls == expected_calls diff --git a/tests/test_tutorial/test_connect/test_select/test_tutorial003_py310.py b/tests/test_tutorial/test_connect/test_select/test_tutorial003_py310.py deleted file mode 100644 index 5b710c4358..0000000000 --- a/tests/test_tutorial/test_connect/test_select/test_tutorial003_py310.py +++ /dev/null @@ -1,89 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ....conftest import get_testing_print_function, needs_py310 - -expected_calls = [ - [ - "Created hero:", - { - "age": None, - "id": 1, - "secret_name": "Dive Wilson", - "team_id": 2, - "name": "Deadpond", - }, - ], - [ - "Created hero:", - { - "age": 48, - "id": 2, - "secret_name": "Tommy Sharp", - "team_id": 1, - "name": "Rusty-Man", - }, - ], - [ - "Created hero:", - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": None, - "name": "Spider-Boy", - }, - ], - [ - "Hero:", - { - "age": None, - "id": 1, - "secret_name": "Dive Wilson", - "team_id": 2, - "name": "Deadpond", - }, - "Team:", - {"id": 2, "name": "Z-Force", "headquarters": "Sister Margaret's Bar"}, - ], - [ - "Hero:", - { - "age": 48, - "id": 2, - "secret_name": "Tommy Sharp", - "team_id": 1, - "name": "Rusty-Man", - }, - "Team:", - {"id": 1, "name": "Preventers", "headquarters": "Sharp Tower"}, - ], - [ - "Hero:", - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": None, - "name": "Spider-Boy", - }, - "Team:", - None, - ], -] - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.connect.select import tutorial003_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_connect/test_select/test_tutorial004.py b/tests/test_tutorial/test_connect/test_select/test_tutorial004.py index ebc273feb1..be41b98dcc 100644 --- a/tests/test_tutorial/test_connect/test_select/test_tutorial004.py +++ b/tests/test_tutorial/test_connect/test_select/test_tutorial004.py @@ -1,8 +1,10 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ....conftest import get_testing_print_function +from ....conftest import PrintMock expected_calls = [ [ @@ -48,15 +50,21 @@ ] -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.connect.select import tutorial004 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] +@pytest.fixture( + name="module", + params=[ + pytest.param("tutorial004_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + module = importlib.import_module( + f"docs_src.tutorial.connect.select.{request.param}" + ) + module.sqlite_url = "sqlite://" + module.engine = create_engine(module.sqlite_url) + return module - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls +def test_tutorial(print_mock: PrintMock, module: ModuleType): + module.main() + assert print_mock.calls == expected_calls diff --git a/tests/test_tutorial/test_connect/test_select/test_tutorial004_py310.py b/tests/test_tutorial/test_connect/test_select/test_tutorial004_py310.py deleted file mode 100644 index 72974ec6cf..0000000000 --- a/tests/test_tutorial/test_connect/test_select/test_tutorial004_py310.py +++ /dev/null @@ -1,63 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ....conftest import get_testing_print_function, needs_py310 - -expected_calls = [ - [ - "Created hero:", - { - "age": None, - "id": 1, - "secret_name": "Dive Wilson", - "team_id": 2, - "name": "Deadpond", - }, - ], - [ - "Created hero:", - { - "age": 48, - "id": 2, - "secret_name": "Tommy Sharp", - "team_id": 1, - "name": "Rusty-Man", - }, - ], - [ - "Created hero:", - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": None, - "name": "Spider-Boy", - }, - ], - [ - "Preventer Hero:", - { - "age": 48, - "id": 2, - "secret_name": "Tommy Sharp", - "team_id": 1, - "name": "Rusty-Man", - }, - ], -] - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.connect.select import tutorial004_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_connect/test_select/test_tutorial005.py b/tests/test_tutorial/test_connect/test_select/test_tutorial005.py index 400c6483cb..3e938f6378 100644 --- a/tests/test_tutorial/test_connect/test_select/test_tutorial005.py +++ b/tests/test_tutorial/test_connect/test_select/test_tutorial005.py @@ -1,8 +1,10 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ....conftest import get_testing_print_function +from ....conftest import PrintMock expected_calls = [ [ @@ -50,15 +52,21 @@ ] -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.connect.select import tutorial005 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] +@pytest.fixture( + name="module", + params=[ + pytest.param("tutorial005_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + module = importlib.import_module( + f"docs_src.tutorial.connect.select.{request.param}" + ) + module.sqlite_url = "sqlite://" + module.engine = create_engine(module.sqlite_url) + return module - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls +def test_tutorial(print_mock: PrintMock, module: ModuleType): + module.main() + assert print_mock.calls == expected_calls diff --git a/tests/test_tutorial/test_connect/test_select/test_tutorial005_py310.py b/tests/test_tutorial/test_connect/test_select/test_tutorial005_py310.py deleted file mode 100644 index a7332c18a7..0000000000 --- a/tests/test_tutorial/test_connect/test_select/test_tutorial005_py310.py +++ /dev/null @@ -1,65 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ....conftest import get_testing_print_function, needs_py310 - -expected_calls = [ - [ - "Created hero:", - { - "age": None, - "id": 1, - "secret_name": "Dive Wilson", - "team_id": 2, - "name": "Deadpond", - }, - ], - [ - "Created hero:", - { - "age": 48, - "id": 2, - "secret_name": "Tommy Sharp", - "team_id": 1, - "name": "Rusty-Man", - }, - ], - [ - "Created hero:", - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": None, - "name": "Spider-Boy", - }, - ], - [ - "Preventer Hero:", - { - "age": 48, - "id": 2, - "secret_name": "Tommy Sharp", - "team_id": 1, - "name": "Rusty-Man", - }, - "Team:", - {"id": 1, "name": "Preventers", "headquarters": "Sharp Tower"}, - ], -] - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.connect.select import tutorial005_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_connect/test_update/test_tutorial001.py b/tests/test_tutorial/test_connect/test_update/test_tutorial001.py index d6875946c1..5e8003b072 100644 --- a/tests/test_tutorial/test_connect/test_update/test_tutorial001.py +++ b/tests/test_tutorial/test_connect/test_update/test_tutorial001.py @@ -1,8 +1,10 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ....conftest import get_testing_print_function +from ....conftest import PrintMock expected_calls = [ [ @@ -48,15 +50,21 @@ ] -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.connect.update import tutorial001 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] +@pytest.fixture( + name="module", + params=[ + pytest.param("tutorial001_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + module = importlib.import_module( + f"docs_src.tutorial.connect.update.{request.param}" + ) + module.sqlite_url = "sqlite://" + module.engine = create_engine(module.sqlite_url) + return module - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls +def test_tutorial(print_mock: PrintMock, module: ModuleType): + module.main() + assert print_mock.calls == expected_calls diff --git a/tests/test_tutorial/test_connect/test_update/test_tutorial001_py310.py b/tests/test_tutorial/test_connect/test_update/test_tutorial001_py310.py deleted file mode 100644 index f3702654c2..0000000000 --- a/tests/test_tutorial/test_connect/test_update/test_tutorial001_py310.py +++ /dev/null @@ -1,63 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ....conftest import get_testing_print_function, needs_py310 - -expected_calls = [ - [ - "Created hero:", - { - "age": None, - "id": 1, - "secret_name": "Dive Wilson", - "team_id": 2, - "name": "Deadpond", - }, - ], - [ - "Created hero:", - { - "age": 48, - "id": 2, - "secret_name": "Tommy Sharp", - "team_id": 1, - "name": "Rusty-Man", - }, - ], - [ - "Created hero:", - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": None, - "name": "Spider-Boy", - }, - ], - [ - "Updated hero:", - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": 1, - "name": "Spider-Boy", - }, - ], -] - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.connect.update import tutorial001_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_create_db_and_table/test_tutorial001.py b/tests/test_tutorial/test_create_db_and_table/test_tutorial001.py index b6a2e72628..336ddc8f75 100644 --- a/tests/test_tutorial/test_create_db_and_table/test_tutorial001.py +++ b/tests/test_tutorial/test_create_db_and_table/test_tutorial001.py @@ -1,10 +1,18 @@ from pathlib import Path +import pytest + from ...conftest import coverage_run -def test_create_db_and_table(cov_tmp_path: Path): - module = "docs_src.tutorial.create_db_and_table.tutorial001" +@pytest.mark.parametrize( + "module_name", + [ + pytest.param("tutorial001_py310"), + ], +) +def test_create_db_and_table(cov_tmp_path: Path, module_name: str): + module = f"docs_src.tutorial.create_db_and_table.{module_name}" result = coverage_run(module=module, cwd=cov_tmp_path) assert "BEGIN" in result.stdout assert 'PRAGMA main.table_info("hero")' in result.stdout diff --git a/tests/test_tutorial/test_create_db_and_table/test_tutorial001_py310.py b/tests/test_tutorial/test_create_db_and_table/test_tutorial001_py310.py deleted file mode 100644 index 465b9f9d58..0000000000 --- a/tests/test_tutorial/test_create_db_and_table/test_tutorial001_py310.py +++ /dev/null @@ -1,19 +0,0 @@ -from pathlib import Path - -from ...conftest import coverage_run, needs_py310 - - -@needs_py310 -def test_create_db_and_table(cov_tmp_path: Path): - module = "docs_src.tutorial.create_db_and_table.tutorial001_py310" - result = coverage_run(module=module, cwd=cov_tmp_path) - assert "BEGIN" in result.stdout - assert 'PRAGMA main.table_info("hero")' in result.stdout - assert "CREATE TABLE hero (" in result.stdout - assert "id INTEGER NOT NULL," in result.stdout - assert "name VARCHAR NOT NULL," in result.stdout - assert "secret_name VARCHAR NOT NULL," in result.stdout - assert "age INTEGER," in result.stdout - assert "PRIMARY KEY (id)" in result.stdout - assert ")" in result.stdout - assert "COMMIT" in result.stdout diff --git a/tests/test_tutorial/test_create_db_and_table/test_tutorial002.py b/tests/test_tutorial/test_create_db_and_table/test_tutorial002.py index 3a24ae1609..0253b7becc 100644 --- a/tests/test_tutorial/test_create_db_and_table/test_tutorial002.py +++ b/tests/test_tutorial/test_create_db_and_table/test_tutorial002.py @@ -1,13 +1,28 @@ +import importlib +from types import ModuleType + +import pytest from sqlalchemy import inspect from sqlalchemy.engine.reflection import Inspector from sqlmodel import create_engine -def test_create_db_and_table(clear_sqlmodel): - from docs_src.tutorial.create_db_and_table import tutorial002 as mod +@pytest.fixture( + name="module", + params=[ + pytest.param("tutorial002_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + module = importlib.import_module( + f"docs_src.tutorial.create_db_and_table.{request.param}" + ) + module.sqlite_url = "sqlite://" + module.engine = create_engine(module.sqlite_url) + return module + - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - mod.create_db_and_tables() - insp: Inspector = inspect(mod.engine) - assert insp.has_table(str(mod.Hero.__tablename__)) +def test_create_db_and_table(module: ModuleType): + module.create_db_and_tables() + insp: Inspector = inspect(module.engine) + assert insp.has_table(str(module.Hero.__tablename__)) diff --git a/tests/test_tutorial/test_create_db_and_table/test_tutorial002_py310.py b/tests/test_tutorial/test_create_db_and_table/test_tutorial002_py310.py deleted file mode 100644 index 3ca3186b9e..0000000000 --- a/tests/test_tutorial/test_create_db_and_table/test_tutorial002_py310.py +++ /dev/null @@ -1,16 +0,0 @@ -from sqlalchemy import inspect -from sqlalchemy.engine.reflection import Inspector -from sqlmodel import create_engine - -from ...conftest import needs_py310 - - -@needs_py310 -def test_create_db_and_table(clear_sqlmodel): - from docs_src.tutorial.create_db_and_table import tutorial002_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - mod.create_db_and_tables() - insp: Inspector = inspect(mod.engine) - assert insp.has_table(str(mod.Hero.__tablename__)) diff --git a/tests/test_tutorial/test_create_db_and_table/test_tutorial003.py b/tests/test_tutorial/test_create_db_and_table/test_tutorial003.py index e5c55c70f3..51c82b42ac 100644 --- a/tests/test_tutorial/test_create_db_and_table/test_tutorial003.py +++ b/tests/test_tutorial/test_create_db_and_table/test_tutorial003.py @@ -1,13 +1,28 @@ +import importlib +from types import ModuleType + +import pytest from sqlalchemy import inspect from sqlalchemy.engine.reflection import Inspector from sqlmodel import create_engine -def test_create_db_and_table(clear_sqlmodel): - from docs_src.tutorial.create_db_and_table import tutorial003 as mod +@pytest.fixture( + name="module", + params=[ + pytest.param("tutorial003_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + module = importlib.import_module( + f"docs_src.tutorial.create_db_and_table.{request.param}" + ) + module.sqlite_url = "sqlite://" + module.engine = create_engine(module.sqlite_url) + return module + - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - mod.create_db_and_tables() - insp: Inspector = inspect(mod.engine) - assert insp.has_table(str(mod.Hero.__tablename__)) +def test_create_db_and_table(module: ModuleType): + module.create_db_and_tables() + insp: Inspector = inspect(module.engine) + assert insp.has_table(str(module.Hero.__tablename__)) diff --git a/tests/test_tutorial/test_create_db_and_table/test_tutorial003_py310.py b/tests/test_tutorial/test_create_db_and_table/test_tutorial003_py310.py deleted file mode 100644 index a1806ce250..0000000000 --- a/tests/test_tutorial/test_create_db_and_table/test_tutorial003_py310.py +++ /dev/null @@ -1,16 +0,0 @@ -from sqlalchemy import inspect -from sqlalchemy.engine.reflection import Inspector -from sqlmodel import create_engine - -from ...conftest import needs_py310 - - -@needs_py310 -def test_create_db_and_table(clear_sqlmodel): - from docs_src.tutorial.create_db_and_table import tutorial003_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - mod.create_db_and_tables() - insp: Inspector = inspect(mod.engine) - assert insp.has_table(str(mod.Hero.__tablename__)) diff --git a/tests/test_tutorial/test_delete/test_tutorial001_py310_tutorial002_py310.py b/tests/test_tutorial/test_delete/test_tutorial001_py310_tutorial002_py310.py deleted file mode 100644 index 0f97e7489f..0000000000 --- a/tests/test_tutorial/test_delete/test_tutorial001_py310_tutorial002_py310.py +++ /dev/null @@ -1,88 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - -expected_calls = [ - [ - "Hero 1:", - {"id": 2, "name": "Spider-Boy", "secret_name": "Pedro Parqueador", "age": None}, - ], - [ - "Hero 2:", - { - "id": 7, - "name": "Captain North America", - "secret_name": "Esteban Rogelios", - "age": 93, - }, - ], - [ - "Updated hero 1:", - { - "id": 2, - "name": "Spider-Youngster", - "secret_name": "Pedro Parqueador", - "age": 16, - }, - ], - [ - "Updated hero 2:", - { - "id": 7, - "name": "Captain North America Except Canada", - "secret_name": "Esteban Rogelios", - "age": 110, - }, - ], - [ - "Hero: ", - { - "id": 2, - "name": "Spider-Youngster", - "secret_name": "Pedro Parqueador", - "age": 16, - }, - ], - [ - "Deleted hero:", - { - "id": 2, - "name": "Spider-Youngster", - "secret_name": "Pedro Parqueador", - "age": 16, - }, - ], - ["There's no hero named Spider-Youngster"], -] - - -@needs_py310 -def test_tutorial001(clear_sqlmodel): - from docs_src.tutorial.delete import tutorial001_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls - - -@needs_py310 -def test_tutorial002(clear_sqlmodel): - from docs_src.tutorial.delete import tutorial002_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_delete/test_tutorial001_tutorial002.py b/tests/test_tutorial/test_delete/test_tutorial001_tutorial002.py index 1d6497b327..a8ad8a167d 100644 --- a/tests/test_tutorial/test_delete/test_tutorial001_tutorial002.py +++ b/tests/test_tutorial/test_delete/test_tutorial001_tutorial002.py @@ -1,8 +1,10 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock expected_calls = [ [ @@ -58,29 +60,33 @@ ] -def test_tutorial001(clear_sqlmodel): - from docs_src.tutorial.delete import tutorial001 as mod +@pytest.fixture(name="module") +def get_module(request: pytest.FixtureRequest) -> ModuleType: + module = importlib.import_module(f"docs_src.tutorial.delete.{request.param}") + module.sqlite_url = "sqlite://" + module.engine = create_engine(module.sqlite_url) + return module - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls - - -def test_tutorial002(clear_sqlmodel): - from docs_src.tutorial.delete import tutorial002 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] +@pytest.mark.parametrize( + "module", + [ + pytest.param("tutorial001_py310"), + ], + indirect=True, +) +def test_tutorial001(print_mock: PrintMock, module: ModuleType): + module.main() + assert print_mock.calls == expected_calls - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls +@pytest.mark.parametrize( + "module", + [ + pytest.param("tutorial002_py310"), + ], + indirect=True, +) +def test_tutorial002(print_mock: PrintMock, module: ModuleType): + module.main() + assert print_mock.calls == expected_calls diff --git a/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_py310_tests_main.py b/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_py310_tests_main.py deleted file mode 100644 index 781de7c772..0000000000 --- a/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_py310_tests_main.py +++ /dev/null @@ -1,25 +0,0 @@ -import subprocess -from pathlib import Path - -from ....conftest import needs_py310 - - -@needs_py310 -def test_run_tests(clear_sqlmodel): - from docs_src.tutorial.fastapi.app_testing.tutorial001_py310 import test_main as mod - - test_path = Path(mod.__file__).resolve().parent - top_level_path = Path(__file__).resolve().parent.parent.parent.parent.parent - result = subprocess.run( - [ - "coverage", - "run", - "--parallel-mode", - "-m", - "pytest", - test_path, - ], - cwd=top_level_path, - capture_output=True, - ) - assert result.returncode == 0, result.stdout.decode("utf-8") diff --git a/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_py39_tests_main.py b/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_py39_tests_main.py deleted file mode 100644 index 6dbcc80d56..0000000000 --- a/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_py39_tests_main.py +++ /dev/null @@ -1,25 +0,0 @@ -import subprocess -from pathlib import Path - -from ....conftest import needs_py39 - - -@needs_py39 -def test_run_tests(clear_sqlmodel): - from docs_src.tutorial.fastapi.app_testing.tutorial001_py39 import test_main as mod - - test_path = Path(mod.__file__).resolve().parent - top_level_path = Path(__file__).resolve().parent.parent.parent.parent.parent - result = subprocess.run( - [ - "coverage", - "run", - "--parallel-mode", - "-m", - "pytest", - test_path, - ], - cwd=top_level_path, - capture_output=True, - ) - assert result.returncode == 0, result.stdout.decode("utf-8") diff --git a/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests001.py b/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests001.py index 4da11c2121..5e31c012ac 100644 --- a/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests001.py +++ b/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests001.py @@ -1,18 +1,40 @@ import importlib +import sys +from dataclasses import dataclass +from types import ModuleType import pytest -from docs_src.tutorial.fastapi.app_testing.tutorial001 import main as app_mod -from docs_src.tutorial.fastapi.app_testing.tutorial001 import test_main_001 as test_mod +@dataclass +class Modules: + app: ModuleType + test: ModuleType -@pytest.fixture(name="prepare", autouse=True) -def prepare_fixture(clear_sqlmodel): + +@pytest.fixture( + name="modules_path", + params=[ + pytest.param("tutorial001_py310"), + ], +) +def get_modules_path(request: pytest.FixtureRequest) -> str: + return f"docs_src.tutorial.fastapi.app_testing.{request.param}" + + +@pytest.fixture(name="modules") +def load_modules(clear_sqlmodel, modules_path: str) -> Modules: # Trigger side effects of registering table models in SQLModel # This has to be called after clear_sqlmodel - importlib.reload(app_mod) - importlib.reload(test_mod) + app_mod_path = f"{modules_path}.main" + if app_mod_path in sys.modules: + app_mod = sys.modules[app_mod_path] + importlib.reload(app_mod) + else: + app_mod = importlib.import_module(app_mod_path) + test_mod = importlib.import_module(f"{modules_path}.test_main_001") + return Modules(app=app_mod, test=test_mod) -def test_tutorial(): - test_mod.test_create_hero() +def test_tutorial(modules: Modules): + modules.test.test_create_hero() diff --git a/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests002.py b/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests002.py index 241e92323b..43114fa3a9 100644 --- a/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests002.py +++ b/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests002.py @@ -1,18 +1,40 @@ import importlib +import sys +from dataclasses import dataclass +from types import ModuleType import pytest -from docs_src.tutorial.fastapi.app_testing.tutorial001 import main as app_mod -from docs_src.tutorial.fastapi.app_testing.tutorial001 import test_main_002 as test_mod +@dataclass +class Modules: + app: ModuleType + test: ModuleType -@pytest.fixture(name="prepare", autouse=True) -def prepare_fixture(clear_sqlmodel): + +@pytest.fixture( + name="modules_path", + params=[ + pytest.param("tutorial001_py310"), + ], +) +def get_modules_path(request: pytest.FixtureRequest) -> str: + return f"docs_src.tutorial.fastapi.app_testing.{request.param}" + + +@pytest.fixture(name="modules") +def load_modules(clear_sqlmodel, modules_path: str) -> Modules: # Trigger side effects of registering table models in SQLModel # This has to be called after clear_sqlmodel - importlib.reload(app_mod) - importlib.reload(test_mod) + app_mod_path = f"{modules_path}.main" + if app_mod_path in sys.modules: + app_mod = sys.modules[app_mod_path] + importlib.reload(app_mod) + else: + app_mod = importlib.import_module(app_mod_path) # pragma: no cover + test_mod = importlib.import_module(f"{modules_path}.test_main_002") + return Modules(app=app_mod, test=test_mod) -def test_tutorial(): - test_mod.test_create_hero() +def test_tutorial(modules: Modules): + modules.test.test_create_hero() diff --git a/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests003.py b/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests003.py index 32e0161bad..79423eeae4 100644 --- a/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests003.py +++ b/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests003.py @@ -1,18 +1,40 @@ import importlib +import sys +from dataclasses import dataclass +from types import ModuleType import pytest -from docs_src.tutorial.fastapi.app_testing.tutorial001 import main as app_mod -from docs_src.tutorial.fastapi.app_testing.tutorial001 import test_main_003 as test_mod +@dataclass +class Modules: + app: ModuleType + test: ModuleType -@pytest.fixture(name="prepare", autouse=True) -def prepare_fixture(clear_sqlmodel): + +@pytest.fixture( + name="modules_path", + params=[ + pytest.param("tutorial001_py310"), + ], +) +def get_modules_path(request: pytest.FixtureRequest) -> str: + return f"docs_src.tutorial.fastapi.app_testing.{request.param}" + + +@pytest.fixture(name="modules") +def load_modules(clear_sqlmodel, modules_path: str) -> Modules: # Trigger side effects of registering table models in SQLModel # This has to be called after clear_sqlmodel - importlib.reload(app_mod) - importlib.reload(test_mod) + app_mod_path = f"{modules_path}.main" + if app_mod_path in sys.modules: + app_mod = sys.modules[app_mod_path] + importlib.reload(app_mod) + else: + app_mod = importlib.import_module(app_mod_path) # pragma: no cover + test_mod = importlib.import_module(f"{modules_path}.test_main_003") + return Modules(app=app_mod, test=test_mod) -def test_tutorial(): - test_mod.test_create_hero() +def test_tutorial(modules: Modules): + modules.test.test_create_hero() diff --git a/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests004.py b/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests004.py index c6402b2429..3e1e262ca5 100644 --- a/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests004.py +++ b/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests004.py @@ -1,18 +1,40 @@ import importlib +import sys +from dataclasses import dataclass +from types import ModuleType import pytest -from docs_src.tutorial.fastapi.app_testing.tutorial001 import main as app_mod -from docs_src.tutorial.fastapi.app_testing.tutorial001 import test_main_004 as test_mod +@dataclass +class Modules: + app: ModuleType + test: ModuleType -@pytest.fixture(name="prepare", autouse=True) -def prepare_fixture(clear_sqlmodel): + +@pytest.fixture( + name="modules_path", + params=[ + pytest.param("tutorial001_py310"), + ], +) +def get_modules_path(request: pytest.FixtureRequest) -> str: + return f"docs_src.tutorial.fastapi.app_testing.{request.param}" + + +@pytest.fixture(name="modules") +def load_modules(clear_sqlmodel, modules_path: str) -> Modules: # Trigger side effects of registering table models in SQLModel # This has to be called after clear_sqlmodel - importlib.reload(app_mod) - importlib.reload(test_mod) + app_mod_path = f"{modules_path}.main" + if app_mod_path in sys.modules: + app_mod = sys.modules[app_mod_path] + importlib.reload(app_mod) + else: + app_mod = importlib.import_module(app_mod_path) # pragma: no cover + test_mod = importlib.import_module(f"{modules_path}.test_main_004") + return Modules(app=app_mod, test=test_mod) -def test_tutorial(): - test_mod.test_create_hero() +def test_tutorial(modules: Modules): + modules.test.test_create_hero() diff --git a/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests005.py b/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests005.py index cc550c4008..b8467ee447 100644 --- a/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests005.py +++ b/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests005.py @@ -3,9 +3,11 @@ import pytest from sqlmodel import Session -from docs_src.tutorial.fastapi.app_testing.tutorial001 import main as app_mod -from docs_src.tutorial.fastapi.app_testing.tutorial001 import test_main_005 as test_mod -from docs_src.tutorial.fastapi.app_testing.tutorial001.test_main_005 import ( +from docs_src.tutorial.fastapi.app_testing.tutorial001_py310 import main as app_mod +from docs_src.tutorial.fastapi.app_testing.tutorial001_py310 import ( + test_main_005 as test_mod, +) +from docs_src.tutorial.fastapi.app_testing.tutorial001_py310.test_main_005 import ( session_fixture, ) diff --git a/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests006.py b/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests006.py index 67c9ac6ad4..ae8e702153 100644 --- a/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests006.py +++ b/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests006.py @@ -4,9 +4,11 @@ from fastapi.testclient import TestClient from sqlmodel import Session -from docs_src.tutorial.fastapi.app_testing.tutorial001 import main as app_mod -from docs_src.tutorial.fastapi.app_testing.tutorial001 import test_main_006 as test_mod -from docs_src.tutorial.fastapi.app_testing.tutorial001.test_main_006 import ( +from docs_src.tutorial.fastapi.app_testing.tutorial001_py310 import main as app_mod +from docs_src.tutorial.fastapi.app_testing.tutorial001_py310 import ( + test_main_006 as test_mod, +) +from docs_src.tutorial.fastapi.app_testing.tutorial001_py310.test_main_006 import ( client_fixture, session_fixture, ) diff --git a/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests_main.py b/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests_main.py index d7c1329b38..56650ab4ce 100644 --- a/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests_main.py +++ b/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests_main.py @@ -1,11 +1,26 @@ +import importlib import subprocess from pathlib import Path +from types import ModuleType +import pytest -def test_run_tests(clear_sqlmodel): - from docs_src.tutorial.fastapi.app_testing.tutorial001 import test_main as mod - test_path = Path(mod.__file__).resolve().parent +@pytest.fixture( + name="module", + params=[ + pytest.param("tutorial001_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + module = importlib.import_module( + f"docs_src.tutorial.fastapi.app_testing.{request.param}.test_main" + ) + return module + + +def test_run_tests(module: ModuleType): + test_path = Path(module.__file__).resolve().parent top_level_path = Path(__file__).resolve().parent.parent.parent.parent.parent result = subprocess.run( [ diff --git a/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001.py index f293199b40..343a32ede0 100644 --- a/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001.py @@ -1,18 +1,30 @@ -from dirty_equals import IsDict +import importlib +from types import ModuleType + +import pytest +from dirty_equals import IsOneOf from fastapi.testclient import TestClient from sqlmodel import create_engine from sqlmodel.pool import StaticPool -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.delete import tutorial001 as mod - +@pytest.fixture( + name="module", + params=[ + pytest.param("tutorial001_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.fastapi.delete.{request.param}") mod.sqlite_url = "sqlite://" mod.engine = create_engine( mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool ) + return mod + - with TestClient(mod.app) as client: +def test_tutorial(module: ModuleType): + with TestClient(module.app) as client: hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} hero2_data = { "name": "Spider-Boy", @@ -285,16 +297,10 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), + "age": { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, }, }, "HeroPublic": { @@ -304,16 +310,10 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), + "age": { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, "id": {"title": "Id", "type": "integer"}, }, }, @@ -321,53 +321,56 @@ def test_tutorial(clear_sqlmodel): "title": "HeroUpdate", "type": "object", "properties": { - "name": IsDict( - { - "title": "Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Name", "type": "string"} - ), - "secret_name": IsDict( - { - "title": "Secret Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Secret Name", "type": "string"} - ), - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), + "name": { + "title": "Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "secret_name": { + "title": "Secret Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "age": { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, }, }, "ValidationError": { "title": "ValidationError", "required": ["loc", "msg", "type"], "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] + "properties": IsOneOf( + { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [ + {"type": "string"}, + {"type": "integer"}, + ] + }, }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, + { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [ + {"type": "string"}, + {"type": "integer"}, + ] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, + }, + ), }, } }, diff --git a/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001_py310.py b/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001_py310.py deleted file mode 100644 index 2757c878bc..0000000000 --- a/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001_py310.py +++ /dev/null @@ -1,377 +0,0 @@ -from dirty_equals import IsDict -from fastapi.testclient import TestClient -from sqlmodel import create_engine -from sqlmodel.pool import StaticPool - -from ....conftest import needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.delete import tutorial001_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine( - mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool - ) - - with TestClient(mod.app) as client: - hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} - hero2_data = { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "id": 9000, - } - hero3_data = { - "name": "Rusty-Man", - "secret_name": "Tommy Sharp", - "age": 48, - } - response = client.post("/heroes/", json=hero1_data) - assert response.status_code == 200, response.text - response = client.post("/heroes/", json=hero2_data) - assert response.status_code == 200, response.text - hero2 = response.json() - hero2_id = hero2["id"] - response = client.post("/heroes/", json=hero3_data) - assert response.status_code == 200, response.text - response = client.get(f"/heroes/{hero2_id}") - assert response.status_code == 200, response.text - response = client.get("/heroes/9000") - assert response.status_code == 404, response.text - response = client.get("/heroes/") - assert response.status_code == 200, response.text - data = response.json() - assert len(data) == 3 - response = client.patch( - f"/heroes/{hero2_id}", json={"secret_name": "Spider-Youngster"} - ) - assert response.status_code == 200, response.text - response = client.patch("/heroes/9001", json={"name": "Dragon Cube X"}) - assert response.status_code == 404, response.text - - response = client.delete(f"/heroes/{hero2_id}") - assert response.status_code == 200, response.text - response = client.get("/heroes/") - assert response.status_code == 200, response.text - data = response.json() - assert len(data) == 2 - - response = client.delete("/heroes/9000") - assert response.status_code == 404, response.text - - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/heroes/": { - "get": { - "summary": "Read Heroes", - "operationId": "read_heroes_heroes__get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Offset", - "type": "integer", - "default": 0, - }, - "name": "offset", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Limit", - "maximum": 100.0, - "type": "integer", - "default": 100, - }, - "name": "limit", - "in": "query", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Heroes Heroes Get", - "type": "array", - "items": { - "$ref": "#/components/schemas/HeroPublic" - }, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "post": { - "summary": "Create Hero", - "operationId": "create_hero_heroes__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroCreate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/heroes/{hero_id}": { - "get": { - "summary": "Read Hero", - "operationId": "read_hero_heroes__hero_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "delete": { - "summary": "Delete Hero", - "operationId": "delete_hero_heroes__hero_id__delete", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "patch": { - "summary": "Update Hero", - "operationId": "update_hero_heroes__hero_id__patch", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroUpdate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - } - }, - }, - "HeroCreate": { - "title": "HeroCreate", - "required": ["name", "secret_name"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - }, - }, - "HeroPublic": { - "title": "HeroPublic", - "required": ["name", "secret_name", "id"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "id": {"title": "Id", "type": "integer"}, - }, - }, - "HeroUpdate": { - "title": "HeroUpdate", - "type": "object", - "properties": { - "name": IsDict( - { - "title": "Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Name", "type": "string"} - ), - "secret_name": IsDict( - { - "title": "Secret Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Secret Name", "type": "string"} - ), - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001_py39.py b/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001_py39.py deleted file mode 100644 index 3299086bd0..0000000000 --- a/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001_py39.py +++ /dev/null @@ -1,377 +0,0 @@ -from dirty_equals import IsDict -from fastapi.testclient import TestClient -from sqlmodel import create_engine -from sqlmodel.pool import StaticPool - -from ....conftest import needs_py39 - - -@needs_py39 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.delete import tutorial001_py39 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine( - mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool - ) - - with TestClient(mod.app) as client: - hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} - hero2_data = { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "id": 9000, - } - hero3_data = { - "name": "Rusty-Man", - "secret_name": "Tommy Sharp", - "age": 48, - } - response = client.post("/heroes/", json=hero1_data) - assert response.status_code == 200, response.text - response = client.post("/heroes/", json=hero2_data) - assert response.status_code == 200, response.text - hero2 = response.json() - hero2_id = hero2["id"] - response = client.post("/heroes/", json=hero3_data) - assert response.status_code == 200, response.text - response = client.get(f"/heroes/{hero2_id}") - assert response.status_code == 200, response.text - response = client.get("/heroes/9000") - assert response.status_code == 404, response.text - response = client.get("/heroes/") - assert response.status_code == 200, response.text - data = response.json() - assert len(data) == 3 - response = client.patch( - f"/heroes/{hero2_id}", json={"secret_name": "Spider-Youngster"} - ) - assert response.status_code == 200, response.text - response = client.patch("/heroes/9001", json={"name": "Dragon Cube X"}) - assert response.status_code == 404, response.text - - response = client.delete(f"/heroes/{hero2_id}") - assert response.status_code == 200, response.text - response = client.get("/heroes/") - assert response.status_code == 200, response.text - data = response.json() - assert len(data) == 2 - - response = client.delete("/heroes/9000") - assert response.status_code == 404, response.text - - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/heroes/": { - "get": { - "summary": "Read Heroes", - "operationId": "read_heroes_heroes__get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Offset", - "type": "integer", - "default": 0, - }, - "name": "offset", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Limit", - "maximum": 100.0, - "type": "integer", - "default": 100, - }, - "name": "limit", - "in": "query", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Heroes Heroes Get", - "type": "array", - "items": { - "$ref": "#/components/schemas/HeroPublic" - }, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "post": { - "summary": "Create Hero", - "operationId": "create_hero_heroes__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroCreate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/heroes/{hero_id}": { - "get": { - "summary": "Read Hero", - "operationId": "read_hero_heroes__hero_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "delete": { - "summary": "Delete Hero", - "operationId": "delete_hero_heroes__hero_id__delete", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "patch": { - "summary": "Update Hero", - "operationId": "update_hero_heroes__hero_id__patch", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroUpdate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - } - }, - }, - "HeroCreate": { - "title": "HeroCreate", - "required": ["name", "secret_name"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - }, - }, - "HeroPublic": { - "title": "HeroPublic", - "required": ["name", "secret_name", "id"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "id": {"title": "Id", "type": "integer"}, - }, - }, - "HeroUpdate": { - "title": "HeroUpdate", - "type": "object", - "properties": { - "name": IsDict( - { - "title": "Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Name", "type": "string"} - ), - "secret_name": IsDict( - { - "title": "Secret Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Secret Name", "type": "string"} - ), - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001.py index 4047539f0a..d918988a16 100644 --- a/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001.py @@ -1,18 +1,32 @@ -from dirty_equals import IsDict +import importlib +from types import ModuleType + +import pytest +from dirty_equals import IsOneOf from fastapi.testclient import TestClient from sqlmodel import create_engine from sqlmodel.pool import StaticPool -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.limit_and_offset import tutorial001 as mod - +@pytest.fixture( + name="module", + params=[ + pytest.param("tutorial001_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module( + f"docs_src.tutorial.fastapi.limit_and_offset.{request.param}" + ) mod.sqlite_url = "sqlite://" mod.engine = create_engine( mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool ) + return mod + - with TestClient(mod.app) as client: +def test_tutorial(module: ModuleType): + with TestClient(module.app) as client: hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} hero2_data = { "name": "Spider-Boy", @@ -218,16 +232,10 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), + "age": { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, }, }, "HeroPublic": { @@ -237,16 +245,10 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), + "age": { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, "id": {"title": "Id", "type": "integer"}, }, }, @@ -254,17 +256,38 @@ def test_tutorial(clear_sqlmodel): "title": "ValidationError", "required": ["loc", "msg", "type"], "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] + "properties": IsOneOf( + { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [ + {"type": "string"}, + {"type": "integer"}, + ] + }, }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, + { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [ + {"type": "string"}, + {"type": "integer"}, + ] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, + }, + ), }, } }, diff --git a/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001_py310.py b/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001_py310.py deleted file mode 100644 index 480b92a121..0000000000 --- a/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001_py310.py +++ /dev/null @@ -1,274 +0,0 @@ -from dirty_equals import IsDict -from fastapi.testclient import TestClient -from sqlmodel import create_engine -from sqlmodel.pool import StaticPool - -from ....conftest import needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.limit_and_offset import tutorial001_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine( - mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool - ) - - with TestClient(mod.app) as client: - hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} - hero2_data = { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "id": 9000, - } - hero3_data = { - "name": "Rusty-Man", - "secret_name": "Tommy Sharp", - "age": 48, - } - response = client.post("/heroes/", json=hero1_data) - assert response.status_code == 200, response.text - response = client.post("/heroes/", json=hero2_data) - assert response.status_code == 200, response.text - hero2 = response.json() - hero_id = hero2["id"] - response = client.post("/heroes/", json=hero3_data) - assert response.status_code == 200, response.text - response = client.get(f"/heroes/{hero_id}") - assert response.status_code == 200, response.text - response = client.get("/heroes/9000") - assert response.status_code == 404, response.text - - response = client.get("/heroes/") - assert response.status_code == 200, response.text - data = response.json() - assert len(data) == 3 - - response = client.get("/heroes/", params={"limit": 2}) - assert response.status_code == 200, response.text - data = response.json() - assert len(data) == 2 - assert data[0]["name"] == hero1_data["name"] - assert data[1]["name"] == hero2_data["name"] - - response = client.get("/heroes/", params={"offset": 1}) - assert response.status_code == 200, response.text - data = response.json() - assert len(data) == 2 - assert data[0]["name"] == hero2_data["name"] - assert data[1]["name"] == hero3_data["name"] - - response = client.get("/heroes/", params={"offset": 1, "limit": 1}) - assert response.status_code == 200, response.text - data = response.json() - assert len(data) == 1 - assert data[0]["name"] == hero2_data["name"] - - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/heroes/": { - "get": { - "summary": "Read Heroes", - "operationId": "read_heroes_heroes__get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Offset", - "type": "integer", - "default": 0, - }, - "name": "offset", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Limit", - "maximum": 100.0, - "type": "integer", - "default": 100, - }, - "name": "limit", - "in": "query", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Heroes Heroes Get", - "type": "array", - "items": { - "$ref": "#/components/schemas/HeroPublic" - }, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "post": { - "summary": "Create Hero", - "operationId": "create_hero_heroes__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroCreate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/heroes/{hero_id}": { - "get": { - "summary": "Read Hero", - "operationId": "read_hero_heroes__hero_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - } - }, - }, - "HeroCreate": { - "title": "HeroCreate", - "required": ["name", "secret_name"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - }, - }, - "HeroPublic": { - "title": "HeroPublic", - "required": ["name", "secret_name", "id"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "id": {"title": "Id", "type": "integer"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001_py39.py b/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001_py39.py deleted file mode 100644 index 0a9d5c9ef0..0000000000 --- a/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001_py39.py +++ /dev/null @@ -1,274 +0,0 @@ -from dirty_equals import IsDict -from fastapi.testclient import TestClient -from sqlmodel import create_engine -from sqlmodel.pool import StaticPool - -from ....conftest import needs_py39 - - -@needs_py39 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.limit_and_offset import tutorial001_py39 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine( - mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool - ) - - with TestClient(mod.app) as client: - hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} - hero2_data = { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "id": 9000, - } - hero3_data = { - "name": "Rusty-Man", - "secret_name": "Tommy Sharp", - "age": 48, - } - response = client.post("/heroes/", json=hero1_data) - assert response.status_code == 200, response.text - response = client.post("/heroes/", json=hero2_data) - assert response.status_code == 200, response.text - hero2 = response.json() - hero_id = hero2["id"] - response = client.post("/heroes/", json=hero3_data) - assert response.status_code == 200, response.text - response = client.get(f"/heroes/{hero_id}") - assert response.status_code == 200, response.text - response = client.get("/heroes/9000") - assert response.status_code == 404, response.text - - response = client.get("/heroes/") - assert response.status_code == 200, response.text - data = response.json() - assert len(data) == 3 - - response = client.get("/heroes/", params={"limit": 2}) - assert response.status_code == 200, response.text - data = response.json() - assert len(data) == 2 - assert data[0]["name"] == hero1_data["name"] - assert data[1]["name"] == hero2_data["name"] - - response = client.get("/heroes/", params={"offset": 1}) - assert response.status_code == 200, response.text - data = response.json() - assert len(data) == 2 - assert data[0]["name"] == hero2_data["name"] - assert data[1]["name"] == hero3_data["name"] - - response = client.get("/heroes/", params={"offset": 1, "limit": 1}) - assert response.status_code == 200, response.text - data = response.json() - assert len(data) == 1 - assert data[0]["name"] == hero2_data["name"] - - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/heroes/": { - "get": { - "summary": "Read Heroes", - "operationId": "read_heroes_heroes__get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Offset", - "type": "integer", - "default": 0, - }, - "name": "offset", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Limit", - "maximum": 100.0, - "type": "integer", - "default": 100, - }, - "name": "limit", - "in": "query", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Heroes Heroes Get", - "type": "array", - "items": { - "$ref": "#/components/schemas/HeroPublic" - }, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "post": { - "summary": "Create Hero", - "operationId": "create_hero_heroes__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroCreate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/heroes/{hero_id}": { - "get": { - "summary": "Read Hero", - "operationId": "read_hero_heroes__hero_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - } - }, - }, - "HeroCreate": { - "title": "HeroCreate", - "required": ["name", "secret_name"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - }, - }, - "HeroPublic": { - "title": "HeroPublic", - "required": ["name", "secret_name", "id"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "id": {"title": "Id", "type": "integer"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001.py index 276a021c54..4fd90fe22f 100644 --- a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001.py @@ -1,4 +1,8 @@ -from dirty_equals import IsDict +import importlib +from types import ModuleType + +import pytest +from dirty_equals import IsOneOf from fastapi.testclient import TestClient from sqlalchemy import inspect from sqlalchemy.engine.reflection import Inspector @@ -6,15 +10,25 @@ from sqlmodel.pool import StaticPool -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.multiple_models import tutorial001 as mod - +@pytest.fixture( + name="module", + params=[ + pytest.param("tutorial001_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module( + f"docs_src.tutorial.fastapi.multiple_models.{request.param}" + ) mod.sqlite_url = "sqlite://" mod.engine = create_engine( mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool ) + return mod + - with TestClient(mod.app) as client: +def test_tutorial(module: ModuleType): + with TestClient(module.app) as client: hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} hero2_data = { "name": "Spider-Boy", @@ -142,16 +156,10 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), + "age": { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, }, }, "HeroPublic": { @@ -162,41 +170,56 @@ def test_tutorial(clear_sqlmodel): "id": {"title": "Id", "type": "integer"}, "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), + "age": { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, }, }, "ValidationError": { "title": "ValidationError", "required": ["loc", "msg", "type"], "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] + "properties": IsOneOf( + { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [ + {"type": "string"}, + {"type": "integer"}, + ] + }, }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, + { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [ + {"type": "string"}, + {"type": "integer"}, + ] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, + }, + ), }, } }, } # Test inherited indexes - insp: Inspector = inspect(mod.engine) - indexes = insp.get_indexes(str(mod.Hero.__tablename__)) + insp: Inspector = inspect(module.engine) + indexes = insp.get_indexes(str(module.Hero.__tablename__)) expected_indexes = [ { "name": "ix_hero_name", diff --git a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001_py310.py b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001_py310.py deleted file mode 100644 index b6f082a0f8..0000000000 --- a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001_py310.py +++ /dev/null @@ -1,220 +0,0 @@ -from dirty_equals import IsDict -from fastapi.testclient import TestClient -from sqlalchemy import inspect -from sqlalchemy.engine.reflection import Inspector -from sqlmodel import create_engine -from sqlmodel.pool import StaticPool - -from ....conftest import needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.multiple_models import tutorial001_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine( - mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool - ) - - with TestClient(mod.app) as client: - hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} - hero2_data = { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "id": 9000, - } - response = client.post("/heroes/", json=hero1_data) - data = response.json() - - assert response.status_code == 200, response.text - assert data["name"] == hero1_data["name"] - assert data["secret_name"] == hero1_data["secret_name"] - assert data["id"] is not None - assert data["age"] is None - - response = client.post("/heroes/", json=hero2_data) - data = response.json() - - assert response.status_code == 200, response.text - assert data["name"] == hero2_data["name"] - assert data["secret_name"] == hero2_data["secret_name"] - assert data["id"] != hero2_data["id"], ( - "Now it's not possible to predefine the ID from the request, " - "it's now set by the database" - ) - assert data["age"] is None - - response = client.get("/heroes/") - data = response.json() - - assert response.status_code == 200, response.text - assert len(data) == 2 - assert data[0]["name"] == hero1_data["name"] - assert data[0]["secret_name"] == hero1_data["secret_name"] - assert data[1]["name"] == hero2_data["name"] - assert data[1]["secret_name"] == hero2_data["secret_name"] - assert data[1]["id"] != hero2_data["id"] - - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/heroes/": { - "get": { - "summary": "Read Heroes", - "operationId": "read_heroes_heroes__get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Heroes Heroes Get", - "type": "array", - "items": { - "$ref": "#/components/schemas/HeroPublic" - }, - } - } - }, - } - }, - }, - "post": { - "summary": "Create Hero", - "operationId": "create_hero_heroes__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroCreate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - } - }, - }, - "HeroCreate": { - "title": "HeroCreate", - "required": ["name", "secret_name"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - }, - }, - "HeroPublic": { - "title": "HeroPublic", - "required": ["id", "name", "secret_name"], - "type": "object", - "properties": { - "id": {"title": "Id", "type": "integer"}, - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } - - # Test inherited indexes - insp: Inspector = inspect(mod.engine) - indexes = insp.get_indexes(str(mod.Hero.__tablename__)) - expected_indexes = [ - { - "name": "ix_hero_name", - "dialect_options": {}, - "column_names": ["name"], - "unique": 0, - }, - { - "name": "ix_hero_age", - "dialect_options": {}, - "column_names": ["age"], - "unique": 0, - }, - ] - for index in expected_indexes: - assert index in indexes, "This expected index should be in the indexes in DB" - # Now that this index was checked, remove it from the list of indexes - indexes.pop(indexes.index(index)) - assert len(indexes) == 0, "The database should only have the expected indexes" diff --git a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001_py39.py b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001_py39.py deleted file mode 100644 index 82365ced61..0000000000 --- a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001_py39.py +++ /dev/null @@ -1,221 +0,0 @@ -from dirty_equals import IsDict -from fastapi.testclient import TestClient -from sqlalchemy import inspect -from sqlalchemy.engine.reflection import Inspector -from sqlmodel import create_engine -from sqlmodel.pool import StaticPool - -from ....conftest import needs_py39 - - -@needs_py39 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.multiple_models import tutorial001_py39 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine( - mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool - ) - - with TestClient(mod.app) as client: - hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} - hero2_data = { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "id": 9000, - } - response = client.post("/heroes/", json=hero1_data) - data = response.json() - - assert response.status_code == 200, response.text - assert data["name"] == hero1_data["name"] - assert data["secret_name"] == hero1_data["secret_name"] - assert data["id"] is not None - assert data["age"] is None - - response = client.post("/heroes/", json=hero2_data) - data = response.json() - - assert response.status_code == 200, response.text - assert data["name"] == hero2_data["name"] - assert data["secret_name"] == hero2_data["secret_name"] - assert data["id"] != hero2_data["id"], ( - "Now it's not possible to predefine the ID from the request, " - "it's now set by the database" - ) - assert data["age"] is None - - response = client.get("/heroes/") - data = response.json() - - assert response.status_code == 200, response.text - assert len(data) == 2 - assert data[0]["name"] == hero1_data["name"] - assert data[0]["secret_name"] == hero1_data["secret_name"] - assert data[1]["name"] == hero2_data["name"] - assert data[1]["secret_name"] == hero2_data["secret_name"] - assert data[1]["id"] != hero2_data["id"] - - response = client.get("/openapi.json") - - assert response.status_code == 200, response.text - - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/heroes/": { - "get": { - "summary": "Read Heroes", - "operationId": "read_heroes_heroes__get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Heroes Heroes Get", - "type": "array", - "items": { - "$ref": "#/components/schemas/HeroPublic" - }, - } - } - }, - } - }, - }, - "post": { - "summary": "Create Hero", - "operationId": "create_hero_heroes__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroCreate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - } - }, - }, - "HeroCreate": { - "title": "HeroCreate", - "required": ["name", "secret_name"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - }, - }, - "HeroPublic": { - "title": "HeroPublic", - "required": ["id", "name", "secret_name"], - "type": "object", - "properties": { - "id": {"title": "Id", "type": "integer"}, - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } - - # Test inherited indexes - insp: Inspector = inspect(mod.engine) - indexes = insp.get_indexes(str(mod.Hero.__tablename__)) - expected_indexes = [ - { - "name": "ix_hero_name", - "dialect_options": {}, - "column_names": ["name"], - "unique": 0, - }, - { - "name": "ix_hero_age", - "dialect_options": {}, - "column_names": ["age"], - "unique": 0, - }, - ] - for index in expected_indexes: - assert index in indexes, "This expected index should be in the indexes in DB" - # Now that this index was checked, remove it from the list of indexes - indexes.pop(indexes.index(index)) - assert len(indexes) == 0, "The database should only have the expected indexes" diff --git a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002.py b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002.py index 8327c6d566..a4804125ea 100644 --- a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002.py +++ b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002.py @@ -1,4 +1,8 @@ -from dirty_equals import IsDict +import importlib +from types import ModuleType + +import pytest +from dirty_equals import IsOneOf from fastapi.testclient import TestClient from sqlalchemy import inspect from sqlalchemy.engine.reflection import Inspector @@ -6,15 +10,25 @@ from sqlmodel.pool import StaticPool -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.multiple_models import tutorial002 as mod - +@pytest.fixture( + name="module", + params=[ + pytest.param("tutorial002_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module( + f"docs_src.tutorial.fastapi.multiple_models.{request.param}" + ) mod.sqlite_url = "sqlite://" mod.engine = create_engine( mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool ) + return mod + - with TestClient(mod.app) as client: +def test_tutorial(module: ModuleType): + with TestClient(module.app) as client: hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} hero2_data = { "name": "Spider-Boy", @@ -142,16 +156,10 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), + "age": { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, }, }, "HeroPublic": { @@ -161,16 +169,10 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), + "age": { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, "id": {"title": "Id", "type": "integer"}, }, }, @@ -178,25 +180,46 @@ def test_tutorial(clear_sqlmodel): "title": "ValidationError", "required": ["loc", "msg", "type"], "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] + "properties": IsOneOf( + { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [ + {"type": "string"}, + {"type": "integer"}, + ] + }, }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, + { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [ + {"type": "string"}, + {"type": "integer"}, + ] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, + }, + ), }, } }, } # Test inherited indexes - insp: Inspector = inspect(mod.engine) - indexes = insp.get_indexes(str(mod.Hero.__tablename__)) + insp: Inspector = inspect(module.engine) + indexes = insp.get_indexes(str(module.Hero.__tablename__)) expected_indexes = [ { "name": "ix_hero_age", diff --git a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002_py310.py b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002_py310.py deleted file mode 100644 index 30edc4dea3..0000000000 --- a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002_py310.py +++ /dev/null @@ -1,221 +0,0 @@ -from dirty_equals import IsDict -from fastapi.testclient import TestClient -from sqlalchemy import inspect -from sqlalchemy.engine.reflection import Inspector -from sqlmodel import create_engine -from sqlmodel.pool import StaticPool - -from ....conftest import needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.multiple_models import tutorial002_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine( - mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool - ) - - with TestClient(mod.app) as client: - hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} - hero2_data = { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "id": 9000, - } - response = client.post("/heroes/", json=hero1_data) - data = response.json() - - assert response.status_code == 200, response.text - assert data["name"] == hero1_data["name"] - assert data["secret_name"] == hero1_data["secret_name"] - assert data["id"] is not None - assert data["age"] is None - - response = client.post("/heroes/", json=hero2_data) - data = response.json() - - assert response.status_code == 200, response.text - assert data["name"] == hero2_data["name"] - assert data["secret_name"] == hero2_data["secret_name"] - assert data["id"] != hero2_data["id"], ( - "Now it's not possible to predefine the ID from the request, " - "it's now set by the database" - ) - assert data["age"] is None - - response = client.get("/heroes/") - data = response.json() - - assert response.status_code == 200, response.text - assert len(data) == 2 - assert data[0]["name"] == hero1_data["name"] - assert data[0]["secret_name"] == hero1_data["secret_name"] - assert data[1]["name"] == hero2_data["name"] - assert data[1]["secret_name"] == hero2_data["secret_name"] - assert data[1]["id"] != hero2_data["id"] - - response = client.get("/openapi.json") - - assert response.status_code == 200, response.text - - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/heroes/": { - "get": { - "summary": "Read Heroes", - "operationId": "read_heroes_heroes__get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Heroes Heroes Get", - "type": "array", - "items": { - "$ref": "#/components/schemas/HeroPublic" - }, - } - } - }, - } - }, - }, - "post": { - "summary": "Create Hero", - "operationId": "create_hero_heroes__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroCreate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - } - }, - }, - "HeroCreate": { - "title": "HeroCreate", - "required": ["name", "secret_name"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - }, - }, - "HeroPublic": { - "title": "HeroPublic", - "required": ["name", "secret_name", "id"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "id": {"title": "Id", "type": "integer"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } - - # Test inherited indexes - insp: Inspector = inspect(mod.engine) - indexes = insp.get_indexes(str(mod.Hero.__tablename__)) - expected_indexes = [ - { - "name": "ix_hero_age", - "dialect_options": {}, - "column_names": ["age"], - "unique": 0, - }, - { - "name": "ix_hero_name", - "dialect_options": {}, - "column_names": ["name"], - "unique": 0, - }, - ] - for index in expected_indexes: - assert index in indexes, "This expected index should be in the indexes in DB" - # Now that this index was checked, remove it from the list of indexes - indexes.pop(indexes.index(index)) - assert len(indexes) == 0, "The database should only have the expected indexes" diff --git a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002_py39.py b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002_py39.py deleted file mode 100644 index 2b86d3facc..0000000000 --- a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002_py39.py +++ /dev/null @@ -1,221 +0,0 @@ -from dirty_equals import IsDict -from fastapi.testclient import TestClient -from sqlalchemy import inspect -from sqlalchemy.engine.reflection import Inspector -from sqlmodel import create_engine -from sqlmodel.pool import StaticPool - -from ....conftest import needs_py39 - - -@needs_py39 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.multiple_models import tutorial002_py39 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine( - mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool - ) - - with TestClient(mod.app) as client: - hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} - hero2_data = { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "id": 9000, - } - response = client.post("/heroes/", json=hero1_data) - data = response.json() - - assert response.status_code == 200, response.text - assert data["name"] == hero1_data["name"] - assert data["secret_name"] == hero1_data["secret_name"] - assert data["id"] is not None - assert data["age"] is None - - response = client.post("/heroes/", json=hero2_data) - data = response.json() - - assert response.status_code == 200, response.text - assert data["name"] == hero2_data["name"] - assert data["secret_name"] == hero2_data["secret_name"] - assert data["id"] != hero2_data["id"], ( - "Now it's not possible to predefine the ID from the request, " - "it's now set by the database" - ) - assert data["age"] is None - - response = client.get("/heroes/") - data = response.json() - - assert response.status_code == 200, response.text - assert len(data) == 2 - assert data[0]["name"] == hero1_data["name"] - assert data[0]["secret_name"] == hero1_data["secret_name"] - assert data[1]["name"] == hero2_data["name"] - assert data[1]["secret_name"] == hero2_data["secret_name"] - assert data[1]["id"] != hero2_data["id"] - - response = client.get("/openapi.json") - - assert response.status_code == 200, response.text - - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/heroes/": { - "get": { - "summary": "Read Heroes", - "operationId": "read_heroes_heroes__get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Heroes Heroes Get", - "type": "array", - "items": { - "$ref": "#/components/schemas/HeroPublic" - }, - } - } - }, - } - }, - }, - "post": { - "summary": "Create Hero", - "operationId": "create_hero_heroes__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroCreate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - } - }, - }, - "HeroCreate": { - "title": "HeroCreate", - "required": ["name", "secret_name"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - }, - }, - "HeroPublic": { - "title": "HeroPublic", - "required": ["name", "secret_name", "id"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "id": {"title": "Id", "type": "integer"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } - - # Test inherited indexes - insp: Inspector = inspect(mod.engine) - indexes = insp.get_indexes(str(mod.Hero.__tablename__)) - expected_indexes = [ - { - "name": "ix_hero_age", - "dialect_options": {}, - "column_names": ["age"], - "unique": 0, - }, - { - "name": "ix_hero_name", - "dialect_options": {}, - "column_names": ["name"], - "unique": 0, - }, - ] - for index in expected_indexes: - assert index in indexes, "This expected index should be in the indexes in DB" - # Now that this index was checked, remove it from the list of indexes - indexes.pop(indexes.index(index)) - assert len(indexes) == 0, "The database should only have the expected indexes" diff --git a/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001.py index 9b1d527565..c934523b6a 100644 --- a/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001.py @@ -1,18 +1,30 @@ -from dirty_equals import IsDict +import importlib +from types import ModuleType + +import pytest +from dirty_equals import IsOneOf from fastapi.testclient import TestClient from sqlmodel import create_engine from sqlmodel.pool import StaticPool -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.read_one import tutorial001 as mod - +@pytest.fixture( + name="module", + params=[ + pytest.param("tutorial001_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.fastapi.read_one.{request.param}") mod.sqlite_url = "sqlite://" mod.engine = create_engine( mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool ) + return mod - with TestClient(mod.app) as client: + +def test_tutorial(module: ModuleType): + with TestClient(module.app) as client: hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} hero2_data = { "name": "Spider-Boy", @@ -163,16 +175,10 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), + "age": { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, }, }, "HeroPublic": { @@ -182,16 +188,10 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), + "age": { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, "id": {"title": "Id", "type": "integer"}, }, }, @@ -199,17 +199,38 @@ def test_tutorial(clear_sqlmodel): "title": "ValidationError", "required": ["loc", "msg", "type"], "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] + "properties": IsOneOf( + { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [ + {"type": "string"}, + {"type": "integer"}, + ] + }, }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, + { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [ + {"type": "string"}, + {"type": "integer"}, + ] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, + }, + ), }, } }, diff --git a/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001_py310.py b/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001_py310.py deleted file mode 100644 index f18b0d65cf..0000000000 --- a/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001_py310.py +++ /dev/null @@ -1,219 +0,0 @@ -from dirty_equals import IsDict -from fastapi.testclient import TestClient -from sqlmodel import create_engine -from sqlmodel.pool import StaticPool - -from ....conftest import needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.read_one import tutorial001_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine( - mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool - ) - - with TestClient(mod.app) as client: - hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} - hero2_data = { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "id": 9000, - } - response = client.post("/heroes/", json=hero1_data) - assert response.status_code == 200, response.text - response = client.post("/heroes/", json=hero2_data) - assert response.status_code == 200, response.text - hero2 = response.json() - response = client.get("/heroes/") - assert response.status_code == 200, response.text - data = response.json() - assert len(data) == 2 - - hero_id = hero2["id"] - response = client.get(f"/heroes/{hero_id}") - assert response.status_code == 200, response.text - data = response.json() - assert data == hero2 - - response = client.get("/heroes/9000") - assert response.status_code == 404, response.text - - response = client.get("/openapi.json") - - assert response.status_code == 200, response.text - - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/heroes/": { - "get": { - "summary": "Read Heroes", - "operationId": "read_heroes_heroes__get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Heroes Heroes Get", - "type": "array", - "items": { - "$ref": "#/components/schemas/HeroPublic" - }, - } - } - }, - } - }, - }, - "post": { - "summary": "Create Hero", - "operationId": "create_hero_heroes__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroCreate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/heroes/{hero_id}": { - "get": { - "summary": "Read Hero", - "operationId": "read_hero_heroes__hero_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - } - }, - }, - "HeroCreate": { - "title": "HeroCreate", - "required": ["name", "secret_name"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - }, - }, - "HeroPublic": { - "title": "HeroPublic", - "required": ["name", "secret_name", "id"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "id": {"title": "Id", "type": "integer"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001_py39.py b/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001_py39.py deleted file mode 100644 index 4423d1a713..0000000000 --- a/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001_py39.py +++ /dev/null @@ -1,219 +0,0 @@ -from dirty_equals import IsDict -from fastapi.testclient import TestClient -from sqlmodel import create_engine -from sqlmodel.pool import StaticPool - -from ....conftest import needs_py39 - - -@needs_py39 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.read_one import tutorial001_py39 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine( - mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool - ) - - with TestClient(mod.app) as client: - hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} - hero2_data = { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "id": 9000, - } - response = client.post("/heroes/", json=hero1_data) - assert response.status_code == 200, response.text - response = client.post("/heroes/", json=hero2_data) - assert response.status_code == 200, response.text - hero2 = response.json() - response = client.get("/heroes/") - assert response.status_code == 200, response.text - data = response.json() - assert len(data) == 2 - - hero_id = hero2["id"] - response = client.get(f"/heroes/{hero_id}") - assert response.status_code == 200, response.text - data = response.json() - assert data == hero2 - - response = client.get("/heroes/9000") - assert response.status_code == 404, response.text - - response = client.get("/openapi.json") - - assert response.status_code == 200, response.text - - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/heroes/": { - "get": { - "summary": "Read Heroes", - "operationId": "read_heroes_heroes__get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Heroes Heroes Get", - "type": "array", - "items": { - "$ref": "#/components/schemas/HeroPublic" - }, - } - } - }, - } - }, - }, - "post": { - "summary": "Create Hero", - "operationId": "create_hero_heroes__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroCreate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/heroes/{hero_id}": { - "get": { - "summary": "Read Hero", - "operationId": "read_hero_heroes__hero_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - } - }, - }, - "HeroCreate": { - "title": "HeroCreate", - "required": ["name", "secret_name"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - }, - }, - "HeroPublic": { - "title": "HeroPublic", - "required": ["name", "secret_name", "id"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "id": {"title": "Id", "type": "integer"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001.py index 4b4f47b762..fb3d8a125a 100644 --- a/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001.py @@ -1,18 +1,32 @@ -from dirty_equals import IsDict +import importlib +from types import ModuleType + +import pytest +from dirty_equals import IsOneOf from fastapi.testclient import TestClient from sqlmodel import create_engine from sqlmodel.pool import StaticPool -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.relationships import tutorial001 as mod - +@pytest.fixture( + name="module", + params=[ + pytest.param("tutorial001_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module( + f"docs_src.tutorial.fastapi.relationships.{request.param}" + ) mod.sqlite_url = "sqlite://" mod.engine = create_engine( mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool ) + return mod - with TestClient(mod.app) as client: + +def test_tutorial(module: ModuleType): + with TestClient(module.app) as client: team_preventers = {"name": "Preventers", "headquarters": "Sharp Tower"} team_z_force = {"name": "Z-Force", "headquarters": "Sister Margaret's Bar"} response = client.post("/teams/", json=team_preventers) @@ -532,26 +546,14 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "team_id": IsDict( - { - "title": "Team Id", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Team Id", "type": "integer"} - ), + "age": { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, + "team_id": { + "title": "Team Id", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, }, }, "HeroPublic": { @@ -561,26 +563,14 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "team_id": IsDict( - { - "title": "Team Id", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Team Id", "type": "integer"} - ), + "age": { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, + "team_id": { + "title": "Team Id", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, "id": {"title": "Id", "type": "integer"}, }, }, @@ -591,85 +581,43 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "team_id": IsDict( - { - "title": "Team Id", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Team Id", "type": "integer"} - ), + "age": { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, + "team_id": { + "title": "Team Id", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, "id": {"title": "Id", "type": "integer"}, - "team": IsDict( - { - "anyOf": [ - {"$ref": "#/components/schemas/TeamPublic"}, - {"type": "null"}, - ] - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"$ref": "#/components/schemas/TeamPublic"} - ), + "team": { + "anyOf": [ + {"$ref": "#/components/schemas/TeamPublic"}, + {"type": "null"}, + ] + }, }, }, "HeroUpdate": { "title": "HeroUpdate", "type": "object", "properties": { - "name": IsDict( - { - "title": "Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Name", "type": "string"} - ), - "secret_name": IsDict( - { - "title": "Secret Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Secret Name", "type": "string"} - ), - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "team_id": IsDict( - { - "title": "Team Id", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Team Id", "type": "integer"} - ), + "name": { + "title": "Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "secret_name": { + "title": "Secret Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "age": { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, + "team_id": { + "title": "Team Id", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, }, }, "TeamCreate": { @@ -711,53 +659,56 @@ def test_tutorial(clear_sqlmodel): "title": "TeamUpdate", "type": "object", "properties": { - "id": IsDict( - { - "title": "Id", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Id", "type": "integer"} - ), - "name": IsDict( - { - "title": "Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Name", "type": "string"} - ), - "headquarters": IsDict( - { - "title": "Headquarters", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Headquarters", "type": "string"} - ), + "id": { + "title": "Id", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, + "name": { + "title": "Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "headquarters": { + "title": "Headquarters", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, }, }, "ValidationError": { "title": "ValidationError", "required": ["loc", "msg", "type"], "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] + "properties": IsOneOf( + { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [ + {"type": "string"}, + {"type": "integer"}, + ] + }, }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, + { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [ + {"type": "string"}, + {"type": "integer"}, + ] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, + }, + ), }, } }, diff --git a/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001_py310.py b/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001_py310.py deleted file mode 100644 index dcb78f597d..0000000000 --- a/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001_py310.py +++ /dev/null @@ -1,767 +0,0 @@ -from dirty_equals import IsDict -from fastapi.testclient import TestClient -from sqlmodel import create_engine -from sqlmodel.pool import StaticPool - -from ....conftest import needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.relationships import tutorial001_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine( - mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool - ) - - with TestClient(mod.app) as client: - team_preventers = {"name": "Preventers", "headquarters": "Sharp Tower"} - team_z_force = {"name": "Z-Force", "headquarters": "Sister Margaret's Bar"} - response = client.post("/teams/", json=team_preventers) - assert response.status_code == 200, response.text - team_preventers_data = response.json() - team_preventers_id = team_preventers_data["id"] - response = client.post("/teams/", json=team_z_force) - assert response.status_code == 200, response.text - team_z_force_data = response.json() - team_z_force_id = team_z_force_data["id"] - response = client.get("/teams/") - data = response.json() - assert len(data) == 2 - response = client.get("/teams/9000") - assert response.status_code == 404, response.text - response = client.patch( - f"/teams/{team_preventers_id}", json={"headquarters": "Preventers Tower"} - ) - data = response.json() - assert response.status_code == 200, response.text - assert data["name"] == team_preventers["name"] - assert data["headquarters"] == "Preventers Tower" - response = client.patch("/teams/9000", json={"name": "Freedom League"}) - assert response.status_code == 404, response.text - - hero1_data = { - "name": "Deadpond", - "secret_name": "Dive Wilson", - "team_id": team_z_force_id, - } - hero2_data = { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "id": 9000, - } - hero3_data = { - "name": "Rusty-Man", - "secret_name": "Tommy Sharp", - "age": 48, - "team_id": team_preventers_id, - } - response = client.post("/heroes/", json=hero1_data) - assert response.status_code == 200, response.text - hero1 = response.json() - hero1_id = hero1["id"] - response = client.post("/heroes/", json=hero2_data) - assert response.status_code == 200, response.text - hero2 = response.json() - hero2_id = hero2["id"] - response = client.post("/heroes/", json=hero3_data) - assert response.status_code == 200, response.text - response = client.get("/heroes/9000") - assert response.status_code == 404, response.text - response = client.get("/heroes/") - assert response.status_code == 200, response.text - data = response.json() - assert len(data) == 3 - response = client.get(f"/heroes/{hero1_id}") - assert response.status_code == 200, response.text - data = response.json() - assert data["name"] == hero1_data["name"] - assert data["team"]["name"] == team_z_force["name"] - response = client.patch( - f"/heroes/{hero2_id}", json={"secret_name": "Spider-Youngster"} - ) - assert response.status_code == 200, response.text - response = client.patch("/heroes/9001", json={"name": "Dragon Cube X"}) - assert response.status_code == 404, response.text - response = client.delete(f"/heroes/{hero2_id}") - assert response.status_code == 200, response.text - response = client.get("/heroes/") - assert response.status_code == 200, response.text - data = response.json() - assert len(data) == 2 - response = client.delete("/heroes/9000") - assert response.status_code == 404, response.text - - response = client.get(f"/teams/{team_preventers_id}") - data = response.json() - assert response.status_code == 200, response.text - assert data["name"] == team_preventers_data["name"] - assert data["heroes"][0]["name"] == hero3_data["name"] - - response = client.delete(f"/teams/{team_preventers_id}") - assert response.status_code == 200, response.text - response = client.delete("/teams/9000") - assert response.status_code == 404, response.text - response = client.get("/teams/") - assert response.status_code == 200, response.text - data = response.json() - assert len(data) == 1 - - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/heroes/": { - "get": { - "summary": "Read Heroes", - "operationId": "read_heroes_heroes__get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Offset", - "type": "integer", - "default": 0, - }, - "name": "offset", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Limit", - "maximum": 100.0, - "type": "integer", - "default": 100, - }, - "name": "limit", - "in": "query", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Heroes Heroes Get", - "type": "array", - "items": { - "$ref": "#/components/schemas/HeroPublic" - }, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "post": { - "summary": "Create Hero", - "operationId": "create_hero_heroes__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroCreate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/heroes/{hero_id}": { - "get": { - "summary": "Read Hero", - "operationId": "read_hero_heroes__hero_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublicWithTeam" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "delete": { - "summary": "Delete Hero", - "operationId": "delete_hero_heroes__hero_id__delete", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "patch": { - "summary": "Update Hero", - "operationId": "update_hero_heroes__hero_id__patch", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroUpdate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/teams/": { - "get": { - "summary": "Read Teams", - "operationId": "read_teams_teams__get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Offset", - "type": "integer", - "default": 0, - }, - "name": "offset", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Limit", - "maximum": 100.0, - "type": "integer", - "default": 100, - }, - "name": "limit", - "in": "query", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Teams Teams Get", - "type": "array", - "items": { - "$ref": "#/components/schemas/TeamPublic" - }, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "post": { - "summary": "Create Team", - "operationId": "create_team_teams__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TeamCreate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TeamPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/teams/{team_id}": { - "get": { - "summary": "Read Team", - "operationId": "read_team_teams__team_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Team Id", "type": "integer"}, - "name": "team_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TeamPublicWithHeroes" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "delete": { - "summary": "Delete Team", - "operationId": "delete_team_teams__team_id__delete", - "parameters": [ - { - "required": True, - "schema": {"title": "Team Id", "type": "integer"}, - "name": "team_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "patch": { - "summary": "Update Team", - "operationId": "update_team_teams__team_id__patch", - "parameters": [ - { - "required": True, - "schema": {"title": "Team Id", "type": "integer"}, - "name": "team_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TeamUpdate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TeamPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - } - }, - }, - "HeroCreate": { - "title": "HeroCreate", - "required": ["name", "secret_name"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "team_id": IsDict( - { - "title": "Team Id", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Team Id", "type": "integer"} - ), - }, - }, - "HeroPublic": { - "title": "HeroPublic", - "required": ["name", "secret_name", "id"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "team_id": IsDict( - { - "title": "Team Id", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Team Id", "type": "integer"} - ), - "id": {"title": "Id", "type": "integer"}, - }, - }, - "HeroPublicWithTeam": { - "title": "HeroPublicWithTeam", - "required": ["name", "secret_name", "id"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "team_id": IsDict( - { - "title": "Team Id", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Team Id", "type": "integer"} - ), - "id": {"title": "Id", "type": "integer"}, - "team": IsDict( - { - "anyOf": [ - {"$ref": "#/components/schemas/TeamPublic"}, - {"type": "null"}, - ] - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"$ref": "#/components/schemas/TeamPublic"} - ), - }, - }, - "HeroUpdate": { - "title": "HeroUpdate", - "type": "object", - "properties": { - "name": IsDict( - { - "title": "Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Name", "type": "string"} - ), - "secret_name": IsDict( - { - "title": "Secret Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Secret Name", "type": "string"} - ), - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "team_id": IsDict( - { - "title": "Team Id", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Team Id", "type": "integer"} - ), - }, - }, - "TeamCreate": { - "title": "TeamCreate", - "required": ["name", "headquarters"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "headquarters": {"title": "Headquarters", "type": "string"}, - }, - }, - "TeamPublic": { - "title": "TeamPublic", - "required": ["name", "headquarters", "id"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "headquarters": {"title": "Headquarters", "type": "string"}, - "id": {"title": "Id", "type": "integer"}, - }, - }, - "TeamPublicWithHeroes": { - "title": "TeamPublicWithHeroes", - "required": ["name", "headquarters", "id"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "headquarters": {"title": "Headquarters", "type": "string"}, - "id": {"title": "Id", "type": "integer"}, - "heroes": { - "title": "Heroes", - "type": "array", - "items": {"$ref": "#/components/schemas/HeroPublic"}, - "default": [], - }, - }, - }, - "TeamUpdate": { - "title": "TeamUpdate", - "type": "object", - "properties": { - "id": IsDict( - { - "title": "Id", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Id", "type": "integer"} - ), - "name": IsDict( - { - "title": "Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Name", "type": "string"} - ), - "headquarters": IsDict( - { - "title": "Headquarters", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Headquarters", "type": "string"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001_py39.py b/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001_py39.py deleted file mode 100644 index 5ef7338d44..0000000000 --- a/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001_py39.py +++ /dev/null @@ -1,767 +0,0 @@ -from dirty_equals import IsDict -from fastapi.testclient import TestClient -from sqlmodel import create_engine -from sqlmodel.pool import StaticPool - -from ....conftest import needs_py39 - - -@needs_py39 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.relationships import tutorial001_py39 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine( - mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool - ) - - with TestClient(mod.app) as client: - team_preventers = {"name": "Preventers", "headquarters": "Sharp Tower"} - team_z_force = {"name": "Z-Force", "headquarters": "Sister Margaret's Bar"} - response = client.post("/teams/", json=team_preventers) - assert response.status_code == 200, response.text - team_preventers_data = response.json() - team_preventers_id = team_preventers_data["id"] - response = client.post("/teams/", json=team_z_force) - assert response.status_code == 200, response.text - team_z_force_data = response.json() - team_z_force_id = team_z_force_data["id"] - response = client.get("/teams/") - data = response.json() - assert len(data) == 2 - response = client.get("/teams/9000") - assert response.status_code == 404, response.text - response = client.patch( - f"/teams/{team_preventers_id}", json={"headquarters": "Preventers Tower"} - ) - data = response.json() - assert response.status_code == 200, response.text - assert data["name"] == team_preventers["name"] - assert data["headquarters"] == "Preventers Tower" - response = client.patch("/teams/9000", json={"name": "Freedom League"}) - assert response.status_code == 404, response.text - - hero1_data = { - "name": "Deadpond", - "secret_name": "Dive Wilson", - "team_id": team_z_force_id, - } - hero2_data = { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "id": 9000, - } - hero3_data = { - "name": "Rusty-Man", - "secret_name": "Tommy Sharp", - "age": 48, - "team_id": team_preventers_id, - } - response = client.post("/heroes/", json=hero1_data) - assert response.status_code == 200, response.text - hero1 = response.json() - hero1_id = hero1["id"] - response = client.post("/heroes/", json=hero2_data) - assert response.status_code == 200, response.text - hero2 = response.json() - hero2_id = hero2["id"] - response = client.post("/heroes/", json=hero3_data) - assert response.status_code == 200, response.text - response = client.get("/heroes/9000") - assert response.status_code == 404, response.text - response = client.get("/heroes/") - assert response.status_code == 200, response.text - data = response.json() - assert len(data) == 3 - response = client.get(f"/heroes/{hero1_id}") - assert response.status_code == 200, response.text - data = response.json() - assert data["name"] == hero1_data["name"] - assert data["team"]["name"] == team_z_force["name"] - response = client.patch( - f"/heroes/{hero2_id}", json={"secret_name": "Spider-Youngster"} - ) - assert response.status_code == 200, response.text - response = client.patch("/heroes/9001", json={"name": "Dragon Cube X"}) - assert response.status_code == 404, response.text - response = client.delete(f"/heroes/{hero2_id}") - assert response.status_code == 200, response.text - response = client.get("/heroes/") - assert response.status_code == 200, response.text - data = response.json() - assert len(data) == 2 - response = client.delete("/heroes/9000") - assert response.status_code == 404, response.text - - response = client.get(f"/teams/{team_preventers_id}") - data = response.json() - assert response.status_code == 200, response.text - assert data["name"] == team_preventers_data["name"] - assert data["heroes"][0]["name"] == hero3_data["name"] - - response = client.delete(f"/teams/{team_preventers_id}") - assert response.status_code == 200, response.text - response = client.delete("/teams/9000") - assert response.status_code == 404, response.text - response = client.get("/teams/") - assert response.status_code == 200, response.text - data = response.json() - assert len(data) == 1 - - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/heroes/": { - "get": { - "summary": "Read Heroes", - "operationId": "read_heroes_heroes__get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Offset", - "type": "integer", - "default": 0, - }, - "name": "offset", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Limit", - "maximum": 100.0, - "type": "integer", - "default": 100, - }, - "name": "limit", - "in": "query", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Heroes Heroes Get", - "type": "array", - "items": { - "$ref": "#/components/schemas/HeroPublic" - }, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "post": { - "summary": "Create Hero", - "operationId": "create_hero_heroes__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroCreate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/heroes/{hero_id}": { - "get": { - "summary": "Read Hero", - "operationId": "read_hero_heroes__hero_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublicWithTeam" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "delete": { - "summary": "Delete Hero", - "operationId": "delete_hero_heroes__hero_id__delete", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "patch": { - "summary": "Update Hero", - "operationId": "update_hero_heroes__hero_id__patch", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroUpdate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/teams/": { - "get": { - "summary": "Read Teams", - "operationId": "read_teams_teams__get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Offset", - "type": "integer", - "default": 0, - }, - "name": "offset", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Limit", - "maximum": 100.0, - "type": "integer", - "default": 100, - }, - "name": "limit", - "in": "query", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Teams Teams Get", - "type": "array", - "items": { - "$ref": "#/components/schemas/TeamPublic" - }, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "post": { - "summary": "Create Team", - "operationId": "create_team_teams__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TeamCreate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TeamPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/teams/{team_id}": { - "get": { - "summary": "Read Team", - "operationId": "read_team_teams__team_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Team Id", "type": "integer"}, - "name": "team_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TeamPublicWithHeroes" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "delete": { - "summary": "Delete Team", - "operationId": "delete_team_teams__team_id__delete", - "parameters": [ - { - "required": True, - "schema": {"title": "Team Id", "type": "integer"}, - "name": "team_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "patch": { - "summary": "Update Team", - "operationId": "update_team_teams__team_id__patch", - "parameters": [ - { - "required": True, - "schema": {"title": "Team Id", "type": "integer"}, - "name": "team_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TeamUpdate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TeamPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - } - }, - }, - "HeroCreate": { - "title": "HeroCreate", - "required": ["name", "secret_name"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "team_id": IsDict( - { - "title": "Team Id", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Team Id", "type": "integer"} - ), - }, - }, - "HeroPublic": { - "title": "HeroPublic", - "required": ["name", "secret_name", "id"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "team_id": IsDict( - { - "title": "Team Id", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Team Id", "type": "integer"} - ), - "id": {"title": "Id", "type": "integer"}, - }, - }, - "HeroPublicWithTeam": { - "title": "HeroPublicWithTeam", - "required": ["name", "secret_name", "id"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "team_id": IsDict( - { - "title": "Team Id", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Team Id", "type": "integer"} - ), - "id": {"title": "Id", "type": "integer"}, - "team": IsDict( - { - "anyOf": [ - {"$ref": "#/components/schemas/TeamPublic"}, - {"type": "null"}, - ] - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"$ref": "#/components/schemas/TeamPublic"} - ), - }, - }, - "HeroUpdate": { - "title": "HeroUpdate", - "type": "object", - "properties": { - "name": IsDict( - { - "title": "Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Name", "type": "string"} - ), - "secret_name": IsDict( - { - "title": "Secret Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Secret Name", "type": "string"} - ), - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "team_id": IsDict( - { - "title": "Team Id", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Team Id", "type": "integer"} - ), - }, - }, - "TeamCreate": { - "title": "TeamCreate", - "required": ["name", "headquarters"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "headquarters": {"title": "Headquarters", "type": "string"}, - }, - }, - "TeamPublic": { - "title": "TeamPublic", - "required": ["name", "headquarters", "id"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "headquarters": {"title": "Headquarters", "type": "string"}, - "id": {"title": "Id", "type": "integer"}, - }, - }, - "TeamPublicWithHeroes": { - "title": "TeamPublicWithHeroes", - "required": ["name", "headquarters", "id"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "headquarters": {"title": "Headquarters", "type": "string"}, - "id": {"title": "Id", "type": "integer"}, - "heroes": { - "title": "Heroes", - "type": "array", - "items": {"$ref": "#/components/schemas/HeroPublic"}, - "default": [], - }, - }, - }, - "TeamUpdate": { - "title": "TeamUpdate", - "type": "object", - "properties": { - "id": IsDict( - { - "title": "Id", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Id", "type": "integer"} - ), - "name": IsDict( - { - "title": "Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Name", "type": "string"} - ), - "headquarters": IsDict( - { - "title": "Headquarters", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Headquarters", "type": "string"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001.py index 8f273bbd93..0dbb0fa800 100644 --- a/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001.py @@ -1,18 +1,32 @@ -from dirty_equals import IsDict +import importlib +from types import ModuleType + +import pytest +from dirty_equals import IsOneOf from fastapi.testclient import TestClient from sqlmodel import create_engine from sqlmodel.pool import StaticPool -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.response_model import tutorial001 as mod - +@pytest.fixture( + name="module", + params=[ + pytest.param("tutorial001_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module( + f"docs_src.tutorial.fastapi.response_model.{request.param}" + ) mod.sqlite_url = "sqlite://" mod.engine = create_engine( mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool ) + return mod + - with TestClient(mod.app) as client: +def test_tutorial(module: ModuleType): + with TestClient(module.app) as client: hero_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} response = client.post("/heroes/", json=hero_data) data = response.json() @@ -114,45 +128,54 @@ def test_tutorial(clear_sqlmodel): "required": ["name", "secret_name"], "type": "object", "properties": { - "id": IsDict( - { - "title": "Id", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Id", "type": "integer"} - ), + "id": { + "title": "Id", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), + "age": { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, }, }, "ValidationError": { "title": "ValidationError", "required": ["loc", "msg", "type"], "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] + "properties": IsOneOf( + { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [ + {"type": "string"}, + {"type": "integer"}, + ] + }, }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, + { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [ + {"type": "string"}, + {"type": "integer"}, + ] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, + }, + ), }, } }, diff --git a/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001_py310.py b/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001_py310.py deleted file mode 100644 index d249cc4e90..0000000000 --- a/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001_py310.py +++ /dev/null @@ -1,162 +0,0 @@ -from dirty_equals import IsDict -from fastapi.testclient import TestClient -from sqlmodel import create_engine -from sqlmodel.pool import StaticPool - -from ....conftest import needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.response_model import tutorial001_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine( - mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool - ) - - with TestClient(mod.app) as client: - hero_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} - response = client.post("/heroes/", json=hero_data) - data = response.json() - - assert response.status_code == 200, response.text - assert data["name"] == hero_data["name"] - assert data["secret_name"] == hero_data["secret_name"] - assert data["id"] is not None - assert data["age"] is None - - response = client.get("/heroes/") - data = response.json() - - assert response.status_code == 200, response.text - assert len(data) == 1 - assert data[0]["name"] == hero_data["name"] - assert data[0]["secret_name"] == hero_data["secret_name"] - - response = client.get("/openapi.json") - - assert response.status_code == 200, response.text - - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/heroes/": { - "get": { - "summary": "Read Heroes", - "operationId": "read_heroes_heroes__get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Heroes Heroes Get", - "type": "array", - "items": { - "$ref": "#/components/schemas/Hero" - }, - } - } - }, - } - }, - }, - "post": { - "summary": "Create Hero", - "operationId": "create_hero_heroes__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Hero"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Hero"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - } - }, - }, - "Hero": { - "title": "Hero", - "required": ["name", "secret_name"], - "type": "object", - "properties": { - "id": IsDict( - { - "title": "Id", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Id", "type": "integer"} - ), - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001_py39.py b/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001_py39.py deleted file mode 100644 index b9fb2be03f..0000000000 --- a/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001_py39.py +++ /dev/null @@ -1,162 +0,0 @@ -from dirty_equals import IsDict -from fastapi.testclient import TestClient -from sqlmodel import create_engine -from sqlmodel.pool import StaticPool - -from ....conftest import needs_py39 - - -@needs_py39 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.response_model import tutorial001_py39 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine( - mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool - ) - - with TestClient(mod.app) as client: - hero_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} - response = client.post("/heroes/", json=hero_data) - data = response.json() - - assert response.status_code == 200, response.text - assert data["name"] == hero_data["name"] - assert data["secret_name"] == hero_data["secret_name"] - assert data["id"] is not None - assert data["age"] is None - - response = client.get("/heroes/") - data = response.json() - - assert response.status_code == 200, response.text - assert len(data) == 1 - assert data[0]["name"] == hero_data["name"] - assert data[0]["secret_name"] == hero_data["secret_name"] - - response = client.get("/openapi.json") - - assert response.status_code == 200, response.text - - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/heroes/": { - "get": { - "summary": "Read Heroes", - "operationId": "read_heroes_heroes__get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Heroes Heroes Get", - "type": "array", - "items": { - "$ref": "#/components/schemas/Hero" - }, - } - } - }, - } - }, - }, - "post": { - "summary": "Create Hero", - "operationId": "create_hero_heroes__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Hero"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Hero"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - } - }, - }, - "Hero": { - "title": "Hero", - "required": ["name", "secret_name"], - "type": "object", - "properties": { - "id": IsDict( - { - "title": "Id", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Id", "type": "integer"} - ), - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001.py index 388cfa9b2b..837c7b0fc4 100644 --- a/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001.py @@ -1,18 +1,32 @@ -from dirty_equals import IsDict +import importlib +from types import ModuleType + +import pytest +from dirty_equals import IsOneOf from fastapi.testclient import TestClient from sqlmodel import create_engine from sqlmodel.pool import StaticPool -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.session_with_dependency import tutorial001 as mod - +@pytest.fixture( + name="module", + params=[ + pytest.param("tutorial001_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module( + f"docs_src.tutorial.fastapi.session_with_dependency.{request.param}" + ) mod.sqlite_url = "sqlite://" mod.engine = create_engine( mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool ) + return mod - with TestClient(mod.app) as client: + +def test_tutorial(module: ModuleType): + with TestClient(module.app) as client: hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} hero2_data = { "name": "Spider-Boy", @@ -285,16 +299,10 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), + "age": { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, }, }, "HeroPublic": { @@ -304,16 +312,10 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), + "age": { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, "id": {"title": "Id", "type": "integer"}, }, }, @@ -321,53 +323,56 @@ def test_tutorial(clear_sqlmodel): "title": "HeroUpdate", "type": "object", "properties": { - "name": IsDict( - { - "title": "Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Name", "type": "string"} - ), - "secret_name": IsDict( - { - "title": "Secret Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Secret Name", "type": "string"} - ), - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), + "name": { + "title": "Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "secret_name": { + "title": "Secret Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "age": { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, }, }, "ValidationError": { "title": "ValidationError", "required": ["loc", "msg", "type"], "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] + "properties": IsOneOf( + { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [ + {"type": "string"}, + {"type": "integer"}, + ] + }, }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, + { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [ + {"type": "string"}, + {"type": "integer"}, + ] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, + }, + ), }, } }, diff --git a/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001_py310.py b/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001_py310.py deleted file mode 100644 index 65bab47735..0000000000 --- a/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001_py310.py +++ /dev/null @@ -1,379 +0,0 @@ -from dirty_equals import IsDict -from fastapi.testclient import TestClient -from sqlmodel import create_engine -from sqlmodel.pool import StaticPool - -from ....conftest import needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.session_with_dependency import ( - tutorial001_py310 as mod, - ) - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine( - mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool - ) - - with TestClient(mod.app) as client: - hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} - hero2_data = { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "id": 9000, - } - hero3_data = { - "name": "Rusty-Man", - "secret_name": "Tommy Sharp", - "age": 48, - } - response = client.post("/heroes/", json=hero1_data) - assert response.status_code == 200, response.text - response = client.post("/heroes/", json=hero2_data) - assert response.status_code == 200, response.text - hero2 = response.json() - hero2_id = hero2["id"] - response = client.post("/heroes/", json=hero3_data) - assert response.status_code == 200, response.text - response = client.get(f"/heroes/{hero2_id}") - assert response.status_code == 200, response.text - response = client.get("/heroes/9000") - assert response.status_code == 404, response.text - response = client.get("/heroes/") - assert response.status_code == 200, response.text - data = response.json() - assert len(data) == 3 - response = client.patch( - f"/heroes/{hero2_id}", json={"secret_name": "Spider-Youngster"} - ) - assert response.status_code == 200, response.text - response = client.patch("/heroes/9001", json={"name": "Dragon Cube X"}) - assert response.status_code == 404, response.text - - response = client.delete(f"/heroes/{hero2_id}") - assert response.status_code == 200, response.text - response = client.get("/heroes/") - assert response.status_code == 200, response.text - data = response.json() - assert len(data) == 2 - - response = client.delete("/heroes/9000") - assert response.status_code == 404, response.text - - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/heroes/": { - "get": { - "summary": "Read Heroes", - "operationId": "read_heroes_heroes__get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Offset", - "type": "integer", - "default": 0, - }, - "name": "offset", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Limit", - "maximum": 100.0, - "type": "integer", - "default": 100, - }, - "name": "limit", - "in": "query", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Heroes Heroes Get", - "type": "array", - "items": { - "$ref": "#/components/schemas/HeroPublic" - }, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "post": { - "summary": "Create Hero", - "operationId": "create_hero_heroes__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroCreate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/heroes/{hero_id}": { - "get": { - "summary": "Read Hero", - "operationId": "read_hero_heroes__hero_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "delete": { - "summary": "Delete Hero", - "operationId": "delete_hero_heroes__hero_id__delete", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "patch": { - "summary": "Update Hero", - "operationId": "update_hero_heroes__hero_id__patch", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroUpdate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - } - }, - }, - "HeroCreate": { - "title": "HeroCreate", - "required": ["name", "secret_name"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - }, - }, - "HeroPublic": { - "title": "HeroPublic", - "required": ["name", "secret_name", "id"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "id": {"title": "Id", "type": "integer"}, - }, - }, - "HeroUpdate": { - "title": "HeroUpdate", - "type": "object", - "properties": { - "name": IsDict( - { - "title": "Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Name", "type": "string"} - ), - "secret_name": IsDict( - { - "title": "Secret Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Secret Name", "type": "string"} - ), - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001_py39.py b/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001_py39.py deleted file mode 100644 index cdab85df17..0000000000 --- a/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001_py39.py +++ /dev/null @@ -1,379 +0,0 @@ -from dirty_equals import IsDict -from fastapi.testclient import TestClient -from sqlmodel import create_engine -from sqlmodel.pool import StaticPool - -from ....conftest import needs_py39 - - -@needs_py39 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.session_with_dependency import ( - tutorial001_py39 as mod, - ) - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine( - mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool - ) - - with TestClient(mod.app) as client: - hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} - hero2_data = { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "id": 9000, - } - hero3_data = { - "name": "Rusty-Man", - "secret_name": "Tommy Sharp", - "age": 48, - } - response = client.post("/heroes/", json=hero1_data) - assert response.status_code == 200, response.text - response = client.post("/heroes/", json=hero2_data) - assert response.status_code == 200, response.text - hero2 = response.json() - hero2_id = hero2["id"] - response = client.post("/heroes/", json=hero3_data) - assert response.status_code == 200, response.text - response = client.get(f"/heroes/{hero2_id}") - assert response.status_code == 200, response.text - response = client.get("/heroes/9000") - assert response.status_code == 404, response.text - response = client.get("/heroes/") - assert response.status_code == 200, response.text - data = response.json() - assert len(data) == 3 - response = client.patch( - f"/heroes/{hero2_id}", json={"secret_name": "Spider-Youngster"} - ) - assert response.status_code == 200, response.text - response = client.patch("/heroes/9001", json={"name": "Dragon Cube X"}) - assert response.status_code == 404, response.text - - response = client.delete(f"/heroes/{hero2_id}") - assert response.status_code == 200, response.text - response = client.get("/heroes/") - assert response.status_code == 200, response.text - data = response.json() - assert len(data) == 2 - - response = client.delete("/heroes/9000") - assert response.status_code == 404, response.text - - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/heroes/": { - "get": { - "summary": "Read Heroes", - "operationId": "read_heroes_heroes__get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Offset", - "type": "integer", - "default": 0, - }, - "name": "offset", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Limit", - "maximum": 100.0, - "type": "integer", - "default": 100, - }, - "name": "limit", - "in": "query", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Heroes Heroes Get", - "type": "array", - "items": { - "$ref": "#/components/schemas/HeroPublic" - }, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "post": { - "summary": "Create Hero", - "operationId": "create_hero_heroes__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroCreate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/heroes/{hero_id}": { - "get": { - "summary": "Read Hero", - "operationId": "read_hero_heroes__hero_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "delete": { - "summary": "Delete Hero", - "operationId": "delete_hero_heroes__hero_id__delete", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "patch": { - "summary": "Update Hero", - "operationId": "update_hero_heroes__hero_id__patch", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroUpdate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - } - }, - }, - "HeroCreate": { - "title": "HeroCreate", - "required": ["name", "secret_name"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - }, - }, - "HeroPublic": { - "title": "HeroPublic", - "required": ["name", "secret_name", "id"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "id": {"title": "Id", "type": "integer"}, - }, - }, - "HeroUpdate": { - "title": "HeroUpdate", - "type": "object", - "properties": { - "name": IsDict( - { - "title": "Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Name", "type": "string"} - ), - "secret_name": IsDict( - { - "title": "Secret Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Secret Name", "type": "string"} - ), - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_fastapi/test_simple_hero_api/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_simple_hero_api/test_tutorial001.py index 9df7e50b81..7234a52d9b 100644 --- a/tests/test_tutorial/test_fastapi/test_simple_hero_api/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_simple_hero_api/test_tutorial001.py @@ -1,18 +1,32 @@ -from dirty_equals import IsDict +import importlib +from types import ModuleType + +import pytest +from dirty_equals import IsOneOf from fastapi.testclient import TestClient from sqlmodel import create_engine from sqlmodel.pool import StaticPool -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.simple_hero_api import tutorial001 as mod - +@pytest.fixture( + name="module", + params=[ + pytest.param("tutorial001_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module( + f"docs_src.tutorial.fastapi.simple_hero_api.{request.param}" + ) mod.sqlite_url = "sqlite://" mod.engine = create_engine( mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool ) + return mod + - with TestClient(mod.app) as client: +def test_tutorial(module: ModuleType): + with TestClient(module.app) as client: hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} hero2_data = { "name": "Spider-Boy", @@ -120,45 +134,54 @@ def test_tutorial(clear_sqlmodel): "required": ["name", "secret_name"], "type": "object", "properties": { - "id": IsDict( - { - "title": "Id", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Id", "type": "integer"} - ), + "id": { + "title": "Id", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), + "age": { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, }, }, "ValidationError": { "title": "ValidationError", "required": ["loc", "msg", "type"], "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] + "properties": IsOneOf( + { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [ + {"type": "string"}, + {"type": "integer"}, + ] + }, }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, + { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [ + {"type": "string"}, + {"type": "integer"}, + ] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, + }, + ), }, } }, diff --git a/tests/test_tutorial/test_fastapi/test_simple_hero_api/test_tutorial001_py310.py b/tests/test_tutorial/test_fastapi/test_simple_hero_api/test_tutorial001_py310.py deleted file mode 100644 index a47513dde2..0000000000 --- a/tests/test_tutorial/test_fastapi/test_simple_hero_api/test_tutorial001_py310.py +++ /dev/null @@ -1,168 +0,0 @@ -from dirty_equals import IsDict -from fastapi.testclient import TestClient -from sqlmodel import create_engine -from sqlmodel.pool import StaticPool - -from ....conftest import needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.simple_hero_api import tutorial001_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine( - mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool - ) - - with TestClient(mod.app) as client: - hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} - hero2_data = { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "id": 9000, - } - response = client.post("/heroes/", json=hero1_data) - data = response.json() - - assert response.status_code == 200, response.text - assert data["name"] == hero1_data["name"] - assert data["secret_name"] == hero1_data["secret_name"] - assert data["id"] is not None - assert data["age"] is None - - response = client.post("/heroes/", json=hero2_data) - data = response.json() - - assert response.status_code == 200, response.text - assert data["name"] == hero2_data["name"] - assert data["secret_name"] == hero2_data["secret_name"] - assert data["id"] == hero2_data["id"], ( - "Up to this point it's still possible to " - "set the ID of the hero in the request" - ) - assert data["age"] is None - - response = client.get("/heroes/") - data = response.json() - - assert response.status_code == 200, response.text - assert len(data) == 2 - assert data[0]["name"] == hero1_data["name"] - assert data[0]["secret_name"] == hero1_data["secret_name"] - assert data[1]["name"] == hero2_data["name"] - assert data[1]["secret_name"] == hero2_data["secret_name"] - assert data[1]["id"] == hero2_data["id"] - - response = client.get("/openapi.json") - - assert response.status_code == 200, response.text - - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/heroes/": { - "get": { - "summary": "Read Heroes", - "operationId": "read_heroes_heroes__get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - }, - "post": { - "summary": "Create Hero", - "operationId": "create_hero_heroes__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Hero"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - } - }, - }, - "Hero": { - "title": "Hero", - "required": ["name", "secret_name"], - "type": "object", - "properties": { - "id": IsDict( - { - "title": "Id", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Id", "type": "integer"} - ), - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001.py index 25daadf74b..b5ec2a975a 100644 --- a/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001.py @@ -1,18 +1,30 @@ -from dirty_equals import IsDict +import importlib +from types import ModuleType + +import pytest +from dirty_equals import IsOneOf from fastapi.testclient import TestClient from sqlmodel import create_engine from sqlmodel.pool import StaticPool -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.teams import tutorial001 as mod - +@pytest.fixture( + name="module", + params=[ + pytest.param("tutorial001_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.fastapi.teams.{request.param}") mod.sqlite_url = "sqlite://" mod.engine = create_engine( mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool ) + return mod - with TestClient(mod.app) as client: + +def test_tutorial(module: ModuleType): + with TestClient(module.app) as client: hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} hero2_data = { "name": "Spider-Boy", @@ -519,26 +531,14 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "team_id": IsDict( - { - "title": "Team Id", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Team Id", "type": "integer"} - ), + "age": { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, + "team_id": { + "title": "Team Id", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, }, }, "HeroPublic": { @@ -548,26 +548,14 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "team_id": IsDict( - { - "title": "Team Id", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Team Id", "type": "integer"} - ), + "age": { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, + "team_id": { + "title": "Team Id", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, "id": {"title": "Id", "type": "integer"}, }, }, @@ -575,46 +563,22 @@ def test_tutorial(clear_sqlmodel): "title": "HeroUpdate", "type": "object", "properties": { - "name": IsDict( - { - "title": "Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Name", "type": "string"} - ), - "secret_name": IsDict( - { - "title": "Secret Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Secret Name", "type": "string"} - ), - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "team_id": IsDict( - { - "title": "Team Id", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Team Id", "type": "integer"} - ), + "name": { + "title": "Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "secret_name": { + "title": "Secret Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "age": { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, + "team_id": { + "title": "Team Id", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, }, }, "TeamCreate": { @@ -640,43 +604,52 @@ def test_tutorial(clear_sqlmodel): "title": "TeamUpdate", "type": "object", "properties": { - "name": IsDict( - { - "title": "Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Name", "type": "string"} - ), - "headquarters": IsDict( - { - "title": "Headquarters", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Headquarters", "type": "string"} - ), + "name": { + "title": "Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "headquarters": { + "title": "Headquarters", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, }, }, "ValidationError": { "title": "ValidationError", "required": ["loc", "msg", "type"], "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] + "properties": IsOneOf( + { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [ + {"type": "string"}, + {"type": "integer"}, + ] + }, }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, + { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [ + {"type": "string"}, + {"type": "integer"}, + ] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, + }, + ), }, } }, diff --git a/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001_py310.py b/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001_py310.py deleted file mode 100644 index 63f8a1d70b..0000000000 --- a/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001_py310.py +++ /dev/null @@ -1,686 +0,0 @@ -from dirty_equals import IsDict -from fastapi.testclient import TestClient -from sqlmodel import create_engine -from sqlmodel.pool import StaticPool - -from ....conftest import needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.teams import tutorial001_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine( - mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool - ) - - with TestClient(mod.app) as client: - hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} - hero2_data = { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "id": 9000, - } - hero3_data = { - "name": "Rusty-Man", - "secret_name": "Tommy Sharp", - "age": 48, - } - response = client.post("/heroes/", json=hero1_data) - assert response.status_code == 200, response.text - response = client.post("/heroes/", json=hero2_data) - assert response.status_code == 200, response.text - hero2 = response.json() - hero2_id = hero2["id"] - response = client.post("/heroes/", json=hero3_data) - assert response.status_code == 200, response.text - response = client.get(f"/heroes/{hero2_id}") - assert response.status_code == 200, response.text - response = client.get("/heroes/9000") - assert response.status_code == 404, response.text - response = client.get("/heroes/") - assert response.status_code == 200, response.text - data = response.json() - assert len(data) == 3 - response = client.patch( - f"/heroes/{hero2_id}", json={"secret_name": "Spider-Youngster"} - ) - assert response.status_code == 200, response.text - response = client.patch("/heroes/9001", json={"name": "Dragon Cube X"}) - assert response.status_code == 404, response.text - response = client.delete(f"/heroes/{hero2_id}") - assert response.status_code == 200, response.text - response = client.get("/heroes/") - assert response.status_code == 200, response.text - data = response.json() - assert len(data) == 2 - response = client.delete("/heroes/9000") - assert response.status_code == 404, response.text - - team_preventers = {"name": "Preventers", "headquarters": "Sharp Tower"} - team_z_force = {"name": "Z-Force", "headquarters": "Sister Margaret's Bar"} - response = client.post("/teams/", json=team_preventers) - assert response.status_code == 200, response.text - team_preventers_data = response.json() - team_preventers_id = team_preventers_data["id"] - response = client.post("/teams/", json=team_z_force) - assert response.status_code == 200, response.text - team_z_force_data = response.json() - team_z_force_data["id"] - response = client.get("/teams/") - data = response.json() - assert len(data) == 2 - response = client.get(f"/teams/{team_preventers_id}") - data = response.json() - assert response.status_code == 200, response.text - assert data == team_preventers_data - response = client.get("/teams/9000") - assert response.status_code == 404, response.text - response = client.patch( - f"/teams/{team_preventers_id}", json={"headquarters": "Preventers Tower"} - ) - data = response.json() - assert response.status_code == 200, response.text - assert data["name"] == team_preventers["name"] - assert data["headquarters"] == "Preventers Tower" - response = client.patch("/teams/9000", json={"name": "Freedom League"}) - assert response.status_code == 404, response.text - response = client.delete(f"/teams/{team_preventers_id}") - assert response.status_code == 200, response.text - response = client.delete("/teams/9000") - assert response.status_code == 404, response.text - response = client.get("/teams/") - assert response.status_code == 200, response.text - data = response.json() - assert len(data) == 1 - - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/heroes/": { - "get": { - "summary": "Read Heroes", - "operationId": "read_heroes_heroes__get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Offset", - "type": "integer", - "default": 0, - }, - "name": "offset", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Limit", - "maximum": 100.0, - "type": "integer", - "default": 100, - }, - "name": "limit", - "in": "query", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Heroes Heroes Get", - "type": "array", - "items": { - "$ref": "#/components/schemas/HeroPublic" - }, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "post": { - "summary": "Create Hero", - "operationId": "create_hero_heroes__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroCreate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/heroes/{hero_id}": { - "get": { - "summary": "Read Hero", - "operationId": "read_hero_heroes__hero_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "delete": { - "summary": "Delete Hero", - "operationId": "delete_hero_heroes__hero_id__delete", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "patch": { - "summary": "Update Hero", - "operationId": "update_hero_heroes__hero_id__patch", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroUpdate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/teams/": { - "get": { - "summary": "Read Teams", - "operationId": "read_teams_teams__get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Offset", - "type": "integer", - "default": 0, - }, - "name": "offset", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Limit", - "maximum": 100.0, - "type": "integer", - "default": 100, - }, - "name": "limit", - "in": "query", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Teams Teams Get", - "type": "array", - "items": { - "$ref": "#/components/schemas/TeamPublic" - }, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "post": { - "summary": "Create Team", - "operationId": "create_team_teams__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TeamCreate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TeamPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/teams/{team_id}": { - "get": { - "summary": "Read Team", - "operationId": "read_team_teams__team_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Team Id", "type": "integer"}, - "name": "team_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TeamPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "delete": { - "summary": "Delete Team", - "operationId": "delete_team_teams__team_id__delete", - "parameters": [ - { - "required": True, - "schema": {"title": "Team Id", "type": "integer"}, - "name": "team_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "patch": { - "summary": "Update Team", - "operationId": "update_team_teams__team_id__patch", - "parameters": [ - { - "required": True, - "schema": {"title": "Team Id", "type": "integer"}, - "name": "team_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TeamUpdate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TeamPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - } - }, - }, - "HeroCreate": { - "title": "HeroCreate", - "required": ["name", "secret_name"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "team_id": IsDict( - { - "title": "Team Id", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Team Id", "type": "integer"} - ), - }, - }, - "HeroPublic": { - "title": "HeroPublic", - "required": ["name", "secret_name", "id"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "team_id": IsDict( - { - "title": "Team Id", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Team Id", "type": "integer"} - ), - "id": {"title": "Id", "type": "integer"}, - }, - }, - "HeroUpdate": { - "title": "HeroUpdate", - "type": "object", - "properties": { - "name": IsDict( - { - "title": "Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Name", "type": "string"} - ), - "secret_name": IsDict( - { - "title": "Secret Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Secret Name", "type": "string"} - ), - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "team_id": IsDict( - { - "title": "Team Id", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Team Id", "type": "integer"} - ), - }, - }, - "TeamCreate": { - "title": "TeamCreate", - "required": ["name", "headquarters"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "headquarters": {"title": "Headquarters", "type": "string"}, - }, - }, - "TeamPublic": { - "title": "TeamPublic", - "required": ["name", "headquarters", "id"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "headquarters": {"title": "Headquarters", "type": "string"}, - "id": {"title": "Id", "type": "integer"}, - }, - }, - "TeamUpdate": { - "title": "TeamUpdate", - "type": "object", - "properties": { - "name": IsDict( - { - "title": "Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Name", "type": "string"} - ), - "headquarters": IsDict( - { - "title": "Headquarters", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Headquarters", "type": "string"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001_py39.py b/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001_py39.py deleted file mode 100644 index 30b68e0ed9..0000000000 --- a/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001_py39.py +++ /dev/null @@ -1,686 +0,0 @@ -from dirty_equals import IsDict -from fastapi.testclient import TestClient -from sqlmodel import create_engine -from sqlmodel.pool import StaticPool - -from ....conftest import needs_py39 - - -@needs_py39 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.teams import tutorial001_py39 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine( - mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool - ) - - with TestClient(mod.app) as client: - hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} - hero2_data = { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "id": 9000, - } - hero3_data = { - "name": "Rusty-Man", - "secret_name": "Tommy Sharp", - "age": 48, - } - response = client.post("/heroes/", json=hero1_data) - assert response.status_code == 200, response.text - response = client.post("/heroes/", json=hero2_data) - assert response.status_code == 200, response.text - hero2 = response.json() - hero2_id = hero2["id"] - response = client.post("/heroes/", json=hero3_data) - assert response.status_code == 200, response.text - response = client.get(f"/heroes/{hero2_id}") - assert response.status_code == 200, response.text - response = client.get("/heroes/9000") - assert response.status_code == 404, response.text - response = client.get("/heroes/") - assert response.status_code == 200, response.text - data = response.json() - assert len(data) == 3 - response = client.patch( - f"/heroes/{hero2_id}", json={"secret_name": "Spider-Youngster"} - ) - assert response.status_code == 200, response.text - response = client.patch("/heroes/9001", json={"name": "Dragon Cube X"}) - assert response.status_code == 404, response.text - response = client.delete(f"/heroes/{hero2_id}") - assert response.status_code == 200, response.text - response = client.get("/heroes/") - assert response.status_code == 200, response.text - data = response.json() - assert len(data) == 2 - response = client.delete("/heroes/9000") - assert response.status_code == 404, response.text - - team_preventers = {"name": "Preventers", "headquarters": "Sharp Tower"} - team_z_force = {"name": "Z-Force", "headquarters": "Sister Margaret's Bar"} - response = client.post("/teams/", json=team_preventers) - assert response.status_code == 200, response.text - team_preventers_data = response.json() - team_preventers_id = team_preventers_data["id"] - response = client.post("/teams/", json=team_z_force) - assert response.status_code == 200, response.text - team_z_force_data = response.json() - team_z_force_data["id"] - response = client.get("/teams/") - data = response.json() - assert len(data) == 2 - response = client.get(f"/teams/{team_preventers_id}") - data = response.json() - assert response.status_code == 200, response.text - assert data == team_preventers_data - response = client.get("/teams/9000") - assert response.status_code == 404, response.text - response = client.patch( - f"/teams/{team_preventers_id}", json={"headquarters": "Preventers Tower"} - ) - data = response.json() - assert response.status_code == 200, response.text - assert data["name"] == team_preventers["name"] - assert data["headquarters"] == "Preventers Tower" - response = client.patch("/teams/9000", json={"name": "Freedom League"}) - assert response.status_code == 404, response.text - response = client.delete(f"/teams/{team_preventers_id}") - assert response.status_code == 200, response.text - response = client.delete("/teams/9000") - assert response.status_code == 404, response.text - response = client.get("/teams/") - assert response.status_code == 200, response.text - data = response.json() - assert len(data) == 1 - - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/heroes/": { - "get": { - "summary": "Read Heroes", - "operationId": "read_heroes_heroes__get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Offset", - "type": "integer", - "default": 0, - }, - "name": "offset", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Limit", - "maximum": 100.0, - "type": "integer", - "default": 100, - }, - "name": "limit", - "in": "query", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Heroes Heroes Get", - "type": "array", - "items": { - "$ref": "#/components/schemas/HeroPublic" - }, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "post": { - "summary": "Create Hero", - "operationId": "create_hero_heroes__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroCreate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/heroes/{hero_id}": { - "get": { - "summary": "Read Hero", - "operationId": "read_hero_heroes__hero_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "delete": { - "summary": "Delete Hero", - "operationId": "delete_hero_heroes__hero_id__delete", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "patch": { - "summary": "Update Hero", - "operationId": "update_hero_heroes__hero_id__patch", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroUpdate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/teams/": { - "get": { - "summary": "Read Teams", - "operationId": "read_teams_teams__get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Offset", - "type": "integer", - "default": 0, - }, - "name": "offset", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Limit", - "maximum": 100.0, - "type": "integer", - "default": 100, - }, - "name": "limit", - "in": "query", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Teams Teams Get", - "type": "array", - "items": { - "$ref": "#/components/schemas/TeamPublic" - }, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "post": { - "summary": "Create Team", - "operationId": "create_team_teams__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TeamCreate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TeamPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/teams/{team_id}": { - "get": { - "summary": "Read Team", - "operationId": "read_team_teams__team_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Team Id", "type": "integer"}, - "name": "team_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TeamPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "delete": { - "summary": "Delete Team", - "operationId": "delete_team_teams__team_id__delete", - "parameters": [ - { - "required": True, - "schema": {"title": "Team Id", "type": "integer"}, - "name": "team_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "patch": { - "summary": "Update Team", - "operationId": "update_team_teams__team_id__patch", - "parameters": [ - { - "required": True, - "schema": {"title": "Team Id", "type": "integer"}, - "name": "team_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TeamUpdate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TeamPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - } - }, - }, - "HeroCreate": { - "title": "HeroCreate", - "required": ["name", "secret_name"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "team_id": IsDict( - { - "title": "Team Id", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Team Id", "type": "integer"} - ), - }, - }, - "HeroPublic": { - "title": "HeroPublic", - "required": ["name", "secret_name", "id"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "team_id": IsDict( - { - "title": "Team Id", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Team Id", "type": "integer"} - ), - "id": {"title": "Id", "type": "integer"}, - }, - }, - "HeroUpdate": { - "title": "HeroUpdate", - "type": "object", - "properties": { - "name": IsDict( - { - "title": "Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Name", "type": "string"} - ), - "secret_name": IsDict( - { - "title": "Secret Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Secret Name", "type": "string"} - ), - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "team_id": IsDict( - { - "title": "Team Id", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Team Id", "type": "integer"} - ), - }, - }, - "TeamCreate": { - "title": "TeamCreate", - "required": ["name", "headquarters"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "headquarters": {"title": "Headquarters", "type": "string"}, - }, - }, - "TeamPublic": { - "title": "TeamPublic", - "required": ["name", "headquarters", "id"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "headquarters": {"title": "Headquarters", "type": "string"}, - "id": {"title": "Id", "type": "integer"}, - }, - }, - "TeamUpdate": { - "title": "TeamUpdate", - "type": "object", - "properties": { - "name": IsDict( - { - "title": "Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Name", "type": "string"} - ), - "headquarters": IsDict( - { - "title": "Headquarters", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Headquarters", "type": "string"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_fastapi/test_update/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_update/test_tutorial001.py index 0856b24676..417a054383 100644 --- a/tests/test_tutorial/test_fastapi/test_update/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_update/test_tutorial001.py @@ -1,18 +1,30 @@ -from dirty_equals import IsDict +import importlib +from types import ModuleType + +import pytest +from dirty_equals import IsOneOf from fastapi.testclient import TestClient from sqlmodel import create_engine from sqlmodel.pool import StaticPool -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.update import tutorial001 as mod - +@pytest.fixture( + name="module", + params=[ + pytest.param("tutorial001_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.fastapi.update.{request.param}") mod.sqlite_url = "sqlite://" mod.engine = create_engine( mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool ) + return mod + - with TestClient(mod.app) as client: +def test_tutorial(module: ModuleType): + with TestClient(module.app) as client: hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} hero2_data = { "name": "Spider-Boy", @@ -49,16 +61,16 @@ def test_tutorial(clear_sqlmodel): data = response.json() assert response.status_code == 200, response.text assert data["name"] == hero2_data["name"], "The name should not be set to none" - assert ( - data["secret_name"] == "Spider-Youngster" - ), "The secret name should be updated" + assert data["secret_name"] == "Spider-Youngster", ( + "The secret name should be updated" + ) response = client.patch(f"/heroes/{hero3_id}", json={"age": None}) data = response.json() assert response.status_code == 200, response.text assert data["name"] == hero3_data["name"] assert data["age"] is None, ( - "A field should be updatable to None, even if " "that's the default" + "A field should be updatable to None, even if that's the default" ) response = client.patch("/heroes/9001", json={"name": "Dragon Cube X"}) @@ -264,16 +276,10 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), + "age": { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, }, }, "HeroPublic": { @@ -283,16 +289,10 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), + "age": { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, "id": {"title": "Id", "type": "integer"}, }, }, @@ -300,53 +300,56 @@ def test_tutorial(clear_sqlmodel): "title": "HeroUpdate", "type": "object", "properties": { - "name": IsDict( - { - "title": "Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Name", "type": "string"} - ), - "secret_name": IsDict( - { - "title": "Secret Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Secret Name", "type": "string"} - ), - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), + "name": { + "title": "Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "secret_name": { + "title": "Secret Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "age": { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, }, }, "ValidationError": { "title": "ValidationError", "required": ["loc", "msg", "type"], "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] + "properties": IsOneOf( + { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [ + {"type": "string"}, + {"type": "integer"}, + ] + }, }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, + { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [ + {"type": "string"}, + {"type": "integer"}, + ] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, + }, + ), }, } }, diff --git a/tests/test_tutorial/test_fastapi/test_update/test_tutorial001_py310.py b/tests/test_tutorial/test_fastapi/test_update/test_tutorial001_py310.py deleted file mode 100644 index d79b2ecea3..0000000000 --- a/tests/test_tutorial/test_fastapi/test_update/test_tutorial001_py310.py +++ /dev/null @@ -1,356 +0,0 @@ -from dirty_equals import IsDict -from fastapi.testclient import TestClient -from sqlmodel import create_engine -from sqlmodel.pool import StaticPool - -from ....conftest import needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.update import tutorial001_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine( - mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool - ) - - with TestClient(mod.app) as client: - hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} - hero2_data = { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "id": 9000, - } - hero3_data = { - "name": "Rusty-Man", - "secret_name": "Tommy Sharp", - "age": 48, - } - response = client.post("/heroes/", json=hero1_data) - assert response.status_code == 200, response.text - response = client.post("/heroes/", json=hero2_data) - assert response.status_code == 200, response.text - hero2 = response.json() - hero2_id = hero2["id"] - response = client.post("/heroes/", json=hero3_data) - assert response.status_code == 200, response.text - hero3 = response.json() - hero3_id = hero3["id"] - response = client.get(f"/heroes/{hero2_id}") - assert response.status_code == 200, response.text - response = client.get("/heroes/9000") - assert response.status_code == 404, response.text - response = client.get("/heroes/") - assert response.status_code == 200, response.text - data = response.json() - assert len(data) == 3 - - response = client.patch( - f"/heroes/{hero2_id}", json={"secret_name": "Spider-Youngster"} - ) - data = response.json() - assert response.status_code == 200, response.text - assert data["name"] == hero2_data["name"], "The name should not be set to none" - assert ( - data["secret_name"] == "Spider-Youngster" - ), "The secret name should be updated" - - response = client.patch(f"/heroes/{hero3_id}", json={"age": None}) - data = response.json() - assert response.status_code == 200, response.text - assert data["name"] == hero3_data["name"] - assert data["age"] is None, ( - "A field should be updatable to None, even if " "that's the default" - ) - - response = client.patch("/heroes/9001", json={"name": "Dragon Cube X"}) - assert response.status_code == 404, response.text - - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/heroes/": { - "get": { - "summary": "Read Heroes", - "operationId": "read_heroes_heroes__get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Offset", - "type": "integer", - "default": 0, - }, - "name": "offset", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Limit", - "maximum": 100.0, - "type": "integer", - "default": 100, - }, - "name": "limit", - "in": "query", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Heroes Heroes Get", - "type": "array", - "items": { - "$ref": "#/components/schemas/HeroPublic" - }, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "post": { - "summary": "Create Hero", - "operationId": "create_hero_heroes__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroCreate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/heroes/{hero_id}": { - "get": { - "summary": "Read Hero", - "operationId": "read_hero_heroes__hero_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "patch": { - "summary": "Update Hero", - "operationId": "update_hero_heroes__hero_id__patch", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroUpdate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - } - }, - }, - "HeroCreate": { - "title": "HeroCreate", - "required": ["name", "secret_name"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - }, - }, - "HeroPublic": { - "title": "HeroPublic", - "required": ["name", "secret_name", "id"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "id": {"title": "Id", "type": "integer"}, - }, - }, - "HeroUpdate": { - "title": "HeroUpdate", - "type": "object", - "properties": { - "name": IsDict( - { - "title": "Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Name", "type": "string"} - ), - "secret_name": IsDict( - { - "title": "Secret Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Secret Name", "type": "string"} - ), - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_fastapi/test_update/test_tutorial001_py39.py b/tests/test_tutorial/test_fastapi/test_update/test_tutorial001_py39.py deleted file mode 100644 index 1be81dec2e..0000000000 --- a/tests/test_tutorial/test_fastapi/test_update/test_tutorial001_py39.py +++ /dev/null @@ -1,356 +0,0 @@ -from dirty_equals import IsDict -from fastapi.testclient import TestClient -from sqlmodel import create_engine -from sqlmodel.pool import StaticPool - -from ....conftest import needs_py39 - - -@needs_py39 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.update import tutorial001_py39 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine( - mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool - ) - - with TestClient(mod.app) as client: - hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} - hero2_data = { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "id": 9000, - } - hero3_data = { - "name": "Rusty-Man", - "secret_name": "Tommy Sharp", - "age": 48, - } - response = client.post("/heroes/", json=hero1_data) - assert response.status_code == 200, response.text - response = client.post("/heroes/", json=hero2_data) - assert response.status_code == 200, response.text - hero2 = response.json() - hero2_id = hero2["id"] - response = client.post("/heroes/", json=hero3_data) - assert response.status_code == 200, response.text - hero3 = response.json() - hero3_id = hero3["id"] - response = client.get(f"/heroes/{hero2_id}") - assert response.status_code == 200, response.text - response = client.get("/heroes/9000") - assert response.status_code == 404, response.text - response = client.get("/heroes/") - assert response.status_code == 200, response.text - data = response.json() - assert len(data) == 3 - - response = client.patch( - f"/heroes/{hero2_id}", json={"secret_name": "Spider-Youngster"} - ) - data = response.json() - assert response.status_code == 200, response.text - assert data["name"] == hero2_data["name"], "The name should not be set to none" - assert ( - data["secret_name"] == "Spider-Youngster" - ), "The secret name should be updated" - - response = client.patch(f"/heroes/{hero3_id}", json={"age": None}) - data = response.json() - assert response.status_code == 200, response.text - assert data["name"] == hero3_data["name"] - assert data["age"] is None, ( - "A field should be updatable to None, even if " "that's the default" - ) - - response = client.patch("/heroes/9001", json={"name": "Dragon Cube X"}) - assert response.status_code == 404, response.text - - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/heroes/": { - "get": { - "summary": "Read Heroes", - "operationId": "read_heroes_heroes__get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Offset", - "type": "integer", - "default": 0, - }, - "name": "offset", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Limit", - "maximum": 100.0, - "type": "integer", - "default": 100, - }, - "name": "limit", - "in": "query", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Heroes Heroes Get", - "type": "array", - "items": { - "$ref": "#/components/schemas/HeroPublic" - }, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "post": { - "summary": "Create Hero", - "operationId": "create_hero_heroes__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroCreate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/heroes/{hero_id}": { - "get": { - "summary": "Read Hero", - "operationId": "read_hero_heroes__hero_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "patch": { - "summary": "Update Hero", - "operationId": "update_hero_heroes__hero_id__patch", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroUpdate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - } - }, - }, - "HeroCreate": { - "title": "HeroCreate", - "required": ["name", "secret_name"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - }, - }, - "HeroPublic": { - "title": "HeroPublic", - "required": ["name", "secret_name", "id"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "id": {"title": "Id", "type": "integer"}, - }, - }, - "HeroUpdate": { - "title": "HeroUpdate", - "type": "object", - "properties": { - "name": IsDict( - { - "title": "Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Name", "type": "string"} - ), - "secret_name": IsDict( - { - "title": "Secret Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Secret Name", "type": "string"} - ), - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_fastapi/test_update/test_tutorial002.py b/tests/test_tutorial/test_fastapi/test_update/test_tutorial002.py index 32e343ed61..1dc2f2906c 100644 --- a/tests/test_tutorial/test_fastapi/test_update/test_tutorial002.py +++ b/tests/test_tutorial/test_fastapi/test_update/test_tutorial002.py @@ -1,18 +1,30 @@ -from dirty_equals import IsDict +import importlib +from types import ModuleType + +import pytest +from dirty_equals import IsOneOf from fastapi.testclient import TestClient from sqlmodel import Session, create_engine from sqlmodel.pool import StaticPool -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.update import tutorial002 as mod - +@pytest.fixture( + name="module", + params=[ + pytest.param("tutorial002_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.fastapi.update.{request.param}") mod.sqlite_url = "sqlite://" mod.engine = create_engine( mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool ) + return mod - with TestClient(mod.app) as client: + +def test_tutorial(module: ModuleType): + with TestClient(module.app) as client: hero1_data = { "name": "Deadpond", "secret_name": "Dive Wilson", @@ -60,16 +72,16 @@ def test_tutorial(clear_sqlmodel): assert "hashed_password" not in response_hero # Test hashed passwords - with Session(mod.engine) as session: - hero1_db = session.get(mod.Hero, hero1_id) + with Session(module.engine) as session: + hero1_db = session.get(module.Hero, hero1_id) assert hero1_db assert not hasattr(hero1_db, "password") assert hero1_db.hashed_password == "not really hashed chimichanga hehehe" - hero2_db = session.get(mod.Hero, hero2_id) + hero2_db = session.get(module.Hero, hero2_id) assert hero2_db assert not hasattr(hero2_db, "password") assert hero2_db.hashed_password == "not really hashed auntmay hehehe" - hero3_db = session.get(mod.Hero, hero3_id) + hero3_db = session.get(module.Hero, hero3_id) assert hero3_db assert not hasattr(hero3_db, "password") assert hero3_db.hashed_password == "not really hashed bestpreventer hehehe" @@ -80,13 +92,13 @@ def test_tutorial(clear_sqlmodel): data = response.json() assert response.status_code == 200, response.text assert data["name"] == hero2_data["name"], "The name should not be set to none" - assert ( - data["secret_name"] == "Spider-Youngster" - ), "The secret name should be updated" + assert data["secret_name"] == "Spider-Youngster", ( + "The secret name should be updated" + ) assert "password" not in data assert "hashed_password" not in data - with Session(mod.engine) as session: - hero2b_db = session.get(mod.Hero, hero2_id) + with Session(module.engine) as session: + hero2b_db = session.get(module.Hero, hero2_id) assert hero2b_db assert not hasattr(hero2b_db, "password") assert hero2b_db.hashed_password == "not really hashed auntmay hehehe" @@ -95,13 +107,13 @@ def test_tutorial(clear_sqlmodel): data = response.json() assert response.status_code == 200, response.text assert data["name"] == hero3_data["name"] - assert ( - data["age"] is None - ), "A field should be updatable to None, even if that's the default" + assert data["age"] is None, ( + "A field should be updatable to None, even if that's the default" + ) assert "password" not in data assert "hashed_password" not in data - with Session(mod.engine) as session: - hero3b_db = session.get(mod.Hero, hero3_id) + with Session(module.engine) as session: + hero3b_db = session.get(module.Hero, hero3_id) assert hero3b_db assert not hasattr(hero3b_db, "password") assert hero3b_db.hashed_password == "not really hashed bestpreventer hehehe" @@ -116,8 +128,8 @@ def test_tutorial(clear_sqlmodel): assert data["age"] is None assert "password" not in data assert "hashed_password" not in data - with Session(mod.engine) as session: - hero3b_db = session.get(mod.Hero, hero3_id) + with Session(module.engine) as session: + hero3b_db = session.get(module.Hero, hero3_id) assert hero3b_db assert not hasattr(hero3b_db, "password") assert ( @@ -327,16 +339,10 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "anyOf": [{"type": "integer"}, {"type": "null"}], - "title": "Age", - } - ) - | IsDict( - # TODO: Remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), + "age": { + "anyOf": [{"type": "integer"}, {"type": "null"}], + "title": "Age", + }, "password": {"type": "string", "title": "Password"}, }, }, @@ -347,16 +353,10 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "anyOf": [{"type": "integer"}, {"type": "null"}], - "title": "Age", - } - ) - | IsDict( - # TODO: Remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), + "age": { + "anyOf": [{"type": "integer"}, {"type": "null"}], + "title": "Age", + }, "id": {"title": "Id", "type": "integer"}, }, }, @@ -364,63 +364,60 @@ def test_tutorial(clear_sqlmodel): "title": "HeroUpdate", "type": "object", "properties": { - "name": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Name", - } - ) - | IsDict( - # TODO: Remove when deprecating Pydantic v1 - {"title": "Name", "type": "string"} - ), - "secret_name": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Secret Name", - } - ) - | IsDict( - # TODO: Remove when deprecating Pydantic v1 - {"title": "Secret Name", "type": "string"} - ), - "age": IsDict( - { - "anyOf": [{"type": "integer"}, {"type": "null"}], - "title": "Age", - } - ) - | IsDict( - # TODO: Remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "password": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Password", - } - ) - | IsDict( - # TODO: Remove when deprecating Pydantic v1 - {"title": "Password", "type": "string"} - ), + "name": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Name", + }, + "secret_name": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Secret Name", + }, + "age": { + "anyOf": [{"type": "integer"}, {"type": "null"}], + "title": "Age", + }, + "password": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Password", + }, }, }, "ValidationError": { "title": "ValidationError", "required": ["loc", "msg", "type"], "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] + "properties": IsOneOf( + { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [ + {"type": "string"}, + {"type": "integer"}, + ] + }, }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, + { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [ + {"type": "string"}, + {"type": "integer"}, + ] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, + }, + ), }, } }, diff --git a/tests/test_tutorial/test_fastapi/test_update/test_tutorial002_py310.py b/tests/test_tutorial/test_fastapi/test_update/test_tutorial002_py310.py deleted file mode 100644 index b05f5b2133..0000000000 --- a/tests/test_tutorial/test_fastapi/test_update/test_tutorial002_py310.py +++ /dev/null @@ -1,430 +0,0 @@ -from dirty_equals import IsDict -from fastapi.testclient import TestClient -from sqlmodel import Session, create_engine -from sqlmodel.pool import StaticPool - -from ....conftest import needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.update import tutorial002_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine( - mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool - ) - - with TestClient(mod.app) as client: - hero1_data = { - "name": "Deadpond", - "secret_name": "Dive Wilson", - "password": "chimichanga", - } - hero2_data = { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "id": 9000, - "password": "auntmay", - } - hero3_data = { - "name": "Rusty-Man", - "secret_name": "Tommy Sharp", - "age": 48, - "password": "bestpreventer", - } - response = client.post("/heroes/", json=hero1_data) - assert response.status_code == 200, response.text - hero1 = response.json() - assert "password" not in hero1 - assert "hashed_password" not in hero1 - hero1_id = hero1["id"] - response = client.post("/heroes/", json=hero2_data) - assert response.status_code == 200, response.text - hero2 = response.json() - hero2_id = hero2["id"] - response = client.post("/heroes/", json=hero3_data) - assert response.status_code == 200, response.text - hero3 = response.json() - hero3_id = hero3["id"] - response = client.get(f"/heroes/{hero2_id}") - assert response.status_code == 200, response.text - fetched_hero2 = response.json() - assert "password" not in fetched_hero2 - assert "hashed_password" not in fetched_hero2 - response = client.get("/heroes/9000") - assert response.status_code == 404, response.text - response = client.get("/heroes/") - assert response.status_code == 200, response.text - data = response.json() - assert len(data) == 3 - for response_hero in data: - assert "password" not in response_hero - assert "hashed_password" not in response_hero - - # Test hashed passwords - with Session(mod.engine) as session: - hero1_db = session.get(mod.Hero, hero1_id) - assert hero1_db - assert not hasattr(hero1_db, "password") - assert hero1_db.hashed_password == "not really hashed chimichanga hehehe" - hero2_db = session.get(mod.Hero, hero2_id) - assert hero2_db - assert not hasattr(hero2_db, "password") - assert hero2_db.hashed_password == "not really hashed auntmay hehehe" - hero3_db = session.get(mod.Hero, hero3_id) - assert hero3_db - assert not hasattr(hero3_db, "password") - assert hero3_db.hashed_password == "not really hashed bestpreventer hehehe" - - response = client.patch( - f"/heroes/{hero2_id}", json={"secret_name": "Spider-Youngster"} - ) - data = response.json() - assert response.status_code == 200, response.text - assert data["name"] == hero2_data["name"], "The name should not be set to none" - assert ( - data["secret_name"] == "Spider-Youngster" - ), "The secret name should be updated" - assert "password" not in data - assert "hashed_password" not in data - with Session(mod.engine) as session: - hero2b_db = session.get(mod.Hero, hero2_id) - assert hero2b_db - assert not hasattr(hero2b_db, "password") - assert hero2b_db.hashed_password == "not really hashed auntmay hehehe" - - response = client.patch(f"/heroes/{hero3_id}", json={"age": None}) - data = response.json() - assert response.status_code == 200, response.text - assert data["name"] == hero3_data["name"] - assert ( - data["age"] is None - ), "A field should be updatable to None, even if that's the default" - assert "password" not in data - assert "hashed_password" not in data - with Session(mod.engine) as session: - hero3b_db = session.get(mod.Hero, hero3_id) - assert hero3b_db - assert not hasattr(hero3b_db, "password") - assert hero3b_db.hashed_password == "not really hashed bestpreventer hehehe" - - # Test update dict, hashed_password - response = client.patch( - f"/heroes/{hero3_id}", json={"password": "philantroplayboy"} - ) - data = response.json() - assert response.status_code == 200, response.text - assert data["name"] == hero3_data["name"] - assert data["age"] is None - assert "password" not in data - assert "hashed_password" not in data - with Session(mod.engine) as session: - hero3b_db = session.get(mod.Hero, hero3_id) - assert hero3b_db - assert not hasattr(hero3b_db, "password") - assert ( - hero3b_db.hashed_password == "not really hashed philantroplayboy hehehe" - ) - - response = client.patch("/heroes/9001", json={"name": "Dragon Cube X"}) - assert response.status_code == 404, response.text - - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/heroes/": { - "get": { - "summary": "Read Heroes", - "operationId": "read_heroes_heroes__get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Offset", - "type": "integer", - "default": 0, - }, - "name": "offset", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Limit", - "maximum": 100, - "type": "integer", - "default": 100, - }, - "name": "limit", - "in": "query", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Heroes Heroes Get", - "type": "array", - "items": { - "$ref": "#/components/schemas/HeroPublic" - }, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "post": { - "summary": "Create Hero", - "operationId": "create_hero_heroes__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroCreate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/heroes/{hero_id}": { - "get": { - "summary": "Read Hero", - "operationId": "read_hero_heroes__hero_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "patch": { - "summary": "Update Hero", - "operationId": "update_hero_heroes__hero_id__patch", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroUpdate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - } - }, - }, - "HeroCreate": { - "title": "HeroCreate", - "required": ["name", "secret_name", "password"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "anyOf": [{"type": "integer"}, {"type": "null"}], - "title": "Age", - } - ) - | IsDict( - # TODO: Remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "password": {"type": "string", "title": "Password"}, - }, - }, - "HeroPublic": { - "title": "HeroPublic", - "required": ["name", "secret_name", "id"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "anyOf": [{"type": "integer"}, {"type": "null"}], - "title": "Age", - } - ) - | IsDict( - # TODO: Remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "id": {"title": "Id", "type": "integer"}, - }, - }, - "HeroUpdate": { - "title": "HeroUpdate", - "type": "object", - "properties": { - "name": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Name", - } - ) - | IsDict( - # TODO: Remove when deprecating Pydantic v1 - {"title": "Name", "type": "string"} - ), - "secret_name": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Secret Name", - } - ) - | IsDict( - # TODO: Remove when deprecating Pydantic v1 - {"title": "Secret Name", "type": "string"} - ), - "age": IsDict( - { - "anyOf": [{"type": "integer"}, {"type": "null"}], - "title": "Age", - } - ) - | IsDict( - # TODO: Remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "password": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Password", - } - ) - | IsDict( - # TODO: Remove when deprecating Pydantic v1 - {"title": "Password", "type": "string"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_fastapi/test_update/test_tutorial002_py39.py b/tests/test_tutorial/test_fastapi/test_update/test_tutorial002_py39.py deleted file mode 100644 index 807e33421a..0000000000 --- a/tests/test_tutorial/test_fastapi/test_update/test_tutorial002_py39.py +++ /dev/null @@ -1,430 +0,0 @@ -from dirty_equals import IsDict -from fastapi.testclient import TestClient -from sqlmodel import Session, create_engine -from sqlmodel.pool import StaticPool - -from ....conftest import needs_py39 - - -@needs_py39 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.update import tutorial002_py39 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine( - mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool - ) - - with TestClient(mod.app) as client: - hero1_data = { - "name": "Deadpond", - "secret_name": "Dive Wilson", - "password": "chimichanga", - } - hero2_data = { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "id": 9000, - "password": "auntmay", - } - hero3_data = { - "name": "Rusty-Man", - "secret_name": "Tommy Sharp", - "age": 48, - "password": "bestpreventer", - } - response = client.post("/heroes/", json=hero1_data) - assert response.status_code == 200, response.text - hero1 = response.json() - assert "password" not in hero1 - assert "hashed_password" not in hero1 - hero1_id = hero1["id"] - response = client.post("/heroes/", json=hero2_data) - assert response.status_code == 200, response.text - hero2 = response.json() - hero2_id = hero2["id"] - response = client.post("/heroes/", json=hero3_data) - assert response.status_code == 200, response.text - hero3 = response.json() - hero3_id = hero3["id"] - response = client.get(f"/heroes/{hero2_id}") - assert response.status_code == 200, response.text - fetched_hero2 = response.json() - assert "password" not in fetched_hero2 - assert "hashed_password" not in fetched_hero2 - response = client.get("/heroes/9000") - assert response.status_code == 404, response.text - response = client.get("/heroes/") - assert response.status_code == 200, response.text - data = response.json() - assert len(data) == 3 - for response_hero in data: - assert "password" not in response_hero - assert "hashed_password" not in response_hero - - # Test hashed passwords - with Session(mod.engine) as session: - hero1_db = session.get(mod.Hero, hero1_id) - assert hero1_db - assert not hasattr(hero1_db, "password") - assert hero1_db.hashed_password == "not really hashed chimichanga hehehe" - hero2_db = session.get(mod.Hero, hero2_id) - assert hero2_db - assert not hasattr(hero2_db, "password") - assert hero2_db.hashed_password == "not really hashed auntmay hehehe" - hero3_db = session.get(mod.Hero, hero3_id) - assert hero3_db - assert not hasattr(hero3_db, "password") - assert hero3_db.hashed_password == "not really hashed bestpreventer hehehe" - - response = client.patch( - f"/heroes/{hero2_id}", json={"secret_name": "Spider-Youngster"} - ) - data = response.json() - assert response.status_code == 200, response.text - assert data["name"] == hero2_data["name"], "The name should not be set to none" - assert ( - data["secret_name"] == "Spider-Youngster" - ), "The secret name should be updated" - assert "password" not in data - assert "hashed_password" not in data - with Session(mod.engine) as session: - hero2b_db = session.get(mod.Hero, hero2_id) - assert hero2b_db - assert not hasattr(hero2b_db, "password") - assert hero2b_db.hashed_password == "not really hashed auntmay hehehe" - - response = client.patch(f"/heroes/{hero3_id}", json={"age": None}) - data = response.json() - assert response.status_code == 200, response.text - assert data["name"] == hero3_data["name"] - assert ( - data["age"] is None - ), "A field should be updatable to None, even if that's the default" - assert "password" not in data - assert "hashed_password" not in data - with Session(mod.engine) as session: - hero3b_db = session.get(mod.Hero, hero3_id) - assert hero3b_db - assert not hasattr(hero3b_db, "password") - assert hero3b_db.hashed_password == "not really hashed bestpreventer hehehe" - - # Test update dict, hashed_password - response = client.patch( - f"/heroes/{hero3_id}", json={"password": "philantroplayboy"} - ) - data = response.json() - assert response.status_code == 200, response.text - assert data["name"] == hero3_data["name"] - assert data["age"] is None - assert "password" not in data - assert "hashed_password" not in data - with Session(mod.engine) as session: - hero3b_db = session.get(mod.Hero, hero3_id) - assert hero3b_db - assert not hasattr(hero3b_db, "password") - assert ( - hero3b_db.hashed_password == "not really hashed philantroplayboy hehehe" - ) - - response = client.patch("/heroes/9001", json={"name": "Dragon Cube X"}) - assert response.status_code == 404, response.text - - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/heroes/": { - "get": { - "summary": "Read Heroes", - "operationId": "read_heroes_heroes__get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Offset", - "type": "integer", - "default": 0, - }, - "name": "offset", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Limit", - "maximum": 100, - "type": "integer", - "default": 100, - }, - "name": "limit", - "in": "query", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Heroes Heroes Get", - "type": "array", - "items": { - "$ref": "#/components/schemas/HeroPublic" - }, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "post": { - "summary": "Create Hero", - "operationId": "create_hero_heroes__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroCreate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/heroes/{hero_id}": { - "get": { - "summary": "Read Hero", - "operationId": "read_hero_heroes__hero_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "patch": { - "summary": "Update Hero", - "operationId": "update_hero_heroes__hero_id__patch", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroUpdate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - } - }, - }, - "HeroCreate": { - "title": "HeroCreate", - "required": ["name", "secret_name", "password"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "anyOf": [{"type": "integer"}, {"type": "null"}], - "title": "Age", - } - ) - | IsDict( - # TODO: Remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "password": {"type": "string", "title": "Password"}, - }, - }, - "HeroPublic": { - "title": "HeroPublic", - "required": ["name", "secret_name", "id"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "anyOf": [{"type": "integer"}, {"type": "null"}], - "title": "Age", - } - ) - | IsDict( - # TODO: Remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "id": {"title": "Id", "type": "integer"}, - }, - }, - "HeroUpdate": { - "title": "HeroUpdate", - "type": "object", - "properties": { - "name": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Name", - } - ) - | IsDict( - # TODO: Remove when deprecating Pydantic v1 - {"title": "Name", "type": "string"} - ), - "secret_name": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Secret Name", - } - ) - | IsDict( - # TODO: Remove when deprecating Pydantic v1 - {"title": "Secret Name", "type": "string"} - ), - "age": IsDict( - { - "anyOf": [{"type": "integer"}, {"type": "null"}], - "title": "Age", - } - ) - | IsDict( - # TODO: Remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "password": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Password", - } - ) - | IsDict( - # TODO: Remove when deprecating Pydantic v1 - {"title": "Password", "type": "string"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_indexes/test_tutorial001.py b/tests/test_tutorial/test_indexes/test_tutorial001.py index f33db5bcc7..c81d581a2e 100644 --- a/tests/test_tutorial/test_indexes/test_tutorial001.py +++ b/tests/test_tutorial/test_indexes/test_tutorial001.py @@ -1,24 +1,30 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlalchemy import inspect from sqlalchemy.engine.reflection import Inspector from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.indexes import tutorial001 as mod - +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial001_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.indexes.{request.param}") mod.sqlite_url = "sqlite://" mod.engine = create_engine(mod.sqlite_url) - calls = [] + return mod - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() + assert print_mock.calls == [ [{"secret_name": "Dive Wilson", "age": None, "id": 1, "name": "Deadpond"}] ] diff --git a/tests/test_tutorial/test_indexes/test_tutorial001_py310.py b/tests/test_tutorial/test_indexes/test_tutorial001_py310.py deleted file mode 100644 index cfee262b2b..0000000000 --- a/tests/test_tutorial/test_indexes/test_tutorial001_py310.py +++ /dev/null @@ -1,46 +0,0 @@ -from unittest.mock import patch - -from sqlalchemy import inspect -from sqlalchemy.engine.reflection import Inspector -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.indexes import tutorial001_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ - [{"secret_name": "Dive Wilson", "age": None, "id": 1, "name": "Deadpond"}] - ] - - insp: Inspector = inspect(mod.engine) - indexes = insp.get_indexes(str(mod.Hero.__tablename__)) - expected_indexes = [ - { - "name": "ix_hero_name", - "dialect_options": {}, - "column_names": ["name"], - "unique": 0, - }, - { - "name": "ix_hero_age", - "dialect_options": {}, - "column_names": ["age"], - "unique": 0, - }, - ] - for index in expected_indexes: - assert index in indexes, "This expected index should be in the indexes in DB" - # Now that this index was checked, remove it from the list of indexes - indexes.pop(indexes.index(index)) - assert len(indexes) == 0, "The database should only have the expected indexes" diff --git a/tests/test_tutorial/test_indexes/test_tutorial002.py b/tests/test_tutorial/test_indexes/test_tutorial002.py index 893043dad1..d41dbe8f3a 100644 --- a/tests/test_tutorial/test_indexes/test_tutorial002.py +++ b/tests/test_tutorial/test_indexes/test_tutorial002.py @@ -1,24 +1,30 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlalchemy import inspect from sqlalchemy.engine.reflection import Inspector from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.indexes import tutorial002 as mod - +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial002_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.indexes.{request.param}") mod.sqlite_url = "sqlite://" mod.engine = create_engine(mod.sqlite_url) - calls = [] + return mod - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() + assert print_mock.calls == [ [{"name": "Tarantula", "secret_name": "Natalia Roman-on", "age": 32, "id": 4}], [{"name": "Black Lion", "secret_name": "Trevor Challa", "age": 35, "id": 5}], ] diff --git a/tests/test_tutorial/test_indexes/test_tutorial002_py310.py b/tests/test_tutorial/test_indexes/test_tutorial002_py310.py deleted file mode 100644 index 089b6828e9..0000000000 --- a/tests/test_tutorial/test_indexes/test_tutorial002_py310.py +++ /dev/null @@ -1,47 +0,0 @@ -from unittest.mock import patch - -from sqlalchemy import inspect -from sqlalchemy.engine.reflection import Inspector -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.indexes import tutorial002_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ - [{"name": "Tarantula", "secret_name": "Natalia Roman-on", "age": 32, "id": 4}], - [{"name": "Black Lion", "secret_name": "Trevor Challa", "age": 35, "id": 5}], - ] - - insp: Inspector = inspect(mod.engine) - indexes = insp.get_indexes(str(mod.Hero.__tablename__)) - expected_indexes = [ - { - "name": "ix_hero_name", - "dialect_options": {}, - "column_names": ["name"], - "unique": 0, - }, - { - "name": "ix_hero_age", - "dialect_options": {}, - "column_names": ["age"], - "unique": 0, - }, - ] - for index in expected_indexes: - assert index in indexes, "This expected index should be in the indexes in DB" - # Now that this index was checked, remove it from the list of indexes - indexes.pop(indexes.index(index)) - assert len(indexes) == 0, "The database should only have the expected indexes" diff --git a/tests/test_tutorial/test_insert/test_tutorial001.py b/tests/test_tutorial/test_insert/test_tutorial001.py index 3a5162c08a..a816a3a202 100644 --- a/tests/test_tutorial/test_insert/test_tutorial001.py +++ b/tests/test_tutorial/test_insert/test_tutorial001.py @@ -1,11 +1,24 @@ -from sqlmodel import Session, create_engine, select +import importlib +from types import ModuleType +import pytest +from sqlmodel import Session, create_engine, select -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.insert import tutorial001 as mod +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial001_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.insert.{request.param}") mod.sqlite_url = "sqlite://" mod.engine = create_engine(mod.sqlite_url) + return mod + + +def test_tutorial(mod: ModuleType): mod.main() with Session(mod.engine) as session: heroes = session.exec(select(mod.Hero)).all() diff --git a/tests/test_tutorial/test_insert/test_tutorial001_py310.py b/tests/test_tutorial/test_insert/test_tutorial001_py310.py deleted file mode 100644 index 47cbc4cde6..0000000000 --- a/tests/test_tutorial/test_insert/test_tutorial001_py310.py +++ /dev/null @@ -1,30 +0,0 @@ -from sqlmodel import Session, create_engine, select - -from ...conftest import needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.insert import tutorial001_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - mod.main() - with Session(mod.engine) as session: - heroes = session.exec(select(mod.Hero)).all() - heroes_by_name = {hero.name: hero for hero in heroes} - deadpond = heroes_by_name["Deadpond"] - spider_boy = heroes_by_name["Spider-Boy"] - rusty_man = heroes_by_name["Rusty-Man"] - assert deadpond.name == "Deadpond" - assert deadpond.age is None - assert deadpond.id is not None - assert deadpond.secret_name == "Dive Wilson" - assert spider_boy.name == "Spider-Boy" - assert spider_boy.age is None - assert spider_boy.id is not None - assert spider_boy.secret_name == "Pedro Parqueador" - assert rusty_man.name == "Rusty-Man" - assert rusty_man.age == 48 - assert rusty_man.id is not None - assert rusty_man.secret_name == "Tommy Sharp" diff --git a/tests/test_tutorial/test_insert/test_tutorial002.py b/tests/test_tutorial/test_insert/test_tutorial002.py index c450ec044d..b1f5058615 100644 --- a/tests/test_tutorial/test_insert/test_tutorial002.py +++ b/tests/test_tutorial/test_insert/test_tutorial002.py @@ -1,11 +1,24 @@ -from sqlmodel import Session, create_engine, select +import importlib +from types import ModuleType +import pytest +from sqlmodel import Session, create_engine, select -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.insert import tutorial002 as mod +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial002_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.insert.{request.param}") mod.sqlite_url = "sqlite://" mod.engine = create_engine(mod.sqlite_url) + return mod + + +def test_tutorial(mod: ModuleType): mod.main() with Session(mod.engine) as session: heroes = session.exec(select(mod.Hero)).all() diff --git a/tests/test_tutorial/test_insert/test_tutorial002_py310.py b/tests/test_tutorial/test_insert/test_tutorial002_py310.py deleted file mode 100644 index fb62810baf..0000000000 --- a/tests/test_tutorial/test_insert/test_tutorial002_py310.py +++ /dev/null @@ -1,30 +0,0 @@ -from sqlmodel import Session, create_engine, select - -from ...conftest import needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.insert import tutorial002_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - mod.main() - with Session(mod.engine) as session: - heroes = session.exec(select(mod.Hero)).all() - heroes_by_name = {hero.name: hero for hero in heroes} - deadpond = heroes_by_name["Deadpond"] - spider_boy = heroes_by_name["Spider-Boy"] - rusty_man = heroes_by_name["Rusty-Man"] - assert deadpond.name == "Deadpond" - assert deadpond.age is None - assert deadpond.id is not None - assert deadpond.secret_name == "Dive Wilson" - assert spider_boy.name == "Spider-Boy" - assert spider_boy.age is None - assert spider_boy.id is not None - assert spider_boy.secret_name == "Pedro Parqueador" - assert rusty_man.name == "Rusty-Man" - assert rusty_man.age == 48 - assert rusty_man.id is not None - assert rusty_man.secret_name == "Tommy Sharp" diff --git a/tests/test_tutorial/test_insert/test_tutorial003.py b/tests/test_tutorial/test_insert/test_tutorial003.py index df2112b25a..fe138b0f8d 100644 --- a/tests/test_tutorial/test_insert/test_tutorial003.py +++ b/tests/test_tutorial/test_insert/test_tutorial003.py @@ -1,11 +1,24 @@ -from sqlmodel import Session, create_engine, select +import importlib +from types import ModuleType +import pytest +from sqlmodel import Session, create_engine, select -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.insert import tutorial003 as mod +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial003_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.insert.{request.param}") mod.sqlite_url = "sqlite://" mod.engine = create_engine(mod.sqlite_url) + return mod + + +def test_tutorial(mod: ModuleType): mod.main() with Session(mod.engine) as session: heroes = session.exec(select(mod.Hero)).all() diff --git a/tests/test_tutorial/test_insert/test_tutorial003_py310.py b/tests/test_tutorial/test_insert/test_tutorial003_py310.py deleted file mode 100644 index 5bca713e60..0000000000 --- a/tests/test_tutorial/test_insert/test_tutorial003_py310.py +++ /dev/null @@ -1,30 +0,0 @@ -from sqlmodel import Session, create_engine, select - -from ...conftest import needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.insert import tutorial003_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - mod.main() - with Session(mod.engine) as session: - heroes = session.exec(select(mod.Hero)).all() - heroes_by_name = {hero.name: hero for hero in heroes} - deadpond = heroes_by_name["Deadpond"] - spider_boy = heroes_by_name["Spider-Boy"] - rusty_man = heroes_by_name["Rusty-Man"] - assert deadpond.name == "Deadpond" - assert deadpond.age is None - assert deadpond.id is not None - assert deadpond.secret_name == "Dive Wilson" - assert spider_boy.name == "Spider-Boy" - assert spider_boy.age is None - assert spider_boy.id is not None - assert spider_boy.secret_name == "Pedro Parqueador" - assert rusty_man.name == "Rusty-Man" - assert rusty_man.age == 48 - assert rusty_man.id is not None - assert rusty_man.secret_name == "Tommy Sharp" diff --git a/tests/test_tutorial/test_limit_and_offset/test_tutorial001.py b/tests/test_tutorial/test_limit_and_offset/test_tutorial001.py index 244f91083f..d2a10df7a1 100644 --- a/tests/test_tutorial/test_limit_and_offset/test_tutorial001.py +++ b/tests/test_tutorial/test_limit_and_offset/test_tutorial001.py @@ -1,8 +1,24 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock + + +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial001_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.offset_and_limit.{request.param}") + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + return mod + expected_calls = [ [ @@ -20,15 +36,6 @@ ] -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.offset_and_limit import tutorial001 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() + assert print_mock.calls == expected_calls diff --git a/tests/test_tutorial/test_limit_and_offset/test_tutorial001_py310.py b/tests/test_tutorial/test_limit_and_offset/test_tutorial001_py310.py deleted file mode 100644 index 4f4974c853..0000000000 --- a/tests/test_tutorial/test_limit_and_offset/test_tutorial001_py310.py +++ /dev/null @@ -1,35 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - -expected_calls = [ - [ - [ - {"id": 1, "name": "Deadpond", "secret_name": "Dive Wilson", "age": None}, - { - "id": 2, - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "age": None, - }, - {"id": 3, "name": "Rusty-Man", "secret_name": "Tommy Sharp", "age": 48}, - ] - ] -] - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.offset_and_limit import tutorial001_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_limit_and_offset/test_tutorial002.py b/tests/test_tutorial/test_limit_and_offset/test_tutorial002.py index e9dee0cb35..dc7c451b2d 100644 --- a/tests/test_tutorial/test_limit_and_offset/test_tutorial002.py +++ b/tests/test_tutorial/test_limit_and_offset/test_tutorial002.py @@ -1,8 +1,24 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock + + +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial002_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.offset_and_limit.{request.param}") + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + return mod + expected_calls = [ [ @@ -20,15 +36,6 @@ ] -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.offset_and_limit import tutorial002 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() + assert print_mock.calls == expected_calls diff --git a/tests/test_tutorial/test_limit_and_offset/test_tutorial002_py310.py b/tests/test_tutorial/test_limit_and_offset/test_tutorial002_py310.py deleted file mode 100644 index 1f86d1960e..0000000000 --- a/tests/test_tutorial/test_limit_and_offset/test_tutorial002_py310.py +++ /dev/null @@ -1,35 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - -expected_calls = [ - [ - [ - { - "id": 4, - "name": "Tarantula", - "secret_name": "Natalia Roman-on", - "age": 32, - }, - {"id": 5, "name": "Black Lion", "secret_name": "Trevor Challa", "age": 35}, - {"id": 6, "name": "Dr. Weird", "secret_name": "Steve Weird", "age": 36}, - ] - ] -] - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.offset_and_limit import tutorial002_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_limit_and_offset/test_tutorial003.py b/tests/test_tutorial/test_limit_and_offset/test_tutorial003.py index 7192f7ef43..89b655e749 100644 --- a/tests/test_tutorial/test_limit_and_offset/test_tutorial003.py +++ b/tests/test_tutorial/test_limit_and_offset/test_tutorial003.py @@ -1,8 +1,24 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock + + +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial003_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.offset_and_limit.{request.param}") + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + return mod + expected_calls = [ [ @@ -18,15 +34,6 @@ ] -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.offset_and_limit import tutorial003 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() + assert print_mock.calls == expected_calls diff --git a/tests/test_tutorial/test_limit_and_offset/test_tutorial003_py310.py b/tests/test_tutorial/test_limit_and_offset/test_tutorial003_py310.py deleted file mode 100644 index 993999156d..0000000000 --- a/tests/test_tutorial/test_limit_and_offset/test_tutorial003_py310.py +++ /dev/null @@ -1,33 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - -expected_calls = [ - [ - [ - { - "id": 7, - "name": "Captain North America", - "secret_name": "Esteban Rogelios", - "age": 93, - } - ] - ] -] - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.offset_and_limit import tutorial003_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_limit_and_offset/test_tutorial004.py b/tests/test_tutorial/test_limit_and_offset/test_tutorial004.py index eb15a1560e..33a29679af 100644 --- a/tests/test_tutorial/test_limit_and_offset/test_tutorial004.py +++ b/tests/test_tutorial/test_limit_and_offset/test_tutorial004.py @@ -1,22 +1,28 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.offset_and_limit import tutorial004 as mod - +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial004_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.offset_and_limit.{request.param}") mod.sqlite_url = "sqlite://" mod.engine = create_engine(mod.sqlite_url) - calls = [] + return mod - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() + assert print_mock.calls == [ [ [ {"name": "Dr. Weird", "secret_name": "Steve Weird", "age": 36, "id": 6}, diff --git a/tests/test_tutorial/test_limit_and_offset/test_tutorial004_py310.py b/tests/test_tutorial/test_limit_and_offset/test_tutorial004_py310.py deleted file mode 100644 index 4ca736589f..0000000000 --- a/tests/test_tutorial/test_limit_and_offset/test_tutorial004_py310.py +++ /dev/null @@ -1,27 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.offset_and_limit import tutorial004_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ - [ - [ - {"name": "Dr. Weird", "secret_name": "Steve Weird", "age": 36, "id": 6}, - {"name": "Rusty-Man", "secret_name": "Tommy Sharp", "age": 48, "id": 3}, - ] - ] - ] diff --git a/tests/test_tutorial/test_many_to_many/test_tutorial001.py b/tests/test_tutorial/test_many_to_many/test_tutorial001.py index 70bfe9a649..798d9f2b03 100644 --- a/tests/test_tutorial/test_many_to_many/test_tutorial001.py +++ b/tests/test_tutorial/test_many_to_many/test_tutorial001.py @@ -1,8 +1,24 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock + + +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial001_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.many_to_many.{request.param}") + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + return mod + expected_calls = [ [ @@ -35,15 +51,6 @@ ] -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.many_to_many import tutorial001 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() + assert print_mock.calls == expected_calls diff --git a/tests/test_tutorial/test_many_to_many/test_tutorial001_py310.py b/tests/test_tutorial/test_many_to_many/test_tutorial001_py310.py deleted file mode 100644 index bf31d9c695..0000000000 --- a/tests/test_tutorial/test_many_to_many/test_tutorial001_py310.py +++ /dev/null @@ -1,50 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - -expected_calls = [ - [ - "Deadpond:", - {"id": 1, "secret_name": "Dive Wilson", "age": None, "name": "Deadpond"}, - ], - [ - "Deadpond teams:", - [ - {"id": 1, "name": "Z-Force", "headquarters": "Sister Margaret's Bar"}, - {"id": 2, "name": "Preventers", "headquarters": "Sharp Tower"}, - ], - ], - [ - "Rusty-Man:", - {"id": 2, "secret_name": "Tommy Sharp", "age": 48, "name": "Rusty-Man"}, - ], - [ - "Rusty-Man Teams:", - [{"id": 2, "name": "Preventers", "headquarters": "Sharp Tower"}], - ], - [ - "Spider-Boy:", - {"id": 3, "secret_name": "Pedro Parqueador", "age": None, "name": "Spider-Boy"}, - ], - [ - "Spider-Boy Teams:", - [{"id": 2, "name": "Preventers", "headquarters": "Sharp Tower"}], - ], -] - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.many_to_many import tutorial001_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_many_to_many/test_tutorial001_py39.py b/tests/test_tutorial/test_many_to_many/test_tutorial001_py39.py deleted file mode 100644 index cb7a4d8456..0000000000 --- a/tests/test_tutorial/test_many_to_many/test_tutorial001_py39.py +++ /dev/null @@ -1,50 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py39 - -expected_calls = [ - [ - "Deadpond:", - {"id": 1, "secret_name": "Dive Wilson", "age": None, "name": "Deadpond"}, - ], - [ - "Deadpond teams:", - [ - {"id": 1, "name": "Z-Force", "headquarters": "Sister Margaret's Bar"}, - {"id": 2, "name": "Preventers", "headquarters": "Sharp Tower"}, - ], - ], - [ - "Rusty-Man:", - {"id": 2, "secret_name": "Tommy Sharp", "age": 48, "name": "Rusty-Man"}, - ], - [ - "Rusty-Man Teams:", - [{"id": 2, "name": "Preventers", "headquarters": "Sharp Tower"}], - ], - [ - "Spider-Boy:", - {"id": 3, "secret_name": "Pedro Parqueador", "age": None, "name": "Spider-Boy"}, - ], - [ - "Spider-Boy Teams:", - [{"id": 2, "name": "Preventers", "headquarters": "Sharp Tower"}], - ], -] - - -@needs_py39 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.many_to_many import tutorial001_py39 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_many_to_many/test_tutorial002.py b/tests/test_tutorial/test_many_to_many/test_tutorial002.py index d4d7d95e89..101a2a8ae3 100644 --- a/tests/test_tutorial/test_many_to_many/test_tutorial002.py +++ b/tests/test_tutorial/test_many_to_many/test_tutorial002.py @@ -1,8 +1,24 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock + + +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial002_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.many_to_many.{request.param}") + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + return mod + expected_calls = [ [ @@ -62,15 +78,6 @@ ] -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.many_to_many import tutorial002 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() + assert print_mock.calls == expected_calls diff --git a/tests/test_tutorial/test_many_to_many/test_tutorial002_py310.py b/tests/test_tutorial/test_many_to_many/test_tutorial002_py310.py deleted file mode 100644 index ad7c892fcd..0000000000 --- a/tests/test_tutorial/test_many_to_many/test_tutorial002_py310.py +++ /dev/null @@ -1,77 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - -expected_calls = [ - [ - "Deadpond:", - {"id": 1, "secret_name": "Dive Wilson", "age": None, "name": "Deadpond"}, - ], - [ - "Deadpond teams:", - [ - {"id": 1, "name": "Z-Force", "headquarters": "Sister Margaret's Bar"}, - {"id": 2, "name": "Preventers", "headquarters": "Sharp Tower"}, - ], - ], - [ - "Rusty-Man:", - {"id": 2, "secret_name": "Tommy Sharp", "age": 48, "name": "Rusty-Man"}, - ], - [ - "Rusty-Man Teams:", - [{"id": 2, "name": "Preventers", "headquarters": "Sharp Tower"}], - ], - [ - "Spider-Boy:", - {"id": 3, "secret_name": "Pedro Parqueador", "age": None, "name": "Spider-Boy"}, - ], - [ - "Spider-Boy Teams:", - [{"id": 2, "name": "Preventers", "headquarters": "Sharp Tower"}], - ], - [ - "Updated Spider-Boy's Teams:", - [ - {"id": 2, "name": "Preventers", "headquarters": "Sharp Tower"}, - {"id": 1, "name": "Z-Force", "headquarters": "Sister Margaret's Bar"}, - ], - ], - [ - "Z-Force heroes:", - [ - {"id": 1, "secret_name": "Dive Wilson", "age": None, "name": "Deadpond"}, - { - "id": 3, - "secret_name": "Pedro Parqueador", - "age": None, - "name": "Spider-Boy", - }, - ], - ], - [ - "Reverted Z-Force's heroes:", - [{"id": 1, "secret_name": "Dive Wilson", "age": None, "name": "Deadpond"}], - ], - [ - "Reverted Spider-Boy's teams:", - [{"id": 2, "name": "Preventers", "headquarters": "Sharp Tower"}], - ], -] - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.many_to_many import tutorial002_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_many_to_many/test_tutorial002_py39.py b/tests/test_tutorial/test_many_to_many/test_tutorial002_py39.py deleted file mode 100644 index c0df48d73c..0000000000 --- a/tests/test_tutorial/test_many_to_many/test_tutorial002_py39.py +++ /dev/null @@ -1,77 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py39 - -expected_calls = [ - [ - "Deadpond:", - {"id": 1, "secret_name": "Dive Wilson", "age": None, "name": "Deadpond"}, - ], - [ - "Deadpond teams:", - [ - {"id": 1, "name": "Z-Force", "headquarters": "Sister Margaret's Bar"}, - {"id": 2, "name": "Preventers", "headquarters": "Sharp Tower"}, - ], - ], - [ - "Rusty-Man:", - {"id": 2, "secret_name": "Tommy Sharp", "age": 48, "name": "Rusty-Man"}, - ], - [ - "Rusty-Man Teams:", - [{"id": 2, "name": "Preventers", "headquarters": "Sharp Tower"}], - ], - [ - "Spider-Boy:", - {"id": 3, "secret_name": "Pedro Parqueador", "age": None, "name": "Spider-Boy"}, - ], - [ - "Spider-Boy Teams:", - [{"id": 2, "name": "Preventers", "headquarters": "Sharp Tower"}], - ], - [ - "Updated Spider-Boy's Teams:", - [ - {"id": 2, "name": "Preventers", "headquarters": "Sharp Tower"}, - {"id": 1, "name": "Z-Force", "headquarters": "Sister Margaret's Bar"}, - ], - ], - [ - "Z-Force heroes:", - [ - {"id": 1, "secret_name": "Dive Wilson", "age": None, "name": "Deadpond"}, - { - "id": 3, - "secret_name": "Pedro Parqueador", - "age": None, - "name": "Spider-Boy", - }, - ], - ], - [ - "Reverted Z-Force's heroes:", - [{"id": 1, "secret_name": "Dive Wilson", "age": None, "name": "Deadpond"}], - ], - [ - "Reverted Spider-Boy's teams:", - [{"id": 2, "name": "Preventers", "headquarters": "Sharp Tower"}], - ], -] - - -@needs_py39 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.many_to_many import tutorial002_py39 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_many_to_many/test_tutorial003.py b/tests/test_tutorial/test_many_to_many/test_tutorial003.py index 35489b01ce..672aabab72 100644 --- a/tests/test_tutorial/test_many_to_many/test_tutorial003.py +++ b/tests/test_tutorial/test_many_to_many/test_tutorial003.py @@ -1,8 +1,24 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock + + +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial003_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.many_to_many.{request.param}") + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + return mod + expected_calls = [ [ @@ -58,15 +74,6 @@ ] -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.many_to_many import tutorial003 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() + assert print_mock.calls == expected_calls diff --git a/tests/test_tutorial/test_many_to_many/test_tutorial003_py310.py b/tests/test_tutorial/test_many_to_many/test_tutorial003_py310.py deleted file mode 100644 index 78a699c741..0000000000 --- a/tests/test_tutorial/test_many_to_many/test_tutorial003_py310.py +++ /dev/null @@ -1,73 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - -expected_calls = [ - [ - "Z-Force hero:", - {"name": "Deadpond", "secret_name": "Dive Wilson", "id": 1, "age": None}, - "is training:", - False, - ], - [ - "Preventers hero:", - {"name": "Deadpond", "secret_name": "Dive Wilson", "id": 1, "age": None}, - "is training:", - True, - ], - [ - "Preventers hero:", - {"name": "Spider-Boy", "secret_name": "Pedro Parqueador", "id": 2, "age": None}, - "is training:", - True, - ], - [ - "Preventers hero:", - {"name": "Rusty-Man", "secret_name": "Tommy Sharp", "id": 3, "age": 48}, - "is training:", - False, - ], - [ - "Updated Spider-Boy's Teams:", - [ - {"team_id": 2, "is_training": True, "hero_id": 2}, - {"team_id": 1, "is_training": True, "hero_id": 2}, - ], - ], - [ - "Z-Force heroes:", - [ - {"team_id": 1, "is_training": False, "hero_id": 1}, - {"team_id": 1, "is_training": True, "hero_id": 2}, - ], - ], - [ - "Spider-Boy team:", - {"headquarters": "Sharp Tower", "id": 2, "name": "Preventers"}, - "is training:", - False, - ], - [ - "Spider-Boy team:", - {"headquarters": "Sister Margaret's Bar", "id": 1, "name": "Z-Force"}, - "is training:", - True, - ], -] - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.many_to_many import tutorial003_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_many_to_many/test_tutorial003_py39.py b/tests/test_tutorial/test_many_to_many/test_tutorial003_py39.py deleted file mode 100644 index 8fed921d82..0000000000 --- a/tests/test_tutorial/test_many_to_many/test_tutorial003_py39.py +++ /dev/null @@ -1,73 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py39 - -expected_calls = [ - [ - "Z-Force hero:", - {"name": "Deadpond", "secret_name": "Dive Wilson", "id": 1, "age": None}, - "is training:", - False, - ], - [ - "Preventers hero:", - {"name": "Deadpond", "secret_name": "Dive Wilson", "id": 1, "age": None}, - "is training:", - True, - ], - [ - "Preventers hero:", - {"name": "Spider-Boy", "secret_name": "Pedro Parqueador", "id": 2, "age": None}, - "is training:", - True, - ], - [ - "Preventers hero:", - {"name": "Rusty-Man", "secret_name": "Tommy Sharp", "id": 3, "age": 48}, - "is training:", - False, - ], - [ - "Updated Spider-Boy's Teams:", - [ - {"team_id": 2, "is_training": True, "hero_id": 2}, - {"team_id": 1, "is_training": True, "hero_id": 2}, - ], - ], - [ - "Z-Force heroes:", - [ - {"team_id": 1, "is_training": False, "hero_id": 1}, - {"team_id": 1, "is_training": True, "hero_id": 2}, - ], - ], - [ - "Spider-Boy team:", - {"headquarters": "Sharp Tower", "id": 2, "name": "Preventers"}, - "is training:", - False, - ], - [ - "Spider-Boy team:", - {"headquarters": "Sister Margaret's Bar", "id": 1, "name": "Z-Force"}, - "is training:", - True, - ], -] - - -@needs_py39 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.many_to_many import tutorial003_py39 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_one/test_tutorial001.py b/tests/test_tutorial/test_one/test_tutorial001.py index deb133b985..3c95328072 100644 --- a/tests/test_tutorial/test_one/test_tutorial001.py +++ b/tests/test_tutorial/test_one/test_tutorial001.py @@ -1,22 +1,28 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.one import tutorial001 as mod - +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial001_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.one.{request.param}") mod.sqlite_url = "sqlite://" mod.engine = create_engine(mod.sqlite_url) - calls = [] + return mod - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() + assert print_mock.calls == [ [ "Hero:", { diff --git a/tests/test_tutorial/test_one/test_tutorial001_py310.py b/tests/test_tutorial/test_one/test_tutorial001_py310.py deleted file mode 100644 index 6de878087f..0000000000 --- a/tests/test_tutorial/test_one/test_tutorial001_py310.py +++ /dev/null @@ -1,30 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.one import tutorial001_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ - [ - "Hero:", - { - "name": "Tarantula", - "secret_name": "Natalia Roman-on", - "age": 32, - "id": 4, - }, - ] - ] diff --git a/tests/test_tutorial/test_one/test_tutorial002.py b/tests/test_tutorial/test_one/test_tutorial002.py index 7106564122..7c2197113b 100644 --- a/tests/test_tutorial/test_one/test_tutorial002.py +++ b/tests/test_tutorial/test_one/test_tutorial002.py @@ -1,19 +1,25 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.one import tutorial002 as mod - +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial002_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.one.{request.param}") mod.sqlite_url = "sqlite://" mod.engine = create_engine(mod.sqlite_url) - calls = [] + return mod - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [["Hero:", None]] +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() + assert print_mock.calls == [["Hero:", None]] diff --git a/tests/test_tutorial/test_one/test_tutorial002_py310.py b/tests/test_tutorial/test_one/test_tutorial002_py310.py deleted file mode 100644 index afdfc54593..0000000000 --- a/tests/test_tutorial/test_one/test_tutorial002_py310.py +++ /dev/null @@ -1,20 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.one import tutorial002_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [["Hero:", None]] diff --git a/tests/test_tutorial/test_one/test_tutorial003.py b/tests/test_tutorial/test_one/test_tutorial003.py index 40a73d042b..80287855f3 100644 --- a/tests/test_tutorial/test_one/test_tutorial003.py +++ b/tests/test_tutorial/test_one/test_tutorial003.py @@ -1,22 +1,28 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.one import tutorial003 as mod - +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial003_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.one.{request.param}") mod.sqlite_url = "sqlite://" mod.engine = create_engine(mod.sqlite_url) - calls = [] + return mod - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() + assert print_mock.calls == [ [ "Hero:", {"name": "Deadpond", "secret_name": "Dive Wilson", "age": None, "id": 1}, diff --git a/tests/test_tutorial/test_one/test_tutorial003_py310.py b/tests/test_tutorial/test_one/test_tutorial003_py310.py deleted file mode 100644 index 8eb8b8612b..0000000000 --- a/tests/test_tutorial/test_one/test_tutorial003_py310.py +++ /dev/null @@ -1,25 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.one import tutorial003_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ - [ - "Hero:", - {"name": "Deadpond", "secret_name": "Dive Wilson", "age": None, "id": 1}, - ] - ] diff --git a/tests/test_tutorial/test_one/test_tutorial004.py b/tests/test_tutorial/test_one/test_tutorial004.py index 5bd652577d..302b9ea0f3 100644 --- a/tests/test_tutorial/test_one/test_tutorial004.py +++ b/tests/test_tutorial/test_one/test_tutorial004.py @@ -1,17 +1,27 @@ -from unittest.mock import patch +import importlib +from types import ModuleType import pytest from sqlalchemy.exc import MultipleResultsFound from sqlmodel import Session, create_engine, delete -from ...conftest import get_testing_print_function +from ...conftest import PrintMock -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.one import tutorial004 as mod - +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial004_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.one.{request.param}") mod.sqlite_url = "sqlite://" mod.engine = create_engine(mod.sqlite_url) + return mod + + +def test_tutorial(print_mock: PrintMock, mod: ModuleType): with pytest.raises(MultipleResultsFound): mod.main() with Session(mod.engine) as session: @@ -21,13 +31,8 @@ def test_tutorial(clear_sqlmodel): session.add(mod.Hero(name="Test Hero", secret_name="Secret Test Hero", age=24)) session.commit() - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.select_heroes() - assert calls == [ + mod.select_heroes() + assert print_mock.calls == [ [ "Hero:", { diff --git a/tests/test_tutorial/test_one/test_tutorial004_py310.py b/tests/test_tutorial/test_one/test_tutorial004_py310.py deleted file mode 100644 index cf365a4fe5..0000000000 --- a/tests/test_tutorial/test_one/test_tutorial004_py310.py +++ /dev/null @@ -1,41 +0,0 @@ -from unittest.mock import patch - -import pytest -from sqlalchemy.exc import MultipleResultsFound -from sqlmodel import Session, create_engine, delete - -from ...conftest import get_testing_print_function, needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.one import tutorial004_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - with pytest.raises(MultipleResultsFound): - mod.main() - with Session(mod.engine) as session: - # TODO: create delete() function - # TODO: add overloads for .exec() with delete object - session.exec(delete(mod.Hero)) - session.add(mod.Hero(name="Test Hero", secret_name="Secret Test Hero", age=24)) - session.commit() - - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.select_heroes() - assert calls == [ - [ - "Hero:", - { - "id": 1, - "name": "Test Hero", - "secret_name": "Secret Test Hero", - "age": 24, - }, - ] - ] diff --git a/tests/test_tutorial/test_one/test_tutorial005.py b/tests/test_tutorial/test_one/test_tutorial005.py index 0c25ffa39d..d795b4c70a 100644 --- a/tests/test_tutorial/test_one/test_tutorial005.py +++ b/tests/test_tutorial/test_one/test_tutorial005.py @@ -1,17 +1,27 @@ -from unittest.mock import patch +import importlib +from types import ModuleType import pytest from sqlalchemy.exc import NoResultFound from sqlmodel import Session, create_engine, delete -from ...conftest import get_testing_print_function +from ...conftest import PrintMock -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.one import tutorial005 as mod - +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial005_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.one.{request.param}") mod.sqlite_url = "sqlite://" mod.engine = create_engine(mod.sqlite_url) + return mod + + +def test_tutorial(print_mock: PrintMock, mod: ModuleType): with pytest.raises(NoResultFound): mod.main() with Session(mod.engine) as session: @@ -21,13 +31,8 @@ def test_tutorial(clear_sqlmodel): session.add(mod.Hero(name="Test Hero", secret_name="Secret Test Hero", age=24)) session.commit() - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.select_heroes() - assert calls == [ + mod.select_heroes() + assert print_mock.calls == [ [ "Hero:", { diff --git a/tests/test_tutorial/test_one/test_tutorial005_py310.py b/tests/test_tutorial/test_one/test_tutorial005_py310.py deleted file mode 100644 index f1fce7d764..0000000000 --- a/tests/test_tutorial/test_one/test_tutorial005_py310.py +++ /dev/null @@ -1,41 +0,0 @@ -from unittest.mock import patch - -import pytest -from sqlalchemy.exc import NoResultFound -from sqlmodel import Session, create_engine, delete - -from ...conftest import get_testing_print_function, needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.one import tutorial005_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - with pytest.raises(NoResultFound): - mod.main() - with Session(mod.engine) as session: - # TODO: create delete() function - # TODO: add overloads for .exec() with delete object - session.exec(delete(mod.Hero)) - session.add(mod.Hero(name="Test Hero", secret_name="Secret Test Hero", age=24)) - session.commit() - - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.select_heroes() - assert calls == [ - [ - "Hero:", - { - "id": 1, - "name": "Test Hero", - "secret_name": "Secret Test Hero", - "age": 24, - }, - ] - ] diff --git a/tests/test_tutorial/test_one/test_tutorial006.py b/tests/test_tutorial/test_one/test_tutorial006.py index 01c1af4602..fe65e1910f 100644 --- a/tests/test_tutorial/test_one/test_tutorial006.py +++ b/tests/test_tutorial/test_one/test_tutorial006.py @@ -1,22 +1,28 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.one import tutorial006 as mod - +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial006_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.one.{request.param}") mod.sqlite_url = "sqlite://" mod.engine = create_engine(mod.sqlite_url) - calls = [] + return mod - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() + assert print_mock.calls == [ [ "Hero:", {"name": "Deadpond", "secret_name": "Dive Wilson", "age": None, "id": 1}, diff --git a/tests/test_tutorial/test_one/test_tutorial006_py310.py b/tests/test_tutorial/test_one/test_tutorial006_py310.py deleted file mode 100644 index ad8577c7ae..0000000000 --- a/tests/test_tutorial/test_one/test_tutorial006_py310.py +++ /dev/null @@ -1,25 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.one import tutorial006_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ - [ - "Hero:", - {"name": "Deadpond", "secret_name": "Dive Wilson", "age": None, "id": 1}, - ] - ] diff --git a/tests/test_tutorial/test_one/test_tutorial007.py b/tests/test_tutorial/test_one/test_tutorial007.py index e8b984b050..0be06e5117 100644 --- a/tests/test_tutorial/test_one/test_tutorial007.py +++ b/tests/test_tutorial/test_one/test_tutorial007.py @@ -1,22 +1,28 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.one import tutorial007 as mod - +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial007_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.one.{request.param}") mod.sqlite_url = "sqlite://" mod.engine = create_engine(mod.sqlite_url) - calls = [] + return mod - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() + assert print_mock.calls == [ [ "Hero:", {"name": "Deadpond", "secret_name": "Dive Wilson", "age": None, "id": 1}, diff --git a/tests/test_tutorial/test_one/test_tutorial007_py310.py b/tests/test_tutorial/test_one/test_tutorial007_py310.py deleted file mode 100644 index 15b2306fc6..0000000000 --- a/tests/test_tutorial/test_one/test_tutorial007_py310.py +++ /dev/null @@ -1,25 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.one import tutorial007_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ - [ - "Hero:", - {"name": "Deadpond", "secret_name": "Dive Wilson", "age": None, "id": 1}, - ] - ] diff --git a/tests/test_tutorial/test_one/test_tutorial008.py b/tests/test_tutorial/test_one/test_tutorial008.py index e0ea766f37..aa60727896 100644 --- a/tests/test_tutorial/test_one/test_tutorial008.py +++ b/tests/test_tutorial/test_one/test_tutorial008.py @@ -1,22 +1,28 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.one import tutorial008 as mod - +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial008_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.one.{request.param}") mod.sqlite_url = "sqlite://" mod.engine = create_engine(mod.sqlite_url) - calls = [] + return mod - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() + assert print_mock.calls == [ [ "Hero:", {"name": "Deadpond", "secret_name": "Dive Wilson", "age": None, "id": 1}, diff --git a/tests/test_tutorial/test_one/test_tutorial008_py310.py b/tests/test_tutorial/test_one/test_tutorial008_py310.py deleted file mode 100644 index c7d1fe55c9..0000000000 --- a/tests/test_tutorial/test_one/test_tutorial008_py310.py +++ /dev/null @@ -1,25 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.one import tutorial008_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ - [ - "Hero:", - {"name": "Deadpond", "secret_name": "Dive Wilson", "age": None, "id": 1}, - ] - ] diff --git a/tests/test_tutorial/test_one/test_tutorial009.py b/tests/test_tutorial/test_one/test_tutorial009.py index 63e01fe741..d7fcc46879 100644 --- a/tests/test_tutorial/test_one/test_tutorial009.py +++ b/tests/test_tutorial/test_one/test_tutorial009.py @@ -1,19 +1,25 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.one import tutorial009 as mod - +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial009_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.one.{request.param}") mod.sqlite_url = "sqlite://" mod.engine = create_engine(mod.sqlite_url) - calls = [] + return mod - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [["Hero:", None]] +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() + assert print_mock.calls == [["Hero:", None]] diff --git a/tests/test_tutorial/test_one/test_tutorial009_py310.py b/tests/test_tutorial/test_one/test_tutorial009_py310.py deleted file mode 100644 index 8e9fda5f73..0000000000 --- a/tests/test_tutorial/test_one/test_tutorial009_py310.py +++ /dev/null @@ -1,20 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.one import tutorial009_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [["Hero:", None]] diff --git a/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial001.py b/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial001.py index 30ec9fdc36..b11a496d48 100644 --- a/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial001.py +++ b/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial001.py @@ -1,10 +1,27 @@ -from unittest.mock import patch +import importlib +from types import ModuleType import pytest from sqlalchemy.exc import SAWarning from sqlmodel import create_engine -from ....conftest import get_testing_print_function +from ....conftest import PrintMock + + +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial001_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module( + f"docs_src.tutorial.relationship_attributes.back_populates.{request.param}" + ) + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + return mod + expected_calls = [ [ @@ -272,18 +289,7 @@ ] -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.back_populates import ( - tutorial001 as mod, - ) - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - with pytest.warns(SAWarning): - mod.main() - assert calls == expected_calls +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + with pytest.warns(SAWarning): + mod.main() + assert print_mock.calls == expected_calls diff --git a/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial001_py310.py b/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial001_py310.py deleted file mode 100644 index 384056ad7b..0000000000 --- a/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial001_py310.py +++ /dev/null @@ -1,290 +0,0 @@ -from unittest.mock import patch - -import pytest -from sqlalchemy.exc import SAWarning -from sqlmodel import create_engine - -from ....conftest import get_testing_print_function, needs_py310 - -expected_calls = [ - [ - "Created hero:", - { - "age": None, - "id": 1, - "secret_name": "Dive Wilson", - "team_id": 1, - "name": "Deadpond", - }, - ], - [ - "Created hero:", - { - "age": 48, - "id": 2, - "secret_name": "Tommy Sharp", - "team_id": 2, - "name": "Rusty-Man", - }, - ], - [ - "Created hero:", - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": None, - "name": "Spider-Boy", - }, - ], - [ - "Updated hero:", - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": 2, - "name": "Spider-Boy", - }, - ], - [ - "Team Wakaland:", - {"headquarters": "Wakaland Capital City", "id": 3, "name": "Wakaland"}, - ], - [ - "Preventers new hero:", - { - "age": 32, - "id": 6, - "secret_name": "Natalia Roman-on", - "team_id": 2, - "name": "Tarantula", - }, - ], - [ - "Preventers new hero:", - { - "age": 36, - "id": 7, - "secret_name": "Steve Weird", - "team_id": 2, - "name": "Dr. Weird", - }, - ], - [ - "Preventers new hero:", - { - "age": 93, - "id": 8, - "secret_name": "Esteban Rogelios", - "team_id": 2, - "name": "Captain North America", - }, - ], - [ - "Preventers heroes:", - [ - { - "age": 48, - "id": 2, - "secret_name": "Tommy Sharp", - "team_id": 2, - "name": "Rusty-Man", - }, - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": 2, - "name": "Spider-Boy", - }, - { - "age": 32, - "id": 6, - "secret_name": "Natalia Roman-on", - "team_id": 2, - "name": "Tarantula", - }, - { - "age": 36, - "id": 7, - "secret_name": "Steve Weird", - "team_id": 2, - "name": "Dr. Weird", - }, - { - "age": 93, - "id": 8, - "secret_name": "Esteban Rogelios", - "team_id": 2, - "name": "Captain North America", - }, - ], - ], - [ - "Hero Spider-Boy:", - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": 2, - "name": "Spider-Boy", - }, - ], - [ - "Preventers Team:", - {"headquarters": "Sharp Tower", "id": 2, "name": "Preventers"}, - ], - [ - "Preventers Team Heroes:", - [ - { - "age": 48, - "id": 2, - "secret_name": "Tommy Sharp", - "team_id": 2, - "name": "Rusty-Man", - }, - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": 2, - "name": "Spider-Boy", - }, - { - "age": 32, - "id": 6, - "secret_name": "Natalia Roman-on", - "team_id": 2, - "name": "Tarantula", - }, - { - "age": 36, - "id": 7, - "secret_name": "Steve Weird", - "team_id": 2, - "name": "Dr. Weird", - }, - { - "age": 93, - "id": 8, - "secret_name": "Esteban Rogelios", - "team_id": 2, - "name": "Captain North America", - }, - ], - ], - [ - "Spider-Boy without team:", - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": 2, - "name": "Spider-Boy", - }, - ], - [ - "Preventers Team Heroes again:", - [ - { - "age": 48, - "id": 2, - "secret_name": "Tommy Sharp", - "team_id": 2, - "name": "Rusty-Man", - }, - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": 2, - "name": "Spider-Boy", - }, - { - "age": 32, - "id": 6, - "secret_name": "Natalia Roman-on", - "team_id": 2, - "name": "Tarantula", - }, - { - "age": 36, - "id": 7, - "secret_name": "Steve Weird", - "team_id": 2, - "name": "Dr. Weird", - }, - { - "age": 93, - "id": 8, - "secret_name": "Esteban Rogelios", - "team_id": 2, - "name": "Captain North America", - }, - ], - ], - ["After committing"], - [ - "Spider-Boy after commit:", - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": None, - "name": "Spider-Boy", - }, - ], - [ - "Preventers Team Heroes after commit:", - [ - { - "age": 48, - "id": 2, - "secret_name": "Tommy Sharp", - "team_id": 2, - "name": "Rusty-Man", - }, - { - "age": 32, - "id": 6, - "secret_name": "Natalia Roman-on", - "team_id": 2, - "name": "Tarantula", - }, - { - "age": 36, - "id": 7, - "secret_name": "Steve Weird", - "team_id": 2, - "name": "Dr. Weird", - }, - { - "age": 93, - "id": 8, - "secret_name": "Esteban Rogelios", - "team_id": 2, - "name": "Captain North America", - }, - ], - ], -] - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.back_populates import ( - tutorial001_py310 as mod, - ) - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - with pytest.warns(SAWarning): - mod.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial001_py39.py b/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial001_py39.py deleted file mode 100644 index 0597a88e89..0000000000 --- a/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial001_py39.py +++ /dev/null @@ -1,290 +0,0 @@ -from unittest.mock import patch - -import pytest -from sqlalchemy.exc import SAWarning -from sqlmodel import create_engine - -from ....conftest import get_testing_print_function, needs_py39 - -expected_calls = [ - [ - "Created hero:", - { - "age": None, - "id": 1, - "secret_name": "Dive Wilson", - "team_id": 1, - "name": "Deadpond", - }, - ], - [ - "Created hero:", - { - "age": 48, - "id": 2, - "secret_name": "Tommy Sharp", - "team_id": 2, - "name": "Rusty-Man", - }, - ], - [ - "Created hero:", - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": None, - "name": "Spider-Boy", - }, - ], - [ - "Updated hero:", - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": 2, - "name": "Spider-Boy", - }, - ], - [ - "Team Wakaland:", - {"headquarters": "Wakaland Capital City", "id": 3, "name": "Wakaland"}, - ], - [ - "Preventers new hero:", - { - "age": 32, - "id": 6, - "secret_name": "Natalia Roman-on", - "team_id": 2, - "name": "Tarantula", - }, - ], - [ - "Preventers new hero:", - { - "age": 36, - "id": 7, - "secret_name": "Steve Weird", - "team_id": 2, - "name": "Dr. Weird", - }, - ], - [ - "Preventers new hero:", - { - "age": 93, - "id": 8, - "secret_name": "Esteban Rogelios", - "team_id": 2, - "name": "Captain North America", - }, - ], - [ - "Preventers heroes:", - [ - { - "age": 48, - "id": 2, - "secret_name": "Tommy Sharp", - "team_id": 2, - "name": "Rusty-Man", - }, - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": 2, - "name": "Spider-Boy", - }, - { - "age": 32, - "id": 6, - "secret_name": "Natalia Roman-on", - "team_id": 2, - "name": "Tarantula", - }, - { - "age": 36, - "id": 7, - "secret_name": "Steve Weird", - "team_id": 2, - "name": "Dr. Weird", - }, - { - "age": 93, - "id": 8, - "secret_name": "Esteban Rogelios", - "team_id": 2, - "name": "Captain North America", - }, - ], - ], - [ - "Hero Spider-Boy:", - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": 2, - "name": "Spider-Boy", - }, - ], - [ - "Preventers Team:", - {"headquarters": "Sharp Tower", "id": 2, "name": "Preventers"}, - ], - [ - "Preventers Team Heroes:", - [ - { - "age": 48, - "id": 2, - "secret_name": "Tommy Sharp", - "team_id": 2, - "name": "Rusty-Man", - }, - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": 2, - "name": "Spider-Boy", - }, - { - "age": 32, - "id": 6, - "secret_name": "Natalia Roman-on", - "team_id": 2, - "name": "Tarantula", - }, - { - "age": 36, - "id": 7, - "secret_name": "Steve Weird", - "team_id": 2, - "name": "Dr. Weird", - }, - { - "age": 93, - "id": 8, - "secret_name": "Esteban Rogelios", - "team_id": 2, - "name": "Captain North America", - }, - ], - ], - [ - "Spider-Boy without team:", - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": 2, - "name": "Spider-Boy", - }, - ], - [ - "Preventers Team Heroes again:", - [ - { - "age": 48, - "id": 2, - "secret_name": "Tommy Sharp", - "team_id": 2, - "name": "Rusty-Man", - }, - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": 2, - "name": "Spider-Boy", - }, - { - "age": 32, - "id": 6, - "secret_name": "Natalia Roman-on", - "team_id": 2, - "name": "Tarantula", - }, - { - "age": 36, - "id": 7, - "secret_name": "Steve Weird", - "team_id": 2, - "name": "Dr. Weird", - }, - { - "age": 93, - "id": 8, - "secret_name": "Esteban Rogelios", - "team_id": 2, - "name": "Captain North America", - }, - ], - ], - ["After committing"], - [ - "Spider-Boy after commit:", - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": None, - "name": "Spider-Boy", - }, - ], - [ - "Preventers Team Heroes after commit:", - [ - { - "age": 48, - "id": 2, - "secret_name": "Tommy Sharp", - "team_id": 2, - "name": "Rusty-Man", - }, - { - "age": 32, - "id": 6, - "secret_name": "Natalia Roman-on", - "team_id": 2, - "name": "Tarantula", - }, - { - "age": 36, - "id": 7, - "secret_name": "Steve Weird", - "team_id": 2, - "name": "Dr. Weird", - }, - { - "age": 93, - "id": 8, - "secret_name": "Esteban Rogelios", - "team_id": 2, - "name": "Captain North America", - }, - ], - ], -] - - -@needs_py39 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.back_populates import ( - tutorial001_py39 as mod, - ) - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - with pytest.warns(SAWarning): - mod.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial002.py b/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial002.py index 98c01a9d54..3bb48d7f5e 100644 --- a/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial002.py +++ b/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial002.py @@ -1,8 +1,26 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ....conftest import get_testing_print_function +from ....conftest import PrintMock + + +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial002_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module( + f"docs_src.tutorial.relationship_attributes.back_populates.{request.param}" + ) + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + return mod + expected_calls = [ [ @@ -263,17 +281,6 @@ ] -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.back_populates import ( - tutorial002 as mod, - ) - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() + assert print_mock.calls == expected_calls diff --git a/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial002_py310.py b/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial002_py310.py deleted file mode 100644 index 50a891f310..0000000000 --- a/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial002_py310.py +++ /dev/null @@ -1,280 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ....conftest import get_testing_print_function, needs_py310 - -expected_calls = [ - [ - "Created hero:", - { - "age": None, - "id": 1, - "secret_name": "Dive Wilson", - "team_id": 1, - "name": "Deadpond", - }, - ], - [ - "Created hero:", - { - "age": 48, - "id": 2, - "secret_name": "Tommy Sharp", - "team_id": 2, - "name": "Rusty-Man", - }, - ], - [ - "Created hero:", - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": None, - "name": "Spider-Boy", - }, - ], - [ - "Updated hero:", - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": 2, - "name": "Spider-Boy", - }, - ], - [ - "Team Wakaland:", - {"id": 3, "name": "Wakaland", "headquarters": "Wakaland Capital City"}, - ], - [ - "Preventers new hero:", - { - "age": 32, - "id": 6, - "secret_name": "Natalia Roman-on", - "team_id": 2, - "name": "Tarantula", - }, - ], - [ - "Preventers new hero:", - { - "age": 36, - "id": 7, - "secret_name": "Steve Weird", - "team_id": 2, - "name": "Dr. Weird", - }, - ], - [ - "Preventers new hero:", - { - "age": 93, - "id": 8, - "secret_name": "Esteban Rogelios", - "team_id": 2, - "name": "Captain North America", - }, - ], - [ - "Preventers heroes:", - [ - { - "age": 48, - "id": 2, - "secret_name": "Tommy Sharp", - "team_id": 2, - "name": "Rusty-Man", - }, - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": 2, - "name": "Spider-Boy", - }, - { - "age": 32, - "id": 6, - "secret_name": "Natalia Roman-on", - "team_id": 2, - "name": "Tarantula", - }, - { - "age": 36, - "id": 7, - "secret_name": "Steve Weird", - "team_id": 2, - "name": "Dr. Weird", - }, - { - "age": 93, - "id": 8, - "secret_name": "Esteban Rogelios", - "team_id": 2, - "name": "Captain North America", - }, - ], - ], - [ - "Hero Spider-Boy:", - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": 2, - "name": "Spider-Boy", - }, - ], - [ - "Preventers Team:", - {"id": 2, "name": "Preventers", "headquarters": "Sharp Tower"}, - ], - [ - "Preventers Team Heroes:", - [ - { - "age": 48, - "id": 2, - "secret_name": "Tommy Sharp", - "team_id": 2, - "name": "Rusty-Man", - }, - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": 2, - "name": "Spider-Boy", - }, - { - "age": 32, - "id": 6, - "secret_name": "Natalia Roman-on", - "team_id": 2, - "name": "Tarantula", - }, - { - "age": 36, - "id": 7, - "secret_name": "Steve Weird", - "team_id": 2, - "name": "Dr. Weird", - }, - { - "age": 93, - "id": 8, - "secret_name": "Esteban Rogelios", - "team_id": 2, - "name": "Captain North America", - }, - ], - ], - [ - "Spider-Boy without team:", - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": 2, - "name": "Spider-Boy", - }, - ], - [ - "Preventers Team Heroes again:", - [ - { - "age": 48, - "id": 2, - "secret_name": "Tommy Sharp", - "team_id": 2, - "name": "Rusty-Man", - }, - { - "age": 32, - "id": 6, - "secret_name": "Natalia Roman-on", - "team_id": 2, - "name": "Tarantula", - }, - { - "age": 36, - "id": 7, - "secret_name": "Steve Weird", - "team_id": 2, - "name": "Dr. Weird", - }, - { - "age": 93, - "id": 8, - "secret_name": "Esteban Rogelios", - "team_id": 2, - "name": "Captain North America", - }, - ], - ], - ["After committing"], - [ - "Spider-Boy after commit:", - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": None, - "name": "Spider-Boy", - }, - ], - [ - "Preventers Team Heroes after commit:", - [ - { - "age": 48, - "id": 2, - "secret_name": "Tommy Sharp", - "team_id": 2, - "name": "Rusty-Man", - }, - { - "age": 32, - "id": 6, - "secret_name": "Natalia Roman-on", - "team_id": 2, - "name": "Tarantula", - }, - { - "age": 36, - "id": 7, - "secret_name": "Steve Weird", - "team_id": 2, - "name": "Dr. Weird", - }, - { - "age": 93, - "id": 8, - "secret_name": "Esteban Rogelios", - "team_id": 2, - "name": "Captain North America", - }, - ], - ], -] - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.back_populates import ( - tutorial002_py310 as mod, - ) - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial002_py39.py b/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial002_py39.py deleted file mode 100644 index 3da6ce4aac..0000000000 --- a/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial002_py39.py +++ /dev/null @@ -1,280 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ....conftest import get_testing_print_function, needs_py39 - -expected_calls = [ - [ - "Created hero:", - { - "age": None, - "id": 1, - "secret_name": "Dive Wilson", - "team_id": 1, - "name": "Deadpond", - }, - ], - [ - "Created hero:", - { - "age": 48, - "id": 2, - "secret_name": "Tommy Sharp", - "team_id": 2, - "name": "Rusty-Man", - }, - ], - [ - "Created hero:", - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": None, - "name": "Spider-Boy", - }, - ], - [ - "Updated hero:", - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": 2, - "name": "Spider-Boy", - }, - ], - [ - "Team Wakaland:", - {"id": 3, "name": "Wakaland", "headquarters": "Wakaland Capital City"}, - ], - [ - "Preventers new hero:", - { - "age": 32, - "id": 6, - "secret_name": "Natalia Roman-on", - "team_id": 2, - "name": "Tarantula", - }, - ], - [ - "Preventers new hero:", - { - "age": 36, - "id": 7, - "secret_name": "Steve Weird", - "team_id": 2, - "name": "Dr. Weird", - }, - ], - [ - "Preventers new hero:", - { - "age": 93, - "id": 8, - "secret_name": "Esteban Rogelios", - "team_id": 2, - "name": "Captain North America", - }, - ], - [ - "Preventers heroes:", - [ - { - "age": 48, - "id": 2, - "secret_name": "Tommy Sharp", - "team_id": 2, - "name": "Rusty-Man", - }, - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": 2, - "name": "Spider-Boy", - }, - { - "age": 32, - "id": 6, - "secret_name": "Natalia Roman-on", - "team_id": 2, - "name": "Tarantula", - }, - { - "age": 36, - "id": 7, - "secret_name": "Steve Weird", - "team_id": 2, - "name": "Dr. Weird", - }, - { - "age": 93, - "id": 8, - "secret_name": "Esteban Rogelios", - "team_id": 2, - "name": "Captain North America", - }, - ], - ], - [ - "Hero Spider-Boy:", - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": 2, - "name": "Spider-Boy", - }, - ], - [ - "Preventers Team:", - {"id": 2, "name": "Preventers", "headquarters": "Sharp Tower"}, - ], - [ - "Preventers Team Heroes:", - [ - { - "age": 48, - "id": 2, - "secret_name": "Tommy Sharp", - "team_id": 2, - "name": "Rusty-Man", - }, - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": 2, - "name": "Spider-Boy", - }, - { - "age": 32, - "id": 6, - "secret_name": "Natalia Roman-on", - "team_id": 2, - "name": "Tarantula", - }, - { - "age": 36, - "id": 7, - "secret_name": "Steve Weird", - "team_id": 2, - "name": "Dr. Weird", - }, - { - "age": 93, - "id": 8, - "secret_name": "Esteban Rogelios", - "team_id": 2, - "name": "Captain North America", - }, - ], - ], - [ - "Spider-Boy without team:", - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": 2, - "name": "Spider-Boy", - }, - ], - [ - "Preventers Team Heroes again:", - [ - { - "age": 48, - "id": 2, - "secret_name": "Tommy Sharp", - "team_id": 2, - "name": "Rusty-Man", - }, - { - "age": 32, - "id": 6, - "secret_name": "Natalia Roman-on", - "team_id": 2, - "name": "Tarantula", - }, - { - "age": 36, - "id": 7, - "secret_name": "Steve Weird", - "team_id": 2, - "name": "Dr. Weird", - }, - { - "age": 93, - "id": 8, - "secret_name": "Esteban Rogelios", - "team_id": 2, - "name": "Captain North America", - }, - ], - ], - ["After committing"], - [ - "Spider-Boy after commit:", - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": None, - "name": "Spider-Boy", - }, - ], - [ - "Preventers Team Heroes after commit:", - [ - { - "age": 48, - "id": 2, - "secret_name": "Tommy Sharp", - "team_id": 2, - "name": "Rusty-Man", - }, - { - "age": 32, - "id": 6, - "secret_name": "Natalia Roman-on", - "team_id": 2, - "name": "Tarantula", - }, - { - "age": 36, - "id": 7, - "secret_name": "Steve Weird", - "team_id": 2, - "name": "Dr. Weird", - }, - { - "age": 93, - "id": 8, - "secret_name": "Esteban Rogelios", - "team_id": 2, - "name": "Captain North America", - }, - ], - ], -] - - -@needs_py39 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.back_populates import ( - tutorial002_py39 as mod, - ) - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial003.py b/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial003.py index 2ed66f76ca..fe0210d06e 100644 --- a/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial003.py +++ b/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial003.py @@ -1,15 +1,28 @@ +import importlib +from types import ModuleType + +import pytest from sqlalchemy import inspect from sqlalchemy.engine.reflection import Inspector from sqlmodel import create_engine -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.back_populates import ( - tutorial003 as mod, +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial003_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module( + f"docs_src.tutorial.relationship_attributes.back_populates.{request.param}" ) - mod.sqlite_url = "sqlite://" mod.engine = create_engine(mod.sqlite_url) + return mod + + +def test_tutorial(mod: ModuleType): mod.main() insp: Inspector = inspect(mod.engine) assert insp.has_table(str(mod.Hero.__tablename__)) diff --git a/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial003_py310.py b/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial003_py310.py deleted file mode 100644 index 82e0c1c03b..0000000000 --- a/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial003_py310.py +++ /dev/null @@ -1,21 +0,0 @@ -from sqlalchemy import inspect -from sqlalchemy.engine.reflection import Inspector -from sqlmodel import create_engine - -from ....conftest import needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.back_populates import ( - tutorial003_py310 as mod, - ) - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - mod.main() - insp: Inspector = inspect(mod.engine) - assert insp.has_table(str(mod.Hero.__tablename__)) - assert insp.has_table(str(mod.Weapon.__tablename__)) - assert insp.has_table(str(mod.Power.__tablename__)) - assert insp.has_table(str(mod.Team.__tablename__)) diff --git a/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial003_py39.py b/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial003_py39.py deleted file mode 100644 index d6059cb485..0000000000 --- a/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial003_py39.py +++ /dev/null @@ -1,21 +0,0 @@ -from sqlalchemy import inspect -from sqlalchemy.engine.reflection import Inspector -from sqlmodel import create_engine - -from ....conftest import needs_py39 - - -@needs_py39 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.back_populates import ( - tutorial003_py39 as mod, - ) - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - mod.main() - insp: Inspector = inspect(mod.engine) - assert insp.has_table(str(mod.Hero.__tablename__)) - assert insp.has_table(str(mod.Weapon.__tablename__)) - assert insp.has_table(str(mod.Power.__tablename__)) - assert insp.has_table(str(mod.Team.__tablename__)) diff --git a/tests/test_tutorial/test_relationship_attributes/test_create_and_update_relationships/test_tutorial001.py b/tests/test_tutorial/test_relationship_attributes/test_create_and_update_relationships/test_tutorial001.py index 7ced57c835..bb55823478 100644 --- a/tests/test_tutorial/test_relationship_attributes/test_create_and_update_relationships/test_tutorial001.py +++ b/tests/test_tutorial/test_relationship_attributes/test_create_and_update_relationships/test_tutorial001.py @@ -1,8 +1,26 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ....conftest import get_testing_print_function +from ....conftest import PrintMock + + +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial001_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module( + f"docs_src.tutorial.relationship_attributes.create_and_update_relationships.{request.param}" + ) + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + return mod + expected_calls = [ [ @@ -82,17 +100,6 @@ ] -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.create_and_update_relationships import ( - tutorial001 as mod, - ) - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() + assert print_mock.calls == expected_calls diff --git a/tests/test_tutorial/test_relationship_attributes/test_create_and_update_relationships/test_tutorial001_py310.py b/tests/test_tutorial/test_relationship_attributes/test_create_and_update_relationships/test_tutorial001_py310.py deleted file mode 100644 index c239b6d55c..0000000000 --- a/tests/test_tutorial/test_relationship_attributes/test_create_and_update_relationships/test_tutorial001_py310.py +++ /dev/null @@ -1,99 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ....conftest import get_testing_print_function, needs_py310 - -expected_calls = [ - [ - "Created hero:", - { - "age": None, - "id": 1, - "secret_name": "Dive Wilson", - "team_id": 1, - "name": "Deadpond", - }, - ], - [ - "Created hero:", - { - "age": 48, - "id": 2, - "secret_name": "Tommy Sharp", - "team_id": 2, - "name": "Rusty-Man", - }, - ], - [ - "Created hero:", - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": None, - "name": "Spider-Boy", - }, - ], - [ - "Updated hero:", - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": 2, - "name": "Spider-Boy", - }, - ], - [ - "Team Wakaland:", - {"id": 3, "headquarters": "Wakaland Capital City", "name": "Wakaland"}, - ], - [ - "Preventers new hero:", - { - "age": 32, - "id": 6, - "secret_name": "Natalia Roman-on", - "team_id": 2, - "name": "Tarantula", - }, - ], - [ - "Preventers new hero:", - { - "age": 36, - "id": 7, - "secret_name": "Steve Weird", - "team_id": 2, - "name": "Dr. Weird", - }, - ], - [ - "Preventers new hero:", - { - "age": 93, - "id": 8, - "secret_name": "Esteban Rogelios", - "team_id": 2, - "name": "Captain North America", - }, - ], -] - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.create_and_update_relationships import ( - tutorial001_py310 as mod, - ) - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_relationship_attributes/test_create_and_update_relationships/test_tutorial001_py39.py b/tests/test_tutorial/test_relationship_attributes/test_create_and_update_relationships/test_tutorial001_py39.py deleted file mode 100644 index c569eed0d5..0000000000 --- a/tests/test_tutorial/test_relationship_attributes/test_create_and_update_relationships/test_tutorial001_py39.py +++ /dev/null @@ -1,99 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ....conftest import get_testing_print_function, needs_py39 - -expected_calls = [ - [ - "Created hero:", - { - "age": None, - "id": 1, - "secret_name": "Dive Wilson", - "team_id": 1, - "name": "Deadpond", - }, - ], - [ - "Created hero:", - { - "age": 48, - "id": 2, - "secret_name": "Tommy Sharp", - "team_id": 2, - "name": "Rusty-Man", - }, - ], - [ - "Created hero:", - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": None, - "name": "Spider-Boy", - }, - ], - [ - "Updated hero:", - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": 2, - "name": "Spider-Boy", - }, - ], - [ - "Team Wakaland:", - {"id": 3, "headquarters": "Wakaland Capital City", "name": "Wakaland"}, - ], - [ - "Preventers new hero:", - { - "age": 32, - "id": 6, - "secret_name": "Natalia Roman-on", - "team_id": 2, - "name": "Tarantula", - }, - ], - [ - "Preventers new hero:", - { - "age": 36, - "id": 7, - "secret_name": "Steve Weird", - "team_id": 2, - "name": "Dr. Weird", - }, - ], - [ - "Preventers new hero:", - { - "age": 93, - "id": 8, - "secret_name": "Esteban Rogelios", - "team_id": 2, - "name": "Captain North America", - }, - ], -] - - -@needs_py39 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.create_and_update_relationships import ( - tutorial001_py39 as mod, - ) - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_relationship_attributes/test_define_relationship_attributes/test_tutorial001.py b/tests/test_tutorial/test_relationship_attributes/test_define_relationship_attributes/test_tutorial001.py index 14b38ca52e..fdd1ce6443 100644 --- a/tests/test_tutorial/test_relationship_attributes/test_define_relationship_attributes/test_tutorial001.py +++ b/tests/test_tutorial/test_relationship_attributes/test_define_relationship_attributes/test_tutorial001.py @@ -1,8 +1,26 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ....conftest import get_testing_print_function +from ....conftest import PrintMock + + +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial001_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module( + f"docs_src.tutorial.relationship_attributes.define_relationship_attributes.{request.param}" + ) + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + return mod + expected_calls = [ [ @@ -38,17 +56,6 @@ ] -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.define_relationship_attributes import ( - tutorial001 as mod, - ) - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() + assert print_mock.calls == expected_calls diff --git a/tests/test_tutorial/test_relationship_attributes/test_define_relationship_attributes/test_tutorial001_py310.py b/tests/test_tutorial/test_relationship_attributes/test_define_relationship_attributes/test_tutorial001_py310.py deleted file mode 100644 index f595dcaa04..0000000000 --- a/tests/test_tutorial/test_relationship_attributes/test_define_relationship_attributes/test_tutorial001_py310.py +++ /dev/null @@ -1,55 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ....conftest import get_testing_print_function, needs_py310 - -expected_calls = [ - [ - "Created hero:", - { - "name": "Deadpond", - "age": None, - "team_id": 1, - "id": 1, - "secret_name": "Dive Wilson", - }, - ], - [ - "Created hero:", - { - "name": "Rusty-Man", - "age": 48, - "team_id": 2, - "id": 2, - "secret_name": "Tommy Sharp", - }, - ], - [ - "Created hero:", - { - "name": "Spider-Boy", - "age": None, - "team_id": None, - "id": 3, - "secret_name": "Pedro Parqueador", - }, - ], -] - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.define_relationship_attributes import ( - tutorial001_py310 as mod, - ) - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_relationship_attributes/test_define_relationship_attributes/test_tutorial001_py39.py b/tests/test_tutorial/test_relationship_attributes/test_define_relationship_attributes/test_tutorial001_py39.py deleted file mode 100644 index d54c610d19..0000000000 --- a/tests/test_tutorial/test_relationship_attributes/test_define_relationship_attributes/test_tutorial001_py39.py +++ /dev/null @@ -1,55 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ....conftest import get_testing_print_function, needs_py39 - -expected_calls = [ - [ - "Created hero:", - { - "name": "Deadpond", - "age": None, - "team_id": 1, - "id": 1, - "secret_name": "Dive Wilson", - }, - ], - [ - "Created hero:", - { - "name": "Rusty-Man", - "age": 48, - "team_id": 2, - "id": 2, - "secret_name": "Tommy Sharp", - }, - ], - [ - "Created hero:", - { - "name": "Spider-Boy", - "age": None, - "team_id": None, - "id": 3, - "secret_name": "Pedro Parqueador", - }, - ], -] - - -@needs_py39 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.define_relationship_attributes import ( - tutorial001_py39 as mod, - ) - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial001.py b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial001.py index 863a84eb1c..30e991a755 100644 --- a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial001.py +++ b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial001.py @@ -1,24 +1,30 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ....conftest import get_testing_print_function +from ....conftest import PrintMock -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.cascade_delete_relationships import ( - tutorial001 as mod, +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial001_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module( + f"docs_src.tutorial.relationship_attributes.cascade_delete_relationships.{request.param}" ) - mod.sqlite_url = "sqlite://" mod.engine = create_engine(mod.sqlite_url) - calls = [] + return mod - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() + assert print_mock.calls == [ [ "Created hero:", { diff --git a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial001_py310.py b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial001_py310.py deleted file mode 100644 index 3262d2b244..0000000000 --- a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial001_py310.py +++ /dev/null @@ -1,73 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ....conftest import get_testing_print_function, needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.cascade_delete_relationships import ( - tutorial001_py310 as mod, - ) - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ - [ - "Created hero:", - { - "name": "Deadpond", - "secret_name": "Dive Wilson", - "team_id": 1, - "id": 1, - "age": None, - }, - ], - [ - "Created hero:", - { - "name": "Rusty-Man", - "secret_name": "Tommy Sharp", - "team_id": 2, - "id": 2, - "age": 48, - }, - ], - [ - "Created hero:", - { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "team_id": None, - "id": 3, - "age": None, - }, - ], - [ - "Updated hero:", - { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "team_id": 2, - "id": 3, - "age": None, - }, - ], - [ - "Team Wakaland:", - {"name": "Wakaland", "id": 3, "headquarters": "Wakaland Capital City"}, - ], - [ - "Deleted team:", - {"name": "Wakaland", "id": 3, "headquarters": "Wakaland Capital City"}, - ], - ["Black Lion not found:", None], - ["Princess Sure-E not found:", None], - ] diff --git a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial001_py39.py b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial001_py39.py deleted file mode 100644 index 840c354e83..0000000000 --- a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial001_py39.py +++ /dev/null @@ -1,73 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ....conftest import get_testing_print_function, needs_py39 - - -@needs_py39 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.cascade_delete_relationships import ( - tutorial001_py39 as mod, - ) - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ - [ - "Created hero:", - { - "name": "Deadpond", - "secret_name": "Dive Wilson", - "team_id": 1, - "id": 1, - "age": None, - }, - ], - [ - "Created hero:", - { - "name": "Rusty-Man", - "secret_name": "Tommy Sharp", - "team_id": 2, - "id": 2, - "age": 48, - }, - ], - [ - "Created hero:", - { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "team_id": None, - "id": 3, - "age": None, - }, - ], - [ - "Updated hero:", - { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "team_id": 2, - "id": 3, - "age": None, - }, - ], - [ - "Team Wakaland:", - {"name": "Wakaland", "id": 3, "headquarters": "Wakaland Capital City"}, - ], - [ - "Deleted team:", - {"name": "Wakaland", "id": 3, "headquarters": "Wakaland Capital City"}, - ], - ["Black Lion not found:", None], - ["Princess Sure-E not found:", None], - ] diff --git a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial002.py b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial002.py index a7d7a26364..cb70c8a5e9 100644 --- a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial002.py +++ b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial002.py @@ -1,24 +1,30 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ....conftest import get_testing_print_function +from ....conftest import PrintMock -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.cascade_delete_relationships import ( - tutorial002 as mod, +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial002_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module( + f"docs_src.tutorial.relationship_attributes.cascade_delete_relationships.{request.param}" ) - mod.sqlite_url = "sqlite://" mod.engine = create_engine(mod.sqlite_url) - calls = [] + return mod - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() + assert print_mock.calls == [ [ "Created hero:", { diff --git a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial002_py310.py b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial002_py310.py deleted file mode 100644 index 5c755f3a29..0000000000 --- a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial002_py310.py +++ /dev/null @@ -1,91 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ....conftest import get_testing_print_function, needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.cascade_delete_relationships import ( - tutorial002_py310 as mod, - ) - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ - [ - "Created hero:", - { - "age": None, - "id": 1, - "name": "Deadpond", - "secret_name": "Dive Wilson", - "team_id": 1, - }, - ], - [ - "Created hero:", - { - "age": 48, - "id": 2, - "name": "Rusty-Man", - "secret_name": "Tommy Sharp", - "team_id": 2, - }, - ], - [ - "Created hero:", - { - "age": None, - "id": 3, - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "team_id": None, - }, - ], - [ - "Updated hero:", - { - "age": None, - "id": 3, - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "team_id": 2, - }, - ], - [ - "Team Wakaland:", - {"headquarters": "Wakaland Capital City", "id": 3, "name": "Wakaland"}, - ], - [ - "Deleted team:", - {"headquarters": "Wakaland Capital City", "id": 3, "name": "Wakaland"}, - ], - [ - "Black Lion has no team:", - { - "age": 35, - "id": 4, - "name": "Black Lion", - "secret_name": "Trevor Challa", - "team_id": None, - }, - ], - [ - "Princess Sure-E has no team:", - { - "age": None, - "id": 5, - "name": "Princess Sure-E", - "secret_name": "Sure-E", - "team_id": None, - }, - ], - ] diff --git a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial002_py39.py b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial002_py39.py deleted file mode 100644 index 9937f6da4c..0000000000 --- a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial002_py39.py +++ /dev/null @@ -1,91 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ....conftest import get_testing_print_function, needs_py39 - - -@needs_py39 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.cascade_delete_relationships import ( - tutorial002_py39 as mod, - ) - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ - [ - "Created hero:", - { - "age": None, - "id": 1, - "name": "Deadpond", - "secret_name": "Dive Wilson", - "team_id": 1, - }, - ], - [ - "Created hero:", - { - "age": 48, - "id": 2, - "name": "Rusty-Man", - "secret_name": "Tommy Sharp", - "team_id": 2, - }, - ], - [ - "Created hero:", - { - "age": None, - "id": 3, - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "team_id": None, - }, - ], - [ - "Updated hero:", - { - "age": None, - "id": 3, - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "team_id": 2, - }, - ], - [ - "Team Wakaland:", - {"headquarters": "Wakaland Capital City", "id": 3, "name": "Wakaland"}, - ], - [ - "Deleted team:", - {"headquarters": "Wakaland Capital City", "id": 3, "name": "Wakaland"}, - ], - [ - "Black Lion has no team:", - { - "age": 35, - "id": 4, - "name": "Black Lion", - "secret_name": "Trevor Challa", - "team_id": None, - }, - ], - [ - "Princess Sure-E has no team:", - { - "age": None, - "id": 5, - "name": "Princess Sure-E", - "secret_name": "Sure-E", - "team_id": None, - }, - ], - ] diff --git a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial003.py b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial003.py index a3d3bc0f05..419c87b0eb 100644 --- a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial003.py +++ b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial003.py @@ -1,24 +1,30 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from tests.conftest import get_testing_print_function +from ....conftest import PrintMock -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.cascade_delete_relationships import ( - tutorial003 as mod, +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial003_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module( + f"docs_src.tutorial.relationship_attributes.cascade_delete_relationships.{request.param}" ) - mod.sqlite_url = "sqlite://" mod.engine = create_engine(mod.sqlite_url) - calls = [] + return mod - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() + assert print_mock.calls == [ [ "Created hero:", { diff --git a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial003_py310.py b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial003_py310.py deleted file mode 100644 index f9975f25f7..0000000000 --- a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial003_py310.py +++ /dev/null @@ -1,91 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ....conftest import get_testing_print_function, needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.cascade_delete_relationships import ( - tutorial003_py310 as mod, - ) - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ - [ - "Created hero:", - { - "age": None, - "id": 1, - "name": "Deadpond", - "secret_name": "Dive Wilson", - "team_id": 1, - }, - ], - [ - "Created hero:", - { - "age": 48, - "id": 2, - "name": "Rusty-Man", - "secret_name": "Tommy Sharp", - "team_id": 2, - }, - ], - [ - "Created hero:", - { - "age": None, - "id": 3, - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "team_id": None, - }, - ], - [ - "Updated hero:", - { - "age": None, - "id": 3, - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "team_id": 2, - }, - ], - [ - "Team Wakaland:", - {"id": 3, "headquarters": "Wakaland Capital City", "name": "Wakaland"}, - ], - [ - "Deleted team:", - {"id": 3, "headquarters": "Wakaland Capital City", "name": "Wakaland"}, - ], - [ - "Black Lion has no team:", - { - "age": 35, - "id": 4, - "name": "Black Lion", - "secret_name": "Trevor Challa", - "team_id": None, - }, - ], - [ - "Princess Sure-E has no team:", - { - "age": None, - "id": 5, - "name": "Princess Sure-E", - "secret_name": "Sure-E", - "team_id": None, - }, - ], - ] diff --git a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial003_py39.py b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial003_py39.py deleted file mode 100644 index b68bc6237d..0000000000 --- a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial003_py39.py +++ /dev/null @@ -1,91 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ....conftest import get_testing_print_function, needs_py39 - - -@needs_py39 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.cascade_delete_relationships import ( - tutorial003_py39 as mod, - ) - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ - [ - "Created hero:", - { - "age": None, - "id": 1, - "name": "Deadpond", - "secret_name": "Dive Wilson", - "team_id": 1, - }, - ], - [ - "Created hero:", - { - "age": 48, - "id": 2, - "name": "Rusty-Man", - "secret_name": "Tommy Sharp", - "team_id": 2, - }, - ], - [ - "Created hero:", - { - "age": None, - "id": 3, - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "team_id": None, - }, - ], - [ - "Updated hero:", - { - "age": None, - "id": 3, - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "team_id": 2, - }, - ], - [ - "Team Wakaland:", - {"id": 3, "headquarters": "Wakaland Capital City", "name": "Wakaland"}, - ], - [ - "Deleted team:", - {"id": 3, "headquarters": "Wakaland Capital City", "name": "Wakaland"}, - ], - [ - "Black Lion has no team:", - { - "age": 35, - "id": 4, - "name": "Black Lion", - "secret_name": "Trevor Challa", - "team_id": None, - }, - ], - [ - "Princess Sure-E has no team:", - { - "age": None, - "id": 5, - "name": "Princess Sure-E", - "secret_name": "Sure-E", - "team_id": None, - }, - ], - ] diff --git a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial004.py b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial004.py index d5da12e6a5..e0bc89a090 100644 --- a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial004.py +++ b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial004.py @@ -1,36 +1,39 @@ -from unittest.mock import patch +import importlib +from types import ModuleType import pytest from sqlalchemy.exc import IntegrityError from sqlmodel import Session, create_engine, select -from tests.conftest import get_testing_print_function +from ....conftest import PrintMock -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.cascade_delete_relationships import ( - tutorial004 as mod, +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial004_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module( + f"docs_src.tutorial.relationship_attributes.cascade_delete_relationships.{request.param}" ) - mod.sqlite_url = "sqlite://" mod.engine = create_engine(mod.sqlite_url) - calls = [] + return mod - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.create_db_and_tables() - mod.create_heroes() - mod.select_deleted_heroes() - with Session(mod.engine) as session: - team = session.exec( - select(mod.Team).where(mod.Team.name == "Wakaland") - ).one() - team.heroes.clear() - session.add(team) - session.commit() - mod.delete_team() - assert calls == [ +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.create_db_and_tables() + mod.create_heroes() + mod.select_deleted_heroes() + with Session(mod.engine) as session: + team = session.exec(select(mod.Team).where(mod.Team.name == "Wakaland")).one() + team.heroes.clear() + session.add(team) + session.commit() + mod.delete_team() + assert print_mock.calls == [ [ "Created hero:", { diff --git a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial004_py310.py b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial004_py310.py deleted file mode 100644 index 3ce37700cf..0000000000 --- a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial004_py310.py +++ /dev/null @@ -1,107 +0,0 @@ -from unittest.mock import patch - -import pytest -from sqlalchemy.exc import IntegrityError -from sqlmodel import Session, create_engine, select - -from tests.conftest import get_testing_print_function, needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.cascade_delete_relationships import ( - tutorial004_py310 as mod, - ) - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.create_db_and_tables() - mod.create_heroes() - mod.select_deleted_heroes() - with Session(mod.engine) as session: - team = session.exec( - select(mod.Team).where(mod.Team.name == "Wakaland") - ).one() - team.heroes.clear() - session.add(team) - session.commit() - mod.delete_team() - assert calls == [ - [ - "Created hero:", - { - "age": None, - "id": 1, - "name": "Deadpond", - "secret_name": "Dive Wilson", - "team_id": 1, - }, - ], - [ - "Created hero:", - { - "age": 48, - "id": 2, - "name": "Rusty-Man", - "secret_name": "Tommy Sharp", - "team_id": 2, - }, - ], - [ - "Created hero:", - { - "age": None, - "id": 3, - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "team_id": None, - }, - ], - [ - "Updated hero:", - { - "age": None, - "id": 3, - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "team_id": 2, - }, - ], - [ - "Team Wakaland:", - {"headquarters": "Wakaland Capital City", "id": 3, "name": "Wakaland"}, - ], - [ - "Black Lion has no team:", - { - "age": 35, - "id": 4, - "name": "Black Lion", - "secret_name": "Trevor Challa", - "team_id": 3, - }, - ], - [ - "Princess Sure-E has no team:", - { - "age": None, - "id": 5, - "name": "Princess Sure-E", - "secret_name": "Sure-E", - "team_id": 3, - }, - ], - [ - "Deleted team:", - {"headquarters": "Wakaland Capital City", "id": 3, "name": "Wakaland"}, - ], - ] - - with pytest.raises(IntegrityError) as exc: - mod.main() - assert "FOREIGN KEY constraint failed" in str(exc.value) diff --git a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial004_py39.py b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial004_py39.py deleted file mode 100644 index 1c51fc0c90..0000000000 --- a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial004_py39.py +++ /dev/null @@ -1,107 +0,0 @@ -from unittest.mock import patch - -import pytest -from sqlalchemy.exc import IntegrityError -from sqlmodel import Session, create_engine, select - -from tests.conftest import get_testing_print_function, needs_py39 - - -@needs_py39 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.cascade_delete_relationships import ( - tutorial004_py39 as mod, - ) - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.create_db_and_tables() - mod.create_heroes() - mod.select_deleted_heroes() - with Session(mod.engine) as session: - team = session.exec( - select(mod.Team).where(mod.Team.name == "Wakaland") - ).one() - team.heroes.clear() - session.add(team) - session.commit() - mod.delete_team() - assert calls == [ - [ - "Created hero:", - { - "age": None, - "id": 1, - "name": "Deadpond", - "secret_name": "Dive Wilson", - "team_id": 1, - }, - ], - [ - "Created hero:", - { - "age": 48, - "id": 2, - "name": "Rusty-Man", - "secret_name": "Tommy Sharp", - "team_id": 2, - }, - ], - [ - "Created hero:", - { - "age": None, - "id": 3, - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "team_id": None, - }, - ], - [ - "Updated hero:", - { - "age": None, - "id": 3, - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "team_id": 2, - }, - ], - [ - "Team Wakaland:", - {"headquarters": "Wakaland Capital City", "id": 3, "name": "Wakaland"}, - ], - [ - "Black Lion has no team:", - { - "age": 35, - "id": 4, - "name": "Black Lion", - "secret_name": "Trevor Challa", - "team_id": 3, - }, - ], - [ - "Princess Sure-E has no team:", - { - "age": None, - "id": 5, - "name": "Princess Sure-E", - "secret_name": "Sure-E", - "team_id": 3, - }, - ], - [ - "Deleted team:", - {"headquarters": "Wakaland Capital City", "id": 3, "name": "Wakaland"}, - ], - ] - - with pytest.raises(IntegrityError) as exc: - mod.main() - assert "FOREIGN KEY constraint failed" in str(exc.value) diff --git a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial005.py b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial005.py index a6a00608a9..bdcb2f647b 100644 --- a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial005.py +++ b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial005.py @@ -1,24 +1,30 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from tests.conftest import get_testing_print_function +from ....conftest import PrintMock -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.cascade_delete_relationships import ( - tutorial005 as mod, +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial005_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module( + f"docs_src.tutorial.relationship_attributes.cascade_delete_relationships.{request.param}" ) - mod.sqlite_url = "sqlite://" mod.engine = create_engine(mod.sqlite_url) - calls = [] + return mod - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() + assert print_mock.calls == [ [ "Created hero:", { diff --git a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial005_py310.py b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial005_py310.py deleted file mode 100644 index 54ad1b79de..0000000000 --- a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial005_py310.py +++ /dev/null @@ -1,95 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from tests.conftest import get_testing_print_function, needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.cascade_delete_relationships import ( - tutorial005_py310 as mod, - ) - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ - [ - "Created hero:", - { - "name": "Deadpond", - "secret_name": "Dive Wilson", - "team_id": 1, - "id": 1, - "age": None, - }, - ], - [ - "Created hero:", - { - "name": "Rusty-Man", - "secret_name": "Tommy Sharp", - "team_id": 2, - "id": 2, - "age": 48, - }, - ], - [ - "Created hero:", - { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "team_id": None, - "id": 3, - "age": None, - }, - ], - [ - "Updated hero:", - { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "team_id": 2, - "id": 3, - "age": None, - }, - ], - [ - "Team Wakaland:", - {"id": 3, "headquarters": "Wakaland Capital City", "name": "Wakaland"}, - ], - [ - "Team with removed heroes:", - {"id": 3, "headquarters": "Wakaland Capital City", "name": "Wakaland"}, - ], - [ - "Deleted team:", - {"id": 3, "headquarters": "Wakaland Capital City", "name": "Wakaland"}, - ], - [ - "Black Lion has no team:", - { - "name": "Black Lion", - "secret_name": "Trevor Challa", - "team_id": None, - "id": 4, - "age": 35, - }, - ], - [ - "Princess Sure-E has no team:", - { - "name": "Princess Sure-E", - "secret_name": "Sure-E", - "team_id": None, - "id": 5, - "age": None, - }, - ], - ] diff --git a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial005_py39.py b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial005_py39.py deleted file mode 100644 index 8151ab9232..0000000000 --- a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial005_py39.py +++ /dev/null @@ -1,95 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from tests.conftest import get_testing_print_function, needs_py39 - - -@needs_py39 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.cascade_delete_relationships import ( - tutorial005_py39 as mod, - ) - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ - [ - "Created hero:", - { - "name": "Deadpond", - "secret_name": "Dive Wilson", - "team_id": 1, - "id": 1, - "age": None, - }, - ], - [ - "Created hero:", - { - "name": "Rusty-Man", - "secret_name": "Tommy Sharp", - "team_id": 2, - "id": 2, - "age": 48, - }, - ], - [ - "Created hero:", - { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "team_id": None, - "id": 3, - "age": None, - }, - ], - [ - "Updated hero:", - { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "team_id": 2, - "id": 3, - "age": None, - }, - ], - [ - "Team Wakaland:", - {"id": 3, "headquarters": "Wakaland Capital City", "name": "Wakaland"}, - ], - [ - "Team with removed heroes:", - {"id": 3, "headquarters": "Wakaland Capital City", "name": "Wakaland"}, - ], - [ - "Deleted team:", - {"id": 3, "headquarters": "Wakaland Capital City", "name": "Wakaland"}, - ], - [ - "Black Lion has no team:", - { - "name": "Black Lion", - "secret_name": "Trevor Challa", - "team_id": None, - "id": 4, - "age": 35, - }, - ], - [ - "Princess Sure-E has no team:", - { - "name": "Princess Sure-E", - "secret_name": "Sure-E", - "team_id": None, - "id": 5, - "age": None, - }, - ], - ] diff --git a/tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial001.py b/tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial001.py index 9fc70012d8..a600772810 100644 --- a/tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial001.py +++ b/tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial001.py @@ -1,8 +1,26 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ....conftest import get_testing_print_function +from ....conftest import PrintMock + + +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial001_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module( + f"docs_src.tutorial.relationship_attributes.read_relationships.{request.param}" + ) + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + return mod + expected_calls = [ [ @@ -90,17 +108,6 @@ ] -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.read_relationships import ( - tutorial001 as mod, - ) - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() + assert print_mock.calls == expected_calls diff --git a/tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial001_py310.py b/tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial001_py310.py deleted file mode 100644 index 9a4e3cc53b..0000000000 --- a/tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial001_py310.py +++ /dev/null @@ -1,107 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ....conftest import get_testing_print_function, needs_py310 - -expected_calls = [ - [ - "Created hero:", - { - "age": None, - "id": 1, - "secret_name": "Dive Wilson", - "team_id": 1, - "name": "Deadpond", - }, - ], - [ - "Created hero:", - { - "age": 48, - "id": 2, - "secret_name": "Tommy Sharp", - "team_id": 2, - "name": "Rusty-Man", - }, - ], - [ - "Created hero:", - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": None, - "name": "Spider-Boy", - }, - ], - [ - "Updated hero:", - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": 2, - "name": "Spider-Boy", - }, - ], - [ - "Team Wakaland:", - {"headquarters": "Wakaland Capital City", "id": 3, "name": "Wakaland"}, - ], - [ - "Preventers new hero:", - { - "age": 32, - "id": 6, - "secret_name": "Natalia Roman-on", - "team_id": 2, - "name": "Tarantula", - }, - ], - [ - "Preventers new hero:", - { - "age": 36, - "id": 7, - "secret_name": "Steve Weird", - "team_id": 2, - "name": "Dr. Weird", - }, - ], - [ - "Preventers new hero:", - { - "age": 93, - "id": 8, - "secret_name": "Esteban Rogelios", - "team_id": 2, - "name": "Captain North America", - }, - ], - [ - "Spider-Boy's team:", - {"headquarters": "Sharp Tower", "id": 2, "name": "Preventers"}, - ], - [ - "Spider-Boy's team again:", - {"headquarters": "Sharp Tower", "id": 2, "name": "Preventers"}, - ], -] - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.read_relationships import ( - tutorial001_py310 as mod, - ) - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial001_py39.py b/tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial001_py39.py deleted file mode 100644 index 6b23980665..0000000000 --- a/tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial001_py39.py +++ /dev/null @@ -1,107 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ....conftest import get_testing_print_function, needs_py39 - -expected_calls = [ - [ - "Created hero:", - { - "age": None, - "id": 1, - "secret_name": "Dive Wilson", - "team_id": 1, - "name": "Deadpond", - }, - ], - [ - "Created hero:", - { - "age": 48, - "id": 2, - "secret_name": "Tommy Sharp", - "team_id": 2, - "name": "Rusty-Man", - }, - ], - [ - "Created hero:", - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": None, - "name": "Spider-Boy", - }, - ], - [ - "Updated hero:", - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": 2, - "name": "Spider-Boy", - }, - ], - [ - "Team Wakaland:", - {"headquarters": "Wakaland Capital City", "id": 3, "name": "Wakaland"}, - ], - [ - "Preventers new hero:", - { - "age": 32, - "id": 6, - "secret_name": "Natalia Roman-on", - "team_id": 2, - "name": "Tarantula", - }, - ], - [ - "Preventers new hero:", - { - "age": 36, - "id": 7, - "secret_name": "Steve Weird", - "team_id": 2, - "name": "Dr. Weird", - }, - ], - [ - "Preventers new hero:", - { - "age": 93, - "id": 8, - "secret_name": "Esteban Rogelios", - "team_id": 2, - "name": "Captain North America", - }, - ], - [ - "Spider-Boy's team:", - {"headquarters": "Sharp Tower", "id": 2, "name": "Preventers"}, - ], - [ - "Spider-Boy's team again:", - {"headquarters": "Sharp Tower", "id": 2, "name": "Preventers"}, - ], -] - - -@needs_py39 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.read_relationships import ( - tutorial001_py39 as mod, - ) - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial002.py b/tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial002.py index d827b1ff15..d4c2003ed5 100644 --- a/tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial002.py +++ b/tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial002.py @@ -1,8 +1,26 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ....conftest import get_testing_print_function +from ....conftest import PrintMock + + +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial002_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module( + f"docs_src.tutorial.relationship_attributes.read_relationships.{request.param}" + ) + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + return mod + expected_calls = [ [ @@ -132,17 +150,6 @@ ] -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.read_relationships import ( - tutorial002 as mod, - ) - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() + assert print_mock.calls == expected_calls diff --git a/tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial002_py310.py b/tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial002_py310.py deleted file mode 100644 index 0cc9ae3326..0000000000 --- a/tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial002_py310.py +++ /dev/null @@ -1,149 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ....conftest import get_testing_print_function, needs_py310 - -expected_calls = [ - [ - "Created hero:", - { - "age": None, - "id": 1, - "secret_name": "Dive Wilson", - "team_id": 1, - "name": "Deadpond", - }, - ], - [ - "Created hero:", - { - "age": 48, - "id": 2, - "secret_name": "Tommy Sharp", - "team_id": 2, - "name": "Rusty-Man", - }, - ], - [ - "Created hero:", - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": None, - "name": "Spider-Boy", - }, - ], - [ - "Updated hero:", - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": 2, - "name": "Spider-Boy", - }, - ], - [ - "Team Wakaland:", - {"id": 3, "name": "Wakaland", "headquarters": "Wakaland Capital City"}, - ], - [ - "Preventers new hero:", - { - "age": 32, - "id": 6, - "secret_name": "Natalia Roman-on", - "team_id": 2, - "name": "Tarantula", - }, - ], - [ - "Preventers new hero:", - { - "age": 36, - "id": 7, - "secret_name": "Steve Weird", - "team_id": 2, - "name": "Dr. Weird", - }, - ], - [ - "Preventers new hero:", - { - "age": 93, - "id": 8, - "secret_name": "Esteban Rogelios", - "team_id": 2, - "name": "Captain North America", - }, - ], - [ - "Preventers heroes:", - [ - { - "age": 48, - "id": 2, - "secret_name": "Tommy Sharp", - "team_id": 2, - "name": "Rusty-Man", - }, - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": 2, - "name": "Spider-Boy", - }, - { - "age": 32, - "id": 6, - "secret_name": "Natalia Roman-on", - "team_id": 2, - "name": "Tarantula", - }, - { - "age": 36, - "id": 7, - "secret_name": "Steve Weird", - "team_id": 2, - "name": "Dr. Weird", - }, - { - "age": 93, - "id": 8, - "secret_name": "Esteban Rogelios", - "team_id": 2, - "name": "Captain North America", - }, - ], - ], - [ - "Spider-Boy without team:", - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": None, - "name": "Spider-Boy", - }, - ], -] - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.read_relationships import ( - tutorial002_py310 as mod, - ) - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial002_py39.py b/tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial002_py39.py deleted file mode 100644 index 891f4ca6a9..0000000000 --- a/tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial002_py39.py +++ /dev/null @@ -1,149 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ....conftest import get_testing_print_function, needs_py39 - -expected_calls = [ - [ - "Created hero:", - { - "age": None, - "id": 1, - "secret_name": "Dive Wilson", - "team_id": 1, - "name": "Deadpond", - }, - ], - [ - "Created hero:", - { - "age": 48, - "id": 2, - "secret_name": "Tommy Sharp", - "team_id": 2, - "name": "Rusty-Man", - }, - ], - [ - "Created hero:", - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": None, - "name": "Spider-Boy", - }, - ], - [ - "Updated hero:", - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": 2, - "name": "Spider-Boy", - }, - ], - [ - "Team Wakaland:", - {"id": 3, "name": "Wakaland", "headquarters": "Wakaland Capital City"}, - ], - [ - "Preventers new hero:", - { - "age": 32, - "id": 6, - "secret_name": "Natalia Roman-on", - "team_id": 2, - "name": "Tarantula", - }, - ], - [ - "Preventers new hero:", - { - "age": 36, - "id": 7, - "secret_name": "Steve Weird", - "team_id": 2, - "name": "Dr. Weird", - }, - ], - [ - "Preventers new hero:", - { - "age": 93, - "id": 8, - "secret_name": "Esteban Rogelios", - "team_id": 2, - "name": "Captain North America", - }, - ], - [ - "Preventers heroes:", - [ - { - "age": 48, - "id": 2, - "secret_name": "Tommy Sharp", - "team_id": 2, - "name": "Rusty-Man", - }, - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": 2, - "name": "Spider-Boy", - }, - { - "age": 32, - "id": 6, - "secret_name": "Natalia Roman-on", - "team_id": 2, - "name": "Tarantula", - }, - { - "age": 36, - "id": 7, - "secret_name": "Steve Weird", - "team_id": 2, - "name": "Dr. Weird", - }, - { - "age": 93, - "id": 8, - "secret_name": "Esteban Rogelios", - "team_id": 2, - "name": "Captain North America", - }, - ], - ], - [ - "Spider-Boy without team:", - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": None, - "name": "Spider-Boy", - }, - ], -] - - -@needs_py39 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.read_relationships import ( - tutorial002_py39 as mod, - ) - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_select/test_tutorial001_py310_tutorial002_py310.py b/tests/test_tutorial/test_select/test_tutorial001_py310_tutorial002_py310.py deleted file mode 100644 index 7521b6b717..0000000000 --- a/tests/test_tutorial/test_select/test_tutorial001_py310_tutorial002_py310.py +++ /dev/null @@ -1,57 +0,0 @@ -from typing import Any, Dict, List, Union -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - - -def check_calls(calls: List[List[Union[str, Dict[str, Any]]]]): - assert calls[0][0] == { - "name": "Deadpond", - "secret_name": "Dive Wilson", - "age": None, - "id": 1, - } - assert calls[1][0] == { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "age": None, - "id": 2, - } - assert calls[2][0] == { - "name": "Rusty-Man", - "secret_name": "Tommy Sharp", - "age": 48, - "id": 3, - } - - -@needs_py310 -def test_tutorial_001(clear_sqlmodel): - from docs_src.tutorial.select import tutorial001_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - check_calls(calls) - - -@needs_py310 -def test_tutorial_002(clear_sqlmodel): - from docs_src.tutorial.select import tutorial002_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - check_calls(calls) diff --git a/tests/test_tutorial/test_select/test_tutorial001_tutorial002.py b/tests/test_tutorial/test_select/test_tutorial001_tutorial002.py index fc8d546a19..c808b81721 100644 --- a/tests/test_tutorial/test_select/test_tutorial001_tutorial002.py +++ b/tests/test_tutorial/test_select/test_tutorial001_tutorial002.py @@ -1,12 +1,14 @@ -from typing import Any, Dict, List, Union -from unittest.mock import patch +import importlib +from types import ModuleType +from typing import Any +import pytest from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock -def check_calls(calls: List[List[Union[str, Dict[str, Any]]]]): +def check_calls(calls: list[list[str | dict[str, Any]]]): assert calls[0][0] == { "name": "Deadpond", "secret_name": "Dive Wilson", @@ -27,29 +29,33 @@ def check_calls(calls: List[List[Union[str, Dict[str, Any]]]]): } -def test_tutorial_001(clear_sqlmodel): - from docs_src.tutorial.select import tutorial001 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - check_calls(calls) - - -def test_tutorial_002(clear_sqlmodel): - from docs_src.tutorial.select import tutorial002 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - check_calls(calls) +@pytest.fixture(name="module") +def get_module(request: pytest.FixtureRequest) -> ModuleType: + module = importlib.import_module(f"docs_src.tutorial.select.{request.param}") + module.sqlite_url = "sqlite://" + module.engine = create_engine(module.sqlite_url) + return module + + +@pytest.mark.parametrize( + "module", + [ + pytest.param("tutorial001_py310"), + ], + indirect=True, +) +def test_tutorial_001(print_mock: PrintMock, module: ModuleType): + module.main() + check_calls(print_mock.calls) + + +@pytest.mark.parametrize( + "module", + [ + pytest.param("tutorial002_py310"), + ], + indirect=True, +) +def test_tutorial_002(print_mock: PrintMock, module: ModuleType): + module.main() + check_calls(print_mock.calls) diff --git a/tests/test_tutorial/test_select/test_tutorial003_py310_tutorial004_py310.py b/tests/test_tutorial/test_select/test_tutorial003_py310_tutorial004_py310.py deleted file mode 100644 index 0fa69df4a1..0000000000 --- a/tests/test_tutorial/test_select/test_tutorial003_py310_tutorial004_py310.py +++ /dev/null @@ -1,59 +0,0 @@ -from typing import Any, Dict, List, Union -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - - -def check_calls(calls: List[List[Union[str, Dict[str, Any]]]]): - assert calls[0][0] == [ - { - "name": "Deadpond", - "secret_name": "Dive Wilson", - "age": None, - "id": 1, - }, - { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "age": None, - "id": 2, - }, - { - "name": "Rusty-Man", - "secret_name": "Tommy Sharp", - "age": 48, - "id": 3, - }, - ] - - -@needs_py310 -def test_tutorial_003(clear_sqlmodel): - from docs_src.tutorial.select import tutorial003_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - check_calls(calls) - - -@needs_py310 -def test_tutorial_002(clear_sqlmodel): - from docs_src.tutorial.select import tutorial004_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - check_calls(calls) diff --git a/tests/test_tutorial/test_select/test_tutorial003_tutorial004.py b/tests/test_tutorial/test_select/test_tutorial003_tutorial004.py index bfda0cb189..1784717237 100644 --- a/tests/test_tutorial/test_select/test_tutorial003_tutorial004.py +++ b/tests/test_tutorial/test_select/test_tutorial003_tutorial004.py @@ -1,12 +1,14 @@ -from typing import Any, Dict, List, Union -from unittest.mock import patch +import importlib +from types import ModuleType +from typing import Any +import pytest from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock -def check_calls(calls: List[List[Union[str, Dict[str, Any]]]]): +def check_calls(calls: list[list[str | dict[str, Any]]]): assert calls[0][0] == [ { "name": "Deadpond", @@ -29,29 +31,33 @@ def check_calls(calls: List[List[Union[str, Dict[str, Any]]]]): ] -def test_tutorial_003(clear_sqlmodel): - from docs_src.tutorial.select import tutorial003 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - check_calls(calls) - - -def test_tutorial_002(clear_sqlmodel): - from docs_src.tutorial.select import tutorial004 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - check_calls(calls) +@pytest.fixture(name="module") +def get_module(request: pytest.FixtureRequest) -> ModuleType: + module = importlib.import_module(f"docs_src.tutorial.select.{request.param}") + module.sqlite_url = "sqlite://" + module.engine = create_engine(module.sqlite_url) + return module + + +@pytest.mark.parametrize( + "module", + [ + pytest.param("tutorial003_py310"), + ], + indirect=True, +) +def test_tutorial_003(print_mock: PrintMock, module: ModuleType): + module.main() + check_calls(print_mock.calls) + + +@pytest.mark.parametrize( + "module", + [ + pytest.param("tutorial004_py310"), + ], + indirect=True, +) +def test_tutorial_004(print_mock: PrintMock, module: ModuleType): + module.main() + check_calls(print_mock.calls) diff --git a/tests/test_tutorial/test_update/test_tutorial001_py310_tutorial002_py310.py b/tests/test_tutorial/test_update/test_tutorial001_py310_tutorial002_py310.py deleted file mode 100644 index cefb75f333..0000000000 --- a/tests/test_tutorial/test_update/test_tutorial001_py310_tutorial002_py310.py +++ /dev/null @@ -1,56 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - -expected_calls = [ - [ - "Hero:", - { - "id": 2, - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "age": None, - }, - ], - [ - "Updated hero:", - { - "id": 2, - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "age": 16, - }, - ], -] - - -@needs_py310 -def test_tutorial001(clear_sqlmodel): - from docs_src.tutorial.update import tutorial001_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls - - -@needs_py310 -def test_tutorial002(clear_sqlmodel): - from docs_src.tutorial.update import tutorial002_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_update/test_tutorial001_tutorial002.py b/tests/test_tutorial/test_update/test_tutorial001_tutorial002.py index be81f410bf..ca52230a62 100644 --- a/tests/test_tutorial/test_update/test_tutorial001_tutorial002.py +++ b/tests/test_tutorial/test_update/test_tutorial001_tutorial002.py @@ -1,8 +1,10 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock expected_calls = [ [ @@ -26,29 +28,33 @@ ] -def test_tutorial001(clear_sqlmodel): - from docs_src.tutorial.update import tutorial001 as mod +@pytest.fixture(name="module") +def get_module(request: pytest.FixtureRequest) -> ModuleType: + module = importlib.import_module(f"docs_src.tutorial.update.{request.param}") + module.sqlite_url = "sqlite://" + module.engine = create_engine(module.sqlite_url) + return module - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls - - -def test_tutorial002(clear_sqlmodel): - from docs_src.tutorial.update import tutorial002 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] +@pytest.mark.parametrize( + "module", + [ + pytest.param("tutorial001_py310"), + ], + indirect=True, +) +def test_tutorial001(print_mock: PrintMock, module: ModuleType): + module.main() + assert print_mock.calls == expected_calls - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls +@pytest.mark.parametrize( + "module", + [ + pytest.param("tutorial002_py310"), + ], + indirect=True, +) +def test_tutorial002(print_mock: PrintMock, module: ModuleType): + module.main() + assert print_mock.calls == expected_calls diff --git a/tests/test_tutorial/test_update/test_tutorial003_py310_tutorial004_py310.py b/tests/test_tutorial/test_update/test_tutorial003_py310_tutorial004_py310.py deleted file mode 100644 index 31dc601901..0000000000 --- a/tests/test_tutorial/test_update/test_tutorial003_py310_tutorial004_py310.py +++ /dev/null @@ -1,69 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - -expected_calls = [ - [ - "Hero 1:", - {"id": 2, "name": "Spider-Boy", "secret_name": "Pedro Parqueador", "age": None}, - ], - [ - "Hero 2:", - { - "id": 7, - "name": "Captain North America", - "secret_name": "Esteban Rogelios", - "age": 93, - }, - ], - [ - "Updated hero 1:", - { - "id": 2, - "name": "Spider-Youngster", - "secret_name": "Pedro Parqueador", - "age": 16, - }, - ], - [ - "Updated hero 2:", - { - "id": 7, - "name": "Captain North America Except Canada", - "secret_name": "Esteban Rogelios", - "age": 110, - }, - ], -] - - -@needs_py310 -def test_tutorial003(clear_sqlmodel): - from docs_src.tutorial.update import tutorial003_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls - - -@needs_py310 -def test_tutorial004(clear_sqlmodel): - from docs_src.tutorial.update import tutorial004_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_update/test_tutorial003_tutorial004.py b/tests/test_tutorial/test_update/test_tutorial003_tutorial004.py index 0f705aa699..a1c008850f 100644 --- a/tests/test_tutorial/test_update/test_tutorial003_tutorial004.py +++ b/tests/test_tutorial/test_update/test_tutorial003_tutorial004.py @@ -1,8 +1,10 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock expected_calls = [ [ @@ -39,29 +41,33 @@ ] -def test_tutorial003(clear_sqlmodel): - from docs_src.tutorial.update import tutorial003 as mod +@pytest.fixture(name="module") +def get_module(request: pytest.FixtureRequest) -> ModuleType: + module = importlib.import_module(f"docs_src.tutorial.update.{request.param}") + module.sqlite_url = "sqlite://" + module.engine = create_engine(module.sqlite_url) + return module - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls - - -def test_tutorial004(clear_sqlmodel): - from docs_src.tutorial.update import tutorial004 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] +@pytest.mark.parametrize( + "module", + [ + pytest.param("tutorial003_py310"), + ], + indirect=True, +) +def test_tutorial003(print_mock: PrintMock, module: ModuleType): + module.main() + assert print_mock.calls == expected_calls - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls +@pytest.mark.parametrize( + "module", + [ + pytest.param("tutorial004_py310"), + ], + indirect=True, +) +def test_tutorial004(print_mock: PrintMock, module: ModuleType): + module.main() + assert print_mock.calls == expected_calls diff --git a/tests/test_tutorial/test_where/test_tutorial001.py b/tests/test_tutorial/test_where/test_tutorial001.py index bba13269a1..92f0d82492 100644 --- a/tests/test_tutorial/test_where/test_tutorial001.py +++ b/tests/test_tutorial/test_where/test_tutorial001.py @@ -1,22 +1,28 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.where import tutorial001 as mod - +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial001_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.where.{request.param}") mod.sqlite_url = "sqlite://" mod.engine = create_engine(mod.sqlite_url) - calls = [] + return mod - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() + assert print_mock.calls == [ [ { "name": "Deadpond", diff --git a/tests/test_tutorial/test_where/test_tutorial001_py310.py b/tests/test_tutorial/test_where/test_tutorial001_py310.py deleted file mode 100644 index 44e734ad7d..0000000000 --- a/tests/test_tutorial/test_where/test_tutorial001_py310.py +++ /dev/null @@ -1,29 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.where import tutorial001_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ - [ - { - "name": "Deadpond", - "secret_name": "Dive Wilson", - "age": None, - "id": 1, - } - ] - ] diff --git a/tests/test_tutorial/test_where/test_tutorial002.py b/tests/test_tutorial/test_where/test_tutorial002.py index 80d60ff555..c1f68d092f 100644 --- a/tests/test_tutorial/test_where/test_tutorial002.py +++ b/tests/test_tutorial/test_where/test_tutorial002.py @@ -1,22 +1,28 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.where import tutorial002 as mod - +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial002_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.where.{request.param}") mod.sqlite_url = "sqlite://" mod.engine = create_engine(mod.sqlite_url) - calls = [] + return mod - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() + assert print_mock.calls == [ [ { "name": "Spider-Boy", diff --git a/tests/test_tutorial/test_where/test_tutorial002_py310.py b/tests/test_tutorial/test_where/test_tutorial002_py310.py deleted file mode 100644 index 00d88ecdde..0000000000 --- a/tests/test_tutorial/test_where/test_tutorial002_py310.py +++ /dev/null @@ -1,30 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.where import tutorial002_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ - [ - { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "age": None, - "id": 2, - } - ], - [{"name": "Rusty-Man", "secret_name": "Tommy Sharp", "age": 48, "id": 3}], - ] diff --git a/tests/test_tutorial/test_where/test_tutorial003.py b/tests/test_tutorial/test_where/test_tutorial003.py index 4794d846ff..ec018a0bfc 100644 --- a/tests/test_tutorial/test_where/test_tutorial003.py +++ b/tests/test_tutorial/test_where/test_tutorial003.py @@ -1,21 +1,27 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.where import tutorial003 as mod - +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial003_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.where.{request.param}") mod.sqlite_url = "sqlite://" mod.engine = create_engine(mod.sqlite_url) - calls = [] + return mod - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() expected_calls = [ [{"id": 6, "name": "Dr. Weird", "secret_name": "Steve Weird", "age": 36}], @@ -29,6 +35,7 @@ def test_tutorial(clear_sqlmodel): } ], ] + calls = print_mock.calls for call in expected_calls: assert call in calls, "This expected item should be in the list" # Now that this item was checked, remove it from the list diff --git a/tests/test_tutorial/test_where/test_tutorial003_py310.py b/tests/test_tutorial/test_where/test_tutorial003_py310.py deleted file mode 100644 index 2d84c2ca82..0000000000 --- a/tests/test_tutorial/test_where/test_tutorial003_py310.py +++ /dev/null @@ -1,37 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.where import tutorial003_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - - expected_calls = [ - [{"id": 6, "name": "Dr. Weird", "secret_name": "Steve Weird", "age": 36}], - [{"id": 3, "name": "Rusty-Man", "secret_name": "Tommy Sharp", "age": 48}], - [ - { - "id": 7, - "name": "Captain North America", - "secret_name": "Esteban Rogelios", - "age": 93, - } - ], - ] - for call in expected_calls: - assert call in calls, "This expected item should be in the list" - # Now that this item was checked, remove it from the list - calls.pop(calls.index(call)) - assert len(calls) == 0, "The list should only have the expected items" diff --git a/tests/test_tutorial/test_where/test_tutorial004.py b/tests/test_tutorial/test_where/test_tutorial004.py index 682babd43a..54ee881732 100644 --- a/tests/test_tutorial/test_where/test_tutorial004.py +++ b/tests/test_tutorial/test_where/test_tutorial004.py @@ -1,21 +1,27 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.where import tutorial004 as mod - +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial004_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.where.{request.param}") mod.sqlite_url = "sqlite://" mod.engine = create_engine(mod.sqlite_url) - calls = [] + return mod - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() expected_calls = [ [{"id": 5, "name": "Black Lion", "secret_name": "Trevor Challa", "age": 35}], [{"id": 6, "name": "Dr. Weird", "secret_name": "Steve Weird", "age": 36}], @@ -29,6 +35,7 @@ def test_tutorial(clear_sqlmodel): } ], ] + calls = print_mock.calls for call in expected_calls: assert call in calls, "This expected item should be in the list" # Now that this item was checked, remove it from the list diff --git a/tests/test_tutorial/test_where/test_tutorial004_py310.py b/tests/test_tutorial/test_where/test_tutorial004_py310.py deleted file mode 100644 index 04566cbbec..0000000000 --- a/tests/test_tutorial/test_where/test_tutorial004_py310.py +++ /dev/null @@ -1,37 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.where import tutorial004_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - expected_calls = [ - [{"id": 5, "name": "Black Lion", "secret_name": "Trevor Challa", "age": 35}], - [{"id": 6, "name": "Dr. Weird", "secret_name": "Steve Weird", "age": 36}], - [{"id": 3, "name": "Rusty-Man", "secret_name": "Tommy Sharp", "age": 48}], - [ - { - "id": 7, - "name": "Captain North America", - "secret_name": "Esteban Rogelios", - "age": 93, - } - ], - ] - for call in expected_calls: - assert call in calls, "This expected item should be in the list" - # Now that this item was checked, remove it from the list - calls.pop(calls.index(call)) - assert len(calls) == 0, "The list should only have the expected items" diff --git a/tests/test_tutorial/test_where/test_tutorial005.py b/tests/test_tutorial/test_where/test_tutorial005.py index b6bfd2ce88..fe425345b6 100644 --- a/tests/test_tutorial/test_where/test_tutorial005.py +++ b/tests/test_tutorial/test_where/test_tutorial005.py @@ -1,21 +1,27 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.where import tutorial005 as mod - +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial005_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.where.{request.param}") mod.sqlite_url = "sqlite://" mod.engine = create_engine(mod.sqlite_url) - calls = [] + return mod - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() + assert print_mock.calls == [ [{"name": "Tarantula", "secret_name": "Natalia Roman-on", "age": 32, "id": 4}] ] diff --git a/tests/test_tutorial/test_where/test_tutorial005_py310.py b/tests/test_tutorial/test_where/test_tutorial005_py310.py deleted file mode 100644 index d238fff4f8..0000000000 --- a/tests/test_tutorial/test_where/test_tutorial005_py310.py +++ /dev/null @@ -1,22 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.where import tutorial005_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ - [{"name": "Tarantula", "secret_name": "Natalia Roman-on", "age": 32, "id": 4}] - ] diff --git a/tests/test_tutorial/test_where/test_tutorial006.py b/tests/test_tutorial/test_where/test_tutorial006.py index e5406dfbb0..7ccda9af04 100644 --- a/tests/test_tutorial/test_where/test_tutorial006.py +++ b/tests/test_tutorial/test_where/test_tutorial006.py @@ -1,22 +1,28 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.where import tutorial006 as mod - +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial006_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.where.{request.param}") mod.sqlite_url = "sqlite://" mod.engine = create_engine(mod.sqlite_url) - calls = [] + return mod - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() + assert print_mock.calls == [ [{"name": "Tarantula", "secret_name": "Natalia Roman-on", "age": 32, "id": 4}], [{"name": "Black Lion", "secret_name": "Trevor Challa", "age": 35, "id": 5}], ] diff --git a/tests/test_tutorial/test_where/test_tutorial006_py310.py b/tests/test_tutorial/test_where/test_tutorial006_py310.py deleted file mode 100644 index 8a4924fc09..0000000000 --- a/tests/test_tutorial/test_where/test_tutorial006_py310.py +++ /dev/null @@ -1,23 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.where import tutorial006_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ - [{"name": "Tarantula", "secret_name": "Natalia Roman-on", "age": 32, "id": 4}], - [{"name": "Black Lion", "secret_name": "Trevor Challa", "age": 35, "id": 5}], - ] diff --git a/tests/test_tutorial/test_where/test_tutorial006b.py b/tests/test_tutorial/test_where/test_tutorial006b.py new file mode 100644 index 0000000000..5198922ed0 --- /dev/null +++ b/tests/test_tutorial/test_where/test_tutorial006b.py @@ -0,0 +1,34 @@ +import importlib +from types import ModuleType + +import pytest +from sqlmodel import create_engine + +from ...conftest import PrintMock + + +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial006b_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.where.{request.param}") + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + return mod + + +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() + assert print_mock.calls == [ + [ + { + "name": "Deadpond", + "secret_name": "Dive Wilson", + "age": None, + "id": 1, + } + ] + ] diff --git a/tests/test_tutorial/test_where/test_tutorial007.py b/tests/test_tutorial/test_where/test_tutorial007.py index 878e81f932..d2ee397458 100644 --- a/tests/test_tutorial/test_where/test_tutorial007.py +++ b/tests/test_tutorial/test_where/test_tutorial007.py @@ -1,22 +1,28 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.where import tutorial007 as mod - +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial007_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.where.{request.param}") mod.sqlite_url = "sqlite://" mod.engine = create_engine(mod.sqlite_url) - calls = [] + return mod - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() + assert print_mock.calls == [ [{"id": 5, "name": "Black Lion", "secret_name": "Trevor Challa", "age": 35}], [{"id": 6, "name": "Dr. Weird", "secret_name": "Steve Weird", "age": 36}], ] diff --git a/tests/test_tutorial/test_where/test_tutorial007_py310.py b/tests/test_tutorial/test_where/test_tutorial007_py310.py deleted file mode 100644 index a2110a19dc..0000000000 --- a/tests/test_tutorial/test_where/test_tutorial007_py310.py +++ /dev/null @@ -1,23 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.where import tutorial007_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ - [{"id": 5, "name": "Black Lion", "secret_name": "Trevor Challa", "age": 35}], - [{"id": 6, "name": "Dr. Weird", "secret_name": "Steve Weird", "age": 36}], - ] diff --git a/tests/test_tutorial/test_where/test_tutorial008.py b/tests/test_tutorial/test_where/test_tutorial008.py index 08f4c49b9d..fdf067cd72 100644 --- a/tests/test_tutorial/test_where/test_tutorial008.py +++ b/tests/test_tutorial/test_where/test_tutorial008.py @@ -1,22 +1,28 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.where import tutorial008 as mod - +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial008_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.where.{request.param}") mod.sqlite_url = "sqlite://" mod.engine = create_engine(mod.sqlite_url) - calls = [] + return mod - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() + assert print_mock.calls == [ [{"id": 5, "name": "Black Lion", "secret_name": "Trevor Challa", "age": 35}], [{"id": 6, "name": "Dr. Weird", "secret_name": "Steve Weird", "age": 36}], ] diff --git a/tests/test_tutorial/test_where/test_tutorial008_py310.py b/tests/test_tutorial/test_where/test_tutorial008_py310.py deleted file mode 100644 index 887ac70abd..0000000000 --- a/tests/test_tutorial/test_where/test_tutorial008_py310.py +++ /dev/null @@ -1,23 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.where import tutorial008_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ - [{"id": 5, "name": "Black Lion", "secret_name": "Trevor Challa", "age": 35}], - [{"id": 6, "name": "Dr. Weird", "secret_name": "Steve Weird", "age": 36}], - ] diff --git a/tests/test_tutorial/test_where/test_tutorial009.py b/tests/test_tutorial/test_where/test_tutorial009.py index 2583f330cb..8bd92b2071 100644 --- a/tests/test_tutorial/test_where/test_tutorial009.py +++ b/tests/test_tutorial/test_where/test_tutorial009.py @@ -1,22 +1,28 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.where import tutorial009 as mod - +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial009_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.where.{request.param}") mod.sqlite_url = "sqlite://" mod.engine = create_engine(mod.sqlite_url) - calls = [] + return mod - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() + assert print_mock.calls == [ [{"name": "Tarantula", "secret_name": "Natalia Roman-on", "age": 32, "id": 4}], [{"name": "Black Lion", "secret_name": "Trevor Challa", "age": 35, "id": 5}], [ diff --git a/tests/test_tutorial/test_where/test_tutorial009_py310.py b/tests/test_tutorial/test_where/test_tutorial009_py310.py deleted file mode 100644 index 9bbef9b9f8..0000000000 --- a/tests/test_tutorial/test_where/test_tutorial009_py310.py +++ /dev/null @@ -1,31 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.where import tutorial009_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ - [{"name": "Tarantula", "secret_name": "Natalia Roman-on", "age": 32, "id": 4}], - [{"name": "Black Lion", "secret_name": "Trevor Challa", "age": 35, "id": 5}], - [ - { - "name": "Captain North America", - "secret_name": "Esteban Rogelios", - "age": 93, - "id": 7, - } - ], - ] diff --git a/tests/test_tutorial/test_where/test_tutorial010.py b/tests/test_tutorial/test_where/test_tutorial010.py deleted file mode 100644 index 71ef75d3a4..0000000000 --- a/tests/test_tutorial/test_where/test_tutorial010.py +++ /dev/null @@ -1,30 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function - - -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.where import tutorial010 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ - [{"name": "Tarantula", "secret_name": "Natalia Roman-on", "age": 32, "id": 4}], - [{"name": "Black Lion", "secret_name": "Trevor Challa", "age": 35, "id": 5}], - [ - { - "name": "Captain North America", - "secret_name": "Esteban Rogelios", - "age": 93, - "id": 7, - } - ], - ] diff --git a/tests/test_tutorial/test_where/test_tutorial010_py310.py b/tests/test_tutorial/test_where/test_tutorial010_py310.py deleted file mode 100644 index e990abed44..0000000000 --- a/tests/test_tutorial/test_where/test_tutorial010_py310.py +++ /dev/null @@ -1,31 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.where import tutorial010_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ - [{"name": "Tarantula", "secret_name": "Natalia Roman-on", "age": 32, "id": 4}], - [{"name": "Black Lion", "secret_name": "Trevor Challa", "age": 35, "id": 5}], - [ - { - "name": "Captain North America", - "secret_name": "Esteban Rogelios", - "age": 93, - "id": 7, - } - ], - ] diff --git a/tests/test_tutorial/test_where/test_tutorial011.py b/tests/test_tutorial/test_where/test_tutorial011.py index 8006cd0708..4b547cee42 100644 --- a/tests/test_tutorial/test_where/test_tutorial011.py +++ b/tests/test_tutorial/test_where/test_tutorial011.py @@ -1,21 +1,27 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.where import tutorial011 as mod - +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial011_py310"), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.where.{request.param}") mod.sqlite_url = "sqlite://" mod.engine = create_engine(mod.sqlite_url) - calls = [] + return mod - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() expected_calls = [ [{"id": 5, "name": "Black Lion", "secret_name": "Trevor Challa", "age": 35}], [{"id": 6, "name": "Dr. Weird", "secret_name": "Steve Weird", "age": 36}], @@ -29,6 +35,7 @@ def test_tutorial(clear_sqlmodel): } ], ] + calls = print_mock.calls for call in expected_calls: assert call in calls, "This expected item should be in the list" # Now that this item was checked, remove it from the list diff --git a/tests/test_tutorial/test_where/test_tutorial011_py310.py b/tests/test_tutorial/test_where/test_tutorial011_py310.py deleted file mode 100644 index aee809b15b..0000000000 --- a/tests/test_tutorial/test_where/test_tutorial011_py310.py +++ /dev/null @@ -1,37 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.where import tutorial011_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - expected_calls = [ - [{"id": 5, "name": "Black Lion", "secret_name": "Trevor Challa", "age": 35}], - [{"id": 6, "name": "Dr. Weird", "secret_name": "Steve Weird", "age": 36}], - [{"id": 3, "name": "Rusty-Man", "secret_name": "Tommy Sharp", "age": 48}], - [ - { - "id": 7, - "name": "Captain North America", - "secret_name": "Esteban Rogelios", - "age": 93, - } - ], - ] - for call in expected_calls: - assert call in calls, "This expected item should be in the list" - # Now that this item was checked, remove it from the list - calls.pop(calls.index(call)) - assert len(calls) == 0, "The list should only have the expected items" diff --git a/tests/test_update.py b/tests/test_update.py new file mode 100644 index 0000000000..de4bd6cdd2 --- /dev/null +++ b/tests/test_update.py @@ -0,0 +1,20 @@ +from sqlmodel import Field, SQLModel + + +def test_sqlmodel_update(): + class Organization(SQLModel, table=True): + id: int = Field(default=None, primary_key=True) + name: str + headquarters: str + + class OrganizationUpdate(SQLModel): + name: str + + org = Organization(name="Example Org", city="New York", headquarters="NYC HQ") + org_in = OrganizationUpdate(name="Updated org") + org.sqlmodel_update( + org_in, + update={ + "headquarters": "-", # This field is in Organization, but not in OrganizationUpdate + }, + ) diff --git a/tests/test_validation.py b/tests/test_validation.py index 3265922070..47fbca87c2 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -1,42 +1,8 @@ -from typing import Optional - import pytest from pydantic.error_wrappers import ValidationError from sqlmodel import SQLModel -from .conftest import needs_pydanticv1, needs_pydanticv2 - - -@needs_pydanticv1 -def test_validation_pydantic_v1(clear_sqlmodel): - """Test validation of implicit and explicit None values. - - # For consistency with pydantic, validators are not to be called on - # arguments that are not explicitly provided. - - https://github.com/tiangolo/sqlmodel/issues/230 - https://github.com/samuelcolvin/pydantic/issues/1223 - - """ - from pydantic import validator - - class Hero(SQLModel): - name: Optional[str] = None - secret_name: Optional[str] = None - age: Optional[int] = None - - @validator("name", "secret_name", "age") - def reject_none(cls, v): - assert v is not None - return v - - Hero.validate({"age": 25}) - - with pytest.raises(ValidationError): - Hero.validate({"name": None, "age": 25}) - -@needs_pydanticv2 def test_validation_pydantic_v2(clear_sqlmodel): """Test validation of implicit and explicit None values. @@ -50,9 +16,9 @@ def test_validation_pydantic_v2(clear_sqlmodel): from pydantic import field_validator class Hero(SQLModel): - name: Optional[str] = None - secret_name: Optional[str] = None - age: Optional[int] = None + name: str | None = None + secret_name: str | None = None + age: int | None = None @field_validator("name", "secret_name", "age") def reject_none(cls, v): diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000000..7eb54274f3 --- /dev/null +++ b/uv.lock @@ -0,0 +1,2113 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" + +[[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.0" +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/16/ce/8a777047513153587e5434fd752e89334ac33e379aa3497db860eeb60377/anyio-4.12.0.tar.gz", hash = "sha256:73c693b567b0c55130c104d0b43a9baf3aa6a31fc6110116509f27bf75e21ec0", size = 228266, upload-time = "2025-11-28T23:37:38.911Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl", hash = "sha256:dad2376a628f98eeca4881fc56cd06affd18f659b17a747d3ff0307ced94b1bb", size = 113362, upload-time = "2025-11-28T23:36:57.897Z" }, +] + +[[package]] +name = "babel" +version = "2.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" }, +] + +[[package]] +name = "backrefs" +version = "6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/86/e3/bb3a439d5cb255c4774724810ad8073830fac9c9dee123555820c1bcc806/backrefs-6.1.tar.gz", hash = "sha256:3bba1749aafe1db9b915f00e0dd166cba613b6f788ffd63060ac3485dc9be231", size = 7011962, upload-time = "2025-11-15T14:52:08.323Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ee/c216d52f58ea75b5e1841022bbae24438b19834a29b163cb32aa3a2a7c6e/backrefs-6.1-py310-none-any.whl", hash = "sha256:2a2ccb96302337ce61ee4717ceacfbf26ba4efb1d55af86564b8bbaeda39cac1", size = 381059, upload-time = "2025-11-15T14:51:59.758Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9a/8da246d988ded941da96c7ed945d63e94a445637eaad985a0ed88787cb89/backrefs-6.1-py311-none-any.whl", hash = "sha256:e82bba3875ee4430f4de4b6db19429a27275d95a5f3773c57e9e18abc23fd2b7", size = 392854, upload-time = "2025-11-15T14:52:01.194Z" }, + { url = "https://files.pythonhosted.org/packages/37/c9/fd117a6f9300c62bbc33bc337fd2b3c6bfe28b6e9701de336b52d7a797ad/backrefs-6.1-py312-none-any.whl", hash = "sha256:c64698c8d2269343d88947c0735cb4b78745bd3ba590e10313fbf3f78c34da5a", size = 398770, upload-time = "2025-11-15T14:52:02.584Z" }, + { url = "https://files.pythonhosted.org/packages/eb/95/7118e935b0b0bd3f94dfec2d852fd4e4f4f9757bdb49850519acd245cd3a/backrefs-6.1-py313-none-any.whl", hash = "sha256:4c9d3dc1e2e558965202c012304f33d4e0e477e1c103663fd2c3cc9bb18b0d05", size = 400726, upload-time = "2025-11-15T14:52:04.093Z" }, + { url = "https://files.pythonhosted.org/packages/1d/72/6296bad135bfafd3254ae3648cd152980a424bd6fed64a101af00cc7ba31/backrefs-6.1-py314-none-any.whl", hash = "sha256:13eafbc9ccd5222e9c1f0bec563e6d2a6d21514962f11e7fc79872fd56cbc853", size = 412584, upload-time = "2025-11-15T14:52:05.233Z" }, + { url = "https://files.pythonhosted.org/packages/02/e3/a4fa1946722c4c7b063cc25043a12d9ce9b4323777f89643be74cef2993c/backrefs-6.1-py39-none-any.whl", hash = "sha256:a9e99b8a4867852cad177a6430e31b0f6e495d65f8c6c134b68c14c3c95bf4b0", size = 381058, upload-time = "2025-11-15T14:52:06.698Z" }, +] + +[[package]] +name = "black" +version = "26.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "mypy-extensions" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "platformdirs" }, + { name = "pytokens" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/c5/61175d618685d42b005847464b8fb4743a67b1b8fdb75e50e5a96c31a27a/black-26.3.1.tar.gz", hash = "sha256:2c50f5063a9641c7eed7795014ba37b0f5fa227f3d408b968936e24bc0566b07", size = 666155, upload-time = "2026-03-12T03:36:03.593Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/a8/11170031095655d36ebc6664fe0897866f6023892396900eec0e8fdc4299/black-26.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:86a8b5035fce64f5dcd1b794cf8ec4d31fe458cf6ce3986a30deb434df82a1d2", size = 1866562, upload-time = "2026-03-12T03:39:58.639Z" }, + { url = "https://files.pythonhosted.org/packages/69/ce/9e7548d719c3248c6c2abfd555d11169457cbd584d98d179111338423790/black-26.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5602bdb96d52d2d0672f24f6ffe5218795736dd34807fd0fd55ccd6bf206168b", size = 1703623, upload-time = "2026-03-12T03:40:00.347Z" }, + { url = "https://files.pythonhosted.org/packages/7f/0a/8d17d1a9c06f88d3d030d0b1d4373c1551146e252afe4547ed601c0e697f/black-26.3.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c54a4a82e291a1fee5137371ab488866b7c86a3305af4026bdd4dc78642e1ac", size = 1768388, upload-time = "2026-03-12T03:40:01.765Z" }, + { url = "https://files.pythonhosted.org/packages/52/79/c1ee726e221c863cde5164f925bacf183dfdf0397d4e3f94889439b947b4/black-26.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:6e131579c243c98f35bce64a7e08e87fb2d610544754675d4a0e73a070a5aa3a", size = 1412969, upload-time = "2026-03-12T03:40:03.252Z" }, + { url = "https://files.pythonhosted.org/packages/73/a5/15c01d613f5756f68ed8f6d4ec0a1e24b82b18889fa71affd3d1f7fad058/black-26.3.1-cp310-cp310-win_arm64.whl", hash = "sha256:5ed0ca58586c8d9a487352a96b15272b7fa55d139fc8496b519e78023a8dab0a", size = 1220345, upload-time = "2026-03-12T03:40:04.892Z" }, + { url = "https://files.pythonhosted.org/packages/17/57/5f11c92861f9c92eb9dddf515530bc2d06db843e44bdcf1c83c1427824bc/black-26.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:28ef38aee69e4b12fda8dba75e21f9b4f979b490c8ac0baa7cb505369ac9e1ff", size = 1851987, upload-time = "2026-03-12T03:40:06.248Z" }, + { url = "https://files.pythonhosted.org/packages/54/aa/340a1463660bf6831f9e39646bf774086dbd8ca7fc3cded9d59bbdf4ad0a/black-26.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf9bf162ed91a26f1adba8efda0b573bc6924ec1408a52cc6f82cb73ec2b142c", size = 1689499, upload-time = "2026-03-12T03:40:07.642Z" }, + { url = "https://files.pythonhosted.org/packages/f3/01/b726c93d717d72733da031d2de10b92c9fa4c8d0c67e8a8a372076579279/black-26.3.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:474c27574d6d7037c1bc875a81d9be0a9a4f9ee95e62800dab3cfaadbf75acd5", size = 1754369, upload-time = "2026-03-12T03:40:09.279Z" }, + { url = "https://files.pythonhosted.org/packages/e3/09/61e91881ca291f150cfc9eb7ba19473c2e59df28859a11a88248b5cbbc4d/black-26.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:5e9d0d86df21f2e1677cc4bd090cd0e446278bcbbe49bf3659c308c3e402843e", size = 1413613, upload-time = "2026-03-12T03:40:10.943Z" }, + { url = "https://files.pythonhosted.org/packages/16/73/544f23891b22e7efe4d8f812371ab85b57f6a01b2fc45e3ba2e52ba985b8/black-26.3.1-cp311-cp311-win_arm64.whl", hash = "sha256:9a5e9f45e5d5e1c5b5c29b3bd4265dcc90e8b92cf4534520896ed77f791f4da5", size = 1219719, upload-time = "2026-03-12T03:40:12.597Z" }, + { url = "https://files.pythonhosted.org/packages/dc/f8/da5eae4fc75e78e6dceb60624e1b9662ab00d6b452996046dfa9b8a6025b/black-26.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e6f89631eb88a7302d416594a32faeee9fb8fb848290da9d0a5f2903519fc1", size = 1895920, upload-time = "2026-03-12T03:40:13.921Z" }, + { url = "https://files.pythonhosted.org/packages/2c/9f/04e6f26534da2e1629b2b48255c264cabf5eedc5141d04516d9d68a24111/black-26.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41cd2012d35b47d589cb8a16faf8a32ef7a336f56356babd9fcf70939ad1897f", size = 1718499, upload-time = "2026-03-12T03:40:15.239Z" }, + { url = "https://files.pythonhosted.org/packages/04/91/a5935b2a63e31b331060c4a9fdb5a6c725840858c599032a6f3aac94055f/black-26.3.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f76ff19ec5297dd8e66eb64deda23631e642c9393ab592826fd4bdc97a4bce7", size = 1794994, upload-time = "2026-03-12T03:40:17.124Z" }, + { url = "https://files.pythonhosted.org/packages/e7/0a/86e462cdd311a3c2a8ece708d22aba17d0b2a0d5348ca34b40cdcbea512e/black-26.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:ddb113db38838eb9f043623ba274cfaf7d51d5b0c22ecb30afe58b1bb8322983", size = 1420867, upload-time = "2026-03-12T03:40:18.83Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e5/22515a19cb7eaee3440325a6b0d95d2c0e88dd180cb011b12ae488e031d1/black-26.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:dfdd51fc3e64ea4f35873d1b3fb25326773d55d2329ff8449139ebaad7357efb", size = 1230124, upload-time = "2026-03-12T03:40:20.425Z" }, + { url = "https://files.pythonhosted.org/packages/f5/77/5728052a3c0450c53d9bb3945c4c46b91baa62b2cafab6801411b6271e45/black-26.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:855822d90f884905362f602880ed8b5df1b7e3ee7d0db2502d4388a954cc8c54", size = 1895034, upload-time = "2026-03-12T03:40:21.813Z" }, + { url = "https://files.pythonhosted.org/packages/52/73/7cae55fdfdfbe9d19e9a8d25d145018965fe2079fa908101c3733b0c55a0/black-26.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8a33d657f3276328ce00e4d37fe70361e1ec7614da5d7b6e78de5426cb56332f", size = 1718503, upload-time = "2026-03-12T03:40:23.666Z" }, + { url = "https://files.pythonhosted.org/packages/e1/87/af89ad449e8254fdbc74654e6467e3c9381b61472cc532ee350d28cfdafb/black-26.3.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f1cd08e99d2f9317292a311dfe578fd2a24b15dbce97792f9c4d752275c1fa56", size = 1793557, upload-time = "2026-03-12T03:40:25.497Z" }, + { url = "https://files.pythonhosted.org/packages/43/10/d6c06a791d8124b843bf325ab4ac7d2f5b98731dff84d6064eafd687ded1/black-26.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:c7e72339f841b5a237ff14f7d3880ddd0fc7f98a1199e8c4327f9a4f478c1839", size = 1422766, upload-time = "2026-03-12T03:40:27.14Z" }, + { url = "https://files.pythonhosted.org/packages/59/4f/40a582c015f2d841ac24fed6390bd68f0fc896069ff3a886317959c9daf8/black-26.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:afc622538b430aa4c8c853f7f63bc582b3b8030fd8c80b70fb5fa5b834e575c2", size = 1232140, upload-time = "2026-03-12T03:40:28.882Z" }, + { url = "https://files.pythonhosted.org/packages/d5/da/e36e27c9cebc1311b7579210df6f1c86e50f2d7143ae4fcf8a5017dc8809/black-26.3.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2d6bfaf7fd0993b420bed691f20f9492d53ce9a2bcccea4b797d34e947318a78", size = 1889234, upload-time = "2026-03-12T03:40:30.964Z" }, + { url = "https://files.pythonhosted.org/packages/0e/7b/9871acf393f64a5fa33668c19350ca87177b181f44bb3d0c33b2d534f22c/black-26.3.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f89f2ab047c76a9c03f78d0d66ca519e389519902fa27e7a91117ef7611c0568", size = 1720522, upload-time = "2026-03-12T03:40:32.346Z" }, + { url = "https://files.pythonhosted.org/packages/03/87/e766c7f2e90c07fb7586cc787c9ae6462b1eedab390191f2b7fc7f6170a9/black-26.3.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b07fc0dab849d24a80a29cfab8d8a19187d1c4685d8a5e6385a5ce323c1f015f", size = 1787824, upload-time = "2026-03-12T03:40:33.636Z" }, + { url = "https://files.pythonhosted.org/packages/ac/94/2424338fb2d1875e9e83eed4c8e9c67f6905ec25afd826a911aea2b02535/black-26.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:0126ae5b7c09957da2bdbd91a9ba1207453feada9e9fe51992848658c6c8e01c", size = 1445855, upload-time = "2026-03-12T03:40:35.442Z" }, + { url = "https://files.pythonhosted.org/packages/86/43/0c3338bd928afb8ee7471f1a4eec3bdbe2245ccb4a646092a222e8669840/black-26.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:92c0ec1f2cc149551a2b7b47efc32c866406b6891b0ee4625e95967c8f4acfb1", size = 1258109, upload-time = "2026-03-12T03:40:36.832Z" }, + { url = "https://files.pythonhosted.org/packages/8e/0d/52d98722666d6fc6c3dd4c76df339501d6efd40e0ff95e6186a7b7f0befd/black-26.3.1-py3-none-any.whl", hash = "sha256:2bd5aa94fc267d38bb21a70d7410a89f1a1d318841855f698746f8e7f51acd1b", size = 207542, upload-time = "2026-03-12T03:36:01.668Z" }, +] + +[[package]] +name = "cairocffi" +version = "1.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/70/c5/1a4dc131459e68a173cbdab5fad6b524f53f9c1ef7861b7698e998b837cc/cairocffi-1.7.1.tar.gz", hash = "sha256:2e48ee864884ec4a3a34bfa8c9ab9999f688286eb714a15a43ec9d068c36557b", size = 88096, upload-time = "2024-06-18T10:56:06.741Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/d8/ba13451aa6b745c49536e87b6bf8f629b950e84bd0e8308f7dc6883b67e2/cairocffi-1.7.1-py3-none-any.whl", hash = "sha256:9803a0e11f6c962f3b0ae2ec8ba6ae45e957a146a004697a1ac1bbf16b073b3f", size = 75611, upload-time = "2024-06-18T10:55:59.489Z" }, +] + +[[package]] +name = "cairosvg" +version = "2.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cairocffi" }, + { name = "cssselect2" }, + { name = "defusedxml" }, + { name = "pillow" }, + { name = "tinycss2" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/07/e8412a13019b3f737972dea23a2c61ca42becafc16c9338f4ca7a0caa993/cairosvg-2.9.0.tar.gz", hash = "sha256:1debb00cd2da11350d8b6f5ceb739f1b539196d71d5cf5eb7363dbd1bfbc8dc5", size = 40877, upload-time = "2026-03-13T15:42:00.564Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/e0/5011747466414c12cac8a8df77aa235068669a6a5a5df301a96209db6054/cairosvg-2.9.0-py3-none-any.whl", hash = "sha256:4b82d07d145377dffdfc19d9791bd5fb65539bb4da0adecf0bdbd9cd4ffd7c68", size = 45962, upload-time = "2026-03-14T13:56:33.512Z" }, +] + +[[package]] +name = "certifi" +version = "2025.11.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" }, + { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" }, + { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, + { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, + { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, + { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, + { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, + { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, + { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, + { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, + { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" }, + { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" }, + { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" }, + { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" }, + { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" }, + { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" }, + { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" }, + { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" }, + { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" }, + { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" }, + { url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" }, + { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, + { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, + { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, + { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, + { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, + { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, + { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, + { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, + { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, + { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[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.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/45/2c665ca77ec32ad67e25c77daf1cee28ee4558f3bc571cdbaf88a00b9f23/coverage-7.13.0.tar.gz", hash = "sha256:a394aa27f2d7ff9bc04cf703817773a59ad6dfbd577032e690f961d2460ee936", size = 820905, upload-time = "2025-12-08T13:14:38.055Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/08/bdd7ccca14096f7eb01412b87ac11e5d16e4cb54b6e328afc9dee8bdaec1/coverage-7.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:02d9fb9eccd48f6843c98a37bd6817462f130b86da8660461e8f5e54d4c06070", size = 217979, upload-time = "2025-12-08T13:12:14.505Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f0/d1302e3416298a28b5663ae1117546a745d9d19fde7e28402b2c5c3e2109/coverage-7.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:367449cf07d33dc216c083f2036bb7d976c6e4903ab31be400ad74ad9f85ce98", size = 218496, upload-time = "2025-12-08T13:12:16.237Z" }, + { url = "https://files.pythonhosted.org/packages/07/26/d36c354c8b2a320819afcea6bffe72839efd004b98d1d166b90801d49d57/coverage-7.13.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cdb3c9f8fef0a954c632f64328a3935988d33a6604ce4bf67ec3e39670f12ae5", size = 245237, upload-time = "2025-12-08T13:12:17.858Z" }, + { url = "https://files.pythonhosted.org/packages/91/52/be5e85631e0eec547873d8b08dd67a5f6b111ecfe89a86e40b89b0c1c61c/coverage-7.13.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d10fd186aac2316f9bbb46ef91977f9d394ded67050ad6d84d94ed6ea2e8e54e", size = 247061, upload-time = "2025-12-08T13:12:19.132Z" }, + { url = "https://files.pythonhosted.org/packages/0f/45/a5e8fa0caf05fbd8fa0402470377bff09cc1f026d21c05c71e01295e55ab/coverage-7.13.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f88ae3e69df2ab62fb0bc5219a597cb890ba5c438190ffa87490b315190bb33", size = 248928, upload-time = "2025-12-08T13:12:20.702Z" }, + { url = "https://files.pythonhosted.org/packages/f5/42/ffb5069b6fd1b95fae482e02f3fecf380d437dd5a39bae09f16d2e2e7e01/coverage-7.13.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c4be718e51e86f553bcf515305a158a1cd180d23b72f07ae76d6017c3cc5d791", size = 245931, upload-time = "2025-12-08T13:12:22.243Z" }, + { url = "https://files.pythonhosted.org/packages/95/6e/73e809b882c2858f13e55c0c36e94e09ce07e6165d5644588f9517efe333/coverage-7.13.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a00d3a393207ae12f7c49bb1c113190883b500f48979abb118d8b72b8c95c032", size = 246968, upload-time = "2025-12-08T13:12:23.52Z" }, + { url = "https://files.pythonhosted.org/packages/87/08/64ebd9e64b6adb8b4a4662133d706fbaccecab972e0b3ccc23f64e2678ad/coverage-7.13.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a7b1cd820e1b6116f92c6128f1188e7afe421c7e1b35fa9836b11444e53ebd9", size = 244972, upload-time = "2025-12-08T13:12:24.781Z" }, + { url = "https://files.pythonhosted.org/packages/12/97/f4d27c6fe0cb375a5eced4aabcaef22de74766fb80a3d5d2015139e54b22/coverage-7.13.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:37eee4e552a65866f15dedd917d5e5f3d59805994260720821e2c1b51ac3248f", size = 245241, upload-time = "2025-12-08T13:12:28.041Z" }, + { url = "https://files.pythonhosted.org/packages/0c/94/42f8ae7f633bf4c118bf1038d80472f9dade88961a466f290b81250f7ab7/coverage-7.13.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:62d7c4f13102148c78d7353c6052af6d899a7f6df66a32bddcc0c0eb7c5326f8", size = 245847, upload-time = "2025-12-08T13:12:29.337Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2f/6369ca22b6b6d933f4f4d27765d313d8914cc4cce84f82a16436b1a233db/coverage-7.13.0-cp310-cp310-win32.whl", hash = "sha256:24e4e56304fdb56f96f80eabf840eab043b3afea9348b88be680ec5986780a0f", size = 220573, upload-time = "2025-12-08T13:12:30.905Z" }, + { url = "https://files.pythonhosted.org/packages/f1/dc/a6a741e519acceaeccc70a7f4cfe5d030efc4b222595f0677e101af6f1f3/coverage-7.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:74c136e4093627cf04b26a35dab8cbfc9b37c647f0502fc313376e11726ba303", size = 221509, upload-time = "2025-12-08T13:12:32.09Z" }, + { url = "https://files.pythonhosted.org/packages/f1/dc/888bf90d8b1c3d0b4020a40e52b9f80957d75785931ec66c7dfaccc11c7d/coverage-7.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0dfa3855031070058add1a59fdfda0192fd3e8f97e7c81de0596c145dea51820", size = 218104, upload-time = "2025-12-08T13:12:33.333Z" }, + { url = "https://files.pythonhosted.org/packages/8d/ea/069d51372ad9c380214e86717e40d1a743713a2af191cfba30a0911b0a4a/coverage-7.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4fdb6f54f38e334db97f72fa0c701e66d8479af0bc3f9bfb5b90f1c30f54500f", size = 218606, upload-time = "2025-12-08T13:12:34.498Z" }, + { url = "https://files.pythonhosted.org/packages/68/09/77b1c3a66c2aa91141b6c4471af98e5b1ed9b9e6d17255da5eb7992299e3/coverage-7.13.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7e442c013447d1d8d195be62852270b78b6e255b79b8675bad8479641e21fd96", size = 248999, upload-time = "2025-12-08T13:12:36.02Z" }, + { url = "https://files.pythonhosted.org/packages/0a/32/2e2f96e9d5691eaf1181d9040f850b8b7ce165ea10810fd8e2afa534cef7/coverage-7.13.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ed5630d946859de835a85e9a43b721123a8a44ec26e2830b296d478c7fd4259", size = 250925, upload-time = "2025-12-08T13:12:37.221Z" }, + { url = "https://files.pythonhosted.org/packages/7b/45/b88ddac1d7978859b9a39a8a50ab323186148f1d64bc068f86fc77706321/coverage-7.13.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f15a931a668e58087bc39d05d2b4bf4b14ff2875b49c994bbdb1c2217a8daeb", size = 253032, upload-time = "2025-12-08T13:12:38.763Z" }, + { url = "https://files.pythonhosted.org/packages/71/cb/e15513f94c69d4820a34b6bf3d2b1f9f8755fa6021be97c7065442d7d653/coverage-7.13.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:30a3a201a127ea57f7e14ba43c93c9c4be8b7d17a26e03bb49e6966d019eede9", size = 249134, upload-time = "2025-12-08T13:12:40.382Z" }, + { url = "https://files.pythonhosted.org/packages/09/61/d960ff7dc9e902af3310ce632a875aaa7860f36d2bc8fc8b37ee7c1b82a5/coverage-7.13.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7a485ff48fbd231efa32d58f479befce52dcb6bfb2a88bb7bf9a0b89b1bc8030", size = 250731, upload-time = "2025-12-08T13:12:41.992Z" }, + { url = "https://files.pythonhosted.org/packages/98/34/c7c72821794afc7c7c2da1db8f00c2c98353078aa7fb6b5ff36aac834b52/coverage-7.13.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:22486cdafba4f9e471c816a2a5745337742a617fef68e890d8baf9f3036d7833", size = 248795, upload-time = "2025-12-08T13:12:43.331Z" }, + { url = "https://files.pythonhosted.org/packages/0a/5b/e0f07107987a43b2def9aa041c614ddb38064cbf294a71ef8c67d43a0cdd/coverage-7.13.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:263c3dbccc78e2e331e59e90115941b5f53e85cfcc6b3b2fbff1fd4e3d2c6ea8", size = 248514, upload-time = "2025-12-08T13:12:44.546Z" }, + { url = "https://files.pythonhosted.org/packages/71/c2/c949c5d3b5e9fc6dd79e1b73cdb86a59ef14f3709b1d72bf7668ae12e000/coverage-7.13.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e5330fa0cc1f5c3c4c3bb8e101b742025933e7848989370a1d4c8c5e401ea753", size = 249424, upload-time = "2025-12-08T13:12:45.759Z" }, + { url = "https://files.pythonhosted.org/packages/11/f1/bbc009abd6537cec0dffb2cc08c17a7f03de74c970e6302db4342a6e05af/coverage-7.13.0-cp311-cp311-win32.whl", hash = "sha256:0f4872f5d6c54419c94c25dd6ae1d015deeb337d06e448cd890a1e89a8ee7f3b", size = 220597, upload-time = "2025-12-08T13:12:47.378Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f6/d9977f2fb51c10fbaed0718ce3d0a8541185290b981f73b1d27276c12d91/coverage-7.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51a202e0f80f241ccb68e3e26e19ab5b3bf0f813314f2c967642f13ebcf1ddfe", size = 221536, upload-time = "2025-12-08T13:12:48.7Z" }, + { url = "https://files.pythonhosted.org/packages/be/ad/3fcf43fd96fb43e337a3073dea63ff148dcc5c41ba7a14d4c7d34efb2216/coverage-7.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:d2a9d7f1c11487b1c69367ab3ac2d81b9b3721f097aa409a3191c3e90f8f3dd7", size = 220206, upload-time = "2025-12-08T13:12:50.365Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f1/2619559f17f31ba00fc40908efd1fbf1d0a5536eb75dc8341e7d660a08de/coverage-7.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0b3d67d31383c4c68e19a88e28fc4c2e29517580f1b0ebec4a069d502ce1e0bf", size = 218274, upload-time = "2025-12-08T13:12:52.095Z" }, + { url = "https://files.pythonhosted.org/packages/2b/11/30d71ae5d6e949ff93b2a79a2c1b4822e00423116c5c6edfaeef37301396/coverage-7.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:581f086833d24a22c89ae0fe2142cfaa1c92c930adf637ddf122d55083fb5a0f", size = 218638, upload-time = "2025-12-08T13:12:53.418Z" }, + { url = "https://files.pythonhosted.org/packages/79/c2/fce80fc6ded8d77e53207489d6065d0fed75db8951457f9213776615e0f5/coverage-7.13.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0a3a30f0e257df382f5f9534d4ce3d4cf06eafaf5192beb1a7bd066cb10e78fb", size = 250129, upload-time = "2025-12-08T13:12:54.744Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b6/51b5d1eb6fcbb9a1d5d6984e26cbe09018475c2922d554fd724dd0f056ee/coverage-7.13.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:583221913fbc8f53b88c42e8dbb8fca1d0f2e597cb190ce45916662b8b9d9621", size = 252885, upload-time = "2025-12-08T13:12:56.401Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f8/972a5affea41de798691ab15d023d3530f9f56a72e12e243f35031846ff7/coverage-7.13.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f5d9bd30756fff3e7216491a0d6d520c448d5124d3d8e8f56446d6412499e74", size = 253974, upload-time = "2025-12-08T13:12:57.718Z" }, + { url = "https://files.pythonhosted.org/packages/8a/56/116513aee860b2c7968aa3506b0f59b22a959261d1dbf3aea7b4450a7520/coverage-7.13.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a23e5a1f8b982d56fa64f8e442e037f6ce29322f1f9e6c2344cd9e9f4407ee57", size = 250538, upload-time = "2025-12-08T13:12:59.254Z" }, + { url = "https://files.pythonhosted.org/packages/d6/75/074476d64248fbadf16dfafbf93fdcede389ec821f74ca858d7c87d2a98c/coverage-7.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9b01c22bc74a7fb44066aaf765224c0d933ddf1f5047d6cdfe4795504a4493f8", size = 251912, upload-time = "2025-12-08T13:13:00.604Z" }, + { url = "https://files.pythonhosted.org/packages/f2/d2/aa4f8acd1f7c06024705c12609d8698c51b27e4d635d717cd1934c9668e2/coverage-7.13.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:898cce66d0836973f48dda4e3514d863d70142bdf6dfab932b9b6a90ea5b222d", size = 250054, upload-time = "2025-12-08T13:13:01.892Z" }, + { url = "https://files.pythonhosted.org/packages/19/98/8df9e1af6a493b03694a1e8070e024e7d2cdc77adedc225a35e616d505de/coverage-7.13.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:3ab483ea0e251b5790c2aac03acde31bff0c736bf8a86829b89382b407cd1c3b", size = 249619, upload-time = "2025-12-08T13:13:03.236Z" }, + { url = "https://files.pythonhosted.org/packages/d8/71/f8679231f3353018ca66ef647fa6fe7b77e6bff7845be54ab84f86233363/coverage-7.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1d84e91521c5e4cb6602fe11ece3e1de03b2760e14ae4fcf1a4b56fa3c801fcd", size = 251496, upload-time = "2025-12-08T13:13:04.511Z" }, + { url = "https://files.pythonhosted.org/packages/04/86/9cb406388034eaf3c606c22094edbbb82eea1fa9d20c0e9efadff20d0733/coverage-7.13.0-cp312-cp312-win32.whl", hash = "sha256:193c3887285eec1dbdb3f2bd7fbc351d570ca9c02ca756c3afbc71b3c98af6ef", size = 220808, upload-time = "2025-12-08T13:13:06.422Z" }, + { url = "https://files.pythonhosted.org/packages/1c/59/af483673df6455795daf5f447c2f81a3d2fcfc893a22b8ace983791f6f34/coverage-7.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:4f3e223b2b2db5e0db0c2b97286aba0036ca000f06aca9b12112eaa9af3d92ae", size = 221616, upload-time = "2025-12-08T13:13:07.95Z" }, + { url = "https://files.pythonhosted.org/packages/64/b0/959d582572b30a6830398c60dd419c1965ca4b5fb38ac6b7093a0d50ca8d/coverage-7.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:086cede306d96202e15a4b77ace8472e39d9f4e5f9fd92dd4fecdfb2313b2080", size = 220261, upload-time = "2025-12-08T13:13:09.581Z" }, + { url = "https://files.pythonhosted.org/packages/7c/cc/bce226595eb3bf7d13ccffe154c3c487a22222d87ff018525ab4dd2e9542/coverage-7.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:28ee1c96109974af104028a8ef57cec21447d42d0e937c0275329272e370ebcf", size = 218297, upload-time = "2025-12-08T13:13:10.977Z" }, + { url = "https://files.pythonhosted.org/packages/3b/9f/73c4d34600aae03447dff3d7ad1d0ac649856bfb87d1ca7d681cfc913f9e/coverage-7.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d1e97353dcc5587b85986cda4ff3ec98081d7e84dd95e8b2a6d59820f0545f8a", size = 218673, upload-time = "2025-12-08T13:13:12.562Z" }, + { url = "https://files.pythonhosted.org/packages/63/ab/8fa097db361a1e8586535ae5073559e6229596b3489ec3ef2f5b38df8cb2/coverage-7.13.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:99acd4dfdfeb58e1937629eb1ab6ab0899b131f183ee5f23e0b5da5cba2fec74", size = 249652, upload-time = "2025-12-08T13:13:13.909Z" }, + { url = "https://files.pythonhosted.org/packages/90/3a/9bfd4de2ff191feb37ef9465855ca56a6f2f30a3bca172e474130731ac3d/coverage-7.13.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ff45e0cd8451e293b63ced93161e189780baf444119391b3e7d25315060368a6", size = 252251, upload-time = "2025-12-08T13:13:15.553Z" }, + { url = "https://files.pythonhosted.org/packages/df/61/b5d8105f016e1b5874af0d7c67542da780ccd4a5f2244a433d3e20ceb1ad/coverage-7.13.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f4f72a85316d8e13234cafe0a9f81b40418ad7a082792fa4165bd7d45d96066b", size = 253492, upload-time = "2025-12-08T13:13:16.849Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b8/0fad449981803cc47a4694768b99823fb23632150743f9c83af329bb6090/coverage-7.13.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:11c21557d0e0a5a38632cbbaca5f008723b26a89d70db6315523df6df77d6232", size = 249850, upload-time = "2025-12-08T13:13:18.142Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e9/8d68337c3125014d918cf4327d5257553a710a2995a6a6de2ac77e5aa429/coverage-7.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76541dc8d53715fb4f7a3a06b34b0dc6846e3c69bc6204c55653a85dd6220971", size = 251633, upload-time = "2025-12-08T13:13:19.56Z" }, + { url = "https://files.pythonhosted.org/packages/55/14/d4112ab26b3a1bc4b3c1295d8452dcf399ed25be4cf649002fb3e64b2d93/coverage-7.13.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6e9e451dee940a86789134b6b0ffbe31c454ade3b849bb8a9d2cca2541a8e91d", size = 249586, upload-time = "2025-12-08T13:13:20.883Z" }, + { url = "https://files.pythonhosted.org/packages/2c/a9/22b0000186db663b0d82f86c2f1028099ae9ac202491685051e2a11a5218/coverage-7.13.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:5c67dace46f361125e6b9cace8fe0b729ed8479f47e70c89b838d319375c8137", size = 249412, upload-time = "2025-12-08T13:13:22.22Z" }, + { url = "https://files.pythonhosted.org/packages/a1/2e/42d8e0d9e7527fba439acdc6ed24a2b97613b1dc85849b1dd935c2cffef0/coverage-7.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f59883c643cb19630500f57016f76cfdcd6845ca8c5b5ea1f6e17f74c8e5f511", size = 251191, upload-time = "2025-12-08T13:13:23.899Z" }, + { url = "https://files.pythonhosted.org/packages/a4/af/8c7af92b1377fd8860536aadd58745119252aaaa71a5213e5a8e8007a9f5/coverage-7.13.0-cp313-cp313-win32.whl", hash = "sha256:58632b187be6f0be500f553be41e277712baa278147ecb7559983c6d9faf7ae1", size = 220829, upload-time = "2025-12-08T13:13:25.182Z" }, + { url = "https://files.pythonhosted.org/packages/58/f9/725e8bf16f343d33cbe076c75dc8370262e194ff10072c0608b8e5cf33a3/coverage-7.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:73419b89f812f498aca53f757dd834919b48ce4799f9d5cad33ca0ae442bdb1a", size = 221640, upload-time = "2025-12-08T13:13:26.836Z" }, + { url = "https://files.pythonhosted.org/packages/8a/ff/e98311000aa6933cc79274e2b6b94a2fe0fe3434fca778eba82003675496/coverage-7.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:eb76670874fdd6091eedcc856128ee48c41a9bbbb9c3f1c7c3cf169290e3ffd6", size = 220269, upload-time = "2025-12-08T13:13:28.116Z" }, + { url = "https://files.pythonhosted.org/packages/cf/cf/bbaa2e1275b300343ea865f7d424cc0a2e2a1df6925a070b2b2d5d765330/coverage-7.13.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6e63ccc6e0ad8986386461c3c4b737540f20426e7ec932f42e030320896c311a", size = 218990, upload-time = "2025-12-08T13:13:29.463Z" }, + { url = "https://files.pythonhosted.org/packages/21/1d/82f0b3323b3d149d7672e7744c116e9c170f4957e0c42572f0366dbb4477/coverage-7.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:494f5459ffa1bd45e18558cd98710c36c0b8fbfa82a5eabcbe671d80ecffbfe8", size = 219340, upload-time = "2025-12-08T13:13:31.524Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e3/fe3fd4702a3832a255f4d43013eacb0ef5fc155a5960ea9269d8696db28b/coverage-7.13.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:06cac81bf10f74034e055e903f5f946e3e26fc51c09fc9f584e4a1605d977053", size = 260638, upload-time = "2025-12-08T13:13:32.965Z" }, + { url = "https://files.pythonhosted.org/packages/ad/01/63186cb000307f2b4da463f72af9b85d380236965574c78e7e27680a2593/coverage-7.13.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f2ffc92b46ed6e6760f1d47a71e56b5664781bc68986dbd1836b2b70c0ce2071", size = 262705, upload-time = "2025-12-08T13:13:34.378Z" }, + { url = "https://files.pythonhosted.org/packages/7c/a1/c0dacef0cc865f2455d59eed3548573ce47ed603205ffd0735d1d78b5906/coverage-7.13.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0602f701057c6823e5db1b74530ce85f17c3c5be5c85fc042ac939cbd909426e", size = 265125, upload-time = "2025-12-08T13:13:35.73Z" }, + { url = "https://files.pythonhosted.org/packages/ef/92/82b99223628b61300bd382c205795533bed021505eab6dd86e11fb5d7925/coverage-7.13.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:25dc33618d45456ccb1d37bce44bc78cf269909aa14c4db2e03d63146a8a1493", size = 259844, upload-time = "2025-12-08T13:13:37.69Z" }, + { url = "https://files.pythonhosted.org/packages/cf/2c/89b0291ae4e6cd59ef042708e1c438e2290f8c31959a20055d8768349ee2/coverage-7.13.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:71936a8b3b977ddd0b694c28c6a34f4fff2e9dd201969a4ff5d5fc7742d614b0", size = 262700, upload-time = "2025-12-08T13:13:39.525Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f9/a5f992efae1996245e796bae34ceb942b05db275e4b34222a9a40b9fbd3b/coverage-7.13.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:936bc20503ce24770c71938d1369461f0c5320830800933bc3956e2a4ded930e", size = 260321, upload-time = "2025-12-08T13:13:41.172Z" }, + { url = "https://files.pythonhosted.org/packages/4c/89/a29f5d98c64fedbe32e2ac3c227fbf78edc01cc7572eee17d61024d89889/coverage-7.13.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:af0a583efaacc52ae2521f8d7910aff65cdb093091d76291ac5820d5e947fc1c", size = 259222, upload-time = "2025-12-08T13:13:43.282Z" }, + { url = "https://files.pythonhosted.org/packages/b3/c3/940fe447aae302a6701ee51e53af7e08b86ff6eed7631e5740c157ee22b9/coverage-7.13.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f1c23e24a7000da892a312fb17e33c5f94f8b001de44b7cf8ba2e36fbd15859e", size = 261411, upload-time = "2025-12-08T13:13:44.72Z" }, + { url = "https://files.pythonhosted.org/packages/eb/31/12a4aec689cb942a89129587860ed4d0fd522d5fda81237147fde554b8ae/coverage-7.13.0-cp313-cp313t-win32.whl", hash = "sha256:5f8a0297355e652001015e93be345ee54393e45dc3050af4a0475c5a2b767d46", size = 221505, upload-time = "2025-12-08T13:13:46.332Z" }, + { url = "https://files.pythonhosted.org/packages/65/8c/3b5fe3259d863572d2b0827642c50c3855d26b3aefe80bdc9eba1f0af3b0/coverage-7.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6abb3a4c52f05e08460bd9acf04fec027f8718ecaa0d09c40ffbc3fbd70ecc39", size = 222569, upload-time = "2025-12-08T13:13:47.79Z" }, + { url = "https://files.pythonhosted.org/packages/b0/39/f71fa8316a96ac72fc3908839df651e8eccee650001a17f2c78cdb355624/coverage-7.13.0-cp313-cp313t-win_arm64.whl", hash = "sha256:3ad968d1e3aa6ce5be295ab5fe3ae1bf5bb4769d0f98a80a0252d543a2ef2e9e", size = 220841, upload-time = "2025-12-08T13:13:49.243Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4b/9b54bedda55421449811dcd5263a2798a63f48896c24dfb92b0f1b0845bd/coverage-7.13.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:453b7ec753cf5e4356e14fe858064e5520c460d3bbbcb9c35e55c0d21155c256", size = 218343, upload-time = "2025-12-08T13:13:50.811Z" }, + { url = "https://files.pythonhosted.org/packages/59/df/c3a1f34d4bba2e592c8979f924da4d3d4598b0df2392fbddb7761258e3dc/coverage-7.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:af827b7cbb303e1befa6c4f94fd2bf72f108089cfa0f8abab8f4ca553cf5ca5a", size = 218672, upload-time = "2025-12-08T13:13:52.284Z" }, + { url = "https://files.pythonhosted.org/packages/07/62/eec0659e47857698645ff4e6ad02e30186eb8afd65214fd43f02a76537cb/coverage-7.13.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9987a9e4f8197a1000280f7cc089e3ea2c8b3c0a64d750537809879a7b4ceaf9", size = 249715, upload-time = "2025-12-08T13:13:53.791Z" }, + { url = "https://files.pythonhosted.org/packages/23/2d/3c7ff8b2e0e634c1f58d095f071f52ed3c23ff25be524b0ccae8b71f99f8/coverage-7.13.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3188936845cd0cb114fa6a51842a304cdbac2958145d03be2377ec41eb285d19", size = 252225, upload-time = "2025-12-08T13:13:55.274Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ac/fb03b469d20e9c9a81093575003f959cf91a4a517b783aab090e4538764b/coverage-7.13.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2bdb3babb74079f021696cb46b8bb5f5661165c385d3a238712b031a12355be", size = 253559, upload-time = "2025-12-08T13:13:57.161Z" }, + { url = "https://files.pythonhosted.org/packages/29/62/14afa9e792383c66cc0a3b872a06ded6e4ed1079c7d35de274f11d27064e/coverage-7.13.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7464663eaca6adba4175f6c19354feea61ebbdd735563a03d1e472c7072d27bb", size = 249724, upload-time = "2025-12-08T13:13:58.692Z" }, + { url = "https://files.pythonhosted.org/packages/31/b7/333f3dab2939070613696ab3ee91738950f0467778c6e5a5052e840646b7/coverage-7.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8069e831f205d2ff1f3d355e82f511eb7c5522d7d413f5db5756b772ec8697f8", size = 251582, upload-time = "2025-12-08T13:14:00.642Z" }, + { url = "https://files.pythonhosted.org/packages/81/cb/69162bda9381f39b2287265d7e29ee770f7c27c19f470164350a38318764/coverage-7.13.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6fb2d5d272341565f08e962cce14cdf843a08ac43bd621783527adb06b089c4b", size = 249538, upload-time = "2025-12-08T13:14:02.556Z" }, + { url = "https://files.pythonhosted.org/packages/e0/76/350387b56a30f4970abe32b90b2a434f87d29f8b7d4ae40d2e8a85aacfb3/coverage-7.13.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5e70f92ef89bac1ac8a99b3324923b4749f008fdbd7aa9cb35e01d7a284a04f9", size = 249349, upload-time = "2025-12-08T13:14:04.015Z" }, + { url = "https://files.pythonhosted.org/packages/86/0d/7f6c42b8d59f4c7e43ea3059f573c0dcfed98ba46eb43c68c69e52ae095c/coverage-7.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4b5de7d4583e60d5fd246dd57fcd3a8aa23c6e118a8c72b38adf666ba8e7e927", size = 251011, upload-time = "2025-12-08T13:14:05.505Z" }, + { url = "https://files.pythonhosted.org/packages/d7/f1/4bb2dff379721bb0b5c649d5c5eaf438462cad824acf32eb1b7ca0c7078e/coverage-7.13.0-cp314-cp314-win32.whl", hash = "sha256:a6c6e16b663be828a8f0b6c5027d36471d4a9f90d28444aa4ced4d48d7d6ae8f", size = 221091, upload-time = "2025-12-08T13:14:07.127Z" }, + { url = "https://files.pythonhosted.org/packages/ba/44/c239da52f373ce379c194b0ee3bcc121020e397242b85f99e0afc8615066/coverage-7.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:0900872f2fdb3ee5646b557918d02279dc3af3dfb39029ac4e945458b13f73bc", size = 221904, upload-time = "2025-12-08T13:14:08.542Z" }, + { url = "https://files.pythonhosted.org/packages/89/1f/b9f04016d2a29c2e4a0307baefefad1a4ec5724946a2b3e482690486cade/coverage-7.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:3a10260e6a152e5f03f26db4a407c4c62d3830b9af9b7c0450b183615f05d43b", size = 220480, upload-time = "2025-12-08T13:14:10.958Z" }, + { url = "https://files.pythonhosted.org/packages/16/d4/364a1439766c8e8647860584171c36010ca3226e6e45b1753b1b249c5161/coverage-7.13.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9097818b6cc1cfb5f174e3263eba4a62a17683bcfe5c4b5d07f4c97fa51fbf28", size = 219074, upload-time = "2025-12-08T13:14:13.345Z" }, + { url = "https://files.pythonhosted.org/packages/ce/f4/71ba8be63351e099911051b2089662c03d5671437a0ec2171823c8e03bec/coverage-7.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0018f73dfb4301a89292c73be6ba5f58722ff79f51593352759c1790ded1cabe", size = 219342, upload-time = "2025-12-08T13:14:15.02Z" }, + { url = "https://files.pythonhosted.org/packages/5e/25/127d8ed03d7711a387d96f132589057213e3aef7475afdaa303412463f22/coverage-7.13.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:166ad2a22ee770f5656e1257703139d3533b4a0b6909af67c6b4a3adc1c98657", size = 260713, upload-time = "2025-12-08T13:14:16.907Z" }, + { url = "https://files.pythonhosted.org/packages/fd/db/559fbb6def07d25b2243663b46ba9eb5a3c6586c0c6f4e62980a68f0ee1c/coverage-7.13.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f6aaef16d65d1787280943f1c8718dc32e9cf141014e4634d64446702d26e0ff", size = 262825, upload-time = "2025-12-08T13:14:18.68Z" }, + { url = "https://files.pythonhosted.org/packages/37/99/6ee5bf7eff884766edb43bd8736b5e1c5144d0fe47498c3779326fe75a35/coverage-7.13.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e999e2dcc094002d6e2c7bbc1fb85b58ba4f465a760a8014d97619330cdbbbf3", size = 265233, upload-time = "2025-12-08T13:14:20.55Z" }, + { url = "https://files.pythonhosted.org/packages/d8/90/92f18fe0356ea69e1f98f688ed80cec39f44e9f09a1f26a1bbf017cc67f2/coverage-7.13.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:00c3d22cf6fb1cf3bf662aaaa4e563be8243a5ed2630339069799835a9cc7f9b", size = 259779, upload-time = "2025-12-08T13:14:22.367Z" }, + { url = "https://files.pythonhosted.org/packages/90/5d/b312a8b45b37a42ea7d27d7d3ff98ade3a6c892dd48d1d503e773503373f/coverage-7.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22ccfe8d9bb0d6134892cbe1262493a8c70d736b9df930f3f3afae0fe3ac924d", size = 262700, upload-time = "2025-12-08T13:14:24.309Z" }, + { url = "https://files.pythonhosted.org/packages/63/f8/b1d0de5c39351eb71c366f872376d09386640840a2e09b0d03973d791e20/coverage-7.13.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:9372dff5ea15930fea0445eaf37bbbafbc771a49e70c0aeed8b4e2c2614cc00e", size = 260302, upload-time = "2025-12-08T13:14:26.068Z" }, + { url = "https://files.pythonhosted.org/packages/aa/7c/d42f4435bc40c55558b3109a39e2d456cddcec37434f62a1f1230991667a/coverage-7.13.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:69ac2c492918c2461bc6ace42d0479638e60719f2a4ef3f0815fa2df88e9f940", size = 259136, upload-time = "2025-12-08T13:14:27.604Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d3/23413241dc04d47cfe19b9a65b32a2edd67ecd0b817400c2843ebc58c847/coverage-7.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:739c6c051a7540608d097b8e13c76cfa85263ced467168dc6b477bae3df7d0e2", size = 261467, upload-time = "2025-12-08T13:14:29.09Z" }, + { url = "https://files.pythonhosted.org/packages/13/e6/6e063174500eee216b96272c0d1847bf215926786f85c2bd024cf4d02d2f/coverage-7.13.0-cp314-cp314t-win32.whl", hash = "sha256:fe81055d8c6c9de76d60c94ddea73c290b416e061d40d542b24a5871bad498b7", size = 221875, upload-time = "2025-12-08T13:14:31.106Z" }, + { url = "https://files.pythonhosted.org/packages/3b/46/f4fb293e4cbe3620e3ac2a3e8fd566ed33affb5861a9b20e3dd6c1896cbc/coverage-7.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:445badb539005283825959ac9fa4a28f712c214b65af3a2c464f1adc90f5fcbc", size = 222982, upload-time = "2025-12-08T13:14:33.1Z" }, + { url = "https://files.pythonhosted.org/packages/68/62/5b3b9018215ed9733fbd1ae3b2ed75c5de62c3b55377a52cae732e1b7805/coverage-7.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:de7f6748b890708578fc4b7bb967d810aeb6fcc9bff4bb77dbca77dab2f9df6a", size = 221016, upload-time = "2025-12-08T13:14:34.601Z" }, + { url = "https://files.pythonhosted.org/packages/8d/4c/1968f32fb9a2604645827e11ff84a31e59d532e01995f904723b4f5328b3/coverage-7.13.0-py3-none-any.whl", hash = "sha256:850d2998f380b1e266459ca5b47bc9e7daf9af1d070f66317972f382d46f1904", size = 210068, upload-time = "2025-12-08T13:14:36.236Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "cryptography" +version = "46.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", size = 7225004, upload-time = "2025-10-15T23:16:52.239Z" }, + { url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667, upload-time = "2025-10-15T23:16:54.369Z" }, + { url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" }, + { url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" }, + { url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800, upload-time = "2025-10-15T23:17:00.378Z" }, + { url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707, upload-time = "2025-10-15T23:17:01.98Z" }, + { url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541, upload-time = "2025-10-15T23:17:04.078Z" }, + { url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464, upload-time = "2025-10-15T23:17:05.483Z" }, + { url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838, upload-time = "2025-10-15T23:17:07.425Z" }, + { url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596, upload-time = "2025-10-15T23:17:09.343Z" }, + { url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782, upload-time = "2025-10-15T23:17:11.22Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381, upload-time = "2025-10-15T23:17:12.829Z" }, + { url = "https://files.pythonhosted.org/packages/96/92/8a6a9525893325fc057a01f654d7efc2c64b9de90413adcf605a85744ff4/cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018", size = 3055988, upload-time = "2025-10-15T23:17:14.65Z" }, + { url = "https://files.pythonhosted.org/packages/7e/bf/80fbf45253ea585a1e492a6a17efcb93467701fa79e71550a430c5e60df0/cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", size = 3514451, upload-time = "2025-10-15T23:17:16.142Z" }, + { url = "https://files.pythonhosted.org/packages/2e/af/9b302da4c87b0beb9db4e756386a7c6c5b8003cd0e742277888d352ae91d/cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", size = 2928007, upload-time = "2025-10-15T23:17:18.04Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e2/a510aa736755bffa9d2f75029c229111a1d02f8ecd5de03078f4c18d91a3/cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217", size = 7158012, upload-time = "2025-10-15T23:17:19.982Z" }, + { url = "https://files.pythonhosted.org/packages/73/dc/9aa866fbdbb95b02e7f9d086f1fccfeebf8953509b87e3f28fff927ff8a0/cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5", size = 4288728, upload-time = "2025-10-15T23:17:21.527Z" }, + { url = "https://files.pythonhosted.org/packages/c5/fd/bc1daf8230eaa075184cbbf5f8cd00ba9db4fd32d63fb83da4671b72ed8a/cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715", size = 4435078, upload-time = "2025-10-15T23:17:23.042Z" }, + { url = "https://files.pythonhosted.org/packages/82/98/d3bd5407ce4c60017f8ff9e63ffee4200ab3e23fe05b765cab805a7db008/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54", size = 4293460, upload-time = "2025-10-15T23:17:24.885Z" }, + { url = "https://files.pythonhosted.org/packages/26/e9/e23e7900983c2b8af7a08098db406cf989d7f09caea7897e347598d4cd5b/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459", size = 3995237, upload-time = "2025-10-15T23:17:26.449Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/af68c509d4a138cfe299d0d7ddb14afba15233223ebd933b4bbdbc7155d3/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422", size = 4967344, upload-time = "2025-10-15T23:17:28.06Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e3/8643d077c53868b681af077edf6b3cb58288b5423610f21c62aadcbe99f4/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7", size = 4466564, upload-time = "2025-10-15T23:17:29.665Z" }, + { url = "https://files.pythonhosted.org/packages/0e/43/c1e8726fa59c236ff477ff2b5dc071e54b21e5a1e51aa2cee1676f1c986f/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044", size = 4292415, upload-time = "2025-10-15T23:17:31.686Z" }, + { url = "https://files.pythonhosted.org/packages/42/f9/2f8fefdb1aee8a8e3256a0568cffc4e6d517b256a2fe97a029b3f1b9fe7e/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665", size = 4931457, upload-time = "2025-10-15T23:17:33.478Z" }, + { url = "https://files.pythonhosted.org/packages/79/30/9b54127a9a778ccd6d27c3da7563e9f2d341826075ceab89ae3b41bf5be2/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3", size = 4466074, upload-time = "2025-10-15T23:17:35.158Z" }, + { url = "https://files.pythonhosted.org/packages/ac/68/b4f4a10928e26c941b1b6a179143af9f4d27d88fe84a6a3c53592d2e76bf/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20", size = 4420569, upload-time = "2025-10-15T23:17:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/a3/49/3746dab4c0d1979888f125226357d3262a6dd40e114ac29e3d2abdf1ec55/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de", size = 4681941, upload-time = "2025-10-15T23:17:39.236Z" }, + { url = "https://files.pythonhosted.org/packages/fd/30/27654c1dbaf7e4a3531fa1fc77986d04aefa4d6d78259a62c9dc13d7ad36/cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914", size = 3022339, upload-time = "2025-10-15T23:17:40.888Z" }, + { url = "https://files.pythonhosted.org/packages/f6/30/640f34ccd4d2a1bc88367b54b926b781b5a018d65f404d409aba76a84b1c/cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db", size = 3494315, upload-time = "2025-10-15T23:17:42.769Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8b/88cc7e3bd0a8e7b861f26981f7b820e1f46aa9d26cc482d0feba0ecb4919/cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21", size = 2919331, upload-time = "2025-10-15T23:17:44.468Z" }, + { url = "https://files.pythonhosted.org/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", size = 7218248, upload-time = "2025-10-15T23:17:46.294Z" }, + { url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089, upload-time = "2025-10-15T23:17:48.269Z" }, + { url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" }, + { url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280, upload-time = "2025-10-15T23:17:52.964Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958, upload-time = "2025-10-15T23:17:54.965Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714, upload-time = "2025-10-15T23:17:56.754Z" }, + { url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970, upload-time = "2025-10-15T23:17:58.588Z" }, + { url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236, upload-time = "2025-10-15T23:18:00.897Z" }, + { url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642, upload-time = "2025-10-15T23:18:02.749Z" }, + { url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126, upload-time = "2025-10-15T23:18:04.85Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573, upload-time = "2025-10-15T23:18:06.908Z" }, + { url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695, upload-time = "2025-10-15T23:18:08.672Z" }, + { url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720, upload-time = "2025-10-15T23:18:10.632Z" }, + { url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740, upload-time = "2025-10-15T23:18:12.277Z" }, + { url = "https://files.pythonhosted.org/packages/d9/cd/1a8633802d766a0fa46f382a77e096d7e209e0817892929655fe0586ae32/cryptography-46.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a23582810fedb8c0bc47524558fb6c56aac3fc252cb306072fd2815da2a47c32", size = 3689163, upload-time = "2025-10-15T23:18:13.821Z" }, + { url = "https://files.pythonhosted.org/packages/4c/59/6b26512964ace6480c3e54681a9859c974172fb141c38df11eadd8416947/cryptography-46.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e7aec276d68421f9574040c26e2a7c3771060bc0cff408bae1dcb19d3ab1e63c", size = 3429474, upload-time = "2025-10-15T23:18:15.477Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/e60e46adab4362a682cf142c7dcb5bf79b782ab2199b0dcb81f55970807f/cryptography-46.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7ce938a99998ed3c8aa7e7272dca1a610401ede816d36d0693907d863b10d9ea", size = 3698132, upload-time = "2025-10-15T23:18:17.056Z" }, + { url = "https://files.pythonhosted.org/packages/da/38/f59940ec4ee91e93d3311f7532671a5cef5570eb04a144bf203b58552d11/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:191bb60a7be5e6f54e30ba16fdfae78ad3a342a0599eb4193ba88e3f3d6e185b", size = 4243992, upload-time = "2025-10-15T23:18:18.695Z" }, + { url = "https://files.pythonhosted.org/packages/b0/0c/35b3d92ddebfdfda76bb485738306545817253d0a3ded0bfe80ef8e67aa5/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c70cc23f12726be8f8bc72e41d5065d77e4515efae3690326764ea1b07845cfb", size = 4409944, upload-time = "2025-10-15T23:18:20.597Z" }, + { url = "https://files.pythonhosted.org/packages/99/55/181022996c4063fc0e7666a47049a1ca705abb9c8a13830f074edb347495/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:9394673a9f4de09e28b5356e7fff97d778f8abad85c9d5ac4a4b7e25a0de7717", size = 4242957, upload-time = "2025-10-15T23:18:22.18Z" }, + { url = "https://files.pythonhosted.org/packages/ba/af/72cd6ef29f9c5f731251acadaeb821559fe25f10852f44a63374c9ca08c1/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94cd0549accc38d1494e1f8de71eca837d0509d0d44bf11d158524b0e12cebf9", size = 4409447, upload-time = "2025-10-15T23:18:24.209Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c3/e90f4a4feae6410f914f8ebac129b9ae7a8c92eb60a638012dde42030a9d/cryptography-46.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6b5063083824e5509fdba180721d55909ffacccc8adbec85268b48439423d78c", size = 3438528, upload-time = "2025-10-15T23:18:26.227Z" }, +] + +[[package]] +name = "cssselect2" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tinycss2" }, + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/86/fd7f58fc498b3166f3a7e8e0cddb6e620fe1da35b02248b1bd59e95dbaaa/cssselect2-0.8.0.tar.gz", hash = "sha256:7674ffb954a3b46162392aee2a3a0aedb2e14ecf99fcc28644900f4e6e3e9d3a", size = 35716, upload-time = "2025-03-05T14:46:07.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/e7/aa315e6a749d9b96c2504a1ba0ba031ba2d0517e972ce22682e3fccecb09/cssselect2-0.8.0-py3-none-any.whl", hash = "sha256:46fc70ebc41ced7a32cd42d58b1884d72ade23d21e5a4eaaf022401c13f0e76e", size = 15454, upload-time = "2025-03-05T14:46:06.463Z" }, +] + +[[package]] +name = "cyclic" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/9f/becc4fea44301f232e4eba17752001bd708e3c042fef37a72b9af7ddf4b5/cyclic-1.0.0.tar.gz", hash = "sha256:ecddd56cb831ee3e6b79f61ecb0ad71caee606c507136867782911aa01c3e5eb", size = 2167, upload-time = "2018-09-26T16:47:07.285Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/c0/9f59d2ebd9d585e1681c51767eb138bcd9d0ea770f6fc003cd875c7f5e62/cyclic-1.0.0-py3-none-any.whl", hash = "sha256:32d8181d7698f426bce6f14f4c3921ef95b6a84af9f96192b59beb05bc00c3ed", size = 2547, upload-time = "2018-09-26T16:47:05.609Z" }, +] + +[[package]] +name = "defusedxml" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, +] + +[[package]] +name = "dirty-equals" +version = "0.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/1d/c5913ac9d6615515a00f4bdc71356d302437cb74ff2e9aaccd3c14493b78/dirty_equals-0.11.tar.gz", hash = "sha256:f4ac74ee88f2d11e2fa0f65eb30ee4f07105c5f86f4dc92b09eb1138775027c3", size = 128067, upload-time = "2025-11-17T01:51:24.451Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/8d/dbff05239043271dbeace563a7686212a3dd517864a35623fe4d4a64ca19/dirty_equals-0.11-py3-none-any.whl", hash = "sha256:b1d7093273fc2f9be12f443a8ead954ef6daaf6746fd42ef3a5616433ee85286", size = 28051, upload-time = "2025-11-17T01:51:22.849Z" }, +] + +[[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 = "fastapi" +version = "0.136.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4e/d9/e66315807e41e69e7f6a1b42a162dada2f249c5f06ad3f1a95f84ab336ef/fastapi-0.136.0.tar.gz", hash = "sha256:cf08e067cc66e106e102d9ba659463abfac245200752f8a5b7b1e813de4ff73e", size = 396607, upload-time = "2026-04-16T11:47:13.623Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/a3/0bd5f0cdb0bbc92650e8dc457e9250358411ee5d1b65e42b6632387daf81/fastapi-0.136.0-py3-none-any.whl", hash = "sha256:8793d44ec7378e2be07f8a013cf7f7aa47d6327d0dfe9804862688ec4541a6b4", size = 117556, upload-time = "2026-04-16T11:47:11.922Z" }, +] + +[[package]] +name = "ghp-import" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, +] + +[[package]] +name = "greenlet" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/e5/40dbda2736893e3e53d25838e0f19a2b417dfc122b9989c91918db30b5d3/greenlet-3.3.0.tar.gz", hash = "sha256:a82bb225a4e9e4d653dd2fb7b8b2d36e4fb25bc0165422a11e48b88e9e6f78fb", size = 190651, upload-time = "2025-12-04T14:49:44.05Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/6a/33d1702184d94106d3cdd7bfb788e19723206fce152e303473ca3b946c7b/greenlet-3.3.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:6f8496d434d5cb2dce025773ba5597f71f5410ae499d5dd9533e0653258cdb3d", size = 273658, upload-time = "2025-12-04T14:23:37.494Z" }, + { url = "https://files.pythonhosted.org/packages/d6/b7/2b5805bbf1907c26e434f4e448cd8b696a0b71725204fa21a211ff0c04a7/greenlet-3.3.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b96dc7eef78fd404e022e165ec55327f935b9b52ff355b067eb4a0267fc1cffb", size = 574810, upload-time = "2025-12-04T14:50:04.154Z" }, + { url = "https://files.pythonhosted.org/packages/94/38/343242ec12eddf3d8458c73f555c084359883d4ddc674240d9e61ec51fd6/greenlet-3.3.0-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:73631cd5cccbcfe63e3f9492aaa664d278fda0ce5c3d43aeda8e77317e38efbd", size = 586248, upload-time = "2025-12-04T14:57:39.35Z" }, + { url = "https://files.pythonhosted.org/packages/b6/a8/15d0aa26c0036a15d2659175af00954aaaa5d0d66ba538345bd88013b4d7/greenlet-3.3.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7dee147740789a4632cace364816046e43310b59ff8fb79833ab043aefa72fd5", size = 586910, upload-time = "2025-12-04T14:25:59.705Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9b/68d5e3b7ccaba3907e5532cf8b9bf16f9ef5056a008f195a367db0ff32db/greenlet-3.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:39b28e339fc3c348427560494e28d8a6f3561c8d2bcf7d706e1c624ed8d822b9", size = 1547206, upload-time = "2025-12-04T15:04:21.027Z" }, + { url = "https://files.pythonhosted.org/packages/66/bd/e3086ccedc61e49f91e2cfb5ffad9d8d62e5dc85e512a6200f096875b60c/greenlet-3.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b3c374782c2935cc63b2a27ba8708471de4ad1abaa862ffdb1ef45a643ddbb7d", size = 1613359, upload-time = "2025-12-04T14:27:26.548Z" }, + { url = "https://files.pythonhosted.org/packages/f4/6b/d4e73f5dfa888364bbf02efa85616c6714ae7c631c201349782e5b428925/greenlet-3.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:b49e7ed51876b459bd645d83db257f0180e345d3f768a35a85437a24d5a49082", size = 300740, upload-time = "2025-12-04T14:47:52.773Z" }, + { url = "https://files.pythonhosted.org/packages/1f/cb/48e964c452ca2b92175a9b2dca037a553036cb053ba69e284650ce755f13/greenlet-3.3.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e29f3018580e8412d6aaf5641bb7745d38c85228dacf51a73bd4e26ddf2a6a8e", size = 274908, upload-time = "2025-12-04T14:23:26.435Z" }, + { url = "https://files.pythonhosted.org/packages/28/da/38d7bff4d0277b594ec557f479d65272a893f1f2a716cad91efeb8680953/greenlet-3.3.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a687205fb22794e838f947e2194c0566d3812966b41c78709554aa883183fb62", size = 577113, upload-time = "2025-12-04T14:50:05.493Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f2/89c5eb0faddc3ff014f1c04467d67dee0d1d334ab81fadbf3744847f8a8a/greenlet-3.3.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4243050a88ba61842186cb9e63c7dfa677ec146160b0efd73b855a3d9c7fcf32", size = 590338, upload-time = "2025-12-04T14:57:41.136Z" }, + { url = "https://files.pythonhosted.org/packages/dc/a6/e959a127b630a58e23529972dbc868c107f9d583b5a9f878fb858c46bc1a/greenlet-3.3.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cb3a8ec3db4a3b0eb8a3c25436c2d49e3505821802074969db017b87bc6a948", size = 590206, upload-time = "2025-12-04T14:26:01.254Z" }, + { url = "https://files.pythonhosted.org/packages/48/60/29035719feb91798693023608447283b266b12efc576ed013dd9442364bb/greenlet-3.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2de5a0b09eab81fc6a382791b995b1ccf2b172a9fec934747a7a23d2ff291794", size = 1550668, upload-time = "2025-12-04T15:04:22.439Z" }, + { url = "https://files.pythonhosted.org/packages/0a/5f/783a23754b691bfa86bd72c3033aa107490deac9b2ef190837b860996c9f/greenlet-3.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4449a736606bd30f27f8e1ff4678ee193bc47f6ca810d705981cfffd6ce0d8c5", size = 1615483, upload-time = "2025-12-04T14:27:28.083Z" }, + { url = "https://files.pythonhosted.org/packages/1d/d5/c339b3b4bc8198b7caa4f2bd9fd685ac9f29795816d8db112da3d04175bb/greenlet-3.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:7652ee180d16d447a683c04e4c5f6441bae7ba7b17ffd9f6b3aff4605e9e6f71", size = 301164, upload-time = "2025-12-04T14:42:51.577Z" }, + { url = "https://files.pythonhosted.org/packages/f8/0a/a3871375c7b9727edaeeea994bfff7c63ff7804c9829c19309ba2e058807/greenlet-3.3.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:b01548f6e0b9e9784a2c99c5651e5dc89ffcbe870bc5fb2e5ef864e9cc6b5dcb", size = 276379, upload-time = "2025-12-04T14:23:30.498Z" }, + { url = "https://files.pythonhosted.org/packages/43/ab/7ebfe34dce8b87be0d11dae91acbf76f7b8246bf9d6b319c741f99fa59c6/greenlet-3.3.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:349345b770dc88f81506c6861d22a6ccd422207829d2c854ae2af8025af303e3", size = 597294, upload-time = "2025-12-04T14:50:06.847Z" }, + { url = "https://files.pythonhosted.org/packages/a4/39/f1c8da50024feecd0793dbd5e08f526809b8ab5609224a2da40aad3a7641/greenlet-3.3.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e8e18ed6995e9e2c0b4ed264d2cf89260ab3ac7e13555b8032b25a74c6d18655", size = 607742, upload-time = "2025-12-04T14:57:42.349Z" }, + { url = "https://files.pythonhosted.org/packages/75/b0/6bde0b1011a60782108c01de5913c588cf51a839174538d266de15e4bf4d/greenlet-3.3.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:047ab3df20ede6a57c35c14bf5200fcf04039d50f908270d3f9a7a82064f543b", size = 609885, upload-time = "2025-12-04T14:26:02.368Z" }, + { url = "https://files.pythonhosted.org/packages/49/0e/49b46ac39f931f59f987b7cd9f34bfec8ef81d2a1e6e00682f55be5de9f4/greenlet-3.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2d9ad37fc657b1102ec880e637cccf20191581f75c64087a549e66c57e1ceb53", size = 1567424, upload-time = "2025-12-04T15:04:23.757Z" }, + { url = "https://files.pythonhosted.org/packages/05/f5/49a9ac2dff7f10091935def9165c90236d8f175afb27cbed38fb1d61ab6b/greenlet-3.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83cd0e36932e0e7f36a64b732a6f60c2fc2df28c351bae79fbaf4f8092fe7614", size = 1636017, upload-time = "2025-12-04T14:27:29.688Z" }, + { url = "https://files.pythonhosted.org/packages/6c/79/3912a94cf27ec503e51ba493692d6db1e3cd8ac7ac52b0b47c8e33d7f4f9/greenlet-3.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a7a34b13d43a6b78abf828a6d0e87d3385680eaf830cd60d20d52f249faabf39", size = 301964, upload-time = "2025-12-04T14:36:58.316Z" }, + { url = "https://files.pythonhosted.org/packages/02/2f/28592176381b9ab2cafa12829ba7b472d177f3acc35d8fbcf3673d966fff/greenlet-3.3.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:a1e41a81c7e2825822f4e068c48cb2196002362619e2d70b148f20a831c00739", size = 275140, upload-time = "2025-12-04T14:23:01.282Z" }, + { url = "https://files.pythonhosted.org/packages/2c/80/fbe937bf81e9fca98c981fe499e59a3f45df2a04da0baa5c2be0dca0d329/greenlet-3.3.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f515a47d02da4d30caaa85b69474cec77b7929b2e936ff7fb853d42f4bf8808", size = 599219, upload-time = "2025-12-04T14:50:08.309Z" }, + { url = "https://files.pythonhosted.org/packages/c2/ff/7c985128f0514271b8268476af89aee6866df5eec04ac17dcfbc676213df/greenlet-3.3.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d2d9fd66bfadf230b385fdc90426fcd6eb64db54b40c495b72ac0feb5766c54", size = 610211, upload-time = "2025-12-04T14:57:43.968Z" }, + { url = "https://files.pythonhosted.org/packages/fd/8e/424b8c6e78bd9837d14ff7df01a9829fc883ba2ab4ea787d4f848435f23f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:087ea5e004437321508a8d6f20efc4cfec5e3c30118e1417ea96ed1d93950527", size = 612833, upload-time = "2025-12-04T14:26:03.669Z" }, + { url = "https://files.pythonhosted.org/packages/b5/ba/56699ff9b7c76ca12f1cdc27a886d0f81f2189c3455ff9f65246780f713d/greenlet-3.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ab97cf74045343f6c60a39913fa59710e4bd26a536ce7ab2397adf8b27e67c39", size = 1567256, upload-time = "2025-12-04T15:04:25.276Z" }, + { url = "https://files.pythonhosted.org/packages/1e/37/f31136132967982d698c71a281a8901daf1a8fbab935dce7c0cf15f942cc/greenlet-3.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5375d2e23184629112ca1ea89a53389dddbffcf417dad40125713d88eb5f96e8", size = 1636483, upload-time = "2025-12-04T14:27:30.804Z" }, + { url = "https://files.pythonhosted.org/packages/7e/71/ba21c3fb8c5dce83b8c01f458a42e99ffdb1963aeec08fff5a18588d8fd7/greenlet-3.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:9ee1942ea19550094033c35d25d20726e4f1c40d59545815e1128ac58d416d38", size = 301833, upload-time = "2025-12-04T14:32:23.929Z" }, + { url = "https://files.pythonhosted.org/packages/d7/7c/f0a6d0ede2c7bf092d00bc83ad5bafb7e6ec9b4aab2fbdfa6f134dc73327/greenlet-3.3.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:60c2ef0f578afb3c8d92ea07ad327f9a062547137afe91f38408f08aacab667f", size = 275671, upload-time = "2025-12-04T14:23:05.267Z" }, + { url = "https://files.pythonhosted.org/packages/44/06/dac639ae1a50f5969d82d2e3dd9767d30d6dbdbab0e1a54010c8fe90263c/greenlet-3.3.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a5d554d0712ba1de0a6c94c640f7aeba3f85b3a6e1f2899c11c2c0428da9365", size = 646360, upload-time = "2025-12-04T14:50:10.026Z" }, + { url = "https://files.pythonhosted.org/packages/e0/94/0fb76fe6c5369fba9bf98529ada6f4c3a1adf19e406a47332245ef0eb357/greenlet-3.3.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3a898b1e9c5f7307ebbde4102908e6cbfcb9ea16284a3abe15cab996bee8b9b3", size = 658160, upload-time = "2025-12-04T14:57:45.41Z" }, + { url = "https://files.pythonhosted.org/packages/b8/14/bab308fc2c1b5228c3224ec2bf928ce2e4d21d8046c161e44a2012b5203e/greenlet-3.3.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5773edda4dc00e173820722711d043799d3adb4f01731f40619e07ea2750b955", size = 660166, upload-time = "2025-12-04T14:26:05.099Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d2/91465d39164eaa0085177f61983d80ffe746c5a1860f009811d498e7259c/greenlet-3.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ac0549373982b36d5fd5d30beb8a7a33ee541ff98d2b502714a09f1169f31b55", size = 1615193, upload-time = "2025-12-04T15:04:27.041Z" }, + { url = "https://files.pythonhosted.org/packages/42/1b/83d110a37044b92423084d52d5d5a3b3a73cafb51b547e6d7366ff62eff1/greenlet-3.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d198d2d977460358c3b3a4dc844f875d1adb33817f0613f663a656f463764ccc", size = 1683653, upload-time = "2025-12-04T14:27:32.366Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/9030e6f9aa8fd7808e9c31ba4c38f87c4f8ec324ee67431d181fe396d705/greenlet-3.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:73f51dd0e0bdb596fb0417e475fa3c5e32d4c83638296e560086b8d7da7c4170", size = 305387, upload-time = "2025-12-04T14:26:51.063Z" }, + { url = "https://files.pythonhosted.org/packages/a0/66/bd6317bc5932accf351fc19f177ffba53712a202f9df10587da8df257c7e/greenlet-3.3.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:d6ed6f85fae6cdfdb9ce04c9bf7a08d666cfcfb914e7d006f44f840b46741931", size = 282638, upload-time = "2025-12-04T14:25:20.941Z" }, + { url = "https://files.pythonhosted.org/packages/30/cf/cc81cb030b40e738d6e69502ccbd0dd1bced0588e958f9e757945de24404/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9125050fcf24554e69c4cacb086b87b3b55dc395a8b3ebe6487b045b2614388", size = 651145, upload-time = "2025-12-04T14:50:11.039Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ea/1020037b5ecfe95ca7df8d8549959baceb8186031da83d5ecceff8b08cd2/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:87e63ccfa13c0a0f6234ed0add552af24cc67dd886731f2261e46e241608bee3", size = 654236, upload-time = "2025-12-04T14:57:47.007Z" }, + { url = "https://files.pythonhosted.org/packages/57/b9/f8025d71a6085c441a7eaff0fd928bbb275a6633773667023d19179fe815/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3c6e9b9c1527a78520357de498b0e709fb9e2f49c3a513afd5a249007261911b", size = 653783, upload-time = "2025-12-04T14:26:06.225Z" }, + { url = "https://files.pythonhosted.org/packages/f6/c7/876a8c7a7485d5d6b5c6821201d542ef28be645aa024cfe1145b35c120c1/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:286d093f95ec98fdd92fcb955003b8a3d054b4e2cab3e2707a5039e7b50520fd", size = 1614857, upload-time = "2025-12-04T15:04:28.484Z" }, + { url = "https://files.pythonhosted.org/packages/4f/dc/041be1dff9f23dac5f48a43323cd0789cb798342011c19a248d9c9335536/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c10513330af5b8ae16f023e8ddbfb486ab355d04467c4679c5cfe4659975dd9", size = 1676034, upload-time = "2025-12-04T14:27:33.531Z" }, +] + +[[package]] +name = "griffe-typingdoc" +version = "0.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "griffelib" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/26/28182e0c8055842bf3da774dee1d5b789c0f236c078dcbdca1937b5214dc/griffe_typingdoc-0.3.1.tar.gz", hash = "sha256:2ff4703115cb7f8a65b9fdcdd1f3c3a15f813b6554621b52eaad094c4782ce96", size = 31218, upload-time = "2026-02-21T09:38:54.409Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/c4/cf543fbde49e1ae44830ef0840a4d6ee9f4e4f338138a7766d4e37cf6440/griffe_typingdoc-0.3.1-py3-none-any.whl", hash = "sha256:ecbd457ef6883126b8b6023abf12e08c58e1c152238a2f0e2afdd67a64b07021", size = 10092, upload-time = "2026-02-20T14:53:47.84Z" }, +] + +[[package]] +name = "griffe-warnings-deprecated" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "griffelib" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/9e/fc86f1e9270f143a395a601de81aa42a871722c34d4b3c7763658dc2e04d/griffe_warnings_deprecated-1.1.1.tar.gz", hash = "sha256:9261369bf2acb8b5d24a0dc7895cce788208513d4349031d4ea315b979b2e99f", size = 26262, upload-time = "2026-02-21T09:38:55.858Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/3c/c2a9eee79bf2c8002d2fa370534bee93fdca39e8b1fc82e83d552d5d2c07/griffe_warnings_deprecated-1.1.1-py3-none-any.whl", hash = "sha256:4b7d765e82ca9139ed44ffe7bdebed0d3a46ce014ad5a35a2c22e9a16288737a", size = 6565, upload-time = "2026-02-20T15:35:23.577Z" }, +] + +[[package]] +name = "griffelib" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ad/06/eccbd311c9e2b3ca45dbc063b93134c57a1ccc7607c5e545264ad092c4a9/griffelib-2.0.0.tar.gz", hash = "sha256:e504d637a089f5cab9b5daf18f7645970509bf4f53eda8d79ed71cce8bd97934", size = 166312, upload-time = "2026-03-23T21:06:55.954Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/51/c936033e16d12b627ea334aaaaf42229c37620d0f15593456ab69ab48161/griffelib-2.0.0-py3-none-any.whl", hash = "sha256:01284878c966508b6d6f1dbff9b6fa607bc062d8261c5c7253cb285b06422a7f", size = 142004, upload-time = "2026-02-09T19:09:40.561Z" }, +] + +[[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 = "hjson" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/e5/0b56d723a76ca67abadbf7fb71609fb0ea7e6926e94fcca6c65a85b36a0e/hjson-3.1.0.tar.gz", hash = "sha256:55af475a27cf83a7969c808399d7bccdec8fb836a07ddbd574587593b9cdcf75", size = 40541, upload-time = "2022-08-13T02:53:01.919Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/7f/13cd798d180af4bf4c0ceddeefba2b864a63c71645abc0308b768d67bb81/hjson-3.1.0-py3-none-any.whl", hash = "sha256:65713cdcf13214fb554eb8b4ef803419733f4f5e551047c9b711098ab7186b89", size = 54018, upload-time = "2022-08-13T02:52:59.899Z" }, +] + +[[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 = "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 = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "markdown" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/7dd27d9d863b3376fcf23a5a13cb5d024aed1db46f963f1b5735ae43b3be/markdown-3.10.tar.gz", hash = "sha256:37062d4f2aa4b2b6b32aefb80faa300f82cc790cb949a35b8caede34f2b68c0e", size = 364931, upload-time = "2025-11-03T19:51:15.007Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/81/54e3ce63502cd085a0c556652a4e1b919c45a446bd1e5300e10c44c8c521/markdown-3.10-py3-none-any.whl", hash = "sha256:b5b99d6951e2e4948d939255596523444c0e677c669700b1d17aa4a8a464cb7c", size = 107678, upload-time = "2025-11-03T19:51:13.887Z" }, +] + +[[package]] +name = "markdown-include-variants" +version = "0.0.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c5/47/ec9eae4a6d2f336d95681df43720e2b25b045dc3ed44ae6d30a5ce2f5dff/markdown_include_variants-0.0.8.tar.gz", hash = "sha256:46d812340c64dcd3646b1eaa356bafb31626dd7b4955d15c44ff8c48c6357227", size = 46882, upload-time = "2025-12-12T16:11:04.254Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/0e/130958e7ec50d13f2ee7b6c142df5c352520a9251baf1652e41262703857/markdown_include_variants-0.0.8-py3-none-any.whl", hash = "sha256:425a300ae25fbcd598506cba67859a9dfa047333e869e0ff2e11a5e354b326dc", size = 8120, upload-time = "2025-12-12T16:11:02.881Z" }, +] + +[[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 = "mdx-include" +version = "1.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cyclic" }, + { name = "markdown" }, + { name = "rcslice" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bf/f0/f395a9cf164471d3c7bbe58cbd64d74289575a8b85a962b49a804ab7ed34/mdx_include-1.4.2.tar.gz", hash = "sha256:992f9fbc492b5cf43f7d8cb4b90b52a4e4c5fdd7fd04570290a83eea5c84f297", size = 15051, upload-time = "2022-07-26T05:46:14.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/40/6844997dee251103c5a4c4eb0d1d2f2162b7c29ffc4e86de3cd68d269be2/mdx_include-1.4.2-py3-none-any.whl", hash = "sha256:cfbeadd59985f27a9b70cb7ab0a3d209892fe1bb1aa342df055e0b135b3c9f34", size = 11591, upload-time = "2022-07-26T05:46:11.518Z" }, +] + +[[package]] +name = "mergedeep" +version = "1.3.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, +] + +[[package]] +name = "mkdocs" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "ghp-import" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mergedeep" }, + { name = "mkdocs-get-deps" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "pyyaml" }, + { name = "pyyaml-env-tag" }, + { name = "watchdog" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" }, +] + +[[package]] +name = "mkdocs-autorefs" +version = "1.4.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mkdocs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/51/fa/9124cd63d822e2bcbea1450ae68cdc3faf3655c69b455f3a7ed36ce6c628/mkdocs_autorefs-1.4.3.tar.gz", hash = "sha256:beee715b254455c4aa93b6ef3c67579c399ca092259cc41b7d9342573ff1fc75", size = 55425, upload-time = "2025-08-26T14:23:17.223Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/4d/7123b6fa2278000688ebd338e2a06d16870aaf9eceae6ba047ea05f92df1/mkdocs_autorefs-1.4.3-py3-none-any.whl", hash = "sha256:469d85eb3114801d08e9cc55d102b3ba65917a869b893403b8987b601cf55dc9", size = 25034, upload-time = "2025-08-26T14:23:15.906Z" }, +] + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mergedeep" }, + { name = "platformdirs" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/f5/ed29cd50067784976f25ed0ed6fcd3c2ce9eb90650aa3b2796ddf7b6870b/mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c", size = 10239, upload-time = "2023-11-20T17:51:09.981Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521, upload-time = "2023-11-20T17:51:08.587Z" }, +] + +[[package]] +name = "mkdocs-macros-plugin" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "hjson" }, + { name = "jinja2" }, + { name = "mkdocs" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "python-dateutil" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "super-collections" }, + { name = "termcolor" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/92/15/e6a44839841ebc9c5872fa0e6fad1c3757424e4fe026093b68e9f386d136/mkdocs_macros_plugin-1.5.0.tar.gz", hash = "sha256:12aa45ce7ecb7a445c66b9f649f3dd05e9b92e8af6bc65e4acd91d26f878c01f", size = 37730, upload-time = "2025-11-13T08:08:55.545Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/62/9fffba5bb9ed3d31a932ad35038ba9483d59850256ee0fea7f1187173983/mkdocs_macros_plugin-1.5.0-py3-none-any.whl", hash = "sha256:c10fabd812bf50f9170609d0ed518e54f1f0e12c334ac29141723a83c881dd6f", size = 44626, upload-time = "2025-11-13T08:08:53.878Z" }, +] + +[[package]] +name = "mkdocs-material" +version = "9.7.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "backrefs" }, + { name = "colorama" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "mkdocs" }, + { name = "mkdocs-material-extensions" }, + { name = "paginate" }, + { name = "pygments" }, + { name = "pymdown-extensions" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/45/29/6d2bcf41ae40802c4beda2432396fff97b8456fb496371d1bc7aad6512ec/mkdocs_material-9.7.6.tar.gz", hash = "sha256:00bdde50574f776d328b1862fe65daeaf581ec309bd150f7bff345a098c64a69", size = 4097959, upload-time = "2026-03-19T15:41:58.161Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/01/bc663630c510822c95c47a66af9fa7a443c295b47d5f041e5e6ae62ef659/mkdocs_material-9.7.6-py3-none-any.whl", hash = "sha256:71b84353921b8ea1ba84fe11c50912cc512da8fe0881038fcc9a0761c0e635ba", size = 9305470, upload-time = "2026-03-19T15:41:55.217Z" }, +] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847, upload-time = "2023-11-22T19:09:45.208Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" }, +] + +[[package]] +name = "mkdocs-redirects" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mkdocs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/a8/6d44a6cf07e969c7420cb36ab287b0669da636a2044de38a7d2208d5a758/mkdocs_redirects-1.2.2.tar.gz", hash = "sha256:3094981b42ffab29313c2c1b8ac3969861109f58b2dd58c45fc81cd44bfa0095", size = 7162, upload-time = "2024-11-07T14:57:21.109Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/ec/38443b1f2a3821bbcb24e46cd8ba979154417794d54baf949fefde1c2146/mkdocs_redirects-1.2.2-py3-none-any.whl", hash = "sha256:7dbfa5647b79a3589da4401403d69494bd1f4ad03b9c15136720367e1f340ed5", size = 6142, upload-time = "2024-11-07T14:57:19.143Z" }, +] + +[[package]] +name = "mkdocstrings" +version = "1.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mkdocs" }, + { name = "mkdocs-autorefs" }, + { name = "pymdown-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/46/62/0dfc5719514115bf1781f44b1d7f2a0923fcc01e9c5d7990e48a05c9ae5d/mkdocstrings-1.0.3.tar.gz", hash = "sha256:ab670f55040722b49bb45865b2e93b824450fb4aef638b00d7acb493a9020434", size = 100946, upload-time = "2026-02-07T14:31:40.973Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/41/1cf02e3df279d2dd846a1bf235a928254eba9006dd22b4a14caa71aed0f7/mkdocstrings-1.0.3-py3-none-any.whl", hash = "sha256:0d66d18430c2201dc7fe85134277382baaa15e6b30979f3f3bdbabd6dbdb6046", size = 35523, upload-time = "2026-02-07T14:31:39.27Z" }, +] + +[package.optional-dependencies] +python = [ + { name = "mkdocstrings-python" }, +] + +[[package]] +name = "mkdocstrings-python" +version = "2.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "griffelib" }, + { name = "mkdocs-autorefs" }, + { name = "mkdocstrings" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/29/33/c225eaf898634bdda489a6766fc35d1683c640bffe0e0acd10646b13536d/mkdocstrings_python-2.0.3.tar.gz", hash = "sha256:c518632751cc869439b31c9d3177678ad2bfa5c21b79b863956ad68fc92c13b8", size = 199083, upload-time = "2026-02-20T10:38:36.368Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/28/79f0f8de97cce916d5ae88a7bee1ad724855e83e6019c0b4d5b3fabc80f3/mkdocstrings_python-2.0.3-py3-none-any.whl", hash = "sha256:0b83513478bdfd803ff05aa43e9b1fca9dd22bcd9471f09ca6257f009bc5ee12", size = 104779, upload-time = "2026-02-20T10:38:34.517Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "paginate" +version = "0.5.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252, upload-time = "2024-08-25T14:17:24.139Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" }, +] + +[[package]] +name = "pathspec" +version = "1.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, +] + +[[package]] +name = "pillow" +version = "12.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/aa/d0b28e1c811cd4d5f5c2bfe2e022292bd255ae5744a3b9ac7d6c8f72dd75/pillow-12.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:a4e8f36e677d3336f35089648c8955c51c6d386a13cf6ee9c189c5f5bd713a9f", size = 5354355, upload-time = "2026-04-01T14:42:15.402Z" }, + { url = "https://files.pythonhosted.org/packages/27/8e/1d5b39b8ae2bd7650d0c7b6abb9602d16043ead9ebbfef4bc4047454da2a/pillow-12.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e589959f10d9824d39b350472b92f0ce3b443c0a3442ebf41c40cb8361c5b97", size = 4695871, upload-time = "2026-04-01T14:42:18.234Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c5/dcb7a6ca6b7d3be41a76958e90018d56c8462166b3ef223150360850c8da/pillow-12.2.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a52edc8bfff4429aaabdf4d9ee0daadbbf8562364f940937b941f87a4290f5ff", size = 6269734, upload-time = "2026-04-01T14:42:20.608Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f1/aa1bb13b2f4eba914e9637893c73f2af8e48d7d4023b9d3750d4c5eb2d0c/pillow-12.2.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:975385f4776fafde056abb318f612ef6285b10a1f12b8570f3647ad0d74b48ec", size = 8076080, upload-time = "2026-04-01T14:42:23.095Z" }, + { url = "https://files.pythonhosted.org/packages/a1/2a/8c79d6a53169937784604a8ae8d77e45888c41537f7f6f65ed1f407fe66d/pillow-12.2.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd9c0c7a0c681a347b3194c500cb1e6ca9cab053ea4d82a5cf45b6b754560136", size = 6382236, upload-time = "2026-04-01T14:42:25.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/42/bbcb6051030e1e421d103ce7a8ecadf837aa2f39b8f82ef1a8d37c3d4ebc/pillow-12.2.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:88d387ff40b3ff7c274947ed3125dedf5262ec6919d83946753b5f3d7c67ea4c", size = 7070220, upload-time = "2026-04-01T14:42:28.68Z" }, + { url = "https://files.pythonhosted.org/packages/3f/e1/c2a7d6dd8cfa6b231227da096fd2d58754bab3603b9d73bf609d3c18b64f/pillow-12.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:51c4167c34b0d8ba05b547a3bb23578d0ba17b80a5593f93bd8ecb123dd336a3", size = 6493124, upload-time = "2026-04-01T14:42:31.579Z" }, + { url = "https://files.pythonhosted.org/packages/5f/41/7c8617da5d32e1d2f026e509484fdb6f3ad7efaef1749a0c1928adbb099e/pillow-12.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:34c0d99ecccea270c04882cb3b86e7b57296079c9a4aff88cb3b33563d95afaa", size = 7194324, upload-time = "2026-04-01T14:42:34.615Z" }, + { url = "https://files.pythonhosted.org/packages/2d/de/a777627e19fd6d62f84070ee1521adde5eeda4855b5cf60fe0b149118bca/pillow-12.2.0-cp310-cp310-win32.whl", hash = "sha256:b85f66ae9eb53e860a873b858b789217ba505e5e405a24b85c0464822fe88032", size = 6376363, upload-time = "2026-04-01T14:42:37.19Z" }, + { url = "https://files.pythonhosted.org/packages/e7/34/fc4cb5204896465842767b96d250c08410f01f2f28afc43b257de842eed5/pillow-12.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:673aa32138f3e7531ccdbca7b3901dba9b70940a19ccecc6a37c77d5fdeb05b5", size = 7083523, upload-time = "2026-04-01T14:42:39.62Z" }, + { url = "https://files.pythonhosted.org/packages/2d/a0/32852d36bc7709f14dc3f64f929a275e958ad8c19a6deba9610d458e28b3/pillow-12.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:3e080565d8d7c671db5802eedfb438e5565ffa40115216eabb8cd52d0ecce024", size = 2463318, upload-time = "2026-04-01T14:42:42.063Z" }, + { url = "https://files.pythonhosted.org/packages/68/e1/748f5663efe6edcfc4e74b2b93edfb9b8b99b67f21a854c3ae416500a2d9/pillow-12.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:8be29e59487a79f173507c30ddf57e733a357f67881430449bb32614075a40ab", size = 5354347, upload-time = "2026-04-01T14:42:44.255Z" }, + { url = "https://files.pythonhosted.org/packages/47/a1/d5ff69e747374c33a3b53b9f98cca7889fce1fd03d79cdc4e1bccc6c5a87/pillow-12.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71cde9a1e1551df7d34a25462fc60325e8a11a82cc2e2f54578e5e9a1e153d65", size = 4695873, upload-time = "2026-04-01T14:42:46.452Z" }, + { url = "https://files.pythonhosted.org/packages/df/21/e3fbdf54408a973c7f7f89a23b2cb97a7ef30c61ab4142af31eee6aebc88/pillow-12.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f490f9368b6fc026f021db16d7ec2fbf7d89e2edb42e8ec09d2c60505f5729c7", size = 6280168, upload-time = "2026-04-01T14:42:49.228Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f1/00b7278c7dd52b17ad4329153748f87b6756ec195ff786c2bdf12518337d/pillow-12.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8bd7903a5f2a4545f6fd5935c90058b89d30045568985a71c79f5fd6edf9b91e", size = 8088188, upload-time = "2026-04-01T14:42:51.735Z" }, + { url = "https://files.pythonhosted.org/packages/ad/cf/220a5994ef1b10e70e85748b75649d77d506499352be135a4989c957b701/pillow-12.2.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3997232e10d2920a68d25191392e3a4487d8183039e1c74c2297f00ed1c50705", size = 6394401, upload-time = "2026-04-01T14:42:54.343Z" }, + { url = "https://files.pythonhosted.org/packages/e9/bd/e51a61b1054f09437acfbc2ff9106c30d1eb76bc1453d428399946781253/pillow-12.2.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e74473c875d78b8e9d5da2a70f7099549f9eb37ded4e2f6a463e60125bccd176", size = 7079655, upload-time = "2026-04-01T14:42:56.954Z" }, + { url = "https://files.pythonhosted.org/packages/6b/3d/45132c57d5fb4b5744567c3817026480ac7fc3ce5d4c47902bc0e7f6f853/pillow-12.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:56a3f9c60a13133a98ecff6197af34d7824de9b7b38c3654861a725c970c197b", size = 6503105, upload-time = "2026-04-01T14:42:59.847Z" }, + { url = "https://files.pythonhosted.org/packages/7d/2e/9df2fc1e82097b1df3dce58dc43286aa01068e918c07574711fcc53e6fb4/pillow-12.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:90e6f81de50ad6b534cab6e5aef77ff6e37722b2f5d908686f4a5c9eba17a909", size = 7203402, upload-time = "2026-04-01T14:43:02.664Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2e/2941e42858ebb67e50ae741473de81c2984e6eff7b397017623c676e2e8d/pillow-12.2.0-cp311-cp311-win32.whl", hash = "sha256:8c984051042858021a54926eb597d6ee3012393ce9c181814115df4c60b9a808", size = 6378149, upload-time = "2026-04-01T14:43:05.274Z" }, + { url = "https://files.pythonhosted.org/packages/69/42/836b6f3cd7f3e5fa10a1f1a5420447c17966044c8fbf589cc0452d5502db/pillow-12.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e6b2a0c538fc200b38ff9eb6628228b77908c319a005815f2dde585a0664b60", size = 7082626, upload-time = "2026-04-01T14:43:08.557Z" }, + { url = "https://files.pythonhosted.org/packages/c2/88/549194b5d6f1f494b485e493edc6693c0a16f4ada488e5bd974ed1f42fad/pillow-12.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:9a8a34cc89c67a65ea7437ce257cea81a9dad65b29805f3ecee8c8fe8ff25ffe", size = 2463531, upload-time = "2026-04-01T14:43:10.743Z" }, + { url = "https://files.pythonhosted.org/packages/58/be/7482c8a5ebebbc6470b3eb791812fff7d5e0216c2be3827b30b8bb6603ed/pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5", size = 5308279, upload-time = "2026-04-01T14:43:13.246Z" }, + { url = "https://files.pythonhosted.org/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421", size = 4695490, upload-time = "2026-04-01T14:43:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/de/af/4e8e6869cbed569d43c416fad3dc4ecb944cb5d9492defaed89ddd6fe871/pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987", size = 6284462, upload-time = "2026-04-01T14:43:18.268Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9e/c05e19657fd57841e476be1ab46c4d501bffbadbafdc31a6d665f8b737b6/pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76", size = 8094744, upload-time = "2026-04-01T14:43:20.716Z" }, + { url = "https://files.pythonhosted.org/packages/2b/54/1789c455ed10176066b6e7e6da1b01e50e36f94ba584dc68d9eebfe9156d/pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005", size = 6398371, upload-time = "2026-04-01T14:43:23.443Z" }, + { url = "https://files.pythonhosted.org/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780", size = 7087215, upload-time = "2026-04-01T14:43:26.758Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f8/2f6825e441d5b1959d2ca5adec984210f1ec086435b0ed5f52c19b3b8a6e/pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5", size = 6509783, upload-time = "2026-04-01T14:43:29.56Z" }, + { url = "https://files.pythonhosted.org/packages/67/f9/029a27095ad20f854f9dba026b3ea6428548316e057e6fc3545409e86651/pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5", size = 7212112, upload-time = "2026-04-01T14:43:32.091Z" }, + { url = "https://files.pythonhosted.org/packages/be/42/025cfe05d1be22dbfdb4f264fe9de1ccda83f66e4fc3aac94748e784af04/pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940", size = 6378489, upload-time = "2026-04-01T14:43:34.601Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5", size = 7084129, upload-time = "2026-04-01T14:43:37.213Z" }, + { url = "https://files.pythonhosted.org/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414", size = 2463612, upload-time = "2026-04-01T14:43:39.421Z" }, + { url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" }, + { url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" }, + { url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" }, + { url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094, upload-time = "2026-04-01T14:43:48.438Z" }, + { url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402, upload-time = "2026-04-01T14:43:51.292Z" }, + { url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005, upload-time = "2026-04-01T14:43:54.242Z" }, + { url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669, upload-time = "2026-04-01T14:43:57.335Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194, upload-time = "2026-04-01T14:43:59.864Z" }, + { url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423, upload-time = "2026-04-01T14:44:02.74Z" }, + { url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667, upload-time = "2026-04-01T14:44:05.381Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580, upload-time = "2026-04-01T14:44:08.39Z" }, + { url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896, upload-time = "2026-04-01T14:44:11.197Z" }, + { url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266, upload-time = "2026-04-01T14:44:13.947Z" }, + { url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508, upload-time = "2026-04-01T14:44:16.312Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927, upload-time = "2026-04-01T14:44:18.89Z" }, + { url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624, upload-time = "2026-04-01T14:44:21.115Z" }, + { url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252, upload-time = "2026-04-01T14:44:23.663Z" }, + { url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550, upload-time = "2026-04-01T14:44:26.772Z" }, + { url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114, upload-time = "2026-04-01T14:44:29.615Z" }, + { url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667, upload-time = "2026-04-01T14:44:32.773Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966, upload-time = "2026-04-01T14:44:35.252Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241, upload-time = "2026-04-01T14:44:37.875Z" }, + { url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" }, + { url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" }, + { url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" }, + { url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" }, + { url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" }, + { url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" }, + { url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" }, + { url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" }, + { url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" }, + { url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" }, + { url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" }, + { url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" }, + { url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" }, + { url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" }, + { url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" }, + { url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" }, + { url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" }, + { url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b7/2437044fb910f499610356d1352e3423753c98e34f915252aafecc64889f/pillow-12.2.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0538bd5e05efec03ae613fd89c4ce0368ecd2ba239cc25b9f9be7ed426b0af1f", size = 5273969, upload-time = "2026-04-01T14:45:55.538Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f4/8316e31de11b780f4ac08ef3654a75555e624a98db1056ecb2122d008d5a/pillow-12.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:394167b21da716608eac917c60aa9b969421b5dcbbe02ae7f013e7b85811c69d", size = 4659674, upload-time = "2026-04-01T14:45:58.093Z" }, + { url = "https://files.pythonhosted.org/packages/d4/37/664fca7201f8bb2aa1d20e2c3d5564a62e6ae5111741966c8319ca802361/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5d04bfa02cc2d23b497d1e90a0f927070043f6cbf303e738300532379a4b4e0f", size = 5288479, upload-time = "2026-04-01T14:46:01.141Z" }, + { url = "https://files.pythonhosted.org/packages/49/62/5b0ed78fce87346be7a5cfcfaaad91f6a1f98c26f86bdbafa2066c647ef6/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0c838a5125cee37e68edec915651521191cef1e6aa336b855f495766e77a366e", size = 7032230, upload-time = "2026-04-01T14:46:03.874Z" }, + { url = "https://files.pythonhosted.org/packages/c3/28/ec0fc38107fc32536908034e990c47914c57cd7c5a3ece4d8d8f7ffd7e27/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a6c9fa44005fa37a91ebfc95d081e8079757d2e904b27103f4f5fa6f0bf78c0", size = 5355404, upload-time = "2026-04-01T14:46:06.33Z" }, + { url = "https://files.pythonhosted.org/packages/5e/8b/51b0eddcfa2180d60e41f06bd6d0a62202b20b59c68f5a132e615b75aecf/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25373b66e0dd5905ed63fa3cae13c82fbddf3079f2c8bf15c6fb6a35586324c1", size = 6002215, upload-time = "2026-04-01T14:46:08.83Z" }, + { url = "https://files.pythonhosted.org/packages/bc/60/5382c03e1970de634027cee8e1b7d39776b778b81812aaf45b694dfe9e28/pillow-12.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e", size = 7080946, upload-time = "2026-04-01T14:46:11.734Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, +] + +[[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 = "prek" +version = "0.3.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/ff/5b7a2a9c4fa3dd2ffc8b13a9ec22aa550deda5b39ab273f8e02863b12642/prek-0.3.9.tar.gz", hash = "sha256:f82b92d81f42f1f90a47f5fbbf492373e25ef1f790080215b2722dd6da66510e", size = 423801, upload-time = "2026-04-13T12:30:38.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/08/c11a6b7834b461223763b6b1552f32c9199393685d52d555de621e900ee7/prek-0.3.9-py3-none-linux_armv6l.whl", hash = "sha256:3ed793d51bfaa27bddb64d525d7acb77a7c8644f549412d82252e3eb0b88aad8", size = 5337784, upload-time = "2026-04-13T12:30:46.044Z" }, + { url = "https://files.pythonhosted.org/packages/15/d9/974b02832a645c6411069c713e3191ce807f9962006da108e4727efd2fa1/prek-0.3.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:399c58400c0bd0b82a93a3c09dc1bfd88d8d0cfb242d414d2ed247187b06ead1", size = 5713864, upload-time = "2026-04-13T12:30:27.007Z" }, + { url = "https://files.pythonhosted.org/packages/40/e1/4ed14bef15eb30039a75177b0807ac007095a5a110284706ccf900a8d512/prek-0.3.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e2ea1ffb124e92f081b8e2ca5b5a623a733efb3be0c5b1f4b7ffe2ee17d1f20c", size = 5290437, upload-time = "2026-04-13T12:30:30.658Z" }, + { url = "https://files.pythonhosted.org/packages/67/80/d5c3015e9da161dede566bfeef41f098f92470613157daa4f7377ab08d58/prek-0.3.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:aaf639f95b7301639298311d8d44aad0d0b4864e9736083ad3c71ce9765d37ab", size = 5536208, upload-time = "2026-04-13T12:30:47.964Z" }, + { url = "https://files.pythonhosted.org/packages/c8/54/8cdc5eb1018437d7828740defd322e7a96459c02fc8961160c4120325313/prek-0.3.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff104863b187fa443ea8451ca55d51e2c6e94f99f00d88784b5c3c4c623f1ebe", size = 5251785, upload-time = "2026-04-13T12:30:39.78Z" }, + { url = "https://files.pythonhosted.org/packages/bd/e2/a5fc35a0fd3167224a000ca1b6235ecbdea0ac77e24af5979a75b0e6b5a4/prek-0.3.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:039ecaf87c63a3e67cca645ebd5bc5eb6aafa6c9d929e9a27b2921e7849d7ef9", size = 5668548, upload-time = "2026-04-13T12:30:24.914Z" }, + { url = "https://files.pythonhosted.org/packages/09/e8/a189ee79f401c259f66f8af587f899d4d5bfb04e0ca371bfd01e49871007/prek-0.3.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3bde2a3d045705095983c7f78ba04f72a7565fe1c2b4e85f5628502a254754ff", size = 6660927, upload-time = "2026-04-13T12:30:44.495Z" }, + { url = "https://files.pythonhosted.org/packages/a4/5a/54117316e98ff62a14911ad1488a3a0945530242a2ce3e92f7a40b6ccc02/prek-0.3.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28a0960a21543563e2c8e19aaad176cc8423a87aac3c914d0f313030d7a9244a", size = 5932244, upload-time = "2026-04-13T12:30:49.532Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f9/e88d4361f59be7adeeb3a8a3819d69d286d86fe6f7606840af6734362675/prek-0.3.9-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:0dfb5d5171d7523271909246ee306b4dc3d5b63752e7dd7c7e8a8908fc9490d1", size = 5542139, upload-time = "2026-04-13T12:30:41.266Z" }, + { url = "https://files.pythonhosted.org/packages/11/1f/204837115087bb8d063bda754a7fe975428c5d5b6548c30dd749f8ab85d4/prek-0.3.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:82b791bd36c1430c84d3ae7220a85152babc7eaf00f70adcb961bd594e756ba3", size = 5392519, upload-time = "2026-04-13T12:30:32.603Z" }, + { url = "https://files.pythonhosted.org/packages/bd/00/de57b5795e670b6d38e7eda6d9ac6fd6d757ca22f725e5054b042104cd53/prek-0.3.9-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:6eac6d2f736b041118f053a1487abed468a70dd85a8688eaf87bb42d3dcecf20", size = 5222780, upload-time = "2026-04-13T12:30:36.576Z" }, + { url = "https://files.pythonhosted.org/packages/f5/14/0bc055c305d92980b151f2ec00c14d28fe94c6d51180ca07fded28771cbf/prek-0.3.9-py3-none-musllinux_1_1_i686.whl", hash = "sha256:5517e46e761367a3759b3168eabc120840ffbca9dfbc53187167298a98f87dc4", size = 5524310, upload-time = "2026-04-13T12:30:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d1/eebc2b69be0de36cd84adbe0a0710f4deb468a90e30525be027d6db02d54/prek-0.3.9-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:92024778cf78683ca32687bb249ab6a7d5c33887b5ee1d1a9f6d0c14228f4cf3", size = 6043751, upload-time = "2026-04-13T12:30:29.101Z" }, + { url = "https://files.pythonhosted.org/packages/46/cb/be98c04e702cbc0b0328cd745ff4634ace69ad5a84461bde36f88a7be873/prek-0.3.9-py3-none-win32.whl", hash = "sha256:7f89c55e5f480f5d073769e319924ad69d4bf9f98c5cb46a83082e26e634c958", size = 5045940, upload-time = "2026-04-13T12:30:42.882Z" }, + { url = "https://files.pythonhosted.org/packages/a6/b6/b51771d69f6282e34edeb73f23d956da34f2cabbb5ba16ba175cc0a056f9/prek-0.3.9-py3-none-win_amd64.whl", hash = "sha256:7722f3372eaa83b147e70a43cb7b9fe2128c13d0c78d8a1cdbf2a8ec2ee071eb", size = 5435204, upload-time = "2026-04-13T12:30:51.482Z" }, + { url = "https://files.pythonhosted.org/packages/30/8a/f8a87c15b095460eccd67c8d89a086b7a37aac8d363f89544b8ce6ec653d/prek-0.3.9-py3-none-win_arm64.whl", hash = "sha256:0bced6278d6cc8a4b46048979e36bc9da034611dc8facd77ab123177b833a929", size = 5279552, upload-time = "2026-04-13T12:30:53.011Z" }, +] + +[[package]] +name = "pycparser" +version = "2.23" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, +] + +[[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 = "pydantic-settings" +version = "2.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/6d/fffca34caecc4a3f97bda81b2098da5e8ab7efc9a66e819074a11955d87e/pydantic_settings-2.13.1.tar.gz", hash = "sha256:b4c11847b15237fb0171e1462bf540e294affb9b86db4d9aa5c01730bdbe4025", size = 223826, upload-time = "2026-02-19T13:45:08.055Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/4b/ccc026168948fec4f7555b9164c724cf4125eac006e176541483d2c959be/pydantic_settings-2.13.1-py3-none-any.whl", hash = "sha256:d56fd801823dbeae7f0975e1f8c8e25c258eb75d278ea7abb5d9cebb01b56237", size = 58929, upload-time = "2026-02-19T13:45:06.034Z" }, +] + +[[package]] +name = "pygithub" +version = "2.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyjwt", extra = ["crypto"] }, + { name = "pynacl" }, + { name = "requests" }, + { name = "typing-extensions" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/9a/44f918e9be12e49cb8b053f09d5d0733b74df52bf4dabc570da1c3ecd9f6/pygithub-2.9.0.tar.gz", hash = "sha256:a26abda1222febba31238682634cad11d8b966137ed6cc3c5e445b29a11cb0a4", size = 2592289, upload-time = "2026-03-22T21:14:39.053Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/de/72e02bc7674e161b155a4b5a03b2347129d0626115bc97ba5bad5070cac9/pygithub-2.9.0-py3-none-any.whl", hash = "sha256:5e2b260ce327bffce9b00f447b65953ef7078ffe93e5a5425624a3075483927c", size = 449653, upload-time = "2026-03-22T21:14:37.726Z" }, +] + +[[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 = "pyjwt" +version = "2.10.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785, upload-time = "2024-11-28T03:43:29.933Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + +[[package]] +name = "pymdown-extensions" +version = "10.19.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/2d/9f30cee56d4d6d222430d401e85b0a6a1ae229819362f5786943d1a8c03b/pymdown_extensions-10.19.1.tar.gz", hash = "sha256:4969c691009a389fb1f9712dd8e7bd70dcc418d15a0faf70acb5117d022f7de8", size = 847839, upload-time = "2025-12-14T17:25:24.42Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/35/b763e8fbcd51968329b9adc52d188fc97859f85f2ee15fe9f379987d99c5/pymdown_extensions-10.19.1-py3-none-any.whl", hash = "sha256:e8698a66055b1dc0dca2a7f2c9d0ea6f5faa7834a9c432e3535ab96c0c4e509b", size = 266693, upload-time = "2025-12-14T17:25:22.999Z" }, +] + +[[package]] +name = "pynacl" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/46/aeca065d227e2265125aea590c9c47fbf5786128c9400ee0eb7c88931f06/pynacl-1.6.1.tar.gz", hash = "sha256:8d361dac0309f2b6ad33b349a56cd163c98430d409fa503b10b70b3ad66eaa1d", size = 3506616, upload-time = "2025-11-10T16:02:13.195Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/d6/4b2dca33ed512de8f54e5c6074aa06eaeb225bfbcd9b16f33a414389d6bd/pynacl-1.6.1-cp314-cp314t-macosx_10_10_universal2.whl", hash = "sha256:7d7c09749450c385301a3c20dca967a525152ae4608c0a096fe8464bfc3df93d", size = 389109, upload-time = "2025-11-10T16:01:28.79Z" }, + { url = "https://files.pythonhosted.org/packages/3c/30/e8dbb8ff4fa2559bbbb2187ba0d0d7faf728d17cb8396ecf4a898b22d3da/pynacl-1.6.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fc734c1696ffd49b40f7c1779c89ba908157c57345cf626be2e0719488a076d3", size = 808254, upload-time = "2025-11-10T16:01:37.839Z" }, + { url = "https://files.pythonhosted.org/packages/44/f9/f5449c652f31da00249638dbab065ad4969c635119094b79b17c3a4da2ab/pynacl-1.6.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3cd787ec1f5c155dc8ecf39b1333cfef41415dc96d392f1ce288b4fe970df489", size = 1407365, upload-time = "2025-11-10T16:01:40.454Z" }, + { url = "https://files.pythonhosted.org/packages/eb/2f/9aa5605f473b712065c0a193ebf4ad4725d7a245533f0cd7e5dcdbc78f35/pynacl-1.6.1-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b35d93ab2df03ecb3aa506be0d3c73609a51449ae0855c2e89c7ed44abde40b", size = 843842, upload-time = "2025-11-10T16:01:30.524Z" }, + { url = "https://files.pythonhosted.org/packages/32/8d/748f0f6956e207453da8f5f21a70885fbbb2e060d5c9d78e0a4a06781451/pynacl-1.6.1-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dece79aecbb8f4640a1adbb81e4aa3bfb0e98e99834884a80eb3f33c7c30e708", size = 1445559, upload-time = "2025-11-10T16:01:33.663Z" }, + { url = "https://files.pythonhosted.org/packages/78/d0/2387f0dcb0e9816f38373999e48db4728ed724d31accdd4e737473319d35/pynacl-1.6.1-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:c2228054f04bf32d558fb89bb99f163a8197d5a9bf4efa13069a7fa8d4b93fc3", size = 825791, upload-time = "2025-11-10T16:01:34.823Z" }, + { url = "https://files.pythonhosted.org/packages/18/3d/ef6fb7eb072aaf15f280bc66f26ab97e7fc9efa50fb1927683013ef47473/pynacl-1.6.1-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:2b12f1b97346f177affcdfdc78875ff42637cb40dcf79484a97dae3448083a78", size = 1410843, upload-time = "2025-11-10T16:01:36.401Z" }, + { url = "https://files.pythonhosted.org/packages/e3/fb/23824a017526850ee7d8a1cc4cd1e3e5082800522c10832edbbca8619537/pynacl-1.6.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e735c3a1bdfde3834503baf1a6d74d4a143920281cb724ba29fb84c9f49b9c48", size = 801140, upload-time = "2025-11-10T16:01:42.013Z" }, + { url = "https://files.pythonhosted.org/packages/5d/d1/ebc6b182cb98603a35635b727d62f094bc201bf610f97a3bb6357fe688d2/pynacl-1.6.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3384a454adf5d716a9fadcb5eb2e3e72cd49302d1374a60edc531c9957a9b014", size = 1371966, upload-time = "2025-11-10T16:01:43.297Z" }, + { url = "https://files.pythonhosted.org/packages/64/f4/c9d7b6f02924b1f31db546c7bd2a83a2421c6b4a8e6a2e53425c9f2802e0/pynacl-1.6.1-cp314-cp314t-win32.whl", hash = "sha256:d8615ee34d01c8e0ab3f302dcdd7b32e2bcf698ba5f4809e7cc407c8cdea7717", size = 230482, upload-time = "2025-11-10T16:01:47.688Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2c/942477957fba22da7bf99131850e5ebdff66623418ab48964e78a7a8293e/pynacl-1.6.1-cp314-cp314t-win_amd64.whl", hash = "sha256:5f5b35c1a266f8a9ad22525049280a600b19edd1f785bccd01ae838437dcf935", size = 243232, upload-time = "2025-11-10T16:01:45.208Z" }, + { url = "https://files.pythonhosted.org/packages/7a/0c/bdbc0d04a53b96a765ab03aa2cf9a76ad8653d70bf1665459b9a0dedaa1c/pynacl-1.6.1-cp314-cp314t-win_arm64.whl", hash = "sha256:d984c91fe3494793b2a1fb1e91429539c6c28e9ec8209d26d25041ec599ccf63", size = 187907, upload-time = "2025-11-10T16:01:46.328Z" }, + { url = "https://files.pythonhosted.org/packages/49/41/3cfb3b4f3519f6ff62bf71bf1722547644bcfb1b05b8fdbdc300249ba113/pynacl-1.6.1-cp38-abi3-macosx_10_10_universal2.whl", hash = "sha256:a6f9fd6d6639b1e81115c7f8ff16b8dedba1e8098d2756275d63d208b0e32021", size = 387591, upload-time = "2025-11-10T16:01:49.1Z" }, + { url = "https://files.pythonhosted.org/packages/18/21/b8a6563637799f617a3960f659513eccb3fcc655d5fc2be6e9dc6416826f/pynacl-1.6.1-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e49a3f3d0da9f79c1bec2aa013261ab9fa651c7da045d376bd306cf7c1792993", size = 798866, upload-time = "2025-11-10T16:01:55.688Z" }, + { url = "https://files.pythonhosted.org/packages/e8/6c/dc38033bc3ea461e05ae8f15a81e0e67ab9a01861d352ae971c99de23e7c/pynacl-1.6.1-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7713f8977b5d25f54a811ec9efa2738ac592e846dd6e8a4d3f7578346a841078", size = 1398001, upload-time = "2025-11-10T16:01:57.101Z" }, + { url = "https://files.pythonhosted.org/packages/9f/05/3ec0796a9917100a62c5073b20c4bce7bf0fea49e99b7906d1699cc7b61b/pynacl-1.6.1-cp38-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a3becafc1ee2e5ea7f9abc642f56b82dcf5be69b961e782a96ea52b55d8a9fc", size = 834024, upload-time = "2025-11-10T16:01:50.228Z" }, + { url = "https://files.pythonhosted.org/packages/f0/b7/ae9982be0f344f58d9c64a1c25d1f0125c79201634efe3c87305ac7cb3e3/pynacl-1.6.1-cp38-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4ce50d19f1566c391fedc8dc2f2f5be265ae214112ebe55315e41d1f36a7f0a9", size = 1436766, upload-time = "2025-11-10T16:01:51.886Z" }, + { url = "https://files.pythonhosted.org/packages/b4/51/b2ccbf89cf3025a02e044dd68a365cad593ebf70f532299f2c047d2b7714/pynacl-1.6.1-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:543f869140f67d42b9b8d47f922552d7a967e6c116aad028c9bfc5f3f3b3a7b7", size = 817275, upload-time = "2025-11-10T16:01:53.351Z" }, + { url = "https://files.pythonhosted.org/packages/a8/6c/dd9ee8214edf63ac563b08a9b30f98d116942b621d39a751ac3256694536/pynacl-1.6.1-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a2bb472458c7ca959aeeff8401b8efef329b0fc44a89d3775cffe8fad3398ad8", size = 1401891, upload-time = "2025-11-10T16:01:54.587Z" }, + { url = "https://files.pythonhosted.org/packages/0f/c1/97d3e1c83772d78ee1db3053fd674bc6c524afbace2bfe8d419fd55d7ed1/pynacl-1.6.1-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:3206fa98737fdc66d59b8782cecc3d37d30aeec4593d1c8c145825a345bba0f0", size = 772291, upload-time = "2025-11-10T16:01:58.111Z" }, + { url = "https://files.pythonhosted.org/packages/4d/ca/691ff2fe12f3bb3e43e8e8df4b806f6384593d427f635104d337b8e00291/pynacl-1.6.1-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:53543b4f3d8acb344f75fd4d49f75e6572fce139f4bfb4815a9282296ff9f4c0", size = 1370839, upload-time = "2025-11-10T16:01:59.252Z" }, + { url = "https://files.pythonhosted.org/packages/30/27/06fe5389d30391fce006442246062cc35773c84fbcad0209fbbf5e173734/pynacl-1.6.1-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:319de653ef84c4f04e045eb250e6101d23132372b0a61a7acf91bac0fda8e58c", size = 791371, upload-time = "2025-11-10T16:02:01.075Z" }, + { url = "https://files.pythonhosted.org/packages/2c/7a/e2bde8c9d39074a5aa046c7d7953401608d1f16f71e237f4bef3fb9d7e49/pynacl-1.6.1-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:262a8de6bba4aee8a66f5edf62c214b06647461c9b6b641f8cd0cb1e3b3196fe", size = 1363031, upload-time = "2025-11-10T16:02:02.656Z" }, + { url = "https://files.pythonhosted.org/packages/dd/b6/63fd77264dae1087770a1bb414bc604470f58fbc21d83822fc9c76248076/pynacl-1.6.1-cp38-abi3-win32.whl", hash = "sha256:9fd1a4eb03caf8a2fe27b515a998d26923adb9ddb68db78e35ca2875a3830dde", size = 226585, upload-time = "2025-11-10T16:02:07.116Z" }, + { url = "https://files.pythonhosted.org/packages/12/c8/b419180f3fdb72ab4d45e1d88580761c267c7ca6eda9a20dcbcba254efe6/pynacl-1.6.1-cp38-abi3-win_amd64.whl", hash = "sha256:a569a4069a7855f963940040f35e87d8bc084cb2d6347428d5ad20550a0a1a21", size = 238923, upload-time = "2025-11-10T16:02:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/35/76/c34426d532e4dce7ff36e4d92cb20f4cbbd94b619964b93d24e8f5b5510f/pynacl-1.6.1-cp38-abi3-win_arm64.whl", hash = "sha256:5953e8b8cfadb10889a6e7bd0f53041a745d1b3d30111386a1bb37af171e6daf", size = 183970, upload-time = "2025-11-10T16:02:05.786Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.3" +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/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, +] + +[[package]] +name = "pytokens" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/34/b4e015b99031667a7b960f888889c5bd34ef585c85e1cb56a594b92836ac/pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a", size = 23015, upload-time = "2026-01-30T01:03:45.924Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/24/f206113e05cb8ef51b3850e7ef88f20da6f4bf932190ceb48bd3da103e10/pytokens-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a44ed93ea23415c54f3face3b65ef2b844d96aeb3455b8a69b3df6beab6acc5", size = 161522, upload-time = "2026-01-30T01:02:50.393Z" }, + { url = "https://files.pythonhosted.org/packages/d4/e9/06a6bf1b90c2ed81a9c7d2544232fe5d2891d1cd480e8a1809ca354a8eb2/pytokens-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:add8bf86b71a5d9fb5b89f023a80b791e04fba57960aa790cc6125f7f1d39dfe", size = 246945, upload-time = "2026-01-30T01:02:52.399Z" }, + { url = "https://files.pythonhosted.org/packages/69/66/f6fb1007a4c3d8b682d5d65b7c1fb33257587a5f782647091e3408abe0b8/pytokens-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:670d286910b531c7b7e3c0b453fd8156f250adb140146d234a82219459b9640c", size = 259525, upload-time = "2026-01-30T01:02:53.737Z" }, + { url = "https://files.pythonhosted.org/packages/04/92/086f89b4d622a18418bac74ab5db7f68cf0c21cf7cc92de6c7b919d76c88/pytokens-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4e691d7f5186bd2842c14813f79f8884bb03f5995f0575272009982c5ac6c0f7", size = 262693, upload-time = "2026-01-30T01:02:54.871Z" }, + { url = "https://files.pythonhosted.org/packages/b4/7b/8b31c347cf94a3f900bdde750b2e9131575a61fdb620d3d3c75832262137/pytokens-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:27b83ad28825978742beef057bfe406ad6ed524b2d28c252c5de7b4a6dd48fa2", size = 103567, upload-time = "2026-01-30T01:02:56.414Z" }, + { url = "https://files.pythonhosted.org/packages/3d/92/790ebe03f07b57e53b10884c329b9a1a308648fc083a6d4a39a10a28c8fc/pytokens-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d70e77c55ae8380c91c0c18dea05951482e263982911fc7410b1ffd1dadd3440", size = 160864, upload-time = "2026-01-30T01:02:57.882Z" }, + { url = "https://files.pythonhosted.org/packages/13/25/a4f555281d975bfdd1eba731450e2fe3a95870274da73fb12c40aeae7625/pytokens-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a58d057208cb9075c144950d789511220b07636dd2e4708d5645d24de666bdc", size = 248565, upload-time = "2026-01-30T01:02:59.912Z" }, + { url = "https://files.pythonhosted.org/packages/17/50/bc0394b4ad5b1601be22fa43652173d47e4c9efbf0044c62e9a59b747c56/pytokens-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b49750419d300e2b5a3813cf229d4e5a4c728dae470bcc89867a9ad6f25a722d", size = 260824, upload-time = "2026-01-30T01:03:01.471Z" }, + { url = "https://files.pythonhosted.org/packages/4e/54/3e04f9d92a4be4fc6c80016bc396b923d2a6933ae94b5f557c939c460ee0/pytokens-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9907d61f15bf7261d7e775bd5d7ee4d2930e04424bab1972591918497623a16", size = 264075, upload-time = "2026-01-30T01:03:04.143Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1b/44b0326cb5470a4375f37988aea5d61b5cc52407143303015ebee94abfd6/pytokens-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:ee44d0f85b803321710f9239f335aafe16553b39106384cef8e6de40cb4ef2f6", size = 103323, upload-time = "2026-01-30T01:03:05.412Z" }, + { url = "https://files.pythonhosted.org/packages/41/5d/e44573011401fb82e9d51e97f1290ceb377800fb4eed650b96f4753b499c/pytokens-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:140709331e846b728475786df8aeb27d24f48cbcf7bcd449f8de75cae7a45083", size = 160663, upload-time = "2026-01-30T01:03:06.473Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e6/5bbc3019f8e6f21d09c41f8b8654536117e5e211a85d89212d59cbdab381/pytokens-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d6c4268598f762bc8e91f5dbf2ab2f61f7b95bdc07953b602db879b3c8c18e1", size = 255626, upload-time = "2026-01-30T01:03:08.177Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3c/2d5297d82286f6f3d92770289fd439956b201c0a4fc7e72efb9b2293758e/pytokens-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24afde1f53d95348b5a0eb19488661147285ca4dd7ed752bbc3e1c6242a304d1", size = 269779, upload-time = "2026-01-30T01:03:09.756Z" }, + { url = "https://files.pythonhosted.org/packages/20/01/7436e9ad693cebda0551203e0bf28f7669976c60ad07d6402098208476de/pytokens-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ad948d085ed6c16413eb5fec6b3e02fa00dc29a2534f088d3302c47eb59adf9", size = 268076, upload-time = "2026-01-30T01:03:10.957Z" }, + { url = "https://files.pythonhosted.org/packages/2e/df/533c82a3c752ba13ae7ef238b7f8cdd272cf1475f03c63ac6cf3fcfb00b6/pytokens-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:3f901fe783e06e48e8cbdc82d631fca8f118333798193e026a50ce1b3757ea68", size = 103552, upload-time = "2026-01-30T01:03:12.066Z" }, + { url = "https://files.pythonhosted.org/packages/cb/dc/08b1a080372afda3cceb4f3c0a7ba2bde9d6a5241f1edb02a22a019ee147/pytokens-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8bdb9d0ce90cbf99c525e75a2fa415144fd570a1ba987380190e8b786bc6ef9b", size = 160720, upload-time = "2026-01-30T01:03:13.843Z" }, + { url = "https://files.pythonhosted.org/packages/64/0c/41ea22205da480837a700e395507e6a24425151dfb7ead73343d6e2d7ffe/pytokens-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5502408cab1cb18e128570f8d598981c68a50d0cbd7c61312a90507cd3a1276f", size = 254204, upload-time = "2026-01-30T01:03:14.886Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d2/afe5c7f8607018beb99971489dbb846508f1b8f351fcefc225fcf4b2adc0/pytokens-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1", size = 268423, upload-time = "2026-01-30T01:03:15.936Z" }, + { url = "https://files.pythonhosted.org/packages/68/d4/00ffdbd370410c04e9591da9220a68dc1693ef7499173eb3e30d06e05ed1/pytokens-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4", size = 266859, upload-time = "2026-01-30T01:03:17.458Z" }, + { url = "https://files.pythonhosted.org/packages/a7/c9/c3161313b4ca0c601eeefabd3d3b576edaa9afdefd32da97210700e47652/pytokens-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78", size = 103520, upload-time = "2026-01-30T01:03:18.652Z" }, + { url = "https://files.pythonhosted.org/packages/8f/a7/b470f672e6fc5fee0a01d9e75005a0e617e162381974213a945fcd274843/pytokens-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321", size = 160821, upload-time = "2026-01-30T01:03:19.684Z" }, + { url = "https://files.pythonhosted.org/packages/80/98/e83a36fe8d170c911f864bfded690d2542bfcfacb9c649d11a9e6eb9dc41/pytokens-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa", size = 254263, upload-time = "2026-01-30T01:03:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/0f/95/70d7041273890f9f97a24234c00b746e8da86df462620194cef1d411ddeb/pytokens-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d", size = 268071, upload-time = "2026-01-30T01:03:21.888Z" }, + { url = "https://files.pythonhosted.org/packages/da/79/76e6d09ae19c99404656d7db9c35dfd20f2086f3eb6ecb496b5b31163bad/pytokens-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324", size = 271716, upload-time = "2026-01-30T01:03:23.633Z" }, + { url = "https://files.pythonhosted.org/packages/79/37/482e55fa1602e0a7ff012661d8c946bafdc05e480ea5a32f4f7e336d4aa9/pytokens-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9", size = 104539, upload-time = "2026-01-30T01:03:24.788Z" }, + { url = "https://files.pythonhosted.org/packages/30/e8/20e7db907c23f3d63b0be3b8a4fd1927f6da2395f5bcc7f72242bb963dfe/pytokens-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb", size = 168474, upload-time = "2026-01-30T01:03:26.428Z" }, + { url = "https://files.pythonhosted.org/packages/d6/81/88a95ee9fafdd8f5f3452107748fd04c24930d500b9aba9738f3ade642cc/pytokens-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3", size = 290473, upload-time = "2026-01-30T01:03:27.415Z" }, + { url = "https://files.pythonhosted.org/packages/cf/35/3aa899645e29b6375b4aed9f8d21df219e7c958c4c186b465e42ee0a06bf/pytokens-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975", size = 303485, upload-time = "2026-01-30T01:03:28.558Z" }, + { url = "https://files.pythonhosted.org/packages/52/a0/07907b6ff512674d9b201859f7d212298c44933633c946703a20c25e9d81/pytokens-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a", size = 306698, upload-time = "2026-01-30T01:03:29.653Z" }, + { url = "https://files.pythonhosted.org/packages/39/2a/cbbf9250020a4a8dd53ba83a46c097b69e5eb49dd14e708f496f548c6612/pytokens-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918", size = 116287, upload-time = "2026-01-30T01:03:30.912Z" }, + { url = "https://files.pythonhosted.org/packages/c6/78/397db326746f0a342855b81216ae1f0a32965deccfd7c830a2dbc66d2483/pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de", size = 13729, upload-time = "2026-01-30T01:03:45.029Z" }, +] + +[[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 = "pyyaml-env-tag" +version = "1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" }, +] + +[[package]] +name = "rcslice" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/3e/abe47d91d5340b77b003baf96fdf8966c946eb4c5a704a844b5d03e6e578/rcslice-1.1.0.tar.gz", hash = "sha256:a2ce70a60690eb63e52b722e046b334c3aaec5e900b28578f529878782ee5c6e", size = 4414, upload-time = "2018-09-27T12:44:06.601Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/96/7935186fba032312eb8a75e6503440b0e6de76c901421f791408e4debd93/rcslice-1.1.0-py3-none-any.whl", hash = "sha256:1b12fc0c0ca452e8a9fd2b56ac008162f19e250906a4290a7e7a98be3200c2a6", size = 5180, upload-time = "2018-09-27T12:44:05.197Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "rich" +version = "14.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/8d/192f3d7103816158dfd5ea50d098ef2aec19194e6cbccd4b3485bdb2eb2d/ruff-0.15.11.tar.gz", hash = "sha256:f092b21708bf0e7437ce9ada249dfe688ff9a0954fc94abab05dcea7dcd29c33", size = 4637264, upload-time = "2026-04-16T18:46:26.58Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/1e/6aca3427f751295ab011828e15e9bf452200ac74484f1db4be0197b8170b/ruff-0.15.11-py3-none-linux_armv6l.whl", hash = "sha256:e927cfff503135c558eb581a0c9792264aae9507904eb27809cdcff2f2c847b7", size = 10607943, upload-time = "2026-04-16T18:46:05.967Z" }, + { url = "https://files.pythonhosted.org/packages/e7/26/1341c262e74f36d4e84f3d6f4df0ac68cd53331a66bfc5080daa17c84c0b/ruff-0.15.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7a1b5b2938d8f890b76084d4fa843604d787a912541eae85fd7e233398bbb73e", size = 10988592, upload-time = "2026-04-16T18:46:00.742Z" }, + { url = "https://files.pythonhosted.org/packages/03/71/850b1d6ffa9564fbb6740429bad53df1094082fe515c8c1e74b6d8d05f18/ruff-0.15.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d4176f3d194afbdaee6e41b9ccb1a2c287dba8700047df474abfbe773825d1cb", size = 10338501, upload-time = "2026-04-16T18:46:03.723Z" }, + { url = "https://files.pythonhosted.org/packages/f2/11/cc1284d3e298c45a817a6aadb6c3e1d70b45c9b36d8d9cce3387b495a03a/ruff-0.15.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b17c886fb88203ced3afe7f14e8d5ae96e9d2f4ccc0ee66aa19f2c2675a27e4", size = 10670693, upload-time = "2026-04-16T18:46:41.941Z" }, + { url = "https://files.pythonhosted.org/packages/ce/9e/f8288b034ab72b371513c13f9a41d9ba3effac54e24bfb467b007daee2ca/ruff-0.15.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:49fafa220220afe7758a487b048de4c8f9f767f37dfefad46b9dd06759d003eb", size = 10416177, upload-time = "2026-04-16T18:46:21.717Z" }, + { url = "https://files.pythonhosted.org/packages/85/71/504d79abfd3d92532ba6bbe3d1c19fada03e494332a59e37c7c2dabae427/ruff-0.15.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2ab8427e74a00d93b8bda1307b1e60970d40f304af38bccb218e056c220120d", size = 11221886, upload-time = "2026-04-16T18:46:15.086Z" }, + { url = "https://files.pythonhosted.org/packages/43/5a/947e6ab7a5ad603d65b474be15a4cbc6d29832db5d762cd142e4e3a74164/ruff-0.15.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:195072c0c8e1fc8f940652073df082e37a5d9cb43b4ab1e4d0566ab8977a13b7", size = 12075183, upload-time = "2026-04-16T18:46:07.944Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a1/0b7bb6268775fdd3a0818aee8efd8f5b4e231d24dd4d528ced2534023182/ruff-0.15.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a3a0996d486af3920dec930a2e7daed4847dfc12649b537a9335585ada163e9e", size = 11516575, upload-time = "2026-04-16T18:46:31.687Z" }, + { url = "https://files.pythonhosted.org/packages/30/c3/bb5168fc4d233cc06e95f482770d0f3c87945a0cd9f614b90ea8dc2f2833/ruff-0.15.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bef2cb556d509259f1fe440bb9cd33c756222cf0a7afe90d15edf0866702431", size = 11306537, upload-time = "2026-04-16T18:46:36.988Z" }, + { url = "https://files.pythonhosted.org/packages/e4/92/4cfae6441f3967317946f3b788136eecf093729b94d6561f963ed810c82e/ruff-0.15.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:030d921a836d7d4a12cf6e8d984a88b66094ccb0e0f17ddd55067c331191bf19", size = 11296813, upload-time = "2026-04-16T18:46:24.182Z" }, + { url = "https://files.pythonhosted.org/packages/43/26/972784c5dde8313acde8ac71ba8ac65475b85db4a2352a76c9934361f9bc/ruff-0.15.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0e783b599b4577788dbbb66b9addcef87e9a8832f4ce0c19e34bf55543a2f890", size = 10633136, upload-time = "2026-04-16T18:46:39.802Z" }, + { url = "https://files.pythonhosted.org/packages/5b/53/3985a4f185020c2f367f2e08a103032e12564829742a1b417980ce1514a0/ruff-0.15.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ae90592246625ba4a34349d68ec28d4400d75182b71baa196ddb9f82db025ef5", size = 10424701, upload-time = "2026-04-16T18:46:10.381Z" }, + { url = "https://files.pythonhosted.org/packages/d3/57/bf0dfb32241b56c83bb663a826133da4bf17f682ba8c096973065f6e6a68/ruff-0.15.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1f111d62e3c983ed20e0ca2e800f8d77433a5b1161947df99a5c2a3fb60514f0", size = 10873887, upload-time = "2026-04-16T18:46:29.157Z" }, + { url = "https://files.pythonhosted.org/packages/02/05/e48076b2a57dc33ee8c7a957296f97c744ca891a8ffb4ffb1aaa3b3f517d/ruff-0.15.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:06f483d6646f59eaffba9ae30956370d3a886625f511a3108994000480621d1c", size = 11404316, upload-time = "2026-04-16T18:46:19.462Z" }, + { url = "https://files.pythonhosted.org/packages/88/27/0195d15fe7a897cbcba0904792c4b7c9fdd958456c3a17d2ea6093716a9a/ruff-0.15.11-py3-none-win32.whl", hash = "sha256:476a2aa56b7da0b73a3ee80b6b2f0e19cce544245479adde7baa65466664d5f3", size = 10655535, upload-time = "2026-04-16T18:46:12.47Z" }, + { url = "https://files.pythonhosted.org/packages/3a/5e/c927b325bd4c1d3620211a4b96f47864633199feed60fa936025ab27e090/ruff-0.15.11-py3-none-win_amd64.whl", hash = "sha256:8b6756d88d7e234fb0c98c91511aae3cd519d5e3ed271cae31b20f39cb2a12a3", size = 11779692, upload-time = "2026-04-16T18:46:17.268Z" }, + { url = "https://files.pythonhosted.org/packages/63/b6/aeadee5443e49baa2facd51131159fd6301cc4ccfc1541e4df7b021c37dd/ruff-0.15.11-py3-none-win_arm64.whl", hash = "sha256:063fed18cc1bbe0ee7393957284a6fe8b588c6a406a285af3ee3f46da2391ee4", size = 11032614, upload-time = "2026-04-16T18:46:34.487Z" }, +] + +[[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 = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "smokeshow" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "typer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/75/94/c99b76517c268ef8d5c2ff88faba5a019664bd69e4754944afa294b4f24c/smokeshow-0.5.0.tar.gz", hash = "sha256:91dcabc29ac3116bff59b4d8a7bda4ae3ccc4c70742a38cec7127b8162e4a0f6", size = 101349, upload-time = "2025-01-07T19:41:51.732Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/10/0d23e4953eb7c1e1ad848084b3115f19234f34f907658ed11bed0d826aee/smokeshow-0.5.0-py3-none-any.whl", hash = "sha256:da12a960fc7cb525efc4035a0c3c9363b6217ea7e66bc39b9ed3cd8bed6eeedc", size = 8389, upload-time = "2025-01-07T19:41:49.194Z" }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.49" +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/09/45/461788f35e0364a8da7bda51a1fe1b09762d0c32f12f63727998d85a873b/sqlalchemy-2.0.49.tar.gz", hash = "sha256:d15950a57a210e36dd4cec1aac22787e2a4d57ba9318233e2ef8b2daf9ff2d5f", size = 9898221, upload-time = "2026-04-03T16:38:11.704Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/76/f908955139842c362aa877848f42f9249642d5b69e06cee9eae5111da1bd/sqlalchemy-2.0.49-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:42e8804962f9e6f4be2cbaedc0c3718f08f60a16910fa3d86da5a1e3b1bfe60f", size = 2159321, upload-time = "2026-04-03T16:50:11.8Z" }, + { url = "https://files.pythonhosted.org/packages/24/e2/17ba0b7bfbd8de67196889b6d951de269e8a46057d92baca162889beb16d/sqlalchemy-2.0.49-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc992c6ed024c8c3c592c5fc9846a03dd68a425674900c70122c77ea16c5fb0b", size = 3238937, upload-time = "2026-04-03T16:54:45.731Z" }, + { url = "https://files.pythonhosted.org/packages/90/1e/410dd499c039deacff395eec01a9da057125fcd0c97e3badc252c6a2d6a7/sqlalchemy-2.0.49-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6eb188b84269f357669b62cb576b5b918de10fb7c728a005fa0ebb0b758adce1", size = 3237188, upload-time = "2026-04-03T16:56:53.217Z" }, + { url = "https://files.pythonhosted.org/packages/ab/06/e797a8b98a3993ac4bc785309b9b6d005457fc70238ee6cefa7c8867a92e/sqlalchemy-2.0.49-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:62557958002b69699bdb7f5137c6714ca1133f045f97b3903964f47db97ea339", size = 3190061, upload-time = "2026-04-03T16:54:47.489Z" }, + { url = "https://files.pythonhosted.org/packages/44/d3/5a9f7ef580af1031184b38235da6ac58c3b571df01c9ec061c44b2b0c5a6/sqlalchemy-2.0.49-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:da9b91bca419dc9b9267ffadde24eae9b1a6bffcd09d0a207e5e3af99a03ce0d", size = 3211477, upload-time = "2026-04-03T16:56:55.056Z" }, + { url = "https://files.pythonhosted.org/packages/69/ec/7be8c8cb35f038e963a203e4fe5a028989167cc7299927b7cf297c271e37/sqlalchemy-2.0.49-cp310-cp310-win32.whl", hash = "sha256:5e61abbec255be7b122aa461021daa7c3f310f3e743411a67079f9b3cc91ece3", size = 2119965, upload-time = "2026-04-03T17:00:50.009Z" }, + { url = "https://files.pythonhosted.org/packages/b5/31/0defb93e3a10b0cf7d1271aedd87251a08c3a597ee4f353281769b547b5a/sqlalchemy-2.0.49-cp310-cp310-win_amd64.whl", hash = "sha256:0c98c59075b890df8abfcc6ad632879540f5791c68baebacb4f833713b510e75", size = 2142935, upload-time = "2026-04-03T17:00:51.675Z" }, + { url = "https://files.pythonhosted.org/packages/60/b5/e3617cc67420f8f403efebd7b043128f94775e57e5b84e7255203390ceae/sqlalchemy-2.0.49-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c5070135e1b7409c4161133aa525419b0062088ed77c92b1da95366ec5cbebbe", size = 2159126, upload-time = "2026-04-03T16:50:13.242Z" }, + { url = "https://files.pythonhosted.org/packages/20/9b/91ca80403b17cd389622a642699e5f6564096b698e7cdcbcbb6409898bc4/sqlalchemy-2.0.49-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ac7a3e245fd0310fd31495eb61af772e637bdf7d88ee81e7f10a3f271bff014", size = 3315509, upload-time = "2026-04-03T16:54:49.332Z" }, + { url = "https://files.pythonhosted.org/packages/b1/61/0722511d98c54de95acb327824cb759e8653789af2b1944ab1cc69d32565/sqlalchemy-2.0.49-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d4e5a0ceba319942fa6b585cf82539288a61e314ef006c1209f734551ab9536", size = 3315014, upload-time = "2026-04-03T16:56:56.376Z" }, + { url = "https://files.pythonhosted.org/packages/46/55/d514a653ffeb4cebf4b54c47bec32ee28ad89d39fafba16eeed1d81dccd5/sqlalchemy-2.0.49-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3ddcb27fb39171de36e207600116ac9dfd4ae46f86c82a9bf3934043e80ebb88", size = 3267388, upload-time = "2026-04-03T16:54:51.272Z" }, + { url = "https://files.pythonhosted.org/packages/2f/16/0dcc56cb6d3335c1671a2258f5d2cb8267c9a2260e27fde53cbfb1b3540a/sqlalchemy-2.0.49-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:32fe6a41ad97302db2931f05bb91abbcc65b5ce4c675cd44b972428dd2947700", size = 3289602, upload-time = "2026-04-03T16:56:57.63Z" }, + { url = "https://files.pythonhosted.org/packages/51/6c/f8ab6fb04470a133cd80608db40aa292e6bae5f162c3a3d4ab19544a67af/sqlalchemy-2.0.49-cp311-cp311-win32.whl", hash = "sha256:46d51518d53edfbe0563662c96954dc8fcace9832332b914375f45a99b77cc9a", size = 2119044, upload-time = "2026-04-03T17:00:53.455Z" }, + { url = "https://files.pythonhosted.org/packages/c4/59/55a6d627d04b6ebb290693681d7683c7da001eddf90b60cfcc41ee907978/sqlalchemy-2.0.49-cp311-cp311-win_amd64.whl", hash = "sha256:951d4a210744813be63019f3df343bf233b7432aadf0db54c75802247330d3af", size = 2143642, upload-time = "2026-04-03T17:00:54.769Z" }, + { url = "https://files.pythonhosted.org/packages/49/b3/2de412451330756aaaa72d27131db6dde23995efe62c941184e15242a5fa/sqlalchemy-2.0.49-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4bbccb45260e4ff1b7db0be80a9025bb1e6698bdb808b83fff0000f7a90b2c0b", size = 2157681, upload-time = "2026-04-03T16:53:07.132Z" }, + { url = "https://files.pythonhosted.org/packages/50/84/b2a56e2105bd11ebf9f0b93abddd748e1a78d592819099359aa98134a8bf/sqlalchemy-2.0.49-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb37f15714ec2652d574f021d479e78cd4eb9d04396dca36568fdfffb3487982", size = 3338976, upload-time = "2026-04-03T17:07:40Z" }, + { url = "https://files.pythonhosted.org/packages/2c/fa/65fcae2ed62f84ab72cf89536c7c3217a156e71a2c111b1305ab6f0690e2/sqlalchemy-2.0.49-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3bb9ec6436a820a4c006aad1ac351f12de2f2dbdaad171692ee457a02429b672", size = 3351937, upload-time = "2026-04-03T17:12:23.374Z" }, + { url = "https://files.pythonhosted.org/packages/f8/2f/6fd118563572a7fe475925742eb6b3443b2250e346a0cc27d8d408e73773/sqlalchemy-2.0.49-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8d6efc136f44a7e8bc8088507eaabbb8c2b55b3dbb63fe102c690da0ddebe55e", size = 3281646, upload-time = "2026-04-03T17:07:41.949Z" }, + { url = "https://files.pythonhosted.org/packages/c5/d7/410f4a007c65275b9cf82354adb4bb8ba587b176d0a6ee99caa16fe638f8/sqlalchemy-2.0.49-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e06e617e3d4fd9e51d385dfe45b077a41e9d1b033a7702551e3278ac597dc750", size = 3316695, upload-time = "2026-04-03T17:12:25.642Z" }, + { url = "https://files.pythonhosted.org/packages/d9/95/81f594aa60ded13273a844539041ccf1e66c5a7bed0a8e27810a3b52d522/sqlalchemy-2.0.49-cp312-cp312-win32.whl", hash = "sha256:83101a6930332b87653886c01d1ee7e294b1fe46a07dd9a2d2b4f91bcc88eec0", size = 2117483, upload-time = "2026-04-03T17:05:40.896Z" }, + { url = "https://files.pythonhosted.org/packages/47/9e/fd90114059175cac64e4fafa9bf3ac20584384d66de40793ae2e2f26f3bb/sqlalchemy-2.0.49-cp312-cp312-win_amd64.whl", hash = "sha256:618a308215b6cececb6240b9abde545e3acdabac7ae3e1d4e666896bf5ba44b4", size = 2144494, upload-time = "2026-04-03T17:05:42.282Z" }, + { url = "https://files.pythonhosted.org/packages/ae/81/81755f50eb2478eaf2049728491d4ea4f416c1eb013338682173259efa09/sqlalchemy-2.0.49-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df2d441bacf97022e81ad047e1597552eb3f83ca8a8f1a1fdd43cd7fe3898120", size = 2154547, upload-time = "2026-04-03T16:53:08.64Z" }, + { url = "https://files.pythonhosted.org/packages/a2/bc/3494270da80811d08bcfa247404292428c4fe16294932bce5593f215cad9/sqlalchemy-2.0.49-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8e20e511dc15265fb433571391ba313e10dd8ea7e509d51686a51313b4ac01a2", size = 3280782, upload-time = "2026-04-03T17:07:43.508Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f5/038741f5e747a5f6ea3e72487211579d8cbea5eb9827a9cbd61d0108c4bd/sqlalchemy-2.0.49-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47604cb2159f8bbd5a1ab48a714557156320f20871ee64d550d8bf2683d980d3", size = 3297156, upload-time = "2026-04-03T17:12:27.697Z" }, + { url = "https://files.pythonhosted.org/packages/88/50/a6af0ff9dc954b43a65ca9b5367334e45d99684c90a3d3413fc19a02d43c/sqlalchemy-2.0.49-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:22d8798819f86720bc646ab015baff5ea4c971d68121cb36e2ebc2ee43ead2b7", size = 3228832, upload-time = "2026-04-03T17:07:45.38Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d1/5f6bdad8de0bf546fc74370939621396515e0cdb9067402d6ba1b8afbe9a/sqlalchemy-2.0.49-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9b1c058c171b739e7c330760044803099c7fff11511e3ab3573e5327116a9c33", size = 3267000, upload-time = "2026-04-03T17:12:29.657Z" }, + { url = "https://files.pythonhosted.org/packages/f7/30/ad62227b4a9819a5e1c6abff77c0f614fa7c9326e5a3bdbee90f7139382b/sqlalchemy-2.0.49-cp313-cp313-win32.whl", hash = "sha256:a143af2ea6672f2af3f44ed8f9cd020e9cc34c56f0e8db12019d5d9ecf41cb3b", size = 2115641, upload-time = "2026-04-03T17:05:43.989Z" }, + { url = "https://files.pythonhosted.org/packages/17/3a/7215b1b7d6d49dc9a87211be44562077f5f04f9bb5a59552c1c8e2d98173/sqlalchemy-2.0.49-cp313-cp313-win_amd64.whl", hash = "sha256:12b04d1db2663b421fe072d638a138460a51d5a862403295671c4f3987fb9148", size = 2141498, upload-time = "2026-04-03T17:05:45.7Z" }, + { url = "https://files.pythonhosted.org/packages/28/4b/52a0cb2687a9cd1648252bb257be5a1ba2c2ded20ba695c65756a55a15a4/sqlalchemy-2.0.49-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24bd94bb301ec672d8f0623eba9226cc90d775d25a0c92b5f8e4965d7f3a1518", size = 3560807, upload-time = "2026-04-03T16:58:31.666Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d8/fda95459204877eed0458550d6c7c64c98cc50c2d8d618026737de9ed41a/sqlalchemy-2.0.49-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a51d3db74ba489266ef55c7a4534eb0b8db9a326553df481c11e5d7660c8364d", size = 3527481, upload-time = "2026-04-03T17:06:00.155Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0a/2aac8b78ac6487240cf7afef8f203ca783e8796002dc0cf65c4ee99ff8bb/sqlalchemy-2.0.49-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:55250fe61d6ebfd6934a272ee16ef1244e0f16b7af6cd18ab5b1fc9f08631db0", size = 3468565, upload-time = "2026-04-03T16:58:33.414Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3d/ce71cfa82c50a373fd2148b3c870be05027155ce791dc9a5dcf439790b8b/sqlalchemy-2.0.49-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:46796877b47034b559a593d7e4b549aba151dae73f9e78212a3478161c12ab08", size = 3477769, upload-time = "2026-04-03T17:06:02.787Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e8/0a9f5c1f7c6f9ca480319bf57c2d7423f08d31445974167a27d14483c948/sqlalchemy-2.0.49-cp313-cp313t-win32.whl", hash = "sha256:9c4969a86e41454f2858256c39bdfb966a20961e9b58bf8749b65abf447e9a8d", size = 2143319, upload-time = "2026-04-03T17:02:04.328Z" }, + { url = "https://files.pythonhosted.org/packages/0e/51/fb5240729fbec73006e137c4f7a7918ffd583ab08921e6ff81a999d6517a/sqlalchemy-2.0.49-cp313-cp313t-win_amd64.whl", hash = "sha256:b9870d15ef00e4d0559ae10ee5bc71b654d1f20076dbe8bc7ed19b4c0625ceba", size = 2175104, upload-time = "2026-04-03T17:02:05.989Z" }, + { url = "https://files.pythonhosted.org/packages/55/33/bf28f618c0a9597d14e0b9ee7d1e0622faff738d44fe986ee287cdf1b8d0/sqlalchemy-2.0.49-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:233088b4b99ebcbc5258c755a097aa52fbf90727a03a5a80781c4b9c54347a2e", size = 2156356, upload-time = "2026-04-03T16:53:09.914Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a7/5f476227576cb8644650eff68cc35fa837d3802b997465c96b8340ced1e2/sqlalchemy-2.0.49-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57ca426a48eb2c682dae8204cd89ea8ab7031e2675120a47924fabc7caacbc2a", size = 3276486, upload-time = "2026-04-03T17:07:46.9Z" }, + { url = "https://files.pythonhosted.org/packages/2e/84/efc7c0bf3a1c5eef81d397f6fddac855becdbb11cb38ff957888603014a7/sqlalchemy-2.0.49-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:685e93e9c8f399b0c96a624799820176312f5ceef958c0f88215af4013d29066", size = 3281479, upload-time = "2026-04-03T17:12:32.226Z" }, + { url = "https://files.pythonhosted.org/packages/91/68/bb406fa4257099c67bd75f3f2261b129c63204b9155de0d450b37f004698/sqlalchemy-2.0.49-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e0400fa22f79acc334d9a6b185dc00a44a8e6578aa7e12d0ddcd8434152b187", size = 3226269, upload-time = "2026-04-03T17:07:48.678Z" }, + { url = "https://files.pythonhosted.org/packages/67/84/acb56c00cca9f251f437cb49e718e14f7687505749ea9255d7bd8158a6df/sqlalchemy-2.0.49-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a05977bffe9bffd2229f477fa75eabe3192b1b05f408961d1bebff8d1cd4d401", size = 3248260, upload-time = "2026-04-03T17:12:34.381Z" }, + { url = "https://files.pythonhosted.org/packages/56/19/6a20ea25606d1efd7bd1862149bb2a22d1451c3f851d23d887969201633f/sqlalchemy-2.0.49-cp314-cp314-win32.whl", hash = "sha256:0f2fa354ba106eafff2c14b0cc51f22801d1e8b2e4149342023bd6f0955de5f5", size = 2118463, upload-time = "2026-04-03T17:05:47.093Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4f/8297e4ed88e80baa1f5aa3c484a0ee29ef3c69c7582f206c916973b75057/sqlalchemy-2.0.49-cp314-cp314-win_amd64.whl", hash = "sha256:77641d299179c37b89cf2343ca9972c88bb6eef0d5fc504a2f86afd15cd5adf5", size = 2144204, upload-time = "2026-04-03T17:05:48.694Z" }, + { url = "https://files.pythonhosted.org/packages/1f/33/95e7216df810c706e0cd3655a778604bbd319ed4f43333127d465a46862d/sqlalchemy-2.0.49-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c1dc3368794d522f43914e03312202523cc89692f5389c32bea0233924f8d977", size = 3565474, upload-time = "2026-04-03T16:58:35.128Z" }, + { url = "https://files.pythonhosted.org/packages/0c/a4/ed7b18d8ccf7f954a83af6bb73866f5bc6f5636f44c7731fbb741f72cc4f/sqlalchemy-2.0.49-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c821c47ecfe05cc32140dcf8dc6fd5d21971c86dbd56eabfe5ba07a64910c01", size = 3530567, upload-time = "2026-04-03T17:06:04.587Z" }, + { url = "https://files.pythonhosted.org/packages/73/a3/20faa869c7e21a827c4a2a42b41353a54b0f9f5e96df5087629c306df71e/sqlalchemy-2.0.49-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9c04bff9a5335eb95c6ecf1c117576a0aa560def274876fd156cfe5510fccc61", size = 3474282, upload-time = "2026-04-03T16:58:37.131Z" }, + { url = "https://files.pythonhosted.org/packages/b7/50/276b9a007aa0764304ad467eceb70b04822dc32092492ee5f322d559a4dc/sqlalchemy-2.0.49-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7f605a456948c35260e7b2a39f8952a26f077fd25653c37740ed186b90aaa68a", size = 3480406, upload-time = "2026-04-03T17:06:07.176Z" }, + { url = "https://files.pythonhosted.org/packages/e5/c3/c80fcdb41905a2df650c2a3e0337198b6848876e63d66fe9188ef9003d24/sqlalchemy-2.0.49-cp314-cp314t-win32.whl", hash = "sha256:6270d717b11c5476b0cbb21eedc8d4dbb7d1a956fd6c15a23e96f197a6193158", size = 2149151, upload-time = "2026-04-03T17:02:07.281Z" }, + { url = "https://files.pythonhosted.org/packages/05/52/9f1a62feab6ed368aff068524ff414f26a6daebc7361861035ae00b05530/sqlalchemy-2.0.49-cp314-cp314t-win_amd64.whl", hash = "sha256:275424295f4256fd301744b8f335cff367825d270f155d522b30c7bf49903ee7", size = 2184178, upload-time = "2026-04-03T17:02:08.623Z" }, + { url = "https://files.pythonhosted.org/packages/e5/30/8519fdde58a7bdf155b714359791ad1dc018b47d60269d5d160d311fdc36/sqlalchemy-2.0.49-py3-none-any.whl", hash = "sha256:ec44cfa7ef1a728e88ad41674de50f6db8cfdb3e2af84af86e0041aaf02d43d0", size = 1942158, upload-time = "2026-04-03T16:53:44.135Z" }, +] + +[[package]] +name = "sqlmodel" +source = { editable = "." } +dependencies = [ + { name = "pydantic" }, + { name = "sqlalchemy" }, + { name = "typing-extensions" }, +] + +[package.dev-dependencies] +dev = [ + { name = "black" }, + { name = "cairosvg" }, + { name = "coverage", extra = ["toml"] }, + { name = "dirty-equals" }, + { name = "fastapi" }, + { name = "griffe-typingdoc" }, + { name = "griffe-warnings-deprecated" }, + { name = "httpx" }, + { name = "jinja2" }, + { name = "markdown-include-variants" }, + { name = "mdx-include" }, + { name = "mkdocs-macros-plugin" }, + { name = "mkdocs-material" }, + { name = "mkdocs-redirects" }, + { name = "mkdocstrings", extra = ["python"] }, + { name = "pillow" }, + { name = "prek" }, + { name = "pytest" }, + { name = "pyyaml" }, + { name = "ruff" }, + { name = "ty" }, + { name = "typer" }, + { name = "typing-extensions" }, +] +docs = [ + { name = "black" }, + { name = "cairosvg" }, + { name = "griffe-typingdoc" }, + { name = "griffe-warnings-deprecated" }, + { name = "markdown-include-variants" }, + { name = "mdx-include" }, + { name = "mkdocs-macros-plugin" }, + { name = "mkdocs-material" }, + { name = "mkdocs-redirects" }, + { name = "mkdocstrings", extra = ["python"] }, + { name = "pillow" }, + { name = "pyyaml" }, + { name = "typer" }, +] +github-actions = [ + { name = "httpx" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pygithub" }, + { name = "smokeshow" }, +] +tests = [ + { name = "black" }, + { name = "coverage", extra = ["toml"] }, + { name = "dirty-equals" }, + { name = "fastapi" }, + { name = "httpx" }, + { name = "jinja2" }, + { name = "pytest" }, + { name = "ruff" }, + { name = "ty" }, + { name = "typing-extensions" }, +] + +[package.metadata] +requires-dist = [ + { name = "pydantic", specifier = ">=2.11.0" }, + { name = "sqlalchemy", specifier = ">=2.0.14,<2.1.0" }, + { name = "typing-extensions", specifier = ">=4.5.0" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "black", specifier = ">=24.1.0" }, + { name = "cairosvg", specifier = ">=2.9.0" }, + { name = "coverage", extras = ["toml"], specifier = ">=6.2" }, + { name = "dirty-equals", specifier = ">=0.11" }, + { name = "fastapi", specifier = ">=0.128.0" }, + { name = "griffe-typingdoc", specifier = ">=0.3.0" }, + { name = "griffe-warnings-deprecated", specifier = ">=1.1.0" }, + { name = "httpx", specifier = ">=0.28.1" }, + { name = "jinja2", specifier = ">=3.1.6" }, + { name = "markdown-include-variants", specifier = ">=0.0.8" }, + { name = "mdx-include", specifier = ">=1.4.1" }, + { name = "mkdocs-macros-plugin", specifier = ">=1.5.0" }, + { name = "mkdocs-material", specifier = ">=9.7.5" }, + { name = "mkdocs-redirects", specifier = ">=1.2.1" }, + { name = "mkdocstrings", extras = ["python"], specifier = ">=1.0.3" }, + { name = "pillow", specifier = ">=12.1.1" }, + { name = "prek", specifier = ">=0.2.24,<1.0.0" }, + { name = "pytest", specifier = ">=7.0.1" }, + { name = "pyyaml", specifier = ">=5.3.1" }, + { name = "ruff", specifier = ">=0.15.6" }, + { name = "ty", specifier = ">=0.0.25" }, + { name = "typer", specifier = ">=0.24.1" }, + { name = "typing-extensions", specifier = ">=4.15.0" }, +] +docs = [ + { name = "black", specifier = ">=24.1.0" }, + { name = "cairosvg", specifier = ">=2.9.0" }, + { name = "griffe-typingdoc", specifier = ">=0.3.0" }, + { name = "griffe-warnings-deprecated", specifier = ">=1.1.0" }, + { name = "markdown-include-variants", specifier = ">=0.0.8" }, + { name = "mdx-include", specifier = ">=1.4.1" }, + { name = "mkdocs-macros-plugin", specifier = ">=1.5.0" }, + { name = "mkdocs-material", specifier = ">=9.7.5" }, + { name = "mkdocs-redirects", specifier = ">=1.2.1" }, + { name = "mkdocstrings", extras = ["python"], specifier = ">=1.0.3" }, + { name = "pillow", specifier = ">=12.1.1" }, + { name = "pyyaml", specifier = ">=5.3.1" }, + { name = "typer", specifier = ">=0.24.1" }, +] +github-actions = [ + { name = "httpx", specifier = ">=0.28.1" }, + { name = "pydantic", specifier = ">=2.5.3" }, + { name = "pydantic-settings", specifier = ">=2.1.0" }, + { name = "pygithub", specifier = ">=2.3.0" }, + { name = "smokeshow", specifier = ">=0.5.0" }, +] +tests = [ + { name = "black", specifier = ">=24.1.0" }, + { name = "coverage", extras = ["toml"], specifier = ">=6.2" }, + { name = "dirty-equals", specifier = ">=0.11" }, + { name = "fastapi", specifier = ">=0.128.0" }, + { name = "httpx", specifier = ">=0.28.1" }, + { name = "jinja2", specifier = ">=3.1.6" }, + { name = "pytest", specifier = ">=7.0.1" }, + { name = "ruff", specifier = ">=0.15.6" }, + { name = "ty", specifier = ">=0.0.25" }, + { name = "typing-extensions", specifier = ">=4.15.0" }, +] + +[[package]] +name = "starlette" +version = "0.50.0" +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/ba/b8/73a0e6a6e079a9d9cfa64113d771e421640b6f679a52eeb9b32f72d871a1/starlette-0.50.0.tar.gz", hash = "sha256:a2a17b22203254bcbc2e1f926d2d55f3f9497f769416b3190768befe598fa3ca", size = 2646985, upload-time = "2025-11-01T15:25:27.516Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", size = 74033, upload-time = "2025-11-01T15:25:25.461Z" }, +] + +[[package]] +name = "super-collections" +version = "0.6.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "hjson" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e0/de/a0c3d1244912c260638f0f925e190e493ccea37ecaea9bbad7c14413b803/super_collections-0.6.2.tar.gz", hash = "sha256:0c8d8abacd9fad2c7c1c715f036c29f5db213f8cac65f24d45ecba12b4da187a", size = 31315, upload-time = "2025-09-30T00:37:08.067Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/43/47c7cf84b3bd74a8631b02d47db356656bb8dff6f2e61a4c749963814d0d/super_collections-0.6.2-py3-none-any.whl", hash = "sha256:291b74d26299e9051d69ad9d89e61b07b6646f86a57a2f5ab3063d206eee9c56", size = 16173, upload-time = "2025-09-30T00:37:07.104Z" }, +] + +[[package]] +name = "termcolor" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/87/56/ab275c2b56a5e2342568838f0d5e3e66a32354adcc159b495e374cda43f5/termcolor-3.2.0.tar.gz", hash = "sha256:610e6456feec42c4bcd28934a8c87a06c3fa28b01561d46aa09a9881b8622c58", size = 14423, upload-time = "2025-10-25T19:11:42.586Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/d5/141f53d7c1eb2a80e6d3e9a390228c3222c27705cbe7f048d3623053f3ca/termcolor-3.2.0-py3-none-any.whl", hash = "sha256:a10343879eba4da819353c55cb8049b0933890c2ebf9ad5d3ecd2bb32ea96ea6", size = 7698, upload-time = "2025-10-25T19:11:41.536Z" }, +] + +[[package]] +name = "tinycss2" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/ae/2ca4913e5c0f09781d75482874c3a95db9105462a92ddd303c7d285d3df2/tinycss2-1.5.1.tar.gz", hash = "sha256:d339d2b616ba90ccce58da8495a78f46e55d4d25f9fd71dfd526f07e7d53f957", size = 88195, upload-time = "2025-11-23T10:29:10.082Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/45/c7b5c3168458db837e8ceab06dc77824e18202679d0463f0e8f002143a97/tinycss2-1.5.1-py3-none-any.whl", hash = "sha256:3415ba0f5839c062696996998176c4a3751d18b7edaaeeb658c9ce21ec150661", size = 28404, upload-time = "2025-11-23T10:29:08.676Z" }, +] + +[[package]] +name = "tomli" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236, upload-time = "2025-10-08T22:01:00.137Z" }, + { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084, upload-time = "2025-10-08T22:01:01.63Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832, upload-time = "2025-10-08T22:01:02.543Z" }, + { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052, upload-time = "2025-10-08T22:01:03.836Z" }, + { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555, upload-time = "2025-10-08T22:01:04.834Z" }, + { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128, upload-time = "2025-10-08T22:01:05.84Z" }, + { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445, upload-time = "2025-10-08T22:01:06.896Z" }, + { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165, upload-time = "2025-10-08T22:01:08.107Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891, upload-time = "2025-10-08T22:01:09.082Z" }, + { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796, upload-time = "2025-10-08T22:01:10.266Z" }, + { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121, upload-time = "2025-10-08T22:01:11.332Z" }, + { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070, upload-time = "2025-10-08T22:01:12.498Z" }, + { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859, upload-time = "2025-10-08T22:01:13.551Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296, upload-time = "2025-10-08T22:01:14.614Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124, upload-time = "2025-10-08T22:01:15.629Z" }, + { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698, upload-time = "2025-10-08T22:01:16.51Z" }, + { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819, upload-time = "2025-10-08T22:01:17.964Z" }, + { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766, upload-time = "2025-10-08T22:01:18.959Z" }, + { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771, upload-time = "2025-10-08T22:01:20.106Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586, upload-time = "2025-10-08T22:01:21.164Z" }, + { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792, upload-time = "2025-10-08T22:01:22.417Z" }, + { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909, upload-time = "2025-10-08T22:01:23.859Z" }, + { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946, upload-time = "2025-10-08T22:01:24.893Z" }, + { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705, upload-time = "2025-10-08T22:01:26.153Z" }, + { url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244, upload-time = "2025-10-08T22:01:27.06Z" }, + { url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637, upload-time = "2025-10-08T22:01:28.059Z" }, + { url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925, upload-time = "2025-10-08T22:01:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045, upload-time = "2025-10-08T22:01:31.98Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835, upload-time = "2025-10-08T22:01:32.989Z" }, + { url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109, upload-time = "2025-10-08T22:01:34.052Z" }, + { url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930, upload-time = "2025-10-08T22:01:35.082Z" }, + { url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964, upload-time = "2025-10-08T22:01:36.057Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065, upload-time = "2025-10-08T22:01:37.27Z" }, + { url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088, upload-time = "2025-10-08T22:01:38.235Z" }, + { url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193, upload-time = "2025-10-08T22:01:39.712Z" }, + { url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488, upload-time = "2025-10-08T22:01:40.773Z" }, + { url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669, upload-time = "2025-10-08T22:01:41.824Z" }, + { url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709, upload-time = "2025-10-08T22:01:43.177Z" }, + { url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563, upload-time = "2025-10-08T22:01:44.233Z" }, + { url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756, upload-time = "2025-10-08T22:01:45.234Z" }, + { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" }, +] + +[[package]] +name = "ty" +version = "0.0.31" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/31/cc/5ea5d3a72216c8c2bf77d83066dd4f3553532d0aacc03d4a8397dd9845e1/ty-0.0.31.tar.gz", hash = "sha256:4a4094292d9671caf3b510c7edf36991acd9c962bb5d97205374ffed9f541c45", size = 5516619, upload-time = "2026-04-15T15:47:59.87Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/10/ea805cbbd75d5d50792551a2b383de8521eeab0c44f38c73e12819ced65e/ty-0.0.31-py3-none-linux_armv6l.whl", hash = "sha256:761651dc17ad7bc0abfc1b04b3f0e84df263ed435d34f29760b3da739ab02d35", size = 10834749, upload-time = "2026-04-15T15:48:14.877Z" }, + { url = "https://files.pythonhosted.org/packages/d9/4c/fabf951850401d24d36b21bced088a366c6827e1c37dab4523afff84c4b2/ty-0.0.31-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:c529922395a07231c27488f0290651e05d27d149f7e0aa807678f1f7e9c58a5e", size = 10626012, upload-time = "2026-04-15T15:48:22.554Z" }, + { url = "https://files.pythonhosted.org/packages/04/b0/4a5aff88d2544f19514a59c8f693d63144aa7307fe2ee5df608333ab5460/ty-0.0.31-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5f345df2b87d747859e72c2cbc9be607ea1bbc8bc93dd32fa3d03ea091cb4fee", size = 10075790, upload-time = "2026-04-15T15:47:46.959Z" }, + { url = "https://files.pythonhosted.org/packages/d5/73/9d4dcad12cd4e85274014f2c0510ef93f590b2a1e5148de3a9f276098dad/ty-0.0.31-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4b207eddcfbafd376132689d3435b14efcb531289cb59cd961c6a611133bd54", size = 10590286, upload-time = "2026-04-15T15:48:06.222Z" }, + { url = "https://files.pythonhosted.org/packages/47/45/fe40adde18692359ded174ae7ddbfac056e876eb0f43b65be74fde7f6072/ty-0.0.31-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:663778b220f357067488ce68bfc52335ccbd161549776f70dcbde6bbde82f77a", size = 10623824, upload-time = "2026-04-15T15:48:12.965Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e8/0ffa2e09b548e6daa9ebc368d68b767dc2405ca4cbeadb7ede0e2cb21059/ty-0.0.31-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3506cfe87dfade0fb2960dd4fffd4fd8089003587b3445c0a1a295c9d83764fb", size = 11156864, upload-time = "2026-04-15T15:48:08.473Z" }, + { url = "https://files.pythonhosted.org/packages/08/e9/fd44c2075115d569593ee9473d7e2a38b750fd7e783421c95eb528c15df5/ty-0.0.31-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8b3f3d8492f08e81916026354c1d1599e9ddfa1241804141a74d5662fc710085", size = 11696401, upload-time = "2026-04-15T15:48:17.355Z" }, + { url = "https://files.pythonhosted.org/packages/4e/50/35aad8eadf964d23e2a4faa5b38a206aa85c78833c8ce335dddd2c34ba63/ty-0.0.31-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a97de32ee6a619393a4c495e056a1c547de7877510f3152e61345c71d774d2d0", size = 11374903, upload-time = "2026-04-15T15:47:55.893Z" }, + { url = "https://files.pythonhosted.org/packages/c8/37/01eccd25d23f5aaa7f7ff1a87b5b215469f6b202cf689a1812b71c1e7f6b/ty-0.0.31-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c906354ce441e342646582bc9b8f48a676f79f3d061e25de15ff870e015ca14e", size = 11206624, upload-time = "2026-04-15T15:47:51.778Z" }, + { url = "https://files.pythonhosted.org/packages/f4/70/baad2914cb097453f127a221f8addb2b41926098059cd773c75e6a662fc4/ty-0.0.31-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:275bb7c82afcbf89fe2dbef1b2692f2bc98451f1ee2c8eb809ddd91317822388", size = 10575089, upload-time = "2026-04-15T15:47:49.448Z" }, + { url = "https://files.pythonhosted.org/packages/83/12/bae3a7bba2e785eb72ce00f9da70eedcb8c5e8299efecbd16e6e436abd82/ty-0.0.31-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:405da247027c6efd1e264886b6ac4a86ab3a4f09200b02e33630efe85f119e53", size = 10642315, upload-time = "2026-04-15T15:48:19.661Z" }, + { url = "https://files.pythonhosted.org/packages/93/9e/cad04d5d839bc60355cea98c7e09d724ea65f47184def0fae8b90dc54591/ty-0.0.31-py3-none-musllinux_1_2_i686.whl", hash = "sha256:54d9835608eed196853d6643f645c50ce83bcc7fe546cdb3e210c1bcf7c58c09", size = 10834473, upload-time = "2026-04-15T15:48:02.091Z" }, + { url = "https://files.pythonhosted.org/packages/e3/ba/84112d280182d37690d3d2b4018b2667e42bc281585e607015635310016a/ty-0.0.31-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5ee11be9b07e8c0c6b455ff075a0abe4f194de9476f57624db98eec9df618355", size = 11315785, upload-time = "2026-04-15T15:48:10.754Z" }, + { url = "https://files.pythonhosted.org/packages/50/9f/ac42dc223d7e0950e97a1854567a8b3e7fe09ad7375adbf91bfb43290482/ty-0.0.31-py3-none-win32.whl", hash = "sha256:7286587aacf3eef0956062d6492b893b02f82b0f22c5e230008e13ff0d216a8b", size = 10187657, upload-time = "2026-04-15T15:48:04.264Z" }, + { url = "https://files.pythonhosted.org/packages/75/3e/57ba7ea7ecb2f4751644ba91756e2be70e33ef5952c0c41a256a0e4c2437/ty-0.0.31-py3-none-win_amd64.whl", hash = "sha256:81134e25d2a2562ab372f24de8f9bd05034d27d30377a5d7540f259791c6234c", size = 11205258, upload-time = "2026-04-15T15:47:53.759Z" }, + { url = "https://files.pythonhosted.org/packages/88/39/bca669095ccf0a400af941fdf741578d4c2d6719f1b7f10e6dbec10aa862/ty-0.0.31-py3-none-win_arm64.whl", hash = "sha256:e9cb15fad26545c6a608f40f227af3a5513cb376998ca6feddd47ca7d93ffafa", size = 10590392, upload-time = "2026-04-15T15:47:57.968Z" }, +] + +[[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 = "urllib3" +version = "2.6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/24/a2a2ed9addd907787d7aa0355ba36a6cadf1768b934c652ea78acbd59dcd/urllib3-2.6.2.tar.gz", hash = "sha256:016f9c98bb7e98085cb2b4b17b87d2c702975664e4f060c6532e64d1c1a5e797", size = 432930, upload-time = "2025-12-11T15:56:40.252Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/b9/4095b668ea3678bf6a0af005527f39de12fb026516fb3df17495a733b7f8/urllib3-2.6.2-py3-none-any.whl", hash = "sha256:ec21cddfe7724fc7cb4ba4bea7aa8e2ef36f607a4bab81aa6ce42a13dc3f03dd", size = 131182, upload-time = "2025-12-11T15:56:38.584Z" }, +] + +[[package]] +name = "watchdog" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/56/90994d789c61df619bfc5ce2ecdabd5eeff564e1eb47512bd01b5e019569/watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26", size = 96390, upload-time = "2024-11-01T14:06:24.793Z" }, + { url = "https://files.pythonhosted.org/packages/55/46/9a67ee697342ddf3c6daa97e3a587a56d6c4052f881ed926a849fcf7371c/watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112", size = 88389, upload-time = "2024-11-01T14:06:27.112Z" }, + { url = "https://files.pythonhosted.org/packages/44/65/91b0985747c52064d8701e1075eb96f8c40a79df889e59a399453adfb882/watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3", size = 89020, upload-time = "2024-11-01T14:06:29.876Z" }, + { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" }, + { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" }, + { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" }, + { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" }, + { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" }, + { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" }, + { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, + { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, + { url = "https://files.pythonhosted.org/packages/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902, upload-time = "2024-11-01T14:06:53.119Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380, upload-time = "2024-11-01T14:06:55.19Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, +] + +[[package]] +name = "webencodings" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721, upload-time = "2017-04-05T20:21:34.189Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774, upload-time = "2017-04-05T20:21:32.581Z" }, +]