diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 8979aabf8b..fdca003877 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: "monthly" diff --git a/.github/labeler.yml b/.github/labeler.yml index c5b1f84f3d..cdaefbf2d8 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -17,6 +17,7 @@ lang-all: - docs/*/docs/** - all-globs-to-all-files: - '!docs/en/docs/**' + - '!docs/*/**/_*.md' - '!fastapi/**' - '!pyproject.toml' diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index f78b6730ee..77bce055c6 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -8,9 +8,6 @@ on: - opened - synchronize -env: - UV_SYSTEM_PYTHON: 1 - jobs: changes: runs-on: ubuntu-latest @@ -21,7 +18,7 @@ jobs: outputs: docs: ${{ steps.filter.outputs.docs }} steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 # For pull requests it's not necessary to checkout the code but for the main branch it is - uses: dorny/paths-filter@v3 id: filter @@ -31,13 +28,10 @@ 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 - scripts/mkdocs_hooks.py @@ -48,33 +42,24 @@ jobs: outputs: langs: ${{ steps.show-langs.outputs.langs }} steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: - python-version: "3.11" + python-version-file: ".python-version" - name: Setup uv uses: astral-sh/setup-uv@v7 with: - version: "0.4.15" enable-cache: true cache-dependency-glob: | - requirements**.txt pyproject.toml + uv.lock - name: Install docs extras - run: uv pip install -r requirements-docs.txt - # Install MkDocs Material Insiders here just to put it in the cache for the rest of the steps - - name: Install Material for MkDocs Insiders - if: ( github.event_name != 'pull_request' || github.secret_source == 'Actions' ) - run: uv pip install -r requirements-docs-insiders.txt - env: - TOKEN: ${{ secrets.FASTAPI_MKDOCS_MATERIAL_INSIDERS }} - - name: Verify Docs - run: python ./scripts/docs.py verify-docs + run: uv sync --locked --no-dev --group docs - name: Export Language Codes id: show-langs run: | - echo "langs=$(python ./scripts/docs.py langs-json)" >> $GITHUB_OUTPUT + echo "langs=$(uv run ./scripts/docs.py langs-json)" >> $GITHUB_OUTPUT build-docs: needs: @@ -90,35 +75,29 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: - python-version: "3.11" + python-version-file: ".python-version" - name: Setup uv uses: astral-sh/setup-uv@v7 with: - version: "0.4.15" enable-cache: true cache-dependency-glob: | - requirements**.txt pyproject.toml + uv.lock - name: Install docs extras - run: uv pip install -r requirements-docs.txt - - name: Install Material for MkDocs Insiders - if: ( github.event_name != 'pull_request' || github.secret_source == 'Actions' ) - run: uv pip install -r requirements-docs-insiders.txt - env: - TOKEN: ${{ secrets.FASTAPI_MKDOCS_MATERIAL_INSIDERS }} + run: uv sync --locked --no-dev --group docs - name: Update Languages - run: python ./scripts/docs.py update-languages - - uses: actions/cache@v4 + run: uv run ./scripts/docs.py update-languages + - uses: actions/cache@v5 with: key: mkdocs-cards-${{ matrix.lang }}-${{ github.ref }} path: docs/${{ matrix.lang }}/.cache - name: Build Docs - run: python ./scripts/docs.py build-lang ${{ matrix.lang }} - - uses: actions/upload-artifact@v5 + run: uv run ./scripts/docs.py build-lang ${{ matrix.lang }} + - uses: actions/upload-artifact@v6 with: name: docs-site-${{ matrix.lang }} path: ./site/** diff --git a/.github/workflows/contributors.yml b/.github/workflows/contributors.yml index 7d5449c6a8..f3ced6aa30 100644 --- a/.github/workflows/contributors.yml +++ b/.github/workflows/contributors.yml @@ -10,9 +10,6 @@ on: required: false default: "false" -env: - UV_SYSTEM_PYTHON: 1 - jobs: job: if: github.repository_owner == 'fastapi' @@ -24,21 +21,20 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: - python-version: "3.11" + python-version-file: ".python-version" - name: Setup uv uses: astral-sh/setup-uv@v7 with: - version: "0.4.15" enable-cache: true cache-dependency-glob: | - requirements**.txt pyproject.toml + uv.lock - name: Install Dependencies - run: uv pip install -r requirements-github-actions.txt + run: uv sync --locked --no-dev --group github-actions # Allow debugging with tmate - name: Setup tmate session uses: mxschmitt/action-tmate@v3 @@ -48,6 +44,6 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.FASTAPI_PR_TOKEN }} - name: FastAPI People Contributors - run: python ./scripts/contributors.py + run: uv run ./scripts/contributors.py env: GITHUB_TOKEN: ${{ secrets.FASTAPI_PR_TOKEN }} diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index aa4fd6b657..734fc244d3 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -12,9 +12,6 @@ permissions: pull-requests: write statuses: write -env: - UV_SYSTEM_PYTHON: 1 - jobs: deploy-docs: runs-on: ubuntu-latest @@ -23,23 +20,22 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: - python-version: "3.11" + python-version-file: ".python-version" - name: Setup uv uses: astral-sh/setup-uv@v7 with: - version: "0.4.15" enable-cache: true cache-dependency-glob: | - requirements**.txt pyproject.toml + uv.lock - name: Install GitHub Actions dependencies - run: uv 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 }} @@ -49,7 +45,7 @@ jobs: run: | rm -rf ./site mkdir ./site - - uses: actions/download-artifact@v6 + - uses: actions/download-artifact@v7 with: path: ./site/ pattern: docs-site-* @@ -70,14 +66,14 @@ jobs: command: pages deploy ./site --project-name=${{ env.PROJECT_NAME }} --branch=${{ env.BRANCH }} - name: Deploy Docs Status Error if: failure() - 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: "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.deployment-url }} diff --git a/.github/workflows/issue-manager.yml b/.github/workflows/issue-manager.yml index f40ec4dc47..2ae588da13 100644 --- a/.github/workflows/issue-manager.yml +++ b/.github/workflows/issue-manager.yml @@ -41,11 +41,15 @@ jobs: "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." + "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/label-approved.yml b/.github/workflows/label-approved.yml index e6ae3d9636..1307fb8c23 100644 --- a/.github/workflows/label-approved.yml +++ b/.github/workflows/label-approved.yml @@ -8,9 +8,6 @@ on: permissions: pull-requests: write -env: - UV_SYSTEM_PYTHON: 1 - jobs: label-approved: if: github.repository_owner == 'fastapi' @@ -20,23 +17,22 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: - python-version: "3.11" + python-version-file: ".python-version" - name: Setup uv uses: astral-sh/setup-uv@v7 with: - version: "0.4.15" enable-cache: true cache-dependency-glob: | - requirements**.txt pyproject.toml + uv.lock - name: Install GitHub Actions dependencies - run: uv pip install -r requirements-github-actions.txt + run: uv sync --locked --no-dev --group github-actions - name: Label Approved - run: python ./scripts/label_approved.py + run: uv run ./scripts/label_approved.py env: TOKEN: ${{ secrets.GITHUB_TOKEN }} CONFIG: > diff --git a/.github/workflows/latest-changes.yml b/.github/workflows/latest-changes.yml index 2fa832fab5..b9e45ea629 100644 --- a/.github/workflows/latest-changes.yml +++ b/.github/workflows/latest-changes.yml @@ -24,6 +24,8 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" + # pin to actions/checkout@v5 for compatibility with latest-changes + # Ref: https://github.com/actions/checkout/issues/2313 - uses: actions/checkout@v5 with: # To allow latest-changes to commit to the main branch @@ -34,7 +36,7 @@ jobs: if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled == 'true' }} with: limit-access-to-actor: true - - uses: tiangolo/latest-changes@0.4.0 + - uses: tiangolo/latest-changes@0.4.1 with: token: ${{ secrets.GITHUB_TOKEN }} latest_changes_file: docs/en/docs/release-notes.md diff --git a/.github/workflows/notify-translations.yml b/.github/workflows/notify-translations.yml index 04beeb64e9..31f3eb4021 100644 --- a/.github/workflows/notify-translations.yml +++ b/.github/workflows/notify-translations.yml @@ -15,9 +15,6 @@ on: required: false default: 'false' -env: - UV_SYSTEM_PYTHON: 1 - jobs: job: runs-on: ubuntu-latest @@ -28,21 +25,20 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: - python-version: "3.11" + python-version-file: ".python-version" - name: Setup uv uses: astral-sh/setup-uv@v7 with: - version: "0.4.15" enable-cache: true cache-dependency-glob: | - requirements**.txt pyproject.toml + uv.lock - name: Install Dependencies - run: uv pip install -r requirements-github-actions.txt + run: uv sync --locked --no-dev --group github-actions # Allow debugging with tmate - name: Setup tmate session uses: mxschmitt/action-tmate@v3 @@ -52,7 +48,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Notify Translations - run: python ./scripts/notify_translations.py + run: uv run ./scripts/notify_translations.py env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NUMBER: ${{ github.event.inputs.number || null }} diff --git a/.github/workflows/people.yml b/.github/workflows/people.yml index f15b921371..cb3b742788 100644 --- a/.github/workflows/people.yml +++ b/.github/workflows/people.yml @@ -10,9 +10,6 @@ on: required: false default: "false" -env: - UV_SYSTEM_PYTHON: 1 - jobs: job: if: github.repository_owner == 'fastapi' @@ -24,21 +21,20 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: - python-version: "3.11" + python-version-file: ".python-version" - name: Setup uv uses: astral-sh/setup-uv@v7 with: - version: "0.4.15" enable-cache: true cache-dependency-glob: | - requirements**.txt pyproject.toml + uv.lock - name: Install Dependencies - run: uv pip install -r requirements-github-actions.txt + run: uv sync --locked --no-dev --group github-actions # Allow debugging with tmate - name: Setup tmate session uses: mxschmitt/action-tmate@v3 @@ -48,7 +44,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.FASTAPI_PEOPLE }} - name: FastAPI People Experts - run: python ./scripts/people.py + run: uv run ./scripts/people.py env: GITHUB_TOKEN: ${{ secrets.FASTAPI_PEOPLE }} SLEEP_INTERVAL: ${{ vars.PEOPLE_SLEEP_INTERVAL }} diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 0000000000..f027140ed7 --- /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@v5 + 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@v5 + 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@v6 + with: + python-version-file: ".python-version" + - name: Setup uv + uses: astral-sh/setup-uv@v7 + with: + cache-dependency-glob: | + pyproject.toml + uv.lock + - name: Install Dependencies + run: uv sync --locked --extra all + - 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@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@release/v1 + with: + jobs: ${{ toJSON(needs) }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 441eb45608..2232498cb1 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -15,28 +15,25 @@ jobs: - fastapi-slim permissions: id-token: write + contents: read steps: - name: Dump GitHub context env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: - python-version: "3.10" + python-version-file: ".python-version" # Issue ref: https://github.com/actions/setup-python/issues/436 # cache: "pip" # cache-dependency-path: pyproject.toml - - name: Install build dependencies - run: pip install build + - name: Install uv + uses: astral-sh/setup-uv@v7 - name: Build distribution + run: uv build env: TIANGOLO_BUILD_PACKAGE: ${{ matrix.package }} - run: python -m build - name: Publish - uses: pypa/gh-action-pypi-publish@v1.13.0 - - name: Dump GitHub context - env: - GITHUB_CONTEXT: ${{ toJson(github) }} - run: echo "$GITHUB_CONTEXT" + run: uv publish diff --git a/.github/workflows/smokeshow.yml b/.github/workflows/smokeshow.yml index eed5fbec0e..f23b962b70 100644 --- a/.github/workflows/smokeshow.yml +++ b/.github/workflows/smokeshow.yml @@ -8,12 +8,8 @@ on: permissions: statuses: write -env: - UV_SYSTEM_PYTHON: 1 - jobs: smokeshow: - if: ${{ github.event.workflow_run.conclusion == 'success' }} runs-on: ubuntu-latest steps: @@ -21,20 +17,18 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: actions/setup-python@v6 with: - python-version: '3.9' + python-version-file: ".python-version" - name: Setup uv uses: astral-sh/setup-uv@v7 with: - version: "0.4.15" - enable-cache: true cache-dependency-glob: | - requirements**.txt pyproject.toml - - run: uv pip install -r requirements-github-actions.txt - - uses: actions/download-artifact@v6 + uv.lock + - run: uv sync --locked --no-dev --group github-actions + - uses: actions/download-artifact@v7 with: name: coverage-html path: htmlcov @@ -44,7 +38,7 @@ jobs: - name: Upload coverage to Smokeshow run: | for i in 1 2 3 4 5; do - if smokeshow upload htmlcov; then + if uv run smokeshow upload htmlcov; then echo "Smokeshow upload success!" break fi diff --git a/.github/workflows/sponsors.yml b/.github/workflows/sponsors.yml index 7d29469a52..88590ffa6c 100644 --- a/.github/workflows/sponsors.yml +++ b/.github/workflows/sponsors.yml @@ -10,9 +10,6 @@ on: required: false default: "false" -env: - UV_SYSTEM_PYTHON: 1 - jobs: job: if: github.repository_owner == 'fastapi' @@ -24,21 +21,20 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: - python-version: "3.11" + python-version-file: ".python-version" - name: Setup uv uses: astral-sh/setup-uv@v7 with: - version: "0.4.15" enable-cache: true cache-dependency-glob: | - requirements**.txt pyproject.toml + uv.lock - name: Install Dependencies - run: uv pip install -r requirements-github-actions.txt + run: uv sync --locked --no-dev --group github-actions # Allow debugging with tmate - name: Setup tmate session uses: mxschmitt/action-tmate@v3 @@ -46,7 +42,7 @@ jobs: with: limit-access-to-actor: true - name: FastAPI People Sponsors - run: python ./scripts/sponsors.py + run: uv run ./scripts/sponsors.py env: SPONSORS_TOKEN: ${{ secrets.SPONSORS_TOKEN }} PR_TOKEN: ${{ secrets.FASTAPI_PR_TOKEN }} diff --git a/.github/workflows/test-redistribute.yml b/.github/workflows/test-redistribute.yml index a44f0b6815..0491d33ba7 100644 --- a/.github/workflows/test-redistribute.yml +++ b/.github/workflows/test-redistribute.yml @@ -22,11 +22,11 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: - python-version: "3.10" + python-version-file: ".python-version" - name: Install build dependencies run: pip install build - name: Build source distribution @@ -40,7 +40,7 @@ jobs: - name: Install test dependencies run: | cd dist/fastapi*/ - pip install -r requirements-tests.txt + pip install --group tests --editable .[all] env: TIANGOLO_BUILD_PACKAGE: ${{ matrix.package }} - name: Run source distribution tests diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9c3e2218b9..891f767175 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,59 +13,45 @@ on: - cron: "0 0 * * 1" env: - UV_SYSTEM_PYTHON: 1 + UV_NO_SYNC: true jobs: - lint: - runs-on: ubuntu-latest - steps: - - name: Dump GitHub context - env: - GITHUB_CONTEXT: ${{ toJson(github) }} - run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v5 - - name: Set up Python - uses: actions/setup-python@v6 - with: - python-version: "3.11" - - name: Setup uv - uses: astral-sh/setup-uv@v7 - with: - version: "0.4.15" - enable-cache: true - cache-dependency-glob: | - requirements**.txt - pyproject.toml - - name: Install Dependencies - run: uv pip install -r requirements-tests.txt - - name: Install Pydantic v2 - run: uv pip install --upgrade "pydantic>=2.0.2,<3.0.0" - - name: Lint - run: bash scripts/lint.sh - test: - runs-on: ubuntu-latest strategy: matrix: - python-version: - - "3.14" - - "3.13" - - "3.12" - - "3.11" - - "3.10" - - "3.9" - - "3.8" - pydantic-version: ["pydantic-v1", "pydantic-v2"] - exclude: - - python-version: "3.14" - pydantic-version: "pydantic-v1" + os: [ windows-latest, macos-latest ] + python-version: [ "3.14" ] + include: + - os: ubuntu-latest + python-version: "3.9" + coverage: coverage + - os: macos-latest + python-version: "3.10" + coverage: coverage + - os: windows-latest + python-version: "3.12" + coverage: coverage + - os: ubuntu-latest + python-version: "3.13" + coverage: coverage + # Ubuntu with 3.13 needs coverage for CodSpeed benchmarks + - os: ubuntu-latest + python-version: "3.13" + coverage: coverage + codspeed: codspeed + - os: ubuntu-latest + python-version: "3.14" + coverage: coverage fail-fast: false + runs-on: ${{ matrix.os }} + env: + UV_PYTHON: ${{ matrix.python-version }} steps: - name: Dump GitHub context env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: @@ -73,33 +59,34 @@ jobs: - name: Setup uv uses: astral-sh/setup-uv@v7 with: - version: "0.4.15" enable-cache: true cache-dependency-glob: | - requirements**.txt pyproject.toml + uv.lock - name: Install Dependencies - run: uv pip install -r requirements-tests.txt - - name: Install Pydantic v1 - if: matrix.pydantic-version == 'pydantic-v1' - run: uv pip install "pydantic>=1.10.0,<2.0.0" - - name: Install Pydantic v2 - if: matrix.pydantic-version == 'pydantic-v2' - run: uv pip install --upgrade "pydantic>=2.0.2,<3.0.0" - # TODO: Remove this once Python 3.8 is no longer supported - - name: Install older AnyIO in Python 3.8 - if: matrix.python-version == '3.8' - run: uv pip install "anyio[trio]<4.0.0" + run: uv sync --locked --no-dev --group tests --extra all - run: mkdir coverage - name: Test - run: bash scripts/test.sh + if: matrix.codspeed != 'codspeed' + run: uv run bash scripts/test.sh env: COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }} CONTEXT: ${{ runner.os }}-py${{ matrix.python-version }} - - name: Store coverage files - uses: actions/upload-artifact@v5 + - name: CodSpeed benchmarks + if: matrix.codspeed == 'codspeed' + uses: CodSpeedHQ/action@v4 + env: + COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }} + CONTEXT: ${{ runner.os }}-py${{ matrix.python-version }} with: - name: coverage-${{ matrix.python-version }}-${{ matrix.pydantic-version }} + mode: simulation + run: uv run coverage run -m pytest tests/ --codspeed + # Do not store coverage for all possible combinations to avoid file size max errors in Smokeshow + - name: Store coverage files + if: matrix.coverage == 'coverage' + uses: actions/upload-artifact@v6 + with: + name: coverage-${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('**/coverage/.coverage.*') }} path: coverage include-hidden-files: true @@ -111,36 +98,35 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: actions/setup-python@v6 with: - python-version: '3.8' + python-version-file: ".python-version" - name: Setup uv uses: astral-sh/setup-uv@v7 with: - version: "0.4.15" enable-cache: true cache-dependency-glob: | - requirements**.txt pyproject.toml + uv.lock - name: Install Dependencies - run: uv pip install -r requirements-tests.txt + run: uv sync --locked --no-dev --group tests --extra all - name: Get coverage files - uses: actions/download-artifact@v6 + uses: actions/download-artifact@v7 with: pattern: coverage-* path: coverage merge-multiple: true - 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@v5 + uses: actions/upload-artifact@v6 with: name: coverage-html path: htmlcov include-hidden-files: true + - run: uv run coverage report --fail-under=100 # https://github.com/marketplace/actions/alls-green#why check: # This job does nothing and is only used for the branch protection diff --git a/.github/workflows/topic-repos.yml b/.github/workflows/topic-repos.yml index 22b37d59d7..46f6d60847 100644 --- a/.github/workflows/topic-repos.yml +++ b/.github/workflows/topic-repos.yml @@ -5,9 +5,6 @@ on: - cron: "0 12 1 * *" workflow_dispatch: -env: - UV_SYSTEM_PYTHON: 1 - jobs: topic-repos: if: github.repository_owner == 'fastapi' @@ -19,22 +16,21 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: - python-version: "3.11" + python-version-file: ".python-version" - name: Setup uv uses: astral-sh/setup-uv@v7 with: - version: "0.4.15" enable-cache: true cache-dependency-glob: | - requirements**.txt pyproject.toml + uv.lock - name: Install GitHub Actions dependencies - run: uv pip install -r requirements-github-actions.txt + run: uv sync --locked --no-dev --group github-actions - name: Update Topic Repos - run: python ./scripts/topic_repos.py + run: uv run ./scripts/topic_repos.py env: GITHUB_TOKEN: ${{ secrets.FASTAPI_PR_TOKEN }} diff --git a/.github/workflows/translate.yml b/.github/workflows/translate.yml index a7fcf84df1..83518614b0 100644 --- a/.github/workflows/translate.yml +++ b/.github/workflows/translate.yml @@ -1,6 +1,9 @@ name: Translate on: + # schedule: + # - cron: "0 5 15 * *" # Run at 05:00 on the 15 of every month + workflow_dispatch: inputs: debug_enabled: @@ -16,7 +19,7 @@ on: - update-outdated - add-missing - update-and-add - - remove-all-removable + - remove-removable language: description: Language to translate to as a letter code (e.g. "es" for Spanish) type: string @@ -27,14 +30,49 @@ on: type: string required: false default: "" - -env: - UV_SYSTEM_PYTHON: 1 + commit_in_place: + description: Commit changes directly instead of making a PR + type: boolean + required: false + default: false jobs: - job: - if: github.repository_owner == 'fastapi' + langs: runs-on: ubuntu-latest + outputs: + langs: ${{ steps.show-langs.outputs.langs }} + commands: ${{ steps.show-langs.outputs.commands }} + steps: + - uses: actions/checkout@v6 + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version-file: ".python-version" + - name: Setup uv + uses: astral-sh/setup-uv@v7 + with: + cache-dependency-glob: | + pyproject.toml + uv.lock + - name: Install Dependencies + run: uv sync --locked --no-dev --group github-actions --group translations + - name: Export Language Codes + id: show-langs + run: | + echo "langs=$(uv run ./scripts/translate.py llm-translatable-json)" >> $GITHUB_OUTPUT + echo "commands=$(uv run ./scripts/translate.py commands-json)" >> $GITHUB_OUTPUT + env: + LANGUAGE: ${{ github.event.inputs.language }} + COMMAND: ${{ github.event.inputs.command }} + + translate: + if: github.repository_owner == 'fastapi' + needs: langs + runs-on: ubuntu-latest + strategy: + matrix: + lang: ${{ fromJson(needs.langs.outputs.langs) }} + command: ${{ fromJson(needs.langs.outputs.commands) }} permissions: contents: write steps: @@ -42,21 +80,21 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 + with: + fetch-depth: 0 - name: Set up Python uses: actions/setup-python@v6 with: - python-version: "3.11" + python-version-file: ".python-version" - name: Setup uv uses: astral-sh/setup-uv@v7 with: - version: "0.4.15" - enable-cache: true cache-dependency-glob: | - requirements**.txt pyproject.toml + uv.lock - name: Install Dependencies - run: uv pip install -r requirements-github-actions.txt -r requirements-translations.txt + run: uv sync --locked --no-dev --group github-actions --group translations # Allow debugging with tmate - name: Setup tmate session uses: mxschmitt/action-tmate@v3 @@ -68,10 +106,12 @@ jobs: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - name: FastAPI Translate run: | - python ./scripts/translate.py ${{ github.event.inputs.command }} - python ./scripts/translate.py make-pr + uv run ./scripts/translate.py ${{ matrix.command }} + uv run ./scripts/translate.py make-pr env: GITHUB_TOKEN: ${{ secrets.FASTAPI_TRANSLATIONS }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - LANGUAGE: ${{ github.event.inputs.language }} + LANGUAGE: ${{ matrix.lang }} EN_PATH: ${{ github.event.inputs.en_path }} + COMMAND: ${{ matrix.command }} + COMMIT_IN_PLACE: ${{ github.event.inputs.commit_in_place }} diff --git a/.gitignore b/.gitignore index ef6364a9a7..243cdb93a5 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,5 @@ archive.zip # macOS .DS_Store + +.codspeed diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8e5eba4c4b..d88b70b7b6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,25 +1,66 @@ # 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 + - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 hooks: - - id: check-added-large-files - - id: check-toml - - id: check-yaml + - id: check-added-large-files + args: ['--maxkb=750'] + exclude: ^uv.lock$ + - 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.14.3 + - --unsafe + - id: end-of-file-fixer + - id: trailing-whitespace + + - repo: local hooks: - - id: ruff + - 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: add-permalinks-pages + language: unsupported + name: add-permalinks-pages + entry: uv run ./scripts/docs.py add-permalinks-pages 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 + - --update-existing + files: ^docs/en/docs/.*\.md$ + + - id: generate-readme + language: unsupported + name: generate README.md from index.md + entry: uv run ./scripts/docs.py generate-readme + files: ^docs/en/docs/index\.md|docs/en/data/sponsors\.yml|scripts/docs\.py$ + pass_filenames: false + + - id: update-languages + language: unsupported + name: update languages + entry: uv run ./scripts/docs.py update-languages + files: ^docs/.*|scripts/docs\.py$ + pass_filenames: false + + - id: ensure-non-translated + language: unsupported + name: ensure non-translated files are not modified + entry: uv run ./scripts/docs.py ensure-non-translated + files: ^docs/(?!en/).*|^scripts/docs\.py$ + pass_filenames: false + + - id: fix-translations + language: unsupported + name: fix translations + entry: uv run ./scripts/translation_fixer.py fix-pages + files: ^docs/(?!en/).*/docs/.*\.md$ diff --git a/.python-version b/.python-version new file mode 100644 index 0000000000..2c0733315e --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.11 diff --git a/README.md b/README.md index 09cd38da16..1057b86942 100644 --- a/README.md +++ b/README.md @@ -40,11 +40,16 @@ The key features are: * **Robust**: Get production-ready code. With automatic interactive documentation. * **Standards-based**: Based on (and fully compatible with) the open standards for APIs: OpenAPI (previously known as Swagger) and JSON Schema. -* estimation based on tests on an internal development team, building production applications. +* estimation based on tests conducted by an internal development team, building production applications. ## Sponsors +### Keystone Sponsor + + + +### Gold and Silver Sponsors @@ -56,6 +61,7 @@ The key features are: + @@ -114,6 +120,12 @@ The key features are: --- +## FastAPI mini documentary + +There's a FastAPI mini documentary released at the end of 2025, you can watch it online: + +FastAPI Mini Documentary + ## **Typer**, the FastAPI of CLIs @@ -447,6 +459,58 @@ For a more complete example including more features, see the FastAPI Cloud, go and join the waiting list if you haven't. 🚀 + +If you already have a **FastAPI Cloud** account (we invited you from the waiting list 😉), you can deploy your application with one command. + +Before deploying, make sure you are logged in: + +
+ +```console +$ fastapi login + +You are logged in to FastAPI Cloud 🚀 +``` + +
+ +Then deploy your app: + +
+ +```console +$ fastapi deploy + +Deploying to FastAPI Cloud... + +✅ Deployment successful! + +🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev +``` + +
+ +That's it! Now you can access your app at that URL. ✨ + +#### About FastAPI Cloud + +**FastAPI Cloud** is built by the same author and team behind **FastAPI**. + +It streamlines the process of **building**, **deploying**, and **accessing** an API with minimal effort. + +It brings the same **developer experience** of building apps with FastAPI to **deploying** them to the cloud. 🎉 + +FastAPI Cloud is the primary sponsor and funding provider for the *FastAPI and friends* open source projects. ✨ + +#### Deploy to other cloud providers + +FastAPI is open source and based on standards. You can deploy FastAPI apps to any cloud provider you choose. + +Follow your cloud provider's guides to deploy FastAPI apps with them. 🤓 + ## Performance Independent TechEmpower benchmarks show **FastAPI** applications running under Uvicorn as one of the fastest Python frameworks available, only below Starlette and Uvicorn themselves (used internally by FastAPI). (*) diff --git a/docs/de/docs/_llm-test.md b/docs/de/docs/_llm-test.md index 72846ef06d..bc7ce363c7 100644 --- a/docs/de/docs/_llm-test.md +++ b/docs/de/docs/_llm-test.md @@ -15,7 +15,7 @@ So verwenden: Die Tests: -## Codeschnipsel { #code-snippets} +## Codeschnipsel { #code-snippets } //// tab | Test @@ -53,7 +53,7 @@ Siehe zum Beispiel den Abschnitt `### Quotes` in `docs/de/llm-prompt.md`. //// -## Anführungszeichen in Codeschnipseln { #quotes-in-code-snippets} +## Anführungszeichen in Codeschnipseln { #quotes-in-code-snippets } //// tab | Test @@ -443,7 +443,7 @@ Für einige sprachspezifische Anweisungen, siehe z. B. den Abschnitt `### Headin * die Workload * das Deployment -* bereitstellen +* deployen * das SDK * das Software Development Kit diff --git a/docs/de/docs/advanced/additional-responses.md b/docs/de/docs/advanced/additional-responses.md index 218dd6c4fb..0f9b122516 100644 --- a/docs/de/docs/advanced/additional-responses.md +++ b/docs/de/docs/advanced/additional-responses.md @@ -26,7 +26,7 @@ Jedes dieser Response-`dict`s kann einen Schlüssel `model` haben, welcher ein P Um beispielsweise eine weitere Response mit dem Statuscode `404` und einem Pydantic-Modell `Message` zu deklarieren, können Sie schreiben: -{* ../../docs_src/additional_responses/tutorial001.py hl[18,22] *} +{* ../../docs_src/additional_responses/tutorial001_py39.py hl[18,22] *} /// note | Hinweis @@ -175,7 +175,7 @@ Sie können denselben `responses`-Parameter verwenden, um verschiedene Medientyp Sie können beispielsweise einen zusätzlichen Medientyp `image/png` hinzufügen und damit deklarieren, dass Ihre *Pfadoperation* ein JSON-Objekt (mit dem Medientyp `application/json`) oder ein PNG-Bild zurückgeben kann: -{* ../../docs_src/additional_responses/tutorial002.py hl[19:24,28] *} +{* ../../docs_src/additional_responses/tutorial002_py310.py hl[17:22,26] *} /// note | Hinweis @@ -203,7 +203,7 @@ Sie können beispielsweise eine Response mit dem Statuscode `404` deklarieren, d Und eine Response mit dem Statuscode `200`, die Ihr `response_model` verwendet, aber ein benutzerdefiniertes Beispiel (`example`) enthält: -{* ../../docs_src/additional_responses/tutorial003.py hl[20:31] *} +{* ../../docs_src/additional_responses/tutorial003_py39.py hl[20:31] *} Es wird alles kombiniert und in Ihre OpenAPI eingebunden und in der API-Dokumentation angezeigt: @@ -237,7 +237,7 @@ Mit dieser Technik können Sie einige vordefinierte Responses in Ihren *Pfadoper Zum Beispiel: -{* ../../docs_src/additional_responses/tutorial004.py hl[13:17,26] *} +{* ../../docs_src/additional_responses/tutorial004_py310.py hl[11:15,24] *} ## Weitere Informationen zu OpenAPI-Responses { #more-information-about-openapi-responses } diff --git a/docs/de/docs/advanced/advanced-dependencies.md b/docs/de/docs/advanced/advanced-dependencies.md index da5f28c7c2..e60df28834 100644 --- a/docs/de/docs/advanced/advanced-dependencies.md +++ b/docs/de/docs/advanced/advanced-dependencies.md @@ -70,12 +70,22 @@ Wenn Sie das hier alles verstanden haben, wissen Sie bereits, wie diese Sicherhe Sie benötigen diese technischen Details höchstwahrscheinlich nicht. -Diese Details sind hauptsächlich nützlich, wenn Sie eine FastAPI-Anwendung haben, die älter als 0.118.0 ist, und Sie auf Probleme mit Abhängigkeiten mit `yield` stoßen. +Diese Details sind hauptsächlich nützlich, wenn Sie eine FastAPI-Anwendung haben, die älter als 0.121.0 ist, und Sie auf Probleme mit Abhängigkeiten mit `yield` stoßen. /// Abhängigkeiten mit `yield` haben sich im Laufe der Zeit weiterentwickelt, um verschiedene Anwendungsfälle abzudecken und einige Probleme zu beheben, hier ist eine Zusammenfassung der Änderungen. +### Abhängigkeiten mit `yield` und `scope` { #dependencies-with-yield-and-scope } + +In Version 0.121.0 hat FastAPI Unterstützung für `Depends(scope="function")` für Abhängigkeiten mit `yield` hinzugefügt. + +Mit `Depends(scope="function")` wird der Exit-Code nach `yield` direkt nach dem Ende der *Pfadoperation-Funktion* ausgeführt, bevor die Response an den Client gesendet wird. + +Und bei Verwendung von `Depends(scope="request")` (dem Default) wird der Exit-Code nach `yield` ausgeführt, nachdem die Response gesendet wurde. + +Mehr dazu finden Sie in der Dokumentation zu [Abhängigkeiten mit `yield` – Frühes Beenden und `scope`](../tutorial/dependencies/dependencies-with-yield.md#early-exit-and-scope). + ### Abhängigkeiten mit `yield` und `StreamingResponse`, Technische Details { #dependencies-with-yield-and-streamingresponse-technical-details } Vor FastAPI 0.118.0 wurde bei Verwendung einer Abhängigkeit mit `yield` der Exit-Code nach der *Pfadoperation-Funktion* ausgeführt, aber unmittelbar bevor die Response gesendet wurde. @@ -134,7 +144,7 @@ Dies wurde in Version 0.110.0 geändert, um unbehandelten Speicherverbrauch durc ### Hintergrundtasks und Abhängigkeiten mit `yield`, Technische Details { #background-tasks-and-dependencies-with-yield-technical-details } -Vor FastAPI 0.106.0 war das Werfen von Exceptions nach `yield` nicht möglich, der Exit-Code in Abhängigkeiten mit `yield` wurde ausgeführt, nachdem die Response gesendet wurde, sodass [Exceptionhandler](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank} bereits ausgeführt worden wären. +Vor FastAPI 0.106.0 war das Werfen von Exceptions nach `yield` nicht möglich, der Exit-Code in Abhängigkeiten mit `yield` wurde ausgeführt, nachdem die Response gesendet wurde, sodass [Exceptionhandler](../tutorial/handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank} bereits ausgeführt worden wären. Dies war so designt, hauptsächlich um die Verwendung derselben von Abhängigkeiten „geyieldeten“ Objekte in Hintergrundtasks zu ermöglichen, da der Exit-Code erst ausgeführt wurde, nachdem die Hintergrundtasks abgeschlossen waren. diff --git a/docs/de/docs/advanced/async-tests.md b/docs/de/docs/advanced/async-tests.md index ad82452052..7206f136fd 100644 --- a/docs/de/docs/advanced/async-tests.md +++ b/docs/de/docs/advanced/async-tests.md @@ -32,11 +32,11 @@ Betrachten wir als einfaches Beispiel eine Dateistruktur ähnlich der in [Größ Die Datei `main.py` hätte als Inhalt: -{* ../../docs_src/async_tests/main.py *} +{* ../../docs_src/async_tests/app_a_py39/main.py *} Die Datei `test_main.py` hätte die Tests für `main.py`, das könnte jetzt so aussehen: -{* ../../docs_src/async_tests/test_main.py *} +{* ../../docs_src/async_tests/app_a_py39/test_main.py *} ## Es ausführen { #run-it } @@ -56,7 +56,7 @@ $ pytest Der Marker `@pytest.mark.anyio` teilt pytest mit, dass diese Testfunktion asynchron aufgerufen werden soll: -{* ../../docs_src/async_tests/test_main.py hl[7] *} +{* ../../docs_src/async_tests/app_a_py39/test_main.py hl[7] *} /// tip | Tipp @@ -66,7 +66,7 @@ Beachten Sie, dass die Testfunktion jetzt `async def` ist und nicht nur `def` wi Dann können wir einen `AsyncClient` mit der App erstellen und mit `await` asynchrone Requests an ihn senden. -{* ../../docs_src/async_tests/test_main.py hl[9:12] *} +{* ../../docs_src/async_tests/app_a_py39/test_main.py hl[9:12] *} Das ist das Äquivalent zu: diff --git a/docs/de/docs/advanced/behind-a-proxy.md b/docs/de/docs/advanced/behind-a-proxy.md index 036916cbe2..1c74590503 100644 --- a/docs/de/docs/advanced/behind-a-proxy.md +++ b/docs/de/docs/advanced/behind-a-proxy.md @@ -44,7 +44,7 @@ $ fastapi run --forwarded-allow-ips="*" Angenommen, Sie definieren eine *Pfadoperation* `/items/`: -{* ../../docs_src/behind_a_proxy/tutorial001_01.py hl[6] *} +{* ../../docs_src/behind_a_proxy/tutorial001_01_py39.py hl[6] *} Wenn der Client versucht, zu `/items` zu gehen, würde er standardmäßig zu `/items/` umgeleitet. @@ -64,7 +64,7 @@ Wenn Sie mehr über HTTPS erfahren möchten, lesen Sie den Leitfaden [Über HTTP /// -### Wie Proxy-Forwarded-Header funktionieren +### Wie Proxy-Forwarded-Header funktionieren { #how-proxy-forwarded-headers-work } Hier ist eine visuelle Darstellung, wie der **Proxy** weitergeleitete Header zwischen dem Client und dem **Anwendungsserver** hinzufügt: @@ -115,7 +115,7 @@ In diesem Fall würde der ursprüngliche Pfad `/app` tatsächlich unter `/api/v1 Auch wenn Ihr gesamter Code unter der Annahme geschrieben ist, dass es nur `/app` gibt. -{* ../../docs_src/behind_a_proxy/tutorial001.py hl[6] *} +{* ../../docs_src/behind_a_proxy/tutorial001_py39.py hl[6] *} Und der Proxy würde das **Pfadpräfix** on-the-fly **„entfernen“**, bevor er den Request an den Anwendungsserver (wahrscheinlich Uvicorn via FastAPI CLI) übermittelt, dafür sorgend, dass Ihre Anwendung davon überzeugt ist, dass sie unter `/app` bereitgestellt wird, sodass Sie nicht Ihren gesamten Code dahingehend aktualisieren müssen, das Präfix `/api/v1` zu verwenden. @@ -193,7 +193,7 @@ Sie können den aktuellen `root_path` abrufen, der von Ihrer Anwendung für jede Hier fügen wir ihn, nur zu Demonstrationszwecken, in die Nachricht ein. -{* ../../docs_src/behind_a_proxy/tutorial001.py hl[8] *} +{* ../../docs_src/behind_a_proxy/tutorial001_py39.py hl[8] *} Wenn Sie Uvicorn dann starten mit: @@ -220,7 +220,7 @@ wäre die http://127.0.0.1:8000/app gehen, sehen Sie die normale Response: +Aber wenn Sie mit Ihrem Browser auf http://127.0.0.1:8000/app gehen, sehen Sie die normale Response: ```JSON { @@ -400,7 +400,7 @@ Wenn Sie eine benutzerdefinierte Liste von Servern (`servers`) übergeben und es Zum Beispiel: -{* ../../docs_src/behind_a_proxy/tutorial003.py hl[4:7] *} +{* ../../docs_src/behind_a_proxy/tutorial003_py39.py hl[4:7] *} Erzeugt ein OpenAPI-Schema, wie: @@ -443,11 +443,19 @@ Die Dokumentationsoberfläche interagiert mit dem von Ihnen ausgewählten Server /// +/// note | Technische Details + +Die Eigenschaft `servers` in der OpenAPI-Spezifikation ist optional. + +Wenn Sie den Parameter `servers` nicht angeben und `root_path` den Wert `/` hat, wird die Eigenschaft `servers` im generierten OpenAPI-Schema standardmäßig vollständig weggelassen, was dem Äquivalent eines einzelnen Servers mit einem `url`-Wert von `/` entspricht. + +/// + ### Den automatischen Server von `root_path` deaktivieren { #disable-automatic-server-from-root-path } Wenn Sie nicht möchten, dass **FastAPI** einen automatischen Server inkludiert, welcher `root_path` verwendet, können Sie den Parameter `root_path_in_servers=False` verwenden: -{* ../../docs_src/behind_a_proxy/tutorial004.py hl[9] *} +{* ../../docs_src/behind_a_proxy/tutorial004_py39.py hl[9] *} Dann wird er nicht in das OpenAPI-Schema aufgenommen. diff --git a/docs/de/docs/advanced/custom-response.md b/docs/de/docs/advanced/custom-response.md index 8714086e59..1226fcb20c 100644 --- a/docs/de/docs/advanced/custom-response.md +++ b/docs/de/docs/advanced/custom-response.md @@ -30,7 +30,7 @@ Das liegt daran, dass FastAPI standardmäßig jedes enthaltene Element überprü Wenn Sie jedoch sicher sind, dass der von Ihnen zurückgegebene Inhalt **mit JSON serialisierbar** ist, können Sie ihn direkt an die Response-Klasse übergeben und die zusätzliche Arbeit vermeiden, die FastAPI hätte, indem es Ihren zurückgegebenen Inhalt durch den `jsonable_encoder` leitet, bevor es ihn an die Response-Klasse übergibt. -{* ../../docs_src/custom_response/tutorial001b.py hl[2,7] *} +{* ../../docs_src/custom_response/tutorial001b_py39.py hl[2,7] *} /// info | Info @@ -55,7 +55,7 @@ Um eine Response mit HTML direkt von **FastAPI** zurückzugeben, verwenden Sie ` * Importieren Sie `HTMLResponse`. * Übergeben Sie `HTMLResponse` als den Parameter `response_class` Ihres *Pfadoperation-Dekorators*. -{* ../../docs_src/custom_response/tutorial002.py hl[2,7] *} +{* ../../docs_src/custom_response/tutorial002_py39.py hl[2,7] *} /// info | Info @@ -73,7 +73,7 @@ Wie in [Eine Response direkt zurückgeben](response-directly.md){.internal-link Das gleiche Beispiel von oben, das eine `HTMLResponse` zurückgibt, könnte so aussehen: -{* ../../docs_src/custom_response/tutorial003.py hl[2,7,19] *} +{* ../../docs_src/custom_response/tutorial003_py39.py hl[2,7,19] *} /// warning | Achtung @@ -97,7 +97,7 @@ Die `response_class` wird dann nur zur Dokumentation der OpenAPI-*Pfadoperation* Es könnte zum Beispiel so etwas sein: -{* ../../docs_src/custom_response/tutorial004.py hl[7,21,23] *} +{* ../../docs_src/custom_response/tutorial004_py39.py hl[7,21,23] *} In diesem Beispiel generiert die Funktion `generate_html_response()` bereits eine `Response` und gibt sie zurück, anstatt das HTML in einem `str` zurückzugeben. @@ -136,7 +136,7 @@ Sie akzeptiert die folgenden Parameter: FastAPI (eigentlich Starlette) fügt automatisch einen Content-Length-Header ein. Außerdem wird es einen Content-Type-Header einfügen, der auf dem media_type basiert, und für Texttypen einen Zeichensatz (charset) anfügen. -{* ../../docs_src/response_directly/tutorial002.py hl[1,18] *} +{* ../../docs_src/response_directly/tutorial002_py39.py hl[1,18] *} ### `HTMLResponse` { #htmlresponse } @@ -146,7 +146,7 @@ Nimmt Text oder Bytes entgegen und gibt eine HTML-Response zurück, wie Sie oben Nimmt Text oder Bytes entgegen und gibt eine Plain-Text-Response zurück. -{* ../../docs_src/custom_response/tutorial005.py hl[2,7,9] *} +{* ../../docs_src/custom_response/tutorial005_py39.py hl[2,7,9] *} ### `JSONResponse` { #jsonresponse } @@ -180,7 +180,7 @@ Dazu muss `ujson` installiert werden, z. B. mit `pip install ujson`. /// -{* ../../docs_src/custom_response/tutorial001.py hl[2,7] *} +{* ../../docs_src/custom_response/tutorial001_py39.py hl[2,7] *} /// tip | Tipp @@ -194,13 +194,13 @@ Gibt eine HTTP-Weiterleitung (HTTP-Redirect) zurück. Verwendet standardmäßig Sie können eine `RedirectResponse` direkt zurückgeben: -{* ../../docs_src/custom_response/tutorial006.py hl[2,9] *} +{* ../../docs_src/custom_response/tutorial006_py39.py hl[2,9] *} --- Oder Sie können sie im Parameter `response_class` verwenden: -{* ../../docs_src/custom_response/tutorial006b.py hl[2,7,9] *} +{* ../../docs_src/custom_response/tutorial006b_py39.py hl[2,7,9] *} Wenn Sie das tun, können Sie die URL direkt von Ihrer *Pfadoperation*-Funktion zurückgeben. @@ -210,13 +210,13 @@ In diesem Fall ist der verwendete `status_code` der Standardcode für die `Redir Sie können den Parameter `status_code` auch in Kombination mit dem Parameter `response_class` verwenden: -{* ../../docs_src/custom_response/tutorial006c.py hl[2,7,9] *} +{* ../../docs_src/custom_response/tutorial006c_py39.py hl[2,7,9] *} ### `StreamingResponse` { #streamingresponse } Nimmt einen asynchronen Generator oder einen normalen Generator/Iterator und streamt den Responsebody. -{* ../../docs_src/custom_response/tutorial007.py hl[2,14] *} +{* ../../docs_src/custom_response/tutorial007_py39.py hl[2,14] *} #### Verwendung von `StreamingResponse` mit dateiartigen Objekten { #using-streamingresponse-with-file-like-objects } @@ -226,7 +226,7 @@ Auf diese Weise müssen Sie nicht alles zuerst in den Arbeitsspeicher lesen und Das umfasst viele Bibliotheken zur Interaktion mit Cloud-Speicher, Videoverarbeitung und anderen. -{* ../../docs_src/custom_response/tutorial008.py hl[2,10:12,14] *} +{* ../../docs_src/custom_response/tutorial008_py39.py hl[2,10:12,14] *} 1. Das ist die Generatorfunktion. Es handelt sich um eine „Generatorfunktion“, da sie `yield`-Anweisungen enthält. 2. Durch die Verwendung eines `with`-Blocks stellen wir sicher, dass das dateiartige Objekt geschlossen wird, nachdem die Generatorfunktion fertig ist. Also, nachdem sie mit dem Senden der Response fertig ist. @@ -255,11 +255,11 @@ Nimmt zur Instanziierung einen anderen Satz von Argumenten entgegen als die ande Datei-Responses enthalten die entsprechenden `Content-Length`-, `Last-Modified`- und `ETag`-Header. -{* ../../docs_src/custom_response/tutorial009.py hl[2,10] *} +{* ../../docs_src/custom_response/tutorial009_py39.py hl[2,10] *} Sie können auch den Parameter `response_class` verwenden: -{* ../../docs_src/custom_response/tutorial009b.py hl[2,8,10] *} +{* ../../docs_src/custom_response/tutorial009b_py39.py hl[2,8,10] *} In diesem Fall können Sie den Dateipfad direkt von Ihrer *Pfadoperation*-Funktion zurückgeben. @@ -273,7 +273,7 @@ Sie möchten etwa, dass Ihre Response eingerücktes und formatiertes JSON zurüc Sie könnten eine `CustomORJSONResponse` erstellen. Das Wichtigste, was Sie tun müssen, ist, eine `Response.render(content)`-Methode zu erstellen, die den Inhalt als `bytes` zurückgibt: -{* ../../docs_src/custom_response/tutorial009c.py hl[9:14,17] *} +{* ../../docs_src/custom_response/tutorial009c_py39.py hl[9:14,17] *} Statt: @@ -299,7 +299,7 @@ Der Parameter, der das definiert, ist `default_response_class`. Im folgenden Beispiel verwendet **FastAPI** standardmäßig `ORJSONResponse` in allen *Pfadoperationen*, anstelle von `JSONResponse`. -{* ../../docs_src/custom_response/tutorial010.py hl[2,4] *} +{* ../../docs_src/custom_response/tutorial010_py39.py hl[2,4] *} /// tip | Tipp diff --git a/docs/de/docs/advanced/dataclasses.md b/docs/de/docs/advanced/dataclasses.md index 12ea8e9eca..52b9634aea 100644 --- a/docs/de/docs/advanced/dataclasses.md +++ b/docs/de/docs/advanced/dataclasses.md @@ -4,7 +4,7 @@ FastAPI basiert auf **Pydantic**, und ich habe Ihnen gezeigt, wie Sie Pydantic-M Aber FastAPI unterstützt auf die gleiche Weise auch die Verwendung von `dataclasses`: -{* ../../docs_src/dataclasses/tutorial001.py hl[1,7:12,19:20] *} +{* ../../docs_src/dataclasses_/tutorial001_py310.py hl[1,6:11,18:19] *} Das ist dank **Pydantic** ebenfalls möglich, da es `dataclasses` intern unterstützt. @@ -32,7 +32,7 @@ Wenn Sie jedoch eine Menge Datenklassen herumliegen haben, ist dies ein guter Tr Sie können `dataclasses` auch im Parameter `response_model` verwenden: -{* ../../docs_src/dataclasses/tutorial002.py hl[1,7:13,19] *} +{* ../../docs_src/dataclasses_/tutorial002_py310.py hl[1,6:12,18] *} Die Datenklasse wird automatisch in eine Pydantic-Datenklasse konvertiert. @@ -48,7 +48,7 @@ In einigen Fällen müssen Sie möglicherweise immer noch Pydantics Version von In diesem Fall können Sie einfach die Standard-`dataclasses` durch `pydantic.dataclasses` ersetzen, was einen direkten Ersatz darstellt: -{* ../../docs_src/dataclasses/tutorial003.py hl[1,5,8:11,14:17,23:25,28] *} +{* ../../docs_src/dataclasses_/tutorial003_py310.py hl[1,4,7:10,13:16,22:24,27] *} 1. Wir importieren `field` weiterhin von Standard-`dataclasses`. diff --git a/docs/de/docs/advanced/events.md b/docs/de/docs/advanced/events.md index f94526b4fc..7e1191b55e 100644 --- a/docs/de/docs/advanced/events.md +++ b/docs/de/docs/advanced/events.md @@ -30,7 +30,7 @@ Beginnen wir mit einem Beispiel und sehen es uns dann im Detail an. Wir erstellen eine asynchrone Funktion `lifespan()` mit `yield` wie folgt: -{* ../../docs_src/events/tutorial003.py hl[16,19] *} +{* ../../docs_src/events/tutorial003_py39.py hl[16,19] *} Hier simulieren wir den langsamen *Startup*, das Laden des Modells, indem wir die (Fake-)Modellfunktion vor dem `yield` in das Dictionary mit Modellen für maschinelles Lernen einfügen. Dieser Code wird ausgeführt, **bevor** die Anwendung **beginnt, Requests entgegenzunehmen**, während des *Startups*. @@ -48,7 +48,7 @@ Möglicherweise müssen Sie eine neue Version starten, oder Sie haben es einfach Das Erste, was auffällt, ist, dass wir eine asynchrone Funktion mit `yield` definieren. Das ist sehr ähnlich zu Abhängigkeiten mit `yield`. -{* ../../docs_src/events/tutorial003.py hl[14:19] *} +{* ../../docs_src/events/tutorial003_py39.py hl[14:19] *} Der erste Teil der Funktion, vor dem `yield`, wird ausgeführt **bevor** die Anwendung startet. @@ -60,7 +60,7 @@ Wie Sie sehen, ist die Funktion mit einem `@asynccontextmanager` versehen. Dadurch wird die Funktion in einen sogenannten „**asynchronen Kontextmanager**“ umgewandelt. -{* ../../docs_src/events/tutorial003.py hl[1,13] *} +{* ../../docs_src/events/tutorial003_py39.py hl[1,13] *} Ein **Kontextmanager** in Python ist etwas, das Sie in einer `with`-Anweisung verwenden können, zum Beispiel kann `open()` als Kontextmanager verwendet werden: @@ -82,7 +82,7 @@ In unserem obigen Codebeispiel verwenden wir ihn nicht direkt, sondern übergebe Der Parameter `lifespan` der `FastAPI`-App benötigt einen **asynchronen Kontextmanager**, wir können ihm also unseren neuen asynchronen Kontextmanager `lifespan` übergeben. -{* ../../docs_src/events/tutorial003.py hl[22] *} +{* ../../docs_src/events/tutorial003_py39.py hl[22] *} ## Alternative Events (deprecatet) { #alternative-events-deprecated } @@ -104,7 +104,7 @@ Diese Funktionen können mit `async def` oder normalem `def` deklariert werden. Um eine Funktion hinzuzufügen, die vor dem Start der Anwendung ausgeführt werden soll, deklarieren Sie diese mit dem Event `startup`: -{* ../../docs_src/events/tutorial001.py hl[8] *} +{* ../../docs_src/events/tutorial001_py39.py hl[8] *} In diesem Fall initialisiert die Eventhandler-Funktion `startup` die „Datenbank“ der Items (nur ein `dict`) mit einigen Werten. @@ -116,7 +116,7 @@ Und Ihre Anwendung empfängt erst dann Requests, wenn alle `startup`-Eventhandle Um eine Funktion hinzuzufügen, die beim Shutdown der Anwendung ausgeführt werden soll, deklarieren Sie sie mit dem Event `shutdown`: -{* ../../docs_src/events/tutorial002.py hl[6] *} +{* ../../docs_src/events/tutorial002_py39.py hl[6] *} Hier schreibt die `shutdown`-Eventhandler-Funktion eine Textzeile `"Application shutdown"` in eine Datei `log.txt`. diff --git a/docs/de/docs/advanced/generate-clients.md b/docs/de/docs/advanced/generate-clients.md index d8836295b6..659343f5bc 100644 --- a/docs/de/docs/advanced/generate-clients.md +++ b/docs/de/docs/advanced/generate-clients.md @@ -167,7 +167,7 @@ Aber für den generierten Client könnten wir die OpenAPI-Operation-IDs direkt v Wir könnten das OpenAPI-JSON in eine Datei `openapi.json` herunterladen und dann mit einem Skript wie dem folgenden **den präfixierten Tag entfernen**: -{* ../../docs_src/generate_clients/tutorial004.py *} +{* ../../docs_src/generate_clients/tutorial004_py39.py *} //// tab | Node.js diff --git a/docs/de/docs/advanced/middleware.md b/docs/de/docs/advanced/middleware.md index 8396a626b6..ccc6a64c3b 100644 --- a/docs/de/docs/advanced/middleware.md +++ b/docs/de/docs/advanced/middleware.md @@ -57,13 +57,13 @@ Erzwingt, dass alle eingehenden geparst, sondern direkt als `bytes` gelesen und die Funktion `magic_data_reader()` wäre dafür verantwortlich, ihn in irgendeiner Weise zu parsen. @@ -153,47 +153,15 @@ Und Sie könnten dies auch tun, wenn der Datentyp im Request nicht JSON ist. In der folgenden Anwendung verwenden wir beispielsweise weder die integrierte Funktionalität von FastAPI zum Extrahieren des JSON-Schemas aus Pydantic-Modellen noch die automatische Validierung für JSON. Tatsächlich deklarieren wir den Request-Content-Type als YAML und nicht als JSON: -//// tab | Pydantic v2 - -{* ../../docs_src/path_operation_advanced_configuration/tutorial007.py hl[17:22, 24] *} - -//// - -//// tab | Pydantic v1 - -{* ../../docs_src/path_operation_advanced_configuration/tutorial007_pv1.py hl[17:22, 24] *} - -//// - -/// info | Info - -In Pydantic Version 1 hieß die Methode zum Abrufen des JSON-Schemas für ein Modell `Item.schema()`, in Pydantic Version 2 heißt die Methode `Item.model_json_schema()`. - -/// +{* ../../docs_src/path_operation_advanced_configuration/tutorial007_py39.py hl[15:20, 22] *} Obwohl wir nicht die standardmäßig integrierte Funktionalität verwenden, verwenden wir dennoch ein Pydantic-Modell, um das JSON-Schema für die Daten, die wir in YAML empfangen möchten, manuell zu generieren. -Dann verwenden wir den Request direkt und extrahieren den Body als `bytes`. Das bedeutet, dass FastAPI nicht einmal versucht, den Request-Payload als JSON zu parsen. +Dann verwenden wir den Request direkt und extrahieren den Body als `bytes`. Das bedeutet, dass FastAPI nicht einmal versucht, die Request-Payload als JSON zu parsen. Und dann parsen wir in unserem Code diesen YAML-Inhalt direkt und verwenden dann wieder dasselbe Pydantic-Modell, um den YAML-Inhalt zu validieren: -//// tab | Pydantic v2 - -{* ../../docs_src/path_operation_advanced_configuration/tutorial007.py hl[26:33] *} - -//// - -//// tab | Pydantic v1 - -{* ../../docs_src/path_operation_advanced_configuration/tutorial007_pv1.py hl[26:33] *} - -//// - -/// info | Info - -In Pydantic Version 1 war die Methode zum Parsen und Validieren eines Objekts `Item.parse_obj()`, in Pydantic Version 2 heißt die Methode `Item.model_validate()`. - -/// +{* ../../docs_src/path_operation_advanced_configuration/tutorial007_py39.py hl[24:31] *} /// tip | Tipp diff --git a/docs/de/docs/advanced/response-change-status-code.md b/docs/de/docs/advanced/response-change-status-code.md index b079e241d1..b209c2d671 100644 --- a/docs/de/docs/advanced/response-change-status-code.md +++ b/docs/de/docs/advanced/response-change-status-code.md @@ -20,7 +20,7 @@ Sie können einen Parameter vom Typ `Response` in Ihrer *Pfadoperation-Funktion* Anschließend können Sie den `status_code` in diesem *vorübergehenden* Response-Objekt festlegen. -{* ../../docs_src/response_change_status_code/tutorial001.py hl[1,9,12] *} +{* ../../docs_src/response_change_status_code/tutorial001_py39.py hl[1,9,12] *} Und dann können Sie jedes benötigte Objekt zurückgeben, wie Sie es normalerweise tun würden (ein `dict`, ein Datenbankmodell usw.). diff --git a/docs/de/docs/advanced/response-cookies.md b/docs/de/docs/advanced/response-cookies.md index 02fe99c26a..87e636cfaf 100644 --- a/docs/de/docs/advanced/response-cookies.md +++ b/docs/de/docs/advanced/response-cookies.md @@ -6,7 +6,7 @@ Sie können einen Parameter vom Typ `Response` in Ihrer *Pfadoperation-Funktion* Und dann können Sie Cookies in diesem *vorübergehenden* Response-Objekt setzen. -{* ../../docs_src/response_cookies/tutorial002.py hl[1, 8:9] *} +{* ../../docs_src/response_cookies/tutorial002_py39.py hl[1, 8:9] *} Anschließend können Sie wie gewohnt jedes gewünschte Objekt zurückgeben (ein `dict`, ein Datenbankmodell, usw.). @@ -24,7 +24,7 @@ Dazu können Sie eine Response erstellen, wie unter [Eine Response direkt zurüc Setzen Sie dann Cookies darin und geben Sie sie dann zurück: -{* ../../docs_src/response_cookies/tutorial001.py hl[10:12] *} +{* ../../docs_src/response_cookies/tutorial001_py39.py hl[10:12] *} /// tip | Tipp diff --git a/docs/de/docs/advanced/response-directly.md b/docs/de/docs/advanced/response-directly.md index d995173730..0a28a6d0e2 100644 --- a/docs/de/docs/advanced/response-directly.md +++ b/docs/de/docs/advanced/response-directly.md @@ -34,7 +34,7 @@ Sie können beispielsweise kein Pydantic-Modell in eine `JSONResponse` einfügen In diesen Fällen können Sie den `jsonable_encoder` verwenden, um Ihre Daten zu konvertieren, bevor Sie sie an eine Response übergeben: -{* ../../docs_src/response_directly/tutorial001.py hl[6:7,21:22] *} +{* ../../docs_src/response_directly/tutorial001_py310.py hl[5:6,20:21] *} /// note | Technische Details @@ -54,7 +54,7 @@ Nehmen wir an, Sie möchten eine Response-Objekt festlegen. -{* ../../docs_src/response_headers/tutorial002.py hl[1, 7:8] *} +{* ../../docs_src/response_headers/tutorial002_py39.py hl[1, 7:8] *} Anschließend können Sie wie gewohnt jedes gewünschte Objekt zurückgeben (ein `dict`, ein Datenbankmodell, usw.). @@ -22,7 +22,7 @@ Sie können auch Header hinzufügen, wenn Sie eine `Response` direkt zurückgebe Erstellen Sie eine Response wie in [Eine Response direkt zurückgeben](response-directly.md){.internal-link target=_blank} beschrieben und übergeben Sie die Header als zusätzlichen Parameter: -{* ../../docs_src/response_headers/tutorial001.py hl[10:12] *} +{* ../../docs_src/response_headers/tutorial001_py39.py hl[10:12] *} /// note | Technische Details diff --git a/docs/de/docs/advanced/settings.md b/docs/de/docs/advanced/settings.md index ccd7f373dd..ea4540e10e 100644 --- a/docs/de/docs/advanced/settings.md +++ b/docs/de/docs/advanced/settings.md @@ -60,23 +60,7 @@ Auf die gleiche Weise wie bei Pydantic-Modellen deklarieren Sie Klassenattribute Sie können dieselben Validierungs-Funktionen und -Tools verwenden, die Sie für Pydantic-Modelle verwenden, z. B. verschiedene Datentypen und zusätzliche Validierungen mit `Field()`. -//// tab | Pydantic v2 - -{* ../../docs_src/settings/tutorial001.py hl[2,5:8,11] *} - -//// - -//// tab | Pydantic v1 - -/// info | Info - -In Pydantic v1 würden Sie `BaseSettings` direkt von `pydantic` statt von `pydantic_settings` importieren. - -/// - -{* ../../docs_src/settings/tutorial001_pv1.py hl[2,5:8,11] *} - -//// +{* ../../docs_src/settings/tutorial001_py39.py hl[2,5:8,11] *} /// tip | Tipp @@ -92,7 +76,7 @@ Als Nächstes werden die Daten konvertiert und validiert. Wenn Sie also dieses ` Dann können Sie das neue `settings`-Objekt in Ihrer Anwendung verwenden: -{* ../../docs_src/settings/tutorial001.py hl[18:20] *} +{* ../../docs_src/settings/tutorial001_py39.py hl[18:20] *} ### Den Server ausführen { #run-the-server } @@ -126,11 +110,11 @@ Sie könnten diese Einstellungen in eine andere Moduldatei einfügen, wie Sie in Sie könnten beispielsweise eine Datei `config.py` haben mit: -{* ../../docs_src/settings/app01/config.py *} +{* ../../docs_src/settings/app01_py39/config.py *} Und dann verwenden Sie diese in einer Datei `main.py`: -{* ../../docs_src/settings/app01/main.py hl[3,11:13] *} +{* ../../docs_src/settings/app01_py39/main.py hl[3,11:13] *} /// tip | Tipp @@ -148,7 +132,7 @@ Dies könnte besonders beim Testen nützlich sein, da es sehr einfach ist, eine Ausgehend vom vorherigen Beispiel könnte Ihre Datei `config.py` so aussehen: -{* ../../docs_src/settings/app02/config.py hl[10] *} +{* ../../docs_src/settings/app02_an_py39/config.py hl[10] *} Beachten Sie, dass wir jetzt keine Standardinstanz `settings = Settings()` erstellen. @@ -174,7 +158,7 @@ Und dann können wir das von der *Pfadoperation-Funktion* als Abhängigkeit einf Dann wäre es sehr einfach, beim Testen ein anderes Einstellungsobjekt bereitzustellen, indem man eine Abhängigkeitsüberschreibung für `get_settings` erstellt: -{* ../../docs_src/settings/app02/test_main.py hl[9:10,13,21] *} +{* ../../docs_src/settings/app02_an_py39/test_main.py hl[9:10,13,21] *} Bei der Abhängigkeitsüberschreibung legen wir einen neuen Wert für `admin_email` fest, wenn wir das neue `Settings`-Objekt erstellen, und geben dann dieses neue Objekt zurück. @@ -215,9 +199,7 @@ APP_NAME="ChimichangApp" Und dann aktualisieren Sie Ihre `config.py` mit: -//// tab | Pydantic v2 - -{* ../../docs_src/settings/app03_an/config.py hl[9] *} +{* ../../docs_src/settings/app03_an_py39/config.py hl[9] *} /// tip | Tipp @@ -225,26 +207,6 @@ Das Attribut `model_config` wird nur für die Pydantic-Konfiguration verwendet. /// -//// - -//// tab | Pydantic v1 - -{* ../../docs_src/settings/app03_an/config_pv1.py hl[9:10] *} - -/// tip | Tipp - -Die Klasse `Config` wird nur für die Pydantic-Konfiguration verwendet. Weitere Informationen finden Sie unter Pydantic Model Config. - -/// - -//// - -/// info | Info - -In Pydantic Version 1 erfolgte die Konfiguration in einer internen Klasse `Config`, in Pydantic Version 2 erfolgt sie in einem Attribut `model_config`. Dieses Attribut akzeptiert ein `dict`. Um automatische Codevervollständigung und Inline-Fehlerberichte zu erhalten, können Sie `SettingsConfigDict` importieren und verwenden, um dieses `dict` zu definieren. - -/// - Hier definieren wir die Konfiguration `env_file` innerhalb Ihrer Pydantic-`Settings`-Klasse und setzen den Wert auf den Dateinamen mit der dotenv-Datei, die wir verwenden möchten. ### Die `Settings` nur einmal laden mittels `lru_cache` { #creating-the-settings-only-once-with-lru-cache } diff --git a/docs/de/docs/advanced/sub-applications.md b/docs/de/docs/advanced/sub-applications.md index d634aac23b..081574d0a9 100644 --- a/docs/de/docs/advanced/sub-applications.md +++ b/docs/de/docs/advanced/sub-applications.md @@ -10,7 +10,7 @@ Wenn Sie zwei unabhängige FastAPI-Anwendungen mit deren eigenen unabhängigen O Erstellen Sie zunächst die Hauptanwendung **FastAPI** und deren *Pfadoperationen*: -{* ../../docs_src/sub_applications/tutorial001.py hl[3, 6:8] *} +{* ../../docs_src/sub_applications/tutorial001_py39.py hl[3, 6:8] *} ### Unteranwendung { #sub-application } @@ -18,7 +18,7 @@ Erstellen Sie dann Ihre Unteranwendung und deren *Pfadoperationen*. Diese Unteranwendung ist nur eine weitere Standard-FastAPI-Anwendung, aber diese wird „gemountet“: -{* ../../docs_src/sub_applications/tutorial001.py hl[11, 14:16] *} +{* ../../docs_src/sub_applications/tutorial001_py39.py hl[11, 14:16] *} ### Die Unteranwendung mounten { #mount-the-sub-application } @@ -26,7 +26,7 @@ Mounten Sie in Ihrer Top-Level-Anwendung `app` die Unteranwendung `subapi`. In diesem Fall wird sie im Pfad `/subapi` gemountet: -{* ../../docs_src/sub_applications/tutorial001.py hl[11, 19] *} +{* ../../docs_src/sub_applications/tutorial001_py39.py hl[11, 19] *} ### Die automatische API-Dokumentation testen { #check-the-automatic-api-docs } diff --git a/docs/de/docs/advanced/templates.md b/docs/de/docs/advanced/templates.md index 65c7998b8a..97a45e6126 100644 --- a/docs/de/docs/advanced/templates.md +++ b/docs/de/docs/advanced/templates.md @@ -27,7 +27,7 @@ $ pip install jinja2 * Deklarieren Sie einen `Request`-Parameter in der *Pfadoperation*, welcher ein Template zurückgibt. * Verwenden Sie die von Ihnen erstellten `templates`, um eine `TemplateResponse` zu rendern und zurückzugeben, übergeben Sie den Namen des Templates, das Requestobjekt und ein „Kontext“-Dictionary mit Schlüssel-Wert-Paaren, die innerhalb des Jinja2-Templates verwendet werden sollen. -{* ../../docs_src/templates/tutorial001.py hl[4,11,15:18] *} +{* ../../docs_src/templates/tutorial001_py39.py hl[4,11,15:18] *} /// note | Hinweis diff --git a/docs/de/docs/advanced/testing-events.md b/docs/de/docs/advanced/testing-events.md index 569518c51d..5b12f3f186 100644 --- a/docs/de/docs/advanced/testing-events.md +++ b/docs/de/docs/advanced/testing-events.md @@ -2,11 +2,11 @@ Wenn Sie `lifespan` in Ihren Tests ausführen müssen, können Sie den `TestClient` mit einer `with`-Anweisung verwenden: -{* ../../docs_src/app_testing/tutorial004.py hl[9:15,18,27:28,30:32,41:43] *} +{* ../../docs_src/app_testing/tutorial004_py39.py hl[9:15,18,27:28,30:32,41:43] *} Sie können mehr Details unter [„Lifespan in Tests ausführen in der offiziellen Starlette-Dokumentation.“](https://www.starlette.dev/lifespan/#running-lifespan-in-tests) nachlesen. Für die deprecateten Events `startup` und `shutdown` können Sie den `TestClient` wie folgt verwenden: -{* ../../docs_src/app_testing/tutorial003.py hl[9:12,20:24] *} +{* ../../docs_src/app_testing/tutorial003_py39.py hl[9:12,20:24] *} diff --git a/docs/de/docs/advanced/testing-websockets.md b/docs/de/docs/advanced/testing-websockets.md index f25aa4fd04..9ecca7a4f7 100644 --- a/docs/de/docs/advanced/testing-websockets.md +++ b/docs/de/docs/advanced/testing-websockets.md @@ -4,7 +4,7 @@ Sie können den schon bekannten `TestClient` zum Testen von WebSockets verwenden Dazu verwenden Sie den `TestClient` in einer `with`-Anweisung, eine Verbindung zum WebSocket herstellend: -{* ../../docs_src/app_testing/tutorial002.py hl[27:31] *} +{* ../../docs_src/app_testing/tutorial002_py39.py hl[27:31] *} /// note | Hinweis diff --git a/docs/de/docs/advanced/using-request-directly.md b/docs/de/docs/advanced/using-request-directly.md index 8ec6741d07..36d73b8067 100644 --- a/docs/de/docs/advanced/using-request-directly.md +++ b/docs/de/docs/advanced/using-request-directly.md @@ -29,7 +29,7 @@ Angenommen, Sie möchten auf die IP-Adresse/den Host des Clients in Ihrer *Pfado Dazu müssen Sie direkt auf den Request zugreifen. -{* ../../docs_src/using_request_directly/tutorial001.py hl[1,7:8] *} +{* ../../docs_src/using_request_directly/tutorial001_py39.py hl[1,7:8] *} Durch die Deklaration eines *Pfadoperation-Funktionsparameters*, dessen Typ der `Request` ist, weiß **FastAPI**, dass es den `Request` diesem Parameter übergeben soll. diff --git a/docs/de/docs/advanced/websockets.md b/docs/de/docs/advanced/websockets.md index 5f662770f0..05ae5a4b31 100644 --- a/docs/de/docs/advanced/websockets.md +++ b/docs/de/docs/advanced/websockets.md @@ -38,13 +38,13 @@ In der Produktion hätten Sie eine der oben genannten Optionen. Aber es ist der einfachste Weg, sich auf die Serverseite von WebSockets zu konzentrieren und ein funktionierendes Beispiel zu haben: -{* ../../docs_src/websockets/tutorial001.py hl[2,6:38,41:43] *} +{* ../../docs_src/websockets/tutorial001_py39.py hl[2,6:38,41:43] *} ## Einen `websocket` erstellen { #create-a-websocket } Erstellen Sie in Ihrer **FastAPI**-Anwendung einen `websocket`: -{* ../../docs_src/websockets/tutorial001.py hl[1,46:47] *} +{* ../../docs_src/websockets/tutorial001_py39.py hl[1,46:47] *} /// note | Technische Details @@ -58,7 +58,7 @@ Sie könnten auch `from starlette.websockets import WebSocket` verwenden. In Ihrer WebSocket-Route können Sie Nachrichten `await`en und Nachrichten senden. -{* ../../docs_src/websockets/tutorial001.py hl[48:52] *} +{* ../../docs_src/websockets/tutorial001_py39.py hl[48:52] *} Sie können Binär-, Text- und JSON-Daten empfangen und senden. diff --git a/docs/de/docs/advanced/wsgi.md b/docs/de/docs/advanced/wsgi.md index 1de9739dde..3cd776a6ab 100644 --- a/docs/de/docs/advanced/wsgi.md +++ b/docs/de/docs/advanced/wsgi.md @@ -12,7 +12,7 @@ Wrappen Sie dann die WSGI-Anwendung (z. B. Flask) mit der Middleware. Und dann mounten Sie das auf einem Pfad. -{* ../../docs_src/wsgi/tutorial001.py hl[2:3,3] *} +{* ../../docs_src/wsgi/tutorial001_py39.py hl[2:3,3] *} ## Es testen { #check-it } diff --git a/docs/de/docs/deployment/cloud.md b/docs/de/docs/deployment/cloud.md index ca1ba3b3bf..ad3ff76dbf 100644 --- a/docs/de/docs/deployment/cloud.md +++ b/docs/de/docs/deployment/cloud.md @@ -1,16 +1,24 @@ -# FastAPI bei Cloudanbietern bereitstellen { #deploy-fastapi-on-cloud-providers } +# FastAPI bei Cloudanbietern deployen { #deploy-fastapi-on-cloud-providers } Sie können praktisch **jeden Cloudanbieter** verwenden, um Ihre FastAPI-Anwendung bereitzustellen. -In den meisten Fällen bieten die großen Cloudanbieter Anleitungen zum Bereitstellen von FastAPI an. +In den meisten Fällen bieten die großen Cloudanbieter Anleitungen zum Deployment von FastAPI an. + +## FastAPI Cloud { #fastapi-cloud } + +**FastAPI Cloud** wurde vom selben Autor und Team hinter **FastAPI** entwickelt. + +Es vereinfacht den Prozess des **Erstellens**, **Deployens** und **Zugreifens** auf eine API mit minimalem Aufwand. + +Es bringt die gleiche **Developer-Experience** beim Erstellen von Apps mit FastAPI auch zum **Deployment** in der Cloud. 🎉 + +FastAPI Cloud ist der Hauptsponsor und Finanzierungsgeber für die *FastAPI and friends* Open-Source-Projekte. ✨ ## Cloudanbieter – Sponsoren { #cloud-providers-sponsors } -Einige Cloudanbieter ✨ [**sponsern FastAPI**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨, dies stellt die kontinuierliche und gesunde **Entwicklung** von FastAPI und seinem **Ökosystem** sicher. +Einige andere Cloudanbieter ✨ [**sponsern FastAPI**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨ ebenfalls. 🙇 -Und es zeigt ihr wahres Engagement für FastAPI und seine **Community** (Sie), da sie Ihnen nicht nur einen **guten Service** bieten möchten, sondern auch sicherstellen möchten, dass Sie ein **gutes und gesundes Framework**, FastAPI, haben. 🙇 - -Vielleicht möchten Sie deren Dienste ausprobieren und deren Anleitungen folgen: +Sie könnten diese ebenfalls in Betracht ziehen, deren Anleitungen folgen und ihre Dienste ausprobieren: * Render * Railway diff --git a/docs/de/docs/deployment/concepts.md b/docs/de/docs/deployment/concepts.md index ef0f458a7e..dde922805f 100644 --- a/docs/de/docs/deployment/concepts.md +++ b/docs/de/docs/deployment/concepts.md @@ -1,6 +1,6 @@ # Deployment-Konzepte { #deployments-concepts } -Bei dem Deployment – der Bereitstellung – einer **FastAPI**-Anwendung, oder eigentlich jeder Art von Web-API, gibt es mehrere Konzepte, die Sie wahrscheinlich interessieren, und mithilfe der Sie die **am besten geeignete** Methode zur **Bereitstellung Ihrer Anwendung** finden können. +Bei dem Deployment – der Bereitstellung – einer **FastAPI**-Anwendung, oder eigentlich jeder Art von Web-API, gibt es mehrere Konzepte, die Sie wahrscheinlich interessieren, und mithilfe der Sie die **am besten geeignete** Methode zum **Deployment Ihrer Anwendung** finden können. Einige wichtige Konzepte sind: @@ -15,11 +15,11 @@ Wir werden sehen, wie diese sich auf das **Deployment** auswirken. Letztendlich besteht das ultimative Ziel darin, **Ihre API-Clients** auf **sichere** Weise zu versorgen, um **Unterbrechungen** zu vermeiden und die **Rechenressourcen** (z. B. entfernte Server/virtuelle Maschinen) so effizient wie möglich zu nutzen. 🚀 -Ich erzähle Ihnen hier etwas mehr über diese **Konzepte**, was Ihnen hoffentlich die **Intuition** gibt, die Sie benötigen, um zu entscheiden, wie Sie Ihre API in sehr unterschiedlichen Umgebungen bereitstellen, möglicherweise sogar in **zukünftigen**, die jetzt noch nicht existieren. +Ich erzähle Ihnen hier etwas mehr über diese **Konzepte**, was Ihnen hoffentlich die **Intuition** gibt, die Sie benötigen, um zu entscheiden, wie Sie Ihre API in sehr unterschiedlichen Umgebungen deployen, möglicherweise sogar in **zukünftigen**, die jetzt noch nicht existieren. -Durch die Berücksichtigung dieser Konzepte können Sie die beste Variante der Bereitstellung **Ihrer eigenen APIs** **evaluieren und konzipieren**. +Durch die Berücksichtigung dieser Konzepte können Sie die beste Variante des Deployments **Ihrer eigenen APIs** **evaluieren und konzipieren**. -In den nächsten Kapiteln werde ich Ihnen mehr **konkrete Rezepte** für die Bereitstellung von FastAPI-Anwendungen geben. +In den nächsten Kapiteln werde ich Ihnen mehr **konkrete Rezepte** für das Deployment von FastAPI-Anwendungen geben. Aber schauen wir uns zunächst einmal diese grundlegenden **konzeptionellen Ideen** an. Diese Konzepte gelten auch für jede andere Art von Web-API. 💡 @@ -271,7 +271,7 @@ In diesem Fall müssen Sie sich darüber keine Sorgen machen. 🤷 ### Beispiele für Strategien für Vorab-Schritte { #examples-of-previous-steps-strategies } -Es hängt **stark** davon ab, wie Sie **Ihr System bereitstellen**, und hängt wahrscheinlich mit der Art und Weise zusammen, wie Sie Programme starten, Neustarts durchführen, usw. +Es hängt **stark** davon ab, wie Sie **Ihr System deployen**, und hängt wahrscheinlich mit der Art und Weise zusammen, wie Sie Programme starten, Neustarts durchführen, usw. Hier sind einige mögliche Ideen: @@ -307,7 +307,7 @@ Sie können einfache Tools wie `htop` verwenden, um die in Ihrem Server verwende ## Zusammenfassung { #recap } -Sie haben hier einige der wichtigsten Konzepte gelesen, die Sie wahrscheinlich berücksichtigen müssen, wenn Sie entscheiden, wie Sie Ihre Anwendung bereitstellen: +Sie haben hier einige der wichtigsten Konzepte gelesen, die Sie wahrscheinlich berücksichtigen müssen, wenn Sie entscheiden, wie Sie Ihre Anwendung deployen: * Sicherheit – HTTPS * Beim Hochfahren ausführen diff --git a/docs/de/docs/deployment/docker.md b/docs/de/docs/deployment/docker.md index 52ac999132..d4b74635d3 100644 --- a/docs/de/docs/deployment/docker.md +++ b/docs/de/docs/deployment/docker.md @@ -1,6 +1,6 @@ # FastAPI in Containern – Docker { #fastapi-in-containers-docker } -Beim Deployment von FastAPI-Anwendungen besteht ein gängiger Ansatz darin, ein **Linux-Containerimage** zu erstellen. Normalerweise erfolgt dies mit **Docker**. Sie können dieses Containerimage dann auf eine von mehreren möglichen Arten bereitstellen. +Beim Deployment von FastAPI-Anwendungen besteht ein gängiger Ansatz darin, ein **Linux-Containerimage** zu erstellen. Normalerweise erfolgt dies mit **Docker**. Sie können dieses Containerimage dann auf eine von mehreren möglichen Arten deployen. Die Verwendung von Linux-Containern bietet mehrere Vorteile, darunter **Sicherheit**, **Replizierbarkeit**, **Einfachheit** und andere. @@ -40,7 +40,7 @@ Linux-Container werden mit demselben Linux-Kernel des Hosts (Maschine, virtuelle Auf diese Weise verbrauchen Container **wenig Ressourcen**, eine Menge vergleichbar mit der direkten Ausführung der Prozesse (eine virtuelle Maschine würde viel mehr verbrauchen). -Container verfügen außerdem über ihre eigenen **isoliert** laufenden Prozesse (üblicherweise nur einen Prozess), über ihr eigenes Dateisystem und ihr eigenes Netzwerk, was die Bereitstellung, Sicherheit, Entwicklung usw. vereinfacht. +Container verfügen außerdem über ihre eigenen **isoliert** laufenden Prozesse (üblicherweise nur einen Prozess), über ihr eigenes Dateisystem und ihr eigenes Netzwerk, was Deployment, Sicherheit, Entwicklung usw. vereinfacht. ## Was ist ein Containerimage { #what-is-a-container-image } @@ -598,7 +598,7 @@ Zum Beispiel: * Mit einem **Kubernetes**-Cluster * Mit einem Docker Swarm Mode-Cluster * Mit einem anderen Tool wie Nomad -* Mit einem Cloud-Dienst, der Ihr Containerimage nimmt und es bereitstellt +* Mit einem Cloud-Dienst, der Ihr Containerimage nimmt und es deployt ## Docker-Image mit `uv` { #docker-image-with-uv } diff --git a/docs/de/docs/deployment/fastapicloud.md b/docs/de/docs/deployment/fastapicloud.md new file mode 100644 index 0000000000..18c3bb8a44 --- /dev/null +++ b/docs/de/docs/deployment/fastapicloud.md @@ -0,0 +1,65 @@ +# FastAPI Cloud { #fastapi-cloud } + +Sie können Ihre FastAPI-App in der FastAPI Cloud mit **einem einzigen Befehl** deployen – tragen Sie sich in die Warteliste ein, falls noch nicht geschehen. 🚀 + +## Anmelden { #login } + +Stellen Sie sicher, dass Sie bereits ein **FastAPI-Cloud-Konto** haben (wir haben Sie von der Warteliste eingeladen 😉). + +Melden Sie sich dann an: + +
+ +```console +$ fastapi login + +You are logged in to FastAPI Cloud 🚀 +``` + +
+ +## Deployen { #deploy } + +Stellen Sie Ihre App jetzt mit **einem einzigen Befehl** bereit: + +
+ +```console +$ fastapi deploy + +Deploying to FastAPI Cloud... + +✅ Deployment successful! + +🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev +``` + +
+ +Das war’s! Jetzt können Sie Ihre App unter dieser URL aufrufen. ✨ + +## Über FastAPI Cloud { #about-fastapi-cloud } + +**FastAPI Cloud** wird vom gleichen Autor und Team hinter **FastAPI** entwickelt. + +Es vereinfacht den Prozess des **Erstellens**, **Deployens** und **Nutzens** einer API mit minimalem Aufwand. + +Es bringt die gleiche **Developer-Experience** beim Erstellen von Apps mit FastAPI auch zum **Deployment** in der Cloud. 🎉 + +Es kümmert sich außerdem um das meiste, was beim Deployen einer App nötig ist, zum Beispiel: + +* HTTPS +* Replikation, mit Autoscaling basierend auf Requests +* usw. + +FastAPI Cloud ist Hauptsponsor und Finanzierer der Open-Source-Projekte *FastAPI and friends*. ✨ + +## Bei anderen Cloudanbietern deployen { #deploy-to-other-cloud-providers } + +FastAPI ist Open Source und basiert auf Standards. Sie können FastAPI-Apps bei jedem Cloudanbieter Ihrer Wahl deployen. + +Folgen Sie den Anleitungen Ihres Cloudanbieters, um dort FastAPI-Apps zu deployen. 🤓 + +## Auf den eigenen Server deployen { #deploy-your-own-server } + +Ich werde Ihnen später in diesem **Deployment-Leitfaden** auch alle Details zeigen, sodass Sie verstehen, was passiert, was geschehen muss und wie Sie FastAPI-Apps selbst deployen können, auch auf Ihre eigenen Server. 🤓 diff --git a/docs/de/docs/deployment/index.md b/docs/de/docs/deployment/index.md index 65c76edcea..cb3e537463 100644 --- a/docs/de/docs/deployment/index.md +++ b/docs/de/docs/deployment/index.md @@ -14,7 +14,9 @@ Das steht im Gegensatz zu den **Entwicklungsphasen**, in denen Sie ständig den Es gibt mehrere Möglichkeiten, dies zu tun, abhängig von Ihrem spezifischen Anwendungsfall und den von Ihnen verwendeten Tools. -Sie könnten mithilfe einer Kombination von Tools selbst **einen Server bereitstellen**, Sie könnten einen **Cloud-Dienst** nutzen, der einen Teil der Arbeit für Sie erledigt, oder andere mögliche Optionen. +Sie könnten mithilfe einer Kombination von Tools selbst **einen Server deployen**, Sie könnten einen **Cloud-Dienst** nutzen, der einen Teil der Arbeit für Sie erledigt, oder andere mögliche Optionen. + +Zum Beispiel haben wir, das Team hinter FastAPI, **FastAPI Cloud** entwickelt, um das Deployment von FastAPI-Apps in der Cloud so reibungslos wie möglich zu gestalten, mit derselben Developer-Experience wie beim Arbeiten mit FastAPI. Ich zeige Ihnen einige der wichtigsten Konzepte, die Sie beim Deployment einer **FastAPI**-Anwendung wahrscheinlich berücksichtigen sollten (obwohl das meiste davon auch für jede andere Art von Webanwendung gilt). diff --git a/docs/de/docs/deployment/server-workers.md b/docs/de/docs/deployment/server-workers.md index 169ed822b4..7b68f1b1a6 100644 --- a/docs/de/docs/deployment/server-workers.md +++ b/docs/de/docs/deployment/server-workers.md @@ -11,7 +11,7 @@ Schauen wir uns die Deployment-Konzepte von früher noch einmal an: Bis zu diesem Punkt, in allen Tutorials in der Dokumentation, haben Sie wahrscheinlich ein **Serverprogramm** ausgeführt, zum Beispiel mit dem `fastapi`-Befehl, der Uvicorn startet, und einen **einzelnen Prozess** ausführt. -Wenn Sie Anwendungen bereitstellen, möchten Sie wahrscheinlich eine gewisse **Replikation von Prozessen**, um **mehrere Kerne** zu nutzen und mehr Requests bearbeiten zu können. +Wenn Sie Anwendungen deployen, möchten Sie wahrscheinlich eine gewisse **Replikation von Prozessen**, um **mehrere Kerne** zu nutzen und mehr Requests bearbeiten zu können. Wie Sie im vorherigen Kapitel über [Deployment-Konzepte](concepts.md){.internal-link target=_blank} gesehen haben, gibt es mehrere Strategien, die Sie anwenden können. diff --git a/docs/de/docs/fastapi-cli.md b/docs/de/docs/fastapi-cli.md index ab9c8373e9..86a797a9e6 100644 --- a/docs/de/docs/fastapi-cli.md +++ b/docs/de/docs/fastapi-cli.md @@ -66,7 +66,7 @@ Das Ausführen von `fastapi run` startet FastAPI standardmäßig im Produktionsm Standardmäßig ist **Autoreload** deaktiviert. Es horcht auch auf der IP-Adresse `0.0.0.0`, was alle verfügbaren IP-Adressen bedeutet, so wird es öffentlich zugänglich für jeden, der mit der Maschine kommunizieren kann. So würden Sie es normalerweise in der Produktion ausführen, beispielsweise in einem Container. -In den meisten Fällen würden (und sollten) Sie einen „Terminierungsproxy“ haben, der HTTPS für Sie verwaltet. Dies hängt davon ab, wie Sie Ihre Anwendung bereitstellen. Ihr Anbieter könnte dies für Sie erledigen, oder Sie müssen es selbst einrichten. +In den meisten Fällen würden (und sollten) Sie einen „Terminierungsproxy“ haben, der HTTPS für Sie verwaltet. Dies hängt davon ab, wie Sie Ihre Anwendung deployen. Ihr Anbieter könnte dies für Sie erledigen, oder Sie müssen es selbst einrichten. /// tip | Tipp diff --git a/docs/de/docs/how-to/authentication-error-status-code.md b/docs/de/docs/how-to/authentication-error-status-code.md new file mode 100644 index 0000000000..c743b54d9e --- /dev/null +++ b/docs/de/docs/how-to/authentication-error-status-code.md @@ -0,0 +1,17 @@ +# Alte 403-Authentifizierungsfehler-Statuscodes verwenden { #use-old-403-authentication-error-status-codes } + +Vor FastAPI-Version `0.122.0` verwendeten die integrierten Sicherheits-Utilities den HTTP-Statuscode `403 Forbidden`, wenn sie dem Client nach einer fehlgeschlagenen Authentifizierung einen Fehler zurückgaben. + +Ab FastAPI-Version `0.122.0` verwenden sie den passenderen HTTP-Statuscode `401 Unauthorized` und geben in der Response einen sinnvollen `WWW-Authenticate`-Header zurück, gemäß den HTTP-Spezifikationen, RFC 7235, RFC 9110. + +Aber falls Ihre Clients aus irgendeinem Grund vom alten Verhalten abhängen, können Sie darauf zurückgreifen, indem Sie in Ihren Sicherheitsklassen die Methode `make_not_authenticated_error` überschreiben. + +Sie können beispielsweise eine Unterklasse von `HTTPBearer` erstellen, die einen Fehler `403 Forbidden` zurückgibt, statt des Default-`401 Unauthorized`-Fehlers: + +{* ../../docs_src/authentication_error_status_code/tutorial001_an_py39.py hl[9:13] *} + +/// tip | Tipp + +Beachten Sie, dass die Funktion die Exception-Instanz zurückgibt; sie wirft sie nicht. Das Werfen erfolgt im restlichen internen Code. + +/// diff --git a/docs/de/docs/how-to/conditional-openapi.md b/docs/de/docs/how-to/conditional-openapi.md index f6a2fad3bc..6e665cc4c0 100644 --- a/docs/de/docs/how-to/conditional-openapi.md +++ b/docs/de/docs/how-to/conditional-openapi.md @@ -29,7 +29,7 @@ Sie können problemlos dieselben Pydantic-Einstellungen verwenden, um Ihre gener Zum Beispiel: -{* ../../docs_src/conditional_openapi/tutorial001.py hl[6,11] *} +{* ../../docs_src/conditional_openapi/tutorial001_py39.py hl[6,11] *} Hier deklarieren wir die Einstellung `openapi_url` mit dem gleichen Defaultwert `"/openapi.json"`. diff --git a/docs/de/docs/how-to/configure-swagger-ui.md b/docs/de/docs/how-to/configure-swagger-ui.md index 351cb996c5..1c3f5c0c5a 100644 --- a/docs/de/docs/how-to/configure-swagger-ui.md +++ b/docs/de/docs/how-to/configure-swagger-ui.md @@ -18,7 +18,7 @@ Ohne Änderung der Einstellungen ist die Syntaxhervorhebung standardmäßig akti Sie können sie jedoch deaktivieren, indem Sie `syntaxHighlight` auf `False` setzen: -{* ../../docs_src/configure_swagger_ui/tutorial001.py hl[3] *} +{* ../../docs_src/configure_swagger_ui/tutorial001_py39.py hl[3] *} ... und dann zeigt die Swagger-Oberfläche die Syntaxhervorhebung nicht mehr an: @@ -28,7 +28,7 @@ Sie können sie jedoch deaktivieren, indem Sie `syntaxHighlight` auf `False` set Auf die gleiche Weise könnten Sie das Theme der Syntaxhervorhebung mit dem Schlüssel `"syntaxHighlight.theme"` festlegen (beachten Sie, dass er einen Punkt in der Mitte hat): -{* ../../docs_src/configure_swagger_ui/tutorial002.py hl[3] *} +{* ../../docs_src/configure_swagger_ui/tutorial002_py39.py hl[3] *} Obige Konfiguration würde das Theme für die Farbe der Syntaxhervorhebung ändern: @@ -40,13 +40,13 @@ FastAPI enthält einige Defaultkonfigurationsparameter, die für die meisten Anw Es umfasst die folgenden Defaultkonfigurationen: -{* ../../fastapi/openapi/docs.py ln[8:23] hl[17:23] *} +{* ../../fastapi/openapi/docs.py ln[9:24] hl[18:24] *} Sie können jede davon überschreiben, indem Sie im Argument `swagger_ui_parameters` einen anderen Wert festlegen. Um beispielsweise `deepLinking` zu deaktivieren, könnten Sie folgende Einstellungen an `swagger_ui_parameters` übergeben: -{* ../../docs_src/configure_swagger_ui/tutorial003.py hl[3] *} +{* ../../docs_src/configure_swagger_ui/tutorial003_py39.py hl[3] *} ## Andere Parameter der Swagger-Oberfläche { #other-swagger-ui-parameters } diff --git a/docs/de/docs/how-to/custom-docs-ui-assets.md b/docs/de/docs/how-to/custom-docs-ui-assets.md index 6b8b1a1767..6b1d654adf 100644 --- a/docs/de/docs/how-to/custom-docs-ui-assets.md +++ b/docs/de/docs/how-to/custom-docs-ui-assets.md @@ -18,7 +18,7 @@ Der erste Schritt besteht darin, die automatischen Dokumentationen zu deaktivier Um diese zu deaktivieren, setzen Sie deren URLs beim Erstellen Ihrer `FastAPI`-App auf `None`: -{* ../../docs_src/custom_docs_ui/tutorial001.py hl[8] *} +{* ../../docs_src/custom_docs_ui/tutorial001_py39.py hl[8] *} ### Die benutzerdefinierten Dokumentationen hinzufügen { #include-the-custom-docs } @@ -34,7 +34,7 @@ Sie können die internen Funktionen von FastAPI wiederverwenden, um die HTML-Sei Und ähnlich für ReDoc ... -{* ../../docs_src/custom_docs_ui/tutorial001.py hl[2:6,11:19,22:24,27:33] *} +{* ../../docs_src/custom_docs_ui/tutorial001_py39.py hl[2:6,11:19,22:24,27:33] *} /// tip | Tipp @@ -50,7 +50,7 @@ Swagger UI erledigt das hinter den Kulissen für Sie, benötigt aber diesen „U Um nun testen zu können, ob alles funktioniert, erstellen Sie eine *Pfadoperation*: -{* ../../docs_src/custom_docs_ui/tutorial001.py hl[36:38] *} +{* ../../docs_src/custom_docs_ui/tutorial001_py39.py hl[36:38] *} ### Es testen { #test-it } @@ -118,7 +118,7 @@ Danach könnte Ihre Dateistruktur wie folgt aussehen: * Importieren Sie `StaticFiles`. * „Mounten“ Sie eine `StaticFiles()`-Instanz in einem bestimmten Pfad. -{* ../../docs_src/custom_docs_ui/tutorial002.py hl[7,11] *} +{* ../../docs_src/custom_docs_ui/tutorial002_py39.py hl[7,11] *} ### Die statischen Dateien testen { #test-the-static-files } @@ -144,7 +144,7 @@ Wie bei der Verwendung eines benutzerdefinierten CDN besteht der erste Schritt d Um sie zu deaktivieren, setzen Sie deren URLs beim Erstellen Ihrer `FastAPI`-App auf `None`: -{* ../../docs_src/custom_docs_ui/tutorial002.py hl[9] *} +{* ../../docs_src/custom_docs_ui/tutorial002_py39.py hl[9] *} ### Die benutzerdefinierten Dokumentationen für statische Dateien hinzufügen { #include-the-custom-docs-for-static-files } @@ -160,7 +160,7 @@ Auch hier können Sie die internen Funktionen von FastAPI wiederverwenden, um di Und ähnlich für ReDoc ... -{* ../../docs_src/custom_docs_ui/tutorial002.py hl[2:6,14:22,25:27,30:36] *} +{* ../../docs_src/custom_docs_ui/tutorial002_py39.py hl[2:6,14:22,25:27,30:36] *} /// tip | Tipp @@ -176,7 +176,7 @@ Swagger UI erledigt das hinter den Kulissen für Sie, benötigt aber diesen „U Um nun testen zu können, ob alles funktioniert, erstellen Sie eine *Pfadoperation*: -{* ../../docs_src/custom_docs_ui/tutorial002.py hl[39:41] *} +{* ../../docs_src/custom_docs_ui/tutorial002_py39.py hl[39:41] *} ### Benutzeroberfläche mit statischen Dateien testen { #test-static-files-ui } diff --git a/docs/de/docs/how-to/custom-request-and-route.md b/docs/de/docs/how-to/custom-request-and-route.md index 246717c04d..017de20967 100644 --- a/docs/de/docs/how-to/custom-request-and-route.md +++ b/docs/de/docs/how-to/custom-request-and-route.md @@ -42,7 +42,7 @@ Wenn der Header kein `gzip` enthält, wird nicht versucht, den Body zu dekomprim Auf diese Weise kann dieselbe Routenklasse gzip-komprimierte oder unkomprimierte Requests verarbeiten. -{* ../../docs_src/custom_request_and_route/tutorial001.py hl[8:15] *} +{* ../../docs_src/custom_request_and_route/tutorial001_an_py310.py hl[9:16] *} ### Eine benutzerdefinierte `GzipRoute`-Klasse erstellen { #create-a-custom-gziproute-class } @@ -54,7 +54,7 @@ Diese Methode gibt eine Funktion zurück. Und diese Funktion empfängt einen Requests verwendet. -{* ../../docs_src/extending_openapi/tutorial001.py hl[13:14,25:26] *} +{* ../../docs_src/extending_openapi/tutorial001_py39.py hl[13:14,25:26] *} ### Die Methode überschreiben { #override-the-method } Jetzt können Sie die Methode `.openapi()` durch Ihre neue Funktion ersetzen. -{* ../../docs_src/extending_openapi/tutorial001.py hl[29] *} +{* ../../docs_src/extending_openapi/tutorial001_py39.py hl[29] *} ### Es testen { #check-it } diff --git a/docs/de/docs/how-to/graphql.md b/docs/de/docs/how-to/graphql.md index d2958dcd9e..5c908cec4a 100644 --- a/docs/de/docs/how-to/graphql.md +++ b/docs/de/docs/how-to/graphql.md @@ -35,7 +35,7 @@ Abhängig von Ihrem Anwendungsfall könnten Sie eine andere Bibliothek vorziehen Hier ist eine kleine Vorschau, wie Sie Strawberry mit FastAPI integrieren können: -{* ../../docs_src/graphql/tutorial001.py hl[3,22,25] *} +{* ../../docs_src/graphql_/tutorial001_py39.py hl[3,22,25] *} Weitere Informationen zu Strawberry finden Sie in der Strawberry-Dokumentation. diff --git a/docs/de/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md b/docs/de/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md index 7f60492ee9..a8eff3b2b0 100644 --- a/docs/de/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md +++ b/docs/de/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md @@ -2,21 +2,23 @@ Wenn Sie eine ältere FastAPI-App haben, nutzen Sie möglicherweise Pydantic Version 1. -FastAPI unterstützt seit Version 0.100.0 sowohl Pydantic v1 als auch v2. +FastAPI Version 0.100.0 unterstützte sowohl Pydantic v1 als auch v2. Es verwendete, was auch immer Sie installiert hatten. -Wenn Sie Pydantic v2 installiert hatten, wurde dieses verwendet. Wenn stattdessen Pydantic v1 installiert war, wurde jenes verwendet. +FastAPI Version 0.119.0 führte eine teilweise Unterstützung für Pydantic v1 innerhalb von Pydantic v2 (als `pydantic.v1`) ein, um die Migration zu v2 zu erleichtern. -Pydantic v1 ist jetzt deprecatet und die Unterstützung dafür wird in den nächsten Versionen von FastAPI entfernt, Sie sollten also zu **Pydantic v2 migrieren**. Auf diese Weise erhalten Sie die neuesten Features, Verbesserungen und Fixes. +FastAPI 0.126.0 entfernte die Unterstützung für Pydantic v1, während `pydantic.v1` noch eine Weile unterstützt wurde. /// warning | Achtung -Außerdem hat das Pydantic-Team die Unterstützung für Pydantic v1 in den neuesten Python-Versionen eingestellt, beginnend mit **Python 3.14**. +Das Pydantic-Team hat die Unterstützung für Pydantic v1 in den neuesten Python-Versionen eingestellt, beginnend mit **Python 3.14**. + +Dies schließt `pydantic.v1` ein, das unter Python 3.14 und höher nicht mehr unterstützt wird. Wenn Sie die neuesten Features von Python nutzen möchten, müssen Sie sicherstellen, dass Sie Pydantic v2 verwenden. /// -Wenn Sie eine ältere FastAPI-App mit Pydantic v1 haben, zeige ich Ihnen hier, wie Sie sie zu Pydantic v2 migrieren, und die **neuen Features in FastAPI 0.119.0**, die Ihnen bei einer schrittweisen Migration helfen. +Wenn Sie eine ältere FastAPI-App mit Pydantic v1 haben, zeige ich Ihnen hier, wie Sie sie zu Pydantic v2 migrieren, und die **Features in FastAPI 0.119.0**, die Ihnen bei einer schrittweisen Migration helfen. ## Offizieller Leitfaden { #official-guide } @@ -44,7 +46,7 @@ Danach können Sie die Tests ausführen und prüfen, ob alles funktioniert. Fall ## Pydantic v1 in v2 { #pydantic-v1-in-v2 } -Pydantic v2 enthält alles aus Pydantic v1 als Untermodul `pydantic.v1`. +Pydantic v2 enthält alles aus Pydantic v1 als Untermodul `pydantic.v1`. Dies wird aber in Versionen oberhalb von Python 3.13 nicht mehr unterstützt. Das bedeutet, Sie können die neueste Version von Pydantic v2 installieren und die alten Pydantic‑v1‑Komponenten aus diesem Untermodul importieren und verwenden, als hätten Sie das alte Pydantic v1 installiert. diff --git a/docs/de/docs/how-to/separate-openapi-schemas.md b/docs/de/docs/how-to/separate-openapi-schemas.md index 31653590b5..16f9c8a144 100644 --- a/docs/de/docs/how-to/separate-openapi-schemas.md +++ b/docs/de/docs/how-to/separate-openapi-schemas.md @@ -1,6 +1,6 @@ # Separate OpenAPI-Schemas für Eingabe und Ausgabe oder nicht { #separate-openapi-schemas-for-input-and-output-or-not } -Bei Verwendung von **Pydantic v2** ist die generierte OpenAPI etwas genauer und **korrekter** als zuvor. 😎 +Seit der Veröffentlichung von **Pydantic v2** ist die generierte OpenAPI etwas genauer und **korrekter** als zuvor. 😎 Tatsächlich gibt es in einigen Fällen sogar **zwei JSON-Schemas** in OpenAPI für dasselbe Pydantic-Modell, für Eingabe und Ausgabe, je nachdem, ob sie **Defaultwerte** haben. @@ -100,5 +100,3 @@ Und jetzt wird es ein einziges Schema für die Eingabe und Ausgabe des Modells g
- -Dies ist das gleiche Verhalten wie in Pydantic v1. 🤓 diff --git a/docs/de/docs/index.md b/docs/de/docs/index.md index 4be65071bb..11fb6c9832 100644 --- a/docs/de/docs/index.md +++ b/docs/de/docs/index.md @@ -46,20 +46,26 @@ Seine Schlüssel-Merkmale sind: * **Robust**: Erhalten Sie produktionsreifen Code. Mit automatischer, interaktiver Dokumentation. * **Standards-basiert**: Basierend auf (und vollständig kompatibel mit) den offenen Standards für APIs: OpenAPI (früher bekannt als Swagger) und JSON Schema. -* Schätzung basierend auf Tests in einem internen Entwicklungsteam, das Produktionsanwendungen erstellt. +* Schätzung basierend auf Tests, die von einem internen Entwicklungsteam durchgeführt wurden, das Produktionsanwendungen erstellt. ## Sponsoren { #sponsors } -{% if sponsors %} +### Keystone-Sponsor { #keystone-sponsor } + +{% for sponsor in sponsors.keystone -%} + +{% endfor -%} + +### Gold- und Silber-Sponsoren { #gold-and-silver-sponsors } + {% for sponsor in sponsors.gold -%} {% endfor -%} {%- for sponsor in sponsors.silver -%} {% endfor %} -{% endif %} @@ -111,6 +117,12 @@ Seine Schlüssel-Merkmale sind: --- +## FastAPI Mini-Dokumentarfilm { #fastapi-mini-documentary } + +Es gibt einen FastAPI-Mini-Dokumentarfilm, veröffentlicht Ende 2025, Sie können ihn online ansehen: + +FastAPI Mini-Dokumentarfilm + ## **Typer**, das FastAPI der CLIs { #typer-the-fastapi-of-clis } @@ -227,7 +239,7 @@ INFO: Application startup complete.
-Was der Befehl fastapi dev main.py macht ... +Über den Befehl fastapi dev main.py ... Der Befehl `fastapi dev` liest Ihre `main.py`-Datei, erkennt die **FastAPI**-App darin und startet einen Server mit Uvicorn. @@ -270,7 +282,7 @@ Sie sehen die alternative automatische Dokumentation (bereitgestellt von Body eines `PUT`-Requests zu empfangen. @@ -320,7 +332,7 @@ Gehen Sie jetzt auf Dependency Injection**. +* Ein sehr leistungsfähiges und einfach zu bedienendes System für **Dependency Injection**. * Sicherheit und Authentifizierung, einschließlich Unterstützung für **OAuth2** mit **JWT-Tokens** und **HTTP Basic** Authentifizierung. * Fortgeschrittenere (aber ebenso einfache) Techniken zur Deklaration **tief verschachtelter JSON-Modelle** (dank Pydantic). * **GraphQL**-Integration mit Strawberry und anderen Bibliotheken. @@ -444,6 +456,58 @@ Für ein vollständigeres Beispiel, mit weiteren Funktionen, siehe das FastAPI Cloud deployen, gehen Sie und treten Sie der Warteliste bei, falls noch nicht geschehen. 🚀 + +Wenn Sie bereits ein **FastAPI Cloud**-Konto haben (wir haben Sie von der Warteliste eingeladen 😉), können Sie Ihre Anwendung mit einem einzigen Befehl deployen. + +Stellen Sie vor dem Deployen sicher, dass Sie eingeloggt sind: + +
+ +```console +$ fastapi login + +You are logged in to FastAPI Cloud 🚀 +``` + +
+ +Stellen Sie dann Ihre App bereit: + +
+ +```console +$ fastapi deploy + +Deploying to FastAPI Cloud... + +✅ Deployment successful! + +🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev +``` + +
+ +Das war’s! Jetzt können Sie unter dieser URL auf Ihre App zugreifen. ✨ + +#### Über FastAPI Cloud { #about-fastapi-cloud } + +**FastAPI Cloud** wird vom selben Autor und Team hinter **FastAPI** entwickelt. + +Es vereinfacht den Prozess des **Erstellens**, **Deployens** und **Zugreifens** auf eine API mit minimalem Aufwand. + +Es bringt die gleiche **Developer-Experience** beim Erstellen von Apps mit FastAPI auch zum **Deployment** in der Cloud. 🎉 + +FastAPI Cloud ist der Hauptsponsor und Finanzierer der *FastAPI and friends* Open-Source-Projekte. ✨ + +#### Bei anderen Cloudanbietern deployen { #deploy-to-other-cloud-providers } + +FastAPI ist Open Source und basiert auf Standards. Sie können FastAPI-Apps bei jedem Cloudanbieter Ihrer Wahl deployen. + +Folgen Sie den Anleitungen Ihres Cloudanbieters, um FastAPI-Apps dort bereitzustellen. 🤓 + ## Performanz { #performance } Unabhängige TechEmpower-Benchmarks zeigen **FastAPI**-Anwendungen, die unter Uvicorn laufen, als eines der schnellsten verfügbaren Python-Frameworks, nur hinter Starlette und Uvicorn selbst (intern von FastAPI verwendet). (*) diff --git a/docs/de/docs/project-generation.md b/docs/de/docs/project-generation.md index e6da4949c1..62702d8529 100644 --- a/docs/de/docs/project-generation.md +++ b/docs/de/docs/project-generation.md @@ -9,20 +9,20 @@ GitHub-Repository: Verkettet sie mit einem Leerzeichen in der Mitte. +* Verkettet sie mit einem Leerzeichen in der Mitte. -{* ../../docs_src/python_types/tutorial001.py hl[2] *} +{* ../../docs_src/python_types/tutorial001_py39.py hl[2] *} ### Es bearbeiten { #edit-it } @@ -78,7 +78,7 @@ Das war's. Das sind die „Typhinweise“: -{* ../../docs_src/python_types/tutorial002.py hl[1] *} +{* ../../docs_src/python_types/tutorial002_py39.py hl[1] *} Das ist nicht das gleiche wie das Deklarieren von Defaultwerten, wie es hier der Fall ist: @@ -106,7 +106,7 @@ Hier können Sie durch die Optionen blättern, bis Sie diejenige finden, bei der Sehen Sie sich diese Funktion an, sie hat bereits Typhinweise: -{* ../../docs_src/python_types/tutorial003.py hl[1] *} +{* ../../docs_src/python_types/tutorial003_py39.py hl[1] *} Da der Editor die Typen der Variablen kennt, erhalten Sie nicht nur Code-Vervollständigung, sondern auch eine Fehlerprüfung: @@ -114,7 +114,7 @@ Da der Editor die Typen der Variablen kennt, erhalten Sie nicht nur Code-Vervoll Jetzt, da Sie wissen, dass Sie das reparieren müssen, konvertieren Sie `age` mittels `str(age)` in einen String: -{* ../../docs_src/python_types/tutorial004.py hl[2] *} +{* ../../docs_src/python_types/tutorial004_py39.py hl[2] *} ## Deklarieren von Typen { #declaring-types } @@ -133,7 +133,7 @@ Zum Beispiel diese: * `bool` * `bytes` -{* ../../docs_src/python_types/tutorial005.py hl[1] *} +{* ../../docs_src/python_types/tutorial005_py39.py hl[1] *} ### Generische Typen mit Typ-Parametern { #generic-types-with-type-parameters } @@ -161,56 +161,24 @@ Wenn Sie über die **neueste Version von Python** verfügen, verwenden Sie die B Definieren wir zum Beispiel eine Variable, die eine `list` von `str` – eine Liste von Strings – sein soll. -//// tab | Python 3.9+ - Deklarieren Sie die Variable mit der gleichen Doppelpunkt-Syntax (`:`). Als Typ nehmen Sie `list`. Da die Liste ein Typ ist, welcher innere Typen enthält, werden diese von eckigen Klammern umfasst: -```Python hl_lines="1" -{!> ../../docs_src/python_types/tutorial006_py39.py!} -``` - -//// - -//// tab | Python 3.8+ - -Von `typing` importieren Sie `List` (mit Großbuchstaben `L`): - -```Python hl_lines="1" -{!> ../../docs_src/python_types/tutorial006.py!} -``` - -Deklarieren Sie die Variable mit der gleichen Doppelpunkt-Syntax (`:`). - -Als Typ nehmen Sie das `List`, das Sie von `typing` importiert haben. - -Da die Liste ein Typ ist, welcher innere Typen enthält, werden diese von eckigen Klammern umfasst: - -```Python hl_lines="4" -{!> ../../docs_src/python_types/tutorial006.py!} -``` - -//// +{* ../../docs_src/python_types/tutorial006_py39.py hl[1] *} /// info | Info Die inneren Typen in den eckigen Klammern werden als „Typ-Parameter“ bezeichnet. -In diesem Fall ist `str` der Typ-Parameter, der an `List` übergeben wird (oder `list` in Python 3.9 und darüber). +In diesem Fall ist `str` der Typ-Parameter, der an `list` übergeben wird. /// Das bedeutet: Die Variable `items` ist eine Liste – `list` – und jedes der Elemente in dieser Liste ist ein String – `str`. -/// tip | Tipp - -Wenn Sie Python 3.9 oder höher verwenden, müssen Sie `List` nicht von `typing` importieren, Sie können stattdessen den regulären `list`-Typ verwenden. - -/// - Auf diese Weise kann Ihr Editor Sie auch bei der Bearbeitung von Einträgen aus der Liste unterstützen: @@ -225,21 +193,7 @@ Und trotzdem weiß der Editor, dass es sich um ein `str` handelt, und bietet ent Das Gleiche gilt für die Deklaration eines Tupels – `tuple` – und einer Menge – `set`: -//// tab | Python 3.9+ - -```Python hl_lines="1" -{!> ../../docs_src/python_types/tutorial007_py39.py!} -``` - -//// - -//// tab | Python 3.8+ - -```Python hl_lines="1 4" -{!> ../../docs_src/python_types/tutorial007.py!} -``` - -//// +{* ../../docs_src/python_types/tutorial007_py39.py hl[1] *} Das bedeutet: @@ -254,21 +208,7 @@ Der erste Typ-Parameter ist für die Schlüssel des `dict`. Der zweite Typ-Parameter ist für die Werte des `dict`: -//// tab | Python 3.9+ - -```Python hl_lines="1" -{!> ../../docs_src/python_types/tutorial008_py39.py!} -``` - -//// - -//// tab | Python 3.8+ - -```Python hl_lines="1 4" -{!> ../../docs_src/python_types/tutorial008.py!} -``` - -//// +{* ../../docs_src/python_types/tutorial008_py39.py hl[1] *} Das bedeutet: @@ -282,7 +222,7 @@ Sie können deklarieren, dass eine Variable einer von **verschiedenen Typen** se In Python 3.6 und höher (inklusive Python 3.10) können Sie den `Union`-Typ von `typing` verwenden und die möglichen Typen innerhalb der eckigen Klammern auflisten. -In Python 3.10 gibt es zusätzlich eine **neue Syntax**, die es erlaubt, die möglichen Typen getrennt von einem vertikalen Balken (`|`) aufzulisten. +In Python 3.10 gibt es zusätzlich eine **neue Syntax**, die es erlaubt, die möglichen Typen getrennt von einem vertikalen Balken (`|`) aufzulisten. //// tab | Python 3.10+ @@ -292,10 +232,10 @@ In Python 3.10 gibt es zusätzlich eine **neue Syntax**, die es erlaubt, die mö //// -//// tab | Python 3.8+ +//// tab | Python 3.9+ ```Python hl_lines="1 4" -{!> ../../docs_src/python_types/tutorial008b.py!} +{!> ../../docs_src/python_types/tutorial008b_py39.py!} ``` //// @@ -309,7 +249,7 @@ Sie können deklarieren, dass ein Wert ein `str`, aber vielleicht auch `None` se In Python 3.6 und darüber (inklusive Python 3.10) können Sie das deklarieren, indem Sie `Optional` vom `typing` Modul importieren und verwenden. ```Python hl_lines="1 4" -{!../../docs_src/python_types/tutorial009.py!} +{!../../docs_src/python_types/tutorial009_py39.py!} ``` Wenn Sie `Optional[str]` anstelle von nur `str` verwenden, wird Ihr Editor Ihnen dabei helfen, Fehler zu erkennen, bei denen Sie annehmen könnten, dass ein Wert immer eine String (`str`) ist, obwohl er auch `None` sein könnte. @@ -326,18 +266,18 @@ Das bedeutet auch, dass Sie in Python 3.10 `Something | None` verwenden können: //// -//// tab | Python 3.8+ +//// tab | Python 3.9+ ```Python hl_lines="1 4" -{!> ../../docs_src/python_types/tutorial009.py!} +{!> ../../docs_src/python_types/tutorial009_py39.py!} ``` //// -//// tab | Python 3.8+ Alternative +//// tab | Python 3.9+ Alternative ```Python hl_lines="1 4" -{!> ../../docs_src/python_types/tutorial009b.py!} +{!> ../../docs_src/python_types/tutorial009b_py39.py!} ``` //// @@ -353,11 +293,11 @@ Beide sind äquivalent und im Hintergrund dasselbe, aber ich empfehle `Union` st Ich denke, `Union[SomeType, None]` ist expliziter bezüglich seiner Bedeutung. -Es geht nur um Wörter und Namen. Aber diese Worte können beeinflussen, wie Sie und Ihre Teamkollegen über den Code denken. +Es geht nur um Worte und Namen. Aber diese Worte können beeinflussen, wie Sie und Ihre Teamkollegen über den Code denken. Nehmen wir zum Beispiel diese Funktion: -{* ../../docs_src/python_types/tutorial009c.py hl[1,4] *} +{* ../../docs_src/python_types/tutorial009c_py39.py hl[1,4] *} Der Parameter `name` ist definiert als `Optional[str]`, aber er ist **nicht optional**, Sie können die Funktion nicht ohne diesen Parameter aufrufen: @@ -390,13 +330,13 @@ Sie können die eingebauten Typen als Generics verwenden (mit eckigen Klammern u * `set` * `dict` -Verwenden Sie für den Rest, wie unter Python 3.8, das `typing`-Modul: +Und ebenso wie bei früheren Python-Versionen, aus dem `typing`-Modul: * `Union` -* `Optional` (so wie unter Python 3.8) +* `Optional` * ... und andere. -In Python 3.10 können Sie als Alternative zu den Generics `Union` und `Optional` den vertikalen Balken (`|`) verwenden, um Vereinigungen von Typen zu deklarieren, das ist besser und einfacher. +In Python 3.10 können Sie als Alternative zu den Generics `Union` und `Optional` den vertikalen Balken (`|`) verwenden, um Vereinigungen von Typen zu deklarieren, das ist besser und einfacher. //// @@ -409,7 +349,7 @@ Sie können die eingebauten Typen als Generics verwenden (mit eckigen Klammern u * `set` * `dict` -Verwenden Sie für den Rest, wie unter Python 3.8, das `typing`-Modul: +Und Generics aus dem `typing`-Modul: * `Union` * `Optional` @@ -417,29 +357,17 @@ Verwenden Sie für den Rest, wie unter Python 3.8, das `typing`-Modul: //// -//// tab | Python 3.8+ - -* `List` -* `Tuple` -* `Set` -* `Dict` -* `Union` -* `Optional` -* ... und andere. - -//// - ### Klassen als Typen { #classes-as-types } Sie können auch eine Klasse als Typ einer Variablen deklarieren. Nehmen wir an, Sie haben eine Klasse `Person`, mit einem Namen: -{* ../../docs_src/python_types/tutorial010.py hl[1:3] *} +{* ../../docs_src/python_types/tutorial010_py39.py hl[1:3] *} Dann können Sie eine Variable vom Typ `Person` deklarieren: -{* ../../docs_src/python_types/tutorial010.py hl[6] *} +{* ../../docs_src/python_types/tutorial010_py39.py hl[6] *} Und wiederum bekommen Sie die volle Editor-Unterstützung: @@ -463,29 +391,7 @@ Und Sie erhalten volle Editor-Unterstützung für dieses Objekt. Ein Beispiel aus der offiziellen Pydantic Dokumentation: -//// tab | Python 3.10+ - -```Python -{!> ../../docs_src/python_types/tutorial011_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!> ../../docs_src/python_types/tutorial011_py39.py!} -``` - -//// - -//// tab | Python 3.8+ - -```Python -{!> ../../docs_src/python_types/tutorial011.py!} -``` - -//// +{* ../../docs_src/python_types/tutorial011_py310.py *} /// info | Info @@ -507,27 +413,9 @@ Pydantic verhält sich speziell, wenn Sie `Optional` oder `Union[Something, None Python bietet auch die Möglichkeit, **zusätzliche Metadaten** in Typhinweisen unterzubringen, mittels `Annotated`. -//// tab | Python 3.9+ +Seit Python 3.9 ist `Annotated` ein Teil der Standardbibliothek, Sie können es von `typing` importieren. -In Python 3.9 ist `Annotated` ein Teil der Standardbibliothek, Sie können es von `typing` importieren. - -```Python hl_lines="1 4" -{!> ../../docs_src/python_types/tutorial013_py39.py!} -``` - -//// - -//// tab | Python 3.8+ - -In Versionen niedriger als Python 3.9 importieren Sie `Annotated` von `typing_extensions`. - -Es wird bereits mit **FastAPI** installiert sein. - -```Python hl_lines="1 4" -{!> ../../docs_src/python_types/tutorial013.py!} -``` - -//// +{* ../../docs_src/python_types/tutorial013_py39.py hl[1,4] *} Python selbst macht nichts mit `Annotated`. Für Editoren und andere Tools ist der Typ immer noch `str`. diff --git a/docs/de/docs/resources/index.md b/docs/de/docs/resources/index.md index 2c5046c731..52b32b1480 100644 --- a/docs/de/docs/resources/index.md +++ b/docs/de/docs/resources/index.md @@ -1,3 +1,3 @@ # Ressourcen { #resources } -Zusätzliche Ressourcen, externe Links, Artikel und mehr. ✈️ +Zusätzliche Ressourcen, externe Links und mehr. ✈️ diff --git a/docs/de/docs/tutorial/background-tasks.md b/docs/de/docs/tutorial/background-tasks.md index 2c381ccfac..1d34430dc6 100644 --- a/docs/de/docs/tutorial/background-tasks.md +++ b/docs/de/docs/tutorial/background-tasks.md @@ -15,7 +15,7 @@ Hierzu zählen beispielsweise: Importieren Sie zunächst `BackgroundTasks` und definieren Sie einen Parameter in Ihrer *Pfadoperation-Funktion* mit der Typdeklaration `BackgroundTasks`: -{* ../../docs_src/background_tasks/tutorial001.py hl[1,13] *} +{* ../../docs_src/background_tasks/tutorial001_py39.py hl[1,13] *} **FastAPI** erstellt für Sie das Objekt vom Typ `BackgroundTasks` und übergibt es als diesen Parameter. @@ -31,13 +31,13 @@ In diesem Fall schreibt die Taskfunktion in eine Datei (den Versand einer E-Mail Und da der Schreibvorgang nicht `async` und `await` verwendet, definieren wir die Funktion mit normalem `def`: -{* ../../docs_src/background_tasks/tutorial001.py hl[6:9] *} +{* ../../docs_src/background_tasks/tutorial001_py39.py hl[6:9] *} ## Den Hintergrundtask hinzufügen { #add-the-background-task } Übergeben Sie innerhalb Ihrer *Pfadoperation-Funktion* Ihre Taskfunktion mit der Methode `.add_task()` an das *Hintergrundtasks*-Objekt: -{* ../../docs_src/background_tasks/tutorial001.py hl[14] *} +{* ../../docs_src/background_tasks/tutorial001_py39.py hl[14] *} `.add_task()` erhält als Argumente: diff --git a/docs/de/docs/tutorial/bigger-applications.md b/docs/de/docs/tutorial/bigger-applications.md index 928d50adff..963baf44d5 100644 --- a/docs/de/docs/tutorial/bigger-applications.md +++ b/docs/de/docs/tutorial/bigger-applications.md @@ -85,9 +85,7 @@ Sie können die *Pfadoperationen* für dieses Modul mit `APIRouter` erstellen. Sie importieren ihn und erstellen eine „Instanz“ auf die gleiche Weise wie mit der Klasse `FastAPI`: -```Python hl_lines="1 3" title="app/routers/users.py" -{!../../docs_src/bigger_applications/app/routers/users.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/routers/users.py hl[1,3] title["app/routers/users.py"] *} ### *Pfadoperationen* mit `APIRouter` { #path-operations-with-apirouter } @@ -95,9 +93,7 @@ Und dann verwenden Sie ihn, um Ihre *Pfadoperationen* zu deklarieren. Verwenden Sie ihn auf die gleiche Weise wie die Klasse `FastAPI`: -```Python hl_lines="6 11 16" title="app/routers/users.py" -{!../../docs_src/bigger_applications/app/routers/users.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/routers/users.py hl[6,11,16] title["app/routers/users.py"] *} Sie können sich `APIRouter` als eine „Mini-`FastAPI`“-Klasse vorstellen. @@ -121,35 +117,7 @@ Also fügen wir sie in ihr eigenes `dependencies`-Modul (`app/dependencies.py`) Wir werden nun eine einfache Abhängigkeit verwenden, um einen benutzerdefinierten `X-Token`-Header zu lesen: -//// tab | Python 3.9+ - -```Python hl_lines="3 6-8" title="app/dependencies.py" -{!> ../../docs_src/bigger_applications/app_an_py39/dependencies.py!} -``` - -//// - -//// tab | Python 3.8+ - -```Python hl_lines="1 5-7" title="app/dependencies.py" -{!> ../../docs_src/bigger_applications/app_an/dependencies.py!} -``` - -//// - -//// tab | Python 3.8+ nicht annotiert - -/// tip | Tipp - -Bevorzugen Sie die `Annotated`-Version, falls möglich. - -/// - -```Python hl_lines="1 4-6" title="app/dependencies.py" -{!> ../../docs_src/bigger_applications/app/dependencies.py!} -``` - -//// +{* ../../docs_src/bigger_applications/app_an_py39/dependencies.py hl[3,6:8] title["app/dependencies.py"] *} /// tip | Tipp @@ -181,9 +149,7 @@ Wir wissen, dass alle *Pfadoperationen* in diesem Modul folgendes haben: Anstatt also alles zu jeder *Pfadoperation* hinzuzufügen, können wir es dem `APIRouter` hinzufügen. -```Python hl_lines="5-10 16 21" title="app/routers/items.py" -{!../../docs_src/bigger_applications/app/routers/items.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/routers/items.py hl[5:10,16,21] title["app/routers/items.py"] *} Da der Pfad jeder *Pfadoperation* mit `/` beginnen muss, wie in: @@ -242,9 +208,7 @@ Und wir müssen die Abhängigkeitsfunktion aus dem Modul `app.dependencies` impo Daher verwenden wir einen relativen Import mit `..` für die Abhängigkeiten: -```Python hl_lines="3" title="app/routers/items.py" -{!../../docs_src/bigger_applications/app/routers/items.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/routers/items.py hl[3] title["app/routers/items.py"] *} #### Wie relative Importe funktionieren { #how-relative-imports-work } @@ -315,9 +279,7 @@ Wir fügen weder das Präfix `/items` noch `tags=["items"]` zu jeder *Pfadoperat Aber wir können immer noch _mehr_ `tags` hinzufügen, die auf eine bestimmte *Pfadoperation* angewendet werden, sowie einige zusätzliche `responses`, die speziell für diese *Pfadoperation* gelten: -```Python hl_lines="30-31" title="app/routers/items.py" -{!../../docs_src/bigger_applications/app/routers/items.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/routers/items.py hl[30:31] title["app/routers/items.py"] *} /// tip | Tipp @@ -343,17 +305,13 @@ Sie importieren und erstellen wie gewohnt eine `FastAPI`-Klasse. Und wir können sogar [globale Abhängigkeiten](dependencies/global-dependencies.md){.internal-link target=_blank} deklarieren, die mit den Abhängigkeiten für jeden `APIRouter` kombiniert werden: -```Python hl_lines="1 3 7" title="app/main.py" -{!../../docs_src/bigger_applications/app/main.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[1,3,7] title["app/main.py"] *} ### Den `APIRouter` importieren { #import-the-apirouter } Jetzt importieren wir die anderen Submodule, die `APIRouter` haben: -```Python hl_lines="4-5" title="app/main.py" -{!../../docs_src/bigger_applications/app/main.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[4:5] title["app/main.py"] *} Da es sich bei den Dateien `app/routers/users.py` und `app/routers/items.py` um Submodule handelt, die Teil desselben Python-Packages `app` sind, können wir einen einzelnen Punkt `.` verwenden, um sie mit „relativen Imports“ zu importieren. @@ -416,17 +374,13 @@ würde der `router` von `users` den von `items` überschreiben und wir könnten Um also beide in derselben Datei verwenden zu können, importieren wir die Submodule direkt: -```Python hl_lines="5" title="app/main.py" -{!../../docs_src/bigger_applications/app/main.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[5] title["app/main.py"] *} ### Die `APIRouter` für `users` und `items` inkludieren { #include-the-apirouters-for-users-and-items } Inkludieren wir nun die `router` aus diesen Submodulen `users` und `items`: -```Python hl_lines="10-11" title="app/main.py" -{!../../docs_src/bigger_applications/app/main.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[10:11] title["app/main.py"] *} /// info | Info @@ -466,17 +420,13 @@ Sie enthält einen `APIRouter` mit einigen administrativen *Pfadoperationen*, di In diesem Beispiel wird es ganz einfach sein. Nehmen wir jedoch an, dass wir, da sie mit anderen Projekten in der Organisation geteilt wird, sie nicht ändern und kein `prefix`, `dependencies`, `tags`, usw. direkt zum `APIRouter` hinzufügen können: -```Python hl_lines="3" title="app/internal/admin.py" -{!../../docs_src/bigger_applications/app/internal/admin.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/internal/admin.py hl[3] title["app/internal/admin.py"] *} Aber wir möchten immer noch ein benutzerdefiniertes `prefix` festlegen, wenn wir den `APIRouter` einbinden, sodass alle seine *Pfadoperationen* mit `/admin` beginnen, wir möchten es mit den `dependencies` sichern, die wir bereits für dieses Projekt haben, und wir möchten `tags` und `responses` hinzufügen. Wir können das alles deklarieren, ohne den ursprünglichen `APIRouter` ändern zu müssen, indem wir diese Parameter an `app.include_router()` übergeben: -```Python hl_lines="14-17" title="app/main.py" -{!../../docs_src/bigger_applications/app/main.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[14:17] title["app/main.py"] *} Auf diese Weise bleibt der ursprüngliche `APIRouter` unverändert, sodass wir dieselbe `app/internal/admin.py`-Datei weiterhin mit anderen Projekten in der Organisation teilen können. @@ -497,9 +447,7 @@ Wir können *Pfadoperationen* auch direkt zur `FastAPI`-App hinzufügen. Hier machen wir es ... nur um zu zeigen, dass wir es können 🤷: -```Python hl_lines="21-23" title="app/main.py" -{!../../docs_src/bigger_applications/app/main.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[21:23] title["app/main.py"] *} und es wird korrekt funktionieren, zusammen mit allen anderen *Pfadoperationen*, die mit `app.include_router()` hinzugefügt wurden. diff --git a/docs/de/docs/tutorial/body-nested-models.md b/docs/de/docs/tutorial/body-nested-models.md index 324d31928d..65a5d7c1de 100644 --- a/docs/de/docs/tutorial/body-nested-models.md +++ b/docs/de/docs/tutorial/body-nested-models.md @@ -14,35 +14,14 @@ Das bewirkt, dass `tags` eine Liste ist, wenngleich es nichts über den Typ der Aber Python erlaubt es, Listen mit inneren Typen, auch „Typ-Parameter“ genannt, zu deklarieren. -### `List` von `typing` importieren { #import-typings-list } - -In Python 3.9 oder darüber können Sie einfach `list` verwenden, um diese Typannotationen zu deklarieren, wie wir unten sehen werden. 💡 - -In Python-Versionen vor 3.9 (3.6 und darüber), müssen Sie zuerst `List` von Pythons Standardmodul `typing` importieren. - -{* ../../docs_src/body_nested_models/tutorial002.py hl[1] *} - ### Eine `list` mit einem Typ-Parameter deklarieren { #declare-a-list-with-a-type-parameter } -Um Typen wie `list`, `dict`, `tuple` mit inneren Typ-Parametern (inneren Typen) zu deklarieren: - -* Wenn Sie eine Python-Version kleiner als 3.9 verwenden, importieren Sie das Äquivalent zum entsprechenden Typ vom `typing`-Modul -* Überreichen Sie den/die inneren Typ(en) von eckigen Klammern umschlossen, `[` und `]`, als „Typ-Parameter“ - -In Python 3.9 wäre das: +Um Typen zu deklarieren, die Typ-Parameter (innere Typen) haben, wie `list`, `dict`, `tuple`, übergeben Sie den/die inneren Typ(en) als „Typ-Parameter“ in eckigen Klammern: `[` und `]` ```Python my_list: list[str] ``` -Und in Python-Versionen vor 3.9: - -```Python -from typing import List - -my_list: List[str] -``` - Das ist alles Standard-Python-Syntax für Typdeklarationen. Verwenden Sie dieselbe Standardsyntax für Modellattribute mit inneren Typen. @@ -178,12 +157,6 @@ Beachten Sie, wie `Offer` eine Liste von `Item`s hat, die ihrerseits eine option Wenn das äußerste Element des JSON-Bodys, das Sie erwarten, ein JSON-`array` (eine Python-`list`) ist, können Sie den Typ im Funktionsparameter deklarieren, mit der gleichen Syntax wie in Pydantic-Modellen: -```Python -images: List[Image] -``` - -oder in Python 3.9 und darüber: - ```Python images: list[Image] ``` diff --git a/docs/de/docs/tutorial/body-updates.md b/docs/de/docs/tutorial/body-updates.md index aa62199feb..d260998e91 100644 --- a/docs/de/docs/tutorial/body-updates.md +++ b/docs/de/docs/tutorial/body-updates.md @@ -50,14 +50,6 @@ Wenn Sie Teil-Aktualisierungen entgegennehmen, ist der `exclude_unset`-Parameter Wie in `item.model_dump(exclude_unset=True)`. -/// info | Info - -In Pydantic v1 hieß diese Methode `.dict()`, in Pydantic v2 wurde sie deprecatet (aber immer noch unterstützt) und in `.model_dump()` umbenannt. - -Die Beispiele hier verwenden `.dict()` für die Kompatibilität mit Pydantic v1, Sie sollten jedoch stattdessen `.model_dump()` verwenden, wenn Sie Pydantic v2 verwenden können. - -/// - Das wird ein `dict` erstellen, mit nur den Daten, die gesetzt wurden, als das `item`-Modell erstellt wurde, Defaultwerte ausgeschlossen. Sie können das verwenden, um ein `dict` zu erstellen, das nur die (im Request) gesendeten Daten enthält, ohne Defaultwerte: @@ -68,14 +60,6 @@ Sie können das verwenden, um ein `dict` zu erstellen, das nur die (im deprecatet (aber immer noch unterstützt) und in `.model_copy()` umbenannt. - -Die Beispiele hier verwenden `.copy()` für die Kompatibilität mit Pydantic v1, Sie sollten jedoch stattdessen `.model_copy()` verwenden, wenn Sie Pydantic v2 verwenden können. - -/// - Wie in `stored_item_model.model_copy(update=update_data)`: {* ../../docs_src/body_updates/tutorial002_py310.py hl[33] *} diff --git a/docs/de/docs/tutorial/body.md b/docs/de/docs/tutorial/body.md index 1e6382b6f2..cdf3122f2f 100644 --- a/docs/de/docs/tutorial/body.md +++ b/docs/de/docs/tutorial/body.md @@ -127,14 +127,6 @@ Innerhalb der Funktion können Sie alle Attribute des Modellobjekts direkt verwe {* ../../docs_src/body/tutorial002_py310.py *} -/// info | Info - -In Pydantic v1 hieß die Methode `.dict()`, sie wurde in Pydantic v2 deprecatet (aber weiterhin unterstützt) und in `.model_dump()` umbenannt. - -Die Beispiele hier verwenden `.dict()` zur Kompatibilität mit Pydantic v1, aber Sie sollten stattdessen `.model_dump()` verwenden, wenn Sie Pydantic v2 nutzen können. - -/// - ## Requestbody- + Pfad-Parameter { #request-body-path-parameters } Sie können Pfad-Parameter und den Requestbody gleichzeitig deklarieren. @@ -162,7 +154,7 @@ Die Funktionsparameter werden wie folgt erkannt: FastAPI weiß, dass der Wert von `q` nicht erforderlich ist, aufgrund des definierten Defaultwertes `= None`. -Das `str | None` (Python 3.10+) oder `Union` in `Union[str, None]` (Python 3.8+) wird von FastAPI nicht verwendet, um zu bestimmen, dass der Wert nicht erforderlich ist. FastAPI weiß, dass er nicht erforderlich ist, weil er einen Defaultwert von `= None` hat. +Das `str | None` (Python 3.10+) oder `Union` in `Union[str, None]` (Python 3.9+) wird von FastAPI nicht verwendet, um zu bestimmen, dass der Wert nicht erforderlich ist. FastAPI weiß, dass er nicht erforderlich ist, weil er einen Defaultwert von `= None` hat. Das Hinzufügen der Typannotationen ermöglicht jedoch Ihrem Editor, Ihnen eine bessere Unterstützung zu bieten und Fehler zu erkennen. diff --git a/docs/de/docs/tutorial/cookie-param-models.md b/docs/de/docs/tutorial/cookie-param-models.md index 2baf3d70dd..25718bd33a 100644 --- a/docs/de/docs/tutorial/cookie-param-models.md +++ b/docs/de/docs/tutorial/cookie-param-models.md @@ -50,7 +50,7 @@ Ihre API hat jetzt die Macht, ihre eigene Response**. diff --git a/docs/de/docs/tutorial/cors.md b/docs/de/docs/tutorial/cors.md index 191a7b4ef3..81f0f36051 100644 --- a/docs/de/docs/tutorial/cors.md +++ b/docs/de/docs/tutorial/cors.md @@ -46,7 +46,7 @@ Sie können auch angeben, ob Ihr Backend erlaubt: * Bestimmte HTTP-Methoden (`POST`, `PUT`) oder alle mit der Wildcard `"*"`. * Bestimmte HTTP-Header oder alle mit der Wildcard `"*"`. -{* ../../docs_src/cors/tutorial001.py hl[2,6:11,13:19] *} +{* ../../docs_src/cors/tutorial001_py39.py hl[2,6:11,13:19] *} Die von der `CORSMiddleware`-Implementierung verwendeten Defaultparameter sind standardmäßig restriktiv, daher müssen Sie bestimmte Origins, Methoden oder Header ausdrücklich aktivieren, damit Browser sie in einem Cross-Domain-Kontext verwenden dürfen. diff --git a/docs/de/docs/tutorial/debugging.md b/docs/de/docs/tutorial/debugging.md index 0a31f86536..0d12877c10 100644 --- a/docs/de/docs/tutorial/debugging.md +++ b/docs/de/docs/tutorial/debugging.md @@ -6,7 +6,7 @@ Sie können den Debugger in Ihrem Editor verbinden, zum Beispiel mit Visual Stud Importieren und führen Sie `uvicorn` direkt in Ihrer FastAPI-Anwendung aus: -{* ../../docs_src/debugging/tutorial001.py hl[1,15] *} +{* ../../docs_src/debugging/tutorial001_py39.py hl[1,15] *} ### Über `__name__ == "__main__"` { #about-name-main } diff --git a/docs/de/docs/tutorial/dependencies/classes-as-dependencies.md b/docs/de/docs/tutorial/dependencies/classes-as-dependencies.md index 3d4493f353..7df0842eb1 100644 --- a/docs/de/docs/tutorial/dependencies/classes-as-dependencies.md +++ b/docs/de/docs/tutorial/dependencies/classes-as-dependencies.md @@ -101,7 +101,7 @@ Jetzt können Sie Ihre Abhängigkeit mithilfe dieser Klasse deklarieren. Beachten Sie, wie wir `CommonQueryParams` im obigen Code zweimal schreiben: -//// tab | Python 3.8+ +//// tab | Python 3.9+ ```Python commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] @@ -109,7 +109,7 @@ commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] //// -//// tab | Python 3.8+ nicht annotiert +//// tab | Python 3.9+ nicht annotiert /// tip | Tipp @@ -137,7 +137,7 @@ Aus diesem extrahiert FastAPI die deklarierten Parameter, und dieses ist es, was In diesem Fall hat das erste `CommonQueryParams` in: -//// tab | Python 3.8+ +//// tab | Python 3.9+ ```Python commons: Annotated[CommonQueryParams, ... @@ -145,7 +145,7 @@ commons: Annotated[CommonQueryParams, ... //// -//// tab | Python 3.8+ nicht annotiert +//// tab | Python 3.9+ nicht annotiert /// tip | Tipp @@ -163,7 +163,7 @@ commons: CommonQueryParams ... Sie könnten tatsächlich einfach schreiben: -//// tab | Python 3.8+ +//// tab | Python 3.9+ ```Python commons: Annotated[Any, Depends(CommonQueryParams)] @@ -171,7 +171,7 @@ commons: Annotated[Any, Depends(CommonQueryParams)] //// -//// tab | Python 3.8+ nicht annotiert +//// tab | Python 3.9+ nicht annotiert /// tip | Tipp @@ -197,7 +197,7 @@ Es wird jedoch empfohlen, den Typ zu deklarieren, da Ihr Editor so weiß, was al Aber Sie sehen, dass wir hier etwas Codeduplizierung haben, indem wir `CommonQueryParams` zweimal schreiben: -//// tab | Python 3.8+ +//// tab | Python 3.9+ ```Python commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] @@ -205,7 +205,7 @@ commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] //// -//// tab | Python 3.8+ nicht annotiert +//// tab | Python 3.9+ nicht annotiert /// tip | Tipp @@ -225,7 +225,7 @@ In diesem speziellen Fall können Sie Folgendes tun: Anstatt zu schreiben: -//// tab | Python 3.8+ +//// tab | Python 3.9+ ```Python commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] @@ -233,7 +233,7 @@ commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] //// -//// tab | Python 3.8+ nicht annotiert +//// tab | Python 3.9+ nicht annotiert /// tip | Tipp @@ -249,7 +249,7 @@ commons: CommonQueryParams = Depends(CommonQueryParams) ... schreiben Sie: -//// tab | Python 3.8+ +//// tab | Python 3.9+ ```Python commons: Annotated[CommonQueryParams, Depends()] @@ -257,7 +257,7 @@ commons: Annotated[CommonQueryParams, Depends()] //// -//// tab | Python 3.8 nicht annotiert +//// tab | Python 3.9+ nicht annotiert /// tip | Tipp diff --git a/docs/de/docs/tutorial/dependencies/dependencies-with-yield.md b/docs/de/docs/tutorial/dependencies/dependencies-with-yield.md index e65b073a2e..0083e7e7ea 100644 --- a/docs/de/docs/tutorial/dependencies/dependencies-with-yield.md +++ b/docs/de/docs/tutorial/dependencies/dependencies-with-yield.md @@ -29,15 +29,15 @@ Sie könnten damit beispielsweise eine Datenbanksession erstellen und diese nach Nur der Code vor und einschließlich der `yield`-Anweisung wird ausgeführt, bevor eine Response erzeugt wird: -{* ../../docs_src/dependencies/tutorial007.py hl[2:4] *} +{* ../../docs_src/dependencies/tutorial007_py39.py hl[2:4] *} Der ge`yield`ete Wert ist das, was in *Pfadoperationen* und andere Abhängigkeiten eingefügt wird: -{* ../../docs_src/dependencies/tutorial007.py hl[4] *} +{* ../../docs_src/dependencies/tutorial007_py39.py hl[4] *} Der auf die `yield`-Anweisung folgende Code wird nach der Response ausgeführt: -{* ../../docs_src/dependencies/tutorial007.py hl[5:6] *} +{* ../../docs_src/dependencies/tutorial007_py39.py hl[5:6] *} /// tip | Tipp @@ -57,7 +57,7 @@ Sie können also mit `except SomeException` diese bestimmte Exception innerhalb Auf die gleiche Weise können Sie `finally` verwenden, um sicherzustellen, dass die Exit-Schritte ausgeführt werden, unabhängig davon, ob eine Exception geworfen wurde oder nicht. -{* ../../docs_src/dependencies/tutorial007.py hl[3,5] *} +{* ../../docs_src/dependencies/tutorial007_py39.py hl[3,5] *} ## Unterabhängigkeiten mit `yield` { #sub-dependencies-with-yield } @@ -184,6 +184,51 @@ Wenn Sie in dem Code der *Pfadoperation-Funktion* irgendeine Exception auslösen /// +## Frühes Beenden und `scope` { #early-exit-and-scope } + +Normalerweise wird der Exit-Code von Abhängigkeiten mit `yield` ausgeführt **nachdem die Response** an den Client gesendet wurde. + +Wenn Sie aber wissen, dass Sie die Abhängigkeit nach der Rückkehr aus der *Pfadoperation-Funktion* nicht mehr benötigen, können Sie `Depends(scope="function")` verwenden, um FastAPI mitzuteilen, dass es die Abhängigkeit nach der Rückkehr aus der *Pfadoperation-Funktion* schließen soll, jedoch **bevor** die **Response gesendet wird**. + +{* ../../docs_src/dependencies/tutorial008e_an_py39.py hl[12,16] *} + +`Depends()` erhält einen `scope`-Parameter, der sein kann: + +* `"function"`: startet die Abhängigkeit vor der *Pfadoperation-Funktion*, die den Request bearbeitet, beendet die Abhängigkeit nach dem Ende der *Pfadoperation-Funktion*, aber **bevor** die Response an den Client zurückgesendet wird. Die Abhängigkeitsfunktion wird also **um** die *Pfadoperation-**Funktion*** **herum** ausgeführt. +* `"request"`: startet die Abhängigkeit vor der *Pfadoperation-Funktion*, die den Request bearbeitet (ähnlich wie bei `"function"`), beendet sie jedoch **nachdem** die Response an den Client zurückgesendet wurde. Die Abhängigkeitsfunktion wird also **um** den **Request**- und Response-Zyklus **herum** ausgeführt. + +Wenn nicht angegeben und die Abhängigkeit `yield` hat, hat sie standardmäßig einen `scope` von `"request"`. + +### `scope` für Unterabhängigkeiten { #scope-for-sub-dependencies } + +Wenn Sie eine Abhängigkeit mit `scope="request"` (dem Default) deklarieren, muss jede Unterabhängigkeit ebenfalls einen `scope` von `"request"` haben. + +Eine Abhängigkeit mit `scope` von `"function"` kann jedoch Abhängigkeiten mit `scope` von `"function"` und `scope` von `"request"` haben. + +Das liegt daran, dass jede Abhängigkeit in der Lage sein muss, ihren Exit-Code vor den Unterabhängigkeiten auszuführen, da sie diese während ihres Exit-Codes möglicherweise noch verwenden muss. + +```mermaid +sequenceDiagram + +participant client as Client +participant dep_req as Abhängigkeit scope="request" +participant dep_func as Abhängigkeit scope="function" +participant operation as Pfadoperation + + client ->> dep_req: Startet den Request + Note over dep_req: Führt den Code bis zum yield aus + dep_req ->> dep_func: Reicht Abhängigkeit weiter + Note over dep_func: Führt den Code bis zum yield aus + dep_func ->> operation: Führt Pfadoperation mit Abhängigkeit aus + operation ->> dep_func: Kehrt aus Pfadoperation zurück + Note over dep_func: Führt Code nach yield aus + Note over dep_func: ✅ Abhängigkeit geschlossen + dep_func ->> client: Sendet Response an Client + Note over client: Response gesendet + Note over dep_req: Führt Code nach yield aus + Note over dep_req: ✅ Abhängigkeit geschlossen +``` + ## Abhängigkeiten mit `yield`, `HTTPException`, `except` und Hintergrundtasks { #dependencies-with-yield-httpexception-except-and-background-tasks } Abhängigkeiten mit `yield` haben sich im Laufe der Zeit weiterentwickelt, um verschiedene Anwendungsfälle abzudecken und einige Probleme zu beheben. @@ -223,7 +268,7 @@ In Python können Sie Kontextmanager erstellen, indem Sie deprecatet (aber weiterhin unterstützt) und in `.model_dump()` umbenannt. - -Die Beispiele hier verwenden `.dict()` für die Kompatibilität mit Pydantic v1, aber Sie sollten `.model_dump()` verwenden, wenn Sie Pydantic v2 verwenden können. - -/// - -### Über `**user_in.dict()` { #about-user-in-dict } - -#### Die `.dict()`-Methode von Pydantic { #pydantics-dict } +#### Pydantics `.model_dump()` { #pydantics-model-dump } `user_in` ist ein Pydantic-Modell der Klasse `UserIn`. -Pydantic-Modelle haben eine `.dict()`-Methode, die ein `dict` mit den Daten des Modells zurückgibt. +Pydantic-Modelle haben eine `.model_dump()`-Methode, die ein `dict` mit den Daten des Modells zurückgibt. Wenn wir also ein Pydantic-Objekt `user_in` erstellen, etwa so: @@ -47,7 +39,7 @@ user_in = UserIn(username="john", password="secret", email="john.doe@example.com und dann aufrufen: ```Python -user_dict = user_in.dict() +user_dict = user_in.model_dump() ``` haben wir jetzt ein `dict` mit den Daten in der Variablen `user_dict` (es ist ein `dict` statt eines Pydantic-Modellobjekts). @@ -103,20 +95,20 @@ UserInDB( #### Ein Pydantic-Modell aus dem Inhalt eines anderen { #a-pydantic-model-from-the-contents-of-another } -Da wir im obigen Beispiel `user_dict` von `user_in.dict()` bekommen haben, wäre dieser Code: +Da wir im obigen Beispiel `user_dict` von `user_in.model_dump()` bekommen haben, wäre dieser Code: ```Python -user_dict = user_in.dict() +user_dict = user_in.model_dump() UserInDB(**user_dict) ``` gleichwertig zu: ```Python -UserInDB(**user_in.dict()) +UserInDB(**user_in.model_dump()) ``` -... weil `user_in.dict()` ein `dict` ist, und dann lassen wir Python es „entpacken“, indem wir es an `UserInDB` mit vorangestelltem `**` übergeben. +... weil `user_in.model_dump()` ein `dict` ist, und dann lassen wir Python es „entpacken“, indem wir es an `UserInDB` mit vorangestelltem `**` übergeben. Auf diese Weise erhalten wir ein Pydantic-Modell aus den Daten eines anderen Pydantic-Modells. @@ -125,7 +117,7 @@ Auf diese Weise erhalten wir ein Pydantic-Modell aus den Daten eines anderen Pyd Und dann fügen wir das zusätzliche Schlüsselwort-Argument `hashed_password=hashed_password` hinzu, wie in: ```Python -UserInDB(**user_in.dict(), hashed_password=hashed_password) +UserInDB(**user_in.model_dump(), hashed_password=hashed_password) ``` ... was so ist wie: @@ -180,7 +172,6 @@ Wenn Sie eine FastAPI Cloud deployen, treten Sie der Warteliste bei, falls Sie es noch nicht getan haben. 🚀 + +Wenn Sie bereits ein **FastAPI Cloud**-Konto haben (wir haben Sie von der Warteliste eingeladen 😉), können Sie Ihre Anwendung mit einem Befehl deployen. + +Vor dem Deployen, stellen Sie sicher, dass Sie eingeloggt sind: + +
+ +```console +$ fastapi login + +You are logged in to FastAPI Cloud 🚀 +``` + +
+ +Dann stellen Sie Ihre App bereit: + +
+ +```console +$ fastapi deploy + +Deploying to FastAPI Cloud... + +✅ Deployment successful! + +🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev +``` + +
+ +Das war's! Jetzt können Sie Ihre App unter dieser URL aufrufen. ✨ + ## Zusammenfassung, Schritt für Schritt { #recap-step-by-step } ### Schritt 1: `FastAPI` importieren { #step-1-import-fastapi } -{* ../../docs_src/first_steps/tutorial001.py hl[1] *} +{* ../../docs_src/first_steps/tutorial001_py39.py hl[1] *} `FastAPI` ist eine Python-Klasse, die die gesamte Funktionalität für Ihre API bereitstellt. @@ -161,7 +197,7 @@ Sie können alle Requests zuständig ist, die an: @@ -284,7 +320,7 @@ Das ist unsere „**Pfadoperation-Funktion**“: * **Operation**: ist `get`. * **Funktion**: ist die Funktion direkt unter dem „Dekorator“ (unter `@app.get("/")`). -{* ../../docs_src/first_steps/tutorial001.py hl[7] *} +{* ../../docs_src/first_steps/tutorial001_py39.py hl[7] *} Dies ist eine Python-Funktion. @@ -296,7 +332,7 @@ In diesem Fall handelt es sich um eine `async`-Funktion. Sie könnten sie auch als normale Funktion anstelle von `async def` definieren: -{* ../../docs_src/first_steps/tutorial003.py hl[7] *} +{* ../../docs_src/first_steps/tutorial003_py39.py hl[7] *} /// note | Hinweis @@ -306,7 +342,7 @@ Wenn Sie den Unterschied nicht kennen, lesen Sie [Async: *„In Eile?“*](../as ### Schritt 5: den Inhalt zurückgeben { #step-5-return-the-content } -{* ../../docs_src/first_steps/tutorial001.py hl[8] *} +{* ../../docs_src/first_steps/tutorial001_py39.py hl[8] *} Sie können ein `dict`, eine `list`, einzelne Werte wie `str`, `int`, usw. zurückgeben. @@ -314,6 +350,26 @@ Sie können auch Pydantic-Modelle zurückgeben (dazu später mehr). Es gibt viele andere Objekte und Modelle, die automatisch zu JSON konvertiert werden (einschließlich ORMs, usw.). Versuchen Sie, Ihre Lieblingsobjekte zu verwenden. Es ist sehr wahrscheinlich, dass sie bereits unterstützt werden. +### Schritt 6: Deployen { #step-6-deploy-it } + +Stellen Sie Ihre App in der **FastAPI Cloud** mit einem Befehl bereit: `fastapi deploy`. 🎉 + +#### Über FastAPI Cloud { #about-fastapi-cloud } + +**FastAPI Cloud** wird vom selben Autor und Team hinter **FastAPI** entwickelt. + +Es vereinfacht den Prozess des Erstellens, Deployens und des Zugriffs auf eine API mit minimalem Aufwand. + +Es bringt die gleiche **Developer-Experience** beim Erstellen von Apps mit FastAPI auch zum **Deployment** in der Cloud. 🎉 + +FastAPI Cloud ist der Hauptsponsor und Finanzierer der „FastAPI and friends“ Open-Source-Projekte. ✨ + +#### Zu anderen Cloudanbietern deployen { #deploy-to-other-cloud-providers } + +FastAPI ist Open Source und basiert auf Standards. Sie können FastAPI-Apps bei jedem Cloudanbieter Ihrer Wahl deployen. + +Folgen Sie den Anleitungen Ihres Cloudanbieters, um dort FastAPI-Apps bereitzustellen. 🤓 + ## Zusammenfassung { #recap } * Importieren Sie `FastAPI`. @@ -321,3 +377,4 @@ Es gibt viele andere Objekte und Modelle, die automatisch zu JSON konvertiert we * Schreiben Sie einen **Pfadoperation-Dekorator** unter Verwendung von Dekoratoren wie `@app.get("/")`. * Definieren Sie eine **Pfadoperation-Funktion**, zum Beispiel `def root(): ...`. * Starten Sie den Entwicklungsserver mit dem Befehl `fastapi dev`. +* Optional: Ihre App mit `fastapi deploy` deployen. diff --git a/docs/de/docs/tutorial/handling-errors.md b/docs/de/docs/tutorial/handling-errors.md index 58e4607c5a..d890b44629 100644 --- a/docs/de/docs/tutorial/handling-errors.md +++ b/docs/de/docs/tutorial/handling-errors.md @@ -25,7 +25,7 @@ Um HTTP-`ValidationError`. - -**FastAPI** verwendet diesen so, dass, wenn Sie ein Pydantic-Modell in `response_model` verwenden und Ihre Daten einen Fehler haben, Sie den Fehler in Ihrem Log sehen. - -Aber der Client/Benutzer wird ihn nicht sehen. Stattdessen erhält der Client einen „Internal Server Error“ mit einem HTTP-Statuscode `500`. - -Es sollte so sein, denn wenn Sie einen Pydantic `ValidationError` in Ihrer *Response* oder irgendwo anders in Ihrem Code haben (nicht im *Request* des Clients), ist es tatsächlich ein Fehler in Ihrem Code. - -Und während Sie den Fehler beheben, sollten Ihre Clients/Benutzer keinen Zugriff auf interne Informationen über den Fehler haben, da das eine Sicherheitslücke aufdecken könnte. - ### Überschreiben des `HTTPException`-Fehlerhandlers { #override-the-httpexception-error-handler } Auf die gleiche Weise können Sie den `HTTPException`-Handler überschreiben. Zum Beispiel könnten Sie eine Klartext-Response statt JSON für diese Fehler zurückgeben wollen: -{* ../../docs_src/handling_errors/tutorial004.py hl[3:4,9:11,22] *} +{* ../../docs_src/handling_errors/tutorial004_py39.py hl[3:4,9:11,25] *} /// note | Technische Details @@ -188,13 +169,21 @@ Sie könnten auch `from starlette.responses import PlainTextResponse` verwenden. /// +/// warning | Achtung + +Beachten Sie, dass der `RequestValidationError` Informationen über den Dateinamen und die Zeile enthält, in der der Validierungsfehler auftritt, sodass Sie ihn bei Bedarf mit den relevanten Informationen in Ihren Logs anzeigen können. + +Das bedeutet aber auch, dass, wenn Sie ihn einfach in einen String umwandeln und diese Informationen direkt zurückgeben, Sie möglicherweise ein paar Informationen über Ihr System preisgeben. Daher extrahiert und zeigt der Code hier jeden Fehler getrennt. + +/// + ### Verwenden des `RequestValidationError`-Bodys { #use-the-requestvalidationerror-body } Der `RequestValidationError` enthält den empfangenen `body` mit den ungültigen Daten. Sie könnten diesen während der Entwicklung Ihrer Anwendung verwenden, um den Body zu loggen und zu debuggen, ihn an den Benutzer zurückzugeben usw. -{* ../../docs_src/handling_errors/tutorial005.py hl[14] *} +{* ../../docs_src/handling_errors/tutorial005_py39.py hl[14] *} Versuchen Sie nun, einen ungültigen Artikel zu senden: @@ -250,6 +239,6 @@ from starlette.exceptions import HTTPException as StarletteHTTPException Wenn Sie die Exception zusammen mit den gleichen Default-Exceptionhandlern von **FastAPI** verwenden möchten, können Sie die Default-Exceptionhandler aus `fastapi.exception_handlers` importieren und wiederverwenden: -{* ../../docs_src/handling_errors/tutorial006.py hl[2:5,15,21] *} +{* ../../docs_src/handling_errors/tutorial006_py39.py hl[2:5,15,21] *} In diesem Beispiel geben Sie nur den Fehler mit einer sehr ausdrucksstarken Nachricht aus, aber Sie verstehen das Prinzip. Sie können die Exception verwenden und dann einfach die Default-Exceptionhandler wiederverwenden. diff --git a/docs/de/docs/tutorial/metadata.md b/docs/de/docs/tutorial/metadata.md index 44d02e6d89..ee88a21d63 100644 --- a/docs/de/docs/tutorial/metadata.md +++ b/docs/de/docs/tutorial/metadata.md @@ -18,7 +18,7 @@ Sie können die folgenden Felder festlegen, die in der OpenAPI-Spezifikation und Sie können diese wie folgt setzen: -{* ../../docs_src/metadata/tutorial001.py hl[3:16, 19:32] *} +{* ../../docs_src/metadata/tutorial001_py39.py hl[3:16, 19:32] *} /// tip | Tipp @@ -36,7 +36,7 @@ Seit OpenAPI 3.1.0 und FastAPI 0.99.0 können Sie die `license_info` auch mit ei Zum Beispiel: -{* ../../docs_src/metadata/tutorial001_1.py hl[31] *} +{* ../../docs_src/metadata/tutorial001_1_py39.py hl[31] *} ## Metadaten für Tags { #metadata-for-tags } @@ -58,7 +58,7 @@ Versuchen wir es mit einem Beispiel mit Tags für `users` und `items`. Erstellen Sie Metadaten für Ihre Tags und übergeben Sie diese an den Parameter `openapi_tags`: -{* ../../docs_src/metadata/tutorial004.py hl[3:16,18] *} +{* ../../docs_src/metadata/tutorial004_py39.py hl[3:16,18] *} Beachten Sie, dass Sie Markdown innerhalb der Beschreibungen verwenden können. Zum Beispiel wird „login“ in Fettschrift (**login**) und „fancy“ in Kursivschrift (_fancy_) angezeigt. @@ -72,7 +72,7 @@ Sie müssen nicht für alle von Ihnen verwendeten Tags Metadaten hinzufügen. Verwenden Sie den Parameter `tags` mit Ihren *Pfadoperationen* (und `APIRouter`n), um diese verschiedenen Tags zuzuweisen: -{* ../../docs_src/metadata/tutorial004.py hl[21,26] *} +{* ../../docs_src/metadata/tutorial004_py39.py hl[21,26] *} /// info | Info @@ -100,7 +100,7 @@ Sie können das aber mit dem Parameter `openapi_url` konfigurieren. Um beispielsweise festzulegen, dass es unter `/api/v1/openapi.json` bereitgestellt wird: -{* ../../docs_src/metadata/tutorial002.py hl[3] *} +{* ../../docs_src/metadata/tutorial002_py39.py hl[3] *} Wenn Sie das OpenAPI-Schema vollständig deaktivieren möchten, können Sie `openapi_url=None` festlegen, wodurch auch die Dokumentationsbenutzeroberflächen deaktiviert werden, die es verwenden. @@ -117,4 +117,4 @@ Sie können die beiden enthaltenen Dokumentationsbenutzeroberflächen konfigurie Um beispielsweise Swagger UI so einzustellen, dass sie unter `/documentation` bereitgestellt wird, und ReDoc zu deaktivieren: -{* ../../docs_src/metadata/tutorial003.py hl[3] *} +{* ../../docs_src/metadata/tutorial003_py39.py hl[3] *} diff --git a/docs/de/docs/tutorial/middleware.md b/docs/de/docs/tutorial/middleware.md index 6410deba1a..540a18c4d0 100644 --- a/docs/de/docs/tutorial/middleware.md +++ b/docs/de/docs/tutorial/middleware.md @@ -31,7 +31,7 @@ Die Middleware-Funktion erhält: * Dann gibt es die von der entsprechenden *Pfadoperation* generierte `response` zurück. * Sie können die `response` dann weiter modifizieren, bevor Sie sie zurückgeben. -{* ../../docs_src/middleware/tutorial001.py hl[8:9,11,14] *} +{* ../../docs_src/middleware/tutorial001_py39.py hl[8:9,11,14] *} /// tip | Tipp @@ -57,7 +57,7 @@ Und auch nachdem die `response` generiert wurde, bevor sie zurückgegeben wird. Sie könnten beispielsweise einen benutzerdefinierten Header `X-Process-Time` hinzufügen, der die Zeit in Sekunden enthält, die benötigt wurde, um den Request zu verarbeiten und eine Response zu generieren: -{* ../../docs_src/middleware/tutorial001.py hl[10,12:13] *} +{* ../../docs_src/middleware/tutorial001_py39.py hl[10,12:13] *} /// tip | Tipp diff --git a/docs/de/docs/tutorial/path-operation-configuration.md b/docs/de/docs/tutorial/path-operation-configuration.md index c483f4e405..3427b30528 100644 --- a/docs/de/docs/tutorial/path-operation-configuration.md +++ b/docs/de/docs/tutorial/path-operation-configuration.md @@ -46,7 +46,7 @@ In diesem Fall macht es Sinn, die Tags in einem `Enum` zu speichern. **FastAPI** unterstützt das auf die gleiche Weise wie einfache Strings: -{* ../../docs_src/path_operation_configuration/tutorial002b.py hl[1,8:10,13,18] *} +{* ../../docs_src/path_operation_configuration/tutorial002b_py39.py hl[1,8:10,13,18] *} ## Zusammenfassung und Beschreibung { #summary-and-description } @@ -92,7 +92,7 @@ Daher, wenn Sie keine vergeben, wird **FastAPI** automatisch eine für „Erfolg Wenn Sie eine *Pfadoperation* als deprecatet kennzeichnen möchten, ohne sie zu entfernen, fügen Sie den Parameter `deprecated` hinzu: -{* ../../docs_src/path_operation_configuration/tutorial006.py hl[16] *} +{* ../../docs_src/path_operation_configuration/tutorial006_py39.py hl[16] *} Sie wird in der interaktiven Dokumentation gut sichtbar als deprecatet markiert werden: diff --git a/docs/de/docs/tutorial/path-params-numeric-validations.md b/docs/de/docs/tutorial/path-params-numeric-validations.md index 5b74749447..8b52e8b42f 100644 --- a/docs/de/docs/tutorial/path-params-numeric-validations.md +++ b/docs/de/docs/tutorial/path-params-numeric-validations.md @@ -54,7 +54,7 @@ Für **FastAPI** spielt es keine Rolle. Es erkennt die Parameter anhand ihrer Na Sie können Ihre Funktion also so deklarieren: -{* ../../docs_src/path_params_numeric_validations/tutorial002.py hl[7] *} +{* ../../docs_src/path_params_numeric_validations/tutorial002_py39.py hl[7] *} Aber bedenken Sie, dass Sie dieses Problem nicht haben, wenn Sie `Annotated` verwenden, da es nicht darauf ankommt, dass Sie keine Funktionsparameter-Defaultwerte für `Query()` oder `Path()` verwenden. @@ -83,7 +83,7 @@ Wenn Sie: Python wird nichts mit diesem `*` machen, aber es wird wissen, dass alle folgenden Parameter als Schlüsselwortargumente (Schlüssel-Wert-Paare) verwendet werden sollen, auch bekannt als kwargs. Selbst wenn diese keinen Defaultwert haben. -{* ../../docs_src/path_params_numeric_validations/tutorial003.py hl[7] *} +{* ../../docs_src/path_params_numeric_validations/tutorial003_py39.py hl[7] *} ### Besser mit `Annotated` { #better-with-annotated } diff --git a/docs/de/docs/tutorial/path-params.md b/docs/de/docs/tutorial/path-params.md index 1db288fb87..1de4973155 100644 --- a/docs/de/docs/tutorial/path-params.md +++ b/docs/de/docs/tutorial/path-params.md @@ -2,7 +2,7 @@ Sie können Pfad-„Parameter“ oder -„Variablen“ mit der gleichen Syntax deklarieren, welche in Python-Formatstrings verwendet wird: -{* ../../docs_src/path_params/tutorial001.py hl[6:7] *} +{* ../../docs_src/path_params/tutorial001_py39.py hl[6:7] *} Der Wert des Pfad-Parameters `item_id` wird Ihrer Funktion als das Argument `item_id` übergeben. @@ -16,7 +16,7 @@ Wenn Sie dieses Beispiel ausführen und auf Enumerationen (oder Enums) gibt es in Python seit Version 3.4. - -/// /// tip | Tipp @@ -158,7 +153,7 @@ Falls Sie sich fragen, was „AlexNet“, „ResNet“ und „LeNet“ ist, das Dann erstellen Sie einen *Pfad-Parameter*, der als Typ die gerade erstellte Enum-Klasse hat (`ModelName`): -{* ../../docs_src/path_params/tutorial005.py hl[16] *} +{* ../../docs_src/path_params/tutorial005_py39.py hl[16] *} ### Die API-Dokumentation testen { #check-the-docs } @@ -174,13 +169,13 @@ Der *Pfad-Parameter* wird ein *Query ist die Menge von Schlüssel-Wert-Paaren, die nach dem `?` in einer URL folgen und durch `&`-Zeichen getrennt sind. @@ -127,7 +127,7 @@ Wenn Sie keinen spezifischen Wert haben wollen, sondern der Parameter einfach op Aber wenn Sie wollen, dass ein Query-Parameter erforderlich ist, vergeben Sie einfach keinen Defaultwert: -{* ../../docs_src/query_params/tutorial005.py hl[6:7] *} +{* ../../docs_src/query_params/tutorial005_py39.py hl[6:7] *} Hier ist `needy` ein erforderlicher Query-Parameter vom Typ `str`. diff --git a/docs/de/docs/tutorial/response-model.md b/docs/de/docs/tutorial/response-model.md index 7b77125cb5..f759bb2571 100644 --- a/docs/de/docs/tutorial/response-model.md +++ b/docs/de/docs/tutorial/response-model.md @@ -183,7 +183,7 @@ Es kann Fälle geben, bei denen Sie etwas zurückgeben, das kein gültiges Pydan Der häufigste Anwendungsfall ist, wenn Sie [eine Response direkt zurückgeben, wie es später im Handbuch für fortgeschrittene Benutzer erläutert wird](../advanced/response-directly.md){.internal-link target=_blank}. -{* ../../docs_src/response_model/tutorial003_02.py hl[8,10:11] *} +{* ../../docs_src/response_model/tutorial003_02_py39.py hl[8,10:11] *} Dieser einfache Anwendungsfall wird automatisch von FastAPI gehandhabt, weil die Annotation des Rückgabetyps die Klasse (oder eine Unterklasse von) `Response` ist. @@ -193,7 +193,7 @@ Und Tools werden auch glücklich sein, weil sowohl `RedirectResponse` als auch ` Sie können auch eine Unterklasse von `Response` in der Typannotation verwenden. -{* ../../docs_src/response_model/tutorial003_03.py hl[8:9] *} +{* ../../docs_src/response_model/tutorial003_03_py39.py hl[8:9] *} Das wird ebenfalls funktionieren, weil `RedirectResponse` eine Unterklasse von `Response` ist, und FastAPI sich um diesen einfachen Anwendungsfall automatisch kümmert. @@ -252,20 +252,6 @@ Wenn Sie also den Artikel mit der ID `foo` bei der *Pfadoperation* anfragen, wir /// info | Info -In Pydantic v1 hieß diese Methode `.dict()`, in Pydantic v2 wurde sie deprecatet (aber immer noch unterstützt) und in `.model_dump()` umbenannt. - -Die Beispiele hier verwenden `.dict()` für die Kompatibilität mit Pydantic v1, Sie sollten jedoch stattdessen `.model_dump()` verwenden, wenn Sie Pydantic v2 verwenden können. - -/// - -/// info | Info - -FastAPI verwendet `.dict()` von Pydantic Modellen, mit dessen `exclude_unset`-Parameter, um das zu erreichen. - -/// - -/// info | Info - Sie können auch: * `response_model_exclude_defaults=True` diff --git a/docs/de/docs/tutorial/response-status-code.md b/docs/de/docs/tutorial/response-status-code.md index 928003c3fc..fd17c99336 100644 --- a/docs/de/docs/tutorial/response-status-code.md +++ b/docs/de/docs/tutorial/response-status-code.md @@ -8,7 +8,7 @@ Genauso wie Sie ein Responsemodell angeben können, können Sie auch den HTTP-St * `@app.delete()` * usw. -{* ../../docs_src/response_status_code/tutorial001.py hl[6] *} +{* ../../docs_src/response_status_code/tutorial001_py39.py hl[6] *} /// note | Hinweis @@ -74,7 +74,7 @@ Um mehr über die einzelnen Statuscodes zu erfahren und welcher wofür verwendet Lassen Sie uns das vorherige Beispiel noch einmal anschauen: -{* ../../docs_src/response_status_code/tutorial001.py hl[6] *} +{* ../../docs_src/response_status_code/tutorial001_py39.py hl[6] *} `201` ist der Statuscode für „Created“ („Erzeugt“). @@ -82,7 +82,7 @@ Aber Sie müssen sich nicht merken, was jeder dieser Codes bedeutet. Sie können die Annehmlichkeit von Variablen aus `fastapi.status` nutzen. -{* ../../docs_src/response_status_code/tutorial002.py hl[1,6] *} +{* ../../docs_src/response_status_code/tutorial002_py39.py hl[1,6] *} Diese sind nur eine Annehmlichkeit, sie enthalten dieselbe Zahl, aber so können Sie die Autovervollständigung Ihres Editors verwenden, um sie zu finden: diff --git a/docs/de/docs/tutorial/schema-extra-example.md b/docs/de/docs/tutorial/schema-extra-example.md index e2ffed292e..07fe8c5d92 100644 --- a/docs/de/docs/tutorial/schema-extra-example.md +++ b/docs/de/docs/tutorial/schema-extra-example.md @@ -8,36 +8,14 @@ Hier sind mehrere Möglichkeiten, das zu tun. Sie können `examples` („Beispiele“) für ein Pydantic-Modell deklarieren, welche dem generierten JSON-Schema hinzugefügt werden. -//// tab | Pydantic v2 - {* ../../docs_src/schema_extra_example/tutorial001_py310.py hl[13:24] *} -//// - -//// tab | Pydantic v1 - -{* ../../docs_src/schema_extra_example/tutorial001_pv1_py310.py hl[13:23] *} - -//// - Diese zusätzlichen Informationen werden unverändert zum für dieses Modell ausgegebenen **JSON-Schema** hinzugefügt und in der API-Dokumentation verwendet. -//// tab | Pydantic v2 - -In Pydantic Version 2 würden Sie das Attribut `model_config` verwenden, das ein `dict` akzeptiert, wie beschrieben in Pydantic-Dokumentation: Configuration. +Sie können das Attribut `model_config` verwenden, das ein `dict` akzeptiert, wie beschrieben in Pydantic-Dokumentation: Configuration. Sie können `json_schema_extra` setzen, mit einem `dict`, das alle zusätzlichen Daten enthält, die im generierten JSON-Schema angezeigt werden sollen, einschließlich `examples`. -//// - -//// tab | Pydantic v1 - -In Pydantic Version 1 würden Sie eine interne Klasse `Config` und `schema_extra` verwenden, wie beschrieben in Pydantic-Dokumentation: Schema customization. - -Sie können `schema_extra` setzen, mit einem `dict`, das alle zusätzlichen Daten enthält, die im generierten JSON-Schema angezeigt werden sollen, einschließlich `examples`. - -//// - /// tip | Tipp Mit derselben Technik können Sie das JSON-Schema erweitern und Ihre eigenen benutzerdefinierten Zusatzinformationen hinzufügen. diff --git a/docs/de/docs/tutorial/sql-databases.md b/docs/de/docs/tutorial/sql-databases.md index cf9731aee6..3af4ecdfc6 100644 --- a/docs/de/docs/tutorial/sql-databases.md +++ b/docs/de/docs/tutorial/sql-databases.md @@ -65,7 +65,7 @@ Es gibt ein paar Unterschiede: * `Field(primary_key=True)` sagt SQLModel, dass die `id` der **Primärschlüssel** in der SQL-Datenbank ist (Sie können mehr über SQL-Primärschlüssel in der SQLModel-Dokumentation erfahren). - Durch das Festlegen des Typs als `int | None` wird SQLModel wissen, dass diese Spalte ein `INTEGER` in der SQL-Datenbank sein sollte und dass sie `NULLABLE` sein sollte. + **Hinweis:** Wir verwenden für das Primärschlüsselfeld `int | None`, damit wir im Python-Code *ein Objekt ohne `id` erstellen* können (`id=None`), in der Annahme, dass die Datenbank sie *beim Speichern generiert*. SQLModel versteht, dass die Datenbank die `id` bereitstellt, und *definiert die Spalte im Datenbankschema als ein Nicht-Null-`INTEGER`*. Siehe die SQLModel-Dokumentation zu Primärschlüsseln für Details. * `Field(index=True)` sagt SQLModel, dass es einen **SQL-Index** für diese Spalte erstellen soll, was schnelleres Suchen in der Datenbank ermöglicht, wenn Daten mittels dieser Spalte gefiltert werden. diff --git a/docs/de/docs/tutorial/static-files.md b/docs/de/docs/tutorial/static-files.md index 0c4e7c8abd..9ba2501756 100644 --- a/docs/de/docs/tutorial/static-files.md +++ b/docs/de/docs/tutorial/static-files.md @@ -7,7 +7,7 @@ Mit `StaticFiles` können Sie statische Dateien aus einem Verzeichnis automatisc * Importieren Sie `StaticFiles`. * „Mounten“ Sie eine `StaticFiles()`-Instanz in einem bestimmten Pfad. -{* ../../docs_src/static_files/tutorial001.py hl[2,6] *} +{* ../../docs_src/static_files/tutorial001_py39.py hl[2,6] *} /// note | Technische Details diff --git a/docs/de/docs/tutorial/testing.md b/docs/de/docs/tutorial/testing.md index 9c28a2a223..d889b1e1fb 100644 --- a/docs/de/docs/tutorial/testing.md +++ b/docs/de/docs/tutorial/testing.md @@ -30,7 +30,7 @@ Verwenden Sie das `TestClient`-Objekt auf die gleiche Weise wie `httpx`. Schreiben Sie einfache `assert`-Anweisungen mit den Standard-Python-Ausdrücken, die Sie überprüfen müssen (wiederum, Standard-`pytest`). -{* ../../docs_src/app_testing/tutorial001.py hl[2,12,15:18] *} +{* ../../docs_src/app_testing/tutorial001_py39.py hl[2,12,15:18] *} /// tip | Tipp @@ -76,7 +76,7 @@ Nehmen wir an, Sie haben eine Dateistruktur wie in [Größere Anwendungen](bigge In der Datei `main.py` haben Sie Ihre **FastAPI**-Anwendung: -{* ../../docs_src/app_testing/main.py *} +{* ../../docs_src/app_testing/app_a_py39/main.py *} ### Testdatei { #testing-file } @@ -93,7 +93,7 @@ Dann könnten Sie eine Datei `test_main.py` mit Ihren Tests haben. Sie könnte s Da sich diese Datei im selben Package befindet, können Sie relative Importe verwenden, um das Objekt `app` aus dem `main`-Modul (`main.py`) zu importieren: -{* ../../docs_src/app_testing/test_main.py hl[3] *} +{* ../../docs_src/app_testing/app_a_py39/test_main.py hl[3] *} ... und haben den Code für die Tests wie zuvor. @@ -122,63 +122,13 @@ Sie verfügt über eine `POST`-Operation, die mehrere Fehler zurückgeben könnt Beide *Pfadoperationen* erfordern einen `X-Token`-Header. -//// tab | Python 3.10+ - -```Python -{!> ../../docs_src/app_testing/app_b_an_py310/main.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!> ../../docs_src/app_testing/app_b_an_py39/main.py!} -``` - -//// - -//// tab | Python 3.8+ - -```Python -{!> ../../docs_src/app_testing/app_b_an/main.py!} -``` - -//// - -//// tab | Python 3.10+ nicht annotiert - -/// tip | Tipp - -Bevorzugen Sie die `Annotated`-Version, falls möglich. - -/// - -```Python -{!> ../../docs_src/app_testing/app_b_py310/main.py!} -``` - -//// - -//// tab | Python 3.8+ nicht annotiert - -/// tip | Tipp - -Bevorzugen Sie die `Annotated`-Version, falls möglich. - -/// - -```Python -{!> ../../docs_src/app_testing/app_b/main.py!} -``` - -//// +{* ../../docs_src/app_testing/app_b_an_py310/main.py *} ### Erweiterte Testdatei { #extended-testing-file } Anschließend könnten Sie `test_main.py` mit den erweiterten Tests aktualisieren: -{* ../../docs_src/app_testing/app_b/test_main.py *} +{* ../../docs_src/app_testing/app_b_an_py310/test_main.py *} Wenn Sie möchten, dass der Client Informationen im Request übergibt und Sie nicht wissen, wie das geht, können Sie suchen (googeln), wie es mit `httpx` gemacht wird, oder sogar, wie es mit `requests` gemacht wird, da das Design von HTTPX auf dem Design von Requests basiert. diff --git a/docs/de/docs/virtual-environments.md b/docs/de/docs/virtual-environments.md index 497f1b44db..11da496c54 100644 --- a/docs/de/docs/virtual-environments.md +++ b/docs/de/docs/virtual-environments.md @@ -242,6 +242,26 @@ $ python -m pip install --upgrade pip +/// tip | Tipp + +Manchmal kann beim Versuch, `pip` zu aktualisieren, der Fehler **`No module named pip`** auftreten. + +Wenn das passiert, installieren und aktualisieren Sie `pip` mit dem folgenden Befehl: + +
+ +```console +$ python -m ensurepip --upgrade + +---> 100% +``` + +
+ +Dieser Befehl installiert `pip`, falls es noch nicht installiert ist, und stellt außerdem sicher, dass die installierte Version von `pip` mindestens so aktuell ist wie die in `ensurepip` verfügbare. + +/// + ## `.gitignore` hinzufügen { #add-gitignore } Wenn Sie **Git** verwenden (was Sie sollten), fügen Sie eine `.gitignore`-Datei hinzu, um alles in Ihrem `.venv` von Git auszuschließen. diff --git a/docs/de/llm-prompt.md b/docs/de/llm-prompt.md index df202d2ff5..2d345bf6d1 100644 --- a/docs/de/llm-prompt.md +++ b/docs/de/llm-prompt.md @@ -4,213 +4,197 @@ Translate to German (Deutsch). Language code: de. - -### Definitions - -"hyphen" - The character «-» - Unicode U+002D (HYPHEN-MINUS) - Alternative names: hyphen, dash, minus sign - -"dash" - The character «–» - Unicode U+2013 (EN DASH) - German name: Halbgeviertstrich - - ### Grammar to use when talking to the reader -Use the formal grammar (use «Sie» instead of «Du»). - +Use the formal grammar (use `Sie` instead of `Du`). ### Quotes -1) Convert neutral double quotes («"») and English double typographic quotes («“» and «”») to German double typographic quotes («„» and «“»). Convert neutral single quotes («'») and English single typographic quotes («‘» and «’») to German single typographic quotes («‚» and «‘»). Do NOT convert «`"» to «„», do NOT convert «"`» to «“». +1) Convert neutral double quotes (`"`) to German double typographic quotes (`„` and `“`). Convert neutral single quotes (`'`) to German single typographic quotes (`‚` and `‘`). + +Do NOT convert quotes in code snippets and code blocks to their German typographic equivalents. Examples: - Source (English): +Source (English): - ««« - "Hello world" - “Hello Universe” - "He said: 'Hello'" - “my name is ‘Nils’” - `"__main__"` - `"items"` - »»» +``` +"Hello world" +“Hello Universe” +"He said: 'Hello'" +“my name is ‘Nils’” +`"__main__"` +`"items"` +``` - Result (German): - - ««« - „Hallo Welt“ - „Hallo Universum“ - „Er sagte: ‚Hallo‘“ - „Mein Name ist ‚Nils‘“ - `"__main__"` - `"items"` - »»» +Result (German): +``` +„Hallo Welt“ +„Hallo Universum“ +„Er sagte: ‚Hallo‘“ +„Mein Name ist ‚Nils‘“ +`"__main__"` +`"items"` +``` ### Ellipsis -1) Make sure there is a space between an ellipsis and a word following or preceding the ellipsis. +- Make sure there is a space between an ellipsis and a word following or preceding the ellipsis. Examples: - Source (English): +Source (English): - ««« - ...as we intended. - ...this would work: - ...etc. - others... - More to come... - »»» +``` +...as we intended. +...this would work: +...etc. +others... +More to come... +``` - Result (German): +Result (German): - ««« - ... wie wir es beabsichtigt hatten. - ... das würde funktionieren: - ... usw. - Andere ... - Später mehr ... - »»» - -2) This does not apply in URLs, code blocks, and code snippets. Do not remove or add spaces there. +``` +... wie wir es beabsichtigt hatten. +... das würde funktionieren: +... usw. +Andere ... +Später mehr ... +``` +- This does not apply in URLs, code blocks, and code snippets. Do not remove or add spaces there. ### Headings -1) Translate headings using the infinite form. +- Translate headings using the infinite form. Examples: - Source (English): +Source (English): - ««« - ## Create a Project { #create-a-project } - »»» +``` +## Create a Project { #create-a-project } +``` - Translate with (German): +Result (German): - ««« - ## Ein Projekt erstellen { #create-a-project } - »»» +``` +## Ein Projekt erstellen { #create-a-project } +``` - Do NOT translate with (German): +Do NOT translate with (German): - ««« - ## Erstellen Sie ein Projekt { #create-a-project } - »»» +``` +## Erstellen Sie ein Projekt { #create-a-project } +``` - Source (English): +Source (English): - ««« - # Install Packages { #install-packages } - »»» +``` +# Install Packages { #install-packages } +``` - Translate with (German): +Translate with (German): - ««« - # Pakete installieren { #install-packages } - »»» +``` +# Pakete installieren { #install-packages } +``` - Do NOT translate with (German): +Do NOT translate with (German): - ««« - # Installieren Sie Pakete { #install-packages } - »»» +``` +# Installieren Sie Pakete { #install-packages } +``` - Source (English): +Source (English): - ««« - ### Run Your Program { #run-your-program } - »»» +``` +### Run Your Program { #run-your-program } +``` - Translate with (German): +Translate with (German): - ««« - ### Ihr Programm ausführen { #run-your-program } - »»» +``` +### Ihr Programm ausführen { #run-your-program } +``` - Do NOT translate with (German): +Do NOT translate with (German): - ««« - ### Führen Sie Ihr Programm aus { #run-your-program } - »»» +``` +### Führen Sie Ihr Programm aus { #run-your-program } +``` -2) Make sure that the translated part of the heading does not end with a period. +- Make sure that the translated part of the heading does not end with a period. Example: - Source (English): +Source (English): - ««« - ## Another module with `APIRouter` { #another-module-with-apirouter } - »»» +``` +## Another module with `APIRouter` { #another-module-with-apirouter } +``` - Translate with (German): +Translate with (German): - ««« - ## Ein weiteres Modul mit `APIRouter` { #another-module-with-apirouter } - »»» +``` +## Ein weiteres Modul mit `APIRouter` { #another-module-with-apirouter } +``` - Do NOT translate with (German) – notice the added period: +Do NOT translate with (German) – notice the added period: - ««« - ## Ein weiteres Modul mit `APIRouter`. { #another-module-with-apirouter } - »»» +``` +## Ein weiteres Modul mit `APIRouter`. { #another-module-with-apirouter } +``` -3) Replace occurrences of literal « - » (a space followed by a hyphen followed by a space) with « – » (a space followed by a dash followed by a space) in the translated part of the heading. +- Replace occurrences of literal ` - ` (a space followed by a hyphen followed by a space) with ` – ` (a space followed by a dash followed by a space) in the translated part of the heading. Example: - Source (English): +Source (English): - ««« - # FastAPI in Containers - Docker { #fastapi-in-containers-docker } - »»» +``` +# FastAPI in Containers - Docker { #fastapi-in-containers-docker } +``` - Translate with (German) – notice the dash: +Translate with (German) – notice the dash: - ««« - # FastAPI in Containern – Docker { #fastapi-in-containers-docker } - »»» +``` +# FastAPI in Containern – Docker { #fastapi-in-containers-docker } +``` - Do NOT translate with (German) – notice the hyphen: +Do NOT translate with (German) – notice the hyphen: - ««« - # FastAPI in Containern - Docker { #fastapi-in-containers-docker } - »»» +``` +# FastAPI in Containern - Docker { #fastapi-in-containers-docker } +``` -3.1) Do not apply rule 3 when there is no space before or no space after the hyphen. +- Do not apply rule 3 when there is no space before or no space after the hyphen. Example: - Source (English): +Source (English): - ««« - ## Type hints and annotations { #type-hints-and-annotations } - »»» +``` +## Type hints and annotations { #type-hints-and-annotations } +``` - Translate with (German) – notice the hyphen: +Translate with (German) - notice the hyphen: - ««« - ## Typhinweise und -annotationen { #type-hints-and-annotations } - »»» +``` +## Typhinweise und -annotationen { #type-hints-and-annotations } +``` - Do NOT translate with (German) – notice the dash: +Do NOT translate with (German) - notice the dash: - ««« - ## Typhinweise und –annotationen { #type-hints-and-annotations } - »»» +``` +## Typhinweise und –annotationen { #type-hints-and-annotations } +``` -3.2) Do not apply rule 3 to the untranslated part of the heading inside curly brackets, which you shall not translate. +- Do not modify the hyphens in the content in headers inside of curly braces, which you shall not translate. - -### German instructions, when to use and when not to use hyphens in words (written in first person, which is you) +### German instructions, when to use and when not to use hyphens in words (written in first person, which is you). In der Regel versuche ich so weit wie möglich Worte zusammenzuschreiben, also ohne Bindestrich, es sei denn, es ist Konkretesding-Klassevondingen, etwa «Pydantic-Modell» (aber: «Datenbankmodell»), «Python-Modul» (aber: «Standardmodul»). Ich setze auch einen Bindestrich, wenn er die gleichen Buchstaben verbindet, etwa «Enum-Member», «Cloud-Dienst», «Template-Engine». Oder wenn das Wort sonst einfach zu lang wird, etwa, «Performance-Optimierung». Oder um etwas visuell besser zu dokumentieren, etwa «Pfadoperation-Dekorator», «Pfadoperation-Funktion». @@ -219,120 +203,122 @@ In der Regel versuche ich so weit wie möglich Worte zusammenzuschreiben, also o Ich versuche nicht, alles einzudeutschen. Das bezieht sich besonders auf Begriffe aus dem Bereich der Programmierung. Ich wandele zwar korrekt in Großschreibung um und setze Bindestriche, wo notwendig, aber ansonsten lasse ich solch ein Wort unverändert. Beispielsweise wird aus dem englischen Wort «string» in der deutschen Übersetzung «String», aber nicht «Zeichenkette». Oder aus dem englischen Wort «request body» wird in der deutschen Übersetzung «Requestbody», aber nicht «Anfragekörper». Oder aus dem englischen «response» wird im Deutschen «Response», aber nicht «Antwort». - ### List of English terms and their preferred German translations -Below is a list of English terms and their preferred German translations, separated by a colon («:»). Use these translations, do not use your own. If an existing translation does not use these terms, update it to use them. In the below list, a term or a translation may be followed by an explanation in brackets, which explains when to translate the term this way. If a translation is preceded by «NOT», then that means: do NOT use this translation for this term. English nouns, starting with the word «the», have the German genus – «der», «die», «das» – prepended to their German translation, to help you to grammatically decline them in the translation. They are given in singular case, unless they have «(plural)» attached, which means they are given in plural case. Verbs are given in the full infinitive – starting with the word «to». +Below is a list of English terms and their preferred German translations, separated by a colon (:). Use these translations, do not use your own. If an existing translation does not use these terms, update it to use them. In the below list, a term or a translation may be followed by an explanation in brackets, which explains when to translate the term this way. If a translation is preceded by `NOT`, then that means: do NOT use this translation for this term. English nouns, starting with the word `the`, have the German genus – `der`, `die`, `das` – prepended to their German translation, to help you to grammatically decline them in the translation. They are given in singular case, unless they have `(plural)` attached, which means they are given in plural case. Verbs are given in the full infinitive – starting with the word `to`. -* «/// check»: «/// check | Testen» -* «/// danger»: «/// danger | Gefahr» -* «/// info»: «/// info | Info» -* «/// note | Technical Details»: «/// note | Technische Details» -* «/// note»: «/// note | Hinweis» -* «/// tip»: «/// tip | Tipp» -* «/// warning»: «/// warning | Achtung» -* «you»: «Sie» -* «your»: «Ihr» -* «e.g»: «z. B.» -* «etc.»: «usw.» -* «ref»: «Ref.» -* «the Tutorial - User guide»: «das Tutorial – Benutzerhandbuch» -* «the Advanced User Guide»: «das Handbuch für fortgeschrittene Benutzer» -* «the SQLModel docs»: «die SQLModel-Dokumentation» -* «the docs»: «die Dokumentation» (use singular case) -* «the env var»: «die Umgebungsvariable» -* «the `PATH` environment variable»: «die `PATH`-Umgebungsvariable» -* «the `PATH`»: «der `PATH`» -* «the `requirements.txt`»: «die `requirements.txt`» -* «the API Router»: «der API-Router» -* «the Authorization-Header»: «der Autorisierungsheader» -* «the `Authorization`-Header»: «der `Authorization`-Header» -* «the background task»: «der Hintergrundtask» -* «the button»: «der Button» -* «the cloud provider»: «der Cloudanbieter» -* «the CLI»: «Das CLI» -* «the command line interface»: «Das Kommandozeileninterface» -* «the default value»: «der Defaultwert» -* «the default value»: NOT «der Standardwert» -* «the default declaration»: «die Default-Deklaration» -* «the dict»: «das Dict» -* «the dictionary»: «das Dictionary» -* «the enumeration»: «die Enumeration» -* «the enum»: «das Enum» -* «the engine»: «die Engine» -* «the error response»: «die Error-Response» -* «the event»: «das Event» -* «the exception»: «die Exception» -* «the exception handler»: «der Exceptionhandler» -* «the form model»: «das Formularmodell» -* «the form body»: «der Formularbody» -* «the header»: «der Header» -* «the headers» (plural): «die Header» -* «in headers» (plural): «in Headern» -* «the forwarded header»: «der Forwarded-Header» -* «the lifespan event»: «das Lifespan-Event» -* «the lock»: «der Lock» -* «the locking»: «das Locking» -* «the mobile application»: «die Mobile-Anwendung» -* «the model object»: «das Modellobjekt» -* «the mounting»: «das Mounten» -* «mounted»: «gemountet» -* «the origin»: «das Origin» -* «the override»: «Die Überschreibung» -* «the parameter»: «der Parameter» -* «the parameters» (plural): «die Parameter» -* «the function parameter»: «der Funktionsparameter» -* «the default parameter»: «der Defaultparameter» -* «the body parameter»: «der Body-Parameter» -* «the request body parameter»: «der Requestbody-Parameter» -* «the path parameter»: «der Pfad-Parameter» -* «the query parameter»: «der Query-Parameter» -* «the cookie parameter»: «der Cookie-Parameter» -* «the header parameter»: «der Header-Parameter» -* «the form parameter»: «der Formular-Parameter» -* «the payload»: «die Payload» -* «the performance»: NOT «die Performance» -* «the query»: «die Query» -* «the recap»: «die Zusammenfassung» -* «the request» (what the client sends to the server): «der Request» -* «the request body»: «der Requestbody» -* «the request bodies» (plural): «die Requestbodys» -* «the response» (what the server sends back to the client): «die Response» -* «the return type»: «der Rückgabetyp» -* «the return value»: «der Rückgabewert» -* «the startup» (the event of the app): «der Startup» -* «the shutdown» (the event of the app): «der Shutdown» -* «the startup event»: «das Startup-Event» -* «the shutdown event»: «das Shutdown-Event» -* «the startup» (of the server): «das Hochfahren» -* «the startup» (the company): «das Startup» -* «the SDK»: «das SDK» -* «the tag»: «der Tag» -* «the type annotation»: «die Typannotation» -* «the type hint»: «der Typhinweis» -* «the wildcard»: «die Wildcard» -* «the worker class»: «die Workerklasse» -* «the worker class»: NOT «die Arbeiterklasse» -* «the worker process»: «der Workerprozess» -* «the worker process»: NOT «der Arbeiterprozess» -* «to commit»: «committen» -* «to modify»: «ändern» -* «to serve» (an application): «bereitstellen» -* «to serve» (a response): «ausliefern» -* «to serve»: NOT «bedienen» -* «to upgrade»: «aktualisieren» -* «to wrap»: «wrappen» -* «to wrap»: NOT «hüllen» -* «`foo` as a `type`»: «`foo` vom Typ `type`» -* «`foo` as a `type`»: «`foo`, ein `type`» -* «FastAPI's X»: «FastAPIs X» -* «Starlette's Y»: «Starlettes Y» -* «X is case-sensitive»: «Groß-/Klein­schrei­bung ist relevant in X» -* «X is case-insensitive»: «Groß-/Klein­schrei­bung ist nicht relevant in X» -* «standard Python»: «Standard-Python» -* «deprecated»: «deprecatet» +* /// check: /// check | Testen +* /// danger: /// danger | Gefahr +* /// info: /// info | Info +* /// note | Technical Details: /// note | Technische Details +* /// note: /// note | Hinweis +* /// tip: /// tip | Tipp +* /// warning: /// warning | Achtung +* you: Sie +* your: Ihr +* e.g: z. B. +* etc.: usw. +* ref: Ref. +* the Tutorial - User guide: das Tutorial – Benutzerhandbuch +* the Advanced User Guide: das Handbuch für fortgeschrittene Benutzer +* the SQLModel docs: die SQLModel-Dokumentation +* the docs: die Dokumentation (use singular case) +* the env var: die Umgebungsvariable +* the `PATH` environment variable: die `PATH`-Umgebungsvariable +* the `PATH`: der `PATH` +* the `requirements.txt`: die `requirements.txt` +* the API Router: der API-Router +* the Authorization-Header: der Autorisierungsheader +* the `Authorization`-Header: der `Authorization`-Header +* the background task: der Hintergrundtask +* the button: der Button +* the cloud provider: der Cloudanbieter +* the CLI: Das CLI +* the coverage: Die Testabdeckung +* the command line interface: Das Kommandozeileninterface +* the default value: der Defaultwert +* the default value: NOT der Standardwert +* the default declaration: die Default-Deklaration +* the deployment: das Deployment +* the dict: das Dict +* the dictionary: das Dictionary +* the enumeration: die Enumeration +* the enum: das Enum +* the engine: die Engine +* the error response: die Error-Response +* the event: das Event +* the exception: die Exception +* the exception handler: der Exceptionhandler +* the form model: das Formularmodell +* the form body: der Formularbody +* the header: der Header +* the headers (plural): die Header +* in headers (plural): in Headern +* the forwarded header: der Forwarded-Header +* the lifespan event: das Lifespan-Event +* the lock: der Lock +* the locking: das Locking +* the mobile application: die Mobile-Anwendung +* the model object: das Modellobjekt +* the mounting: das Mounten +* mounted: gemountet +* the origin: das Origin +* the override: Die Überschreibung +* the parameter: der Parameter +* the parameters (plural): die Parameter +* the function parameter: der Funktionsparameter +* the default parameter: der Defaultparameter +* the body parameter: der Body-Parameter +* the request body parameter: der Requestbody-Parameter +* the path parameter: der Pfad-Parameter +* the query parameter: der Query-Parameter +* the cookie parameter: der Cookie-Parameter +* the header parameter: der Header-Parameter +* the form parameter: der Formular-Parameter +* the payload: die Payload +* the performance: NOT die Performance +* the query: die Query +* the recap: die Zusammenfassung +* the request (what the client sends to the server): der Request +* the request body: der Requestbody +* the request bodies (plural): die Requestbodys +* the response (what the server sends back to the client): die Response +* the return type: der Rückgabetyp +* the return value: der Rückgabewert +* the startup (the event of the app): der Startup +* the shutdown (the event of the app): der Shutdown +* the startup event: das Startup-Event +* the shutdown event: das Shutdown-Event +* the startup (of the server): das Hochfahren +* the startup (the company): das Startup +* the SDK: das SDK +* the tag: der Tag +* the type annotation: die Typannotation +* the type hint: der Typhinweis +* the wildcard: die Wildcard +* the worker class: die Workerklasse +* the worker class: NOT die Arbeiterklasse +* the worker process: der Workerprozess +* the worker process: NOT der Arbeiterprozess +* to commit: committen +* to deploy (in the cloud): deployen +* to modify: ändern +* to serve (an application): bereitstellen +* to serve (a response): ausliefern +* to serve: NOT bedienen +* to upgrade: aktualisieren +* to wrap: wrappen +* to wrap: NOT hüllen +* `foo` as a `type`: `foo` vom Typ `type` +* `foo` as a `type`: `foo`, ein `type` +* FastAPI's X: FastAPIs X +* Starlette's Y: Starlettes Y +* X is case-sensitive: Groß-/Klein­schrei­bung ist relevant in X +* X is case-insensitive: Groß-/Klein­schrei­bung ist nicht relevant in X +* standard Python: Standard-Python +* deprecated: deprecatet ### Other rules -Preserve indentation. Keep emoticons. Encode in utf-8. Use Linux line breaks (LF). +Preserve indentation. Keep emojis. Encode in utf-8. Use Linux line breaks (LF). diff --git a/docs/em/docs/advanced/additional-responses.md b/docs/em/docs/advanced/additional-responses.md deleted file mode 100644 index 655fc7ab6f..0000000000 --- a/docs/em/docs/advanced/additional-responses.md +++ /dev/null @@ -1,247 +0,0 @@ -# 🌖 📨 🗄 - -/// warning - -👉 👍 🏧 ❔. - -🚥 👆 ▶️ ⏮️ **FastAPI**, 👆 💪 🚫 💪 👉. - -/// - -👆 💪 📣 🌖 📨, ⏮️ 🌖 👔 📟, 🔉 🆎, 📛, ♒️. - -👈 🌖 📨 🔜 🔌 🗄 🔗, 👫 🔜 😑 🛠️ 🩺. - -✋️ 👈 🌖 📨 👆 ✔️ ⚒ 💭 👆 📨 `Response` 💖 `JSONResponse` 🔗, ⏮️ 👆 👔 📟 & 🎚. - -## 🌖 📨 ⏮️ `model` - -👆 💪 🚶‍♀️ 👆 *➡ 🛠️ 👨‍🎨* 🔢 `responses`. - -⚫️ 📨 `dict`, 🔑 👔 📟 🔠 📨, 💖 `200`, & 💲 🎏 `dict`Ⓜ ⏮️ ℹ 🔠 👫. - -🔠 👈 📨 `dict`Ⓜ 💪 ✔️ 🔑 `model`, ⚗ Pydantic 🏷, 💖 `response_model`. - -**FastAPI** 🔜 ✊ 👈 🏷, 🏗 🚮 🎻 🔗 & 🔌 ⚫️ ☑ 🥉 🗄. - -🖼, 📣 ➕1️⃣ 📨 ⏮️ 👔 📟 `404` & Pydantic 🏷 `Message`, 👆 💪 ✍: - -{* ../../docs_src/additional_responses/tutorial001.py hl[18,22] *} - -/// note - -✔️ 🤯 👈 👆 ✔️ 📨 `JSONResponse` 🔗. - -/// - -/// info - -`model` 🔑 🚫 🍕 🗄. - -**FastAPI** 🔜 ✊ Pydantic 🏷 ⚪️➡️ 📤, 🏗 `JSON Schema`, & 🚮 ⚫️ ☑ 🥉. - -☑ 🥉: - -* 🔑 `content`, 👈 ✔️ 💲 ➕1️⃣ 🎻 🎚 (`dict`) 👈 🔌: - * 🔑 ⏮️ 📻 🆎, ✅ `application/json`, 👈 🔌 💲 ➕1️⃣ 🎻 🎚, 👈 🔌: - * 🔑 `schema`, 👈 ✔️ 💲 🎻 🔗 ⚪️➡️ 🏷, 📥 ☑ 🥉. - * **FastAPI** 🚮 🔗 📥 🌐 🎻 🔗 ➕1️⃣ 🥉 👆 🗄 ↩️ ✅ ⚫️ 🔗. 👉 🌌, 🎏 🈸 & 👩‍💻 💪 ⚙️ 👈 🎻 🔗 🔗, 🚚 👻 📟 ⚡ 🧰, ♒️. - -/// - -🏗 📨 🗄 👉 *➡ 🛠️* 🔜: - -```JSON hl_lines="3-12" -{ - "responses": { - "404": { - "description": "Additional Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Message" - } - } - } - }, - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Item" - } - } - } - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - } - } - } -} -``` - -🔗 🔗 ➕1️⃣ 🥉 🔘 🗄 🔗: - -```JSON hl_lines="4-16" -{ - "components": { - "schemas": { - "Message": { - "title": "Message", - "required": [ - "message" - ], - "type": "object", - "properties": { - "message": { - "title": "Message", - "type": "string" - } - } - }, - "Item": { - "title": "Item", - "required": [ - "id", - "value" - ], - "type": "object", - "properties": { - "id": { - "title": "Id", - "type": "string" - }, - "value": { - "title": "Value", - "type": "string" - } - } - }, - "ValidationError": { - "title": "ValidationError", - "required": [ - "loc", - "msg", - "type" - ], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "type": "string" - } - }, - "msg": { - "title": "Message", - "type": "string" - }, - "type": { - "title": "Error Type", - "type": "string" - } - } - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": { - "$ref": "#/components/schemas/ValidationError" - } - } - } - } - } - } -} -``` - -## 🌖 🔉 🆎 👑 📨 - -👆 💪 ⚙️ 👉 🎏 `responses` 🔢 🚮 🎏 🔉 🆎 🎏 👑 📨. - -🖼, 👆 💪 🚮 🌖 📻 🆎 `image/png`, 📣 👈 👆 *➡ 🛠️* 💪 📨 🎻 🎚 (⏮️ 📻 🆎 `application/json`) ⚖️ 🇩🇴 🖼: - -{* ../../docs_src/additional_responses/tutorial002.py hl[19:24,28] *} - -/// note - -👀 👈 👆 ✔️ 📨 🖼 ⚙️ `FileResponse` 🔗. - -/// - -/// info - -🚥 👆 ✔ 🎏 📻 🆎 🎯 👆 `responses` 🔢, FastAPI 🔜 🤔 📨 ✔️ 🎏 📻 🆎 👑 📨 🎓 (🔢 `application/json`). - -✋️ 🚥 👆 ✔️ ✔ 🛃 📨 🎓 ⏮️ `None` 🚮 📻 🆎, FastAPI 🔜 ⚙️ `application/json` 🙆 🌖 📨 👈 ✔️ 👨‍💼 🏷. - -/// - -## 🌀 ℹ - -👆 💪 🌀 📨 ℹ ⚪️➡️ 💗 🥉, 🔌 `response_model`, `status_code`, & `responses` 🔢. - -👆 💪 📣 `response_model`, ⚙️ 🔢 👔 📟 `200` (⚖️ 🛃 1️⃣ 🚥 👆 💪), & ⤴️ 📣 🌖 ℹ 👈 🎏 📨 `responses`, 🔗 🗄 🔗. - -**FastAPI** 🔜 🚧 🌖 ℹ ⚪️➡️ `responses`, & 🌀 ⚫️ ⏮️ 🎻 🔗 ⚪️➡️ 👆 🏷. - -🖼, 👆 💪 📣 📨 ⏮️ 👔 📟 `404` 👈 ⚙️ Pydantic 🏷 & ✔️ 🛃 `description`. - -& 📨 ⏮️ 👔 📟 `200` 👈 ⚙️ 👆 `response_model`, ✋️ 🔌 🛃 `example`: - -{* ../../docs_src/additional_responses/tutorial003.py hl[20:31] *} - -⚫️ 🔜 🌐 🌀 & 🔌 👆 🗄, & 🎦 🛠️ 🩺: - - - -## 🌀 🔢 📨 & 🛃 🕐 - -👆 💪 💚 ✔️ 🔁 📨 👈 ✔ 📚 *➡ 🛠️*, ✋️ 👆 💚 🌀 👫 ⏮️ 🛃 📨 💚 🔠 *➡ 🛠️*. - -📚 💼, 👆 💪 ⚙️ 🐍 ⚒ "🏗" `dict` ⏮️ `**dict_to_unpack`: - -```Python -old_dict = { - "old key": "old value", - "second old key": "second old value", -} -new_dict = {**old_dict, "new key": "new value"} -``` - -📥, `new_dict` 🔜 🔌 🌐 🔑-💲 👫 ⚪️➡️ `old_dict` ➕ 🆕 🔑-💲 👫: - -```Python -{ - "old key": "old value", - "second old key": "second old value", - "new key": "new value", -} -``` - -👆 💪 ⚙️ 👈 ⚒ 🏤-⚙️ 🔢 📨 👆 *➡ 🛠️* & 🌀 👫 ⏮️ 🌖 🛃 🕐. - -🖼: - -{* ../../docs_src/additional_responses/tutorial004.py hl[13:17,26] *} - -## 🌖 ℹ 🔃 🗄 📨 - -👀 ⚫️❔ ⚫️❔ 👆 💪 🔌 📨, 👆 💪 ✅ 👉 📄 🗄 🔧: - -* 🗄 📨 🎚, ⚫️ 🔌 `Response Object`. -* 🗄 📨 🎚, 👆 💪 🔌 🕳 ⚪️➡️ 👉 🔗 🔠 📨 🔘 👆 `responses` 🔢. ✅ `description`, `headers`, `content` (🔘 👉 👈 👆 📣 🎏 🔉 🆎 & 🎻 🔗), & `links`. diff --git a/docs/em/docs/advanced/additional-status-codes.md b/docs/em/docs/advanced/additional-status-codes.md deleted file mode 100644 index 907c7e68ee..0000000000 --- a/docs/em/docs/advanced/additional-status-codes.md +++ /dev/null @@ -1,41 +0,0 @@ -# 🌖 👔 📟 - -🔢, **FastAPI** 🔜 📨 📨 ⚙️ `JSONResponse`, 🚮 🎚 👆 📨 ⚪️➡️ 👆 *➡ 🛠️* 🔘 👈 `JSONResponse`. - -⚫️ 🔜 ⚙️ 🔢 👔 📟 ⚖️ 1️⃣ 👆 ⚒ 👆 *➡ 🛠️*. - -## 🌖 👔 📟 - -🚥 👆 💚 📨 🌖 👔 📟 ↖️ ⚪️➡️ 👑 1️⃣, 👆 💪 👈 🛬 `Response` 🔗, 💖 `JSONResponse`, & ⚒ 🌖 👔 📟 🔗. - -🖼, ➡️ 💬 👈 👆 💚 ✔️ *➡ 🛠️* 👈 ✔ ℹ 🏬, & 📨 🇺🇸🔍 👔 📟 2️⃣0️⃣0️⃣ "👌" 🕐❔ 🏆. - -✋️ 👆 💚 ⚫️ 🚫 🆕 🏬. & 🕐❔ 🏬 🚫 🔀 ⏭, ⚫️ ✍ 👫, & 📨 🇺🇸🔍 👔 📟 2️⃣0️⃣1️⃣ "✍". - -🏆 👈, 🗄 `JSONResponse`, & 📨 👆 🎚 📤 🔗, ⚒ `status_code` 👈 👆 💚: - -{* ../../docs_src/additional_status_codes/tutorial001.py hl[4,25] *} - -/// warning - -🕐❔ 👆 📨 `Response` 🔗, 💖 🖼 🔛, ⚫️ 🔜 📨 🔗. - -⚫️ 🏆 🚫 🎻 ⏮️ 🏷, ♒️. - -⚒ 💭 ⚫️ ✔️ 📊 👆 💚 ⚫️ ✔️, & 👈 💲 ☑ 🎻 (🚥 👆 ⚙️ `JSONResponse`). - -/// - -/// note | 📡 ℹ - -👆 💪 ⚙️ `from starlette.responses import JSONResponse`. - -**FastAPI** 🚚 🎏 `starlette.responses` `fastapi.responses` 🏪 👆, 👩‍💻. ✋️ 🌅 💪 📨 👟 🔗 ⚪️➡️ 💃. 🎏 ⏮️ `status`. - -/// - -## 🗄 & 🛠️ 🩺 - -🚥 👆 📨 🌖 👔 📟 & 📨 🔗, 👫 🏆 🚫 🔌 🗄 🔗 (🛠️ 🩺), ↩️ FastAPI 🚫 ✔️ 🌌 💭 ⏪ ⚫️❔ 👆 🚶 📨. - -✋️ 👆 💪 📄 👈 👆 📟, ⚙️: [🌖 📨](additional-responses.md){.internal-link target=_blank}. diff --git a/docs/em/docs/advanced/advanced-dependencies.md b/docs/em/docs/advanced/advanced-dependencies.md deleted file mode 100644 index 3404c26872..0000000000 --- a/docs/em/docs/advanced/advanced-dependencies.md +++ /dev/null @@ -1,65 +0,0 @@ -# 🏧 🔗 - -## 🔗 🔗 - -🌐 🔗 👥 ✔️ 👀 🔧 🔢 ⚖️ 🎓. - -✋️ 📤 💪 💼 🌐❔ 👆 💚 💪 ⚒ 🔢 🔛 🔗, 🍵 ✔️ 📣 📚 🎏 🔢 ⚖️ 🎓. - -➡️ 🌈 👈 👥 💚 ✔️ 🔗 👈 ✅ 🚥 🔢 🔢 `q` 🔌 🔧 🎚. - -✋️ 👥 💚 💪 🔗 👈 🔧 🎚. - -## "🇧🇲" 👐 - -🐍 📤 🌌 ⚒ 👐 🎓 "🇧🇲". - -🚫 🎓 ⚫️ (❔ ⏪ 🇧🇲), ✋️ 👐 👈 🎓. - -👈, 👥 📣 👩‍🔬 `__call__`: - -{* ../../docs_src/dependencies/tutorial011.py hl[10] *} - -👉 💼, 👉 `__call__` ⚫️❔ **FastAPI** 🔜 ⚙️ ✅ 🌖 🔢 & 🎧-🔗, & 👉 ⚫️❔ 🔜 🤙 🚶‍♀️ 💲 🔢 👆 *➡ 🛠️ 🔢* ⏪. - -## 🔗 👐 - -& 🔜, 👥 💪 ⚙️ `__init__` 📣 🔢 👐 👈 👥 💪 ⚙️ "🔗" 🔗: - -{* ../../docs_src/dependencies/tutorial011.py hl[7] *} - -👉 💼, **FastAPI** 🏆 🚫 ⏱ 👆 ⚖️ 💅 🔃 `__init__`, 👥 🔜 ⚙️ ⚫️ 🔗 👆 📟. - -## ✍ 👐 - -👥 💪 ✍ 👐 👉 🎓 ⏮️: - -{* ../../docs_src/dependencies/tutorial011.py hl[16] *} - -& 👈 🌌 👥 💪 "🔗" 👆 🔗, 👈 🔜 ✔️ `"bar"` 🔘 ⚫️, 🔢 `checker.fixed_content`. - -## ⚙️ 👐 🔗 - -⤴️, 👥 💪 ⚙️ 👉 `checker` `Depends(checker)`, ↩️ `Depends(FixedContentQueryChecker)`, ↩️ 🔗 👐, `checker`, 🚫 🎓 ⚫️. - -& 🕐❔ ❎ 🔗, **FastAPI** 🔜 🤙 👉 `checker` 💖: - -```Python -checker(q="somequery") -``` - -...& 🚶‍♀️ ⚫️❔ 👈 📨 💲 🔗 👆 *➡ 🛠️ 🔢* 🔢 `fixed_content_included`: - -{* ../../docs_src/dependencies/tutorial011.py hl[20] *} - -/// tip - -🌐 👉 💪 😑 🎭. & ⚫️ 💪 🚫 📶 🆑 ❔ ⚫️ ⚠. - -👫 🖼 😫 🙅, ✋️ 🎦 ❔ ⚫️ 🌐 👷. - -📃 🔃 💂‍♂, 📤 🚙 🔢 👈 🛠️ 👉 🎏 🌌. - -🚥 👆 🤔 🌐 👉, 👆 ⏪ 💭 ❔ 👈 🚙 🧰 💂‍♂ 👷 🔘. - -/// diff --git a/docs/em/docs/advanced/async-tests.md b/docs/em/docs/advanced/async-tests.md deleted file mode 100644 index 283d4aa097..0000000000 --- a/docs/em/docs/advanced/async-tests.md +++ /dev/null @@ -1,93 +0,0 @@ -# 🔁 💯 - -👆 ✔️ ⏪ 👀 ❔ 💯 👆 **FastAPI** 🈸 ⚙️ 🚚 `TestClient`. 🆙 🔜, 👆 ✔️ 🕴 👀 ❔ ✍ 🔁 💯, 🍵 ⚙️ `async` 🔢. - -➖ 💪 ⚙️ 🔁 🔢 👆 💯 💪 ⚠, 🖼, 🕐❔ 👆 🔬 👆 💽 🔁. 🌈 👆 💚 💯 📨 📨 👆 FastAPI 🈸 & ⤴️ ✔ 👈 👆 👩‍💻 ⏪ ✍ ☑ 💽 💽, ⏪ ⚙️ 🔁 💽 🗃. - -➡️ 👀 ❔ 👥 💪 ⚒ 👈 👷. - -## pytest.mark.anyio - -🚥 👥 💚 🤙 🔁 🔢 👆 💯, 👆 💯 🔢 ✔️ 🔁. AnyIO 🚚 👌 📁 👉, 👈 ✔ 👥 ✔ 👈 💯 🔢 🤙 🔁. - -## 🇸🇲 - -🚥 👆 **FastAPI** 🈸 ⚙️ 😐 `def` 🔢 ↩️ `async def`, ⚫️ `async` 🈸 🔘. - -`TestClient` 🔨 🎱 🔘 🤙 🔁 FastAPI 🈸 👆 😐 `def` 💯 🔢, ⚙️ 🐩 ✳. ✋️ 👈 🎱 🚫 👷 🚫🔜 🕐❔ 👥 ⚙️ ⚫️ 🔘 🔁 🔢. 🏃 👆 💯 🔁, 👥 💪 🙅‍♂ 📏 ⚙️ `TestClient` 🔘 👆 💯 🔢. - -`TestClient` ⚓️ 🔛 🇸🇲, & ↩️, 👥 💪 ⚙️ ⚫️ 🔗 💯 🛠️. - -## 🖼 - -🙅 🖼, ➡️ 🤔 📁 📊 🎏 1️⃣ 🔬 [🦏 🈸](../tutorial/bigger-applications.md){.internal-link target=_blank} & [🔬](../tutorial/testing.md){.internal-link target=_blank}: - -``` -. -├── app -│   ├── __init__.py -│   ├── main.py -│   └── test_main.py -``` - -📁 `main.py` 🔜 ✔️: - -{* ../../docs_src/async_tests/main.py *} - -📁 `test_main.py` 🔜 ✔️ 💯 `main.py`, ⚫️ 💪 👀 💖 👉 🔜: - -{* ../../docs_src/async_tests/test_main.py *} - -## 🏃 ⚫️ - -👆 💪 🏃 👆 💯 🐌 📨: - -
- -```console -$ pytest - ----> 100% -``` - -
- -## ℹ - -📑 `@pytest.mark.anyio` 💬 ✳ 👈 👉 💯 🔢 🔜 🤙 🔁: - -{* ../../docs_src/async_tests/test_main.py hl[7] *} - -/// tip - -🗒 👈 💯 🔢 🔜 `async def` ↩️ `def` ⏭ 🕐❔ ⚙️ `TestClient`. - -/// - -⤴️ 👥 💪 ✍ `AsyncClient` ⏮️ 📱, & 📨 🔁 📨 ⚫️, ⚙️ `await`. - -{* ../../docs_src/async_tests/test_main.py hl[9:12] *} - -👉 🌓: - -```Python -response = client.get('/') -``` - -...👈 👥 ⚙️ ⚒ 👆 📨 ⏮️ `TestClient`. - -/// tip - -🗒 👈 👥 ⚙️ 🔁/⌛ ⏮️ 🆕 `AsyncClient` - 📨 🔁. - -/// - -## 🎏 🔁 🔢 🤙 - -🔬 🔢 🔜 🔁, 👆 💪 🔜 🤙 (& `await`) 🎏 `async` 🔢 ↖️ ⚪️➡️ 📨 📨 👆 FastAPI 🈸 👆 💯, ⚫️❔ 👆 🔜 🤙 👫 🙆 🙆 👆 📟. - -/// tip - -🚥 👆 ⚔ `RuntimeError: Task attached to a different loop` 🕐❔ 🛠️ 🔁 🔢 🤙 👆 💯 (✅ 🕐❔ ⚙️ ✳ MotorClient) 💭 🔗 🎚 👈 💪 🎉 ➰ 🕴 🏞 🔁 🔢, ✅ `'@app.on_event("startup")` ⏲. - -/// diff --git a/docs/em/docs/advanced/behind-a-proxy.md b/docs/em/docs/advanced/behind-a-proxy.md deleted file mode 100644 index 8b14152c9d..0000000000 --- a/docs/em/docs/advanced/behind-a-proxy.md +++ /dev/null @@ -1,359 +0,0 @@ -# ⛅ 🗳 - -⚠, 👆 5️⃣📆 💪 ⚙️ **🗳** 💽 💖 Traefik ⚖️ 👌 ⏮️ 📳 👈 🚮 ➕ ➡ 🔡 👈 🚫 👀 👆 🈸. - -👫 💼 👆 💪 ⚙️ `root_path` 🔗 👆 🈸. - -`root_path` 🛠️ 🚚 🔫 🔧 (👈 FastAPI 🏗 🔛, 🔘 💃). - -`root_path` ⚙️ 🍵 👫 🎯 💼. - -& ⚫️ ⚙️ 🔘 🕐❔ 🗜 🎧-🈸. - -## 🗳 ⏮️ 🎞 ➡ 🔡 - -✔️ 🗳 ⏮️ 🎞 ➡ 🔡, 👉 💼, ⛓ 👈 👆 💪 📣 ➡ `/app` 👆 📟, ✋️ ⤴️, 👆 🚮 🧽 🔛 🔝 (🗳) 👈 🔜 🚮 👆 **FastAPI** 🈸 🔽 ➡ 💖 `/api/v1`. - -👉 💼, ⏮️ ➡ `/app` 🔜 🤙 🍦 `/api/v1/app`. - -✋️ 🌐 👆 📟 ✍ 🤔 📤 `/app`. - -& 🗳 🔜 **"❎"** **➡ 🔡** 🔛 ✈ ⏭ 📶 📨 Uvicorn, 🚧 👆 🈸 🤔 👈 ⚫️ 🍦 `/app`, 👈 👆 🚫 ✔️ ℹ 🌐 👆 📟 🔌 🔡 `/api/v1`. - -🆙 📥, 🌐 🔜 👷 🛎. - -✋️ ⤴️, 🕐❔ 👆 📂 🛠️ 🩺 🎚 (🕸), ⚫️ 🔜 ⌛ 🤚 🗄 🔗 `/openapi.json`, ↩️ `/api/v1/openapi.json`. - -, 🕸 (👈 🏃 🖥) 🔜 🔄 🏆 `/openapi.json` & 🚫🔜 💪 🤚 🗄 🔗. - -↩️ 👥 ✔️ 🗳 ⏮️ ➡ 🔡 `/api/v1` 👆 📱, 🕸 💪 ☕ 🗄 🔗 `/api/v1/openapi.json`. - -```mermaid -graph LR - -browser("Browser") -proxy["Proxy on http://0.0.0.0:9999/api/v1/app"] -server["Server on http://127.0.0.1:8000/app"] - -browser --> proxy -proxy --> server -``` - -/// tip - -📢 `0.0.0.0` 🛎 ⚙️ ⛓ 👈 📋 👂 🔛 🌐 📢 💪 👈 🎰/💽. - -/// - -🩺 🎚 🔜 💪 🗄 🔗 📣 👈 👉 🛠️ `server` 🔎 `/api/v1` (⛅ 🗳). 🖼: - -```JSON hl_lines="4-8" -{ - "openapi": "3.0.2", - // More stuff here - "servers": [ - { - "url": "/api/v1" - } - ], - "paths": { - // More stuff here - } -} -``` - -👉 🖼, "🗳" 💪 🕳 💖 **Traefik**. & 💽 🔜 🕳 💖 **Uvicorn**, 🏃‍♂ 👆 FastAPI 🈸. - -### 🚚 `root_path` - -🏆 👉, 👆 💪 ⚙️ 📋 ⏸ 🎛 `--root-path` 💖: - -
- -```console -$ uvicorn main:app --root-path /api/v1 - -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -``` - -
- -🚥 👆 ⚙️ Hypercorn, ⚫️ ✔️ 🎛 `--root-path`. - -/// note | 📡 ℹ - -🔫 🔧 🔬 `root_path` 👉 ⚙️ 💼. - - & `--root-path` 📋 ⏸ 🎛 🚚 👈 `root_path`. - -/// - -### ✅ ⏮️ `root_path` - -👆 💪 🤚 ⏮️ `root_path` ⚙️ 👆 🈸 🔠 📨, ⚫️ 🍕 `scope` 📖 (👈 🍕 🔫 🔌). - -📥 👥 ✅ ⚫️ 📧 🎦 🎯. - -{* ../../docs_src/behind_a_proxy/tutorial001.py hl[8] *} - -⤴️, 🚥 👆 ▶️ Uvicorn ⏮️: - -
- -```console -$ uvicorn main:app --root-path /api/v1 - -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -``` - -
- -📨 🔜 🕳 💖: - -```JSON -{ - "message": "Hello World", - "root_path": "/api/v1" -} -``` - -### ⚒ `root_path` FastAPI 📱 - -👐, 🚥 👆 🚫 ✔️ 🌌 🚚 📋 ⏸ 🎛 💖 `--root-path` ⚖️ 🌓, 👆 💪 ⚒ `root_path` 🔢 🕐❔ 🏗 👆 FastAPI 📱: - -{* ../../docs_src/behind_a_proxy/tutorial002.py hl[3] *} - -🚶‍♀️ `root_path` `FastAPI` 🔜 🌓 🚶‍♀️ `--root-path` 📋 ⏸ 🎛 Uvicorn ⚖️ Hypercorn. - -### 🔃 `root_path` - -✔️ 🤯 👈 💽 (Uvicorn) 🏆 🚫 ⚙️ 👈 `root_path` 🕳 🙆 🌘 🚶‍♀️ ⚫️ 📱. - -✋️ 🚥 👆 🚶 ⏮️ 👆 🖥 http://127.0.0.1:8000/app 👆 🔜 👀 😐 📨: - -```JSON -{ - "message": "Hello World", - "root_path": "/api/v1" -} -``` - -, ⚫️ 🏆 🚫 ⌛ 🔐 `http://127.0.0.1:8000/api/v1/app`. - -Uvicorn 🔜 ⌛ 🗳 🔐 Uvicorn `http://127.0.0.1:8000/app`, & ⤴️ ⚫️ 🔜 🗳 🎯 🚮 ➕ `/api/v1` 🔡 🔛 🔝. - -## 🔃 🗳 ⏮️ 🎞 ➡ 🔡 - -✔️ 🤯 👈 🗳 ⏮️ 🎞 ➡ 🔡 🕴 1️⃣ 🌌 🔗 ⚫️. - -🎲 📚 💼 🔢 🔜 👈 🗳 🚫 ✔️ 🏚 ➡ 🔡. - -💼 💖 👈 (🍵 🎞 ➡ 🔡), 🗳 🔜 👂 🔛 🕳 💖 `https://myawesomeapp.com`, & ⤴️ 🚥 🖥 🚶 `https://myawesomeapp.com/api/v1/app` & 👆 💽 (✅ Uvicorn) 👂 🔛 `http://127.0.0.1:8000` 🗳 (🍵 🎞 ➡ 🔡) 🔜 🔐 Uvicorn 🎏 ➡: `http://127.0.0.1:8000/api/v1/app`. - -## 🔬 🌐 ⏮️ Traefik - -👆 💪 💪 🏃 🥼 🌐 ⏮️ 🎞 ➡ 🔡 ⚙️ Traefik. - -⏬ Traefik, ⚫️ 👁 💱, 👆 💪 ⚗ 🗜 📁 & 🏃 ⚫️ 🔗 ⚪️➡️ 📶. - -⤴️ ✍ 📁 `traefik.toml` ⏮️: - -```TOML hl_lines="3" -[entryPoints] - [entryPoints.http] - address = ":9999" - -[providers] - [providers.file] - filename = "routes.toml" -``` - -👉 💬 Traefik 👂 🔛 ⛴ 9️⃣9️⃣9️⃣9️⃣ & ⚙️ ➕1️⃣ 📁 `routes.toml`. - -/// tip - -👥 ⚙️ ⛴ 9️⃣9️⃣9️⃣9️⃣ ↩️ 🐩 🇺🇸🔍 ⛴ 8️⃣0️⃣ 👈 👆 🚫 ✔️ 🏃 ⚫️ ⏮️ 📡 (`sudo`) 😌. - -/// - -🔜 ✍ 👈 🎏 📁 `routes.toml`: - -```TOML hl_lines="5 12 20" -[http] - [http.middlewares] - - [http.middlewares.api-stripprefix.stripPrefix] - prefixes = ["/api/v1"] - - [http.routers] - - [http.routers.app-http] - entryPoints = ["http"] - service = "app" - rule = "PathPrefix(`/api/v1`)" - middlewares = ["api-stripprefix"] - - [http.services] - - [http.services.app] - [http.services.app.loadBalancer] - [[http.services.app.loadBalancer.servers]] - url = "http://127.0.0.1:8000" -``` - -👉 📁 🔗 Traefik ⚙️ ➡ 🔡 `/api/v1`. - -& ⤴️ ⚫️ 🔜 ❎ 🚮 📨 👆 Uvicorn 🏃‍♂ 🔛 `http://127.0.0.1:8000`. - -🔜 ▶️ Traefik: - -
- -```console -$ ./traefik --configFile=traefik.toml - -INFO[0000] Configuration loaded from file: /home/user/awesomeapi/traefik.toml -``` - -
- -& 🔜 ▶️ 👆 📱 ⏮️ Uvicorn, ⚙️ `--root-path` 🎛: - -
- -```console -$ uvicorn main:app --root-path /api/v1 - -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -``` - -
- -### ✅ 📨 - -🔜, 🚥 👆 🚶 📛 ⏮️ ⛴ Uvicorn: http://127.0.0.1:8000/app, 👆 🔜 👀 😐 📨: - -```JSON -{ - "message": "Hello World", - "root_path": "/api/v1" -} -``` - -/// tip - -👀 👈 ✋️ 👆 🔐 ⚫️ `http://127.0.0.1:8000/app` ⚫️ 🎦 `root_path` `/api/v1`, ✊ ⚪️➡️ 🎛 `--root-path`. - -/// - -& 🔜 📂 📛 ⏮️ ⛴ Traefik, ✅ ➡ 🔡: http://127.0.0.1:9999/api/v1/app. - -👥 🤚 🎏 📨: - -```JSON -{ - "message": "Hello World", - "root_path": "/api/v1" -} -``` - -✋️ 👉 🕰 📛 ⏮️ 🔡 ➡ 🚚 🗳: `/api/v1`. - -↗️, 💭 📥 👈 👱 🔜 🔐 📱 🔘 🗳, ⏬ ⏮️ ➡ 🔡 `/app/v1` "☑" 1️⃣. - -& ⏬ 🍵 ➡ 🔡 (`http://127.0.0.1:8000/app`), 🚚 Uvicorn 🔗, 🔜 🎯 _🗳_ (Traefik) 🔐 ⚫️. - -👈 🎦 ❔ 🗳 (Traefik) ⚙️ ➡ 🔡 & ❔ 💽 (Uvicorn) ⚙️ `root_path` ⚪️➡️ 🎛 `--root-path`. - -### ✅ 🩺 🎚 - -✋️ 📥 🎊 🍕. 👶 - -"🛂" 🌌 🔐 📱 🔜 🔘 🗳 ⏮️ ➡ 🔡 👈 👥 🔬. , 👥 🔜 ⌛, 🚥 👆 🔄 🩺 🎚 🍦 Uvicorn 🔗, 🍵 ➡ 🔡 📛, ⚫️ 🏆 🚫 👷, ↩️ ⚫️ ⌛ 🔐 🔘 🗳. - -👆 💪 ✅ ⚫️ http://127.0.0.1:8000/docs: - - - -✋️ 🚥 👥 🔐 🩺 🎚 "🛂" 📛 ⚙️ 🗳 ⏮️ ⛴ `9999`, `/api/v1/docs`, ⚫️ 👷 ☑ ❗ 👶 - -👆 💪 ✅ ⚫️ http://127.0.0.1:9999/api/v1/docs: - - - -▶️️ 👥 💚 ⚫️. 👶 👶 - -👉 ↩️ FastAPI ⚙️ 👉 `root_path` ✍ 🔢 `server` 🗄 ⏮️ 📛 🚚 `root_path`. - -## 🌖 💽 - -/// warning - -👉 🌅 🏧 ⚙️ 💼. 💭 🆓 🚶 ⚫️. - -/// - -🔢, **FastAPI** 🔜 ✍ `server` 🗄 🔗 ⏮️ 📛 `root_path`. - -✋️ 👆 💪 🚚 🎏 🎛 `servers`, 🖼 🚥 👆 💚 *🎏* 🩺 🎚 🔗 ⏮️ 🏗 & 🏭 🌐. - -🚥 👆 🚶‍♀️ 🛃 📇 `servers` & 📤 `root_path` (↩️ 👆 🛠️ 👨‍❤‍👨 ⛅ 🗳), **FastAPI** 🔜 📩 "💽" ⏮️ 👉 `root_path` ▶️ 📇. - -🖼: - -{* ../../docs_src/behind_a_proxy/tutorial003.py hl[4:7] *} - -🔜 🏗 🗄 🔗 💖: - -```JSON hl_lines="5-7" -{ - "openapi": "3.0.2", - // More stuff here - "servers": [ - { - "url": "/api/v1" - }, - { - "url": "https://stag.example.com", - "description": "Staging environment" - }, - { - "url": "https://prod.example.com", - "description": "Production environment" - } - ], - "paths": { - // More stuff here - } -} -``` - -/// tip - -👀 🚘-🏗 💽 ⏮️ `url` 💲 `/api/v1`, ✊ ⚪️➡️ `root_path`. - -/// - -🩺 🎚 http://127.0.0.1:9999/api/v1/docs ⚫️ 🔜 👀 💖: - - - -/// tip - -🩺 🎚 🔜 🔗 ⏮️ 💽 👈 👆 🖊. - -/// - -### ❎ 🏧 💽 ⚪️➡️ `root_path` - -🚥 👆 🚫 💚 **FastAPI** 🔌 🏧 💽 ⚙️ `root_path`, 👆 💪 ⚙️ 🔢 `root_path_in_servers=False`: - -{* ../../docs_src/behind_a_proxy/tutorial004.py hl[9] *} - -& ⤴️ ⚫️ 🏆 🚫 🔌 ⚫️ 🗄 🔗. - -## 🗜 🎧-🈸 - -🚥 👆 💪 🗻 🎧-🈸 (🔬 [🎧 🈸 - 🗻](sub-applications.md){.internal-link target=_blank}) ⏪ ⚙️ 🗳 ⏮️ `root_path`, 👆 💪 ⚫️ 🛎, 👆 🔜 ⌛. - -FastAPI 🔜 🔘 ⚙️ `root_path` 🎆, ⚫️ 🔜 👷. 👶 diff --git a/docs/em/docs/advanced/custom-response.md b/docs/em/docs/advanced/custom-response.md deleted file mode 100644 index ab95b3e7be..0000000000 --- a/docs/em/docs/advanced/custom-response.md +++ /dev/null @@ -1,303 +0,0 @@ -# 🛃 📨 - 🕸, 🎏, 📁, 🎏 - -🔢, **FastAPI** 🔜 📨 📨 ⚙️ `JSONResponse`. - -👆 💪 🔐 ⚫️ 🛬 `Response` 🔗 👀 [📨 📨 🔗](response-directly.md){.internal-link target=_blank}. - -✋️ 🚥 👆 📨 `Response` 🔗, 📊 🏆 🚫 🔁 🗜, & 🧾 🏆 🚫 🔁 🏗 (🖼, 🔌 🎯 "📻 🆎", 🇺🇸🔍 🎚 `Content-Type` 🍕 🏗 🗄). - -✋️ 👆 💪 📣 `Response` 👈 👆 💚 ⚙️, *➡ 🛠️ 👨‍🎨*. - -🎚 👈 👆 📨 ⚪️➡️ 👆 *➡ 🛠️ 🔢* 🔜 🚮 🔘 👈 `Response`. - -& 🚥 👈 `Response` ✔️ 🎻 📻 🆎 (`application/json`), 💖 💼 ⏮️ `JSONResponse` & `UJSONResponse`, 💽 👆 📨 🔜 🔁 🗜 (& ⛽) ⏮️ 🙆 Pydantic `response_model` 👈 👆 📣 *➡ 🛠️ 👨‍🎨*. - -/// note - -🚥 👆 ⚙️ 📨 🎓 ⏮️ 🙅‍♂ 📻 🆎, FastAPI 🔜 ⌛ 👆 📨 ✔️ 🙅‍♂ 🎚, ⚫️ 🔜 🚫 📄 📨 📁 🚮 🏗 🗄 🩺. - -/// - -## ⚙️ `ORJSONResponse` - -🖼, 🚥 👆 ✊ 🎭, 👆 💪 ❎ & ⚙️ `orjson` & ⚒ 📨 `ORJSONResponse`. - -🗄 `Response` 🎓 (🎧-🎓) 👆 💚 ⚙️ & 📣 ⚫️ *➡ 🛠️ 👨‍🎨*. - -⭕ 📨, 📨 `Response` 🔗 🌅 ⏩ 🌘 🛬 📖. - -👉 ↩️ 🔢, FastAPI 🔜 ✔ 🔠 🏬 🔘 & ⚒ 💭 ⚫️ 🎻 ⏮️ 🎻, ⚙️ 🎏 [🎻 🔗 🔢](../tutorial/encoder.md){.internal-link target=_blank} 🔬 🔰. 👉 ⚫️❔ ✔ 👆 📨 **❌ 🎚**, 🖼 💽 🏷. - -✋️ 🚥 👆 🎯 👈 🎚 👈 👆 🛬 **🎻 ⏮️ 🎻**, 👆 💪 🚶‍♀️ ⚫️ 🔗 📨 🎓 & ❎ ➕ 🌥 👈 FastAPI 🔜 ✔️ 🚶‍♀️ 👆 📨 🎚 🔘 `jsonable_encoder` ⏭ 🚶‍♀️ ⚫️ 📨 🎓. - -{* ../../docs_src/custom_response/tutorial001b.py hl[2,7] *} - -/// info - -🔢 `response_class` 🔜 ⚙️ 🔬 "📻 🆎" 📨. - -👉 💼, 🇺🇸🔍 🎚 `Content-Type` 🔜 ⚒ `application/json`. - - & ⚫️ 🔜 📄 ✅ 🗄. - -/// - -/// tip - -`ORJSONResponse` ⏳ 🕴 💪 FastAPI, 🚫 💃. - -/// - -## 🕸 📨 - -📨 📨 ⏮️ 🕸 🔗 ⚪️➡️ **FastAPI**, ⚙️ `HTMLResponse`. - -* 🗄 `HTMLResponse`. -* 🚶‍♀️ `HTMLResponse` 🔢 `response_class` 👆 *➡ 🛠️ 👨‍🎨*. - -{* ../../docs_src/custom_response/tutorial002.py hl[2,7] *} - -/// info - -🔢 `response_class` 🔜 ⚙️ 🔬 "📻 🆎" 📨. - -👉 💼, 🇺🇸🔍 🎚 `Content-Type` 🔜 ⚒ `text/html`. - - & ⚫️ 🔜 📄 ✅ 🗄. - -/// - -### 📨 `Response` - -👀 [📨 📨 🔗](response-directly.md){.internal-link target=_blank}, 👆 💪 🔐 📨 🔗 👆 *➡ 🛠️*, 🛬 ⚫️. - -🎏 🖼 ⚪️➡️ 🔛, 🛬 `HTMLResponse`, 💪 👀 💖: - -{* ../../docs_src/custom_response/tutorial003.py hl[2,7,19] *} - -/// warning - -`Response` 📨 🔗 👆 *➡ 🛠️ 🔢* 🏆 🚫 📄 🗄 (🖼, `Content-Type` 🏆 🚫 📄) & 🏆 🚫 ⭐ 🏧 🎓 🩺. - -/// - -/// info - -↗️, ☑ `Content-Type` 🎚, 👔 📟, ♒️, 🔜 👟 ⚪️➡️ `Response` 🎚 👆 📨. - -/// - -### 📄 🗄 & 🔐 `Response` - -🚥 👆 💚 🔐 📨 ⚪️➡️ 🔘 🔢 ✋️ 🎏 🕰 📄 "📻 🆎" 🗄, 👆 💪 ⚙️ `response_class` 🔢 & 📨 `Response` 🎚. - -`response_class` 🔜 ⤴️ ⚙️ 🕴 📄 🗄 *➡ 🛠️*, ✋️ 👆 `Response` 🔜 ⚙️. - -#### 📨 `HTMLResponse` 🔗 - -🖼, ⚫️ 💪 🕳 💖: - -{* ../../docs_src/custom_response/tutorial004.py hl[7,21,23] *} - -👉 🖼, 🔢 `generate_html_response()` ⏪ 🏗 & 📨 `Response` ↩️ 🛬 🕸 `str`. - -🛬 🏁 🤙 `generate_html_response()`, 👆 ⏪ 🛬 `Response` 👈 🔜 🔐 🔢 **FastAPI** 🎭. - -✋️ 👆 🚶‍♀️ `HTMLResponse` `response_class` 💁‍♂️, **FastAPI** 🔜 💭 ❔ 📄 ⚫️ 🗄 & 🎓 🩺 🕸 ⏮️ `text/html`: - - - -## 💪 📨 - -📥 💪 📨. - -✔️ 🤯 👈 👆 💪 ⚙️ `Response` 📨 🕳 🙆, ⚖️ ✍ 🛃 🎧-🎓. - -/// note | 📡 ℹ - -👆 💪 ⚙️ `from starlette.responses import HTMLResponse`. - -**FastAPI** 🚚 🎏 `starlette.responses` `fastapi.responses` 🏪 👆, 👩‍💻. ✋️ 🌅 💪 📨 👟 🔗 ⚪️➡️ 💃. - -/// - -### `Response` - -👑 `Response` 🎓, 🌐 🎏 📨 😖 ⚪️➡️ ⚫️. - -👆 💪 📨 ⚫️ 🔗. - -⚫️ 🚫 📄 🔢: - -* `content` - `str` ⚖️ `bytes`. -* `status_code` - `int` 🇺🇸🔍 👔 📟. -* `headers` - `dict` 🎻. -* `media_type` - `str` 🤝 📻 🆎. 🤶 Ⓜ. `"text/html"`. - -FastAPI (🤙 💃) 🔜 🔁 🔌 🎚-📐 🎚. ⚫️ 🔜 🔌 🎚-🆎 🎚, ⚓️ 🔛 = & 🔁 = ✍ 🆎. - -{* ../../docs_src/response_directly/tutorial002.py hl[1,18] *} - -### `HTMLResponse` - -✊ ✍ ⚖️ 🔢 & 📨 🕸 📨, 👆 ✍ 🔛. - -### `PlainTextResponse` - -✊ ✍ ⚖️ 🔢 & 📨 ✅ ✍ 📨. - -{* ../../docs_src/custom_response/tutorial005.py hl[2,7,9] *} - -### `JSONResponse` - -✊ 💽 & 📨 `application/json` 🗜 📨. - -👉 🔢 📨 ⚙️ **FastAPI**, 👆 ✍ 🔛. - -### `ORJSONResponse` - -⏩ 🎛 🎻 📨 ⚙️ `orjson`, 👆 ✍ 🔛. - -### `UJSONResponse` - -🎛 🎻 📨 ⚙️ `ujson`. - -/// warning - -`ujson` 🌘 💛 🌘 🐍 🏗-🛠️ ❔ ⚫️ 🍵 📐-💼. - -/// - -{* ../../docs_src/custom_response/tutorial001.py hl[2,7] *} - -/// tip - -⚫️ 💪 👈 `ORJSONResponse` 💪 ⏩ 🎛. - -/// - -### `RedirectResponse` - -📨 🇺🇸🔍 ❎. ⚙️ 3️⃣0️⃣7️⃣ 👔 📟 (🍕 ❎) 🔢. - -👆 💪 📨 `RedirectResponse` 🔗: - -{* ../../docs_src/custom_response/tutorial006.py hl[2,9] *} - ---- - -⚖️ 👆 💪 ⚙️ ⚫️ `response_class` 🔢: - - -{* ../../docs_src/custom_response/tutorial006b.py hl[2,7,9] *} - -🚥 👆 👈, ⤴️ 👆 💪 📨 📛 🔗 ⚪️➡️ 👆 *➡ 🛠️* 🔢. - -👉 💼, `status_code` ⚙️ 🔜 🔢 1️⃣ `RedirectResponse`, ❔ `307`. - ---- - -👆 💪 ⚙️ `status_code` 🔢 🌀 ⏮️ `response_class` 🔢: - -{* ../../docs_src/custom_response/tutorial006c.py hl[2,7,9] *} - -### `StreamingResponse` - -✊ 🔁 🚂 ⚖️ 😐 🚂/🎻 & 🎏 📨 💪. - -{* ../../docs_src/custom_response/tutorial007.py hl[2,14] *} - -#### ⚙️ `StreamingResponse` ⏮️ 📁-💖 🎚 - -🚥 👆 ✔️ 📁-💖 🎚 (✅ 🎚 📨 `open()`), 👆 💪 ✍ 🚂 🔢 🔁 🤭 👈 📁-💖 🎚. - -👈 🌌, 👆 🚫 ✔️ ✍ ⚫️ 🌐 🥇 💾, & 👆 💪 🚶‍♀️ 👈 🚂 🔢 `StreamingResponse`, & 📨 ⚫️. - -👉 🔌 📚 🗃 🔗 ⏮️ ☁ 💾, 📹 🏭, & 🎏. - -```{ .python .annotate hl_lines="2 10-12 14" } -{!../../docs_src/custom_response/tutorial008.py!} -``` - -1️⃣. 👉 🚂 🔢. ⚫️ "🚂 🔢" ↩️ ⚫️ 🔌 `yield` 📄 🔘. -2️⃣. ⚙️ `with` 🍫, 👥 ⚒ 💭 👈 📁-💖 🎚 📪 ⏮️ 🚂 🔢 🔨. , ⏮️ ⚫️ 🏁 📨 📨. -3️⃣. 👉 `yield from` 💬 🔢 🔁 🤭 👈 👜 🌟 `file_like`. & ⤴️, 🔠 🍕 🔁, 🌾 👈 🍕 👟 ⚪️➡️ 👉 🚂 🔢. - - , ⚫️ 🚂 🔢 👈 📨 "🏭" 👷 🕳 🙆 🔘. - - 🔨 ⚫️ 👉 🌌, 👥 💪 🚮 ⚫️ `with` 🍫, & 👈 🌌, 🚚 👈 ⚫️ 📪 ⏮️ 🏁. - -/// tip - -👀 👈 📥 👥 ⚙️ 🐩 `open()` 👈 🚫 🐕‍🦺 `async` & `await`, 👥 📣 ➡ 🛠️ ⏮️ 😐 `def`. - -/// - -### `FileResponse` - -🔁 🎏 📁 📨. - -✊ 🎏 ⚒ ❌ 🔗 🌘 🎏 📨 🆎: - -* `path` - 📁 📁 🎏. -* `headers` - 🙆 🛃 🎚 🔌, 📖. -* `media_type` - 🎻 🤝 📻 🆎. 🚥 🔢, 📁 ⚖️ ➡ 🔜 ⚙️ 🔑 📻 🆎. -* `filename` - 🚥 ⚒, 👉 🔜 🔌 📨 `Content-Disposition`. - -📁 📨 🔜 🔌 ☑ `Content-Length`, `Last-Modified` & `ETag` 🎚. - -{* ../../docs_src/custom_response/tutorial009.py hl[2,10] *} - -👆 💪 ⚙️ `response_class` 🔢: - -{* ../../docs_src/custom_response/tutorial009b.py hl[2,8,10] *} - -👉 💼, 👆 💪 📨 📁 ➡ 🔗 ⚪️➡️ 👆 *➡ 🛠️* 🔢. - -## 🛃 📨 🎓 - -👆 💪 ✍ 👆 👍 🛃 📨 🎓, 😖 ⚪️➡️ `Response` & ⚙️ ⚫️. - -🖼, ➡️ 💬 👈 👆 💚 ⚙️ `orjson`, ✋️ ⏮️ 🛃 ⚒ 🚫 ⚙️ 🔌 `ORJSONResponse` 🎓. - -➡️ 💬 👆 💚 ⚫️ 📨 🔂 & 📁 🎻, 👆 💚 ⚙️ Orjson 🎛 `orjson.OPT_INDENT_2`. - -👆 💪 ✍ `CustomORJSONResponse`. 👑 👜 👆 ✔️ ✍ `Response.render(content)` 👩‍🔬 👈 📨 🎚 `bytes`: - -{* ../../docs_src/custom_response/tutorial009c.py hl[9:14,17] *} - -🔜 ↩️ 🛬: - -```json -{"message": "Hello World"} -``` - -...👉 📨 🔜 📨: - -```json -{ - "message": "Hello World" -} -``` - -↗️, 👆 🔜 🎲 🔎 🌅 👍 🌌 ✊ 📈 👉 🌘 ❕ 🎻. 👶 - -## 🔢 📨 🎓 - -🕐❔ 🏗 **FastAPI** 🎓 👐 ⚖️ `APIRouter` 👆 💪 ✔ ❔ 📨 🎓 ⚙️ 🔢. - -🔢 👈 🔬 👉 `default_response_class`. - -🖼 🔛, **FastAPI** 🔜 ⚙️ `ORJSONResponse` 🔢, 🌐 *➡ 🛠️*, ↩️ `JSONResponse`. - -{* ../../docs_src/custom_response/tutorial010.py hl[2,4] *} - -/// tip - -👆 💪 🔐 `response_class` *➡ 🛠️* ⏭. - -/// - -## 🌖 🧾 - -👆 💪 📣 📻 🆎 & 📚 🎏 ℹ 🗄 ⚙️ `responses`: [🌖 📨 🗄](additional-responses.md){.internal-link target=_blank}. diff --git a/docs/em/docs/advanced/dataclasses.md b/docs/em/docs/advanced/dataclasses.md deleted file mode 100644 index 4dd597262a..0000000000 --- a/docs/em/docs/advanced/dataclasses.md +++ /dev/null @@ -1,97 +0,0 @@ -# ⚙️ 🎻 - -FastAPI 🏗 🔛 🔝 **Pydantic**, & 👤 ✔️ 🌏 👆 ❔ ⚙️ Pydantic 🏷 📣 📨 & 📨. - -✋️ FastAPI 🐕‍🦺 ⚙️ `dataclasses` 🎏 🌌: - -{* ../../docs_src/dataclasses/tutorial001.py hl[1,7:12,19:20] *} - -👉 🐕‍🦺 👏 **Pydantic**, ⚫️ ✔️ 🔗 🐕‍🦺 `dataclasses`. - -, ⏮️ 📟 🔛 👈 🚫 ⚙️ Pydantic 🎯, FastAPI ⚙️ Pydantic 🗜 📚 🐩 🎻 Pydantic 👍 🍛 🎻. - -& ↗️, ⚫️ 🐕‍🦺 🎏: - -* 💽 🔬 -* 💽 🛠️ -* 💽 🧾, ♒️. - -👉 👷 🎏 🌌 ⏮️ Pydantic 🏷. & ⚫️ 🤙 🏆 🎏 🌌 🔘, ⚙️ Pydantic. - -/// info - -✔️ 🤯 👈 🎻 💪 🚫 🌐 Pydantic 🏷 💪. - -, 👆 5️⃣📆 💪 ⚙️ Pydantic 🏷. - -✋️ 🚥 👆 ✔️ 📚 🎻 🤥 🤭, 👉 👌 🎱 ⚙️ 👫 🏋️ 🕸 🛠️ ⚙️ FastAPI. 👶 - -/// - -## 🎻 `response_model` - -👆 💪 ⚙️ `dataclasses` `response_model` 🔢: - -{* ../../docs_src/dataclasses/tutorial002.py hl[1,7:13,19] *} - -🎻 🔜 🔁 🗜 Pydantic 🎻. - -👉 🌌, 🚮 🔗 🔜 🎦 🆙 🛠️ 🩺 👩‍💻 🔢: - - - -## 🎻 🔁 📊 📊 - -👆 💪 🌀 `dataclasses` ⏮️ 🎏 🆎 ✍ ⚒ 🐦 📊 📊. - -💼, 👆 💪 ✔️ ⚙️ Pydantic ⏬ `dataclasses`. 🖼, 🚥 👆 ✔️ ❌ ⏮️ 🔁 🏗 🛠️ 🧾. - -👈 💼, 👆 💪 🎯 💱 🐩 `dataclasses` ⏮️ `pydantic.dataclasses`, ❔ 💧-♻: - -```{ .python .annotate hl_lines="1 5 8-11 14-17 23-25 28" } -{!../../docs_src/dataclasses/tutorial003.py!} -``` - -1️⃣. 👥 🗄 `field` ⚪️➡️ 🐩 `dataclasses`. - -2️⃣. `pydantic.dataclasses` 💧-♻ `dataclasses`. - -3️⃣. `Author` 🎻 🔌 📇 `Item` 🎻. - -4️⃣. `Author` 🎻 ⚙️ `response_model` 🔢. - -5️⃣. 👆 💪 ⚙️ 🎏 🐩 🆎 ✍ ⏮️ 🎻 📨 💪. - - 👉 💼, ⚫️ 📇 `Item` 🎻. - -6️⃣. 📥 👥 🛬 📖 👈 🔌 `items` ❔ 📇 🎻. - - FastAPI 🎯 💽 🎻. - -7️⃣. 📥 `response_model` ⚙️ 🆎 ✍ 📇 `Author` 🎻. - - 🔄, 👆 💪 🌀 `dataclasses` ⏮️ 🐩 🆎 ✍. - -8️⃣. 👀 👈 👉 *➡ 🛠️ 🔢* ⚙️ 🥔 `def` ↩️ `async def`. - - 🕧, FastAPI 👆 💪 🌀 `def` & `async def` 💪. - - 🚥 👆 💪 ↗️ 🔃 🕐❔ ⚙️ ❔, ✅ 👅 📄 _"🏃 ❓" _ 🩺 🔃 `async` & `await`. - -9️⃣. 👉 *➡ 🛠️ 🔢* 🚫 🛬 🎻 (👐 ⚫️ 💪), ✋️ 📇 📖 ⏮️ 🔗 💽. - - FastAPI 🔜 ⚙️ `response_model` 🔢 (👈 🔌 🎻) 🗜 📨. - -👆 💪 🌀 `dataclasses` ⏮️ 🎏 🆎 ✍ 📚 🎏 🌀 📨 🏗 📊 📊. - -✅-📟 ✍ 💁‍♂ 🔛 👀 🌅 🎯 ℹ. - -## 💡 🌅 - -👆 💪 🌀 `dataclasses` ⏮️ 🎏 Pydantic 🏷, 😖 ⚪️➡️ 👫, 🔌 👫 👆 👍 🏷, ♒️. - -💡 🌅, ✅ Pydantic 🩺 🔃 🎻. - -## ⏬ - -👉 💪 ↩️ FastAPI ⏬ `0.67.0`. 👶 diff --git a/docs/em/docs/advanced/events.md b/docs/em/docs/advanced/events.md deleted file mode 100644 index dcaac710e1..0000000000 --- a/docs/em/docs/advanced/events.md +++ /dev/null @@ -1,163 +0,0 @@ -# 🔆 🎉 - -👆 💪 🔬 ⚛ (📟) 👈 🔜 🛠️ ⏭ 🈸 **▶️ 🆙**. 👉 ⛓ 👈 👉 📟 🔜 🛠️ **🕐**, **⏭** 🈸 **▶️ 📨 📨**. - -🎏 🌌, 👆 💪 🔬 ⚛ (📟) 👈 🔜 🛠️ 🕐❔ 🈸 **🤫 🔽**. 👉 💼, 👉 📟 🔜 🛠️ **🕐**, **⏮️** ✔️ 🍵 🎲 **📚 📨**. - -↩️ 👉 📟 🛠️ ⏭ 🈸 **▶️** ✊ 📨, & ▶️️ ⏮️ ⚫️ **🏁** 🚚 📨, ⚫️ 📔 🎂 🈸 **🔆** (🔤 "🔆" 🔜 ⚠ 🥈 👶). - -👉 💪 📶 ⚠ ⚒ 🆙 **ℹ** 👈 👆 💪 ⚙️ 🎂 📱, & 👈 **💰** 👪 📨, &/⚖️ 👈 👆 💪 **🧹 🆙** ⏮️. 🖼, 💽 🔗 🎱, ⚖️ 🚚 🔗 🎰 🏫 🏷. - -## ⚙️ 💼 - -➡️ ▶️ ⏮️ 🖼 **⚙️ 💼** & ⤴️ 👀 ❔ ❎ ⚫️ ⏮️ 👉. - -➡️ 🌈 👈 👆 ✔️ **🎰 🏫 🏷** 👈 👆 💚 ⚙️ 🍵 📨. 👶 - -🎏 🏷 🔗 👪 📨,, ⚫️ 🚫 1️⃣ 🏷 📍 📨, ⚖️ 1️⃣ 📍 👩‍💻 ⚖️ 🕳 🎏. - -➡️ 🌈 👈 🚚 🏷 💪 **✊ 🕰**, ↩️ ⚫️ ✔️ ✍ 📚 **💽 ⚪️➡️ 💾**. 👆 🚫 💚 ⚫️ 🔠 📨. - -👆 💪 📐 ⚫️ 🔝 🎚 🕹/📁, ✋️ 👈 🔜 ⛓ 👈 ⚫️ 🔜 **📐 🏷** 🚥 👆 🏃‍♂ 🙅 🏧 💯, ⤴️ 👈 💯 🔜 **🐌** ↩️ ⚫️ 🔜 ✔️ ⌛ 🏷 📐 ⏭ 💆‍♂ 💪 🏃 🔬 🍕 📟. - -👈 ⚫️❔ 👥 🔜 ❎, ➡️ 📐 🏷 ⏭ 📨 🍵, ✋️ 🕴 ▶️️ ⏭ 🈸 ▶️ 📨 📨, 🚫 ⏪ 📟 ➖ 📐. - -## 🔆 - -👆 💪 🔬 👉 *🕴* & *🤫* ⚛ ⚙️ `lifespan` 🔢 `FastAPI` 📱, & "🔑 👨‍💼" (👤 🔜 🎦 👆 ⚫️❔ 👈 🥈). - -➡️ ▶️ ⏮️ 🖼 & ⤴️ 👀 ⚫️ ℹ. - -👥 ✍ 🔁 🔢 `lifespan()` ⏮️ `yield` 💖 👉: - -{* ../../docs_src/events/tutorial003.py hl[16,19] *} - -📥 👥 ⚖ 😥 *🕴* 🛠️ 🚚 🏷 🚮 (❌) 🏷 🔢 📖 ⏮️ 🎰 🏫 🏷 ⏭ `yield`. 👉 📟 🔜 🛠️ **⏭** 🈸 **▶️ ✊ 📨**, ⏮️ *🕴*. - -& ⤴️, ▶️️ ⏮️ `yield`, 👥 🚚 🏷. 👉 📟 🔜 🛠️ **⏮️** 🈸 **🏁 🚚 📨**, ▶️️ ⏭ *🤫*. 👉 💪, 🖼, 🚀 ℹ 💖 💾 ⚖️ 💻. - -/// tip - -`shutdown` 🔜 🔨 🕐❔ 👆 **⛔️** 🈸. - -🎲 👆 💪 ▶️ 🆕 ⏬, ⚖️ 👆 🤚 🎡 🏃 ⚫️. 🤷 - -/// - -### 🔆 🔢 - -🥇 👜 👀, 👈 👥 ⚖ 🔁 🔢 ⏮️ `yield`. 👉 📶 🎏 🔗 ⏮️ `yield`. - -{* ../../docs_src/events/tutorial003.py hl[14:19] *} - -🥇 🍕 🔢, ⏭ `yield`, 🔜 🛠️ **⏭** 🈸 ▶️. - -& 🍕 ⏮️ `yield` 🔜 🛠️ **⏮️** 🈸 ✔️ 🏁. - -### 🔁 🔑 👨‍💼 - -🚥 👆 ✅, 🔢 🎀 ⏮️ `@asynccontextmanager`. - -👈 🗜 🔢 🔘 🕳 🤙 "**🔁 🔑 👨‍💼**". - -{* ../../docs_src/events/tutorial003.py hl[1,13] *} - -**🔑 👨‍💼** 🐍 🕳 👈 👆 💪 ⚙️ `with` 📄, 🖼, `open()` 💪 ⚙️ 🔑 👨‍💼: - -```Python -with open("file.txt") as file: - file.read() -``` - -⏮️ ⏬ 🐍, 📤 **🔁 🔑 👨‍💼**. 👆 🔜 ⚙️ ⚫️ ⏮️ `async with`: - -```Python -async with lifespan(app): - await do_stuff() -``` - -🕐❔ 👆 ✍ 🔑 👨‍💼 ⚖️ 🔁 🔑 👨‍💼 💖 🔛, ⚫️❔ ⚫️ 🔨 👈, ⏭ 🛬 `with` 🍫, ⚫️ 🔜 🛠️ 📟 ⏭ `yield`, & ⏮️ ❎ `with` 🍫, ⚫️ 🔜 🛠️ 📟 ⏮️ `yield`. - -👆 📟 🖼 🔛, 👥 🚫 ⚙️ ⚫️ 🔗, ✋️ 👥 🚶‍♀️ ⚫️ FastAPI ⚫️ ⚙️ ⚫️. - -`lifespan` 🔢 `FastAPI` 📱 ✊ **🔁 🔑 👨‍💼**, 👥 💪 🚶‍♀️ 👆 🆕 `lifespan` 🔁 🔑 👨‍💼 ⚫️. - -{* ../../docs_src/events/tutorial003.py hl[22] *} - -## 🎛 🎉 (😢) - -/// warning - -👍 🌌 🍵 *🕴* & *🤫* ⚙️ `lifespan` 🔢 `FastAPI` 📱 🔬 🔛. - -👆 💪 🎲 🚶 👉 🍕. - -/// - -📤 🎛 🌌 🔬 👉 ⚛ 🛠️ ⏮️ *🕴* & ⏮️ *🤫*. - -👆 💪 🔬 🎉 🐕‍🦺 (🔢) 👈 💪 🛠️ ⏭ 🈸 ▶️ 🆙, ⚖️ 🕐❔ 🈸 🤫 🔽. - -👫 🔢 💪 📣 ⏮️ `async def` ⚖️ 😐 `def`. - -### `startup` 🎉 - -🚮 🔢 👈 🔜 🏃 ⏭ 🈸 ▶️, 📣 ⚫️ ⏮️ 🎉 `"startup"`: - -{* ../../docs_src/events/tutorial001.py hl[8] *} - -👉 💼, `startup` 🎉 🐕‍🦺 🔢 🔜 🔢 🏬 "💽" ( `dict`) ⏮️ 💲. - -👆 💪 🚮 🌅 🌘 1️⃣ 🎉 🐕‍🦺 🔢. - -& 👆 🈸 🏆 🚫 ▶️ 📨 📨 ⏭ 🌐 `startup` 🎉 🐕‍🦺 ✔️ 🏁. - -### `shutdown` 🎉 - -🚮 🔢 👈 🔜 🏃 🕐❔ 🈸 🤫 🔽, 📣 ⚫️ ⏮️ 🎉 `"shutdown"`: - -{* ../../docs_src/events/tutorial002.py hl[6] *} - -📥, `shutdown` 🎉 🐕‍🦺 🔢 🔜 ✍ ✍ ⏸ `"Application shutdown"` 📁 `log.txt`. - -/// info - -`open()` 🔢, `mode="a"` ⛓ "🎻",, ⏸ 🔜 🚮 ⏮️ ⚫️❔ 🔛 👈 📁, 🍵 📁 ⏮️ 🎚. - -/// - -/// tip - -👀 👈 👉 💼 👥 ⚙️ 🐩 🐍 `open()` 🔢 👈 🔗 ⏮️ 📁. - -, ⚫️ 🔌 👤/🅾 (🔢/🔢), 👈 🚚 "⌛" 👜 ✍ 💾. - -✋️ `open()` 🚫 ⚙️ `async` & `await`. - -, 👥 📣 🎉 🐕‍🦺 🔢 ⏮️ 🐩 `def` ↩️ `async def`. - -/// - -/// info - -👆 💪 ✍ 🌅 🔃 👫 🎉 🐕‍🦺 💃 🎉' 🩺. - -/// - -### `startup` & `shutdown` 👯‍♂️ - -📤 ↕ 🤞 👈 ⚛ 👆 *🕴* & *🤫* 🔗, 👆 💪 💚 ▶️ 🕳 & ⤴️ 🏁 ⚫️, 📎 ℹ & ⤴️ 🚀 ⚫️, ♒️. - -🔨 👈 👽 🔢 👈 🚫 💰 ⚛ ⚖️ 🔢 👯‍♂️ 🌅 ⚠ 👆 🔜 💪 🏪 💲 🌐 🔢 ⚖️ 🎏 🎱. - -↩️ 👈, ⚫️ 🔜 👍 ↩️ ⚙️ `lifespan` 🔬 🔛. - -## 📡 ℹ - -📡 ℹ 😟 🤓. 👶 - -🔘, 🔫 📡 🔧, 👉 🍕 🔆 🛠️, & ⚫️ 🔬 🎉 🤙 `startup` & `shutdown`. - -## 🎧 🈸 - -👶 ✔️ 🤯 👈 👫 🔆 🎉 (🕴 & 🤫) 🔜 🕴 🛠️ 👑 🈸, 🚫 [🎧 🈸 - 🗻](sub-applications.md){.internal-link target=_blank}. diff --git a/docs/em/docs/advanced/generate-clients.md b/docs/em/docs/advanced/generate-clients.md deleted file mode 100644 index a680c9051e..0000000000 --- a/docs/em/docs/advanced/generate-clients.md +++ /dev/null @@ -1,238 +0,0 @@ -# 🏗 👩‍💻 - -**FastAPI** ⚓️ 🔛 🗄 🔧, 👆 🤚 🏧 🔗 ⏮️ 📚 🧰, 🔌 🏧 🛠️ 🩺 (🚚 🦁 🎚). - -1️⃣ 🎯 📈 👈 🚫 🎯 ⭐ 👈 👆 💪 **🏗 👩‍💻** (🕣 🤙 **📱** ) 👆 🛠️, 📚 🎏 **🛠️ 🇪🇸**. - -## 🗄 👩‍💻 🚂 - -📤 📚 🧰 🏗 👩‍💻 ⚪️➡️ **🗄**. - -⚠ 🧰 🗄 🚂. - -🚥 👆 🏗 **🕸**, 📶 😌 🎛 🗄-📕-🇦🇪. - -## 🏗 📕 🕸 👩‍💻 - -➡️ ▶️ ⏮️ 🙅 FastAPI 🈸: - -{* ../../docs_src/generate_clients/tutorial001.py hl[9:11,14:15,18,19,23] *} - -👀 👈 *➡ 🛠️* 🔬 🏷 👫 ⚙️ 📨 🚀 & 📨 🚀, ⚙️ 🏷 `Item` & `ResponseMessage`. - -### 🛠️ 🩺 - -🚥 👆 🚶 🛠️ 🩺, 👆 🔜 👀 👈 ⚫️ ✔️ **🔗** 📊 📨 📨 & 📨 📨: - - - -👆 💪 👀 👈 🔗 ↩️ 👫 📣 ⏮️ 🏷 📱. - -👈 ℹ 💪 📱 **🗄 🔗**, & ⤴️ 🎦 🛠️ 🩺 (🦁 🎚). - -& 👈 🎏 ℹ ⚪️➡️ 🏷 👈 🔌 🗄 ⚫️❔ 💪 ⚙️ **🏗 👩‍💻 📟**. - -### 🏗 📕 👩‍💻 - -🔜 👈 👥 ✔️ 📱 ⏮️ 🏷, 👥 💪 🏗 👩‍💻 📟 🕸. - -#### ❎ `openapi-ts` - -👆 💪 ❎ `openapi-ts` 👆 🕸 📟 ⏮️: - -
- -```console -$ npm install @hey-api/openapi-ts --save-dev - ----> 100% -``` - -
- -#### 🏗 👩‍💻 📟 - -🏗 👩‍💻 📟 👆 💪 ⚙️ 📋 ⏸ 🈸 `openapi-ts` 👈 🔜 🔜 ❎. - -↩️ ⚫️ ❎ 🇧🇿 🏗, 👆 🎲 🚫🔜 💪 🤙 👈 📋 🔗, ✋️ 👆 🔜 🚮 ⚫️ 🔛 👆 `package.json` 📁. - -⚫️ 💪 👀 💖 👉: - -```JSON hl_lines="7" -{ - "name": "frontend-app", - "version": "1.0.0", - "description": "", - "main": "index.js", - "scripts": { - "generate-client": "openapi-ts --input http://localhost:8000/openapi.json --output ./src/client --client axios" - }, - "author": "", - "license": "", - "devDependencies": { - "@hey-api/openapi-ts": "^0.27.38", - "typescript": "^4.6.2" - } -} -``` - -⏮️ ✔️ 👈 ☕ `generate-client` ✍ 📤, 👆 💪 🏃 ⚫️ ⏮️: - -
- -```console -$ npm run generate-client - -frontend-app@1.0.0 generate-client /home/user/code/frontend-app -> openapi-ts --input http://localhost:8000/openapi.json --output ./src/client --client axios -``` - -
- -👈 📋 🔜 🏗 📟 `./src/client` & 🔜 ⚙️ `axios` (🕸 🇺🇸🔍 🗃) 🔘. - -### 🔄 👅 👩‍💻 📟 - -🔜 👆 💪 🗄 & ⚙️ 👩‍💻 📟, ⚫️ 💪 👀 💖 👉, 👀 👈 👆 🤚 ✍ 👩‍🔬: - - - -👆 🔜 🤚 ✍ 🚀 📨: - - - -/// tip - -👀 ✍ `name` & `price`, 👈 🔬 FastAPI 🈸, `Item` 🏷. - -/// - -👆 🔜 ✔️ ⏸ ❌ 📊 👈 👆 📨: - - - -📨 🎚 🔜 ✔️ ✍: - - - -## FastAPI 📱 ⏮️ 🔖 - -📚 💼 👆 FastAPI 📱 🔜 🦏, & 👆 🔜 🎲 ⚙️ 🔖 🎏 🎏 👪 *➡ 🛠️*. - -🖼, 👆 💪 ✔️ 📄 **🏬** & ➕1️⃣ 📄 **👩‍💻**, & 👫 💪 👽 🔖: - - -{* ../../docs_src/generate_clients/tutorial002.py hl[23,28,36] *} - -### 🏗 📕 👩‍💻 ⏮️ 🔖 - -🚥 👆 🏗 👩‍💻 FastAPI 📱 ⚙️ 🔖, ⚫️ 🔜 🛎 🎏 👩‍💻 📟 ⚓️ 🔛 🔖. - -👉 🌌 👆 🔜 💪 ✔️ 👜 ✔ & 👪 ☑ 👩‍💻 📟: - - - -👉 💼 👆 ✔️: - -* `ItemsService` -* `UsersService` - -### 👩‍💻 👩‍🔬 📛 - -▶️️ 🔜 🏗 👩‍🔬 📛 💖 `createItemItemsPost` 🚫 👀 📶 🧹: - -```TypeScript -ItemsService.createItemItemsPost({name: "Plumbus", price: 5}) -``` - -...👈 ↩️ 👩‍💻 🚂 ⚙️ 🗄 🔗 **🛠️ 🆔** 🔠 *➡ 🛠️*. - -🗄 🚚 👈 🔠 🛠️ 🆔 😍 🤭 🌐 *➡ 🛠️*, FastAPI ⚙️ **🔢 📛**, **➡**, & **🇺🇸🔍 👩‍🔬/🛠️** 🏗 👈 🛠️ 🆔, ↩️ 👈 🌌 ⚫️ 💪 ⚒ 💭 👈 🛠️ 🆔 😍. - -✋️ 👤 🔜 🎦 👆 ❔ 📉 👈 ⏭. 👶 - -## 🛃 🛠️ 🆔 & 👍 👩‍🔬 📛 - -👆 💪 **🔀** 🌌 👫 🛠️ 🆔 **🏗** ⚒ 👫 🙅 & ✔️ **🙅 👩‍🔬 📛** 👩‍💻. - -👉 💼 👆 🔜 ✔️ 🚚 👈 🔠 🛠️ 🆔 **😍** 🎏 🌌. - -🖼, 👆 💪 ⚒ 💭 👈 🔠 *➡ 🛠️* ✔️ 🔖, & ⤴️ 🏗 🛠️ 🆔 ⚓️ 🔛 **🔖** & *➡ 🛠️* **📛** (🔢 📛). - -### 🛃 🏗 😍 🆔 🔢 - -FastAPI ⚙️ **😍 🆔** 🔠 *➡ 🛠️*, ⚫️ ⚙️ **🛠️ 🆔** & 📛 🙆 💪 🛃 🏷, 📨 ⚖️ 📨. - -👆 💪 🛃 👈 🔢. ⚫️ ✊ `APIRoute` & 🔢 🎻. - -🖼, 📥 ⚫️ ⚙️ 🥇 🔖 (👆 🔜 🎲 ✔️ 🕴 1️⃣ 🔖) & *➡ 🛠️* 📛 (🔢 📛). - -👆 💪 ⤴️ 🚶‍♀️ 👈 🛃 🔢 **FastAPI** `generate_unique_id_function` 🔢: - -{* ../../docs_src/generate_clients/tutorial003.py hl[8:9,12] *} - -### 🏗 📕 👩‍💻 ⏮️ 🛃 🛠️ 🆔 - -🔜 🚥 👆 🏗 👩‍💻 🔄, 👆 🔜 👀 👈 ⚫️ ✔️ 📉 👩‍🔬 📛: - - - -👆 👀, 👩‍🔬 📛 🔜 ✔️ 🔖 & ⤴️ 🔢 📛, 🔜 👫 🚫 🔌 ℹ ⚪️➡️ 📛 ➡ & 🇺🇸🔍 🛠️. - -### 🗜 🗄 🔧 👩‍💻 🚂 - -🏗 📟 ✔️ **❎ ℹ**. - -👥 ⏪ 💭 👈 👉 👩‍🔬 🔗 **🏬** ↩️ 👈 🔤 `ItemsService` (✊ ⚪️➡️ 🔖), ✋️ 👥 ✔️ 📛 🔡 👩‍🔬 📛 💁‍♂️. 👶 - -👥 🔜 🎲 💚 🚧 ⚫️ 🗄 🏢, 👈 🔜 🚚 👈 🛠️ 🆔 **😍**. - -✋️ 🏗 👩‍💻 👥 💪 **🔀** 🗄 🛠️ 🆔 ▶️️ ⏭ 🏭 👩‍💻, ⚒ 👈 👩‍🔬 📛 👌 & **🧹**. - -👥 💪 ⏬ 🗄 🎻 📁 `openapi.json` & ⤴️ 👥 💪 **❎ 👈 🔡 🔖** ⏮️ ✍ 💖 👉: - -{* ../../docs_src/generate_clients/tutorial004.py *} - -⏮️ 👈, 🛠️ 🆔 🔜 📁 ⚪️➡️ 👜 💖 `items-get_items` `get_items`, 👈 🌌 👩‍💻 🚂 💪 🏗 🙅 👩‍🔬 📛. - -### 🏗 📕 👩‍💻 ⏮️ 🗜 🗄 - -🔜 🔚 🏁 📁 `openapi.json`, 👆 🔜 🔀 `package.json` ⚙️ 👈 🇧🇿 📁, 🖼: - -```JSON hl_lines="7" -{ - "name": "frontend-app", - "version": "1.0.0", - "description": "", - "main": "index.js", - "scripts": { - "generate-client": "openapi-ts --input ./openapi.json --output ./src/client --client axios" - }, - "author": "", - "license": "", - "devDependencies": { - "@hey-api/openapi-ts": "^0.27.38", - "typescript": "^4.6.2" - } -} -``` - -⏮️ 🏭 🆕 👩‍💻, 👆 🔜 🔜 ✔️ **🧹 👩‍🔬 📛**, ⏮️ 🌐 **✍**, **⏸ ❌**, ♒️: - - - -## 💰 - -🕐❔ ⚙️ 🔁 🏗 👩‍💻 👆 🔜 **✍** : - -* 👩‍🔬. -* 📨 🚀 💪, 🔢 🔢, ♒️. -* 📨 🚀. - -👆 🔜 ✔️ **⏸ ❌** 🌐. - -& 🕐❔ 👆 ℹ 👩‍💻 📟, & **♻** 🕸, ⚫️ 🔜 ✔️ 🙆 🆕 *➡ 🛠️* 💪 👩‍🔬, 🗝 🕐 ❎, & 🙆 🎏 🔀 🔜 🎨 🔛 🏗 📟. 👶 - -👉 ⛓ 👈 🚥 🕳 🔀 ⚫️ 🔜 **🎨** 🔛 👩‍💻 📟 🔁. & 🚥 👆 **🏗** 👩‍💻 ⚫️ 🔜 ❌ 👅 🚥 👆 ✔️ 🙆 **🔖** 📊 ⚙️. - -, 👆 🔜 **🔍 📚 ❌** 📶 ⏪ 🛠️ 🛵 ↩️ ✔️ ⌛ ❌ 🎦 🆙 👆 🏁 👩‍💻 🏭 & ⤴️ 🔄 ℹ 🌐❔ ⚠. 👶 diff --git a/docs/em/docs/advanced/index.md b/docs/em/docs/advanced/index.md deleted file mode 100644 index 48ef8e46d0..0000000000 --- a/docs/em/docs/advanced/index.md +++ /dev/null @@ -1,27 +0,0 @@ -# 🏧 👩‍💻 🦮 - -## 🌖 ⚒ - -👑 [🔰 - 👩‍💻 🦮](../tutorial/index.md){.internal-link target=_blank} 🔜 🥃 🤝 👆 🎫 🔘 🌐 👑 ⚒ **FastAPI**. - -⏭ 📄 👆 🔜 👀 🎏 🎛, 📳, & 🌖 ⚒. - -/// tip - -⏭ 📄 **🚫 🎯 "🏧"**. - - & ⚫️ 💪 👈 👆 ⚙️ 💼, ⚗ 1️⃣ 👫. - -/// - -## ✍ 🔰 🥇 - -👆 💪 ⚙️ 🏆 ⚒ **FastAPI** ⏮️ 💡 ⚪️➡️ 👑 [🔰 - 👩‍💻 🦮](../tutorial/index.md){.internal-link target=_blank}. - -& ⏭ 📄 🤔 👆 ⏪ ✍ ⚫️, & 🤔 👈 👆 💭 👈 👑 💭. - -## 🏎.🅾 ↗️ - -🚥 👆 🔜 💖 ✊ 🏧-🔰 ↗️ 🔗 👉 📄 🩺, 👆 💪 💚 ✅: 💯-💾 🛠️ ⏮️ FastAPI & ☁ **🏎.🅾**. - -👫 ⏳ 🩸 1️⃣0️⃣ 💯 🌐 💰 🛠️ **FastAPI**. 👶 👶 diff --git a/docs/em/docs/advanced/middleware.md b/docs/em/docs/advanced/middleware.md deleted file mode 100644 index 22d707062f..0000000000 --- a/docs/em/docs/advanced/middleware.md +++ /dev/null @@ -1,95 +0,0 @@ -# 🏧 🛠️ - -👑 🔰 👆 ✍ ❔ 🚮 [🛃 🛠️](../tutorial/middleware.md){.internal-link target=_blank} 👆 🈸. - -& ⤴️ 👆 ✍ ❔ 🍵 [⚜ ⏮️ `CORSMiddleware`](../tutorial/cors.md){.internal-link target=_blank}. - -👉 📄 👥 🔜 👀 ❔ ⚙️ 🎏 🛠️. - -## ❎ 🔫 🛠️ - -**FastAPI** ⚓️ 🔛 💃 & 🛠️ 🔫 🔧, 👆 💪 ⚙️ 🙆 🔫 🛠️. - -🛠️ 🚫 ✔️ ⚒ FastAPI ⚖️ 💃 👷, 📏 ⚫️ ⏩ 🔫 🔌. - -🏢, 🔫 🛠️ 🎓 👈 ⌛ 📨 🔫 📱 🥇 ❌. - -, 🧾 🥉-🥳 🔫 🛠️ 👫 🔜 🎲 💬 👆 🕳 💖: - -```Python -from unicorn import UnicornMiddleware - -app = SomeASGIApp() - -new_app = UnicornMiddleware(app, some_config="rainbow") -``` - -✋️ FastAPI (🤙 💃) 🚚 🙅 🌌 ⚫️ 👈 ⚒ 💭 👈 🔗 🛠️ 🍵 💽 ❌ & 🛃 ⚠ 🐕‍🦺 👷 ☑. - -👈, 👆 ⚙️ `app.add_middleware()` (🖼 ⚜). - -```Python -from fastapi import FastAPI -from unicorn import UnicornMiddleware - -app = FastAPI() - -app.add_middleware(UnicornMiddleware, some_config="rainbow") -``` - -`app.add_middleware()` 📨 🛠️ 🎓 🥇 ❌ & 🙆 🌖 ❌ 🚶‍♀️ 🛠️. - -## 🛠️ 🛠️ - -**FastAPI** 🔌 📚 🛠️ ⚠ ⚙️ 💼, 👥 🔜 👀 ⏭ ❔ ⚙️ 👫. - -/// note | 📡 ℹ - -⏭ 🖼, 👆 💪 ⚙️ `from starlette.middleware.something import SomethingMiddleware`. - -**FastAPI** 🚚 📚 🛠️ `fastapi.middleware` 🏪 👆, 👩‍💻. ✋️ 🌅 💪 🛠️ 👟 🔗 ⚪️➡️ 💃. - -/// - -## `HTTPSRedirectMiddleware` - -🛠️ 👈 🌐 📨 📨 🔜 👯‍♂️ `https` ⚖️ `wss`. - -🙆 📨 📨 `http` ⚖️ `ws` 🔜 ❎ 🔐 ⚖ ↩️. - -{* ../../docs_src/advanced_middleware/tutorial001.py hl[2,6] *} - -## `TrustedHostMiddleware` - -🛠️ 👈 🌐 📨 📨 ✔️ ☑ ⚒ `Host` 🎚, ✔ 💂‍♂ 🛡 🇺🇸🔍 🦠 🎚 👊. - -{* ../../docs_src/advanced_middleware/tutorial002.py hl[2,6:8] *} - -📄 ❌ 🐕‍🦺: - -* `allowed_hosts` - 📇 🆔 📛 👈 🔜 ✔ 📛. 🃏 🆔 ✅ `*.example.com` 🐕‍🦺 🎀 📁. ✔ 🙆 📛 👯‍♂️ ⚙️ `allowed_hosts=["*"]` ⚖️ 🚫 🛠️. - -🚥 📨 📨 🔨 🚫 ✔ ☑ ⤴️ `400` 📨 🔜 📨. - -## `GZipMiddleware` - -🍵 🗜 📨 🙆 📨 👈 🔌 `"gzip"` `Accept-Encoding` 🎚. - -🛠️ 🔜 🍵 👯‍♂️ 🐩 & 🎥 📨. - -{* ../../docs_src/advanced_middleware/tutorial003.py hl[2,6] *} - -📄 ❌ 🐕‍🦺: - -* `minimum_size` - 🚫 🗜 📨 👈 🤪 🌘 👉 💯 📐 🔢. 🔢 `500`. - -## 🎏 🛠️ - -📤 📚 🎏 🔫 🛠️. - -🖼: - -* Uvicorn `ProxyHeadersMiddleware` -* 🇸🇲 - -👀 🎏 💪 🛠️ ✅ 💃 🛠️ 🩺 & 🔫 👌 📇. diff --git a/docs/em/docs/advanced/openapi-callbacks.md b/docs/em/docs/advanced/openapi-callbacks.md deleted file mode 100644 index b0a8216681..0000000000 --- a/docs/em/docs/advanced/openapi-callbacks.md +++ /dev/null @@ -1,186 +0,0 @@ -# 🗄 ⏲ - -👆 💪 ✍ 🛠️ ⏮️ *➡ 🛠️* 👈 💪 ⏲ 📨 *🔢 🛠️* ✍ 👱 🙆 (🎲 🎏 👩‍💻 👈 🔜 *⚙️* 👆 🛠️). - -🛠️ 👈 🔨 🕐❔ 👆 🛠️ 📱 🤙 *🔢 🛠️* 📛 "⏲". ↩️ 🖥 👈 🔢 👩‍💻 ✍ 📨 📨 👆 🛠️ & ⤴️ 👆 🛠️ *🤙 🔙*, 📨 📨 *🔢 🛠️* (👈 🎲 ✍ 🎏 👩‍💻). - -👉 💼, 👆 💪 💚 📄 ❔ 👈 🔢 🛠️ *🔜* 👀 💖. ⚫️❔ *➡ 🛠️* ⚫️ 🔜 ✔️, ⚫️❔ 💪 ⚫️ 🔜 ⌛, ⚫️❔ 📨 ⚫️ 🔜 📨, ♒️. - -## 📱 ⏮️ ⏲ - -➡️ 👀 🌐 👉 ⏮️ 🖼. - -🌈 👆 🛠️ 📱 👈 ✔ 🏗 🧾. - -👉 🧾 🔜 ✔️ `id`, `title` (📦), `customer`, & `total`. - -👩‍💻 👆 🛠️ (🔢 👩‍💻) 🔜 ✍ 🧾 👆 🛠️ ⏮️ 🏤 📨. - -⤴️ 👆 🛠️ 🔜 (➡️ 🌈): - -* 📨 🧾 🕴 🔢 👩‍💻. -* 📈 💸. -* 📨 📨 🔙 🛠️ 👩‍💻 (🔢 👩‍💻). - * 👉 🔜 🔨 📨 🏤 📨 (⚪️➡️ *👆 🛠️*) *🔢 🛠️* 🚚 👈 🔢 👩‍💻 (👉 "⏲"). - -## 😐 **FastAPI** 📱 - -➡️ 🥇 👀 ❔ 😐 🛠️ 📱 🔜 👀 💖 ⏭ ❎ ⏲. - -⚫️ 🔜 ✔️ *➡ 🛠️* 👈 🔜 📨 `Invoice` 💪, & 🔢 🔢 `callback_url` 👈 🔜 🔌 📛 ⏲. - -👉 🍕 📶 😐, 🌅 📟 🎲 ⏪ 😰 👆: - -{* ../../docs_src/openapi_callbacks/tutorial001.py hl[9:13,36:53] *} - -/// tip - -`callback_url` 🔢 🔢 ⚙️ Pydantic 📛 🆎. - -/// - -🕴 🆕 👜 `callbacks=messages_callback_router.routes` ❌ *➡ 🛠️ 👨‍🎨*. 👥 🔜 👀 ⚫️❔ 👈 ⏭. - -## 🔬 ⏲ - -☑ ⏲ 📟 🔜 🪀 🙇 🔛 👆 👍 🛠️ 📱. - -& ⚫️ 🔜 🎲 🪀 📚 ⚪️➡️ 1️⃣ 📱 ⏭. - -⚫️ 💪 1️⃣ ⚖️ 2️⃣ ⏸ 📟, 💖: - -```Python -callback_url = "https://example.com/api/v1/invoices/events/" -httpx.post(callback_url, json={"description": "Invoice paid", "paid": True}) -``` - -✋️ 🎲 🏆 ⚠ 🍕 ⏲ ⚒ 💭 👈 👆 🛠️ 👩‍💻 (🔢 👩‍💻) 🛠️ *🔢 🛠️* ☑, 🛄 💽 👈 *👆 🛠️* 🔜 📨 📨 💪 ⏲, ♒️. - -, ⚫️❔ 👥 🔜 ⏭ 🚮 📟 📄 ❔ 👈 *🔢 🛠️* 🔜 👀 💖 📨 ⏲ ⚪️➡️ *👆 🛠️*. - -👈 🧾 🔜 🎦 🆙 🦁 🎚 `/docs` 👆 🛠️, & ⚫️ 🔜 ➡️ 🔢 👩‍💻 💭 ❔ 🏗 *🔢 🛠️*. - -👉 🖼 🚫 🛠️ ⏲ ⚫️ (👈 💪 ⏸ 📟), 🕴 🧾 🍕. - -/// tip - -☑ ⏲ 🇺🇸🔍 📨. - -🕐❔ 🛠️ ⏲ 👆, 👆 💪 ⚙️ 🕳 💖 🇸🇲 ⚖️ 📨. - -/// - -## ✍ ⏲ 🧾 📟 - -👉 📟 🏆 🚫 🛠️ 👆 📱, 👥 🕴 💪 ⚫️ *📄* ❔ 👈 *🔢 🛠️* 🔜 👀 💖. - -✋️, 👆 ⏪ 💭 ❔ 💪 ✍ 🏧 🧾 🛠️ ⏮️ **FastAPI**. - -👥 🔜 ⚙️ 👈 🎏 💡 📄 ❔ *🔢 🛠️* 🔜 👀 💖... 🏗 *➡ 🛠️(Ⓜ)* 👈 🔢 🛠️ 🔜 🛠️ (🕐 👆 🛠️ 🔜 🤙). - -/// tip - -🕐❔ ✍ 📟 📄 ⏲, ⚫️ 💪 ⚠ 🌈 👈 👆 👈 *🔢 👩‍💻*. & 👈 👆 ⏳ 🛠️ *🔢 🛠️*, 🚫 *👆 🛠️*. - -🍕 🛠️ 👉 ☝ 🎑 ( *🔢 👩‍💻*) 💪 ℹ 👆 💭 💖 ⚫️ 🌅 ⭐ 🌐❔ 🚮 🔢, Pydantic 🏷 💪, 📨, ♒️. 👈 *🔢 🛠️*. - -/// - -### ✍ ⏲ `APIRouter` - -🥇 ✍ 🆕 `APIRouter` 👈 🔜 🔌 1️⃣ ⚖️ 🌅 ⏲. - -{* ../../docs_src/openapi_callbacks/tutorial001.py hl[3,25] *} - -### ✍ ⏲ *➡ 🛠️* - -✍ ⏲ *➡ 🛠️* ⚙️ 🎏 `APIRouter` 👆 ✍ 🔛. - -⚫️ 🔜 👀 💖 😐 FastAPI *➡ 🛠️*: - -* ⚫️ 🔜 🎲 ✔️ 📄 💪 ⚫️ 🔜 📨, ✅ `body: InvoiceEvent`. -* & ⚫️ 💪 ✔️ 📄 📨 ⚫️ 🔜 📨, ✅ `response_model=InvoiceEventReceived`. - -{* ../../docs_src/openapi_callbacks/tutorial001.py hl[16:18,21:22,28:32] *} - -📤 2️⃣ 👑 🔺 ⚪️➡️ 😐 *➡ 🛠️*: - -* ⚫️ 🚫 💪 ✔️ 🙆 ☑ 📟, ↩️ 👆 📱 🔜 🙅 🤙 👉 📟. ⚫️ 🕴 ⚙️ 📄 *🔢 🛠️*. , 🔢 💪 ✔️ `pass`. -* *➡* 💪 🔌 🗄 3️⃣ 🧬 (👀 🌖 🔛) 🌐❔ ⚫️ 💪 ⚙️ 🔢 ⏮️ 🔢 & 🍕 ⏮️ 📨 📨 *👆 🛠️*. - -### ⏲ ➡ 🧬 - -⏲ *➡* 💪 ✔️ 🗄 3️⃣ 🧬 👈 💪 🔌 🍕 ⏮️ 📨 📨 *👆 🛠️*. - -👉 💼, ⚫️ `str`: - -```Python -"{$callback_url}/invoices/{$request.body.id}" -``` - -, 🚥 👆 🛠️ 👩‍💻 (🔢 👩‍💻) 📨 📨 *👆 🛠️* : - -``` -https://yourapi.com/invoices/?callback_url=https://www.external.org/events -``` - -⏮️ 🎻 💪: - -```JSON -{ - "id": "2expen51ve", - "customer": "Mr. Richie Rich", - "total": "9999" -} -``` - -⤴️ *👆 🛠️* 🔜 🛠️ 🧾, & ☝ ⏪, 📨 ⏲ 📨 `callback_url` ( *🔢 🛠️*): - -``` -https://www.external.org/events/invoices/2expen51ve -``` - -⏮️ 🎻 💪 ⚗ 🕳 💖: - -```JSON -{ - "description": "Payment celebration", - "paid": true -} -``` - -& ⚫️ 🔜 ⌛ 📨 ⚪️➡️ 👈 *🔢 🛠️* ⏮️ 🎻 💪 💖: - -```JSON -{ - "ok": true -} -``` - -/// tip - -👀 ❔ ⏲ 📛 ⚙️ 🔌 📛 📨 🔢 🔢 `callback_url` (`https://www.external.org/events`) & 🧾 `id` ⚪️➡️ 🔘 🎻 💪 (`2expen51ve`). - -/// - -### 🚮 ⏲ 📻 - -👉 ☝ 👆 ✔️ *⏲ ➡ 🛠️(Ⓜ)* 💪 (1️⃣(Ⓜ) 👈 *🔢 👩‍💻* 🔜 🛠️ *🔢 🛠️*) ⏲ 📻 👆 ✍ 🔛. - -🔜 ⚙️ 🔢 `callbacks` *👆 🛠️ ➡ 🛠️ 👨‍🎨* 🚶‍♀️ 🔢 `.routes` (👈 🤙 `list` 🛣/*➡ 🛠️*) ⚪️➡️ 👈 ⏲ 📻: - -{* ../../docs_src/openapi_callbacks/tutorial001.py hl[35] *} - -/// tip - -👀 👈 👆 🚫 🚶‍♀️ 📻 ⚫️ (`invoices_callback_router`) `callback=`, ✋️ 🔢 `.routes`, `invoices_callback_router.routes`. - -/// - -### ✅ 🩺 - -🔜 👆 💪 ▶️ 👆 📱 ⏮️ Uvicorn & 🚶 http://127.0.0.1:8000/docs. - -👆 🔜 👀 👆 🩺 ✅ "⏲" 📄 👆 *➡ 🛠️* 👈 🎦 ❔ *🔢 🛠️* 🔜 👀 💖: - - diff --git a/docs/em/docs/advanced/path-operation-advanced-configuration.md b/docs/em/docs/advanced/path-operation-advanced-configuration.md deleted file mode 100644 index 9d9d5fa8da..0000000000 --- a/docs/em/docs/advanced/path-operation-advanced-configuration.md +++ /dev/null @@ -1,172 +0,0 @@ -# ➡ 🛠️ 🏧 📳 - -## 🗄 { - -/// warning - -🚥 👆 🚫 "🕴" 🗄, 👆 🎲 🚫 💪 👉. - -/// - -👆 💪 ⚒ 🗄 `operationId` ⚙️ 👆 *➡ 🛠️* ⏮️ 🔢 `operation_id`. - -👆 🔜 ✔️ ⚒ 💭 👈 ⚫️ 😍 🔠 🛠️. - -{* ../../docs_src/path_operation_advanced_configuration/tutorial001.py hl[6] *} - -### ⚙️ *➡ 🛠️ 🔢* 📛 { - -🚥 👆 💚 ⚙️ 👆 🔗' 🔢 📛 `operationId`Ⓜ, 👆 💪 🔁 🤭 🌐 👫 & 🔐 🔠 *➡ 🛠️* `operation_id` ⚙️ 👫 `APIRoute.name`. - -👆 🔜 ⚫️ ⏮️ ❎ 🌐 👆 *➡ 🛠️*. - -{* ../../docs_src/path_operation_advanced_configuration/tutorial002.py hl[2,12:21,24] *} - -/// tip - -🚥 👆 ❎ 🤙 `app.openapi()`, 👆 🔜 ℹ `operationId`Ⓜ ⏭ 👈. - -/// - -/// warning - -🚥 👆 👉, 👆 ✔️ ⚒ 💭 🔠 1️⃣ 👆 *➡ 🛠️ 🔢* ✔️ 😍 📛. - -🚥 👫 🎏 🕹 (🐍 📁). - -/// - -## 🚫 ⚪️➡️ 🗄 - -🚫 *➡ 🛠️* ⚪️➡️ 🏗 🗄 🔗 (& ➡️, ⚪️➡️ 🏧 🧾 ⚙️), ⚙️ 🔢 `include_in_schema` & ⚒ ⚫️ `False`: - -{* ../../docs_src/path_operation_advanced_configuration/tutorial003.py hl[6] *} - -## 🏧 📛 ⚪️➡️ #️⃣ - -👆 💪 📉 ⏸ ⚙️ ⚪️➡️ #️⃣ *➡ 🛠️ 🔢* 🗄. - -❎ `\f` (😖 "📨 🍼" 🦹) 🤕 **FastAPI** 🔁 🔢 ⚙️ 🗄 👉 ☝. - -⚫️ 🏆 🚫 🎦 🆙 🧾, ✋️ 🎏 🧰 (✅ 🐉) 🔜 💪 ⚙️ 🎂. - -{* ../../docs_src/path_operation_advanced_configuration/tutorial004.py hl[19:29] *} - -## 🌖 📨 - -👆 🎲 ✔️ 👀 ❔ 📣 `response_model` & `status_code` *➡ 🛠️*. - -👈 🔬 🗃 🔃 👑 📨 *➡ 🛠️*. - -👆 💪 📣 🌖 📨 ⏮️ 👫 🏷, 👔 📟, ♒️. - -📤 🎂 📃 📥 🧾 🔃 ⚫️, 👆 💪 ✍ ⚫️ [🌖 📨 🗄](additional-responses.md){.internal-link target=_blank}. - -## 🗄 ➕ - -🕐❔ 👆 📣 *➡ 🛠️* 👆 🈸, **FastAPI** 🔁 🏗 🔗 🗃 🔃 👈 *➡ 🛠️* 🔌 🗄 🔗. - -/// note | 📡 ℹ - -🗄 🔧 ⚫️ 🤙 🛠️ 🎚. - -/// - -⚫️ ✔️ 🌐 ℹ 🔃 *➡ 🛠️* & ⚙️ 🏗 🏧 🧾. - -⚫️ 🔌 `tags`, `parameters`, `requestBody`, `responses`, ♒️. - -👉 *➡ 🛠️*-🎯 🗄 🔗 🛎 🏗 🔁 **FastAPI**, ✋️ 👆 💪 ↔ ⚫️. - -/// tip - -👉 🔅 🎚 ↔ ☝. - -🚥 👆 🕴 💪 📣 🌖 📨, 🌅 🏪 🌌 ⚫️ ⏮️ [🌖 📨 🗄](additional-responses.md){.internal-link target=_blank}. - -/// - -👆 💪 ↔ 🗄 🔗 *➡ 🛠️* ⚙️ 🔢 `openapi_extra`. - -### 🗄 ↔ - -👉 `openapi_extra` 💪 👍, 🖼, 📣 [🗄 ↔](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#specificationExtensions): - -{* ../../docs_src/path_operation_advanced_configuration/tutorial005.py hl[6] *} - -🚥 👆 📂 🏧 🛠️ 🩺, 👆 ↔ 🔜 🎦 🆙 🔝 🎯 *➡ 🛠️*. - - - -& 🚥 👆 👀 📉 🗄 ( `/openapi.json` 👆 🛠️), 👆 🔜 👀 👆 ↔ 🍕 🎯 *➡ 🛠️* 💁‍♂️: - -```JSON hl_lines="22" -{ - "openapi": "3.0.2", - "info": { - "title": "FastAPI", - "version": "0.1.0" - }, - "paths": { - "/items/": { - "get": { - "summary": "Read Items", - "operationId": "read_items_items__get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {} - } - } - } - }, - "x-aperture-labs-portal": "blue" - } - } - } -} -``` - -### 🛃 🗄 *➡ 🛠️* 🔗 - -📖 `openapi_extra` 🔜 🙇 🔗 ⏮️ 🔁 🏗 🗄 🔗 *➡ 🛠️*. - -, 👆 💪 🚮 🌖 💽 🔁 🏗 🔗. - -🖼, 👆 💪 💭 ✍ & ✔ 📨 ⏮️ 👆 👍 📟, 🍵 ⚙️ 🏧 ⚒ FastAPI ⏮️ Pydantic, ✋️ 👆 💪 💚 🔬 📨 🗄 🔗. - -👆 💪 👈 ⏮️ `openapi_extra`: - -{* ../../docs_src/path_operation_advanced_configuration/tutorial006.py hl[20:37,39:40] *} - -👉 🖼, 👥 🚫 📣 🙆 Pydantic 🏷. 👐, 📨 💪 🚫 🎻 🎻, ⚫️ ✍ 🔗 `bytes`, & 🔢 `magic_data_reader()` 🔜 🈚 🎻 ⚫️ 🌌. - -👐, 👥 💪 📣 📈 🔗 📨 💪. - -### 🛃 🗄 🎚 🆎 - -⚙️ 👉 🎏 🎱, 👆 💪 ⚙️ Pydantic 🏷 🔬 🎻 🔗 👈 ⤴️ 🔌 🛃 🗄 🔗 📄 *➡ 🛠️*. - -& 👆 💪 👉 🚥 💽 🆎 📨 🚫 🎻. - -🖼, 👉 🈸 👥 🚫 ⚙️ FastAPI 🛠️ 🛠️ ⚗ 🎻 🔗 ⚪️➡️ Pydantic 🏷 🚫 🏧 🔬 🎻. 👐, 👥 📣 📨 🎚 🆎 📁, 🚫 🎻: - -{* ../../docs_src/path_operation_advanced_configuration/tutorial007.py hl[17:22,24] *} - -👐, 👐 👥 🚫 ⚙️ 🔢 🛠️ 🛠️, 👥 ⚙️ Pydantic 🏷 ❎ 🏗 🎻 🔗 💽 👈 👥 💚 📨 📁. - -⤴️ 👥 ⚙️ 📨 🔗, & ⚗ 💪 `bytes`. 👉 ⛓ 👈 FastAPI 🏆 🚫 🔄 🎻 📨 🚀 🎻. - -& ⤴️ 👆 📟, 👥 🎻 👈 📁 🎚 🔗, & ⤴️ 👥 🔄 ⚙️ 🎏 Pydantic 🏷 ✔ 📁 🎚: - -{* ../../docs_src/path_operation_advanced_configuration/tutorial007.py hl[26:33] *} - -/// tip - -📥 👥 🏤-⚙️ 🎏 Pydantic 🏷. - -✋️ 🎏 🌌, 👥 💪 ✔️ ✔ ⚫️ 🎏 🌌. - -/// diff --git a/docs/em/docs/advanced/response-change-status-code.md b/docs/em/docs/advanced/response-change-status-code.md deleted file mode 100644 index 4933484dd3..0000000000 --- a/docs/em/docs/advanced/response-change-status-code.md +++ /dev/null @@ -1,31 +0,0 @@ -# 📨 - 🔀 👔 📟 - -👆 🎲 ✍ ⏭ 👈 👆 💪 ⚒ 🔢 [📨 👔 📟](../tutorial/response-status-code.md){.internal-link target=_blank}. - -✋️ 💼 👆 💪 📨 🎏 👔 📟 🌘 🔢. - -## ⚙️ 💼 - -🖼, 🌈 👈 👆 💚 📨 🇺🇸🔍 👔 📟 "👌" `200` 🔢. - -✋️ 🚥 💽 🚫 🔀, 👆 💚 ✍ ⚫️, & 📨 🇺🇸🔍 👔 📟 "✍" `201`. - -✋️ 👆 💚 💪 ⛽ & 🗜 💽 👆 📨 ⏮️ `response_model`. - -📚 💼, 👆 💪 ⚙️ `Response` 🔢. - -## ⚙️ `Response` 🔢 - -👆 💪 📣 🔢 🆎 `Response` 👆 *➡ 🛠️ 🔢* (👆 💪 🍪 & 🎚). - -& ⤴️ 👆 💪 ⚒ `status_code` 👈 *🔀* 📨 🎚. - -{* ../../docs_src/response_change_status_code/tutorial001.py hl[1,9,12] *} - -& ⤴️ 👆 💪 📨 🙆 🎚 👆 💪, 👆 🛎 🔜 ( `dict`, 💽 🏷, ♒️). - -& 🚥 👆 📣 `response_model`, ⚫️ 🔜 ⚙️ ⛽ & 🗜 🎚 👆 📨. - -**FastAPI** 🔜 ⚙️ 👈 *🔀* 📨 ⚗ 👔 📟 (🍪 & 🎚), & 🔜 🚮 👫 🏁 📨 👈 🔌 💲 👆 📨, ⛽ 🙆 `response_model`. - -👆 💪 📣 `Response` 🔢 🔗, & ⚒ 👔 📟 👫. ✋️ ✔️ 🤯 👈 🏁 1️⃣ ⚒ 🔜 🏆. diff --git a/docs/em/docs/advanced/response-cookies.md b/docs/em/docs/advanced/response-cookies.md deleted file mode 100644 index a6e37ad74d..0000000000 --- a/docs/em/docs/advanced/response-cookies.md +++ /dev/null @@ -1,51 +0,0 @@ -# 📨 🍪 - -## ⚙️ `Response` 🔢 - -👆 💪 📣 🔢 🆎 `Response` 👆 *➡ 🛠️ 🔢*. - -& ⤴️ 👆 💪 ⚒ 🍪 👈 *🔀* 📨 🎚. - -{* ../../docs_src/response_cookies/tutorial002.py hl[1,8:9] *} - -& ⤴️ 👆 💪 📨 🙆 🎚 👆 💪, 👆 🛎 🔜 ( `dict`, 💽 🏷, ♒️). - -& 🚥 👆 📣 `response_model`, ⚫️ 🔜 ⚙️ ⛽ & 🗜 🎚 👆 📨. - -**FastAPI** 🔜 ⚙️ 👈 *🔀* 📨 ⚗ 🍪 (🎚 & 👔 📟), & 🔜 🚮 👫 🏁 📨 👈 🔌 💲 👆 📨, ⛽ 🙆 `response_model`. - -👆 💪 📣 `Response` 🔢 🔗, & ⚒ 🍪 (& 🎚) 👫. - -## 📨 `Response` 🔗 - -👆 💪 ✍ 🍪 🕐❔ 🛬 `Response` 🔗 👆 📟. - -👈, 👆 💪 ✍ 📨 🔬 [📨 📨 🔗](response-directly.md){.internal-link target=_blank}. - -⤴️ ⚒ 🍪 ⚫️, & ⤴️ 📨 ⚫️: - -{* ../../docs_src/response_cookies/tutorial001.py hl[10:12] *} - -/// tip - -✔️ 🤯 👈 🚥 👆 📨 📨 🔗 ↩️ ⚙️ `Response` 🔢, FastAPI 🔜 📨 ⚫️ 🔗. - -, 👆 🔜 ✔️ ⚒ 💭 👆 💽 ☑ 🆎. 🤶 Ⓜ. ⚫️ 🔗 ⏮️ 🎻, 🚥 👆 🛬 `JSONResponse`. - - & 👈 👆 🚫 📨 🙆 📊 👈 🔜 ✔️ ⛽ `response_model`. - -/// - -### 🌅 ℹ - -/// note | 📡 ℹ - -👆 💪 ⚙️ `from starlette.responses import Response` ⚖️ `from starlette.responses import JSONResponse`. - -**FastAPI** 🚚 🎏 `starlette.responses` `fastapi.responses` 🏪 👆, 👩‍💻. ✋️ 🌅 💪 📨 👟 🔗 ⚪️➡️ 💃. - - & `Response` 💪 ⚙️ 🛎 ⚒ 🎚 & 🍪, **FastAPI** 🚚 ⚫️ `fastapi.Response`. - -/// - -👀 🌐 💪 🔢 & 🎛, ✅ 🧾 💃. diff --git a/docs/em/docs/advanced/response-directly.md b/docs/em/docs/advanced/response-directly.md deleted file mode 100644 index 29819a2055..0000000000 --- a/docs/em/docs/advanced/response-directly.md +++ /dev/null @@ -1,65 +0,0 @@ -# 📨 📨 🔗 - -🕐❔ 👆 ✍ **FastAPI** *➡ 🛠️* 👆 💪 🛎 📨 🙆 📊 ⚪️➡️ ⚫️: `dict`, `list`, Pydantic 🏷, 💽 🏷, ♒️. - -🔢, **FastAPI** 🔜 🔁 🗜 👈 📨 💲 🎻 ⚙️ `jsonable_encoder` 🔬 [🎻 🔗 🔢](../tutorial/encoder.md){.internal-link target=_blank}. - -⤴️, ⛅ 🎑, ⚫️ 🔜 🚮 👈 🎻-🔗 💽 (✅ `dict`) 🔘 `JSONResponse` 👈 🔜 ⚙️ 📨 📨 👩‍💻. - -✋️ 👆 💪 📨 `JSONResponse` 🔗 ⚪️➡️ 👆 *➡ 🛠️*. - -⚫️ 💪 ⚠, 🖼, 📨 🛃 🎚 ⚖️ 🍪. - -## 📨 `Response` - -👐, 👆 💪 📨 🙆 `Response` ⚖️ 🙆 🎧-🎓 ⚫️. - -/// tip - -`JSONResponse` ⚫️ 🎧-🎓 `Response`. - -/// - -& 🕐❔ 👆 📨 `Response`, **FastAPI** 🔜 🚶‍♀️ ⚫️ 🔗. - -⚫️ 🏆 🚫 🙆 💽 🛠️ ⏮️ Pydantic 🏷, ⚫️ 🏆 🚫 🗜 🎚 🙆 🆎, ♒️. - -👉 🤝 👆 📚 💪. 👆 💪 📨 🙆 📊 🆎, 🔐 🙆 💽 📄 ⚖️ 🔬, ♒️. - -## ⚙️ `jsonable_encoder` `Response` - -↩️ **FastAPI** 🚫 🙆 🔀 `Response` 👆 📨, 👆 ✔️ ⚒ 💭 ⚫️ 🎚 🔜 ⚫️. - -🖼, 👆 🚫🔜 🚮 Pydantic 🏷 `JSONResponse` 🍵 🥇 🏭 ⚫️ `dict` ⏮️ 🌐 📊 🆎 (💖 `datetime`, `UUID`, ♒️) 🗜 🎻-🔗 🆎. - -📚 💼, 👆 💪 ⚙️ `jsonable_encoder` 🗜 👆 📊 ⏭ 🚶‍♀️ ⚫️ 📨: - -{* ../../docs_src/response_directly/tutorial001.py hl[6:7,21:22] *} - -/// note | 📡 ℹ - -👆 💪 ⚙️ `from starlette.responses import JSONResponse`. - -**FastAPI** 🚚 🎏 `starlette.responses` `fastapi.responses` 🏪 👆, 👩‍💻. ✋️ 🌅 💪 📨 👟 🔗 ⚪️➡️ 💃. - -/// - -## 🛬 🛃 `Response` - -🖼 🔛 🎦 🌐 🍕 👆 💪, ✋️ ⚫️ 🚫 📶 ⚠, 👆 💪 ✔️ 📨 `item` 🔗, & **FastAPI** 🔜 🚮 ⚫️ `JSONResponse` 👆, 🏭 ⚫️ `dict`, ♒️. 🌐 👈 🔢. - -🔜, ➡️ 👀 ❔ 👆 💪 ⚙️ 👈 📨 🛃 📨. - -➡️ 💬 👈 👆 💚 📨 📂 📨. - -👆 💪 🚮 👆 📂 🎚 🎻, 🚮 ⚫️ `Response`, & 📨 ⚫️: - -{* ../../docs_src/response_directly/tutorial002.py hl[1,18] *} - -## 🗒 - -🕐❔ 👆 📨 `Response` 🔗 🚮 📊 🚫 ✔, 🗜 (🎻), 🚫 📄 🔁. - -✋️ 👆 💪 📄 ⚫️ 🔬 [🌖 📨 🗄](additional-responses.md){.internal-link target=_blank}. - -👆 💪 👀 ⏪ 📄 ❔ ⚙️/📣 👉 🛃 `Response`Ⓜ ⏪ ✔️ 🏧 💽 🛠️, 🧾, ♒️. diff --git a/docs/em/docs/advanced/response-headers.md b/docs/em/docs/advanced/response-headers.md deleted file mode 100644 index c255380d67..0000000000 --- a/docs/em/docs/advanced/response-headers.md +++ /dev/null @@ -1,41 +0,0 @@ -# 📨 🎚 - -## ⚙️ `Response` 🔢 - -👆 💪 📣 🔢 🆎 `Response` 👆 *➡ 🛠️ 🔢* (👆 💪 🍪). - -& ⤴️ 👆 💪 ⚒ 🎚 👈 *🔀* 📨 🎚. - -{* ../../docs_src/response_headers/tutorial002.py hl[1,7:8] *} - -& ⤴️ 👆 💪 📨 🙆 🎚 👆 💪, 👆 🛎 🔜 ( `dict`, 💽 🏷, ♒️). - -& 🚥 👆 📣 `response_model`, ⚫️ 🔜 ⚙️ ⛽ & 🗜 🎚 👆 📨. - -**FastAPI** 🔜 ⚙️ 👈 *🔀* 📨 ⚗ 🎚 (🍪 & 👔 📟), & 🔜 🚮 👫 🏁 📨 👈 🔌 💲 👆 📨, ⛽ 🙆 `response_model`. - -👆 💪 📣 `Response` 🔢 🔗, & ⚒ 🎚 (& 🍪) 👫. - -## 📨 `Response` 🔗 - -👆 💪 🚮 🎚 🕐❔ 👆 📨 `Response` 🔗. - -✍ 📨 🔬 [📨 📨 🔗](response-directly.md){.internal-link target=_blank} & 🚶‍♀️ 🎚 🌖 🔢: - -{* ../../docs_src/response_headers/tutorial001.py hl[10:12] *} - -/// note | 📡 ℹ - -👆 💪 ⚙️ `from starlette.responses import Response` ⚖️ `from starlette.responses import JSONResponse`. - -**FastAPI** 🚚 🎏 `starlette.responses` `fastapi.responses` 🏪 👆, 👩‍💻. ✋️ 🌅 💪 📨 👟 🔗 ⚪️➡️ 💃. - - & `Response` 💪 ⚙️ 🛎 ⚒ 🎚 & 🍪, **FastAPI** 🚚 ⚫️ `fastapi.Response`. - -/// - -## 🛃 🎚 - -✔️ 🤯 👈 🛃 © 🎚 💪 🚮 ⚙️ '✖-' 🔡. - -✋️ 🚥 👆 ✔️ 🛃 🎚 👈 👆 💚 👩‍💻 🖥 💪 👀, 👆 💪 🚮 👫 👆 ⚜ 📳 (✍ 🌅 [⚜ (✖️-🇨🇳 ℹ 🤝)](../tutorial/cors.md){.internal-link target=_blank}), ⚙️ 🔢 `expose_headers` 📄 💃 ⚜ 🩺. diff --git a/docs/em/docs/advanced/security/http-basic-auth.md b/docs/em/docs/advanced/security/http-basic-auth.md deleted file mode 100644 index 73736f3b35..0000000000 --- a/docs/em/docs/advanced/security/http-basic-auth.md +++ /dev/null @@ -1,107 +0,0 @@ -# 🇺🇸🔍 🔰 🔐 - -🙅 💼, 👆 💪 ⚙️ 🇺🇸🔍 🔰 🔐. - -🇺🇸🔍 🔰 🔐, 🈸 ⌛ 🎚 👈 🔌 🆔 & 🔐. - -🚥 ⚫️ 🚫 📨 ⚫️, ⚫️ 📨 🇺🇸🔍 4️⃣0️⃣1️⃣ "⛔" ❌. - -& 📨 🎚 `WWW-Authenticate` ⏮️ 💲 `Basic`, & 📦 `realm` 🔢. - -👈 💬 🖥 🎦 🛠️ 📋 🆔 & 🔐. - -⤴️, 🕐❔ 👆 🆎 👈 🆔 & 🔐, 🖥 📨 👫 🎚 🔁. - -## 🙅 🇺🇸🔍 🔰 🔐 - -* 🗄 `HTTPBasic` & `HTTPBasicCredentials`. -* ✍ "`security` ⚖" ⚙️ `HTTPBasic`. -* ⚙️ 👈 `security` ⏮️ 🔗 👆 *➡ 🛠️*. -* ⚫️ 📨 🎚 🆎 `HTTPBasicCredentials`: - * ⚫️ 🔌 `username` & `password` 📨. - -{* ../../docs_src/security/tutorial006.py hl[2,6,10] *} - -🕐❔ 👆 🔄 📂 📛 🥇 🕰 (⚖️ 🖊 "🛠️" 🔼 🩺) 🖥 🔜 💭 👆 👆 🆔 & 🔐: - - - -## ✅ 🆔 - -📥 🌅 🏁 🖼. - -⚙️ 🔗 ✅ 🚥 🆔 & 🔐 ☑. - -👉, ⚙️ 🐍 🐩 🕹 `secrets` ✅ 🆔 & 🔐. - -`secrets.compare_digest()` 💪 ✊ `bytes` ⚖️ `str` 👈 🕴 🔌 🔠 🦹 (🕐 🇪🇸), 👉 ⛓ ⚫️ 🚫🔜 👷 ⏮️ 🦹 💖 `á`, `Sebastián`. - -🍵 👈, 👥 🥇 🗜 `username` & `password` `bytes` 🔢 👫 ⏮️ 🔠-8️⃣. - -⤴️ 👥 💪 ⚙️ `secrets.compare_digest()` 🚚 👈 `credentials.username` `"stanleyjobson"`, & 👈 `credentials.password` `"swordfish"`. - -{* ../../docs_src/security/tutorial007.py hl[1,11:21] *} - -👉 🔜 🎏: - -```Python -if not (credentials.username == "stanleyjobson") or not (credentials.password == "swordfish"): - # Return some error - ... -``` - -✋️ ⚙️ `secrets.compare_digest()` ⚫️ 🔜 🔐 🛡 🆎 👊 🤙 "🕰 👊". - -### ⏲ 👊 - -✋️ ⚫️❔ "⏲ 👊"❓ - -➡️ 🌈 👊 🔄 💭 🆔 & 🔐. - -& 👫 📨 📨 ⏮️ 🆔 `johndoe` & 🔐 `love123`. - -⤴️ 🐍 📟 👆 🈸 🔜 🌓 🕳 💖: - -```Python -if "johndoe" == "stanleyjobson" and "love123" == "swordfish": - ... -``` - -✋️ ▶️️ 🙍 🐍 🔬 🥇 `j` `johndoe` 🥇 `s` `stanleyjobson`, ⚫️ 🔜 📨 `False`, ↩️ ⚫️ ⏪ 💭 👈 📚 2️⃣ 🎻 🚫 🎏, 💭 👈 "📤 🙅‍♂ 💪 🗑 🌅 📊 ⚖ 🎂 🔤". & 👆 🈸 🔜 💬 "❌ 👩‍💻 ⚖️ 🔐". - -✋️ ⤴️ 👊 🔄 ⏮️ 🆔 `stanleyjobsox` & 🔐 `love123`. - -& 👆 🈸 📟 🔨 🕳 💖: - -```Python -if "stanleyjobsox" == "stanleyjobson" and "love123" == "swordfish": - ... -``` - -🐍 🔜 ✔️ 🔬 🎂 `stanleyjobso` 👯‍♂️ `stanleyjobsox` & `stanleyjobson` ⏭ 🤔 👈 👯‍♂️ 🎻 🚫 🎏. ⚫️ 🔜 ✊ ➕ ⏲ 📨 🔙 "❌ 👩‍💻 ⚖️ 🔐". - -#### 🕰 ❔ ℹ 👊 - -👈 ☝, 👀 👈 💽 ✊ ⏲ 📏 📨 "❌ 👩‍💻 ⚖️ 🔐" 📨, 👊 🔜 💭 👈 👫 🤚 _🕳_ ▶️️, ▶️ 🔤 ▶️️. - -& ⤴️ 👫 💪 🔄 🔄 🤔 👈 ⚫️ 🎲 🕳 🌖 🎏 `stanleyjobsox` 🌘 `johndoe`. - -#### "🕴" 👊 - -↗️, 👊 🔜 🚫 🔄 🌐 👉 ✋, 👫 🔜 ✍ 📋 ⚫️, 🎲 ⏮️ 💯 ⚖️ 💯 💯 📍 🥈. & 🔜 🤚 1️⃣ ➕ ☑ 🔤 🕰. - -✋️ 🔨 👈, ⏲ ⚖️ 📆 👊 🔜 ✔️ 💭 ☑ 🆔 & 🔐, ⏮️ "ℹ" 👆 🈸, ⚙️ 🕰 ✊ ❔. - -#### 🔧 ⚫️ ⏮️ `secrets.compare_digest()` - -✋️ 👆 📟 👥 🤙 ⚙️ `secrets.compare_digest()`. - -📏, ⚫️ 🔜 ✊ 🎏 🕰 🔬 `stanleyjobsox` `stanleyjobson` 🌘 ⚫️ ✊ 🔬 `johndoe` `stanleyjobson`. & 🎏 🔐. - -👈 🌌, ⚙️ `secrets.compare_digest()` 👆 🈸 📟, ⚫️ 🔜 🔒 🛡 👉 🎂 ↔ 💂‍♂ 👊. - -### 📨 ❌ - -⏮️ 🔍 👈 🎓 ❌, 📨 `HTTPException` ⏮️ 👔 📟 4️⃣0️⃣1️⃣ (🎏 📨 🕐❔ 🙅‍♂ 🎓 🚚) & 🚮 🎚 `WWW-Authenticate` ⚒ 🖥 🎦 💳 📋 🔄: - -{* ../../docs_src/security/tutorial007.py hl[23:27] *} diff --git a/docs/em/docs/advanced/security/index.md b/docs/em/docs/advanced/security/index.md deleted file mode 100644 index 5cdc475053..0000000000 --- a/docs/em/docs/advanced/security/index.md +++ /dev/null @@ -1,19 +0,0 @@ -# 🏧 💂‍♂ - -## 🌖 ⚒ - -📤 ➕ ⚒ 🍵 💂‍♂ ↖️ ⚪️➡️ 🕐 📔 [🔰 - 👩‍💻 🦮: 💂‍♂](../../tutorial/security/index.md){.internal-link target=_blank}. - -/// tip - -⏭ 📄 **🚫 🎯 "🏧"**. - - & ⚫️ 💪 👈 👆 ⚙️ 💼, ⚗ 1️⃣ 👫. - -/// - -## ✍ 🔰 🥇 - -⏭ 📄 🤔 👆 ⏪ ✍ 👑 [🔰 - 👩‍💻 🦮: 💂‍♂](../../tutorial/security/index.md){.internal-link target=_blank}. - -👫 🌐 ⚓️ 🔛 🎏 🔧, ✋️ ✔ ➕ 🛠️. diff --git a/docs/em/docs/advanced/security/oauth2-scopes.md b/docs/em/docs/advanced/security/oauth2-scopes.md deleted file mode 100644 index 9e3bc00587..0000000000 --- a/docs/em/docs/advanced/security/oauth2-scopes.md +++ /dev/null @@ -1,274 +0,0 @@ -# Oauth2️⃣ ↔ - -👆 💪 ⚙️ Oauth2️⃣ ↔ 🔗 ⏮️ **FastAPI**, 👫 🛠️ 👷 💎. - -👉 🔜 ✔ 👆 ✔️ 🌖 👌-🧽 ✔ ⚙️, 📄 Oauth2️⃣ 🐩, 🛠️ 🔘 👆 🗄 🈸 (& 🛠️ 🩺). - -Oauth2️⃣ ⏮️ ↔ 🛠️ ⚙️ 📚 🦏 🤝 🐕‍🦺, 💖 👱📔, 🇺🇸🔍, 📂, 🤸‍♂, 👱📔, ♒️. 👫 ⚙️ ⚫️ 🚚 🎯 ✔ 👩‍💻 & 🈸. - -🔠 🕰 👆 "🕹 ⏮️" 👱📔, 🇺🇸🔍, 📂, 🤸‍♂, 👱📔, 👈 🈸 ⚙️ Oauth2️⃣ ⏮️ ↔. - -👉 📄 👆 🔜 👀 ❔ 🛠️ 🤝 & ✔ ⏮️ 🎏 Oauth2️⃣ ⏮️ ↔ 👆 **FastAPI** 🈸. - -/// warning - -👉 🌅 ⚖️ 🌘 🏧 📄. 🚥 👆 ▶️, 👆 💪 🚶 ⚫️. - -👆 🚫 🎯 💪 Oauth2️⃣ ↔, & 👆 💪 🍵 🤝 & ✔ 👐 👆 💚. - -✋️ Oauth2️⃣ ⏮️ ↔ 💪 🎆 🛠️ 🔘 👆 🛠️ (⏮️ 🗄) & 👆 🛠️ 🩺. - -👐, 👆 🛠️ 📚 ↔, ⚖️ 🙆 🎏 💂‍♂/✔ 📄, 👐 👆 💪, 👆 📟. - -📚 💼, Oauth2️⃣ ⏮️ ↔ 💪 👹. - -✋️ 🚥 👆 💭 👆 💪 ⚫️, ⚖️ 👆 😟, 🚧 👂. - -/// - -## Oauth2️⃣ ↔ & 🗄 - -Oauth2️⃣ 🔧 🔬 "↔" 📇 🎻 🎏 🚀. - -🎚 🔠 👉 🎻 💪 ✔️ 🙆 📁, ✋️ 🔜 🚫 🔌 🚀. - -👫 ↔ 🎨 "✔". - -🗄 (✅ 🛠️ 🩺), 👆 💪 🔬 "💂‍♂ ⚖". - -🕐❔ 1️⃣ 👫 💂‍♂ ⚖ ⚙️ Oauth2️⃣, 👆 💪 📣 & ⚙️ ↔. - -🔠 "↔" 🎻 (🍵 🚀). - -👫 🛎 ⚙️ 📣 🎯 💂‍♂ ✔, 🖼: - -* `users:read` ⚖️ `users:write` ⚠ 🖼. -* `instagram_basic` ⚙️ 👱📔 / 👱📔. -* `https://www.googleapis.com/auth/drive` ⚙️ 🇺🇸🔍. - -/// info - -Oauth2️⃣ "↔" 🎻 👈 📣 🎯 ✔ ✔. - -⚫️ 🚫 🤔 🚥 ⚫️ ✔️ 🎏 🦹 💖 `:` ⚖️ 🚥 ⚫️ 📛. - -👈 ℹ 🛠️ 🎯. - -Oauth2️⃣ 👫 🎻. - -/// - -## 🌐 🎑 - -🥇, ➡️ 🔜 👀 🍕 👈 🔀 ⚪️➡️ 🖼 👑 **🔰 - 👩‍💻 🦮** [Oauth2️⃣ ⏮️ 🔐 (& 🔁), 📨 ⏮️ 🥙 🤝](../../tutorial/security/oauth2-jwt.md){.internal-link target=_blank}. 🔜 ⚙️ Oauth2️⃣ ↔: - -{* ../../docs_src/security/tutorial005.py hl[2,4,8,12,46,64,105,107:115,121:125,129:135,140,156] *} - -🔜 ➡️ 📄 👈 🔀 🔁 🔁. - -## Oauth2️⃣ 💂‍♂ ⚖ - -🥇 🔀 👈 🔜 👥 📣 Oauth2️⃣ 💂‍♂ ⚖ ⏮️ 2️⃣ 💪 ↔, `me` & `items`. - -`scopes` 🔢 📨 `dict` ⏮️ 🔠 ↔ 🔑 & 📛 💲: - -{* ../../docs_src/security/tutorial005.py hl[62:65] *} - -↩️ 👥 🔜 📣 📚 ↔, 👫 🔜 🎦 🆙 🛠️ 🩺 🕐❔ 👆 🕹-/✔. - -& 👆 🔜 💪 🖊 ❔ ↔ 👆 💚 🤝 🔐: `me` & `items`. - -👉 🎏 🛠️ ⚙️ 🕐❔ 👆 🤝 ✔ ⏪ 🚨 ⏮️ 👱📔, 🇺🇸🔍, 📂, ♒️: - - - -## 🥙 🤝 ⏮️ ↔ - -🔜, 🔀 🤝 *➡ 🛠️* 📨 ↔ 📨. - -👥 ⚙️ 🎏 `OAuth2PasswordRequestForm`. ⚫️ 🔌 🏠 `scopes` ⏮️ `list` `str`, ⏮️ 🔠 ↔ ⚫️ 📨 📨. - -& 👥 📨 ↔ 🍕 🥙 🤝. - -/// danger - -🦁, 📥 👥 ❎ ↔ 📨 🔗 🤝. - -✋️ 👆 🈸, 💂‍♂, 👆 🔜 ⚒ 💭 👆 🕴 🚮 ↔ 👈 👩‍💻 🤙 💪 ✔️, ⚖️ 🕐 👆 ✔️ 🔁. - -/// - -{* ../../docs_src/security/tutorial005.py hl[156] *} - -## 📣 ↔ *➡ 🛠️* & 🔗 - -🔜 👥 📣 👈 *➡ 🛠️* `/users/me/items/` 🚚 ↔ `items`. - -👉, 👥 🗄 & ⚙️ `Security` ⚪️➡️ `fastapi`. - -👆 💪 ⚙️ `Security` 📣 🔗 (💖 `Depends`), ✋️ `Security` 📨 🔢 `scopes` ⏮️ 📇 ↔ (🎻). - -👉 💼, 👥 🚶‍♀️ 🔗 🔢 `get_current_active_user` `Security` (🎏 🌌 👥 🔜 ⏮️ `Depends`). - -✋️ 👥 🚶‍♀️ `list` ↔, 👉 💼 ⏮️ 1️⃣ ↔: `items` (⚫️ 💪 ✔️ 🌅). - -& 🔗 🔢 `get_current_active_user` 💪 📣 🎧-🔗, 🚫 🕴 ⏮️ `Depends` ✋️ ⏮️ `Security`. 📣 🚮 👍 🎧-🔗 🔢 (`get_current_user`), & 🌖 ↔ 📄. - -👉 💼, ⚫️ 🚚 ↔ `me` (⚫️ 💪 🚚 🌅 🌘 1️⃣ ↔). - -/// note - -👆 🚫 🎯 💪 🚮 🎏 ↔ 🎏 🥉. - -👥 🔨 ⚫️ 📥 🎦 ❔ **FastAPI** 🍵 ↔ 📣 🎏 🎚. - -/// - -{* ../../docs_src/security/tutorial005.py hl[4,140,169] *} - -/// info | 📡 ℹ - -`Security` 🤙 🏿 `Depends`, & ⚫️ ✔️ 1️⃣ ➕ 🔢 👈 👥 🔜 👀 ⏪. - -✋️ ⚙️ `Security` ↩️ `Depends`, **FastAPI** 🔜 💭 👈 ⚫️ 💪 📣 💂‍♂ ↔, ⚙️ 👫 🔘, & 📄 🛠️ ⏮️ 🗄. - -✋️ 🕐❔ 👆 🗄 `Query`, `Path`, `Depends`, `Security` & 🎏 ⚪️➡️ `fastapi`, 👈 🤙 🔢 👈 📨 🎁 🎓. - -/// - -## ⚙️ `SecurityScopes` - -🔜 ℹ 🔗 `get_current_user`. - -👉 1️⃣ ⚙️ 🔗 🔛. - -📥 👥 ⚙️ 🎏 Oauth2️⃣ ⚖ 👥 ✍ ⏭, 📣 ⚫️ 🔗: `oauth2_scheme`. - -↩️ 👉 🔗 🔢 🚫 ✔️ 🙆 ↔ 📄 ⚫️, 👥 💪 ⚙️ `Depends` ⏮️ `oauth2_scheme`, 👥 🚫 ✔️ ⚙️ `Security` 🕐❔ 👥 🚫 💪 ✔ 💂‍♂ ↔. - -👥 📣 🎁 🔢 🆎 `SecurityScopes`, 🗄 ⚪️➡️ `fastapi.security`. - -👉 `SecurityScopes` 🎓 🎏 `Request` (`Request` ⚙️ 🤚 📨 🎚 🔗). - -{* ../../docs_src/security/tutorial005.py hl[8,105] *} - -## ⚙️ `scopes` - -🔢 `security_scopes` 🔜 🆎 `SecurityScopes`. - -⚫️ 🔜 ✔️ 🏠 `scopes` ⏮️ 📇 ⚗ 🌐 ↔ ✔ ⚫️ & 🌐 🔗 👈 ⚙️ 👉 🎧-🔗. 👈 ⛓, 🌐 "⚓️"... 👉 💪 🔊 😨, ⚫️ 🔬 🔄 ⏪ 🔛. - -`security_scopes` 🎚 (🎓 `SecurityScopes`) 🚚 `scope_str` 🔢 ⏮️ 👁 🎻, 🔌 👈 ↔ 👽 🚀 (👥 🔜 ⚙️ ⚫️). - -👥 ✍ `HTTPException` 👈 👥 💪 🏤-⚙️ (`raise`) ⏪ 📚 ☝. - -👉 ⚠, 👥 🔌 ↔ 🚚 (🚥 🙆) 🎻 👽 🚀 (⚙️ `scope_str`). 👥 🚮 👈 🎻 ⚗ ↔ `WWW-Authenticate` 🎚 (👉 🍕 🔌). - -{* ../../docs_src/security/tutorial005.py hl[105,107:115] *} - -## ✔ `username` & 💽 💠 - -👥 ✔ 👈 👥 🤚 `username`, & ⚗ ↔. - -& ⤴️ 👥 ✔ 👈 📊 ⏮️ Pydantic 🏷 (✊ `ValidationError` ⚠), & 🚥 👥 🤚 ❌ 👂 🥙 🤝 ⚖️ ⚖ 📊 ⏮️ Pydantic, 👥 🤚 `HTTPException` 👥 ✍ ⏭. - -👈, 👥 ℹ Pydantic 🏷 `TokenData` ⏮️ 🆕 🏠 `scopes`. - -⚖ 📊 ⏮️ Pydantic 👥 💪 ⚒ 💭 👈 👥 ✔️, 🖼, ⚫️❔ `list` `str` ⏮️ ↔ & `str` ⏮️ `username`. - -↩️, 🖼, `dict`, ⚖️ 🕳 🙆, ⚫️ 💪 💔 🈸 ☝ ⏪, ⚒ ⚫️ 💂‍♂ ⚠. - -👥 ✔ 👈 👥 ✔️ 👩‍💻 ⏮️ 👈 🆔, & 🚥 🚫, 👥 🤚 👈 🎏 ⚠ 👥 ✍ ⏭. - -{* ../../docs_src/security/tutorial005.py hl[46,116:128] *} - -## ✔ `scopes` - -👥 🔜 ✔ 👈 🌐 ↔ ✔, 👉 🔗 & 🌐 ⚓️ (🔌 *➡ 🛠️*), 🔌 ↔ 🚚 🤝 📨, ⏪ 🤚 `HTTPException`. - -👉, 👥 ⚙️ `security_scopes.scopes`, 👈 🔌 `list` ⏮️ 🌐 👫 ↔ `str`. - -{* ../../docs_src/security/tutorial005.py hl[129:135] *} - -## 🔗 🌲 & ↔ - -➡️ 📄 🔄 👉 🔗 🌲 & ↔. - -`get_current_active_user` 🔗 ✔️ 🎧-🔗 🔛 `get_current_user`, ↔ `"me"` 📣 `get_current_active_user` 🔜 🔌 📇 ✔ ↔ `security_scopes.scopes` 🚶‍♀️ `get_current_user`. - -*➡ 🛠️* ⚫️ 📣 ↔, `"items"`, 👉 🔜 📇 `security_scopes.scopes` 🚶‍♀️ `get_current_user`. - -📥 ❔ 🔗 🔗 & ↔ 👀 💖: - -* *➡ 🛠️* `read_own_items` ✔️: - * ✔ ↔ `["items"]` ⏮️ 🔗: - * `get_current_active_user`: - * 🔗 🔢 `get_current_active_user` ✔️: - * ✔ ↔ `["me"]` ⏮️ 🔗: - * `get_current_user`: - * 🔗 🔢 `get_current_user` ✔️: - * 🙅‍♂ ↔ ✔ ⚫️. - * 🔗 ⚙️ `oauth2_scheme`. - * `security_scopes` 🔢 🆎 `SecurityScopes`: - * 👉 `security_scopes` 🔢 ✔️ 🏠 `scopes` ⏮️ `list` ⚗ 🌐 👫 ↔ 📣 🔛,: - * `security_scopes.scopes` 🔜 🔌 `["me", "items"]` *➡ 🛠️* `read_own_items`. - * `security_scopes.scopes` 🔜 🔌 `["me"]` *➡ 🛠️* `read_users_me`, ↩️ ⚫️ 📣 🔗 `get_current_active_user`. - * `security_scopes.scopes` 🔜 🔌 `[]` (🕳) *➡ 🛠️* `read_system_status`, ↩️ ⚫️ 🚫 📣 🙆 `Security` ⏮️ `scopes`, & 🚮 🔗, `get_current_user`, 🚫 📣 🙆 `scope` 👯‍♂️. - -/// tip - -⚠ & "🎱" 👜 📥 👈 `get_current_user` 🔜 ✔️ 🎏 📇 `scopes` ✅ 🔠 *➡ 🛠️*. - -🌐 ⚓️ 🔛 `scopes` 📣 🔠 *➡ 🛠️* & 🔠 🔗 🔗 🌲 👈 🎯 *➡ 🛠️*. - -/// - -## 🌖 ℹ 🔃 `SecurityScopes` - -👆 💪 ⚙️ `SecurityScopes` 🙆 ☝, & 💗 🥉, ⚫️ 🚫 ✔️ "🌱" 🔗. - -⚫️ 🔜 🕧 ✔️ 💂‍♂ ↔ 📣 ⏮️ `Security` 🔗 & 🌐 ⚓️ **👈 🎯** *➡ 🛠️* & **👈 🎯** 🔗 🌲. - -↩️ `SecurityScopes` 🔜 ✔️ 🌐 ↔ 📣 ⚓️, 👆 💪 ⚙️ ⚫️ ✔ 👈 🤝 ✔️ 🚚 ↔ 🇨🇫 🔗 🔢, & ⤴️ 📣 🎏 ↔ 📄 🎏 *➡ 🛠️*. - -👫 🔜 ✅ ➡ 🔠 *➡ 🛠️*. - -## ✅ ⚫️ - -🚥 👆 📂 🛠️ 🩺, 👆 💪 🔓 & ✔ ❔ ↔ 👆 💚 ✔. - - - -🚥 👆 🚫 🖊 🙆 ↔, 👆 🔜 "🔓", ✋️ 🕐❔ 👆 🔄 🔐 `/users/me/` ⚖️ `/users/me/items/` 👆 🔜 🤚 ❌ 💬 👈 👆 🚫 ✔️ 🥃 ✔. 👆 🔜 💪 🔐 `/status/`. - -& 🚥 👆 🖊 ↔ `me` ✋️ 🚫 ↔ `items`, 👆 🔜 💪 🔐 `/users/me/` ✋️ 🚫 `/users/me/items/`. - -👈 ⚫️❔ 🔜 🔨 🥉 🥳 🈸 👈 🔄 🔐 1️⃣ 👫 *➡ 🛠️* ⏮️ 🤝 🚚 👩‍💻, ⚓️ 🔛 ❔ 📚 ✔ 👩‍💻 🤝 🈸. - -## 🔃 🥉 🥳 🛠️ - -👉 🖼 👥 ⚙️ Oauth2️⃣ "🔐" 💧. - -👉 ☑ 🕐❔ 👥 🚨 👆 👍 🈸, 🎲 ⏮️ 👆 👍 🕸. - -↩️ 👥 💪 💙 ⚫️ 📨 `username` & `password`, 👥 🎛 ⚫️. - -✋️ 🚥 👆 🏗 Oauth2️⃣ 🈸 👈 🎏 🔜 🔗 (➡, 🚥 👆 🏗 🤝 🐕‍🦺 🌓 👱📔, 🇺🇸🔍, 📂, ♒️.) 👆 🔜 ⚙️ 1️⃣ 🎏 💧. - -🌅 ⚠ 🔑 💧. - -🏆 🔐 📟 💧, ✋️ 🌖 🏗 🛠️ ⚫️ 🚚 🌅 📶. ⚫️ 🌅 🏗, 📚 🐕‍🦺 🔚 🆙 ✔ 🔑 💧. - -/// note - -⚫️ ⚠ 👈 🔠 🤝 🐕‍🦺 📛 👫 💧 🎏 🌌, ⚒ ⚫️ 🍕 👫 🏷. - -✋️ 🔚, 👫 🛠️ 🎏 Oauth2️⃣ 🐩. - -/// - -**FastAPI** 🔌 🚙 🌐 👫 Oauth2️⃣ 🤝 💧 `fastapi.security.oauth2`. - -## `Security` 👨‍🎨 `dependencies` - -🎏 🌌 👆 💪 🔬 `list` `Depends` 👨‍🎨 `dependencies` 🔢 (🔬 [🔗 ➡ 🛠️ 👨‍🎨](../../tutorial/dependencies/dependencies-in-path-operation-decorators.md){.internal-link target=_blank}), 👆 💪 ⚙️ `Security` ⏮️ `scopes` 📤. diff --git a/docs/em/docs/advanced/settings.md b/docs/em/docs/advanced/settings.md deleted file mode 100644 index 7fdd0d68a6..0000000000 --- a/docs/em/docs/advanced/settings.md +++ /dev/null @@ -1,396 +0,0 @@ -# ⚒ & 🌐 🔢 - -📚 💼 👆 🈸 💪 💪 🔢 ⚒ ⚖️ 📳, 🖼 ㊙ 🔑, 💽 🎓, 🎓 📧 🐕‍🦺, ♒️. - -🏆 👫 ⚒ 🔢 (💪 🔀), 💖 💽 📛. & 📚 💪 🚿, 💖 ㊙. - -👉 🤔 ⚫️ ⚠ 🚚 👫 🌐 🔢 👈 ✍ 🈸. - -## 🌐 🔢 - -/// tip - -🚥 👆 ⏪ 💭 ⚫️❔ "🌐 🔢" & ❔ ⚙️ 👫, 💭 🆓 🚶 ⏭ 📄 🔛. - -/// - -🌐 🔢 (💭 "🇨🇻 {") 🔢 👈 🖖 🏞 🐍 📟, 🏃‍♂ ⚙️, & 💪 ✍ 👆 🐍 📟 (⚖️ 🎏 📋 👍). - -👆 💪 ✍ & ⚙️ 🌐 🔢 🐚, 🍵 💆‍♂ 🐍: - -//// tab | 💾, 🇸🇻, 🚪 🎉 - -
- -```console -// You could create an env var MY_NAME with -$ export MY_NAME="Wade Wilson" - -// Then you could use it with other programs, like -$ echo "Hello $MY_NAME" - -Hello Wade Wilson -``` - -
- -//// - -//// tab | 🚪 📋 - -
- -```console -// Create an env var MY_NAME -$ $Env:MY_NAME = "Wade Wilson" - -// Use it with other programs, like -$ echo "Hello $Env:MY_NAME" - -Hello Wade Wilson -``` - -
- -//// - -### ✍ 🇨🇻 {🐍 - -👆 💪 ✍ 🌐 🔢 🏞 🐍, 📶 (⚖️ ⏮️ 🙆 🎏 👩‍🔬), & ⤴️ ✍ 👫 🐍. - -🖼 👆 💪 ✔️ 📁 `main.py` ⏮️: - -```Python hl_lines="3" -import os - -name = os.getenv("MY_NAME", "World") -print(f"Hello {name} from Python") -``` - -/// tip - -🥈 ❌ `os.getenv()` 🔢 💲 📨. - -🚥 🚫 🚚, ⚫️ `None` 🔢, 📥 👥 🚚 `"World"` 🔢 💲 ⚙️. - -/// - -⤴️ 👆 💪 🤙 👈 🐍 📋: - -
- -```console -// Here we don't set the env var yet -$ python main.py - -// As we didn't set the env var, we get the default value - -Hello World from Python - -// But if we create an environment variable first -$ export MY_NAME="Wade Wilson" - -// And then call the program again -$ python main.py - -// Now it can read the environment variable - -Hello Wade Wilson from Python -``` - -
- -🌐 🔢 💪 ⚒ 🏞 📟, ✋️ 💪 ✍ 📟, & 🚫 ✔️ 🏪 (💕 `git`) ⏮️ 🎂 📁, ⚫️ ⚠ ⚙️ 👫 📳 ⚖️ ⚒. - -👆 💪 ✍ 🌐 🔢 🕴 🎯 📋 👼, 👈 🕴 💪 👈 📋, & 🕴 🚮 📐. - -👈, ✍ ⚫️ ▶️️ ⏭ 📋 ⚫️, 🔛 🎏 ⏸: - -
- -```console -// Create an env var MY_NAME in line for this program call -$ MY_NAME="Wade Wilson" python main.py - -// Now it can read the environment variable - -Hello Wade Wilson from Python - -// The env var no longer exists afterwards -$ python main.py - -Hello World from Python -``` - -
- -/// tip - -👆 💪 ✍ 🌅 🔃 ⚫️ 1️⃣2️⃣-⚖ 📱: 📁. - -/// - -### 🆎 & 🔬 - -👫 🌐 🔢 💪 🕴 🍵 ✍ 🎻, 👫 🔢 🐍 & ✔️ 🔗 ⏮️ 🎏 📋 & 🎂 ⚙️ (& ⏮️ 🎏 🏃‍♂ ⚙️, 💾, 🚪, 🇸🇻). - -👈 ⛓ 👈 🙆 💲 ✍ 🐍 ⚪️➡️ 🌐 🔢 🔜 `str`, & 🙆 🛠️ 🎏 🆎 ⚖️ 🔬 ✔️ 🔨 📟. - -## Pydantic `Settings` - -👐, Pydantic 🚚 👑 🚙 🍵 👫 ⚒ 👟 ⚪️➡️ 🌐 🔢 ⏮️ Pydantic: ⚒ 🧾. - -### ✍ `Settings` 🎚 - -🗄 `BaseSettings` ⚪️➡️ Pydantic & ✍ 🎧-🎓, 📶 🌅 💖 ⏮️ Pydantic 🏷. - -🎏 🌌 ⏮️ Pydantic 🏷, 👆 📣 🎓 🔢 ⏮️ 🆎 ✍, & 🎲 🔢 💲. - -👆 💪 ⚙️ 🌐 🎏 🔬 ⚒ & 🧰 👆 ⚙️ Pydantic 🏷, 💖 🎏 📊 🆎 & 🌖 🔬 ⏮️ `Field()`. - -{* ../../docs_src/settings/tutorial001.py hl[2,5:8,11] *} - -/// tip - -🚥 👆 💚 🕳 ⏩ 📁 & 📋, 🚫 ⚙️ 👉 🖼, ⚙️ 🏁 1️⃣ 🔛. - -/// - -⤴️, 🕐❔ 👆 ✍ 👐 👈 `Settings` 🎓 (👉 💼, `settings` 🎚), Pydantic 🔜 ✍ 🌐 🔢 💼-😛 🌌,, ↖-💼 🔢 `APP_NAME` 🔜 ✍ 🔢 `app_name`. - -⏭ ⚫️ 🔜 🗜 & ✔ 💽. , 🕐❔ 👆 ⚙️ 👈 `settings` 🎚, 👆 🔜 ✔️ 📊 🆎 👆 📣 (✅ `items_per_user` 🔜 `int`). - -### ⚙️ `settings` - -⤴️ 👆 💪 ⚙️ 🆕 `settings` 🎚 👆 🈸: - -{* ../../docs_src/settings/tutorial001.py hl[18:20] *} - -### 🏃 💽 - -⏭, 👆 🔜 🏃 💽 🚶‍♀️ 📳 🌐 🔢, 🖼 👆 💪 ⚒ `ADMIN_EMAIL` & `APP_NAME` ⏮️: - -
- -```console -$ ADMIN_EMAIL="deadpool@example.com" APP_NAME="ChimichangApp" uvicorn main:app - -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -``` - -
- -/// tip - -⚒ 💗 🇨🇻 {👁 📋 🎏 👫 ⏮️ 🚀, & 🚮 👫 🌐 ⏭ 📋. - -/// - -& ⤴️ `admin_email` ⚒ 🔜 ⚒ `"deadpool@example.com"`. - -`app_name` 🔜 `"ChimichangApp"`. - -& `items_per_user` 🔜 🚧 🚮 🔢 💲 `50`. - -## ⚒ ➕1️⃣ 🕹 - -👆 💪 🚮 👈 ⚒ ➕1️⃣ 🕹 📁 👆 👀 [🦏 🈸 - 💗 📁](../tutorial/bigger-applications.md){.internal-link target=_blank}. - -🖼, 👆 💪 ✔️ 📁 `config.py` ⏮️: - -{* ../../docs_src/settings/app01/config.py *} - -& ⤴️ ⚙️ ⚫️ 📁 `main.py`: - -{* ../../docs_src/settings/app01/main.py hl[3,11:13] *} - -/// tip - -👆 🔜 💪 📁 `__init__.py` 👆 👀 🔛 [🦏 🈸 - 💗 📁](../tutorial/bigger-applications.md){.internal-link target=_blank}. - -/// - -## ⚒ 🔗 - -🍾 ⚫️ 5️⃣📆 ⚠ 🚚 ⚒ ⚪️➡️ 🔗, ↩️ ✔️ 🌐 🎚 ⏮️ `settings` 👈 ⚙️ 🌐. - -👉 💪 ✴️ ⚠ ⏮️ 🔬, ⚫️ 📶 ⏩ 🔐 🔗 ⏮️ 👆 👍 🛃 ⚒. - -### 📁 📁 - -👟 ⚪️➡️ ⏮️ 🖼, 👆 `config.py` 📁 💪 👀 💖: - -{* ../../docs_src/settings/app02/config.py hl[10] *} - -👀 👈 🔜 👥 🚫 ✍ 🔢 👐 `settings = Settings()`. - -### 👑 📱 📁 - -🔜 👥 ✍ 🔗 👈 📨 🆕 `config.Settings()`. - -{* ../../docs_src/settings/app02/main.py hl[5,11:12] *} - -/// tip - -👥 🔜 🔬 `@lru_cache` 🍖. - -🔜 👆 💪 🤔 `get_settings()` 😐 🔢. - -/// - -& ⤴️ 👥 💪 🚚 ⚫️ ⚪️➡️ *➡ 🛠️ 🔢* 🔗 & ⚙️ ⚫️ 🙆 👥 💪 ⚫️. - -{* ../../docs_src/settings/app02/main.py hl[16,18:20] *} - -### ⚒ & 🔬 - -⤴️ ⚫️ 🔜 📶 ⏩ 🚚 🎏 ⚒ 🎚 ⏮️ 🔬 🏗 🔗 🔐 `get_settings`: - -{* ../../docs_src/settings/app02/test_main.py hl[9:10,13,21] *} - -🔗 🔐 👥 ⚒ 🆕 💲 `admin_email` 🕐❔ 🏗 🆕 `Settings` 🎚, & ⤴️ 👥 📨 👈 🆕 🎚. - -⤴️ 👥 💪 💯 👈 ⚫️ ⚙️. - -## 👂 `.env` 📁 - -🚥 👆 ✔️ 📚 ⚒ 👈 🎲 🔀 📚, 🎲 🎏 🌐, ⚫️ 5️⃣📆 ⚠ 🚮 👫 🔛 📁 & ⤴️ ✍ 👫 ⚪️➡️ ⚫️ 🚥 👫 🌐 🔢. - -👉 💡 ⚠ 🥃 👈 ⚫️ ✔️ 📛, 👫 🌐 🔢 🛎 🥉 📁 `.env`, & 📁 🤙 "🇨🇻". - -/// tip - -📁 ▶️ ⏮️ ❣ (`.`) 🕵‍♂ 📁 🖥-💖 ⚙️, 💖 💾 & 🇸🇻. - -✋️ 🇨🇻 📁 🚫 🤙 ✔️ ✔️ 👈 ☑ 📁. - -/// - -Pydantic ✔️ 🐕‍🦺 👂 ⚪️➡️ 👉 🆎 📁 ⚙️ 🔢 🗃. 👆 💪 ✍ 🌖 Pydantic ⚒: 🇨🇻 (.🇨🇻) 🐕‍🦺. - -/// tip - -👉 👷, 👆 💪 `pip install python-dotenv`. - -/// - -### `.env` 📁 - -👆 💪 ✔️ `.env` 📁 ⏮️: - -```bash -ADMIN_EMAIL="deadpool@example.com" -APP_NAME="ChimichangApp" -``` - -### ✍ ⚒ ⚪️➡️ `.env` - -& ⤴️ ℹ 👆 `config.py` ⏮️: - -{* ../../docs_src/settings/app03/config.py hl[9:10] *} - -📥 👥 ✍ 🎓 `Config` 🔘 👆 Pydantic `Settings` 🎓, & ⚒ `env_file` 📁 ⏮️ 🇨🇻 📁 👥 💚 ⚙️. - -/// tip - -`Config` 🎓 ⚙️ Pydantic 📳. 👆 💪 ✍ 🌖 Pydantic 🏷 📁 - -/// - -### 🏗 `Settings` 🕴 🕐 ⏮️ `lru_cache` - -👂 📁 ⚪️➡️ 💾 🛎 ⚠ (🐌) 🛠️, 👆 🎲 💚 ⚫️ 🕴 🕐 & ⤴️ 🏤-⚙️ 🎏 ⚒ 🎚, ↩️ 👂 ⚫️ 🔠 📨. - -✋️ 🔠 🕰 👥: - -```Python -Settings() -``` - -🆕 `Settings` 🎚 🔜 ✍, & 🏗 ⚫️ 🔜 ✍ `.env` 📁 🔄. - -🚥 🔗 🔢 💖: - -```Python -def get_settings(): - return Settings() -``` - -👥 🔜 ✍ 👈 🎚 🔠 📨, & 👥 🔜 👂 `.env` 📁 🔠 📨. 👶 👶 - -✋️ 👥 ⚙️ `@lru_cache` 👨‍🎨 🔛 🔝, `Settings` 🎚 🔜 ✍ 🕴 🕐, 🥇 🕰 ⚫️ 🤙. 👶 👶 - -{* ../../docs_src/settings/app03/main.py hl[1,10] *} - -⤴️ 🙆 🏁 🤙 `get_settings()` 🔗 ⏭ 📨, ↩️ 🛠️ 🔗 📟 `get_settings()` & 🏗 🆕 `Settings` 🎚, ⚫️ 🔜 📨 🎏 🎚 👈 📨 🔛 🥇 🤙, 🔄 & 🔄. - -#### `lru_cache` 📡 ℹ - -`@lru_cache` 🔀 🔢 ⚫️ 🎀 📨 🎏 💲 👈 📨 🥇 🕰, ↩️ 💻 ⚫️ 🔄, 🛠️ 📟 🔢 🔠 🕰. - -, 🔢 🔛 ⚫️ 🔜 🛠️ 🕐 🔠 🌀 ❌. & ⤴️ 💲 📨 🔠 👈 🌀 ❌ 🔜 ⚙️ 🔄 & 🔄 🕐❔ 🔢 🤙 ⏮️ ⚫️❔ 🎏 🌀 ❌. - -🖼, 🚥 👆 ✔️ 🔢: - -```Python -@lru_cache -def say_hi(name: str, salutation: str = "Ms."): - return f"Hello {salutation} {name}" -``` - -👆 📋 💪 🛠️ 💖 👉: - -```mermaid -sequenceDiagram - -participant code as Code -participant function as say_hi() -participant execute as Execute function - - rect rgba(0, 255, 0, .1) - code ->> function: say_hi(name="Camila") - function ->> execute: execute function code - execute ->> code: return the result - end - - rect rgba(0, 255, 255, .1) - code ->> function: say_hi(name="Camila") - function ->> code: return stored result - end - - rect rgba(0, 255, 0, .1) - code ->> function: say_hi(name="Rick") - function ->> execute: execute function code - execute ->> code: return the result - end - - rect rgba(0, 255, 0, .1) - code ->> function: say_hi(name="Rick", salutation="Mr.") - function ->> execute: execute function code - execute ->> code: return the result - end - - rect rgba(0, 255, 255, .1) - code ->> function: say_hi(name="Rick") - function ->> code: return stored result - end - - rect rgba(0, 255, 255, .1) - code ->> function: say_hi(name="Camila") - function ->> code: return stored result - end -``` - -💼 👆 🔗 `get_settings()`, 🔢 🚫 ✊ 🙆 ❌, ⚫️ 🕧 📨 🎏 💲. - -👈 🌌, ⚫️ 🎭 🌖 🚥 ⚫️ 🌐 🔢. ✋️ ⚫️ ⚙️ 🔗 🔢, ⤴️ 👥 💪 🔐 ⚫️ 💪 🔬. - -`@lru_cache` 🍕 `functools` ❔ 🍕 🐍 🐩 🗃, 👆 💪 ✍ 🌅 🔃 ⚫️ 🐍 🩺 `@lru_cache`. - -## 🌃 - -👆 💪 ⚙️ Pydantic ⚒ 🍵 ⚒ ⚖️ 📳 👆 🈸, ⏮️ 🌐 🏋️ Pydantic 🏷. - -* ⚙️ 🔗 👆 💪 📉 🔬. -* 👆 💪 ⚙️ `.env` 📁 ⏮️ ⚫️. -* ⚙️ `@lru_cache` ➡️ 👆 ❎ 👂 🇨🇻 📁 🔄 & 🔄 🔠 📨, ⏪ 🤝 👆 🔐 ⚫️ ⏮️ 🔬. diff --git a/docs/em/docs/advanced/sub-applications.md b/docs/em/docs/advanced/sub-applications.md deleted file mode 100644 index 7a802cd77f..0000000000 --- a/docs/em/docs/advanced/sub-applications.md +++ /dev/null @@ -1,67 +0,0 @@ -# 🎧 🈸 - 🗻 - -🚥 👆 💪 ✔️ 2️⃣ 🔬 FastAPI 🈸, ⏮️ 👫 👍 🔬 🗄 & 👫 👍 🩺 ⚜, 👆 💪 ✔️ 👑 📱 & "🗻" 1️⃣ (⚖️ 🌅) 🎧-🈸(Ⓜ). - -## 🗜 **FastAPI** 🈸 - -"🗜" ⛓ ❎ 🍕 "🔬" 🈸 🎯 ➡, 👈 ⤴️ ✊ 💅 🚚 🌐 🔽 👈 ➡, ⏮️ _➡ 🛠️_ 📣 👈 🎧-🈸. - -### 🔝-🎚 🈸 - -🥇, ✍ 👑, 🔝-🎚, **FastAPI** 🈸, & 🚮 *➡ 🛠️*: - -{* ../../docs_src/sub_applications/tutorial001.py hl[3,6:8] *} - -### 🎧-🈸 - -⤴️, ✍ 👆 🎧-🈸, & 🚮 *➡ 🛠️*. - -👉 🎧-🈸 ➕1️⃣ 🐩 FastAPI 🈸, ✋️ 👉 1️⃣ 👈 🔜 "🗻": - -{* ../../docs_src/sub_applications/tutorial001.py hl[11,14:16] *} - -### 🗻 🎧-🈸 - -👆 🔝-🎚 🈸, `app`, 🗻 🎧-🈸, `subapi`. - -👉 💼, ⚫️ 🔜 📌 ➡ `/subapi`: - -{* ../../docs_src/sub_applications/tutorial001.py hl[11,19] *} - -### ✅ 🏧 🛠️ 🩺 - -🔜, 🏃 `uvicorn` ⏮️ 👑 📱, 🚥 👆 📁 `main.py`, ⚫️ 🔜: - -
- -```console -$ uvicorn main:app --reload - -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -``` - -
- -& 📂 🩺 http://127.0.0.1:8000/docs. - -👆 🔜 👀 🏧 🛠️ 🩺 👑 📱, 🔌 🕴 🚮 👍 _➡ 🛠️_: - - - -& ⤴️, 📂 🩺 🎧-🈸, http://127.0.0.1:8000/subapi/docs. - -👆 🔜 👀 🏧 🛠️ 🩺 🎧-🈸, ✅ 🕴 🚮 👍 _➡ 🛠️_, 🌐 🔽 ☑ 🎧-➡ 🔡 `/subapi`: - - - -🚥 👆 🔄 🔗 ⏮️ 🙆 2️⃣ 👩‍💻 🔢, 👫 🔜 👷 ☑, ↩️ 🖥 🔜 💪 💬 🔠 🎯 📱 ⚖️ 🎧-📱. - -### 📡 ℹ: `root_path` - -🕐❔ 👆 🗻 🎧-🈸 🔬 🔛, FastAPI 🔜 ✊ 💅 🔗 🗻 ➡ 🎧-🈸 ⚙️ 🛠️ ⚪️➡️ 🔫 🔧 🤙 `root_path`. - -👈 🌌, 🎧-🈸 🔜 💭 ⚙️ 👈 ➡ 🔡 🩺 🎚. - -& 🎧-🈸 💪 ✔️ 🚮 👍 📌 🎧-🈸 & 🌐 🔜 👷 ☑, ↩️ FastAPI 🍵 🌐 👉 `root_path`Ⓜ 🔁. - -👆 🔜 💡 🌅 🔃 `root_path` & ❔ ⚙️ ⚫️ 🎯 📄 🔃 [⛅ 🗳](behind-a-proxy.md){.internal-link target=_blank}. diff --git a/docs/em/docs/advanced/templates.md b/docs/em/docs/advanced/templates.md deleted file mode 100644 index 2e8f562280..0000000000 --- a/docs/em/docs/advanced/templates.md +++ /dev/null @@ -1,84 +0,0 @@ -# 📄 - -👆 💪 ⚙️ 🙆 📄 🚒 👆 💚 ⏮️ **FastAPI**. - -⚠ ⚒ Jinja2️⃣, 🎏 1️⃣ ⚙️ 🏺 & 🎏 🧰. - -📤 🚙 🔗 ⚫️ 💪 👈 👆 💪 ⚙️ 🔗 👆 **FastAPI** 🈸 (🚚 💃). - -## ❎ 🔗 - -❎ `jinja2`: - -
- -```console -$ pip install jinja2 - ----> 100% -``` - -
- -## ⚙️ `Jinja2Templates` - -* 🗄 `Jinja2Templates`. -* ✍ `templates` 🎚 👈 👆 💪 🏤-⚙️ ⏪. -* 📣 `Request` 🔢 *➡ 🛠️* 👈 🔜 📨 📄. -* ⚙️ `templates` 👆 ✍ ✍ & 📨 `TemplateResponse`, 🚶‍♀️ `request` 1️⃣ 🔑-💲 👫 Jinja2️⃣ "🔑". - -{* ../../docs_src/templates/tutorial001.py hl[4,11,15:18] *} - -/// note - -👀 👈 👆 ✔️ 🚶‍♀️ `request` 🍕 🔑-💲 👫 🔑 Jinja2️⃣. , 👆 ✔️ 📣 ⚫️ 👆 *➡ 🛠️*. - -/// - -/// tip - -📣 `response_class=HTMLResponse` 🩺 🎚 🔜 💪 💭 👈 📨 🔜 🕸. - -/// - -/// note | 📡 ℹ - -👆 💪 ⚙️ `from starlette.templating import Jinja2Templates`. - -**FastAPI** 🚚 🎏 `starlette.templating` `fastapi.templating` 🏪 👆, 👩‍💻. ✋️ 🌅 💪 📨 👟 🔗 ⚪️➡️ 💃. 🎏 ⏮️ `Request` & `StaticFiles`. - -/// - -## ✍ 📄 - -⤴️ 👆 💪 ✍ 📄 `templates/item.html` ⏮️: - -```jinja hl_lines="7" -{!../../docs_src/templates/templates/item.html!} -``` - -⚫️ 🔜 🎦 `id` ✊ ⚪️➡️ "🔑" `dict` 👆 🚶‍♀️: - -```Python -{"request": request, "id": id} -``` - -## 📄 & 🎻 📁 - -& 👆 💪 ⚙️ `url_for()` 🔘 📄, & ⚙️ ⚫️, 🖼, ⏮️ `StaticFiles` 👆 📌. - -```jinja hl_lines="4" -{!../../docs_src/templates/templates/item.html!} -``` - -👉 🖼, ⚫️ 🔜 🔗 🎚 📁 `static/styles.css` ⏮️: - -```CSS hl_lines="4" -{!../../docs_src/templates/static/styles.css!} -``` - -& ↩️ 👆 ⚙️ `StaticFiles`, 👈 🎚 📁 🔜 🍦 🔁 👆 **FastAPI** 🈸 📛 `/static/styles.css`. - -## 🌅 ℹ - -🌅 ℹ, 🔌 ❔ 💯 📄, ✅ 💃 🩺 🔛 📄. diff --git a/docs/em/docs/advanced/testing-dependencies.md b/docs/em/docs/advanced/testing-dependencies.md deleted file mode 100644 index b2b4b480d0..0000000000 --- a/docs/em/docs/advanced/testing-dependencies.md +++ /dev/null @@ -1,53 +0,0 @@ -# 🔬 🔗 ⏮️ 🔐 - -## 🔑 🔗 ⏮️ 🔬 - -📤 😐 🌐❔ 👆 💪 💚 🔐 🔗 ⏮️ 🔬. - -👆 🚫 💚 ⏮️ 🔗 🏃 (🚫 🙆 🎧-🔗 ⚫️ 💪 ✔️). - -↩️, 👆 💚 🚚 🎏 🔗 👈 🔜 ⚙️ 🕴 ⏮️ 💯 (🎲 🕴 🎯 💯), & 🔜 🚚 💲 👈 💪 ⚙️ 🌐❔ 💲 ⏮️ 🔗 ⚙️. - -### ⚙️ 💼: 🔢 🐕‍🦺 - -🖼 💪 👈 👆 ✔️ 🔢 🤝 🐕‍🦺 👈 👆 💪 🤙. - -👆 📨 ⚫️ 🤝 & ⚫️ 📨 🔓 👩‍💻. - -👉 🐕‍🦺 5️⃣📆 🔌 👆 📍 📨, & 🤙 ⚫️ 💪 ✊ ➕ 🕰 🌘 🚥 👆 ✔️ 🔧 🎁 👩‍💻 💯. - -👆 🎲 💚 💯 🔢 🐕‍🦺 🕐, ✋️ 🚫 🎯 🤙 ⚫️ 🔠 💯 👈 🏃. - -👉 💼, 👆 💪 🔐 🔗 👈 🤙 👈 🐕‍🦺, & ⚙️ 🛃 🔗 👈 📨 🎁 👩‍💻, 🕴 👆 💯. - -### ⚙️ `app.dependency_overrides` 🔢 - -👫 💼, 👆 **FastAPI** 🈸 ✔️ 🔢 `app.dependency_overrides`, ⚫️ 🙅 `dict`. - -🔐 🔗 🔬, 👆 🚮 🔑 ⏮️ 🔗 (🔢), & 💲, 👆 🔗 🔐 (➕1️⃣ 🔢). - -& ⤴️ **FastAPI** 🔜 🤙 👈 🔐 ↩️ ⏮️ 🔗. - -{* ../../docs_src/dependency_testing/tutorial001.py hl[28:29,32] *} - -/// tip - -👆 💪 ⚒ 🔗 🔐 🔗 ⚙️ 🙆 👆 **FastAPI** 🈸. - -⏮️ 🔗 💪 ⚙️ *➡ 🛠️ 🔢*, *➡ 🛠️ 👨‍🎨* (🕐❔ 👆 🚫 ⚙️ 📨 💲), `.include_router()` 🤙, ♒️. - -FastAPI 🔜 💪 🔐 ⚫️. - -/// - -⤴️ 👆 💪 ⏲ 👆 🔐 (❎ 👫) ⚒ `app.dependency_overrides` 🛁 `dict`: - -```Python -app.dependency_overrides = {} -``` - -/// tip - -🚥 👆 💚 🔐 🔗 🕴 ⏮️ 💯, 👆 💪 ⚒ 🔐 ▶️ 💯 (🔘 💯 🔢) & ⏲ ⚫️ 🔚 (🔚 💯 🔢). - -/// diff --git a/docs/em/docs/advanced/testing-events.md b/docs/em/docs/advanced/testing-events.md deleted file mode 100644 index f62e9e0694..0000000000 --- a/docs/em/docs/advanced/testing-events.md +++ /dev/null @@ -1,5 +0,0 @@ -# 🔬 🎉: 🕴 - 🤫 - -🕐❔ 👆 💪 👆 🎉 🐕‍🦺 (`startup` & `shutdown`) 🏃 👆 💯, 👆 💪 ⚙️ `TestClient` ⏮️ `with` 📄: - -{* ../../docs_src/app_testing/tutorial003.py hl[9:12,20:24] *} diff --git a/docs/em/docs/advanced/testing-websockets.md b/docs/em/docs/advanced/testing-websockets.md deleted file mode 100644 index 96aa8b765d..0000000000 --- a/docs/em/docs/advanced/testing-websockets.md +++ /dev/null @@ -1,13 +0,0 @@ -# 🔬 *️⃣ - -👆 💪 ⚙️ 🎏 `TestClient` 💯*️⃣. - -👉, 👆 ⚙️ `TestClient` `with` 📄, 🔗*️⃣: - -{* ../../docs_src/app_testing/tutorial002.py hl[27:31] *} - -/// note - -🌅 ℹ, ✅ 💃 🧾 🔬 *️⃣ . - -/// diff --git a/docs/em/docs/advanced/using-request-directly.md b/docs/em/docs/advanced/using-request-directly.md deleted file mode 100644 index 238557e5e0..0000000000 --- a/docs/em/docs/advanced/using-request-directly.md +++ /dev/null @@ -1,56 +0,0 @@ -# ⚙️ 📨 🔗 - -🆙 🔜, 👆 ✔️ 📣 🍕 📨 👈 👆 💪 ⏮️ 👫 🆎. - -✊ 📊 ⚪️➡️: - -* ➡ 🔢. -* 🎚. -* 🍪. -* ♒️. - -& 🔨, **FastAPI** ⚖ 👈 💽, 🏭 ⚫️ & 🏭 🧾 👆 🛠️ 🔁. - -✋️ 📤 ⚠ 🌐❔ 👆 💪 💪 🔐 `Request` 🎚 🔗. - -## ℹ 🔃 `Request` 🎚 - -**FastAPI** 🤙 **💃** 🔘, ⏮️ 🧽 📚 🧰 🔛 🔝, 👆 💪 ⚙️ 💃 `Request` 🎚 🔗 🕐❔ 👆 💪. - -⚫️ 🔜 ⛓ 👈 🚥 👆 🤚 📊 ⚪️➡️ `Request` 🎚 🔗 (🖼, ✍ 💪) ⚫️ 🏆 🚫 ✔, 🗜 ⚖️ 📄 (⏮️ 🗄, 🏧 🛠️ 👩‍💻 🔢) FastAPI. - -👐 🙆 🎏 🔢 📣 🛎 (🖼, 💪 ⏮️ Pydantic 🏷) 🔜 ✔, 🗜, ✍, ♒️. - -✋️ 📤 🎯 💼 🌐❔ ⚫️ ⚠ 🤚 `Request` 🎚. - -## ⚙️ `Request` 🎚 🔗 - -➡️ 🌈 👆 💚 🤚 👩‍💻 📢 📢/🦠 🔘 👆 *➡ 🛠️ 🔢*. - -👈 👆 💪 🔐 📨 🔗. - -{* ../../docs_src/using_request_directly/tutorial001.py hl[1,7:8] *} - -📣 *➡ 🛠️ 🔢* 🔢 ⏮️ 🆎 ➖ `Request` **FastAPI** 🔜 💭 🚶‍♀️ `Request` 👈 🔢. - -/// tip - -🗒 👈 👉 💼, 👥 📣 ➡ 🔢 ⤴️ 📨 🔢. - -, ➡ 🔢 🔜 ⚗, ✔, 🗜 ✔ 🆎 & ✍ ⏮️ 🗄. - -🎏 🌌, 👆 💪 📣 🙆 🎏 🔢 🛎, & ➡, 🤚 `Request` 💁‍♂️. - -/// - -## `Request` 🧾 - -👆 💪 ✍ 🌅 ℹ 🔃 `Request` 🎚 🛂 💃 🧾 🕸. - -/// note | 📡 ℹ - -👆 💪 ⚙️ `from starlette.requests import Request`. - -**FastAPI** 🚚 ⚫️ 🔗 🏪 👆, 👩‍💻. ✋️ ⚫️ 👟 🔗 ⚪️➡️ 💃. - -/// diff --git a/docs/em/docs/advanced/websockets.md b/docs/em/docs/advanced/websockets.md deleted file mode 100644 index a097778c7a..0000000000 --- a/docs/em/docs/advanced/websockets.md +++ /dev/null @@ -1,186 +0,0 @@ -# *️⃣ - -👆 💪 ⚙️ *️⃣ ⏮️ **FastAPI**. - -## ❎ `WebSockets` - -🥇 👆 💪 ❎ `WebSockets`: - -
- -```console -$ pip install websockets - ----> 100% -``` - -
- -## *️⃣ 👩‍💻 - -### 🏭 - -👆 🏭 ⚙️, 👆 🎲 ✔️ 🕸 ✍ ⏮️ 🏛 🛠️ 💖 😥, Vue.js ⚖️ 📐. - -& 🔗 ⚙️ *️⃣ ⏮️ 👆 👩‍💻 👆 🔜 🎲 ⚙️ 👆 🕸 🚙. - -⚖️ 👆 💪 ✔️ 🇦🇸 📱 🈸 👈 🔗 ⏮️ 👆 *️⃣ 👩‍💻 🔗, 🇦🇸 📟. - -⚖️ 👆 5️⃣📆 ✔️ 🙆 🎏 🌌 🔗 ⏮️ *️⃣ 🔗. - ---- - -✋️ 👉 🖼, 👥 🔜 ⚙️ 📶 🙅 🕸 📄 ⏮️ 🕸, 🌐 🔘 📏 🎻. - -👉, ↗️, 🚫 ⚖ & 👆 🚫🔜 ⚙️ ⚫️ 🏭. - -🏭 👆 🔜 ✔️ 1️⃣ 🎛 🔛. - -✋️ ⚫️ 🙅 🌌 🎯 🔛 💽-🚄 *️⃣ & ✔️ 👷 🖼: - -{* ../../docs_src/websockets/tutorial001.py hl[2,6:38,41:43] *} - -## ✍ `websocket` - -👆 **FastAPI** 🈸, ✍ `websocket`: - -{* ../../docs_src/websockets/tutorial001.py hl[1,46:47] *} - -/// note | 📡 ℹ - -👆 💪 ⚙️ `from starlette.websockets import WebSocket`. - -**FastAPI** 🚚 🎏 `WebSocket` 🔗 🏪 👆, 👩‍💻. ✋️ ⚫️ 👟 🔗 ⚪️➡️ 💃. - -/// - -## ⌛ 📧 & 📨 📧 - -👆 *️⃣ 🛣 👆 💪 `await` 📧 & 📨 📧. - -{* ../../docs_src/websockets/tutorial001.py hl[48:52] *} - -👆 💪 📨 & 📨 💱, ✍, & 🎻 💽. - -## 🔄 ⚫️ - -🚥 👆 📁 📛 `main.py`, 🏃 👆 🈸 ⏮️: - -
- -```console -$ uvicorn main:app --reload - -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -``` - -
- -📂 👆 🖥 http://127.0.0.1:8000. - -👆 🔜 👀 🙅 📃 💖: - - - -👆 💪 🆎 📧 🔢 📦, & 📨 👫: - - - -& 👆 **FastAPI** 🈸 ⏮️ *️⃣ 🔜 📨 🔙: - - - -👆 💪 📨 (& 📨) 📚 📧: - - - -& 🌐 👫 🔜 ⚙️ 🎏 *️⃣ 🔗. - -## ⚙️ `Depends` & 🎏 - -*️⃣ 🔗 👆 💪 🗄 ⚪️➡️ `fastapi` & ⚙️: - -* `Depends` -* `Security` -* `Cookie` -* `Header` -* `Path` -* `Query` - -👫 👷 🎏 🌌 🎏 FastAPI 🔗/*➡ 🛠️*: - -{* ../../docs_src/websockets/tutorial002.py hl[66:77,76:91] *} - -/// info - -👉 *️⃣ ⚫️ 🚫 🤙 ⚒ 🔑 🤚 `HTTPException`, ↩️ 👥 🤚 `WebSocketException`. - -👆 💪 ⚙️ 📪 📟 ⚪️➡️ ☑ 📟 🔬 🔧. - -/// - -### 🔄 *️⃣ ⏮️ 🔗 - -🚥 👆 📁 📛 `main.py`, 🏃 👆 🈸 ⏮️: - -
- -```console -$ uvicorn main:app --reload - -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -``` - -
- -📂 👆 🖥 http://127.0.0.1:8000. - -📤 👆 💪 ⚒: - -* "🏬 🆔", ⚙️ ➡. -* "🤝" ⚙️ 🔢 🔢. - -/// tip - -👀 👈 🔢 `token` 🔜 🍵 🔗. - -/// - -⏮️ 👈 👆 💪 🔗 *️⃣ & ⤴️ 📨 & 📨 📧: - - - -## 🚚 🔀 & 💗 👩‍💻 - -🕐❔ *️⃣ 🔗 📪, `await websocket.receive_text()` 🔜 🤚 `WebSocketDisconnect` ⚠, ❔ 👆 💪 ⤴️ ✊ & 🍵 💖 👉 🖼. - -{* ../../docs_src/websockets/tutorial003.py hl[81:83] *} - -🔄 ⚫️ 👅: - -* 📂 📱 ⏮️ 📚 🖥 📑. -* ✍ 📧 ⚪️➡️ 👫. -* ⤴️ 🔐 1️⃣ 📑. - -👈 🔜 🤚 `WebSocketDisconnect` ⚠, & 🌐 🎏 👩‍💻 🔜 📨 📧 💖: - -``` -Client #1596980209979 left the chat -``` - -/// tip - -📱 🔛 ⭐ & 🙅 🖼 🎦 ❔ 🍵 & 📻 📧 📚 *️⃣ 🔗. - -✋️ ✔️ 🤯 👈, 🌐 🍵 💾, 👁 📇, ⚫️ 🔜 🕴 👷 ⏪ 🛠️ 🏃, & 🔜 🕴 👷 ⏮️ 👁 🛠️. - -🚥 👆 💪 🕳 ⏩ 🛠️ ⏮️ FastAPI ✋️ 👈 🌖 🏋️, 🐕‍🦺 ✳, ✳ ⚖️ 🎏, ✅ 🗜/📻. - -/// - -## 🌅 ℹ - -💡 🌅 🔃 🎛, ✅ 💃 🧾: - -* `WebSocket` 🎓. -* 🎓-⚓️ *️⃣ 🚚. diff --git a/docs/em/docs/advanced/wsgi.md b/docs/em/docs/advanced/wsgi.md deleted file mode 100644 index d923347d5a..0000000000 --- a/docs/em/docs/advanced/wsgi.md +++ /dev/null @@ -1,35 +0,0 @@ -# ✅ 🇨🇻 - 🏺, ✳, 🎏 - -👆 💪 🗻 🇨🇻 🈸 👆 👀 ⏮️ [🎧 🈸 - 🗻](sub-applications.md){.internal-link target=_blank}, [⛅ 🗳](behind-a-proxy.md){.internal-link target=_blank}. - -👈, 👆 💪 ⚙️ `WSGIMiddleware` & ⚙️ ⚫️ 🎁 👆 🇨🇻 🈸, 🖼, 🏺, ✳, ♒️. - -## ⚙️ `WSGIMiddleware` - -👆 💪 🗄 `WSGIMiddleware`. - -⤴️ 🎁 🇨🇻 (✅ 🏺) 📱 ⏮️ 🛠️. - -& ⤴️ 🗻 👈 🔽 ➡. - -{* ../../docs_src/wsgi/tutorial001.py hl[2:3,22] *} - -## ✅ ⚫️ - -🔜, 🔠 📨 🔽 ➡ `/v1/` 🔜 🍵 🏺 🈸. - -& 🎂 🔜 🍵 **FastAPI**. - -🚥 👆 🏃 ⚫️ ⏮️ Uvicorn & 🚶 http://localhost:8000/v1/ 👆 🔜 👀 📨 ⚪️➡️ 🏺: - -```txt -Hello, World from Flask! -``` - -& 🚥 👆 🚶 http://localhost:8000/v2 👆 🔜 👀 📨 ⚪️➡️ FastAPI: - -```JSON -{ - "message": "Hello World" -} -``` diff --git a/docs/em/docs/alternatives.md b/docs/em/docs/alternatives.md deleted file mode 100644 index 4cbac7539f..0000000000 --- a/docs/em/docs/alternatives.md +++ /dev/null @@ -1,485 +0,0 @@ -# 🎛, 🌈 & 🔺 - -⚫️❔ 😮 **FastAPI**, ❔ ⚫️ 🔬 🎏 🎛 & ⚫️❔ ⚫️ 🇭🇲 ⚪️➡️ 👫. - -## 🎶 - -**FastAPI** 🚫🔜 🔀 🚥 🚫 ⏮️ 👷 🎏. - -📤 ✔️ 📚 🧰 ✍ ⏭ 👈 ✔️ ℹ 😮 🚮 🏗. - -👤 ✔️ ❎ 🏗 🆕 🛠️ 📚 1️⃣2️⃣🗓️. 🥇 👤 🔄 ❎ 🌐 ⚒ 📔 **FastAPI** ⚙️ 📚 🎏 🛠️, 🔌-🔌, & 🧰. - -✋️ ☝, 📤 🙅‍♂ 🎏 🎛 🌘 🏗 🕳 👈 🚚 🌐 👫 ⚒, ✊ 🏆 💭 ⚪️➡️ ⏮️ 🧰, & 🌀 👫 🏆 🌌 💪, ⚙️ 🇪🇸 ⚒ 👈 ➖🚫 💪 ⏭ (🐍 3️⃣.6️⃣ ➕ 🆎 🔑). - -## ⏮️ 🧰 - -### - -⚫️ 🌅 🌟 🐍 🛠️ & 🛎 🕴. ⚫️ ⚙️ 🏗 ⚙️ 💖 👱📔. - -⚫️ 📶 😆 🔗 ⏮️ 🔗 💽 (💖 ✳ ⚖️ ✳),, ✔️ ☁ 💽 (💖 🗄, ✳, 👸, ♒️) 👑 🏪 🚒 🚫 📶 ⏩. - -⚫️ ✍ 🏗 🕸 👩‍💻, 🚫 ✍ 🔗 ⚙️ 🏛 🕸 (💖 😥, Vue.js & 📐) ⚖️ 🎏 ⚙️ (💖 📳) 🔗 ⏮️ ⚫️. - -### ✳ 🎂 🛠️ - -✳ 🎂 🛠️ ✍ 🗜 🧰 🏗 🕸 🔗 ⚙️ ✳ 🔘, 📉 🚮 🛠️ 🛠️. - -⚫️ ⚙️ 📚 🏢 ✅ 🦎, 🟥 👒 & 🎟. - -⚫️ 🕐 🥇 🖼 **🏧 🛠️ 🧾**, & 👉 🎯 🕐 🥇 💭 👈 😮 "🔎" **FastAPI**. - -/// note - -✳ 🎂 🛠️ ✍ ✡ 🇺🇸🏛. 🎏 👼 💃 & Uvicorn, 🔛 ❔ **FastAPI** ⚓️. - -/// - -/// check | 😮 **FastAPI** - -✔️ 🏧 🛠️ 🧾 🕸 👩‍💻 🔢. - -/// - -### 🏺 - -🏺 "🕸", ⚫️ 🚫 🔌 💽 🛠️ 🚫 📚 👜 👈 👟 🔢 ✳. - -👉 🦁 & 💪 ✔ 🔨 👜 💖 ⚙️ ☁ 💽 👑 💽 💾 ⚙️. - -⚫️ 📶 🙅, ⚫️ 📶 🏋️ 💡, 👐 🧾 🤚 🙁 📡 ☝. - -⚫️ 🛎 ⚙️ 🎏 🈸 👈 🚫 🎯 💪 💽, 👩‍💻 🧾, ⚖️ 🙆 📚 ⚒ 👈 👟 🏤-🏗 ✳. 👐 📚 👫 ⚒ 💪 🚮 ⏮️ 🔌-🔌. - -👉 ⚖ 🍕, & ➖ "🕸" 👈 💪 ↔ 📔 ⚫️❔ ⚫️❔ 💪 🔑 ⚒ 👈 👤 💚 🚧. - -👐 🦁 🏺, ⚫️ 😑 💖 👍 🏏 🏗 🔗. ⏭ 👜 🔎 "✳ 🎂 🛠️" 🏺. - -/// check | 😮 **FastAPI** - -◾-🛠️. ⚒ ⚫️ ⏩ 🌀 & 🏏 🧰 & 🍕 💪. - -✔️ 🙅 & ⏩ ⚙️ 🕹 ⚙️. - -/// - -### 📨 - -**FastAPI** 🚫 🤙 🎛 **📨**. 👫 ↔ 📶 🎏. - -⚫️ 🔜 🤙 ⚠ ⚙️ 📨 *🔘* FastAPI 🈸. - -✋️, FastAPI 🤚 🌈 ⚪️➡️ 📨. - -**📨** 🗃 *🔗* ⏮️ 🔗 (👩‍💻), ⏪ **FastAPI** 🗃 *🏗* 🔗 (💽). - -👫, 🌖 ⚖️ 🌘, 🔄 🔚, 🔗 🔠 🎏. - -📨 ✔️ 📶 🙅 & 🏋️ 🔧, ⚫️ 📶 ⏩ ⚙️, ⏮️ 🤔 🔢. ✋️ 🎏 🕰, ⚫️ 📶 🏋️ & 🛃. - -👈 ⚫️❔, 💬 🛂 🕸: - -> 📨 1️⃣ 🏆 ⏬ 🐍 📦 🌐 🕰 - -🌌 👆 ⚙️ ⚫️ 📶 🙅. 🖼, `GET` 📨, 👆 🔜 ✍: - -```Python -response = requests.get("http://example.com/some/url") -``` - -FastAPI 😑 🛠️ *➡ 🛠️* 💪 👀 💖: - -```Python hl_lines="1" -@app.get("/some/url") -def read_url(): - return {"message": "Hello World"} -``` - -👀 🔀 `requests.get(...)` & `@app.get(...)`. - -/// check | 😮 **FastAPI** - -* ✔️ 🙅 & 🏋️ 🛠️. -* ⚙️ 🇺🇸🔍 👩‍🔬 📛 (🛠️) 🔗, 🎯 & 🏋️ 🌌. -* ✔️ 🤔 🔢, ✋️ 🏋️ 🛃. - -/// - -### 🦁 / 🗄 - -👑 ⚒ 👤 💚 ⚪️➡️ ✳ 🎂 🛠️ 🏧 🛠️ 🧾. - -⤴️ 👤 🔎 👈 📤 🐩 📄 🔗, ⚙️ 🎻 (⚖️ 📁, ↔ 🎻) 🤙 🦁. - -& 📤 🕸 👩‍💻 🔢 🦁 🛠️ ⏪ ✍. , 💆‍♂ 💪 🏗 🦁 🧾 🛠️ 🔜 ✔ ⚙️ 👉 🕸 👩‍💻 🔢 🔁. - -☝, 🦁 👐 💾 🏛, 📁 🗄. - -👈 ⚫️❔ 🕐❔ 💬 🔃 ⏬ 2️⃣.0️⃣ ⚫️ ⚠ 💬 "🦁", & ⏬ 3️⃣ ➕ "🗄". - -/// check | 😮 **FastAPI** - -🛠️ & ⚙️ 📂 🐩 🛠️ 🔧, ↩️ 🛃 🔗. - - & 🛠️ 🐩-⚓️ 👩‍💻 🔢 🧰: - -* 🦁 🎚 -* 📄 - -👫 2️⃣ 👐 ➖ 📶 🌟 & ⚖, ✋️ 🔨 ⏩ 🔎, 👆 💪 🔎 💯 🌖 🎛 👩‍💻 🔢 🗄 (👈 👆 💪 ⚙️ ⏮️ **FastAPI**). - -/// - -### 🏺 🎂 🛠️ - -📤 📚 🏺 🎂 🛠️, ✋️ ⏮️ 💰 🕰 & 👷 🔘 🔬 👫, 👤 🔎 👈 📚 😞 ⚖️ 🚫, ⏮️ 📚 🧍 ❔ 👈 ⚒ 👫 🙃. - -### 🍭 - -1️⃣ 👑 ⚒ 💪 🛠️ ⚙️ 📊 "🛠️" ❔ ✊ 📊 ⚪️➡️ 📟 (🐍) & 🏭 ⚫️ 🔘 🕳 👈 💪 📨 🔘 🕸. 🖼, 🏭 🎚 ⚗ 📊 ⚪️➡️ 💽 🔘 🎻 🎚. 🏭 `datetime` 🎚 🔘 🎻, ♒️. - -➕1️⃣ 🦏 ⚒ 💚 🔗 💽 🔬, ⚒ 💭 👈 💽 ☑, 🤝 🎯 🔢. 🖼, 👈 🏑 `int`, & 🚫 🎲 🎻. 👉 ✴️ ⚠ 📨 💽. - -🍵 💽 🔬 ⚙️, 👆 🔜 ✔️ 🌐 ✅ ✋, 📟. - -👫 ⚒ ⚫️❔ 🍭 🏗 🚚. ⚫️ 👑 🗃, & 👤 ✔️ ⚙️ ⚫️ 📚 ⏭. - -✋️ ⚫️ ✍ ⏭ 📤 🔀 🐍 🆎 🔑. , 🔬 🔠 🔗 👆 💪 ⚙️ 🎯 🇨🇻 & 🎓 🚚 🍭. - -/// check | 😮 **FastAPI** - -⚙️ 📟 🔬 "🔗" 👈 🚚 💽 🆎 & 🔬, 🔁. - -/// - -### Webarg - -➕1️⃣ 🦏 ⚒ ✔ 🔗 📊 ⚪️➡️ 📨 📨. - -Webarg 🧰 👈 ⚒ 🚚 👈 🔛 🔝 📚 🛠️, 🔌 🏺. - -⚫️ ⚙️ 🍭 🔘 💽 🔬. & ⚫️ ✍ 🎏 👩‍💻. - -⚫️ 👑 🧰 & 👤 ✔️ ⚙️ ⚫️ 📚 💁‍♂️, ⏭ ✔️ **FastAPI**. - -/// info - -Webarg ✍ 🎏 🍭 👩‍💻. - -/// - -/// check | 😮 **FastAPI** - -✔️ 🏧 🔬 📨 📨 💽. - -/// - -### APISpec - -🍭 & Webarg 🚚 🔬, ✍ & 🛠️ 🔌-🔌. - -✋️ 🧾 ❌. ⤴️ APISpec ✍. - -⚫️ 🔌-📚 🛠️ (& 📤 🔌-💃 💁‍♂️). - -🌌 ⚫️ 👷 👈 👆 ✍ 🔑 🔗 ⚙️ 📁 📁 🔘 #️⃣ 🔠 🔢 🚚 🛣. - -& ⚫️ 🏗 🗄 🔗. - -👈 ❔ ⚫️ 👷 🏺, 💃, 🆘, ♒️. - -✋️ ⤴️, 👥 ✔️ 🔄 ⚠ ✔️ ◾-❕, 🔘 🐍 🎻 (🦏 📁). - -👨‍🎨 💪 🚫 ℹ 🌅 ⏮️ 👈. & 🚥 👥 🔀 🔢 ⚖️ 🍭 🔗 & 💭 🔀 👈 📁#️⃣, 🏗 🔗 🔜 ❌. - -/// info - -APISpec ✍ 🎏 🍭 👩‍💻. - -/// - -/// check | 😮 **FastAPI** - -🐕‍🦺 📂 🐩 🛠️, 🗄. - -/// - -### 🏺-Apispec - -⚫️ 🏺 🔌 -, 👈 👔 👯‍♂️ Webarg, 🍭 & APISpec. - -⚫️ ⚙️ ℹ ⚪️➡️ Webarg & 🍭 🔁 🏗 🗄 🔗, ⚙️ APISpec. - -⚫️ 👑 🧰, 📶 🔽-📈. ⚫️ 🔜 🌌 🌖 🌟 🌘 📚 🏺 🔌-🔌 👅 📤. ⚫️ 💪 ↩️ 🚮 🧾 ➖ 💁‍♂️ 🩲 & 📝. - -👉 ❎ ✔️ ✍ 📁 (➕1️⃣ ❕) 🔘 🐍 ✍. - -👉 🌀 🏺, 🏺-Apispec ⏮️ 🍭 & Webarg 👇 💕 👩‍💻 📚 ⏭ 🏗 **FastAPI**. - -⚙️ ⚫️ ↘️ 🏗 📚 🏺 🌕-📚 🚂. 👫 👑 📚 👤 (& 📚 🔢 🏉) ✔️ ⚙️ 🆙 🔜: - -* https://github.com/tiangolo/full-stack -* https://github.com/tiangolo/full-stack-flask-couchbase -* https://github.com/tiangolo/full-stack-flask-couchdb - -& 👫 🎏 🌕-📚 🚂 🧢 [**FastAPI** 🏗 🚂](project-generation.md){.internal-link target=_blank}. - -/// info - -🏺-Apispec ✍ 🎏 🍭 👩‍💻. - -/// - -/// check | 😮 **FastAPI** - -🏗 🗄 🔗 🔁, ⚪️➡️ 🎏 📟 👈 🔬 🛠️ & 🔬. - -/// - -### NestJS (& 📐) - -👉 ➖🚫 🚫 🐍, NestJS 🕸 (📕) ✳ 🛠️ 😮 📐. - -⚫️ 🏆 🕳 🙁 🎏 ⚫️❔ 💪 🔨 ⏮️ 🏺-Apispec. - -⚫️ ✔️ 🛠️ 🔗 💉 ⚙️, 😮 📐 2️⃣. ⚫️ 🚚 🏤-® "💉" (💖 🌐 🎏 🔗 💉 ⚙️ 👤 💭),, ⚫️ 🚮 🎭 & 📟 🔁. - -🔢 🔬 ⏮️ 📕 🆎 (🎏 🐍 🆎 🔑), 👨‍🎨 🐕‍🦺 👍. - -✋️ 📕 📊 🚫 🛡 ⏮️ 📹 🕸, ⚫️ 🚫🔜 ⚓️ 🔛 🆎 🔬 🔬, 🛠️ & 🧾 🎏 🕰. ↩️ 👉 & 🔧 🚫, 🤚 🔬, 🛠️ & 🏧 🔗 ⚡, ⚫️ 💪 🚮 👨‍🎨 📚 🥉. , ⚫️ ▶️️ 🔁. - -⚫️ 💪 🚫 🍵 🔁 🏷 📶 👍. , 🚥 🎻 💪 📨 🎻 🎚 👈 ✔️ 🔘 🏑 👈 🔄 🐦 🎻 🎚, ⚫️ 🚫🔜 ☑ 📄 & ✔. - -/// check | 😮 **FastAPI** - -⚙️ 🐍 🆎 ✔️ 👑 👨‍🎨 🐕‍🦺. - -✔️ 🏋️ 🔗 💉 ⚙️. 🔎 🌌 📉 📟 🔁. - -/// - -### 🤣 - -⚫️ 🕐 🥇 📶 ⏩ 🐍 🛠️ ⚓️ 🔛 `asyncio`. ⚫️ ⚒ 📶 🎏 🏺. - -/// note | 📡 ℹ - -⚫️ ⚙️ `uvloop` ↩️ 🔢 🐍 `asyncio` ➰. 👈 ⚫️❔ ⚒ ⚫️ ⏩. - -⚫️ 🎯 😮 Uvicorn & 💃, 👈 ⏳ ⏩ 🌘 🤣 📂 📇. - -/// - -/// check | 😮 **FastAPI** - -🔎 🌌 ✔️ 😜 🎭. - -👈 ⚫️❔ **FastAPI** ⚓️ 🔛 💃, ⚫️ ⏩ 🛠️ 💪 (💯 🥉-🥳 📇). - -/// - -### 🦅 - -🦅 ➕1️⃣ ↕ 🎭 🐍 🛠️, ⚫️ 🔧 ⭐, & 👷 🏛 🎏 🛠️ 💖 🤗. - -⚫️ 🏗 ✔️ 🔢 👈 📨 2️⃣ 🔢, 1️⃣ "📨" & 1️⃣ "📨". ⤴️ 👆 "✍" 🍕 ⚪️➡️ 📨, & "✍" 🍕 📨. ↩️ 👉 🔧, ⚫️ 🚫 💪 📣 📨 🔢 & 💪 ⏮️ 🐩 🐍 🆎 🔑 🔢 🔢. - -, 💽 🔬, 🛠️, & 🧾, ✔️ ⌛ 📟, 🚫 🔁. ⚖️ 👫 ✔️ 🛠️ 🛠️ 🔛 🔝 🦅, 💖 🤗. 👉 🎏 🔺 🔨 🎏 🛠️ 👈 😮 🦅 🔧, ✔️ 1️⃣ 📨 🎚 & 1️⃣ 📨 🎚 🔢. - -/// check | 😮 **FastAPI** - -🔎 🌌 🤚 👑 🎭. - -⤴️ ⏮️ 🤗 (🤗 ⚓️ 🔛 🦅) 😮 **FastAPI** 📣 `response` 🔢 🔢. - -👐 FastAPI ⚫️ 📦, & ⚙️ ✴️ ⚒ 🎚, 🍪, & 🎛 👔 📟. - -/// - -### - -👤 🔎 ♨ 🥇 ▶️ 🏗 **FastAPI**. & ⚫️ ✔️ 🎏 💭: - -* ⚓️ 🔛 🐍 🆎 🔑. -* 🔬 & 🧾 ⚪️➡️ 👫 🆎. -* 🔗 💉 ⚙️. - -⚫️ 🚫 ⚙️ 💽 🔬, 🛠️ & 🧾 🥉-🥳 🗃 💖 Pydantic, ⚫️ ✔️ 🚮 👍. , 👫 💽 🆎 🔑 🔜 🚫 ♻ 💪. - -⚫️ 🚚 🐥 🍖 🌅 🔁 📳. & ⚫️ ⚓️ 🔛 🇨🇻 (↩️ 🔫), ⚫️ 🚫 🔧 ✊ 📈 ↕-🎭 🚚 🧰 💖 Uvicorn, 💃 & 🤣. - -🔗 💉 ⚙️ 🚚 🏤-® 🔗 & 🔗 ❎ 🧢 🔛 📣 🆎. , ⚫️ 🚫 💪 📣 🌅 🌘 1️⃣ "🦲" 👈 🚚 🎯 🆎. - -🛣 📣 👁 🥉, ⚙️ 🔢 📣 🎏 🥉 (↩️ ⚙️ 👨‍🎨 👈 💪 🥉 ▶️️ 🔛 🔝 🔢 👈 🍵 🔗). 👉 🔐 ❔ ✳ 🔨 ⚫️ 🌘 ❔ 🏺 (& 💃) 🔨 ⚫️. ⚫️ 🎏 📟 👜 👈 📶 😆 🔗. - -/// check | 😮 **FastAPI** - -🔬 ➕ 🔬 💽 🆎 ⚙️ "🔢" 💲 🏷 🔢. 👉 📉 👨‍🎨 🐕‍🦺, & ⚫️ 🚫 💪 Pydantic ⏭. - -👉 🤙 😮 🛠️ 🍕 Pydantic, 🐕‍🦺 🎏 🔬 📄 👗 (🌐 👉 🛠️ 🔜 ⏪ 💪 Pydantic). - -/// - -### 🤗 - -🤗 🕐 🥇 🛠️ 🛠️ 📄 🛠️ 🔢 🆎 ⚙️ 🐍 🆎 🔑. 👉 👑 💭 👈 😮 🎏 🧰 🎏. - -⚫️ ⚙️ 🛃 🆎 🚮 📄 ↩️ 🐩 🐍 🆎, ✋️ ⚫️ 🦏 🔁 ⏩. - -⚫️ 🕐 🥇 🛠️ 🏗 🛃 🔗 📣 🎂 🛠️ 🎻. - -⚫️ 🚫 ⚓️ 🔛 🐩 💖 🗄 & 🎻 🔗. ⚫️ 🚫🔜 🎯 🛠️ ⚫️ ⏮️ 🎏 🧰, 💖 🦁 🎚. ✋️ 🔄, ⚫️ 📶 💡 💭. - -⚫️ ✔️ 😌, ⭐ ⚒: ⚙️ 🎏 🛠️, ⚫️ 💪 ✍ 🔗 & 🇳🇨. - -⚫️ ⚓️ 🔛 ⏮️ 🐩 🔁 🐍 🕸 🛠️ (🇨🇻), ⚫️ 💪 🚫 🍵 *️⃣ & 🎏 👜, 👐 ⚫️ ✔️ ↕ 🎭 💁‍♂️. - -/// info - -🤗 ✍ ✡ 🗄, 🎏 👼 `isort`, 👑 🧰 🔁 😇 🗄 🐍 📁. - -/// - -/// check | 💭 😮 **FastAPI** - -🤗 😮 🍕 APIStar, & 1️⃣ 🧰 👤 🔎 🏆 👍, 🌟 APIStar. - -🤗 ℹ 😍 **FastAPI** ⚙️ 🐍 🆎 🔑 📣 🔢, & 🏗 🔗 ⚖ 🛠️ 🔁. - -🤗 😮 **FastAPI** 📣 `response` 🔢 🔢 ⚒ 🎚 & 🍪. - -/// - -### APIStar (<= 0️⃣.5️⃣) - -▶️️ ⏭ 🤔 🏗 **FastAPI** 👤 🔎 **APIStar** 💽. ⚫️ ✔️ 🌖 🌐 👤 👀 & ✔️ 👑 🔧. - -⚫️ 🕐 🥇 🛠️ 🛠️ ⚙️ 🐍 🆎 🔑 📣 🔢 & 📨 👈 👤 ⏱ 👀 (⏭ NestJS & ♨). 👤 🔎 ⚫️ 🌅 ⚖️ 🌘 🎏 🕰 🤗. ✋️ APIStar ⚙️ 🗄 🐩. - -⚫️ ✔️ 🏧 💽 🔬, 💽 🛠️ & 🗄 🔗 ⚡ ⚓️ 🔛 🎏 🆎 🔑 📚 🥉. - -💪 🔗 🔑 🚫 ⚙️ 🎏 🐍 🆎 🔑 💖 Pydantic, ⚫️ 🍖 🌅 🎏 🍭,, 👨‍🎨 🐕‍🦺 🚫🔜 👍, ✋️, APIStar 🏆 💪 🎛. - -⚫️ ✔️ 🏆 🎭 📇 🕰 (🕴 💥 💃). - -🥇, ⚫️ 🚫 ✔️ 🏧 🛠️ 🧾 🕸 🎚, ✋️ 👤 💭 👤 💪 🚮 🦁 🎚 ⚫️. - -⚫️ ✔️ 🔗 💉 ⚙️. ⚫️ ✔ 🏤-® 🦲, 🎏 🧰 🔬 🔛. ✋️, ⚫️ 👑 ⚒. - -👤 🙅 💪 ⚙️ ⚫️ 🌕 🏗, ⚫️ 🚫 ✔️ 💂‍♂ 🛠️,, 👤 🚫 🚫 ❎ 🌐 ⚒ 👤 ✔️ ⏮️ 🌕-📚 🚂 ⚓️ 🔛 🏺-Apispec. 👤 ✔️ 👇 📈 🏗 ✍ 🚲 📨 ❎ 👈 🛠️. - -✋️ ⤴️, 🏗 🎯 🔀. - -⚫️ 🙅‍♂ 📏 🛠️ 🕸 🛠️, 👼 💪 🎯 🔛 💃. - -🔜 APIStar ⚒ 🧰 ✔ 🗄 🔧, 🚫 🕸 🛠️. - -/// info - -APIStar ✍ ✡ 🇺🇸🏛. 🎏 👨 👈 ✍: - -* ✳ 🎂 🛠️ -* 💃 (❔ **FastAPI** ⚓️) -* Uvicorn (⚙️ 💃 & **FastAPI**) - -/// - -/// check | 😮 **FastAPI** - -🔀. - -💭 📣 💗 👜 (💽 🔬, 🛠️ & 🧾) ⏮️ 🎏 🐍 🆎, 👈 🎏 🕰 🚚 👑 👨‍🎨 🐕‍🦺, 🕳 👤 🤔 💎 💭. - - & ⏮️ 🔎 📏 🕰 🎏 🛠️ & 🔬 📚 🎏 🎛, APIStar 🏆 🎛 💪. - -⤴️ APIStar ⛔️ 🔀 💽 & 💃 ✍, & 🆕 👻 🏛 ✅ ⚙️. 👈 🏁 🌈 🏗 **FastAPI**. - -👤 🤔 **FastAPI** "🛐 👨‍💼" APIStar, ⏪ 📉 & 📈 ⚒, ⌨ ⚙️, & 🎏 🍕, ⚓️ 🔛 🏫 ⚪️➡️ 🌐 👉 ⏮️ 🧰. - -/// - -## ⚙️ **FastAPI** - -### Pydantic - -Pydantic 🗃 🔬 💽 🔬, 🛠️ & 🧾 (⚙️ 🎻 🔗) ⚓️ 🔛 🐍 🆎 🔑. - -👈 ⚒ ⚫️ 📶 🏋️. - -⚫️ ⭐ 🍭. 👐 ⚫️ ⏩ 🌘 🍭 📇. & ⚫️ ⚓️ 🔛 🎏 🐍 🆎 🔑, 👨‍🎨 🐕‍🦺 👑. - -/// check | **FastAPI** ⚙️ ⚫️ - -🍵 🌐 💽 🔬, 💽 🛠️ & 🏧 🏷 🧾 (⚓️ 🔛 🎻 🔗). - -**FastAPI** ⤴️ ✊ 👈 🎻 🔗 💽 & 🚮 ⚫️ 🗄, ↖️ ⚪️➡️ 🌐 🎏 👜 ⚫️ 🔨. - -/// - -### 💃 - -💃 💿 🔫 🛠️/🧰, ❔ 💯 🏗 ↕-🎭 ✳ 🐕‍🦺. - -⚫️ 📶 🙅 & 🏋️. ⚫️ 🔧 💪 🏧, & ✔️ 🔧 🦲. - -⚫️ ✔️: - -* 🤙 🎆 🎭. -* *️⃣ 🐕‍🦺. -* -🛠️ 🖥 📋. -* 🕴 & 🤫 🎉. -* 💯 👩‍💻 🏗 🔛 🇸🇲. -* ⚜, 🗜, 🎻 📁, 🎏 📨. -* 🎉 & 🍪 🐕‍🦺. -* 1️⃣0️⃣0️⃣ 💯 💯 💰. -* 1️⃣0️⃣0️⃣ 💯 🆎 ✍ ✍. -* 👩‍❤‍👨 🏋️ 🔗. - -💃 ⏳ ⏩ 🐍 🛠️ 💯. 🕴 💥 Uvicorn, ❔ 🚫 🛠️, ✋️ 💽. - -💃 🚚 🌐 🔰 🕸 🕸 🛠️. - -✋️ ⚫️ 🚫 🚚 🏧 💽 🔬, 🛠️ ⚖️ 🧾. - -👈 1️⃣ 👑 👜 👈 **FastAPI** 🚮 🔛 🔝, 🌐 ⚓️ 🔛 🐍 🆎 🔑 (⚙️ Pydantic). 👈, ➕ 🔗 💉 ⚙️, 💂‍♂ 🚙, 🗄 🔗 ⚡, ♒️. - -/// note | 📡 ℹ - -🔫 🆕 "🐩" ➖ 🛠️ ✳ 🐚 🏉 👨‍🎓. ⚫️ 🚫 "🐍 🐩" (🇩🇬), 👐 👫 🛠️ 🔨 👈. - -👐, ⚫️ ⏪ ➖ ⚙️ "🐩" 📚 🧰. 👉 📉 📉 🛠️, 👆 💪 🎛 Uvicorn 🙆 🎏 🔫 💽 (💖 👸 ⚖️ Hypercorn), ⚖️ 👆 💪 🚮 🔫 🔗 🧰, 💖 `python-socketio`. - -/// - -/// check | **FastAPI** ⚙️ ⚫️ - -🍵 🌐 🐚 🕸 🍕. ❎ ⚒ 🔛 🔝. - -🎓 `FastAPI` ⚫️ 😖 🔗 ⚪️➡️ 🎓 `Starlette`. - -, 🕳 👈 👆 💪 ⏮️ 💃, 👆 💪 ⚫️ 🔗 ⏮️ **FastAPI**, ⚫️ 🌖 💃 🔛 💊. - -/// - -### Uvicorn - -Uvicorn 🌩-⏩ 🔫 💽, 🏗 🔛 uvloop & httptool. - -⚫️ 🚫 🕸 🛠️, ✋️ 💽. 🖼, ⚫️ 🚫 🚚 🧰 🕹 ➡. 👈 🕳 👈 🛠️ 💖 💃 (⚖️ **FastAPI**) 🔜 🚚 🔛 🔝. - -⚫️ 👍 💽 💃 & **FastAPI**. - -/// check | **FastAPI** 👍 ⚫️ - -👑 🕸 💽 🏃 **FastAPI** 🈸. - -👆 💪 🌀 ⚫️ ⏮️ 🐁, ✔️ 🔁 👁-🛠️ 💽. - -✅ 🌅 ℹ [🛠️](deployment/index.md){.internal-link target=_blank} 📄. - -/// - -## 📇 & 🚅 - -🤔, 🔬, & 👀 🔺 🖖 Uvicorn, 💃 & FastAPI, ✅ 📄 🔃 [📇](benchmarks.md){.internal-link target=_blank}. diff --git a/docs/em/docs/async.md b/docs/em/docs/async.md deleted file mode 100644 index cde778b0ff..0000000000 --- a/docs/em/docs/async.md +++ /dev/null @@ -1,442 +0,0 @@ -# 🛠️ & 🔁 / ⌛ - -ℹ 🔃 `async def` ❕ *➡ 🛠️ 🔢* & 🖥 🔃 🔁 📟, 🛠️, & 🔁. - -## 🏃 ❓ - -🆑;👩‍⚕️: - -🚥 👆 ⚙️ 🥉 🥳 🗃 👈 💬 👆 🤙 👫 ⏮️ `await`, 💖: - -```Python -results = await some_library() -``` - -⤴️, 📣 👆 *➡ 🛠️ 🔢* ⏮️ `async def` 💖: - -```Python hl_lines="2" -@app.get('/') -async def read_results(): - results = await some_library() - return results -``` - -/// note - -👆 💪 🕴 ⚙️ `await` 🔘 🔢 ✍ ⏮️ `async def`. - -/// - ---- - -🚥 👆 ⚙️ 🥉 🥳 🗃 👈 🔗 ⏮️ 🕳 (💽, 🛠️, 📁 ⚙️, ♒️.) & 🚫 ✔️ 🐕‍🦺 ⚙️ `await`, (👉 ⏳ 💼 🌅 💽 🗃), ⤴️ 📣 👆 *➡ 🛠️ 🔢* 🛎, ⏮️ `def`, 💖: - -```Python hl_lines="2" -@app.get('/') -def results(): - results = some_library() - return results -``` - ---- - -🚥 👆 🈸 (😫) 🚫 ✔️ 🔗 ⏮️ 🕳 🙆 & ⌛ ⚫️ 📨, ⚙️ `async def`. - ---- - -🚥 👆 🚫 💭, ⚙️ 😐 `def`. - ---- - -**🗒**: 👆 💪 🌀 `def` & `async def` 👆 *➡ 🛠️ 🔢* 🌅 👆 💪 & 🔬 🔠 1️⃣ ⚙️ 🏆 🎛 👆. FastAPI 🔜 ▶️️ 👜 ⏮️ 👫. - -😆, 🙆 💼 🔛, FastAPI 🔜 👷 🔁 & 📶 ⏩. - -✋️ 📄 📶 🔛, ⚫️ 🔜 💪 🎭 🛠️. - -## 📡 ℹ - -🏛 ⏬ 🐍 ✔️ 🐕‍🦺 **"🔁 📟"** ⚙️ 🕳 🤙 **"🔁"**, ⏮️ **`async` & `await`** ❕. - -➡️ 👀 👈 🔤 🍕 📄 🔛: - -* **🔁 📟** -* **`async` & `await`** -* **🔁** - -## 🔁 📟 - -🔁 📟 ⛓ 👈 🇪🇸 👶 ✔️ 🌌 💬 💻 / 📋 👶 👈 ☝ 📟, ⚫️ 👶 🔜 ✔️ ⌛ *🕳 🙆* 🏁 👱 🙆. ➡️ 💬 👈 *🕳 🙆* 🤙 "🐌-📁" 👶. - -, ⏮️ 👈 🕰, 💻 💪 🚶 & 🎏 👷, ⏪ "🐌-📁" 👶 🏁. - -⤴️ 💻 / 📋 👶 🔜 👟 🔙 🔠 🕰 ⚫️ ✔️ 🤞 ↩️ ⚫️ ⌛ 🔄, ⚖️ 🕐❔ ⚫️ 👶 🏁 🌐 👷 ⚫️ ✔️ 👈 ☝. & ⚫️ 👶 🔜 👀 🚥 🙆 📋 ⚫️ ⌛ ✔️ ⏪ 🏁, 🤸 ⚫️❔ ⚫️ ✔️. - -⏭, ⚫️ 👶 ✊ 🥇 📋 🏁 (➡️ 💬, 👆 "🐌-📁" 👶) & 😣 ⚫️❔ ⚫️ ✔️ ⏮️ ⚫️. - -👈 "⌛ 🕳 🙆" 🛎 🔗 👤/🅾 🛠️ 👈 📶 "🐌" (🔬 🚅 🕹 & 💾 💾), 💖 ⌛: - -* 📊 ⚪️➡️ 👩‍💻 📨 🔘 🕸 -* 📊 📨 👆 📋 📨 👩‍💻 🔘 🕸 -* 🎚 📁 💾 ✍ ⚙️ & 🤝 👆 📋 -* 🎚 👆 📋 🤝 ⚙️ ✍ 💾 -* 🛰 🛠️ 🛠️ -* 💽 🛠️ 🏁 -* 💽 🔢 📨 🏁 -* ♒️. - -🛠️ 🕰 🍴 ✴️ ⌛ 👤/🅾 🛠️, 👫 🤙 👫 "👤/🅾 🔗" 🛠️. - -⚫️ 🤙 "🔁" ↩️ 💻 / 📋 🚫 ✔️ "🔁" ⏮️ 🐌 📋, ⌛ ☑ 🙍 👈 📋 🏁, ⏪ 🔨 🕳, 💪 ✊ 📋 🏁 & 😣 👷. - -↩️ 👈, 💆‍♂ "🔁" ⚙️, 🕐 🏁, 📋 💪 ⌛ ⏸ 🐥 👄 (⏲) 💻 / 📋 🏁 ⚫️❔ ⚫️ 🚶, & ⤴️ 👟 🔙 ✊ 🏁 & 😣 👷 ⏮️ 👫. - -"🔁" (👽 "🔁") 👫 🛎 ⚙️ ⚖ "🔁", ↩️ 💻 / 📋 ⏩ 🌐 📶 🔁 ⏭ 🔀 🎏 📋, 🚥 👈 🔁 🔌 ⌛. - -### 🛠️ & 🍔 - -👉 💭 **🔁** 📟 🔬 🔛 🕣 🤙 **"🛠️"**. ⚫️ 🎏 ⚪️➡️ **"🔁"**. - -**🛠️** & **🔁** 👯‍♂️ 🔗 "🎏 👜 😥 🌅 ⚖️ 🌘 🎏 🕰". - -✋️ ℹ 🖖 *🛠️* & *🔁* 🎏. - -👀 🔺, 🌈 📄 📖 🔃 🍔: - -### 🛠️ 🍔 - -👆 🚶 ⏮️ 👆 🥰 🤚 ⏩ 🥕, 👆 🧍 ⏸ ⏪ 🏧 ✊ ✔ ⚪️➡️ 👫👫 🚪 👆. 👶 - - - -⤴️ ⚫️ 👆 🔄, 👆 🥉 👆 ✔ 2️⃣ 📶 🎀 🍔 👆 🥰 & 👆. 👶 👶 - - - -🏧 💬 🕳 🍳 👨‍🍳 👫 💭 👫 ✔️ 🏗 👆 🍔 (✋️ 👫 ⏳ 🏗 🕐 ⏮️ 👩‍💻). - - - -👆 💸. 👶 - -🏧 🤝 👆 🔢 👆 🔄. - - - -⏪ 👆 ⌛, 👆 🚶 ⏮️ 👆 🥰 & ⚒ 🏓, 👆 🧎 & 💬 ⏮️ 👆 🥰 📏 🕰 (👆 🍔 📶 🎀 & ✊ 🕰 🏗). - -👆 🏖 🏓 ⏮️ 👆 🥰, ⏪ 👆 ⌛ 🍔, 👆 💪 💸 👈 🕰 😮 ❔ 👌, 🐨 & 🙃 👆 🥰 👶 👶 👶. - - - -⏪ ⌛ & 💬 👆 🥰, ⚪️➡️ 🕰 🕰, 👆 ✅ 🔢 🖥 🔛 ⏲ 👀 🚥 ⚫️ 👆 🔄 ⏪. - -⤴️ ☝, ⚫️ 😒 👆 🔄. 👆 🚶 ⏲, 🤚 👆 🍔 & 👟 🔙 🏓. - - - -👆 & 👆 🥰 🍴 🍔 & ✔️ 👌 🕰. 👶 - - - -/// info - -🌹 🖼 👯 🍏. 👶 - -/// - ---- - -🌈 👆 💻 / 📋 👶 👈 📖. - -⏪ 👆 ⏸, 👆 ⛽ 👶, ⌛ 👆 🔄, 🚫 🔨 🕳 📶 "😌". ✋️ ⏸ ⏩ ↩️ 🏧 🕴 ✊ ✔ (🚫 🏗 👫), 👈 👌. - -⤴️, 🕐❔ ⚫️ 👆 🔄, 👆 ☑ "😌" 👷, 👆 🛠️ 🍣, 💭 ⚫️❔ 👆 💚, 🤚 👆 🥰 ⚒, 💸, ✅ 👈 👆 🤝 ☑ 💵 ⚖️ 💳, ✅ 👈 👆 🈚 ☑, ✅ 👈 ✔ ✔️ ☑ 🏬, ♒️. - -✋️ ⤴️, ✋️ 👆 🚫 ✔️ 👆 🍔, 👆 👷 ⏮️ 🏧 "🔛 ⏸" ⏸, ↩️ 👆 ✔️ ⌛ 👶 👆 🍔 🔜. - -✋️ 👆 🚶 ↖️ ⚪️➡️ ⏲ & 🧎 🏓 ⏮️ 🔢 👆 🔄, 👆 💪 🎛 👶 👆 🙋 👆 🥰, & "👷" 👶 👶 🔛 👈. ⤴️ 👆 🔄 🔨 🕳 📶 "😌" 😏 ⏮️ 👆 🥰 👶. - -⤴️ 🏧 👶 💬 "👤 🏁 ⏮️ 🔨 🍔" 🚮 👆 🔢 🔛 ⏲ 🖥, ✋️ 👆 🚫 🦘 💖 😜 ⏪ 🕐❔ 🖥 🔢 🔀 👆 🔄 🔢. 👆 💭 🙅‍♂ 1️⃣ 🔜 📎 👆 🍔 ↩️ 👆 ✔️ 🔢 👆 🔄, & 👫 ✔️ 👫. - -👆 ⌛ 👆 🥰 🏁 📖 (🏁 ⏮️ 👷 👶 / 📋 ➖ 🛠️ 👶), 😀 🖐 & 💬 👈 👆 🔜 🍔 ⏸. - -⤴️ 👆 🚶 ⏲ 👶, ▶️ 📋 👈 🔜 🏁 👶, ⚒ 🍔, 💬 👏 & ✊ 👫 🏓. 👈 🏁 👈 🔁 / 📋 🔗 ⏮️ ⏲ ⏹. 👈 🔄, ✍ 🆕 📋, "🍴 🍔" 👶 👶, ✋️ ⏮️ 1️⃣ "🤚 🍔" 🏁 ⏹. - -### 🔗 🍔 - -🔜 ➡️ 🌈 👫 ➖🚫 🚫 "🛠️ 🍔", ✋️ "🔗 🍔". - -👆 🚶 ⏮️ 👆 🥰 🤚 🔗 ⏩ 🥕. - -👆 🧍 ⏸ ⏪ 📚 (➡️ 💬 8️⃣) 🏧 👈 🎏 🕰 🍳 ✊ ✔ ⚪️➡️ 👫👫 🚪 👆. - -👱 ⏭ 👆 ⌛ 👫 🍔 🔜 ⏭ 🍂 ⏲ ↩️ 🔠 8️⃣ 🏧 🚶 & 🏗 🍔 ▶️️ ↖️ ⏭ 💆‍♂ ⏭ ✔. - - - -⤴️ ⚫️ 😒 👆 🔄, 👆 🥉 👆 ✔ 2️⃣ 📶 🎀 🍔 👆 🥰 & 👆. - -👆 💸 👶. - - - -🏧 🚶 👨‍🍳. - -👆 ⌛, 🧍 🚪 ⏲ 👶, 👈 🙅‍♂ 1️⃣ 🙆 ✊ 👆 🍔 ⏭ 👆, 📤 🙅‍♂ 🔢 🔄. - - - -👆 & 👆 🥰 😩 🚫 ➡️ 🙆 🤚 🚪 👆 & ✊ 👆 🍔 🕐❔ 👫 🛬, 👆 🚫🔜 💸 🙋 👆 🥰. 👶 - -👉 "🔁" 👷, 👆 "🔁" ⏮️ 🏧/🍳 👶 👶. 👆 ✔️ ⌛ 👶 & 📤 ☑ 🙍 👈 🏧/🍳 👶 👶 🏁 🍔 & 🤝 👫 👆, ⚖️ ⏪, 👱 🙆 💪 ✊ 👫. - - - -⤴️ 👆 🏧/🍳 👶 👶 😒 👟 🔙 ⏮️ 👆 🍔, ⏮️ 📏 🕰 ⌛ 👶 📤 🚪 ⏲. - - - -👆 ✊ 👆 🍔 & 🚶 🏓 ⏮️ 👆 🥰. - -👆 🍴 👫, & 👆 🔨. ⏹ - - - -📤 🚫 🌅 💬 ⚖️ 😏 🌅 🕰 💸 ⌛ 👶 🚪 ⏲. 👶 - -/// info - -🌹 🖼 👯 🍏. 👶 - -/// - ---- - -👉 😐 🔗 🍔, 👆 💻 / 📋 👶 ⏮️ 2️⃣ 🕹 (👆 & 👆 🥰), 👯‍♂️ ⌛ 👶 & 💡 👫 🙋 👶 "⌛ 🔛 ⏲" 👶 📏 🕰. - -⏩ 🥕 🏪 ✔️ 8️⃣ 🕹 (🏧/🍳). ⏪ 🛠️ 🍔 🏪 💪 ✔️ ✔️ 🕴 2️⃣ (1️⃣ 🏧 & 1️⃣ 🍳). - -✋️, 🏁 💡 🚫 🏆. 👶 - ---- - -👉 🔜 🔗 🌓 📖 🍔. 👶 - -🌅 "🎰 👨‍❤‍👨" 🖼 👉, 🌈 🏦. - -🆙 ⏳, 🏆 🏦 ✔️ 💗 🏧 👶 👶 👶 👶 👶 👶 👶 👶 & 🦏 ⏸ 👶 👶 👶 👶 👶 👶 👶 👶. - -🌐 🏧 🔨 🌐 👷 ⏮️ 1️⃣ 👩‍💻 ⏮️ 🎏 👶 👶 👶. - -& 👆 ✔️ ⌛ 👶 ⏸ 📏 🕰 ⚖️ 👆 💸 👆 🔄. - -👆 🎲 🚫🔜 💚 ✊ 👆 🥰 👶 ⏮️ 👆 👷 🏦 👶. - -### 🍔 🏁 - -👉 😐 "⏩ 🥕 🍔 ⏮️ 👆 🥰", 📤 📚 ⌛ 👶, ⚫️ ⚒ 📚 🌅 🔑 ✔️ 🛠️ ⚙️ ⏸ 👶 👶. - -👉 💼 🌅 🕸 🈸. - -📚, 📚 👩‍💻, ✋️ 👆 💽 ⌛ 👶 👫 🚫--👍 🔗 📨 👫 📨. - -& ⤴️ ⌛ 👶 🔄 📨 👟 🔙. - -👉 "⌛" 👶 ⚖ ⏲, ✋️, ⚖ ⚫️ 🌐, ⚫️ 📚 ⌛ 🔚. - -👈 ⚫️❔ ⚫️ ⚒ 📚 🔑 ⚙️ 🔁 ⏸ 👶 👶 📟 🕸 🔗. - -👉 😇 🔀 ⚫️❔ ⚒ ✳ 🌟 (✋️ ✳ 🚫 🔗) & 👈 💪 🚶 🛠️ 🇪🇸. - -& 👈 🎏 🎚 🎭 👆 🤚 ⏮️ **FastAPI**. - -& 👆 💪 ✔️ 🔁 & 🔀 🎏 🕰, 👆 🤚 ↕ 🎭 🌘 🌅 💯 ✳ 🛠️ & 🔛 🇷🇪 ⏮️ 🚶, ❔ ✍ 🇪🇸 🔐 🅱 (🌐 👏 💃). - -### 🛠️ 👍 🌘 🔁 ❓ - -😆 ❗ 👈 🚫 🛐 📖. - -🛠️ 🎏 🌘 🔁. & ⚫️ 👻 🔛 **🎯** 😐 👈 🔌 📚 ⌛. ↩️ 👈, ⚫️ 🛎 📚 👍 🌘 🔁 🕸 🈸 🛠️. ✋️ 🚫 🌐. - -, ⚖ 👈 👅, 🌈 📄 📏 📖: - -> 👆 ✔️ 🧹 🦏, 💩 🏠. - -*😆, 👈 🎂 📖*. - ---- - -📤 🙅‍♂ ⌛ 👶 🙆, 📚 👷 🔨, 🔛 💗 🥉 🏠. - -👆 💪 ✔️ 🔄 🍔 🖼, 🥇 🏠 🧖‍♂, ⤴️ 👨‍🍳, ✋️ 👆 🚫 ⌛ 👶 🕳, 🧹 & 🧹, 🔄 🚫🔜 📉 🕳. - -⚫️ 🔜 ✊ 🎏 💸 🕰 🏁 ⏮️ ⚖️ 🍵 🔄 (🛠️) & 👆 🔜 ✔️ ⌛ 🎏 💸 👷. - -✋️ 👉 💼, 🚥 👆 💪 ✊️ 8️⃣ 👰-🏧/🍳/🔜-🧹, & 🔠 1️⃣ 👫 (➕ 👆) 💪 ✊ 🏒 🏠 🧹 ⚫️, 👆 💪 🌐 👷 **🔗**, ⏮️ ➕ ℹ, & 🏁 🌅 🔜. - -👉 😐, 🔠 1️⃣ 🧹 (🔌 👆) 🔜 🕹, 🤸 👫 🍕 👨‍🏭. - -& 🏆 🛠️ 🕰 ✊ ☑ 👷 (↩️ ⌛), & 👷 💻 ⌛ 💽, 👫 🤙 👫 ⚠ "💽 🎁". - ---- - -⚠ 🖼 💽 🔗 🛠️ 👜 👈 🚚 🏗 🧪 🏭. - -🖼: - -* **🎧** ⚖️ **🖼 🏭**. -* **💻 👓**: 🖼 ✍ 💯 🔅, 🔠 🔅 ✔️ 3️⃣ 💲 / 🎨, 🏭 👈 🛎 🚚 💻 🕳 🔛 📚 🔅, 🌐 🎏 🕰. -* **🎰 🏫**: ⚫️ 🛎 🚚 📚 "✖" & "🖼" ✖. 💭 🦏 📋 ⏮️ 🔢 & ✖ 🌐 👫 👯‍♂️ 🎏 🕰. -* **⏬ 🏫**: 👉 🎧-🏑 🎰 🏫,, 🎏 ✔. ⚫️ 👈 📤 🚫 👁 📋 🔢 ✖, ✋️ 🦏 ⚒ 👫, & 📚 💼, 👆 ⚙️ 🎁 🕹 🏗 & / ⚖️ ⚙️ 👈 🏷. - -### 🛠️ ➕ 🔁: 🕸 ➕ 🎰 🏫 - -⏮️ **FastAPI** 👆 💪 ✊ 📈 🛠️ 👈 📶 ⚠ 🕸 🛠️ (🎏 👑 🧲 ✳). - -✋️ 👆 💪 🐄 💰 🔁 & 💾 (✔️ 💗 🛠️ 🏃‍♂ 🔗) **💽 🎁** ⚖ 💖 👈 🎰 🏫 ⚙️. - -👈, ➕ 🙅 👐 👈 🐍 👑 🇪🇸 **💽 🧪**, 🎰 🏫 & ✴️ ⏬ 🏫, ⚒ FastAPI 📶 👍 🏏 💽 🧪 / 🎰 🏫 🕸 🔗 & 🈸 (👪 📚 🎏). - -👀 ❔ 🏆 👉 🔁 🏭 👀 📄 🔃 [🛠️](deployment/index.md){.internal-link target=_blank}. - -## `async` & `await` - -🏛 ⏬ 🐍 ✔️ 📶 🏋️ 🌌 🔬 🔁 📟. 👉 ⚒ ⚫️ 👀 💖 😐 "🔁" 📟 & "⌛" 👆 ▶️️ 🙍. - -🕐❔ 📤 🛠️ 👈 🔜 🚚 ⌛ ⏭ 🤝 🏁 & ✔️ 🐕‍🦺 👉 🆕 🐍 ⚒, 👆 💪 📟 ⚫️ 💖: - -```Python -burgers = await get_burgers(2) -``` - -🔑 📥 `await`. ⚫️ 💬 🐍 👈 ⚫️ ✔️ ⌛ ⏸ `get_burgers(2)` 🏁 🔨 🚮 👜 👶 ⏭ ♻ 🏁 `burgers`. ⏮️ 👈, 🐍 🔜 💭 👈 ⚫️ 💪 🚶 & 🕳 🙆 👶 👶 👐 (💖 📨 ➕1️⃣ 📨). - -`await` 👷, ⚫️ ✔️ 🔘 🔢 👈 🐕‍🦺 👉 🔀. 👈, 👆 📣 ⚫️ ⏮️ `async def`: - -```Python hl_lines="1" -async def get_burgers(number: int): - # Do some asynchronous stuff to create the burgers - return burgers -``` - -...↩️ `def`: - -```Python hl_lines="2" -# This is not asynchronous -def get_sequential_burgers(number: int): - # Do some sequential stuff to create the burgers - return burgers -``` - -⏮️ `async def`, 🐍 💭 👈, 🔘 👈 🔢, ⚫️ ✔️ 🤔 `await` 🧬, & 👈 ⚫️ 💪 "⏸" ⏸ 🛠️ 👈 🔢 & 🚶 🕳 🙆 👶 ⏭ 👟 🔙. - -🕐❔ 👆 💚 🤙 `async def` 🔢, 👆 ✔️ "⌛" ⚫️. , 👉 🏆 🚫 👷: - -```Python -# This won't work, because get_burgers was defined with: async def -burgers = get_burgers(2) -``` - ---- - -, 🚥 👆 ⚙️ 🗃 👈 💬 👆 👈 👆 💪 🤙 ⚫️ ⏮️ `await`, 👆 💪 ✍ *➡ 🛠️ 🔢* 👈 ⚙️ ⚫️ ⏮️ `async def`, 💖: - -```Python hl_lines="2-3" -@app.get('/burgers') -async def read_burgers(): - burgers = await get_burgers(2) - return burgers -``` - -### 🌅 📡 ℹ - -👆 💪 ✔️ 👀 👈 `await` 💪 🕴 ⚙️ 🔘 🔢 🔬 ⏮️ `async def`. - -✋️ 🎏 🕰, 🔢 🔬 ⏮️ `async def` ✔️ "⌛". , 🔢 ⏮️ `async def` 💪 🕴 🤙 🔘 🔢 🔬 ⏮️ `async def` 💁‍♂️. - -, 🔃 🥚 & 🐔, ❔ 👆 🤙 🥇 `async` 🔢 ❓ - -🚥 👆 👷 ⏮️ **FastAPI** 👆 🚫 ✔️ 😟 🔃 👈, ↩️ 👈 "🥇" 🔢 🔜 👆 *➡ 🛠️ 🔢*, & FastAPI 🔜 💭 ❔ ▶️️ 👜. - -✋️ 🚥 👆 💚 ⚙️ `async` / `await` 🍵 FastAPI, 👆 💪 ⚫️ 👍. - -### ✍ 👆 👍 🔁 📟 - -💃 (& **FastAPI**) ⚓️ 🔛 AnyIO, ❔ ⚒ ⚫️ 🔗 ⏮️ 👯‍♂️ 🐍 🐩 🗃 & 🎻. - -🎯, 👆 💪 🔗 ⚙️ AnyIO 👆 🏧 🛠️ ⚙️ 💼 👈 🚚 🌅 🏧 ⚓ 👆 👍 📟. - -& 🚥 👆 🚫 ⚙️ FastAPI, 👆 💪 ✍ 👆 👍 🔁 🈸 ⏮️ AnyIO 🏆 🔗 & 🤚 🚮 💰 (✅ *📊 🛠️*). - -### 🎏 📨 🔁 📟 - -👉 👗 ⚙️ `async` & `await` 📶 🆕 🇪🇸. - -✋️ ⚫️ ⚒ 👷 ⏮️ 🔁 📟 📚 ⏩. - -👉 🎏 ❕ (⚖️ 🌖 🌓) 🔌 ⏳ 🏛 ⏬ 🕸 (🖥 & ✳). - -✋️ ⏭ 👈, 🚚 🔁 📟 🌖 🏗 & ⚠. - -⏮️ ⏬ 🐍, 👆 💪 ✔️ ⚙️ 🧵 ⚖️ 🐁. ✋️ 📟 🌌 🌖 🏗 🤔, ℹ, & 💭 🔃. - -⏮️ ⏬ ✳ / 🖥 🕸, 👆 🔜 ✔️ ⚙️ "⏲". ❔ ↘️ "⏲ 🔥😈". - -## 🔁 - -**🔁** 📶 🎀 ⚖ 👜 📨 `async def` 🔢. 🐍 💭 👈 ⚫️ 🕳 💖 🔢 👈 ⚫️ 💪 ▶️ & 👈 ⚫️ 🔜 🔚 ☝, ✋️ 👈 ⚫️ 5️⃣📆 ⏸ ⏸ 🔘 💁‍♂️, 🕐❔ 📤 `await` 🔘 ⚫️. - -✋️ 🌐 👉 🛠️ ⚙️ 🔁 📟 ⏮️ `async` & `await` 📚 🕰 🔬 ⚙️ "🔁". ⚫️ ⭐ 👑 🔑 ⚒ 🚶, "🔁". - -## 🏁 - -➡️ 👀 🎏 🔤 ⚪️➡️ 🔛: - -> 🏛 ⏬ 🐍 ✔️ 🐕‍🦺 **"🔁 📟"** ⚙️ 🕳 🤙 **"🔁"**, ⏮️ **`async` & `await`** ❕. - -👈 🔜 ⚒ 🌅 🔑 🔜. 👶 - -🌐 👈 ⚫️❔ 🏋️ FastAPI (🔘 💃) & ⚫️❔ ⚒ ⚫️ ✔️ ✅ 🎆 🎭. - -## 📶 📡 ℹ - -/// warning - -👆 💪 🎲 🚶 👉. - -👉 📶 📡 ℹ ❔ **FastAPI** 👷 🔘. - -🚥 👆 ✔️ 📡 💡 (🈶-🏋, 🧵, 🍫, ♒️.) & 😟 🔃 ❔ FastAPI 🍵 `async def` 🆚 😐 `def`, 🚶 ⤴️. - -/// - -### ➡ 🛠️ 🔢 - -🕐❔ 👆 📣 *➡ 🛠️ 🔢* ⏮️ 😐 `def` ↩️ `async def`, ⚫️ 🏃 🔢 🧵 👈 ⤴️ ⌛, ↩️ ➖ 🤙 🔗 (⚫️ 🔜 🍫 💽). - -🚥 👆 👟 ⚪️➡️ ➕1️⃣ 🔁 🛠️ 👈 🔨 🚫 👷 🌌 🔬 🔛 & 👆 ⚙️ ⚖ 🙃 📊-🕴 *➡ 🛠️ 🔢* ⏮️ ✅ `def` 🤪 🎭 📈 (🔃 1️⃣0️⃣0️⃣ 💓), 🙏 🗒 👈 **FastAPI** ⭐ 🔜 🔄. 👫 💼, ⚫️ 👻 ⚙️ `async def` 🚥 👆 *➡ 🛠️ 🔢* ⚙️ 📟 👈 🎭 🚧 👤/🅾. - -, 👯‍♂️ ⚠, 🤞 👈 **FastAPI** 🔜 [⏩](index.md#_15){.internal-link target=_blank} 🌘 (⚖️ 🌘 ⭐) 👆 ⏮️ 🛠️. - -### 🔗 - -🎏 ✔ [🔗](tutorial/dependencies/index.md){.internal-link target=_blank}. 🚥 🔗 🐩 `def` 🔢 ↩️ `async def`, ⚫️ 🏃 🔢 🧵. - -### 🎧-🔗 - -👆 💪 ✔️ 💗 🔗 & [🎧-🔗](tutorial/dependencies/sub-dependencies.md){.internal-link target=_blank} 🚫 🔠 🎏 (🔢 🔢 🔑), 👫 💪 ✍ ⏮️ `async def` & ⏮️ 😐 `def`. ⚫️ 🔜 👷, & 🕐 ✍ ⏮️ 😐 `def` 🔜 🤙 🔛 🔢 🧵 (⚪️➡️ 🧵) ↩️ ➖ "⌛". - -### 🎏 🚙 🔢 - -🙆 🎏 🚙 🔢 👈 👆 🤙 🔗 💪 ✍ ⏮️ 😐 `def` ⚖️ `async def` & FastAPI 🏆 🚫 📉 🌌 👆 🤙 ⚫️. - -👉 🔅 🔢 👈 FastAPI 🤙 👆: *➡ 🛠️ 🔢* & 🔗. - -🚥 👆 🚙 🔢 😐 🔢 ⏮️ `def`, ⚫️ 🔜 🤙 🔗 (👆 ✍ ⚫️ 👆 📟), 🚫 🧵, 🚥 🔢 ✍ ⏮️ `async def` ⤴️ 👆 🔜 `await` 👈 🔢 🕐❔ 👆 🤙 ⚫️ 👆 📟. - ---- - -🔄, 👉 📶 📡 ℹ 👈 🔜 🎲 ⚠ 🚥 👆 👟 🔎 👫. - -⏪, 👆 🔜 👍 ⏮️ 📄 ⚪️➡️ 📄 🔛: 🏃 ❓. diff --git a/docs/em/docs/benchmarks.md b/docs/em/docs/benchmarks.md deleted file mode 100644 index 003c3f62de..0000000000 --- a/docs/em/docs/benchmarks.md +++ /dev/null @@ -1,34 +0,0 @@ -# 📇 - -🔬 🇸🇲 📇 🎦 **FastAPI** 🈸 🏃‍♂ 🔽 Uvicorn 1️⃣ ⏩ 🐍 🛠️ 💪, 🕴 🔛 💃 & Uvicorn 👫 (⚙️ 🔘 FastAPI). (*) - -✋️ 🕐❔ ✅ 📇 & 🔺 👆 🔜 ✔️ 📄 🤯. - -## 📇 & 🚅 - -🕐❔ 👆 ✅ 📇, ⚫️ ⚠ 👀 📚 🧰 🎏 🆎 🔬 🌓. - -🎯, 👀 Uvicorn, 💃 & FastAPI 🔬 👯‍♂️ (👪 📚 🎏 🧰). - -🙅 ⚠ ❎ 🧰, 👍 🎭 ⚫️ 🔜 🤚. & 🏆 📇 🚫 💯 🌖 ⚒ 🚚 🧰. - -🔗 💖: - -* **Uvicorn**: 🔫 💽 - * **💃**: (⚙️ Uvicorn) 🕸 🕸 - * **FastAPI**: (⚙️ 💃) 🛠️ 🕸 ⏮️ 📚 🌖 ⚒ 🏗 🔗, ⏮️ 💽 🔬, ♒️. - -* **Uvicorn**: - * 🔜 ✔️ 🏆 🎭, ⚫️ 🚫 ✔️ 🌅 ➕ 📟 ↖️ ⚪️➡️ 💽 ⚫️. - * 👆 🚫🔜 ✍ 🈸 Uvicorn 🔗. 👈 🔜 ⛓ 👈 👆 📟 🔜 ✔️ 🔌 🌖 ⚖️ 🌘, 🌘, 🌐 📟 🚚 💃 (⚖️ **FastAPI**). & 🚥 👆 👈, 👆 🏁 🈸 🔜 ✔️ 🎏 🌥 ✔️ ⚙️ 🛠️ & 📉 👆 📱 📟 & 🐛. - * 🚥 👆 ⚖ Uvicorn, 🔬 ⚫️ 🛡 👸, Hypercorn, ✳, ♒️. 🈸 💽. -* **💃**: - * 🔜 ✔️ ⏭ 🏆 🎭, ⏮️ Uvicorn. 👐, 💃 ⚙️ Uvicorn 🏃. , ⚫️ 🎲 💪 🕴 🤚 "🐌" 🌘 Uvicorn ✔️ 🛠️ 🌅 📟. - * ✋️ ⚫️ 🚚 👆 🧰 🏗 🙅 🕸 🈸, ⏮️ 🕹 ⚓️ 🔛 ➡, ♒️. - * 🚥 👆 ⚖ 💃, 🔬 ⚫️ 🛡 🤣, 🏺, ✳, ♒️. 🕸 🛠️ (⚖️ 🕸). -* **FastAPI**: - * 🎏 🌌 👈 💃 ⚙️ Uvicorn & 🚫🔜 ⏩ 🌘 ⚫️, **FastAPI** ⚙️ 💃, ⚫️ 🚫🔜 ⏩ 🌘 ⚫️. - * FastAPI 🚚 🌅 ⚒ 🔛 🔝 💃. ⚒ 👈 👆 🌖 🕧 💪 🕐❔ 🏗 🔗, 💖 💽 🔬 & 🛠️. & ⚙️ ⚫️, 👆 🤚 🏧 🧾 🆓 (🏧 🧾 🚫 🚮 🌥 🏃‍♂ 🈸, ⚫️ 🏗 🔛 🕴). - * 🚥 👆 🚫 ⚙️ FastAPI & ⚙️ 💃 🔗 (⚖️ ➕1️⃣ 🧰, 💖 🤣, 🏺, 🆘, ♒️) 👆 🔜 ✔️ 🛠️ 🌐 💽 🔬 & 🛠️ 👆. , 👆 🏁 🈸 🔜 ✔️ 🎏 🌥 🚥 ⚫️ 🏗 ⚙️ FastAPI. & 📚 💼, 👉 💽 🔬 & 🛠️ 🦏 💸 📟 ✍ 🈸. - * , ⚙️ FastAPI 👆 ♻ 🛠️ 🕰, 🐛, ⏸ 📟, & 👆 🔜 🎲 🤚 🎏 🎭 (⚖️ 👍) 👆 🔜 🚥 👆 🚫 ⚙️ ⚫️ (👆 🔜 ✔️ 🛠️ ⚫️ 🌐 👆 📟). - * 🚥 👆 ⚖ FastAPI, 🔬 ⚫️ 🛡 🕸 🈸 🛠️ (⚖️ ⚒ 🧰) 👈 🚚 💽 🔬, 🛠️ & 🧾, 💖 🏺-apispec, NestJS, ♨, ♒️. 🛠️ ⏮️ 🛠️ 🏧 💽 🔬, 🛠️ & 🧾. diff --git a/docs/em/docs/deployment/concepts.md b/docs/em/docs/deployment/concepts.md deleted file mode 100644 index bbb017277c..0000000000 --- a/docs/em/docs/deployment/concepts.md +++ /dev/null @@ -1,323 +0,0 @@ -# 🛠️ 🔧 - -🕐❔ 🛠️ **FastAPI** 🈸, ⚖️ 🤙, 🙆 🆎 🕸 🛠️, 📤 📚 🔧 👈 👆 🎲 💅 🔃, & ⚙️ 👫 👆 💪 🔎 **🏆 ☑** 🌌 **🛠️ 👆 🈸**. - -⚠ 🔧: - -* 💂‍♂ - 🇺🇸🔍 -* 🏃‍♂ 🔛 🕴 -* ⏏ -* 🧬 (🔢 🛠️ 🏃) -* 💾 -* ⏮️ 🔁 ⏭ ▶️ - -👥 🔜 👀 ❔ 👫 🔜 📉 **🛠️**. - -🔚, 🏆 🎯 💪 **🍦 👆 🛠️ 👩‍💻** 🌌 👈 **🔐**, **❎ 📉**, & ⚙️ **📊 ℹ** (🖼 🛰 💽/🕹 🎰) ♻ 💪. 👶 - -👤 🔜 💬 👆 🍖 🌖 🔃 👫 **🔧** 📥, & 👈 🔜 🤞 🤝 👆 **🤔** 👆 🔜 💪 💭 ❔ 🛠️ 👆 🛠️ 📶 🎏 🌐, 🎲 **🔮** 🕐 👈 🚫 🔀. - -🤔 👫 🔧, 👆 🔜 💪 **🔬 & 🔧** 🏆 🌌 🛠️ **👆 👍 🔗**. - -⏭ 📃, 👤 🔜 🤝 👆 🌅 **🧱 🍮** 🛠️ FastAPI 🈸. - -✋️ 🔜, ➡️ ✅ 👉 ⚠ **⚛ 💭**. 👫 🔧 ✔ 🙆 🎏 🆎 🕸 🛠️. 👶 - -## 💂‍♂ - 🇺🇸🔍 - -[⏮️ 📃 🔃 🇺🇸🔍](https.md){.internal-link target=_blank} 👥 🇭🇲 🔃 ❔ 🇺🇸🔍 🚚 🔐 👆 🛠️. - -👥 👀 👈 🇺🇸🔍 🛎 🚚 🦲 **🔢** 👆 🈸 💽, **🤝 ❎ 🗳**. - -& 📤 ✔️ 🕳 🈚 **♻ 🇺🇸🔍 📄**, ⚫️ 💪 🎏 🦲 ⚖️ ⚫️ 💪 🕳 🎏. - -### 🖼 🧰 🇺🇸🔍 - -🧰 👆 💪 ⚙️ 🤝 ❎ 🗳: - -* Traefik - * 🔁 🍵 📄 🔕 👶 -* 📥 - * 🔁 🍵 📄 🔕 👶 -* 👌 - * ⏮️ 🔢 🦲 💖 Certbot 📄 🔕 -* ✳ - * ⏮️ 🔢 🦲 💖 Certbot 📄 🔕 -* Kubernetes ⏮️ 🚧 🕹 💖 👌 - * ⏮️ 🔢 🦲 💖 🛂-👨‍💼 📄 🔕 -* 🍵 🔘 ☁ 🐕‍🦺 🍕 👫 🐕‍🦺 (✍ 🔛 👶) - -➕1️⃣ 🎛 👈 👆 💪 ⚙️ **☁ 🐕‍🦺** 👈 🔨 🌖 👷 ✅ ⚒ 🆙 🇺🇸🔍. ⚫️ 💪 ✔️ 🚫 ⚖️ 🈚 👆 🌅, ♒️. ✋️ 👈 💼, 👆 🚫🔜 ✔️ ⚒ 🆙 🤝 ❎ 🗳 👆. - -👤 🔜 🎦 👆 🧱 🖼 ⏭ 📃. - ---- - -⤴️ ⏭ 🔧 🤔 🌐 🔃 📋 🏃 👆 ☑ 🛠️ (✅ Uvicorn). - -## 📋 & 🛠️ - -👥 🔜 💬 📚 🔃 🏃 "**🛠️**", ⚫️ ⚠ ✔️ ☯ 🔃 ⚫️❔ ⚫️ ⛓, & ⚫️❔ 🔺 ⏮️ 🔤 "**📋**". - -### ⚫️❔ 📋 - -🔤 **📋** 🛎 ⚙️ 🔬 📚 👜: - -* **📟** 👈 👆 ✍, **🐍 📁**. -* **📁** 👈 💪 **🛠️** 🏃‍♂ ⚙️, 🖼: `python`, `python.exe` ⚖️ `uvicorn`. -* 🎯 📋 ⏪ ⚫️ **🏃‍♂** 🔛 🏗 ⚙️, ⚙️ 💽, & ♻ 👜 🔛 💾. 👉 🤙 **🛠️**. - -### ⚫️❔ 🛠️ - -🔤 **🛠️** 🛎 ⚙️ 🌖 🎯 🌌, 🕴 🔗 👜 👈 🏃 🏃‍♂ ⚙️ (💖 🏁 ☝ 🔛): - -* 🎯 📋 ⏪ ⚫️ **🏃‍♂** 🔛 🏃‍♂ ⚙️. - * 👉 🚫 🔗 📁, 🚫 📟, ⚫️ 🔗 **🎯** 👜 👈 ➖ **🛠️** & 🔄 🏃‍♂ ⚙️. -* 🙆 📋, 🙆 📟, **💪 🕴 👜** 🕐❔ ⚫️ ➖ **🛠️**. , 🕐❔ 📤 **🛠️ 🏃**. -* 🛠️ 💪 **❎** (⚖️ "💥") 👆, ⚖️ 🏃‍♂ ⚙️. 👈 ☝, ⚫️ ⛔️ 🏃/➖ 🛠️, & ⚫️ 💪 **🙅‍♂ 📏 👜**. -* 🔠 🈸 👈 👆 ✔️ 🏃 🔛 👆 💻 ✔️ 🛠️ ⛅ ⚫️, 🔠 🏃‍♂ 📋, 🔠 🚪, ♒️. & 📤 🛎 📚 🛠️ 🏃 **🎏 🕰** ⏪ 💻 🔛. -* 📤 💪 **💗 🛠️** **🎏 📋** 🏃 🎏 🕰. - -🚥 👆 ✅ 👅 "📋 👨‍💼" ⚖️ "⚙️ 🖥" (⚖️ 🎏 🧰) 👆 🏃‍♂ ⚙️, 👆 🔜 💪 👀 📚 👈 🛠️ 🏃‍♂. - -& , 🖼, 👆 🔜 🎲 👀 👈 📤 💗 🛠️ 🏃 🎏 🖥 📋 (🦎, 💄, 📐, ♒️). 👫 🛎 🏃 1️⃣ 🛠️ 📍 📑, ➕ 🎏 ➕ 🛠️. - - - ---- - -🔜 👈 👥 💭 🔺 🖖 ⚖ **🛠️** & **📋**, ➡️ 😣 💬 🔃 🛠️. - -## 🏃‍♂ 🔛 🕴 - -🌅 💼, 🕐❔ 👆 ✍ 🕸 🛠️, 👆 💚 ⚫️ **🕧 🏃‍♂**, ➡, 👈 👆 👩‍💻 💪 🕧 🔐 ⚫️. 👉 ↗️, 🚥 👆 ✔️ 🎯 🤔 ⚫️❔ 👆 💚 ⚫️ 🏃 🕴 🎯 ⚠, ✋️ 🌅 🕰 👆 💚 ⚫️ 🕧 🏃‍♂ & **💪**. - -### 🛰 💽 - -🕐❔ 👆 ⚒ 🆙 🛰 💽 (☁ 💽, 🕹 🎰, ♒️.) 🙅 👜 👆 💪 🏃 Uvicorn (⚖️ 🎏) ❎, 🎏 🌌 👆 🕐❔ 🛠️ 🌐. - -& ⚫️ 🔜 👷 & 🔜 ⚠ **⏮️ 🛠️**. - -✋️ 🚥 👆 🔗 💽 💸, **🏃‍♂ 🛠️** 🔜 🎲 ☠️. - -& 🚥 💽 ⏏ (🖼 ⏮️ ℹ, ⚖️ 🛠️ ⚪️➡️ ☁ 🐕‍🦺) 👆 🎲 **🏆 🚫 👀 ⚫️**. & ↩️ 👈, 👆 🏆 🚫 💭 👈 👆 ✔️ ⏏ 🛠️ ❎. , 👆 🛠️ 🔜 🚧 ☠️. 👶 - -### 🏃 🔁 🔛 🕴 - -🏢, 👆 🔜 🎲 💚 💽 📋 (✅ Uvicorn) ▶️ 🔁 🔛 💽 🕴, & 🍵 💪 🙆 **🗿 🏥**, ✔️ 🛠️ 🕧 🏃 ⏮️ 👆 🛠️ (✅ Uvicorn 🏃‍♂ 👆 FastAPI 📱). - -### 🎏 📋 - -🏆 👉, 👆 🔜 🛎 ✔️ **🎏 📋** 👈 🔜 ⚒ 💭 👆 🈸 🏃 🔛 🕴. & 📚 💼, ⚫️ 🔜 ⚒ 💭 🎏 🦲 ⚖️ 🈸 🏃, 🖼, 💽. - -### 🖼 🧰 🏃 🕴 - -🖼 🧰 👈 💪 👉 👨‍🏭: - -* ☁ -* Kubernetes -* ☁ ✍ -* ☁ 🐝 📳 -* ✳ -* 👨‍💻 -* 🍵 🔘 ☁ 🐕‍🦺 🍕 👫 🐕‍🦺 -* 🎏... - -👤 🔜 🤝 👆 🌅 🧱 🖼 ⏭ 📃. - -## ⏏ - -🎏 ⚒ 💭 👆 🈸 🏃 🔛 🕴, 👆 🎲 💚 ⚒ 💭 ⚫️ **⏏** ⏮️ ❌. - -### 👥 ⚒ ❌ - -👥, 🗿, ⚒ **❌**, 🌐 🕰. 🖥 🌖 *🕧* ✔️ **🐛** 🕵‍♂ 🎏 🥉. 👶 - -& 👥 👩‍💻 🚧 📉 📟 👥 🔎 👈 🐛 & 👥 🛠️ 🆕 ⚒ (🎲 ❎ 🆕 🐛 💁‍♂️ 👶). - -### 🤪 ❌ 🔁 🍵 - -🕐❔ 🏗 🕸 🔗 ⏮️ FastAPI, 🚥 📤 ❌ 👆 📟, FastAPI 🔜 🛎 🔌 ⚫️ 👁 📨 👈 ⏲ ❌. 🛡 - -👩‍💻 🔜 🤚 **5️⃣0️⃣0️⃣ 🔗 💽 ❌** 👈 📨, ✋️ 🈸 🔜 😣 👷 ⏭ 📨 ↩️ 💥 🍕. - -### 🦏 ❌ - 💥 - -👐, 📤 5️⃣📆 💼 🌐❔ 👥 ✍ 📟 👈 **💥 🎂 🈸** ⚒ Uvicorn & 🐍 💥. 👶 - -& , 👆 🔜 🎲 🚫 💚 🈸 🚧 ☠️ ↩️ 📤 ❌ 1️⃣ 🥉, 👆 🎲 💚 ⚫️ **😣 🏃** 🌘 *➡ 🛠️* 👈 🚫 💔. - -### ⏏ ⏮️ 💥 - -✋️ 👈 💼 ⏮️ 🤙 👎 ❌ 👈 💥 🏃‍♂ **🛠️**, 👆 🔜 💚 🔢 🦲 👈 🈚 **🔁** 🛠️, 🌘 👩‍❤‍👨 🕰... - -/// tip - -...👐 🚥 🎂 🈸 **💥 ⏪** ⚫️ 🎲 🚫 ⚒ 🔑 🚧 🔁 ⚫️ ♾. ✋️ 📚 💼, 👆 🔜 🎲 👀 ⚫️ ⏮️ 🛠️, ⚖️ 🌘 ▶️️ ⏮️ 🛠️. - -➡️ 🎯 🔛 👑 💼, 🌐❔ ⚫️ 💪 💥 🍕 🎯 💼 **🔮**, & ⚫️ ⚒ 🔑 ⏏ ⚫️. - -/// - -👆 🔜 🎲 💚 ✔️ 👜 🈚 🔁 👆 🈸 **🔢 🦲**, ↩️ 👈 ☝, 🎏 🈸 ⏮️ Uvicorn & 🐍 ⏪ 💥, 📤 🕳 🎏 📟 🎏 📱 👈 💪 🕳 🔃 ⚫️. - -### 🖼 🧰 ⏏ 🔁 - -🏆 💼, 🎏 🧰 👈 ⚙️ **🏃 📋 🔛 🕴** ⚙️ 🍵 🏧 **⏏**. - -🖼, 👉 💪 🍵: - -* ☁ -* Kubernetes -* ☁ ✍ -* ☁ 🐝 📳 -* ✳ -* 👨‍💻 -* 🍵 🔘 ☁ 🐕‍🦺 🍕 👫 🐕‍🦺 -* 🎏... - -## 🧬 - 🛠️ & 💾 - -⏮️ FastAPI 🈸, ⚙️ 💽 📋 💖 Uvicorn, 🏃‍♂ ⚫️ 🕐 **1️⃣ 🛠️** 💪 🍦 💗 👩‍💻 🔁. - -✋️ 📚 💼, 👆 🔜 💚 🏃 📚 👨‍🏭 🛠️ 🎏 🕰. - -### 💗 🛠️ - 👨‍🏭 - -🚥 👆 ✔️ 🌅 👩‍💻 🌘 ⚫️❔ 👁 🛠️ 💪 🍵 (🖼 🚥 🕹 🎰 🚫 💁‍♂️ 🦏) & 👆 ✔️ **💗 🐚** 💽 💽, ⤴️ 👆 💪 ✔️ **💗 🛠️** 🏃‍♂ ⏮️ 🎏 🈸 🎏 🕰, & 📎 🌐 📨 👪 👫. - -🕐❔ 👆 🏃 **💗 🛠️** 🎏 🛠️ 📋, 👫 🛎 🤙 **👨‍🏭**. - -### 👨‍🏭 🛠️ & ⛴ - -💭 ⚪️➡️ 🩺 [🔃 🇺🇸🔍](https.md){.internal-link target=_blank} 👈 🕴 1️⃣ 🛠️ 💪 👂 🔛 1️⃣ 🌀 ⛴ & 📢 📢 💽 ❓ - -👉 ☑. - -, 💪 ✔️ **💗 🛠️** 🎏 🕰, 📤 ✔️ **👁 🛠️ 👂 🔛 ⛴** 👈 ⤴️ 📶 📻 🔠 👨‍🏭 🛠️ 🌌. - -### 💾 📍 🛠️ - -🔜, 🕐❔ 📋 📐 👜 💾, 🖼, 🎰 🏫 🏷 🔢, ⚖️ 🎚 ⭕ 📁 🔢, 🌐 👈 **🍴 👄 💾 (💾)** 💽. - -& 💗 🛠️ 🛎 **🚫 💰 🙆 💾**. 👉 ⛓ 👈 🔠 🏃 🛠️ ✔️ 🚮 👍 👜, 🔢, & 💾. & 🚥 👆 😩 ⭕ 💸 💾 👆 📟, **🔠 🛠️** 🔜 🍴 🌓 💸 💾. - -### 💽 💾 - -🖼, 🚥 👆 📟 📐 🎰 🏫 🏷 ⏮️ **1️⃣ 💾 📐**, 🕐❔ 👆 🏃 1️⃣ 🛠️ ⏮️ 👆 🛠️, ⚫️ 🔜 🍴 🌘 1️⃣ 💾 💾. & 🚥 👆 ▶️ **4️⃣ 🛠️** (4️⃣ 👨‍🏭), 🔠 🔜 🍴 1️⃣ 💾 💾. 🌐, 👆 🛠️ 🔜 🍴 **4️⃣ 💾 💾**. - -& 🚥 👆 🛰 💽 ⚖️ 🕹 🎰 🕴 ✔️ 3️⃣ 💾 💾, 🔄 📐 🌅 🌘 4️⃣ 💾 💾 🔜 🤕 ⚠. 👶 - -### 💗 🛠️ - 🖼 - -👉 🖼, 📤 **👨‍💼 🛠️** 👈 ▶️ & 🎛 2️⃣ **👨‍🏭 🛠️**. - -👉 👨‍💼 🛠️ 🔜 🎲 1️⃣ 👂 🔛 **⛴** 📢. & ⚫️ 🔜 📶 🌐 📻 👨‍🏭 🛠️. - -👈 👨‍🏭 🛠️ 🔜 🕐 🏃‍♂ 👆 🈸, 👫 🔜 🎭 👑 📊 📨 **📨** & 📨 **📨**, & 👫 🔜 📐 🕳 👆 🚮 🔢 💾. - - - -& ↗️, 🎏 🎰 🔜 🎲 ✔️ **🎏 🛠️** 🏃 👍, ↖️ ⚪️➡️ 👆 🈸. - -😌 ℹ 👈 🌐 **💽 ⚙️** 🔠 🛠️ 💪 **🪀** 📚 🤭 🕰, ✋️ **💾 (💾)** 🛎 🚧 🌖 ⚖️ 🌘 **⚖**. - -🚥 👆 ✔️ 🛠️ 👈 🔨 ⭐ 💸 📊 🔠 🕰 & 👆 ✔️ 📚 👩‍💻, ⤴️ **💽 🛠️** 🔜 🎲 *⚖* (↩️ 🕧 🔜 🆙 & 🔽 🔜). - -### 🖼 🧬 🧰 & 🎛 - -📤 💪 📚 🎯 🏆 👉, & 👤 🔜 💬 👆 🌅 🔃 🎯 🎛 ⏭ 📃, 🖼 🕐❔ 💬 🔃 ☁ & 📦. - -👑 ⚛ 🤔 👈 📤 ✔️ **👁** 🦲 🚚 **⛴** **📢 📢**. & ⤴️ ⚫️ ✔️ ✔️ 🌌 **📶** 📻 🔁 **🛠️/👨‍🏭**. - -📥 💪 🌀 & 🎛: - -* **🐁** 🛠️ **Uvicorn 👨‍🏭** - * 🐁 🔜 **🛠️ 👨‍💼** 👂 🔛 **📢** & **⛴**, 🧬 🔜 ✔️ **💗 Uvicorn 👨‍🏭 🛠️** -* **Uvicorn** 🛠️ **Uvicorn 👨‍🏭** - * 1️⃣ Uvicorn **🛠️ 👨‍💼** 🔜 👂 🔛 **📢** & **⛴**, & ⚫️ 🔜 ▶️ **💗 Uvicorn 👨‍🏭 🛠️** -* **Kubernetes** & 🎏 📎 **📦 ⚙️** - * 🕳 **☁** 🧽 🔜 👂 🔛 **📢** & **⛴**. 🧬 🔜 ✔️ **💗 📦**, 🔠 ⏮️ **1️⃣ Uvicorn 🛠️** 🏃‍♂ -* **☁ 🐕‍🦺** 👈 🍵 👉 👆 - * ☁ 🐕‍🦺 🔜 🎲 **🍵 🧬 👆**. ⚫️ 🔜 🎲 ➡️ 👆 🔬 **🛠️ 🏃**, ⚖️ **📦 🖼** ⚙️, 🙆 💼, ⚫️ 🔜 🌅 🎲 **👁 Uvicorn 🛠️**, & ☁ 🐕‍🦺 🔜 🈚 🔁 ⚫️. - -/// tip - -🚫 😟 🚥 👫 🏬 🔃 **📦**, ☁, ⚖️ Kubernetes 🚫 ⚒ 📚 🔑. - -👤 🔜 💬 👆 🌅 🔃 📦 🖼, ☁, Kubernetes, ♒️. 🔮 📃: [FastAPI 📦 - ☁](docker.md){.internal-link target=_blank}. - -/// - -## ⏮️ 🔁 ⏭ ▶️ - -📤 📚 💼 🌐❔ 👆 💚 🎭 📶 **⏭ ▶️** 👆 🈸. - -🖼, 👆 💪 💚 🏃 **💽 🛠️**. - -✋️ 🌅 💼, 👆 🔜 💚 🎭 👉 🔁 🕴 **🕐**. - -, 👆 🔜 💚 ✔️ **👁 🛠️** 🎭 👈 **⏮️ 🔁**, ⏭ ▶️ 🈸. - -& 👆 🔜 ✔️ ⚒ 💭 👈 ⚫️ 👁 🛠️ 🏃 👈 ⏮️ 🔁 ** 🚥 ⏮️, 👆 ▶️ **💗 🛠️** (💗 👨‍🏭) 🈸 ⚫️. 🚥 👈 🔁 🏃 **💗 🛠️**, 👫 🔜 **❎** 👷 🏃‍♂ ⚫️ 🔛 **🔗**, & 🚥 📶 🕳 💎 💖 💽 🛠️, 👫 💪 🤕 ⚔ ⏮️ 🔠 🎏. - -↗️, 📤 💼 🌐❔ 📤 🙅‍♂ ⚠ 🏃 ⏮️ 🔁 💗 🕰, 👈 💼, ⚫️ 📚 ⏩ 🍵. - -/// tip - -, ✔️ 🤯 👈 ⚓️ 🔛 👆 🖥, 💼 👆 **5️⃣📆 🚫 💪 🙆 ⏮️ 🔁** ⏭ ▶️ 👆 🈸. - -👈 💼, 👆 🚫🔜 ✔️ 😟 🔃 🙆 👉. 🤷 - -/// - -### 🖼 ⏮️ 🔁 🎛 - -👉 🔜 **🪀 🙇** 🔛 🌌 👆 **🛠️ 👆 ⚙️**, & ⚫️ 🔜 🎲 🔗 🌌 👆 ▶️ 📋, 🚚 ⏏, ♒️. - -📥 💪 💭: - -* "🕑 📦" Kubernetes 👈 🏃 ⏭ 👆 📱 📦 -* 🎉 ✍ 👈 🏃 ⏮️ 🔁 & ⤴️ ▶️ 👆 🈸 - * 👆 🔜 💪 🌌 ▶️/⏏ *👈* 🎉 ✍, 🔍 ❌, ♒️. - -/// tip - -👤 🔜 🤝 👆 🌅 🧱 🖼 🔨 👉 ⏮️ 📦 🔮 📃: [FastAPI 📦 - ☁](docker.md){.internal-link target=_blank}. - -/// - -## ℹ 🛠️ - -👆 💽(Ⓜ) () **ℹ**, 👆 💪 🍴 ⚖️ **⚙️**, ⏮️ 👆 📋, 📊 🕰 🔛 💽, & 💾 💾 💪. - -❔ 🌅 ⚙️ ℹ 👆 💚 😩/♻ ❓ ⚫️ 💪 ⏩ 💭 "🚫 🌅", ✋️ 🌌, 👆 🔜 🎲 💚 🍴 **🌅 💪 🍵 💥**. - -🚥 👆 💸 3️⃣ 💽 ✋️ 👆 ⚙️ 🕴 🐥 🍖 👫 💾 & 💽, 👆 🎲 **🗑 💸** 👶, & 🎲 **🗑 💽 🔦 🏋️** 👶, ♒️. - -👈 💼, ⚫️ 💪 👻 ✔️ 🕴 2️⃣ 💽 & ⚙️ ↕ 🌐 👫 ℹ (💽, 💾, 💾, 🕸 💿, ♒️). - -🔛 🎏 ✋, 🚥 👆 ✔️ 2️⃣ 💽 & 👆 ⚙️ **1️⃣0️⃣0️⃣ 💯 👫 💽 & 💾**, ☝ 1️⃣ 🛠️ 🔜 💭 🌅 💾, & 💽 🔜 ✔️ ⚙️ 💾 "💾" (❔ 💪 💯 🕰 🐌), ⚖️ **💥**. ⚖️ 1️⃣ 🛠️ 💪 💪 📊 & 🔜 ✔️ ⌛ ⏭ 💽 🆓 🔄. - -👉 💼, ⚫️ 🔜 👍 🤚 **1️⃣ ➕ 💽** & 🏃 🛠️ 🔛 ⚫️ 👈 👫 🌐 ✔️ **🥃 💾 & 💽 🕰**. - -📤 🤞 👈 🤔 👆 ✔️ **🌵** ⚙️ 👆 🛠️. 🎲 ⚫️ 🚶 🦠, ⚖️ 🎲 🎏 🐕‍🦺 ⚖️ 🤖 ▶️ ⚙️ ⚫️. & 👆 💪 💚 ✔️ ➕ ℹ 🔒 👈 💼. - -👆 💪 🚮 **❌ 🔢** 🎯, 🖼, 🕳 **🖖 5️⃣0️⃣ 💯 9️⃣0️⃣ 💯** ℹ 🛠️. ☝ 👈 📚 🎲 👑 👜 👆 🔜 💚 ⚖ & ⚙️ ⚒ 👆 🛠️. - -👆 💪 ⚙️ 🙅 🧰 💖 `htop` 👀 💽 & 💾 ⚙️ 👆 💽 ⚖️ 💸 ⚙️ 🔠 🛠️. ⚖️ 👆 💪 ⚙️ 🌖 🏗 ⚖ 🧰, ❔ 5️⃣📆 📎 🤭 💽, ♒️. - -## 🌃 - -👆 ✔️ 👂 📥 👑 🔧 👈 👆 🔜 🎲 💪 ✔️ 🤯 🕐❔ 🤔 ❔ 🛠️ 👆 🈸: - -* 💂‍♂ - 🇺🇸🔍 -* 🏃‍♂ 🔛 🕴 -* ⏏ -* 🧬 (🔢 🛠️ 🏃) -* 💾 -* ⏮️ 🔁 ⏭ ▶️ - -🤔 👉 💭 & ❔ ✔ 👫 🔜 🤝 👆 🤔 💪 ✊ 🙆 🚫 🕐❔ 🛠️ & 🛠️ 👆 🛠️. 👶 - -⏭ 📄, 👤 🔜 🤝 👆 🌅 🧱 🖼 💪 🎛 👆 💪 ⏩. 👶 diff --git a/docs/em/docs/deployment/docker.md b/docs/em/docs/deployment/docker.md deleted file mode 100644 index 2152f1a0ee..0000000000 --- a/docs/em/docs/deployment/docker.md +++ /dev/null @@ -1,731 +0,0 @@ -# FastAPI 📦 - ☁ - -🕐❔ 🛠️ FastAPI 🈸 ⚠ 🎯 🏗 **💾 📦 🖼**. ⚫️ 🛎 🔨 ⚙️ **☁**. 👆 💪 ⤴️ 🛠️ 👈 📦 🖼 1️⃣ 👩‍❤‍👨 💪 🌌. - -⚙️ 💾 📦 ✔️ 📚 📈 ✅ **💂‍♂**, **🔬**, **🦁**, & 🎏. - -/// tip - -🏃 & ⏪ 💭 👉 💩 ❓ 🦘 [`Dockerfile` 🔛 👶](#fastapi). - -/// - -
-📁 🎮 👶 - -```Dockerfile -FROM python:3.9 - -WORKDIR /code - -COPY ./requirements.txt /code/requirements.txt - -RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt - -COPY ./app /code/app - -CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"] - -# If running behind a proxy like Nginx or Traefik add --proxy-headers -# CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80", "--proxy-headers"] -``` - -
- -## ⚫️❔ 📦 - -📦 (✴️ 💾 📦) 📶 **💿** 🌌 📦 🈸 ✅ 🌐 👫 🔗 & 💪 📁 ⏪ 🚧 👫 ❎ ⚪️➡️ 🎏 📦 (🎏 🈸 ⚖️ 🦲) 🎏 ⚙️. - -💾 📦 🏃 ⚙️ 🎏 💾 💾 🦠 (🎰, 🕹 🎰, ☁ 💽, ♒️). 👉 ⛓ 👈 👫 📶 💿 (🔬 🌕 🕹 🎰 👍 🎂 🏃‍♂ ⚙️). - -👉 🌌, 📦 🍴 **🐥 ℹ**, 💸 ⭐ 🏃‍♂ 🛠️ 🔗 (🕹 🎰 🔜 🍴 🌅 🌅). - -📦 ✔️ 👫 👍 **❎** 🏃‍♂ 🛠️ (🛎 1️⃣ 🛠️), 📁 ⚙️, & 🕸, 🔬 🛠️, 💂‍♂, 🛠️, ♒️. - -## ⚫️❔ 📦 🖼 - -**📦** 🏃 ⚪️➡️ **📦 🖼**. - -📦 🖼 **🎻** ⏬ 🌐 📁, 🌐 🔢, & 🔢 📋/📋 👈 🔜 🎁 📦. **🎻** 📥 ⛓ 👈 📦 **🖼** 🚫 🏃, ⚫️ 🚫 ➖ 🛠️, ⚫️ 🕴 📦 📁 & 🗃. - -🔅 "**📦 🖼**" 👈 🏪 🎻 🎚,"**📦**" 🛎 🔗 🏃‍♂ 👐, 👜 👈 ➖ **🛠️**. - -🕐❔ **📦** ▶️ & 🏃‍♂ (▶️ ⚪️➡️ **📦 🖼**) ⚫️ 💪 ✍ ⚖️ 🔀 📁, 🌐 🔢, ♒️. 👈 🔀 🔜 🔀 🕴 👈 📦, ✋️ 🔜 🚫 😣 👽 📦 🖼 (🔜 🚫 🖊 💾). - -📦 🖼 ⭐ **📋** 📁 & 🎚, ✅ `python` & 📁 `main.py`. - -& **📦** ⚫️ (🔅 **📦 🖼**) ☑ 🏃 👐 🖼, ⭐ **🛠️**. 👐, 📦 🏃 🕴 🕐❔ ⚫️ ✔️ **🛠️ 🏃** (& 🛎 ⚫️ 🕴 👁 🛠️). 📦 ⛔️ 🕐❔ 📤 🙅‍♂ 🛠️ 🏃 ⚫️. - -## 📦 🖼 - -☁ ✔️ 1️⃣ 👑 🧰 ✍ & 🛠️ **📦 🖼** & **📦**. - -& 📤 📢 ☁ 🎡 ⏮️ 🏤-⚒ **🛂 📦 🖼** 📚 🧰, 🌐, 💽, & 🈸. - -🖼, 📤 🛂 🐍 🖼. - -& 📤 📚 🎏 🖼 🎏 👜 💖 💽, 🖼: - -* -* -* -* , ♒️. - -⚙️ 🏤-⚒ 📦 🖼 ⚫️ 📶 ⏩ **🌀** & ⚙️ 🎏 🧰. 🖼, 🔄 👅 🆕 💽. 🌅 💼, 👆 💪 ⚙️ **🛂 🖼**, & 🔗 👫 ⏮️ 🌐 🔢. - -👈 🌌, 📚 💼 👆 💪 💡 🔃 📦 & ☁ & 🏤-⚙️ 👈 💡 ⏮️ 📚 🎏 🧰 & 🦲. - -, 👆 🔜 🏃 **💗 📦** ⏮️ 🎏 👜, 💖 💽, 🐍 🈸, 🕸 💽 ⏮️ 😥 🕸 🈸, & 🔗 👫 👯‍♂️ 📨 👫 🔗 🕸. - -🌐 📦 🧾 ⚙️ (💖 ☁ ⚖️ Kubernetes) ✔️ 👫 🕸 ⚒ 🛠️ 🔘 👫. - -## 📦 & 🛠️ - -**📦 🖼** 🛎 🔌 🚮 🗃 🔢 📋 ⚖️ 📋 👈 🔜 🏃 🕐❔ **📦** ▶️ & 🔢 🚶‍♀️ 👈 📋. 📶 🎏 ⚫️❔ 🔜 🚥 ⚫️ 📋 ⏸. - -🕐❔ **📦** ▶️, ⚫️ 🔜 🏃 👈 📋/📋 (👐 👆 💪 🔐 ⚫️ & ⚒ ⚫️ 🏃 🎏 📋/📋). - -📦 🏃 📏 **👑 🛠️** (📋 ⚖️ 📋) 🏃. - -📦 🛎 ✔️ **👁 🛠️**, ✋️ ⚫️ 💪 ▶️ ✳ ⚪️➡️ 👑 🛠️, & 👈 🌌 👆 🔜 ✔️ **💗 🛠️** 🎏 📦. - -✋️ ⚫️ 🚫 💪 ✔️ 🏃‍♂ 📦 🍵 **🌘 1️⃣ 🏃‍♂ 🛠️**. 🚥 👑 🛠️ ⛔️, 📦 ⛔️. - -## 🏗 ☁ 🖼 FastAPI - -🆗, ➡️ 🏗 🕳 🔜 ❗ 👶 - -👤 🔜 🎦 👆 ❔ 🏗 **☁ 🖼** FastAPI **⚪️➡️ 🖌**, ⚓️ 🔛 **🛂 🐍** 🖼. - -👉 ⚫️❔ 👆 🔜 💚 **🏆 💼**, 🖼: - -* ⚙️ **Kubernetes** ⚖️ 🎏 🧰 -* 🕐❔ 🏃‍♂ 🔛 **🍓 👲** -* ⚙️ ☁ 🐕‍🦺 👈 🔜 🏃 📦 🖼 👆, ♒️. - -### 📦 📄 - -👆 🔜 🛎 ✔️ **📦 📄** 👆 🈸 📁. - -⚫️ 🔜 🪀 ✴️ 🔛 🧰 👆 ⚙️ **❎** 👈 📄. - -🌅 ⚠ 🌌 ⚫️ ✔️ 📁 `requirements.txt` ⏮️ 📦 📛 & 👫 ⏬, 1️⃣ 📍 ⏸. - -👆 🔜 ↗️ ⚙️ 🎏 💭 👆 ✍ [🔃 FastAPI ⏬](versions.md){.internal-link target=_blank} ⚒ ↔ ⏬. - -🖼, 👆 `requirements.txt` 💪 👀 💖: - -``` -fastapi>=0.68.0,<0.69.0 -pydantic>=1.8.0,<2.0.0 -uvicorn>=0.15.0,<0.16.0 -``` - -& 👆 🔜 🛎 ❎ 👈 📦 🔗 ⏮️ `pip`, 🖼: - -
- -```console -$ pip install -r requirements.txt ----> 100% -Successfully installed fastapi pydantic uvicorn -``` - -
- -/// info - -📤 🎏 📁 & 🧰 🔬 & ❎ 📦 🔗. - -👤 🔜 🎦 👆 🖼 ⚙️ 🎶 ⏪ 📄 🔛. 👶 - -/// - -### ✍ **FastAPI** 📟 - -* ✍ `app` 📁 & ⛔ ⚫️. -* ✍ 🛁 📁 `__init__.py`. -* ✍ `main.py` 📁 ⏮️: - -```Python -from typing import Union - -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} -``` - -### 📁 - -🔜 🎏 🏗 📁 ✍ 📁 `Dockerfile` ⏮️: - -```{ .dockerfile .annotate } -# (1) -FROM python:3.9 - -# (2) -WORKDIR /code - -# (3) -COPY ./requirements.txt /code/requirements.txt - -# (4) -RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt - -# (5) -COPY ./app /code/app - -# (6) -CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"] -``` - -1️⃣. ▶️ ⚪️➡️ 🛂 🐍 🧢 🖼. - -2️⃣. ⚒ ⏮️ 👷 📁 `/code`. - - 👉 🌐❔ 👥 🔜 🚮 `requirements.txt` 📁 & `app` 📁. - -3️⃣. 📁 📁 ⏮️ 📄 `/code` 📁. - - 📁 **🕴** 📁 ⏮️ 📄 🥇, 🚫 🎂 📟. - - 👉 📁 **🚫 🔀 🛎**, ☁ 🔜 🔍 ⚫️ & ⚙️ **💾** 👉 🔁, 🛠️ 💾 ⏭ 🔁 💁‍♂️. - -4️⃣. ❎ 📦 🔗 📄 📁. - - `--no-cache-dir` 🎛 💬 `pip` 🚫 🖊 ⏬ 📦 🌐, 👈 🕴 🚥 `pip` 🔜 🏃 🔄 ❎ 🎏 📦, ✋️ 👈 🚫 💼 🕐❔ 👷 ⏮️ 📦. - - /// note - - `--no-cache-dir` 🕴 🔗 `pip`, ⚫️ ✔️ 🕳 ⏮️ ☁ ⚖️ 📦. - - /// - - `--upgrade` 🎛 💬 `pip` ♻ 📦 🚥 👫 ⏪ ❎. - - ↩️ ⏮️ 🔁 🖨 📁 💪 🔍 **☁ 💾**, 👉 🔁 🔜 **⚙️ ☁ 💾** 🕐❔ 💪. - - ⚙️ 💾 👉 🔁 🔜 **🖊** 👆 📚 **🕰** 🕐❔ 🏗 🖼 🔄 & 🔄 ⏮️ 🛠️, ↩️ **⏬ & ❎** 🌐 🔗 **🔠 🕰**. - -5️⃣. 📁 `./app` 📁 🔘 `/code` 📁. - - 👉 ✔️ 🌐 📟 ❔ ⚫️❔ **🔀 🌅 🛎** ☁ **💾** 🏆 🚫 ⚙️ 👉 ⚖️ 🙆 **📄 🔁** 💪. - - , ⚫️ ⚠ 🚮 👉 **🏘 🔚** `Dockerfile`, 🔬 📦 🖼 🏗 🕰. - -6️⃣. ⚒ **📋** 🏃 `uvicorn` 💽. - - `CMD` ✊ 📇 🎻, 🔠 👫 🎻 ⚫️❔ 👆 🔜 🆎 📋 ⏸ 👽 🚀. - - 👉 📋 🔜 🏃 ⚪️➡️ **⏮️ 👷 📁**, 🎏 `/code` 📁 👆 ⚒ 🔛 ⏮️ `WORKDIR /code`. - - ↩️ 📋 🔜 ▶️ `/code` & 🔘 ⚫️ 📁 `./app` ⏮️ 👆 📟, **Uvicorn** 🔜 💪 👀 & **🗄** `app` ⚪️➡️ `app.main`. - -/// tip - -📄 ⚫️❔ 🔠 ⏸ 🔨 🖊 🔠 🔢 💭 📟. 👶 - -/// - -👆 🔜 🔜 ✔️ 📁 📊 💖: - -``` -. -├── app -│   ├── __init__.py -│ └── main.py -├── Dockerfile -└── requirements.txt -``` - -#### ⛅ 🤝 ❎ 🗳 - -🚥 👆 🏃‍♂ 👆 📦 ⛅ 🤝 ❎ 🗳 (📐 ⚙) 💖 👌 ⚖️ Traefik, 🚮 🎛 `--proxy-headers`, 👉 🔜 💬 Uvicorn 💙 🎚 📨 👈 🗳 💬 ⚫️ 👈 🈸 🏃 ⛅ 🇺🇸🔍, ♒️. - -```Dockerfile -CMD ["uvicorn", "app.main:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "80"] -``` - -#### ☁ 💾 - -📤 ⚠ 🎱 👉 `Dockerfile`, 👥 🥇 📁 **📁 ⏮️ 🔗 😞**, 🚫 🎂 📟. ➡️ 👤 💬 👆 ⚫️❔ 👈. - -```Dockerfile -COPY ./requirements.txt /code/requirements.txt -``` - -☁ & 🎏 🧰 **🏗** 👉 📦 🖼 **🔁**, 🚮 **1️⃣ 🧽 🔛 🔝 🎏**, ▶️ ⚪️➡️ 🔝 `Dockerfile` & ❎ 🙆 📁 ✍ 🔠 👩‍🌾 `Dockerfile`. - -☁ & 🎏 🧰 ⚙️ **🔗 💾** 🕐❔ 🏗 🖼, 🚥 📁 🚫 🔀 ↩️ 🏁 🕰 🏗 📦 🖼, ⤴️ ⚫️ 🔜 **🏤-⚙️ 🎏 🧽** ✍ 🏁 🕰, ↩️ 🖨 📁 🔄 & 🏗 🆕 🧽 ⚪️➡️ 🖌. - -❎ 📁 📁 🚫 🎯 📉 👜 💁‍♂️ 🌅, ✋️ ↩️ ⚫️ ⚙️ 💾 👈 🔁, ⚫️ 💪 **⚙️ 💾 ⏭ 🔁**. 🖼, ⚫️ 💪 ⚙️ 💾 👩‍🌾 👈 ❎ 🔗 ⏮️: - -```Dockerfile -RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt -``` - -📁 ⏮️ 📦 📄 **🏆 🚫 🔀 🛎**. , 🖨 🕴 👈 📁, ☁ 🔜 💪 **⚙️ 💾** 👈 🔁. - -& ⤴️, ☁ 🔜 💪 **⚙️ 💾 ⏭ 🔁** 👈 ⏬ & ❎ 👈 🔗. & 📥 🌐❔ 👥 **🖊 📚 🕰**. 👶 ...& ❎ 😩 ⌛. 👶 👶 - -⏬ & ❎ 📦 🔗 **💪 ✊ ⏲**, ✋️ ⚙️ **💾** 🔜 **✊ 🥈** 🌅. - -& 👆 🔜 🏗 📦 🖼 🔄 & 🔄 ⏮️ 🛠️ ✅ 👈 👆 📟 🔀 👷, 📤 📚 📈 🕰 👉 🔜 🖊. - -⤴️, 🏘 🔚 `Dockerfile`, 👥 📁 🌐 📟. 👉 ⚫️❔ **🔀 🏆 🛎**, 👥 🚮 ⚫️ 🏘 🔚, ↩️ 🌖 🕧, 🕳 ⏮️ 👉 🔁 🔜 🚫 💪 ⚙️ 💾. - -```Dockerfile -COPY ./app /code/app -``` - -### 🏗 ☁ 🖼 - -🔜 👈 🌐 📁 🥉, ➡️ 🏗 📦 🖼. - -* 🚶 🏗 📁 (🌐❔ 👆 `Dockerfile` , ⚗ 👆 `app` 📁). -* 🏗 👆 FastAPI 🖼: - -
- -```console -$ docker build -t myimage . - ----> 100% -``` - -
- -/// tip - -👀 `.` 🔚, ⚫️ 🌓 `./`, ⚫️ 💬 ☁ 📁 ⚙️ 🏗 📦 🖼. - -👉 💼, ⚫️ 🎏 ⏮️ 📁 (`.`). - -/// - -### ▶️ ☁ 📦 - -* 🏃 📦 ⚓️ 🔛 👆 🖼: - -
- -```console -$ docker run -d --name mycontainer -p 80:80 myimage -``` - -
- -## ✅ ⚫️ - -👆 🔜 💪 ✅ ⚫️ 👆 ☁ 📦 📛, 🖼: http://192.168.99.100/items/5?q=somequery ⚖️ http://127.0.0.1/items/5?q=somequery (⚖️ 🌓, ⚙️ 👆 ☁ 🦠). - -👆 🔜 👀 🕳 💖: - -```JSON -{"item_id": 5, "q": "somequery"} -``` - -## 🎓 🛠️ 🩺 - -🔜 👆 💪 🚶 http://192.168.99.100/docs ⚖️ http://127.0.0.1/docs (⚖️ 🌓, ⚙️ 👆 ☁ 🦠). - -👆 🔜 👀 🏧 🎓 🛠️ 🧾 (🚚 🦁 🎚): - -![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) - -## 🎛 🛠️ 🩺 - -& 👆 💪 🚶 http://192.168.99.100/redoc ⚖️ http://127.0.0.1/redoc (⚖️ 🌓, ⚙️ 👆 ☁ 🦠). - -👆 🔜 👀 🎛 🏧 🧾 (🚚 📄): - -![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) - -## 🏗 ☁ 🖼 ⏮️ 👁-📁 FastAPI - -🚥 👆 FastAPI 👁 📁, 🖼, `main.py` 🍵 `./app` 📁, 👆 📁 📊 💪 👀 💖 👉: - -``` -. -├── Dockerfile -├── main.py -└── requirements.txt -``` - -⤴️ 👆 🔜 ✔️ 🔀 🔗 ➡ 📁 📁 🔘 `Dockerfile`: - -```{ .dockerfile .annotate hl_lines="10 13" } -FROM python:3.9 - -WORKDIR /code - -COPY ./requirements.txt /code/requirements.txt - -RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt - -# (1) -COPY ./main.py /code/ - -# (2) -CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "80"] -``` - -1️⃣. 📁 `main.py` 📁 `/code` 📁 🔗 (🍵 🙆 `./app` 📁). - -2️⃣. 🏃 Uvicorn & 💬 ⚫️ 🗄 `app` 🎚 ⚪️➡️ `main` (↩️ 🏭 ⚪️➡️ `app.main`). - -⤴️ 🔆 Uvicorn 📋 ⚙️ 🆕 🕹 `main` ↩️ `app.main` 🗄 FastAPI 🎚 `app`. - -## 🛠️ 🔧 - -➡️ 💬 🔄 🔃 🎏 [🛠️ 🔧](concepts.md){.internal-link target=_blank} ⚖ 📦. - -📦 ✴️ 🧰 📉 🛠️ **🏗 & 🛠️** 🈸, ✋️ 👫 🚫 🛠️ 🎯 🎯 🍵 👉 **🛠️ 🔧**, & 📤 📚 💪 🎛. - -**👍 📰** 👈 ⏮️ 🔠 🎏 🎛 📤 🌌 📔 🌐 🛠️ 🔧. 👶 - -➡️ 📄 👉 **🛠️ 🔧** ⚖ 📦: - -* 🇺🇸🔍 -* 🏃‍♂ 🔛 🕴 -* ⏏ -* 🧬 (🔢 🛠️ 🏃) -* 💾 -* ⏮️ 🔁 ⏭ ▶️ - -## 🇺🇸🔍 - -🚥 👥 🎯 🔛 **📦 🖼** FastAPI 🈸 (& ⏪ 🏃‍♂ **📦**), 🇺🇸🔍 🛎 🔜 🍵 **🗜** ➕1️⃣ 🧰. - -⚫️ 💪 ➕1️⃣ 📦, 🖼 ⏮️ Traefik, 🚚 **🇺🇸🔍** & **🏧** 🛠️ **📄**. - -/// tip - -Traefik ✔️ 🛠️ ⏮️ ☁, Kubernetes, & 🎏, ⚫️ 📶 ⏩ ⚒ 🆙 & 🔗 🇺🇸🔍 👆 📦 ⏮️ ⚫️. - -/// - -👐, 🇺🇸🔍 💪 🍵 ☁ 🐕‍🦺 1️⃣ 👫 🐕‍🦺 (⏪ 🏃 🈸 📦). - -## 🏃‍♂ 🔛 🕴 & ⏏ - -📤 🛎 ➕1️⃣ 🧰 🈚 **▶️ & 🏃‍♂** 👆 📦. - -⚫️ 💪 **☁** 🔗, **☁ ✍**, **Kubernetes**, **☁ 🐕‍🦺**, ♒️. - -🌅 (⚖️ 🌐) 💼, 📤 🙅 🎛 🛠️ 🏃 📦 🔛 🕴 & 🛠️ ⏏ 🔛 ❌. 🖼, ☁, ⚫️ 📋 ⏸ 🎛 `--restart`. - -🍵 ⚙️ 📦, ⚒ 🈸 🏃 🔛 🕴 & ⏮️ ⏏ 💪 ⚠ & ⚠. ✋️ 🕐❔ **👷 ⏮️ 📦** 🌅 💼 👈 🛠️ 🔌 🔢. 👶 - -## 🧬 - 🔢 🛠️ - -🚥 👆 ✔️ 🌑 🎰 ⏮️ **☁**, ☁ 🐝 📳, 🖖, ⚖️ ➕1️⃣ 🎏 🏗 ⚙️ 🛠️ 📎 📦 🔛 💗 🎰, ⤴️ 👆 🔜 🎲 💚 **🍵 🧬** **🌑 🎚** ↩️ ⚙️ **🛠️ 👨‍💼** (💖 🐁 ⏮️ 👨‍🏭) 🔠 📦. - -1️⃣ 📚 📎 📦 🧾 ⚙️ 💖 Kubernetes 🛎 ✔️ 🛠️ 🌌 🚚 **🧬 📦** ⏪ 🔗 **📐 ⚖** 📨 📨. 🌐 **🌑 🎚**. - -📚 💼, 👆 🔜 🎲 💚 🏗 **☁ 🖼 ⚪️➡️ 🖌** [🔬 🔛](#_6), ❎ 👆 🔗, & 🏃‍♂ **👁 Uvicorn 🛠️** ↩️ 🏃‍♂ 🕳 💖 🐁 ⏮️ Uvicorn 👨‍🏭. - -### 📐 ⚙ - -🕐❔ ⚙️ 📦, 👆 🔜 🛎 ✔️ 🦲 **👂 🔛 👑 ⛴**. ⚫️ 💪 🎲 ➕1️⃣ 📦 👈 **🤝 ❎ 🗳** 🍵 **🇺🇸🔍** ⚖️ 🎏 🧰. - -👉 🦲 🔜 ✊ **📐** 📨 & 📎 👈 👪 👨‍🏭 (🤞) **⚖** 🌌, ⚫️ 🛎 🤙 **📐 ⚙**. - -/// tip - -🎏 **🤝 ❎ 🗳** 🦲 ⚙️ 🇺🇸🔍 🔜 🎲 **📐 ⚙**. - -/// - -& 🕐❔ 👷 ⏮️ 📦, 🎏 ⚙️ 👆 ⚙️ ▶️ & 🛠️ 👫 🔜 ⏪ ✔️ 🔗 🧰 📶 **🕸 📻** (✅ 🇺🇸🔍 📨) ⚪️➡️ 👈 **📐 ⚙** (👈 💪 **🤝 ❎ 🗳**) 📦(Ⓜ) ⏮️ 👆 📱. - -### 1️⃣ 📐 ⚙ - 💗 👨‍🏭 📦 - -🕐❔ 👷 ⏮️ **Kubernetes** ⚖️ 🎏 📎 📦 🧾 ⚙️, ⚙️ 👫 🔗 🕸 🛠️ 🔜 ✔ 👁 **📐 ⚙** 👈 👂 🔛 👑 **⛴** 📶 📻 (📨) 🎲 **💗 📦** 🏃 👆 📱. - -🔠 👫 📦 🏃‍♂ 👆 📱 🔜 🛎 ✔️ **1️⃣ 🛠️** (✅ Uvicorn 🛠️ 🏃 👆 FastAPI 🈸). 👫 🔜 🌐 **🌓 📦**, 🏃‍♂ 🎏 👜, ✋️ 🔠 ⏮️ 🚮 👍 🛠️, 💾, ♒️. 👈 🌌 👆 🔜 ✊ 📈 **🛠️** **🎏 🐚** 💽, ⚖️ **🎏 🎰**. - -& 📎 📦 ⚙️ ⏮️ **📐 ⚙** 🔜 **📎 📨** 🔠 1️⃣ 📦 ⏮️ 👆 📱 **🔄**. , 🔠 📨 💪 🍵 1️⃣ 💗 **🔁 📦** 🏃 👆 📱. - -& 🛎 👉 **📐 ⚙** 🔜 💪 🍵 📨 👈 🚶 *🎏* 📱 👆 🌑 (✅ 🎏 🆔, ⚖️ 🔽 🎏 📛 ➡ 🔡), & 🔜 📶 👈 📻 ▶️️ 📦 *👈 🎏* 🈸 🏃‍♂ 👆 🌑. - -### 1️⃣ 🛠️ 📍 📦 - -👉 🆎 😐, 👆 🎲 🔜 💚 ✔️ **👁 (Uvicorn) 🛠️ 📍 📦**, 👆 🔜 ⏪ 🚚 🧬 🌑 🎚. - -, 👉 💼, 👆 **🔜 🚫** 💚 ✔️ 🛠️ 👨‍💼 💖 🐁 ⏮️ Uvicorn 👨‍🏭, ⚖️ Uvicorn ⚙️ 🚮 👍 Uvicorn 👨‍🏭. 👆 🔜 💚 ✔️ **👁 Uvicorn 🛠️** 📍 📦 (✋️ 🎲 💗 📦). - -✔️ ➕1️⃣ 🛠️ 👨‍💼 🔘 📦 (🔜 ⏮️ 🐁 ⚖️ Uvicorn 🛠️ Uvicorn 👨‍🏭) 🔜 🕴 🚮 **🙃 🔀** 👈 👆 🌅 🎲 ⏪ ✊ 💅 ⏮️ 👆 🌑 ⚙️. - -### 📦 ⏮️ 💗 🛠️ & 🎁 💼 - -↗️, 📤 **🎁 💼** 🌐❔ 👆 💪 💚 ✔️ **📦** ⏮️ **🐁 🛠️ 👨‍💼** ▶️ 📚 **Uvicorn 👨‍🏭 🛠️** 🔘. - -📚 💼, 👆 💪 ⚙️ **🛂 ☁ 🖼** 👈 🔌 **🐁** 🛠️ 👨‍💼 🏃‍♂ 💗 **Uvicorn 👨‍🏭 🛠️**, & 🔢 ⚒ 🔆 🔢 👨‍🏭 ⚓️ 🔛 ⏮️ 💽 🐚 🔁. 👤 🔜 💬 👆 🌅 🔃 ⚫️ 🔛 [🛂 ☁ 🖼 ⏮️ 🐁 - Uvicorn](#-uvicorn). - -📥 🖼 🕐❔ 👈 💪 ⚒ 🔑: - -#### 🙅 📱 - -👆 💪 💚 🛠️ 👨‍💼 📦 🚥 👆 🈸 **🙅 🥃** 👈 👆 🚫 💪 (🐥 🚫) 👌-🎶 🔢 🛠️ 💁‍♂️ 🌅, & 👆 💪 ⚙️ 🏧 🔢 (⏮️ 🛂 ☁ 🖼), & 👆 🏃‍♂ ⚫️ 🔛 **👁 💽**, 🚫 🌑. - -#### ☁ ✍ - -👆 💪 🛠️ **👁 💽** (🚫 🌑) ⏮️ **☁ ✍**, 👆 🚫🔜 ✔️ ⏩ 🌌 🛠️ 🧬 📦 (⏮️ ☁ ✍) ⏪ 🛡 🔗 🕸 & **📐 ⚖**. - -⤴️ 👆 💪 💚 ✔️ **👁 📦** ⏮️ **🛠️ 👨‍💼** ▶️ **📚 👨‍🏭 🛠️** 🔘. - -#### 🤴 & 🎏 🤔 - -👆 💪 ✔️ **🎏 🤔** 👈 🔜 ⚒ ⚫️ ⏩ ✔️ **👁 📦** ⏮️ **💗 🛠️** ↩️ ✔️ **💗 📦** ⏮️ **👁 🛠️** 🔠 👫. - -🖼 (🪀 🔛 👆 🖥) 👆 💪 ✔️ 🧰 💖 🤴 🏭 🎏 📦 👈 🔜 ✔️ 🔐 **🔠 📨** 👈 👟. - -👉 💼, 🚥 👆 ✔️ **💗 📦**, 🔢, 🕐❔ 🤴 👟 **✍ ⚖**, ⚫️ 🔜 🤚 🕐 **👁 📦 🔠 🕰** (📦 👈 🍵 👈 🎯 📨), ↩️ 🤚 **📈 ⚖** 🌐 🔁 📦. - -⤴️, 👈 💼, ⚫️ 💪 🙅 ✔️ **1️⃣ 📦** ⏮️ **💗 🛠️**, & 🇧🇿 🧰 (✅ 🤴 🏭) 🔛 🎏 📦 📈 🤴 ⚖ 🌐 🔗 🛠️ & 🎦 👈 ⚖ 🔛 👈 👁 📦. - ---- - -👑 ☝, **👌** 👉 **🚫 ✍ 🗿** 👈 👆 ✔️ 😄 ⏩. 👆 💪 ⚙️ 👫 💭 **🔬 👆 👍 ⚙️ 💼** & 💭 ⚫️❔ 👍 🎯 👆 ⚙️, ✅ 👅 ❔ 🛠️ 🔧: - -* 💂‍♂ - 🇺🇸🔍 -* 🏃‍♂ 🔛 🕴 -* ⏏ -* 🧬 (🔢 🛠️ 🏃) -* 💾 -* ⏮️ 🔁 ⏭ ▶️ - -## 💾 - -🚥 👆 🏃 **👁 🛠️ 📍 📦** 👆 🔜 ✔️ 🌅 ⚖️ 🌘 👍-🔬, ⚖, & 📉 💸 💾 🍴 🔠 👈 📦 (🌅 🌘 1️⃣ 🚥 👫 🔁). - -& ⤴️ 👆 💪 ⚒ 👈 🎏 💾 📉 & 📄 👆 📳 👆 📦 🧾 ⚙️ (🖼 **Kubernetes**). 👈 🌌 ⚫️ 🔜 💪 **🔁 📦** **💪 🎰** ✊ 🔘 🏧 💸 💾 💪 👫, & 💸 💪 🎰 🌑. - -🚥 👆 🈸 **🙅**, 👉 🔜 🎲 **🚫 ⚠**, & 👆 💪 🚫 💪 ✔ 🏋️ 💾 📉. ✋️ 🚥 👆 **⚙️ 📚 💾** (🖼 ⏮️ **🎰 🏫** 🏷), 👆 🔜 ✅ ❔ 🌅 💾 👆 😩 & 🔆 **🔢 📦** 👈 🏃 **🔠 🎰** (& 🎲 🚮 🌖 🎰 👆 🌑). - -🚥 👆 🏃 **💗 🛠️ 📍 📦** (🖼 ⏮️ 🛂 ☁ 🖼) 👆 🔜 ✔️ ⚒ 💭 👈 🔢 🛠️ ▶️ 🚫 **🍴 🌖 💾** 🌘 ⚫️❔ 💪. - -## ⏮️ 🔁 ⏭ ▶️ & 📦 - -🚥 👆 ⚙️ 📦 (✅ ☁, Kubernetes), ⤴️ 📤 2️⃣ 👑 🎯 👆 💪 ⚙️. - -### 💗 📦 - -🚥 👆 ✔️ **💗 📦**, 🎲 🔠 1️⃣ 🏃 **👁 🛠️** (🖼, **Kubernetes** 🌑), ⤴️ 👆 🔜 🎲 💚 ✔️ **🎏 📦** 🔨 👷 **⏮️ 📶** 👁 📦, 🏃 👁 🛠️, **⏭** 🏃 🔁 👨‍🏭 📦. - -/// info - -🚥 👆 ⚙️ Kubernetes, 👉 🔜 🎲 🕑 📦. - -/// - -🚥 👆 ⚙️ 💼 📤 🙅‍♂ ⚠ 🏃‍♂ 👈 ⏮️ 📶 **💗 🕰 🔗** (🖼 🚥 👆 🚫 🏃 💽 🛠️, ✋️ ✅ 🚥 💽 🔜), ⤴️ 👆 💪 🚮 👫 🔠 📦 ▶️️ ⏭ ▶️ 👑 🛠️. - -### 👁 📦 - -🚥 👆 ✔️ 🙅 🖥, ⏮️ **👁 📦** 👈 ⤴️ ▶️ 💗 **👨‍🏭 🛠️** (⚖️ 1️⃣ 🛠️), ⤴️ 👆 💪 🏃 👈 ⏮️ 🔁 🎏 📦, ▶️️ ⏭ ▶️ 🛠️ ⏮️ 📱. 🛂 ☁ 🖼 🐕‍🦺 👉 🔘. - -## 🛂 ☁ 🖼 ⏮️ 🐁 - Uvicorn - -📤 🛂 ☁ 🖼 👈 🔌 🐁 🏃‍♂ ⏮️ Uvicorn 👨‍🏭, ℹ ⏮️ 📃: [💽 👨‍🏭 - 🐁 ⏮️ Uvicorn](server-workers.md){.internal-link target=_blank}. - -👉 🖼 🔜 ⚠ ✴️ ⚠ 🔬 🔛: [📦 ⏮️ 💗 🛠️ & 🎁 💼](#_18). - -* tiangolo/uvicorn-🐁-fastapi. - -/// warning - -📤 ↕ 🤞 👈 👆 **🚫** 💪 👉 🧢 🖼 ⚖️ 🙆 🎏 🎏 1️⃣, & 🔜 👻 📆 🏗 🖼 ⚪️➡️ 🖌 [🔬 🔛: 🏗 ☁ 🖼 FastAPI](#fastapi). - -/// - -👉 🖼 ✔️ **🚘-📳** 🛠️ 🔌 ⚒ **🔢 👨‍🏭 🛠️** ⚓️ 🔛 💽 🐚 💪. - -⚫️ ✔️ **🤔 🔢**, ✋️ 👆 💪 🔀 & ℹ 🌐 📳 ⏮️ **🌐 🔢** ⚖️ 📳 📁. - -⚫️ 🐕‍🦺 🏃 **⏮️ 🔁 ⏭ ▶️** ⏮️ ✍. - -/// tip - -👀 🌐 📳 & 🎛, 🚶 ☁ 🖼 📃: Tiangolo/uvicorn-🐁-fastapi. - -/// - -### 🔢 🛠️ 🔛 🛂 ☁ 🖼 - -**🔢 🛠️** 🔛 👉 🖼 **📊 🔁** ⚪️➡️ 💽 **🐚** 💪. - -👉 ⛓ 👈 ⚫️ 🔜 🔄 **🗜** 🌅 **🎭** ⚪️➡️ 💽 💪. - -👆 💪 🔆 ⚫️ ⏮️ 📳 ⚙️ **🌐 🔢**, ♒️. - -✋️ ⚫️ ⛓ 👈 🔢 🛠️ 🪀 🔛 💽 📦 🏃, **💸 💾 🍴** 🔜 🪀 🔛 👈. - -, 🚥 👆 🈸 🍴 📚 💾 (🖼 ⏮️ 🎰 🏫 🏷), & 👆 💽 ✔️ 📚 💽 🐚 **✋️ 🐥 💾**, ⤴️ 👆 📦 💪 🔚 🆙 🔄 ⚙️ 🌅 💾 🌘 ⚫️❔ 💪, & 🤕 🎭 📚 (⚖️ 💥). 👶 - -### ✍ `Dockerfile` - -📥 ❔ 👆 🔜 ✍ `Dockerfile` ⚓️ 🔛 👉 🖼: - -```Dockerfile -FROM tiangolo/uvicorn-gunicorn-fastapi:python3.9 - -COPY ./requirements.txt /app/requirements.txt - -RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt - -COPY ./app /app -``` - -### 🦏 🈸 - -🚥 👆 ⏩ 📄 🔃 🏗 [🦏 🈸 ⏮️ 💗 📁](../tutorial/bigger-applications.md){.internal-link target=_blank}, 👆 `Dockerfile` 💪 ↩️ 👀 💖: - -```Dockerfile hl_lines="7" -FROM tiangolo/uvicorn-gunicorn-fastapi:python3.9 - -COPY ./requirements.txt /app/requirements.txt - -RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt - -COPY ./app /app/app -``` - -### 🕐❔ ⚙️ - -👆 🔜 🎲 **🚫** ⚙️ 👉 🛂 🧢 🖼 (⚖️ 🙆 🎏 🎏 1️⃣) 🚥 👆 ⚙️ **Kubernetes** (⚖️ 🎏) & 👆 ⏪ ⚒ **🧬** 🌑 🎚, ⏮️ 💗 **📦**. 📚 💼, 👆 👍 📆 **🏗 🖼 ⚪️➡️ 🖌** 🔬 🔛: [🏗 ☁ 🖼 FastAPI](#fastapi). - -👉 🖼 🔜 ⚠ ✴️ 🎁 💼 🔬 🔛 [📦 ⏮️ 💗 🛠️ & 🎁 💼](#_18). 🖼, 🚥 👆 🈸 **🙅 🥃** 👈 ⚒ 🔢 🔢 🛠️ ⚓️ 🔛 💽 👷 👍, 👆 🚫 💚 😥 ⏮️ ❎ 🛠️ 🧬 🌑 🎚, & 👆 🚫 🏃 🌅 🌘 1️⃣ 📦 ⏮️ 👆 📱. ⚖️ 🚥 👆 🛠️ ⏮️ **☁ ✍**, 🏃 🔛 👁 💽, ♒️. - -## 🛠️ 📦 🖼 - -⏮️ ✔️ 📦 (☁) 🖼 📤 📚 🌌 🛠️ ⚫️. - -🖼: - -* ⏮️ **☁ ✍** 👁 💽 -* ⏮️ **Kubernetes** 🌑 -* ⏮️ ☁ 🐝 📳 🌑 -* ⏮️ ➕1️⃣ 🧰 💖 🖖 -* ⏮️ ☁ 🐕‍🦺 👈 ✊ 👆 📦 🖼 & 🛠️ ⚫️ - -## ☁ 🖼 ⏮️ 🎶 - -🚥 👆 ⚙️ 🎶 🛠️ 👆 🏗 🔗, 👆 💪 ⚙️ ☁ 👁-▶️ 🏗: - -```{ .dockerfile .annotate } -# (1) -FROM python:3.9 as requirements-stage - -# (2) -WORKDIR /tmp - -# (3) -RUN pip install poetry - -# (4) -COPY ./pyproject.toml ./poetry.lock* /tmp/ - -# (5) -RUN poetry export -f requirements.txt --output requirements.txt --without-hashes - -# (6) -FROM python:3.9 - -# (7) -WORKDIR /code - -# (8) -COPY --from=requirements-stage /tmp/requirements.txt /code/requirements.txt - -# (9) -RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt - -# (10) -COPY ./app /code/app - -# (11) -CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"] -``` - -1️⃣. 👉 🥇 ▶️, ⚫️ 🌟 `requirements-stage`. - -2️⃣. ⚒ `/tmp` ⏮️ 👷 📁. - - 📥 🌐❔ 👥 🔜 🏗 📁 `requirements.txt` - -3️⃣. ❎ 🎶 👉 ☁ ▶️. - -4️⃣. 📁 `pyproject.toml` & `poetry.lock` 📁 `/tmp` 📁. - - ↩️ ⚫️ ⚙️ `./poetry.lock*` (▶️ ⏮️ `*`), ⚫️ 🏆 🚫 💥 🚥 👈 📁 🚫 💪. - -5️⃣. 🏗 `requirements.txt` 📁. - -6️⃣. 👉 🏁 ▶️, 🕳 📥 🔜 🛡 🏁 📦 🖼. - -7️⃣. ⚒ ⏮️ 👷 📁 `/code`. - -8️⃣. 📁 `requirements.txt` 📁 `/code` 📁. - - 👉 📁 🕴 🖖 ⏮️ ☁ ▶️, 👈 ⚫️❔ 👥 ⚙️ `--from-requirements-stage` 📁 ⚫️. - -9️⃣. ❎ 📦 🔗 🏗 `requirements.txt` 📁. - -1️⃣0️⃣. 📁 `app` 📁 `/code` 📁. - -1️⃣1️⃣. 🏃 `uvicorn` 📋, 💬 ⚫️ ⚙️ `app` 🎚 🗄 ⚪️➡️ `app.main`. - -/// tip - -🖊 💭 🔢 👀 ⚫️❔ 🔠 ⏸ 🔨. - -/// - -**☁ ▶️** 🍕 `Dockerfile` 👈 👷 **🍕 📦 🖼** 👈 🕴 ⚙️ 🏗 📁 ⚙️ ⏪. - -🥇 ▶️ 🔜 🕴 ⚙️ **❎ 🎶** & **🏗 `requirements.txt`** ⏮️ 👆 🏗 🔗 ⚪️➡️ 🎶 `pyproject.toml` 📁. - -👉 `requirements.txt` 📁 🔜 ⚙️ ⏮️ `pip` ⏪ **⏭ ▶️**. - -🏁 📦 🖼 **🕴 🏁 ▶️** 🛡. ⏮️ ▶️(Ⓜ) 🔜 ❎. - -🕐❔ ⚙️ 🎶, ⚫️ 🔜 ⚒ 🔑 ⚙️ **☁ 👁-▶️ 🏗** ↩️ 👆 🚫 🤙 💪 ✔️ 🎶 & 🚮 🔗 ❎ 🏁 📦 🖼, 👆 **🕴 💪** ✔️ 🏗 `requirements.txt` 📁 ❎ 👆 🏗 🔗. - -⤴️ ⏭ (& 🏁) ▶️ 👆 🔜 🏗 🖼 🌅 ⚖️ 🌘 🎏 🌌 🔬 ⏭. - -### ⛅ 🤝 ❎ 🗳 - 🎶 - -🔄, 🚥 👆 🏃‍♂ 👆 📦 ⛅ 🤝 ❎ 🗳 (📐 ⚙) 💖 👌 ⚖️ Traefik, 🚮 🎛 `--proxy-headers` 📋: - -```Dockerfile -CMD ["uvicorn", "app.main:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "80"] -``` - -## 🌃 - -⚙️ 📦 ⚙️ (✅ ⏮️ **☁** & **Kubernetes**) ⚫️ ▶️️ 📶 🎯 🍵 🌐 **🛠️ 🔧**: - -* 🇺🇸🔍 -* 🏃‍♂ 🔛 🕴 -* ⏏ -* 🧬 (🔢 🛠️ 🏃) -* 💾 -* ⏮️ 🔁 ⏭ ▶️ - -🌅 💼, 👆 🎲 🏆 🚫 💚 ⚙️ 🙆 🧢 🖼, & ↩️ **🏗 📦 🖼 ⚪️➡️ 🖌** 1️⃣ ⚓️ 🔛 🛂 🐍 ☁ 🖼. - -✊ 💅 **✔** 👩‍🌾 `Dockerfile` & **☁ 💾** 👆 💪 **📉 🏗 🕰**, 📉 👆 📈 (& ❎ 😩). 👶 - -🎯 🎁 💼, 👆 💪 💚 ⚙️ 🛂 ☁ 🖼 FastAPI. 👶 diff --git a/docs/em/docs/deployment/https.md b/docs/em/docs/deployment/https.md deleted file mode 100644 index 6d2641a92c..0000000000 --- a/docs/em/docs/deployment/https.md +++ /dev/null @@ -1,199 +0,0 @@ -# 🔃 🇺🇸🔍 - -⚫️ ⏩ 🤔 👈 🇺🇸🔍 🕳 👈 "🛠️" ⚖️ 🚫. - -✋️ ⚫️ 🌌 🌖 🏗 🌘 👈. - -/// tip - -🚥 👆 🏃 ⚖️ 🚫 💅, 😣 ⏮️ ⏭ 📄 🔁 🔁 👩‍🌾 ⚒ 🌐 🆙 ⏮️ 🎏 ⚒. - -/// - -**💡 🔰 🇺🇸🔍**, ⚪️➡️ 🏬 🤔, ✅ https://howhttps.works/. - -🔜, ⚪️➡️ **👩‍💻 🤔**, 📥 📚 👜 ✔️ 🤯 ⏪ 💭 🔃 🇺🇸🔍: - -* 🇺🇸🔍, **💽** 💪 **✔️ "📄"** 🏗 **🥉 🥳**. - * 📚 📄 🤙 **🏆** ⚪️➡️ 🥉 🥳, 🚫 "🏗". -* 📄 ✔️ **1️⃣2️⃣🗓️**. - * 👫 **🕛**. - * & ⤴️ 👫 💪 **♻**, **🏆 🔄** ⚪️➡️ 🥉 🥳. -* 🔐 🔗 🔨 **🕸 🎚**. - * 👈 1️⃣ 🧽 **🔛 🇺🇸🔍**. - * , **📄 & 🔐** 🍵 🔨 **⏭ 🇺🇸🔍**. -* **🕸 🚫 💭 🔃 "🆔"**. 🕴 🔃 📢 📢. - * ℹ 🔃 **🎯 🆔** 📨 🚶 **🇺🇸🔍 💽**. -* **🇺🇸🔍 📄** "✔" **🎯 🆔**, ✋️ 🛠️ & 🔐 🔨 🕸 🎚, **⏭ 💭** ❔ 🆔 ➖ 🙅 ⏮️. -* **🔢**, 👈 🔜 ⛓ 👈 👆 💪 🕴 ✔️ **1️⃣ 🇺🇸🔍 📄 📍 📢 📢**. - * 🙅‍♂ 🤔 ❔ 🦏 👆 💽 ⚖️ ❔ 🤪 🔠 🈸 👆 ✔️ 🔛 ⚫️ 💪. - * 📤 **⚗** 👉, 👐. -* 📤 **↔** **🤝** 🛠️ (1️⃣ 🚚 🔐 🕸 🎚, ⏭ 🇺🇸🔍) 🤙 **👲**. - * 👉 👲 ↔ ✔ 1️⃣ 👁 💽 (⏮️ **👁 📢 📢**) ✔️ **📚 🇺🇸🔍 📄** & 🍦 **💗 🇺🇸🔍 🆔/🈸**. - * 👉 👷, **👁** 🦲 (📋) 🏃 🔛 💽, 👂 🔛 **📢 📢 📢**, 🔜 ✔️ **🌐 🇺🇸🔍 📄** 💽. -* **⏮️** 🏆 🔐 🔗, 📻 🛠️ **🇺🇸🔍**. - * 🎚 **🗜**, ✋️ 👫 ➖ 📨 ⏮️ **🇺🇸🔍 🛠️**. - -⚫️ ⚠ 💡 ✔️ **1️⃣ 📋/🇺🇸🔍 💽** 🏃 🔛 💽 (🎰, 🦠, ♒️.) & **🛠️ 🌐 🇺🇸🔍 🍕**: 📨 **🗜 🇺🇸🔍 📨**, 📨 **🗜 🇺🇸🔍 📨** ☑ 🇺🇸🔍 🈸 🏃 🎏 💽 ( **FastAPI** 🈸, 👉 💼), ✊ **🇺🇸🔍 📨** ⚪️➡️ 🈸, **🗜 ⚫️** ⚙️ ☑ **🇺🇸🔍 📄** & 📨 ⚫️ 🔙 👩‍💻 ⚙️ **🇺🇸🔍**. 👉 💽 🛎 🤙 **🤝 ❎ 🗳**. - -🎛 👆 💪 ⚙️ 🤝 ❎ 🗳: - -* Traefik (👈 💪 🍵 📄 🔕) -* 📥 (👈 💪 🍵 📄 🔕) -* 👌 -* ✳ - -## ➡️ 🗜 - -⏭ ➡️ 🗜, 👫 **🇺🇸🔍 📄** 💲 💙 🥉 🥳. - -🛠️ 📎 1️⃣ 👫 📄 ⚙️ ⚠, 🚚 📠 & 📄 😥. - -✋️ ⤴️ **➡️ 🗜** ✍. - -⚫️ 🏗 ⚪️➡️ 💾 🏛. ⚫️ 🚚 **🇺🇸🔍 📄 🆓**, 🏧 🌌. 👫 📄 ⚙️ 🌐 🐩 🔐 💂‍♂, & 📏-🖖 (🔃 3️⃣ 🗓️), **💂‍♂ 🤙 👍** ↩️ 👫 📉 🔆. - -🆔 🔐 ✔ & 📄 🏗 🔁. 👉 ✔ 🏧 🔕 👫 📄. - -💭 🏧 🛠️ & 🔕 👫 📄 👈 👆 💪 ✔️ **🔐 🇺🇸🔍, 🆓, ♾**. - -## 🇺🇸🔍 👩‍💻 - -📥 🖼 ❔ 🇺🇸🔍 🛠️ 💪 👀 💖, 🔁 🔁, 💸 🙋 ✴️ 💭 ⚠ 👩‍💻. - -### 🆔 📛 - -⚫️ 🔜 🎲 🌐 ▶️ 👆 **🏗** **🆔 📛**. ⤴️, 👆 🔜 🔗 ⚫️ 🏓 💽 (🎲 👆 🎏 ☁ 🐕‍🦺). - -👆 🔜 🎲 🤚 ☁ 💽 (🕹 🎰) ⚖️ 🕳 🎏, & ⚫️ 🔜 ✔️ 🔧 **📢 📢 📢**. - -🏓 💽(Ⓜ) 👆 🔜 🔗 ⏺ ("`A record`") ☝ **👆 🆔** 📢 **📢 📢 👆 💽**. - -👆 🔜 🎲 👉 🕐, 🥇 🕰, 🕐❔ ⚒ 🌐 🆙. - -/// tip - -👉 🆔 📛 🍕 🌌 ⏭ 🇺🇸🔍, ✋️ 🌐 🪀 🔛 🆔 & 📢 📢, ⚫️ 💸 💬 ⚫️ 📥. - -/// - -### 🏓 - -🔜 ➡️ 🎯 🔛 🌐 ☑ 🇺🇸🔍 🍕. - -🥇, 🖥 🔜 ✅ ⏮️ **🏓 💽** ⚫️❔ **📢 🆔**, 👉 💼, `someapp.example.com`. - -🏓 💽 🔜 💬 🖥 ⚙️ 🎯 **📢 📢**. 👈 🔜 📢 📢 📢 ⚙️ 👆 💽, 👈 👆 🔗 🏓 💽. - - - -### 🤝 🤝 ▶️ - -🖥 🔜 ⤴️ 🔗 ⏮️ 👈 📢 📢 🔛 **⛴ 4️⃣4️⃣3️⃣** (🇺🇸🔍 ⛴). - -🥇 🍕 📻 🛠️ 🔗 🖖 👩‍💻 & 💽 & 💭 🔐 🔑 👫 🔜 ⚙️, ♒️. - - - -👉 🔗 🖖 👩‍💻 & 💽 🛠️ 🤝 🔗 🤙 **🤝 🤝**. - -### 🤝 ⏮️ 👲 ↔ - -**🕴 1️⃣ 🛠️** 💽 💪 👂 🔛 🎯 **⛴** 🎯 **📢 📢**. 📤 💪 🎏 🛠️ 👂 🔛 🎏 ⛴ 🎏 📢 📢, ✋️ 🕴 1️⃣ 🔠 🌀 📢 📢 & ⛴. - -🤝 (🇺🇸🔍) ⚙️ 🎯 ⛴ `443` 🔢. 👈 ⛴ 👥 🔜 💪. - -🕴 1️⃣ 🛠️ 💪 👂 🔛 👉 ⛴, 🛠️ 👈 🔜 ⚫️ 🔜 **🤝 ❎ 🗳**. - -🤝 ❎ 🗳 🔜 ✔️ 🔐 1️⃣ ⚖️ 🌅 **🤝 📄** (🇺🇸🔍 📄). - -⚙️ **👲 ↔** 🔬 🔛, 🤝 ❎ 🗳 🔜 ✅ ❔ 🤝 (🇺🇸🔍) 📄 💪 ⚫️ 🔜 ⚙️ 👉 🔗, ⚙️ 1️⃣ 👈 🏏 🆔 📈 👩‍💻. - -👉 💼, ⚫️ 🔜 ⚙️ 📄 `someapp.example.com`. - - - -👩‍💻 ⏪ **💙** 👨‍💼 👈 🏗 👈 🤝 📄 (👉 💼 ➡️ 🗜, ✋️ 👥 🔜 👀 🔃 👈 ⏪), ⚫️ 💪 **✔** 👈 📄 ☑. - -⤴️, ⚙️ 📄, 👩‍💻 & 🤝 ❎ 🗳 **💭 ❔ 🗜** 🎂 **🕸 📻**. 👉 🏁 **🤝 🤝** 🍕. - -⏮️ 👉, 👩‍💻 & 💽 ✔️ **🗜 🕸 🔗**, 👉 ⚫️❔ 🤝 🚚. & ⤴️ 👫 💪 ⚙️ 👈 🔗 ▶️ ☑ **🇺🇸🔍 📻**. - -& 👈 ⚫️❔ **🇺🇸🔍** , ⚫️ ✅ **🇺🇸🔍** 🔘 **🔐 🤝 🔗** ↩️ 😁 (💽) 🕸 🔗. - -/// tip - -👀 👈 🔐 📻 🔨 **🕸 🎚**, 🚫 🇺🇸🔍 🎚. - -/// - -### 🇺🇸🔍 📨 - -🔜 👈 👩‍💻 & 💽 (🎯 🖥 & 🤝 ❎ 🗳) ✔️ **🗜 🕸 🔗**, 👫 💪 ▶️ **🇺🇸🔍 📻**. - -, 👩‍💻 📨 **🇺🇸🔍 📨**. 👉 🇺🇸🔍 📨 🔘 🗜 🤝 🔗. - - - -### 🗜 📨 - -🤝 ❎ 🗳 🔜 ⚙️ 🔐 ✔ **🗜 📨**, & 🔜 📶 **✅ (🗜) 🇺🇸🔍 📨** 🛠️ 🏃 🈸 (🖼 🛠️ ⏮️ Uvicorn 🏃‍♂ FastAPI 🈸). - - - -### 🇺🇸🔍 📨 - -🈸 🔜 🛠️ 📨 & 📨 **✅ (💽) 🇺🇸🔍 📨** 🤝 ❎ 🗳. - - - -### 🇺🇸🔍 📨 - -🤝 ❎ 🗳 🔜 ⤴️ **🗜 📨** ⚙️ ⚛ ✔ ⏭ (👈 ▶️ ⏮️ 📄 `someapp.example.com`), & 📨 ⚫️ 🔙 🖥. - -⏭, 🖥 🔜 ✔ 👈 📨 ☑ & 🗜 ⏮️ ▶️️ 🔐 🔑, ♒️. ⚫️ 🔜 ⤴️ **🗜 📨** & 🛠️ ⚫️. - - - -👩‍💻 (🖥) 🔜 💭 👈 📨 👟 ⚪️➡️ ☑ 💽 ↩️ ⚫️ ⚙️ ⚛ 👫 ✔ ⚙️ **🇺🇸🔍 📄** ⏭. - -### 💗 🈸 - -🎏 💽 (⚖️ 💽), 📤 💪 **💗 🈸**, 🖼, 🎏 🛠️ 📋 ⚖️ 💽. - -🕴 1️⃣ 🛠️ 💪 🚚 🎯 📢 & ⛴ (🤝 ❎ 🗳 👆 🖼) ✋️ 🎏 🈸/🛠️ 💪 🏃 🔛 💽(Ⓜ) 💁‍♂️, 📏 👫 🚫 🔄 ⚙️ 🎏 **🌀 📢 📢 & ⛴**. - - - -👈 🌌, 🤝 ❎ 🗳 💪 🍵 🇺🇸🔍 & 📄 **💗 🆔**, 💗 🈸, & ⤴️ 📶 📨 ▶️️ 🈸 🔠 💼. - -### 📄 🔕 - -☝ 🔮, 🔠 📄 🔜 **🕛** (🔃 3️⃣ 🗓️ ⏮️ 🏗 ⚫️). - -& ⤴️, 📤 🔜 ➕1️⃣ 📋 (💼 ⚫️ ➕1️⃣ 📋, 💼 ⚫️ 💪 🎏 🤝 ❎ 🗳) 👈 🔜 💬 ➡️ 🗜, & ♻ 📄(Ⓜ). - - - -**🤝 📄** **🔗 ⏮️ 🆔 📛**, 🚫 ⏮️ 📢 📢. - -, ♻ 📄, 🔕 📋 💪 **🎦** 🛃 (➡️ 🗜) 👈 ⚫️ 👐 **"👍" & 🎛 👈 🆔**. - -👈, & 🏗 🎏 🈸 💪, 📤 📚 🌌 ⚫️ 💪 ⚫️. 🌟 🌌: - -* **🔀 🏓 ⏺**. - * 👉, 🔕 📋 💪 🐕‍🦺 🔗 🏓 🐕‍🦺,, ⚓️ 🔛 🏓 🐕‍🦺 👆 ⚙️, 👉 5️⃣📆 ⚖️ 💪 🚫 🎛. -* **🏃 💽** (🌘 ⏮️ 📄 🛠️ 🛠️) 🔛 📢 📢 📢 🔗 ⏮️ 🆔. - * 👥 💬 🔛, 🕴 1️⃣ 🛠️ 💪 👂 🔛 🎯 📢 & ⛴. - * 👉 1️⃣ 🤔 ⚫️❔ ⚫️ 📶 ⚠ 🕐❔ 🎏 🤝 ❎ 🗳 ✊ 💅 📄 🔕 🛠️. - * ⏪, 👆 💪 ✔️ ⛔️ 🤝 ❎ 🗳 😖, ▶️ 🔕 📋 📎 📄, ⤴️ 🔗 👫 ⏮️ 🤝 ❎ 🗳, & ⤴️ ⏏ 🤝 ❎ 🗳. 👉 🚫 💯, 👆 📱(Ⓜ) 🔜 🚫 💪 ⏮️ 🕰 👈 🤝 ❎ 🗳 📆. - -🌐 👉 🔕 🛠️, ⏪ 🍦 📱, 1️⃣ 👑 🤔 ⚫️❔ 👆 🔜 💚 ✔️ **🎏 ⚙️ 🍵 🇺🇸🔍** ⏮️ 🤝 ❎ 🗳 ↩️ ⚙️ 🤝 📄 ⏮️ 🈸 💽 🔗 (✅ Uvicorn). - -## 🌃 - -✔️ **🇺🇸🔍** 📶 ⚠, & **🎯** 🏆 💼. 🌅 🎯 👆 👩‍💻 ✔️ 🚮 🤭 🇺🇸🔍 🔃 **🤔 👉 🔧** & ❔ 👫 👷. - -✋️ 🕐 👆 💭 🔰 ℹ **🇺🇸🔍 👩‍💻** 👆 💪 💪 🌀 & 🔗 🎏 🧰 ℹ 👆 🛠️ 🌐 🙅 🌌. - -⏭ 📃, 👤 🔜 🎦 👆 📚 🧱 🖼 ❔ ⚒ 🆙 **🇺🇸🔍** **FastAPI** 🈸. 👶 diff --git a/docs/em/docs/deployment/index.md b/docs/em/docs/deployment/index.md deleted file mode 100644 index 9bcf427b69..0000000000 --- a/docs/em/docs/deployment/index.md +++ /dev/null @@ -1,21 +0,0 @@ -# 🛠️ - -🛠️ **FastAPI** 🈸 📶 ⏩. - -## ⚫️❔ 🔨 🛠️ ⛓ - -**🛠️** 🈸 ⛓ 🎭 💪 📶 ⚒ ⚫️ **💪 👩‍💻**. - -**🕸 🛠️**, ⚫️ 🛎 🔌 🚮 ⚫️ **🛰 🎰**, ⏮️ **💽 📋** 👈 🚚 👍 🎭, ⚖, ♒️, 👈 👆 **👩‍💻** 💪 **🔐** 🈸 ♻ & 🍵 🔁 ⚖️ ⚠. - -👉 🔅 **🛠️** ▶️, 🌐❔ 👆 🕧 🔀 📟, 💔 ⚫️ & ♻ ⚫️, ⛔️ & 🔁 🛠️ 💽, ♒️. - -## 🛠️ 🎛 - -📤 📚 🌌 ⚫️ ⚓️ 🔛 👆 🎯 ⚙️ 💼 & 🧰 👈 👆 ⚙️. - -👆 💪 **🛠️ 💽** 👆 ⚙️ 🌀 🧰, 👆 💪 ⚙️ **☁ 🐕‍🦺** 👈 🔨 🍕 👷 👆, ⚖️ 🎏 💪 🎛. - -👤 🔜 🎦 👆 👑 🔧 👆 🔜 🎲 ✔️ 🤯 🕐❔ 🛠️ **FastAPI** 🈸 (👐 🌅 ⚫️ ✔ 🙆 🎏 🆎 🕸 🈸). - -👆 🔜 👀 🌖 ℹ ✔️ 🤯 & ⚒ ⚫️ ⏭ 📄. 👶 diff --git a/docs/em/docs/deployment/manually.md b/docs/em/docs/deployment/manually.md deleted file mode 100644 index 4fa2d13e2d..0000000000 --- a/docs/em/docs/deployment/manually.md +++ /dev/null @@ -1,159 +0,0 @@ -# 🏃 💽 ❎ - Uvicorn - -👑 👜 👆 💪 🏃 **FastAPI** 🈸 🛰 💽 🎰 🔫 💽 📋 💖 **Uvicorn**. - -📤 3️⃣ 👑 🎛: - -* Uvicorn: ↕ 🎭 🔫 💽. -* Hypercorn: 🔫 💽 🔗 ⏮️ 🇺🇸🔍/2️⃣ & 🎻 👪 🎏 ⚒. -* 👸: 🔫 💽 🏗 ✳ 📻. - -## 💽 🎰 & 💽 📋 - -📤 🤪 ℹ 🔃 📛 ✔️ 🤯. 👶 - -🔤 "**💽**" 🛎 ⚙️ 🔗 👯‍♂️ 🛰/☁ 💻 (⚛ ⚖️ 🕹 🎰) & 📋 👈 🏃‍♂ 🔛 👈 🎰 (✅ Uvicorn). - -✔️ 👈 🤯 🕐❔ 👆 ✍ "💽" 🏢, ⚫️ 💪 🔗 1️⃣ 📚 2️⃣ 👜. - -🕐❔ 🔗 🛰 🎰, ⚫️ ⚠ 🤙 ⚫️ **💽**, ✋️ **🎰**, **💾** (🕹 🎰), **🕸**. 👈 🌐 🔗 🆎 🛰 🎰, 🛎 🏃‍♂ 💾, 🌐❔ 👆 🏃 📋. - -## ❎ 💽 📋 - -👆 💪 ❎ 🔫 🔗 💽 ⏮️: - -//// tab | Uvicorn - -* Uvicorn, 🌩-⏩ 🔫 💽, 🏗 🔛 uvloop & httptool. - -
- -```console -$ pip install "uvicorn[standard]" - ----> 100% -``` - -
- -/// tip - -❎ `standard`, Uvicorn 🔜 ❎ & ⚙️ 👍 ➕ 🔗. - -👈 ✅ `uvloop`, ↕-🎭 💧-♻ `asyncio`, 👈 🚚 🦏 🛠️ 🎭 📈. - -/// - -//// - -//// tab | Hypercorn - -* Hypercorn, 🔫 💽 🔗 ⏮️ 🇺🇸🔍/2️⃣. - -
- -```console -$ pip install hypercorn - ----> 100% -``` - -
- -...⚖️ 🙆 🎏 🔫 💽. - -//// - -## 🏃 💽 📋 - -👆 💪 ⤴️ 🏃 👆 🈸 🎏 🌌 👆 ✔️ ⌛ 🔰, ✋️ 🍵 `--reload` 🎛, ✅: - -//// tab | Uvicorn - -
- -```console -$ uvicorn main:app --host 0.0.0.0 --port 80 - -INFO: Uvicorn running on http://0.0.0.0:80 (Press CTRL+C to quit) -``` - -
- -//// - -//// tab | Hypercorn - -
- -```console -$ hypercorn main:app --bind 0.0.0.0:80 - -Running on 0.0.0.0:8080 over http (CTRL + C to quit) -``` - -
- -//// - -/// warning - -💭 ❎ `--reload` 🎛 🚥 👆 ⚙️ ⚫️. - - `--reload` 🎛 🍴 🌅 🌅 ℹ, 🌅 ⚠, ♒️. - -⚫️ ℹ 📚 ⏮️ **🛠️**, ✋️ 👆 **🚫🔜 🚫** ⚙️ ⚫️ **🏭**. - -/// - -## Hypercorn ⏮️ 🎻 - -💃 & **FastAPI** ⚓️ 🔛 AnyIO, ❔ ⚒ 👫 🔗 ⏮️ 👯‍♂️ 🐍 🐩 🗃 & 🎻. - -👐, Uvicorn ⏳ 🕴 🔗 ⏮️ ✳, & ⚫️ 🛎 ⚙️ `uvloop`, ↕-🎭 💧-♻ `asyncio`. - -✋️ 🚥 👆 💚 🔗 ⚙️ **🎻**, ⤴️ 👆 💪 ⚙️ **Hypercorn** ⚫️ 🐕‍🦺 ⚫️. 👶 - -### ❎ Hypercorn ⏮️ 🎻 - -🥇 👆 💪 ❎ Hypercorn ⏮️ 🎻 🐕‍🦺: - -
- -```console -$ pip install "hypercorn[trio]" ----> 100% -``` - -
- -### 🏃 ⏮️ 🎻 - -⤴️ 👆 💪 🚶‍♀️ 📋 ⏸ 🎛 `--worker-class` ⏮️ 💲 `trio`: - -
- -```console -$ hypercorn main:app --worker-class trio -``` - -
- -& 👈 🔜 ▶️ Hypercorn ⏮️ 👆 📱 ⚙️ 🎻 👩‍💻. - -🔜 👆 💪 ⚙️ 🎻 🔘 👆 📱. ⚖️ 👍, 👆 💪 ⚙️ AnyIO, 🚧 👆 📟 🔗 ⏮️ 👯‍♂️ 🎻 & ✳. 👶 - -## 🛠️ 🔧 - -👫 🖼 🏃 💽 📋 (📧.Ⓜ Uvicorn), ▶️ **👁 🛠️**, 👂 🔛 🌐 📢 (`0.0.0.0`) 🔛 🔁 ⛴ (✅ `80`). - -👉 🔰 💭. ✋️ 👆 🔜 🎲 💚 ✊ 💅 🌖 👜, 💖: - -* 💂‍♂ - 🇺🇸🔍 -* 🏃‍♂ 🔛 🕴 -* ⏏ -* 🧬 (🔢 🛠️ 🏃) -* 💾 -* ⏮️ 🔁 ⏭ ▶️ - -👤 🔜 💬 👆 🌅 🔃 🔠 👫 🔧, ❔ 💭 🔃 👫, & 🧱 🖼 ⏮️ 🎛 🍵 👫 ⏭ 📃. 👶 diff --git a/docs/em/docs/deployment/server-workers.md b/docs/em/docs/deployment/server-workers.md deleted file mode 100644 index eb29b2376a..0000000000 --- a/docs/em/docs/deployment/server-workers.md +++ /dev/null @@ -1,181 +0,0 @@ -# 💽 👨‍🏭 - 🐁 ⏮️ Uvicorn - -➡️ ✅ 🔙 👈 🛠️ 🔧 ⚪️➡️ ⏭: - -* 💂‍♂ - 🇺🇸🔍 -* 🏃‍♂ 🔛 🕴 -* ⏏ -* **🧬 (🔢 🛠️ 🏃)** -* 💾 -* ⏮️ 🔁 ⏭ ▶️ - -🆙 👉 ☝, ⏮️ 🌐 🔰 🩺, 👆 ✔️ 🎲 🏃‍♂ **💽 📋** 💖 Uvicorn, 🏃‍♂ **👁 🛠️**. - -🕐❔ 🛠️ 🈸 👆 🔜 🎲 💚 ✔️ **🧬 🛠️** ✊ 📈 **💗 🐚** & 💪 🍵 🌅 📨. - -👆 👀 ⏮️ 📃 🔃 [🛠️ 🔧](concepts.md){.internal-link target=_blank}, 📤 💗 🎛 👆 💪 ⚙️. - -📥 👤 🔜 🎦 👆 ❔ ⚙️ **🐁** ⏮️ **Uvicorn 👨‍🏭 🛠️**. - -/// info - -🚥 👆 ⚙️ 📦, 🖼 ⏮️ ☁ ⚖️ Kubernetes, 👤 🔜 💬 👆 🌅 🔃 👈 ⏭ 📃: [FastAPI 📦 - ☁](docker.md){.internal-link target=_blank}. - -🎯, 🕐❔ 🏃 🔛 **Kubernetes** 👆 🔜 🎲 **🚫** 💚 ⚙️ 🐁 & ↩️ 🏃 **👁 Uvicorn 🛠️ 📍 📦**, ✋️ 👤 🔜 💬 👆 🔃 ⚫️ ⏪ 👈 📃. - -/// - -## 🐁 ⏮️ Uvicorn 👨‍🏭 - -**🐁** ✴️ 🈸 💽 ⚙️ **🇨🇻 🐩**. 👈 ⛓ 👈 🐁 💪 🍦 🈸 💖 🏺 & ✳. 🐁 ⚫️ 🚫 🔗 ⏮️ **FastAPI**, FastAPI ⚙️ 🆕 **🔫 🐩**. - -✋️ 🐁 🐕‍🦺 👷 **🛠️ 👨‍💼** & 🤝 👩‍💻 💬 ⚫️ ❔ 🎯 **👨‍🏭 🛠️ 🎓** ⚙️. ⤴️ 🐁 🔜 ▶️ 1️⃣ ⚖️ 🌖 **👨‍🏭 🛠️** ⚙️ 👈 🎓. - -& **Uvicorn** ✔️ **🐁-🔗 👨‍🏭 🎓**. - -⚙️ 👈 🌀, 🐁 🔜 🚫 **🛠️ 👨‍💼**, 👂 🔛 **⛴** & **📢**. & ⚫️ 🔜 **📶** 📻 👨‍🏭 🛠️ 🏃 **Uvicorn 🎓**. - -& ⤴️ 🐁-🔗 **Uvicorn 👨‍🏭** 🎓 🔜 🈚 🏭 📊 📨 🐁 🔫 🐩 FastAPI ⚙️ ⚫️. - -## ❎ 🐁 & Uvicorn - -
- -```console -$ pip install "uvicorn[standard]" gunicorn - ----> 100% -``` - -
- -👈 🔜 ❎ 👯‍♂️ Uvicorn ⏮️ `standard` ➕ 📦 (🤚 ↕ 🎭) & 🐁. - -## 🏃 🐁 ⏮️ Uvicorn 👨‍🏭 - -⤴️ 👆 💪 🏃 🐁 ⏮️: - -
- -```console -$ gunicorn main:app --workers 4 --worker-class uvicorn.workers.UvicornWorker --bind 0.0.0.0:80 - -[19499] [INFO] Starting gunicorn 20.1.0 -[19499] [INFO] Listening at: http://0.0.0.0:80 (19499) -[19499] [INFO] Using worker: uvicorn.workers.UvicornWorker -[19511] [INFO] Booting worker with pid: 19511 -[19513] [INFO] Booting worker with pid: 19513 -[19514] [INFO] Booting worker with pid: 19514 -[19515] [INFO] Booting worker with pid: 19515 -[19511] [INFO] Started server process [19511] -[19511] [INFO] Waiting for application startup. -[19511] [INFO] Application startup complete. -[19513] [INFO] Started server process [19513] -[19513] [INFO] Waiting for application startup. -[19513] [INFO] Application startup complete. -[19514] [INFO] Started server process [19514] -[19514] [INFO] Waiting for application startup. -[19514] [INFO] Application startup complete. -[19515] [INFO] Started server process [19515] -[19515] [INFO] Waiting for application startup. -[19515] [INFO] Application startup complete. -``` - -
- -➡️ 👀 ⚫️❔ 🔠 👈 🎛 ⛓: - -* `main:app`: 👉 🎏 ❕ ⚙️ Uvicorn, `main` ⛓ 🐍 🕹 📛 "`main`",, 📁 `main.py`. & `app` 📛 🔢 👈 **FastAPI** 🈸. - * 👆 💪 🌈 👈 `main:app` 🌓 🐍 `import` 📄 💖: - - ```Python - from main import app - ``` - - * , ❤ `main:app` 🔜 🌓 🐍 `import` 🍕 `from main import app`. -* `--workers`: 🔢 👨‍🏭 🛠️ ⚙️, 🔠 🔜 🏃 Uvicorn 👨‍🏭, 👉 💼, 4️⃣ 👨‍🏭. -* `--worker-class`: 🐁-🔗 👨‍🏭 🎓 ⚙️ 👨‍🏭 🛠️. - * 📥 👥 🚶‍♀️ 🎓 👈 🐁 💪 🗄 & ⚙️ ⏮️: - - ```Python - import uvicorn.workers.UvicornWorker - ``` - -* `--bind`: 👉 💬 🐁 📢 & ⛴ 👂, ⚙️ ❤ (`:`) 🎏 📢 & ⛴. - * 🚥 👆 🏃‍♂ Uvicorn 🔗, ↩️ `--bind 0.0.0.0:80` (🐁 🎛) 👆 🔜 ⚙️ `--host 0.0.0.0` & `--port 80`. - -🔢, 👆 💪 👀 👈 ⚫️ 🎦 **🕹** (🛠️ 🆔) 🔠 🛠️ (⚫️ 🔢). - -👆 💪 👀 👈: - -* 🐁 **🛠️ 👨‍💼** ▶️ ⏮️ 🕹 `19499` (👆 💼 ⚫️ 🔜 🎏 🔢). -* ⤴️ ⚫️ ▶️ `Listening at: http://0.0.0.0:80`. -* ⤴️ ⚫️ 🔍 👈 ⚫️ ✔️ ⚙️ 👨‍🏭 🎓 `uvicorn.workers.UvicornWorker`. -* & ⤴️ ⚫️ ▶️ **4️⃣ 👨‍🏭**, 🔠 ⏮️ 🚮 👍 🕹: `19511`, `19513`, `19514`, & `19515`. - -🐁 🔜 ✊ 💅 🛠️ **☠️ 🛠️** & **🔁** 🆕 🕐 🚥 💚 🚧 🔢 👨‍🏭. 👈 ℹ 🍕 ⏮️ **⏏** 🔧 ⚪️➡️ 📇 🔛. - -👐, 👆 🔜 🎲 💚 ✔️ 🕳 🏞 ⚒ 💭 **⏏ 🐁** 🚥 💪, & **🏃 ⚫️ 🔛 🕴**, ♒️. - -## Uvicorn ⏮️ 👨‍🏭 - -Uvicorn ✔️ 🎛 ▶️ & 🏃 📚 **👨‍🏭 🛠️**. - -👐, 🔜, Uvicorn 🛠️ 🚚 👨‍🏭 🛠️ 🌅 📉 🌘 🐁. , 🚥 👆 💚 ✔️ 🛠️ 👨‍💼 👉 🎚 (🐍 🎚), ⤴️ ⚫️ 💪 👍 🔄 ⏮️ 🐁 🛠️ 👨‍💼. - -🙆 💼, 👆 🔜 🏃 ⚫️ 💖 👉: - -
- -```console -$ uvicorn main:app --host 0.0.0.0 --port 8080 --workers 4 -INFO: Uvicorn running on http://0.0.0.0:8080 (Press CTRL+C to quit) -INFO: Started parent process [27365] -INFO: Started server process [27368] -INFO: Waiting for application startup. -INFO: Application startup complete. -INFO: Started server process [27369] -INFO: Waiting for application startup. -INFO: Application startup complete. -INFO: Started server process [27370] -INFO: Waiting for application startup. -INFO: Application startup complete. -INFO: Started server process [27367] -INFO: Waiting for application startup. -INFO: Application startup complete. -``` - -
- -🕴 🆕 🎛 📥 `--workers` 💬 Uvicorn ▶️ 4️⃣ 👨‍🏭 🛠️. - -👆 💪 👀 👈 ⚫️ 🎦 **🕹** 🔠 🛠️, `27365` 👪 🛠️ (👉 **🛠️ 👨‍💼**) & 1️⃣ 🔠 👨‍🏭 🛠️: `27368`, `27369`, `27370`, & `27367`. - -## 🛠️ 🔧 - -📥 👆 👀 ❔ ⚙️ **🐁** (⚖️ Uvicorn) 🛠️ **Uvicorn 👨‍🏭 🛠️** **🔁** 🛠️ 🈸, ✊ 📈 **💗 🐚** 💽, & 💪 🍦 **🌅 📨**. - -⚪️➡️ 📇 🛠️ 🔧 ⚪️➡️ 🔛, ⚙️ 👨‍🏭 🔜 ✴️ ℹ ⏮️ **🧬** 🍕, & 🐥 🍖 ⏮️ **⏏**, ✋️ 👆 💪 ✊ 💅 🎏: - -* **💂‍♂ - 🇺🇸🔍** -* **🏃‍♂ 🔛 🕴** -* ***⏏*** -* 🧬 (🔢 🛠️ 🏃) -* **💾** -* **⏮️ 🔁 ⏭ ▶️** - -## 📦 & ☁ - -⏭ 📃 🔃 [FastAPI 📦 - ☁](docker.md){.internal-link target=_blank} 👤 🔜 💬 🎛 👆 💪 ⚙️ 🍵 🎏 **🛠️ 🔧**. - -👤 🔜 🎦 👆 **🛂 ☁ 🖼** 👈 🔌 **🐁 ⏮️ Uvicorn 👨‍🏭** & 🔢 📳 👈 💪 ⚠ 🙅 💼. - -📤 👤 🔜 🎦 👆 ❔ **🏗 👆 👍 🖼 ⚪️➡️ 🖌** 🏃 👁 Uvicorn 🛠️ (🍵 🐁). ⚫️ 🙅 🛠️ & 🎲 ⚫️❔ 👆 🔜 💚 🕐❔ ⚙️ 📎 📦 🧾 ⚙️ 💖 **Kubernetes**. - -## 🌃 - -👆 💪 ⚙️ **🐁** (⚖️ Uvicorn) 🛠️ 👨‍💼 ⏮️ Uvicorn 👨‍🏭 ✊ 📈 **👁-🐚 💽**, 🏃 **💗 🛠️ 🔗**. - -👆 💪 ⚙️ 👉 🧰 & 💭 🚥 👆 ⚒ 🆙 **👆 👍 🛠️ ⚙️** ⏪ ✊ 💅 🎏 🛠️ 🔧 👆. - -✅ 👅 ⏭ 📃 💡 🔃 **FastAPI** ⏮️ 📦 (✅ ☁ & Kubernetes). 👆 🔜 👀 👈 👈 🧰 ✔️ 🙅 🌌 ❎ 🎏 **🛠️ 🔧** 👍. 👶 diff --git a/docs/em/docs/deployment/versions.md b/docs/em/docs/deployment/versions.md deleted file mode 100644 index 6c9b8f9bb2..0000000000 --- a/docs/em/docs/deployment/versions.md +++ /dev/null @@ -1,93 +0,0 @@ -# 🔃 FastAPI ⏬ - -**FastAPI** ⏪ ➖ ⚙️ 🏭 📚 🈸 & ⚙️. & 💯 💰 🚧 1️⃣0️⃣0️⃣ 💯. ✋️ 🚮 🛠️ 🚚 🔜. - -🆕 ⚒ 🚮 🛎, 🐛 🔧 🛎, & 📟 🔁 📉. - -👈 ⚫️❔ ⏮️ ⏬ `0.x.x`, 👉 🎨 👈 🔠 ⏬ 💪 ⚠ ✔️ 💔 🔀. 👉 ⏩ ⚛ 🛠️ 🏛. - -👆 💪 ✍ 🏭 🈸 ⏮️ **FastAPI** ▶️️ 🔜 (& 👆 ✔️ 🎲 🔨 ⚫️ 🕰), 👆 ✔️ ⚒ 💭 👈 👆 ⚙️ ⏬ 👈 👷 ☑ ⏮️ 🎂 👆 📟. - -## 📌 👆 `fastapi` ⏬ - -🥇 👜 👆 🔜 "📌" ⏬ **FastAPI** 👆 ⚙️ 🎯 📰 ⏬ 👈 👆 💭 👷 ☑ 👆 🈸. - -🖼, ➡️ 💬 👆 ⚙️ ⏬ `0.45.0` 👆 📱. - -🚥 👆 ⚙️ `requirements.txt` 📁 👆 💪 ✔ ⏬ ⏮️: - -```txt -fastapi==0.45.0 -``` - -👈 🔜 ⛓ 👈 👆 🔜 ⚙️ ⚫️❔ ⏬ `0.45.0`. - -⚖️ 👆 💪 📌 ⚫️ ⏮️: - -```txt -fastapi>=0.45.0,<0.46.0 -``` - -👈 🔜 ⛓ 👈 👆 🔜 ⚙️ ⏬ `0.45.0` ⚖️ 🔛, ✋️ 🌘 🌘 `0.46.0`, 🖼, ⏬ `0.45.2` 🔜 🚫. - -🚥 👆 ⚙️ 🙆 🎏 🧰 🛠️ 👆 👷‍♂, 💖 🎶, Pipenv, ⚖️ 🎏, 👫 🌐 ✔️ 🌌 👈 👆 💪 ⚙️ 🔬 🎯 ⏬ 👆 📦. - -## 💪 ⏬ - -👆 💪 👀 💪 ⏬ (✅ ✅ ⚫️❔ ⏮️ 📰) [🚀 🗒](../release-notes.md){.internal-link target=_blank}. - -## 🔃 ⏬ - -📄 ⚛ 🛠️ 🏛, 🙆 ⏬ 🔛 `1.0.0` 💪 ⚠ 🚮 💔 🔀. - -FastAPI ⏩ 🏛 👈 🙆 "🐛" ⏬ 🔀 🐛 🔧 & 🚫-💔 🔀. - -/// tip - -"🐛" 🏁 🔢, 🖼, `0.2.3`, 🐛 ⏬ `3`. - -/// - -, 👆 🔜 💪 📌 ⏬ 💖: - -```txt -fastapi>=0.45.0,<0.46.0 -``` - -💔 🔀 & 🆕 ⚒ 🚮 "🇺🇲" ⏬. - -/// tip - -"🇺🇲" 🔢 🖕, 🖼, `0.2.3`, 🇺🇲 ⏬ `2`. - -/// - -## ♻ FastAPI ⏬ - -👆 🔜 🚮 💯 👆 📱. - -⏮️ **FastAPI** ⚫️ 📶 ⏩ (👏 💃), ✅ 🩺: [🔬](../tutorial/testing.md){.internal-link target=_blank} - -⏮️ 👆 ✔️ 💯, ⤴️ 👆 💪 ♻ **FastAPI** ⏬ 🌖 ⏮️ 1️⃣, & ⚒ 💭 👈 🌐 👆 📟 👷 ☑ 🏃 👆 💯. - -🚥 🌐 👷, ⚖️ ⏮️ 👆 ⚒ 💪 🔀, & 🌐 👆 💯 🚶‍♀️, ⤴️ 👆 💪 📌 👆 `fastapi` 👈 🆕 ⏮️ ⏬. - -## 🔃 💃 - -👆 🚫🔜 🚫 📌 ⏬ `starlette`. - -🎏 ⏬ **FastAPI** 🔜 ⚙️ 🎯 🆕 ⏬ 💃. - -, 👆 💪 ➡️ **FastAPI** ⚙️ ☑ 💃 ⏬. - -## 🔃 Pydantic - -Pydantic 🔌 💯 **FastAPI** ⏮️ 🚮 👍 💯, 🆕 ⏬ Pydantic (🔛 `1.0.0`) 🕧 🔗 ⏮️ FastAPI. - -👆 💪 📌 Pydantic 🙆 ⏬ 🔛 `1.0.0` 👈 👷 👆 & 🔛 `2.0.0`. - -🖼: - -```txt -pydantic>=1.2.0,<2.0.0 -``` diff --git a/docs/em/docs/features.md b/docs/em/docs/features.md deleted file mode 100644 index ccbed0cae3..0000000000 --- a/docs/em/docs/features.md +++ /dev/null @@ -1,201 +0,0 @@ -# ⚒ - -## FastAPI ⚒ - -**FastAPI** 🤝 👆 📄: - -### ⚓️ 🔛 📂 🐩 - -* 🗄 🛠️ 🏗, ✅ 📄 🛠️, 🔢, 💪 📨, 💂‍♂, ♒️. -* 🏧 📊 🏷 🧾 ⏮️ 🎻 🔗 (🗄 ⚫️ 🧢 🔛 🎻 🔗). -* 🔧 🤭 👫 🐩, ⏮️ 😔 🔬. ↩️ 👎 🧽 🔛 🔝. -* 👉 ✔ ⚙️ 🏧 **👩‍💻 📟 ⚡** 📚 🇪🇸. - -### 🏧 🩺 - -🎓 🛠️ 🧾 & 🔬 🕸 👩‍💻 🔢. 🛠️ ⚓️ 🔛 🗄, 📤 💗 🎛, 2️⃣ 🔌 🔢. - -* 🦁 🎚, ⏮️ 🎓 🔬, 🤙 & 💯 👆 🛠️ 🔗 ⚪️➡️ 🖥. - -![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) - -* 🎛 🛠️ 🧾 ⏮️ 📄. - -![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) - -### 🏛 🐍 - -⚫️ 🌐 ⚓️ 🔛 🐩 **🐍 3️⃣.6️⃣ 🆎** 📄 (👏 Pydantic). 🙅‍♂ 🆕 ❕ 💡. 🐩 🏛 🐍. - -🚥 👆 💪 2️⃣ ⏲ ↗️ ❔ ⚙️ 🐍 🆎 (🚥 👆 🚫 ⚙️ FastAPI), ✅ 📏 🔰: [🐍 🆎](python-types.md){.internal-link target=_blank}. - -👆 ✍ 🐩 🐍 ⏮️ 🆎: - -```Python -from datetime import date - -from pydantic import BaseModel - -# Declare a variable as a str -# and get editor support inside the function -def main(user_id: str): - return user_id - - -# A Pydantic model -class User(BaseModel): - id: int - name: str - joined: date -``` - -👈 💪 ⤴️ ⚙️ 💖: - -```Python -my_user: User = User(id=3, name="John Doe", joined="2018-07-19") - -second_user_data = { - "id": 4, - "name": "Mary", - "joined": "2018-11-30", -} - -my_second_user: User = User(**second_user_data) -``` - -/// info - -`**second_user_data` ⛓: - -🚶‍♀️ 🔑 & 💲 `second_user_data` #️⃣ 🔗 🔑-💲 ❌, 🌓: `User(id=4, name="Mary", joined="2018-11-30")` - -/// - -### 👨‍🎨 🐕‍🦺 - -🌐 🛠️ 🏗 ⏩ & 🏋️ ⚙️, 🌐 🚫 💯 🔛 💗 👨‍🎨 ⏭ ▶️ 🛠️, 🚚 🏆 🛠️ 💡. - -🏁 🐍 👩‍💻 🔬 ⚫️ 🆑 👈 🌅 ⚙️ ⚒ "✍". - -🎂 **FastAPI** 🛠️ ⚓️ 😌 👈. ✍ 👷 🌐. - -👆 🔜 🛎 💪 👟 🔙 🩺. - -📥 ❔ 👆 👨‍🎨 💪 ℹ 👆: - -* 🎙 🎙 📟: - -![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) - -* 🗒: - -![editor support](https://fastapi.tiangolo.com/img/pycharm-completion.png) - -👆 🔜 🤚 🛠️ 📟 👆 5️⃣📆 🤔 💪 ⏭. 🖼, `price` 🔑 🔘 🎻 💪 (👈 💪 ✔️ 🐦) 👈 👟 ⚪️➡️ 📨. - -🙅‍♂ 🌖 ⌨ ❌ 🔑 📛, 👟 🔙 & ➡ 🖖 🩺, ⚖️ 📜 🆙 & 🔽 🔎 🚥 👆 😒 ⚙️ `username` ⚖️ `user_name`. - -### 📏 - -⚫️ ✔️ 🤔 **🔢** 🌐, ⏮️ 📦 📳 🌐. 🌐 🔢 💪 👌-🎧 ⚫️❔ 👆 💪 & 🔬 🛠️ 👆 💪. - -✋️ 🔢, ⚫️ 🌐 **"👷"**. - -### 🔬 - -* 🔬 🌅 (⚖️ 🌐 ❓) 🐍 **💽 🆎**, 🔌: - * 🎻 🎚 (`dict`). - * 🎻 🎻 (`list`) ⚖ 🏬 🆎. - * 🎻 (`str`) 🏑, 🔬 🕙 & 👟 📐. - * 🔢 (`int`, `float`) ⏮️ 🕙 & 👟 💲, ♒️. - -* 🔬 🌅 😍 🆎, 💖: - * 📛. - * 📧. - * 🆔. - * ...& 🎏. - -🌐 🔬 🍵 👍-🏛 & 🏋️ **Pydantic**. - -### 💂‍♂ & 🤝 - -💂‍♂ & 🤝 🛠️. 🍵 🙆 ⚠ ⏮️ 💽 ⚖️ 📊 🏷. - -🌐 💂‍♂ ⚖ 🔬 🗄, 🔌: - -* 🇺🇸🔍 🔰. -* **Oauth2️⃣** (⏮️ **🥙 🤝**). ✅ 🔰 🔛 [Oauth2️⃣ ⏮️ 🥙](tutorial/security/oauth2-jwt.md){.internal-link target=_blank}. -* 🛠️ 🔑: - * 🎚. - * 🔢 🔢. - * 🍪, ♒️. - -➕ 🌐 💂‍♂ ⚒ ⚪️➡️ 💃 (🔌 **🎉 🍪**). - -🌐 🏗 ♻ 🧰 & 🦲 👈 ⏩ 🛠️ ⏮️ 👆 ⚙️, 📊 🏪, 🔗 & ☁ 💽, ♒️. - -### 🔗 💉 - -FastAPI 🔌 📶 ⏩ ⚙️, ✋️ 📶 🏋️ 🔗 💉 ⚙️. - -* 🔗 💪 ✔️ 🔗, 🏗 🔗 ⚖️ **"📊" 🔗**. -* 🌐 **🔁 🍵** 🛠️. -* 🌐 🔗 💪 🚚 💽 ⚪️➡️ 📨 & **↔ ➡ 🛠️** ⚛ & 🏧 🧾. -* **🏧 🔬** *➡ 🛠️* 🔢 🔬 🔗. -* 🐕‍🦺 🏗 👩‍💻 🤝 ⚙️, **💽 🔗**, ♒️. -* **🙅‍♂ ⚠** ⏮️ 💽, 🕸, ♒️. ✋️ ⏩ 🛠️ ⏮️ 🌐 👫. - -### ♾ "🔌-🔌" - -⚖️ 🎏 🌌, 🙅‍♂ 💪 👫, 🗄 & ⚙️ 📟 👆 💪. - -🙆 🛠️ 🏗 🙅 ⚙️ (⏮️ 🔗) 👈 👆 💪 ✍ "🔌-" 👆 🈸 2️⃣ ⏸ 📟 ⚙️ 🎏 📊 & ❕ ⚙️ 👆 *➡ 🛠️*. - -### 💯 - -* 1️⃣0️⃣0️⃣ 💯 💯 💰. -* 1️⃣0️⃣0️⃣ 💯 🆎 ✍ 📟 🧢. -* ⚙️ 🏭 🈸. - -## 💃 ⚒ - -**FastAPI** 🍕 🔗 ⏮️ (& ⚓️ 🔛) 💃. , 🙆 🌖 💃 📟 👆 ✔️, 🔜 👷. - -`FastAPI` 🤙 🎧-🎓 `Starlette`. , 🚥 👆 ⏪ 💭 ⚖️ ⚙️ 💃, 🌅 🛠️ 🔜 👷 🎏 🌌. - -⏮️ **FastAPI** 👆 🤚 🌐 **💃**'Ⓜ ⚒ (FastAPI 💃 🔛 💊): - -* 🤙 🎆 🎭. ⚫️ 1️⃣ ⏩ 🐍 🛠️ 💪, 🔛 🇷🇪 ⏮️ **✳** & **🚶**. -* ** *️⃣ ** 🐕‍🦺. -* -🛠️ 🖥 📋. -* 🕴 & 🤫 🎉. -* 💯 👩‍💻 🏗 🔛 🇸🇲. -* **⚜**, 🗜, 🎻 📁, 🎏 📨. -* **🎉 & 🍪** 🐕‍🦺. -* 1️⃣0️⃣0️⃣ 💯 💯 💰. -* 1️⃣0️⃣0️⃣ 💯 🆎 ✍ ✍. - -## Pydantic ⚒ - -**FastAPI** 🍕 🔗 ⏮️ (& ⚓️ 🔛) Pydantic. , 🙆 🌖 Pydantic 📟 👆 ✔️, 🔜 👷. - -✅ 🔢 🗃 ⚓️ 🔛 Pydantic, 🐜Ⓜ, 🏭Ⓜ 💽. - -👉 ⛓ 👈 📚 💼 👆 💪 🚶‍♀️ 🎏 🎚 👆 🤚 ⚪️➡️ 📨 **🔗 💽**, 🌐 ✔ 🔁. - -🎏 ✔ 🎏 🌌 🤭, 📚 💼 👆 💪 🚶‍♀️ 🎚 👆 🤚 ⚪️➡️ 💽 **🔗 👩‍💻**. - -⏮️ **FastAPI** 👆 🤚 🌐 **Pydantic**'Ⓜ ⚒ (FastAPI ⚓️ 🔛 Pydantic 🌐 💽 🚚): - -* **🙅‍♂ 🔠**: - * 🙅‍♂ 🆕 🔗 🔑 ◾-🇪🇸 💡. - * 🚥 👆 💭 🐍 🆎 👆 💭 ❔ ⚙️ Pydantic. -* 🤾 🎆 ⏮️ 👆 **💾/🧶/🧠**: - * ↩️ Pydantic 📊 📊 👐 🎓 👆 🔬; 🚘-🛠️, 🧽, ✍ & 👆 🤔 🔜 🌐 👷 ☑ ⏮️ 👆 ✔ 💽. -* ✔ **🏗 📊**: - * ⚙️ 🔗 Pydantic 🏷, 🐍 `typing`'Ⓜ `List` & `Dict`, ♒️. - * & 💳 ✔ 🏗 💽 🔗 🎯 & 💪 🔬, ✅ & 📄 🎻 🔗. - * 👆 💪 ✔️ 🙇 **🐦 🎻** 🎚 & ✔️ 👫 🌐 ✔ & ✍. -* **🏧**: - * Pydantic ✔ 🛃 📊 🆎 🔬 ⚖️ 👆 💪 ↔ 🔬 ⏮️ 👩‍🔬 🔛 🏷 🎀 ⏮️ 💳 👨‍🎨. -* 1️⃣0️⃣0️⃣ 💯 💯 💰. diff --git a/docs/em/docs/help-fastapi.md b/docs/em/docs/help-fastapi.md deleted file mode 100644 index 9d802f9e4a..0000000000 --- a/docs/em/docs/help-fastapi.md +++ /dev/null @@ -1,269 +0,0 @@ -# ℹ FastAPI - 🤚 ℹ - -👆 💖 **FastAPI**❓ - -🔜 👆 💖 ℹ FastAPI, 🎏 👩‍💻, & 📕 ❓ - -⚖️ 🔜 👆 💖 🤚 ℹ ⏮️ **FastAPI**❓ - -📤 📶 🙅 🌌 ℹ (📚 🔌 1️⃣ ⚖️ 2️⃣ 🖊). - -& 📤 📚 🌌 🤚 ℹ 💁‍♂️. - -## 👱📔 📰 - -👆 💪 👱📔 (🐌) [**FastAPI & 👨‍👧‍👦** 📰](newsletter.md){.internal-link target=_blank} 🚧 ℹ 🔃: - -* 📰 🔃 FastAPI & 👨‍👧‍👦 👶 -* 🦮 👶 -* ⚒ 👶 -* 💔 🔀 👶 -* 💁‍♂ & 🎱 👶 - -## ⏩ FastAPI 🔛 👱📔 - -⏩ 🐶 Fastapi 🔛 **👱📔** 🤚 📰 📰 🔃 **FastAPI**. 👶 - -## ✴ **FastAPI** 📂 - -👆 💪 "✴" FastAPI 📂 (🖊 ✴ 🔼 🔝 ▶️️): https://github.com/fastapi/fastapi. 👶 👶 - -❎ ✴, 🎏 👩‍💻 🔜 💪 🔎 ⚫️ 🌅 💪 & 👀 👈 ⚫️ ✔️ ⏪ ⚠ 🎏. - -## ⌚ 📂 🗃 🚀 - -👆 💪 "⌚" FastAPI 📂 (🖊 "⌚" 🔼 🔝 ▶️️): https://github.com/fastapi/fastapi. 👶 - -📤 👆 💪 🖊 "🚀 🕴". - -🔨 ⚫️, 👆 🔜 📨 📨 (👆 📧) 🕐❔ 📤 🆕 🚀 (🆕 ⏬) **FastAPI** ⏮️ 🐛 🔧 & 🆕 ⚒. - -## 🔗 ⏮️ 📕 - -👆 💪 🔗 ⏮️ 👤 (🇹🇦 🇩🇬 / `tiangolo`), 📕. - -👆 💪: - -* ⏩ 👤 🔛 **📂**. - * 👀 🎏 📂 ℹ 🏗 👤 ✔️ ✍ 👈 💪 ℹ 👆. - * ⏩ 👤 👀 🕐❔ 👤 ✍ 🆕 📂 ℹ 🏗. -* ⏩ 👤 🔛 **👱📔** ⚖️ . - * 💬 👤 ❔ 👆 ⚙️ FastAPI (👤 💌 👂 👈). - * 👂 🕐❔ 👤 ⚒ 🎉 ⚖️ 🚀 🆕 🧰. - * 👆 💪 ⏩ 🐶 Fastapi 🔛 👱📔 (🎏 🏧). -* 🔗 ⏮️ 👤 🔛 **👱📔**. - * 👂 🕐❔ 👤 ⚒ 🎉 ⚖️ 🚀 🆕 🧰 (👐 👤 ⚙️ 👱📔 🌖 🛎 🤷 ♂). -* ✍ ⚫️❔ 👤 ✍ (⚖️ ⏩ 👤) 🔛 **🇸🇲.** ⚖️ **🔉**. - * ✍ 🎏 💭, 📄, & ✍ 🔃 🧰 👤 ✔️ ✍. - * ⏩ 👤 ✍ 🕐❔ 👤 ✍ 🕳 🆕. - -## 👱📔 🔃 **FastAPI** - -👱📔 🔃 **FastAPI** & ➡️ 👤 & 🎏 💭 ⚫️❔ 👆 💖 ⚫️. 👶 - -👤 💌 👂 🔃 ❔ **FastAPI** 💆‍♂ ⚙️, ⚫️❔ 👆 ✔️ 💖 ⚫️, ❔ 🏗/🏢 👆 ⚙️ ⚫️, ♒️. - -## 🗳 FastAPI - -* 🗳 **FastAPI** 📐. -* 🗳 **FastAPI** 📱. -* 💬 👆 ⚙️ **FastAPI** 🔛 ℹ. - -## ℹ 🎏 ⏮️ ❔ 📂 - -👆 💪 🔄 & ℹ 🎏 ⏮️ 👫 ❔: - -* 📂 💬 -* 📂 ❔ - -📚 💼 👆 5️⃣📆 ⏪ 💭 ❔ 📚 ❔. 👶 - -🚥 👆 🤝 📚 👫👫 ⏮️ 👫 ❔, 👆 🔜 ▶️️ 🛂 [FastAPI 🕴](fastapi-people.md#_2){.internal-link target=_blank}. 👶 - -💭, 🏆 ⚠ ☝: 🔄 😇. 👫👫 👟 ⏮️ 👫 😩 & 📚 💼 🚫 💭 🏆 🌌, ✋️ 🔄 🏆 👆 💪 😇. 👶 - -💭 **FastAPI** 👪 😇 & 👍. 🎏 🕰, 🚫 🚫 🎭 ⚖️ 😛 🎭 ⤵ 🎏. 👥 ✔️ ✊ 💅 🔠 🎏. - ---- - -📥 ❔ ℹ 🎏 ⏮️ ❔ (💬 ⚖️ ❔): - -### 🤔 ❔ - -* ✅ 🚥 👆 💪 🤔 ⚫️❔ **🎯** & ⚙️ 💼 👨‍💼 💬. - -* ⤴️ ✅ 🚥 ❔ (⭕ 👪 ❔) **🆑**. - -* 📚 💼 ❔ 💭 🔃 👽 ⚗ ⚪️➡️ 👩‍💻, ✋️ 📤 💪 **👍** 1️⃣. 🚥 👆 💪 🤔 ⚠ & ⚙️ 💼 👍, 👆 💪 💪 🤔 👍 **🎛 ⚗**. - -* 🚥 👆 💪 🚫 🤔 ❔, 💭 🌖 **ℹ**. - -### 🔬 ⚠ - -🌅 💼 & 🏆 ❔ 📤 🕳 🔗 👨‍💼 **⏮️ 📟**. - -📚 💼 👫 🔜 🕴 📁 🧬 📟, ✋️ 👈 🚫 🥃 **🔬 ⚠**. - -* 👆 💪 💭 👫 🚚 ⭐, 🔬, 🖼, 👈 👆 💪 **📁-📋** & 🏃 🌐 👀 🎏 ❌ ⚖️ 🎭 👫 👀, ⚖️ 🤔 👫 ⚙️ 💼 👍. - -* 🚥 👆 😟 💁‍♂️ 👍, 👆 💪 🔄 **✍ 🖼** 💖 👈 👆, 🧢 🔛 📛 ⚠. ✔️ 🤯 👈 👉 💪 ✊ 📚 🕰 & ⚫️ 💪 👻 💭 👫 ✍ ⚠ 🥇. - -### 🤔 ⚗ - -* ⏮️ 💆‍♂ 💪 🤔 ❔, 👆 💪 🤝 👫 💪 **❔**. - -* 📚 💼, ⚫️ 👍 🤔 👫 **📈 ⚠ ⚖️ ⚙️ 💼**, ↩️ 📤 5️⃣📆 👍 🌌 ❎ ⚫️ 🌘 ⚫️❔ 👫 🔄. - -### 💭 🔐 - -🚥 👫 📨, 📤 ↕ 🤞 👆 🔜 ✔️ ❎ 👫 ⚠, ㊗, **👆 💂**❗ 🦸 - -* 🔜, 🚥 👈 ❎ 👫 ⚠, 👆 💪 💭 👫: - - * 📂 💬: ™ 🏤 **❔**. - * 📂 ❔: **🔐** ❔**. - -## ⌚ 📂 🗃 - -👆 💪 "⌚" FastAPI 📂 (🖊 "⌚" 🔼 🔝 ▶️️): https://github.com/fastapi/fastapi. 👶 - -🚥 👆 🖊 "👀" ↩️ "🚀 🕴" 👆 🔜 📨 📨 🕐❔ 👱 ✍ 🆕 ❔ ⚖️ ❔. 👆 💪 ✔ 👈 👆 🕴 💚 🚨 🔃 🆕 ❔, ⚖️ 💬, ⚖️ 🎸, ♒️. - -⤴️ 👆 💪 🔄 & ℹ 👫 ❎ 👈 ❔. - -## 💭 ❔ - -👆 💪 ✍ 🆕 ❔ 📂 🗃, 🖼: - -* 💭 **❔** ⚖️ 💭 🔃 **⚠**. -* 🤔 🆕 **⚒**. - -**🗒**: 🚥 👆 ⚫️, ⤴️ 👤 🔜 💭 👆 ℹ 🎏. 👶 - -## 📄 🚲 📨 - -👆 💪 ℹ 👤 📄 🚲 📨 ⚪️➡️ 🎏. - -🔄, 🙏 🔄 👆 🏆 😇. 👶 - ---- - -📥 ⚫️❔ ✔️ 🤯 & ❔ 📄 🚲 📨: - -### 🤔 ⚠ - -* 🥇, ⚒ 💭 👆 **🤔 ⚠** 👈 🚲 📨 🔄 ❎. ⚫️ 💪 ✔️ 📏 💬 📂 💬 ⚖️ ❔. - -* 📤 👍 🤞 👈 🚲 📨 🚫 🤙 💪 ↩️ ⚠ 💪 ❎ **🎏 🌌**. ⤴️ 👆 💪 🤔 ⚖️ 💭 🔃 👈. - -### 🚫 😟 🔃 👗 - -* 🚫 😟 💁‍♂️ 🌅 🔃 👜 💖 💕 📧 👗, 👤 🔜 🥬 & 🔗 🛃 💕 ❎. - -* 🚫 😟 🔃 👗 🚫, 📤 ⏪ 🏧 🧰 ✅ 👈. - -& 🚥 📤 🙆 🎏 👗 ⚖️ ⚖ 💪, 👤 🔜 💭 🔗 👈, ⚖️ 👤 🔜 🚮 💕 🔛 🔝 ⏮️ 💪 🔀. - -### ✅ 📟 - -* ✅ & ✍ 📟, 👀 🚥 ⚫️ ⚒ 🔑, **🏃 ⚫️ 🌐** & 👀 🚥 ⚫️ 🤙 ❎ ⚠. - -* ⤴️ **🏤** 💬 👈 👆 👈, 👈 ❔ 👤 🔜 💭 👆 🤙 ✅ ⚫️. - -/// info - -👐, 👤 💪 🚫 🎯 💙 🎸 👈 ✔️ 📚 ✔. - -📚 🕰 ⚫️ ✔️ 🔨 👈 📤 🎸 ⏮️ 3️⃣, 5️⃣ ⚖️ 🌅 ✔, 🎲 ↩️ 📛 😌, ✋️ 🕐❔ 👤 ✅ 🎸, 👫 🤙 💔, ✔️ 🐛, ⚖️ 🚫 ❎ ⚠ 👫 🛄 ❎. 👶 - -, ⚫️ 🤙 ⚠ 👈 👆 🤙 ✍ & 🏃 📟, & ➡️ 👤 💭 🏤 👈 👆. 👶 - -/// - -* 🚥 🇵🇷 💪 📉 🌌, 👆 💪 💭 👈, ✋️ 📤 🙅‍♂ 💪 💁‍♂️ 😟, 📤 5️⃣📆 📚 🤔 ☝ 🎑 (& 👤 🔜 ✔️ 👇 👍 👍 👶), ⚫️ 👻 🚥 👆 💪 🎯 🔛 ⚛ 👜. - -### 💯 - -* ℹ 👤 ✅ 👈 🇵🇷 ✔️ **💯**. - -* ✅ 👈 💯 **❌** ⏭ 🇵🇷. 👶 - -* ⤴️ ✅ 👈 💯 **🚶‍♀️** ⏮️ 🇵🇷. 👶 - -* 📚 🎸 🚫 ✔️ 💯, 👆 💪 **🎗** 👫 🚮 💯, ⚖️ 👆 💪 **🤔** 💯 👆. 👈 1️⃣ 👜 👈 🍴 🌅 🕰 & 👆 💪 ℹ 📚 ⏮️ 👈. - -* ⤴️ 🏤 ⚫️❔ 👆 🔄, 👈 🌌 👤 🔜 💭 👈 👆 ✅ ⚫️. 👶 - -## ✍ 🚲 📨 - -👆 💪 [📉](contributing.md){.internal-link target=_blank} ℹ 📟 ⏮️ 🚲 📨, 🖼: - -* 🔧 🤭 👆 🔎 🔛 🧾. -* 💰 📄, 📹, ⚖️ 📻 👆 ✍ ⚖️ 🔎 🔃 FastAPI ✍ 👉 📁. - * ⚒ 💭 👆 🚮 👆 🔗 ▶️ 🔗 📄. -* ℹ [💬 🧾](contributing.md#_9){.internal-link target=_blank} 👆 🇪🇸. - * 👆 💪 ℹ 📄 ✍ ✍ 🎏. -* 🛠️ 🆕 🧾 📄. -* 🔧 ♻ ❔/🐛. - * ⚒ 💭 🚮 💯. -* 🚮 🆕 ⚒. - * ⚒ 💭 🚮 💯. - * ⚒ 💭 🚮 🧾 🚥 ⚫️ 🔗. - -## ℹ 🚧 FastAPI - -ℹ 👤 🚧 **FastAPI**❗ 👶 - -📤 📚 👷, & 🏆 ⚫️, **👆** 💪 ⚫️. - -👑 📋 👈 👆 💪 ▶️️ 🔜: - -* [ℹ 🎏 ⏮️ ❔ 📂](#i){.internal-link target=_blank} (👀 📄 🔛). -* [📄 🚲 📨](#i){.internal-link target=_blank} (👀 📄 🔛). - -👈 2️⃣ 📋 ⚫️❔ **🍴 🕰 🏆**. 👈 👑 👷 🏆 FastAPI. - -🚥 👆 💪 ℹ 👤 ⏮️ 👈, **👆 🤝 👤 🚧 FastAPI** & ⚒ 💭 ⚫️ 🚧 **🛠️ ⏩ & 👻**. 👶 - -## 🛑 💬 - -🛑 👶 😧 💬 💽 👶 & 🤙 👅 ⏮️ 🎏 FastAPI 👪. - -/// tip - -❔, 💭 👫 📂 💬, 📤 🌅 👍 🤞 👆 🔜 📨 ℹ [FastAPI 🕴](fastapi-people.md#_2){.internal-link target=_blank}. - -⚙️ 💬 🕴 🎏 🏢 💬. - -/// - -### 🚫 ⚙️ 💬 ❔ - -✔️ 🤯 👈 💬 ✔ 🌅 "🆓 💬", ⚫️ ⏩ 💭 ❔ 👈 💁‍♂️ 🏢 & 🌅 ⚠ ❔,, 👆 💪 🚫 📨 ❔. - -📂, 📄 🔜 🦮 👆 ✍ ▶️️ ❔ 👈 👆 💪 🌖 💪 🤚 👍 ❔, ⚖️ ❎ ⚠ 👆 ⏭ 💬. & 📂 👤 💪 ⚒ 💭 👤 🕧 ❔ 🌐, 🚥 ⚫️ ✊ 🕰. 👤 💪 🚫 🤙 👈 ⏮️ 💬 ⚙️. 👶 - -💬 💬 ⚙️ 🚫 💪 📇 📂, ❔ & ❔ 5️⃣📆 🤚 💸 💬. & 🕴 🕐 📂 💯 ▶️️ [FastAPI 🕴](fastapi-people.md#_2){.internal-link target=_blank}, 👆 🔜 🌅 🎲 📨 🌅 🙋 📂. - -🔛 🎏 🚄, 📤 💯 👩‍💻 💬 ⚙️, 📤 ↕ 🤞 👆 🔜 🔎 👱 💬 📤, 🌖 🌐 🕰. 👶 - -## 💰 📕 - -👆 💪 💰 🐕‍🦺 📕 (👤) 🔘 📂 💰. - -📤 👆 💪 🛍 👤 ☕ 👶 👶 💬 👏. 👶 - -& 👆 💪 ▶️️ 🥇1st ⚖️ 🌟 💰 FastAPI. 👶 👶 - -## 💰 🧰 👈 🏋️ FastAPI - -👆 ✔️ 👀 🧾, FastAPI 🧍 🔛 ⌚ 🐘, 💃 & Pydantic. - -👆 💪 💰: - -* ✡ 🍏 (Pydantic) -* 🗜 (💃, Uvicorn) - ---- - -👏 ❗ 👶 diff --git a/docs/em/docs/history-design-future.md b/docs/em/docs/history-design-future.md deleted file mode 100644 index fe36643959..0000000000 --- a/docs/em/docs/history-design-future.md +++ /dev/null @@ -1,79 +0,0 @@ -# 📖, 🔧 & 🔮 - -🕰 🏁, **FastAPI** 👩‍💻 💭: - -> ⚫️❔ 📖 👉 🏗 ❓ ⚫️ 😑 ✔️ 👟 ⚪️➡️ 🕳 👌 👩‍❤‍👨 🗓️ [...] - -📥 🐥 🍖 👈 📖. - -## 🎛 - -👤 ✔️ 🏗 🔗 ⏮️ 🏗 📄 📚 1️⃣2️⃣🗓️ (🎰 🏫, 📎 ⚙️, 🔁 👨‍🏭, ☁ 💽, ♒️), ↘️ 📚 🏉 👩‍💻. - -🍕 👈, 👤 💪 🔬, 💯 & ⚙️ 📚 🎛. - -📖 **FastAPI** 👑 🍕 📖 🚮 ⏪. - -🙆‍♀ 📄 [🎛](alternatives.md){.internal-link target=_blank}: - -
- -**FastAPI** 🚫🔜 🔀 🚥 🚫 ⏮️ 👷 🎏. - -📤 ✔️ 📚 🧰 ✍ ⏭ 👈 ✔️ ℹ 😮 🚮 🏗. - -👤 ✔️ ❎ 🏗 🆕 🛠️ 📚 1️⃣2️⃣🗓️. 🥇 👤 🔄 ❎ 🌐 ⚒ 📔 **FastAPI** ⚙️ 📚 🎏 🛠️, 🔌-🔌, & 🧰. - -✋️ ☝, 📤 🙅‍♂ 🎏 🎛 🌘 🏗 🕳 👈 🚚 🌐 👫 ⚒, ✊ 🏆 💭 ⚪️➡️ ⏮️ 🧰, & 🌀 👫 🏆 🌌 💪, ⚙️ 🇪🇸 ⚒ 👈 ➖🚫 💪 ⏭ (🐍 3️⃣.6️⃣ ➕ 🆎 🔑). - -
- -## 🔬 - -⚙️ 🌐 ⏮️ 🎛 👤 ✔️ 🤞 💡 ⚪️➡️ 🌐 👫, ✊ 💭, & 🌀 👫 🏆 🌌 👤 💪 🔎 👤 & 🏉 👩‍💻 👤 ✔️ 👷 ⏮️. - -🖼, ⚫️ 🆑 👈 🎲 ⚫️ 🔜 ⚓️ 🔛 🐩 🐍 🆎 🔑. - -, 🏆 🎯 ⚙️ ⏪ ♻ 🐩. - -, ⏭ ▶️ 📟 **FastAPI**, 👤 💸 📚 🗓️ 🎓 🔌 🗄, 🎻 🔗, Oauth2️⃣, ♒️. 🎯 👫 💛, 🔀, & 🔺. - -## 🔧 - -⤴️ 👤 💸 🕰 🔧 👩‍💻 "🛠️" 👤 💚 ✔️ 👩‍💻 (👩‍💻 ⚙️ FastAPI). - -👤 💯 📚 💭 🏆 🌟 🐍 👨‍🎨: 🗒, 🆚 📟, 🎠 🧢 👨‍🎨. - -🏁 🐍 👩‍💻 🔬, 👈 📔 🔃 8️⃣0️⃣ 💯 👩‍💻. - -⚫️ ⛓ 👈 **FastAPI** 🎯 💯 ⏮️ 👨‍🎨 ⚙️ 8️⃣0️⃣ 💯 🐍 👩‍💻. & 🏆 🎏 👨‍🎨 😑 👷 ➡, 🌐 🚮 💰 🔜 👷 🌖 🌐 👨‍🎨. - -👈 🌌 👤 💪 🔎 🏆 🌌 📉 📟 ❎ 🌅 💪, ✔️ 🛠️ 🌐, 🆎 & ❌ ✅, ♒️. - -🌐 🌌 👈 🚚 🏆 🛠️ 💡 🌐 👩‍💻. - -## 📄 - -⏮️ 🔬 📚 🎛, 👤 💭 👈 👤 🔜 ⚙️ **Pydantic** 🚮 📈. - -⤴️ 👤 📉 ⚫️, ⚒ ⚫️ 🍕 🛠️ ⏮️ 🎻 🔗, 🐕‍🦺 🎏 🌌 🔬 ⚛ 📄, & 📉 👨‍🎨 🐕‍🦺 (🆎 ✅, ✍) ⚓️ 🔛 💯 📚 👨‍🎨. - -⏮️ 🛠️, 👤 📉 **💃**, 🎏 🔑 📄. - -## 🛠️ - -🕰 👤 ▶️ 🏗 **FastAPI** ⚫️, 🏆 🍖 ⏪ 🥉, 🔧 🔬, 📄 & 🧰 🔜, & 💡 🔃 🐩 & 🔧 🆑 & 🍋. - -## 🔮 - -👉 ☝, ⚫️ ⏪ 🆑 👈 **FastAPI** ⏮️ 🚮 💭 ➖ ⚠ 📚 👫👫. - -⚫️ 💆‍♂ 👐 🤭 ⏮️ 🎛 ♣ 📚 ⚙️ 💼 👍. - -📚 👩‍💻 & 🏉 ⏪ 🪀 🔛 **FastAPI** 👫 🏗 (🔌 👤 & 👇 🏉). - -✋️, 📤 📚 📈 & ⚒ 👟. - -**FastAPI** ✔️ 👑 🔮 ⤴️. - -& [👆 ℹ](help-fastapi.md){.internal-link target=_blank} 📉 👍. diff --git a/docs/em/docs/how-to/conditional-openapi.md b/docs/em/docs/how-to/conditional-openapi.md deleted file mode 100644 index e47ea0c35c..0000000000 --- a/docs/em/docs/how-to/conditional-openapi.md +++ /dev/null @@ -1,56 +0,0 @@ -# 🎲 🗄 - -🚥 👆 💪, 👆 💪 ⚙️ ⚒ & 🌐 🔢 🔗 🗄 ✔ ⚓️ 🔛 🌐, & ❎ ⚫️ 🍕. - -## 🔃 💂‍♂, 🔗, & 🩺 - -🕵‍♂ 👆 🧾 👩‍💻 🔢 🏭 *🚫🔜 🚫* 🌌 🛡 👆 🛠️. - -👈 🚫 🚮 🙆 ➕ 💂‍♂ 👆 🛠️, *➡ 🛠️* 🔜 💪 🌐❔ 👫. - -🚥 📤 💂‍♂ ⚠ 👆 📟, ⚫️ 🔜 🔀. - -🕵‍♂ 🧾 ⚒ ⚫️ 🌅 ⚠ 🤔 ❔ 🔗 ⏮️ 👆 🛠️, & 💪 ⚒ ⚫️ 🌅 ⚠ 👆 ℹ ⚫️ 🏭. ⚫️ 💪 🤔 🎯 📨 💂‍♂ 🔘 🌌. - -🚥 👆 💚 🔐 👆 🛠️, 📤 📚 👍 👜 👆 💪, 🖼: - -* ⚒ 💭 👆 ✔️ 👍 🔬 Pydantic 🏷 👆 📨 💪 & 📨. -* 🔗 🙆 ✔ ✔ & 🔑 ⚙️ 🔗. -* 🙅 🏪 🔢 🔐, 🕴 🔐#️⃣. -* 🛠️ & ⚙️ 👍-💭 🔐 🧰, 💖 🇸🇲 & 🥙 🤝, ♒️. -* 🚮 🌅 🧽 ✔ 🎛 ⏮️ Oauth2️⃣ ↔ 🌐❔ 💪. -* ...♒️. - -👐, 👆 5️⃣📆 ✔️ 📶 🎯 ⚙️ 💼 🌐❔ 👆 🤙 💪 ❎ 🛠️ 🩺 🌐 (✅ 🏭) ⚖️ ⚓️ 🔛 📳 ⚪️➡️ 🌐 🔢. - -## 🎲 🗄 ⚪️➡️ ⚒ & 🇨🇻 { - -👆 💪 💪 ⚙️ 🎏 Pydantic ⚒ 🔗 👆 🏗 🗄 & 🩺 ⚜. - -🖼: - -{* ../../docs_src/conditional_openapi/tutorial001.py hl[6,11] *} - -📥 👥 📣 ⚒ `openapi_url` ⏮️ 🎏 🔢 `"/openapi.json"`. - -& ⤴️ 👥 ⚙️ ⚫️ 🕐❔ 🏗 `FastAPI` 📱. - -⤴️ 👆 💪 ❎ 🗄 (✅ 🎚 🩺) ⚒ 🌐 🔢 `OPENAPI_URL` 🛁 🎻, 💖: - -
- -```console -$ OPENAPI_URL= uvicorn main:app - -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -``` - -
- -⤴️ 🚥 👆 🚶 📛 `/openapi.json`, `/docs`, ⚖️ `/redoc` 👆 🔜 🤚 `404 Not Found` ❌ 💖: - -```JSON -{ - "detail": "Not Found" -} -``` diff --git a/docs/em/docs/how-to/custom-request-and-route.md b/docs/em/docs/how-to/custom-request-and-route.md deleted file mode 100644 index 32d7fbfec5..0000000000 --- a/docs/em/docs/how-to/custom-request-and-route.md +++ /dev/null @@ -1,109 +0,0 @@ -# 🛃 📨 & APIRoute 🎓 - -💼, 👆 5️⃣📆 💚 🔐 ⚛ ⚙️ `Request` & `APIRoute` 🎓. - -🎯, 👉 5️⃣📆 👍 🎛 ⚛ 🛠️. - -🖼, 🚥 👆 💚 ✍ ⚖️ 🔬 📨 💪 ⏭ ⚫️ 🛠️ 👆 🈸. - -/// danger - -👉 "🏧" ⚒. - -🚥 👆 ▶️ ⏮️ **FastAPI** 👆 💪 💚 🚶 👉 📄. - -/// - -## ⚙️ 💼 - -⚙️ 💼 🔌: - -* 🏭 🚫-🎻 📨 💪 🎻 (✅ `msgpack`). -* 🗜 🗜-🗜 📨 💪. -* 🔁 🚨 🌐 📨 💪. - -## 🚚 🛃 📨 💪 🔢 - -➡️ 👀 ❔ ⚒ ⚙️ 🛃 `Request` 🏿 🗜 🗜 📨. - -& `APIRoute` 🏿 ⚙️ 👈 🛃 📨 🎓. - -### ✍ 🛃 `GzipRequest` 🎓 - -/// tip - -👉 🧸 🖼 🎦 ❔ ⚫️ 👷, 🚥 👆 💪 🗜 🐕‍🦺, 👆 💪 ⚙️ 🚚 [`GzipMiddleware`](../advanced/middleware.md#gzipmiddleware){.internal-link target=_blank}. - -/// - -🥇, 👥 ✍ `GzipRequest` 🎓, ❔ 🔜 📁 `Request.body()` 👩‍🔬 🗜 💪 🔍 ☑ 🎚. - -🚥 📤 🙅‍♂ `gzip` 🎚, ⚫️ 🔜 🚫 🔄 🗜 💪. - -👈 🌌, 🎏 🛣 🎓 💪 🍵 🗜 🗜 ⚖️ 🗜 📨. - -{* ../../docs_src/custom_request_and_route/tutorial001.py hl[8:15] *} - -### ✍ 🛃 `GzipRoute` 🎓 - -⏭, 👥 ✍ 🛃 🏿 `fastapi.routing.APIRoute` 👈 🔜 ⚒ ⚙️ `GzipRequest`. - -👉 🕰, ⚫️ 🔜 📁 👩‍🔬 `APIRoute.get_route_handler()`. - -👉 👩‍🔬 📨 🔢. & 👈 🔢 ⚫️❔ 🔜 📨 📨 & 📨 📨. - -📥 👥 ⚙️ ⚫️ ✍ `GzipRequest` ⚪️➡️ ⏮️ 📨. - -{* ../../docs_src/custom_request_and_route/tutorial001.py hl[18:26] *} - -/// note | 📡 ℹ - -`Request` ✔️ `request.scope` 🔢, 👈 🐍 `dict` ⚗ 🗃 🔗 📨. - - `Request` ✔️ `request.receive`, 👈 🔢 "📨" 💪 📨. - - `scope` `dict` & `receive` 🔢 👯‍♂️ 🍕 🔫 🔧. - - & 👈 2️⃣ 👜, `scope` & `receive`, ⚫️❔ 💪 ✍ 🆕 `Request` 👐. - -💡 🌅 🔃 `Request` ✅ 💃 🩺 🔃 📨. - -/// - -🕴 👜 🔢 📨 `GzipRequest.get_route_handler` 🔨 🎏 🗜 `Request` `GzipRequest`. - -🔨 👉, 👆 `GzipRequest` 🔜 ✊ 💅 🗜 📊 (🚥 💪) ⏭ 🚶‍♀️ ⚫️ 👆 *➡ 🛠️*. - -⏮️ 👈, 🌐 🏭 ⚛ 🎏. - -✋️ ↩️ 👆 🔀 `GzipRequest.body`, 📨 💪 🔜 🔁 🗜 🕐❔ ⚫️ 📐 **FastAPI** 🕐❔ 💪. - -## 🔐 📨 💪 ⚠ 🐕‍🦺 - -/// tip - -❎ 👉 🎏 ⚠, ⚫️ 🎲 📚 ⏩ ⚙️ `body` 🛃 🐕‍🦺 `RequestValidationError` ([🚚 ❌](../tutorial/handling-errors.md#requestvalidationerror){.internal-link target=_blank}). - -✋️ 👉 🖼 ☑ & ⚫️ 🎦 ❔ 🔗 ⏮️ 🔗 🦲. - -/// - -👥 💪 ⚙️ 👉 🎏 🎯 🔐 📨 💪 ⚠ 🐕‍🦺. - -🌐 👥 💪 🍵 📨 🔘 `try`/`except` 🍫: - -{* ../../docs_src/custom_request_and_route/tutorial002.py hl[13,15] *} - -🚥 ⚠ 📉, `Request` 👐 🔜 ↔, 👥 💪 ✍ & ⚒ ⚙️ 📨 💪 🕐❔ 🚚 ❌: - -{* ../../docs_src/custom_request_and_route/tutorial002.py hl[16:18] *} - -## 🛃 `APIRoute` 🎓 📻 - -👆 💪 ⚒ `route_class` 🔢 `APIRouter`: - -{* ../../docs_src/custom_request_and_route/tutorial003.py hl[26] *} - -👉 🖼, *➡ 🛠️* 🔽 `router` 🔜 ⚙️ 🛃 `TimedRoute` 🎓, & 🔜 ✔️ ➕ `X-Response-Time` 🎚 📨 ⏮️ 🕰 ⚫️ ✊ 🏗 📨: - -{* ../../docs_src/custom_request_and_route/tutorial003.py hl[13:20] *} diff --git a/docs/em/docs/how-to/extending-openapi.md b/docs/em/docs/how-to/extending-openapi.md deleted file mode 100644 index c3e6c7f667..0000000000 --- a/docs/em/docs/how-to/extending-openapi.md +++ /dev/null @@ -1,83 +0,0 @@ -# ↔ 🗄 - -/// warning - -👉 👍 🏧 ⚒. 👆 🎲 💪 🚶 ⚫️. - -🚥 👆 📄 🔰 - 👩‍💻 🦮, 👆 💪 🎲 🚶 👉 📄. - -🚥 👆 ⏪ 💭 👈 👆 💪 🔀 🏗 🗄 🔗, 😣 👂. - -/// - -📤 💼 🌐❔ 👆 💪 💪 🔀 🏗 🗄 🔗. - -👉 📄 👆 🔜 👀 ❔. - -## 😐 🛠️ - -😐 (🔢) 🛠️, ⏩. - -`FastAPI` 🈸 (👐) ✔️ `.openapi()` 👩‍🔬 👈 📈 📨 🗄 🔗. - -🍕 🈸 🎚 🏗, *➡ 🛠️* `/openapi.json` (⚖️ ⚫️❔ 👆 ⚒ 👆 `openapi_url`) ®. - -⚫️ 📨 🎻 📨 ⏮️ 🏁 🈸 `.openapi()` 👩‍🔬. - -🔢, ⚫️❔ 👩‍🔬 `.openapi()` 🔨 ✅ 🏠 `.openapi_schema` 👀 🚥 ⚫️ ✔️ 🎚 & 📨 👫. - -🚥 ⚫️ 🚫, ⚫️ 🏗 👫 ⚙️ 🚙 🔢 `fastapi.openapi.utils.get_openapi`. - -& 👈 🔢 `get_openapi()` 📨 🔢: - -* `title`: 🗄 📛, 🎦 🩺. -* `version`: ⏬ 👆 🛠️, ✅ `2.5.0`. -* `openapi_version`: ⏬ 🗄 🔧 ⚙️. 🔢, ⏪: `3.0.2`. -* `description`: 📛 👆 🛠️. -* `routes`: 📇 🛣, 👫 🔠 ® *➡ 🛠️*. 👫 ✊ ⚪️➡️ `app.routes`. - -## 🔑 🔢 - -⚙️ ℹ 🔛, 👆 💪 ⚙️ 🎏 🚙 🔢 🏗 🗄 🔗 & 🔐 🔠 🍕 👈 👆 💪. - -🖼, ➡️ 🚮 📄 🗄 ↔ 🔌 🛃 🔱. - -### 😐 **FastAPI** - -🥇, ✍ 🌐 👆 **FastAPI** 🈸 🛎: - -{* ../../docs_src/extending_openapi/tutorial001.py hl[1,4,7:9] *} - -### 🏗 🗄 🔗 - -⤴️, ⚙️ 🎏 🚙 🔢 🏗 🗄 🔗, 🔘 `custom_openapi()` 🔢: - -{* ../../docs_src/extending_openapi/tutorial001.py hl[2,15:20] *} - -### 🔀 🗄 🔗 - -🔜 👆 💪 🚮 📄 ↔, ❎ 🛃 `x-logo` `info` "🎚" 🗄 🔗: - -{* ../../docs_src/extending_openapi/tutorial001.py hl[21:23] *} - -### 💾 🗄 🔗 - -👆 💪 ⚙️ 🏠 `.openapi_schema` "💾", 🏪 👆 🏗 🔗. - -👈 🌌, 👆 🈸 🏆 🚫 ✔️ 🏗 🔗 🔠 🕰 👩‍💻 📂 👆 🛠️ 🩺. - -⚫️ 🔜 🏗 🕴 🕐, & ⤴️ 🎏 💾 🔗 🔜 ⚙️ ⏭ 📨. - -{* ../../docs_src/extending_openapi/tutorial001.py hl[13:14,24:25] *} - -### 🔐 👩‍🔬 - -🔜 👆 💪 ❎ `.openapi()` 👩‍🔬 ⏮️ 👆 🆕 🔢. - -{* ../../docs_src/extending_openapi/tutorial001.py hl[28] *} - -### ✅ ⚫️ - -🕐 👆 🚶 http://127.0.0.1:8000/redoc 👆 🔜 👀 👈 👆 ⚙️ 👆 🛃 🔱 (👉 🖼, **FastAPI**'Ⓜ 🔱): - - diff --git a/docs/em/docs/how-to/graphql.md b/docs/em/docs/how-to/graphql.md deleted file mode 100644 index 083e9ebd28..0000000000 --- a/docs/em/docs/how-to/graphql.md +++ /dev/null @@ -1,60 +0,0 @@ -# 🕹 - -**FastAPI** ⚓️ 🔛 **🔫** 🐩, ⚫️ 📶 ⏩ 🛠️ 🙆 **🕹** 🗃 🔗 ⏮️ 🔫. - -👆 💪 🌀 😐 FastAPI *➡ 🛠️* ⏮️ 🕹 🔛 🎏 🈸. - -/// tip - -**🕹** ❎ 📶 🎯 ⚙️ 💼. - -⚫️ ✔️ **📈** & **⚠** 🕐❔ 🔬 ⚠ **🕸 🔗**. - -⚒ 💭 👆 🔬 🚥 **💰** 👆 ⚙️ 💼 ⚖ **👐**. 👶 - -/// - -## 🕹 🗃 - -📥 **🕹** 🗃 👈 ✔️ **🔫** 🐕‍🦺. 👆 💪 ⚙️ 👫 ⏮️ **FastAPI**: - -* 🍓 👶 - * ⏮️ 🩺 FastAPI -* 👸 - * ⏮️ 🩺 FastAPI -* 🍟 - * ⏮️ 🍟 🔫 🚚 🔫 🛠️ -* - * ⏮️ 💃-Graphene3️⃣ - -## 🕹 ⏮️ 🍓 - -🚥 👆 💪 ⚖️ 💚 👷 ⏮️ **🕹**, **🍓** **👍** 🗃 ⚫️ ✔️ 🔧 🔐 **FastAPI** 🔧, ⚫️ 🌐 ⚓️ 🔛 **🆎 ✍**. - -⚓️ 🔛 👆 ⚙️ 💼, 👆 5️⃣📆 💖 ⚙️ 🎏 🗃, ✋️ 🚥 👆 💭 👤, 👤 🔜 🎲 🤔 👆 🔄 **🍓**. - -📥 🤪 🎮 ❔ 👆 💪 🛠️ 🍓 ⏮️ FastAPI: - -{* ../../docs_src/graphql/tutorial001.py hl[3,22,25:26] *} - -👆 💪 💡 🌅 🔃 🍓 🍓 🧾. - -& 🩺 🔃 🍓 ⏮️ FastAPI. - -## 🗝 `GraphQLApp` ⚪️➡️ 💃 - -⏮️ ⏬ 💃 🔌 `GraphQLApp` 🎓 🛠️ ⏮️ . - -⚫️ 😢 ⚪️➡️ 💃, ✋️ 🚥 👆 ✔️ 📟 👈 ⚙️ ⚫️, 👆 💪 💪 **↔** 💃-Graphene3️⃣, 👈 📔 🎏 ⚙️ 💼 & ✔️ **🌖 🌓 🔢**. - -/// tip - -🚥 👆 💪 🕹, 👤 🔜 👍 👆 ✅ 👅 🍓, ⚫️ ⚓️ 🔛 🆎 ✍ ↩️ 🛃 🎓 & 🆎. - -/// - -## 💡 🌅 - -👆 💪 💡 🌅 🔃 **🕹** 🛂 🕹 🧾. - -👆 💪 ✍ 🌅 🔃 🔠 👈 🗃 🔬 🔛 👫 🔗. diff --git a/docs/em/docs/index.md b/docs/em/docs/index.md deleted file mode 100644 index 5f5fc2e393..0000000000 --- a/docs/em/docs/index.md +++ /dev/null @@ -1,474 +0,0 @@ -# FastAPI - - - -

- FastAPI -

-

- FastAPI 🛠️, ↕ 🎭, ⏩ 💡, ⏩ 📟, 🔜 🏭 -

-

- - Test - - - Coverage - - - Package version - - - Supported Python versions - -

- ---- - -**🧾**: https://fastapi.tiangolo.com - -**ℹ 📟**: https://github.com/fastapi/fastapi - ---- - -FastAPI 🏛, ⏩ (↕-🎭), 🕸 🛠️ 🏗 🛠️ ⏮️ 🐍 3️⃣.8️⃣ ➕ ⚓️ 🔛 🐩 🐍 🆎 🔑. - -🔑 ⚒: - -* **⏩**: 📶 ↕ 🎭, 🔛 🇷🇪 ⏮️ **✳** & **🚶** (👏 💃 & Pydantic). [1️⃣ ⏩ 🐍 🛠️ 💪](#_15). -* **⏩ 📟**: 📈 🚅 🛠️ ⚒ 🔃 2️⃣0️⃣0️⃣ 💯 3️⃣0️⃣0️⃣ 💯. * -* **👩‍❤‍👨 🐛**: 📉 🔃 4️⃣0️⃣ 💯 🗿 (👩‍💻) 📉 ❌. * -* **🏋️**: 👑 👨‍🎨 🐕‍🦺. 🛠️ 🌐. 🌘 🕰 🛠️. -* **⏩**: 🔧 ⏩ ⚙️ & 💡. 🌘 🕰 👂 🩺. -* **📏**: 📉 📟 ❎. 💗 ⚒ ⚪️➡️ 🔠 🔢 📄. 👩‍❤‍👨 🐛. -* **🏋️**: 🤚 🏭-🔜 📟. ⏮️ 🏧 🎓 🧾. -* **🐩-⚓️**: ⚓️ 🔛 (& 🍕 🔗 ⏮️) 📂 🐩 🔗: 🗄 (⏪ 💭 🦁) & 🎻 🔗. - -* ⚖ ⚓️ 🔛 💯 🔛 🔗 🛠️ 🏉, 🏗 🏭 🈸. - -## 💰 - - - -{% if sponsors %} -{% for sponsor in sponsors.gold -%} - -{% endfor -%} -{%- for sponsor in sponsors.silver -%} - -{% endfor %} -{% endif %} - - - -🎏 💰 - -## 🤔 - -"_[...] 👤 ⚙️ **FastAPI** 📚 👫 📆. [...] 👤 🤙 📆 ⚙️ ⚫️ 🌐 👇 🏉 **⚗ 🐕‍🦺 🤸‍♂**. 👫 💆‍♂ 🛠️ 🔘 🐚 **🖥** 🏬 & **📠** 🏬._" - -
🧿 🇵🇰 - 🤸‍♂ (🇦🇪)
- ---- - -"_👥 🛠️ **FastAPI** 🗃 🤖 **🎂** 💽 👈 💪 🔢 🚚 **🔮**. [👨📛]_" - -
🇮🇹 🇸🇻, 👨📛 👨📛, & 🇱🇰 🕉 🕉 - 🙃 (🇦🇪)
- ---- - -"_**📺** 🙏 📣 📂-ℹ 🚀 👆 **⚔ 🧾** 🎶 🛠️: **📨**❗ [🏗 ⏮️ **FastAPI**]_" - -
✡ 🍏, 👖 🇪🇸, 🌲 🍏 - 📺 (🇦🇪)
- ---- - -"_👤 🤭 🌕 😄 🔃 **FastAPI**. ⚫️ 🎊 ❗_" - -
✡ 🇭🇰 - 🐍 🔢 📻 🦠 (🇦🇪)
- ---- - -"_🤙, ⚫️❔ 👆 ✔️ 🏗 👀 💎 💠 & 🇵🇱. 📚 🌌, ⚫️ ⚫️❔ 👤 💚 **🤗** - ⚫️ 🤙 😍 👀 👱 🏗 👈._" - -
✡ 🗄 - 🤗 👼 (🇦🇪)
- ---- - -"_🚥 👆 👀 💡 1️⃣ **🏛 🛠️** 🏗 🎂 🔗, ✅ 👅 **FastAPI** [...] ⚫️ ⏩, ⏩ ⚙️ & ⏩ 💡 [...]_" - -"_👥 ✔️ 🎛 🤭 **FastAPI** 👆 **🔗** [...] 👤 💭 👆 🔜 💖 ⚫️ [...]_" - -
🇱🇨 🇸🇲 - ✡ Honnibal - 💥 👲 🕴 - 🌈 👼 (🇦🇪) - (🇦🇪)
- ---- - -"_🚥 🙆 👀 🏗 🏭 🐍 🛠️, 👤 🔜 🏆 👍 **FastAPI**. ⚫️ **💎 🏗**, **🙅 ⚙️** & **🏆 🛠️**, ⚫️ ✔️ ▶️️ **🔑 🦲** 👆 🛠️ 🥇 🛠️ 🎛 & 🚘 📚 🏧 & 🐕‍🦺 ✅ 👆 🕹 🔫 👨‍💻._" - -
🇹🇦 🍰 - 📻 (🇦🇪)
- ---- - -## **🏎**, FastAPI 🇳🇨 - - - -🚥 👆 🏗 📱 ⚙️ 📶 ↩️ 🕸 🛠️, ✅ 👅 **🏎**. - -**🏎** FastAPI 🐥 👪. & ⚫️ 🎯 **FastAPI 🇳🇨**. 👶 👶 👶 - -## 📄 - -🐍 3️⃣.7️⃣ ➕ - -FastAPI 🧍 🔛 ⌚ 🐘: - -* 💃 🕸 🍕. -* Pydantic 📊 🍕. - -## 👷‍♂ - -
- -```console -$ pip install "fastapi[standard]" - ----> 100% -``` - -
- -👆 🔜 💪 🔫 💽, 🏭 ✅ Uvicorn ⚖️ Hypercorn. - -
- -```console -$ pip install "uvicorn[standard]" - ----> 100% -``` - -
- -## 🖼 - -### ✍ ⚫️ - -* ✍ 📁 `main.py` ⏮️: - -```Python -from typing import Union - -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} -``` - -
-⚖️ ⚙️ async def... - -🚥 👆 📟 ⚙️ `async` / `await`, ⚙️ `async def`: - -```Python hl_lines="9 14" -from typing import Union - -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -async def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -async def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} -``` - -**🗒**: - -🚥 👆 🚫 💭, ✅ _"🏃 ❓" _ 📄 🔃 `async` & `await` 🩺. - -
- -### 🏃 ⚫️ - -🏃 💽 ⏮️: - -
- -```console -$ uvicorn main:app --reload - -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -INFO: Started reloader process [28720] -INFO: Started server process [28722] -INFO: Waiting for application startup. -INFO: Application startup complete. -``` - -
- -
-🔃 📋 uvicorn main:app --reload... - -📋 `uvicorn main:app` 🔗: - -* `main`: 📁 `main.py` (🐍 "🕹"). -* `app`: 🎚 ✍ 🔘 `main.py` ⏮️ ⏸ `app = FastAPI()`. -* `--reload`: ⚒ 💽 ⏏ ⏮️ 📟 🔀. 🕴 👉 🛠️. - -
- -### ✅ ⚫️ - -📂 👆 🖥 http://127.0.0.1:8000/items/5?q=somequery. - -👆 🔜 👀 🎻 📨: - -```JSON -{"item_id": 5, "q": "somequery"} -``` - -👆 ⏪ ✍ 🛠️ 👈: - -* 📨 🇺🇸🔍 📨 _➡_ `/` & `/items/{item_id}`. -* 👯‍♂️ _➡_ ✊ `GET` 🛠️ (💭 🇺🇸🔍 _👩‍🔬_). -* _➡_ `/items/{item_id}` ✔️ _➡ 🔢_ `item_id` 👈 🔜 `int`. -* _➡_ `/items/{item_id}` ✔️ 📦 `str` _🔢 = `q`. - -### 🎓 🛠️ 🩺 - -🔜 🚶 http://127.0.0.1:8000/docs. - -👆 🔜 👀 🏧 🎓 🛠️ 🧾 (🚚 🦁 🎚): - -![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) - -### 🎛 🛠️ 🩺 - -& 🔜, 🚶 http://127.0.0.1:8000/redoc. - -👆 🔜 👀 🎛 🏧 🧾 (🚚 📄): - -![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) - -## 🖼 ♻ - -🔜 🔀 📁 `main.py` 📨 💪 ⚪️➡️ `PUT` 📨. - -📣 💪 ⚙️ 🐩 🐍 🆎, 👏 Pydantic. - -```Python hl_lines="4 9-12 25-27" -from typing import Union - -from fastapi import FastAPI -from pydantic import BaseModel - -app = FastAPI() - - -class Item(BaseModel): - name: str - price: float - is_offer: Union[bool, None] = None - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} - - -@app.put("/items/{item_id}") -def update_item(item_id: int, item: Item): - return {"item_name": item.name, "item_id": item_id} -``` - -💽 🔜 🔃 🔁 (↩️ 👆 🚮 `--reload` `uvicorn` 📋 🔛). - -### 🎓 🛠️ 🩺 ♻ - -🔜 🚶 http://127.0.0.1:8000/docs. - -* 🎓 🛠️ 🧾 🔜 🔁 ℹ, 🔌 🆕 💪: - -![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) - -* 🖊 🔛 🔼 "🔄 ⚫️ 👅", ⚫️ ✔ 👆 🥧 🔢 & 🔗 🔗 ⏮️ 🛠️: - -![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) - -* ⤴️ 🖊 🔛 "🛠️" 🔼, 👩‍💻 🔢 🔜 🔗 ⏮️ 👆 🛠️, 📨 🔢, 🤚 🏁 & 🎦 👫 🔛 🖥: - -![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) - -### 🎛 🛠️ 🩺 ♻ - -& 🔜, 🚶 http://127.0.0.1:8000/redoc. - -* 🎛 🧾 🔜 🎨 🆕 🔢 🔢 & 💪: - -![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) - -### 🌃 - -📄, 👆 📣 **🕐** 🆎 🔢, 💪, ♒️. 🔢 🔢. - -👆 👈 ⏮️ 🐩 🏛 🐍 🆎. - -👆 🚫 ✔️ 💡 🆕 ❕, 👩‍🔬 ⚖️ 🎓 🎯 🗃, ♒️. - -🐩 **🐍 3️⃣.7️⃣ ➕**. - -🖼, `int`: - -```Python -item_id: int -``` - -⚖️ 🌖 🏗 `Item` 🏷: - -```Python -item: Item -``` - -...& ⏮️ 👈 👁 📄 👆 🤚: - -* 👨‍🎨 🐕‍🦺, 🔌: - * 🛠️. - * 🆎 ✅. -* 🔬 💽: - * 🏧 & 🆑 ❌ 🕐❔ 📊 ❌. - * 🔬 🙇 🐦 🎻 🎚. -* 🛠️ 🔢 💽: 👟 ⚪️➡️ 🕸 🐍 💽 & 🆎. 👂 ⚪️➡️: - * 🎻. - * ➡ 🔢. - * 🔢 🔢. - * 🍪. - * 🎚. - * 📨. - * 📁. -* 🛠️ 🔢 📊: 🗜 ⚪️➡️ 🐍 💽 & 🆎 🕸 💽 (🎻): - * 🗜 🐍 🆎 (`str`, `int`, `float`, `bool`, `list`, ♒️). - * `datetime` 🎚. - * `UUID` 🎚. - * 💽 🏷. - * ...& 📚 🌖. -* 🏧 🎓 🛠️ 🧾, 🔌 2️⃣ 🎛 👩‍💻 🔢: - * 🦁 🎚. - * 📄. - ---- - -👟 🔙 ⏮️ 📟 🖼, **FastAPI** 🔜: - -* ✔ 👈 📤 `item_id` ➡ `GET` & `PUT` 📨. -* ✔ 👈 `item_id` 🆎 `int` `GET` & `PUT` 📨. - * 🚥 ⚫️ 🚫, 👩‍💻 🔜 👀 ⚠, 🆑 ❌. -* ✅ 🚥 📤 📦 🔢 🔢 📛 `q` ( `http://127.0.0.1:8000/items/foo?q=somequery`) `GET` 📨. - * `q` 🔢 📣 ⏮️ `= None`, ⚫️ 📦. - * 🍵 `None` ⚫️ 🔜 🚚 (💪 💼 ⏮️ `PUT`). -* `PUT` 📨 `/items/{item_id}`, ✍ 💪 🎻: - * ✅ 👈 ⚫️ ✔️ ✔ 🔢 `name` 👈 🔜 `str`. - * ✅ 👈 ⚫️ ✔️ ✔ 🔢 `price` 👈 ✔️ `float`. - * ✅ 👈 ⚫️ ✔️ 📦 🔢 `is_offer`, 👈 🔜 `bool`, 🚥 🎁. - * 🌐 👉 🔜 👷 🙇 🐦 🎻 🎚. -* 🗜 ⚪️➡️ & 🎻 🔁. -* 📄 🌐 ⏮️ 🗄, 👈 💪 ⚙️: - * 🎓 🧾 ⚙️. - * 🏧 👩‍💻 📟 ⚡ ⚙️, 📚 🇪🇸. -* 🚚 2️⃣ 🎓 🧾 🕸 🔢 🔗. - ---- - -👥 🖌 🧽, ✋️ 👆 ⏪ 🤚 💭 ❔ ⚫️ 🌐 👷. - -🔄 🔀 ⏸ ⏮️: - -```Python - return {"item_name": item.name, "item_id": item_id} -``` - -...⚪️➡️: - -```Python - ... "item_name": item.name ... -``` - -...: - -```Python - ... "item_price": item.price ... -``` - -...& 👀 ❔ 👆 👨‍🎨 🔜 🚘-🏁 🔢 & 💭 👫 🆎: - -![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) - -🌅 🏁 🖼 🔌 🌅 ⚒, 👀 🔰 - 👩‍💻 🦮. - -**🚘 🚨**: 🔰 - 👩‍💻 🦮 🔌: - -* 📄 **🔢** ⚪️➡️ 🎏 🎏 🥉: **🎚**, **🍪**, **📨 🏑** & **📁**. -* ❔ ⚒ **🔬 ⚛** `maximum_length` ⚖️ `regex`. -* 📶 🏋️ & ⏩ ⚙️ **🔗 💉** ⚙️. -* 💂‍♂ & 🤝, ✅ 🐕‍🦺 **Oauth2️⃣** ⏮️ **🥙 🤝** & **🇺🇸🔍 🔰** 🔐. -* 🌅 🏧 (✋️ 😨 ⏩) ⚒ 📣 **🙇 🐦 🎻 🏷** (👏 Pydantic). -* **🕹** 🛠️ ⏮️ 🍓 & 🎏 🗃. -* 📚 ➕ ⚒ (👏 💃): - * ** *️⃣ ** - * 📶 ⏩ 💯 ⚓️ 🔛 🇸🇲 & `pytest` - * **⚜** - * **🍪 🎉** - * ...& 🌖. - -## 🎭 - -🔬 🇸🇲 📇 🎦 **FastAPI** 🈸 🏃‍♂ 🔽 Uvicorn 1️⃣ ⏩ 🐍 🛠️ 💪, 🕴 🔛 💃 & Uvicorn 👫 (⚙️ 🔘 FastAPI). (*) - -🤔 🌖 🔃 ⚫️, 👀 📄 📇. - -## 📦 🔗 - -⚙️ Pydantic: - -* email-validator - 📧 🔬. - -⚙️ 💃: - -* httpx - ✔ 🚥 👆 💚 ⚙️ `TestClient`. -* jinja2 - ✔ 🚥 👆 💚 ⚙️ 🔢 📄 📳. -* python-multipart - ✔ 🚥 👆 💚 🐕‍🦺 📨 "✍", ⏮️ `request.form()`. -* itsdangerous - ✔ `SessionMiddleware` 🐕‍🦺. -* pyyaml - ✔ 💃 `SchemaGenerator` 🐕‍🦺 (👆 🎲 🚫 💪 ⚫️ ⏮️ FastAPI). - -⚙️ FastAPI / 💃: - -* uvicorn - 💽 👈 📐 & 🍦 👆 🈸. -* orjson - ✔ 🚥 👆 💚 ⚙️ `ORJSONResponse`. -* ujson - ✔ 🚥 👆 💚 ⚙️ `UJSONResponse`. - -👆 💪 ❎ 🌐 👫 ⏮️ `pip install "fastapi[all]"`. - -## 🛂 - -👉 🏗 ® 🔽 ⚖ 🇩🇪 🛂. diff --git a/docs/em/docs/project-generation.md b/docs/em/docs/project-generation.md deleted file mode 100644 index ef6a218215..0000000000 --- a/docs/em/docs/project-generation.md +++ /dev/null @@ -1,84 +0,0 @@ -# 🏗 ⚡ - 📄 - -👆 💪 ⚙️ 🏗 🚂 🤚 ▶️, ⚫️ 🔌 📚 ▶️ ⚒ 🆙, 💂‍♂, 💽 & 🛠️ 🔗 ⏪ ⌛ 👆. - -🏗 🚂 🔜 🕧 ✔️ 📶 🙃 🖥 👈 👆 🔜 ℹ & 🛠️ 👆 👍 💪, ✋️ ⚫️ 💪 👍 ▶️ ☝ 👆 🏗. - -## 🌕 📚 FastAPI ✳ - -📂: https://github.com/tiangolo/full-stack-fastapi-postgresql - -### 🌕 📚 FastAPI ✳ - ⚒ - -* 🌕 **☁** 🛠️ (☁ 🧢). -* ☁ 🐝 📳 🛠️. -* **☁ ✍** 🛠️ & 🛠️ 🇧🇿 🛠️. -* **🏭 🔜** 🐍 🕸 💽 ⚙️ Uvicorn & 🐁. -* 🐍 **FastAPI** 👩‍💻: - * **⏩**: 📶 ↕ 🎭, 🔛 🇷🇪 ⏮️ **✳** & **🚶** (👏 💃 & Pydantic). - * **🏋️**: 👑 👨‍🎨 🐕‍🦺. 🛠️ 🌐. 🌘 🕰 🛠️. - * **⏩**: 🔧 ⏩ ⚙️ & 💡. 🌘 🕰 👂 🩺. - * **📏**: 📉 📟 ❎. 💗 ⚒ ⚪️➡️ 🔠 🔢 📄. - * **🏋️**: 🤚 🏭-🔜 📟. ⏮️ 🏧 🎓 🧾. - * **🐩-⚓️**: ⚓️ 🔛 (& 🍕 🔗 ⏮️) 📂 🐩 🔗: 🗄 & 🎻 🔗. - * **📚 🎏 ⚒** 🔌 🏧 🔬, 🛠️, 🎓 🧾, 🤝 ⏮️ Oauth2️⃣ 🥙 🤝, ♒️. -* **🔐 🔐** 🔁 🔢. -* **🥙 🤝** 🤝. -* **🇸🇲** 🏷 (🔬 🏺 ↔, 👫 💪 ⚙️ ⏮️ 🥒 👨‍🏭 🔗). -* 🔰 ▶️ 🏷 👩‍💻 (🔀 & ❎ 👆 💪). -* **⚗** 🛠️. -* **⚜** (✖️ 🇨🇳 ℹ 🤝). -* **🥒** 👨‍🏭 👈 💪 🗄 & ⚙️ 🏷 & 📟 ⚪️➡️ 🎂 👩‍💻 🍕. -* 🎂 👩‍💻 💯 ⚓️ 🔛 **✳**, 🛠️ ⏮️ ☁, 👆 💪 💯 🌕 🛠️ 🔗, 🔬 🔛 💽. ⚫️ 🏃 ☁, ⚫️ 💪 🏗 🆕 💽 🏪 ⚪️➡️ 🖌 🔠 🕰 (👆 💪 ⚙️ ✳, ✳, ✳, ⚖️ ⚫️❔ 👆 💚, & 💯 👈 🛠️ 👷). -* ⏩ 🐍 🛠️ ⏮️ **📂 💾** 🛰 ⚖️-☁ 🛠️ ⏮️ ↔ 💖 ⚛ ⚗ ⚖️ 🎙 🎙 📟 📂. -* **🎦** 🕸: - * 🏗 ⏮️ 🎦 ✳. - * **🥙 🤝** 🚚. - * 💳 🎑. - * ⏮️ 💳, 👑 🕹 🎑. - * 👑 🕹 ⏮️ 👩‍💻 🏗 & 📕. - * 👤 👩‍💻 📕. - * **🇷🇪**. - * **🎦-📻**. - * **Vuetify** 🌹 🧽 🔧 🦲. - * **📕**. - * ☁ 💽 ⚓️ 🔛 **👌** (📶 🤾 🎆 ⏮️ 🎦-📻). - * ☁ 👁-▶️ 🏗, 👆 🚫 💪 🖊 ⚖️ 💕 ✍ 📟. - * 🕸 💯 🏃 🏗 🕰 (💪 🔕 💁‍♂️). - * ⚒ 🔧 💪, ⚫️ 👷 👅 📦, ✋️ 👆 💪 🏤-🏗 ⏮️ 🎦 ✳ ⚖️ ✍ ⚫️ 👆 💪, & 🏤-⚙️ ⚫️❔ 👆 💚. -* ** *️⃣ ** ✳ 💽, 👆 💪 🔀 ⚫️ ⚙️ 📁 & ✳ 💪. -* **🥀** 🥒 👨‍🏭 ⚖. -* 📐 ⚖ 🖖 🕸 & 👩‍💻 ⏮️ **Traefik**, 👆 💪 ✔️ 👯‍♂️ 🔽 🎏 🆔, 👽 ➡, ✋️ 🍦 🎏 📦. -* Traefik 🛠️, ✅ ➡️ 🗜 **🇺🇸🔍** 📄 🏧 ⚡. -* ✳ **🆑** (🔁 🛠️), 🔌 🕸 & 👩‍💻 🔬. - -## 🌕 📚 FastAPI 🗄 - -📂: https://github.com/tiangolo/full-stack-fastapi-couchbase - -👶 👶 **⚠** 👶 👶 - -🚥 👆 ▶️ 🆕 🏗 ⚪️➡️ 🖌, ✅ 🎛 📥. - -🖼, 🏗 🚂 🌕 📚 FastAPI ✳ 💪 👍 🎛, ⚫️ 🎯 🚧 & ⚙️. & ⚫️ 🔌 🌐 🆕 ⚒ & 📈. - -👆 🆓 ⚙️ 🗄-⚓️ 🚂 🚥 👆 💚, ⚫️ 🔜 🎲 👷 👌, & 🚥 👆 ⏪ ✔️ 🏗 🏗 ⏮️ ⚫️ 👈 👌 👍 (& 👆 🎲 ⏪ ℹ ⚫️ ♣ 👆 💪). - -👆 💪 ✍ 🌅 🔃 ⚫️ 🩺 🏦. - -## 🌕 📚 FastAPI ✳ - -...💪 👟 ⏪, ⚓️ 🔛 👇 🕰 🚚 & 🎏 ⚖. 👶 👶 - -## 🎰 🏫 🏷 ⏮️ 🌈 & FastAPI - -📂: https://github.com/microsoft/cookiecutter-spacy-fastapi - -### 🎰 🏫 🏷 ⏮️ 🌈 & FastAPI - ⚒ - -* **🌈** 🕜 🏷 🛠️. -* **☁ 🧠 🔎** 📨 📁 🏗. -* **🏭 🔜** 🐍 🕸 💽 ⚙️ Uvicorn & 🐁. -* **☁ 👩‍💻** Kubernetes (🦲) 🆑/💿 🛠️ 🏗. -* **🤸‍♂** 💪 ⚒ 1️⃣ 🌈 🏗 🇪🇸 ⏮️ 🏗 🖥. -* **💪 🏧** 🎏 🏷 🛠️ (Pytorch, 🇸🇲), 🚫 🌈. diff --git a/docs/em/docs/python-types.md b/docs/em/docs/python-types.md deleted file mode 100644 index d2af23bb9a..0000000000 --- a/docs/em/docs/python-types.md +++ /dev/null @@ -1,542 +0,0 @@ -# 🐍 🆎 🎶 - -🐍 ✔️ 🐕‍🦺 📦 "🆎 🔑". - -👫 **"🆎 🔑"** 🎁 ❕ 👈 ✔ 📣 🆎 🔢. - -📣 🆎 👆 🔢, 👨‍🎨 & 🧰 💪 🤝 👆 👍 🐕‍🦺. - -👉 **⏩ 🔰 / ↗️** 🔃 🐍 🆎 🔑. ⚫️ 📔 🕴 💯 💪 ⚙️ 👫 ⏮️ **FastAPI**... ❔ 🤙 📶 🐥. - -**FastAPI** 🌐 ⚓️ 🔛 👫 🆎 🔑, 👫 🤝 ⚫️ 📚 📈 & 💰. - -✋️ 🚥 👆 🙅 ⚙️ **FastAPI**, 👆 🔜 💰 ⚪️➡️ 🏫 🍖 🔃 👫. - -/// note - -🚥 👆 🐍 🕴, & 👆 ⏪ 💭 🌐 🔃 🆎 🔑, 🚶 ⏭ 📃. - -/// - -## 🎯 - -➡️ ▶️ ⏮️ 🙅 🖼: - -```Python -{!../../docs_src/python_types/tutorial001.py!} -``` - -🤙 👉 📋 🔢: - -``` -John Doe -``` - -🔢 🔨 📄: - -* ✊ `first_name` & `last_name`. -* 🗜 🥇 🔤 🔠 1️⃣ ↖ 💼 ⏮️ `title()`. -* 🔢 👫 ⏮️ 🚀 🖕. - -```Python hl_lines="2" -{!../../docs_src/python_types/tutorial001.py!} -``` - -### ✍ ⚫️ - -⚫️ 📶 🙅 📋. - -✋️ 🔜 🌈 👈 👆 ✍ ⚫️ ⚪️➡️ 🖌. - -☝ 👆 🔜 ✔️ ▶️ 🔑 🔢, 👆 ✔️ 🔢 🔜... - -✋️ ⤴️ 👆 ✔️ 🤙 "👈 👩‍🔬 👈 🗜 🥇 🔤 ↖ 💼". - -⚫️ `upper`❓ ⚫️ `uppercase`❓ `first_uppercase`❓ `capitalize`❓ - -⤴️, 👆 🔄 ⏮️ 🗝 👩‍💻 👨‍👧‍👦, 👨‍🎨 ✍. - -👆 🆎 🥇 🔢 🔢, `first_name`, ⤴️ ❣ (`.`) & ⤴️ 🎯 `Ctrl+Space` ⏲ 🛠️. - -✋️, 😞, 👆 🤚 🕳 ⚠: - - - -### 🚮 🆎 - -➡️ 🔀 👁 ⏸ ⚪️➡️ ⏮️ ⏬. - -👥 🔜 🔀 ⚫️❔ 👉 🧬, 🔢 🔢, ⚪️➡️: - -```Python - first_name, last_name -``` - -: - -```Python - first_name: str, last_name: str -``` - -👈 ⚫️. - -👈 "🆎 🔑": - -```Python hl_lines="1" -{!../../docs_src/python_types/tutorial002.py!} -``` - -👈 🚫 🎏 📣 🔢 💲 💖 🔜 ⏮️: - -```Python - first_name="john", last_name="doe" -``` - -⚫️ 🎏 👜. - -👥 ⚙️ ❤ (`:`), 🚫 🌓 (`=`). - -& ❎ 🆎 🔑 🛎 🚫 🔀 ⚫️❔ 🔨 ⚪️➡️ ⚫️❔ 🔜 🔨 🍵 👫. - -✋️ 🔜, 🌈 👆 🔄 🖕 🏗 👈 🔢, ✋️ ⏮️ 🆎 🔑. - -🎏 ☝, 👆 🔄 ⏲ 📋 ⏮️ `Ctrl+Space` & 👆 👀: - - - -⏮️ 👈, 👆 💪 📜, 👀 🎛, ⏭ 👆 🔎 1️⃣ 👈 "💍 🔔": - - - -## 🌅 🎯 - -✅ 👉 🔢, ⚫️ ⏪ ✔️ 🆎 🔑: - -```Python hl_lines="1" -{!../../docs_src/python_types/tutorial003.py!} -``` - -↩️ 👨‍🎨 💭 🆎 🔢, 👆 🚫 🕴 🤚 🛠️, 👆 🤚 ❌ ✅: - - - -🔜 👆 💭 👈 👆 ✔️ 🔧 ⚫️, 🗜 `age` 🎻 ⏮️ `str(age)`: - -```Python hl_lines="2" -{!../../docs_src/python_types/tutorial004.py!} -``` - -## 📣 🆎 - -👆 👀 👑 🥉 📣 🆎 🔑. 🔢 🔢. - -👉 👑 🥉 👆 🔜 ⚙️ 👫 ⏮️ **FastAPI**. - -### 🙅 🆎 - -👆 💪 📣 🌐 🐩 🐍 🆎, 🚫 🕴 `str`. - -👆 💪 ⚙️, 🖼: - -* `int` -* `float` -* `bool` -* `bytes` - -```Python hl_lines="1" -{!../../docs_src/python_types/tutorial005.py!} -``` - -### 💊 🆎 ⏮️ 🆎 🔢 - -📤 📊 📊 👈 💪 🔌 🎏 💲, 💖 `dict`, `list`, `set` & `tuple`. & 🔗 💲 💪 ✔️ 👫 👍 🆎 💁‍♂️. - -👉 🆎 👈 ✔️ 🔗 🆎 🤙 "**💊**" 🆎. & ⚫️ 💪 📣 👫, ⏮️ 👫 🔗 🆎. - -📣 👈 🆎 & 🔗 🆎, 👆 💪 ⚙️ 🐩 🐍 🕹 `typing`. ⚫️ 🔀 🎯 🐕‍🦺 👫 🆎 🔑. - -#### 🆕 ⏬ 🐍 - -❕ ⚙️ `typing` **🔗** ⏮️ 🌐 ⏬, ⚪️➡️ 🐍 3️⃣.6️⃣ ⏪ 🕐, ✅ 🐍 3️⃣.9️⃣, 🐍 3️⃣.1️⃣0️⃣, ♒️. - -🐍 🏧, **🆕 ⏬** 👟 ⏮️ 📉 🐕‍🦺 👉 🆎 ✍ & 📚 💼 👆 🏆 🚫 💪 🗄 & ⚙️ `typing` 🕹 📣 🆎 ✍. - -🚥 👆 💪 ⚒ 🌖 ⏮️ ⏬ 🐍 👆 🏗, 👆 🔜 💪 ✊ 📈 👈 ➕ 🦁. 👀 🖼 🔛. - -#### 📇 - -🖼, ➡️ 🔬 🔢 `list` `str`. - -//// tab | 🐍 3️⃣.6️⃣ & 🔛 - -⚪️➡️ `typing`, 🗄 `List` (⏮️ 🔠 `L`): - -```Python hl_lines="1" -{!> ../../docs_src/python_types/tutorial006.py!} -``` - -📣 🔢, ⏮️ 🎏 ❤ (`:`) ❕. - -🆎, 🚮 `List` 👈 👆 🗄 ⚪️➡️ `typing`. - -📇 🆎 👈 🔌 🔗 🆎, 👆 🚮 👫 ⬜ 🗜: - -```Python hl_lines="4" -{!> ../../docs_src/python_types/tutorial006.py!} -``` - -//// - -//// tab | 🐍 3️⃣.9️⃣ & 🔛 - -📣 🔢, ⏮️ 🎏 ❤ (`:`) ❕. - -🆎, 🚮 `list`. - -📇 🆎 👈 🔌 🔗 🆎, 👆 🚮 👫 ⬜ 🗜: - -```Python hl_lines="1" -{!> ../../docs_src/python_types/tutorial006_py39.py!} -``` - -//// - -/// info - -👈 🔗 🆎 ⬜ 🗜 🤙 "🆎 🔢". - -👉 💼, `str` 🆎 🔢 🚶‍♀️ `List` (⚖️ `list` 🐍 3️⃣.9️⃣ & 🔛). - -/// - -👈 ⛓: "🔢 `items` `list`, & 🔠 🏬 👉 📇 `str`". - -/// tip - -🚥 👆 ⚙️ 🐍 3️⃣.9️⃣ ⚖️ 🔛, 👆 🚫 ✔️ 🗄 `List` ⚪️➡️ `typing`, 👆 💪 ⚙️ 🎏 🥔 `list` 🆎 ↩️. - -/// - -🔨 👈, 👆 👨‍🎨 💪 🚚 🐕‍🦺 ⏪ 🏭 🏬 ⚪️➡️ 📇: - - - -🍵 🆎, 👈 🌖 💪 🏆. - -👀 👈 🔢 `item` 1️⃣ 🔣 📇 `items`. - -& , 👨‍🎨 💭 ⚫️ `str`, & 🚚 🐕‍🦺 👈. - -#### 🔢 & ⚒ - -👆 🔜 🎏 📣 `tuple`Ⓜ & `set`Ⓜ: - -//// tab | 🐍 3️⃣.6️⃣ & 🔛 - -```Python hl_lines="1 4" -{!> ../../docs_src/python_types/tutorial007.py!} -``` - -//// - -//// tab | 🐍 3️⃣.9️⃣ & 🔛 - -```Python hl_lines="1" -{!> ../../docs_src/python_types/tutorial007_py39.py!} -``` - -//// - -👉 ⛓: - -* 🔢 `items_t` `tuple` ⏮️ 3️⃣ 🏬, `int`, ➕1️⃣ `int`, & `str`. -* 🔢 `items_s` `set`, & 🔠 🚮 🏬 🆎 `bytes`. - -#### #️⃣ - -🔬 `dict`, 👆 🚶‍♀️ 2️⃣ 🆎 🔢, 🎏 ❕. - -🥇 🆎 🔢 🔑 `dict`. - -🥈 🆎 🔢 💲 `dict`: - -//// tab | 🐍 3️⃣.6️⃣ & 🔛 - -```Python hl_lines="1 4" -{!> ../../docs_src/python_types/tutorial008.py!} -``` - -//// - -//// tab | 🐍 3️⃣.9️⃣ & 🔛 - -```Python hl_lines="1" -{!> ../../docs_src/python_types/tutorial008_py39.py!} -``` - -//// - -👉 ⛓: - -* 🔢 `prices` `dict`: - * 🔑 👉 `dict` 🆎 `str` (➡️ 💬, 📛 🔠 🏬). - * 💲 👉 `dict` 🆎 `float` (➡️ 💬, 🔖 🔠 🏬). - -#### 🇪🇺 - -👆 💪 📣 👈 🔢 💪 🙆 **📚 🆎**, 🖼, `int` ⚖️ `str`. - -🐍 3️⃣.6️⃣ & 🔛 (✅ 🐍 3️⃣.1️⃣0️⃣) 👆 💪 ⚙️ `Union` 🆎 ⚪️➡️ `typing` & 🚮 🔘 ⬜ 🗜 💪 🆎 🚫. - -🐍 3️⃣.1️⃣0️⃣ 📤 **🎛 ❕** 🌐❔ 👆 💪 🚮 💪 🆎 👽 ⏸ ⏸ (`|`). - -//// tab | 🐍 3️⃣.6️⃣ & 🔛 - -```Python hl_lines="1 4" -{!> ../../docs_src/python_types/tutorial008b.py!} -``` - -//// - -//// tab | 🐍 3️⃣.1️⃣0️⃣ & 🔛 - -```Python hl_lines="1" -{!> ../../docs_src/python_types/tutorial008b_py310.py!} -``` - -//// - -👯‍♂️ 💼 👉 ⛓ 👈 `item` 💪 `int` ⚖️ `str`. - -#### 🎲 `None` - -👆 💪 📣 👈 💲 💪 ✔️ 🆎, 💖 `str`, ✋️ 👈 ⚫️ 💪 `None`. - -🐍 3️⃣.6️⃣ & 🔛 (✅ 🐍 3️⃣.1️⃣0️⃣) 👆 💪 📣 ⚫️ 🏭 & ⚙️ `Optional` ⚪️➡️ `typing` 🕹. - -```Python hl_lines="1 4" -{!../../docs_src/python_types/tutorial009.py!} -``` - -⚙️ `Optional[str]` ↩️ `str` 🔜 ➡️ 👨‍🎨 ℹ 👆 🔍 ❌ 🌐❔ 👆 💪 🤔 👈 💲 🕧 `str`, 🕐❔ ⚫️ 💪 🤙 `None` 💁‍♂️. - -`Optional[Something]` 🤙 ⌨ `Union[Something, None]`, 👫 🌓. - -👉 ⛓ 👈 🐍 3️⃣.1️⃣0️⃣, 👆 💪 ⚙️ `Something | None`: - -//// tab | 🐍 3️⃣.6️⃣ & 🔛 - -```Python hl_lines="1 4" -{!> ../../docs_src/python_types/tutorial009.py!} -``` - -//// - -//// tab | 🐍 3️⃣.6️⃣ & 🔛 - 🎛 - -```Python hl_lines="1 4" -{!> ../../docs_src/python_types/tutorial009b.py!} -``` - -//// - -//// tab | 🐍 3️⃣.1️⃣0️⃣ & 🔛 - -```Python hl_lines="1" -{!> ../../docs_src/python_types/tutorial009_py310.py!} -``` - -//// - -#### ⚙️ `Union` ⚖️ `Optional` - -🚥 👆 ⚙️ 🐍 ⏬ 🔛 3️⃣.1️⃣0️⃣, 📥 💁‍♂ ⚪️➡️ 👇 📶 **🤔** ☝ 🎑: - -* 👶 ❎ ⚙️ `Optional[SomeType]` -* ↩️ 👶 **⚙️ `Union[SomeType, None]`** 👶. - -👯‍♂️ 🌓 & 🔘 👫 🎏, ✋️ 👤 🔜 👍 `Union` ↩️ `Optional` ↩️ 🔤 "**📦**" 🔜 😑 🔑 👈 💲 📦, & ⚫️ 🤙 ⛓ "⚫️ 💪 `None`", 🚥 ⚫️ 🚫 📦 & ✔. - -👤 💭 `Union[SomeType, None]` 🌖 🔑 🔃 ⚫️❔ ⚫️ ⛓. - -⚫️ 🔃 🔤 & 📛. ✋️ 👈 🔤 💪 📉 ❔ 👆 & 👆 🤽‍♂ 💭 🔃 📟. - -🖼, ➡️ ✊ 👉 🔢: - -```Python hl_lines="1 4" -{!../../docs_src/python_types/tutorial009c.py!} -``` - -🔢 `name` 🔬 `Optional[str]`, ✋️ ⚫️ **🚫 📦**, 👆 🚫🔜 🤙 🔢 🍵 🔢: - -```Python -say_hi() # Oh, no, this throws an error! 😱 -``` - -`name` 🔢 **✔** (🚫 *📦*) ↩️ ⚫️ 🚫 ✔️ 🔢 💲. , `name` 🚫 `None` 💲: - -```Python -say_hi(name=None) # This works, None is valid 🎉 -``` - -👍 📰, 🕐 👆 🔛 🐍 3️⃣.1️⃣0️⃣ 👆 🏆 🚫 ✔️ 😟 🔃 👈, 👆 🔜 💪 🎯 ⚙️ `|` 🔬 🇪🇺 🆎: - -```Python hl_lines="1 4" -{!../../docs_src/python_types/tutorial009c_py310.py!} -``` - -& ⤴️ 👆 🏆 🚫 ✔️ 😟 🔃 📛 💖 `Optional` & `Union`. 👶 - -#### 💊 🆎 - -👉 🆎 👈 ✊ 🆎 🔢 ⬜ 🗜 🤙 **💊 🆎** ⚖️ **💊**, 🖼: - -//// tab | 🐍 3️⃣.6️⃣ & 🔛 - -* `List` -* `Tuple` -* `Set` -* `Dict` -* `Union` -* `Optional` -* ...& 🎏. - -//// - -//// tab | 🐍 3️⃣.9️⃣ & 🔛 - -👆 💪 ⚙️ 🎏 💽 🆎 💊 (⏮️ ⬜ 🗜 & 🆎 🔘): - -* `list` -* `tuple` -* `set` -* `dict` - - & 🎏 ⏮️ 🐍 3️⃣.6️⃣, ⚪️➡️ `typing` 🕹: - -* `Union` -* `Optional` -* ...& 🎏. - -//// - -//// tab | 🐍 3️⃣.1️⃣0️⃣ & 🔛 - -👆 💪 ⚙️ 🎏 💽 🆎 💊 (⏮️ ⬜ 🗜 & 🆎 🔘): - -* `list` -* `tuple` -* `set` -* `dict` - - & 🎏 ⏮️ 🐍 3️⃣.6️⃣, ⚪️➡️ `typing` 🕹: - -* `Union` -* `Optional` (🎏 ⏮️ 🐍 3️⃣.6️⃣) -* ...& 🎏. - -🐍 3️⃣.1️⃣0️⃣, 🎛 ⚙️ 💊 `Union` & `Optional`, 👆 💪 ⚙️ ⏸ ⏸ (`|`) 📣 🇪🇺 🆎. - -//// - -### 🎓 🆎 - -👆 💪 📣 🎓 🆎 🔢. - -➡️ 💬 👆 ✔️ 🎓 `Person`, ⏮️ 📛: - -```Python hl_lines="1-3" -{!../../docs_src/python_types/tutorial010.py!} -``` - -⤴️ 👆 💪 📣 🔢 🆎 `Person`: - -```Python hl_lines="6" -{!../../docs_src/python_types/tutorial010.py!} -``` - -& ⤴️, 🔄, 👆 🤚 🌐 👨‍🎨 🐕‍🦺: - - - -## Pydantic 🏷 - -Pydantic 🐍 🗃 🎭 📊 🔬. - -👆 📣 "💠" 💽 🎓 ⏮️ 🔢. - -& 🔠 🔢 ✔️ 🆎. - -⤴️ 👆 ✍ 👐 👈 🎓 ⏮️ 💲 & ⚫️ 🔜 ✔ 💲, 🗜 👫 ☑ 🆎 (🚥 👈 💼) & 🤝 👆 🎚 ⏮️ 🌐 💽. - -& 👆 🤚 🌐 👨‍🎨 🐕‍🦺 ⏮️ 👈 📉 🎚. - -🖼 ⚪️➡️ 🛂 Pydantic 🩺: - -//// tab | 🐍 3️⃣.6️⃣ & 🔛 - -```Python -{!> ../../docs_src/python_types/tutorial011.py!} -``` - -//// - -//// tab | 🐍 3️⃣.9️⃣ & 🔛 - -```Python -{!> ../../docs_src/python_types/tutorial011_py39.py!} -``` - -//// - -//// tab | 🐍 3️⃣.1️⃣0️⃣ & 🔛 - -```Python -{!> ../../docs_src/python_types/tutorial011_py310.py!} -``` - -//// - -/// info - -💡 🌖 🔃 Pydantic, ✅ 🚮 🩺. - -/// - -**FastAPI** 🌐 ⚓️ 🔛 Pydantic. - -👆 🔜 👀 📚 🌅 🌐 👉 💡 [🔰 - 👩‍💻 🦮](tutorial/index.md){.internal-link target=_blank}. - -/// tip - -Pydantic ✔️ 🎁 🎭 🕐❔ 👆 ⚙️ `Optional` ⚖️ `Union[Something, None]` 🍵 🔢 💲, 👆 💪 ✍ 🌅 🔃 ⚫️ Pydantic 🩺 🔃 ✔ 📦 🏑. - -/// - -## 🆎 🔑 **FastAPI** - -**FastAPI** ✊ 📈 👫 🆎 🔑 📚 👜. - -⏮️ **FastAPI** 👆 📣 🔢 ⏮️ 🆎 🔑 & 👆 🤚: - -* **👨‍🎨 🐕‍🦺**. -* **🆎 ✅**. - -...and **FastAPI** uses the same declarations : - -* **🔬 📄**: ⚪️➡️ 📨 ➡ 🔢, 🔢 🔢, 🎚, 💪, 🔗, ♒️. -* **🗜 💽**: ⚪️➡️ 📨 🚚 🆎. -* **✔ 💽**: 👟 ⚪️➡️ 🔠 📨: - * 🏭 **🏧 ❌** 📨 👩‍💻 🕐❔ 📊 ❌. -* **📄** 🛠️ ⚙️ 🗄: - * ❔ ⤴️ ⚙️ 🏧 🎓 🧾 👩‍💻 🔢. - -👉 5️⃣📆 🌐 🔊 📝. 🚫 😟. 👆 🔜 👀 🌐 👉 🎯 [🔰 - 👩‍💻 🦮](tutorial/index.md){.internal-link target=_blank}. - -⚠ 👜 👈 ⚙️ 🐩 🐍 🆎, 👁 🥉 (↩️ ❎ 🌖 🎓, 👨‍🎨, ♒️), **FastAPI** 🔜 📚 👷 👆. - -/// info - -🚥 👆 ⏪ 🚶 🔘 🌐 🔰 & 👟 🔙 👀 🌅 🔃 🆎, 👍 ℹ "🎮 🎼" ⚪️➡️ `mypy`. - -/// diff --git a/docs/em/docs/tutorial/background-tasks.md b/docs/em/docs/tutorial/background-tasks.md deleted file mode 100644 index 4cbcbc710d..0000000000 --- a/docs/em/docs/tutorial/background-tasks.md +++ /dev/null @@ -1,84 +0,0 @@ -# 🖥 📋 - -👆 💪 🔬 🖥 📋 🏃 *⏮️* 🛬 📨. - -👉 ⚠ 🛠️ 👈 💪 🔨 ⏮️ 📨, ✋️ 👈 👩‍💻 🚫 🤙 ✔️ ⌛ 🛠️ 🏁 ⏭ 📨 📨. - -👉 🔌, 🖼: - -* 📧 📨 📨 ⏮️ 🎭 🎯: - * 🔗 📧 💽 & 📨 📧 😑 "🐌" (📚 🥈), 👆 💪 📨 📨 ▶️️ ↖️ & 📨 📧 📨 🖥. -* 🏭 💽: - * 🖼, ➡️ 💬 👆 📨 📁 👈 🔜 🚶 🔘 🐌 🛠️, 👆 💪 📨 📨 "🚫" (🇺🇸🔍 2️⃣0️⃣2️⃣) & 🛠️ ⚫️ 🖥. - -## ⚙️ `BackgroundTasks` - -🥇, 🗄 `BackgroundTasks` & 🔬 🔢 👆 *➡ 🛠️ 🔢* ⏮️ 🆎 📄 `BackgroundTasks`: - -{* ../../docs_src/background_tasks/tutorial001.py hl[1,13] *} - -**FastAPI** 🔜 ✍ 🎚 🆎 `BackgroundTasks` 👆 & 🚶‍♀️ ⚫️ 👈 🔢. - -## ✍ 📋 🔢 - -✍ 🔢 🏃 🖥 📋. - -⚫️ 🐩 🔢 👈 💪 📨 🔢. - -⚫️ 💪 `async def` ⚖️ 😐 `def` 🔢, **FastAPI** 🔜 💭 ❔ 🍵 ⚫️ ☑. - -👉 💼, 📋 🔢 🔜 ✍ 📁 (⚖ 📨 📧). - -& ✍ 🛠️ 🚫 ⚙️ `async` & `await`, 👥 🔬 🔢 ⏮️ 😐 `def`: - -{* ../../docs_src/background_tasks/tutorial001.py hl[6:9] *} - -## 🚮 🖥 📋 - -🔘 👆 *➡ 🛠️ 🔢*, 🚶‍♀️ 👆 📋 🔢 *🖥 📋* 🎚 ⏮️ 👩‍🔬 `.add_task()`: - -{* ../../docs_src/background_tasks/tutorial001.py hl[14] *} - -`.add_task()` 📨 ❌: - -* 📋 🔢 🏃 🖥 (`write_notification`). -* 🙆 🔁 ❌ 👈 🔜 🚶‍♀️ 📋 🔢 ✔ (`email`). -* 🙆 🇨🇻 ❌ 👈 🔜 🚶‍♀️ 📋 🔢 (`message="some notification"`). - -## 🔗 💉 - -⚙️ `BackgroundTasks` 👷 ⏮️ 🔗 💉 ⚙️, 👆 💪 📣 🔢 🆎 `BackgroundTasks` 💗 🎚: *➡ 🛠️ 🔢*, 🔗 (☑), 🎧-🔗, ♒️. - -**FastAPI** 💭 ⚫️❔ 🔠 💼 & ❔ 🏤-⚙️ 🎏 🎚, 👈 🌐 🖥 📋 🔗 👯‍♂️ & 🏃 🖥 ⏮️: - -{* ../../docs_src/background_tasks/tutorial002.py hl[13,15,22,25] *} - -👉 🖼, 📧 🔜 ✍ `log.txt` 📁 *⏮️* 📨 📨. - -🚥 📤 🔢 📨, ⚫️ 🔜 ✍ 🕹 🖥 📋. - -& ⤴️ ➕1️⃣ 🖥 📋 🏗 *➡ 🛠️ 🔢* 🔜 ✍ 📧 ⚙️ `email` ➡ 🔢. - -## 📡 ℹ - -🎓 `BackgroundTasks` 👟 🔗 ⚪️➡️ `starlette.background`. - -⚫️ 🗄/🔌 🔗 🔘 FastAPI 👈 👆 💪 🗄 ⚫️ ⚪️➡️ `fastapi` & ❎ 😫 🗄 🎛 `BackgroundTask` (🍵 `s` 🔚) ⚪️➡️ `starlette.background`. - -🕴 ⚙️ `BackgroundTasks` (& 🚫 `BackgroundTask`), ⚫️ ⤴️ 💪 ⚙️ ⚫️ *➡ 🛠️ 🔢* 🔢 & ✔️ **FastAPI** 🍵 🎂 👆, 💖 🕐❔ ⚙️ `Request` 🎚 🔗. - -⚫️ 💪 ⚙️ `BackgroundTask` 😞 FastAPI, ✋️ 👆 ✔️ ✍ 🎚 👆 📟 & 📨 💃 `Response` 🔌 ⚫️. - -👆 💪 👀 🌖 ℹ 💃 🛂 🩺 🖥 📋. - -## ⚠ - -🚥 👆 💪 🎭 🏋️ 🖥 📊 & 👆 🚫 🎯 💪 ⚫️ 🏃 🎏 🛠️ (🖼, 👆 🚫 💪 💰 💾, 🔢, ♒️), 👆 💪 💰 ⚪️➡️ ⚙️ 🎏 🦏 🧰 💖 🥒. - -👫 😑 🚚 🌖 🏗 📳, 📧/👨‍🏭 📤 👨‍💼, 💖 ✳ ⚖️ ✳, ✋️ 👫 ✔ 👆 🏃 🖥 📋 💗 🛠️, & ✴️, 💗 💽. - -✋️ 🚥 👆 💪 🔐 🔢 & 🎚 ⚪️➡️ 🎏 **FastAPI** 📱, ⚖️ 👆 💪 🎭 🤪 🖥 📋 (💖 📨 📧 📨), 👆 💪 🎯 ⚙️ `BackgroundTasks`. - -## 🌃 - -🗄 & ⚙️ `BackgroundTasks` ⏮️ 🔢 *➡ 🛠️ 🔢* & 🔗 🚮 🖥 📋. diff --git a/docs/em/docs/tutorial/bigger-applications.md b/docs/em/docs/tutorial/bigger-applications.md deleted file mode 100644 index 78a321ae6a..0000000000 --- a/docs/em/docs/tutorial/bigger-applications.md +++ /dev/null @@ -1,530 +0,0 @@ -# 🦏 🈸 - 💗 📁 - -🚥 👆 🏗 🈸 ⚖️ 🕸 🛠️, ⚫️ 🛎 💼 👈 👆 💪 🚮 🌐 🔛 👁 📁. - -**FastAPI** 🚚 🏪 🧰 📊 👆 🈸 ⏪ 🚧 🌐 💪. - -/// info - -🚥 👆 👟 ⚪️➡️ 🏺, 👉 🔜 🌓 🏺 📗. - -/// - -## 🖼 📁 📊 - -➡️ 💬 👆 ✔️ 📁 📊 💖 👉: - -``` -. -├── app -│   ├── __init__.py -│   ├── main.py -│   ├── dependencies.py -│   └── routers -│   │ ├── __init__.py -│   │ ├── items.py -│   │ └── users.py -│   └── internal -│   ├── __init__.py -│   └── admin.py -``` - -/// tip - -📤 📚 `__init__.py` 📁: 1️⃣ 🔠 📁 ⚖️ 📁. - -👉 ⚫️❔ ✔ 🏭 📟 ⚪️➡️ 1️⃣ 📁 🔘 ➕1️⃣. - -🖼, `app/main.py` 👆 💪 ✔️ ⏸ 💖: - -``` -from app.routers import items -``` - -/// - -* `app` 📁 🔌 🌐. & ⚫️ ✔️ 🛁 📁 `app/__init__.py`, ⚫️ "🐍 📦" (🗃 "🐍 🕹"): `app`. -* ⚫️ 🔌 `app/main.py` 📁. ⚫️ 🔘 🐍 📦 (📁 ⏮️ 📁 `__init__.py`), ⚫️ "🕹" 👈 📦: `app.main`. -* 📤 `app/dependencies.py` 📁, 💖 `app/main.py`, ⚫️ "🕹": `app.dependencies`. -* 📤 📁 `app/routers/` ⏮️ ➕1️⃣ 📁 `__init__.py`, ⚫️ "🐍 📦": `app.routers`. -* 📁 `app/routers/items.py` 🔘 📦, `app/routers/`,, ⚫️ 🔁: `app.routers.items`. -* 🎏 ⏮️ `app/routers/users.py`, ⚫️ ➕1️⃣ 🔁: `app.routers.users`. -* 📤 📁 `app/internal/` ⏮️ ➕1️⃣ 📁 `__init__.py`, ⚫️ ➕1️⃣ "🐍 📦": `app.internal`. -* & 📁 `app/internal/admin.py` ➕1️⃣ 🔁: `app.internal.admin`. - - - -🎏 📁 📊 ⏮️ 🏤: - -``` -. -├── app # "app" is a Python package -│   ├── __init__.py # this file makes "app" a "Python package" -│   ├── main.py # "main" module, e.g. import app.main -│   ├── dependencies.py # "dependencies" module, e.g. import app.dependencies -│   └── routers # "routers" is a "Python subpackage" -│   │ ├── __init__.py # makes "routers" a "Python subpackage" -│   │ ├── items.py # "items" submodule, e.g. import app.routers.items -│   │ └── users.py # "users" submodule, e.g. import app.routers.users -│   └── internal # "internal" is a "Python subpackage" -│   ├── __init__.py # makes "internal" a "Python subpackage" -│   └── admin.py # "admin" submodule, e.g. import app.internal.admin -``` - -## `APIRouter` - -➡️ 💬 📁 💡 🚚 👩‍💻 🔁 `/app/routers/users.py`. - -👆 💚 ✔️ *➡ 🛠️* 🔗 👆 👩‍💻 👽 ⚪️➡️ 🎂 📟, 🚧 ⚫️ 🏗. - -✋️ ⚫️ 🍕 🎏 **FastAPI** 🈸/🕸 🛠️ (⚫️ 🍕 🎏 "🐍 📦"). - -👆 💪 ✍ *➡ 🛠️* 👈 🕹 ⚙️ `APIRouter`. - -### 🗄 `APIRouter` - -👆 🗄 ⚫️ & ✍ "👐" 🎏 🌌 👆 🔜 ⏮️ 🎓 `FastAPI`: - -```Python hl_lines="1 3" title="app/routers/users.py" -{!../../docs_src/bigger_applications/app/routers/users.py!} -``` - -### *➡ 🛠️* ⏮️ `APIRouter` - -& ⤴️ 👆 ⚙️ ⚫️ 📣 👆 *➡ 🛠️*. - -⚙️ ⚫️ 🎏 🌌 👆 🔜 ⚙️ `FastAPI` 🎓: - -```Python hl_lines="6 11 16" title="app/routers/users.py" -{!../../docs_src/bigger_applications/app/routers/users.py!} -``` - -👆 💪 💭 `APIRouter` "🐩 `FastAPI`" 🎓. - -🌐 🎏 🎛 🐕‍🦺. - -🌐 🎏 `parameters`, `responses`, `dependencies`, `tags`, ♒️. - -/// tip - -👉 🖼, 🔢 🤙 `router`, ✋️ 👆 💪 📛 ⚫️ 👐 👆 💚. - -/// - -👥 🔜 🔌 👉 `APIRouter` 👑 `FastAPI` 📱, ✋️ 🥇, ➡️ ✅ 🔗 & ➕1️⃣ `APIRouter`. - -## 🔗 - -👥 👀 👈 👥 🔜 💪 🔗 ⚙️ 📚 🥉 🈸. - -👥 🚮 👫 👫 👍 `dependencies` 🕹 (`app/dependencies.py`). - -👥 🔜 🔜 ⚙️ 🙅 🔗 ✍ 🛃 `X-Token` 🎚: - -```Python hl_lines="1 4-6" title="app/dependencies.py" -{!../../docs_src/bigger_applications/app/dependencies.py!} -``` - -/// tip - -👥 ⚙️ 💭 🎚 📉 👉 🖼. - -✋️ 🎰 💼 👆 🔜 🤚 👍 🏁 ⚙️ 🛠️ [💂‍♂ 🚙](security/index.md){.internal-link target=_blank}. - -/// - -## ➕1️⃣ 🕹 ⏮️ `APIRouter` - -➡️ 💬 👆 ✔️ 🔗 💡 🚚 "🏬" ⚪️➡️ 👆 🈸 🕹 `app/routers/items.py`. - -👆 ✔️ *➡ 🛠️* : - -* `/items/` -* `/items/{item_id}` - -⚫️ 🌐 🎏 📊 ⏮️ `app/routers/users.py`. - -✋️ 👥 💚 🙃 & 📉 📟 🍖. - -👥 💭 🌐 *➡ 🛠️* 👉 🕹 ✔️ 🎏: - -* ➡ `prefix`: `/items`. -* `tags`: (1️⃣ 🔖: `items`). -* ➕ `responses`. -* `dependencies`: 👫 🌐 💪 👈 `X-Token` 🔗 👥 ✍. - -, ↩️ ❎ 🌐 👈 🔠 *➡ 🛠️*, 👥 💪 🚮 ⚫️ `APIRouter`. - -```Python hl_lines="5-10 16 21" title="app/routers/items.py" -{!../../docs_src/bigger_applications/app/routers/items.py!} -``` - -➡ 🔠 *➡ 🛠️* ✔️ ▶️ ⏮️ `/`, 💖: - -```Python hl_lines="1" -@router.get("/{item_id}") -async def read_item(item_id: str): - ... -``` - -...🔡 🔜 🚫 🔌 🏁 `/`. - -, 🔡 👉 💼 `/items`. - -👥 💪 🚮 📇 `tags` & ➕ `responses` 👈 🔜 ✔ 🌐 *➡ 🛠️* 🔌 👉 📻. - -& 👥 💪 🚮 📇 `dependencies` 👈 🔜 🚮 🌐 *➡ 🛠️* 📻 & 🔜 🛠️/❎ 🔠 📨 ⚒ 👫. - -/// tip - -🗒 👈, 🌅 💖 [🔗 *➡ 🛠️ 👨‍🎨*](dependencies/dependencies-in-path-operation-decorators.md){.internal-link target=_blank}, 🙅‍♂ 💲 🔜 🚶‍♀️ 👆 *➡ 🛠️ 🔢*. - -/// - -🔚 🏁 👈 🏬 ➡ 🔜: - -* `/items/` -* `/items/{item_id}` - -...👥 🎯. - -* 👫 🔜 ™ ⏮️ 📇 🔖 👈 🔌 👁 🎻 `"items"`. - * 👫 "🔖" ✴️ ⚠ 🏧 🎓 🧾 ⚙️ (⚙️ 🗄). -* 🌐 👫 🔜 🔌 🔁 `responses`. -* 🌐 👫 *➡ 🛠️* 🔜 ✔️ 📇 `dependencies` 🔬/🛠️ ⏭ 👫. - * 🚥 👆 📣 🔗 🎯 *➡ 🛠️*, **👫 🔜 🛠️ 💁‍♂️**. - * 📻 🔗 🛠️ 🥇, ⤴️ [`dependencies` 👨‍🎨](dependencies/dependencies-in-path-operation-decorators.md){.internal-link target=_blank}, & ⤴️ 😐 🔢 🔗. - * 👆 💪 🚮 [`Security` 🔗 ⏮️ `scopes`](../advanced/security/oauth2-scopes.md){.internal-link target=_blank}. - -/// tip - -✔️ `dependencies` `APIRouter` 💪 ⚙️, 🖼, 🚚 🤝 🎂 👪 *➡ 🛠️*. 🚥 🔗 🚫 🚮 📦 🔠 1️⃣ 👫. - -/// - -/// check - -`prefix`, `tags`, `responses`, & `dependencies` 🔢 (📚 🎏 💼) ⚒ ⚪️➡️ **FastAPI** ℹ 👆 ❎ 📟 ❎. - -/// - -### 🗄 🔗 - -👉 📟 👨‍❤‍👨 🕹 `app.routers.items`, 📁 `app/routers/items.py`. - -& 👥 💪 🤚 🔗 🔢 ⚪️➡️ 🕹 `app.dependencies`, 📁 `app/dependencies.py`. - -👥 ⚙️ ⚖ 🗄 ⏮️ `..` 🔗: - -```Python hl_lines="3" title="app/routers/items.py" -{!../../docs_src/bigger_applications/app/routers/items.py!} -``` - -#### ❔ ⚖ 🗄 👷 - -/// tip - -🚥 👆 💭 👌 ❔ 🗄 👷, 😣 ⏭ 📄 🔛. - -/// - -👁 ❣ `.`, 💖: - -```Python -from .dependencies import get_token_header -``` - -🔜 ⛓: - -* ▶️ 🎏 📦 👈 👉 🕹 (📁 `app/routers/items.py`) 🖖 (📁 `app/routers/`)... -* 🔎 🕹 `dependencies` (👽 📁 `app/routers/dependencies.py`)... -* & ⚪️➡️ ⚫️, 🗄 🔢 `get_token_header`. - -✋️ 👈 📁 🚫 🔀, 👆 🔗 📁 `app/dependencies.py`. - -💭 ❔ 👆 📱/📁 📊 👀 💖: - - - ---- - -2️⃣ ❣ `..`, 💖: - -```Python -from ..dependencies import get_token_header -``` - -⛓: - -* ▶️ 🎏 📦 👈 👉 🕹 (📁 `app/routers/items.py`) 🖖 (📁 `app/routers/`)... -* 🚶 👪 📦 (📁 `app/`)... -* & 📤, 🔎 🕹 `dependencies` (📁 `app/dependencies.py`)... -* & ⚪️➡️ ⚫️, 🗄 🔢 `get_token_header`. - -👈 👷 ☑ ❗ 👶 - ---- - -🎏 🌌, 🚥 👥 ✔️ ⚙️ 3️⃣ ❣ `...`, 💖: - -```Python -from ...dependencies import get_token_header -``` - -that 🔜 ⛓: - -* ▶️ 🎏 📦 👈 👉 🕹 (📁 `app/routers/items.py`) 🖖 (📁 `app/routers/`)... -* 🚶 👪 📦 (📁 `app/`)... -* ⤴️ 🚶 👪 👈 📦 (📤 🙅‍♂ 👪 📦, `app` 🔝 🎚 👶)... -* & 📤, 🔎 🕹 `dependencies` (📁 `app/dependencies.py`)... -* & ⚪️➡️ ⚫️, 🗄 🔢 `get_token_header`. - -👈 🔜 🔗 📦 🔛 `app/`, ⏮️ 🚮 👍 📁 `__init__.py`, ♒️. ✋️ 👥 🚫 ✔️ 👈. , 👈 🔜 🚮 ❌ 👆 🖼. 👶 - -✋️ 🔜 👆 💭 ❔ ⚫️ 👷, 👆 💪 ⚙️ ⚖ 🗄 👆 👍 📱 🙅‍♂ 🤔 ❔ 🏗 👫. 👶 - -### 🚮 🛃 `tags`, `responses`, & `dependencies` - -👥 🚫 ❎ 🔡 `/items` 🚫 `tags=["items"]` 🔠 *➡ 🛠️* ↩️ 👥 🚮 👫 `APIRouter`. - -✋️ 👥 💪 🚮 _🌅_ `tags` 👈 🔜 ✔ 🎯 *➡ 🛠️*, & ➕ `responses` 🎯 👈 *➡ 🛠️*: - -```Python hl_lines="30-31" title="app/routers/items.py" -{!../../docs_src/bigger_applications/app/routers/items.py!} -``` - -/// tip - -👉 🏁 ➡ 🛠️ 🔜 ✔️ 🌀 🔖: `["items", "custom"]`. - - & ⚫️ 🔜 ✔️ 👯‍♂️ 📨 🧾, 1️⃣ `404` & 1️⃣ `403`. - -/// - -## 👑 `FastAPI` - -🔜, ➡️ 👀 🕹 `app/main.py`. - -📥 🌐❔ 👆 🗄 & ⚙️ 🎓 `FastAPI`. - -👉 🔜 👑 📁 👆 🈸 👈 👔 🌐 👯‍♂️. - -& 🏆 👆 ⚛ 🔜 🔜 🖖 🚮 👍 🎯 🕹, 👑 📁 🔜 🙅. - -### 🗄 `FastAPI` - -👆 🗄 & ✍ `FastAPI` 🎓 🛎. - -& 👥 💪 📣 [🌐 🔗](dependencies/global-dependencies.md){.internal-link target=_blank} 👈 🔜 🌀 ⏮️ 🔗 🔠 `APIRouter`: - -```Python hl_lines="1 3 7" title="app/main.py" -{!../../docs_src/bigger_applications/app/main.py!} -``` - -### 🗄 `APIRouter` - -🔜 👥 🗄 🎏 🔁 👈 ✔️ `APIRouter`Ⓜ: - -```Python hl_lines="5" title="app/main.py" -{!../../docs_src/bigger_applications/app/main.py!} -``` - -📁 `app/routers/users.py` & `app/routers/items.py` 🔁 👈 🍕 🎏 🐍 📦 `app`, 👥 💪 ⚙️ 👁 ❣ `.` 🗄 👫 ⚙️ "⚖ 🗄". - -### ❔ 🏭 👷 - -📄: - -```Python -from .routers import items, users -``` - -⛓: - -* ▶️ 🎏 📦 👈 👉 🕹 (📁 `app/main.py`) 🖖 (📁 `app/`)... -* 👀 📦 `routers` (📁 `app/routers/`)... -* & ⚪️➡️ ⚫️, 🗄 🔁 `items` (📁 `app/routers/items.py`) & `users` (📁 `app/routers/users.py`)... - -🕹 `items` 🔜 ✔️ 🔢 `router` (`items.router`). 👉 🎏 1️⃣ 👥 ✍ 📁 `app/routers/items.py`, ⚫️ `APIRouter` 🎚. - -& ⤴️ 👥 🎏 🕹 `users`. - -👥 💪 🗄 👫 💖: - -```Python -from app.routers import items, users -``` - -/// info - -🥇 ⏬ "⚖ 🗄": - -```Python -from .routers import items, users -``` - -🥈 ⏬ "🎆 🗄": - -```Python -from app.routers import items, users -``` - -💡 🌅 🔃 🐍 📦 & 🕹, ✍ 🛂 🐍 🧾 🔃 🕹. - -/// - -### ❎ 📛 💥 - -👥 🏭 🔁 `items` 🔗, ↩️ 🏭 🚮 🔢 `router`. - -👉 ↩️ 👥 ✔️ ➕1️⃣ 🔢 📛 `router` 🔁 `users`. - -🚥 👥 ✔️ 🗄 1️⃣ ⏮️ 🎏, 💖: - -```Python -from .routers.items import router -from .routers.users import router -``` - -`router` ⚪️➡️ `users` 🔜 📁 1️⃣ ⚪️➡️ `items` & 👥 🚫🔜 💪 ⚙️ 👫 🎏 🕰. - -, 💪 ⚙️ 👯‍♂️ 👫 🎏 📁, 👥 🗄 🔁 🔗: - -```Python hl_lines="5" title="app/main.py" -{!../../docs_src/bigger_applications/app/main.py!} -``` - -### 🔌 `APIRouter`Ⓜ `users` & `items` - -🔜, ➡️ 🔌 `router`Ⓜ ⚪️➡️ 🔁 `users` & `items`: - -```Python hl_lines="10-11" title="app/main.py" -{!../../docs_src/bigger_applications/app/main.py!} -``` - -/// info - -`users.router` 🔌 `APIRouter` 🔘 📁 `app/routers/users.py`. - - & `items.router` 🔌 `APIRouter` 🔘 📁 `app/routers/items.py`. - -/// - -⏮️ `app.include_router()` 👥 💪 🚮 🔠 `APIRouter` 👑 `FastAPI` 🈸. - -⚫️ 🔜 🔌 🌐 🛣 ⚪️➡️ 👈 📻 🍕 ⚫️. - -/// note | 📡 ℹ - -⚫️ 🔜 🤙 🔘 ✍ *➡ 🛠️* 🔠 *➡ 🛠️* 👈 📣 `APIRouter`. - -, ⛅ 🎑, ⚫️ 🔜 🤙 👷 🚥 🌐 🎏 👁 📱. - -/// - -/// check - -👆 🚫 ✔️ 😟 🔃 🎭 🕐❔ ✅ 📻. - -👉 🔜 ✊ ⏲ & 🔜 🕴 🔨 🕴. - -⚫️ 🏆 🚫 📉 🎭. 👶 - -/// - -### 🔌 `APIRouter` ⏮️ 🛃 `prefix`, `tags`, `responses`, & `dependencies` - -🔜, ➡️ 🌈 👆 🏢 🤝 👆 `app/internal/admin.py` 📁. - -⚫️ 🔌 `APIRouter` ⏮️ 📡 *➡ 🛠️* 👈 👆 🏢 💰 🖖 📚 🏗. - -👉 🖼 ⚫️ 🔜 💎 🙅. ✋️ ➡️ 💬 👈 ↩️ ⚫️ 💰 ⏮️ 🎏 🏗 🏢, 👥 🚫🔜 🔀 ⚫️ & 🚮 `prefix`, `dependencies`, `tags`, ♒️. 🔗 `APIRouter`: - -```Python hl_lines="3" title="app/internal/admin.py" -{!../../docs_src/bigger_applications/app/internal/admin.py!} -``` - -✋️ 👥 💚 ⚒ 🛃 `prefix` 🕐❔ ✅ `APIRouter` 👈 🌐 🚮 *➡ 🛠️* ▶️ ⏮️ `/admin`, 👥 💚 🔐 ⚫️ ⏮️ `dependencies` 👥 ⏪ ✔️ 👉 🏗, & 👥 💚 🔌 `tags` & `responses`. - -👥 💪 📣 🌐 👈 🍵 ✔️ 🔀 ⏮️ `APIRouter` 🚶‍♀️ 👈 🔢 `app.include_router()`: - -```Python hl_lines="14-17" title="app/main.py" -{!../../docs_src/bigger_applications/app/main.py!} -``` - -👈 🌌, ⏮️ `APIRouter` 🔜 🚧 ⚗, 👥 💪 💰 👈 🎏 `app/internal/admin.py` 📁 ⏮️ 🎏 🏗 🏢. - -🏁 👈 👆 📱, 🔠 *➡ 🛠️* ⚪️➡️ `admin` 🕹 🔜 ✔️: - -* 🔡 `/admin`. -* 🔖 `admin`. -* 🔗 `get_token_header`. -* 📨 `418`. 👶 - -✋️ 👈 🔜 🕴 📉 👈 `APIRouter` 👆 📱, 🚫 🙆 🎏 📟 👈 ⚙️ ⚫️. - -, 🖼, 🎏 🏗 💪 ⚙️ 🎏 `APIRouter` ⏮️ 🎏 🤝 👩‍🔬. - -### 🔌 *➡ 🛠️* - -👥 💪 🚮 *➡ 🛠️* 🔗 `FastAPI` 📱. - -📥 👥 ⚫️... 🎦 👈 👥 💪 🤷: - -```Python hl_lines="21-23" title="app/main.py" -{!../../docs_src/bigger_applications/app/main.py!} -``` - -& ⚫️ 🔜 👷 ☑, 👯‍♂️ ⏮️ 🌐 🎏 *➡ 🛠️* 🚮 ⏮️ `app.include_router()`. - -/// info | 📶 📡 ℹ - -**🗒**: 👉 📶 📡 ℹ 👈 👆 🎲 💪 **🚶**. - ---- - - `APIRouter`Ⓜ 🚫 "🗻", 👫 🚫 👽 ⚪️➡️ 🎂 🈸. - -👉 ↩️ 👥 💚 🔌 👫 *➡ 🛠️* 🗄 🔗 & 👩‍💻 🔢. - -👥 🚫🔜 ❎ 👫 & "🗻" 👫 ➡ 🎂, *➡ 🛠️* "🖖" (🏤-✍), 🚫 🔌 🔗. - -/// - -## ✅ 🏧 🛠️ 🩺 - -🔜, 🏃 `uvicorn`, ⚙️ 🕹 `app.main` & 🔢 `app`: - -
- -```console -$ uvicorn app.main:app --reload - -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -``` - -
- -& 📂 🩺 http://127.0.0.1:8000/docs. - -👆 🔜 👀 🏧 🛠️ 🩺, ✅ ➡ ⚪️➡️ 🌐 🔁, ⚙️ ☑ ➡ (& 🔡) & ☑ 🔖: - - - -## 🔌 🎏 📻 💗 🕰 ⏮️ 🎏 `prefix` - -👆 💪 ⚙️ `.include_router()` 💗 🕰 ⏮️ *🎏* 📻 ⚙️ 🎏 🔡. - -👉 💪 ⚠, 🖼, 🎦 🎏 🛠️ 🔽 🎏 🔡, ✅ `/api/v1` & `/api/latest`. - -👉 🏧 ⚙️ 👈 👆 5️⃣📆 🚫 🤙 💪, ✋️ ⚫️ 📤 💼 👆. - -## 🔌 `APIRouter` ➕1️⃣ - -🎏 🌌 👆 💪 🔌 `APIRouter` `FastAPI` 🈸, 👆 💪 🔌 `APIRouter` ➕1️⃣ `APIRouter` ⚙️: - -```Python -router.include_router(other_router) -``` - -⚒ 💭 👆 ⚫️ ⏭ 🔌 `router` `FastAPI` 📱, 👈 *➡ 🛠️* ⚪️➡️ `other_router` 🔌. diff --git a/docs/em/docs/tutorial/body-fields.md b/docs/em/docs/tutorial/body-fields.md deleted file mode 100644 index f202284b5b..0000000000 --- a/docs/em/docs/tutorial/body-fields.md +++ /dev/null @@ -1,60 +0,0 @@ -# 💪 - 🏑 - -🎏 🌌 👆 💪 📣 🌖 🔬 & 🗃 *➡ 🛠️ 🔢* 🔢 ⏮️ `Query`, `Path` & `Body`, 👆 💪 📣 🔬 & 🗃 🔘 Pydantic 🏷 ⚙️ Pydantic `Field`. - -## 🗄 `Field` - -🥇, 👆 ✔️ 🗄 ⚫️: - -{* ../../docs_src/body_fields/tutorial001.py hl[4] *} - -/// warning - -👀 👈 `Field` 🗄 🔗 ⚪️➡️ `pydantic`, 🚫 ⚪️➡️ `fastapi` 🌐 🎂 (`Query`, `Path`, `Body`, ♒️). - -/// - -## 📣 🏷 🔢 - -👆 💪 ⤴️ ⚙️ `Field` ⏮️ 🏷 🔢: - -{* ../../docs_src/body_fields/tutorial001.py hl[11:14] *} - -`Field` 👷 🎏 🌌 `Query`, `Path` & `Body`, ⚫️ ✔️ 🌐 🎏 🔢, ♒️. - -/// note | 📡 ℹ - -🤙, `Query`, `Path` & 🎏 👆 🔜 👀 ⏭ ✍ 🎚 🏿 ⚠ `Param` 🎓, ❔ ⚫️ 🏿 Pydantic `FieldInfo` 🎓. - - & Pydantic `Field` 📨 👐 `FieldInfo` 👍. - -`Body` 📨 🎚 🏿 `FieldInfo` 🔗. & 📤 🎏 👆 🔜 👀 ⏪ 👈 🏿 `Body` 🎓. - -💭 👈 🕐❔ 👆 🗄 `Query`, `Path`, & 🎏 ⚪️➡️ `fastapi`, 👈 🤙 🔢 👈 📨 🎁 🎓. - -/// - -/// tip - -👀 ❔ 🔠 🏷 🔢 ⏮️ 🆎, 🔢 💲 & `Field` ✔️ 🎏 📊 *➡ 🛠️ 🔢* 🔢, ⏮️ `Field` ↩️ `Path`, `Query` & `Body`. - -/// - -## 🚮 ➕ ℹ - -👆 💪 📣 ➕ ℹ `Field`, `Query`, `Body`, ♒️. & ⚫️ 🔜 🔌 🏗 🎻 🔗. - -👆 🔜 💡 🌅 🔃 ❎ ➕ ℹ ⏪ 🩺, 🕐❔ 🏫 📣 🖼. - -/// warning - -➕ 🔑 🚶‍♀️ `Field` 🔜 🎁 📉 🗄 🔗 👆 🈸. -👫 🔑 5️⃣📆 🚫 🎯 🍕 🗄 🔧, 🗄 🧰, 🖼 [🗄 💳](https://validator.swagger.io/), 5️⃣📆 🚫 👷 ⏮️ 👆 🏗 🔗. - -/// - -## 🌃 - -👆 💪 ⚙️ Pydantic `Field` 📣 ➕ 🔬 & 🗃 🏷 🔢. - -👆 💪 ⚙️ ➕ 🇨🇻 ❌ 🚶‍♀️ 🌖 🎻 🔗 🗃. diff --git a/docs/em/docs/tutorial/body-multiple-params.md b/docs/em/docs/tutorial/body-multiple-params.md deleted file mode 100644 index 3a2f2bd547..0000000000 --- a/docs/em/docs/tutorial/body-multiple-params.md +++ /dev/null @@ -1,171 +0,0 @@ -# 💪 - 💗 🔢 - -🔜 👈 👥 ✔️ 👀 ❔ ⚙️ `Path` & `Query`, ➡️ 👀 🌅 🏧 ⚙️ 📨 💪 📄. - -## 🌀 `Path`, `Query` & 💪 🔢 - -🥇, ↗️, 👆 💪 🌀 `Path`, `Query` & 📨 💪 🔢 📄 ➡ & **FastAPI** 🔜 💭 ⚫️❔. - -& 👆 💪 📣 💪 🔢 📦, ⚒ 🔢 `None`: - -{* ../../docs_src/body_multiple_params/tutorial001.py hl[19:21] *} - -/// note - -👀 👈, 👉 💼, `item` 👈 🔜 ✊ ⚪️➡️ 💪 📦. ⚫️ ✔️ `None` 🔢 💲. - -/// - -## 💗 💪 🔢 - -⏮️ 🖼, *➡ 🛠️* 🔜 ⌛ 🎻 💪 ⏮️ 🔢 `Item`, 💖: - -```JSON -{ - "name": "Foo", - "description": "The pretender", - "price": 42.0, - "tax": 3.2 -} -``` - -✋️ 👆 💪 📣 💗 💪 🔢, ✅ `item` & `user`: - -{* ../../docs_src/body_multiple_params/tutorial002.py hl[22] *} - -👉 💼, **FastAPI** 🔜 👀 👈 📤 🌅 🌘 1️⃣ 💪 🔢 🔢 (2️⃣ 🔢 👈 Pydantic 🏷). - -, ⚫️ 🔜 ⤴️ ⚙️ 🔢 📛 🔑 (🏑 📛) 💪, & ⌛ 💪 💖: - -```JSON -{ - "item": { - "name": "Foo", - "description": "The pretender", - "price": 42.0, - "tax": 3.2 - }, - "user": { - "username": "dave", - "full_name": "Dave Grohl" - } -} -``` - -/// note - -👀 👈 ✋️ `item` 📣 🎏 🌌 ⏭, ⚫️ 🔜 ⌛ 🔘 💪 ⏮️ 🔑 `item`. - -/// - -**FastAPI** 🔜 🏧 🛠️ ⚪️➡️ 📨, 👈 🔢 `item` 📨 ⚫️ 🎯 🎚 & 🎏 `user`. - -⚫️ 🔜 🎭 🔬 ⚗ 💽, & 🔜 📄 ⚫️ 💖 👈 🗄 🔗 & 🏧 🩺. - -## ⭐ 💲 💪 - -🎏 🌌 📤 `Query` & `Path` 🔬 ➕ 💽 🔢 & ➡ 🔢, **FastAPI** 🚚 🌓 `Body`. - -🖼, ↔ ⏮️ 🏷, 👆 💪 💭 👈 👆 💚 ✔️ ➕1️⃣ 🔑 `importance` 🎏 💪, 🥈 `item` & `user`. - -🚥 👆 📣 ⚫️, ↩️ ⚫️ ⭐ 💲, **FastAPI** 🔜 🤔 👈 ⚫️ 🔢 🔢. - -✋️ 👆 💪 💡 **FastAPI** 😥 ⚫️ ➕1️⃣ 💪 🔑 ⚙️ `Body`: - -{* ../../docs_src/body_multiple_params/tutorial003.py hl[22] *} - -👉 💼, **FastAPI** 🔜 ⌛ 💪 💖: - -```JSON -{ - "item": { - "name": "Foo", - "description": "The pretender", - "price": 42.0, - "tax": 3.2 - }, - "user": { - "username": "dave", - "full_name": "Dave Grohl" - }, - "importance": 5 -} -``` - -🔄, ⚫️ 🔜 🗜 📊 🆎, ✔, 📄, ♒️. - -## 💗 💪 = & 🔢 - -↗️, 👆 💪 📣 🌖 🔢 🔢 🕐❔ 👆 💪, 🌖 🙆 💪 🔢. - -, 🔢, ⭐ 💲 🔬 🔢 🔢, 👆 🚫 ✔️ 🎯 🚮 `Query`, 👆 💪: - -```Python -q: Union[str, None] = None -``` - -⚖️ 🐍 3️⃣.1️⃣0️⃣ & 🔛: - -```Python -q: str | None = None -``` - -🖼: - -{* ../../docs_src/body_multiple_params/tutorial004.py hl[27] *} - -/// info - -`Body` ✔️ 🌐 🎏 ➕ 🔬 & 🗃 🔢 `Query`,`Path` & 🎏 👆 🔜 👀 ⏪. - -/// - -## ⏯ 👁 💪 🔢 - -➡️ 💬 👆 🕴 ✔️ 👁 `item` 💪 🔢 ⚪️➡️ Pydantic 🏷 `Item`. - -🔢, **FastAPI** 🔜 ⤴️ ⌛ 🚮 💪 🔗. - -✋️ 🚥 👆 💚 ⚫️ ⌛ 🎻 ⏮️ 🔑 `item` & 🔘 ⚫️ 🏷 🎚, ⚫️ 🔨 🕐❔ 👆 📣 ➕ 💪 🔢, 👆 💪 ⚙️ 🎁 `Body` 🔢 `embed`: - -```Python -item: Item = Body(embed=True) -``` - -: - -{* ../../docs_src/body_multiple_params/tutorial005.py hl[17] *} - -👉 💼 **FastAPI** 🔜 ⌛ 💪 💖: - -```JSON hl_lines="2" -{ - "item": { - "name": "Foo", - "description": "The pretender", - "price": 42.0, - "tax": 3.2 - } -} -``` - -↩️: - -```JSON -{ - "name": "Foo", - "description": "The pretender", - "price": 42.0, - "tax": 3.2 -} -``` - -## 🌃 - -👆 💪 🚮 💗 💪 🔢 👆 *➡ 🛠️ 🔢*, ✋️ 📨 💪 🕴 ✔️ 👁 💪. - -✋️ **FastAPI** 🔜 🍵 ⚫️, 🤝 👆 ☑ 📊 👆 🔢, & ✔ & 📄 ☑ 🔗 *➡ 🛠️*. - -👆 💪 📣 ⭐ 💲 📨 🍕 💪. - -& 👆 💪 💡 **FastAPI** ⏯ 💪 🔑 🕐❔ 📤 🕴 👁 🔢 📣. diff --git a/docs/em/docs/tutorial/body-nested-models.md b/docs/em/docs/tutorial/body-nested-models.md deleted file mode 100644 index 6c8d5a6100..0000000000 --- a/docs/em/docs/tutorial/body-nested-models.md +++ /dev/null @@ -1,247 +0,0 @@ -# 💪 - 🔁 🏷 - -⏮️ **FastAPI**, 👆 💪 🔬, ✔, 📄, & ⚙️ 🎲 🙇 🐦 🏷 (👏 Pydantic). - -## 📇 🏑 - -👆 💪 🔬 🔢 🏾. 🖼, 🐍 `list`: - -{* ../../docs_src/body_nested_models/tutorial001.py hl[14] *} - -👉 🔜 ⚒ `tags` 📇, 👐 ⚫️ 🚫 📣 🆎 🔣 📇. - -## 📇 🏑 ⏮️ 🆎 🔢 - -✋️ 🐍 ✔️ 🎯 🌌 📣 📇 ⏮️ 🔗 🆎, ⚖️ "🆎 🔢": - -### 🗄 ⌨ `List` - -🐍 3️⃣.9️⃣ & 🔛 👆 💪 ⚙️ 🐩 `list` 📣 👫 🆎 ✍ 👥 🔜 👀 🔛. 👶 - -✋️ 🐍 ⏬ ⏭ 3️⃣.9️⃣ (3️⃣.6️⃣ & 🔛), 👆 🥇 💪 🗄 `List` ⚪️➡️ 🐩 🐍 `typing` 🕹: - -{* ../../docs_src/body_nested_models/tutorial002.py hl[1] *} - -### 📣 `list` ⏮️ 🆎 🔢 - -📣 🆎 👈 ✔️ 🆎 🔢 (🔗 🆎), 💖 `list`, `dict`, `tuple`: - -* 🚥 👆 🐍 ⏬ 🔅 🌘 3️⃣.9️⃣, 🗄 👫 🌓 ⏬ ⚪️➡️ `typing` 🕹 -* 🚶‍♀️ 🔗 🆎(Ⓜ) "🆎 🔢" ⚙️ ⬜ 🗜: `[` & `]` - -🐍 3️⃣.9️⃣ ⚫️ 🔜: - -```Python -my_list: list[str] -``` - -⏬ 🐍 ⏭ 3️⃣.9️⃣, ⚫️ 🔜: - -```Python -from typing import List - -my_list: List[str] -``` - -👈 🌐 🐩 🐍 ❕ 🆎 📄. - -⚙️ 👈 🎏 🐩 ❕ 🏷 🔢 ⏮️ 🔗 🆎. - -, 👆 🖼, 👥 💪 ⚒ `tags` 🎯 "📇 🎻": - -{* ../../docs_src/body_nested_models/tutorial002.py hl[14] *} - -## ⚒ 🆎 - -✋️ ⤴️ 👥 💭 🔃 ⚫️, & 🤔 👈 🔖 🚫🔜 🚫 🔁, 👫 🔜 🎲 😍 🎻. - -& 🐍 ✔️ 🎁 💽 🆎 ⚒ 😍 🏬, `set`. - -⤴️ 👥 💪 📣 `tags` ⚒ 🎻: - -{* ../../docs_src/body_nested_models/tutorial003.py hl[1,14] *} - -⏮️ 👉, 🚥 👆 📨 📨 ⏮️ ❎ 📊, ⚫️ 🔜 🗜 ⚒ 😍 🏬. - -& 🕐❔ 👆 🔢 👈 📊, 🚥 ℹ ✔️ ❎, ⚫️ 🔜 🔢 ⚒ 😍 🏬. - -& ⚫️ 🔜 ✍ / 📄 ➡️ 💁‍♂️. - -## 🐦 🏷 - -🔠 🔢 Pydantic 🏷 ✔️ 🆎. - -✋️ 👈 🆎 💪 ⚫️ ➕1️⃣ Pydantic 🏷. - -, 👆 💪 📣 🙇 🐦 🎻 "🎚" ⏮️ 🎯 🔢 📛, 🆎 & 🔬. - -🌐 👈, 🎲 🐦. - -### 🔬 📊 - -🖼, 👥 💪 🔬 `Image` 🏷: - -{* ../../docs_src/body_nested_models/tutorial004.py hl[9:11] *} - -### ⚙️ 📊 🆎 - -& ⤴️ 👥 💪 ⚙️ ⚫️ 🆎 🔢: - -{* ../../docs_src/body_nested_models/tutorial004.py hl[20] *} - -👉 🔜 ⛓ 👈 **FastAPI** 🔜 ⌛ 💪 🎏: - -```JSON -{ - "name": "Foo", - "description": "The pretender", - "price": 42.0, - "tax": 3.2, - "tags": ["rock", "metal", "bar"], - "image": { - "url": "http://example.com/baz.jpg", - "name": "The Foo live" - } -} -``` - -🔄, 🤸 👈 📄, ⏮️ **FastAPI** 👆 🤚: - -* 👨‍🎨 🐕‍🦺 (🛠️, ♒️), 🐦 🏷 -* 💽 🛠️ -* 💽 🔬 -* 🏧 🧾 - -## 🎁 🆎 & 🔬 - -↖️ ⚪️➡️ 😐 ⭐ 🆎 💖 `str`, `int`, `float`, ♒️. 👆 💪 ⚙️ 🌅 🏗 ⭐ 🆎 👈 😖 ⚪️➡️ `str`. - -👀 🌐 🎛 👆 ✔️, 🛒 🩺 Pydantic 😍 🆎. 👆 🔜 👀 🖼 ⏭ 📃. - -🖼, `Image` 🏷 👥 ✔️ `url` 🏑, 👥 💪 📣 ⚫️ ↩️ `str`, Pydantic `HttpUrl`: - -{* ../../docs_src/body_nested_models/tutorial005.py hl[4,10] *} - -🎻 🔜 ✅ ☑ 📛, & 📄 🎻 🔗 / 🗄 ✅. - -## 🔢 ⏮️ 📇 📊 - -👆 💪 ⚙️ Pydantic 🏷 🏾 `list`, `set`, ♒️: - -{* ../../docs_src/body_nested_models/tutorial006.py hl[20] *} - -👉 🔜 ⌛ (🗜, ✔, 📄, ♒️) 🎻 💪 💖: - -```JSON hl_lines="11" -{ - "name": "Foo", - "description": "The pretender", - "price": 42.0, - "tax": 3.2, - "tags": [ - "rock", - "metal", - "bar" - ], - "images": [ - { - "url": "http://example.com/baz.jpg", - "name": "The Foo live" - }, - { - "url": "http://example.com/dave.jpg", - "name": "The Baz" - } - ] -} -``` - -/// info - -👀 ❔ `images` 🔑 🔜 ✔️ 📇 🖼 🎚. - -/// - -## 🙇 🐦 🏷 - -👆 💪 🔬 🎲 🙇 🐦 🏷: - -{* ../../docs_src/body_nested_models/tutorial007.py hl[9,14,20,23,27] *} - -/// info - -👀 ❔ `Offer` ✔️ 📇 `Item`Ⓜ, ❔ 🔄 ✔️ 📦 📇 `Image`Ⓜ - -/// - -## 💪 😁 📇 - -🚥 🔝 🎚 💲 🎻 💪 👆 ⌛ 🎻 `array` (🐍 `list`), 👆 💪 📣 🆎 🔢 🔢, 🎏 Pydantic 🏷: - -```Python -images: List[Image] -``` - -⚖️ 🐍 3️⃣.9️⃣ & 🔛: - -```Python -images: list[Image] -``` - -: - -{* ../../docs_src/body_nested_models/tutorial008.py hl[15] *} - -## 👨‍🎨 🐕‍🦺 🌐 - -& 👆 🤚 👨‍🎨 🐕‍🦺 🌐. - -🏬 🔘 📇: - - - -👆 🚫 🚫 🤚 👉 😇 👨‍🎨 🐕‍🦺 🚥 👆 👷 🔗 ⏮️ `dict` ↩️ Pydantic 🏷. - -✋️ 👆 🚫 ✔️ 😟 🔃 👫 👯‍♂️, 📨 #️⃣ 🗜 🔁 & 👆 🔢 🗜 🔁 🎻 💁‍♂️. - -## 💪 ❌ `dict`Ⓜ - -👆 💪 📣 💪 `dict` ⏮️ 🔑 🆎 & 💲 🎏 🆎. - -🍵 ✔️ 💭 ⏪ ⚫️❔ ☑ 🏑/🔢 📛 (🔜 💼 ⏮️ Pydantic 🏷). - -👉 🔜 ⚠ 🚥 👆 💚 📨 🔑 👈 👆 🚫 ⏪ 💭. - ---- - -🎏 ⚠ 💼 🕐❔ 👆 💚 ✔️ 🔑 🎏 🆎, ✅ `int`. - -👈 ⚫️❔ 👥 🔜 👀 📥. - -👉 💼, 👆 🔜 🚫 🙆 `dict` 📏 ⚫️ ✔️ `int` 🔑 ⏮️ `float` 💲: - -{* ../../docs_src/body_nested_models/tutorial009.py hl[9] *} - -/// tip - -✔️ 🤯 👈 🎻 🕴 🐕‍🦺 `str` 🔑. - -✋️ Pydantic ✔️ 🏧 💽 🛠️. - -👉 ⛓ 👈, ✋️ 👆 🛠️ 👩‍💻 💪 🕴 📨 🎻 🔑, 📏 👈 🎻 🔌 😁 🔢, Pydantic 🔜 🗜 👫 & ✔ 👫. - - & `dict` 👆 📨 `weights` 🔜 🤙 ✔️ `int` 🔑 & `float` 💲. - -/// - -## 🌃 - -⏮️ **FastAPI** 👆 ✔️ 🔆 💪 🚚 Pydantic 🏷, ⏪ 🚧 👆 📟 🙅, 📏 & 😍. - -✋️ ⏮️ 🌐 💰: - -* 👨‍🎨 🐕‍🦺 (🛠️ 🌐 ❗) -* 💽 🛠️ (.Ⓜ.. ✍ / 🛠️) -* 💽 🔬 -* 🔗 🧾 -* 🏧 🩺 diff --git a/docs/em/docs/tutorial/body-updates.md b/docs/em/docs/tutorial/body-updates.md deleted file mode 100644 index 7e2fbfaf79..0000000000 --- a/docs/em/docs/tutorial/body-updates.md +++ /dev/null @@ -1,100 +0,0 @@ -# 💪 - ℹ - -## ℹ ❎ ⏮️ `PUT` - -ℹ 🏬 👆 💪 ⚙️ 🇺🇸🔍 `PUT` 🛠️. - -👆 💪 ⚙️ `jsonable_encoder` 🗜 🔢 💽 📊 👈 💪 🏪 🎻 (✅ ⏮️ ☁ 💽). 🖼, 🏭 `datetime` `str`. - -{* ../../docs_src/body_updates/tutorial001.py hl[30:35] *} - -`PUT` ⚙️ 📨 💽 👈 🔜 ❎ ♻ 💽. - -### ⚠ 🔃 ❎ - -👈 ⛓ 👈 🚥 👆 💚 ℹ 🏬 `bar` ⚙️ `PUT` ⏮️ 💪 ⚗: - -```Python -{ - "name": "Barz", - "price": 3, - "description": None, -} -``` - -↩️ ⚫️ 🚫 🔌 ⏪ 🏪 🔢 `"tax": 20.2`, 🔢 🏷 🔜 ✊ 🔢 💲 `"tax": 10.5`. - -& 📊 🔜 🖊 ⏮️ 👈 "🆕" `tax` `10.5`. - -## 🍕 ℹ ⏮️ `PATCH` - -👆 💪 ⚙️ 🇺🇸🔍 `PATCH` 🛠️ *🍕* ℹ 💽. - -👉 ⛓ 👈 👆 💪 📨 🕴 💽 👈 👆 💚 ℹ, 🍂 🎂 🐣. - -/// note - -`PATCH` 🌘 🛎 ⚙️ & 💭 🌘 `PUT`. - - & 📚 🏉 ⚙️ 🕴 `PUT`, 🍕 ℹ. - -👆 **🆓** ⚙️ 👫 👐 👆 💚, **FastAPI** 🚫 🚫 🙆 🚫. - -✋️ 👉 🦮 🎦 👆, 🌖 ⚖️ 🌘, ❔ 👫 🎯 ⚙️. - -/// - -### ⚙️ Pydantic `exclude_unset` 🔢 - -🚥 👆 💚 📨 🍕 ℹ, ⚫️ 📶 ⚠ ⚙️ 🔢 `exclude_unset` Pydantic 🏷 `.dict()`. - -💖 `item.dict(exclude_unset=True)`. - -👈 🔜 🏗 `dict` ⏮️ 🕴 💽 👈 ⚒ 🕐❔ 🏗 `item` 🏷, 🚫 🔢 💲. - -⤴️ 👆 💪 ⚙️ 👉 🏗 `dict` ⏮️ 🕴 💽 👈 ⚒ (📨 📨), 🚫 🔢 💲: - -{* ../../docs_src/body_updates/tutorial002.py hl[34] *} - -### ⚙️ Pydantic `update` 🔢 - -🔜, 👆 💪 ✍ 📁 ♻ 🏷 ⚙️ `.copy()`, & 🚶‍♀️ `update` 🔢 ⏮️ `dict` ⚗ 💽 ℹ. - -💖 `stored_item_model.copy(update=update_data)`: - -{* ../../docs_src/body_updates/tutorial002.py hl[35] *} - -### 🍕 ℹ 🌃 - -📄, ✔ 🍕 ℹ 👆 🔜: - -* (⚗) ⚙️ `PATCH` ↩️ `PUT`. -* 🗃 🏪 💽. -* 🚮 👈 💽 Pydantic 🏷. -* 🏗 `dict` 🍵 🔢 💲 ⚪️➡️ 🔢 🏷 (⚙️ `exclude_unset`). - * 👉 🌌 👆 💪 ℹ 🕴 💲 🤙 ⚒ 👩‍💻, ↩️ 🔐 💲 ⏪ 🏪 ⏮️ 🔢 💲 👆 🏷. -* ✍ 📁 🏪 🏷, 🛠️ ⚫️ 🔢 ⏮️ 📨 🍕 ℹ (⚙️ `update` 🔢). -* 🗜 📁 🏷 🕳 👈 💪 🏪 👆 💽 (🖼, ⚙️ `jsonable_encoder`). - * 👉 ⭐ ⚙️ 🏷 `.dict()` 👩‍🔬 🔄, ✋️ ⚫️ ⚒ 💭 (& 🗜) 💲 💽 🆎 👈 💪 🗜 🎻, 🖼, `datetime` `str`. -* 🖊 💽 👆 💽. -* 📨 ℹ 🏷. - -{* ../../docs_src/body_updates/tutorial002.py hl[30:37] *} - -/// tip - -👆 💪 🤙 ⚙️ 👉 🎏 ⚒ ⏮️ 🇺🇸🔍 `PUT` 🛠️. - -✋️ 🖼 📥 ⚙️ `PATCH` ↩️ ⚫️ ✍ 👫 ⚙️ 💼. - -/// - -/// note - -👀 👈 🔢 🏷 ✔. - -, 🚥 👆 💚 📨 🍕 ℹ 👈 💪 🚫 🌐 🔢, 👆 💪 ✔️ 🏷 ⏮️ 🌐 🔢 ™ 📦 (⏮️ 🔢 💲 ⚖️ `None`). - -🔬 ⚪️➡️ 🏷 ⏮️ 🌐 📦 💲 **ℹ** & 🏷 ⏮️ ✔ 💲 **🏗**, 👆 💪 ⚙️ 💭 🔬 [➕ 🏷](extra-models.md){.internal-link target=_blank}. - -/// diff --git a/docs/em/docs/tutorial/body.md b/docs/em/docs/tutorial/body.md deleted file mode 100644 index 09e1d7ccaa..0000000000 --- a/docs/em/docs/tutorial/body.md +++ /dev/null @@ -1,162 +0,0 @@ -# 📨 💪 - -🕐❔ 👆 💪 📨 📊 ⚪️➡️ 👩‍💻 (➡️ 💬, 🖥) 👆 🛠️, 👆 📨 ⚫️ **📨 💪**. - -**📨** 💪 📊 📨 👩‍💻 👆 🛠️. **📨** 💪 💽 👆 🛠️ 📨 👩‍💻. - -👆 🛠️ 🌖 🕧 ✔️ 📨 **📨** 💪. ✋️ 👩‍💻 🚫 🎯 💪 📨 **📨** 💪 🌐 🕰. - -📣 **📨** 💪, 👆 ⚙️ Pydantic 🏷 ⏮️ 🌐 👫 🏋️ & 💰. - -/// info - -📨 💽, 👆 🔜 ⚙️ 1️⃣: `POST` (🌅 ⚠), `PUT`, `DELETE` ⚖️ `PATCH`. - -📨 💪 ⏮️ `GET` 📨 ✔️ ⚠ 🎭 🔧, 👐, ⚫️ 🐕‍🦺 FastAPI, 🕴 📶 🏗/😕 ⚙️ 💼. - -⚫️ 🚫, 🎓 🩺 ⏮️ 🦁 🎚 🏆 🚫 🎦 🧾 💪 🕐❔ ⚙️ `GET`, & 🗳 🖕 💪 🚫 🐕‍🦺 ⚫️. - -/// - -## 🗄 Pydantic `BaseModel` - -🥇, 👆 💪 🗄 `BaseModel` ⚪️➡️ `pydantic`: - -{* ../../docs_src/body/tutorial001.py hl[4] *} - -## ✍ 👆 💽 🏷 - -⤴️ 👆 📣 👆 💽 🏷 🎓 👈 😖 ⚪️➡️ `BaseModel`. - -⚙️ 🐩 🐍 🆎 🌐 🔢: - -{* ../../docs_src/body/tutorial001.py hl[7:11] *} - -🎏 🕐❔ 📣 🔢 🔢, 🕐❔ 🏷 🔢 ✔️ 🔢 💲, ⚫️ 🚫 ✔. ⏪, ⚫️ ✔. ⚙️ `None` ⚒ ⚫️ 📦. - -🖼, 👉 🏷 🔛 📣 🎻 "`object`" (⚖️ 🐍 `dict`) 💖: - -```JSON -{ - "name": "Foo", - "description": "An optional description", - "price": 45.2, - "tax": 3.5 -} -``` - -... `description` & `tax` 📦 (⏮️ 🔢 💲 `None`), 👉 🎻 "`object`" 🔜 ☑: - -```JSON -{ - "name": "Foo", - "price": 45.2 -} -``` - -## 📣 ⚫️ 🔢 - -🚮 ⚫️ 👆 *➡ 🛠️*, 📣 ⚫️ 🎏 🌌 👆 📣 ➡ & 🔢 🔢: - -{* ../../docs_src/body/tutorial001.py hl[18] *} - -...& 📣 🚮 🆎 🏷 👆 ✍, `Item`. - -## 🏁 - -⏮️ 👈 🐍 🆎 📄, **FastAPI** 🔜: - -* ✍ 💪 📨 🎻. -* 🗜 🔗 🆎 (🚥 💪). -* ✔ 💽. - * 🚥 💽 ❌, ⚫️ 🔜 📨 👌 & 🆑 ❌, ☠️ ⚫️❔ 🌐❔ & ⚫️❔ ❌ 📊. -* 🤝 👆 📨 📊 🔢 `item`. - * 👆 📣 ⚫️ 🔢 🆎 `Item`, 👆 🔜 ✔️ 🌐 👨‍🎨 🐕‍🦺 (🛠️, ♒️) 🌐 🔢 & 👫 🆎. -* 🏗 🎻 🔗 🔑 👆 🏷, 👆 💪 ⚙️ 👫 🙆 🙆 👆 💖 🚥 ⚫️ ⚒ 🔑 👆 🏗. -* 👈 🔗 🔜 🍕 🏗 🗄 🔗, & ⚙️ 🏧 🧾 . - -## 🏧 🩺 - -🎻 🔗 👆 🏷 🔜 🍕 👆 🗄 🏗 🔗, & 🔜 🎦 🎓 🛠️ 🩺: - - - -& 🔜 ⚙️ 🛠️ 🩺 🔘 🔠 *➡ 🛠️* 👈 💪 👫: - - - -## 👨‍🎨 🐕‍🦺 - -👆 👨‍🎨, 🔘 👆 🔢 👆 🔜 🤚 🆎 🔑 & 🛠️ 🌐 (👉 🚫🔜 🔨 🚥 👆 📨 `dict` ↩️ Pydantic 🏷): - - - -👆 🤚 ❌ ✅ ❌ 🆎 🛠️: - - - -👉 🚫 🤞, 🎂 🛠️ 🏗 🤭 👈 🔧. - -& ⚫️ 🙇 💯 🔧 🌓, ⏭ 🙆 🛠️, 🚚 ⚫️ 🔜 👷 ⏮️ 🌐 👨‍🎨. - -📤 🔀 Pydantic ⚫️ 🐕‍🦺 👉. - -⏮️ 🖼 ✊ ⏮️ 🎙 🎙 📟. - -✋️ 👆 🔜 🤚 🎏 👨‍🎨 🐕‍🦺 ⏮️ 🗒 & 🌅 🎏 🐍 👨‍🎨: - - - -/// tip - -🚥 👆 ⚙️ 🗒 👆 👨‍🎨, 👆 💪 ⚙️ Pydantic 🗒 📁. - -⚫️ 📉 👨‍🎨 🐕‍🦺 Pydantic 🏷, ⏮️: - -* 🚘-🛠️ -* 🆎 ✅ -* 🛠️ -* 🔎 -* 🔬 - -/// - -## ⚙️ 🏷 - -🔘 🔢, 👆 💪 🔐 🌐 🔢 🏷 🎚 🔗: - -{* ../../docs_src/body/tutorial002.py hl[21] *} - -## 📨 💪 ➕ ➡ 🔢 - -👆 💪 📣 ➡ 🔢 & 📨 💪 🎏 🕰. - -**FastAPI** 🔜 🤔 👈 🔢 🔢 👈 🏏 ➡ 🔢 🔜 **✊ ⚪️➡️ ➡**, & 👈 🔢 🔢 👈 📣 Pydantic 🏷 🔜 **✊ ⚪️➡️ 📨 💪**. - -{* ../../docs_src/body/tutorial003.py hl[17:18] *} - -## 📨 💪 ➕ ➡ ➕ 🔢 🔢 - -👆 💪 📣 **💪**, **➡** & **🔢** 🔢, 🌐 🎏 🕰. - -**FastAPI** 🔜 🤔 🔠 👫 & ✊ 📊 ⚪️➡️ ☑ 🥉. - -{* ../../docs_src/body/tutorial004.py hl[18] *} - -🔢 🔢 🔜 🤔 ⏩: - -* 🚥 🔢 📣 **➡**, ⚫️ 🔜 ⚙️ ➡ 🔢. -* 🚥 🔢 **⭐ 🆎** (💖 `int`, `float`, `str`, `bool`, ♒️) ⚫️ 🔜 🔬 **🔢** 🔢. -* 🚥 🔢 📣 🆎 **Pydantic 🏷**, ⚫️ 🔜 🔬 📨 **💪**. - -/// note - -FastAPI 🔜 💭 👈 💲 `q` 🚫 ✔ ↩️ 🔢 💲 `= None`. - - `Union` `Union[str, None]` 🚫 ⚙️ FastAPI, ✋️ 🔜 ✔ 👆 👨‍🎨 🤝 👆 👍 🐕‍🦺 & 🔍 ❌. - -/// - -## 🍵 Pydantic - -🚥 👆 🚫 💚 ⚙️ Pydantic 🏷, 👆 💪 ⚙️ **💪** 🔢. 👀 🩺 [💪 - 💗 🔢: ⭐ 💲 💪](body-multiple-params.md#_2){.internal-link target=_blank}. diff --git a/docs/em/docs/tutorial/cookie-params.md b/docs/em/docs/tutorial/cookie-params.md deleted file mode 100644 index 4699fe2a54..0000000000 --- a/docs/em/docs/tutorial/cookie-params.md +++ /dev/null @@ -1,35 +0,0 @@ -# 🍪 🔢 - -👆 💪 🔬 🍪 🔢 🎏 🌌 👆 🔬 `Query` & `Path` 🔢. - -## 🗄 `Cookie` - -🥇 🗄 `Cookie`: - -{* ../../docs_src/cookie_params/tutorial001.py hl[3] *} - -## 📣 `Cookie` 🔢 - -⤴️ 📣 🍪 🔢 ⚙️ 🎏 📊 ⏮️ `Path` & `Query`. - -🥇 💲 🔢 💲, 👆 💪 🚶‍♀️ 🌐 ➕ 🔬 ⚖️ ✍ 🔢: - -{* ../../docs_src/cookie_params/tutorial001.py hl[9] *} - -/// note | 📡 ℹ - -`Cookie` "👭" 🎓 `Path` & `Query`. ⚫️ 😖 ⚪️➡️ 🎏 ⚠ `Param` 🎓. - -✋️ 💭 👈 🕐❔ 👆 🗄 `Query`, `Path`, `Cookie` & 🎏 ⚪️➡️ `fastapi`, 👈 🤙 🔢 👈 📨 🎁 🎓. - -/// - -/// info - -📣 🍪, 👆 💪 ⚙️ `Cookie`, ↩️ ⏪ 🔢 🔜 🔬 🔢 🔢. - -/// - -## 🌃 - -📣 🍪 ⏮️ `Cookie`, ⚙️ 🎏 ⚠ ⚓ `Query` & `Path`. diff --git a/docs/em/docs/tutorial/cors.md b/docs/em/docs/tutorial/cors.md deleted file mode 100644 index 44ab4adc59..0000000000 --- a/docs/em/docs/tutorial/cors.md +++ /dev/null @@ -1,85 +0,0 @@ -# ⚜ (✖️-🇨🇳 ℹ 🤝) - -⚜ ⚖️ "✖️-🇨🇳 ℹ 🤝" 🔗 ⚠ 🕐❔ 🕸 🏃‍♂ 🖥 ✔️ 🕸 📟 👈 🔗 ⏮️ 👩‍💻, & 👩‍💻 🎏 "🇨🇳" 🌘 🕸. - -## 🇨🇳 - -🇨🇳 🌀 🛠️ (`http`, `https`), 🆔 (`myapp.com`, `localhost`, `localhost.tiangolo.com`), & ⛴ (`80`, `443`, `8080`). - -, 🌐 👫 🎏 🇨🇳: - -* `http://localhost` -* `https://localhost` -* `http://localhost:8080` - -🚥 👫 🌐 `localhost`, 👫 ⚙️ 🎏 🛠️ ⚖️ ⛴,, 👫 🎏 "🇨🇳". - -## 🔁 - -, ➡️ 💬 👆 ✔️ 🕸 🏃 👆 🖥 `http://localhost:8080`, & 🚮 🕸 🔄 🔗 ⏮️ 👩‍💻 🏃 `http://localhost` (↩️ 👥 🚫 ✔ ⛴, 🖥 🔜 🤔 🔢 ⛴ `80`). - -⤴️, 🖥 🔜 📨 🇺🇸🔍 `OPTIONS` 📨 👩‍💻, & 🚥 👩‍💻 📨 ☑ 🎚 ✔ 📻 ⚪️➡️ 👉 🎏 🇨🇳 (`http://localhost:8080`) ⤴️ 🖥 🔜 ➡️ 🕸 🕸 📨 🚮 📨 👩‍💻. - -🏆 👉, 👩‍💻 🔜 ✔️ 📇 "✔ 🇨🇳". - -👉 💼, ⚫️ 🔜 ✔️ 🔌 `http://localhost:8080` 🕸 👷 ☑. - -## 🃏 - -⚫️ 💪 📣 📇 `"*"` ("🃏") 💬 👈 🌐 ✔. - -✋️ 👈 🔜 🕴 ✔ 🎯 🆎 📻, 🚫 🌐 👈 🔌 🎓: 🍪, ✔ 🎚 💖 📚 ⚙️ ⏮️ 📨 🤝, ♒️. - -, 🌐 👷 ☑, ⚫️ 👻 ✔ 🎯 ✔ 🇨🇳. - -## ⚙️ `CORSMiddleware` - -👆 💪 🔗 ⚫️ 👆 **FastAPI** 🈸 ⚙️ `CORSMiddleware`. - -* 🗄 `CORSMiddleware`. -* ✍ 📇 ✔ 🇨🇳 (🎻). -* 🚮 ⚫️ "🛠️" 👆 **FastAPI** 🈸. - -👆 💪 ✔ 🚥 👆 👩‍💻 ✔: - -* 🎓 (✔ 🎚, 🍪, ♒️). -* 🎯 🇺🇸🔍 👩‍🔬 (`POST`, `PUT`) ⚖️ 🌐 👫 ⏮️ 🃏 `"*"`. -* 🎯 🇺🇸🔍 🎚 ⚖️ 🌐 👫 ⏮️ 🃏 `"*"`. - -{* ../../docs_src/cors/tutorial001.py hl[2,6:11,13:19] *} - -🔢 🔢 ⚙️ `CORSMiddleware` 🛠️ 🚫 🔢, 👆 🔜 💪 🎯 🛠️ 🎯 🇨🇳, 👩‍🔬, ⚖️ 🎚, ✔ 🖥 ✔ ⚙️ 👫 ✖️-🆔 🔑. - -📄 ❌ 🐕‍🦺: - -* `allow_origins` - 📇 🇨🇳 👈 🔜 ✔ ⚒ ✖️-🇨🇳 📨. 🤶 Ⓜ. `['https://example.org', 'https://www.example.org']`. 👆 💪 ⚙️ `['*']` ✔ 🙆 🇨🇳. -* `allow_origin_regex` - 🎻 🎻 🏏 🛡 🇨🇳 👈 🔜 ✔ ⚒ ✖️-🇨🇳 📨. ✅ `'https://.*\.example\.org'`. -* `allow_methods` - 📇 🇺🇸🔍 👩‍🔬 👈 🔜 ✔ ✖️-🇨🇳 📨. 🔢 `['GET']`. 👆 💪 ⚙️ `['*']` ✔ 🌐 🐩 👩‍🔬. -* `allow_headers` - 📇 🇺🇸🔍 📨 🎚 👈 🔜 🐕‍🦺 ✖️-🇨🇳 📨. 🔢 `[]`. 👆 💪 ⚙️ `['*']` ✔ 🌐 🎚. `Accept`, `Accept-Language`, `Content-Language` & `Content-Type` 🎚 🕧 ✔ 🙅 ⚜ 📨. -* `allow_credentials` - 🎦 👈 🍪 🔜 🐕‍🦺 ✖️-🇨🇳 📨. 🔢 `False`. , `allow_origins` 🚫🔜 ⚒ `['*']` 🎓 ✔, 🇨🇳 🔜 ✔. -* `expose_headers` - 🎦 🙆 📨 🎚 👈 🔜 ⚒ ♿ 🖥. 🔢 `[]`. -* `max_age` - ⚒ 🔆 🕰 🥈 🖥 💾 ⚜ 📨. 🔢 `600`. - -🛠️ 📨 2️⃣ 🎯 🆎 🇺🇸🔍 📨... - -### ⚜ 🛫 📨 - -👉 🙆 `OPTIONS` 📨 ⏮️ `Origin` & `Access-Control-Request-Method` 🎚. - -👉 💼 🛠️ 🔜 🆘 📨 📨 & 📨 ⏮️ ☑ ⚜ 🎚, & 👯‍♂️ `200` ⚖️ `400` 📨 🎓 🎯. - -### 🙅 📨 - -🙆 📨 ⏮️ `Origin` 🎚. 👉 💼 🛠️ 🔜 🚶‍♀️ 📨 🔘 😐, ✋️ 🔜 🔌 ☑ ⚜ 🎚 🔛 📨. - -## 🌅 ℹ - -🌖 ℹ 🔃 , ✅ 🦎 ⚜ 🧾. - -/// note | 📡 ℹ - -👆 💪 ⚙️ `from starlette.middleware.cors import CORSMiddleware`. - -**FastAPI** 🚚 📚 🛠️ `fastapi.middleware` 🏪 👆, 👩‍💻. ✋️ 🌅 💪 🛠️ 👟 🔗 ⚪️➡️ 💃. - -/// diff --git a/docs/em/docs/tutorial/debugging.md b/docs/em/docs/tutorial/debugging.md deleted file mode 100644 index 97e61a763f..0000000000 --- a/docs/em/docs/tutorial/debugging.md +++ /dev/null @@ -1,113 +0,0 @@ -# 🛠️ - -👆 💪 🔗 🕹 👆 👨‍🎨, 🖼 ⏮️ 🎙 🎙 📟 ⚖️ 🗒. - -## 🤙 `uvicorn` - -👆 FastAPI 🈸, 🗄 & 🏃 `uvicorn` 🔗: - -{* ../../docs_src/debugging/tutorial001.py hl[1,15] *} - -### 🔃 `__name__ == "__main__"` - -👑 🎯 `__name__ == "__main__"` ✔️ 📟 👈 🛠️ 🕐❔ 👆 📁 🤙 ⏮️: - -
- -```console -$ python myapp.py -``` - -
- -✋️ 🚫 🤙 🕐❔ ➕1️⃣ 📁 🗄 ⚫️, 💖: - -```Python -from myapp import app -``` - -#### 🌅 ℹ - -➡️ 💬 👆 📁 🌟 `myapp.py`. - -🚥 👆 🏃 ⚫️ ⏮️: - -
- -```console -$ python myapp.py -``` - -
- -⤴️ 🔗 🔢 `__name__` 👆 📁, ✍ 🔁 🐍, 🔜 ✔️ 💲 🎻 `"__main__"`. - -, 📄: - -```Python - uvicorn.run(app, host="0.0.0.0", port=8000) -``` - -🔜 🏃. - ---- - -👉 🏆 🚫 🔨 🚥 👆 🗄 👈 🕹 (📁). - -, 🚥 👆 ✔️ ➕1️⃣ 📁 `importer.py` ⏮️: - -```Python -from myapp import app - -# Some more code -``` - -👈 💼, 🏧 🔢 🔘 `myapp.py` 🔜 🚫 ✔️ 🔢 `__name__` ⏮️ 💲 `"__main__"`. - -, ⏸: - -```Python - uvicorn.run(app, host="0.0.0.0", port=8000) -``` - -🔜 🚫 🛠️. - -/// info - -🌅 ℹ, ✅ 🛂 🐍 🩺. - -/// - -## 🏃 👆 📟 ⏮️ 👆 🕹 - -↩️ 👆 🏃 Uvicorn 💽 🔗 ⚪️➡️ 👆 📟, 👆 💪 🤙 👆 🐍 📋 (👆 FastAPI 🈸) 🔗 ⚪️➡️ 🕹. - ---- - -🖼, 🎙 🎙 📟, 👆 💪: - -* 🚶 "ℹ" 🎛. -* "🚮 📳...". -* 🖊 "🐍" -* 🏃 🕹 ⏮️ 🎛 "`Python: Current File (Integrated Terminal)`". - -⚫️ 🔜 ⤴️ ▶️ 💽 ⏮️ 👆 **FastAPI** 📟, ⛔️ 👆 0️⃣, ♒️. - -📥 ❔ ⚫️ 💪 👀: - - - ---- - -🚥 👆 ⚙️ 🗒, 👆 💪: - -* 📂 "🏃" 🍣. -* 🖊 🎛 "ℹ...". -* ⤴️ 🔑 🍣 🎦 🆙. -* 🖊 📁 ℹ (👉 💼, `main.py`). - -⚫️ 🔜 ⤴️ ▶️ 💽 ⏮️ 👆 **FastAPI** 📟, ⛔️ 👆 0️⃣, ♒️. - -📥 ❔ ⚫️ 💪 👀: - - diff --git a/docs/em/docs/tutorial/dependencies/classes-as-dependencies.md b/docs/em/docs/tutorial/dependencies/classes-as-dependencies.md deleted file mode 100644 index 41938bc7b8..0000000000 --- a/docs/em/docs/tutorial/dependencies/classes-as-dependencies.md +++ /dev/null @@ -1,180 +0,0 @@ -# 🎓 🔗 - -⏭ 🤿 ⏬ 🔘 **🔗 💉** ⚙️, ➡️ ♻ ⏮️ 🖼. - -## `dict` ⚪️➡️ ⏮️ 🖼 - -⏮️ 🖼, 👥 🛬 `dict` ⚪️➡️ 👆 🔗 ("☑"): - -{* ../../docs_src/dependencies/tutorial001.py hl[9] *} - -✋️ ⤴️ 👥 🤚 `dict` 🔢 `commons` *➡ 🛠️ 🔢*. - -& 👥 💭 👈 👨‍🎨 💪 🚫 🚚 📚 🐕‍🦺 (💖 🛠️) `dict`Ⓜ, ↩️ 👫 💪 🚫 💭 👫 🔑 & 💲 🆎. - -👥 💪 👍... - -## ⚫️❔ ⚒ 🔗 - -🆙 🔜 👆 ✔️ 👀 🔗 📣 🔢. - -✋️ 👈 🚫 🕴 🌌 📣 🔗 (👐 ⚫️ 🔜 🎲 🌖 ⚠). - -🔑 ⚖ 👈 🔗 🔜 "🇧🇲". - -"**🇧🇲**" 🐍 🕳 👈 🐍 💪 "🤙" 💖 🔢. - -, 🚥 👆 ✔️ 🎚 `something` (👈 💪 _🚫_ 🔢) & 👆 💪 "🤙" ⚫️ (🛠️ ⚫️) 💖: - -```Python -something() -``` - -⚖️ - -```Python -something(some_argument, some_keyword_argument="foo") -``` - -⤴️ ⚫️ "🇧🇲". - -## 🎓 🔗 - -👆 5️⃣📆 👀 👈 ✍ 👐 🐍 🎓, 👆 ⚙️ 👈 🎏 ❕. - -🖼: - -```Python -class Cat: - def __init__(self, name: str): - self.name = name - - -fluffy = Cat(name="Mr Fluffy") -``` - -👉 💼, `fluffy` 👐 🎓 `Cat`. - -& ✍ `fluffy`, 👆 "🤙" `Cat`. - -, 🐍 🎓 **🇧🇲**. - -⤴️, **FastAPI**, 👆 💪 ⚙️ 🐍 🎓 🔗. - -⚫️❔ FastAPI 🤙 ✅ 👈 ⚫️ "🇧🇲" (🔢, 🎓 ⚖️ 🕳 🙆) & 🔢 🔬. - -🚥 👆 🚶‍♀️ "🇧🇲" 🔗 **FastAPI**, ⚫️ 🔜 🔬 🔢 👈 "🇧🇲", & 🛠️ 👫 🎏 🌌 🔢 *➡ 🛠️ 🔢*. ✅ 🎧-🔗. - -👈 ✔ 🇧🇲 ⏮️ 🙅‍♂ 🔢 🌐. 🎏 ⚫️ 🔜 *➡ 🛠️ 🔢* ⏮️ 🙅‍♂ 🔢. - -⤴️, 👥 💪 🔀 🔗 "☑" `common_parameters` ⚪️➡️ 🔛 🎓 `CommonQueryParams`: - -{* ../../docs_src/dependencies/tutorial002.py hl[11:15] *} - -💸 🙋 `__init__` 👩‍🔬 ⚙️ ✍ 👐 🎓: - -{* ../../docs_src/dependencies/tutorial002.py hl[12] *} - -...⚫️ ✔️ 🎏 🔢 👆 ⏮️ `common_parameters`: - -{* ../../docs_src/dependencies/tutorial001.py hl[9] *} - -📚 🔢 ⚫️❔ **FastAPI** 🔜 ⚙️ "❎" 🔗. - -👯‍♂️ 💼, ⚫️ 🔜 ✔️: - -* 📦 `q` 🔢 🔢 👈 `str`. -* `skip` 🔢 🔢 👈 `int`, ⏮️ 🔢 `0`. -* `limit` 🔢 🔢 👈 `int`, ⏮️ 🔢 `100`. - -👯‍♂️ 💼 💽 🔜 🗜, ✔, 📄 🔛 🗄 🔗, ♒️. - -## ⚙️ ⚫️ - -🔜 👆 💪 📣 👆 🔗 ⚙️ 👉 🎓. - -{* ../../docs_src/dependencies/tutorial002.py hl[19] *} - -**FastAPI** 🤙 `CommonQueryParams` 🎓. 👉 ✍ "👐" 👈 🎓 & 👐 🔜 🚶‍♀️ 🔢 `commons` 👆 🔢. - -## 🆎 ✍ 🆚 `Depends` - -👀 ❔ 👥 ✍ `CommonQueryParams` 🕐 🔛 📟: - -```Python -commons: CommonQueryParams = Depends(CommonQueryParams) -``` - -🏁 `CommonQueryParams`,: - -```Python -... = Depends(CommonQueryParams) -``` - -...⚫️❔ **FastAPI** 🔜 🤙 ⚙️ 💭 ⚫️❔ 🔗. - -⚪️➡️ ⚫️ 👈 FastAPI 🔜 ⚗ 📣 🔢 & 👈 ⚫️❔ FastAPI 🔜 🤙 🤙. - ---- - -👉 💼, 🥇 `CommonQueryParams`,: - -```Python -commons: CommonQueryParams ... -``` - -...🚫 ✔️ 🙆 🎁 🔑 **FastAPI**. FastAPI 🏆 🚫 ⚙️ ⚫️ 💽 🛠️, 🔬, ♒️. (⚫️ ⚙️ `= Depends(CommonQueryParams)` 👈). - -👆 💪 🤙 ✍: - -```Python -commons = Depends(CommonQueryParams) -``` - -...: - -{* ../../docs_src/dependencies/tutorial003.py hl[19] *} - -✋️ 📣 🆎 💡 👈 🌌 👆 👨‍🎨 🔜 💭 ⚫️❔ 🔜 🚶‍♀️ 🔢 `commons`, & ⤴️ ⚫️ 💪 ℹ 👆 ⏮️ 📟 🛠️, 🆎 ✅, ♒️: - - - -## ⌨ - -✋️ 👆 👀 👈 👥 ✔️ 📟 🔁 📥, ✍ `CommonQueryParams` 🕐: - -```Python -commons: CommonQueryParams = Depends(CommonQueryParams) -``` - -**FastAPI** 🚚 ⌨ 👫 💼, 🌐❔ 🔗 *🎯* 🎓 👈 **FastAPI** 🔜 "🤙" ✍ 👐 🎓 ⚫️. - -📚 🎯 💼, 👆 💪 📄: - -↩️ ✍: - -```Python -commons: CommonQueryParams = Depends(CommonQueryParams) -``` - -...👆 ✍: - -```Python -commons: CommonQueryParams = Depends() -``` - -👆 📣 🔗 🆎 🔢, & 👆 ⚙️ `Depends()` 🚮 "🔢" 💲 (👈 ⏮️ `=`) 👈 🔢 🔢, 🍵 🙆 🔢 `Depends()`, ↩️ ✔️ ✍ 🌕 🎓 *🔄* 🔘 `Depends(CommonQueryParams)`. - -🎏 🖼 🔜 ⤴️ 👀 💖: - -{* ../../docs_src/dependencies/tutorial004.py hl[19] *} - -...& **FastAPI** 🔜 💭 ⚫️❔. - -/// tip - -🚥 👈 😑 🌅 😨 🌘 👍, 🤷‍♂ ⚫️, 👆 🚫 *💪* ⚫️. - -⚫️ ⌨. ↩️ **FastAPI** 💅 🔃 🤝 👆 📉 📟 🔁. - -/// diff --git a/docs/em/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md b/docs/em/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md deleted file mode 100644 index ab144a497b..0000000000 --- a/docs/em/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md +++ /dev/null @@ -1,69 +0,0 @@ -# 🔗 ➡ 🛠️ 👨‍🎨 - -💼 👆 🚫 🤙 💪 📨 💲 🔗 🔘 👆 *➡ 🛠️ 🔢*. - -⚖️ 🔗 🚫 📨 💲. - -✋️ 👆 💪 ⚫️ 🛠️/❎. - -📚 💼, ↩️ 📣 *➡ 🛠️ 🔢* 🔢 ⏮️ `Depends`, 👆 💪 🚮 `list` `dependencies` *➡ 🛠️ 👨‍🎨*. - -## 🚮 `dependencies` *➡ 🛠️ 👨‍🎨* - -*➡ 🛠️ 👨‍🎨* 📨 📦 ❌ `dependencies`. - -⚫️ 🔜 `list` `Depends()`: - -{* ../../docs_src/dependencies/tutorial006.py hl[17] *} - -👉 🔗 🔜 🛠️/❎ 🎏 🌌 😐 🔗. ✋️ 👫 💲 (🚥 👫 📨 🙆) 🏆 🚫 🚶‍♀️ 👆 *➡ 🛠️ 🔢*. - -/// tip - -👨‍🎨 ✅ ♻ 🔢 🔢, & 🎦 👫 ❌. - -⚙️ 👉 `dependencies` *➡ 🛠️ 👨‍🎨* 👆 💪 ⚒ 💭 👫 🛠️ ⏪ ❎ 👨‍🎨/🏭 ❌. - -⚫️ 💪 ℹ ❎ 😨 🆕 👩‍💻 👈 👀 ♻ 🔢 👆 📟 & 💪 💭 ⚫️ 🙃. - -/// - -/// info - -👉 🖼 👥 ⚙️ 💭 🛃 🎚 `X-Key` & `X-Token`. - -✋️ 🎰 💼, 🕐❔ 🛠️ 💂‍♂, 👆 🔜 🤚 🌖 💰 ⚪️➡️ ⚙️ 🛠️ [💂‍♂ 🚙 (⏭ 📃)](../security/index.md){.internal-link target=_blank}. - -/// - -## 🔗 ❌ & 📨 💲 - -👆 💪 ⚙️ 🎏 🔗 *🔢* 👆 ⚙️ 🛎. - -### 🔗 📄 - -👫 💪 📣 📨 📄 (💖 🎚) ⚖️ 🎏 🎧-🔗: - -{* ../../docs_src/dependencies/tutorial006.py hl[6,11] *} - -### 🤚 ⚠ - -👫 🔗 💪 `raise` ⚠, 🎏 😐 🔗: - -{* ../../docs_src/dependencies/tutorial006.py hl[8,13] *} - -### 📨 💲 - -& 👫 💪 📨 💲 ⚖️ 🚫, 💲 🏆 🚫 ⚙️. - -, 👆 💪 🏤-⚙️ 😐 🔗 (👈 📨 💲) 👆 ⏪ ⚙️ 👱 🙆, & ✋️ 💲 🏆 🚫 ⚙️, 🔗 🔜 🛠️: - -{* ../../docs_src/dependencies/tutorial006.py hl[9,14] *} - -## 🔗 👪 *➡ 🛠️* - -⏪, 🕐❔ 👂 🔃 ❔ 📊 🦏 🈸 ([🦏 🈸 - 💗 📁](../../tutorial/bigger-applications.md){.internal-link target=_blank}), 🎲 ⏮️ 💗 📁, 👆 🔜 💡 ❔ 📣 👁 `dependencies` 🔢 👪 *➡ 🛠️*. - -## 🌐 🔗 - -⏭ 👥 🔜 👀 ❔ 🚮 🔗 🎂 `FastAPI` 🈸, 👈 👫 ✔ 🔠 *➡ 🛠️*. diff --git a/docs/em/docs/tutorial/dependencies/dependencies-with-yield.md b/docs/em/docs/tutorial/dependencies/dependencies-with-yield.md deleted file mode 100644 index 1b37b1cf29..0000000000 --- a/docs/em/docs/tutorial/dependencies/dependencies-with-yield.md +++ /dev/null @@ -1,232 +0,0 @@ -# 🔗 ⏮️ 🌾 - -FastAPI 🐕‍🦺 🔗 👈 ➕ 🔁 ⏮️ 🏁. - -👉, ⚙️ `yield` ↩️ `return`, & ✍ ➕ 🔁 ⏮️. - -/// tip - -⚒ 💭 ⚙️ `yield` 1️⃣ 👁 🕰. - -/// - -/// note | 📡 ℹ - -🙆 🔢 👈 ☑ ⚙️ ⏮️: - -* `@contextlib.contextmanager` ⚖️ -* `@contextlib.asynccontextmanager` - -🔜 ☑ ⚙️ **FastAPI** 🔗. - -👐, FastAPI ⚙️ 📚 2️⃣ 👨‍🎨 🔘. - -/// - -## 💽 🔗 ⏮️ `yield` - -🖼, 👆 💪 ⚙️ 👉 ✍ 💽 🎉 & 🔐 ⚫️ ⏮️ 🏁. - -🕴 📟 ⏭ & 🔌 `yield` 📄 🛠️ ⏭ 📨 📨: - -{* ../../docs_src/dependencies/tutorial007.py hl[2:4] *} - -🌾 💲 ⚫️❔ 💉 🔘 *➡ 🛠️* & 🎏 🔗: - -{* ../../docs_src/dependencies/tutorial007.py hl[4] *} - -📟 📄 `yield` 📄 🛠️ ⏮️ 📨 ✔️ 🚚: - -{* ../../docs_src/dependencies/tutorial007.py hl[5:6] *} - -/// tip - -👆 💪 ⚙️ `async` ⚖️ 😐 🔢. - -**FastAPI** 🔜 ▶️️ 👜 ⏮️ 🔠, 🎏 ⏮️ 😐 🔗. - -/// - -## 🔗 ⏮️ `yield` & `try` - -🚥 👆 ⚙️ `try` 🍫 🔗 ⏮️ `yield`, 👆 🔜 📨 🙆 ⚠ 👈 🚮 🕐❔ ⚙️ 🔗. - -🖼, 🚥 📟 ☝ 🖕, ➕1️⃣ 🔗 ⚖️ *➡ 🛠️*, ⚒ 💽 💵 "💾" ⚖️ ✍ 🙆 🎏 ❌, 👆 🔜 📨 ⚠ 👆 🔗. - -, 👆 💪 👀 👈 🎯 ⚠ 🔘 🔗 ⏮️ `except SomeException`. - -🎏 🌌, 👆 💪 ⚙️ `finally` ⚒ 💭 🚪 📶 🛠️, 🙅‍♂ 🤔 🚥 📤 ⚠ ⚖️ 🚫. - -{* ../../docs_src/dependencies/tutorial007.py hl[3,5] *} - -## 🎧-🔗 ⏮️ `yield` - -👆 💪 ✔️ 🎧-🔗 & "🌲" 🎧-🔗 🙆 📐 & 💠, & 🙆 ⚖️ 🌐 👫 💪 ⚙️ `yield`. - -**FastAPI** 🔜 ⚒ 💭 👈 "🚪 📟" 🔠 🔗 ⏮️ `yield` 🏃 ☑ ✔. - -🖼, `dependency_c` 💪 ✔️ 🔗 🔛 `dependency_b`, & `dependency_b` 🔛 `dependency_a`: - -{* ../../docs_src/dependencies/tutorial008.py hl[4,12,20] *} - -& 🌐 👫 💪 ⚙️ `yield`. - -👉 💼 `dependency_c`, 🛠️ 🚮 🚪 📟, 💪 💲 ⚪️➡️ `dependency_b` (📥 📛 `dep_b`) 💪. - -& , 🔄, `dependency_b` 💪 💲 ⚪️➡️ `dependency_a` (📥 📛 `dep_a`) 💪 🚮 🚪 📟. - -{* ../../docs_src/dependencies/tutorial008.py hl[16:17,24:25] *} - -🎏 🌌, 👆 💪 ✔️ 🔗 ⏮️ `yield` & `return` 🌀. - -& 👆 💪 ✔️ 👁 🔗 👈 🚚 📚 🎏 🔗 ⏮️ `yield`, ♒️. - -👆 💪 ✔️ 🙆 🌀 🔗 👈 👆 💚. - -**FastAPI** 🔜 ⚒ 💭 🌐 🏃 ☑ ✔. - -/// note | 📡 ℹ - -👉 👷 👏 🐍 🔑 👨‍💼. - -**FastAPI** ⚙️ 👫 🔘 🏆 👉. - -/// - -## 🔗 ⏮️ `yield` & `HTTPException` - -👆 👀 👈 👆 💪 ⚙️ 🔗 ⏮️ `yield` & ✔️ `try` 🍫 👈 ✊ ⚠. - -⚫️ 5️⃣📆 😋 🤚 `HTTPException` ⚖️ 🎏 🚪 📟, ⏮️ `yield`. ✋️ **⚫️ 🏆 🚫 👷**. - -🚪 📟 🔗 ⏮️ `yield` 🛠️ *⏮️* 📨 📨, [⚠ 🐕‍🦺](../handling-errors.md#_4){.internal-link target=_blank} 🔜 ✔️ ⏪ 🏃. 📤 🕳 😽 ⚠ 🚮 👆 🔗 🚪 📟 (⏮️ `yield`). - -, 🚥 👆 🤚 `HTTPException` ⏮️ `yield`, 🔢 (⚖️ 🙆 🛃) ⚠ 🐕‍🦺 👈 ✊ `HTTPException`Ⓜ & 📨 🇺🇸🔍 4️⃣0️⃣0️⃣ 📨 🏆 🚫 📤 ✊ 👈 ⚠ 🚫🔜. - -👉 ⚫️❔ ✔ 🕳 ⚒ 🔗 (✅ 💽 🎉), 🖼, ⚙️ 🖥 📋. - -🖥 📋 🏃 *⏮️* 📨 ✔️ 📨. 📤 🙅‍♂ 🌌 🤚 `HTTPException` ↩️ 📤 🚫 🌌 🔀 📨 👈 *⏪ 📨*. - -✋️ 🚥 🖥 📋 ✍ 💽 ❌, 🌘 👆 💪 💾 ⚖️ 😬 🔐 🎉 🔗 ⏮️ `yield`, & 🎲 🕹 ❌ ⚖️ 📄 ⚫️ 🛰 🕵 ⚙️. - -🚥 👆 ✔️ 📟 👈 👆 💭 💪 🤚 ⚠, 🏆 😐/"🙃" 👜 & 🚮 `try` 🍫 👈 📄 📟. - -🚥 👆 ✔️ 🛃 ⚠ 👈 👆 🔜 💖 🍵 *⏭* 🛬 📨 & 🎲 ❎ 📨, 🎲 🙋‍♀ `HTTPException`, ✍ [🛃 ⚠ 🐕‍🦺](../handling-errors.md#_4){.internal-link target=_blank}. - -/// tip - -👆 💪 🤚 ⚠ 🔌 `HTTPException` *⏭* `yield`. ✋️ 🚫 ⏮️. - -/// - -🔁 🛠️ 🌅 ⚖️ 🌘 💖 👉 📊. 🕰 💧 ⚪️➡️ 🔝 🔝. & 🔠 🏓 1️⃣ 🍕 🔗 ⚖️ 🛠️ 📟. - -```mermaid -sequenceDiagram - -participant client as Client -participant handler as Exception handler -participant dep as Dep with yield -participant operation as Path Operation -participant tasks as Background tasks - - Note over client,tasks: Can raise exception for dependency, handled after response is sent - Note over client,operation: Can raise HTTPException and can change the response - client ->> dep: Start request - Note over dep: Run code up to yield - opt raise - dep -->> handler: Raise HTTPException - handler -->> client: HTTP error response - dep -->> dep: Raise other exception - end - dep ->> operation: Run dependency, e.g. DB session - opt raise - operation -->> dep: Raise HTTPException - dep -->> handler: Auto forward exception - handler -->> client: HTTP error response - operation -->> dep: Raise other exception - dep -->> handler: Auto forward exception - end - operation ->> client: Return response to client - Note over client,operation: Response is already sent, can't change it anymore - opt Tasks - operation -->> tasks: Send background tasks - end - opt Raise other exception - tasks -->> dep: Raise other exception - end - Note over dep: After yield - opt Handle other exception - dep -->> dep: Handle exception, can't change response. E.g. close DB session. - end -``` - -/// info - -🕴 **1️⃣ 📨** 🔜 📨 👩‍💻. ⚫️ 💪 1️⃣ ❌ 📨 ⚖️ ⚫️ 🔜 📨 ⚪️➡️ *➡ 🛠️*. - -⏮️ 1️⃣ 📚 📨 📨, 🙅‍♂ 🎏 📨 💪 📨. - -/// - -/// tip - -👉 📊 🎦 `HTTPException`, ✋️ 👆 💪 🤚 🙆 🎏 ⚠ ❔ 👆 ✍ [🛃 ⚠ 🐕‍🦺](../handling-errors.md#_4){.internal-link target=_blank}. - -🚥 👆 🤚 🙆 ⚠, ⚫️ 🔜 🚶‍♀️ 🔗 ⏮️ 🌾, 🔌 `HTTPException`, & ⤴️ **🔄** ⚠ 🐕‍🦺. 🚥 📤 🙅‍♂ ⚠ 🐕‍🦺 👈 ⚠, ⚫️ 🔜 ⤴️ 🍵 🔢 🔗 `ServerErrorMiddleware`, 🛬 5️⃣0️⃣0️⃣ 🇺🇸🔍 👔 📟, ➡️ 👩‍💻 💭 👈 📤 ❌ 💽. - -/// - -## 🔑 👨‍💼 - -### ⚫️❔ "🔑 👨‍💼" - -"🔑 👨‍💼" 🙆 👈 🐍 🎚 👈 👆 💪 ⚙️ `with` 📄. - -🖼, 👆 💪 ⚙️ `with` ✍ 📁: - -```Python -with open("./somefile.txt") as f: - contents = f.read() - print(contents) -``` - -🔘, `open("./somefile.txt")` ✍ 🎚 👈 🤙 "🔑 👨‍💼". - -🕐❔ `with` 🍫 🏁, ⚫️ ⚒ 💭 🔐 📁, 🚥 📤 ⚠. - -🕐❔ 👆 ✍ 🔗 ⏮️ `yield`, **FastAPI** 🔜 🔘 🗜 ⚫️ 🔑 👨‍💼, & 🌀 ⚫️ ⏮️ 🎏 🔗 🧰. - -### ⚙️ 🔑 👨‍💼 🔗 ⏮️ `yield` - -/// warning - -👉, 🌅 ⚖️ 🌘, "🏧" 💭. - -🚥 👆 ▶️ ⏮️ **FastAPI** 👆 💪 💚 🚶 ⚫️ 🔜. - -/// - -🐍, 👆 💪 ✍ 🔑 👨‍💼 🏗 🎓 ⏮️ 2️⃣ 👩‍🔬: `__enter__()` & `__exit__()`. - -👆 💪 ⚙️ 👫 🔘 **FastAPI** 🔗 ⏮️ `yield` ⚙️ -`with` ⚖️ `async with` 📄 🔘 🔗 🔢: - -{* ../../docs_src/dependencies/tutorial010.py hl[1:9,13] *} - -/// tip - -➕1️⃣ 🌌 ✍ 🔑 👨‍💼 ⏮️: - -* `@contextlib.contextmanager` ⚖️ -* `@contextlib.asynccontextmanager` - -⚙️ 👫 🎀 🔢 ⏮️ 👁 `yield`. - -👈 ⚫️❔ **FastAPI** ⚙️ 🔘 🔗 ⏮️ `yield`. - -✋️ 👆 🚫 ✔️ ⚙️ 👨‍🎨 FastAPI 🔗 (& 👆 🚫🔜 🚫). - -FastAPI 🔜 ⚫️ 👆 🔘. - -/// diff --git a/docs/em/docs/tutorial/dependencies/global-dependencies.md b/docs/em/docs/tutorial/dependencies/global-dependencies.md deleted file mode 100644 index 5a22e5f1c8..0000000000 --- a/docs/em/docs/tutorial/dependencies/global-dependencies.md +++ /dev/null @@ -1,15 +0,0 @@ -# 🌐 🔗 - -🆎 🈸 👆 💪 💚 🚮 🔗 🎂 🈸. - -🎏 🌌 👆 💪 [🚮 `dependencies` *➡ 🛠️ 👨‍🎨*](dependencies-in-path-operation-decorators.md){.internal-link target=_blank}, 👆 💪 🚮 👫 `FastAPI` 🈸. - -👈 💼, 👫 🔜 ✔ 🌐 *➡ 🛠️* 🈸: - -{* ../../docs_src/dependencies/tutorial012.py hl[15] *} - -& 🌐 💭 📄 🔃 [❎ `dependencies` *➡ 🛠️ 👨‍🎨*](dependencies-in-path-operation-decorators.md){.internal-link target=_blank} ✔, ✋️ 👉 💼, 🌐 *➡ 🛠️* 📱. - -## 🔗 👪 *➡ 🛠️* - -⏪, 🕐❔ 👂 🔃 ❔ 📊 🦏 🈸 ([🦏 🈸 - 💗 📁](../../tutorial/bigger-applications.md){.internal-link target=_blank}), 🎲 ⏮️ 💗 📁, 👆 🔜 💡 ❔ 📣 👁 `dependencies` 🔢 👪 *➡ 🛠️*. diff --git a/docs/em/docs/tutorial/dependencies/index.md b/docs/em/docs/tutorial/dependencies/index.md deleted file mode 100644 index ce87d9ee48..0000000000 --- a/docs/em/docs/tutorial/dependencies/index.md +++ /dev/null @@ -1,212 +0,0 @@ -# 🔗 - -**FastAPI** ✔️ 📶 🏋️ ✋️ 🏋️ **🔗 💉** ⚙️. - -⚫️ 🏗 📶 🙅 ⚙️, & ⚒ ⚫️ 📶 ⏩ 🙆 👩‍💻 🛠️ 🎏 🦲 ⏮️ **FastAPI**. - -## ⚫️❔ "🔗 💉" - -**"🔗 💉"** ⛓, 📋, 👈 📤 🌌 👆 📟 (👉 💼, 👆 *➡ 🛠️ 🔢*) 📣 👜 👈 ⚫️ 🚚 👷 & ⚙️: "🔗". - -& ⤴️, 👈 ⚙️ (👉 💼 **FastAPI**) 🔜 ✊ 💅 🔨 ⚫️❔ 💪 🚚 👆 📟 ⏮️ 📚 💪 🔗 ("💉" 🔗). - -👉 📶 ⚠ 🕐❔ 👆 💪: - -* ✔️ 💰 ⚛ (🎏 📟 ⚛ 🔄 & 🔄). -* 💰 💽 🔗. -* 🛠️ 💂‍♂, 🤝, 🔑 📄, ♒️. -* & 📚 🎏 👜... - -🌐 👫, ⏪ 📉 📟 🔁. - -## 🥇 🔁 - -➡️ 👀 📶 🙅 🖼. ⚫️ 🔜 🙅 👈 ⚫️ 🚫 📶 ⚠, 🔜. - -✋️ 👉 🌌 👥 💪 🎯 🔛 ❔ **🔗 💉** ⚙️ 👷. - -### ✍ 🔗, ⚖️ "☑" - -➡️ 🥇 🎯 🔛 🔗. - -⚫️ 🔢 👈 💪 ✊ 🌐 🎏 🔢 👈 *➡ 🛠️ 🔢* 💪 ✊: - -{* ../../docs_src/dependencies/tutorial001.py hl[8:11] *} - -👈 ⚫️. - -**2️⃣ ⏸**. - -& ⚫️ ✔️ 🎏 💠 & 📊 👈 🌐 👆 *➡ 🛠️ 🔢* ✔️. - -👆 💪 💭 ⚫️ *➡ 🛠️ 🔢* 🍵 "👨‍🎨" (🍵 `@app.get("/some-path")`). - -& ⚫️ 💪 📨 🕳 👆 💚. - -👉 💼, 👉 🔗 ⌛: - -* 📦 🔢 🔢 `q` 👈 `str`. -* 📦 🔢 🔢 `skip` 👈 `int`, & 🔢 `0`. -* 📦 🔢 🔢 `limit` 👈 `int`, & 🔢 `100`. - -& ⤴️ ⚫️ 📨 `dict` ⚗ 📚 💲. - -### 🗄 `Depends` - -{* ../../docs_src/dependencies/tutorial001.py hl[3] *} - -### 📣 🔗, "⚓️" - -🎏 🌌 👆 ⚙️ `Body`, `Query`, ♒️. ⏮️ 👆 *➡ 🛠️ 🔢* 🔢, ⚙️ `Depends` ⏮️ 🆕 🔢: - -{* ../../docs_src/dependencies/tutorial001.py hl[15,20] *} - -👐 👆 ⚙️ `Depends` 🔢 👆 🔢 🎏 🌌 👆 ⚙️ `Body`, `Query`, ♒️, `Depends` 👷 👄 🎏. - -👆 🕴 🤝 `Depends` 👁 🔢. - -👉 🔢 🔜 🕳 💖 🔢. - -& 👈 🔢 ✊ 🔢 🎏 🌌 👈 *➡ 🛠️ 🔢* . - -/// tip - -👆 🔜 👀 ⚫️❔ 🎏 "👜", ↖️ ⚪️➡️ 🔢, 💪 ⚙️ 🔗 ⏭ 📃. - -/// - -🕐❔ 🆕 📨 🛬, **FastAPI** 🔜 ✊ 💅: - -* 🤙 👆 🔗 ("☑") 🔢 ⏮️ ☑ 🔢. -* 🤚 🏁 ⚪️➡️ 👆 🔢. -* 🛠️ 👈 🏁 🔢 👆 *➡ 🛠️ 🔢*. - -```mermaid -graph TB - -common_parameters(["common_parameters"]) -read_items["/items/"] -read_users["/users/"] - -common_parameters --> read_items -common_parameters --> read_users -``` - -👉 🌌 👆 ✍ 🔗 📟 🕐 & **FastAPI** ✊ 💅 🤙 ⚫️ 👆 *➡ 🛠️*. - -/// check - -👀 👈 👆 🚫 ✔️ ✍ 🎁 🎓 & 🚶‍♀️ ⚫️ 👱 **FastAPI** "®" ⚫️ ⚖️ 🕳 🎏. - -👆 🚶‍♀️ ⚫️ `Depends` & **FastAPI** 💭 ❔ 🎂. - -/// - -## `async` ⚖️ 🚫 `async` - -🔗 🔜 🤙 **FastAPI** (🎏 👆 *➡ 🛠️ 🔢*), 🎏 🚫 ✔ ⏪ 🔬 👆 🔢. - -👆 💪 ⚙️ `async def` ⚖️ 😐 `def`. - -& 👆 💪 📣 🔗 ⏮️ `async def` 🔘 😐 `def` *➡ 🛠️ 🔢*, ⚖️ `def` 🔗 🔘 `async def` *➡ 🛠️ 🔢*, ♒️. - -⚫️ 🚫 🤔. **FastAPI** 🔜 💭 ⚫️❔. - -/// note - -🚥 👆 🚫 💭, ✅ [🔁: *"🏃 ❓" *](../../async.md){.internal-link target=_blank} 📄 🔃 `async` & `await` 🩺. - -/// - -## 🛠️ ⏮️ 🗄 - -🌐 📨 📄, 🔬 & 📄 👆 🔗 (& 🎧-🔗) 🔜 🛠️ 🎏 🗄 🔗. - -, 🎓 🩺 🔜 ✔️ 🌐 ℹ ⚪️➡️ 👫 🔗 💁‍♂️: - - - -## 🙅 ⚙️ - -🚥 👆 👀 ⚫️, *➡ 🛠️ 🔢* 📣 ⚙️ 🕐❔ *➡* & *🛠️* 🏏, & ⤴️ **FastAPI** ✊ 💅 🤙 🔢 ⏮️ ☑ 🔢, ❎ 📊 ⚪️➡️ 📨. - -🤙, 🌐 (⚖️ 🏆) 🕸 🛠️ 👷 👉 🎏 🌌. - -👆 🙅 🤙 👈 🔢 🔗. 👫 🤙 👆 🛠️ (👉 💼, **FastAPI**). - -⏮️ 🔗 💉 ⚙️, 👆 💪 💬 **FastAPI** 👈 👆 *➡ 🛠️ 🔢* "🪀" 🔛 🕳 🙆 👈 🔜 🛠️ ⏭ 👆 *➡ 🛠️ 🔢*, & **FastAPI** 🔜 ✊ 💅 🛠️ ⚫️ & "💉" 🏁. - -🎏 ⚠ ⚖ 👉 🎏 💭 "🔗 💉": - -* ℹ -* 🐕‍🦺 -* 🐕‍🦺 -* 💉 -* 🦲 - -## **FastAPI** 🔌-🔌 - -🛠️ & "🔌-"Ⓜ 💪 🏗 ⚙️ **🔗 💉** ⚙️. ✋️ 👐, 📤 🤙 **🙅‍♂ 💪 ✍ "🔌-🔌"**, ⚙️ 🔗 ⚫️ 💪 📣 ♾ 🔢 🛠️ & 🔗 👈 ▶️️ 💪 👆 *➡ 🛠️ 🔢*. - -& 🔗 💪 ✍ 📶 🙅 & 🏋️ 🌌 👈 ✔ 👆 🗄 🐍 📦 👆 💪, & 🛠️ 👫 ⏮️ 👆 🛠️ 🔢 👩‍❤‍👨 ⏸ 📟, *🌖*. - -👆 🔜 👀 🖼 👉 ⏭ 📃, 🔃 🔗 & ☁ 💽, 💂‍♂, ♒️. - -## **FastAPI** 🔗 - -🦁 🔗 💉 ⚙️ ⚒ **FastAPI** 🔗 ⏮️: - -* 🌐 🔗 💽 -* ☁ 💽 -* 🔢 📦 -* 🔢 🔗 -* 🤝 & ✔ ⚙️ -* 🛠️ ⚙️ ⚖ ⚙️ -* 📨 💽 💉 ⚙️ -* ♒️. - -## 🙅 & 🏋️ - -👐 🔗 🔗 💉 ⚙️ 📶 🙅 🔬 & ⚙️, ⚫️ 📶 🏋️. - -👆 💪 🔬 🔗 👈 🔄 💪 🔬 🔗 👫. - -🔚, 🔗 🌲 🔗 🏗, & **🔗 💉** ⚙️ ✊ 💅 🔬 🌐 👉 🔗 👆 (& 👫 🎧-🔗) & 🚚 (💉) 🏁 🔠 🔁. - -🖼, ➡️ 💬 👆 ✔️ 4️⃣ 🛠️ 🔗 (*➡ 🛠️*): - -* `/items/public/` -* `/items/private/` -* `/users/{user_id}/activate` -* `/items/pro/` - -⤴️ 👆 💪 🚮 🎏 ✔ 📄 🔠 👫 ⏮️ 🔗 & 🎧-🔗: - -```mermaid -graph TB - -current_user(["current_user"]) -active_user(["active_user"]) -admin_user(["admin_user"]) -paying_user(["paying_user"]) - -public["/items/public/"] -private["/items/private/"] -activate_user["/users/{user_id}/activate"] -pro_items["/items/pro/"] - -current_user --> active_user -active_user --> admin_user -active_user --> paying_user - -current_user --> public -active_user --> private -admin_user --> activate_user -paying_user --> pro_items -``` - -## 🛠️ ⏮️ **🗄** - -🌐 👫 🔗, ⏪ 📣 👫 📄, 🚮 🔢, 🔬, ♒️. 👆 *➡ 🛠️*. - -**FastAPI** 🔜 ✊ 💅 🚮 ⚫️ 🌐 🗄 🔗, 👈 ⚫️ 🎦 🎓 🧾 ⚙️. diff --git a/docs/em/docs/tutorial/dependencies/sub-dependencies.md b/docs/em/docs/tutorial/dependencies/sub-dependencies.md deleted file mode 100644 index 6d622e952f..0000000000 --- a/docs/em/docs/tutorial/dependencies/sub-dependencies.md +++ /dev/null @@ -1,86 +0,0 @@ -# 🎧-🔗 - -👆 💪 ✍ 🔗 👈 ✔️ **🎧-🔗**. - -👫 💪 **⏬** 👆 💪 👫. - -**FastAPI** 🔜 ✊ 💅 🔬 👫. - -## 🥇 🔗 "☑" - -👆 💪 ✍ 🥇 🔗 ("☑") 💖: - -{* ../../docs_src/dependencies/tutorial005.py hl[8:9] *} - -⚫️ 📣 📦 🔢 🔢 `q` `str`, & ⤴️ ⚫️ 📨 ⚫️. - -👉 🙅 (🚫 📶 ⚠), ✋️ 🔜 ℹ 👥 🎯 🔛 ❔ 🎧-🔗 👷. - -## 🥈 🔗, "☑" & "⚓️" - -⤴️ 👆 💪 ✍ ➕1️⃣ 🔗 🔢 ("☑") 👈 🎏 🕰 📣 🔗 🚮 👍 (⚫️ "⚓️" 💁‍♂️): - -{* ../../docs_src/dependencies/tutorial005.py hl[13] *} - -➡️ 🎯 🔛 🔢 📣: - -* ✋️ 👉 🔢 🔗 ("☑") ⚫️, ⚫️ 📣 ➕1️⃣ 🔗 (⚫️ "🪀" 🔛 🕳 🙆). - * ⚫️ 🪀 🔛 `query_extractor`, & 🛠️ 💲 📨 ⚫️ 🔢 `q`. -* ⚫️ 📣 📦 `last_query` 🍪, `str`. - * 🚥 👩‍💻 🚫 🚚 🙆 🔢 `q`, 👥 ⚙️ 🏁 🔢 ⚙️, ❔ 👥 🖊 🍪 ⏭. - -## ⚙️ 🔗 - -⤴️ 👥 💪 ⚙️ 🔗 ⏮️: - -{* ../../docs_src/dependencies/tutorial005.py hl[22] *} - -/// info - -👀 👈 👥 🕴 📣 1️⃣ 🔗 *➡ 🛠️ 🔢*, `query_or_cookie_extractor`. - -✋️ **FastAPI** 🔜 💭 👈 ⚫️ ✔️ ❎ `query_extractor` 🥇, 🚶‍♀️ 🏁 👈 `query_or_cookie_extractor` ⏪ 🤙 ⚫️. - -/// - -```mermaid -graph TB - -query_extractor(["query_extractor"]) -query_or_cookie_extractor(["query_or_cookie_extractor"]) - -read_query["/items/"] - -query_extractor --> query_or_cookie_extractor --> read_query -``` - -## ⚙️ 🎏 🔗 💗 🕰 - -🚥 1️⃣ 👆 🔗 📣 💗 🕰 🎏 *➡ 🛠️*, 🖼, 💗 🔗 ✔️ ⚠ 🎧-🔗, **FastAPI** 🔜 💭 🤙 👈 🎧-🔗 🕴 🕐 📍 📨. - -& ⚫️ 🔜 🖊 📨 💲 "💾" & 🚶‍♀️ ⚫️ 🌐 "⚓️" 👈 💪 ⚫️ 👈 🎯 📨, ↩️ 🤙 🔗 💗 🕰 🎏 📨. - -🏧 😐 🌐❔ 👆 💭 👆 💪 🔗 🤙 🔠 🔁 (🎲 💗 🕰) 🎏 📨 ↩️ ⚙️ "💾" 💲, 👆 💪 ⚒ 🔢 `use_cache=False` 🕐❔ ⚙️ `Depends`: - -```Python hl_lines="1" -async def needy_dependency(fresh_value: str = Depends(get_value, use_cache=False)): - return {"fresh_value": fresh_value} -``` - -## 🌃 - -↖️ ⚪️➡️ 🌐 🎀 🔤 ⚙️ 📥, **🔗 💉** ⚙️ 🙅. - -🔢 👈 👀 🎏 *➡ 🛠️ 🔢*. - -✋️, ⚫️ 📶 🏋️, & ✔ 👆 📣 🎲 🙇 🐦 🔗 "📊" (🌲). - -/// tip - -🌐 👉 💪 🚫 😑 ⚠ ⏮️ 👫 🙅 🖼. - -✋️ 👆 🔜 👀 ❔ ⚠ ⚫️ 📃 🔃 **💂‍♂**. - - & 👆 🔜 👀 💸 📟 ⚫️ 🔜 🖊 👆. - -/// diff --git a/docs/em/docs/tutorial/encoder.md b/docs/em/docs/tutorial/encoder.md deleted file mode 100644 index ad05f701e2..0000000000 --- a/docs/em/docs/tutorial/encoder.md +++ /dev/null @@ -1,35 +0,0 @@ -# 🎻 🔗 🔢 - -📤 💼 🌐❔ 👆 5️⃣📆 💪 🗜 💽 🆎 (💖 Pydantic 🏷) 🕳 🔗 ⏮️ 🎻 (💖 `dict`, `list`, ♒️). - -🖼, 🚥 👆 💪 🏪 ⚫️ 💽. - -👈, **FastAPI** 🚚 `jsonable_encoder()` 🔢. - -## ⚙️ `jsonable_encoder` - -➡️ 🌈 👈 👆 ✔️ 💽 `fake_db` 👈 🕴 📨 🎻 🔗 💽. - -🖼, ⚫️ 🚫 📨 `datetime` 🎚, 👈 🚫 🔗 ⏮️ 🎻. - -, `datetime` 🎚 🔜 ✔️ 🗜 `str` ⚗ 💽 💾 📁. - -🎏 🌌, 👉 💽 🚫🔜 📨 Pydantic 🏷 (🎚 ⏮️ 🔢), 🕴 `dict`. - -👆 💪 ⚙️ `jsonable_encoder` 👈. - -⚫️ 📨 🎚, 💖 Pydantic 🏷, & 📨 🎻 🔗 ⏬: - -{* ../../docs_src/encoder/tutorial001.py hl[5,22] *} - -👉 🖼, ⚫️ 🔜 🗜 Pydantic 🏷 `dict`, & `datetime` `str`. - -🏁 🤙 ⚫️ 🕳 👈 💪 🗜 ⏮️ 🐍 🐩 `json.dumps()`. - -⚫️ 🚫 📨 ⭕ `str` ⚗ 💽 🎻 📁 (🎻). ⚫️ 📨 🐍 🐩 💽 📊 (✅ `dict`) ⏮️ 💲 & 🎧-💲 👈 🌐 🔗 ⏮️ 🎻. - -/// note - -`jsonable_encoder` 🤙 ⚙️ **FastAPI** 🔘 🗜 💽. ✋️ ⚫️ ⚠ 📚 🎏 😐. - -/// diff --git a/docs/em/docs/tutorial/extra-data-types.md b/docs/em/docs/tutorial/extra-data-types.md deleted file mode 100644 index f15a74b4a3..0000000000 --- a/docs/em/docs/tutorial/extra-data-types.md +++ /dev/null @@ -1,62 +0,0 @@ -# ➕ 💽 🆎 - -🆙 🔜, 👆 ✔️ ⚙️ ⚠ 📊 🆎, 💖: - -* `int` -* `float` -* `str` -* `bool` - -✋️ 👆 💪 ⚙️ 🌅 🏗 📊 🆎. - -& 👆 🔜 ✔️ 🎏 ⚒ 👀 🆙 🔜: - -* 👑 👨‍🎨 🐕‍🦺. -* 💽 🛠️ ⚪️➡️ 📨 📨. -* 💽 🛠️ 📨 💽. -* 💽 🔬. -* 🏧 ✍ & 🧾. - -## 🎏 💽 🆎 - -📥 🌖 📊 🆎 👆 💪 ⚙️: - -* `UUID`: - * 🐩 "⭐ 😍 🆔", ⚠ 🆔 📚 💽 & ⚙️. - * 📨 & 📨 🔜 🎨 `str`. -* `datetime.datetime`: - * 🐍 `datetime.datetime`. - * 📨 & 📨 🔜 🎨 `str` 💾 8️⃣6️⃣0️⃣1️⃣ 📁, 💖: `2008-09-15T15:53:00+05:00`. -* `datetime.date`: - * 🐍 `datetime.date`. - * 📨 & 📨 🔜 🎨 `str` 💾 8️⃣6️⃣0️⃣1️⃣ 📁, 💖: `2008-09-15`. -* `datetime.time`: - * 🐍 `datetime.time`. - * 📨 & 📨 🔜 🎨 `str` 💾 8️⃣6️⃣0️⃣1️⃣ 📁, 💖: `14:23:55.003`. -* `datetime.timedelta`: - * 🐍 `datetime.timedelta`. - * 📨 & 📨 🔜 🎨 `float` 🌐 🥈. - * Pydantic ✔ 🎦 ⚫️ "💾 8️⃣6️⃣0️⃣1️⃣ 🕰 ➕ 🔢", 👀 🩺 🌅 ℹ. -* `frozenset`: - * 📨 & 📨, 😥 🎏 `set`: - * 📨, 📇 🔜 ✍, ❎ ❎ & 🏭 ⚫️ `set`. - * 📨, `set` 🔜 🗜 `list`. - * 🏗 🔗 🔜 ✔ 👈 `set` 💲 😍 (⚙️ 🎻 🔗 `uniqueItems`). -* `bytes`: - * 🐩 🐍 `bytes`. - * 📨 & 📨 🔜 😥 `str`. - * 🏗 🔗 🔜 ✔ 👈 ⚫️ `str` ⏮️ `binary` "📁". -* `Decimal`: - * 🐩 🐍 `Decimal`. - * 📨 & 📨, 🍵 🎏 `float`. -* 👆 💪 ✅ 🌐 ☑ Pydantic 📊 🆎 📥: Pydantic 📊 🆎. - -## 🖼 - -📥 🖼 *➡ 🛠️* ⏮️ 🔢 ⚙️ 🔛 🆎. - -{* ../../docs_src/extra_data_types/tutorial001.py hl[1,3,12:16] *} - -🗒 👈 🔢 🔘 🔢 ✔️ 👫 🐠 💽 🆎, & 👆 💪, 🖼, 🎭 😐 📅 🎭, 💖: - -{* ../../docs_src/extra_data_types/tutorial001.py hl[18:19] *} diff --git a/docs/em/docs/tutorial/extra-models.md b/docs/em/docs/tutorial/extra-models.md deleted file mode 100644 index 19ab5b7986..0000000000 --- a/docs/em/docs/tutorial/extra-models.md +++ /dev/null @@ -1,211 +0,0 @@ -# ➕ 🏷 - -▶️ ⏮️ ⏮️ 🖼, ⚫️ 🔜 ⚠ ✔️ 🌅 🌘 1️⃣ 🔗 🏷. - -👉 ✴️ 💼 👩‍💻 🏷, ↩️: - -* **🔢 🏷** 💪 💪 ✔️ 🔐. -* **🔢 🏷** 🔜 🚫 ✔️ 🔐. -* **💽 🏷** 🔜 🎲 💪 ✔️ #️⃣ 🔐. - -/// danger - -🙅 🏪 👩‍💻 🔢 🔐. 🕧 🏪 "🔐 #️⃣" 👈 👆 💪 ⤴️ ✔. - -🚥 👆 🚫 💭, 👆 🔜 💡 ⚫️❔ "🔐#️⃣" [💂‍♂ 📃](security/simple-oauth2.md#_4){.internal-link target=_blank}. - -/// - -## 💗 🏷 - -📥 🏢 💭 ❔ 🏷 💪 👀 💖 ⏮️ 👫 🔐 🏑 & 🥉 🌐❔ 👫 ⚙️: - -{* ../../docs_src/extra_models/tutorial001.py hl[9,11,16,22,24,29:30,33:35,40:41] *} - -### 🔃 `**user_in.dict()` - -#### Pydantic `.dict()` - -`user_in` Pydantic 🏷 🎓 `UserIn`. - -Pydantic 🏷 ✔️ `.dict()` 👩‍🔬 👈 📨 `dict` ⏮️ 🏷 💽. - -, 🚥 👥 ✍ Pydantic 🎚 `user_in` 💖: - -```Python -user_in = UserIn(username="john", password="secret", email="john.doe@example.com") -``` - -& ⤴️ 👥 🤙: - -```Python -user_dict = user_in.dict() -``` - -👥 🔜 ✔️ `dict` ⏮️ 💽 🔢 `user_dict` (⚫️ `dict` ↩️ Pydantic 🏷 🎚). - -& 🚥 👥 🤙: - -```Python -print(user_dict) -``` - -👥 🔜 🤚 🐍 `dict` ⏮️: - -```Python -{ - 'username': 'john', - 'password': 'secret', - 'email': 'john.doe@example.com', - 'full_name': None, -} -``` - -#### 🎁 `dict` - -🚥 👥 ✊ `dict` 💖 `user_dict` & 🚶‍♀️ ⚫️ 🔢 (⚖️ 🎓) ⏮️ `**user_dict`, 🐍 🔜 "🎁" ⚫️. ⚫️ 🔜 🚶‍♀️ 🔑 & 💲 `user_dict` 🔗 🔑-💲 ❌. - -, ▶️ ⏮️ `user_dict` ⚪️➡️ 🔛, ✍: - -```Python -UserInDB(**user_dict) -``` - -🔜 🏁 🕳 🌓: - -```Python -UserInDB( - username="john", - password="secret", - email="john.doe@example.com", - full_name=None, -) -``` - -⚖️ 🌅 ⚫️❔, ⚙️ `user_dict` 🔗, ⏮️ ⚫️❔ 🎚 ⚫️ 💪 ✔️ 🔮: - -```Python -UserInDB( - username = user_dict["username"], - password = user_dict["password"], - email = user_dict["email"], - full_name = user_dict["full_name"], -) -``` - -#### Pydantic 🏷 ⚪️➡️ 🎚 ➕1️⃣ - -🖼 🔛 👥 🤚 `user_dict` ⚪️➡️ `user_in.dict()`, 👉 📟: - -```Python -user_dict = user_in.dict() -UserInDB(**user_dict) -``` - -🔜 🌓: - -```Python -UserInDB(**user_in.dict()) -``` - -...↩️ `user_in.dict()` `dict`, & ⤴️ 👥 ⚒ 🐍 "🎁" ⚫️ 🚶‍♀️ ⚫️ `UserInDB` 🔠 ⏮️ `**`. - -, 👥 🤚 Pydantic 🏷 ⚪️➡️ 💽 ➕1️⃣ Pydantic 🏷. - -#### 🎁 `dict` & ➕ 🇨🇻 - -& ⤴️ ❎ ➕ 🇨🇻 ❌ `hashed_password=hashed_password`, 💖: - -```Python -UserInDB(**user_in.dict(), hashed_password=hashed_password) -``` - -...🔚 🆙 💆‍♂ 💖: - -```Python -UserInDB( - username = user_dict["username"], - password = user_dict["password"], - email = user_dict["email"], - full_name = user_dict["full_name"], - hashed_password = hashed_password, -) -``` - -/// warning - -🔗 🌖 🔢 🤖 💪 💧 💽, ✋️ 👫 ↗️ 🚫 🚚 🙆 🎰 💂‍♂. - -/// - -## 📉 ❎ - -📉 📟 ❎ 1️⃣ 🐚 💭 **FastAPI**. - -📟 ❎ 📈 🤞 🐛, 💂‍♂ ❔, 📟 🔁 ❔ (🕐❔ 👆 ℹ 1️⃣ 🥉 ✋️ 🚫 🎏), ♒️. - -& 👉 🏷 🌐 🤝 📚 💽 & ❎ 🔢 📛 & 🆎. - -👥 💪 👻. - -👥 💪 📣 `UserBase` 🏷 👈 🍦 🧢 👆 🎏 🏷. & ⤴️ 👥 💪 ⚒ 🏿 👈 🏷 👈 😖 🚮 🔢 (🆎 📄, 🔬, ♒️). - -🌐 💽 🛠️, 🔬, 🧾, ♒️. 🔜 👷 🛎. - -👈 🌌, 👥 💪 📣 🔺 🖖 🏷 (⏮️ 🔢 `password`, ⏮️ `hashed_password` & 🍵 🔐): - -{* ../../docs_src/extra_models/tutorial002.py hl[9,15:16,19:20,23:24] *} - -## `Union` ⚖️ `anyOf` - -👆 💪 📣 📨 `Union` 2️⃣ 🆎, 👈 ⛓, 👈 📨 🔜 🙆 2️⃣. - -⚫️ 🔜 🔬 🗄 ⏮️ `anyOf`. - -👈, ⚙️ 🐩 🐍 🆎 🔑 `typing.Union`: - -/// note - -🕐❔ ⚖ `Union`, 🔌 🏆 🎯 🆎 🥇, ⏩ 🌘 🎯 🆎. 🖼 🔛, 🌖 🎯 `PlaneItem` 👟 ⏭ `CarItem` `Union[PlaneItem, CarItem]`. - -/// - -{* ../../docs_src/extra_models/tutorial003.py hl[1,14:15,18:20,33] *} - -### `Union` 🐍 3️⃣.1️⃣0️⃣ - -👉 🖼 👥 🚶‍♀️ `Union[PlaneItem, CarItem]` 💲 ❌ `response_model`. - -↩️ 👥 🚶‍♀️ ⚫️ **💲 ❌** ↩️ 🚮 ⚫️ **🆎 ✍**, 👥 ✔️ ⚙️ `Union` 🐍 3️⃣.1️⃣0️⃣. - -🚥 ⚫️ 🆎 ✍ 👥 💪 ✔️ ⚙️ ⏸ ⏸,: - -```Python -some_variable: PlaneItem | CarItem -``` - -✋️ 🚥 👥 🚮 👈 `response_model=PlaneItem | CarItem` 👥 🔜 🤚 ❌, ↩️ 🐍 🔜 🔄 🎭 **❌ 🛠️** 🖖 `PlaneItem` & `CarItem` ↩️ 🔬 👈 🆎 ✍. - -## 📇 🏷 - -🎏 🌌, 👆 💪 📣 📨 📇 🎚. - -👈, ⚙️ 🐩 🐍 `typing.List` (⚖️ `list` 🐍 3️⃣.9️⃣ & 🔛): - -{* ../../docs_src/extra_models/tutorial004.py hl[1,20] *} - -## 📨 ⏮️ ❌ `dict` - -👆 💪 📣 📨 ⚙️ ✅ ❌ `dict`, 📣 🆎 🔑 & 💲, 🍵 ⚙️ Pydantic 🏷. - -👉 ⚠ 🚥 👆 🚫 💭 ☑ 🏑/🔢 📛 (👈 🔜 💪 Pydantic 🏷) ⏪. - -👉 💼, 👆 💪 ⚙️ `typing.Dict` (⚖️ `dict` 🐍 3️⃣.9️⃣ & 🔛): - -{* ../../docs_src/extra_models/tutorial005.py hl[1,8] *} - -## 🌃 - -⚙️ 💗 Pydantic 🏷 & 😖 ➡ 🔠 💼. - -👆 🚫 💪 ✔️ 👁 💽 🏷 📍 👨‍💼 🚥 👈 👨‍💼 🔜 💪 ✔️ 🎏 "🇵🇸". 💼 ⏮️ 👩‍💻 "👨‍💼" ⏮️ 🇵🇸 ✅ `password`, `password_hash` & 🙅‍♂ 🔐. diff --git a/docs/em/docs/tutorial/first-steps.md b/docs/em/docs/tutorial/first-steps.md deleted file mode 100644 index f9bb3fb756..0000000000 --- a/docs/em/docs/tutorial/first-steps.md +++ /dev/null @@ -1,335 +0,0 @@ -# 🥇 🔁 - -🙅 FastAPI 📁 💪 👀 💖 👉: - -{* ../../docs_src/first_steps/tutorial001.py *} - -📁 👈 📁 `main.py`. - -🏃 🖖 💽: - -
- -```console -$ uvicorn main:app --reload - -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -INFO: Started reloader process [28720] -INFO: Started server process [28722] -INFO: Waiting for application startup. -INFO: Application startup complete. -``` - -
- -/// note - -📋 `uvicorn main:app` 🔗: - -* `main`: 📁 `main.py` (🐍 "🕹"). -* `app`: 🎚 ✍ 🔘 `main.py` ⏮️ ⏸ `app = FastAPI()`. -* `--reload`: ⚒ 💽 ⏏ ⏮️ 📟 🔀. 🕴 ⚙️ 🛠️. - -/// - -🔢, 📤 ⏸ ⏮️ 🕳 💖: - -```hl_lines="4" -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -``` - -👈 ⏸ 🎦 📛 🌐❔ 👆 📱 ➖ 🍦, 👆 🇧🇿 🎰. - -### ✅ ⚫️ - -📂 👆 🖥 http://127.0.0.1:8000. - -👆 🔜 👀 🎻 📨: - -```JSON -{"message": "Hello World"} -``` - -### 🎓 🛠️ 🩺 - -🔜 🚶 http://127.0.0.1:8000/docs. - -👆 🔜 👀 🏧 🎓 🛠️ 🧾 (🚚 🦁 🎚): - -![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) - -### 🎛 🛠️ 🩺 - -& 🔜, 🚶 http://127.0.0.1:8000/redoc. - -👆 🔜 👀 🎛 🏧 🧾 (🚚 📄): - -![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) - -### 🗄 - -**FastAPI** 🏗 "🔗" ⏮️ 🌐 👆 🛠️ ⚙️ **🗄** 🐩 ⚖ 🔗. - -#### "🔗" - -"🔗" 🔑 ⚖️ 📛 🕳. 🚫 📟 👈 🛠️ ⚫️, ✋️ 📝 📛. - -#### 🛠️ "🔗" - -👉 💼, 🗄 🔧 👈 🤔 ❔ 🔬 🔗 👆 🛠️. - -👉 🔗 🔑 🔌 👆 🛠️ ➡, 💪 🔢 👫 ✊, ♒️. - -#### 💽 "🔗" - -⚖ "🔗" 💪 🔗 💠 💽, 💖 🎻 🎚. - -👈 💼, ⚫️ 🔜 ⛓ 🎻 🔢, & 📊 🆎 👫 ✔️, ♒️. - -#### 🗄 & 🎻 🔗 - -🗄 🔬 🛠️ 🔗 👆 🛠️. & 👈 🔗 🔌 🔑 (⚖️ "🔗") 📊 📨 & 📨 👆 🛠️ ⚙️ **🎻 🔗**, 🐩 🎻 📊 🔗. - -#### ✅ `openapi.json` - -🚥 👆 😟 🔃 ❔ 🍣 🗄 🔗 👀 💖, FastAPI 🔁 🏗 🎻 (🔗) ⏮️ 📛 🌐 👆 🛠️. - -👆 💪 👀 ⚫️ 🔗: http://127.0.0.1:8000/openapi.json. - -⚫️ 🔜 🎦 🎻 ▶️ ⏮️ 🕳 💖: - -```JSON -{ - "openapi": "3.0.2", - "info": { - "title": "FastAPI", - "version": "0.1.0" - }, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - - - -... -``` - -#### ⚫️❔ 🗄 - -🗄 🔗 ⚫️❔ 🏋️ 2️⃣ 🎓 🧾 ⚙️ 🔌. - -& 📤 💯 🎛, 🌐 ⚓️ 🔛 🗄. 👆 💪 💪 🚮 🙆 📚 🎛 👆 🈸 🏗 ⏮️ **FastAPI**. - -👆 💪 ⚙️ ⚫️ 🏗 📟 🔁, 👩‍💻 👈 🔗 ⏮️ 👆 🛠️. 🖼, 🕸, 📱 ⚖️ ☁ 🈸. - -## 🌃, 🔁 🔁 - -### 🔁 1️⃣: 🗄 `FastAPI` - -{* ../../docs_src/first_steps/tutorial001.py hl[1] *} - -`FastAPI` 🐍 🎓 👈 🚚 🌐 🛠️ 👆 🛠️. - -/// note | 📡 ℹ - -`FastAPI` 🎓 👈 😖 🔗 ⚪️➡️ `Starlette`. - -👆 💪 ⚙️ 🌐 💃 🛠️ ⏮️ `FastAPI` 💁‍♂️. - -/// - -### 🔁 2️⃣: ✍ `FastAPI` "👐" - -{* ../../docs_src/first_steps/tutorial001.py hl[3] *} - -📥 `app` 🔢 🔜 "👐" 🎓 `FastAPI`. - -👉 🔜 👑 ☝ 🔗 ✍ 🌐 👆 🛠️. - -👉 `app` 🎏 1️⃣ 🔗 `uvicorn` 📋: - -
- -```console -$ uvicorn main:app --reload - -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -``` - -
- -🚥 👆 ✍ 👆 📱 💖: - -{* ../../docs_src/first_steps/tutorial002.py hl[3] *} - -& 🚮 ⚫️ 📁 `main.py`, ⤴️ 👆 🔜 🤙 `uvicorn` 💖: - -
- -```console -$ uvicorn main:my_awesome_api --reload - -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -``` - -
- -### 🔁 3️⃣: ✍ *➡ 🛠️* - -#### ➡ - -"➡" 📥 🔗 🏁 🍕 📛 ▶️ ⚪️➡️ 🥇 `/`. - -, 📛 💖: - -``` -https://example.com/items/foo -``` - -...➡ 🔜: - -``` -/items/foo -``` - -/// info - -"➡" 🛎 🤙 "🔗" ⚖️ "🛣". - -/// - -⏪ 🏗 🛠️, "➡" 👑 🌌 🎏 "⚠" & "ℹ". - -#### 🛠️ - -"🛠️" 📥 🔗 1️⃣ 🇺🇸🔍 "👩‍🔬". - -1️⃣: - -* `POST` -* `GET` -* `PUT` -* `DELETE` - -...& 🌅 😍 🕐: - -* `OPTIONS` -* `HEAD` -* `PATCH` -* `TRACE` - -🇺🇸🔍 🛠️, 👆 💪 🔗 🔠 ➡ ⚙️ 1️⃣ (⚖️ 🌅) 👫 "👩‍🔬". - ---- - -🕐❔ 🏗 🔗, 👆 🛎 ⚙️ 👫 🎯 🇺🇸🔍 👩‍🔬 🎭 🎯 🎯. - -🛎 👆 ⚙️: - -* `POST`: ✍ 💽. -* `GET`: ✍ 💽. -* `PUT`: ℹ 💽. -* `DELETE`: ❎ 💽. - -, 🗄, 🔠 🇺🇸🔍 👩‍🔬 🤙 "🛠️". - -👥 🔜 🤙 👫 "**🛠️**" 💁‍♂️. - -#### 🔬 *➡ 🛠️ 👨‍🎨* - -{* ../../docs_src/first_steps/tutorial001.py hl[6] *} - -`@app.get("/")` 💬 **FastAPI** 👈 🔢 ▶️️ 🔛 🈚 🚚 📨 👈 🚶: - -* ➡ `/` -* ⚙️ get 🛠️ - -/// info | `@decorator` ℹ - -👈 `@something` ❕ 🐍 🤙 "👨‍🎨". - -👆 🚮 ⚫️ 🔛 🔝 🔢. 💖 📶 📔 👒 (👤 💭 👈 🌐❔ ⚖ 👟 ⚪️➡️). - - "👨‍🎨" ✊ 🔢 🔛 & 🔨 🕳 ⏮️ ⚫️. - -👆 💼, 👉 👨‍🎨 💬 **FastAPI** 👈 🔢 🔛 🔗 **➡** `/` ⏮️ **🛠️** `get`. - -⚫️ "**➡ 🛠️ 👨‍🎨**". - -/// - -👆 💪 ⚙️ 🎏 🛠️: - -* `@app.post()` -* `@app.put()` -* `@app.delete()` - -& 🌅 😍 🕐: - -* `@app.options()` -* `@app.head()` -* `@app.patch()` -* `@app.trace()` - -/// tip - -👆 🆓 ⚙️ 🔠 🛠️ (🇺🇸🔍 👩‍🔬) 👆 🎋. - -**FastAPI** 🚫 🛠️ 🙆 🎯 🔑. - -ℹ 📥 🎁 📄, 🚫 📄. - -🖼, 🕐❔ ⚙️ 🕹 👆 🛎 🎭 🌐 🎯 ⚙️ 🕴 `POST` 🛠️. - -/// - -### 🔁 4️⃣: 🔬 **➡ 🛠️ 🔢** - -👉 👆 "**➡ 🛠️ 🔢**": - -* **➡**: `/`. -* **🛠️**: `get`. -* **🔢**: 🔢 🔛 "👨‍🎨" (🔛 `@app.get("/")`). - -{* ../../docs_src/first_steps/tutorial001.py hl[7] *} - -👉 🐍 🔢. - -⚫️ 🔜 🤙 **FastAPI** 🕐❔ ⚫️ 📨 📨 📛 "`/`" ⚙️ `GET` 🛠️. - -👉 💼, ⚫️ `async` 🔢. - ---- - -👆 💪 🔬 ⚫️ 😐 🔢 ↩️ `async def`: - -{* ../../docs_src/first_steps/tutorial003.py hl[7] *} - -/// note - -🚥 👆 🚫 💭 🔺, ✅ [🔁: *"🏃 ❓"*](../async.md#_2){.internal-link target=_blank}. - -/// - -### 🔁 5️⃣: 📨 🎚 - -{* ../../docs_src/first_steps/tutorial001.py hl[8] *} - -👆 💪 📨 `dict`, `list`, ⭐ 💲 `str`, `int`, ♒️. - -👆 💪 📨 Pydantic 🏷 (👆 🔜 👀 🌅 🔃 👈 ⏪). - -📤 📚 🎏 🎚 & 🏷 👈 🔜 🔁 🗜 🎻 (🔌 🐜, ♒️). 🔄 ⚙️ 👆 💕 🕐, ⚫️ 🏆 🎲 👈 👫 ⏪ 🐕‍🦺. - -## 🌃 - -* 🗄 `FastAPI`. -* ✍ `app` 👐. -* ✍ **➡ 🛠️ 👨‍🎨** (💖 `@app.get("/")`). -* ✍ **➡ 🛠️ 🔢** (💖 `def root(): ...` 🔛). -* 🏃 🛠️ 💽 (💖 `uvicorn main:app --reload`). diff --git a/docs/em/docs/tutorial/handling-errors.md b/docs/em/docs/tutorial/handling-errors.md deleted file mode 100644 index 6d72775976..0000000000 --- a/docs/em/docs/tutorial/handling-errors.md +++ /dev/null @@ -1,257 +0,0 @@ -# 🚚 ❌ - -📤 📚 ⚠ 🌐❔ 👆 💪 🚨 ❌ 👩‍💻 👈 ⚙️ 👆 🛠️. - -👉 👩‍💻 💪 🖥 ⏮️ 🕸, 📟 ⚪️➡️ 👱 🙆, ☁ 📳, ♒️. - -👆 💪 💪 💬 👩‍💻 👈: - -* 👩‍💻 🚫 ✔️ 🥃 😌 👈 🛠️. -* 👩‍💻 🚫 ✔️ 🔐 👈 ℹ. -* 🏬 👩‍💻 🔄 🔐 🚫 🔀. -* ♒️. - -👫 💼, 👆 🔜 🛎 📨 **🇺🇸🔍 👔 📟** ↔ **4️⃣0️⃣0️⃣** (⚪️➡️ 4️⃣0️⃣0️⃣ 4️⃣9️⃣9️⃣). - -👉 🎏 2️⃣0️⃣0️⃣ 🇺🇸🔍 👔 📟 (⚪️➡️ 2️⃣0️⃣0️⃣ 2️⃣9️⃣9️⃣). 👈 "2️⃣0️⃣0️⃣" 👔 📟 ⛓ 👈 😫 📤 "🏆" 📨. - -👔 📟 4️⃣0️⃣0️⃣ ↔ ⛓ 👈 📤 ❌ ⚪️➡️ 👩‍💻. - -💭 🌐 👈 **"4️⃣0️⃣4️⃣ 🚫 🔎"** ❌ (& 🤣) ❓ - -## ⚙️ `HTTPException` - -📨 🇺🇸🔍 📨 ⏮️ ❌ 👩‍💻 👆 ⚙️ `HTTPException`. - -### 🗄 `HTTPException` - -{* ../../docs_src/handling_errors/tutorial001.py hl[1] *} - -### 🤚 `HTTPException` 👆 📟 - -`HTTPException` 😐 🐍 ⚠ ⏮️ 🌖 📊 🔗 🔗. - -↩️ ⚫️ 🐍 ⚠, 👆 🚫 `return` ⚫️, 👆 `raise` ⚫️. - -👉 ⛓ 👈 🚥 👆 🔘 🚙 🔢 👈 👆 🤙 🔘 👆 *➡ 🛠️ 🔢*, & 👆 🤚 `HTTPException` ⚪️➡️ 🔘 👈 🚙 🔢, ⚫️ 🏆 🚫 🏃 🎂 📟 *➡ 🛠️ 🔢*, ⚫️ 🔜 ❎ 👈 📨 ▶️️ ↖️ & 📨 🇺🇸🔍 ❌ ⚪️➡️ `HTTPException` 👩‍💻. - -💰 🙋‍♀ ⚠ 🤭 `return`😅 💲 🔜 🌖 ⭐ 📄 🔃 🔗 & 💂‍♂. - -👉 🖼, 🕐❔ 👩‍💻 📨 🏬 🆔 👈 🚫 🔀, 🤚 ⚠ ⏮️ 👔 📟 `404`: - -{* ../../docs_src/handling_errors/tutorial001.py hl[11] *} - -### 📉 📨 - -🚥 👩‍💻 📨 `http://example.com/items/foo` ( `item_id` `"foo"`), 👈 👩‍💻 🔜 📨 🇺🇸🔍 👔 📟 2️⃣0️⃣0️⃣, & 🎻 📨: - -```JSON -{ - "item": "The Foo Wrestlers" -} -``` - -✋️ 🚥 👩‍💻 📨 `http://example.com/items/bar` (🚫-🚫 `item_id` `"bar"`), 👈 👩‍💻 🔜 📨 🇺🇸🔍 👔 📟 4️⃣0️⃣4️⃣ ("🚫 🔎" ❌), & 🎻 📨: - -```JSON -{ - "detail": "Item not found" -} -``` - -/// tip - -🕐❔ 🙋‍♀ `HTTPException`, 👆 💪 🚶‍♀️ 🙆 💲 👈 💪 🗜 🎻 🔢 `detail`, 🚫 🕴 `str`. - -👆 💪 🚶‍♀️ `dict`, `list`, ♒️. - -👫 🍵 🔁 **FastAPI** & 🗜 🎻. - -/// - -## 🚮 🛃 🎚 - -📤 ⚠ 🌐❔ ⚫️ ⚠ 💪 🚮 🛃 🎚 🇺🇸🔍 ❌. 🖼, 🆎 💂‍♂. - -👆 🎲 🏆 🚫 💪 ⚙️ ⚫️ 🔗 👆 📟. - -✋️ 💼 👆 💪 ⚫️ 🏧 😐, 👆 💪 🚮 🛃 🎚: - -{* ../../docs_src/handling_errors/tutorial002.py hl[14] *} - -## ❎ 🛃 ⚠ 🐕‍🦺 - -👆 💪 🚮 🛃 ⚠ 🐕‍🦺 ⏮️ 🎏 ⚠ 🚙 ⚪️➡️ 💃. - -➡️ 💬 👆 ✔️ 🛃 ⚠ `UnicornException` 👈 👆 (⚖️ 🗃 👆 ⚙️) 💪 `raise`. - -& 👆 💚 🍵 👉 ⚠ 🌐 ⏮️ FastAPI. - -👆 💪 🚮 🛃 ⚠ 🐕‍🦺 ⏮️ `@app.exception_handler()`: - -{* ../../docs_src/handling_errors/tutorial003.py hl[5:7,13:18,24] *} - -📥, 🚥 👆 📨 `/unicorns/yolo`, *➡ 🛠️* 🔜 `raise` `UnicornException`. - -✋️ ⚫️ 🔜 🍵 `unicorn_exception_handler`. - -, 👆 🔜 📨 🧹 ❌, ⏮️ 🇺🇸🔍 👔 📟 `418` & 🎻 🎚: - -```JSON -{"message": "Oops! yolo did something. There goes a rainbow..."} -``` - -/// note | 📡 ℹ - -👆 💪 ⚙️ `from starlette.requests import Request` & `from starlette.responses import JSONResponse`. - -**FastAPI** 🚚 🎏 `starlette.responses` `fastapi.responses` 🏪 👆, 👩‍💻. ✋️ 🌅 💪 📨 👟 🔗 ⚪️➡️ 💃. 🎏 ⏮️ `Request`. - -/// - -## 🔐 🔢 ⚠ 🐕‍🦺 - -**FastAPI** ✔️ 🔢 ⚠ 🐕‍🦺. - -👫 🐕‍🦺 🈚 🛬 🔢 🎻 📨 🕐❔ 👆 `raise` `HTTPException` & 🕐❔ 📨 ✔️ ❌ 💽. - -👆 💪 🔐 👫 ⚠ 🐕‍🦺 ⏮️ 👆 👍. - -### 🔐 📨 🔬 ⚠ - -🕐❔ 📨 🔌 ❌ 📊, **FastAPI** 🔘 🤚 `RequestValidationError`. - -& ⚫️ 🔌 🔢 ⚠ 🐕‍🦺 ⚫️. - -🔐 ⚫️, 🗄 `RequestValidationError` & ⚙️ ⚫️ ⏮️ `@app.exception_handler(RequestValidationError)` 🎀 ⚠ 🐕‍🦺. - -⚠ 🐕‍🦺 🔜 📨 `Request` & ⚠. - -{* ../../docs_src/handling_errors/tutorial004.py hl[2,14:16] *} - -🔜, 🚥 👆 🚶 `/items/foo`, ↩️ 💆‍♂ 🔢 🎻 ❌ ⏮️: - -```JSON -{ - "detail": [ - { - "loc": [ - "path", - "item_id" - ], - "msg": "value is not a valid integer", - "type": "type_error.integer" - } - ] -} -``` - -👆 🔜 🤚 ✍ ⏬, ⏮️: - -``` -1 validation error -path -> item_id - value is not a valid integer (type=type_error.integer) -``` - -#### `RequestValidationError` 🆚 `ValidationError` - -/// warning - -👫 📡 ℹ 👈 👆 💪 🚶 🚥 ⚫️ 🚫 ⚠ 👆 🔜. - -/// - -`RequestValidationError` 🎧-🎓 Pydantic `ValidationError`. - -**FastAPI** ⚙️ ⚫️ 👈, 🚥 👆 ⚙️ Pydantic 🏷 `response_model`, & 👆 💽 ✔️ ❌, 👆 🔜 👀 ❌ 👆 🕹. - -✋️ 👩‍💻/👩‍💻 🔜 🚫 👀 ⚫️. ↩️, 👩‍💻 🔜 📨 "🔗 💽 ❌" ⏮️ 🇺🇸🔍 👔 📟 `500`. - -⚫️ 🔜 👉 🌌 ↩️ 🚥 👆 ✔️ Pydantic `ValidationError` 👆 *📨* ⚖️ 🙆 👆 📟 (🚫 👩‍💻 *📨*), ⚫️ 🤙 🐛 👆 📟. - -& ⏪ 👆 🔧 ⚫️, 👆 👩‍💻/👩‍💻 🚫🔜 🚫 ✔️ 🔐 🔗 ℹ 🔃 ❌, 👈 💪 🎦 💂‍♂ ⚠. - -### 🔐 `HTTPException` ❌ 🐕‍🦺 - -🎏 🌌, 👆 💪 🔐 `HTTPException` 🐕‍🦺. - -🖼, 👆 💪 💚 📨 ✅ ✍ 📨 ↩️ 🎻 👫 ❌: - -{* ../../docs_src/handling_errors/tutorial004.py hl[3:4,9:11,22] *} - -/// note | 📡 ℹ - -👆 💪 ⚙️ `from starlette.responses import PlainTextResponse`. - -**FastAPI** 🚚 🎏 `starlette.responses` `fastapi.responses` 🏪 👆, 👩‍💻. ✋️ 🌅 💪 📨 👟 🔗 ⚪️➡️ 💃. - -/// - -### ⚙️ `RequestValidationError` 💪 - -`RequestValidationError` 🔌 `body` ⚫️ 📨 ⏮️ ❌ 💽. - -👆 💪 ⚙️ ⚫️ ⏪ 🛠️ 👆 📱 🕹 💪 & ℹ ⚫️, 📨 ⚫️ 👩‍💻, ♒️. - -{* ../../docs_src/handling_errors/tutorial005.py hl[14] *} - -🔜 🔄 📨 ❌ 🏬 💖: - -```JSON -{ - "title": "towel", - "size": "XL" -} -``` - -👆 🔜 📨 📨 💬 👆 👈 💽 ❌ ⚗ 📨 💪: - -```JSON hl_lines="12-15" -{ - "detail": [ - { - "loc": [ - "body", - "size" - ], - "msg": "value is not a valid integer", - "type": "type_error.integer" - } - ], - "body": { - "title": "towel", - "size": "XL" - } -} -``` - -#### FastAPI `HTTPException` 🆚 💃 `HTTPException` - -**FastAPI** ✔️ 🚮 👍 `HTTPException`. - -& **FastAPI**'Ⓜ `HTTPException` ❌ 🎓 😖 ⚪️➡️ 💃 `HTTPException` ❌ 🎓. - -🕴 🔺, 👈 **FastAPI**'Ⓜ `HTTPException` ✔ 👆 🚮 🎚 🔌 📨. - -👉 💪/⚙️ 🔘 ✳ 2️⃣.0️⃣ & 💂‍♂ 🚙. - -, 👆 💪 🚧 🙋‍♀ **FastAPI**'Ⓜ `HTTPException` 🛎 👆 📟. - -✋️ 🕐❔ 👆 ® ⚠ 🐕‍🦺, 👆 🔜 ® ⚫️ 💃 `HTTPException`. - -👉 🌌, 🚥 🙆 🍕 💃 🔗 📟, ⚖️ 💃 ↔ ⚖️ 🔌 -, 🤚 💃 `HTTPException`, 👆 🐕‍🦺 🔜 💪 ✊ & 🍵 ⚫️. - -👉 🖼, 💪 ✔️ 👯‍♂️ `HTTPException`Ⓜ 🎏 📟, 💃 ⚠ 📁 `StarletteHTTPException`: - -```Python -from starlette.exceptions import HTTPException as StarletteHTTPException -``` - -### 🏤-⚙️ **FastAPI**'Ⓜ ⚠ 🐕‍🦺 - -🚥 👆 💚 ⚙️ ⚠ ⤴️ ⏮️ 🎏 🔢 ⚠ 🐕‍🦺 ⚪️➡️ **FastAPI**, 👆 💪 🗄 & 🏤-⚙️ 🔢 ⚠ 🐕‍🦺 ⚪️➡️ `fastapi.exception_handlers`: - -{* ../../docs_src/handling_errors/tutorial006.py hl[2:5,15,21] *} - -👉 🖼 👆 `print`😅 ❌ ⏮️ 📶 🎨 📧, ✋️ 👆 🤚 💭. 👆 💪 ⚙️ ⚠ & ⤴️ 🏤-⚙️ 🔢 ⚠ 🐕‍🦺. diff --git a/docs/em/docs/tutorial/header-params.md b/docs/em/docs/tutorial/header-params.md deleted file mode 100644 index fa5e3a22b4..0000000000 --- a/docs/em/docs/tutorial/header-params.md +++ /dev/null @@ -1,91 +0,0 @@ -# 🎚 🔢 - -👆 💪 🔬 🎚 🔢 🎏 🌌 👆 🔬 `Query`, `Path` & `Cookie` 🔢. - -## 🗄 `Header` - -🥇 🗄 `Header`: - -{* ../../docs_src/header_params/tutorial001.py hl[3] *} - -## 📣 `Header` 🔢 - -⤴️ 📣 🎚 🔢 ⚙️ 🎏 📊 ⏮️ `Path`, `Query` & `Cookie`. - -🥇 💲 🔢 💲, 👆 💪 🚶‍♀️ 🌐 ➕ 🔬 ⚖️ ✍ 🔢: - -{* ../../docs_src/header_params/tutorial001.py hl[9] *} - -/// note | 📡 ℹ - -`Header` "👭" 🎓 `Path`, `Query` & `Cookie`. ⚫️ 😖 ⚪️➡️ 🎏 ⚠ `Param` 🎓. - -✋️ 💭 👈 🕐❔ 👆 🗄 `Query`, `Path`, `Header`, & 🎏 ⚪️➡️ `fastapi`, 👈 🤙 🔢 👈 📨 🎁 🎓. - -/// - -/// info - -📣 🎚, 👆 💪 ⚙️ `Header`, ↩️ ⏪ 🔢 🔜 🔬 🔢 🔢. - -/// - -## 🏧 🛠️ - -`Header` ✔️ 🐥 ➕ 🛠️ 🔛 🔝 ⚫️❔ `Path`, `Query` & `Cookie` 🚚. - -🌅 🐩 🎚 🎏 "🔠" 🦹, 💭 "➖ 🔣" (`-`). - -✋️ 🔢 💖 `user-agent` ❌ 🐍. - -, 🔢, `Header` 🔜 🗜 🔢 📛 🦹 ⚪️➡️ 🎦 (`_`) 🔠 (`-`) ⚗ & 📄 🎚. - -, 🇺🇸🔍 🎚 💼-😛,, 👆 💪 📣 👫 ⏮️ 🐩 🐍 👗 (💭 "🔡"). - -, 👆 💪 ⚙️ `user_agent` 👆 🛎 🔜 🐍 📟, ↩️ 💆‍♂ 🎯 🥇 🔤 `User_Agent` ⚖️ 🕳 🎏. - -🚥 🤔 👆 💪 ❎ 🏧 🛠️ 🎦 🔠, ⚒ 🔢 `convert_underscores` `Header` `False`: - -{* ../../docs_src/header_params/tutorial002.py hl[10] *} - -/// warning - -⏭ ⚒ `convert_underscores` `False`, 🐻 🤯 👈 🇺🇸🔍 🗳 & 💽 / ⚙️ 🎚 ⏮️ 🎦. - -/// - -## ❎ 🎚 - -⚫️ 💪 📨 ❎ 🎚. 👈 ⛓, 🎏 🎚 ⏮️ 💗 💲. - -👆 💪 🔬 👈 💼 ⚙️ 📇 🆎 📄. - -👆 🔜 📨 🌐 💲 ⚪️➡️ ❎ 🎚 🐍 `list`. - -🖼, 📣 🎚 `X-Token` 👈 💪 😑 🌅 🌘 🕐, 👆 💪 ✍: - -{* ../../docs_src/header_params/tutorial003.py hl[9] *} - -🚥 👆 🔗 ⏮️ 👈 *➡ 🛠️* 📨 2️⃣ 🇺🇸🔍 🎚 💖: - -``` -X-Token: foo -X-Token: bar -``` - -📨 🔜 💖: - -```JSON -{ - "X-Token values": [ - "bar", - "foo" - ] -} -``` - -## 🌃 - -📣 🎚 ⏮️ `Header`, ⚙️ 🎏 ⚠ ⚓ `Query`, `Path` & `Cookie`. - -& 🚫 😟 🔃 🎦 👆 🔢, **FastAPI** 🔜 ✊ 💅 🏭 👫. diff --git a/docs/em/docs/tutorial/index.md b/docs/em/docs/tutorial/index.md deleted file mode 100644 index 5f7532341e..0000000000 --- a/docs/em/docs/tutorial/index.md +++ /dev/null @@ -1,83 +0,0 @@ -# 🔰 - 👩‍💻 🦮 - -👉 🔰 🎦 👆 ❔ ⚙️ **FastAPI** ⏮️ 🌅 🚮 ⚒, 🔁 🔁. - -🔠 📄 📉 🏗 🔛 ⏮️ 🕐, ✋️ ⚫️ 🏗 🎏 ❔, 👈 👆 💪 🚶 🔗 🙆 🎯 1️⃣ ❎ 👆 🎯 🛠️ 💪. - -⚫️ 🏗 👷 🔮 🔗. - -👆 💪 👟 🔙 & 👀 ⚫️❔ ⚫️❔ 👆 💪. - -## 🏃 📟 - -🌐 📟 🍫 💪 📁 & ⚙️ 🔗 (👫 🤙 💯 🐍 📁). - -🏃 🙆 🖼, 📁 📟 📁 `main.py`, & ▶️ `uvicorn` ⏮️: - -
- -```console -$ uvicorn main:app --reload - -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -INFO: Started reloader process [28720] -INFO: Started server process [28722] -INFO: Waiting for application startup. -INFO: Application startup complete. -``` - -
- -⚫️ **🏆 💡** 👈 👆 ✍ ⚖️ 📁 📟, ✍ ⚫️ & 🏃 ⚫️ 🌐. - -⚙️ ⚫️ 👆 👨‍🎨 ⚫️❔ 🤙 🎦 👆 💰 FastAPI, 👀 ❔ 🐥 📟 👆 ✔️ ✍, 🌐 🆎 ✅, ✍, ♒️. - ---- - -## ❎ FastAPI - -🥇 🔁 ❎ FastAPI. - -🔰, 👆 💪 💚 ❎ ⚫️ ⏮️ 🌐 📦 🔗 & ⚒: - -
- -```console -$ pip install "fastapi[all]" - ----> 100% -``` - -
- -...👈 🔌 `uvicorn`, 👈 👆 💪 ⚙️ 💽 👈 🏃 👆 📟. - -/// note - -👆 💪 ❎ ⚫️ 🍕 🍕. - -👉 ⚫️❔ 👆 🔜 🎲 🕐 👆 💚 🛠️ 👆 🈸 🏭: - -``` -pip install "fastapi[standard]" -``` - -❎ `uvicorn` 👷 💽: - -``` -pip install "uvicorn[standard]" -``` - - & 🎏 🔠 📦 🔗 👈 👆 💚 ⚙️. - -/// - -## 🏧 👩‍💻 🦮 - -📤 **🏧 👩‍💻 🦮** 👈 👆 💪 ✍ ⏪ ⏮️ 👉 **🔰 - 👩‍💻 🦮**. - -**🏧 👩‍💻 🦮**, 🏗 🔛 👉, ⚙️ 🎏 🔧, & 💡 👆 ➕ ⚒. - -✋️ 👆 🔜 🥇 ✍ **🔰 - 👩‍💻 🦮** (⚫️❔ 👆 👂 ▶️️ 🔜). - -⚫️ 🔧 👈 👆 💪 🏗 🏁 🈸 ⏮️ **🔰 - 👩‍💻 🦮**, & ⤴️ ↔ ⚫️ 🎏 🌌, ⚓️ 🔛 👆 💪, ⚙️ 🌖 💭 ⚪️➡️ **🏧 👩‍💻 🦮**. diff --git a/docs/em/docs/tutorial/metadata.md b/docs/em/docs/tutorial/metadata.md deleted file mode 100644 index eaf605de1e..0000000000 --- a/docs/em/docs/tutorial/metadata.md +++ /dev/null @@ -1,111 +0,0 @@ -# 🗃 & 🩺 📛 - -👆 💪 🛃 📚 🗃 📳 👆 **FastAPI** 🈸. - -## 🗃 🛠️ - -👆 💪 ⚒ 📄 🏑 👈 ⚙️ 🗄 🔧 & 🏧 🛠️ 🩺 ⚜: - -| 🔢 | 🆎 | 📛 | -|------------|------|-------------| -| `title` | `str` | 📛 🛠️. | -| `description` | `str` | 📏 📛 🛠️. ⚫️ 💪 ⚙️ ✍. | -| `version` | `string` | ⏬ 🛠️. 👉 ⏬ 👆 👍 🈸, 🚫 🗄. 🖼 `2.5.0`. | -| `terms_of_service` | `str` | 📛 ⚖ 🐕‍🦺 🛠️. 🚥 🚚, 👉 ✔️ 📛. | -| `contact` | `dict` | 📧 ℹ 🎦 🛠️. ⚫️ 💪 🔌 📚 🏑.
contact 🏑
🔢🆎📛
namestr⚖ 📛 📧 👨‍💼/🏢.
urlstr📛 ☝ 📧 ℹ. 🔜 📁 📛.
emailstr📧 📢 📧 👨‍💼/🏢. 🔜 📁 📧 📢.
| -| `license_info` | `dict` | 🛂 ℹ 🎦 🛠️. ⚫️ 💪 🔌 📚 🏑.
license_info 🏑
🔢🆎📛
namestr🚚 (🚥 license_info ⚒). 🛂 📛 ⚙️ 🛠️.
urlstr📛 🛂 ⚙️ 🛠️. 🔜 📁 📛.
| - -👆 💪 ⚒ 👫 ⏩: - -{* ../../docs_src/metadata/tutorial001.py hl[3:16,19:31] *} - -/// tip - -👆 💪 ✍ ✍ `description` 🏑 & ⚫️ 🔜 ✍ 🔢. - -/// - -⏮️ 👉 📳, 🏧 🛠️ 🩺 🔜 👀 💖: - - - -## 🗃 🔖 - -👆 💪 🚮 🌖 🗃 🎏 🔖 ⚙️ 👪 👆 ➡ 🛠️ ⏮️ 🔢 `openapi_tags`. - -⚫️ ✊ 📇 ⚗ 1️⃣ 📖 🔠 🔖. - -🔠 📖 💪 🔌: - -* `name` (**✔**): `str` ⏮️ 🎏 📛 👆 ⚙️ `tags` 🔢 👆 *➡ 🛠️* & `APIRouter`Ⓜ. -* `description`: `str` ⏮️ 📏 📛 🔖. ⚫️ 💪 ✔️ ✍ & 🔜 🎦 🩺 🎚. -* `externalDocs`: `dict` 🔬 🔢 🧾 ⏮️: - * `description`: `str` ⏮️ 📏 📛 🔢 🩺. - * `url` (**✔**): `str` ⏮️ 📛 🔢 🧾. - -### ✍ 🗃 🔖 - -➡️ 🔄 👈 🖼 ⏮️ 🔖 `users` & `items`. - -✍ 🗃 👆 🔖 & 🚶‍♀️ ⚫️ `openapi_tags` 🔢: - -{* ../../docs_src/metadata/tutorial004.py hl[3:16,18] *} - -👀 👈 👆 💪 ⚙️ ✍ 🔘 📛, 🖼 "💳" 🔜 🎦 🦁 (**💳**) & "🎀" 🔜 🎦 ❕ (_🎀_). - -/// tip - -👆 🚫 ✔️ 🚮 🗃 🌐 🔖 👈 👆 ⚙️. - -/// - -### ⚙️ 👆 🔖 - -⚙️ `tags` 🔢 ⏮️ 👆 *➡ 🛠️* (& `APIRouter`Ⓜ) 🛠️ 👫 🎏 🔖: - -{* ../../docs_src/metadata/tutorial004.py hl[21,26] *} - -/// info - -✍ 🌅 🔃 🔖 [➡ 🛠️ 📳](path-operation-configuration.md#_3){.internal-link target=_blank}. - -/// - -### ✅ 🩺 - -🔜, 🚥 👆 ✅ 🩺, 👫 🔜 🎦 🌐 🌖 🗃: - - - -### ✔ 🔖 - -✔ 🔠 🔖 🗃 📖 🔬 ✔ 🎦 🩺 🎚. - -🖼, ✋️ `users` 🔜 🚶 ⏮️ `items` 🔤 ✔, ⚫️ 🎦 ⏭ 👫, ↩️ 👥 🚮 👫 🗃 🥇 📖 📇. - -## 🗄 📛 - -🔢, 🗄 🔗 🍦 `/openapi.json`. - -✋️ 👆 💪 🔗 ⚫️ ⏮️ 🔢 `openapi_url`. - -🖼, ⚒ ⚫️ 🍦 `/api/v1/openapi.json`: - -{* ../../docs_src/metadata/tutorial002.py hl[3] *} - -🚥 👆 💚 ❎ 🗄 🔗 🍕 👆 💪 ⚒ `openapi_url=None`, 👈 🔜 ❎ 🧾 👩‍💻 🔢 👈 ⚙️ ⚫️. - -## 🩺 📛 - -👆 💪 🔗 2️⃣ 🧾 👩‍💻 🔢 🔌: - -* **🦁 🎚**: 🍦 `/docs`. - * 👆 💪 ⚒ 🚮 📛 ⏮️ 🔢 `docs_url`. - * 👆 💪 ❎ ⚫️ ⚒ `docs_url=None`. -* **📄**: 🍦 `/redoc`. - * 👆 💪 ⚒ 🚮 📛 ⏮️ 🔢 `redoc_url`. - * 👆 💪 ❎ ⚫️ ⚒ `redoc_url=None`. - -🖼, ⚒ 🦁 🎚 🍦 `/documentation` & ❎ 📄: - -{* ../../docs_src/metadata/tutorial003.py hl[3] *} diff --git a/docs/em/docs/tutorial/middleware.md b/docs/em/docs/tutorial/middleware.md deleted file mode 100644 index c77b105544..0000000000 --- a/docs/em/docs/tutorial/middleware.md +++ /dev/null @@ -1,66 +0,0 @@ -# 🛠️ - -👆 💪 🚮 🛠️ **FastAPI** 🈸. - -"🛠️" 🔢 👈 👷 ⏮️ 🔠 **📨** ⏭ ⚫️ 🛠️ 🙆 🎯 *➡ 🛠️*. & ⏮️ 🔠 **📨** ⏭ 🛬 ⚫️. - -* ⚫️ ✊ 🔠 **📨** 👈 👟 👆 🈸. -* ⚫️ 💪 ⤴️ 🕳 👈 **📨** ⚖️ 🏃 🙆 💪 📟. -* ⤴️ ⚫️ 🚶‍♀️ **📨** 🛠️ 🎂 🈸 ( *➡ 🛠️*). -* ⚫️ ⤴️ ✊ **📨** 🏗 🈸 ( *➡ 🛠️*). -* ⚫️ 💪 🕳 👈 **📨** ⚖️ 🏃 🙆 💪 📟. -* ⤴️ ⚫️ 📨 **📨**. - -/// note | 📡 ℹ - -🚥 👆 ✔️ 🔗 ⏮️ `yield`, 🚪 📟 🔜 🏃 *⏮️* 🛠️. - -🚥 📤 🙆 🖥 📋 (📄 ⏪), 👫 🔜 🏃 *⏮️* 🌐 🛠️. - -/// - -## ✍ 🛠️ - -✍ 🛠️ 👆 ⚙️ 👨‍🎨 `@app.middleware("http")` 🔛 🔝 🔢. - -🛠️ 🔢 📨: - -* `request`. -* 🔢 `call_next` 👈 🔜 📨 `request` 🔢. - * 👉 🔢 🔜 🚶‍♀️ `request` 🔗 *➡ 🛠️*. - * ⤴️ ⚫️ 📨 `response` 🏗 🔗 *➡ 🛠️*. -* 👆 💪 ⤴️ 🔀 🌅 `response` ⏭ 🛬 ⚫️. - -{* ../../docs_src/middleware/tutorial001.py hl[8:9,11,14] *} - -/// tip - -✔️ 🤯 👈 🛃 © 🎚 💪 🚮 ⚙️ '✖-' 🔡. - -✋️ 🚥 👆 ✔️ 🛃 🎚 👈 👆 💚 👩‍💻 🖥 💪 👀, 👆 💪 🚮 👫 👆 ⚜ 📳 ([⚜ (✖️-🇨🇳 ℹ 🤝)](cors.md){.internal-link target=_blank}) ⚙️ 🔢 `expose_headers` 📄 💃 ⚜ 🩺. - -/// - -/// note | 📡 ℹ - -👆 💪 ⚙️ `from starlette.requests import Request`. - -**FastAPI** 🚚 ⚫️ 🏪 👆, 👩‍💻. ✋️ ⚫️ 👟 🔗 ⚪️➡️ 💃. - -/// - -### ⏭ & ⏮️ `response` - -👆 💪 🚮 📟 🏃 ⏮️ `request`, ⏭ 🙆 *➡ 🛠️* 📨 ⚫️. - -& ⏮️ `response` 🏗, ⏭ 🛬 ⚫️. - -🖼, 👆 💪 🚮 🛃 🎚 `X-Process-Time` ⚗ 🕰 🥈 👈 ⚫️ ✊ 🛠️ 📨 & 🏗 📨: - -{* ../../docs_src/middleware/tutorial001.py hl[10,12:13] *} - -## 🎏 🛠️ - -👆 💪 ⏪ ✍ 🌖 🔃 🎏 🛠️ [🏧 👩‍💻 🦮: 🏧 🛠️](../advanced/middleware.md){.internal-link target=_blank}. - -👆 🔜 ✍ 🔃 ❔ 🍵 ⏮️ 🛠️ ⏭ 📄. diff --git a/docs/em/docs/tutorial/path-operation-configuration.md b/docs/em/docs/tutorial/path-operation-configuration.md deleted file mode 100644 index c6030c0890..0000000000 --- a/docs/em/docs/tutorial/path-operation-configuration.md +++ /dev/null @@ -1,107 +0,0 @@ -# ➡ 🛠️ 📳 - -📤 📚 🔢 👈 👆 💪 🚶‍♀️ 👆 *➡ 🛠️ 👨‍🎨* 🔗 ⚫️. - -/// warning - -👀 👈 👫 🔢 🚶‍♀️ 🔗 *➡ 🛠️ 👨‍🎨*, 🚫 👆 *➡ 🛠️ 🔢*. - -/// - -## 📨 👔 📟 - -👆 💪 🔬 (🇺🇸🔍) `status_code` ⚙️ 📨 👆 *➡ 🛠️*. - -👆 💪 🚶‍♀️ 🔗 `int` 📟, 💖 `404`. - -✋️ 🚥 👆 🚫 💭 ⚫️❔ 🔠 🔢 📟, 👆 💪 ⚙️ ⌨ 📉 `status`: - -{* ../../docs_src/path_operation_configuration/tutorial001.py hl[3,17] *} - -👈 👔 📟 🔜 ⚙️ 📨 & 🔜 🚮 🗄 🔗. - -/// note | 📡 ℹ - -👆 💪 ⚙️ `from starlette import status`. - -**FastAPI** 🚚 🎏 `starlette.status` `fastapi.status` 🏪 👆, 👩‍💻. ✋️ ⚫️ 👟 🔗 ⚪️➡️ 💃. - -/// - -## 🔖 - -👆 💪 🚮 🔖 👆 *➡ 🛠️*, 🚶‍♀️ 🔢 `tags` ⏮️ `list` `str` (🛎 1️⃣ `str`): - -{* ../../docs_src/path_operation_configuration/tutorial002.py hl[17,22,27] *} - -👫 🔜 🚮 🗄 🔗 & ⚙️ 🏧 🧾 🔢: - - - -### 🔖 ⏮️ 🔢 - -🚥 👆 ✔️ 🦏 🈸, 👆 5️⃣📆 🔚 🆙 📈 **📚 🔖**, & 👆 🔜 💚 ⚒ 💭 👆 🕧 ⚙️ **🎏 🔖** 🔗 *➡ 🛠️*. - -👫 💼, ⚫️ 💪 ⚒ 🔑 🏪 🔖 `Enum`. - -**FastAPI** 🐕‍🦺 👈 🎏 🌌 ⏮️ ✅ 🎻: - -{* ../../docs_src/path_operation_configuration/tutorial002b.py hl[1,8:10,13,18] *} - -## 📄 & 📛 - -👆 💪 🚮 `summary` & `description`: - -{* ../../docs_src/path_operation_configuration/tutorial003.py hl[20:21] *} - -## 📛 ⚪️➡️ #️⃣ - -📛 😑 📏 & 📔 💗 ⏸, 👆 💪 📣 *➡ 🛠️* 📛 🔢 #️⃣ & **FastAPI** 🔜 ✍ ⚫️ ⚪️➡️ 📤. - -👆 💪 ✍ #️⃣ , ⚫️ 🔜 🔬 & 🖥 ☑ (✊ 🔘 🏧 #️⃣ 📐). - -{* ../../docs_src/path_operation_configuration/tutorial004.py hl[19:27] *} - -⚫️ 🔜 ⚙️ 🎓 🩺: - - - -## 📨 📛 - -👆 💪 ✔ 📨 📛 ⏮️ 🔢 `response_description`: - -{* ../../docs_src/path_operation_configuration/tutorial005.py hl[21] *} - -/// info - -👀 👈 `response_description` 🔗 🎯 📨, `description` 🔗 *➡ 🛠️* 🏢. - -/// - -/// check - -🗄 ✔ 👈 🔠 *➡ 🛠️* 🚚 📨 📛. - -, 🚥 👆 🚫 🚚 1️⃣, **FastAPI** 🔜 🔁 🏗 1️⃣ "🏆 📨". - -/// - - - -## 😢 *➡ 🛠️* - -🚥 👆 💪 ™ *➡ 🛠️* 😢, ✋️ 🍵 ❎ ⚫️, 🚶‍♀️ 🔢 `deprecated`: - -{* ../../docs_src/path_operation_configuration/tutorial006.py hl[16] *} - -⚫️ 🔜 🎯 ™ 😢 🎓 🩺: - - - -✅ ❔ 😢 & 🚫-😢 *➡ 🛠️* 👀 💖: - - - -## 🌃 - -👆 💪 🔗 & 🚮 🗃 👆 *➡ 🛠️* 💪 🚶‍♀️ 🔢 *➡ 🛠️ 👨‍🎨*. diff --git a/docs/em/docs/tutorial/path-params-numeric-validations.md b/docs/em/docs/tutorial/path-params-numeric-validations.md deleted file mode 100644 index b45e0557b6..0000000000 --- a/docs/em/docs/tutorial/path-params-numeric-validations.md +++ /dev/null @@ -1,117 +0,0 @@ -# ➡ 🔢 & 🔢 🔬 - -🎏 🌌 👈 👆 💪 📣 🌅 🔬 & 🗃 🔢 🔢 ⏮️ `Query`, 👆 💪 📣 🎏 🆎 🔬 & 🗃 ➡ 🔢 ⏮️ `Path`. - -## 🗄 ➡ - -🥇, 🗄 `Path` ⚪️➡️ `fastapi`: - -{* ../../docs_src/path_params_numeric_validations/tutorial001.py hl[3] *} - -## 📣 🗃 - -👆 💪 📣 🌐 🎏 🔢 `Query`. - -🖼, 📣 `title` 🗃 💲 ➡ 🔢 `item_id` 👆 💪 🆎: - -{* ../../docs_src/path_params_numeric_validations/tutorial001.py hl[10] *} - -/// note - -➡ 🔢 🕧 ✔ ⚫️ ✔️ 🍕 ➡. - -, 👆 🔜 📣 ⚫️ ⏮️ `...` ™ ⚫️ ✔. - -👐, 🚥 👆 📣 ⚫️ ⏮️ `None` ⚖️ ⚒ 🔢 💲, ⚫️ 🔜 🚫 📉 🕳, ⚫️ 🔜 🕧 🚚. - -/// - -## ✔ 🔢 👆 💪 - -➡️ 💬 👈 👆 💚 📣 🔢 🔢 `q` ✔ `str`. - -& 👆 🚫 💪 📣 🕳 🙆 👈 🔢, 👆 🚫 🤙 💪 ⚙️ `Query`. - -✋️ 👆 💪 ⚙️ `Path` `item_id` ➡ 🔢. - -🐍 🔜 😭 🚥 👆 🚮 💲 ⏮️ "🔢" ⏭ 💲 👈 🚫 ✔️ "🔢". - -✋️ 👆 💪 🏤-✔ 👫, & ✔️ 💲 🍵 🔢 (🔢 🔢 `q`) 🥇. - -⚫️ 🚫 🤔 **FastAPI**. ⚫️ 🔜 🔍 🔢 👫 📛, 🆎 & 🔢 📄 (`Query`, `Path`, ♒️), ⚫️ 🚫 💅 🔃 ✔. - -, 👆 💪 📣 👆 🔢: - -{* ../../docs_src/path_params_numeric_validations/tutorial002.py hl[7] *} - -## ✔ 🔢 👆 💪, 🎱 - -🚥 👆 💚 📣 `q` 🔢 🔢 🍵 `Query` 🚫 🙆 🔢 💲, & ➡ 🔢 `item_id` ⚙️ `Path`, & ✔️ 👫 🎏 ✔, 🐍 ✔️ 🐥 🎁 ❕ 👈. - -🚶‍♀️ `*`, 🥇 🔢 🔢. - -🐍 🏆 🚫 🕳 ⏮️ 👈 `*`, ✋️ ⚫️ 🔜 💭 👈 🌐 📄 🔢 🔜 🤙 🇨🇻 ❌ (🔑-💲 👫), 💭 kwargs. 🚥 👫 🚫 ✔️ 🔢 💲. - -{* ../../docs_src/path_params_numeric_validations/tutorial003.py hl[7] *} - -## 🔢 🔬: 👑 🌘 ⚖️ 🌓 - -⏮️ `Query` & `Path` (& 🎏 👆 🔜 👀 ⏪) 👆 💪 📣 🔢 ⚛. - -📥, ⏮️ `ge=1`, `item_id` 🔜 💪 🔢 🔢 "`g`🅾 🌘 ⚖️ `e`🅾" `1`. - -{* ../../docs_src/path_params_numeric_validations/tutorial004.py hl[8] *} - -## 🔢 🔬: 🌘 🌘 & 🌘 🌘 ⚖️ 🌓 - -🎏 ✔: - -* `gt`: `g`🅾 `t`👲 -* `le`: `l`👭 🌘 ⚖️ `e`🅾 - -{* ../../docs_src/path_params_numeric_validations/tutorial005.py hl[9] *} - -## 🔢 🔬: 🎈, 🌘 🌘 & 🌘 🌘 - -🔢 🔬 👷 `float` 💲. - -📥 🌐❔ ⚫️ ▶️️ ⚠ 💪 📣 gt & 🚫 ge. ⏮️ ⚫️ 👆 💪 🚚, 🖼, 👈 💲 🔜 👑 🌘 `0`, 🚥 ⚫️ 🌘 🌘 `1`. - -, `0.5` 🔜 ☑ 💲. ✋️ `0.0` ⚖️ `0` 🔜 🚫. - -& 🎏 lt. - -{* ../../docs_src/path_params_numeric_validations/tutorial006.py hl[11] *} - -## 🌃 - -⏮️ `Query`, `Path` (& 🎏 👆 🚫 👀) 👆 💪 📣 🗃 & 🎻 🔬 🎏 🌌 ⏮️ [🔢 🔢 & 🎻 🔬](query-params-str-validations.md){.internal-link target=_blank}. - -& 👆 💪 📣 🔢 🔬: - -* `gt`: `g`🅾 `t`👲 -* `ge`: `g`🅾 🌘 ⚖️ `e`🅾 -* `lt`: `l`👭 `t`👲 -* `le`: `l`👭 🌘 ⚖️ `e`🅾 - -/// info - -`Query`, `Path`, & 🎏 🎓 👆 🔜 👀 ⏪ 🏿 ⚠ `Param` 🎓. - -🌐 👫 💰 🎏 🔢 🌖 🔬 & 🗃 👆 ✔️ 👀. - -/// - -/// note | 📡 ℹ - -🕐❔ 👆 🗄 `Query`, `Path` & 🎏 ⚪️➡️ `fastapi`, 👫 🤙 🔢. - -👈 🕐❔ 🤙, 📨 👐 🎓 🎏 📛. - -, 👆 🗄 `Query`, ❔ 🔢. & 🕐❔ 👆 🤙 ⚫️, ⚫️ 📨 👐 🎓 🌟 `Query`. - -👫 🔢 📤 (↩️ ⚙️ 🎓 🔗) 👈 👆 👨‍🎨 🚫 ™ ❌ 🔃 👫 🆎. - -👈 🌌 👆 💪 ⚙️ 👆 😐 👨‍🎨 & 🛠️ 🧰 🍵 ✔️ 🚮 🛃 📳 🤷‍♂ 📚 ❌. - -/// diff --git a/docs/em/docs/tutorial/path-params.md b/docs/em/docs/tutorial/path-params.md deleted file mode 100644 index a914dc9050..0000000000 --- a/docs/em/docs/tutorial/path-params.md +++ /dev/null @@ -1,256 +0,0 @@ -# ➡ 🔢 - -👆 💪 📣 ➡ "🔢" ⚖️ "🔢" ⏮️ 🎏 ❕ ⚙️ 🐍 📁 🎻: - -{* ../../docs_src/path_params/tutorial001.py hl[6:7] *} - -💲 ➡ 🔢 `item_id` 🔜 🚶‍♀️ 👆 🔢 ❌ `item_id`. - -, 🚥 👆 🏃 👉 🖼 & 🚶 http://127.0.0.1:8000/items/foo, 👆 🔜 👀 📨: - -```JSON -{"item_id":"foo"} -``` - -## ➡ 🔢 ⏮️ 🆎 - -👆 💪 📣 🆎 ➡ 🔢 🔢, ⚙️ 🐩 🐍 🆎 ✍: - -{* ../../docs_src/path_params/tutorial002.py hl[7] *} - -👉 💼, `item_id` 📣 `int`. - -/// check - -👉 🔜 🤝 👆 👨‍🎨 🐕‍🦺 🔘 👆 🔢, ⏮️ ❌ ✅, 🛠️, ♒️. - -/// - -## 💽 🛠️ - -🚥 👆 🏃 👉 🖼 & 📂 👆 🖥 http://127.0.0.1:8000/items/3, 👆 🔜 👀 📨: - -```JSON -{"item_id":3} -``` - -/// check - -👀 👈 💲 👆 🔢 📨 (& 📨) `3`, 🐍 `int`, 🚫 🎻 `"3"`. - -, ⏮️ 👈 🆎 📄, **FastAPI** 🤝 👆 🏧 📨 "✍". - -/// - -## 💽 🔬 - -✋️ 🚥 👆 🚶 🖥 http://127.0.0.1:8000/items/foo, 👆 🔜 👀 👌 🇺🇸🔍 ❌: - -```JSON -{ - "detail": [ - { - "loc": [ - "path", - "item_id" - ], - "msg": "value is not a valid integer", - "type": "type_error.integer" - } - ] -} -``` - -↩️ ➡ 🔢 `item_id` ✔️ 💲 `"foo"`, ❔ 🚫 `int`. - -🎏 ❌ 🔜 😑 🚥 👆 🚚 `float` ↩️ `int`,: http://127.0.0.1:8000/items/4.2 - -/// check - -, ⏮️ 🎏 🐍 🆎 📄, **FastAPI** 🤝 👆 💽 🔬. - -👀 👈 ❌ 🎯 🇵🇸 ⚫️❔ ☝ 🌐❔ 🔬 🚫 🚶‍♀️. - -👉 🙃 👍 ⏪ 🛠️ & 🛠️ 📟 👈 🔗 ⏮️ 👆 🛠️. - -/// - -## 🧾 - -& 🕐❔ 👆 📂 👆 🖥 http://127.0.0.1:8000/docs, 👆 🔜 👀 🏧, 🎓, 🛠️ 🧾 💖: - - - -/// check - -🔄, ⏮️ 👈 🎏 🐍 🆎 📄, **FastAPI** 🤝 👆 🏧, 🎓 🧾 (🛠️ 🦁 🎚). - -👀 👈 ➡ 🔢 📣 🔢. - -/// - -## 🐩-⚓️ 💰, 🎛 🧾 - -& ↩️ 🏗 🔗 ⚪️➡️ 🗄 🐩, 📤 📚 🔗 🧰. - -↩️ 👉, **FastAPI** ⚫️ 🚚 🎛 🛠️ 🧾 (⚙️ 📄), ❔ 👆 💪 🔐 http://127.0.0.1:8000/redoc: - - - -🎏 🌌, 📤 📚 🔗 🧰. ✅ 📟 ⚡ 🧰 📚 🇪🇸. - -## Pydantic - -🌐 💽 🔬 🎭 🔽 🚘 Pydantic, 👆 🤚 🌐 💰 ⚪️➡️ ⚫️. & 👆 💭 👆 👍 ✋. - -👆 💪 ⚙️ 🎏 🆎 📄 ⏮️ `str`, `float`, `bool` & 📚 🎏 🏗 📊 🆎. - -📚 👫 🔬 ⏭ 📃 🔰. - -## ✔ 🤔 - -🕐❔ 🏗 *➡ 🛠️*, 👆 💪 🔎 ⚠ 🌐❔ 👆 ✔️ 🔧 ➡. - -💖 `/users/me`, ➡️ 💬 👈 ⚫️ 🤚 📊 🔃 ⏮️ 👩‍💻. - -& ⤴️ 👆 💪 ✔️ ➡ `/users/{user_id}` 🤚 💽 🔃 🎯 👩‍💻 👩‍💻 🆔. - -↩️ *➡ 🛠️* 🔬 ✔, 👆 💪 ⚒ 💭 👈 ➡ `/users/me` 📣 ⏭ 1️⃣ `/users/{user_id}`: - -{* ../../docs_src/path_params/tutorial003.py hl[6,11] *} - -⏪, ➡ `/users/{user_id}` 🔜 🏏 `/users/me`, "💭" 👈 ⚫️ 📨 🔢 `user_id` ⏮️ 💲 `"me"`. - -➡, 👆 🚫🔜 ↔ ➡ 🛠️: - -{* ../../docs_src/path_params/tutorial003b.py hl[6,11] *} - -🥇 🕐 🔜 🕧 ⚙️ ↩️ ➡ 🏏 🥇. - -## 🔁 💲 - -🚥 👆 ✔️ *➡ 🛠️* 👈 📨 *➡ 🔢*, ✋️ 👆 💚 💪 ☑ *➡ 🔢* 💲 🔁, 👆 💪 ⚙️ 🐩 🐍 `Enum`. - -### ✍ `Enum` 🎓 - -🗄 `Enum` & ✍ 🎧-🎓 👈 😖 ⚪️➡️ `str` & ⚪️➡️ `Enum`. - -😖 ⚪️➡️ `str` 🛠️ 🩺 🔜 💪 💭 👈 💲 🔜 🆎 `string` & 🔜 💪 ✍ ☑. - -⤴️ ✍ 🎓 🔢 ⏮️ 🔧 💲, ❔ 🔜 💪 ☑ 💲: - -{* ../../docs_src/path_params/tutorial005.py hl[1,6:9] *} - -/// info - -🔢 (⚖️ 🔢) 💪 🐍 ↩️ ⏬ 3️⃣.4️⃣. - -/// - -/// tip - -🚥 👆 💭, "📊", "🎓", & "🍏" 📛 🎰 🏫 🏷. - -/// - -### 📣 *➡ 🔢* - -⤴️ ✍ *➡ 🔢* ⏮️ 🆎 ✍ ⚙️ 🔢 🎓 👆 ✍ (`ModelName`): - -{* ../../docs_src/path_params/tutorial005.py hl[16] *} - -### ✅ 🩺 - -↩️ 💪 💲 *➡ 🔢* 🔢, 🎓 🩺 💪 🎦 👫 🎆: - - - -### 👷 ⏮️ 🐍 *🔢* - -💲 *➡ 🔢* 🔜 *🔢 👨‍🎓*. - -#### 🔬 *🔢 👨‍🎓* - -👆 💪 🔬 ⚫️ ⏮️ *🔢 👨‍🎓* 👆 ✍ 🔢 `ModelName`: - -{* ../../docs_src/path_params/tutorial005.py hl[17] *} - -#### 🤚 *🔢 💲* - -👆 💪 🤚 ☑ 💲 ( `str` 👉 💼) ⚙️ `model_name.value`, ⚖️ 🏢, `your_enum_member.value`: - -{* ../../docs_src/path_params/tutorial005.py hl[20] *} - -/// tip - -👆 💪 🔐 💲 `"lenet"` ⏮️ `ModelName.lenet.value`. - -/// - -#### 📨 *🔢 👨‍🎓* - -👆 💪 📨 *🔢 👨‍🎓* ⚪️➡️ 👆 *➡ 🛠️*, 🐦 🎻 💪 (✅ `dict`). - -👫 🔜 🗜 👫 🔗 💲 (🎻 👉 💼) ⏭ 🛬 👫 👩‍💻: - -{* ../../docs_src/path_params/tutorial005.py hl[18,21,23] *} - -👆 👩‍💻 👆 🔜 🤚 🎻 📨 💖: - -```JSON -{ - "model_name": "alexnet", - "message": "Deep Learning FTW!" -} -``` - -## ➡ 🔢 ⚗ ➡ - -➡️ 💬 👆 ✔️ *➡ 🛠️* ⏮️ ➡ `/files/{file_path}`. - -✋️ 👆 💪 `file_path` ⚫️ 🔌 *➡*, 💖 `home/johndoe/myfile.txt`. - -, 📛 👈 📁 🔜 🕳 💖: `/files/home/johndoe/myfile.txt`. - -### 🗄 🐕‍🦺 - -🗄 🚫 🐕‍🦺 🌌 📣 *➡ 🔢* 🔌 *➡* 🔘, 👈 💪 ↘️ 😐 👈 ⚠ 💯 & 🔬. - -👐, 👆 💪 ⚫️ **FastAPI**, ⚙️ 1️⃣ 🔗 🧰 ⚪️➡️ 💃. - -& 🩺 🔜 👷, 👐 🚫 ❎ 🙆 🧾 💬 👈 🔢 🔜 🔌 ➡. - -### ➡ 🔌 - -⚙️ 🎛 🔗 ⚪️➡️ 💃 👆 💪 📣 *➡ 🔢* ⚗ *➡* ⚙️ 📛 💖: - -``` -/files/{file_path:path} -``` - -👉 💼, 📛 🔢 `file_path`, & 🏁 🍕, `:path`, 💬 ⚫️ 👈 🔢 🔜 🏏 🙆 *➡*. - -, 👆 💪 ⚙️ ⚫️ ⏮️: - -{* ../../docs_src/path_params/tutorial004.py hl[6] *} - -/// tip - -👆 💪 💪 🔢 🔌 `/home/johndoe/myfile.txt`, ⏮️ 🏁 🔪 (`/`). - -👈 💼, 📛 🔜: `/files//home/johndoe/myfile.txt`, ⏮️ 2️⃣✖️ 🔪 (`//`) 🖖 `files` & `home`. - -/// - -## 🌃 - -⏮️ **FastAPI**, ⚙️ 📏, 🏋️ & 🐩 🐍 🆎 📄, 👆 🤚: - -* 👨‍🎨 🐕‍🦺: ❌ ✅, ✍, ♒️. -* 💽 "" -* 💽 🔬 -* 🛠️ ✍ & 🏧 🧾 - -& 👆 🕴 ✔️ 📣 👫 🕐. - -👈 🎲 👑 ⭐ 📈 **FastAPI** 🔬 🎛 🛠️ (↖️ ⚪️➡️ 🍣 🎭). diff --git a/docs/em/docs/tutorial/query-params-str-validations.md b/docs/em/docs/tutorial/query-params-str-validations.md deleted file mode 100644 index fd077bf8f7..0000000000 --- a/docs/em/docs/tutorial/query-params-str-validations.md +++ /dev/null @@ -1,320 +0,0 @@ -# 🔢 🔢 & 🎻 🔬 - -**FastAPI** ✔ 👆 📣 🌖 ℹ & 🔬 👆 🔢. - -➡️ ✊ 👉 🈸 🖼: - -{* ../../docs_src/query_params_str_validations/tutorial001.py hl[9] *} - -🔢 🔢 `q` 🆎 `Union[str, None]` (⚖️ `str | None` 🐍 3️⃣.1️⃣0️⃣), 👈 ⛓ 👈 ⚫️ 🆎 `str` ✋️ 💪 `None`, & 👐, 🔢 💲 `None`, FastAPI 🔜 💭 ⚫️ 🚫 ✔. - -/// note - -FastAPI 🔜 💭 👈 💲 `q` 🚫 ✔ ↩️ 🔢 💲 `= None`. - - `Union` `Union[str, None]` 🔜 ✔ 👆 👨‍🎨 🤝 👆 👍 🐕‍🦺 & 🔍 ❌. - -/// - -## 🌖 🔬 - -👥 🔜 🛠️ 👈 ✋️ `q` 📦, 🕐❔ ⚫️ 🚚, **🚮 📐 🚫 📉 5️⃣0️⃣ 🦹**. - -### 🗄 `Query` - -🏆 👈, 🥇 🗄 `Query` ⚪️➡️ `fastapi`: - -{* ../../docs_src/query_params_str_validations/tutorial002.py hl[3] *} - -## ⚙️ `Query` 🔢 💲 - -& 🔜 ⚙️ ⚫️ 🔢 💲 👆 🔢, ⚒ 🔢 `max_length` 5️⃣0️⃣: - -{* ../../docs_src/query_params_str_validations/tutorial002.py hl[9] *} - -👥 ✔️ ❎ 🔢 💲 `None` 🔢 ⏮️ `Query()`, 👥 💪 🔜 ⚒ 🔢 💲 ⏮️ 🔢 `Query(default=None)`, ⚫️ 🍦 🎏 🎯 ⚖ 👈 🔢 💲. - -: - -```Python -q: Union[str, None] = Query(default=None) -``` - -...⚒ 🔢 📦, 🎏: - -```Python -q: Union[str, None] = None -``` - -& 🐍 3️⃣.1️⃣0️⃣ & 🔛: - -```Python -q: str | None = Query(default=None) -``` - -...⚒ 🔢 📦, 🎏: - -```Python -q: str | None = None -``` - -✋️ ⚫️ 📣 ⚫️ 🎯 💆‍♂ 🔢 🔢. - -/// info - -✔️ 🤯 👈 🌅 ⚠ 🍕 ⚒ 🔢 📦 🍕: - -```Python -= None -``` - -⚖️: - -```Python -= Query(default=None) -``` - -⚫️ 🔜 ⚙️ 👈 `None` 🔢 💲, & 👈 🌌 ⚒ 🔢 **🚫 ✔**. - - `Union[str, None]` 🍕 ✔ 👆 👨‍🎨 🚚 👻 🐕‍🦺, ✋️ ⚫️ 🚫 ⚫️❔ 💬 FastAPI 👈 👉 🔢 🚫 ✔. - -/// - -⤴️, 👥 💪 🚶‍♀️ 🌅 🔢 `Query`. 👉 💼, `max_length` 🔢 👈 ✔ 🎻: - -```Python -q: Union[str, None] = Query(default=None, max_length=50) -``` - -👉 🔜 ✔ 📊, 🎦 🆑 ❌ 🕐❔ 📊 🚫 ☑, & 📄 🔢 🗄 🔗 *➡ 🛠️*. - -## 🚮 🌅 🔬 - -👆 💪 🚮 🔢 `min_length`: - -{* ../../docs_src/query_params_str_validations/tutorial003.py hl[10] *} - -## 🚮 🥔 🧬 - -👆 💪 🔬 🥔 🧬 👈 🔢 🔜 🏏: - -{* ../../docs_src/query_params_str_validations/tutorial004.py hl[11] *} - -👉 🎯 🥔 🧬 ✅ 👈 📨 🔢 💲: - -* `^`: ▶️ ⏮️ 📄 🦹, 🚫 ✔️ 🦹 ⏭. -* `fixedquery`: ✔️ ☑ 💲 `fixedquery`. -* `$`: 🔚 📤, 🚫 ✔️ 🙆 🌖 🦹 ⏮️ `fixedquery`. - -🚥 👆 💭 💸 ⏮️ 🌐 👉 **"🥔 🧬"** 💭, 🚫 😟. 👫 🏋️ ❔ 📚 👫👫. 👆 💪 📚 💩 🍵 💆‍♂ 🥔 🧬. - -✋️ 🕐❔ 👆 💪 👫 & 🚶 & 💡 👫, 💭 👈 👆 💪 ⏪ ⚙️ 👫 🔗 **FastAPI**. - -## 🔢 💲 - -🎏 🌌 👈 👆 💪 🚶‍♀️ `None` 💲 `default` 🔢, 👆 💪 🚶‍♀️ 🎏 💲. - -➡️ 💬 👈 👆 💚 📣 `q` 🔢 🔢 ✔️ `min_length` `3`, & ✔️ 🔢 💲 `"fixedquery"`: - -{* ../../docs_src/query_params_str_validations/tutorial005.py hl[7] *} - -/// note - -✔️ 🔢 💲 ⚒ 🔢 📦. - -/// - -## ⚒ ⚫️ ✔ - -🕐❔ 👥 🚫 💪 📣 🌅 🔬 ⚖️ 🗃, 👥 💪 ⚒ `q` 🔢 🔢 ✔ 🚫 📣 🔢 💲, 💖: - -```Python -q: str -``` - -↩️: - -```Python -q: Union[str, None] = None -``` - -✋️ 👥 🔜 📣 ⚫️ ⏮️ `Query`, 🖼 💖: - -```Python -q: Union[str, None] = Query(default=None, min_length=3) -``` - -, 🕐❔ 👆 💪 📣 💲 ✔ ⏪ ⚙️ `Query`, 👆 💪 🎯 🚫 📣 🔢 💲: - -{* ../../docs_src/query_params_str_validations/tutorial006.py hl[7] *} - -### ✔ ⏮️ `None` - -👆 💪 📣 👈 🔢 💪 🚫 `None`, ✋️ 👈 ⚫️ ✔. 👉 🔜 ⚡ 👩‍💻 📨 💲, 🚥 💲 `None`. - -👈, 👆 💪 📣 👈 `None` ☑ 🆎 ✋️ ⚙️ `default=...`: - -{* ../../docs_src/query_params_str_validations/tutorial006c.py hl[9] *} - -/// tip - -Pydantic, ❔ ⚫️❔ 🏋️ 🌐 💽 🔬 & 🛠️ FastAPI, ✔️ 🎁 🎭 🕐❔ 👆 ⚙️ `Optional` ⚖️ `Union[Something, None]` 🍵 🔢 💲, 👆 💪 ✍ 🌅 🔃 ⚫️ Pydantic 🩺 🔃 ✔ 📦 🏑. - -/// - -## 🔢 🔢 📇 / 💗 💲 - -🕐❔ 👆 🔬 🔢 🔢 🎯 ⏮️ `Query` 👆 💪 📣 ⚫️ 📨 📇 💲, ⚖️ 🙆‍♀ 🎏 🌌, 📨 💗 💲. - -🖼, 📣 🔢 🔢 `q` 👈 💪 😑 💗 🕰 📛, 👆 💪 ✍: - -{* ../../docs_src/query_params_str_validations/tutorial011.py hl[9] *} - -⤴️, ⏮️ 📛 💖: - -``` -http://localhost:8000/items/?q=foo&q=bar -``` - -👆 🔜 📨 💗 `q` *🔢 🔢'* 💲 (`foo` & `bar`) 🐍 `list` 🔘 👆 *➡ 🛠️ 🔢*, *🔢 🔢* `q`. - -, 📨 👈 📛 🔜: - -```JSON -{ - "q": [ - "foo", - "bar" - ] -} -``` - -/// tip - -📣 🔢 🔢 ⏮️ 🆎 `list`, 💖 🖼 🔛, 👆 💪 🎯 ⚙️ `Query`, ⏪ ⚫️ 🔜 🔬 📨 💪. - -/// - -🎓 🛠️ 🩺 🔜 ℹ ➡️, ✔ 💗 💲: - - - -### 🔢 🔢 📇 / 💗 💲 ⏮️ 🔢 - -& 👆 💪 🔬 🔢 `list` 💲 🚥 👌 🚚: - -{* ../../docs_src/query_params_str_validations/tutorial012.py hl[9] *} - -🚥 👆 🚶: - -``` -http://localhost:8000/items/ -``` - -🔢 `q` 🔜: `["foo", "bar"]` & 👆 📨 🔜: - -```JSON -{ - "q": [ - "foo", - "bar" - ] -} -``` - -#### ⚙️ `list` - -👆 💪 ⚙️ `list` 🔗 ↩️ `List[str]` (⚖️ `list[str]` 🐍 3️⃣.9️⃣ ➕): - -{* ../../docs_src/query_params_str_validations/tutorial013.py hl[7] *} - -/// note - -✔️ 🤯 👈 👉 💼, FastAPI 🏆 🚫 ✅ 🎚 📇. - -🖼, `List[int]` 🔜 ✅ (& 📄) 👈 🎚 📇 🔢. ✋️ `list` 😞 🚫🔜. - -/// - -## 📣 🌅 🗃 - -👆 💪 🚮 🌅 ℹ 🔃 🔢. - -👈 ℹ 🔜 🔌 🏗 🗄 & ⚙️ 🧾 👩‍💻 🔢 & 🔢 🧰. - -/// note - -✔️ 🤯 👈 🎏 🧰 5️⃣📆 ✔️ 🎏 🎚 🗄 🐕‍🦺. - -👫 💪 🚫 🎦 🌐 ➕ ℹ 📣, 👐 🌅 💼, ❌ ⚒ ⏪ 📄 🛠️. - -/// - -👆 💪 🚮 `title`: - -{* ../../docs_src/query_params_str_validations/tutorial007.py hl[10] *} - -& `description`: - -{* ../../docs_src/query_params_str_validations/tutorial008.py hl[13] *} - -## 📛 🔢 - -🌈 👈 👆 💚 🔢 `item-query`. - -💖: - -``` -http://127.0.0.1:8000/items/?item-query=foobaritems -``` - -✋️ `item-query` 🚫 ☑ 🐍 🔢 📛. - -🔐 🔜 `item_query`. - -✋️ 👆 💪 ⚫️ ⚫️❔ `item-query`... - -⤴️ 👆 💪 📣 `alias`, & 👈 📛 ⚫️❔ 🔜 ⚙️ 🔎 🔢 💲: - -{* ../../docs_src/query_params_str_validations/tutorial009.py hl[9] *} - -## 😛 🔢 - -🔜 ➡️ 💬 👆 🚫 💖 👉 🔢 🚫🔜. - -👆 ✔️ 👈 ⚫️ 📤 ⏪ ↩️ 📤 👩‍💻 ⚙️ ⚫️, ✋️ 👆 💚 🩺 🎯 🎦 ⚫️ 😢. - -⤴️ 🚶‍♀️ 🔢 `deprecated=True` `Query`: - -{* ../../docs_src/query_params_str_validations/tutorial010.py hl[18] *} - -🩺 🔜 🎦 ⚫️ 💖 👉: - - - -## 🚫 ⚪️➡️ 🗄 - -🚫 🔢 🔢 ⚪️➡️ 🏗 🗄 🔗 (& ➡️, ⚪️➡️ 🏧 🧾 ⚙️), ⚒ 🔢 `include_in_schema` `Query` `False`: - -{* ../../docs_src/query_params_str_validations/tutorial014.py hl[10] *} - -## 🌃 - -👆 💪 📣 🌖 🔬 & 🗃 👆 🔢. - -💊 🔬 & 🗃: - -* `alias` -* `title` -* `description` -* `deprecated` - -🔬 🎯 🎻: - -* `min_length` -* `max_length` -* `regex` - -👫 🖼 👆 👀 ❔ 📣 🔬 `str` 💲. - -👀 ⏭ 📃 👀 ❔ 📣 🔬 🎏 🆎, 💖 🔢. diff --git a/docs/em/docs/tutorial/query-params.md b/docs/em/docs/tutorial/query-params.md deleted file mode 100644 index 5c8d868a9a..0000000000 --- a/docs/em/docs/tutorial/query-params.md +++ /dev/null @@ -1,187 +0,0 @@ -# 🔢 🔢 - -🕐❔ 👆 📣 🎏 🔢 🔢 👈 🚫 🍕 ➡ 🔢, 👫 🔁 🔬 "🔢" 🔢. - -{* ../../docs_src/query_params/tutorial001.py hl[9] *} - -🔢 ⚒ 🔑-💲 👫 👈 🚶 ⏮️ `?` 📛, 🎏 `&` 🦹. - -🖼, 📛: - -``` -http://127.0.0.1:8000/items/?skip=0&limit=10 -``` - -...🔢 🔢: - -* `skip`: ⏮️ 💲 `0` -* `limit`: ⏮️ 💲 `10` - -👫 🍕 📛, 👫 "🛎" 🎻. - -✋️ 🕐❔ 👆 📣 👫 ⏮️ 🐍 🆎 (🖼 🔛, `int`), 👫 🗜 👈 🆎 & ✔ 🛡 ⚫️. - -🌐 🎏 🛠️ 👈 ⚖ ➡ 🔢 ✔ 🔢 🔢: - -* 👨‍🎨 🐕‍🦺 (🎲) -* 💽 "✍" -* 💽 🔬 -* 🏧 🧾 - -## 🔢 - -🔢 🔢 🚫 🔧 🍕 ➡, 👫 💪 📦 & 💪 ✔️ 🔢 💲. - -🖼 🔛 👫 ✔️ 🔢 💲 `skip=0` & `limit=10`. - -, 🔜 📛: - -``` -http://127.0.0.1:8000/items/ -``` - -🔜 🎏 🔜: - -``` -http://127.0.0.1:8000/items/?skip=0&limit=10 -``` - -✋️ 🚥 👆 🚶, 🖼: - -``` -http://127.0.0.1:8000/items/?skip=20 -``` - -🔢 💲 👆 🔢 🔜: - -* `skip=20`: ↩️ 👆 ⚒ ⚫️ 📛 -* `limit=10`: ↩️ 👈 🔢 💲 - -## 📦 🔢 - -🎏 🌌, 👆 💪 📣 📦 🔢 🔢, ⚒ 👫 🔢 `None`: - -{* ../../docs_src/query_params/tutorial002.py hl[9] *} - -👉 💼, 🔢 🔢 `q` 🔜 📦, & 🔜 `None` 🔢. - -/// check - -👀 👈 **FastAPI** 🙃 🥃 👀 👈 ➡ 🔢 `item_id` ➡ 🔢 & `q` 🚫,, ⚫️ 🔢 🔢. - -/// - -## 🔢 🔢 🆎 🛠️ - -👆 💪 📣 `bool` 🆎, & 👫 🔜 🗜: - -{* ../../docs_src/query_params/tutorial003.py hl[9] *} - -👉 💼, 🚥 👆 🚶: - -``` -http://127.0.0.1:8000/items/foo?short=1 -``` - -⚖️ - -``` -http://127.0.0.1:8000/items/foo?short=True -``` - -⚖️ - -``` -http://127.0.0.1:8000/items/foo?short=true -``` - -⚖️ - -``` -http://127.0.0.1:8000/items/foo?short=on -``` - -⚖️ - -``` -http://127.0.0.1:8000/items/foo?short=yes -``` - -⚖️ 🙆 🎏 💼 📈 (🔠, 🥇 🔤 🔠, ♒️), 👆 🔢 🔜 👀 🔢 `short` ⏮️ `bool` 💲 `True`. ⏪ `False`. - - -## 💗 ➡ & 🔢 🔢 - -👆 💪 📣 💗 ➡ 🔢 & 🔢 🔢 🎏 🕰, **FastAPI** 💭 ❔ ❔. - -& 👆 🚫 ✔️ 📣 👫 🙆 🎯 ✔. - -👫 🔜 🔬 📛: - -{* ../../docs_src/query_params/tutorial004.py hl[8,10] *} - -## ✔ 🔢 🔢 - -🕐❔ 👆 📣 🔢 💲 🚫-➡ 🔢 (🔜, 👥 ✔️ 🕴 👀 🔢 🔢), ⤴️ ⚫️ 🚫 ✔. - -🚥 👆 🚫 💚 🚮 🎯 💲 ✋️ ⚒ ⚫️ 📦, ⚒ 🔢 `None`. - -✋️ 🕐❔ 👆 💚 ⚒ 🔢 🔢 ✔, 👆 💪 🚫 📣 🙆 🔢 💲: - -{* ../../docs_src/query_params/tutorial005.py hl[6:7] *} - -📥 🔢 🔢 `needy` ✔ 🔢 🔢 🆎 `str`. - -🚥 👆 📂 👆 🖥 📛 💖: - -``` -http://127.0.0.1:8000/items/foo-item -``` - -...🍵 ❎ ✔ 🔢 `needy`, 👆 🔜 👀 ❌ 💖: - -```JSON -{ - "detail": [ - { - "loc": [ - "query", - "needy" - ], - "msg": "field required", - "type": "value_error.missing" - } - ] -} -``` - -`needy` 🚚 🔢, 👆 🔜 💪 ⚒ ⚫️ 📛: - -``` -http://127.0.0.1:8000/items/foo-item?needy=sooooneedy -``` - -...👉 🔜 👷: - -```JSON -{ - "item_id": "foo-item", - "needy": "sooooneedy" -} -``` - -& ↗️, 👆 💪 🔬 🔢 ✔, ✔️ 🔢 💲, & 🍕 📦: - -{* ../../docs_src/query_params/tutorial006.py hl[10] *} - -👉 💼, 📤 3️⃣ 🔢 🔢: - -* `needy`, ✔ `str`. -* `skip`, `int` ⏮️ 🔢 💲 `0`. -* `limit`, 📦 `int`. - -/// tip - -👆 💪 ⚙️ `Enum`Ⓜ 🎏 🌌 ⏮️ [➡ 🔢](path-params.md#_7){.internal-link target=_blank}. - -/// diff --git a/docs/em/docs/tutorial/request-files.md b/docs/em/docs/tutorial/request-files.md deleted file mode 100644 index c3bdeafd48..0000000000 --- a/docs/em/docs/tutorial/request-files.md +++ /dev/null @@ -1,172 +0,0 @@ -# 📨 📁 - -👆 💪 🔬 📁 📂 👩‍💻 ⚙️ `File`. - -/// info - -📨 📂 📁, 🥇 ❎ `python-multipart`. - -🤶 Ⓜ. `pip install python-multipart`. - -👉 ↩️ 📂 📁 📨 "📨 💽". - -/// - -## 🗄 `File` - -🗄 `File` & `UploadFile` ⚪️➡️ `fastapi`: - -{* ../../docs_src/request_files/tutorial001.py hl[1] *} - -## 🔬 `File` 🔢 - -✍ 📁 🔢 🎏 🌌 👆 🔜 `Body` ⚖️ `Form`: - -{* ../../docs_src/request_files/tutorial001.py hl[7] *} - -/// info - -`File` 🎓 👈 😖 🔗 ⚪️➡️ `Form`. - -✋️ 💭 👈 🕐❔ 👆 🗄 `Query`, `Path`, `File` & 🎏 ⚪️➡️ `fastapi`, 👈 🤙 🔢 👈 📨 🎁 🎓. - -/// - -/// tip - -📣 📁 💪, 👆 💪 ⚙️ `File`, ↩️ ⏪ 🔢 🔜 🔬 🔢 🔢 ⚖️ 💪 (🎻) 🔢. - -/// - -📁 🔜 📂 "📨 💽". - -🚥 👆 📣 🆎 👆 *➡ 🛠️ 🔢* 🔢 `bytes`, **FastAPI** 🔜 ✍ 📁 👆 & 👆 🔜 📨 🎚 `bytes`. - -✔️ 🤯 👈 👉 ⛓ 👈 🎂 🎚 🔜 🏪 💾. 👉 🔜 👷 👍 🤪 📁. - -✋️ 📤 📚 💼 ❔ 👆 💪 💰 ⚪️➡️ ⚙️ `UploadFile`. - -## 📁 🔢 ⏮️ `UploadFile` - -🔬 📁 🔢 ⏮️ 🆎 `UploadFile`: - -{* ../../docs_src/request_files/tutorial001.py hl[12] *} - -⚙️ `UploadFile` ✔️ 📚 📈 🤭 `bytes`: - -* 👆 🚫 ✔️ ⚙️ `File()` 🔢 💲 🔢. -* ⚫️ ⚙️ "🧵" 📁: - * 📁 🏪 💾 🆙 🔆 📐 📉, & ⏮️ 🚶‍♀️ 👉 📉 ⚫️ 🔜 🏪 💾. -* 👉 ⛓ 👈 ⚫️ 🔜 👷 👍 ⭕ 📁 💖 🖼, 📹, ⭕ 💱, ♒️. 🍵 😩 🌐 💾. -* 👆 💪 🤚 🗃 ⚪️➡️ 📂 📁. -* ⚫️ ✔️ 📁-💖 `async` 🔢. -* ⚫️ 🎦 ☑ 🐍 `SpooledTemporaryFile` 🎚 👈 👆 💪 🚶‍♀️ 🔗 🎏 🗃 👈 ⌛ 📁-💖 🎚. - -### `UploadFile` - -`UploadFile` ✔️ 📄 🔢: - -* `filename`: `str` ⏮️ ⏮️ 📁 📛 👈 📂 (✅ `myimage.jpg`). -* `content_type`: `str` ⏮️ 🎚 🆎 (📁 🆎 / 📻 🆎) (✅ `image/jpeg`). -* `file`: `SpooledTemporaryFile` ( 📁-💖 🎚). 👉 ☑ 🐍 📁 👈 👆 💪 🚶‍♀️ 🔗 🎏 🔢 ⚖️ 🗃 👈 ⌛ "📁-💖" 🎚. - -`UploadFile` ✔️ 📄 `async` 👩‍🔬. 👫 🌐 🤙 🔗 📁 👩‍🔬 🔘 (⚙️ 🔗 `SpooledTemporaryFile`). - -* `write(data)`: ✍ `data` (`str` ⚖️ `bytes`) 📁. -* `read(size)`: ✍ `size` (`int`) 🔢/🦹 📁. -* `seek(offset)`: 🚶 🔢 🧘 `offset` (`int`) 📁. - * 🤶 Ⓜ., `await myfile.seek(0)` 🔜 🚶 ▶️ 📁. - * 👉 ✴️ ⚠ 🚥 👆 🏃 `await myfile.read()` 🕐 & ⤴️ 💪 ✍ 🎚 🔄. -* `close()`: 🔐 📁. - -🌐 👫 👩‍🔬 `async` 👩‍🔬, 👆 💪 "⌛" 👫. - -🖼, 🔘 `async` *➡ 🛠️ 🔢* 👆 💪 🤚 🎚 ⏮️: - -```Python -contents = await myfile.read() -``` - -🚥 👆 🔘 😐 `def` *➡ 🛠️ 🔢*, 👆 💪 🔐 `UploadFile.file` 🔗, 🖼: - -```Python -contents = myfile.file.read() -``` - -/// note | `async` 📡 ℹ - -🕐❔ 👆 ⚙️ `async` 👩‍🔬, **FastAPI** 🏃 📁 👩‍🔬 🧵 & ⌛ 👫. - -/// - -/// note | 💃 📡 ℹ - -**FastAPI**'Ⓜ `UploadFile` 😖 🔗 ⚪️➡️ **💃**'Ⓜ `UploadFile`, ✋️ 🚮 💪 🍕 ⚒ ⚫️ 🔗 ⏮️ **Pydantic** & 🎏 🍕 FastAPI. - -/// - -## ⚫️❔ "📨 💽" - -🌌 🕸 📨 (`
`) 📨 💽 💽 🛎 ⚙️ "🎁" 🔢 👈 📊, ⚫️ 🎏 ⚪️➡️ 🎻. - -**FastAPI** 🔜 ⚒ 💭 ✍ 👈 📊 ⚪️➡️ ▶️️ 🥉 ↩️ 🎻. - -/// note | 📡 ℹ - -📊 ⚪️➡️ 📨 🛎 🗜 ⚙️ "📻 🆎" `application/x-www-form-urlencoded` 🕐❔ ⚫️ 🚫 🔌 📁. - -✋️ 🕐❔ 📨 🔌 📁, ⚫️ 🗜 `multipart/form-data`. 🚥 👆 ⚙️ `File`, **FastAPI** 🔜 💭 ⚫️ ✔️ 🤚 📁 ⚪️➡️ ☑ 🍕 💪. - -🚥 👆 💚 ✍ 🌖 🔃 👉 🔢 & 📨 🏑, 👳 🏇 🕸 🩺 POST. - -/// - -/// warning - -👆 💪 📣 💗 `File` & `Form` 🔢 *➡ 🛠️*, ✋️ 👆 💪 🚫 📣 `Body` 🏑 👈 👆 ⌛ 📨 🎻, 📨 🔜 ✔️ 💪 🗜 ⚙️ `multipart/form-data` ↩️ `application/json`. - -👉 🚫 🚫 **FastAPI**, ⚫️ 🍕 🇺🇸🔍 🛠️. - -/// - -## 📦 📁 📂 - -👆 💪 ⚒ 📁 📦 ⚙️ 🐩 🆎 ✍ & ⚒ 🔢 💲 `None`: - -{* ../../docs_src/request_files/tutorial001_02.py hl[9,17] *} - -## `UploadFile` ⏮️ 🌖 🗃 - -👆 💪 ⚙️ `File()` ⏮️ `UploadFile`, 🖼, ⚒ 🌖 🗃: - -{* ../../docs_src/request_files/tutorial001_03.py hl[13] *} - -## 💗 📁 📂 - -⚫️ 💪 📂 📚 📁 🎏 🕰. - -👫 🔜 👨‍💼 🎏 "📨 🏑" 📨 ⚙️ "📨 💽". - -⚙️ 👈, 📣 📇 `bytes` ⚖️ `UploadFile`: - -{* ../../docs_src/request_files/tutorial002.py hl[10,15] *} - -👆 🔜 📨, 📣, `list` `bytes` ⚖️ `UploadFile`Ⓜ. - -/// note | 📡 ℹ - -👆 💪 ⚙️ `from starlette.responses import HTMLResponse`. - -**FastAPI** 🚚 🎏 `starlette.responses` `fastapi.responses` 🏪 👆, 👩‍💻. ✋️ 🌅 💪 📨 👟 🔗 ⚪️➡️ 💃. - -/// - -### 💗 📁 📂 ⏮️ 🌖 🗃 - -& 🎏 🌌 ⏭, 👆 💪 ⚙️ `File()` ⚒ 🌖 🔢, `UploadFile`: - -{* ../../docs_src/request_files/tutorial003.py hl[18] *} - -## 🌃 - -⚙️ `File`, `bytes`, & `UploadFile` 📣 📁 📂 📨, 📨 📨 💽. diff --git a/docs/em/docs/tutorial/request-forms-and-files.md b/docs/em/docs/tutorial/request-forms-and-files.md deleted file mode 100644 index 680b1a96a5..0000000000 --- a/docs/em/docs/tutorial/request-forms-and-files.md +++ /dev/null @@ -1,37 +0,0 @@ -# 📨 📨 & 📁 - -👆 💪 🔬 📁 & 📨 🏑 🎏 🕰 ⚙️ `File` & `Form`. - -/// info - -📨 📂 📁 & /⚖️ 📨 📊, 🥇 ❎ `python-multipart`. - -🤶 Ⓜ. `pip install python-multipart`. - -/// - -## 🗄 `File` & `Form` - -{* ../../docs_src/request_forms_and_files/tutorial001.py hl[1] *} - -## 🔬 `File` & `Form` 🔢 - -✍ 📁 & 📨 🔢 🎏 🌌 👆 🔜 `Body` ⚖️ `Query`: - -{* ../../docs_src/request_forms_and_files/tutorial001.py hl[8] *} - -📁 & 📨 🏑 🔜 📂 📨 📊 & 👆 🔜 📨 📁 & 📨 🏑. - -& 👆 💪 📣 📁 `bytes` & `UploadFile`. - -/// warning - -👆 💪 📣 💗 `File` & `Form` 🔢 *➡ 🛠️*, ✋️ 👆 💪 🚫 📣 `Body` 🏑 👈 👆 ⌛ 📨 🎻, 📨 🔜 ✔️ 💪 🗜 ⚙️ `multipart/form-data` ↩️ `application/json`. - -👉 🚫 🚫 **FastAPI**, ⚫️ 🍕 🇺🇸🔍 🛠️. - -/// - -## 🌃 - -⚙️ `File` & `Form` 👯‍♂️ 🕐❔ 👆 💪 📨 💽 & 📁 🎏 📨. diff --git a/docs/em/docs/tutorial/request-forms.md b/docs/em/docs/tutorial/request-forms.md deleted file mode 100644 index 1cc1ea5dcb..0000000000 --- a/docs/em/docs/tutorial/request-forms.md +++ /dev/null @@ -1,69 +0,0 @@ -# 📨 💽 - -🕐❔ 👆 💪 📨 📨 🏑 ↩️ 🎻, 👆 💪 ⚙️ `Form`. - -/// info - -⚙️ 📨, 🥇 ❎ `python-multipart`. - -🤶 Ⓜ. `pip install python-multipart`. - -/// - -## 🗄 `Form` - -🗄 `Form` ⚪️➡️ `fastapi`: - -{* ../../docs_src/request_forms/tutorial001.py hl[1] *} - -## 🔬 `Form` 🔢 - -✍ 📨 🔢 🎏 🌌 👆 🔜 `Body` ⚖️ `Query`: - -{* ../../docs_src/request_forms/tutorial001.py hl[7] *} - -🖼, 1️⃣ 🌌 Oauth2️⃣ 🔧 💪 ⚙️ (🤙 "🔐 💧") ⚫️ ✔ 📨 `username` & `password` 📨 🏑. - -🔌 🚚 🏑 ⚫️❔ 📛 `username` & `password`, & 📨 📨 🏑, 🚫 🎻. - -⏮️ `Form` 👆 💪 📣 🎏 📳 ⏮️ `Body` (& `Query`, `Path`, `Cookie`), 🔌 🔬, 🖼, 📛 (✅ `user-name` ↩️ `username`), ♒️. - -/// info - -`Form` 🎓 👈 😖 🔗 ⚪️➡️ `Body`. - -/// - -/// tip - -📣 📨 💪, 👆 💪 ⚙️ `Form` 🎯, ↩️ 🍵 ⚫️ 🔢 🔜 🔬 🔢 🔢 ⚖️ 💪 (🎻) 🔢. - -/// - -## 🔃 "📨 🏑" - -🌌 🕸 📨 (`
`) 📨 💽 💽 🛎 ⚙️ "🎁" 🔢 👈 📊, ⚫️ 🎏 ⚪️➡️ 🎻. - -**FastAPI** 🔜 ⚒ 💭 ✍ 👈 📊 ⚪️➡️ ▶️️ 🥉 ↩️ 🎻. - -/// note | 📡 ℹ - -📊 ⚪️➡️ 📨 🛎 🗜 ⚙️ "📻 🆎" `application/x-www-form-urlencoded`. - -✋️ 🕐❔ 📨 🔌 📁, ⚫️ 🗜 `multipart/form-data`. 👆 🔜 ✍ 🔃 🚚 📁 ⏭ 📃. - -🚥 👆 💚 ✍ 🌖 🔃 👉 🔢 & 📨 🏑, 👳 🏇 🕸 🩺 POST. - -/// - -/// warning - -👆 💪 📣 💗 `Form` 🔢 *➡ 🛠️*, ✋️ 👆 💪 🚫 📣 `Body` 🏑 👈 👆 ⌛ 📨 🎻, 📨 🔜 ✔️ 💪 🗜 ⚙️ `application/x-www-form-urlencoded` ↩️ `application/json`. - -👉 🚫 🚫 **FastAPI**, ⚫️ 🍕 🇺🇸🔍 🛠️. - -/// - -## 🌃 - -⚙️ `Form` 📣 📨 💽 🔢 🔢. diff --git a/docs/em/docs/tutorial/response-model.md b/docs/em/docs/tutorial/response-model.md deleted file mode 100644 index 477376458d..0000000000 --- a/docs/em/docs/tutorial/response-model.md +++ /dev/null @@ -1,340 +0,0 @@ -# 📨 🏷 - 📨 🆎 - -👆 💪 📣 🆎 ⚙️ 📨 ✍ *➡ 🛠️ 🔢* **📨 🆎**. - -👆 💪 ⚙️ **🆎 ✍** 🎏 🌌 👆 🔜 🔢 💽 🔢 **🔢**, 👆 💪 ⚙️ Pydantic 🏷, 📇, 📖, 📊 💲 💖 🔢, 🎻, ♒️. - -{* ../../docs_src/response_model/tutorial001_01.py hl[18,23] *} - -FastAPI 🔜 ⚙️ 👉 📨 🆎: - -* **✔** 📨 💽. - * 🚥 💽 ❌ (✅ 👆 ❌ 🏑), ⚫️ ⛓ 👈 *👆* 📱 📟 💔, 🚫 🛬 ⚫️❔ ⚫️ 🔜, & ⚫️ 🔜 📨 💽 ❌ ↩️ 🛬 ❌ 💽. 👉 🌌 👆 & 👆 👩‍💻 💪 🎯 👈 👫 🔜 📨 💽 & 💽 💠 📈. -* 🚮 **🎻 🔗** 📨, 🗄 *➡ 🛠️*. - * 👉 🔜 ⚙️ **🏧 🩺**. - * ⚫️ 🔜 ⚙️ 🏧 👩‍💻 📟 ⚡ 🧰. - -✋️ 🏆 🥈: - -* ⚫️ 🔜 **📉 & ⛽** 🔢 📊 ⚫️❔ 🔬 📨 🆎. - * 👉 ✴️ ⚠ **💂‍♂**, 👥 🔜 👀 🌅 👈 🔛. - -## `response_model` 🔢 - -📤 💼 🌐❔ 👆 💪 ⚖️ 💚 📨 💽 👈 🚫 ⚫️❔ ⚫️❔ 🆎 📣. - -🖼, 👆 💪 💚 **📨 📖** ⚖️ 💽 🎚, ✋️ **📣 ⚫️ Pydantic 🏷**. 👉 🌌 Pydantic 🏷 🔜 🌐 💽 🧾, 🔬, ♒️. 🎚 👈 👆 📨 (✅ 📖 ⚖️ 💽 🎚). - -🚥 👆 🚮 📨 🆎 ✍, 🧰 & 👨‍🎨 🔜 😭 ⏮️ (☑) ❌ 💬 👆 👈 👆 🔢 🛬 🆎 (✅#️⃣) 👈 🎏 ⚪️➡️ ⚫️❔ 👆 📣 (✅ Pydantic 🏷). - -📚 💼, 👆 💪 ⚙️ *➡ 🛠️ 👨‍🎨* 🔢 `response_model` ↩️ 📨 🆎. - -👆 💪 ⚙️ `response_model` 🔢 🙆 *➡ 🛠️*: - -* `@app.get()` -* `@app.post()` -* `@app.put()` -* `@app.delete()` -* ♒️. - -{* ../../docs_src/response_model/tutorial001.py hl[17,22,24:27] *} - -/// note - -👀 👈 `response_model` 🔢 "👨‍🎨" 👩‍🔬 (`get`, `post`, ♒️). 🚫 👆 *➡ 🛠️ 🔢*, 💖 🌐 🔢 & 💪. - -/// - -`response_model` 📨 🎏 🆎 👆 🔜 📣 Pydantic 🏷 🏑,, ⚫️ 💪 Pydantic 🏷, ✋️ ⚫️ 💪, ✅ `list` Pydantic 🏷, 💖 `List[Item]`. - -FastAPI 🔜 ⚙️ 👉 `response_model` 🌐 💽 🧾, 🔬, ♒️. & **🗜 & ⛽ 🔢 📊** 🚮 🆎 📄. - -/// tip - -🚥 👆 ✔️ ⚠ 🆎 ✅ 👆 👨‍🎨, ✍, ♒️, 👆 💪 📣 🔢 📨 🆎 `Any`. - -👈 🌌 👆 💬 👨‍🎨 👈 👆 😫 🛬 🕳. ✋️ FastAPI 🔜 💽 🧾, 🔬, 🖥, ♒️. ⏮️ `response_model`. - -/// - -### `response_model` 📫 - -🚥 👆 📣 👯‍♂️ 📨 🆎 & `response_model`, `response_model` 🔜 ✊ 📫 & ⚙️ FastAPI. - -👉 🌌 👆 💪 🚮 ☑ 🆎 ✍ 👆 🔢 🕐❔ 👆 🛬 🆎 🎏 🌘 📨 🏷, ⚙️ 👨‍🎨 & 🧰 💖 ✍. & 👆 💪 ✔️ FastAPI 💽 🔬, 🧾, ♒️. ⚙️ `response_model`. - -👆 💪 ⚙️ `response_model=None` ❎ 🏗 📨 🏷 👈 *➡ 🛠️*, 👆 5️⃣📆 💪 ⚫️ 🚥 👆 ❎ 🆎 ✍ 👜 👈 🚫 ☑ Pydantic 🏑, 👆 🔜 👀 🖼 👈 1️⃣ 📄 🔛. - -## 📨 🎏 🔢 💽 - -📥 👥 📣 `UserIn` 🏷, ⚫️ 🔜 🔌 🔢 🔐: - -{* ../../docs_src/response_model/tutorial002.py hl[9,11] *} - -/// info - -⚙️ `EmailStr`, 🥇 ❎ `email-validator`. - -🤶 Ⓜ. `pip install email-validator` -⚖️ `pip install pydantic[email]`. - -/// - -& 👥 ⚙️ 👉 🏷 📣 👆 🔢 & 🎏 🏷 📣 👆 🔢: - -{* ../../docs_src/response_model/tutorial002.py hl[18] *} - -🔜, 🕐❔ 🖥 🏗 👩‍💻 ⏮️ 🔐, 🛠️ 🔜 📨 🎏 🔐 📨. - -👉 💼, ⚫️ 💪 🚫 ⚠, ↩️ ⚫️ 🎏 👩‍💻 📨 🔐. - -✋️ 🚥 👥 ⚙️ 🎏 🏷 ➕1️⃣ *➡ 🛠️*, 👥 💪 📨 👆 👩‍💻 🔐 🔠 👩‍💻. - -/// danger - -🙅 🏪 ✅ 🔐 👩‍💻 ⚖️ 📨 ⚫️ 📨 💖 👉, 🚥 👆 💭 🌐 ⚠ & 👆 💭 ⚫️❔ 👆 🔨. - -/// - -## 🚮 🔢 🏷 - -👥 💪 ↩️ ✍ 🔢 🏷 ⏮️ 🔢 🔐 & 🔢 🏷 🍵 ⚫️: - -{* ../../docs_src/response_model/tutorial003.py hl[9,11,16] *} - -📥, ✋️ 👆 *➡ 🛠️ 🔢* 🛬 🎏 🔢 👩‍💻 👈 🔌 🔐: - -{* ../../docs_src/response_model/tutorial003.py hl[24] *} - -...👥 📣 `response_model` 👆 🏷 `UserOut`, 👈 🚫 🔌 🔐: - -{* ../../docs_src/response_model/tutorial003.py hl[22] *} - -, **FastAPI** 🔜 ✊ 💅 🖥 👅 🌐 💽 👈 🚫 📣 🔢 🏷 (⚙️ Pydantic). - -### `response_model` ⚖️ 📨 🆎 - -👉 💼, ↩️ 2️⃣ 🏷 🎏, 🚥 👥 ✍ 🔢 📨 🆎 `UserOut`, 👨‍🎨 & 🧰 🔜 😭 👈 👥 🛬 ❌ 🆎, 📚 🎏 🎓. - -👈 ⚫️❔ 👉 🖼 👥 ✔️ 📣 ⚫️ `response_model` 🔢. - -...✋️ 😣 👂 🔛 👀 ❔ ❎ 👈. - -## 📨 🆎 & 💽 🖥 - -➡️ 😣 ⚪️➡️ ⏮️ 🖼. 👥 💚 **✍ 🔢 ⏮️ 1️⃣ 🆎** ✋️ 📨 🕳 👈 🔌 **🌅 💽**. - -👥 💚 FastAPI 🚧 **🖥** 📊 ⚙️ 📨 🏷. - -⏮️ 🖼, ↩️ 🎓 🎏, 👥 ✔️ ⚙️ `response_model` 🔢. ✋️ 👈 ⛓ 👈 👥 🚫 🤚 🐕‍🦺 ⚪️➡️ 👨‍🎨 & 🧰 ✅ 🔢 📨 🆎. - -✋️ 🌅 💼 🌐❔ 👥 💪 🕳 💖 👉, 👥 💚 🏷 **⛽/❎** 📊 👉 🖼. - -& 👈 💼, 👥 💪 ⚙️ 🎓 & 🧬 ✊ 📈 🔢 **🆎 ✍** 🤚 👍 🐕‍🦺 👨‍🎨 & 🧰, & 🤚 FastAPI **💽 🖥**. - -{* ../../docs_src/response_model/tutorial003_01.py hl[9:13,15:16,20] *} - -⏮️ 👉, 👥 🤚 🏭 🐕‍🦺, ⚪️➡️ 👨‍🎨 & ✍ 👉 📟 ☑ ⚖ 🆎, ✋️ 👥 🤚 💽 🖥 ⚪️➡️ FastAPI. - -❔ 🔨 👉 👷 ❓ ➡️ ✅ 👈 👅. 👶 - -### 🆎 ✍ & 🏭 - -🥇 ➡️ 👀 ❔ 👨‍🎨, ✍ & 🎏 🧰 🔜 👀 👉. - -`BaseUser` ✔️ 🧢 🏑. ⤴️ `UserIn` 😖 ⚪️➡️ `BaseUser` & 🚮 `password` 🏑,, ⚫️ 🔜 🔌 🌐 🏑 ⚪️➡️ 👯‍♂️ 🏷. - -👥 ✍ 🔢 📨 🆎 `BaseUser`, ✋️ 👥 🤙 🛬 `UserIn` 👐. - -👨‍🎨, ✍, & 🎏 🧰 🏆 🚫 😭 🔃 👉 ↩️, ⌨ ⚖, `UserIn` 🏿 `BaseUser`, ❔ ⛓ ⚫️ *☑* 🆎 🕐❔ ⚫️❔ ⌛ 🕳 👈 `BaseUser`. - -### FastAPI 💽 🖥 - -🔜, FastAPI, ⚫️ 🔜 👀 📨 🆎 & ⚒ 💭 👈 ⚫️❔ 👆 📨 🔌 **🕴** 🏑 👈 📣 🆎. - -FastAPI 🔨 📚 👜 🔘 ⏮️ Pydantic ⚒ 💭 👈 📚 🎏 🚫 🎓 🧬 🚫 ⚙️ 📨 💽 🖥, ⏪ 👆 💪 🔚 🆙 🛬 🌅 🌅 💽 🌘 ⚫️❔ 👆 📈. - -👉 🌌, 👆 💪 🤚 🏆 👯‍♂️ 🌏: 🆎 ✍ ⏮️ **🏭 🐕‍🦺** & **💽 🖥**. - -## 👀 ⚫️ 🩺 - -🕐❔ 👆 👀 🏧 🩺, 👆 💪 ✅ 👈 🔢 🏷 & 🔢 🏷 🔜 👯‍♂️ ✔️ 👫 👍 🎻 🔗: - - - -& 👯‍♂️ 🏷 🔜 ⚙️ 🎓 🛠️ 🧾: - - - -## 🎏 📨 🆎 ✍ - -📤 5️⃣📆 💼 🌐❔ 👆 📨 🕳 👈 🚫 ☑ Pydantic 🏑 & 👆 ✍ ⚫️ 🔢, 🕴 🤚 🐕‍🦺 🚚 🏭 (👨‍🎨, ✍, ♒️). - -### 📨 📨 🔗 - -🏆 ⚠ 💼 🔜 [🛬 📨 🔗 🔬 ⏪ 🏧 🩺](../advanced/response-directly.md){.internal-link target=_blank}. - -{* ../../docs_src/response_model/tutorial003_02.py hl[8,10:11] *} - -👉 🙅 💼 🍵 🔁 FastAPI ↩️ 📨 🆎 ✍ 🎓 (⚖️ 🏿) `Response`. - -& 🧰 🔜 😄 ↩️ 👯‍♂️ `RedirectResponse` & `JSONResponse` 🏿 `Response`, 🆎 ✍ ☑. - -### ✍ 📨 🏿 - -👆 💪 ⚙️ 🏿 `Response` 🆎 ✍: - -{* ../../docs_src/response_model/tutorial003_03.py hl[8:9] *} - -👉 🔜 👷 ↩️ `RedirectResponse` 🏿 `Response`, & FastAPI 🔜 🔁 🍵 👉 🙅 💼. - -### ❌ 📨 🆎 ✍ - -✋️ 🕐❔ 👆 📨 🎏 ❌ 🎚 👈 🚫 ☑ Pydantic 🆎 (✅ 💽 🎚) & 👆 ✍ ⚫️ 💖 👈 🔢, FastAPI 🔜 🔄 ✍ Pydantic 📨 🏷 ⚪️➡️ 👈 🆎 ✍, & 🔜 ❌. - -🎏 🔜 🔨 🚥 👆 ✔️ 🕳 💖 🇪🇺 🖖 🎏 🆎 🌐❔ 1️⃣ ⚖️ 🌅 👫 🚫 ☑ Pydantic 🆎, 🖼 👉 🔜 ❌ 👶: - -{* ../../docs_src/response_model/tutorial003_04.py hl[10] *} - -...👉 ❌ ↩️ 🆎 ✍ 🚫 Pydantic 🆎 & 🚫 👁 `Response` 🎓 ⚖️ 🏿, ⚫️ 🇪🇺 (🙆 2️⃣) 🖖 `Response` & `dict`. - -### ❎ 📨 🏷 - -▶️ ⚪️➡️ 🖼 🔛, 👆 5️⃣📆 🚫 💚 ✔️ 🔢 💽 🔬, 🧾, 🖥, ♒️. 👈 🎭 FastAPI. - -✋️ 👆 💪 💚 🚧 📨 🆎 ✍ 🔢 🤚 🐕‍🦺 ⚪️➡️ 🧰 💖 👨‍🎨 & 🆎 ☑ (✅ ✍). - -👉 💼, 👆 💪 ❎ 📨 🏷 ⚡ ⚒ `response_model=None`: - -{* ../../docs_src/response_model/tutorial003_05.py hl[9] *} - -👉 🔜 ⚒ FastAPI 🚶 📨 🏷 ⚡ & 👈 🌌 👆 💪 ✔️ 🙆 📨 🆎 ✍ 👆 💪 🍵 ⚫️ 🤕 👆 FastAPI 🈸. 👶 - -## 📨 🏷 🔢 🔢 - -👆 📨 🏷 💪 ✔️ 🔢 💲, 💖: - -{* ../../docs_src/response_model/tutorial004.py hl[11,13:14] *} - -* `description: Union[str, None] = None` (⚖️ `str | None = None` 🐍 3️⃣.1️⃣0️⃣) ✔️ 🔢 `None`. -* `tax: float = 10.5` ✔️ 🔢 `10.5`. -* `tags: List[str] = []` 🔢 🛁 📇: `[]`. - -✋️ 👆 💪 💚 🚫 👫 ⚪️➡️ 🏁 🚥 👫 🚫 🤙 🏪. - -🖼, 🚥 👆 ✔️ 🏷 ⏮️ 📚 📦 🔢 ☁ 💽, ✋️ 👆 🚫 💚 📨 📶 📏 🎻 📨 🌕 🔢 💲. - -### ⚙️ `response_model_exclude_unset` 🔢 - -👆 💪 ⚒ *➡ 🛠️ 👨‍🎨* 🔢 `response_model_exclude_unset=True`: - -{* ../../docs_src/response_model/tutorial004.py hl[24] *} - -& 👈 🔢 💲 🏆 🚫 🔌 📨, 🕴 💲 🤙 ⚒. - -, 🚥 👆 📨 📨 👈 *➡ 🛠️* 🏬 ⏮️ 🆔 `foo`, 📨 (🚫 ✅ 🔢 💲) 🔜: - -```JSON -{ - "name": "Foo", - "price": 50.2 -} -``` - -/// info - -FastAPI ⚙️ Pydantic 🏷 `.dict()` ⏮️ 🚮 `exclude_unset` 🔢 🏆 👉. - -/// - -/// info - -👆 💪 ⚙️: - -* `response_model_exclude_defaults=True` -* `response_model_exclude_none=True` - -🔬 Pydantic 🩺 `exclude_defaults` & `exclude_none`. - -/// - -#### 📊 ⏮️ 💲 🏑 ⏮️ 🔢 - -✋️ 🚥 👆 📊 ✔️ 💲 🏷 🏑 ⏮️ 🔢 💲, 💖 🏬 ⏮️ 🆔 `bar`: - -```Python hl_lines="3 5" -{ - "name": "Bar", - "description": "The bartenders", - "price": 62, - "tax": 20.2 -} -``` - -👫 🔜 🔌 📨. - -#### 📊 ⏮️ 🎏 💲 🔢 - -🚥 📊 ✔️ 🎏 💲 🔢 🕐, 💖 🏬 ⏮️ 🆔 `baz`: - -```Python hl_lines="3 5-6" -{ - "name": "Baz", - "description": None, - "price": 50.2, - "tax": 10.5, - "tags": [] -} -``` - -FastAPI 🙃 🥃 (🤙, Pydantic 🙃 🥃) 🤔 👈, ✋️ `description`, `tax`, & `tags` ✔️ 🎏 💲 🔢, 👫 ⚒ 🎯 (↩️ ✊ ⚪️➡️ 🔢). - -, 👫 🔜 🔌 🎻 📨. - -/// tip - -👀 👈 🔢 💲 💪 🕳, 🚫 🕴 `None`. - -👫 💪 📇 (`[]`), `float` `10.5`, ♒️. - -/// - -### `response_model_include` & `response_model_exclude` - -👆 💪 ⚙️ *➡ 🛠️ 👨‍🎨* 🔢 `response_model_include` & `response_model_exclude`. - -👫 ✊ `set` `str` ⏮️ 📛 🔢 🔌 (❎ 🎂) ⚖️ 🚫 (✅ 🎂). - -👉 💪 ⚙️ ⏩ ⌨ 🚥 👆 ✔️ 🕴 1️⃣ Pydantic 🏷 & 💚 ❎ 💽 ⚪️➡️ 🔢. - -/// tip - -✋️ ⚫️ 👍 ⚙️ 💭 🔛, ⚙️ 💗 🎓, ↩️ 👫 🔢. - -👉 ↩️ 🎻 🔗 🏗 👆 📱 🗄 (& 🩺) 🔜 1️⃣ 🏁 🏷, 🚥 👆 ⚙️ `response_model_include` ⚖️ `response_model_exclude` 🚫 🔢. - -👉 ✔ `response_model_by_alias` 👈 👷 ➡. - -/// - -{* ../../docs_src/response_model/tutorial005.py hl[31,37] *} - -/// tip - -❕ `{"name", "description"}` ✍ `set` ⏮️ 📚 2️⃣ 💲. - -⚫️ 🌓 `set(["name", "description"])`. - -/// - -#### ⚙️ `list`Ⓜ ↩️ `set`Ⓜ - -🚥 👆 💭 ⚙️ `set` & ⚙️ `list` ⚖️ `tuple` ↩️, FastAPI 🔜 🗜 ⚫️ `set` & ⚫️ 🔜 👷 ☑: - -{* ../../docs_src/response_model/tutorial006.py hl[31,37] *} - -## 🌃 - -⚙️ *➡ 🛠️ 👨‍🎨* 🔢 `response_model` 🔬 📨 🏷 & ✴️ 🚚 📢 💽 ⛽ 👅. - -⚙️ `response_model_exclude_unset` 📨 🕴 💲 🎯 ⚒. diff --git a/docs/em/docs/tutorial/response-status-code.md b/docs/em/docs/tutorial/response-status-code.md deleted file mode 100644 index 413ceb9169..0000000000 --- a/docs/em/docs/tutorial/response-status-code.md +++ /dev/null @@ -1,101 +0,0 @@ -# 📨 👔 📟 - -🎏 🌌 👆 💪 ✔ 📨 🏷, 👆 💪 📣 🇺🇸🔍 👔 📟 ⚙️ 📨 ⏮️ 🔢 `status_code` 🙆 *➡ 🛠️*: - -* `@app.get()` -* `@app.post()` -* `@app.put()` -* `@app.delete()` -* ♒️. - -{* ../../docs_src/response_status_code/tutorial001.py hl[6] *} - -/// note - -👀 👈 `status_code` 🔢 "👨‍🎨" 👩‍🔬 (`get`, `post`, ♒️). 🚫 👆 *➡ 🛠️ 🔢*, 💖 🌐 🔢 & 💪. - -/// - -`status_code` 🔢 📨 🔢 ⏮️ 🇺🇸🔍 👔 📟. - -/// info - -`status_code` 💪 👐 📨 `IntEnum`, ✅ 🐍 `http.HTTPStatus`. - -/// - -⚫️ 🔜: - -* 📨 👈 👔 📟 📨. -* 📄 ⚫️ ✅ 🗄 🔗 ( & , 👩‍💻 🔢): - - - -/// note - -📨 📟 (👀 ⏭ 📄) 🎦 👈 📨 🔨 🚫 ✔️ 💪. - -FastAPI 💭 👉, & 🔜 🏭 🗄 🩺 👈 🇵🇸 📤 🙅‍♂ 📨 💪. - -/// - -## 🔃 🇺🇸🔍 👔 📟 - -/// note - -🚥 👆 ⏪ 💭 ⚫️❔ 🇺🇸🔍 👔 📟, 🚶 ⏭ 📄. - -/// - -🇺🇸🔍, 👆 📨 🔢 👔 📟 3️⃣ 9️⃣ 🍕 📨. - -👫 👔 📟 ✔️ 📛 🔗 🤔 👫, ✋️ ⚠ 🍕 🔢. - -📏: - -* `100` & 🔛 "ℹ". 👆 🛎 ⚙️ 👫 🔗. 📨 ⏮️ 👫 👔 📟 🚫🔜 ✔️ 💪. -* **`200`** & 🔛 "🏆" 📨. 👫 🕐 👆 🔜 ⚙️ 🏆. - * `200` 🔢 👔 📟, ❔ ⛓ 🌐 "👌". - * ➕1️⃣ 🖼 🔜 `201`, "✍". ⚫️ 🛎 ⚙️ ⏮️ 🏗 🆕 ⏺ 💽. - * 🎁 💼 `204`, "🙅‍♂ 🎚". 👉 📨 ⚙️ 🕐❔ 📤 🙅‍♂ 🎚 📨 👩‍💻, & 📨 🔜 🚫 ✔️ 💪. -* **`300`** & 🔛 "❎". 📨 ⏮️ 👫 👔 📟 5️⃣📆 ⚖️ 5️⃣📆 🚫 ✔️ 💪, 🌖 `304`, "🚫 🔀", ❔ 🔜 🚫 ✔️ 1️⃣. -* **`400`** & 🔛 "👩‍💻 ❌" 📨. 👫 🥈 🆎 👆 🔜 🎲 ⚙️ 🏆. - * 🖼 `404`, "🚫 🔎" 📨. - * 💊 ❌ ⚪️➡️ 👩‍💻, 👆 💪 ⚙️ `400`. -* `500` & 🔛 💽 ❌. 👆 🌖 🙅 ⚙️ 👫 🔗. 🕐❔ 🕳 🚶 ❌ 🍕 👆 🈸 📟, ⚖️ 💽, ⚫️ 🔜 🔁 📨 1️⃣ 👫 👔 📟. - -/// tip - -💭 🌅 🔃 🔠 👔 📟 & ❔ 📟 ⚫️❔, ✅ 🏇 🧾 🔃 🇺🇸🔍 👔 📟. - -/// - -## ⌨ 💭 📛 - -➡️ 👀 ⏮️ 🖼 🔄: - -{* ../../docs_src/response_status_code/tutorial001.py hl[6] *} - -`201` 👔 📟 "✍". - -✋️ 👆 🚫 ✔️ ✍ ⚫️❔ 🔠 👉 📟 ⛓. - -👆 💪 ⚙️ 🏪 🔢 ⚪️➡️ `fastapi.status`. - -{* ../../docs_src/response_status_code/tutorial002.py hl[1,6] *} - -👫 🏪, 👫 🧑‍🤝‍🧑 🎏 🔢, ✋️ 👈 🌌 👆 💪 ⚙️ 👨‍🎨 📋 🔎 👫: - - - -/// note | 📡 ℹ - -👆 💪 ⚙️ `from starlette import status`. - -**FastAPI** 🚚 🎏 `starlette.status` `fastapi.status` 🏪 👆, 👩‍💻. ✋️ ⚫️ 👟 🔗 ⚪️➡️ 💃. - -/// - -## 🔀 🔢 - -⏪, [🏧 👩‍💻 🦮](../advanced/response-change-status-code.md){.internal-link target=_blank}, 👆 🔜 👀 ❔ 📨 🎏 👔 📟 🌘 🔢 👆 📣 📥. diff --git a/docs/em/docs/tutorial/schema-extra-example.md b/docs/em/docs/tutorial/schema-extra-example.md deleted file mode 100644 index 1bd314c513..0000000000 --- a/docs/em/docs/tutorial/schema-extra-example.md +++ /dev/null @@ -1,110 +0,0 @@ -# 📣 📨 🖼 💽 - -👆 💪 📣 🖼 💽 👆 📱 💪 📨. - -📥 📚 🌌 ⚫️. - -## Pydantic `schema_extra` - -👆 💪 📣 `example` Pydantic 🏷 ⚙️ `Config` & `schema_extra`, 🔬 Pydantic 🩺: 🔗 🛃: - -{* ../../docs_src/schema_extra_example/tutorial001.py hl[15:23] *} - -👈 ➕ ℹ 🔜 🚮-🔢 **🎻 🔗** 👈 🏷, & ⚫️ 🔜 ⚙️ 🛠️ 🩺. - -/// tip - -👆 💪 ⚙️ 🎏 ⚒ ↔ 🎻 🔗 & 🚮 👆 👍 🛃 ➕ ℹ. - -🖼 👆 💪 ⚙️ ⚫️ 🚮 🗃 🕸 👩‍💻 🔢, ♒️. - -/// - -## `Field` 🌖 ❌ - -🕐❔ ⚙️ `Field()` ⏮️ Pydantic 🏷, 👆 💪 📣 ➕ ℹ **🎻 🔗** 🚶‍♀️ 🙆 🎏 ❌ ❌ 🔢. - -👆 💪 ⚙️ 👉 🚮 `example` 🔠 🏑: - -{* ../../docs_src/schema_extra_example/tutorial002.py hl[4,10:13] *} - -/// warning - -🚧 🤯 👈 📚 ➕ ❌ 🚶‍♀️ 🏆 🚫 🚮 🙆 🔬, 🕴 ➕ ℹ, 🧾 🎯. - -/// - -## `example` & `examples` 🗄 - -🕐❔ ⚙️ 🙆: - -* `Path()` -* `Query()` -* `Header()` -* `Cookie()` -* `Body()` -* `Form()` -* `File()` - -👆 💪 📣 💽 `example` ⚖️ 👪 `examples` ⏮️ 🌖 ℹ 👈 🔜 🚮 **🗄**. - -### `Body` ⏮️ `example` - -📥 👥 🚶‍♀️ `example` 📊 ⌛ `Body()`: - -{* ../../docs_src/schema_extra_example/tutorial003.py hl[20:25] *} - -### 🖼 🩺 🎚 - -⏮️ 🙆 👩‍🔬 🔛 ⚫️ 🔜 👀 💖 👉 `/docs`: - - - -### `Body` ⏮️ 💗 `examples` - -👐 👁 `example`, 👆 💪 🚶‍♀️ `examples` ⚙️ `dict` ⏮️ **💗 🖼**, 🔠 ⏮️ ➕ ℹ 👈 🔜 🚮 **🗄** 💁‍♂️. - -🔑 `dict` 🔬 🔠 🖼, & 🔠 💲 ➕1️⃣ `dict`. - -🔠 🎯 🖼 `dict` `examples` 💪 🔌: - -* `summary`: 📏 📛 🖼. -* `description`: 📏 📛 👈 💪 🔌 ✍ ✍. -* `value`: 👉 ☑ 🖼 🎦, ✅ `dict`. -* `externalValue`: 🎛 `value`, 📛 ☝ 🖼. 👐 👉 5️⃣📆 🚫 🐕‍🦺 📚 🧰 `value`. - -{* ../../docs_src/schema_extra_example/tutorial004.py hl[21:47] *} - -### 🖼 🩺 🎚 - -⏮️ `examples` 🚮 `Body()` `/docs` 🔜 👀 💖: - - - -## 📡 ℹ - -/// warning - -👉 📶 📡 ℹ 🔃 🐩 **🎻 🔗** & **🗄**. - -🚥 💭 🔛 ⏪ 👷 👆, 👈 💪 🥃, & 👆 🎲 🚫 💪 👉 ℹ, 💭 🆓 🚶 👫. - -/// - -🕐❔ 👆 🚮 🖼 🔘 Pydantic 🏷, ⚙️ `schema_extra` ⚖️ `Field(example="something")` 👈 🖼 🚮 **🎻 🔗** 👈 Pydantic 🏷. - -& 👈 **🎻 🔗** Pydantic 🏷 🔌 **🗄** 👆 🛠️, & ⤴️ ⚫️ ⚙️ 🩺 🎚. - -**🎻 🔗** 🚫 🤙 ✔️ 🏑 `example` 🐩. ⏮️ ⏬ 🎻 🔗 🔬 🏑 `examples`, ✋️ 🗄 3️⃣.0️⃣.3️⃣ ⚓️ 🔛 🗝 ⏬ 🎻 🔗 👈 🚫 ✔️ `examples`. - -, 🗄 3️⃣.0️⃣.3️⃣ 🔬 🚮 👍 `example` 🔀 ⏬ **🎻 🔗** ⚫️ ⚙️, 🎏 🎯 (✋️ ⚫️ 👁 `example`, 🚫 `examples`), & 👈 ⚫️❔ ⚙️ 🛠️ 🩺 🎚 (⚙️ 🦁 🎚). - -, 👐 `example` 🚫 🍕 🎻 🔗, ⚫️ 🍕 🗄 🛃 ⏬ 🎻 🔗, & 👈 ⚫️❔ 🔜 ⚙️ 🩺 🎚. - -✋️ 🕐❔ 👆 ⚙️ `example` ⚖️ `examples` ⏮️ 🙆 🎏 🚙 (`Query()`, `Body()`, ♒️.) 📚 🖼 🚫 🚮 🎻 🔗 👈 🔬 👈 💽 (🚫 🗄 👍 ⏬ 🎻 🔗), 👫 🚮 🔗 *➡ 🛠️* 📄 🗄 (🏞 🍕 🗄 👈 ⚙️ 🎻 🔗). - -`Path()`, `Query()`, `Header()`, & `Cookie()`, `example` ⚖️ `examples` 🚮 🗄 🔑, `Parameter Object` (🔧). - -& `Body()`, `File()`, & `Form()`, `example` ⚖️ `examples` 📊 🚮 🗄 🔑, `Request Body Object`, 🏑 `content`, 🔛 `Media Type Object` (🔧). - -🔛 🎏 ✋, 📤 🆕 ⏬ 🗄: **3️⃣.1️⃣.0️⃣**, ⏳ 🚀. ⚫️ ⚓️ 🔛 ⏪ 🎻 🔗 & 🏆 🛠️ ⚪️➡️ 🗄 🛃 ⏬ 🎻 🔗 ❎, 💱 ⚒ ⚪️➡️ ⏮️ ⏬ 🎻 🔗, 🌐 👫 🤪 🔺 📉. 👐, 🦁 🎚 ⏳ 🚫 🐕‍🦺 🗄 3️⃣.1️⃣.0️⃣,, 🔜, ⚫️ 👍 😣 ⚙️ 💭 🔛. diff --git a/docs/em/docs/tutorial/security/first-steps.md b/docs/em/docs/tutorial/security/first-steps.md deleted file mode 100644 index 8fb459a651..0000000000 --- a/docs/em/docs/tutorial/security/first-steps.md +++ /dev/null @@ -1,197 +0,0 @@ -# 💂‍♂ - 🥇 🔁 - -➡️ 🌈 👈 👆 ✔️ 👆 **👩‍💻** 🛠️ 🆔. - -& 👆 ✔️ **🕸** ➕1️⃣ 🆔 ⚖️ 🎏 ➡ 🎏 🆔 (⚖️ 📱 🈸). - -& 👆 💚 ✔️ 🌌 🕸 🔓 ⏮️ 👩‍💻, ⚙️ **🆔** & **🔐**. - -👥 💪 ⚙️ **Oauth2️⃣** 🏗 👈 ⏮️ **FastAPI**. - -✋️ ➡️ 🖊 👆 🕰 👂 🌕 📏 🔧 🔎 👈 🐥 🍖 ℹ 👆 💪. - -➡️ ⚙️ 🧰 🚚 **FastAPI** 🍵 💂‍♂. - -## ❔ ⚫️ 👀 - -➡️ 🥇 ⚙️ 📟 & 👀 ❔ ⚫️ 👷, & ⤴️ 👥 🔜 👟 🔙 🤔 ⚫️❔ 😥. - -## ✍ `main.py` - -📁 🖼 📁 `main.py`: - -{* ../../docs_src/security/tutorial001.py *} - -## 🏃 ⚫️ - -/// info - -🥇 ❎ `python-multipart`. - -🤶 Ⓜ. `pip install python-multipart`. - -👉 ↩️ **Oauth2️⃣** ⚙️ "📨 📊" 📨 `username` & `password`. - -/// - -🏃 🖼 ⏮️: - -
- -```console -$ uvicorn main:app --reload - -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -``` - -
- -## ✅ ⚫️ - -🚶 🎓 🩺: http://127.0.0.1:8000/docs. - -👆 🔜 👀 🕳 💖 👉: - - - -/// check | ✔ 🔼 ❗ - -👆 ⏪ ✔️ ✨ 🆕 "✔" 🔼. - - & 👆 *➡ 🛠️* ✔️ 🐥 🔒 🔝-▶️️ ↩ 👈 👆 💪 🖊. - -/// - -& 🚥 👆 🖊 ⚫️, 👆 ✔️ 🐥 ✔ 📨 🆎 `username` & `password` (& 🎏 📦 🏑): - - - -/// note - -⚫️ 🚫 🤔 ⚫️❔ 👆 🆎 📨, ⚫️ 🏆 🚫 👷. ✋️ 👥 🔜 🤚 📤. - -/// - -👉 ↗️ 🚫 🕸 🏁 👩‍💻, ✋️ ⚫️ 👑 🏧 🧰 📄 🖥 🌐 👆 🛠️. - -⚫️ 💪 ⚙️ 🕸 🏉 (👈 💪 👆). - -⚫️ 💪 ⚙️ 🥉 🥳 🈸 & ⚙️. - -& ⚫️ 💪 ⚙️ 👆, ℹ, ✅ & 💯 🎏 🈸. - -## `password` 💧 - -🔜 ➡️ 🚶 🔙 👄 & 🤔 ⚫️❔ 🌐 👈. - -`password` "💧" 1️⃣ 🌌 ("💧") 🔬 Oauth2️⃣, 🍵 💂‍♂ & 🤝. - -Oauth2️⃣ 🔧 👈 👩‍💻 ⚖️ 🛠️ 💪 🔬 💽 👈 🔓 👩‍💻. - -✋️ 👉 💼, 🎏 **FastAPI** 🈸 🔜 🍵 🛠️ & 🤝. - -, ➡️ 📄 ⚫️ ⚪️➡️ 👈 📉 ☝ 🎑: - -* 👩‍💻 🆎 `username` & `password` 🕸, & 🎯 `Enter`. -* 🕸 (🏃‍♂ 👩‍💻 🖥) 📨 👈 `username` & `password` 🎯 📛 👆 🛠️ (📣 ⏮️ `tokenUrl="token"`). -* 🛠️ ✅ 👈 `username` & `password`, & 📨 ⏮️ "🤝" (👥 🚫 🛠️ 🙆 👉). - * "🤝" 🎻 ⏮️ 🎚 👈 👥 💪 ⚙️ ⏪ ✔ 👉 👩‍💻. - * 🛎, 🤝 ⚒ 🕛 ⏮️ 🕰. - * , 👩‍💻 🔜 ✔️ 🕹 🔄 ☝ ⏪. - * & 🚥 🤝 📎, ⚠ 🌘. ⚫️ 🚫 💖 🧲 🔑 👈 🔜 👷 ♾ (🏆 💼). -* 🕸 🏪 👈 🤝 🍕 👱. -* 👩‍💻 🖊 🕸 🚶 ➕1️⃣ 📄 🕸 🕸 📱. -* 🕸 💪 ☕ 🌅 💽 ⚪️➡️ 🛠️. - * ✋️ ⚫️ 💪 🤝 👈 🎯 🔗. - * , 🔓 ⏮️ 👆 🛠️, ⚫️ 📨 🎚 `Authorization` ⏮️ 💲 `Bearer ` ➕ 🤝. - * 🚥 🤝 🔌 `foobar`, 🎚 `Authorization` 🎚 🔜: `Bearer foobar`. - -## **FastAPI**'Ⓜ `OAuth2PasswordBearer` - -**FastAPI** 🚚 📚 🧰, 🎏 🎚 ⚛, 🛠️ 👫 💂‍♂ ⚒. - -👉 🖼 👥 🔜 ⚙️ **Oauth2️⃣**, ⏮️ **🔐** 💧, ⚙️ **📨** 🤝. 👥 👈 ⚙️ `OAuth2PasswordBearer` 🎓. - -/// info - -"📨" 🤝 🚫 🕴 🎛. - -✋️ ⚫️ 🏆 1️⃣ 👆 ⚙️ 💼. - - & ⚫️ 💪 🏆 🏆 ⚙️ 💼, 🚥 👆 Oauth2️⃣ 🕴 & 💭 ⚫️❔ ⚫️❔ 📤 ➕1️⃣ 🎛 👈 ♣ 👻 👆 💪. - -👈 💼, **FastAPI** 🚚 👆 ⏮️ 🧰 🏗 ⚫️. - -/// - -🕐❔ 👥 ✍ 👐 `OAuth2PasswordBearer` 🎓 👥 🚶‍♀️ `tokenUrl` 🔢. 👉 🔢 🔌 📛 👈 👩‍💻 (🕸 🏃 👩‍💻 🖥) 🔜 ⚙️ 📨 `username` & `password` ✔ 🤚 🤝. - -{* ../../docs_src/security/tutorial001.py hl[6] *} - -/// tip - -📥 `tokenUrl="token"` 🔗 ⚖ 📛 `token` 👈 👥 🚫 ✍. ⚫️ ⚖ 📛, ⚫️ 🌓 `./token`. - -↩️ 👥 ⚙️ ⚖ 📛, 🚥 👆 🛠️ 🔎 `https://example.com/`, ⤴️ ⚫️ 🔜 🔗 `https://example.com/token`. ✋️ 🚥 👆 🛠️ 🔎 `https://example.com/api/v1/`, ⤴️ ⚫️ 🔜 🔗 `https://example.com/api/v1/token`. - -⚙️ ⚖ 📛 ⚠ ⚒ 💭 👆 🈸 🚧 👷 🏧 ⚙️ 💼 💖 [⛅ 🗳](../../advanced/behind-a-proxy.md){.internal-link target=_blank}. - -/// - -👉 🔢 🚫 ✍ 👈 🔗 / *➡ 🛠️*, ✋️ 📣 👈 📛 `/token` 🔜 1️⃣ 👈 👩‍💻 🔜 ⚙️ 🤚 🤝. 👈 ℹ ⚙️ 🗄, & ⤴️ 🎓 🛠️ 🧾 ⚙️. - -👥 🔜 🔜 ✍ ☑ ➡ 🛠️. - -/// info - -🚥 👆 📶 ⚠ "✍" 👆 💪 👎 👗 🔢 📛 `tokenUrl` ↩️ `token_url`. - -👈 ↩️ ⚫️ ⚙️ 🎏 📛 🗄 🔌. 👈 🚥 👆 💪 🔬 🌅 🔃 🙆 👫 💂‍♂ ⚖ 👆 💪 📁 & 📋 ⚫️ 🔎 🌖 ℹ 🔃 ⚫️. - -/// - -`oauth2_scheme` 🔢 👐 `OAuth2PasswordBearer`, ✋️ ⚫️ "🇧🇲". - -⚫️ 💪 🤙: - -```Python -oauth2_scheme(some, parameters) -``` - -, ⚫️ 💪 ⚙️ ⏮️ `Depends`. - -### ⚙️ ⚫️ - -🔜 👆 💪 🚶‍♀️ 👈 `oauth2_scheme` 🔗 ⏮️ `Depends`. - -{* ../../docs_src/security/tutorial001.py hl[10] *} - -👉 🔗 🔜 🚚 `str` 👈 🛠️ 🔢 `token` *➡ 🛠️ 🔢*. - -**FastAPI** 🔜 💭 👈 ⚫️ 💪 ⚙️ 👉 🔗 🔬 "💂‍♂ ⚖" 🗄 🔗 (& 🏧 🛠️ 🩺). - -/// info | 📡 ℹ - -**FastAPI** 🔜 💭 👈 ⚫️ 💪 ⚙️ 🎓 `OAuth2PasswordBearer` (📣 🔗) 🔬 💂‍♂ ⚖ 🗄 ↩️ ⚫️ 😖 ⚪️➡️ `fastapi.security.oauth2.OAuth2`, ❔ 🔄 😖 ⚪️➡️ `fastapi.security.base.SecurityBase`. - -🌐 💂‍♂ 🚙 👈 🛠️ ⏮️ 🗄 (& 🏧 🛠️ 🩺) 😖 ⚪️➡️ `SecurityBase`, 👈 ❔ **FastAPI** 💪 💭 ❔ 🛠️ 👫 🗄. - -/// - -## ⚫️❔ ⚫️ 🔨 - -⚫️ 🔜 🚶 & 👀 📨 👈 `Authorization` 🎚, ✅ 🚥 💲 `Bearer ` ➕ 🤝, & 🔜 📨 🤝 `str`. - -🚥 ⚫️ 🚫 👀 `Authorization` 🎚, ⚖️ 💲 🚫 ✔️ `Bearer ` 🤝, ⚫️ 🔜 📨 ⏮️ 4️⃣0️⃣1️⃣ 👔 📟 ❌ (`UNAUTHORIZED`) 🔗. - -👆 🚫 ✔️ ✅ 🚥 🤝 🔀 📨 ❌. 👆 💪 💭 👈 🚥 👆 🔢 🛠️, ⚫️ 🔜 ✔️ `str` 👈 🤝. - -👆 💪 🔄 ⚫️ ⏪ 🎓 🩺: - - - -👥 🚫 ✔ 🔬 🤝, ✋️ 👈 ▶️ ⏪. - -## 🌃 - -, 3️⃣ ⚖️ 4️⃣ ➕ ⏸, 👆 ⏪ ✔️ 🐒 📨 💂‍♂. diff --git a/docs/em/docs/tutorial/security/get-current-user.md b/docs/em/docs/tutorial/security/get-current-user.md deleted file mode 100644 index 2f4a26f352..0000000000 --- a/docs/em/docs/tutorial/security/get-current-user.md +++ /dev/null @@ -1,105 +0,0 @@ -# 🤚 ⏮️ 👩‍💻 - -⏮️ 📃 💂‍♂ ⚙️ (❔ 🧢 🔛 🔗 💉 ⚙️) 🤝 *➡ 🛠️ 🔢* `token` `str`: - -{* ../../docs_src/security/tutorial001.py hl[10] *} - -✋️ 👈 🚫 👈 ⚠. - -➡️ ⚒ ⚫️ 🤝 👥 ⏮️ 👩‍💻. - -## ✍ 👩‍💻 🏷 - -🥇, ➡️ ✍ Pydantic 👩‍💻 🏷. - -🎏 🌌 👥 ⚙️ Pydantic 📣 💪, 👥 💪 ⚙️ ⚫️ 🙆 🙆: - -{* ../../docs_src/security/tutorial002.py hl[5,12:16] *} - -## ✍ `get_current_user` 🔗 - -➡️ ✍ 🔗 `get_current_user`. - -💭 👈 🔗 💪 ✔️ 🎧-🔗 ❓ - -`get_current_user` 🔜 ✔️ 🔗 ⏮️ 🎏 `oauth2_scheme` 👥 ✍ ⏭. - -🎏 👥 🔨 ⏭ *➡ 🛠️* 🔗, 👆 🆕 🔗 `get_current_user` 🔜 📨 `token` `str` ⚪️➡️ 🎧-🔗 `oauth2_scheme`: - -{* ../../docs_src/security/tutorial002.py hl[25] *} - -## 🤚 👩‍💻 - -`get_current_user` 🔜 ⚙️ (❌) 🚙 🔢 👥 ✍, 👈 ✊ 🤝 `str` & 📨 👆 Pydantic `User` 🏷: - -{* ../../docs_src/security/tutorial002.py hl[19:22,26:27] *} - -## 💉 ⏮️ 👩‍💻 - -🔜 👥 💪 ⚙️ 🎏 `Depends` ⏮️ 👆 `get_current_user` *➡ 🛠️*: - -{* ../../docs_src/security/tutorial002.py hl[31] *} - -👀 👈 👥 📣 🆎 `current_user` Pydantic 🏷 `User`. - -👉 🔜 ℹ 🇺🇲 🔘 🔢 ⏮️ 🌐 🛠️ & 🆎 ✅. - -/// tip - -👆 5️⃣📆 💭 👈 📨 💪 📣 ⏮️ Pydantic 🏷. - -📥 **FastAPI** 🏆 🚫 🤚 😨 ↩️ 👆 ⚙️ `Depends`. - -/// - -/// check - -🌌 👉 🔗 ⚙️ 🏗 ✔ 👥 ✔️ 🎏 🔗 (🎏 "☑") 👈 🌐 📨 `User` 🏷. - -👥 🚫 🚫 ✔️ 🕴 1️⃣ 🔗 👈 💪 📨 👈 🆎 💽. - -/// - -## 🎏 🏷 - -👆 💪 🔜 🤚 ⏮️ 👩‍💻 🔗 *➡ 🛠️ 🔢* & 🙅 ⏮️ 💂‍♂ 🛠️ **🔗 💉** 🎚, ⚙️ `Depends`. - -& 👆 💪 ⚙️ 🙆 🏷 ⚖️ 💽 💂‍♂ 📄 (👉 💼, Pydantic 🏷 `User`). - -✋️ 👆 🚫 🚫 ⚙️ 🎯 💽 🏷, 🎓 ⚖️ 🆎. - -👆 💚 ✔️ `id` & `email` & 🚫 ✔️ 🙆 `username` 👆 🏷 ❓ 💭. 👆 💪 ⚙️ 👉 🎏 🧰. - -👆 💚 ✔️ `str`❓ ⚖️ `dict`❓ ⚖️ 💽 🎓 🏷 👐 🔗 ❓ ⚫️ 🌐 👷 🎏 🌌. - -👆 🤙 🚫 ✔️ 👩‍💻 👈 🕹 👆 🈸 ✋️ 🤖, 🤖, ⚖️ 🎏 ⚙️, 👈 ✔️ 🔐 🤝 ❓ 🔄, ⚫️ 🌐 👷 🎏. - -⚙️ 🙆 😇 🏷, 🙆 😇 🎓, 🙆 😇 💽 👈 👆 💪 👆 🈸. **FastAPI** ✔️ 👆 📔 ⏮️ 🔗 💉 ⚙️. - -## 📟 📐 - -👉 🖼 5️⃣📆 😑 🔁. ✔️ 🤯 👈 👥 🌀 💂‍♂, 📊 🏷, 🚙 🔢 & *➡ 🛠️* 🎏 📁. - -✋️ 📥 🔑 ☝. - -💂‍♂ & 🔗 💉 💩 ✍ 🕐. - -& 👆 💪 ⚒ ⚫️ 🏗 👆 💚. & , ✔️ ⚫️ ✍ 🕴 🕐, 👁 🥉. ⏮️ 🌐 💪. - -✋️ 👆 💪 ✔️ 💯 🔗 (*➡ 🛠️*) ⚙️ 🎏 💂‍♂ ⚙️. - -& 🌐 👫 (⚖️ 🙆 ↔ 👫 👈 👆 💚) 💪 ✊ 📈 🏤-⚙️ 👫 🔗 ⚖️ 🙆 🎏 🔗 👆 ✍. - -& 🌐 👉 💯 *➡ 🛠️* 💪 🤪 3️⃣ ⏸: - -{* ../../docs_src/security/tutorial002.py hl[30:32] *} - -## 🌃 - -👆 💪 🔜 🤚 ⏮️ 👩‍💻 🔗 👆 *➡ 🛠️ 🔢*. - -👥 ⏪ 😬 📤. - -👥 💪 🚮 *➡ 🛠️* 👩‍💻/👩‍💻 🤙 📨 `username` & `password`. - -👈 👟 ⏭. diff --git a/docs/em/docs/tutorial/security/index.md b/docs/em/docs/tutorial/security/index.md deleted file mode 100644 index 1a47e55102..0000000000 --- a/docs/em/docs/tutorial/security/index.md +++ /dev/null @@ -1,106 +0,0 @@ -# 💂‍♂ - -📤 📚 🌌 🍵 💂‍♂, 🤝 & ✔. - -& ⚫️ 🛎 🏗 & "⚠" ❔. - -📚 🛠️ & ⚙️ 🍵 💂‍♂ & 🤝 ✊ 🦏 💸 🎯 & 📟 (📚 💼 ⚫️ 💪 5️⃣0️⃣ 💯 ⚖️ 🌅 🌐 📟 ✍). - -**FastAPI** 🚚 📚 🧰 ℹ 👆 🙅 ⏮️ **💂‍♂** 💪, 📉, 🐩 🌌, 🍵 ✔️ 🔬 & 💡 🌐 💂‍♂ 🔧. - -✋️ 🥇, ➡️ ✅ 🤪 🔧. - -## 🏃 ❓ - -🚥 👆 🚫 💅 🔃 🙆 👉 ⚖ & 👆 💪 🚮 💂‍♂ ⏮️ 🤝 ⚓️ 🔛 🆔 & 🔐 *▶️️ 🔜*, 🚶 ⏭ 📃. - -## Oauth2️⃣ - -Oauth2️⃣ 🔧 👈 🔬 📚 🌌 🍵 🤝 & ✔. - -⚫️ 🔬 🔧 & 📔 📚 🏗 ⚙️ 💼. - -⚫️ 🔌 🌌 🔓 ⚙️ "🥉 🥳". - -👈 ⚫️❔ 🌐 ⚙️ ⏮️ "💳 ⏮️ 👱📔, 🇺🇸🔍, 👱📔, 📂" ⚙️ 🔘. - -### ✳ 1️⃣ - -📤 ✳ 1️⃣, ❔ 📶 🎏 ⚪️➡️ Oauth2️⃣, & 🌖 🏗, ⚫️ 🔌 🔗 🔧 🔛 ❔ 🗜 📻. - -⚫️ 🚫 📶 🌟 ⚖️ ⚙️ 🛎. - -Oauth2️⃣ 🚫 ✔ ❔ 🗜 📻, ⚫️ ⌛ 👆 ✔️ 👆 🈸 🍦 ⏮️ 🇺🇸🔍. - -/// tip - -📄 🔃 **🛠️** 👆 🔜 👀 ❔ ⚒ 🆙 🇺🇸🔍 🆓, ⚙️ Traefik & ➡️ 🗜. - -/// - -## 👩‍💻 🔗 - -👩‍💻 🔗 ➕1️⃣ 🔧, 🧢 🔛 **Oauth2️⃣**. - -⚫️ ↔ Oauth2️⃣ ✔ 👜 👈 📶 🌌 Oauth2️⃣, 🔄 ⚒ ⚫️ 🌅 🛠️. - -🖼, 🇺🇸🔍 💳 ⚙️ 👩‍💻 🔗 (❔ 🔘 ⚙️ Oauth2️⃣). - -✋️ 👱📔 💳 🚫 🐕‍🦺 👩‍💻 🔗. ⚫️ ✔️ 🚮 👍 🍛 Oauth2️⃣. - -### 👩‍💻 (🚫 "👩‍💻 🔗") - -📤 "👩‍💻" 🔧. 👈 🔄 ❎ 🎏 👜 **👩‍💻 🔗**, ✋️ 🚫 ⚓️ 🔛 Oauth2️⃣. - -, ⚫️ 🏁 🌖 ⚙️. - -⚫️ 🚫 📶 🌟 ⚖️ ⚙️ 🛎. - -## 🗄 - -🗄 (⏪ 💭 🦁) 📂 🔧 🏗 🔗 (🔜 🍕 💾 🏛). - -**FastAPI** ⚓️ 🔛 **🗄**. - -👈 ⚫️❔ ⚒ ⚫️ 💪 ✔️ 💗 🏧 🎓 🧾 🔢, 📟 ⚡, ♒️. - -🗄 ✔️ 🌌 🔬 💗 💂‍♂ "⚖". - -⚙️ 👫, 👆 💪 ✊ 📈 🌐 👫 🐩-⚓️ 🧰, 🔌 👉 🎓 🧾 ⚙️. - -🗄 🔬 📄 💂‍♂ ⚖: - -* `apiKey`: 🈸 🎯 🔑 👈 💪 👟 ⚪️➡️: - * 🔢 🔢. - * 🎚. - * 🍪. -* `http`: 🐩 🇺🇸🔍 🤝 ⚙️, 🔌: - * `bearer`: 🎚 `Authorization` ⏮️ 💲 `Bearer ` ➕ 🤝. 👉 😖 ⚪️➡️ Oauth2️⃣. - * 🇺🇸🔍 🔰 🤝. - * 🇺🇸🔍 📰, ♒️. -* `oauth2`: 🌐 Oauth2️⃣ 🌌 🍵 💂‍♂ (🤙 "💧"). - * 📚 👫 💧 ☑ 🏗 ✳ 2️⃣.0️⃣ 🤝 🐕‍🦺 (💖 🇺🇸🔍, 👱📔, 👱📔, 📂, ♒️): - * `implicit` - * `clientCredentials` - * `authorizationCode` - * ✋️ 📤 1️⃣ 🎯 "💧" 👈 💪 👌 ⚙️ 🚚 🤝 🎏 🈸 🔗: - * `password`: ⏭ 📃 🔜 📔 🖼 👉. -* `openIdConnect`: ✔️ 🌌 🔬 ❔ 🔎 Oauth2️⃣ 🤝 📊 🔁. - * 👉 🏧 🔍 ⚫️❔ 🔬 👩‍💻 🔗 🔧. - - -/// tip - -🛠️ 🎏 🤝/✔ 🐕‍🦺 💖 🇺🇸🔍, 👱📔, 👱📔, 📂, ♒️. 💪 & 📶 ⏩. - -🌅 🏗 ⚠ 🏗 🤝/✔ 🐕‍🦺 💖 👈, ✋️ **FastAPI** 🤝 👆 🧰 ⚫️ 💪, ⏪ 🔨 🏋️ 🏋‍♂ 👆. - -/// - -## **FastAPI** 🚙 - -FastAPI 🚚 📚 🧰 🔠 👉 💂‍♂ ⚖ `fastapi.security` 🕹 👈 📉 ⚙️ 👉 💂‍♂ 🛠️. - -⏭ 📃 👆 🔜 👀 ❔ 🚮 💂‍♂ 👆 🛠️ ⚙️ 📚 🧰 🚚 **FastAPI**. - -& 👆 🔜 👀 ❔ ⚫️ 🤚 🔁 🛠️ 🔘 🎓 🧾 ⚙️. diff --git a/docs/em/docs/tutorial/security/oauth2-jwt.md b/docs/em/docs/tutorial/security/oauth2-jwt.md deleted file mode 100644 index ee7bc2d28a..0000000000 --- a/docs/em/docs/tutorial/security/oauth2-jwt.md +++ /dev/null @@ -1,275 +0,0 @@ -# Oauth2️⃣ ⏮️ 🔐 (& 🔁), 📨 ⏮️ 🥙 🤝 - -🔜 👈 👥 ✔️ 🌐 💂‍♂ 💧, ➡️ ⚒ 🈸 🤙 🔐, ⚙️ 🥙 🤝 & 🔐 🔐 🔁. - -👉 📟 🕳 👆 💪 🤙 ⚙️ 👆 🈸, 🖊 🔐 #️⃣ 👆 💽, ♒️. - -👥 🔜 ▶️ ⚪️➡️ 🌐❔ 👥 ◀️ ⏮️ 📃 & 📈 ⚫️. - -## 🔃 🥙 - -🥙 ⛓ "🎻 🕸 🤝". - -⚫️ 🐩 🚫 🎻 🎚 📏 💧 🎻 🍵 🚀. ⚫️ 👀 💖 👉: - -``` -eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c -``` - -⚫️ 🚫 🗜,, 🙆 💪 🛡 ℹ ⚪️➡️ 🎚. - -✋️ ⚫️ 🛑. , 🕐❔ 👆 📨 🤝 👈 👆 ♨, 👆 💪 ✔ 👈 👆 🤙 ♨ ⚫️. - -👈 🌌, 👆 💪 ✍ 🤝 ⏮️ 👔, ➡️ 💬, 1️⃣ 🗓️. & ⤴️ 🕐❔ 👩‍💻 👟 🔙 ⏭ 📆 ⏮️ 🤝, 👆 💭 👈 👩‍💻 🕹 👆 ⚙️. - -⏮️ 🗓️, 🤝 🔜 🕛 & 👩‍💻 🔜 🚫 ✔ & 🔜 ✔️ 🛑 🔄 🤚 🆕 🤝. & 🚥 👩‍💻 (⚖️ 🥉 🥳) 🔄 🔀 🤝 🔀 👔, 👆 🔜 💪 🔎 ⚫️, ↩️ 💳 🔜 🚫 🏏. - -🚥 👆 💚 🤾 ⏮️ 🥙 🤝 & 👀 ❔ 👫 👷, ✅ https://jwt.io. - -## ❎ `python-jose` - -👥 💪 ❎ `python-jose` 🏗 & ✔ 🥙 🤝 🐍: - -
- -```console -$ pip install "python-jose[cryptography]" - ----> 100% -``` - -
- -🐍-🇩🇬 🚚 🔐 👩‍💻 ➕. - -📥 👥 ⚙️ 👍 1️⃣: )/⚛. - -/// tip - -👉 🔰 ⏪ ⚙️ PyJWT. - -✋️ ⚫️ ℹ ⚙️ 🐍-🇩🇬 ↩️ ⚫️ 🚚 🌐 ⚒ ⚪️➡️ PyJWT ➕ ➕ 👈 👆 💪 💪 ⏪ 🕐❔ 🏗 🛠️ ⏮️ 🎏 🧰. - -/// - -## 🔐 🔁 - -"🔁" ⛓ 🏭 🎚 (🔐 👉 💼) 🔘 🔁 🔢 (🎻) 👈 👀 💖 🙃. - -🕐❔ 👆 🚶‍♀️ ⚫️❔ 🎏 🎚 (⚫️❔ 🎏 🔐) 👆 🤚 ⚫️❔ 🎏 🙃. - -✋️ 👆 🚫🔜 🗜 ⚪️➡️ 🙃 🔙 🔐. - -### ⚫️❔ ⚙️ 🔐 🔁 - -🚥 👆 💽 📎, 🧙‍♀ 🏆 🚫 ✔️ 👆 👩‍💻' 🔢 🔐, 🕴#️⃣. - -, 🧙‍♀ 🏆 🚫 💪 🔄 ⚙️ 👈 🔐 ➕1️⃣ ⚙️ (📚 👩‍💻 ⚙️ 🎏 🔐 🌐, 👉 🔜 ⚠). - -## ❎ `passlib` - -🇸🇲 👑 🐍 📦 🍵 🔐#️⃣. - -⚫️ 🐕‍🦺 📚 🔐 🔁 📊 & 🚙 👷 ⏮️ 👫. - -👍 📊 "🐡". - -, ❎ 🇸🇲 ⏮️ 🐡: - -
- -```console -$ pip install "passlib[bcrypt]" - ----> 100% -``` - -
- -/// tip - -⏮️ `passlib`, 👆 💪 🔗 ⚫️ 💪 ✍ 🔐 ✍ **✳**, **🏺** 💂‍♂ 🔌-⚖️ 📚 🎏. - -, 👆 🔜 💪, 🖼, 💰 🎏 📊 ⚪️➡️ ✳ 🈸 💽 ⏮️ FastAPI 🈸. ⚖️ 📉 ↔ ✳ 🈸 ⚙️ 🎏 💽. - - & 👆 👩‍💻 🔜 💪 💳 ⚪️➡️ 👆 ✳ 📱 ⚖️ ⚪️➡️ 👆 **FastAPI** 📱, 🎏 🕰. - -/// - -## #️⃣ & ✔ 🔐 - -🗄 🧰 👥 💪 ⚪️➡️ `passlib`. - -✍ 🇸🇲 "🔑". 👉 ⚫️❔ 🔜 ⚙️ #️⃣ & ✔ 🔐. - -/// tip - -🇸🇲 🔑 ✔️ 🛠️ ⚙️ 🎏 🔁 📊, 🔌 😢 🗝 🕐 🕴 ✔ ✔ 👫, ♒️. - -🖼, 👆 💪 ⚙️ ⚫️ ✍ & ✔ 🔐 🏗 ➕1️⃣ ⚙️ (💖 ✳) ✋️ #️⃣ 🙆 🆕 🔐 ⏮️ 🎏 📊 💖 🐡. - - & 🔗 ⏮️ 🌐 👫 🎏 🕰. - -/// - -✍ 🚙 🔢 #️⃣ 🔐 👟 ⚪️➡️ 👩‍💻. - -& ➕1️⃣ 🚙 ✔ 🚥 📨 🔐 🏏 #️⃣ 🏪. - -& ➕1️⃣ 1️⃣ 🔓 & 📨 👩‍💻. - -{* ../../docs_src/security/tutorial004.py hl[7,48,55:56,59:60,69:75] *} - -/// note - -🚥 👆 ✅ 🆕 (❌) 💽 `fake_users_db`, 👆 🔜 👀 ❔ #️⃣ 🔐 👀 💖 🔜: `"$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW"`. - -/// - -## 🍵 🥙 🤝 - -🗄 🕹 ❎. - -✍ 🎲 ㊙ 🔑 👈 🔜 ⚙️ 🛑 🥙 🤝. - -🏗 🔐 🎲 ㊙ 🔑 ⚙️ 📋: - -
- -```console -$ openssl rand -hex 32 - -09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7 -``` - -
- -& 📁 🔢 🔢 `SECRET_KEY` (🚫 ⚙️ 1️⃣ 🖼). - -✍ 🔢 `ALGORITHM` ⏮️ 📊 ⚙️ 🛑 🥙 🤝 & ⚒ ⚫️ `"HS256"`. - -✍ 🔢 👔 🤝. - -🔬 Pydantic 🏷 👈 🔜 ⚙️ 🤝 🔗 📨. - -✍ 🚙 🔢 🏗 🆕 🔐 🤝. - -{* ../../docs_src/security/tutorial004.py hl[6,12:14,28:30,78:86] *} - -## ℹ 🔗 - -ℹ `get_current_user` 📨 🎏 🤝 ⏭, ✋️ 👉 🕰, ⚙️ 🥙 🤝. - -🔣 📨 🤝, ✔ ⚫️, & 📨 ⏮️ 👩‍💻. - -🚥 🤝 ❌, 📨 🇺🇸🔍 ❌ ▶️️ ↖️. - -{* ../../docs_src/security/tutorial004.py hl[89:106] *} - -## ℹ `/token` *➡ 🛠️* - -✍ `timedelta` ⏮️ 👔 🕰 🤝. - -✍ 🎰 🥙 🔐 🤝 & 📨 ⚫️. - -{* ../../docs_src/security/tutorial004.py hl[115:130] *} - -### 📡 ℹ 🔃 🥙 "📄" `sub` - -🥙 🔧 💬 👈 📤 🔑 `sub`, ⏮️ 📄 🤝. - -⚫️ 📦 ⚙️ ⚫️, ✋️ 👈 🌐❔ 👆 🔜 🚮 👩‍💻 🆔, 👥 ⚙️ ⚫️ 📥. - -🥙 5️⃣📆 ⚙️ 🎏 👜 ↖️ ⚪️➡️ ⚖ 👩‍💻 & 🤝 👫 🎭 🛠️ 🔗 🔛 👆 🛠️. - -🖼, 👆 💪 🔬 "🚘" ⚖️ "📰 🏤". - -⤴️ 👆 💪 🚮 ✔ 🔃 👈 👨‍💼, 💖 "💾" (🚘) ⚖️ "✍" (📰). - -& ⤴️, 👆 💪 🤝 👈 🥙 🤝 👩‍💻 (⚖️ 🤖), & 👫 💪 ⚙️ ⚫️ 🎭 👈 🎯 (💾 🚘, ⚖️ ✍ 📰 🏤) 🍵 💆‍♂ ✔️ 🏧, ⏮️ 🥙 🤝 👆 🛠️ 🏗 👈. - -⚙️ 👫 💭, 🥙 💪 ⚙️ 🌌 🌖 🤓 😐. - -📚 💼, 📚 👈 👨‍💼 💪 ✔️ 🎏 🆔, ➡️ 💬 `foo` (👩‍💻 `foo`, 🚘 `foo`, & 📰 🏤 `foo`). - -, ❎ 🆔 💥, 🕐❔ 🏗 🥙 🤝 👩‍💻, 👆 💪 🔡 💲 `sub` 🔑, ✅ ⏮️ `username:`. , 👉 🖼, 💲 `sub` 💪 ✔️: `username:johndoe`. - -⚠ 👜 ✔️ 🤯 👈 `sub` 🔑 🔜 ✔️ 😍 🆔 🤭 🎂 🈸, & ⚫️ 🔜 🎻. - -## ✅ ⚫️ - -🏃 💽 & 🚶 🩺: http://127.0.0.1:8000/docs. - -👆 🔜 👀 👩‍💻 🔢 💖: - - - -✔ 🈸 🎏 🌌 ⏭. - -⚙️ 🎓: - -🆔: `johndoe` -🔐: `secret` - -/// check - -👀 👈 🕳 📟 🔢 🔐 "`secret`", 👥 🕴 ✔️ #️⃣ ⏬. - -/// - - - -🤙 🔗 `/users/me/`, 👆 🔜 🤚 📨: - -```JSON -{ - "username": "johndoe", - "email": "johndoe@example.com", - "full_name": "John Doe", - "disabled": false -} -``` - - - -🚥 👆 📂 👩‍💻 🧰, 👆 💪 👀 ❔ 📊 📨 🕴 🔌 🤝, 🔐 🕴 📨 🥇 📨 🔓 👩‍💻 & 🤚 👈 🔐 🤝, ✋️ 🚫 ⏮️: - - - -/// note - -👀 🎚 `Authorization`, ⏮️ 💲 👈 ▶️ ⏮️ `Bearer `. - -/// - -## 🏧 ⚙️ ⏮️ `scopes` - -Oauth2️⃣ ✔️ 🔑 "↔". - -👆 💪 ⚙️ 👫 🚮 🎯 ⚒ ✔ 🥙 🤝. - -⤴️ 👆 💪 🤝 👉 🤝 👩‍💻 🔗 ⚖️ 🥉 🥳, 🔗 ⏮️ 👆 🛠️ ⏮️ ⚒ 🚫. - -👆 💪 💡 ❔ ⚙️ 👫 & ❔ 👫 🛠️ 🔘 **FastAPI** ⏪ **🏧 👩‍💻 🦮**. - -## 🌃 - -⏮️ ⚫️❔ 👆 ✔️ 👀 🆙 🔜, 👆 💪 ⚒ 🆙 🔐 **FastAPI** 🈸 ⚙️ 🐩 💖 Oauth2️⃣ & 🥙. - -🌖 🙆 🛠️ 🚚 💂‍♂ ▶️️ 👍 🏗 📄 🔜. - -📚 📦 👈 📉 ⚫️ 📚 ✔️ ⚒ 📚 ⚠ ⏮️ 💽 🏷, 💽, & 💪 ⚒. & 👉 📦 👈 📉 👜 💁‍♂️ 🌅 🤙 ✔️ 💂‍♂ ⚠ 🔘. - ---- - -**FastAPI** 🚫 ⚒ 🙆 ⚠ ⏮️ 🙆 💽, 💽 🏷 ⚖️ 🧰. - -⚫️ 🤝 👆 🌐 💪 ⚒ 🕐 👈 👖 👆 🏗 🏆. - -& 👆 💪 ⚙️ 🔗 📚 👍 🚧 & 🛎 ⚙️ 📦 💖 `passlib` & `python-jose`, ↩️ **FastAPI** 🚫 🚚 🙆 🏗 🛠️ 🛠️ 🔢 📦. - -✋️ ⚫️ 🚚 👆 🧰 📉 🛠️ 🌅 💪 🍵 🎯 💪, ⚖, ⚖️ 💂‍♂. - -& 👆 💪 ⚙️ & 🛠️ 🔐, 🐩 🛠️, 💖 Oauth2️⃣ 📶 🙅 🌌. - -👆 💪 💡 🌅 **🏧 👩‍💻 🦮** 🔃 ❔ ⚙️ Oauth2️⃣ "↔", 🌖 👌-🧽 ✔ ⚙️, 📄 👫 🎏 🐩. Oauth2️⃣ ⏮️ ↔ 🛠️ ⚙️ 📚 🦏 🤝 🐕‍🦺, 💖 👱📔, 🇺🇸🔍, 📂, 🤸‍♂, 👱📔, ♒️. ✔ 🥉 🥳 🈸 🔗 ⏮️ 👫 🔗 🔛 👨‍💼 👫 👩‍💻. diff --git a/docs/em/docs/tutorial/security/simple-oauth2.md b/docs/em/docs/tutorial/security/simple-oauth2.md deleted file mode 100644 index 1fd513d48d..0000000000 --- a/docs/em/docs/tutorial/security/simple-oauth2.md +++ /dev/null @@ -1,289 +0,0 @@ -# 🙅 Oauth2️⃣ ⏮️ 🔐 & 📨 - -🔜 ➡️ 🏗 ⚪️➡️ ⏮️ 📃 & 🚮 ❌ 🍕 ✔️ 🏁 💂‍♂ 💧. - -## 🤚 `username` & `password` - -👥 🔜 ⚙️ **FastAPI** 💂‍♂ 🚙 🤚 `username` & `password`. - -Oauth2️⃣ ✔ 👈 🕐❔ ⚙️ "🔐 💧" (👈 👥 ⚙️) 👩‍💻/👩‍💻 🔜 📨 `username` & `password` 🏑 📨 💽. - -& 🔌 💬 👈 🏑 ✔️ 🌟 💖 👈. `user-name` ⚖️ `email` 🚫🔜 👷. - -✋️ 🚫 😟, 👆 💪 🎦 ⚫️ 👆 🎋 👆 🏁 👩‍💻 🕸. - -& 👆 💽 🏷 💪 ⚙️ 🙆 🎏 📛 👆 💚. - -✋️ 💳 *➡ 🛠️*, 👥 💪 ⚙️ 👉 📛 🔗 ⏮️ 🔌 (& 💪, 🖼, ⚙️ 🛠️ 🛠️ 🧾 ⚙️). - -🔌 🇵🇸 👈 `username` & `password` 🔜 📨 📨 💽 (, 🙅‍♂ 🎻 📥). - -### `scope` - -🔌 💬 👈 👩‍💻 💪 📨 ➕1️⃣ 📨 🏑 "`scope`". - -📨 🏑 📛 `scope` (⭐), ✋️ ⚫️ 🤙 📏 🎻 ⏮️ "↔" 🎏 🚀. - -🔠 "↔" 🎻 (🍵 🚀). - -👫 🛎 ⚙️ 📣 🎯 💂‍♂ ✔, 🖼: - -* `users:read` ⚖️ `users:write` ⚠ 🖼. -* `instagram_basic` ⚙️ 👱📔 / 👱📔. -* `https://www.googleapis.com/auth/drive` ⚙️ 🇺🇸🔍. - -/// info - -Oauth2️⃣ "↔" 🎻 👈 📣 🎯 ✔ ✔. - -⚫️ 🚫 🤔 🚥 ⚫️ ✔️ 🎏 🦹 💖 `:` ⚖️ 🚥 ⚫️ 📛. - -👈 ℹ 🛠️ 🎯. - -Oauth2️⃣ 👫 🎻. - -/// - -## 📟 🤚 `username` & `password` - -🔜 ➡️ ⚙️ 🚙 🚚 **FastAPI** 🍵 👉. - -### `OAuth2PasswordRequestForm` - -🥇, 🗄 `OAuth2PasswordRequestForm`, & ⚙️ ⚫️ 🔗 ⏮️ `Depends` *➡ 🛠️* `/token`: - -{* ../../docs_src/security/tutorial003.py hl[4,76] *} - -`OAuth2PasswordRequestForm` 🎓 🔗 👈 📣 📨 💪 ⏮️: - -* `username`. -* `password`. -* 📦 `scope` 🏑 🦏 🎻, ✍ 🎻 🎏 🚀. -* 📦 `grant_type`. - -/// tip - -Oauth2️⃣ 🔌 🤙 *🚚* 🏑 `grant_type` ⏮️ 🔧 💲 `password`, ✋️ `OAuth2PasswordRequestForm` 🚫 🛠️ ⚫️. - -🚥 👆 💪 🛠️ ⚫️, ⚙️ `OAuth2PasswordRequestFormStrict` ↩️ `OAuth2PasswordRequestForm`. - -/// - -* 📦 `client_id` (👥 🚫 💪 ⚫️ 👆 🖼). -* 📦 `client_secret` (👥 🚫 💪 ⚫️ 👆 🖼). - -/// info - -`OAuth2PasswordRequestForm` 🚫 🎁 🎓 **FastAPI** `OAuth2PasswordBearer`. - -`OAuth2PasswordBearer` ⚒ **FastAPI** 💭 👈 ⚫️ 💂‍♂ ⚖. ⚫️ 🚮 👈 🌌 🗄. - -✋️ `OAuth2PasswordRequestForm` 🎓 🔗 👈 👆 💪 ✔️ ✍ 👆, ⚖️ 👆 💪 ✔️ 📣 `Form` 🔢 🔗. - -✋️ ⚫️ ⚠ ⚙️ 💼, ⚫️ 🚚 **FastAPI** 🔗, ⚒ ⚫️ ⏩. - -/// - -### ⚙️ 📨 💽 - -/// tip - -👐 🔗 🎓 `OAuth2PasswordRequestForm` 🏆 🚫 ✔️ 🔢 `scope` ⏮️ 📏 🎻 👽 🚀, ↩️, ⚫️ 🔜 ✔️ `scopes` 🔢 ⏮️ ☑ 📇 🎻 🔠 ↔ 📨. - -👥 🚫 ⚙️ `scopes` 👉 🖼, ✋️ 🛠️ 📤 🚥 👆 💪 ⚫️. - -/// - -🔜, 🤚 👩‍💻 📊 ⚪️➡️ (❌) 💽, ⚙️ `username` ⚪️➡️ 📨 🏑. - -🚥 📤 🙅‍♂ ✅ 👩‍💻, 👥 📨 ❌ 💬 "❌ 🆔 ⚖️ 🔐". - -❌, 👥 ⚙️ ⚠ `HTTPException`: - -{* ../../docs_src/security/tutorial003.py hl[3,77:79] *} - -### ✅ 🔐 - -👉 ☝ 👥 ✔️ 👩‍💻 📊 ⚪️➡️ 👆 💽, ✋️ 👥 🚫 ✅ 🔐. - -➡️ 🚮 👈 💽 Pydantic `UserInDB` 🏷 🥇. - -👆 🔜 🙅 🖊 🔢 🔐,, 👥 🔜 ⚙️ (❌) 🔐 🔁 ⚙️. - -🚥 🔐 🚫 🏏, 👥 📨 🎏 ❌. - -#### 🔐 🔁 - -"🔁" ⛓: 🏭 🎚 (🔐 👉 💼) 🔘 🔁 🔢 (🎻) 👈 👀 💖 🙃. - -🕐❔ 👆 🚶‍♀️ ⚫️❔ 🎏 🎚 (⚫️❔ 🎏 🔐) 👆 🤚 ⚫️❔ 🎏 🙃. - -✋️ 👆 🚫🔜 🗜 ⚪️➡️ 🙃 🔙 🔐. - -##### ⚫️❔ ⚙️ 🔐 🔁 - -🚥 👆 💽 📎, 🧙‍♀ 🏆 🚫 ✔️ 👆 👩‍💻' 🔢 🔐, 🕴#️⃣. - -, 🧙‍♀ 🏆 🚫 💪 🔄 ⚙️ 👈 🎏 🔐 ➕1️⃣ ⚙️ (📚 👩‍💻 ⚙️ 🎏 🔐 🌐, 👉 🔜 ⚠). - -{* ../../docs_src/security/tutorial003.py hl[80:83] *} - -#### 🔃 `**user_dict` - -`UserInDB(**user_dict)` ⛓: - -*🚶‍♀️ 🔑 & 💲 `user_dict` 🔗 🔑-💲 ❌, 🌓:* - -```Python -UserInDB( - username = user_dict["username"], - email = user_dict["email"], - full_name = user_dict["full_name"], - disabled = user_dict["disabled"], - hashed_password = user_dict["hashed_password"], -) -``` - -/// info - -🌅 🏁 🔑 `**👩‍💻_ #️⃣ ` ✅ 🔙 [🧾 **➕ 🏷**](../extra-models.md#user_indict){.internal-link target=_blank}. - -/// - -## 📨 🤝 - -📨 `token` 🔗 🔜 🎻 🎚. - -⚫️ 🔜 ✔️ `token_type`. 👆 💼, 👥 ⚙️ "📨" 🤝, 🤝 🆎 🔜 "`bearer`". - -& ⚫️ 🔜 ✔️ `access_token`, ⏮️ 🎻 ⚗ 👆 🔐 🤝. - -👉 🙅 🖼, 👥 🔜 🍕 😟 & 📨 🎏 `username` 🤝. - -/// tip - -⏭ 📃, 👆 🔜 👀 🎰 🔐 🛠️, ⏮️ 🔐 #️⃣ & 🥙 🤝. - -✋️ 🔜, ➡️ 🎯 🔛 🎯 ℹ 👥 💪. - -/// - -{* ../../docs_src/security/tutorial003.py hl[85] *} - -/// tip - -🔌, 👆 🔜 📨 🎻 ⏮️ `access_token` & `token_type`, 🎏 👉 🖼. - -👉 🕳 👈 👆 ✔️ 👆 👆 📟, & ⚒ 💭 👆 ⚙️ 📚 🎻 🔑. - -⚫️ 🌖 🕴 👜 👈 👆 ✔️ 💭 ☑ 👆, 🛠️ ⏮️ 🔧. - -🎂, **FastAPI** 🍵 ⚫️ 👆. - -/// - -## ℹ 🔗 - -🔜 👥 🔜 ℹ 👆 🔗. - -👥 💚 🤚 `current_user` *🕴* 🚥 👉 👩‍💻 🦁. - -, 👥 ✍ 🌖 🔗 `get_current_active_user` 👈 🔄 ⚙️ `get_current_user` 🔗. - -👯‍♂️ 👉 🔗 🔜 📨 🇺🇸🔍 ❌ 🚥 👩‍💻 🚫 🔀, ⚖️ 🚥 🔕. - -, 👆 🔗, 👥 🔜 🕴 🤚 👩‍💻 🚥 👩‍💻 🔀, ☑ 🔓, & 🦁: - -{* ../../docs_src/security/tutorial003.py hl[58:66,69:72,90] *} - -/// info - -🌖 🎚 `WWW-Authenticate` ⏮️ 💲 `Bearer` 👥 🛬 📥 🍕 🔌. - -🙆 🇺🇸🔍 (❌) 👔 📟 4️⃣0️⃣1️⃣ "⛔" 🤔 📨 `WWW-Authenticate` 🎚. - -💼 📨 🤝 (👆 💼), 💲 👈 🎚 🔜 `Bearer`. - -👆 💪 🤙 🚶 👈 ➕ 🎚 & ⚫️ 🔜 👷. - -✋️ ⚫️ 🚚 📥 🛠️ ⏮️ 🔧. - -, 📤 5️⃣📆 🧰 👈 ⌛ & ⚙️ ⚫️ (🔜 ⚖️ 🔮) & 👈 💪 ⚠ 👆 ⚖️ 👆 👩‍💻, 🔜 ⚖️ 🔮. - -👈 💰 🐩... - -/// - -## 👀 ⚫️ 🎯 - -📂 🎓 🩺: http://127.0.0.1:8000/docs. - -### 🔓 - -🖊 "✔" 🔼. - -⚙️ 🎓: - -👩‍💻: `johndoe` - -🔐: `secret` - - - -⏮️ 🔗 ⚙️, 👆 🔜 👀 ⚫️ 💖: - - - -### 🤚 👆 👍 👩‍💻 💽 - -🔜 ⚙️ 🛠️ `GET` ⏮️ ➡ `/users/me`. - -👆 🔜 🤚 👆 👩‍💻 📊, 💖: - -```JSON -{ - "username": "johndoe", - "email": "johndoe@example.com", - "full_name": "John Doe", - "disabled": false, - "hashed_password": "fakehashedsecret" -} -``` - - - -🚥 👆 🖊 🔒 ℹ & ⏏, & ⤴️ 🔄 🎏 🛠️ 🔄, 👆 🔜 🤚 🇺🇸🔍 4️⃣0️⃣1️⃣ ❌: - -```JSON -{ - "detail": "Not authenticated" -} -``` - -### 🔕 👩‍💻 - -🔜 🔄 ⏮️ 🔕 👩‍💻, 🔓 ⏮️: - -👩‍💻: `alice` - -🔐: `secret2` - -& 🔄 ⚙️ 🛠️ `GET` ⏮️ ➡ `/users/me`. - -👆 🔜 🤚 "🔕 👩‍💻" ❌, 💖: - -```JSON -{ - "detail": "Inactive user" -} -``` - -## 🌃 - -👆 🔜 ✔️ 🧰 🛠️ 🏁 💂‍♂ ⚙️ ⚓️ 🔛 `username` & `password` 👆 🛠️. - -⚙️ 👫 🧰, 👆 💪 ⚒ 💂‍♂ ⚙️ 🔗 ⏮️ 🙆 💽 & ⏮️ 🙆 👩‍💻 ⚖️ 💽 🏷. - -🕴 ℹ ❌ 👈 ⚫️ 🚫 🤙 "🔐". - -⏭ 📃 👆 🔜 👀 ❔ ⚙️ 🔐 🔐 🔁 🗃 & 🥙 🤝. diff --git a/docs/em/docs/tutorial/static-files.md b/docs/em/docs/tutorial/static-files.md deleted file mode 100644 index 27685c06d9..0000000000 --- a/docs/em/docs/tutorial/static-files.md +++ /dev/null @@ -1,40 +0,0 @@ -# 🎻 📁 - -👆 💪 🍦 🎻 📁 🔁 ⚪️➡️ 📁 ⚙️ `StaticFiles`. - -## ⚙️ `StaticFiles` - -* 🗄 `StaticFiles`. -* "🗻" `StaticFiles()` 👐 🎯 ➡. - -{* ../../docs_src/static_files/tutorial001.py hl[2,6] *} - -/// note | 📡 ℹ - -👆 💪 ⚙️ `from starlette.staticfiles import StaticFiles`. - -**FastAPI** 🚚 🎏 `starlette.staticfiles` `fastapi.staticfiles` 🏪 👆, 👩‍💻. ✋️ ⚫️ 🤙 👟 🔗 ⚪️➡️ 💃. - -/// - -### ⚫️❔ "🗜" - -"🗜" ⛓ ❎ 🏁 "🔬" 🈸 🎯 ➡, 👈 ⤴️ ✊ 💅 🚚 🌐 🎧-➡. - -👉 🎏 ⚪️➡️ ⚙️ `APIRouter` 🗻 🈸 🍕 🔬. 🗄 & 🩺 ⚪️➡️ 👆 👑 🈸 🏆 🚫 🔌 🕳 ⚪️➡️ 🗻 🈸, ♒️. - -👆 💪 ✍ 🌅 🔃 👉 **🏧 👩‍💻 🦮**. - -## ℹ - -🥇 `"/static"` 🔗 🎧-➡ 👉 "🎧-🈸" 🔜 "🗻" 🔛. , 🙆 ➡ 👈 ▶️ ⏮️ `"/static"` 🔜 🍵 ⚫️. - -`directory="static"` 🔗 📛 📁 👈 🔌 👆 🎻 📁. - -`name="static"` 🤝 ⚫️ 📛 👈 💪 ⚙️ 🔘 **FastAPI**. - -🌐 👫 🔢 💪 🎏 🌘 "`static`", 🔆 👫 ⏮️ 💪 & 🎯 ℹ 👆 👍 🈸. - -## 🌅 ℹ - -🌖 ℹ & 🎛 ✅ 💃 🩺 🔃 🎻 📁. diff --git a/docs/em/docs/tutorial/testing.md b/docs/em/docs/tutorial/testing.md deleted file mode 100644 index 2e4a531f7d..0000000000 --- a/docs/em/docs/tutorial/testing.md +++ /dev/null @@ -1,185 +0,0 @@ -# 🔬 - -👏 💃, 🔬 **FastAPI** 🈸 ⏩ & 😌. - -⚫️ ⚓️ 🔛 🇸🇲, ❔ 🔄 🏗 ⚓️ 🔛 📨, ⚫️ 📶 😰 & 🏋️. - -⏮️ ⚫️, 👆 💪 ⚙️ 🔗 ⏮️ **FastAPI**. - -## ⚙️ `TestClient` - -/// info - -⚙️ `TestClient`, 🥇 ❎ `httpx`. - -🤶 Ⓜ. `pip install httpx`. - -/// - -🗄 `TestClient`. - -✍ `TestClient` 🚶‍♀️ 👆 **FastAPI** 🈸 ⚫️. - -✍ 🔢 ⏮️ 📛 👈 ▶️ ⏮️ `test_` (👉 🐩 `pytest` 🏛). - -⚙️ `TestClient` 🎚 🎏 🌌 👆 ⏮️ `httpx`. - -✍ 🙅 `assert` 📄 ⏮️ 🐩 🐍 🧬 👈 👆 💪 ✅ (🔄, 🐩 `pytest`). - -{* ../../docs_src/app_testing/tutorial001.py hl[2,12,15:18] *} - -/// tip - -👀 👈 🔬 🔢 😐 `def`, 🚫 `async def`. - - & 🤙 👩‍💻 😐 🤙, 🚫 ⚙️ `await`. - -👉 ✔ 👆 ⚙️ `pytest` 🔗 🍵 🤢. - -/// - -/// note | 📡 ℹ - -👆 💪 ⚙️ `from starlette.testclient import TestClient`. - -**FastAPI** 🚚 🎏 `starlette.testclient` `fastapi.testclient` 🏪 👆, 👩‍💻. ✋️ ⚫️ 👟 🔗 ⚪️➡️ 💃. - -/// - -/// tip - -🚥 👆 💚 🤙 `async` 🔢 👆 💯 ↖️ ⚪️➡️ 📨 📨 👆 FastAPI 🈸 (✅ 🔁 💽 🔢), ✔️ 👀 [🔁 💯](../advanced/async-tests.md){.internal-link target=_blank} 🏧 🔰. - -/// - -## 🎏 💯 - -🎰 🈸, 👆 🎲 🔜 ✔️ 👆 💯 🎏 📁. - -& 👆 **FastAPI** 🈸 5️⃣📆 ✍ 📚 📁/🕹, ♒️. - -### **FastAPI** 📱 📁 - -➡️ 💬 👆 ✔️ 📁 📊 🔬 [🦏 🈸](bigger-applications.md){.internal-link target=_blank}: - -``` -. -├── app -│   ├── __init__.py -│   └── main.py -``` - -📁 `main.py` 👆 ✔️ 👆 **FastAPI** 📱: - - -{* ../../docs_src/app_testing/main.py *} - -### 🔬 📁 - -⤴️ 👆 💪 ✔️ 📁 `test_main.py` ⏮️ 👆 💯. ⚫️ 💪 🖖 🔛 🎏 🐍 📦 (🎏 📁 ⏮️ `__init__.py` 📁): - -``` hl_lines="5" -. -├── app -│   ├── __init__.py -│   ├── main.py -│   └── test_main.py -``` - -↩️ 👉 📁 🎏 📦, 👆 💪 ⚙️ ⚖ 🗄 🗄 🎚 `app` ⚪️➡️ `main` 🕹 (`main.py`): - -{* ../../docs_src/app_testing/test_main.py hl[3] *} - -...& ✔️ 📟 💯 💖 ⏭. - -## 🔬: ↔ 🖼 - -🔜 ➡️ ↔ 👉 🖼 & 🚮 🌖 ℹ 👀 ❔ 💯 🎏 🍕. - -### ↔ **FastAPI** 📱 📁 - -➡️ 😣 ⏮️ 🎏 📁 📊 ⏭: - -``` -. -├── app -│   ├── __init__.py -│   ├── main.py -│   └── test_main.py -``` - -➡️ 💬 👈 🔜 📁 `main.py` ⏮️ 👆 **FastAPI** 📱 ✔️ 🎏 **➡ 🛠️**. - -⚫️ ✔️ `GET` 🛠️ 👈 💪 📨 ❌. - -⚫️ ✔️ `POST` 🛠️ 👈 💪 📨 📚 ❌. - -👯‍♂️ *➡ 🛠️* 🚚 `X-Token` 🎚. - -{* ../../docs_src/app_testing/app_b/main.py *} - -### ↔ 🔬 📁 - -👆 💪 ⤴️ ℹ `test_main.py` ⏮️ ↔ 💯: - -{* ../../docs_src/app_testing/app_b/test_main.py *} - -🕐❔ 👆 💪 👩‍💻 🚶‍♀️ ℹ 📨 & 👆 🚫 💭 ❔, 👆 💪 🔎 (🇺🇸🔍) ❔ ⚫️ `httpx`, ⚖️ ❔ ⚫️ ⏮️ `requests`, 🇸🇲 🔧 ⚓️ 🔛 📨' 🔧. - -⤴️ 👆 🎏 👆 💯. - -🤶 Ⓜ.: - -* 🚶‍♀️ *➡* ⚖️ *🔢* 🔢, 🚮 ⚫️ 📛 ⚫️. -* 🚶‍♀️ 🎻 💪, 🚶‍♀️ 🐍 🎚 (✅ `dict`) 🔢 `json`. -* 🚥 👆 💪 📨 *📨 💽* ↩️ 🎻, ⚙️ `data` 🔢 ↩️. -* 🚶‍♀️ *🎚*, ⚙️ `dict` `headers` 🔢. -* *🍪*, `dict` `cookies` 🔢. - -🌖 ℹ 🔃 ❔ 🚶‍♀️ 💽 👩‍💻 (⚙️ `httpx` ⚖️ `TestClient`) ✅ 🇸🇲 🧾. - -/// info - -🗒 👈 `TestClient` 📨 💽 👈 💪 🗜 🎻, 🚫 Pydantic 🏷. - -🚥 👆 ✔️ Pydantic 🏷 👆 💯 & 👆 💚 📨 🚮 💽 🈸 ⏮️ 🔬, 👆 💪 ⚙️ `jsonable_encoder` 🔬 [🎻 🔗 🔢](encoder.md){.internal-link target=_blank}. - -/// - -## 🏃 ⚫️ - -⏮️ 👈, 👆 💪 ❎ `pytest`: - -
- -```console -$ pip install pytest - ----> 100% -``` - -
- -⚫️ 🔜 🔍 📁 & 💯 🔁, 🛠️ 👫, & 📄 🏁 🔙 👆. - -🏃 💯 ⏮️: - -
- -```console -$ pytest - -================ test session starts ================ -platform linux -- Python 3.6.9, pytest-5.3.5, py-1.8.1, pluggy-0.13.1 -rootdir: /home/user/code/superawesome-cli/app -plugins: forked-1.1.3, xdist-1.31.0, cov-2.8.1 -collected 6 items - ----> 100% - -test_main.py ...... [100%] - -================= 1 passed in 0.03s ================= -``` - -
diff --git a/docs/em/mkdocs.yml b/docs/em/mkdocs.yml deleted file mode 100644 index de18856f44..0000000000 --- a/docs/em/mkdocs.yml +++ /dev/null @@ -1 +0,0 @@ -INHERIT: ../en/mkdocs.yml diff --git a/docs/en/data/contributors.yml b/docs/en/data/contributors.yml index 592c79af0e..0c144cd4ce 100644 --- a/docs/en/data/contributors.yml +++ b/docs/en/data/contributors.yml @@ -1,23 +1,28 @@ tiangolo: login: tiangolo - count: 794 + count: 857 avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=cb5d06e73a9e1998141b1641aa88e443c6717651&v=4 url: https://github.com/tiangolo dependabot: login: dependabot - count: 126 + count: 130 avatarUrl: https://avatars.githubusercontent.com/in/29110?v=4 url: https://github.com/apps/dependabot alejsdev: login: alejsdev - count: 52 - avatarUrl: https://avatars.githubusercontent.com/u/90076947?u=447d12a1b347f466b35378bee4c7104cc9b2c571&v=4 + count: 53 + avatarUrl: https://avatars.githubusercontent.com/u/90076947?u=85ceac49fb87138aebe8d663912e359447329090&v=4 url: https://github.com/alejsdev pre-commit-ci: login: pre-commit-ci - count: 49 + count: 50 avatarUrl: https://avatars.githubusercontent.com/in/68672?v=4 url: https://github.com/apps/pre-commit-ci +YuriiMotov: + login: YuriiMotov + count: 36 + avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=b9b13d598dddfab529a52d264df80a900bfe7060&v=4 + url: https://github.com/YuriiMotov github-actions: login: github-actions count: 26 @@ -33,14 +38,14 @@ dmontagu: count: 17 avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=540f30c937a6450812628b9592a1dfe91bbe148e&v=4 url: https://github.com/dmontagu -YuriiMotov: - login: YuriiMotov - count: 15 - avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=b9b13d598dddfab529a52d264df80a900bfe7060&v=4 - url: https://github.com/YuriiMotov +svlandeg: + login: svlandeg + count: 16 + avatarUrl: https://avatars.githubusercontent.com/u/8796347?u=556c97650c27021911b0b9447ec55e75987b0e8a&v=4 + url: https://github.com/svlandeg nilslindemann: login: nilslindemann - count: 14 + count: 15 avatarUrl: https://avatars.githubusercontent.com/u/6892179?u=1dca6a22195d6cd1ab20737c0e19a4c55d639472&v=4 url: https://github.com/nilslindemann euri10: @@ -48,11 +53,6 @@ euri10: count: 13 avatarUrl: https://avatars.githubusercontent.com/u/1104190?u=321a2e953e6645a7d09b732786c7a8061e0f8a8b&v=4 url: https://github.com/euri10 -svlandeg: - login: svlandeg - count: 13 - avatarUrl: https://avatars.githubusercontent.com/u/8796347?u=556c97650c27021911b0b9447ec55e75987b0e8a&v=4 - url: https://github.com/svlandeg kantandane: login: kantandane count: 13 @@ -103,6 +103,11 @@ waynerv: count: 5 avatarUrl: https://avatars.githubusercontent.com/u/39515546?u=ec35139777597cdbbbddda29bf8b9d4396b429a9&v=4 url: https://github.com/waynerv +musicinmybrain: + login: musicinmybrain + count: 5 + avatarUrl: https://avatars.githubusercontent.com/u/6898909?u=9010312053e7141383b9bdf538036c7f37fbaba0&v=4 + url: https://github.com/musicinmybrain krishnamadhavan: login: krishnamadhavan count: 5 @@ -133,11 +138,6 @@ iudeen: count: 4 avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=f09cdd745e5bf16138f29b42732dd57c7f02bee1&v=4 url: https://github.com/iudeen -musicinmybrain: - login: musicinmybrain - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/6898909?u=9010312053e7141383b9bdf538036c7f37fbaba0&v=4 - url: https://github.com/musicinmybrain philipokiokio: login: philipokiokio count: 4 @@ -483,6 +483,11 @@ nzig: count: 2 avatarUrl: https://avatars.githubusercontent.com/u/7372858?u=e769add36ed73c778cdb136eb10bf96b1e119671&v=4 url: https://github.com/nzig +kristjanvalur: + login: kristjanvalur + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/6009543?u=1419f20bbfff8f031be8cb470962e7e62de2595e&v=4 + url: https://github.com/kristjanvalur yezz123: login: yezz123 count: 2 @@ -548,6 +553,11 @@ DanielKusyDev: count: 2 avatarUrl: https://avatars.githubusercontent.com/u/36250676?u=2ea6114ff751fc48b55f231987a0e2582c6b1bd2&v=4 url: https://github.com/DanielKusyDev +Viicos: + login: Viicos + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/65306057?u=fcd677dc1b9bef12aa103613e5ccb3f8ce305af9&v=4 + url: https://github.com/Viicos DanielYang59: login: DanielYang59 count: 2 diff --git a/docs/en/data/external_links.yml b/docs/en/data/external_links.yml deleted file mode 100644 index b8a5fdb3a0..0000000000 --- a/docs/en/data/external_links.yml +++ /dev/null @@ -1,422 +0,0 @@ -Articles: - English: - - author: Apitally - author_link: https://apitally.io - link: https://apitally.io/blog/getting-started-with-logging-in-fastapi - title: Getting started with logging in FastAPI - - author: Balthazar Rouberol - author_link: https://balthazar-rouberol.com - link: https://blog.balthazar-rouberol.com/how-to-profile-a-fastapi-asynchronous-request - title: How to profile a FastAPI asynchronous request - - author: Stephen Siegert - Neon - link: https://neon.tech/blog/deploy-a-serverless-fastapi-app-with-neon-postgres-and-aws-app-runner-at-any-scale - title: Deploy a Serverless FastAPI App with Neon Postgres and AWS App Runner at any scale - - author: Kurtis Pykes - NVIDIA - link: https://developer.nvidia.com/blog/building-a-machine-learning-microservice-with-fastapi/ - title: Building a Machine Learning Microservice with FastAPI - - author: Ravgeet Dhillon - Twilio - link: https://www.twilio.com/en-us/blog/booking-appointments-twilio-notion-fastapi - title: Booking Appointments with Twilio, Notion, and FastAPI - - author: Abhinav Tripathi - Microsoft Blogs - link: https://devblogs.microsoft.com/cosmosdb/azure-cosmos-db-python-and-fastapi/ - title: Write a Python data layer with Azure Cosmos DB and FastAPI - - author: Donny Peeters - author_link: https://github.com/Donnype - link: https://bitestreams.com/blog/fastapi-sqlalchemy/ - title: 10 Tips for adding SQLAlchemy to FastAPI - - author: Jessica Temporal - author_link: https://jtemporal.com/socials - link: https://jtemporal.com/tips-on-migrating-from-flask-to-fastapi-and-vice-versa/ - title: Tips on migrating from Flask to FastAPI and vice-versa - - author: Ankit Anchlia - author_link: https://linkedin.com/in/aanchlia21 - link: https://hackernoon.com/explore-how-to-effectively-use-jwt-with-fastapi - title: Explore How to Effectively Use JWT With FastAPI - - author: Nicoló Lino - author_link: https://www.nlino.com - link: https://github.com/softwarebloat/python-tracing-demo - title: Instrument FastAPI with OpenTelemetry tracing and visualize traces in Grafana Tempo. - - author: Mikhail Rozhkov, Elena Samuylova - author_link: https://www.linkedin.com/in/mnrozhkov/ - link: https://www.evidentlyai.com/blog/fastapi-tutorial - title: ML serving and monitoring with FastAPI and Evidently - - author: Visual Studio Code Team - author_link: https://code.visualstudio.com/ - link: https://code.visualstudio.com/docs/python/tutorial-fastapi - title: FastAPI Tutorial in Visual Studio Code - - author: Apitally - author_link: https://apitally.io - link: https://blog.apitally.io/fastapi-application-monitoring-made-easy - title: FastAPI application monitoring made easy - - author: John Philip - author_link: https://medium.com/@amjohnphilip - link: https://python.plainenglish.io/building-a-restful-api-with-fastapi-secure-signup-and-login-functionality-included-45cdbcb36106 - title: "Building a RESTful API with FastAPI: Secure Signup and Login Functionality Included" - - author: Keshav Malik - author_link: https://theinfosecguy.xyz/ - link: https://blog.theinfosecguy.xyz/building-a-crud-api-with-fastapi-and-supabase-a-step-by-step-guide - title: Building a CRUD API with FastAPI and Supabase - - author: Adejumo Ridwan Suleiman - author_link: https://www.linkedin.com/in/adejumoridwan/ - link: https://medium.com/python-in-plain-english/build-an-sms-spam-classifier-serverless-database-with-faunadb-and-fastapi-23dbb275bc5b - title: Build an SMS Spam Classifier Serverless Database with FaunaDB and FastAPI - - author: Raf Rasenberg - author_link: https://rafrasenberg.com/about/ - link: https://rafrasenberg.com/fastapi-lambda/ - title: 'FastAPI lambda container: serverless simplified' - - author: Teresa N. Fontanella De Santis - author_link: https://dev.to/ - link: https://dev.to/teresafds/authorization-on-fastapi-with-casbin-41og - title: Authorization on FastAPI with Casbin - - author: New Relic - author_link: https://newrelic.com - link: https://newrelic.com/instant-observability/fastapi/e559ec64-f765-4470-a15f-1901fcebb468 - title: How to monitor FastAPI application performance using Python agent - - author: Jean-Baptiste Rocher - author_link: https://hashnode.com/@jibrocher - link: https://dev.indooroutdoor.io/series/fastapi-react-poll-app - title: Building the Poll App From Django Tutorial With FastAPI And React - - author: Silvan Melchior - author_link: https://github.com/silvanmelchior - link: https://blog.devgenius.io/seamless-fastapi-configuration-with-confz-90949c14ea12 - title: Seamless FastAPI Configuration with ConfZ - - author: Kaustubh Gupta - author_link: https://medium.com/@kaustubhgupta1828/ - link: https://levelup.gitconnected.com/5-advance-features-of-fastapi-you-should-try-7c0ac7eebb3e - title: 5 Advanced Features of FastAPI You Should Try - - author: Kaustubh Gupta - author_link: https://medium.com/@kaustubhgupta1828/ - link: https://www.analyticsvidhya.com/blog/2021/06/deploying-ml-models-as-api-using-fastapi-and-heroku/ - title: Deploying ML Models as API Using FastAPI and Heroku - - link: https://jarmos.netlify.app/posts/using-github-actions-to-deploy-a-fastapi-project-to-heroku/ - title: Using GitHub Actions to Deploy a FastAPI Project to Heroku - author_link: https://jarmos.netlify.app/ - author: Somraj Saha - - author: "@pystar" - author_link: https://pystar.substack.com/ - link: https://pystar.substack.com/p/how-to-create-a-fake-certificate - title: How to Create A Fake Certificate Authority And Generate TLS Certs for FastAPI - - author: Ben Gamble - author_link: https://uk.linkedin.com/in/bengamble7 - link: https://ably.com/blog/realtime-ticket-booking-solution-kafka-fastapi-ably - title: Building a realtime ticket booking solution with Kafka, FastAPI, and Ably - - author: Shahriyar(Shako) Rzayev - author_link: https://www.linkedin.com/in/shahriyar-rzayev/ - link: https://www.azepug.az/posts/fastapi/#building-simple-e-commerce-with-nuxtjs-and-fastapi-series - title: Building simple E-Commerce with NuxtJS and FastAPI - - author: Rodrigo Arenas - author_link: https://rodrigo-arenas.medium.com/ - link: https://medium.com/analytics-vidhya/serve-a-machine-learning-model-using-sklearn-fastapi-and-docker-85aabf96729b - title: "Serve a machine learning model using Sklearn, FastAPI and Docker" - - author: Yashasvi Singh - author_link: https://hashnode.com/@aUnicornDev - link: https://aunicorndev.hashnode.dev/series/supafast-api - title: "Building an API with FastAPI and Supabase and Deploying on Deta" - - author: Navule Pavan Kumar Rao - author_link: https://www.linkedin.com/in/navule/ - link: https://www.tutlinks.com/deploy-fastapi-on-ubuntu-gunicorn-caddy-2/ - title: Deploy FastAPI on Ubuntu and Serve using Caddy 2 Web Server - - author: Patrick Ladon - author_link: https://dev.to/factorlive - link: https://dev.to/factorlive/python-facebook-messenger-webhook-with-fastapi-on-glitch-4n90 - title: Python Facebook messenger webhook with FastAPI on Glitch - - author: Valon Januzaj - author_link: https://www.linkedin.com/in/valon-januzaj-b02692187/ - link: https://valonjanuzaj.medium.com/deploy-a-dockerized-fastapi-application-to-aws-cc757830ba1b - title: Deploy a dockerized FastAPI application to AWS - - author: Amit Chaudhary - author_link: https://x.com/amitness - link: https://amitness.com/2020/06/fastapi-vs-flask/ - title: FastAPI for Flask Users - - author: Louis Guitton - author_link: https://x.com/louis_guitton - link: https://guitton.co/posts/fastapi-monitoring/ - title: How to monitor your FastAPI service - - author: Precious Ndubueze - author_link: https://medium.com/@gabbyprecious2000 - link: https://medium.com/@gabbyprecious2000/creating-a-crud-app-with-fastapi-part-one-7c049292ad37 - title: Creating a CRUD App with FastAPI (Part one) - - author: Farhad Malik - author_link: https://medium.com/@farhadmalik - link: https://towardsdatascience.com/build-and-host-fast-data-science-applications-using-fastapi-823be8a1d6a0 - title: Build And Host Fast Data Science Applications Using FastAPI - - author: Navule Pavan Kumar Rao - author_link: https://www.linkedin.com/in/navule/ - link: https://www.tutlinks.com/deploy-fastapi-on-azure/ - title: Deploy FastAPI on Azure App Service - - author: Davide Fiocco - author_link: https://github.com/davidefiocco - link: https://davidefiocco.github.io/streamlit-fastapi-ml-serving/ - title: Machine learning model serving in Python using FastAPI and streamlit - - author: Netflix - author_link: https://netflixtechblog.com/ - link: https://netflixtechblog.com/introducing-dispatch-da4b8a2a8072 - title: Introducing Dispatch - - author: Stavros Korokithakis - author_link: https://x.com/Stavros - link: https://www.stavros.io/posts/fastapi-with-django/ - title: Using FastAPI with Django - - author: Twilio - author_link: https://www.twilio.com - link: https://www.twilio.com/blog/build-secure-twilio-webhook-python-fastapi - title: Build a Secure Twilio Webhook with Python and FastAPI - - author: Sebastián Ramírez (tiangolo) - author_link: https://x.com/tiangolo - link: https://dev.to/tiangolo/build-a-web-api-from-scratch-with-fastapi-the-workshop-2ehe - title: Build a web API from scratch with FastAPI - the workshop - - author: Paul Sec - author_link: https://x.com/PaulWebSec - link: https://paulsec.github.io/posts/fastapi_plus_zeit_serverless_fu/ - title: FastAPI + Zeit.co = 🚀 - - author: cuongld2 - author_link: https://dev.to/cuongld2 - link: https://dev.to/cuongld2/build-simple-api-service-with-python-fastapi-part-1-581o - title: Build simple API service with Python FastAPI — Part 1 - - author: Paurakh Sharma Humagain - author_link: https://x.com/PaurakhSharma - link: https://dev.to/paurakhsharma/microservice-in-python-using-fastapi-24cc - title: Microservice in Python using FastAPI - - author: Guillermo Cruz - author_link: https://wuilly.com/ - link: https://wuilly.com/2019/10/real-time-notifications-with-python-and-postgres/ - title: Real-time Notifications with Python and Postgres - - author: Navule Pavan Kumar Rao - author_link: https://www.linkedin.com/in/navule/ - link: https://www.tutlinks.com/create-and-deploy-fastapi-app-to-heroku/ - title: Create and Deploy FastAPI app to Heroku without using Docker - - author: Arthur Henrique - author_link: https://x.com/arthurheinrique - link: https://medium.com/@arthur393/another-boilerplate-to-fastapi-azure-pipeline-ci-pytest-3c8d9a4be0bb - title: 'Another Boilerplate to FastAPI: Azure Pipeline CI + Pytest' - - author: Shane Soh - author_link: https://medium.com/@shane.soh - link: https://medium.com/analytics-vidhya/deploy-machine-learning-models-with-keras-fastapi-redis-and-docker-4940df614ece - title: Deploy Machine Learning Models with Keras, FastAPI, Redis and Docker - - author: Mandy Gu - author_link: https://towardsdatascience.com/@mandygu - link: https://towardsdatascience.com/deploying-iris-classifications-with-fastapi-and-docker-7c9b83fdec3a - title: 'Towards Data Science: Deploying Iris Classifications with FastAPI and Docker' - - author: Michael Herman - author_link: https://testdriven.io/authors/herman - link: https://testdriven.io/blog/fastapi-crud/ - title: 'TestDriven.io: Developing and Testing an Asynchronous API with FastAPI and Pytest' - - author: Bernard Brenyah - author_link: https://medium.com/@bbrenyah - link: https://medium.com/python-data/how-to-deploy-tensorflow-2-0-models-as-an-api-service-with-fastapi-docker-128b177e81f3 - title: How To Deploy Tensorflow 2.0 Models As An API Service With FastAPI & Docker - - author: Dylan Anthony - author_link: https://dev.to/dbanty - link: https://dev.to/dbanty/why-i-m-leaving-flask-3ki6 - title: Why I'm Leaving Flask - - author: Mike Moritz - author_link: https://medium.com/@mike.p.moritz - link: https://medium.com/@mike.p.moritz/using-docker-compose-to-deploy-a-lightweight-python-rest-api-with-a-job-queue-37e6072a209b - title: Using Docker Compose to deploy a lightweight Python REST API with a job queue - - author: '@euri10' - author_link: https://gitlab.com/euri10 - link: https://gitlab.com/euri10/fastapi_cheatsheet - title: A FastAPI and Swagger UI visual cheatsheet - - author: Uber Engineering - author_link: https://eng.uber.com - link: https://eng.uber.com/ludwig-v0-2/ - title: 'Uber: Ludwig v0.2 Adds New Features and Other Improvements to its Deep Learning Toolbox [including a FastAPI server]' - - author: Maarten Grootendorst - author_link: https://www.linkedin.com/in/mgrootendorst/ - link: https://towardsdatascience.com/how-to-deploy-a-machine-learning-model-dc51200fe8cf - title: How to Deploy a Machine Learning Model - - author: Johannes Gontrum - author_link: https://x.com/gntrm - link: https://medium.com/@gntrm/jwt-authentication-with-fastapi-and-aws-cognito-1333f7f2729e - title: JWT Authentication with FastAPI and AWS Cognito - - author: Ankush Thakur - author_link: https://geekflare.com/author/ankush/ - link: https://geekflare.com/python-asynchronous-web-frameworks/ - title: Top 5 Asynchronous Web Frameworks for Python - - author: Nico Axtmann - author_link: https://www.linkedin.com/in/nico-axtmann - link: https://medium.com/@nico.axtmann95/deploying-a-scikit-learn-model-with-onnx-und-fastapi-1af398268915 - title: Deploying a scikit-learn model with ONNX and FastAPI - - author: Nils de Bruin - author_link: https://medium.com/@nilsdebruin - link: https://medium.com/data-rebels/fastapi-authentication-revisited-enabling-api-key-authentication-122dc5975680 - title: 'FastAPI authentication revisited: Enabling API key authentication' - - author: Nick Cortale - author_link: https://nickc1.github.io/ - link: https://nickc1.github.io/api,/scikit-learn/2019/01/10/scikit-fastapi.html - title: 'FastAPI and Scikit-Learn: Easily Deploy Models' - - author: Errieta Kostala - author_link: https://dev.to/errietta - link: https://dev.to/errietta/introduction-to-the-fastapi-python-framework-2n10 - title: Introduction to the fastapi python framework - - author: Nils de Bruin - author_link: https://medium.com/@nilsdebruin - link: https://medium.com/data-rebels/fastapi-how-to-add-basic-and-cookie-authentication-a45c85ef47d3 - title: FastAPI — How to add basic and cookie authentication - - author: Nils de Bruin - author_link: https://medium.com/@nilsdebruin - link: https://medium.com/data-rebels/fastapi-google-as-an-external-authentication-provider-3a527672cf33 - title: FastAPI — Google as an external authentication provider - - author: William Hayes - author_link: https://medium.com/@williamhayes - link: https://medium.com/@williamhayes/fastapi-starlette-debug-vs-prod-5f7561db3a59 - title: FastAPI/Starlette debug vs prod - - author: Mukul Mantosh - author_link: https://x.com/MantoshMukul - link: https://www.jetbrains.com/pycharm/guide/tutorials/fastapi-aws-kubernetes/ - title: Developing FastAPI Application using K8s & AWS - - author: KrishNa - author_link: https://medium.com/@krishnardt365 - link: https://medium.com/@krishnardt365/fastapi-docker-and-postgres-91943e71be92 - title: Fastapi, Docker(Docker compose) and Postgres - - author: Devon Ray - author_link: https://devonray.com - link: https://devonray.com/blog/deploying-a-fastapi-project-using-aws-lambda-aurora-cdk - title: Deployment using Docker, Lambda, Aurora, CDK & GH Actions - - author: Shubhendra Kushwaha - author_link: https://www.linkedin.com/in/theshubhendra/ - link: https://theshubhendra.medium.com/mastering-soft-delete-advanced-sqlalchemy-techniques-4678f4738947 - title: 'Mastering Soft Delete: Advanced SQLAlchemy Techniques' - - author: Shubhendra Kushwaha - author_link: https://www.linkedin.com/in/theshubhendra/ - link: https://theshubhendra.medium.com/role-based-row-filtering-advanced-sqlalchemy-techniques-733e6b1328f6 - title: 'Role based row filtering: Advanced SQLAlchemy Techniques' - German: - - author: Marcel Sander (actidoo) - author_link: https://www.actidoo.com - link: https://www.actidoo.com/de/blog/python-fastapi-domain-driven-design - title: Domain-driven Design mit Python und FastAPI - - author: Nico Axtmann - author_link: https://x.com/_nicoax - link: https://blog.codecentric.de/2019/08/inbetriebnahme-eines-scikit-learn-modells-mit-onnx-und-fastapi/ - title: Inbetriebnahme eines scikit-learn-Modells mit ONNX und FastAPI - - author: Felix Schürmeyer - author_link: https://hellocoding.de/autor/felix-schuermeyer/ - link: https://hellocoding.de/blog/coding-language/python/fastapi - title: REST-API Programmieren mittels Python und dem FastAPI Modul - Japanese: - - author: '@bee2' - author_link: https://qiita.com/bee2 - link: https://qiita.com/bee2/items/75d9c0d7ba20e7a4a0e9 - title: '[FastAPI] Python製のASGI Web フレームワーク FastAPIに入門する' - - author: '@bee2' - author_link: https://qiita.com/bee2 - link: https://qiita.com/bee2/items/0ad260ab9835a2087dae - title: PythonのWeb frameworkのパフォーマンス比較 (Django, Flask, responder, FastAPI, japronto) - - author: ライトコードメディア編集部 - author_link: https://rightcode.co.jp/author/jun - link: https://rightcode.co.jp/blog/information-technology/fastapi-tutorial-todo-apps-admin-page-improvement - title: '【第4回】FastAPIチュートリアル: toDoアプリを作ってみよう【管理者ページ改良編】' - - author: ライトコードメディア編集部 - author_link: https://rightcode.co.jp/author/jun - link: https://rightcode.co.jp/blog/information-technology/fastapi-tutorial-todo-apps-authentication-user-registration - title: '【第3回】FastAPIチュートリアル: toDoアプリを作ってみよう【認証・ユーザ登録編】' - - author: ライトコードメディア編集部 - author_link: https://rightcode.co.jp/author/jun - link: https://rightcode.co.jp/blog/information-technology/fastapi-tutorial-todo-apps-model-building - title: '【第2回】FastAPIチュートリアル: ToDoアプリを作ってみよう【モデル構築編】' - - author: ライトコードメディア編集部 - author_link: https://rightcode.co.jp/author/jun - link: https://rightcode.co.jp/blog/information-technology/fastapi-tutorial-todo-apps-environment - title: '【第1回】FastAPIチュートリアル: ToDoアプリを作ってみよう【環境構築編】' - - author: Hikaru Takahashi - author_link: https://qiita.com/hikarut - link: https://qiita.com/hikarut/items/b178af2e2440c67c6ac4 - title: フロントエンド開発者向けのDockerによるPython開発環境構築 - - author: '@angel_katayoku' - author_link: https://qiita.com/angel_katayoku - link: https://qiita.com/angel_katayoku/items/8a458a8952f50b73f420 - title: FastAPIでPOSTされたJSONのレスポンスbodyを受け取る - - author: '@angel_katayoku' - author_link: https://qiita.com/angel_katayoku - link: https://qiita.com/angel_katayoku/items/4fbc1a4e2b33fa2237d2 - title: FastAPIをMySQLと接続してDockerで管理してみる - - author: '@angel_katayoku' - author_link: https://qiita.com/angel_katayoku - link: https://qiita.com/angel_katayoku/items/0e1f5dbbe62efc612a78 - title: FastAPIでCORSを回避 - - author: '@ryoryomaru' - author_link: https://qiita.com/ryoryomaru - link: https://qiita.com/ryoryomaru/items/59958ed385b3571d50de - title: python製の最新APIフレームワーク FastAPI を触ってみた - - author: '@mtitg' - author_link: https://qiita.com/mtitg - link: https://qiita.com/mtitg/items/47770e9a562dd150631d - title: FastAPI|DB接続してCRUDするPython製APIサーバーを構築 - Portuguese: - - author: Eduardo Mendes - author_link: https://bolha.us/@dunossauro - link: https://fastapidozero.dunossauro.com/ - title: FastAPI do ZERO - - author: Jessica Temporal - author_link: https://jtemporal.com/socials - link: https://jtemporal.com/dicas-para-migrar-de-flask-para-fastapi-e-vice-versa/ - title: Dicas para migrar uma aplicação de Flask para FastAPI e vice-versa - Russian: - - author: Troy Köhler - author_link: https://www.linkedin.com/in/trkohler/ - link: https://trkohler.com/fast-api-introduction-to-framework - title: 'FastAPI: знакомимся с фреймворком' - - author: prostomarkeloff - author_link: https://github.com/prostomarkeloff - link: https://habr.com/ru/post/478620/ - title: Почему Вы должны попробовать FastAPI? - - author: Andrey Korchak - author_link: https://habr.com/ru/users/57uff3r/ - link: https://habr.com/ru/post/454440/ - title: 'Мелкая питонячая радость #2: Starlette - Солидная примочка – FastAPI' - Vietnamese: - - author: Nguyễn Nhân - author_link: https://fullstackstation.com/author/figonking/ - link: https://fullstackstation.com/fastapi-trien-khai-bang-docker/ - title: 'FASTAPI: TRIỂN KHAI BẰNG DOCKER' - Taiwanese: - - author: Leon - author_link: http://editor.leonh.space/ - link: https://editor.leonh.space/2022/tortoise/ - title: 'Tortoise ORM / FastAPI 整合快速筆記' - Spanish: - - author: Eduardo Zepeda - author_link: https://coffeebytes.dev/en/authors/eduardo-zepeda/ - link: https://coffeebytes.dev/es/python-fastapi-el-mejor-framework-de-python/ - title: 'Tutorial de FastAPI, ¿el mejor framework de Python?' -Podcasts: - English: - - author: Real Python - author_link: https://realpython.com/ - link: https://realpython.com/podcasts/rpp/72/ - title: Starting With FastAPI and Examining Python's Import System - Episode 72 - - author: Python Bytes FM - author_link: https://pythonbytes.fm/ - link: https://www.pythonpodcast.com/fastapi-web-application-framework-episode-259/ - title: 'Do you dare to press "."? - Episode 247 - Dan #6: SQLModel - use the same models for SQL and FastAPI' - - author: Podcast.`__init__` - author_link: https://www.pythonpodcast.com/ - link: https://www.pythonpodcast.com/fastapi-web-application-framework-episode-259/ - title: Build The Next Generation Of Python Web Applications With FastAPI - Episode 259 - interview to Sebastían Ramírez (tiangolo) - - author: Python Bytes FM - author_link: https://pythonbytes.fm/ - link: https://pythonbytes.fm/episodes/show/123/time-to-right-the-py-wrongs?time_in_sec=855 - title: FastAPI on PythonBytes -Talks: - English: - - author: Jeny Sadadia - author_link: https://github.com/JenySadadia - link: https://www.youtube.com/watch?v=uZdTe8_Z6BQ - title: 'PyCon AU 2023: Testing asynchronous applications with FastAPI and pytest' - - author: Sebastián Ramírez (tiangolo) - author_link: https://x.com/tiangolo - link: https://www.youtube.com/watch?v=PnpTY1f4k2U - title: '[VIRTUAL] Py.Amsterdam''s flying Software Circus: Intro to FastAPI' - - author: Sebastián Ramírez (tiangolo) - author_link: https://x.com/tiangolo - link: https://www.youtube.com/watch?v=z9K5pwb0rt8 - title: 'PyConBY 2020: Serve ML models easily with FastAPI' - - author: Chris Withers - author_link: https://x.com/chriswithers13 - link: https://www.youtube.com/watch?v=3DLwPcrE5mA - title: 'PyCon UK 2019: FastAPI from the ground up' - Taiwanese: - - author: Blueswen - author_link: https://github.com/blueswen - link: https://www.youtube.com/watch?v=y3sumuoDq4w - title: 'PyCon TW 2024: 全方位強化 Python 服務可觀測性:以 FastAPI 和 Grafana Stack 為例' diff --git a/docs/en/data/github_sponsors.yml b/docs/en/data/github_sponsors.yml index 3d8ecdb7a9..971687d8a9 100644 --- a/docs/en/data/github_sponsors.yml +++ b/docs/en/data/github_sponsors.yml @@ -2,67 +2,58 @@ sponsors: - - login: renderinc avatarUrl: https://avatars.githubusercontent.com/u/36424661?v=4 url: https://github.com/renderinc - - login: andrew-propelauth - avatarUrl: https://avatars.githubusercontent.com/u/89474256?u=c98993dec8553c09d424ede67bbe86e5c35f48c9&v=4 - url: https://github.com/andrew-propelauth - - login: blockbee-io - avatarUrl: https://avatars.githubusercontent.com/u/115143449?u=1b8620c2d6567c4df2111a371b85a51f448f9b85&v=4 - url: https://github.com/blockbee-io - - login: zuplo - avatarUrl: https://avatars.githubusercontent.com/u/85497839?v=4 - url: https://github.com/zuplo - - login: coderabbitai - avatarUrl: https://avatars.githubusercontent.com/u/132028505?v=4 - url: https://github.com/coderabbitai - - login: greptileai - avatarUrl: https://avatars.githubusercontent.com/u/140149887?v=4 - url: https://github.com/greptileai - login: subtotal avatarUrl: https://avatars.githubusercontent.com/u/176449348?v=4 url: https://github.com/subtotal + - login: greptileai + avatarUrl: https://avatars.githubusercontent.com/u/140149887?v=4 + url: https://github.com/greptileai + - login: coderabbitai + avatarUrl: https://avatars.githubusercontent.com/u/132028505?v=4 + url: https://github.com/coderabbitai + - login: zuplo + avatarUrl: https://avatars.githubusercontent.com/u/85497839?v=4 + url: https://github.com/zuplo + - login: blockbee-io + avatarUrl: https://avatars.githubusercontent.com/u/115143449?u=1b8620c2d6567c4df2111a371b85a51f448f9b85&v=4 + url: https://github.com/blockbee-io + - login: andrew-propelauth + avatarUrl: https://avatars.githubusercontent.com/u/89474256?u=c98993dec8553c09d424ede67bbe86e5c35f48c9&v=4 + url: https://github.com/andrew-propelauth - login: railwayapp avatarUrl: https://avatars.githubusercontent.com/u/66716858?v=4 url: https://github.com/railwayapp - - login: scalar - avatarUrl: https://avatars.githubusercontent.com/u/301879?v=4 - url: https://github.com/scalar -- - login: dribia - avatarUrl: https://avatars.githubusercontent.com/u/41189616?v=4 - url: https://github.com/dribia - - login: svix - avatarUrl: https://avatars.githubusercontent.com/u/80175132?v=4 - url: https://github.com/svix +- - login: speakeasy-api + avatarUrl: https://avatars.githubusercontent.com/u/91446104?v=4 + url: https://github.com/speakeasy-api - login: stainless-api avatarUrl: https://avatars.githubusercontent.com/u/88061651?v=4 url: https://github.com/stainless-api - - login: speakeasy-api - avatarUrl: https://avatars.githubusercontent.com/u/91446104?v=4 - url: https://github.com/speakeasy-api - - login: databento - avatarUrl: https://avatars.githubusercontent.com/u/64141749?v=4 - url: https://github.com/databento + - login: svix + avatarUrl: https://avatars.githubusercontent.com/u/80175132?v=4 + url: https://github.com/svix - login: permitio avatarUrl: https://avatars.githubusercontent.com/u/71775833?v=4 url: https://github.com/permitio -- - login: BoostryJP - avatarUrl: https://avatars.githubusercontent.com/u/57932412?v=4 - url: https://github.com/BoostryJP - - login: mercedes-benz - avatarUrl: https://avatars.githubusercontent.com/u/34240465?v=4 - url: https://github.com/mercedes-benz + - login: databento + avatarUrl: https://avatars.githubusercontent.com/u/64141749?v=4 + url: https://github.com/databento +- - login: LambdaTest-Inc + avatarUrl: https://avatars.githubusercontent.com/u/171592363?u=96606606a45fa170427206199014f2a5a2a4920b&v=4 + url: https://github.com/LambdaTest-Inc - login: Ponte-Energy-Partners avatarUrl: https://avatars.githubusercontent.com/u/114745848?v=4 url: https://github.com/Ponte-Energy-Partners - - login: LambdaTest-Inc - avatarUrl: https://avatars.githubusercontent.com/u/171592363?u=96606606a45fa170427206199014f2a5a2a4920b&v=4 - url: https://github.com/LambdaTest-Inc - - login: requestly - avatarUrl: https://avatars.githubusercontent.com/u/12287519?v=4 - url: https://github.com/requestly + - login: BoostryJP + avatarUrl: https://avatars.githubusercontent.com/u/57932412?v=4 + url: https://github.com/BoostryJP - login: acsone avatarUrl: https://avatars.githubusercontent.com/u/7601056?v=4 url: https://github.com/acsone -- - login: Trivie +- - login: scalar + avatarUrl: https://avatars.githubusercontent.com/u/301879?v=4 + url: https://github.com/scalar + - login: Trivie avatarUrl: https://avatars.githubusercontent.com/u/8161763?v=4 url: https://github.com/Trivie - - login: takashi-yoneya @@ -77,36 +68,24 @@ sponsors: - - login: alixlahuec avatarUrl: https://avatars.githubusercontent.com/u/29543316?u=44357eb2a93bccf30fb9d389b8befe94a3d00985&v=4 url: https://github.com/alixlahuec - - login: Partho - avatarUrl: https://avatars.githubusercontent.com/u/2034301?u=ce195ac36835cca0cdfe6dd6e897bd38873a1524&v=4 - url: https://github.com/Partho - - login: primer-io avatarUrl: https://avatars.githubusercontent.com/u/62146168?v=4 url: https://github.com/primer-io - - login: xsalagarcia - avatarUrl: https://avatars.githubusercontent.com/u/66035908?v=4 - url: https://github.com/xsalagarcia - - login: upciti avatarUrl: https://avatars.githubusercontent.com/u/43346262?v=4 url: https://github.com/upciti - - login: GonnaFlyMethod - avatarUrl: https://avatars.githubusercontent.com/u/60840539?u=edf70b373fd4f1a83d3eb7c6802f4b6addb572cf&v=4 - url: https://github.com/GonnaFlyMethod - login: ChargeStorm avatarUrl: https://avatars.githubusercontent.com/u/26000165?v=4 url: https://github.com/ChargeStorm - - login: DanielYang59 - avatarUrl: https://avatars.githubusercontent.com/u/80093591?u=63873f701c7c74aac83c906800a1dddc0bc8c92f&v=4 - url: https://github.com/DanielYang59 + - login: ibrahimpelumi6142 + avatarUrl: https://avatars.githubusercontent.com/u/113442282?v=4 + url: https://github.com/ibrahimpelumi6142 - login: nilslindemann avatarUrl: https://avatars.githubusercontent.com/u/6892179?u=1dca6a22195d6cd1ab20737c0e19a4c55d639472&v=4 url: https://github.com/nilslindemann - - login: samuelcolvin avatarUrl: https://avatars.githubusercontent.com/u/4039449?u=42eb3b833047c8c4b4f647a031eaef148c16d93f&v=4 url: https://github.com/samuelcolvin - - login: vincentkoc - avatarUrl: https://avatars.githubusercontent.com/u/25068?u=fbd5b2d51142daa4bdbc21e21953a3b8b8188a4a&v=4 - url: https://github.com/vincentkoc - login: otosky avatarUrl: https://avatars.githubusercontent.com/u/42260747?u=69d089387c743d89427aa4ad8740cfb34045a9e0&v=4 url: https://github.com/otosky @@ -131,139 +110,130 @@ sponsors: - login: Leay15 avatarUrl: https://avatars.githubusercontent.com/u/32212558?u=c4aa9c1737e515959382a5515381757b1fd86c53&v=4 url: https://github.com/Leay15 - - login: Karine-Bauch - avatarUrl: https://avatars.githubusercontent.com/u/90465103?u=7feb1018abb1a5631cfd9a91fea723d1ceb5f49b&v=4 - url: https://github.com/Karine-Bauch - login: jugeeem avatarUrl: https://avatars.githubusercontent.com/u/116043716?u=ae590d79c38ac79c91b9c5caa6887d061e865a3d&v=4 url: https://github.com/jugeeem - - login: connorpark24 - avatarUrl: https://avatars.githubusercontent.com/u/142128990?u=09b84a4beb1f629b77287a837bcf3729785cdd89&v=4 - url: https://github.com/connorpark24 - - login: patsatsia - avatarUrl: https://avatars.githubusercontent.com/u/61111267?u=3271b85f7a37b479c8d0ae0a235182e83c166edf&v=4 - url: https://github.com/patsatsia - - login: anthonycepeda - avatarUrl: https://avatars.githubusercontent.com/u/72019805?u=60bdf46240cff8fca482ff0fc07d963fd5e1a27c&v=4 - url: https://github.com/anthonycepeda - - login: patricioperezv - avatarUrl: https://avatars.githubusercontent.com/u/73832292?u=5f471f156e19ee7920e62ae0f4a47b95580e61cf&v=4 - url: https://github.com/patricioperezv - - login: chickenandstats - avatarUrl: https://avatars.githubusercontent.com/u/79477966?u=ae2b894aa954070db1d7830dab99b49eba4e4567&v=4 - url: https://github.com/chickenandstats + - login: Karine-Bauch + avatarUrl: https://avatars.githubusercontent.com/u/90465103?u=7feb1018abb1a5631cfd9a91fea723d1ceb5f49b&v=4 + url: https://github.com/Karine-Bauch - login: kaoru0310 avatarUrl: https://avatars.githubusercontent.com/u/80977929?u=1b61d10142b490e56af932ddf08a390fae8ee94f&v=4 url: https://github.com/kaoru0310 - - login: DelfinaCare - avatarUrl: https://avatars.githubusercontent.com/u/83734439?v=4 - url: https://github.com/DelfinaCare - - login: knallgelb - avatarUrl: https://avatars.githubusercontent.com/u/2358812?u=c48cb6362b309d74cbf144bd6ad3aed3eb443e82&v=4 - url: https://github.com/knallgelb - - login: dblackrun - avatarUrl: https://avatars.githubusercontent.com/u/3528486?v=4 - url: https://github.com/dblackrun - - login: zsinx6 - avatarUrl: https://avatars.githubusercontent.com/u/3532625?u=ba75a5dc744d1116ccfeaaf30d41cb2fe81fe8dd&v=4 - url: https://github.com/zsinx6 - - login: kennywakeland - avatarUrl: https://avatars.githubusercontent.com/u/3631417?u=7c8f743f1ae325dfadea7c62bbf1abd6a824fc55&v=4 - url: https://github.com/kennywakeland - - login: aacayaco - avatarUrl: https://avatars.githubusercontent.com/u/3634801?u=eaadda178c964178fcb64886f6c732172c8f8219&v=4 - url: https://github.com/aacayaco - - login: anomaly - avatarUrl: https://avatars.githubusercontent.com/u/3654837?v=4 - url: https://github.com/anomaly - - login: mj0331 - avatarUrl: https://avatars.githubusercontent.com/u/3890353?u=1c627ac1a024515b4871de5c3ebbfaa1a57f65d4&v=4 - url: https://github.com/mj0331 - - login: gorhack - avatarUrl: https://avatars.githubusercontent.com/u/4141690?u=ec119ebc4bdf00a7bc84657a71aa17834f4f27f3&v=4 - url: https://github.com/gorhack - - login: Ryandaydev - avatarUrl: https://avatars.githubusercontent.com/u/4292423?u=679ff84cb7b988c5795a5fa583857f574a055763&v=4 - url: https://github.com/Ryandaydev - - login: jaredtrog - avatarUrl: https://avatars.githubusercontent.com/u/4381365?v=4 - url: https://github.com/jaredtrog + - login: chickenandstats + avatarUrl: https://avatars.githubusercontent.com/u/79477966?u=ae2b894aa954070db1d7830dab99b49eba4e4567&v=4 + url: https://github.com/chickenandstats + - login: patricioperezv + avatarUrl: https://avatars.githubusercontent.com/u/73832292?u=5f471f156e19ee7920e62ae0f4a47b95580e61cf&v=4 + url: https://github.com/patricioperezv + - login: anthonycepeda + avatarUrl: https://avatars.githubusercontent.com/u/72019805?u=60bdf46240cff8fca482ff0fc07d963fd5e1a27c&v=4 + url: https://github.com/anthonycepeda + - login: AalbatrossGuy + avatarUrl: https://avatars.githubusercontent.com/u/68378354?u=0bdeea9356d24f638244131f6d8d1e2d2f3601ca&v=4 + url: https://github.com/AalbatrossGuy + - login: patsatsia + avatarUrl: https://avatars.githubusercontent.com/u/61111267?u=3271b85f7a37b479c8d0ae0a235182e83c166edf&v=4 + url: https://github.com/patsatsia - login: oliverxchen avatarUrl: https://avatars.githubusercontent.com/u/4471774?u=534191f25e32eeaadda22dfab4b0a428733d5489&v=4 url: https://github.com/oliverxchen - - login: jstanden - avatarUrl: https://avatars.githubusercontent.com/u/63288?u=c3658d57d2862c607a0e19c2101c3c51876e36ad&v=4 - url: https://github.com/jstanden - - login: paulcwatts - avatarUrl: https://avatars.githubusercontent.com/u/150269?u=1819e145d573b44f0ad74b87206d21cd60331d4e&v=4 - url: https://github.com/paulcwatts - - login: robintw - avatarUrl: https://avatars.githubusercontent.com/u/296686?v=4 - url: https://github.com/robintw - - login: pamelafox - avatarUrl: https://avatars.githubusercontent.com/u/297042?v=4 - url: https://github.com/pamelafox - - login: wshayes - avatarUrl: https://avatars.githubusercontent.com/u/365303?u=07ca03c5ee811eb0920e633cc3c3db73dbec1aa5&v=4 - url: https://github.com/wshayes - - login: koxudaxi - avatarUrl: https://avatars.githubusercontent.com/u/630670?u=507d8577b4b3670546b449c4c2ccbc5af40d72f7&v=4 - url: https://github.com/koxudaxi - - login: falkben - avatarUrl: https://avatars.githubusercontent.com/u/653031?u=ad9838e089058c9e5a0bab94c0eec7cc181e0cd0&v=4 - url: https://github.com/falkben - - login: mintuhouse - avatarUrl: https://avatars.githubusercontent.com/u/769950?u=ecfbd79a97d33177e0d093ddb088283cf7fe8444&v=4 - url: https://github.com/mintuhouse + - login: jaredtrog + avatarUrl: https://avatars.githubusercontent.com/u/4381365?v=4 + url: https://github.com/jaredtrog + - login: Ryandaydev + avatarUrl: https://avatars.githubusercontent.com/u/4292423?u=679ff84cb7b988c5795a5fa583857f574a055763&v=4 + url: https://github.com/Ryandaydev + - login: gorhack + avatarUrl: https://avatars.githubusercontent.com/u/4141690?u=ec119ebc4bdf00a7bc84657a71aa17834f4f27f3&v=4 + url: https://github.com/gorhack + - login: mj0331 + avatarUrl: https://avatars.githubusercontent.com/u/3890353?u=1c627ac1a024515b4871de5c3ebbfaa1a57f65d4&v=4 + url: https://github.com/mj0331 + - login: anomaly + avatarUrl: https://avatars.githubusercontent.com/u/3654837?v=4 + url: https://github.com/anomaly + - login: aacayaco + avatarUrl: https://avatars.githubusercontent.com/u/3634801?u=eaadda178c964178fcb64886f6c732172c8f8219&v=4 + url: https://github.com/aacayaco + - login: kennywakeland + avatarUrl: https://avatars.githubusercontent.com/u/3631417?u=7c8f743f1ae325dfadea7c62bbf1abd6a824fc55&v=4 + url: https://github.com/kennywakeland + - login: zsinx6 + avatarUrl: https://avatars.githubusercontent.com/u/3532625?u=ba75a5dc744d1116ccfeaaf30d41cb2fe81fe8dd&v=4 + url: https://github.com/zsinx6 + - login: dblackrun + avatarUrl: https://avatars.githubusercontent.com/u/3528486?v=4 + url: https://github.com/dblackrun + - login: knallgelb + avatarUrl: https://avatars.githubusercontent.com/u/2358812?u=c48cb6362b309d74cbf144bd6ad3aed3eb443e82&v=4 + url: https://github.com/knallgelb - login: dodo5522 avatarUrl: https://avatars.githubusercontent.com/u/1362607?u=9bf1e0e520cccc547c046610c468ce6115bbcf9f&v=4 url: https://github.com/dodo5522 - - login: wdwinslow - avatarUrl: https://avatars.githubusercontent.com/u/11562137?u=371272f2c69e680e0559a7b0a57385e83a5dc728&v=4 - url: https://github.com/wdwinslow - - login: jsoques - avatarUrl: https://avatars.githubusercontent.com/u/12414216?u=620921d94196546cc8b9eae2cc4cbc3f95bab42f&v=4 - url: https://github.com/jsoques - - login: dannywade - avatarUrl: https://avatars.githubusercontent.com/u/13680237?u=418ee985bd41577b20fde81417fb2d901e875e8a&v=4 - url: https://github.com/dannywade - - login: khadrawy - avatarUrl: https://avatars.githubusercontent.com/u/13686061?u=59f25ef42ecf04c22657aac4238ce0e2d3d30304&v=4 - url: https://github.com/khadrawy - - login: mjohnsey - avatarUrl: https://avatars.githubusercontent.com/u/16784016?u=38fad2e6b411244560b3af99c5f5a4751bc81865&v=4 - url: https://github.com/mjohnsey - - login: enguy-hub - avatarUrl: https://avatars.githubusercontent.com/u/16822912?u=2c45f9e7f427b2f2f3b023d7fdb0d44764c92ae8&v=4 - url: https://github.com/enguy-hub - - login: ashi-agrawal - avatarUrl: https://avatars.githubusercontent.com/u/17105294?u=99c7a854035e5398d8e7b674f2d42baae6c957f8&v=4 - url: https://github.com/ashi-agrawal + - login: mintuhouse + avatarUrl: https://avatars.githubusercontent.com/u/769950?u=ecfbd79a97d33177e0d093ddb088283cf7fe8444&v=4 + url: https://github.com/mintuhouse + - login: falkben + avatarUrl: https://avatars.githubusercontent.com/u/653031?u=ad9838e089058c9e5a0bab94c0eec7cc181e0cd0&v=4 + url: https://github.com/falkben + - login: koxudaxi + avatarUrl: https://avatars.githubusercontent.com/u/630670?u=507d8577b4b3670546b449c4c2ccbc5af40d72f7&v=4 + url: https://github.com/koxudaxi + - login: wshayes + avatarUrl: https://avatars.githubusercontent.com/u/365303?u=07ca03c5ee811eb0920e633cc3c3db73dbec1aa5&v=4 + url: https://github.com/wshayes + - login: pamelafox + avatarUrl: https://avatars.githubusercontent.com/u/297042?v=4 + url: https://github.com/pamelafox + - login: robintw + avatarUrl: https://avatars.githubusercontent.com/u/296686?v=4 + url: https://github.com/robintw + - login: jstanden + avatarUrl: https://avatars.githubusercontent.com/u/63288?u=c3658d57d2862c607a0e19c2101c3c51876e36ad&v=4 + url: https://github.com/jstanden - login: RaamEEIL avatarUrl: https://avatars.githubusercontent.com/u/20320552?v=4 url: https://github.com/RaamEEIL - - login: ternaus - avatarUrl: https://avatars.githubusercontent.com/u/5481618?u=513a26b02a39e7a28d587cd37c6cc877ea368e6e&v=4 - url: https://github.com/ternaus - - login: eseglem - avatarUrl: https://avatars.githubusercontent.com/u/5920492?u=208d419cf667b8ac594c82a8db01932c7e50d057&v=4 - url: https://github.com/eseglem - - login: FernandoCelmer - avatarUrl: https://avatars.githubusercontent.com/u/6262214?u=58ba6d5888fa7f355934e52db19f950e20b38162&v=4 - url: https://github.com/FernandoCelmer - - login: Rehket - avatarUrl: https://avatars.githubusercontent.com/u/7015688?u=3afb0ba200feebbc7f958950e92db34df2a3c172&v=4 - url: https://github.com/Rehket + - login: ashi-agrawal + avatarUrl: https://avatars.githubusercontent.com/u/17105294?u=99c7a854035e5398d8e7b674f2d42baae6c957f8&v=4 + url: https://github.com/ashi-agrawal + - login: mjohnsey + avatarUrl: https://avatars.githubusercontent.com/u/16784016?u=38fad2e6b411244560b3af99c5f5a4751bc81865&v=4 + url: https://github.com/mjohnsey + - login: khadrawy + avatarUrl: https://avatars.githubusercontent.com/u/13686061?u=59f25ef42ecf04c22657aac4238ce0e2d3d30304&v=4 + url: https://github.com/khadrawy + - login: dannywade + avatarUrl: https://avatars.githubusercontent.com/u/13680237?u=418ee985bd41577b20fde81417fb2d901e875e8a&v=4 + url: https://github.com/dannywade + - login: jsoques + avatarUrl: https://avatars.githubusercontent.com/u/12414216?u=620921d94196546cc8b9eae2cc4cbc3f95bab42f&v=4 + url: https://github.com/jsoques + - login: wdwinslow + avatarUrl: https://avatars.githubusercontent.com/u/11562137?u=371272f2c69e680e0559a7b0a57385e83a5dc728&v=4 + url: https://github.com/wdwinslow - login: hiancdtrsnm avatarUrl: https://avatars.githubusercontent.com/u/7343177?v=4 url: https://github.com/hiancdtrsnm -- - login: manoelpqueiroz + - login: Rehket + avatarUrl: https://avatars.githubusercontent.com/u/7015688?u=3afb0ba200feebbc7f958950e92db34df2a3c172&v=4 + url: https://github.com/Rehket + - login: FernandoCelmer + avatarUrl: https://avatars.githubusercontent.com/u/6262214?u=58ba6d5888fa7f355934e52db19f950e20b38162&v=4 + url: https://github.com/FernandoCelmer + - login: eseglem + avatarUrl: https://avatars.githubusercontent.com/u/5920492?u=208d419cf667b8ac594c82a8db01932c7e50d057&v=4 + url: https://github.com/eseglem + - login: ternaus + avatarUrl: https://avatars.githubusercontent.com/u/5481618?u=513a26b02a39e7a28d587cd37c6cc877ea368e6e&v=4 + url: https://github.com/ternaus +- - login: Artur-Galstyan + avatarUrl: https://avatars.githubusercontent.com/u/63471891?u=e8691f386037e51a737cc0ba866cd8c89e5cf109&v=4 + url: https://github.com/Artur-Galstyan + - login: manoelpqueiroz avatarUrl: https://avatars.githubusercontent.com/u/23669137?u=b12e84b28a84369ab5b30bd5a79e5788df5a0756&v=4 url: https://github.com/manoelpqueiroz -- - login: ceb10n - avatarUrl: https://avatars.githubusercontent.com/u/235213?u=edcce471814a1eba9f0cdaa4cd0de18921a940a6&v=4 - url: https://github.com/ceb10n - - login: pawamoy +- - login: pawamoy avatarUrl: https://avatars.githubusercontent.com/u/3999221?u=b030e4c89df2f3a36bc4710b925bdeb6745c9856&v=4 url: https://github.com/pawamoy - login: siavashyj @@ -284,6 +254,9 @@ sponsors: - login: johnl28 avatarUrl: https://avatars.githubusercontent.com/u/54412955?u=47dd06082d1c39caa90c752eb55566e4f3813957&v=4 url: https://github.com/johnl28 + - login: danielunderwood + avatarUrl: https://avatars.githubusercontent.com/u/4472301?v=4 + url: https://github.com/danielunderwood - login: hoenie-ams avatarUrl: https://avatars.githubusercontent.com/u/25708487?u=cda07434f0509ac728d9edf5e681117c0f6b818b&v=4 url: https://github.com/hoenie-ams @@ -294,128 +267,122 @@ sponsors: avatarUrl: https://avatars.githubusercontent.com/u/33275230?u=eb223cad27017bb1e936ee9b429b450d092d0236&v=4 url: https://github.com/engineerjoe440 - login: bnkc - avatarUrl: https://avatars.githubusercontent.com/u/34930566?u=db5e6f4f87836cad26c2aa90ce390ce49041c5a9&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/34930566?u=4771ac4e64066f0847d40e5b29910adabd9b2372&v=4 url: https://github.com/bnkc - login: petercool avatarUrl: https://avatars.githubusercontent.com/u/37613029?u=75aa8c6729e6e8f85a300561c4dbeef9d65c8797&v=4 url: https://github.com/petercool - - login: PunRabbit - avatarUrl: https://avatars.githubusercontent.com/u/70463212?u=1a835cfbc99295a60c8282f6aa6199d1b42241a5&v=4 - url: https://github.com/PunRabbit - login: PelicanQ avatarUrl: https://avatars.githubusercontent.com/u/77930606?v=4 url: https://github.com/PelicanQ + - login: PunRabbit + avatarUrl: https://avatars.githubusercontent.com/u/70463212?u=1a835cfbc99295a60c8282f6aa6199d1b42241a5&v=4 + url: https://github.com/PunRabbit - login: my3 avatarUrl: https://avatars.githubusercontent.com/u/1825270?v=4 url: https://github.com/my3 - - login: danielunderwood - avatarUrl: https://avatars.githubusercontent.com/u/4472301?v=4 - url: https://github.com/danielunderwood - - login: rangulvers - avatarUrl: https://avatars.githubusercontent.com/u/5235430?u=e254d4af4ace5a05fa58372ae677c7d26f0d5a53&v=4 - url: https://github.com/rangulvers - - login: ddanier - avatarUrl: https://avatars.githubusercontent.com/u/113563?u=ed1dc79de72f93bd78581f88ebc6952b62f472da&v=4 - url: https://github.com/ddanier - - login: bryanculbertson - avatarUrl: https://avatars.githubusercontent.com/u/144028?u=defda4f90e93429221cc667500944abde60ebe4a&v=4 - url: https://github.com/bryanculbertson - - login: slafs - avatarUrl: https://avatars.githubusercontent.com/u/210173?v=4 - url: https://github.com/slafs - - login: tochikuji - avatarUrl: https://avatars.githubusercontent.com/u/851759?v=4 - url: https://github.com/tochikuji - - login: miguelgr - avatarUrl: https://avatars.githubusercontent.com/u/1484589?u=54556072b8136efa12ae3b6902032ea2a39ace4b&v=4 - url: https://github.com/miguelgr - login: WillHogan avatarUrl: https://avatars.githubusercontent.com/u/1661551?u=8a80356e3e7d5a417157aba7ea565dabc8678327&v=4 url: https://github.com/WillHogan - - login: hard-coders - avatarUrl: https://avatars.githubusercontent.com/u/9651103?u=95db33927bbff1ed1c07efddeb97ac2ff33068ed&v=4 - url: https://github.com/hard-coders - - login: mntolia - avatarUrl: https://avatars.githubusercontent.com/u/10390224?v=4 - url: https://github.com/mntolia - - login: Zuzah - avatarUrl: https://avatars.githubusercontent.com/u/10934846?u=1ef43e075ddc87bd1178372bf4d95ee6175cae27&v=4 - url: https://github.com/Zuzah - - login: TheR1D - avatarUrl: https://avatars.githubusercontent.com/u/16740832?u=b0dfdbdb27b79729430c71c6128962f77b7b53f7&v=4 - url: https://github.com/TheR1D - - login: joshuatz - avatarUrl: https://avatars.githubusercontent.com/u/17817563?u=f1bf05b690d1fc164218f0b420cdd3acb7913e21&v=4 - url: https://github.com/joshuatz + - login: miguelgr + avatarUrl: https://avatars.githubusercontent.com/u/1484589?u=54556072b8136efa12ae3b6902032ea2a39ace4b&v=4 + url: https://github.com/miguelgr + - login: tochikuji + avatarUrl: https://avatars.githubusercontent.com/u/851759?v=4 + url: https://github.com/tochikuji + - login: ceb10n + avatarUrl: https://avatars.githubusercontent.com/u/235213?u=edcce471814a1eba9f0cdaa4cd0de18921a940a6&v=4 + url: https://github.com/ceb10n + - login: slafs + avatarUrl: https://avatars.githubusercontent.com/u/210173?v=4 + url: https://github.com/slafs + - login: bryanculbertson + avatarUrl: https://avatars.githubusercontent.com/u/144028?u=defda4f90e93429221cc667500944abde60ebe4a&v=4 + url: https://github.com/bryanculbertson + - login: ddanier + avatarUrl: https://avatars.githubusercontent.com/u/113563?u=ed1dc79de72f93bd78581f88ebc6952b62f472da&v=4 + url: https://github.com/ddanier - login: nisutec avatarUrl: https://avatars.githubusercontent.com/u/25281462?u=e562484c451fdfc59053163f64405f8eb262b8b0&v=4 url: https://github.com/nisutec - - login: sdevkota - avatarUrl: https://avatars.githubusercontent.com/u/5250987?u=4ed9a120c89805a8aefda1cbdc0cf6512e64d1b4&v=4 - url: https://github.com/sdevkota - - login: Baghdady92 - avatarUrl: https://avatars.githubusercontent.com/u/5708590?v=4 - url: https://github.com/Baghdady92 - - login: KentShikama - avatarUrl: https://avatars.githubusercontent.com/u/6329898?u=8b236810db9b96333230430837e1f021f9246da1&v=4 - url: https://github.com/KentShikama - - login: katnoria - avatarUrl: https://avatars.githubusercontent.com/u/7674948?u=09767eb13e07e09496c5fee4e5ce21d9eac34a56&v=4 - url: https://github.com/katnoria - - login: harsh183 - avatarUrl: https://avatars.githubusercontent.com/u/7780198?v=4 - url: https://github.com/harsh183 - - login: moonape1226 - avatarUrl: https://avatars.githubusercontent.com/u/8532038?u=d9f8b855a429fff9397c3833c2ff83849ebf989d&v=4 - url: https://github.com/moonape1226 - - login: xncbf - avatarUrl: https://avatars.githubusercontent.com/u/9462045?u=a80a7bb349555b277645632ed66639ff43400614&v=4 - url: https://github.com/xncbf + - login: joshuatz + avatarUrl: https://avatars.githubusercontent.com/u/17817563?u=f1bf05b690d1fc164218f0b420cdd3acb7913e21&v=4 + url: https://github.com/joshuatz + - login: TheR1D + avatarUrl: https://avatars.githubusercontent.com/u/16740832?u=b0dfdbdb27b79729430c71c6128962f77b7b53f7&v=4 + url: https://github.com/TheR1D + - login: Zuzah + avatarUrl: https://avatars.githubusercontent.com/u/10934846?u=1ef43e075ddc87bd1178372bf4d95ee6175cae27&v=4 + url: https://github.com/Zuzah + - login: mntolia + avatarUrl: https://avatars.githubusercontent.com/u/10390224?v=4 + url: https://github.com/mntolia + - login: hard-coders + avatarUrl: https://avatars.githubusercontent.com/u/9651103?u=78d12d1acdf853c817700145e73de7fd9e5d068b&v=4 + url: https://github.com/hard-coders - login: DMantis avatarUrl: https://avatars.githubusercontent.com/u/9536869?u=652dd0d49717803c0cbcbf44f7740e53cf2d4892&v=4 url: https://github.com/DMantis -- - login: morzan1001 + - login: xncbf + avatarUrl: https://avatars.githubusercontent.com/u/9462045?u=a80a7bb349555b277645632ed66639ff43400614&v=4 + url: https://github.com/xncbf + - login: moonape1226 + avatarUrl: https://avatars.githubusercontent.com/u/8532038?u=d9f8b855a429fff9397c3833c2ff83849ebf989d&v=4 + url: https://github.com/moonape1226 + - login: harsh183 + avatarUrl: https://avatars.githubusercontent.com/u/7780198?v=4 + url: https://github.com/harsh183 + - login: katnoria + avatarUrl: https://avatars.githubusercontent.com/u/7674948?u=09767eb13e07e09496c5fee4e5ce21d9eac34a56&v=4 + url: https://github.com/katnoria + - login: KentShikama + avatarUrl: https://avatars.githubusercontent.com/u/6329898?u=8b236810db9b96333230430837e1f021f9246da1&v=4 + url: https://github.com/KentShikama + - login: Baghdady92 + avatarUrl: https://avatars.githubusercontent.com/u/5708590?v=4 + url: https://github.com/Baghdady92 + - login: sdevkota + avatarUrl: https://avatars.githubusercontent.com/u/5250987?u=4ed9a120c89805a8aefda1cbdc0cf6512e64d1b4&v=4 + url: https://github.com/sdevkota + - login: rangulvers + avatarUrl: https://avatars.githubusercontent.com/u/5235430?u=e254d4af4ace5a05fa58372ae677c7d26f0d5a53&v=4 + url: https://github.com/rangulvers +- - login: KOZ39 + avatarUrl: https://avatars.githubusercontent.com/u/38822500?u=9dfc0a697df1c9628f08e20dc3fb17b1afc4e5a7&v=4 + url: https://github.com/KOZ39 + - login: rwxd + avatarUrl: https://avatars.githubusercontent.com/u/40308458?u=cd04a39e3655923be4f25c2ba8a5a07b3da3230a&v=4 + url: https://github.com/rwxd + - login: morzan1001 avatarUrl: https://avatars.githubusercontent.com/u/47593005?u=c30ab7230f82a12a9b938dcb54f84a996931409a&v=4 url: https://github.com/morzan1001 + - login: Olegt0rr + avatarUrl: https://avatars.githubusercontent.com/u/25399456?u=3e87b5239a2f4600975ba13be73054f8567c6060&v=4 + url: https://github.com/Olegt0rr - login: larsyngvelundin avatarUrl: https://avatars.githubusercontent.com/u/34173819?u=74958599695bf83ac9f1addd935a51548a10c6b0&v=4 url: https://github.com/larsyngvelundin - login: andrecorumba avatarUrl: https://avatars.githubusercontent.com/u/37807517?u=9b9be3b41da9bda60957da9ef37b50dbf65baa61&v=4 url: https://github.com/andrecorumba - - login: KOZ39 - avatarUrl: https://avatars.githubusercontent.com/u/38822500?u=9dfc0a697df1c9628f08e20dc3fb17b1afc4e5a7&v=4 - url: https://github.com/KOZ39 - - login: rwxd - avatarUrl: https://avatars.githubusercontent.com/u/40308458?u=cd04a39e3655923be4f25c2ba8a5a07b3da3230a&v=4 - url: https://github.com/rwxd - - login: hippoley - avatarUrl: https://avatars.githubusercontent.com/u/135493401?u=1164ef48a645a7c12664fabc1638fbb7e1c459b0&v=4 - url: https://github.com/hippoley - login: CoderDeltaLAN avatarUrl: https://avatars.githubusercontent.com/u/152043745?u=4ff541efffb7d134e60c5fcf2dd1e343f90bb782&v=4 url: https://github.com/CoderDeltaLAN - - login: chris1ding1 - avatarUrl: https://avatars.githubusercontent.com/u/194386334?u=5500604b50e35ed8a5aeb82ce34aa5d3ee3f88c7&v=4 - url: https://github.com/chris1ding1 - - login: onestn - avatarUrl: https://avatars.githubusercontent.com/u/62360849?u=746dd21c34e7e06eefb11b03e8bb01aaae3c2a4f&v=4 - url: https://github.com/onestn - - login: Rubinskiy - avatarUrl: https://avatars.githubusercontent.com/u/62457878?u=f2e35ed3d196a99cfadb5a29a91950342af07e34&v=4 - url: https://github.com/Rubinskiy + - login: hippoley + avatarUrl: https://avatars.githubusercontent.com/u/135493401?u=1164ef48a645a7c12664fabc1638fbb7e1c459b0&v=4 + url: https://github.com/hippoley - login: nayasinghania avatarUrl: https://avatars.githubusercontent.com/u/74111380?u=752e99a5e139389fdc0a0677122adc08438eb076&v=4 url: https://github.com/nayasinghania + - login: onestn + avatarUrl: https://avatars.githubusercontent.com/u/62360849?u=746dd21c34e7e06eefb11b03e8bb01aaae3c2a4f&v=4 + url: https://github.com/onestn - login: Toothwitch avatarUrl: https://avatars.githubusercontent.com/u/1710406?u=5eebb23b46cd26e48643b9e5179536cad491c17a&v=4 url: https://github.com/Toothwitch - login: andreagrandi avatarUrl: https://avatars.githubusercontent.com/u/636391?u=13d90cb8ec313593a5b71fbd4e33b78d6da736f5&v=4 url: https://github.com/andreagrandi - - login: Olegt0rr - avatarUrl: https://avatars.githubusercontent.com/u/25399456?u=3e87b5239a2f4600975ba13be73054f8567c6060&v=4 - url: https://github.com/Olegt0rr - login: msserpa avatarUrl: https://avatars.githubusercontent.com/u/6334934?u=82c4489eb1559d88d2990d60001901b14f722bbb&v=4 url: https://github.com/msserpa diff --git a/docs/en/data/sponsors.yml b/docs/en/data/sponsors.yml index 943b92adbc..50b1145301 100644 --- a/docs/en/data/sponsors.yml +++ b/docs/en/data/sponsors.yml @@ -1,3 +1,7 @@ +keystone: + - url: https://fastapicloud.com + title: FastAPI Cloud. By the same team behind FastAPI. You code. We Cloud. + img: https://fastapi.tiangolo.com/img/sponsors/fastapicloud.png gold: - url: https://blockbee.io?ref=fastapi title: BlockBee Cryptocurrency Payment Gateway @@ -29,6 +33,9 @@ gold: - url: https://serpapi.com/?utm_source=fastapi_website title: "SerpApi: Web Search API" img: https://fastapi.tiangolo.com/img/sponsors/serpapi.png + - url: https://www.greptile.com/?utm_source=fastapi&utm_medium=sponsorship&utm_campaign=fastapi_sponsor_page + title: "Greptile: The AI Code Reviewer" + img: https://fastapi.tiangolo.com/img/sponsors/greptile.png silver: - url: https://databento.com/?utm_source=fastapi&utm_medium=sponsor&utm_content=display title: Pay as you go for market data diff --git a/docs/en/data/sponsors_badge.yml b/docs/en/data/sponsors_badge.yml index 14f55805c6..d648be5fca 100644 --- a/docs/en/data/sponsors_badge.yml +++ b/docs/en/data/sponsors_badge.yml @@ -47,3 +47,4 @@ logins: - railwayapp - subtotal - requestly + - greptileai diff --git a/docs/en/data/topic_repos.yml b/docs/en/data/topic_repos.yml index 1bb6fd70d0..d089c7e5a7 100644 --- a/docs/en/data/topic_repos.yml +++ b/docs/en/data/topic_repos.yml @@ -1,495 +1,495 @@ - name: full-stack-fastapi-template html_url: https://github.com/fastapi/full-stack-fastapi-template - stars: 38779 + stars: 40334 owner_login: fastapi owner_html_url: https://github.com/fastapi - name: Hello-Python html_url: https://github.com/mouredev/Hello-Python - stars: 32726 + stars: 33628 owner_login: mouredev owner_html_url: https://github.com/mouredev - name: serve html_url: https://github.com/jina-ai/serve - stars: 21779 + stars: 21817 owner_login: jina-ai owner_html_url: https://github.com/jina-ai - name: HivisionIDPhotos html_url: https://github.com/Zeyi-Lin/HivisionIDPhotos - stars: 20028 + stars: 20409 owner_login: Zeyi-Lin owner_html_url: https://github.com/Zeyi-Lin - name: sqlmodel html_url: https://github.com/fastapi/sqlmodel - stars: 17038 + stars: 17415 owner_login: fastapi owner_html_url: https://github.com/fastapi -- name: Douyin_TikTok_Download_API - html_url: https://github.com/Evil0ctal/Douyin_TikTok_Download_API - stars: 14786 - owner_login: Evil0ctal - owner_html_url: https://github.com/Evil0ctal - name: fastapi-best-practices html_url: https://github.com/zhanymkanov/fastapi-best-practices - stars: 13968 + stars: 15776 owner_login: zhanymkanov owner_html_url: https://github.com/zhanymkanov +- name: Douyin_TikTok_Download_API + html_url: https://github.com/Evil0ctal/Douyin_TikTok_Download_API + stars: 15588 + owner_login: Evil0ctal + owner_html_url: https://github.com/Evil0ctal - name: machine-learning-zoomcamp html_url: https://github.com/DataTalksClub/machine-learning-zoomcamp - stars: 12171 + stars: 12447 owner_login: DataTalksClub owner_html_url: https://github.com/DataTalksClub +- name: SurfSense + html_url: https://github.com/MODSetter/SurfSense + stars: 12128 + owner_login: MODSetter + owner_html_url: https://github.com/MODSetter - name: fastapi_mcp html_url: https://github.com/tadata-org/fastapi_mcp - stars: 10976 + stars: 11326 owner_login: tadata-org owner_html_url: https://github.com/tadata-org - name: awesome-fastapi html_url: https://github.com/mjhea0/awesome-fastapi - stars: 10618 + stars: 10901 owner_login: mjhea0 owner_html_url: https://github.com/mjhea0 -- name: SurfSense - html_url: https://github.com/MODSetter/SurfSense - stars: 10243 - owner_login: MODSetter - owner_html_url: https://github.com/MODSetter - name: XHS-Downloader html_url: https://github.com/JoeanAmier/XHS-Downloader - stars: 9062 + stars: 9584 owner_login: JoeanAmier owner_html_url: https://github.com/JoeanAmier -- name: FastUI - html_url: https://github.com/pydantic/FastUI - stars: 8892 - owner_login: pydantic - owner_html_url: https://github.com/pydantic - name: polar html_url: https://github.com/polarsource/polar - stars: 8084 + stars: 8951 owner_login: polarsource owner_html_url: https://github.com/polarsource +- name: FastUI + html_url: https://github.com/pydantic/FastUI + stars: 8934 + owner_login: pydantic + owner_html_url: https://github.com/pydantic - name: FileCodeBox html_url: https://github.com/vastsa/FileCodeBox - stars: 7494 + stars: 7934 owner_login: vastsa owner_html_url: https://github.com/vastsa - name: nonebot2 html_url: https://github.com/nonebot/nonebot2 - stars: 7128 + stars: 7248 owner_login: nonebot owner_html_url: https://github.com/nonebot - name: hatchet html_url: https://github.com/hatchet-dev/hatchet - stars: 6155 + stars: 6392 owner_login: hatchet-dev owner_html_url: https://github.com/hatchet-dev +- name: fastapi-users + html_url: https://github.com/fastapi-users/fastapi-users + stars: 5899 + owner_login: fastapi-users + owner_html_url: https://github.com/fastapi-users - name: serge html_url: https://github.com/serge-chat/serge stars: 5754 owner_login: serge-chat owner_html_url: https://github.com/serge-chat -- name: fastapi-users - html_url: https://github.com/fastapi-users/fastapi-users - stars: 5683 - owner_login: fastapi-users - owner_html_url: https://github.com/fastapi-users - name: strawberry html_url: https://github.com/strawberry-graphql/strawberry - stars: 4452 + stars: 4577 owner_login: strawberry-graphql owner_html_url: https://github.com/strawberry-graphql -- name: chatgpt-web-share - html_url: https://github.com/chatpire/chatgpt-web-share - stars: 4296 - owner_login: chatpire - owner_html_url: https://github.com/chatpire - name: poem html_url: https://github.com/poem-web/poem - stars: 4235 + stars: 4303 owner_login: poem-web owner_html_url: https://github.com/poem-web +- name: chatgpt-web-share + html_url: https://github.com/chatpire/chatgpt-web-share + stars: 4287 + owner_login: chatpire + owner_html_url: https://github.com/chatpire - name: dynaconf html_url: https://github.com/dynaconf/dynaconf - stars: 4174 + stars: 4221 owner_login: dynaconf owner_html_url: https://github.com/dynaconf -- name: atrilabs-engine - html_url: https://github.com/Atri-Labs/atrilabs-engine - stars: 4094 - owner_login: Atri-Labs - owner_html_url: https://github.com/Atri-Labs - name: Kokoro-FastAPI html_url: https://github.com/remsky/Kokoro-FastAPI - stars: 3875 + stars: 4181 owner_login: remsky owner_html_url: https://github.com/remsky +- name: atrilabs-engine + html_url: https://github.com/Atri-Labs/atrilabs-engine + stars: 4090 + owner_login: Atri-Labs + owner_html_url: https://github.com/Atri-Labs +- name: devpush + html_url: https://github.com/hunvreus/devpush + stars: 4037 + owner_login: hunvreus + owner_html_url: https://github.com/hunvreus - name: logfire html_url: https://github.com/pydantic/logfire - stars: 3717 + stars: 3896 owner_login: pydantic owner_html_url: https://github.com/pydantic - name: LitServe html_url: https://github.com/Lightning-AI/LitServe - stars: 3615 + stars: 3756 owner_login: Lightning-AI owner_html_url: https://github.com/Lightning-AI -- name: datamodel-code-generator - html_url: https://github.com/koxudaxi/datamodel-code-generator - stars: 3554 - owner_login: koxudaxi - owner_html_url: https://github.com/koxudaxi - name: huma html_url: https://github.com/danielgtaylor/huma - stars: 3521 + stars: 3702 owner_login: danielgtaylor owner_html_url: https://github.com/danielgtaylor +- name: Yuxi-Know + html_url: https://github.com/xerrors/Yuxi-Know + stars: 3680 + owner_login: xerrors + owner_html_url: https://github.com/xerrors +- name: datamodel-code-generator + html_url: https://github.com/koxudaxi/datamodel-code-generator + stars: 3675 + owner_login: koxudaxi + owner_html_url: https://github.com/koxudaxi - name: fastapi-admin html_url: https://github.com/fastapi-admin/fastapi-admin - stars: 3497 + stars: 3659 owner_login: fastapi-admin owner_html_url: https://github.com/fastapi-admin - name: farfalle html_url: https://github.com/rashadphz/farfalle - stars: 3476 + stars: 3497 owner_login: rashadphz owner_html_url: https://github.com/rashadphz - name: tracecat html_url: https://github.com/TracecatHQ/tracecat - stars: 3310 + stars: 3421 owner_login: TracecatHQ owner_html_url: https://github.com/TracecatHQ - name: opyrator html_url: https://github.com/ml-tooling/opyrator - stars: 3134 + stars: 3136 owner_login: ml-tooling owner_html_url: https://github.com/ml-tooling - name: docarray html_url: https://github.com/docarray/docarray - stars: 3108 + stars: 3111 owner_login: docarray owner_html_url: https://github.com/docarray - name: fastapi-realworld-example-app html_url: https://github.com/nsidnev/fastapi-realworld-example-app - stars: 2945 + stars: 3051 owner_login: nsidnev owner_html_url: https://github.com/nsidnev -- name: uvicorn-gunicorn-fastapi-docker - html_url: https://github.com/tiangolo/uvicorn-gunicorn-fastapi-docker - stars: 2809 - owner_login: tiangolo - owner_html_url: https://github.com/tiangolo -- name: devpush - html_url: https://github.com/hunvreus/devpush - stars: 2784 - owner_login: hunvreus - owner_html_url: https://github.com/hunvreus - name: mcp-context-forge html_url: https://github.com/IBM/mcp-context-forge - stars: 2763 + stars: 3034 owner_login: IBM owner_html_url: https://github.com/IBM -- name: best-of-web-python - html_url: https://github.com/ml-tooling/best-of-web-python - stars: 2630 - owner_login: ml-tooling - owner_html_url: https://github.com/ml-tooling -- name: fastapi-react - html_url: https://github.com/Buuntu/fastapi-react - stars: 2464 - owner_login: Buuntu - owner_html_url: https://github.com/Buuntu +- name: uvicorn-gunicorn-fastapi-docker + html_url: https://github.com/tiangolo/uvicorn-gunicorn-fastapi-docker + stars: 2904 + owner_login: tiangolo + owner_html_url: https://github.com/tiangolo - name: FastAPI-template html_url: https://github.com/s3rius/FastAPI-template - stars: 2453 + stars: 2680 owner_login: s3rius owner_html_url: https://github.com/s3rius -- name: RasaGPT - html_url: https://github.com/paulpierre/RasaGPT - stars: 2444 - owner_login: paulpierre - owner_html_url: https://github.com/paulpierre +- name: best-of-web-python + html_url: https://github.com/ml-tooling/best-of-web-python + stars: 2662 + owner_login: ml-tooling + owner_html_url: https://github.com/ml-tooling +- name: YC-Killer + html_url: https://github.com/sahibzada-allahyar/YC-Killer + stars: 2614 + owner_login: sahibzada-allahyar + owner_html_url: https://github.com/sahibzada-allahyar - name: sqladmin html_url: https://github.com/aminalaee/sqladmin - stars: 2423 + stars: 2587 owner_login: aminalaee owner_html_url: https://github.com/aminalaee -- name: nextpy - html_url: https://github.com/dot-agent/nextpy - stars: 2325 - owner_login: dot-agent - owner_html_url: https://github.com/dot-agent +- name: fastapi-react + html_url: https://github.com/Buuntu/fastapi-react + stars: 2566 + owner_login: Buuntu + owner_html_url: https://github.com/Buuntu +- name: RasaGPT + html_url: https://github.com/paulpierre/RasaGPT + stars: 2456 + owner_login: paulpierre + owner_html_url: https://github.com/paulpierre - name: supabase-py html_url: https://github.com/supabase/supabase-py - stars: 2292 + stars: 2394 owner_login: supabase owner_html_url: https://github.com/supabase -- name: 30-Days-of-Python - html_url: https://github.com/codingforentrepreneurs/30-Days-of-Python - stars: 2214 - owner_login: codingforentrepreneurs - owner_html_url: https://github.com/codingforentrepreneurs -- name: Yuxi-Know - html_url: https://github.com/xerrors/Yuxi-Know - stars: 2212 - owner_login: xerrors - owner_html_url: https://github.com/xerrors -- name: langserve - html_url: https://github.com/langchain-ai/langserve - stars: 2191 - owner_login: langchain-ai - owner_html_url: https://github.com/langchain-ai +- name: nextpy + html_url: https://github.com/dot-agent/nextpy + stars: 2338 + owner_login: dot-agent + owner_html_url: https://github.com/dot-agent - name: fastapi-utils html_url: https://github.com/fastapiutils/fastapi-utils - stars: 2185 + stars: 2289 owner_login: fastapiutils owner_html_url: https://github.com/fastapiutils +- name: langserve + html_url: https://github.com/langchain-ai/langserve + stars: 2234 + owner_login: langchain-ai + owner_html_url: https://github.com/langchain-ai +- name: 30-Days-of-Python + html_url: https://github.com/codingforentrepreneurs/30-Days-of-Python + stars: 2232 + owner_login: codingforentrepreneurs + owner_html_url: https://github.com/codingforentrepreneurs - name: solara html_url: https://github.com/widgetti/solara - stars: 2111 + stars: 2141 owner_login: widgetti owner_html_url: https://github.com/widgetti - name: mangum html_url: https://github.com/Kludex/mangum - stars: 2011 + stars: 2046 owner_login: Kludex owner_html_url: https://github.com/Kludex +- name: fastapi_best_architecture + html_url: https://github.com/fastapi-practices/fastapi_best_architecture + stars: 1963 + owner_login: fastapi-practices + owner_html_url: https://github.com/fastapi-practices +- name: NoteDiscovery + html_url: https://github.com/gamosoft/NoteDiscovery + stars: 1943 + owner_login: gamosoft + owner_html_url: https://github.com/gamosoft - name: agentkit html_url: https://github.com/BCG-X-Official/agentkit - stars: 1826 + stars: 1936 owner_login: BCG-X-Official owner_html_url: https://github.com/BCG-X-Official -- name: python-week-2022 - html_url: https://github.com/rochacbruno/python-week-2022 - stars: 1815 - owner_login: rochacbruno - owner_html_url: https://github.com/rochacbruno -- name: manage-fastapi - html_url: https://github.com/ycd/manage-fastapi - stars: 1787 - owner_login: ycd - owner_html_url: https://github.com/ycd -- name: ormar - html_url: https://github.com/collerek/ormar - stars: 1780 - owner_login: collerek - owner_html_url: https://github.com/collerek - name: vue-fastapi-admin html_url: https://github.com/mizhexiaoxiao/vue-fastapi-admin - stars: 1758 + stars: 1909 owner_login: mizhexiaoxiao owner_html_url: https://github.com/mizhexiaoxiao +- name: manage-fastapi + html_url: https://github.com/ycd/manage-fastapi + stars: 1887 + owner_login: ycd + owner_html_url: https://github.com/ycd - name: openapi-python-client html_url: https://github.com/openapi-generators/openapi-python-client - stars: 1731 + stars: 1879 owner_login: openapi-generators owner_html_url: https://github.com/openapi-generators -- name: piccolo - html_url: https://github.com/piccolo-orm/piccolo - stars: 1711 - owner_login: piccolo-orm - owner_html_url: https://github.com/piccolo-orm -- name: fastapi-cache - html_url: https://github.com/long2ice/fastapi-cache - stars: 1677 - owner_login: long2ice - owner_html_url: https://github.com/long2ice - name: slowapi html_url: https://github.com/laurentS/slowapi - stars: 1669 + stars: 1845 owner_login: laurentS owner_html_url: https://github.com/laurentS -- name: langchain-serve - html_url: https://github.com/jina-ai/langchain-serve - stars: 1632 - owner_login: jina-ai - owner_html_url: https://github.com/jina-ai -- name: termpair - html_url: https://github.com/cs01/termpair - stars: 1621 - owner_login: cs01 - owner_html_url: https://github.com/cs01 -- name: FastAPI-boilerplate - html_url: https://github.com/benavlabs/FastAPI-boilerplate - stars: 1596 - owner_login: benavlabs - owner_html_url: https://github.com/benavlabs -- name: coronavirus-tracker-api - html_url: https://github.com/ExpDev07/coronavirus-tracker-api - stars: 1573 - owner_login: ExpDev07 - owner_html_url: https://github.com/ExpDev07 -- name: fastapi-crudrouter - html_url: https://github.com/awtkns/fastapi-crudrouter - stars: 1553 - owner_login: awtkns - owner_html_url: https://github.com/awtkns -- name: awesome-fastapi-projects - html_url: https://github.com/Kludex/awesome-fastapi-projects - stars: 1485 - owner_login: Kludex - owner_html_url: https://github.com/Kludex -- name: fastapi-pagination - html_url: https://github.com/uriyyo/fastapi-pagination - stars: 1473 - owner_login: uriyyo - owner_html_url: https://github.com/uriyyo -- name: bracket - html_url: https://github.com/evroon/bracket - stars: 1470 - owner_login: evroon - owner_html_url: https://github.com/evroon +- name: piccolo + html_url: https://github.com/piccolo-orm/piccolo + stars: 1843 + owner_login: piccolo-orm + owner_html_url: https://github.com/piccolo-orm +- name: python-week-2022 + html_url: https://github.com/rochacbruno/python-week-2022 + stars: 1813 + owner_login: rochacbruno + owner_html_url: https://github.com/rochacbruno +- name: fastapi-cache + html_url: https://github.com/long2ice/fastapi-cache + stars: 1805 + owner_login: long2ice + owner_html_url: https://github.com/long2ice +- name: ormar + html_url: https://github.com/collerek/ormar + stars: 1785 + owner_login: collerek + owner_html_url: https://github.com/collerek - name: fastapi-langgraph-agent-production-ready-template html_url: https://github.com/wassim249/fastapi-langgraph-agent-production-ready-template - stars: 1456 + stars: 1780 owner_login: wassim249 owner_html_url: https://github.com/wassim249 -- name: fastapi-boilerplate - html_url: https://github.com/teamhide/fastapi-boilerplate - stars: 1424 - owner_login: teamhide - owner_html_url: https://github.com/teamhide -- name: awesome-python-resources - html_url: https://github.com/DjangoEx/awesome-python-resources - stars: 1420 - owner_login: DjangoEx - owner_html_url: https://github.com/DjangoEx -- name: fastapi-amis-admin - html_url: https://github.com/amisadmin/fastapi-amis-admin - stars: 1363 - owner_login: amisadmin - owner_html_url: https://github.com/amisadmin -- name: fastcrud - html_url: https://github.com/benavlabs/fastcrud - stars: 1362 +- name: FastAPI-boilerplate + html_url: https://github.com/benavlabs/FastAPI-boilerplate + stars: 1734 owner_login: benavlabs owner_html_url: https://github.com/benavlabs +- name: termpair + html_url: https://github.com/cs01/termpair + stars: 1724 + owner_login: cs01 + owner_html_url: https://github.com/cs01 +- name: fastapi-crudrouter + html_url: https://github.com/awtkns/fastapi-crudrouter + stars: 1671 + owner_login: awtkns + owner_html_url: https://github.com/awtkns +- name: langchain-serve + html_url: https://github.com/jina-ai/langchain-serve + stars: 1633 + owner_login: jina-ai + owner_html_url: https://github.com/jina-ai +- name: fastapi-pagination + html_url: https://github.com/uriyyo/fastapi-pagination + stars: 1588 + owner_login: uriyyo + owner_html_url: https://github.com/uriyyo +- name: awesome-fastapi-projects + html_url: https://github.com/Kludex/awesome-fastapi-projects + stars: 1583 + owner_login: Kludex + owner_html_url: https://github.com/Kludex +- name: coronavirus-tracker-api + html_url: https://github.com/ExpDev07/coronavirus-tracker-api + stars: 1571 + owner_login: ExpDev07 + owner_html_url: https://github.com/ExpDev07 +- name: bracket + html_url: https://github.com/evroon/bracket + stars: 1549 + owner_login: evroon + owner_html_url: https://github.com/evroon +- name: fastapi-amis-admin + html_url: https://github.com/amisadmin/fastapi-amis-admin + stars: 1491 + owner_login: amisadmin + owner_html_url: https://github.com/amisadmin +- name: fastapi-boilerplate + html_url: https://github.com/teamhide/fastapi-boilerplate + stars: 1452 + owner_login: teamhide + owner_html_url: https://github.com/teamhide +- name: fastcrud + html_url: https://github.com/benavlabs/fastcrud + stars: 1452 + owner_login: benavlabs + owner_html_url: https://github.com/benavlabs +- name: awesome-python-resources + html_url: https://github.com/DjangoEx/awesome-python-resources + stars: 1430 + owner_login: DjangoEx + owner_html_url: https://github.com/DjangoEx +- name: prometheus-fastapi-instrumentator + html_url: https://github.com/trallnag/prometheus-fastapi-instrumentator + stars: 1399 + owner_login: trallnag + owner_html_url: https://github.com/trallnag +- name: fastapi-code-generator + html_url: https://github.com/koxudaxi/fastapi-code-generator + stars: 1371 + owner_login: koxudaxi + owner_html_url: https://github.com/koxudaxi +- name: fastapi-tutorial + html_url: https://github.com/liaogx/fastapi-tutorial + stars: 1346 + owner_login: liaogx + owner_html_url: https://github.com/liaogx - name: budgetml html_url: https://github.com/ebhy/budgetml stars: 1345 owner_login: ebhy owner_html_url: https://github.com/ebhy -- name: fastapi-tutorial - html_url: https://github.com/liaogx/fastapi-tutorial - stars: 1315 - owner_login: liaogx - owner_html_url: https://github.com/liaogx -- name: fastapi_best_architecture - html_url: https://github.com/fastapi-practices/fastapi_best_architecture - stars: 1311 - owner_login: fastapi-practices - owner_html_url: https://github.com/fastapi-practices -- name: fastapi-code-generator - html_url: https://github.com/koxudaxi/fastapi-code-generator - stars: 1270 - owner_login: koxudaxi - owner_html_url: https://github.com/koxudaxi -- name: prometheus-fastapi-instrumentator - html_url: https://github.com/trallnag/prometheus-fastapi-instrumentator - stars: 1264 - owner_login: trallnag - owner_html_url: https://github.com/trallnag -- name: bedrock-chat - html_url: https://github.com/aws-samples/bedrock-chat - stars: 1243 - owner_login: aws-samples - owner_html_url: https://github.com/aws-samples -- name: bolt-python - html_url: https://github.com/slackapi/bolt-python - stars: 1238 - owner_login: slackapi - owner_html_url: https://github.com/slackapi -- name: fastapi_production_template - html_url: https://github.com/zhanymkanov/fastapi_production_template - stars: 1209 - owner_login: zhanymkanov - owner_html_url: https://github.com/zhanymkanov - name: fastapi-scaff html_url: https://github.com/atpuxiner/fastapi-scaff - stars: 1200 + stars: 1331 owner_login: atpuxiner owner_html_url: https://github.com/atpuxiner -- name: langchain-extract - html_url: https://github.com/langchain-ai/langchain-extract - stars: 1173 - owner_login: langchain-ai - owner_html_url: https://github.com/langchain-ai +- name: bolt-python + html_url: https://github.com/slackapi/bolt-python + stars: 1266 + owner_login: slackapi + owner_html_url: https://github.com/slackapi +- name: bedrock-chat + html_url: https://github.com/aws-samples/bedrock-chat + stars: 1266 + owner_login: aws-samples + owner_html_url: https://github.com/aws-samples - name: fastapi-alembic-sqlmodel-async html_url: https://github.com/jonra1993/fastapi-alembic-sqlmodel-async - stars: 1162 + stars: 1260 owner_login: jonra1993 owner_html_url: https://github.com/jonra1993 -- name: odmantic - html_url: https://github.com/art049/odmantic - stars: 1137 - owner_login: art049 - owner_html_url: https://github.com/art049 +- name: fastapi_production_template + html_url: https://github.com/zhanymkanov/fastapi_production_template + stars: 1222 + owner_login: zhanymkanov + owner_html_url: https://github.com/zhanymkanov +- name: langchain-extract + html_url: https://github.com/langchain-ai/langchain-extract + stars: 1179 + owner_login: langchain-ai + owner_html_url: https://github.com/langchain-ai - name: restish html_url: https://github.com/rest-sh/restish - stars: 1129 + stars: 1152 owner_login: rest-sh owner_html_url: https://github.com/rest-sh -- name: kubetorch - html_url: https://github.com/run-house/kubetorch - stars: 1065 - owner_login: run-house - owner_html_url: https://github.com/run-house -- name: flock - html_url: https://github.com/Onelevenvy/flock - stars: 1039 - owner_login: Onelevenvy - owner_html_url: https://github.com/Onelevenvy +- name: odmantic + html_url: https://github.com/art049/odmantic + stars: 1143 + owner_login: art049 + owner_html_url: https://github.com/art049 - name: authx html_url: https://github.com/yezz123/authx - stars: 1017 + stars: 1128 owner_login: yezz123 owner_html_url: https://github.com/yezz123 +- name: SAG + html_url: https://github.com/Zleap-AI/SAG + stars: 1104 + owner_login: Zleap-AI + owner_html_url: https://github.com/Zleap-AI +- name: aktools + html_url: https://github.com/akfamily/aktools + stars: 1072 + owner_login: akfamily + owner_html_url: https://github.com/akfamily +- name: RuoYi-Vue3-FastAPI + html_url: https://github.com/insistence/RuoYi-Vue3-FastAPI + stars: 1063 + owner_login: insistence + owner_html_url: https://github.com/insistence +- name: flock + html_url: https://github.com/Onelevenvy/flock + stars: 1059 + owner_login: Onelevenvy + owner_html_url: https://github.com/Onelevenvy +- name: fastapi-observability + html_url: https://github.com/blueswen/fastapi-observability + stars: 1046 + owner_login: blueswen + owner_html_url: https://github.com/blueswen +- name: enterprise-deep-research + html_url: https://github.com/SalesforceAIResearch/enterprise-deep-research + stars: 1019 + owner_login: SalesforceAIResearch + owner_html_url: https://github.com/SalesforceAIResearch +- name: titiler + html_url: https://github.com/developmentseed/titiler + stars: 1016 + owner_login: developmentseed + owner_html_url: https://github.com/developmentseed +- name: every-pdf + html_url: https://github.com/DDULDDUCK/every-pdf + stars: 1004 + owner_login: DDULDDUCK + owner_html_url: https://github.com/DDULDDUCK - name: autollm html_url: https://github.com/viddexa/autollm - stars: 997 + stars: 1003 owner_login: viddexa owner_html_url: https://github.com/viddexa - name: lanarky html_url: https://github.com/ajndkr/lanarky - stars: 993 + stars: 996 owner_login: ajndkr owner_html_url: https://github.com/ajndkr -- name: RuoYi-Vue3-FastAPI - html_url: https://github.com/insistence/RuoYi-Vue3-FastAPI - stars: 974 - owner_login: insistence - owner_html_url: https://github.com/insistence -- name: aktools - html_url: https://github.com/akfamily/aktools - stars: 972 - owner_login: akfamily - owner_html_url: https://github.com/akfamily -- name: titiler - html_url: https://github.com/developmentseed/titiler - stars: 965 - owner_login: developmentseed - owner_html_url: https://github.com/developmentseed -- name: secure - html_url: https://github.com/TypeError/secure - stars: 953 - owner_login: TypeError - owner_html_url: https://github.com/TypeError -- name: energy-forecasting - html_url: https://github.com/iusztinpaul/energy-forecasting - stars: 949 - owner_login: iusztinpaul - owner_html_url: https://github.com/iusztinpaul -- name: every-pdf - html_url: https://github.com/DDULDDUCK/every-pdf - stars: 942 - owner_login: DDULDDUCK - owner_html_url: https://github.com/DDULDDUCK -- name: langcorn - html_url: https://github.com/msoedov/langcorn - stars: 933 - owner_login: msoedov - owner_html_url: https://github.com/msoedov -- name: fastapi-observability - html_url: https://github.com/blueswen/fastapi-observability - stars: 923 - owner_login: blueswen - owner_html_url: https://github.com/blueswen diff --git a/docs/en/data/translation_reviewers.yml b/docs/en/data/translation_reviewers.yml index 45aa55e5e2..62db8e805d 100644 --- a/docs/en/data/translation_reviewers.yml +++ b/docs/en/data/translation_reviewers.yml @@ -15,7 +15,7 @@ sodaMelon: url: https://github.com/sodaMelon ceb10n: login: ceb10n - count: 116 + count: 117 avatarUrl: https://avatars.githubusercontent.com/u/235213?u=edcce471814a1eba9f0cdaa4cd0de18921a940a6&v=4 url: https://github.com/ceb10n tokusumi: @@ -23,16 +23,16 @@ tokusumi: count: 104 avatarUrl: https://avatars.githubusercontent.com/u/41147016?u=55010621aece725aa702270b54fed829b6a1fe60&v=4 url: https://github.com/tokusumi +hard-coders: + login: hard-coders + count: 96 + avatarUrl: https://avatars.githubusercontent.com/u/9651103?u=78d12d1acdf853c817700145e73de7fd9e5d068b&v=4 + url: https://github.com/hard-coders hasansezertasan: login: hasansezertasan count: 95 avatarUrl: https://avatars.githubusercontent.com/u/13135006?u=99f0b0f0fc47e88e8abb337b4447357939ef93e7&v=4 url: https://github.com/hasansezertasan -hard-coders: - login: hard-coders - count: 93 - avatarUrl: https://avatars.githubusercontent.com/u/9651103?u=95db33927bbff1ed1c07efddeb97ac2ff33068ed&v=4 - url: https://github.com/hard-coders alv2017: login: alv2017 count: 88 @@ -40,7 +40,7 @@ alv2017: url: https://github.com/alv2017 nazarepiedady: login: nazarepiedady - count: 86 + count: 87 avatarUrl: https://avatars.githubusercontent.com/u/31008635?u=f69ddc4ea8bda3bdfac7aa0e2ea38de282e6ee2d&v=4 url: https://github.com/nazarepiedady AlertRED: @@ -48,6 +48,11 @@ AlertRED: count: 81 avatarUrl: https://avatars.githubusercontent.com/u/15695000?u=f5a4944c6df443030409c88da7d7fa0b7ead985c&v=4 url: https://github.com/AlertRED +tiangolo: + login: tiangolo + count: 73 + avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=cb5d06e73a9e1998141b1641aa88e443c6717651&v=4 + url: https://github.com/tiangolo Alexandrhub: login: Alexandrhub count: 68 @@ -63,21 +68,16 @@ cassiobotaro: count: 62 avatarUrl: https://avatars.githubusercontent.com/u/3127847?u=a08022b191ddbd0a6159b2981d9d878b6d5bb71f&v=4 url: https://github.com/cassiobotaro +mattwang44: + login: mattwang44 + count: 61 + avatarUrl: https://avatars.githubusercontent.com/u/24987826?u=58e37fb3927b9124b458945ac4c97aa0f1062d85&v=4 + url: https://github.com/mattwang44 nilslindemann: login: nilslindemann count: 59 avatarUrl: https://avatars.githubusercontent.com/u/6892179?u=1dca6a22195d6cd1ab20737c0e19a4c55d639472&v=4 url: https://github.com/nilslindemann -mattwang44: - login: mattwang44 - count: 59 - avatarUrl: https://avatars.githubusercontent.com/u/24987826?u=58e37fb3927b9124b458945ac4c97aa0f1062d85&v=4 - url: https://github.com/mattwang44 -tiangolo: - login: tiangolo - count: 55 - avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=cb5d06e73a9e1998141b1641aa88e443c6717651&v=4 - url: https://github.com/tiangolo Laineyzhang55: login: Laineyzhang55 count: 48 @@ -88,6 +88,11 @@ Kludex: count: 47 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=df8a3f06ba8f55ae1967a3e2d5ed882903a4e330&v=4 url: https://github.com/Kludex +YuriiMotov: + login: YuriiMotov + count: 46 + avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=b9b13d598dddfab529a52d264df80a900bfe7060&v=4 + url: https://github.com/YuriiMotov komtaki: login: komtaki count: 45 @@ -118,11 +123,6 @@ Winand: count: 40 avatarUrl: https://avatars.githubusercontent.com/u/53390?u=bb0e71a2fc3910a8e0ee66da67c33de40ea695f8&v=4 url: https://github.com/Winand -YuriiMotov: - login: YuriiMotov - count: 40 - avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=b9b13d598dddfab529a52d264df80a900bfe7060&v=4 - url: https://github.com/YuriiMotov solomein-sv: login: solomein-sv count: 38 @@ -136,8 +136,13 @@ JavierSanchezCastro: alejsdev: login: alejsdev count: 37 - avatarUrl: https://avatars.githubusercontent.com/u/90076947?u=447d12a1b347f466b35378bee4c7104cc9b2c571&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/90076947?u=85ceac49fb87138aebe8d663912e359447329090&v=4 url: https://github.com/alejsdev +mezgoodle: + login: mezgoodle + count: 37 + avatarUrl: https://avatars.githubusercontent.com/u/41520940?u=4a9c765af688389d54296845d18b8f6cd6ddf09a&v=4 + url: https://github.com/mezgoodle stlucasgarcia: login: stlucasgarcia count: 36 @@ -153,11 +158,6 @@ timothy-jeong: count: 36 avatarUrl: https://avatars.githubusercontent.com/u/53824764?u=db3d0cea2f5fab64d810113c5039a369699a2774&v=4 url: https://github.com/timothy-jeong -mezgoodle: - login: mezgoodle - count: 35 - avatarUrl: https://avatars.githubusercontent.com/u/41520940?u=4a9c765af688389d54296845d18b8f6cd6ddf09a&v=4 - url: https://github.com/mezgoodle rjNemo: login: rjNemo count: 34 @@ -173,6 +173,11 @@ akarev0: count: 33 avatarUrl: https://avatars.githubusercontent.com/u/53393089?u=6e528bb4789d56af887ce6fe237bea4010885406&v=4 url: https://github.com/akarev0 +Vincy1230: + login: Vincy1230 + count: 33 + avatarUrl: https://avatars.githubusercontent.com/u/81342412?u=ab5e256a4077a4a91f3f9cd2115ba80780454cbe&v=4 + url: https://github.com/Vincy1230 romashevchenko: login: romashevchenko count: 32 @@ -183,11 +188,6 @@ LorhanSohaky: count: 30 avatarUrl: https://avatars.githubusercontent.com/u/16273730?u=095b66f243a2cd6a0aadba9a095009f8aaf18393&v=4 url: https://github.com/LorhanSohaky -Vincy1230: - login: Vincy1230 - count: 30 - avatarUrl: https://avatars.githubusercontent.com/u/81342412?u=ab5e256a4077a4a91f3f9cd2115ba80780454cbe&v=4 - url: https://github.com/Vincy1230 black-redoc: login: black-redoc count: 29 @@ -250,7 +250,7 @@ mycaule: url: https://github.com/mycaule Aruelius: login: Aruelius - count: 24 + count: 25 avatarUrl: https://avatars.githubusercontent.com/u/25380989?u=574f8cfcda3ea77a3f81884f6b26a97068e36a9d&v=4 url: https://github.com/Aruelius wisderfin: @@ -263,6 +263,11 @@ OzgunCaglarArslan: count: 24 avatarUrl: https://avatars.githubusercontent.com/u/86166426?v=4 url: https://github.com/OzgunCaglarArslan +ycd: + login: ycd + count: 23 + avatarUrl: https://avatars.githubusercontent.com/u/62724709?u=f1e7bae394a315da950912c92dc861a8eaf95d4c&v=4 + url: https://github.com/ycd sh0nk: login: sh0nk count: 23 @@ -288,11 +293,6 @@ Attsun1031: count: 20 avatarUrl: https://avatars.githubusercontent.com/u/1175560?v=4 url: https://github.com/Attsun1031 -ycd: - login: ycd - count: 20 - avatarUrl: https://avatars.githubusercontent.com/u/62724709?u=f1e7bae394a315da950912c92dc861a8eaf95d4c&v=4 - url: https://github.com/ycd delhi09: login: delhi09 count: 20 @@ -418,6 +418,11 @@ mattkoehne: count: 14 avatarUrl: https://avatars.githubusercontent.com/u/80362153?v=4 url: https://github.com/mattkoehne +maru0123-2004: + login: maru0123-2004 + count: 14 + avatarUrl: https://avatars.githubusercontent.com/u/43961566?u=16ed8603a4d6a4665cb6c53a7aece6f31379b769&v=4 + url: https://github.com/maru0123-2004 jovicon: login: jovicon count: 13 @@ -436,13 +441,18 @@ jburckel: peidrao: login: peidrao count: 13 - avatarUrl: https://avatars.githubusercontent.com/u/32584628?u=64c634bb10381905038ff7faf3c8c3df47fb799a&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/32584628?u=979c62398e16ff000cc0faa028e028efd679887c&v=4 url: https://github.com/peidrao impocode: login: impocode count: 13 avatarUrl: https://avatars.githubusercontent.com/u/109408819?u=9cdfc5ccb31a2094c520f41b6087012fa9048982&v=4 url: https://github.com/impocode +waketzheng: + login: waketzheng + count: 13 + avatarUrl: https://avatars.githubusercontent.com/u/35413830?u=df19e4fd5bb928e7d086e053ef26a46aad23bf84&v=4 + url: https://github.com/waketzheng wesinalves: login: wesinalves count: 13 @@ -538,21 +548,16 @@ Lufa1u: count: 11 avatarUrl: https://avatars.githubusercontent.com/u/112495876?u=087658920ed9e74311597bdd921d8d2de939d276&v=4 url: https://github.com/Lufa1u -waketzheng: - login: waketzheng - count: 11 - avatarUrl: https://avatars.githubusercontent.com/u/35413830?u=df19e4fd5bb928e7d086e053ef26a46aad23bf84&v=4 - url: https://github.com/waketzheng KNChiu: login: KNChiu count: 11 avatarUrl: https://avatars.githubusercontent.com/u/36751646?v=4 url: https://github.com/KNChiu -maru0123-2004: - login: maru0123-2004 +Zhongheng-Cheng: + login: Zhongheng-Cheng count: 11 - avatarUrl: https://avatars.githubusercontent.com/u/43961566?u=16ed8603a4d6a4665cb6c53a7aece6f31379b769&v=4 - url: https://github.com/maru0123-2004 + avatarUrl: https://avatars.githubusercontent.com/u/95612344?u=a0f7730a3cc7486827965e01a119ad610bda4b0a&v=4 + url: https://github.com/Zhongheng-Cheng mariacamilagl: login: mariacamilagl count: 10 @@ -608,16 +613,16 @@ nick-cjyx9: count: 10 avatarUrl: https://avatars.githubusercontent.com/u/119087246?u=7227a2de948c68fb8396d5beff1ee5b0e057c42e&v=4 url: https://github.com/nick-cjyx9 +marcelomarkus: + login: marcelomarkus + count: 10 + avatarUrl: https://avatars.githubusercontent.com/u/20115018?u=dda090ce9160ef0cd2ff69b1e5ea741283425cba&v=4 + url: https://github.com/marcelomarkus lucasbalieiro: login: lucasbalieiro count: 10 - avatarUrl: https://avatars.githubusercontent.com/u/37416577?u=dad91601ee4f40458d691774ec439aff308344d7&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/37416577?u=d144221c34c08adac8b20e1833d776ffa1c4b1d0&v=4 url: https://github.com/lucasbalieiro -Zhongheng-Cheng: - login: Zhongheng-Cheng - count: 10 - avatarUrl: https://avatars.githubusercontent.com/u/95612344?u=a0f7730a3cc7486827965e01a119ad610bda4b0a&v=4 - url: https://github.com/Zhongheng-Cheng RunningIkkyu: login: RunningIkkyu count: 9 @@ -668,11 +673,6 @@ yodai-yodai: count: 9 avatarUrl: https://avatars.githubusercontent.com/u/7031039?u=4f3593f5931892b931a745cfab846eff6e9332e7&v=4 url: https://github.com/yodai-yodai -marcelomarkus: - login: marcelomarkus - count: 9 - avatarUrl: https://avatars.githubusercontent.com/u/20115018?u=dda090ce9160ef0cd2ff69b1e5ea741283425cba&v=4 - url: https://github.com/marcelomarkus JoaoGustavoRogel: login: JoaoGustavoRogel count: 9 @@ -683,6 +683,11 @@ Yarous: count: 9 avatarUrl: https://avatars.githubusercontent.com/u/61277193?u=5b462347458a373b2d599c6f416d2b75eddbffad&v=4 url: https://github.com/Yarous +Pyth3rEx: + login: Pyth3rEx + count: 9 + avatarUrl: https://avatars.githubusercontent.com/u/26427764?u=087724f74d813c95925d51e354554bd4b6d6bb60&v=4 + url: https://github.com/Pyth3rEx dimaqq: login: dimaqq count: 8 @@ -1006,7 +1011,7 @@ takacs: anton2yakovlev: login: anton2yakovlev count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/44229180?u=bdd445ba99074b378e7298d23c4bf6d707d2c282&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/44229180?u=ac245e57bc834ff80f08ca8128000bb650a77a3d&v=4 url: https://github.com/anton2yakovlev ILoveSorasakiHina: login: ILoveSorasakiHina @@ -1023,6 +1028,11 @@ devluisrodrigues: count: 5 avatarUrl: https://avatars.githubusercontent.com/u/21125286?v=4 url: https://github.com/11kkw +EdmilsonRodrigues: + login: EdmilsonRodrigues + count: 5 + avatarUrl: https://avatars.githubusercontent.com/u/62777025?u=217d6f3cd6cc750bb8818a3af7726c8d74eb7c2d&v=4 + url: https://github.com/EdmilsonRodrigues lpdswing: login: lpdswing count: 4 @@ -1161,8 +1171,13 @@ cookie-byte217: AbolfazlKameli: login: AbolfazlKameli count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/120686133?u=e41743da3c1820efafc59c5870cacd4f4425334c&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/120686133?u=af8f025278cce0d489007071254e4055df60b78c&v=4 url: https://github.com/AbolfazlKameli +SBillion: + login: SBillion + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/1070649?u=3ab493dfc88b39da0eb1600e3b8e7df1c90a5dee&v=4 + url: https://github.com/SBillion tyronedamasceno: login: tyronedamasceno count: 3 @@ -1196,7 +1211,7 @@ Xaraxx: Suyoung789: login: Suyoung789 count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/31277231?u=744bd3e641413e19bfad6b06a90bb0887c3f9332&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/31277231?u=1591aaf651eb860017231a36590050e154c026b6&v=4 url: https://github.com/Suyoung789 akagaeng: login: akagaeng @@ -1211,7 +1226,7 @@ phamquanganh31101998: peebbv6364: login: peebbv6364 count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/26784747?u=75583df215ee01a5cd2dc646aecb81e7dbd33d06&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/26784747?u=3bf07017eb4f4fa3639ba8d4ed19980a34bf8f90&v=4 url: https://github.com/peebbv6364 mrparalon: login: mrparalon @@ -1413,11 +1428,6 @@ Mohammad222PR: count: 3 avatarUrl: https://avatars.githubusercontent.com/u/116789737?u=25810a5fe049d2f1618e2e7417cea011cc353ce4&v=4 url: https://github.com/Mohammad222PR -EdmilsonRodrigues: - login: EdmilsonRodrigues - count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/62777025?u=217d6f3cd6cc750bb8818a3af7726c8d74eb7c2d&v=4 - url: https://github.com/EdmilsonRodrigues blaisep: login: blaisep count: 2 @@ -1806,7 +1816,7 @@ MrL8199: ivintoiu: login: ivintoiu count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/1853336?u=b537c905ad08b69993de8796fb235c8d4d47f039&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/1853336?u=e3de5fd0ab17efc12256b4295285b504ca281440&v=4 url: https://github.com/ivintoiu TechnoService2: login: TechnoService2 @@ -1838,11 +1848,11 @@ NavesSapnis: count: 2 avatarUrl: https://avatars.githubusercontent.com/u/79222417?u=b5b10291b8e9130ca84fd20f0a641e04ed94b6b1&v=4 url: https://github.com/NavesSapnis -eqsdxr: - login: eqsdxr +isgin01: + login: isgin01 count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/157279130?u=d7aaffb29f542b647cf0f6b0e05722490863658a&v=4 - url: https://github.com/eqsdxr + avatarUrl: https://avatars.githubusercontent.com/u/157279130?u=ddffde10876b50f35dc90d1337f507a630530a6a&v=4 + url: https://github.com/isgin01 syedasamina56: login: syedasamina56 count: 2 diff --git a/docs/en/data/translators.yml b/docs/en/data/translators.yml index a4b87e1bf5..940b128da2 100644 --- a/docs/en/data/translators.yml +++ b/docs/en/data/translators.yml @@ -1,6 +1,6 @@ nilslindemann: login: nilslindemann - count: 124 + count: 130 avatarUrl: https://avatars.githubusercontent.com/u/6892179?u=1dca6a22195d6cd1ab20737c0e19a4c55d639472&v=4 url: https://github.com/nilslindemann jaystone776: @@ -8,16 +8,16 @@ jaystone776: count: 46 avatarUrl: https://avatars.githubusercontent.com/u/11191137?u=299205a95e9b6817a43144a48b643346a5aac5cc&v=4 url: https://github.com/jaystone776 +ceb10n: + login: ceb10n + count: 29 + avatarUrl: https://avatars.githubusercontent.com/u/235213?u=edcce471814a1eba9f0cdaa4cd0de18921a940a6&v=4 + url: https://github.com/ceb10n valentinDruzhinin: login: valentinDruzhinin count: 29 avatarUrl: https://avatars.githubusercontent.com/u/12831905?u=aae1ebc675c91e8fa582df4fcc4fc4128106344d&v=4 url: https://github.com/valentinDruzhinin -ceb10n: - login: ceb10n - count: 27 - avatarUrl: https://avatars.githubusercontent.com/u/235213?u=edcce471814a1eba9f0cdaa4cd0de18921a940a6&v=4 - url: https://github.com/ceb10n tokusumi: login: tokusumi count: 23 @@ -28,6 +28,11 @@ SwftAlpc: count: 23 avatarUrl: https://avatars.githubusercontent.com/u/52768429?u=6a3aa15277406520ad37f6236e89466ed44bc5b8&v=4 url: https://github.com/SwftAlpc +tiangolo: + login: tiangolo + count: 22 + avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=cb5d06e73a9e1998141b1641aa88e443c6717651&v=4 + url: https://github.com/tiangolo hasansezertasan: login: hasansezertasan count: 22 @@ -46,7 +51,7 @@ AlertRED: hard-coders: login: hard-coders count: 15 - avatarUrl: https://avatars.githubusercontent.com/u/9651103?u=95db33927bbff1ed1c07efddeb97ac2ff33068ed&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/9651103?u=78d12d1acdf853c817700145e73de7fd9e5d068b&v=4 url: https://github.com/hard-coders Joao-Pedro-P-Holanda: login: Joao-Pedro-P-Holanda @@ -103,11 +108,6 @@ pablocm83: count: 8 avatarUrl: https://avatars.githubusercontent.com/u/28315068?u=3310fbb05bb8bfc50d2c48b6cb64ac9ee4a14549&v=4 url: https://github.com/pablocm83 -tiangolo: - login: tiangolo - count: 7 - avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=cb5d06e73a9e1998141b1641aa88e443c6717651&v=4 - url: https://github.com/tiangolo ptt3199: login: ptt3199 count: 7 @@ -126,13 +126,18 @@ batlopes: lucasbalieiro: login: lucasbalieiro count: 6 - avatarUrl: https://avatars.githubusercontent.com/u/37416577?u=dad91601ee4f40458d691774ec439aff308344d7&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/37416577?u=d144221c34c08adac8b20e1833d776ffa1c4b1d0&v=4 url: https://github.com/lucasbalieiro Alexandrhub: login: Alexandrhub count: 6 avatarUrl: https://avatars.githubusercontent.com/u/119126536?u=9fc0d48f3307817bafecc5861eb2168401a6cb04&v=4 url: https://github.com/Alexandrhub +YuriiMotov: + login: YuriiMotov + count: 6 + avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=b9b13d598dddfab529a52d264df80a900bfe7060&v=4 + url: https://github.com/YuriiMotov Serrones: login: Serrones count: 5 @@ -286,7 +291,7 @@ hsuanchi: alejsdev: login: alejsdev count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/90076947?u=447d12a1b347f466b35378bee4c7104cc9b2c571&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/90076947?u=85ceac49fb87138aebe8d663912e359447329090&v=4 url: https://github.com/alejsdev riroan: login: riroan @@ -543,8 +548,3 @@ EdmilsonRodrigues: count: 2 avatarUrl: https://avatars.githubusercontent.com/u/62777025?u=217d6f3cd6cc750bb8818a3af7726c8d74eb7c2d&v=4 url: https://github.com/EdmilsonRodrigues -YuriiMotov: - login: YuriiMotov - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=b9b13d598dddfab529a52d264df80a900bfe7060&v=4 - url: https://github.com/YuriiMotov diff --git a/docs/en/docs/_llm-test.md b/docs/en/docs/_llm-test.md index e72450b916..d218f7c76f 100644 --- a/docs/en/docs/_llm-test.md +++ b/docs/en/docs/_llm-test.md @@ -6,7 +6,7 @@ Tests added here will be seen by all designers of language specific prompts. Use as follows: -* Have a language specific prompt – `docs/{language code}/llm-prompt.md`. +* Have a language specific prompt - `docs/{language code}/llm-prompt.md`. * Do a fresh translation of this document into your desired target language (see e.g. the `translate-page` command of the `translate.py`). This will create the translation under `docs/{language code}/docs/_llm-test.md`. * Check if things are okay in the translation. * If necessary, improve your language specific prompt, the general prompt, or the English document. @@ -15,7 +15,7 @@ Use as follows: The tests: -## Code snippets { #code-snippets} +## Code snippets { #code-snippets } //// tab | Test @@ -53,7 +53,7 @@ See for example section `### Quotes` in `docs/de/llm-prompt.md`. //// -## Quotes in code snippets { #quotes-in-code-snippets} +## Quotes in code snippets { #quotes-in-code-snippets } //// tab | Test diff --git a/docs/en/docs/advanced/additional-responses.md b/docs/en/docs/advanced/additional-responses.md index 799532c5b2..bb70753edd 100644 --- a/docs/en/docs/advanced/additional-responses.md +++ b/docs/en/docs/advanced/additional-responses.md @@ -26,7 +26,7 @@ Each of those response `dict`s can have a key `model`, containing a Pydantic mod For example, to declare another response with a status code `404` and a Pydantic model `Message`, you can write: -{* ../../docs_src/additional_responses/tutorial001.py hl[18,22] *} +{* ../../docs_src/additional_responses/tutorial001_py39.py hl[18,22] *} /// note @@ -175,7 +175,7 @@ You can use this same `responses` parameter to add different media types for the For example, you can add an additional media type of `image/png`, declaring that your *path operation* can return a JSON object (with media type `application/json`) or a PNG image: -{* ../../docs_src/additional_responses/tutorial002.py hl[19:24,28] *} +{* ../../docs_src/additional_responses/tutorial002_py310.py hl[17:22,26] *} /// note @@ -203,7 +203,7 @@ For example, you can declare a response with a status code `404` that uses a Pyd And a response with a status code `200` that uses your `response_model`, but includes a custom `example`: -{* ../../docs_src/additional_responses/tutorial003.py hl[20:31] *} +{* ../../docs_src/additional_responses/tutorial003_py39.py hl[20:31] *} It will all be combined and included in your OpenAPI, and shown in the API docs: @@ -237,7 +237,7 @@ You can use that technique to reuse some predefined responses in your *path oper For example: -{* ../../docs_src/additional_responses/tutorial004.py hl[13:17,26] *} +{* ../../docs_src/additional_responses/tutorial004_py310.py hl[11:15,24] *} ## More information about OpenAPI responses { #more-information-about-openapi-responses } diff --git a/docs/en/docs/advanced/advanced-dependencies.md b/docs/en/docs/advanced/advanced-dependencies.md index 5d6a40f463..37f5c78f2d 100644 --- a/docs/en/docs/advanced/advanced-dependencies.md +++ b/docs/en/docs/advanced/advanced-dependencies.md @@ -144,7 +144,7 @@ This was changed in version 0.110.0 to fix unhandled memory consumption from for ### Background Tasks and Dependencies with `yield`, Technical Details { #background-tasks-and-dependencies-with-yield-technical-details } -Before FastAPI 0.106.0, raising exceptions after `yield` was not possible, the exit code in dependencies with `yield` was executed *after* the response was sent, so [Exception Handlers](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank} would have already run. +Before FastAPI 0.106.0, raising exceptions after `yield` was not possible, the exit code in dependencies with `yield` was executed *after* the response was sent, so [Exception Handlers](../tutorial/handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank} would have already run. This was designed this way mainly to allow using the same objects "yielded" by dependencies inside of background tasks, because the exit code would be executed after the background tasks were finished. diff --git a/docs/en/docs/advanced/async-tests.md b/docs/en/docs/advanced/async-tests.md index e920e22c3c..65ddc60b2d 100644 --- a/docs/en/docs/advanced/async-tests.md +++ b/docs/en/docs/advanced/async-tests.md @@ -32,11 +32,11 @@ For a simple example, let's consider a file structure similar to the one describ The file `main.py` would have: -{* ../../docs_src/async_tests/main.py *} +{* ../../docs_src/async_tests/app_a_py39/main.py *} The file `test_main.py` would have the tests for `main.py`, it could look like this now: -{* ../../docs_src/async_tests/test_main.py *} +{* ../../docs_src/async_tests/app_a_py39/test_main.py *} ## Run it { #run-it } @@ -56,7 +56,7 @@ $ pytest The marker `@pytest.mark.anyio` tells pytest that this test function should be called asynchronously: -{* ../../docs_src/async_tests/test_main.py hl[7] *} +{* ../../docs_src/async_tests/app_a_py39/test_main.py hl[7] *} /// tip @@ -66,7 +66,7 @@ Note that the test function is now `async def` instead of just `def` as before w Then we can create an `AsyncClient` with the app, and send async requests to it, using `await`. -{* ../../docs_src/async_tests/test_main.py hl[9:12] *} +{* ../../docs_src/async_tests/app_a_py39/test_main.py hl[9:12] *} This is the equivalent to: diff --git a/docs/en/docs/advanced/behind-a-proxy.md b/docs/en/docs/advanced/behind-a-proxy.md index 4d19d29e02..4fef02bd1c 100644 --- a/docs/en/docs/advanced/behind-a-proxy.md +++ b/docs/en/docs/advanced/behind-a-proxy.md @@ -44,7 +44,7 @@ $ fastapi run --forwarded-allow-ips="*" For example, let's say you define a *path operation* `/items/`: -{* ../../docs_src/behind_a_proxy/tutorial001_01.py hl[6] *} +{* ../../docs_src/behind_a_proxy/tutorial001_01_py39.py hl[6] *} If the client tries to go to `/items`, by default, it would be redirected to `/items/`. @@ -64,7 +64,7 @@ If you want to learn more about HTTPS, check the guide [About HTTPS](../deployme /// -### How Proxy Forwarded Headers Work +### How Proxy Forwarded Headers Work { #how-proxy-forwarded-headers-work } Here's a visual representation of how the **proxy** adds forwarded headers between the client and the **application server**: @@ -115,7 +115,7 @@ In this case, the original path `/app` would actually be served at `/api/v1/app` Even though all your code is written assuming there's just `/app`. -{* ../../docs_src/behind_a_proxy/tutorial001.py hl[6] *} +{* ../../docs_src/behind_a_proxy/tutorial001_py39.py hl[6] *} And the proxy would be **"stripping"** the **path prefix** on the fly before transmitting the request to the app server (probably Uvicorn via FastAPI CLI), keeping your application convinced that it is being served at `/app`, so that you don't have to update all your code to include the prefix `/api/v1`. @@ -193,7 +193,7 @@ You can get the current `root_path` used by your application for each request, i Here we are including it in the message just for demonstration purposes. -{* ../../docs_src/behind_a_proxy/tutorial001.py hl[8] *} +{* ../../docs_src/behind_a_proxy/tutorial001_py39.py hl[8] *} Then, if you start Uvicorn with: @@ -220,7 +220,7 @@ The response would be something like: Alternatively, if you don't have a way to provide a command line option like `--root-path` or equivalent, you can set the `root_path` parameter when creating your FastAPI app: -{* ../../docs_src/behind_a_proxy/tutorial002.py hl[3] *} +{* ../../docs_src/behind_a_proxy/tutorial002_py39.py hl[3] *} Passing the `root_path` to `FastAPI` would be the equivalent of passing the `--root-path` command line option to Uvicorn or Hypercorn. @@ -228,7 +228,7 @@ Passing the `root_path` to `FastAPI` would be the equivalent of passing the `--r Keep in mind that the server (Uvicorn) won't use that `root_path` for anything else than passing it to the app. -But if you go with your browser to http://127.0.0.1:8000/app you will see the normal response: +But if you go with your browser to http://127.0.0.1:8000/app you will see the normal response: ```JSON { @@ -400,7 +400,7 @@ If you pass a custom list of `servers` and there's a `root_path` (because your A For example: -{* ../../docs_src/behind_a_proxy/tutorial003.py hl[4:7] *} +{* ../../docs_src/behind_a_proxy/tutorial003_py39.py hl[4:7] *} Will generate an OpenAPI schema like: @@ -443,11 +443,19 @@ The docs UI will interact with the server that you select. /// +/// note | Technical Details + +The `servers` property in the OpenAPI specification is optional. + +If you don't specify the `servers` parameter and `root_path` is equal to `/`, the `servers` property in the generated OpenAPI schema will be omitted entirely by default, which is the equivalent of a single server with a `url` value of `/`. + +/// + ### Disable automatic server from `root_path` { #disable-automatic-server-from-root-path } If you don't want **FastAPI** to include an automatic server using the `root_path`, you can use the parameter `root_path_in_servers=False`: -{* ../../docs_src/behind_a_proxy/tutorial004.py hl[9] *} +{* ../../docs_src/behind_a_proxy/tutorial004_py39.py hl[9] *} and then it won't include it in the OpenAPI schema. diff --git a/docs/en/docs/advanced/custom-response.md b/docs/en/docs/advanced/custom-response.md index 0f3d8b7017..e53409c39d 100644 --- a/docs/en/docs/advanced/custom-response.md +++ b/docs/en/docs/advanced/custom-response.md @@ -30,7 +30,7 @@ This is because by default, FastAPI will inspect every item inside and make sure But if you are certain that the content that you are returning is **serializable with JSON**, you can pass it directly to the response class and avoid the extra overhead that FastAPI would have by passing your return content through the `jsonable_encoder` before passing it to the response class. -{* ../../docs_src/custom_response/tutorial001b.py hl[2,7] *} +{* ../../docs_src/custom_response/tutorial001b_py39.py hl[2,7] *} /// info @@ -55,7 +55,7 @@ To return a response with HTML directly from **FastAPI**, use `HTMLResponse`. * Import `HTMLResponse`. * Pass `HTMLResponse` as the parameter `response_class` of your *path operation decorator*. -{* ../../docs_src/custom_response/tutorial002.py hl[2,7] *} +{* ../../docs_src/custom_response/tutorial002_py39.py hl[2,7] *} /// info @@ -73,7 +73,7 @@ As seen in [Return a Response directly](response-directly.md){.internal-link tar The same example from above, returning an `HTMLResponse`, could look like: -{* ../../docs_src/custom_response/tutorial003.py hl[2,7,19] *} +{* ../../docs_src/custom_response/tutorial003_py39.py hl[2,7,19] *} /// warning @@ -97,7 +97,7 @@ The `response_class` will then be used only to document the OpenAPI *path operat For example, it could be something like: -{* ../../docs_src/custom_response/tutorial004.py hl[7,21,23] *} +{* ../../docs_src/custom_response/tutorial004_py39.py hl[7,21,23] *} In this example, the function `generate_html_response()` already generates and returns a `Response` instead of returning the HTML in a `str`. @@ -136,7 +136,7 @@ It accepts the following parameters: FastAPI (actually Starlette) will automatically include a Content-Length header. It will also include a Content-Type header, based on the `media_type` and appending a charset for text types. -{* ../../docs_src/response_directly/tutorial002.py hl[1,18] *} +{* ../../docs_src/response_directly/tutorial002_py39.py hl[1,18] *} ### `HTMLResponse` { #htmlresponse } @@ -146,7 +146,7 @@ Takes some text or bytes and returns an HTML response, as you read above. Takes some text or bytes and returns a plain text response. -{* ../../docs_src/custom_response/tutorial005.py hl[2,7,9] *} +{* ../../docs_src/custom_response/tutorial005_py39.py hl[2,7,9] *} ### `JSONResponse` { #jsonresponse } @@ -180,7 +180,7 @@ This requires installing `ujson` for example with `pip install ujson`. /// -{* ../../docs_src/custom_response/tutorial001.py hl[2,7] *} +{* ../../docs_src/custom_response/tutorial001_py39.py hl[2,7] *} /// tip @@ -194,14 +194,14 @@ Returns an HTTP redirect. Uses a 307 status code (Temporary Redirect) by default You can return a `RedirectResponse` directly: -{* ../../docs_src/custom_response/tutorial006.py hl[2,9] *} +{* ../../docs_src/custom_response/tutorial006_py39.py hl[2,9] *} --- Or you can use it in the `response_class` parameter: -{* ../../docs_src/custom_response/tutorial006b.py hl[2,7,9] *} +{* ../../docs_src/custom_response/tutorial006b_py39.py hl[2,7,9] *} If you do that, then you can return the URL directly from your *path operation* function. @@ -211,13 +211,13 @@ In this case, the `status_code` used will be the default one for the `RedirectRe You can also use the `status_code` parameter combined with the `response_class` parameter: -{* ../../docs_src/custom_response/tutorial006c.py hl[2,7,9] *} +{* ../../docs_src/custom_response/tutorial006c_py39.py hl[2,7,9] *} ### `StreamingResponse` { #streamingresponse } Takes an async generator or a normal generator/iterator and streams the response body. -{* ../../docs_src/custom_response/tutorial007.py hl[2,14] *} +{* ../../docs_src/custom_response/tutorial007_py39.py hl[2,14] *} #### Using `StreamingResponse` with file-like objects { #using-streamingresponse-with-file-like-objects } @@ -227,7 +227,7 @@ That way, you don't have to read it all first in memory, and you can pass that g This includes many libraries to interact with cloud storage, video processing, and others. -{* ../../docs_src/custom_response/tutorial008.py hl[2,10:12,14] *} +{* ../../docs_src/custom_response/tutorial008_py39.py hl[2,10:12,14] *} 1. This is the generator function. It's a "generator function" because it contains `yield` statements inside. 2. By using a `with` block, we make sure that the file-like object is closed after the generator function is done. So, after it finishes sending the response. @@ -256,11 +256,11 @@ Takes a different set of arguments to instantiate than the other response types: File responses will include appropriate `Content-Length`, `Last-Modified` and `ETag` headers. -{* ../../docs_src/custom_response/tutorial009.py hl[2,10] *} +{* ../../docs_src/custom_response/tutorial009_py39.py hl[2,10] *} You can also use the `response_class` parameter: -{* ../../docs_src/custom_response/tutorial009b.py hl[2,8,10] *} +{* ../../docs_src/custom_response/tutorial009b_py39.py hl[2,8,10] *} In this case, you can return the file path directly from your *path operation* function. @@ -274,7 +274,7 @@ Let's say you want it to return indented and formatted JSON, so you want to use You could create a `CustomORJSONResponse`. The main thing you have to do is create a `Response.render(content)` method that returns the content as `bytes`: -{* ../../docs_src/custom_response/tutorial009c.py hl[9:14,17] *} +{* ../../docs_src/custom_response/tutorial009c_py39.py hl[9:14,17] *} Now instead of returning: @@ -300,7 +300,7 @@ The parameter that defines this is `default_response_class`. In the example below, **FastAPI** will use `ORJSONResponse` by default, in all *path operations*, instead of `JSONResponse`. -{* ../../docs_src/custom_response/tutorial010.py hl[2,4] *} +{* ../../docs_src/custom_response/tutorial010_py39.py hl[2,4] *} /// tip diff --git a/docs/en/docs/advanced/dataclasses.md b/docs/en/docs/advanced/dataclasses.md index b7b9b65c52..dbc91409a5 100644 --- a/docs/en/docs/advanced/dataclasses.md +++ b/docs/en/docs/advanced/dataclasses.md @@ -4,7 +4,7 @@ FastAPI is built on top of **Pydantic**, and I have been showing you how to use But FastAPI also supports using `dataclasses` the same way: -{* ../../docs_src/dataclasses/tutorial001.py hl[1,7:12,19:20] *} +{* ../../docs_src/dataclasses_/tutorial001_py310.py hl[1,6:11,18:19] *} This is still supported thanks to **Pydantic**, as it has internal support for `dataclasses`. @@ -32,7 +32,7 @@ But if you have a bunch of dataclasses laying around, this is a nice trick to us You can also use `dataclasses` in the `response_model` parameter: -{* ../../docs_src/dataclasses/tutorial002.py hl[1,7:13,19] *} +{* ../../docs_src/dataclasses_/tutorial002_py310.py hl[1,6:12,18] *} The dataclass will be automatically converted to a Pydantic dataclass. @@ -48,7 +48,7 @@ In some cases, you might still have to use Pydantic's version of `dataclasses`. In that case, you can simply swap the standard `dataclasses` with `pydantic.dataclasses`, which is a drop-in replacement: -{* ../../docs_src/dataclasses/tutorial003.py hl[1,5,8:11,14:17,23:25,28] *} +{* ../../docs_src/dataclasses_/tutorial003_py310.py hl[1,4,7:10,13:16,22:24,27] *} 1. We still import `field` from standard `dataclasses`. diff --git a/docs/en/docs/advanced/events.md b/docs/en/docs/advanced/events.md index d9e3cb52e7..9414b7a3f0 100644 --- a/docs/en/docs/advanced/events.md +++ b/docs/en/docs/advanced/events.md @@ -30,7 +30,7 @@ Let's start with an example and then see it in detail. We create an async function `lifespan()` with `yield` like this: -{* ../../docs_src/events/tutorial003.py hl[16,19] *} +{* ../../docs_src/events/tutorial003_py39.py hl[16,19] *} Here we are simulating the expensive *startup* operation of loading the model by putting the (fake) model function in the dictionary with machine learning models before the `yield`. This code will be executed **before** the application **starts taking requests**, during the *startup*. @@ -48,7 +48,7 @@ Maybe you need to start a new version, or you just got tired of running it. 🤷 The first thing to notice, is that we are defining an async function with `yield`. This is very similar to Dependencies with `yield`. -{* ../../docs_src/events/tutorial003.py hl[14:19] *} +{* ../../docs_src/events/tutorial003_py39.py hl[14:19] *} The first part of the function, before the `yield`, will be executed **before** the application starts. @@ -60,7 +60,7 @@ If you check, the function is decorated with an `@asynccontextmanager`. That converts the function into something called an "**async context manager**". -{* ../../docs_src/events/tutorial003.py hl[1,13] *} +{* ../../docs_src/events/tutorial003_py39.py hl[1,13] *} A **context manager** in Python is something that you can use in a `with` statement, for example, `open()` can be used as a context manager: @@ -82,7 +82,7 @@ In our code example above, we don't use it directly, but we pass it to FastAPI f The `lifespan` parameter of the `FastAPI` app takes an **async context manager**, so we can pass our new `lifespan` async context manager to it. -{* ../../docs_src/events/tutorial003.py hl[22] *} +{* ../../docs_src/events/tutorial003_py39.py hl[22] *} ## Alternative Events (deprecated) { #alternative-events-deprecated } @@ -104,7 +104,7 @@ These functions can be declared with `async def` or normal `def`. To add a function that should be run before the application starts, declare it with the event `"startup"`: -{* ../../docs_src/events/tutorial001.py hl[8] *} +{* ../../docs_src/events/tutorial001_py39.py hl[8] *} In this case, the `startup` event handler function will initialize the items "database" (just a `dict`) with some values. @@ -116,7 +116,7 @@ And your application won't start receiving requests until all the `startup` even To add a function that should be run when the application is shutting down, declare it with the event `"shutdown"`: -{* ../../docs_src/events/tutorial002.py hl[6] *} +{* ../../docs_src/events/tutorial002_py39.py hl[6] *} Here, the `shutdown` event handler function will write a text line `"Application shutdown"` to a file `log.txt`. diff --git a/docs/en/docs/advanced/generate-clients.md b/docs/en/docs/advanced/generate-clients.md index 897c308086..2d0c2aa0c9 100644 --- a/docs/en/docs/advanced/generate-clients.md +++ b/docs/en/docs/advanced/generate-clients.md @@ -167,7 +167,7 @@ But for the generated client, we could **modify** the OpenAPI operation IDs righ We could download the OpenAPI JSON to a file `openapi.json` and then we could **remove that prefixed tag** with a script like this: -{* ../../docs_src/generate_clients/tutorial004.py *} +{* ../../docs_src/generate_clients/tutorial004_py39.py *} //// tab | Node.js diff --git a/docs/en/docs/advanced/middleware.md b/docs/en/docs/advanced/middleware.md index 8deb0d917d..765b389329 100644 --- a/docs/en/docs/advanced/middleware.md +++ b/docs/en/docs/advanced/middleware.md @@ -57,13 +57,13 @@ Enforces that all incoming requests must either be `https` or `wss`. Any incoming request to `http` or `ws` will be redirected to the secure scheme instead. -{* ../../docs_src/advanced_middleware/tutorial001.py hl[2,6] *} +{* ../../docs_src/advanced_middleware/tutorial001_py39.py hl[2,6] *} ## `TrustedHostMiddleware` { #trustedhostmiddleware } Enforces that all incoming requests have a correctly set `Host` header, in order to guard against HTTP Host Header attacks. -{* ../../docs_src/advanced_middleware/tutorial002.py hl[2,6:8] *} +{* ../../docs_src/advanced_middleware/tutorial002_py39.py hl[2,6:8] *} The following arguments are supported: @@ -78,7 +78,7 @@ Handles GZip responses for any request that includes `"gzip"` in the `Accept-Enc The middleware will handle both standard and streaming responses. -{* ../../docs_src/advanced_middleware/tutorial003.py hl[2,6] *} +{* ../../docs_src/advanced_middleware/tutorial003_py39.py hl[2,6] *} The following arguments are supported: diff --git a/docs/en/docs/advanced/openapi-callbacks.md b/docs/en/docs/advanced/openapi-callbacks.md index 059d893c26..5bd7c2cfd4 100644 --- a/docs/en/docs/advanced/openapi-callbacks.md +++ b/docs/en/docs/advanced/openapi-callbacks.md @@ -31,7 +31,7 @@ It will have a *path operation* that will receive an `Invoice` body, and a query This part is pretty normal, most of the code is probably already familiar to you: -{* ../../docs_src/openapi_callbacks/tutorial001.py hl[9:13,36:53] *} +{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[7:11,34:51] *} /// tip @@ -90,7 +90,7 @@ Temporarily adopting this point of view (of the *external developer*) can help y First create a new `APIRouter` that will contain one or more callbacks. -{* ../../docs_src/openapi_callbacks/tutorial001.py hl[3,25] *} +{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[1,23] *} ### Create the callback *path operation* { #create-the-callback-path-operation } @@ -101,7 +101,7 @@ It should look just like a normal FastAPI *path operation*: * It should probably have a declaration of the body it should receive, e.g. `body: InvoiceEvent`. * And it could also have a declaration of the response it should return, e.g. `response_model=InvoiceEventReceived`. -{* ../../docs_src/openapi_callbacks/tutorial001.py hl[16:18,21:22,28:32] *} +{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[14:16,19:20,26:30] *} There are 2 main differences from a normal *path operation*: @@ -169,7 +169,7 @@ At this point you have the *callback path operation(s)* needed (the one(s) that Now use the parameter `callbacks` in *your API's path operation decorator* to pass the attribute `.routes` (that's actually just a `list` of routes/*path operations*) from that callback router: -{* ../../docs_src/openapi_callbacks/tutorial001.py hl[35] *} +{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[33] *} /// tip diff --git a/docs/en/docs/advanced/openapi-webhooks.md b/docs/en/docs/advanced/openapi-webhooks.md index 416cf4b75b..59f060c032 100644 --- a/docs/en/docs/advanced/openapi-webhooks.md +++ b/docs/en/docs/advanced/openapi-webhooks.md @@ -32,7 +32,7 @@ Webhooks are available in OpenAPI 3.1.0 and above, supported by FastAPI `0.99.0` When you create a **FastAPI** application, there is a `webhooks` attribute that you can use to define *webhooks*, the same way you would define *path operations*, for example with `@app.webhooks.post()`. -{* ../../docs_src/openapi_webhooks/tutorial001.py hl[9:13,36:53] *} +{* ../../docs_src/openapi_webhooks/tutorial001_py39.py hl[9:13,36:53] *} The webhooks that you define will end up in the **OpenAPI** schema and the automatic **docs UI**. diff --git a/docs/en/docs/advanced/path-operation-advanced-configuration.md b/docs/en/docs/advanced/path-operation-advanced-configuration.md index b9961f9f38..e0e3c96a0e 100644 --- a/docs/en/docs/advanced/path-operation-advanced-configuration.md +++ b/docs/en/docs/advanced/path-operation-advanced-configuration.md @@ -12,7 +12,7 @@ You can set the OpenAPI `operationId` to be used in your *path operation* with t You would have to make sure that it is unique for each operation. -{* ../../docs_src/path_operation_advanced_configuration/tutorial001.py hl[6] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial001_py39.py hl[6] *} ### Using the *path operation function* name as the operationId { #using-the-path-operation-function-name-as-the-operationid } @@ -20,7 +20,7 @@ If you want to use your APIs' function names as `operationId`s, you can iterate You should do it after adding all your *path operations*. -{* ../../docs_src/path_operation_advanced_configuration/tutorial002.py hl[2, 12:21, 24] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial002_py39.py hl[2, 12:21, 24] *} /// tip @@ -40,7 +40,7 @@ Even if they are in different modules (Python files). To exclude a *path operation* from the generated OpenAPI schema (and thus, from the automatic documentation systems), use the parameter `include_in_schema` and set it to `False`: -{* ../../docs_src/path_operation_advanced_configuration/tutorial003.py hl[6] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial003_py39.py hl[6] *} ## Advanced description from docstring { #advanced-description-from-docstring } @@ -50,7 +50,7 @@ Adding an `\f` (an escaped "form feed" character) causes **FastAPI** to truncate It won't show up in the documentation, but other tools (such as Sphinx) will be able to use the rest. -{* ../../docs_src/path_operation_advanced_configuration/tutorial004.py hl[19:29] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial004_py310.py hl[17:27] *} ## Additional Responses { #additional-responses } @@ -92,7 +92,7 @@ You can extend the OpenAPI schema for a *path operation* using the parameter `op This `openapi_extra` can be helpful, for example, to declare [OpenAPI Extensions](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#specificationExtensions): -{* ../../docs_src/path_operation_advanced_configuration/tutorial005.py hl[6] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial005_py39.py hl[6] *} If you open the automatic API docs, your extension will show up at the bottom of the specific *path operation*. @@ -139,7 +139,7 @@ For example, you could decide to read and validate the request with your own cod You could do that with `openapi_extra`: -{* ../../docs_src/path_operation_advanced_configuration/tutorial006.py hl[19:36, 39:40] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial006_py39.py hl[19:36, 39:40] *} In this example, we didn't declare any Pydantic model. In fact, the request body is not even parsed as JSON, it is read directly as `bytes`, and the function `magic_data_reader()` would be in charge of parsing it in some way. @@ -153,23 +153,7 @@ And you could do this even if the data type in the request is not JSON. For example, in this application we don't use FastAPI's integrated functionality to extract the JSON Schema from Pydantic models nor the automatic validation for JSON. In fact, we are declaring the request content type as YAML, not JSON: -//// tab | Pydantic v2 - -{* ../../docs_src/path_operation_advanced_configuration/tutorial007.py hl[17:22, 24] *} - -//// - -//// tab | Pydantic v1 - -{* ../../docs_src/path_operation_advanced_configuration/tutorial007_pv1.py hl[17:22, 24] *} - -//// - -/// info - -In Pydantic version 1 the method to get the JSON Schema for a model was called `Item.schema()`, in Pydantic version 2, the method is called `Item.model_json_schema()`. - -/// +{* ../../docs_src/path_operation_advanced_configuration/tutorial007_py39.py hl[15:20, 22] *} Nevertheless, although we are not using the default integrated functionality, we are still using a Pydantic model to manually generate the JSON Schema for the data that we want to receive in YAML. @@ -177,23 +161,7 @@ Then we use the request directly, and extract the body as `bytes`. This means th And then in our code, we parse that YAML content directly, and then we are again using the same Pydantic model to validate the YAML content: -//// tab | Pydantic v2 - -{* ../../docs_src/path_operation_advanced_configuration/tutorial007.py hl[26:33] *} - -//// - -//// tab | Pydantic v1 - -{* ../../docs_src/path_operation_advanced_configuration/tutorial007_pv1.py hl[26:33] *} - -//// - -/// info - -In Pydantic version 1 the method to parse and validate an object was `Item.parse_obj()`, in Pydantic version 2, the method is called `Item.model_validate()`. - -/// +{* ../../docs_src/path_operation_advanced_configuration/tutorial007_py39.py hl[24:31] *} /// tip diff --git a/docs/en/docs/advanced/response-change-status-code.md b/docs/en/docs/advanced/response-change-status-code.md index 912ed0f1a1..d9708aa627 100644 --- a/docs/en/docs/advanced/response-change-status-code.md +++ b/docs/en/docs/advanced/response-change-status-code.md @@ -20,7 +20,7 @@ You can declare a parameter of type `Response` in your *path operation function* And then you can set the `status_code` in that *temporal* response object. -{* ../../docs_src/response_change_status_code/tutorial001.py hl[1,9,12] *} +{* ../../docs_src/response_change_status_code/tutorial001_py39.py hl[1,9,12] *} And then you can return any object you need, as you normally would (a `dict`, a database model, etc). diff --git a/docs/en/docs/advanced/response-cookies.md b/docs/en/docs/advanced/response-cookies.md index 1f41d84b7c..5b6fab112b 100644 --- a/docs/en/docs/advanced/response-cookies.md +++ b/docs/en/docs/advanced/response-cookies.md @@ -6,7 +6,7 @@ You can declare a parameter of type `Response` in your *path operation function* And then you can set cookies in that *temporal* response object. -{* ../../docs_src/response_cookies/tutorial002.py hl[1, 8:9] *} +{* ../../docs_src/response_cookies/tutorial002_py39.py hl[1, 8:9] *} And then you can return any object you need, as you normally would (a `dict`, a database model, etc). @@ -24,7 +24,7 @@ To do that, you can create a response as described in [Return a Response Directl Then set Cookies in it, and then return it: -{* ../../docs_src/response_cookies/tutorial001.py hl[10:12] *} +{* ../../docs_src/response_cookies/tutorial001_py39.py hl[10:12] *} /// tip diff --git a/docs/en/docs/advanced/response-directly.md b/docs/en/docs/advanced/response-directly.md index 3197e1bd46..4374cb9637 100644 --- a/docs/en/docs/advanced/response-directly.md +++ b/docs/en/docs/advanced/response-directly.md @@ -34,7 +34,7 @@ For example, you cannot put a Pydantic model in a `JSONResponse` without first c For those cases, you can use the `jsonable_encoder` to convert your data before passing it to a response: -{* ../../docs_src/response_directly/tutorial001.py hl[6:7,21:22] *} +{* ../../docs_src/response_directly/tutorial001_py310.py hl[5:6,20:21] *} /// note | Technical Details @@ -54,7 +54,7 @@ Let's say that you want to return an -/// info - -In Pydantic v1 it came included with the main package. Now it is distributed as this independent package so that you can choose to install it or not if you don't need that functionality. - -/// - ### Create the `Settings` object { #create-the-settings-object } Import `BaseSettings` from Pydantic and create a sub-class, very much like with a Pydantic model. @@ -60,23 +54,7 @@ The same way as with Pydantic models, you declare class attributes with type ann You can use all the same validation features and tools you use for Pydantic models, like different data types and additional validations with `Field()`. -//// tab | Pydantic v2 - -{* ../../docs_src/settings/tutorial001.py hl[2,5:8,11] *} - -//// - -//// tab | Pydantic v1 - -/// info - -In Pydantic v1 you would import `BaseSettings` directly from `pydantic` instead of from `pydantic_settings`. - -/// - -{* ../../docs_src/settings/tutorial001_pv1.py hl[2,5:8,11] *} - -//// +{* ../../docs_src/settings/tutorial001_py39.py hl[2,5:8,11] *} /// tip @@ -92,7 +70,7 @@ Next it will convert and validate the data. So, when you use that `settings` obj Then you can use the new `settings` object in your application: -{* ../../docs_src/settings/tutorial001.py hl[18:20] *} +{* ../../docs_src/settings/tutorial001_py39.py hl[18:20] *} ### Run the server { #run-the-server } @@ -126,11 +104,11 @@ You could put those settings in another module file as you saw in [Bigger Applic For example, you could have a file `config.py` with: -{* ../../docs_src/settings/app01/config.py *} +{* ../../docs_src/settings/app01_py39/config.py *} And then use it in a file `main.py`: -{* ../../docs_src/settings/app01/main.py hl[3,11:13] *} +{* ../../docs_src/settings/app01_py39/main.py hl[3,11:13] *} /// tip @@ -148,7 +126,7 @@ This could be especially useful during testing, as it's very easy to override a Coming from the previous example, your `config.py` file could look like: -{* ../../docs_src/settings/app02/config.py hl[10] *} +{* ../../docs_src/settings/app02_an_py39/config.py hl[10] *} Notice that now we don't create a default instance `settings = Settings()`. @@ -174,7 +152,7 @@ And then we can require it from the *path operation function* as a dependency an Then it would be very easy to provide a different settings object during testing by creating a dependency override for `get_settings`: -{* ../../docs_src/settings/app02/test_main.py hl[9:10,13,21] *} +{* ../../docs_src/settings/app02_an_py39/test_main.py hl[9:10,13,21] *} In the dependency override we set a new value for the `admin_email` when creating the new `Settings` object, and then we return that new object. @@ -215,9 +193,7 @@ APP_NAME="ChimichangApp" And then update your `config.py` with: -//// tab | Pydantic v2 - -{* ../../docs_src/settings/app03_an/config.py hl[9] *} +{* ../../docs_src/settings/app03_an_py39/config.py hl[9] *} /// tip @@ -225,26 +201,6 @@ The `model_config` attribute is used just for Pydantic configuration. You can re /// -//// - -//// tab | Pydantic v1 - -{* ../../docs_src/settings/app03_an/config_pv1.py hl[9:10] *} - -/// tip - -The `Config` class is used just for Pydantic configuration. You can read more at Pydantic Model Config. - -/// - -//// - -/// info - -In Pydantic version 1 the configuration was done in an internal class `Config`, in Pydantic version 2 it's done in an attribute `model_config`. This attribute takes a `dict`, and to get autocompletion and inline errors you can import and use `SettingsConfigDict` to define that `dict`. - -/// - Here we define the config `env_file` inside of your Pydantic `Settings` class, and set the value to the filename with the dotenv file we want to use. ### Creating the `Settings` only once with `lru_cache` { #creating-the-settings-only-once-with-lru-cache } diff --git a/docs/en/docs/advanced/sub-applications.md b/docs/en/docs/advanced/sub-applications.md index fbd0e1af39..60bb15c029 100644 --- a/docs/en/docs/advanced/sub-applications.md +++ b/docs/en/docs/advanced/sub-applications.md @@ -10,7 +10,7 @@ If you need to have two independent FastAPI applications, with their own indepen First, create the main, top-level, **FastAPI** application, and its *path operations*: -{* ../../docs_src/sub_applications/tutorial001.py hl[3, 6:8] *} +{* ../../docs_src/sub_applications/tutorial001_py39.py hl[3, 6:8] *} ### Sub-application { #sub-application } @@ -18,7 +18,7 @@ Then, create your sub-application, and its *path operations*. This sub-application is just another standard FastAPI application, but this is the one that will be "mounted": -{* ../../docs_src/sub_applications/tutorial001.py hl[11, 14:16] *} +{* ../../docs_src/sub_applications/tutorial001_py39.py hl[11, 14:16] *} ### Mount the sub-application { #mount-the-sub-application } @@ -26,7 +26,7 @@ In your top-level application, `app`, mount the sub-application, `subapi`. In this case, it will be mounted at the path `/subapi`: -{* ../../docs_src/sub_applications/tutorial001.py hl[11, 19] *} +{* ../../docs_src/sub_applications/tutorial001_py39.py hl[11, 19] *} ### Check the automatic API docs { #check-the-automatic-api-docs } diff --git a/docs/en/docs/advanced/templates.md b/docs/en/docs/advanced/templates.md index 356f4d9caf..c843d60f73 100644 --- a/docs/en/docs/advanced/templates.md +++ b/docs/en/docs/advanced/templates.md @@ -27,7 +27,7 @@ $ pip install jinja2 * Declare a `Request` parameter in the *path operation* that will return a template. * Use the `templates` you created to render and return a `TemplateResponse`, pass the name of the template, the request object, and a "context" dictionary with key-value pairs to be used inside of the Jinja2 template. -{* ../../docs_src/templates/tutorial001.py hl[4,11,15:18] *} +{* ../../docs_src/templates/tutorial001_py39.py hl[4,11,15:18] *} /// note diff --git a/docs/en/docs/advanced/testing-events.md b/docs/en/docs/advanced/testing-events.md index dd93374c4c..13c6e2a259 100644 --- a/docs/en/docs/advanced/testing-events.md +++ b/docs/en/docs/advanced/testing-events.md @@ -2,11 +2,11 @@ When you need `lifespan` to run in your tests, you can use the `TestClient` with a `with` statement: -{* ../../docs_src/app_testing/tutorial004.py hl[9:15,18,27:28,30:32,41:43] *} +{* ../../docs_src/app_testing/tutorial004_py39.py hl[9:15,18,27:28,30:32,41:43] *} You can read more details about the ["Running lifespan in tests in the official Starlette documentation site."](https://www.starlette.dev/lifespan/#running-lifespan-in-tests) For the deprecated `startup` and `shutdown` events, you can use the `TestClient` as follows: -{* ../../docs_src/app_testing/tutorial003.py hl[9:12,20:24] *} +{* ../../docs_src/app_testing/tutorial003_py39.py hl[9:12,20:24] *} diff --git a/docs/en/docs/advanced/testing-websockets.md b/docs/en/docs/advanced/testing-websockets.md index 27eb2de2fc..a3cc381a33 100644 --- a/docs/en/docs/advanced/testing-websockets.md +++ b/docs/en/docs/advanced/testing-websockets.md @@ -4,7 +4,7 @@ You can use the same `TestClient` to test WebSockets. For this, you use the `TestClient` in a `with` statement, connecting to the WebSocket: -{* ../../docs_src/app_testing/tutorial002.py hl[27:31] *} +{* ../../docs_src/app_testing/tutorial002_py39.py hl[27:31] *} /// note diff --git a/docs/en/docs/advanced/using-request-directly.md b/docs/en/docs/advanced/using-request-directly.md index c71d3b05d1..bc23f2df90 100644 --- a/docs/en/docs/advanced/using-request-directly.md +++ b/docs/en/docs/advanced/using-request-directly.md @@ -29,7 +29,7 @@ Let's imagine you want to get the client's IP address/host inside of your *path For that you need to access the request directly. -{* ../../docs_src/using_request_directly/tutorial001.py hl[1,7:8] *} +{* ../../docs_src/using_request_directly/tutorial001_py39.py hl[1,7:8] *} By declaring a *path operation function* parameter with the type being the `Request` **FastAPI** will know to pass the `Request` in that parameter. diff --git a/docs/en/docs/advanced/websockets.md b/docs/en/docs/advanced/websockets.md index ce11485a89..286df69432 100644 --- a/docs/en/docs/advanced/websockets.md +++ b/docs/en/docs/advanced/websockets.md @@ -38,13 +38,13 @@ In production you would have one of the options above. But it's the simplest way to focus on the server-side of WebSockets and have a working example: -{* ../../docs_src/websockets/tutorial001.py hl[2,6:38,41:43] *} +{* ../../docs_src/websockets/tutorial001_py39.py hl[2,6:38,41:43] *} ## Create a `websocket` { #create-a-websocket } In your **FastAPI** application, create a `websocket`: -{* ../../docs_src/websockets/tutorial001.py hl[1,46:47] *} +{* ../../docs_src/websockets/tutorial001_py39.py hl[1,46:47] *} /// note | Technical Details @@ -58,7 +58,7 @@ You could also use `from starlette.websockets import WebSocket`. In your WebSocket route you can `await` for messages and send messages. -{* ../../docs_src/websockets/tutorial001.py hl[48:52] *} +{* ../../docs_src/websockets/tutorial001_py39.py hl[48:52] *} You can receive and send binary, text, and JSON data. diff --git a/docs/en/docs/advanced/wsgi.md b/docs/en/docs/advanced/wsgi.md index 29fd2d359c..07147df0a8 100644 --- a/docs/en/docs/advanced/wsgi.md +++ b/docs/en/docs/advanced/wsgi.md @@ -12,7 +12,7 @@ Then wrap the WSGI (e.g. Flask) app with the middleware. And then mount that under a path. -{* ../../docs_src/wsgi/tutorial001.py hl[2:3,3] *} +{* ../../docs_src/wsgi/tutorial001_py39.py hl[2:3,3] *} ## Check it { #check-it } diff --git a/docs/en/docs/contributing.md b/docs/en/docs/contributing.md index ae99059f41..a4d896109b 100644 --- a/docs/en/docs/contributing.md +++ b/docs/en/docs/contributing.md @@ -6,44 +6,20 @@ First, you might want to see the basic ways to [help FastAPI and get help](help- If you already cloned the fastapi repository and you want to deep dive in the code, here are some guidelines to set up your environment. -### Virtual environment - -Follow the instructions to create and activate a [virtual environment](virtual-environments.md){.internal-link target=_blank} for the internal code of `fastapi`. - ### Install requirements -After activating the environment, install the required packages: - -//// tab | `pip` +Create a virtual environment and install the required packages with `uv`:
```console -$ pip install -r requirements.txt +$ uv sync ---> 100% ```
-//// - -//// tab | `uv` - -If you have `uv`: - -
- -```console -$ uv pip install -r requirements.txt - ----> 100% -``` - -
- -//// - It will install all the dependencies and your local FastAPI in your local environment. ### Using your local FastAPI @@ -56,9 +32,9 @@ That way, you don't have to "install" your local version to be able to test ever /// note | Technical Details -This only happens when you install using this included `requirements.txt` instead of running `pip install fastapi` directly. +This only happens when you install using `uv sync` instead of running `pip install fastapi` directly. -That is because inside the `requirements.txt` file, the local version of FastAPI is marked to be installed in "editable" mode, with the `-e` option. +That is because `uv sync` will install the local version of FastAPI in "editable" mode by default. /// @@ -201,252 +177,81 @@ as Uvicorn by default will use the port `8000`, the documentation on port `8008` ### Translations -/// warning | Attention - -**Update on Translations** - -We're updating the way we handle documentation translations. - -Until now, we invited community members to translate pages via pull requests, which were then reviewed by at least two native speakers. While this has helped bring FastAPI to many more users, we’ve also run into several challenges - some languages have only a few translated pages, others are outdated and hard to maintain over time. -To improve this, we’re working on automation tools 🤖 to manage translations more efficiently. Once ready, documentation will be machine-translated and still reviewed by at least two native speakers ✅ before publishing. This will allow us to keep translations up-to-date while reducing the review burden on maintainers. - -What’s changing now: - -* 🚫 We’re no longer accepting new community-submitted translation PRs. - -* ⏳ Existing open PRs will be reviewed and can still be merged if completed within the next 3 weeks (since July 11 2025). - -* 🌐 In the future, we will only support languages where at least three active native speakers are available to review and maintain translations. - -This transition will help us keep translations more consistent and timely while better supporting our contributors 🙌. Thank you to everyone who has contributed so far — your help has been invaluable! 💖 - -/// - - Help with translations is VERY MUCH appreciated! And it can't be done without the help from the community. 🌎 🚀 Here are the steps to help with translations. -#### Tips and guidelines +#### Review Translation PRs + +Translation pull requests are made by LLMs guided with prompts designed by the FastAPI team together with the community of native speakers for each supported language. + +These translations are normally still reviewed by native speakers, and here's where you can help! * Check the currently existing pull requests for your language. You can filter the pull requests by the ones with the label for your language. For example, for Spanish, the label is `lang-es`. -* Review those pull requests, requesting changes or approving them. For the languages I don't speak, I'll wait for several others to review the translation before merging. +* When reviewing a pull request, it's better not to suggest changes in the same pull request, because it is LLM generated, and it won't be possible to make sure that small individual changes are replicated in other similar sections, or that they are preserved when translating the same content again. + +* Instead of adding suggestions to the translation PR, make the suggestions to the LLM prompt file for that language, in a new PR. For example, for Spanish, the LLM prompt file is at: `docs/es/llm-prompt.md`. /// tip -You can add comments with change suggestions to existing pull requests. - Check the docs about adding a pull request review to approve it or request changes. /// +#### Subscribe to Notifications for Your Language + * Check if there's a GitHub Discussion to coordinate translations for your language. You can subscribe to it, and when there's a new pull request to review, an automatic comment will be added to the discussion. -* If you translate pages, add a single pull request per page translated. That will make it much easier for others to review it. - * To check the 2-letter code for the language you want to translate, you can use the table List of ISO 639-1 codes. -#### Existing language - -Let's say you want to translate a page for a language that already has translations for some pages, like Spanish. - -In the case of Spanish, the 2-letter code is `es`. So, the directory for Spanish translations is located at `docs/es/`. - -/// tip - -The main ("official") language is English, located at `docs/en/`. - -/// - -Now run the live server for the docs in Spanish: - -
- -```console -// Use the command "live" and pass the language code as a CLI argument -$ python ./scripts/docs.py live es - -[INFO] Serving on http://127.0.0.1:8008 -[INFO] Start watching changes -[INFO] Start detecting changes -``` - -
- -/// tip - -Alternatively, you can perform the same steps that scripts does manually. - -Go into the language directory, for the Spanish translations it's at `docs/es/`: - -```console -$ cd docs/es/ -``` - -Then run `mkdocs` in that directory: - -```console -$ mkdocs serve --dev-addr 127.0.0.1:8008 -``` - -/// - -Now you can go to http://127.0.0.1:8008 and see your changes live. - -You will see that every language has all the pages. But some pages are not translated and have an info box at the top, about the missing translation. - -Now let's say that you want to add a translation for the section [Features](features.md){.internal-link target=_blank}. - -* Copy the file at: - -``` -docs/en/docs/features.md -``` - -* Paste it in exactly the same location but for the language you want to translate, e.g.: - -``` -docs/es/docs/features.md -``` - -/// tip - -Notice that the only change in the path and file name is the language code, from `en` to `es`. - -/// - -If you go to your browser you will see that now the docs show your new section (the info box at the top is gone). 🎉 - -Now you can translate it all and see how it looks as you save the file. - -#### Don't Translate these Pages - -🚨 Don't translate: - -* Files under `reference/` -* `release-notes.md` -* `fastapi-people.md` -* `external-links.md` -* `newsletter.md` -* `management-tasks.md` -* `management.md` -* `contributing.md` - -Some of these files are updated very frequently and a translation would always be behind, or they include the main content from English source files, etc. - #### Request a New Language Let's say that you want to request translations for a language that is not yet translated, not even some pages. For example, Latin. -If there is no discussion for that language, you can start by requesting the new language. For that, you can follow these steps: - +* The first step would be for you to find other 2 people that would be willing to be reviewing translation PRs for that language with you. +* Once there are at least 3 people that would be willing to commit to help maintain that language, you can continue the next steps. * Create a new discussion following the template. -* Get a few native speakers to comment on the discussion and commit to help review translations for that language. +* Tag the other 2 people that will help with the language, and ask them to confirm there they will help. Once there are several people in the discussion, the FastAPI team can evaluate it and can make it an official translation. -Then the docs will be automatically translated using AI, and the team of native speakers can review the translation, and help tweak the AI prompts. +Then the docs will be automatically translated using LLMs, and the team of native speakers can review the translation, and help tweak the LLM prompts. Once there's a new translation, for example if docs are updated or there's a new section, there will be a comment in the same discussion with the link to the new translation to review. -#### New Language +## Automated Code and AI -/// note +You are encouraged to use all the tools you want to do your work and contribute as efficiently as possible, this includes AI (LLM) tools, etc. Nevertheless, contributions should have meaningful human intervention, judgement, context, etc. -These steps will be performed by the FastAPI team. +If the **human effort** put in a PR, e.g. writing LLM prompts, is **less** than the **effort we would need to put** to **review it**, please **don't** submit the PR. -/// +Think of it this way: we can already write LLM prompts or run automated tools ourselves, and that would be faster than reviewing external PRs. -Checking the link from above (List of ISO 639-1 codes), you can see that the 2-letter code for Latin is `la`. +### Closing Automated and AI PRs -Now you can create a new directory for the new language, running the following script: +If we see PRs that seem AI generated or automated in similar ways, we'll flag them and close them. -
+The same applies to comments and descriptions, please don't copy paste the content generated by an LLM. -```console -// Use the command new-lang, pass the language code as a CLI argument -$ python ./scripts/docs.py new-lang la +### Human Effort Denial of Service -Successfully initialized: docs/la -``` +Using automated tools and AI to submit PRs or comments that we have to carefully review and handle would be the equivalent of a Denial-of-service attack on our human effort. -
+It would be very little effort from the person submitting the PR (an LLM prompt) that generates a large amount of effort on our side (carefully reviewing code). -Now you can check in your code editor the newly created directory `docs/la/`. +Please don't do that. -That command created a file `docs/la/mkdocs.yml` with a simple config that inherits everything from the `en` version: +We'll need to block accounts that spam us with repeated automated PRs or comments. -```yaml -INHERIT: ../en/mkdocs.yml -``` +### Use Tools Wisely -/// tip +As Uncle Ben said: -You could also simply create that file with those contents manually. +
+With great power tools comes great responsibility. +
-/// +Avoid inadvertently doing harm. -That command also created a dummy file `docs/la/index.md` for the main page, you can start by translating that one. - -You can continue with the previous instructions for an "Existing Language" for that process. - -You can make the first pull request with those two files, `docs/la/mkdocs.yml` and `docs/la/index.md`. 🎉 - -#### Preview the result - -As already mentioned above, you can use the `./scripts/docs.py` with the `live` command to preview the results (or `mkdocs serve`). - -Once you are done, you can also test it all as it would look online, including all the other languages. - -To do that, first build all the docs: - -
- -```console -// Use the command "build-all", this will take a bit -$ python ./scripts/docs.py build-all - -Building docs for: en -Building docs for: es -Successfully built docs for: es -``` - -
- -This builds all those independent MkDocs sites for each language, combines them, and generates the final output at `./site/`. - -Then you can serve that with the command `serve`: - -
- -```console -// Use the command "serve" after running "build-all" -$ python ./scripts/docs.py serve - -Warning: this is a very simple server. For development, use mkdocs serve instead. -This is here only to preview a site with translations already built. -Make sure you run the build-all command first. -Serving at: http://127.0.0.1:8008 -``` - -
- -#### Translation specific tips and guidelines - -* Translate only the Markdown documents (`.md`). Do not translate the code examples at `./docs_src`. - -* In code blocks within the Markdown document, translate comments (`# a comment`), but leave the rest unchanged. - -* Do not change anything enclosed in "``" (inline code). - -* In lines starting with `///` translate only the text part after `|`. Leave the rest unchanged. - -* You can translate info boxes like `/// warning` with for example `/// warning | Achtung`. But do not change the word immediately after the `///`, it determines the color of the info box. - -* Do not change the paths in links to images, code files, Markdown documents. - -* However, when a Markdown document is translated, the `#hash-parts` in links to its headings may change. Update these links if possible. - * Search for such links in the translated document using the regex `#[^# ]`. - * Search in all documents already translated into your language for `your-translated-document.md`. For example VS Code has an option "Edit" -> "Find in Files". - * When translating a document, do not "pre-translate" `#hash-parts` that link to headings in untranslated documents. +You have amazing tools at hand, use them wisely to help effectively. diff --git a/docs/en/docs/css/custom.css b/docs/en/docs/css/custom.css index a38df772f9..87111ff64e 100644 --- a/docs/en/docs/css/custom.css +++ b/docs/en/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; diff --git a/docs/en/docs/css/termynal.css b/docs/en/docs/css/termynal.css index 8534f91021..a2564e2860 100644 --- a/docs/en/docs/css/termynal.css +++ b/docs/en/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/en/docs/deployment/cloud.md b/docs/en/docs/deployment/cloud.md index c88c4b51a8..4f5c23e4bc 100644 --- a/docs/en/docs/deployment/cloud.md +++ b/docs/en/docs/deployment/cloud.md @@ -4,13 +4,21 @@ You can use virtually **any cloud provider** to deploy your FastAPI application. In most of the cases, the main cloud providers have guides to deploy FastAPI with them. +## FastAPI Cloud { #fastapi-cloud } + +**FastAPI Cloud** is built by the same author and team behind **FastAPI**. + +It streamlines the process of **building**, **deploying**, and **accessing** an API with minimal effort. + +It brings the same **developer experience** of building apps with FastAPI to **deploying** them to the cloud. 🎉 + +FastAPI Cloud is the primary sponsor and funding provider for the *FastAPI and friends* open source projects. ✨ + ## Cloud Providers - Sponsors { #cloud-providers-sponsors } -Some cloud providers ✨ [**sponsor FastAPI**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨, this ensures the continued and healthy **development** of FastAPI and its **ecosystem**. +Some other cloud providers ✨ [**sponsor FastAPI**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨ too. 🙇 -And it shows their true commitment to FastAPI and its **community** (you), as they not only want to provide you a **good service** but also want to make sure you have a **good and healthy framework**, FastAPI. 🙇 - -You might want to try their services and follow their guides: +You might also want to consider them to follow their guides and try their services: * Render * Railway diff --git a/docs/en/docs/deployment/fastapicloud.md b/docs/en/docs/deployment/fastapicloud.md new file mode 100644 index 0000000000..b0889974fc --- /dev/null +++ b/docs/en/docs/deployment/fastapicloud.md @@ -0,0 +1,65 @@ +# FastAPI Cloud { #fastapi-cloud } + +You can deploy your FastAPI app to FastAPI Cloud with **one command**, go and join the waiting list if you haven't. 🚀 + +## Login { #login } + +Make sure you already have a **FastAPI Cloud** account (we invited you from the waiting list 😉). + +Then log in: + +
+ +```console +$ fastapi login + +You are logged in to FastAPI Cloud 🚀 +``` + +
+ +## Deploy { #deploy } + +Now deploy your app, with **one command**: + +
+ +```console +$ fastapi deploy + +Deploying to FastAPI Cloud... + +✅ Deployment successful! + +🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev +``` + +
+ +That's it! Now you can access your app at that URL. ✨ + +## About FastAPI Cloud { #about-fastapi-cloud } + +**FastAPI Cloud** is built by the same author and team behind **FastAPI**. + +It streamlines the process of **building**, **deploying**, and **accessing** an API with minimal effort. + +It brings the same **developer experience** of building apps with FastAPI to **deploying** them to the cloud. 🎉 + +It will also take care of most of the things you would need when deploying an app, like: + +* HTTPS +* Replication, with autoscaling based on requests +* etc. + +FastAPI Cloud is the primary sponsor and funding provider for the *FastAPI and friends* open source projects. ✨ + +## Deploy to other cloud providers { #deploy-to-other-cloud-providers } + +FastAPI is open source and based on standards. You can deploy FastAPI apps to any cloud provider you choose. + +Follow your cloud provider's guides to deploy FastAPI apps with them. 🤓 + +## Deploy your own server { #deploy-your-own-server } + +I will also teach you later in this **Deployment** guide all the details, so you can understand what is going on, what needs to happen, or how to deploy FastAPI apps on your own, also with your own servers. 🤓 diff --git a/docs/en/docs/deployment/index.md b/docs/en/docs/deployment/index.md index 2364791a7e..8d7521e735 100644 --- a/docs/en/docs/deployment/index.md +++ b/docs/en/docs/deployment/index.md @@ -16,6 +16,8 @@ There are several ways to do it depending on your specific use case and the tool You could **deploy a server** yourself using a combination of tools, you could use a **cloud service** that does part of the work for you, or other possible options. +For example, we, the team behind FastAPI, built **FastAPI Cloud**, to make deploying FastAPI apps to the cloud as streamlined as possible, with the same developer experience of working with FastAPI. + I will show you some of the main concepts you should probably keep in mind when deploying a **FastAPI** application (although most of it applies to any other type of web application). You will see more details to keep in mind and some of the techniques to do it in the next sections. ✨ diff --git a/docs/en/docs/external-links.md b/docs/en/docs/external-links.md index 3ed04e5c55..481cf1d7f3 100644 --- a/docs/en/docs/external-links.md +++ b/docs/en/docs/external-links.md @@ -1,36 +1,22 @@ -# External Links and Articles +# External Links **FastAPI** has a great community constantly growing. There are many posts, articles, tools, and projects, related to **FastAPI**. -Here's an incomplete list of some of them. +You could easily use a search engine or video platform to find many resources related to FastAPI. -/// tip +/// info -If you have an article, project, tool, or anything related to **FastAPI** that is not yet listed here, create a Pull Request adding it. +Before, this page used to list links to external articles. + +But now that FastAPI is the backend framework with the most GitHub stars across languages, and the most starred and used framework in Python, it no longer makes sense to attempt to list all articles written about it. /// -{% for section_name, section_content in external_links.items() %} - -## {{ section_name }} - -{% for lang_name, lang_content in section_content.items() %} - -### {{ lang_name }} - -{% for item in lang_content %} - -* {{ item.title }} by {{ item.author }}. - -{% endfor %} -{% endfor %} -{% endfor %} - ## GitHub Repositories -Most starred GitHub repositories with the topic `fastapi`: +Most starred GitHub repositories with the topic `fastapi`: {% for repo in topic_repos %} diff --git a/docs/en/docs/fastapi-people.md b/docs/en/docs/fastapi-people.md index f2ca26013c..2c07af7647 100644 --- a/docs/en/docs/fastapi-people.md +++ b/docs/en/docs/fastapi-people.md @@ -196,31 +196,11 @@ They have contributed source code, documentation, etc. 📦 There are hundreds of other contributors, you can see them all in the FastAPI GitHub Contributors page. 👷 -## Top Translators - -These are the **Top Translators**. 🌐 - -These users have created the most Pull Requests with [translations to other languages](contributing.md#translations){.internal-link target=_blank} that have been *merged*. - -
- -{% for user in (translators.values() | list)[:50] %} - -{% if user.login not in skip_users %} - -
@{{ user.login }}
Translations: {{ user.count }}
- -{% endif %} - -{% endfor %} - -
- ## Top Translation Reviewers These users are the **Top Translation Reviewers**. 🕵️ -I only speak a few languages (and not very well 😅). So, the reviewers are the ones that have the [**power to approve translations**](contributing.md#translations){.internal-link target=_blank} of the documentation. Without them, there wouldn't be documentation in several other languages. +Translation reviewers have the [**power to approve translations**](contributing.md#translations){.internal-link target=_blank} of the documentation. Without them, there wouldn't be documentation in several other languages.
{% for user in (translation_reviewers.values() | list)[:50] %} diff --git a/docs/en/docs/how-to/authentication-error-status-code.md b/docs/en/docs/how-to/authentication-error-status-code.md new file mode 100644 index 0000000000..f9433e5dd3 --- /dev/null +++ b/docs/en/docs/how-to/authentication-error-status-code.md @@ -0,0 +1,17 @@ +# Use Old 403 Authentication Error Status Codes { #use-old-403-authentication-error-status-codes } + +Before FastAPI version `0.122.0`, when the integrated security utilities returned an error to the client after a failed authentication, they used the HTTP status code `403 Forbidden`. + +Starting with FastAPI version `0.122.0`, they use the more appropriate HTTP status code `401 Unauthorized`, and return a sensible `WWW-Authenticate` header in the response, following the HTTP specifications, RFC 7235, RFC 9110. + +But if for some reason your clients depend on the old behavior, you can revert to it by overriding the method `make_not_authenticated_error` in your security classes. + +For example, you can create a subclass of `HTTPBearer` that returns a `403 Forbidden` error instead of the default `401 Unauthorized` error: + +{* ../../docs_src/authentication_error_status_code/tutorial001_an_py39.py hl[9:13] *} + +/// tip + +Notice that the function returns the exception instance, it doesn't raise it. The raising is done in the rest of the internal code. + +/// diff --git a/docs/en/docs/how-to/conditional-openapi.md b/docs/en/docs/how-to/conditional-openapi.md index e5893e584b..ecb45b4a5b 100644 --- a/docs/en/docs/how-to/conditional-openapi.md +++ b/docs/en/docs/how-to/conditional-openapi.md @@ -29,7 +29,7 @@ You can easily use the same Pydantic settings to configure your generated OpenAP For example: -{* ../../docs_src/conditional_openapi/tutorial001.py hl[6,11] *} +{* ../../docs_src/conditional_openapi/tutorial001_py39.py hl[6,11] *} Here we declare the setting `openapi_url` with the same default of `"/openapi.json"`. diff --git a/docs/en/docs/how-to/configure-swagger-ui.md b/docs/en/docs/how-to/configure-swagger-ui.md index 2d7b99f8fa..ff46dc5b1b 100644 --- a/docs/en/docs/how-to/configure-swagger-ui.md +++ b/docs/en/docs/how-to/configure-swagger-ui.md @@ -18,7 +18,7 @@ Without changing the settings, syntax highlighting is enabled by default: But you can disable it by setting `syntaxHighlight` to `False`: -{* ../../docs_src/configure_swagger_ui/tutorial001.py hl[3] *} +{* ../../docs_src/configure_swagger_ui/tutorial001_py39.py hl[3] *} ...and then Swagger UI won't show the syntax highlighting anymore: @@ -28,7 +28,7 @@ But you can disable it by setting `syntaxHighlight` to `False`: The same way you could set the syntax highlighting theme with the key `"syntaxHighlight.theme"` (notice that it has a dot in the middle): -{* ../../docs_src/configure_swagger_ui/tutorial002.py hl[3] *} +{* ../../docs_src/configure_swagger_ui/tutorial002_py39.py hl[3] *} That configuration would change the syntax highlighting color theme: @@ -40,13 +40,13 @@ FastAPI includes some default configuration parameters appropriate for most of t It includes these default configurations: -{* ../../fastapi/openapi/docs.py ln[8:23] hl[17:23] *} +{* ../../fastapi/openapi/docs.py ln[9:24] hl[18:24] *} You can override any of them by setting a different value in the argument `swagger_ui_parameters`. For example, to disable `deepLinking` you could pass these settings to `swagger_ui_parameters`: -{* ../../docs_src/configure_swagger_ui/tutorial003.py hl[3] *} +{* ../../docs_src/configure_swagger_ui/tutorial003_py39.py hl[3] *} ## Other Swagger UI Parameters { #other-swagger-ui-parameters } diff --git a/docs/en/docs/how-to/custom-docs-ui-assets.md b/docs/en/docs/how-to/custom-docs-ui-assets.md index 91228c8c93..61a97dca2b 100644 --- a/docs/en/docs/how-to/custom-docs-ui-assets.md +++ b/docs/en/docs/how-to/custom-docs-ui-assets.md @@ -18,7 +18,7 @@ The first step is to disable the automatic docs, as by default, those use the de To disable them, set their URLs to `None` when creating your `FastAPI` app: -{* ../../docs_src/custom_docs_ui/tutorial001.py hl[8] *} +{* ../../docs_src/custom_docs_ui/tutorial001_py39.py hl[8] *} ### Include the custom docs { #include-the-custom-docs } @@ -34,7 +34,7 @@ You can reuse FastAPI's internal functions to create the HTML pages for the docs And similarly for ReDoc... -{* ../../docs_src/custom_docs_ui/tutorial001.py hl[2:6,11:19,22:24,27:33] *} +{* ../../docs_src/custom_docs_ui/tutorial001_py39.py hl[2:6,11:19,22:24,27:33] *} /// tip @@ -50,7 +50,7 @@ Swagger UI will handle it behind the scenes for you, but it needs this "redirect Now, to be able to test that everything works, create a *path operation*: -{* ../../docs_src/custom_docs_ui/tutorial001.py hl[36:38] *} +{* ../../docs_src/custom_docs_ui/tutorial001_py39.py hl[36:38] *} ### Test it { #test-it } @@ -118,7 +118,7 @@ After that, your file structure could look like: * Import `StaticFiles`. * "Mount" a `StaticFiles()` instance in a specific path. -{* ../../docs_src/custom_docs_ui/tutorial002.py hl[7,11] *} +{* ../../docs_src/custom_docs_ui/tutorial002_py39.py hl[7,11] *} ### Test the static files { #test-the-static-files } @@ -144,7 +144,7 @@ The same as when using a custom CDN, the first step is to disable the automatic To disable them, set their URLs to `None` when creating your `FastAPI` app: -{* ../../docs_src/custom_docs_ui/tutorial002.py hl[9] *} +{* ../../docs_src/custom_docs_ui/tutorial002_py39.py hl[9] *} ### Include the custom docs for static files { #include-the-custom-docs-for-static-files } @@ -160,7 +160,7 @@ Again, you can reuse FastAPI's internal functions to create the HTML pages for t And similarly for ReDoc... -{* ../../docs_src/custom_docs_ui/tutorial002.py hl[2:6,14:22,25:27,30:36] *} +{* ../../docs_src/custom_docs_ui/tutorial002_py39.py hl[2:6,14:22,25:27,30:36] *} /// tip @@ -176,7 +176,7 @@ Swagger UI will handle it behind the scenes for you, but it needs this "redirect Now, to be able to test that everything works, create a *path operation*: -{* ../../docs_src/custom_docs_ui/tutorial002.py hl[39:41] *} +{* ../../docs_src/custom_docs_ui/tutorial002_py39.py hl[39:41] *} ### Test Static Files UI { #test-static-files-ui } diff --git a/docs/en/docs/how-to/custom-request-and-route.md b/docs/en/docs/how-to/custom-request-and-route.md index 884c8ed04f..bfc60729f8 100644 --- a/docs/en/docs/how-to/custom-request-and-route.md +++ b/docs/en/docs/how-to/custom-request-and-route.md @@ -42,7 +42,7 @@ If there's no `gzip` in the header, it will not try to decompress the body. That way, the same route class can handle gzip compressed or uncompressed requests. -{* ../../docs_src/custom_request_and_route/tutorial001.py hl[8:15] *} +{* ../../docs_src/custom_request_and_route/tutorial001_an_py310.py hl[9:16] *} ### Create a custom `GzipRoute` class { #create-a-custom-gziproute-class } @@ -54,7 +54,7 @@ This method returns a function. And that function is what will receive a request Here we use it to create a `GzipRequest` from the original request. -{* ../../docs_src/custom_request_and_route/tutorial001.py hl[18:26] *} +{* ../../docs_src/custom_request_and_route/tutorial001_an_py310.py hl[19:27] *} /// note | Technical Details @@ -92,18 +92,18 @@ We can also use this same approach to access the request body in an exception ha All we need to do is handle the request inside a `try`/`except` block: -{* ../../docs_src/custom_request_and_route/tutorial002.py hl[13,15] *} +{* ../../docs_src/custom_request_and_route/tutorial002_an_py310.py hl[14,16] *} If an exception occurs, the`Request` instance will still be in scope, so we can read and make use of the request body when handling the error: -{* ../../docs_src/custom_request_and_route/tutorial002.py hl[16:18] *} +{* ../../docs_src/custom_request_and_route/tutorial002_an_py310.py hl[17:19] *} ## Custom `APIRoute` class in a router { #custom-apiroute-class-in-a-router } You can also set the `route_class` parameter of an `APIRouter`: -{* ../../docs_src/custom_request_and_route/tutorial003.py hl[26] *} +{* ../../docs_src/custom_request_and_route/tutorial003_py310.py hl[26] *} In this example, the *path operations* under the `router` will use the custom `TimedRoute` class, and will have an extra `X-Response-Time` header in the response with the time it took to generate the response: -{* ../../docs_src/custom_request_and_route/tutorial003.py hl[13:20] *} +{* ../../docs_src/custom_request_and_route/tutorial003_py310.py hl[13:20] *} diff --git a/docs/en/docs/how-to/extending-openapi.md b/docs/en/docs/how-to/extending-openapi.md index 5e672665ef..2dc262cbad 100644 --- a/docs/en/docs/how-to/extending-openapi.md +++ b/docs/en/docs/how-to/extending-openapi.md @@ -43,19 +43,19 @@ For example, let's add Strawberry documentation. diff --git a/docs/en/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md b/docs/en/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md index e85d122be8..fc90220b89 100644 --- a/docs/en/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md +++ b/docs/en/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md @@ -2,21 +2,23 @@ If you have an old FastAPI app, you might be using Pydantic version 1. -FastAPI has had support for either Pydantic v1 or v2 since version 0.100.0. +FastAPI version 0.100.0 had support for either Pydantic v1 or v2. It would use whichever you had installed. -If you had installed Pydantic v2, it would use it. If instead you had Pydantic v1, it would use that. +FastAPI version 0.119.0 introduced partial support for Pydantic v1 from inside of Pydantic v2 (as `pydantic.v1`), to facilitate the migration to v2. -Pydantic v1 is now deprecated and support for it will be removed in the next versions of FastAPI, you should **migrate to Pydantic v2**. This way you will get the latest features, improvements, and fixes. +FastAPI 0.126.0 dropped support for Pydantic v1, while still supporting `pydantic.v1` for a little while. /// warning -Also, the Pydantic team stopped support for Pydantic v1 for the latest versions of Python, starting with **Python 3.14**. +The Pydantic team stopped support for Pydantic v1 for the latest versions of Python, starting with **Python 3.14**. + +This includes `pydantic.v1`, which is no longer supported in Python 3.14 and above. If you want to use the latest features of Python, you will need to make sure you use Pydantic v2. /// -If you have an old FastAPI app with Pydantic v1, here I'll show you how to migrate it to Pydantic v2, and the **new features in FastAPI 0.119.0** to help you with a gradual migration. +If you have an old FastAPI app with Pydantic v1, here I'll show you how to migrate it to Pydantic v2, and the **features in FastAPI 0.119.0** to help you with a gradual migration. ## Official Guide { #official-guide } @@ -44,7 +46,7 @@ After this, you can run the tests and check if everything works. If it does, you ## Pydantic v1 in v2 { #pydantic-v1-in-v2 } -Pydantic v2 includes everything from Pydantic v1 as a submodule `pydantic.v1`. +Pydantic v2 includes everything from Pydantic v1 as a submodule `pydantic.v1`. But this is no longer supported in versions above Python 3.13. This means that you can install the latest version of Pydantic v2 and import and use the old Pydantic v1 components from this submodule, as if you had the old Pydantic v1 installed. diff --git a/docs/en/docs/how-to/separate-openapi-schemas.md b/docs/en/docs/how-to/separate-openapi-schemas.md index 3c78a56d36..d790c600bb 100644 --- a/docs/en/docs/how-to/separate-openapi-schemas.md +++ b/docs/en/docs/how-to/separate-openapi-schemas.md @@ -1,6 +1,6 @@ # Separate OpenAPI Schemas for Input and Output or Not { #separate-openapi-schemas-for-input-and-output-or-not } -When using **Pydantic v2**, the generated OpenAPI is a bit more exact and **correct** than before. 😎 +Since **Pydantic v2** was released, the generated OpenAPI is a bit more exact and **correct** than before. 😎 In fact, in some cases, it will even have **two JSON Schemas** in OpenAPI for the same Pydantic model, for input and output, depending on if they have **default values**. @@ -100,5 +100,3 @@ And now there will be one single schema for input and output for the model, only
- -This is the same behavior as in Pydantic v1. 🤓 diff --git a/docs/en/docs/img/fastapi-documentary.jpg b/docs/en/docs/img/fastapi-documentary.jpg new file mode 100644 index 0000000000..3ddbfdb389 Binary files /dev/null and b/docs/en/docs/img/fastapi-documentary.jpg differ diff --git a/docs/en/docs/img/sponsors/fastapicloud.png b/docs/en/docs/img/sponsors/fastapicloud.png new file mode 100644 index 0000000000..c23dec2209 Binary files /dev/null and b/docs/en/docs/img/sponsors/fastapicloud.png differ diff --git a/docs/en/docs/img/sponsors/greptile-banner.png b/docs/en/docs/img/sponsors/greptile-banner.png new file mode 100644 index 0000000000..e0909b39d1 Binary files /dev/null and b/docs/en/docs/img/sponsors/greptile-banner.png differ diff --git a/docs/en/docs/img/sponsors/greptile.png b/docs/en/docs/img/sponsors/greptile.png new file mode 100644 index 0000000000..ae3d78cbdc Binary files /dev/null and b/docs/en/docs/img/sponsors/greptile.png differ diff --git a/docs/en/docs/index.md b/docs/en/docs/index.md index 35c46d15f7..5eb47c7b32 100644 --- a/docs/en/docs/index.md +++ b/docs/en/docs/index.md @@ -46,20 +46,26 @@ The key features are: * **Robust**: Get production-ready code. With automatic interactive documentation. * **Standards-based**: Based on (and fully compatible with) the open standards for APIs: OpenAPI (previously known as Swagger) and JSON Schema. -* estimation based on tests on an internal development team, building production applications. +* estimation based on tests conducted by an internal development team, building production applications. ## Sponsors { #sponsors } -{% if sponsors %} +### Keystone Sponsor { #keystone-sponsor } + +{% for sponsor in sponsors.keystone -%} + +{% endfor -%} + +### Gold and Silver Sponsors { #gold-and-silver-sponsors } + {% for sponsor in sponsors.gold -%} {% endfor -%} {%- for sponsor in sponsors.silver -%} {% endfor %} -{% endif %} @@ -111,6 +117,12 @@ The key features are: --- +## FastAPI mini documentary { #fastapi-mini-documentary } + +There's a FastAPI mini documentary released at the end of 2025, you can watch it online: + +FastAPI Mini Documentary + ## **Typer**, the FastAPI of CLIs { #typer-the-fastapi-of-clis } @@ -444,6 +456,58 @@ For a more complete example including more features, see the FastAPI Cloud, go and join the waiting list if you haven't. 🚀 + +If you already have a **FastAPI Cloud** account (we invited you from the waiting list 😉), you can deploy your application with one command. + +Before deploying, make sure you are logged in: + +
+ +```console +$ fastapi login + +You are logged in to FastAPI Cloud 🚀 +``` + +
+ +Then deploy your app: + +
+ +```console +$ fastapi deploy + +Deploying to FastAPI Cloud... + +✅ Deployment successful! + +🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev +``` + +
+ +That's it! Now you can access your app at that URL. ✨ + +#### About FastAPI Cloud { #about-fastapi-cloud } + +**FastAPI Cloud** is built by the same author and team behind **FastAPI**. + +It streamlines the process of **building**, **deploying**, and **accessing** an API with minimal effort. + +It brings the same **developer experience** of building apps with FastAPI to **deploying** them to the cloud. 🎉 + +FastAPI Cloud is the primary sponsor and funding provider for the *FastAPI and friends* open source projects. ✨ + +#### Deploy to other cloud providers { #deploy-to-other-cloud-providers } + +FastAPI is open source and based on standards. You can deploy FastAPI apps to any cloud provider you choose. + +Follow your cloud provider's guides to deploy FastAPI apps with them. 🤓 + ## Performance { #performance } Independent TechEmpower benchmarks show **FastAPI** applications running under Uvicorn as one of the fastest Python frameworks available, only below Starlette and Uvicorn themselves (used internally by FastAPI). (*) diff --git a/docs/en/docs/management-tasks.md b/docs/en/docs/management-tasks.md index 05cd5d27d0..fc6e1eb280 100644 --- a/docs/en/docs/management-tasks.md +++ b/docs/en/docs/management-tasks.md @@ -74,7 +74,7 @@ Make sure you use a supported label from the `translate.yml`. -* The title is correct following the instructions above. +For these language translation PRs, confirm that: + +* The PR was automated (authored by @tiangolo), not made by another user. * It has the labels `lang-all` and `lang-{lang code}`. -* The PR changes only one Markdown file adding a translation. - * Or in some cases, at most two files, if they are small, for the same language, and people reviewed them. - * If it's the first translation for that language, it will have additional `mkdocs.yml` files, for those cases follow the instructions below. -* The PR doesn't add any additional or extraneous files. -* The translation seems to have a similar structure as the original English file. -* The translation doesn't seem to change the original content, for example with obvious additional documentation sections. -* The translation doesn't use different Markdown structures, for example adding HTML tags when the original didn't have them. -* The "admonition" sections, like `tip`, `info`, etc. are not changed or translated. For example: - -``` -/// tip - -This is a tip. - -/// - -``` - -looks like this: - -/// tip - -This is a tip. - -/// - -...it could be translated as: - -``` -/// tip - -Esto es un consejo. - -/// - -``` - -...but needs to keep the exact `tip` keyword. If it was translated to `consejo`, like: - -``` -/// consejo - -Esto es un consejo. - -/// - -``` - -it would change the style to the default one, it would look like: - -/// consejo - -Esto es un consejo. - -/// - -Those don't have to be translated, but if they are, they need to be written as: - -``` -/// tip | consejo - -Esto es un consejo. - -/// - -``` - -Which looks like: - -/// tip | consejo - -Esto es un consejo. - -/// - -## First Translation PR - -When there's a first translation for a language, it will have a `docs/{lang code}/docs/index.md` translated file and a `docs/{lang code}/mkdocs.yml`. - -For example, for Bosnian, it would be: - -* `docs/bs/docs/index.md` -* `docs/bs/mkdocs.yml` - -The `mkdocs.yml` file will have only the following content: - -```YAML -INHERIT: ../en/mkdocs.yml -``` - -The language code would normally be in the ISO 639-1 list of language codes. - -In any case, the language code should be in the file docs/language_names.yml. - -There won't be yet a label for the language code, for example, if it was Bosnian, there wouldn't be a `lang-bs`. Before creating the label and adding it to the PR, create the GitHub Discussion: - -* Go to the Translations GitHub Discussions -* Create a new discussion with the title `Bosnian Translations` (or the language name in English) -* A description of: - -```Markdown -## Bosnian translations - -This is the issue to track translations of the docs to Bosnian. 🚀 - -Here are the [PRs to review with the label `lang-bs`](https://github.com/fastapi/fastapi/pulls?q=is%3Apr+is%3Aopen+sort%3Aupdated-desc+label%3Alang-bs+label%3A%22awaiting-review%22). 🤓 -``` - -Update "Bosnian" with the new language. - -And update the search link to point to the new language label that will be created, like `lang-bs`. - -Create and add the label to that new Discussion just created, like `lang-bs`. - -Then go back to the PR, and add the label, like `lang-bs`, and `lang-all` and `awaiting-review`. - -Now the GitHub action will automatically detect the label `lang-bs` and will post in that Discussion that this PR is waiting to be reviewed. +* If the PR is approved by at least one native speaker, you can merge it. ## Review PRs -If a PR doesn't explain what it does or why, ask for more information. +* If a PR doesn't explain what it does or why, if it seems like it could be useful, ask for more information. Otherwise, feel free to close it. -A PR should have a specific use case that it is solving. +* If a PR seems to be spam, meaningless, only to change statistics (to appear as "contributor") or similar, you can simply mark it as `invalid`, and it will be automatically closed. + +* If a PR seems to be AI generated, and seems like reviewing it would take more time from you than the time it took to write the prompt, mark it as `maybe-ai`, and it will be automatically closed. + +* A PR should have a specific use case that it is solving. * If the PR is for a feature, it should have docs. * Unless it's a feature we want to discourage, like support for a corner case that we don't want users to use. * The docs should include a source example file, not write Python directly in Markdown. -* If the source example(s) file can have different syntax for Python 3.8, 3.9, 3.10, there should be different versions of the file, and they should be shown in tabs in the docs. +* If the source example(s) file can have different syntax for different Python versions, there should be different versions of the file, and they should be shown in tabs in the docs. * There should be tests testing the source example. * Before the PR is applied, the new tests should fail. * After applying the PR, the new tests should pass. @@ -254,27 +144,12 @@ Every month, a GitHub Action updates the FastAPI People data. Those PRs look lik If the tests are passing, you can merge it right away. -## External Links PRs - -When people add external links they edit this file external_links.yml. - -* Make sure the new link is in the correct category (e.g. "Podcasts") and language (e.g. "Japanese"). -* A new link should be at the top of its list. -* The link URL should work (it should not return a 404). -* The content of the link should be about FastAPI. -* The new addition should have these fields: - * `author`: The name of the author. - * `link`: The URL with the content. - * `title`: The title of the link (the title of the article, podcast, etc). - -After checking all these things and ensuring the PR has the right labels, you can merge it. - ## Dependabot PRs Dependabot will create PRs to update dependencies for several things, and those PRs all look similar, but some are way more delicate than others. -* If the PR is for a direct dependency, so, Dependabot is modifying `pyproject.toml`, **don't merge it**. 😱 Let me check it first. There's a good chance that some additional tweaks or updates are needed. -* If the PR updates one of the internal dependencies, for example it's modifying `requirements.txt` files, or GitHub Action versions, if the tests are passing, the release notes (shown in a summary in the PR) don't show any obvious potential breaking change, you can merge it. 😎 +* If the PR is for a direct dependency, so, Dependabot is modifying `pyproject.toml` in the main dependencies, **don't merge it**. 😱 Let me check it first. There's a good chance that some additional tweaks or updates are needed. +* If the PR updates one of the internal dependencies, for example the group `dev` in `pyproject.toml`, or GitHub Action versions, if the tests are passing, the release notes (shown in a summary in the PR) don't show any obvious potential breaking change, you can merge it. 😎 ## Mark GitHub Discussions Answers diff --git a/docs/en/docs/project-generation.md b/docs/en/docs/project-generation.md index 44f058b902..610d23ccb1 100644 --- a/docs/en/docs/project-generation.md +++ b/docs/en/docs/project-generation.md @@ -9,18 +9,18 @@ GitHub Repository: Concatenates them with a space in the middle. -{* ../../docs_src/python_types/tutorial001.py hl[2] *} +{* ../../docs_src/python_types/tutorial001_py39.py hl[2] *} ### Edit it { #edit-it } @@ -78,7 +78,7 @@ That's it. Those are the "type hints": -{* ../../docs_src/python_types/tutorial002.py hl[1] *} +{* ../../docs_src/python_types/tutorial002_py39.py hl[1] *} That is not the same as declaring default values like would be with: @@ -106,7 +106,7 @@ With that, you can scroll, seeing the options, until you find the one that "ring Check this function, it already has type hints: -{* ../../docs_src/python_types/tutorial003.py hl[1] *} +{* ../../docs_src/python_types/tutorial003_py39.py hl[1] *} Because the editor knows the types of the variables, you don't only get completion, you also get error checks: @@ -114,7 +114,7 @@ Because the editor knows the types of the variables, you don't only get completi Now you know that you have to fix it, convert `age` to a string with `str(age)`: -{* ../../docs_src/python_types/tutorial004.py hl[2] *} +{* ../../docs_src/python_types/tutorial004_py39.py hl[2] *} ## Declaring types { #declaring-types } @@ -133,7 +133,7 @@ You can use, for example: * `bool` * `bytes` -{* ../../docs_src/python_types/tutorial005.py hl[1] *} +{* ../../docs_src/python_types/tutorial005_py39.py hl[1] *} ### Generic types with type parameters { #generic-types-with-type-parameters } @@ -161,56 +161,24 @@ If you can use the **latest versions of Python**, use the examples for the lates For example, let's define a variable to be a `list` of `str`. -//// tab | Python 3.9+ - Declare the variable, with the same colon (`:`) syntax. As the type, put `list`. As the list is a type that contains some internal types, you put them in square brackets: -```Python hl_lines="1" -{!> ../../docs_src/python_types/tutorial006_py39.py!} -``` - -//// - -//// tab | Python 3.8+ - -From `typing`, import `List` (with a capital `L`): - -```Python hl_lines="1" -{!> ../../docs_src/python_types/tutorial006.py!} -``` - -Declare the variable, with the same colon (`:`) syntax. - -As the type, put the `List` that you imported from `typing`. - -As the list is a type that contains some internal types, you put them in square brackets: - -```Python hl_lines="4" -{!> ../../docs_src/python_types/tutorial006.py!} -``` - -//// +{* ../../docs_src/python_types/tutorial006_py39.py hl[1] *} /// info Those internal types in the square brackets are called "type parameters". -In this case, `str` is the type parameter passed to `List` (or `list` in Python 3.9 and above). +In this case, `str` is the type parameter passed to `list`. /// That means: "the variable `items` is a `list`, and each of the items in this list is a `str`". -/// tip - -If you use Python 3.9 or above, you don't have to import `List` from `typing`, you can use the same regular `list` type instead. - -/// - By doing that, your editor can provide support even while processing items from the list: @@ -225,21 +193,7 @@ And still, the editor knows it is a `str`, and provides support for that. You would do the same to declare `tuple`s and `set`s: -//// tab | Python 3.9+ - -```Python hl_lines="1" -{!> ../../docs_src/python_types/tutorial007_py39.py!} -``` - -//// - -//// tab | Python 3.8+ - -```Python hl_lines="1 4" -{!> ../../docs_src/python_types/tutorial007.py!} -``` - -//// +{* ../../docs_src/python_types/tutorial007_py39.py hl[1] *} This means: @@ -254,21 +208,7 @@ The first type parameter is for the keys of the `dict`. The second type parameter is for the values of the `dict`: -//// tab | Python 3.9+ - -```Python hl_lines="1" -{!> ../../docs_src/python_types/tutorial008_py39.py!} -``` - -//// - -//// tab | Python 3.8+ - -```Python hl_lines="1 4" -{!> ../../docs_src/python_types/tutorial008.py!} -``` - -//// +{* ../../docs_src/python_types/tutorial008_py39.py hl[1] *} This means: @@ -292,10 +232,10 @@ In Python 3.10 there's also a **new syntax** where you can put the possible type //// -//// tab | Python 3.8+ +//// tab | Python 3.9+ ```Python hl_lines="1 4" -{!> ../../docs_src/python_types/tutorial008b.py!} +{!> ../../docs_src/python_types/tutorial008b_py39.py!} ``` //// @@ -309,7 +249,7 @@ You can declare that a value could have a type, like `str`, but that it could al In Python 3.6 and above (including Python 3.10) you can declare it by importing and using `Optional` from the `typing` module. ```Python hl_lines="1 4" -{!../../docs_src/python_types/tutorial009.py!} +{!../../docs_src/python_types/tutorial009_py39.py!} ``` Using `Optional[str]` instead of just `str` will let the editor help you detect errors where you could be assuming that a value is always a `str`, when it could actually be `None` too. @@ -326,18 +266,18 @@ This also means that in Python 3.10, you can use `Something | None`: //// -//// tab | Python 3.8+ +//// tab | Python 3.9+ ```Python hl_lines="1 4" -{!> ../../docs_src/python_types/tutorial009.py!} +{!> ../../docs_src/python_types/tutorial009_py39.py!} ``` //// -//// tab | Python 3.8+ alternative +//// tab | Python 3.9+ alternative ```Python hl_lines="1 4" -{!> ../../docs_src/python_types/tutorial009b.py!} +{!> ../../docs_src/python_types/tutorial009b_py39.py!} ``` //// @@ -357,7 +297,7 @@ It's just about the words and names. But those words can affect how you and your As an example, let's take this function: -{* ../../docs_src/python_types/tutorial009c.py hl[1,4] *} +{* ../../docs_src/python_types/tutorial009c_py39.py hl[1,4] *} The parameter `name` is defined as `Optional[str]`, but it is **not optional**, you cannot call the function without the parameter: @@ -390,10 +330,10 @@ You can use the same builtin types as generics (with square brackets and types i * `set` * `dict` -And the same as with Python 3.8, from the `typing` module: +And the same as with previous Python versions, from the `typing` module: * `Union` -* `Optional` (the same as with Python 3.8) +* `Optional` * ...and others. In Python 3.10, as an alternative to using the generics `Union` and `Optional`, you can use the vertical bar (`|`) to declare unions of types, that's a lot better and simpler. @@ -409,7 +349,7 @@ You can use the same builtin types as generics (with square brackets and types i * `set` * `dict` -And the same as with Python 3.8, from the `typing` module: +And generics from the `typing` module: * `Union` * `Optional` @@ -417,29 +357,17 @@ And the same as with Python 3.8, from the `typing` module: //// -//// tab | Python 3.8+ - -* `List` -* `Tuple` -* `Set` -* `Dict` -* `Union` -* `Optional` -* ...and others. - -//// - ### Classes as types { #classes-as-types } You can also declare a class as the type of a variable. Let's say you have a class `Person`, with a name: -{* ../../docs_src/python_types/tutorial010.py hl[1:3] *} +{* ../../docs_src/python_types/tutorial010_py39.py hl[1:3] *} Then you can declare a variable to be of type `Person`: -{* ../../docs_src/python_types/tutorial010.py hl[6] *} +{* ../../docs_src/python_types/tutorial010_py39.py hl[6] *} And then, again, you get all the editor support: @@ -463,29 +391,7 @@ And you get all the editor support with that resulting object. An example from the official Pydantic docs: -//// tab | Python 3.10+ - -```Python -{!> ../../docs_src/python_types/tutorial011_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!> ../../docs_src/python_types/tutorial011_py39.py!} -``` - -//// - -//// tab | Python 3.8+ - -```Python -{!> ../../docs_src/python_types/tutorial011.py!} -``` - -//// +{* ../../docs_src/python_types/tutorial011_py310.py *} /// info @@ -507,27 +413,9 @@ Pydantic has a special behavior when you use `Optional` or `Union[Something, Non Python also has a feature that allows putting **additional metadata** in these type hints using `Annotated`. -//// tab | Python 3.9+ +Since Python 3.9, `Annotated` is a part of the standard library, so you can import it from `typing`. -In Python 3.9, `Annotated` is part of the standard library, so you can import it from `typing`. - -```Python hl_lines="1 4" -{!> ../../docs_src/python_types/tutorial013_py39.py!} -``` - -//// - -//// tab | Python 3.8+ - -In versions below Python 3.9, you import `Annotated` from `typing_extensions`. - -It will already be installed with **FastAPI**. - -```Python hl_lines="1 4" -{!> ../../docs_src/python_types/tutorial013.py!} -``` - -//// +{* ../../docs_src/python_types/tutorial013_py39.py hl[1,4] *} Python itself doesn't do anything with this `Annotated`. And for editors and other tools, the type is still `str`. diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 9380fb7d43..45bc9873ec 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,400 @@ hide: ### Docs +* 📝 Add contribution instructions about LLM generated code and comments and automated tools for PRs. PR [#14706](https://github.com/fastapi/fastapi/pull/14706) by [@tiangolo](https://github.com/tiangolo). +* 📝 Update docs for management tasks. PR [#14705](https://github.com/fastapi/fastapi/pull/14705) by [@tiangolo](https://github.com/tiangolo). +* 📝 Update docs about managing translations. PR [#14704](https://github.com/fastapi/fastapi/pull/14704) by [@tiangolo](https://github.com/tiangolo). +* 📝 Update docs for contributing with translations. PR [#14701](https://github.com/fastapi/fastapi/pull/14701) by [@tiangolo](https://github.com/tiangolo). +* 📝 Specify language code for code block. PR [#14656](https://github.com/fastapi/fastapi/pull/14656) by [@YuriiMotov](https://github.com/YuriiMotov). + +### Translations + +* 🌐 Update translations for ko (update-outdated). PR [#14589](https://github.com/fastapi/fastapi/pull/14589) by [@tiangolo](https://github.com/tiangolo). +* 🌐 Update translations for uk (update-outdated). PR [#14587](https://github.com/fastapi/fastapi/pull/14587) by [@tiangolo](https://github.com/tiangolo). +* 🌐 Update translations for es (update-outdated). PR [#14686](https://github.com/fastapi/fastapi/pull/14686) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Add LLM prompt file for Turkish, generated from the existing translations. PR [#14547](https://github.com/fastapi/fastapi/pull/14547) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Add LLM prompt file for Traditional Chinese, generated from the existing translations. PR [#14550](https://github.com/fastapi/fastapi/pull/14550) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Add LLM prompt file for Simplified Chinese, generated from the existing translations. PR [#14549](https://github.com/fastapi/fastapi/pull/14549) by [@tiangolo](https://github.com/tiangolo). + +### Internal + +* ⬆ Bump actions/cache from 4 to 5. PR [#14511](https://github.com/fastapi/fastapi/pull/14511) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump actions/upload-artifact from 5 to 6. PR [#14525](https://github.com/fastapi/fastapi/pull/14525) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump actions/download-artifact from 6 to 7. PR [#14526](https://github.com/fastapi/fastapi/pull/14526) by [@dependabot[bot]](https://github.com/apps/dependabot). +* 👷 Tweak CI input names. PR [#14688](https://github.com/fastapi/fastapi/pull/14688) by [@tiangolo](https://github.com/tiangolo). +* 🔨 Refactor translation script to allow committing in place. PR [#14687](https://github.com/fastapi/fastapi/pull/14687) by [@tiangolo](https://github.com/tiangolo). +* 🐛 Fix translation script path. PR [#14685](https://github.com/fastapi/fastapi/pull/14685) by [@tiangolo](https://github.com/tiangolo). +* ✅ Enable tests in CI for scripts. PR [#14684](https://github.com/fastapi/fastapi/pull/14684) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Add pre-commit local script to fix language translations. PR [#14683](https://github.com/fastapi/fastapi/pull/14683) by [@tiangolo](https://github.com/tiangolo). +* ⬆️ Migrate to uv. PR [#14676](https://github.com/fastapi/fastapi/pull/14676) by [@DoctorJohn](https://github.com/DoctorJohn). +* 🔨 Add LLM translations tool fixer. PR [#14652](https://github.com/fastapi/fastapi/pull/14652) by [@YuriiMotov](https://github.com/YuriiMotov). +* 👥 Update FastAPI People - Sponsors. PR [#14626](https://github.com/fastapi/fastapi/pull/14626) by [@tiangolo](https://github.com/tiangolo). +* 👥 Update FastAPI GitHub topic repositories. PR [#14630](https://github.com/fastapi/fastapi/pull/14630) by [@tiangolo](https://github.com/tiangolo). +* 👥 Update FastAPI People - Contributors and Translators. PR [#14625](https://github.com/fastapi/fastapi/pull/14625) by [@tiangolo](https://github.com/tiangolo). +* 🌐 Update translation prompts. PR [#14619](https://github.com/fastapi/fastapi/pull/14619) by [@tiangolo](https://github.com/tiangolo). +* 🔨 Update LLM translation script to guide reviewers to change the prompt. PR [#14614](https://github.com/fastapi/fastapi/pull/14614) by [@tiangolo](https://github.com/tiangolo). +* 👷 Do not run translations on cron while finishing updating existing languages. PR [#14613](https://github.com/fastapi/fastapi/pull/14613) by [@tiangolo](https://github.com/tiangolo). +* 🔥 Remove test variants for Pydantic v1 in test_request_params. PR [#14612](https://github.com/fastapi/fastapi/pull/14612) by [@tiangolo](https://github.com/tiangolo). +* 🔥 Remove Pydantic v1 specific test variants. PR [#14611](https://github.com/fastapi/fastapi/pull/14611) by [@tiangolo](https://github.com/tiangolo). + +## 0.128.0 + +### Breaking Changes + +* ➖ Drop support for `pydantic.v1`. PR [#14609](https://github.com/fastapi/fastapi/pull/14609) by [@tiangolo](https://github.com/tiangolo). + +### Internal + +* ✅ Run performance tests only on Pydantic v2. PR [#14608](https://github.com/fastapi/fastapi/pull/14608) by [@tiangolo](https://github.com/tiangolo). + +## 0.127.1 + +### Refactors + +* 🔊 Add a custom `FastAPIDeprecationWarning`. PR [#14605](https://github.com/fastapi/fastapi/pull/14605) by [@tiangolo](https://github.com/tiangolo). + +### Docs + +* 📝 Add documentary to website. PR [#14600](https://github.com/fastapi/fastapi/pull/14600) by [@tiangolo](https://github.com/tiangolo). + +### Translations + +* 🌐 Update translations for de (update-outdated). PR [#14602](https://github.com/fastapi/fastapi/pull/14602) by [@nilslindemann](https://github.com/nilslindemann). +* 🌐 Update translations for de (update-outdated). PR [#14581](https://github.com/fastapi/fastapi/pull/14581) by [@nilslindemann](https://github.com/nilslindemann). + +### Internal + +* 🔧 Update pre-commit to use local Ruff instead of hook. PR [#14604](https://github.com/fastapi/fastapi/pull/14604) by [@tiangolo](https://github.com/tiangolo). +* ✅ Add missing tests for code examples. PR [#14569](https://github.com/fastapi/fastapi/pull/14569) by [@YuriiMotov](https://github.com/YuriiMotov). +* 👷 Remove `lint` job from `test` CI workflow. PR [#14593](https://github.com/fastapi/fastapi/pull/14593) by [@YuriiMotov](https://github.com/YuriiMotov). +* 👷 Update secrets check. PR [#14592](https://github.com/fastapi/fastapi/pull/14592) by [@tiangolo](https://github.com/tiangolo). +* 👷 Run CodSpeed tests in parallel to other tests to speed up CI. PR [#14586](https://github.com/fastapi/fastapi/pull/14586) by [@tiangolo](https://github.com/tiangolo). +* 🔨 Update scripts and pre-commit to autofix files. PR [#14585](https://github.com/fastapi/fastapi/pull/14585) by [@tiangolo](https://github.com/tiangolo). + +## 0.127.0 + +### Breaking Changes + +* 🔊 Add deprecation warnings when using `pydantic.v1`. PR [#14583](https://github.com/fastapi/fastapi/pull/14583) by [@tiangolo](https://github.com/tiangolo). + +### Translations + +* 🔧 Add LLM prompt file for Korean, generated from the existing translations. PR [#14546](https://github.com/fastapi/fastapi/pull/14546) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Add LLM prompt file for Japanese, generated from the existing translations. PR [#14545](https://github.com/fastapi/fastapi/pull/14545) by [@tiangolo](https://github.com/tiangolo). + +### Internal + +* ⬆️ Upgrade OpenAI model for translations to gpt-5.2. PR [#14579](https://github.com/fastapi/fastapi/pull/14579) by [@tiangolo](https://github.com/tiangolo). + +## 0.126.0 + +### Upgrades + +* ➖ Drop support for Pydantic v1, keeping short temporary support for Pydantic v2's `pydantic.v1`. PR [#14575](https://github.com/fastapi/fastapi/pull/14575) by [@tiangolo](https://github.com/tiangolo). + * The minimum version of Pydantic installed is now `pydantic >=2.7.0`. + * The `standard` dependencies now include `pydantic-settings >=2.0.0` and `pydantic-extra-types >=2.0.0`. + +### Docs + +* 📝 Fix duplicated variable in `docs_src/python_types/tutorial005_py39.py`. PR [#14565](https://github.com/fastapi/fastapi/pull/14565) by [@paras-verma7454](https://github.com/paras-verma7454). + +### Translations + +* 🔧 Add LLM prompt file for Ukrainian, generated from the existing translations. PR [#14548](https://github.com/fastapi/fastapi/pull/14548) by [@tiangolo](https://github.com/tiangolo). + +### Internal + +* 🔧 Tweak pre-commit to allow committing release-notes. PR [#14577](https://github.com/fastapi/fastapi/pull/14577) by [@tiangolo](https://github.com/tiangolo). +* ⬆️ Use prek as a pre-commit alternative. PR [#14572](https://github.com/fastapi/fastapi/pull/14572) by [@tiangolo](https://github.com/tiangolo). +* 👷 Add performance tests with CodSpeed. PR [#14558](https://github.com/fastapi/fastapi/pull/14558) by [@tiangolo](https://github.com/tiangolo). + +## 0.125.0 + +### Breaking Changes + +* 🔧 Drop support for Python 3.8. PR [#14563](https://github.com/fastapi/fastapi/pull/14563) by [@tiangolo](https://github.com/tiangolo). + * This would actually not be a _breaking_ change as no code would really break. Any Python 3.8 installer would just refuse to install the latest version of FastAPI and would only install 0.124.4. Only marking it as a "breaking change" to make it visible. + +### Refactors + +* ♻️ Upgrade internal syntax to Python 3.9+ 🎉. PR [#14564](https://github.com/fastapi/fastapi/pull/14564) by [@tiangolo](https://github.com/tiangolo). + +### Docs + +* ⚰️ Remove Python 3.8 from CI and remove Python 3.8 examples from source docs. PR [#14559](https://github.com/fastapi/fastapi/pull/14559) by [@YuriiMotov](https://github.com/YuriiMotov) and [@tiangolo](https://github.com/tiangolo). + +### Translations + +* 🌐 Update translations for pt (add-missing). PR [#14539](https://github.com/fastapi/fastapi/pull/14539) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Add LLM prompt file for French, generated from the existing French docs. PR [#14544](https://github.com/fastapi/fastapi/pull/14544) by [@tiangolo](https://github.com/tiangolo). +* 🌐 Sync Portuguese docs (pages found with script). PR [#14554](https://github.com/fastapi/fastapi/pull/14554) by [@YuriiMotov](https://github.com/YuriiMotov). +* 🌐 Sync Spanish docs (outdated pages found with script). PR [#14553](https://github.com/fastapi/fastapi/pull/14553) by [@YuriiMotov](https://github.com/YuriiMotov). +* 🌐 Sync German docs. PR [#14519](https://github.com/fastapi/fastapi/pull/14519) by [@nilslindemann](https://github.com/nilslindemann). +* 🔥 Remove inactive/scarce translations to Vietnamese. PR [#14543](https://github.com/fastapi/fastapi/pull/14543) by [@tiangolo](https://github.com/tiangolo). +* 🔥 Remove inactive/scarce translations to Persian. PR [#14542](https://github.com/fastapi/fastapi/pull/14542) by [@tiangolo](https://github.com/tiangolo). +* 🔥 Remove translation to emoji to simplify the new setup with LLM autotranslations. PR [#14541](https://github.com/fastapi/fastapi/pull/14541) by [@tiangolo](https://github.com/tiangolo). +* 🌐 Update translations for pt (update-outdated). PR [#14537](https://github.com/fastapi/fastapi/pull/14537) by [@tiangolo](https://github.com/tiangolo). +* 🌐 Update translations for es (update-outdated). PR [#14532](https://github.com/fastapi/fastapi/pull/14532) by [@tiangolo](https://github.com/tiangolo). +* 🌐 Update translations for es (add-missing). PR [#14533](https://github.com/fastapi/fastapi/pull/14533) by [@tiangolo](https://github.com/tiangolo). +* 🌐 Remove translations for removed docs. PR [#14516](https://github.com/fastapi/fastapi/pull/14516) by [@tiangolo](https://github.com/tiangolo). + +### Internal + +* ⬆ Bump `markdown-include-variants` from 0.0.7 to 0.0.8. PR [#14556](https://github.com/fastapi/fastapi/pull/14556) by [@YuriiMotov](https://github.com/YuriiMotov). +* 🔧 Temporarily disable translations still in progress, being migrated to the new LLM setup. PR [#14555](https://github.com/fastapi/fastapi/pull/14555) by [@YuriiMotov](https://github.com/YuriiMotov). +* 🔧 Update test workflow config, remove commented code. PR [#14540](https://github.com/fastapi/fastapi/pull/14540) by [@tiangolo](https://github.com/tiangolo). +* 👷 Configure coverage, error on main tests, don't wait for Smokeshow. PR [#14536](https://github.com/fastapi/fastapi/pull/14536) by [@tiangolo](https://github.com/tiangolo). +* 👷 Run Smokeshow always, even on test failures. PR [#14538](https://github.com/fastapi/fastapi/pull/14538) by [@tiangolo](https://github.com/tiangolo). +* 👷 Make Pydantic versions customizable in CI. PR [#14535](https://github.com/fastapi/fastapi/pull/14535) by [@tiangolo](https://github.com/tiangolo). +* 👷 Fix checkout GitHub Action fetch-depth for LLM translations, enable cron monthly. PR [#14531](https://github.com/fastapi/fastapi/pull/14531) by [@tiangolo](https://github.com/tiangolo). +* 👷 Fix Typer command for CI LLM translations. PR [#14530](https://github.com/fastapi/fastapi/pull/14530) by [@tiangolo](https://github.com/tiangolo). +* 👷 Update LLM translation CI, add language matrix and extra commands, prepare for scheduled run. PR [#14529](https://github.com/fastapi/fastapi/pull/14529) by [@tiangolo](https://github.com/tiangolo). +* 👷 Update github-actions user for GitHub Actions workflows. PR [#14528](https://github.com/fastapi/fastapi/pull/14528) by [@tiangolo](https://github.com/tiangolo). +* ➕ Add requirements for translations. PR [#14515](https://github.com/fastapi/fastapi/pull/14515) by [@tiangolo](https://github.com/tiangolo). + +## 0.124.4 + +### Fixes + +* 🐛 Fix parameter aliases. PR [#14371](https://github.com/fastapi/fastapi/pull/14371) by [@YuriiMotov](https://github.com/YuriiMotov). + +## 0.124.3 + +### Fixes + +* 🐛 Fix support for tagged union with discriminator inside of `Annotated` with `Body()`. PR [#14512](https://github.com/fastapi/fastapi/pull/14512) by [@tiangolo](https://github.com/tiangolo). + +### Refactors + +* ✅ Add set of tests for request parameters and alias. PR [#14358](https://github.com/fastapi/fastapi/pull/14358) by [@YuriiMotov](https://github.com/YuriiMotov). + +### Docs + +* 📝 Tweak links format. PR [#14505](https://github.com/fastapi/fastapi/pull/14505) by [@tiangolo](https://github.com/tiangolo). +* 📝 Update docs about re-raising validation errors, do not include string as is to not leak information. PR [#14487](https://github.com/fastapi/fastapi/pull/14487) by [@tiangolo](https://github.com/tiangolo). +* 🔥 Remove external links section. PR [#14486](https://github.com/fastapi/fastapi/pull/14486) by [@tiangolo](https://github.com/tiangolo). + +### Translations + +* 🌐 Sync Russian docs. PR [#14509](https://github.com/fastapi/fastapi/pull/14509) by [@YuriiMotov](https://github.com/YuriiMotov). +* 🌐 Sync German docs. PR [#14488](https://github.com/fastapi/fastapi/pull/14488) by [@nilslindemann](https://github.com/nilslindemann). + +### Internal + +* 👷 Tweak coverage to not pass Smokeshow max file size limit. PR [#14507](https://github.com/fastapi/fastapi/pull/14507) by [@tiangolo](https://github.com/tiangolo). +* ✅ Expand test matrix to include Windows and MacOS. PR [#14171](https://github.com/fastapi/fastapi/pull/14171) by [@svlandeg](https://github.com/svlandeg). + +## 0.124.2 + +### Fixes + +* 🐛 Fix support for `if TYPE_CHECKING`, non-evaluated stringified annotations. PR [#14485](https://github.com/fastapi/fastapi/pull/14485) by [@tiangolo](https://github.com/tiangolo). + +## 0.124.1 + +### Fixes + +* 🐛 Fix handling arbitrary types when using `arbitrary_types_allowed=True`. PR [#14482](https://github.com/fastapi/fastapi/pull/14482) by [@tiangolo](https://github.com/tiangolo). + +### Docs + +* 📝 Add variants for code examples in "Advanced User Guide". PR [#14413](https://github.com/fastapi/fastapi/pull/14413) by [@YuriiMotov](https://github.com/YuriiMotov). +* 📝 Update tech stack in project generation docs. PR [#14472](https://github.com/fastapi/fastapi/pull/14472) by [@alejsdev](https://github.com/alejsdev). + +### Internal + +* ✅ Add test for Pydantic v2, dataclasses, UUID, and `__annotations__`. PR [#14477](https://github.com/fastapi/fastapi/pull/14477) by [@tiangolo](https://github.com/tiangolo). + +## 0.124.0 + +### Features + +* 🚸 Improve tracebacks by adding endpoint metadata. PR [#14306](https://github.com/fastapi/fastapi/pull/14306) by [@savannahostrowski](https://github.com/savannahostrowski). + +### Internal + +* ✏️ Fix typo in `scripts/mkdocs_hooks.py`. PR [#14457](https://github.com/fastapi/fastapi/pull/14457) by [@yujiteshima](https://github.com/yujiteshima). + +## 0.123.10 + +### Fixes + +* 🐛 Fix using class (not instance) dependency that has `__call__` method. PR [#14458](https://github.com/fastapi/fastapi/pull/14458) by [@YuriiMotov](https://github.com/YuriiMotov). +* 🐛 Fix `separate_input_output_schemas=False` with `computed_field`. PR [#14453](https://github.com/fastapi/fastapi/pull/14453) by [@YuriiMotov](https://github.com/YuriiMotov). + +## 0.123.9 + +### Fixes + +* 🐛 Fix OAuth2 scopes in OpenAPI in extra corner cases, parent dependency with scopes, sub-dependency security scheme without scopes. PR [#14459](https://github.com/fastapi/fastapi/pull/14459) by [@tiangolo](https://github.com/tiangolo). + +## 0.123.8 + +### Fixes + +* 🐛 Fix OpenAPI security scheme OAuth2 scopes declaration, deduplicate security schemes with different scopes. PR [#14455](https://github.com/fastapi/fastapi/pull/14455) by [@tiangolo](https://github.com/tiangolo). + +## 0.123.7 + +### Fixes + +* 🐛 Fix evaluating stringified annotations in Python 3.10. PR [#11355](https://github.com/fastapi/fastapi/pull/11355) by [@chaen](https://github.com/chaen). + +## 0.123.6 + +### Fixes + +* 🐛 Fix support for functools wraps and partial combined, for async and regular functions and classes in path operations and dependencies. PR [#14448](https://github.com/fastapi/fastapi/pull/14448) by [@tiangolo](https://github.com/tiangolo). + +## 0.123.5 + +### Features + +* ✨ Allow using dependables with `functools.partial()`. PR [#9753](https://github.com/fastapi/fastapi/pull/9753) by [@lieryan](https://github.com/lieryan). +* ✨ Add support for wrapped functions (e.g. `@functools.wraps()`) used with forward references. PR [#5077](https://github.com/fastapi/fastapi/pull/5077) by [@lucaswiman](https://github.com/lucaswiman). +* ✨ Handle wrapped dependencies. PR [#9555](https://github.com/fastapi/fastapi/pull/9555) by [@phy1729](https://github.com/phy1729). + +### Fixes + +* 🐛 Fix optional sequence handling with new union syntax from Python 3.10. PR [#14430](https://github.com/fastapi/fastapi/pull/14430) by [@Viicos](https://github.com/Viicos). + +### Refactors + +* 🔥 Remove dangling extra condiitonal no longer needed. PR [#14435](https://github.com/fastapi/fastapi/pull/14435) by [@tiangolo](https://github.com/tiangolo). +* ♻️ Refactor internals, update `is_coroutine` check to reuse internal supported variants (unwrap, check class). PR [#14434](https://github.com/fastapi/fastapi/pull/14434) by [@tiangolo](https://github.com/tiangolo). + +### Translations + +* 🌐 Sync German docs. PR [#14367](https://github.com/fastapi/fastapi/pull/14367) by [@nilslindemann](https://github.com/nilslindemann). + +## 0.123.4 + +### Fixes + +* 🐛 Fix OpenAPI schema support for computed fields when using `separate_input_output_schemas=False`. PR [#13207](https://github.com/fastapi/fastapi/pull/13207) by [@vgrafe](https://github.com/vgrafe). + +### Docs + +* 📝 Fix docstring of `servers` parameter. PR [#14405](https://github.com/fastapi/fastapi/pull/14405) by [@YuriiMotov](https://github.com/YuriiMotov). + +## 0.123.3 + +### Fixes + +* 🐛 Fix Query\Header\Cookie parameter model alias. PR [#14360](https://github.com/fastapi/fastapi/pull/14360) by [@YuriiMotov](https://github.com/YuriiMotov). +* 🐛 Fix optional sequence handling in `serialize sequence value` with Pydantic V2. PR [#14297](https://github.com/fastapi/fastapi/pull/14297) by [@YuriiMotov](https://github.com/YuriiMotov). + +## 0.123.2 + +### Fixes + +* 🐛 Fix unformatted `{type_}` in FastAPIError. PR [#14416](https://github.com/fastapi/fastapi/pull/14416) by [@Just-Helpful](https://github.com/Just-Helpful). +* 🐛 Fix parsing extra non-body parameter list. PR [#14356](https://github.com/fastapi/fastapi/pull/14356) by [@YuriiMotov](https://github.com/YuriiMotov). +* 🐛 Fix parsing extra `Form` parameter list. PR [#14303](https://github.com/fastapi/fastapi/pull/14303) by [@YuriiMotov](https://github.com/YuriiMotov). +* 🐛 Fix support for form values with empty strings interpreted as missing (`None` if that's the default), for compatibility with HTML forms. PR [#13537](https://github.com/fastapi/fastapi/pull/13537) by [@MarinPostma](https://github.com/MarinPostma). + +### Docs + +* 📝 Add tip on how to install `pip` in case of `No module named pip` error in `virtual-environments.md`. PR [#14211](https://github.com/fastapi/fastapi/pull/14211) by [@zadevhub](https://github.com/zadevhub). +* 📝 Update Primary Key notes for the SQL databases tutorial to avoid confusion. PR [#14120](https://github.com/fastapi/fastapi/pull/14120) by [@FlaviusRaducu](https://github.com/FlaviusRaducu). +* 📝 Clarify estimation note in documentation. PR [#14070](https://github.com/fastapi/fastapi/pull/14070) by [@SaisakthiM](https://github.com/SaisakthiM). + +## 0.123.1 + +### Fixes + +* 🐛 Avoid accessing non-existing "$ref" key for Pydantic v2 compat remapping. PR [#14361](https://github.com/fastapi/fastapi/pull/14361) by [@svlandeg](https://github.com/svlandeg). +* 🐛 Fix `TypeError` when encoding a decimal with a `NaN` or `Infinity` value. PR [#12935](https://github.com/fastapi/fastapi/pull/12935) by [@kentwelcome](https://github.com/kentwelcome). + +### Internal + +* 🐛 Fix Windows UnicodeEncodeError in CLI test. PR [#14295](https://github.com/fastapi/fastapi/pull/14295) by [@hemanth-thirthahalli](https://github.com/hemanth-thirthahalli). +* 🔧 Update sponsors: add Greptile. PR [#14429](https://github.com/fastapi/fastapi/pull/14429) by [@tiangolo](https://github.com/tiangolo). +* 👥 Update FastAPI GitHub topic repositories. PR [#14426](https://github.com/fastapi/fastapi/pull/14426) by [@tiangolo](https://github.com/tiangolo). +* ⬆ Bump markdown-include-variants from 0.0.6 to 0.0.7. PR [#14423](https://github.com/fastapi/fastapi/pull/14423) by [@YuriiMotov](https://github.com/YuriiMotov). +* 👥 Update FastAPI People - Sponsors. PR [#14422](https://github.com/fastapi/fastapi/pull/14422) by [@tiangolo](https://github.com/tiangolo). +* 👥 Update FastAPI People - Contributors and Translators. PR [#14420](https://github.com/fastapi/fastapi/pull/14420) by [@tiangolo](https://github.com/tiangolo). + +## 0.123.0 + +### Fixes + +* 🐛 Cache dependencies that don't use scopes and don't have sub-dependencies with scopes. PR [#14419](https://github.com/fastapi/fastapi/pull/14419) by [@tiangolo](https://github.com/tiangolo). + +## 0.122.1 + +### Fixes + +* 🐛 Fix hierarchical security scope propagation. PR [#5624](https://github.com/fastapi/fastapi/pull/5624) by [@kristjanvalur](https://github.com/kristjanvalur). + +### Docs + +* 💅 Update CSS to explicitly use emoji font. PR [#14415](https://github.com/fastapi/fastapi/pull/14415) by [@tiangolo](https://github.com/tiangolo). + +### Internal + +* ⬆ Bump markdown-include-variants from 0.0.5 to 0.0.6. PR [#14418](https://github.com/fastapi/fastapi/pull/14418) by [@YuriiMotov](https://github.com/YuriiMotov). + +## 0.122.0 + +### Fixes + +* 🐛 Use `401` status code in security classes when credentials are missing. PR [#13786](https://github.com/fastapi/fastapi/pull/13786) by [@YuriiMotov](https://github.com/YuriiMotov). + * If your code depended on these classes raising the old (less correct) `403` status code, check the new docs about how to override the classes, to use the same old behavior: [Use Old 403 Authentication Error Status Codes](https://fastapi.tiangolo.com/how-to/authentication-error-status-code/). + +### Internal + +* 🔧 Configure labeler to exclude files that start from underscore for `lang-all` label. PR [#14213](https://github.com/fastapi/fastapi/pull/14213) by [@YuriiMotov](https://github.com/YuriiMotov). +* 👷 Add pre-commit config with local script for permalinks. PR [#14398](https://github.com/fastapi/fastapi/pull/14398) by [@tiangolo](https://github.com/tiangolo). +* 💄 Use font Fira Code to fix display of Rich panels in docs in Windows. PR [#14387](https://github.com/fastapi/fastapi/pull/14387) by [@tiangolo](https://github.com/tiangolo). +* 👷 Add custom pre-commit CI. PR [#14397](https://github.com/fastapi/fastapi/pull/14397) by [@tiangolo](https://github.com/tiangolo). +* ⬆ Bump actions/checkout from 5 to 6. PR [#14381](https://github.com/fastapi/fastapi/pull/14381) by [@dependabot[bot]](https://github.com/apps/dependabot). +* 👷 Upgrade `latest-changes` GitHub Action and pin `actions/checkout@v5`. PR [#14403](https://github.com/fastapi/fastapi/pull/14403) by [@svlandeg](https://github.com/svlandeg). +* 🛠️ Add `add-permalinks` and `add-permalinks-page` to `scripts/docs.py`. PR [#14033](https://github.com/fastapi/fastapi/pull/14033) by [@YuriiMotov](https://github.com/YuriiMotov). +* 🔧 Upgrade Material for MkDocs and remove insiders. PR [#14375](https://github.com/fastapi/fastapi/pull/14375) by [@tiangolo](https://github.com/tiangolo). + +## 0.121.3 + +### Refactors + +* ♻️ Make the result of `Depends()` and `Security()` hashable, as a workaround for other tools interacting with these internal parts. PR [#14372](https://github.com/fastapi/fastapi/pull/14372) by [@tiangolo](https://github.com/tiangolo). + +### Upgrades + +* ⬆️ Bump Starlette to <`0.51.0`. PR [#14282](https://github.com/fastapi/fastapi/pull/14282) by [@musicinmybrain](https://github.com/musicinmybrain). + +### Docs + +* 📝 Add missing hash part. PR [#14369](https://github.com/fastapi/fastapi/pull/14369) by [@nilslindemann](https://github.com/nilslindemann). +* 📝 Fix typos in code comments. PR [#14364](https://github.com/fastapi/fastapi/pull/14364) by [@Edge-Seven](https://github.com/Edge-Seven). +* 📝 Add docs for using FastAPI Cloud. PR [#14359](https://github.com/fastapi/fastapi/pull/14359) by [@tiangolo](https://github.com/tiangolo). + +## 0.121.2 + +### Fixes + +* 🐛 Fix handling of JSON Schema attributes named "$ref". PR [#14349](https://github.com/fastapi/fastapi/pull/14349) by [@tiangolo](https://github.com/tiangolo). + +### Docs + +* 📝 Add EuroPython talk & podcast episode with Sebastián Ramírez. PR [#14260](https://github.com/fastapi/fastapi/pull/14260) by [@clytaemnestra](https://github.com/clytaemnestra). +* ✏️ Fix links and add missing permalink in docs. PR [#14217](https://github.com/fastapi/fastapi/pull/14217) by [@YuriiMotov](https://github.com/YuriiMotov). + +### Translations + +* 🌐 Update Portuguese translations with LLM prompt. PR [#14228](https://github.com/fastapi/fastapi/pull/14228) by [@ceb10n](https://github.com/ceb10n). +* 🔨 Add Portuguese translations LLM prompt. PR [#14208](https://github.com/fastapi/fastapi/pull/14208) by [@ceb10n](https://github.com/ceb10n). +* 🌐 Sync Russian docs. PR [#14331](https://github.com/fastapi/fastapi/pull/14331) by [@YuriiMotov](https://github.com/YuriiMotov). +* 🌐 Sync German docs. PR [#14317](https://github.com/fastapi/fastapi/pull/14317) by [@nilslindemann](https://github.com/nilslindemann). + +## 0.121.1 + +### Fixes + +* 🐛 Fix `Depends(func, scope='function')` for top level (parameterless) dependencies. PR [#14301](https://github.com/fastapi/fastapi/pull/14301) by [@luzzodev](https://github.com/luzzodev). + +### Docs + * 📝 Upate docs for advanced dependencies with `yield`, noting the changes in 0.121.0, adding `scope`. PR [#14287](https://github.com/fastapi/fastapi/pull/14287) by [@tiangolo](https://github.com/tiangolo). ### Internal diff --git a/docs/en/docs/resources/index.md b/docs/en/docs/resources/index.md index caefdf1254..f7d48576ff 100644 --- a/docs/en/docs/resources/index.md +++ b/docs/en/docs/resources/index.md @@ -1,3 +1,3 @@ # Resources { #resources } -Additional resources, external links, articles and more. ✈️ +Additional resources, external links, and more. ✈️ diff --git a/docs/en/docs/tutorial/background-tasks.md b/docs/en/docs/tutorial/background-tasks.md index ab44f89c19..be7ecd5871 100644 --- a/docs/en/docs/tutorial/background-tasks.md +++ b/docs/en/docs/tutorial/background-tasks.md @@ -15,7 +15,7 @@ This includes, for example: First, import `BackgroundTasks` and define a parameter in your *path operation function* with a type declaration of `BackgroundTasks`: -{* ../../docs_src/background_tasks/tutorial001.py hl[1,13] *} +{* ../../docs_src/background_tasks/tutorial001_py39.py hl[1,13] *} **FastAPI** will create the object of type `BackgroundTasks` for you and pass it as that parameter. @@ -31,13 +31,13 @@ In this case, the task function will write to a file (simulating sending an emai And as the write operation doesn't use `async` and `await`, we define the function with normal `def`: -{* ../../docs_src/background_tasks/tutorial001.py hl[6:9] *} +{* ../../docs_src/background_tasks/tutorial001_py39.py hl[6:9] *} ## Add the background task { #add-the-background-task } Inside of your *path operation function*, pass your task function to the *background tasks* object with the method `.add_task()`: -{* ../../docs_src/background_tasks/tutorial001.py hl[14] *} +{* ../../docs_src/background_tasks/tutorial001_py39.py hl[14] *} `.add_task()` receives as arguments: diff --git a/docs/en/docs/tutorial/bigger-applications.md b/docs/en/docs/tutorial/bigger-applications.md index 74daa54835..f6cee8036b 100644 --- a/docs/en/docs/tutorial/bigger-applications.md +++ b/docs/en/docs/tutorial/bigger-applications.md @@ -56,7 +56,7 @@ from app.routers import items The same file structure with comments: -``` +```bash . ├── app # "app" is a Python package │   ├── __init__.py # this file makes "app" a "Python package" @@ -85,9 +85,7 @@ You can create the *path operations* for that module using `APIRouter`. You import it and create an "instance" the same way you would with the class `FastAPI`: -```Python hl_lines="1 3" title="app/routers/users.py" -{!../../docs_src/bigger_applications/app/routers/users.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/routers/users.py hl[1,3] title["app/routers/users.py"] *} ### *Path operations* with `APIRouter` { #path-operations-with-apirouter } @@ -95,9 +93,7 @@ And then you use it to declare your *path operations*. Use it the same way you would use the `FastAPI` class: -```Python hl_lines="6 11 16" title="app/routers/users.py" -{!../../docs_src/bigger_applications/app/routers/users.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/routers/users.py hl[6,11,16] title["app/routers/users.py"] *} You can think of `APIRouter` as a "mini `FastAPI`" class. @@ -121,35 +117,7 @@ So we put them in their own `dependencies` module (`app/dependencies.py`). We will now use a simple dependency to read a custom `X-Token` header: -//// tab | Python 3.9+ - -```Python hl_lines="3 6-8" title="app/dependencies.py" -{!> ../../docs_src/bigger_applications/app_an_py39/dependencies.py!} -``` - -//// - -//// tab | Python 3.8+ - -```Python hl_lines="1 5-7" title="app/dependencies.py" -{!> ../../docs_src/bigger_applications/app_an/dependencies.py!} -``` - -//// - -//// tab | Python 3.8+ non-Annotated - -/// tip - -Prefer to use the `Annotated` version if possible. - -/// - -```Python hl_lines="1 4-6" title="app/dependencies.py" -{!> ../../docs_src/bigger_applications/app/dependencies.py!} -``` - -//// +{* ../../docs_src/bigger_applications/app_an_py39/dependencies.py hl[3,6:8] title["app/dependencies.py"] *} /// tip @@ -181,9 +149,7 @@ We know all the *path operations* in this module have the same: So, instead of adding all that to each *path operation*, we can add it to the `APIRouter`. -```Python hl_lines="5-10 16 21" title="app/routers/items.py" -{!../../docs_src/bigger_applications/app/routers/items.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/routers/items.py hl[5:10,16,21] title["app/routers/items.py"] *} As the path of each *path operation* has to start with `/`, like in: @@ -242,9 +208,7 @@ And we need to get the dependency function from the module `app.dependencies`, t So we use a relative import with `..` for the dependencies: -```Python hl_lines="3" title="app/routers/items.py" -{!../../docs_src/bigger_applications/app/routers/items.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/routers/items.py hl[3] title["app/routers/items.py"] *} #### How relative imports work { #how-relative-imports-work } @@ -315,9 +279,7 @@ We are not adding the prefix `/items` nor the `tags=["items"]` to each *path ope But we can still add _more_ `tags` that will be applied to a specific *path operation*, and also some extra `responses` specific to that *path operation*: -```Python hl_lines="30-31" title="app/routers/items.py" -{!../../docs_src/bigger_applications/app/routers/items.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/routers/items.py hl[30:31] title["app/routers/items.py"] *} /// tip @@ -343,17 +305,13 @@ You import and create a `FastAPI` class as normally. And we can even declare [global dependencies](dependencies/global-dependencies.md){.internal-link target=_blank} that will be combined with the dependencies for each `APIRouter`: -```Python hl_lines="1 3 7" title="app/main.py" -{!../../docs_src/bigger_applications/app/main.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[1,3,7] title["app/main.py"] *} ### Import the `APIRouter` { #import-the-apirouter } Now we import the other submodules that have `APIRouter`s: -```Python hl_lines="4-5" title="app/main.py" -{!../../docs_src/bigger_applications/app/main.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[4:5] title["app/main.py"] *} As the files `app/routers/users.py` and `app/routers/items.py` are submodules that are part of the same Python package `app`, we can use a single dot `.` to import them using "relative imports". @@ -416,17 +374,13 @@ the `router` from `users` would overwrite the one from `items` and we wouldn't b So, to be able to use both of them in the same file, we import the submodules directly: -```Python hl_lines="5" title="app/main.py" -{!../../docs_src/bigger_applications/app/main.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[5] title["app/main.py"] *} ### Include the `APIRouter`s for `users` and `items` { #include-the-apirouters-for-users-and-items } Now, let's include the `router`s from the submodules `users` and `items`: -```Python hl_lines="10-11" title="app/main.py" -{!../../docs_src/bigger_applications/app/main.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[10:11] title["app/main.py"] *} /// info @@ -466,17 +420,13 @@ It contains an `APIRouter` with some admin *path operations* that your organizat For this example it will be super simple. But let's say that because it is shared with other projects in the organization, we cannot modify it and add a `prefix`, `dependencies`, `tags`, etc. directly to the `APIRouter`: -```Python hl_lines="3" title="app/internal/admin.py" -{!../../docs_src/bigger_applications/app/internal/admin.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/internal/admin.py hl[3] title["app/internal/admin.py"] *} But we still want to set a custom `prefix` when including the `APIRouter` so that all its *path operations* start with `/admin`, we want to secure it with the `dependencies` we already have for this project, and we want to include `tags` and `responses`. We can declare all that without having to modify the original `APIRouter` by passing those parameters to `app.include_router()`: -```Python hl_lines="14-17" title="app/main.py" -{!../../docs_src/bigger_applications/app/main.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[14:17] title["app/main.py"] *} That way, the original `APIRouter` will stay unmodified, so we can still share that same `app/internal/admin.py` file with other projects in the organization. @@ -497,9 +447,7 @@ We can also add *path operations* directly to the `FastAPI` app. Here we do it... just to show that we can 🤷: -```Python hl_lines="21-23" title="app/main.py" -{!../../docs_src/bigger_applications/app/main.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[21:23] title["app/main.py"] *} and it will work correctly, together with all the other *path operations* added with `app.include_router()`. diff --git a/docs/en/docs/tutorial/body-nested-models.md b/docs/en/docs/tutorial/body-nested-models.md index 445235a420..5fd83a8f3c 100644 --- a/docs/en/docs/tutorial/body-nested-models.md +++ b/docs/en/docs/tutorial/body-nested-models.md @@ -14,35 +14,15 @@ This will make `tags` be a list, although it doesn't declare the type of the ele But Python has a specific way to declare lists with internal types, or "type parameters": -### Import typing's `List` { #import-typings-list } - -In Python 3.9 and above you can use the standard `list` to declare these type annotations as we'll see below. 💡 - -But in Python versions before 3.9 (3.6 and above), you first need to import `List` from standard Python's `typing` module: - -{* ../../docs_src/body_nested_models/tutorial002.py hl[1] *} - ### Declare a `list` with a type parameter { #declare-a-list-with-a-type-parameter } -To declare types that have type parameters (internal types), like `list`, `dict`, `tuple`: - -* If you are in a Python version lower than 3.9, import their equivalent version from the `typing` module -* Pass the internal type(s) as "type parameters" using square brackets: `[` and `]` - -In Python 3.9 it would be: +To declare types that have type parameters (internal types), like `list`, `dict`, `tuple`, +pass the internal type(s) as "type parameters" using square brackets: `[` and `]` ```Python my_list: list[str] ``` -In versions of Python before 3.9, it would be: - -```Python -from typing import List - -my_list: List[str] -``` - That's all standard Python syntax for type declarations. Use that same standard syntax for model attributes with internal types. @@ -178,12 +158,6 @@ Notice how `Offer` has a list of `Item`s, which in turn have an optional list of If the top level value of the JSON body you expect is a JSON `array` (a Python `list`), you can declare the type in the parameter of the function, the same as in Pydantic models: -```Python -images: List[Image] -``` - -or in Python 3.9 and above: - ```Python images: list[Image] ``` diff --git a/docs/en/docs/tutorial/body-updates.md b/docs/en/docs/tutorial/body-updates.md index baeb53ec6f..1b7fd70661 100644 --- a/docs/en/docs/tutorial/body-updates.md +++ b/docs/en/docs/tutorial/body-updates.md @@ -50,14 +50,6 @@ If you want to receive partial updates, it's very useful to use the parameter `e Like `item.model_dump(exclude_unset=True)`. -/// info - -In Pydantic v1 the method was called `.dict()`, it was deprecated (but still supported) in Pydantic v2, and renamed to `.model_dump()`. - -The examples here use `.dict()` for compatibility with Pydantic v1, but you should use `.model_dump()` instead if you can use Pydantic v2. - -/// - That would generate a `dict` with only the data that was set when creating the `item` model, excluding default values. Then you can use this to generate a `dict` with only the data that was set (sent in the request), omitting default values: @@ -68,14 +60,6 @@ Then you can use this to generate a `dict` with only the data that was set (sent Now, you can create a copy of the existing model using `.model_copy()`, and pass the `update` parameter with a `dict` containing the data to update. -/// info - -In Pydantic v1 the method was called `.copy()`, it was deprecated (but still supported) in Pydantic v2, and renamed to `.model_copy()`. - -The examples here use `.copy()` for compatibility with Pydantic v1, but you should use `.model_copy()` instead if you can use Pydantic v2. - -/// - Like `stored_item_model.model_copy(update=update_data)`: {* ../../docs_src/body_updates/tutorial002_py310.py hl[33] *} diff --git a/docs/en/docs/tutorial/body.md b/docs/en/docs/tutorial/body.md index a820802f74..2d0dfcbb59 100644 --- a/docs/en/docs/tutorial/body.md +++ b/docs/en/docs/tutorial/body.md @@ -128,14 +128,6 @@ Inside of the function, you can access all the attributes of the model object di {* ../../docs_src/body/tutorial002_py310.py *} -/// info - -In Pydantic v1 the method was called `.dict()`, it was deprecated (but still supported) in Pydantic v2, and renamed to `.model_dump()`. - -The examples here use `.dict()` for compatibility with Pydantic v1, but you should use `.model_dump()` instead if you can use Pydantic v2. - -/// - ## Request body + path parameters { #request-body-path-parameters } You can declare path parameters and request body at the same time. @@ -163,7 +155,7 @@ The function parameters will be recognized as follows: FastAPI will know that the value of `q` is not required because of the default value `= None`. -The `str | None` (Python 3.10+) or `Union` in `Union[str, None]` (Python 3.8+) is not used by FastAPI to determine that the value is not required, it will know it's not required because it has a default value of `= None`. +The `str | None` (Python 3.10+) or `Union` in `Union[str, None]` (Python 3.9+) is not used by FastAPI to determine that the value is not required, it will know it's not required because it has a default value of `= None`. But adding the type annotations will allow your editor to give you better support and detect errors. diff --git a/docs/en/docs/tutorial/cookie-param-models.md b/docs/en/docs/tutorial/cookie-param-models.md index 96dc5cf3d8..016a65d7f0 100644 --- a/docs/en/docs/tutorial/cookie-param-models.md +++ b/docs/en/docs/tutorial/cookie-param-models.md @@ -50,7 +50,7 @@ Your API now has the power to control its own FastAPI Cloud, go and join the waiting list if you haven't. 🚀 + +If you already have a **FastAPI Cloud** account (we invited you from the waiting list 😉), you can deploy your application with one command. + +Before deploying, make sure you are logged in: + +
+ +```console +$ fastapi login + +You are logged in to FastAPI Cloud 🚀 +``` + +
+ +Then deploy your app: + +
+ +```console +$ fastapi deploy + +Deploying to FastAPI Cloud... + +✅ Deployment successful! + +🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev +``` + +
+ +That's it! Now you can access your app at that URL. ✨ + ## Recap, step by step { #recap-step-by-step } ### Step 1: import `FastAPI` { #step-1-import-fastapi } -{* ../../docs_src/first_steps/tutorial001.py hl[1] *} +{* ../../docs_src/first_steps/tutorial001_py39.py hl[1] *} `FastAPI` is a Python class that provides all the functionality for your API. @@ -161,7 +197,7 @@ You can use all the FastAPI Cloud** with one command: `fastapi deploy`. 🎉 + +#### About FastAPI Cloud { #about-fastapi-cloud } + +**FastAPI Cloud** is built by the same author and team behind **FastAPI**. + +It streamlines the process of **building**, **deploying**, and **accessing** an API with minimal effort. + +It brings the same **developer experience** of building apps with FastAPI to **deploying** them to the cloud. 🎉 + +FastAPI Cloud is the primary sponsor and funding provider for the *FastAPI and friends* open source projects. ✨ + +#### Deploy to other cloud providers { #deploy-to-other-cloud-providers } + +FastAPI is open source and based on standards. You can deploy FastAPI apps to any cloud provider you choose. + +Follow your cloud provider's guides to deploy FastAPI apps with them. 🤓 + ## Recap { #recap } * Import `FastAPI`. @@ -321,3 +377,4 @@ There are many other objects and models that will be automatically converted to * Write a **path operation decorator** using decorators like `@app.get("/")`. * Define a **path operation function**; for example, `def root(): ...`. * Run the development server using the command `fastapi dev`. +* Optionally deploy your app with `fastapi deploy`. diff --git a/docs/en/docs/tutorial/handling-errors.md b/docs/en/docs/tutorial/handling-errors.md index 53501837c9..0e43d7f6f1 100644 --- a/docs/en/docs/tutorial/handling-errors.md +++ b/docs/en/docs/tutorial/handling-errors.md @@ -25,7 +25,7 @@ To return HTTP responses with errors to the client you use `HTTPException`. ### Import `HTTPException` { #import-httpexception } -{* ../../docs_src/handling_errors/tutorial001.py hl[1] *} +{* ../../docs_src/handling_errors/tutorial001_py39.py hl[1] *} ### Raise an `HTTPException` in your code { #raise-an-httpexception-in-your-code } @@ -39,7 +39,7 @@ The benefit of raising an exception over returning a value will be more evident In this example, when the client requests an item by an ID that doesn't exist, raise an exception with a status code of `404`: -{* ../../docs_src/handling_errors/tutorial001.py hl[11] *} +{* ../../docs_src/handling_errors/tutorial001_py39.py hl[11] *} ### The resulting response { #the-resulting-response } @@ -77,7 +77,7 @@ You probably won't need to use it directly in your code. But in case you needed it for an advanced scenario, you can add custom headers: -{* ../../docs_src/handling_errors/tutorial002.py hl[14] *} +{* ../../docs_src/handling_errors/tutorial002_py39.py hl[14] *} ## Install custom exception handlers { #install-custom-exception-handlers } @@ -89,7 +89,7 @@ And you want to handle this exception globally with FastAPI. You could add a custom exception handler with `@app.exception_handler()`: -{* ../../docs_src/handling_errors/tutorial003.py hl[5:7,13:18,24] *} +{* ../../docs_src/handling_errors/tutorial003_py39.py hl[5:7,13:18,24] *} Here, if you request `/unicorns/yolo`, the *path operation* will `raise` a `UnicornException`. @@ -127,7 +127,7 @@ To override it, import the `RequestValidationError` and use it with `@app.except The exception handler will receive a `Request` and the exception. -{* ../../docs_src/handling_errors/tutorial004.py hl[2,14:16] *} +{* ../../docs_src/handling_errors/tutorial004_py39.py hl[2,14:19] *} Now, if you go to `/items/foo`, instead of getting the default JSON error with: @@ -149,36 +149,17 @@ Now, if you go to `/items/foo`, instead of getting the default JSON error with: you will get a text version, with: ``` -1 validation error -path -> item_id - value is not a valid integer (type=type_error.integer) +Validation errors: +Field: ('path', 'item_id'), Error: Input should be a valid integer, unable to parse string as an integer ``` -#### `RequestValidationError` vs `ValidationError` { #requestvalidationerror-vs-validationerror } - -/// warning - -These are technical details that you might skip if it's not important for you now. - -/// - -`RequestValidationError` is a sub-class of Pydantic's `ValidationError`. - -**FastAPI** uses it so that, if you use a Pydantic model in `response_model`, and your data has an error, you will see the error in your log. - -But the client/user will not see it. Instead, the client will receive an "Internal Server Error" with an HTTP status code `500`. - -It should be this way because if you have a Pydantic `ValidationError` in your *response* or anywhere in your code (not in the client's *request*), it's actually a bug in your code. - -And while you fix it, your clients/users shouldn't have access to internal information about the error, as that could expose a security vulnerability. - ### Override the `HTTPException` error handler { #override-the-httpexception-error-handler } The same way, you can override the `HTTPException` handler. For example, you could want to return a plain text response instead of JSON for these errors: -{* ../../docs_src/handling_errors/tutorial004.py hl[3:4,9:11,22] *} +{* ../../docs_src/handling_errors/tutorial004_py39.py hl[3:4,9:11,25] *} /// note | Technical Details @@ -188,13 +169,21 @@ You could also use `from starlette.responses import PlainTextResponse`. /// +/// warning + +Have in mind that the `RequestValidationError` contains the information of the file name and line where the validation error happens so that you can show it in your logs with the relevant information if you want to. + +But that means that if you just convert it to a string and return that information directly, you could be leaking a bit of information about your system, that's why here the code extracts and shows each error independently. + +/// + ### Use the `RequestValidationError` body { #use-the-requestvalidationerror-body } The `RequestValidationError` contains the `body` it received with invalid data. You could use it while developing your app to log the body and debug it, return it to the user, etc. -{* ../../docs_src/handling_errors/tutorial005.py hl[14] *} +{* ../../docs_src/handling_errors/tutorial005_py39.py hl[14] *} Now try sending an invalid item like: @@ -250,6 +239,6 @@ from starlette.exceptions import HTTPException as StarletteHTTPException If you want to use the exception along with the same default exception handlers from **FastAPI**, you can import and reuse the default exception handlers from `fastapi.exception_handlers`: -{* ../../docs_src/handling_errors/tutorial006.py hl[2:5,15,21] *} +{* ../../docs_src/handling_errors/tutorial006_py39.py hl[2:5,15,21] *} In this example you are just printing the error with a very expressive message, but you get the idea. You can use the exception and then just reuse the default exception handlers. diff --git a/docs/en/docs/tutorial/metadata.md b/docs/en/docs/tutorial/metadata.md index 55ef620bfc..941359a154 100644 --- a/docs/en/docs/tutorial/metadata.md +++ b/docs/en/docs/tutorial/metadata.md @@ -18,7 +18,7 @@ You can set the following fields that are used in the OpenAPI specification and You can set them as follows: -{* ../../docs_src/metadata/tutorial001.py hl[3:16, 19:32] *} +{* ../../docs_src/metadata/tutorial001_py39.py hl[3:16, 19:32] *} /// tip @@ -36,7 +36,7 @@ Since OpenAPI 3.1.0 and FastAPI 0.99.0, you can also set the `license_info` with For example: -{* ../../docs_src/metadata/tutorial001_1.py hl[31] *} +{* ../../docs_src/metadata/tutorial001_1_py39.py hl[31] *} ## Metadata for tags { #metadata-for-tags } @@ -58,7 +58,7 @@ Let's try that in an example with tags for `users` and `items`. Create metadata for your tags and pass it to the `openapi_tags` parameter: -{* ../../docs_src/metadata/tutorial004.py hl[3:16,18] *} +{* ../../docs_src/metadata/tutorial004_py39.py hl[3:16,18] *} Notice that you can use Markdown inside of the descriptions, for example "login" will be shown in bold (**login**) and "fancy" will be shown in italics (_fancy_). @@ -72,7 +72,7 @@ You don't have to add metadata for all the tags that you use. Use the `tags` parameter with your *path operations* (and `APIRouter`s) to assign them to different tags: -{* ../../docs_src/metadata/tutorial004.py hl[21,26] *} +{* ../../docs_src/metadata/tutorial004_py39.py hl[21,26] *} /// info @@ -100,7 +100,7 @@ But you can configure it with the parameter `openapi_url`. For example, to set it to be served at `/api/v1/openapi.json`: -{* ../../docs_src/metadata/tutorial002.py hl[3] *} +{* ../../docs_src/metadata/tutorial002_py39.py hl[3] *} If you want to disable the OpenAPI schema completely you can set `openapi_url=None`, that will also disable the documentation user interfaces that use it. @@ -117,4 +117,4 @@ You can configure the two documentation user interfaces included: For example, to set Swagger UI to be served at `/documentation` and disable ReDoc: -{* ../../docs_src/metadata/tutorial003.py hl[3] *} +{* ../../docs_src/metadata/tutorial003_py39.py hl[3] *} diff --git a/docs/en/docs/tutorial/middleware.md b/docs/en/docs/tutorial/middleware.md index d8889fc63e..1e77251c58 100644 --- a/docs/en/docs/tutorial/middleware.md +++ b/docs/en/docs/tutorial/middleware.md @@ -31,7 +31,7 @@ The middleware function receives: * Then it returns the `response` generated by the corresponding *path operation*. * You can then further modify the `response` before returning it. -{* ../../docs_src/middleware/tutorial001.py hl[8:9,11,14] *} +{* ../../docs_src/middleware/tutorial001_py39.py hl[8:9,11,14] *} /// tip @@ -57,7 +57,7 @@ And also after the `response` is generated, before returning it. For example, you could add a custom header `X-Process-Time` containing the time in seconds that it took to process the request and generate a response: -{* ../../docs_src/middleware/tutorial001.py hl[10,12:13] *} +{* ../../docs_src/middleware/tutorial001_py39.py hl[10,12:13] *} /// tip diff --git a/docs/en/docs/tutorial/path-operation-configuration.md b/docs/en/docs/tutorial/path-operation-configuration.md index 522d67288a..59be5ab376 100644 --- a/docs/en/docs/tutorial/path-operation-configuration.md +++ b/docs/en/docs/tutorial/path-operation-configuration.md @@ -46,7 +46,7 @@ In these cases, it could make sense to store the tags in an `Enum`. **FastAPI** supports that the same way as with plain strings: -{* ../../docs_src/path_operation_configuration/tutorial002b.py hl[1,8:10,13,18] *} +{* ../../docs_src/path_operation_configuration/tutorial002b_py39.py hl[1,8:10,13,18] *} ## Summary and description { #summary-and-description } @@ -92,7 +92,7 @@ So, if you don't provide one, **FastAPI** will automatically generate one of "Su If you need to mark a *path operation* as deprecated, but without removing it, pass the parameter `deprecated`: -{* ../../docs_src/path_operation_configuration/tutorial006.py hl[16] *} +{* ../../docs_src/path_operation_configuration/tutorial006_py39.py hl[16] *} It will be clearly marked as deprecated in the interactive docs: diff --git a/docs/en/docs/tutorial/path-params-numeric-validations.md b/docs/en/docs/tutorial/path-params-numeric-validations.md index f7f2d6ceb0..8b1b8a8392 100644 --- a/docs/en/docs/tutorial/path-params-numeric-validations.md +++ b/docs/en/docs/tutorial/path-params-numeric-validations.md @@ -54,7 +54,7 @@ It doesn't matter for **FastAPI**. It will detect the parameters by their names, So, you can declare your function as: -{* ../../docs_src/path_params_numeric_validations/tutorial002.py hl[7] *} +{* ../../docs_src/path_params_numeric_validations/tutorial002_py39.py hl[7] *} But keep in mind that if you use `Annotated`, you won't have this problem, it won't matter as you're not using the function parameter default values for `Query()` or `Path()`. @@ -83,7 +83,7 @@ Pass `*`, as the first parameter of the function. Python won't do anything with that `*`, but it will know that all the following parameters should be called as keyword arguments (key-value pairs), also known as kwargs. Even if they don't have a default value. -{* ../../docs_src/path_params_numeric_validations/tutorial003.py hl[7] *} +{* ../../docs_src/path_params_numeric_validations/tutorial003_py39.py hl[7] *} ### Better with `Annotated` { #better-with-annotated } diff --git a/docs/en/docs/tutorial/path-params.md b/docs/en/docs/tutorial/path-params.md index 457cc2713f..ea4307900c 100644 --- a/docs/en/docs/tutorial/path-params.md +++ b/docs/en/docs/tutorial/path-params.md @@ -2,7 +2,7 @@ You can declare path "parameters" or "variables" with the same syntax used by Python format strings: -{* ../../docs_src/path_params/tutorial001.py hl[6:7] *} +{* ../../docs_src/path_params/tutorial001_py39.py hl[6:7] *} The value of the path parameter `item_id` will be passed to your function as the argument `item_id`. @@ -16,7 +16,7 @@ So, if you run this example and go to Enumerations (or enums) are available in Python since version 3.4. - -/// +{* ../../docs_src/path_params/tutorial005_py39.py hl[1,6:9] *} /// tip @@ -158,7 +152,7 @@ If you are wondering, "AlexNet", "ResNet", and "LeNet" are just names of Machine Then create a *path parameter* with a type annotation using the enum class you created (`ModelName`): -{* ../../docs_src/path_params/tutorial005.py hl[16] *} +{* ../../docs_src/path_params/tutorial005_py39.py hl[16] *} ### Check the docs { #check-the-docs } @@ -174,13 +168,13 @@ The value of the *path parameter* will be an *enumeration member*. You can compare it with the *enumeration member* in your created enum `ModelName`: -{* ../../docs_src/path_params/tutorial005.py hl[17] *} +{* ../../docs_src/path_params/tutorial005_py39.py hl[17] *} #### Get the *enumeration value* { #get-the-enumeration-value } You can get the actual value (a `str` in this case) using `model_name.value`, or in general, `your_enum_member.value`: -{* ../../docs_src/path_params/tutorial005.py hl[20] *} +{* ../../docs_src/path_params/tutorial005_py39.py hl[20] *} /// tip @@ -194,7 +188,7 @@ You can return *enum members* from your *path operation*, even nested in a JSON They will be converted to their corresponding values (strings in this case) before returning them to the client: -{* ../../docs_src/path_params/tutorial005.py hl[18,21,23] *} +{* ../../docs_src/path_params/tutorial005_py39.py hl[18,21,23] *} In your client you will get a JSON response like: @@ -233,7 +227,7 @@ In this case, the name of the parameter is `file_path`, and the last part, `:pat So, you can use it with: -{* ../../docs_src/path_params/tutorial004.py hl[6] *} +{* ../../docs_src/path_params/tutorial004_py39.py hl[6] *} /// tip diff --git a/docs/en/docs/tutorial/query-params-str-validations.md b/docs/en/docs/tutorial/query-params-str-validations.md index adf08a9249..4b8cc9d297 100644 --- a/docs/en/docs/tutorial/query-params-str-validations.md +++ b/docs/en/docs/tutorial/query-params-str-validations.md @@ -55,7 +55,7 @@ q: str | None = None //// -//// tab | Python 3.8+ +//// tab | Python 3.9+ ```Python q: Union[str, None] = None @@ -73,7 +73,7 @@ q: Annotated[str | None] = None //// -//// tab | Python 3.8+ +//// tab | Python 3.9+ ```Python q: Annotated[Union[str, None]] = None @@ -206,20 +206,6 @@ If you feel lost with all these **"regular expression"** ideas, don't worry. The Now you know that whenever you need them you can use them in **FastAPI**. -### Pydantic v1 `regex` instead of `pattern` { #pydantic-v1-regex-instead-of-pattern } - -Before Pydantic version 2 and before FastAPI 0.100.0, the parameter was called `regex` instead of `pattern`, but it's now deprecated. - -You could still see some code using it: - -//// tab | Pydantic v1 - -{* ../../docs_src/query_params_str_validations/tutorial004_regex_an_py310.py hl[11] *} - -//// - -But know that this is deprecated and it should be updated to use the new parameter `pattern`. 🤓 - ## Default values { #default-values } You can, of course, use default values other than `None`. diff --git a/docs/en/docs/tutorial/query-params.md b/docs/en/docs/tutorial/query-params.md index 2323b83c7c..3c9c225fb0 100644 --- a/docs/en/docs/tutorial/query-params.md +++ b/docs/en/docs/tutorial/query-params.md @@ -2,7 +2,7 @@ When you declare other function parameters that are not part of the path parameters, they are automatically interpreted as "query" parameters. -{* ../../docs_src/query_params/tutorial001.py hl[9] *} +{* ../../docs_src/query_params/tutorial001_py39.py hl[9] *} The query is the set of key-value pairs that go after the `?` in a URL, separated by `&` characters. @@ -128,7 +128,7 @@ If you don't want to add a specific value but just make it optional, set the def But when you want to make a query parameter required, you can just not declare any default value: -{* ../../docs_src/query_params/tutorial005.py hl[6:7] *} +{* ../../docs_src/query_params/tutorial005_py39.py hl[6:7] *} Here the query parameter `needy` is a required query parameter of type `str`. diff --git a/docs/en/docs/tutorial/response-model.md b/docs/en/docs/tutorial/response-model.md index 5090dbcdfd..b287cdf50a 100644 --- a/docs/en/docs/tutorial/response-model.md +++ b/docs/en/docs/tutorial/response-model.md @@ -183,7 +183,7 @@ There might be cases where you return something that is not a valid Pydantic fie The most common case would be [returning a Response directly as explained later in the advanced docs](../advanced/response-directly.md){.internal-link target=_blank}. -{* ../../docs_src/response_model/tutorial003_02.py hl[8,10:11] *} +{* ../../docs_src/response_model/tutorial003_02_py39.py hl[8,10:11] *} This simple case is handled automatically by FastAPI because the return type annotation is the class (or a subclass of) `Response`. @@ -193,7 +193,7 @@ And tools will also be happy because both `RedirectResponse` and `JSONResponse` You can also use a subclass of `Response` in the type annotation: -{* ../../docs_src/response_model/tutorial003_03.py hl[8:9] *} +{* ../../docs_src/response_model/tutorial003_03_py39.py hl[8:9] *} This will also work because `RedirectResponse` is a subclass of `Response`, and FastAPI will automatically handle this simple case. @@ -252,20 +252,6 @@ So, if you send a request to that *path operation* for the item with ID `foo`, t /// info -In Pydantic v1 the method was called `.dict()`, it was deprecated (but still supported) in Pydantic v2, and renamed to `.model_dump()`. - -The examples here use `.dict()` for compatibility with Pydantic v1, but you should use `.model_dump()` instead if you can use Pydantic v2. - -/// - -/// info - -FastAPI uses Pydantic model's `.dict()` with its `exclude_unset` parameter to achieve this. - -/// - -/// info - You can also use: * `response_model_exclude_defaults=True` diff --git a/docs/en/docs/tutorial/response-status-code.md b/docs/en/docs/tutorial/response-status-code.md index a2d9757b28..6389592486 100644 --- a/docs/en/docs/tutorial/response-status-code.md +++ b/docs/en/docs/tutorial/response-status-code.md @@ -8,7 +8,7 @@ The same way you can specify a response model, you can also declare the HTTP sta * `@app.delete()` * etc. -{* ../../docs_src/response_status_code/tutorial001.py hl[6] *} +{* ../../docs_src/response_status_code/tutorial001_py39.py hl[6] *} /// note @@ -74,7 +74,7 @@ To know more about each status code and which code is for what, check the Pydantic's docs: Configuration. +You can use the attribute `model_config` that takes a `dict` as described in Pydantic's docs: Configuration. You can set `"json_schema_extra"` with a `dict` containing any additional data you would like to show up in the generated JSON Schema, including `examples`. -//// - -//// tab | Pydantic v1 - -In Pydantic version 1, you would use an internal class `Config` and `schema_extra`, as described in Pydantic's docs: Schema customization. - -You can set `schema_extra` with a `dict` containing any additional data you would like to show up in the generated JSON Schema, including `examples`. - -//// - /// tip You could use the same technique to extend the JSON Schema and add your own custom extra info. diff --git a/docs/en/docs/tutorial/sql-databases.md b/docs/en/docs/tutorial/sql-databases.md index cfa1c9073f..b42e9ba2f2 100644 --- a/docs/en/docs/tutorial/sql-databases.md +++ b/docs/en/docs/tutorial/sql-databases.md @@ -65,7 +65,7 @@ There are a few differences: * `Field(primary_key=True)` tells SQLModel that the `id` is the **primary key** in the SQL database (you can learn more about SQL primary keys in the SQLModel docs). - By having the type as `int | None`, SQLModel will know that this column should be an `INTEGER` in the SQL database and that it should be `NULLABLE`. + **Note:** We use `int | None` for the primary key field so that in Python code we can *create an object without an `id`* (`id=None`), assuming the database will *generate it when saving*. SQLModel understands that the database will provide the `id` and *defines the column as a non-null `INTEGER`* in the database schema. See SQLModel docs on primary keys for details. * `Field(index=True)` tells SQLModel that it should create a **SQL index** for this column, that would allow faster lookups in the database when reading data filtered by this column. diff --git a/docs/en/docs/tutorial/static-files.md b/docs/en/docs/tutorial/static-files.md index 66b934d4f6..8cf5a5a8ae 100644 --- a/docs/en/docs/tutorial/static-files.md +++ b/docs/en/docs/tutorial/static-files.md @@ -7,7 +7,7 @@ You can serve static files automatically from a directory using `StaticFiles`. * Import `StaticFiles`. * "Mount" a `StaticFiles()` instance in a specific path. -{* ../../docs_src/static_files/tutorial001.py hl[2,6] *} +{* ../../docs_src/static_files/tutorial001_py39.py hl[2,6] *} /// note | Technical Details diff --git a/docs/en/docs/tutorial/testing.md b/docs/en/docs/tutorial/testing.md index 3dcf5dc4ac..f61e62ac54 100644 --- a/docs/en/docs/tutorial/testing.md +++ b/docs/en/docs/tutorial/testing.md @@ -30,7 +30,7 @@ Use the `TestClient` object the same way as you do with `httpx`. Write simple `assert` statements with the standard Python expressions that you need to check (again, standard `pytest`). -{* ../../docs_src/app_testing/tutorial001.py hl[2,12,15:18] *} +{* ../../docs_src/app_testing/tutorial001_py39.py hl[2,12,15:18] *} /// tip @@ -76,7 +76,7 @@ Let's say you have a file structure as described in [Bigger Applications](bigger In the file `main.py` you have your **FastAPI** app: -{* ../../docs_src/app_testing/main.py *} +{* ../../docs_src/app_testing/app_a_py39/main.py *} ### Testing file { #testing-file } @@ -92,7 +92,7 @@ Then you could have a file `test_main.py` with your tests. It could live on the Because this file is in the same package, you can use relative imports to import the object `app` from the `main` module (`main.py`): -{* ../../docs_src/app_testing/test_main.py hl[3] *} +{* ../../docs_src/app_testing/app_a_py39/test_main.py hl[3] *} ...and have the code for the tests just like before. @@ -121,63 +121,13 @@ It has a `POST` operation that could return several errors. Both *path operations* require an `X-Token` header. -//// tab | Python 3.10+ - -```Python -{!> ../../docs_src/app_testing/app_b_an_py310/main.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!> ../../docs_src/app_testing/app_b_an_py39/main.py!} -``` - -//// - -//// tab | Python 3.8+ - -```Python -{!> ../../docs_src/app_testing/app_b_an/main.py!} -``` - -//// - -//// tab | Python 3.10+ non-Annotated - -/// tip - -Prefer to use the `Annotated` version if possible. - -/// - -```Python -{!> ../../docs_src/app_testing/app_b_py310/main.py!} -``` - -//// - -//// tab | Python 3.8+ non-Annotated - -/// tip - -Prefer to use the `Annotated` version if possible. - -/// - -```Python -{!> ../../docs_src/app_testing/app_b/main.py!} -``` - -//// +{* ../../docs_src/app_testing/app_b_an_py310/main.py *} ### Extended testing file { #extended-testing-file } You could then update `test_main.py` with the extended tests: -{* ../../docs_src/app_testing/app_b/test_main.py *} +{* ../../docs_src/app_testing/app_b_an_py310/test_main.py *} Whenever you need the client to pass information in the request and you don't know how to, you can search (Google) how to do it in `httpx`, or even how to do it with `requests`, as HTTPX's design is based on Requests' design. diff --git a/docs/en/docs/virtual-environments.md b/docs/en/docs/virtual-environments.md index e9b0a9fc45..c02e43ab91 100644 --- a/docs/en/docs/virtual-environments.md +++ b/docs/en/docs/virtual-environments.md @@ -242,6 +242,26 @@ $ python -m pip install --upgrade pip
+/// tip + +Sometimes, you might get a **`No module named pip`** error when trying to upgrade pip. + +If this happens, install and upgrade pip using the command below: + +
+ +```console +$ python -m ensurepip --upgrade + +---> 100% +``` + +
+ +This command will install pip if it is not already installed and also ensures that the installed version of pip is at least as recent as the one available in `ensurepip`. + +/// + ## Add `.gitignore` { #add-gitignore } If you are using **Git** (you should), add a `.gitignore` file to exclude everything in your `.venv` from Git. diff --git a/docs/en/mkdocs.maybe-insiders.yml b/docs/en/mkdocs.env.yml similarity index 78% rename from docs/en/mkdocs.maybe-insiders.yml rename to docs/en/mkdocs.env.yml index 37fd9338ef..c5f6e07d79 100644 --- a/docs/en/mkdocs.maybe-insiders.yml +++ b/docs/en/mkdocs.env.yml @@ -1,6 +1,5 @@ # Define this here and not in the main mkdocs.yml file because that one is auto # updated and written, and the script would remove the env var -INHERIT: !ENV [INSIDERS_FILE, '../en/mkdocs.no-insiders.yml'] markdown_extensions: pymdownx.highlight: linenums: !ENV [LINENUMS, false] diff --git a/docs/en/mkdocs.insiders.yml b/docs/en/mkdocs.insiders.yml deleted file mode 100644 index 8d6d26e17e..0000000000 --- a/docs/en/mkdocs.insiders.yml +++ /dev/null @@ -1,10 +0,0 @@ -plugins: - social: - cards_layout_options: - logo: ../en/docs/img/icon-white.svg - typeset: -markdown_extensions: - material.extensions.preview: - targets: - include: - - "*" diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index 323035240a..1c29546198 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -1,4 +1,4 @@ -INHERIT: ../en/mkdocs.maybe-insiders.yml +INHERIT: ../en/mkdocs.env.yml site_name: FastAPI site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production site_url: https://fastapi.tiangolo.com/ @@ -52,10 +52,13 @@ theme: repo_name: fastapi/fastapi repo_url: https://github.com/fastapi/fastapi plugins: + social: + cards_layout_options: + logo: ../en/docs/img/icon-white.svg + typeset: null search: null macros: include_yaml: - - external_links: ../en/data/external_links.yml - github_sponsors: ../en/data/github_sponsors.yml - people: ../en/data/people.yml - contributors: ../en/data/contributors.yml @@ -192,6 +195,7 @@ nav: - Deployment: - deployment/index.md - deployment/versions.md + - deployment/fastapicloud.md - deployment/https.md - deployment/manually.md - deployment/concepts.md @@ -210,6 +214,7 @@ nav: - how-to/custom-docs-ui-assets.md - how-to/configure-swagger-ui.md - how-to/testing-database.md + - how-to/authentication-error-status-code.md - Reference (Code API): - reference/index.md - reference/fastapi.md @@ -252,6 +257,10 @@ nav: - management.md - release-notes.md markdown_extensions: + material.extensions.preview: + targets: + include: + - '*' abbr: null attr_list: null footnotes: null @@ -308,30 +317,14 @@ extra: name: de - Deutsch - link: /es/ name: es - español - - link: /fa/ - name: fa - فارسی - - link: /fr/ - name: fr - français - - link: /ja/ - name: ja - 日本語 - link: /ko/ name: ko - 한국어 - link: /pt/ name: pt - português - link: /ru/ name: ru - русский язык - - link: /tr/ - name: tr - Türkçe - link: /uk/ name: uk - українська мова - - link: /vi/ - name: vi - Tiếng Việt - - link: /zh/ - name: zh - 简体中文 - - link: /zh-hant/ - name: zh-hant - 繁體中文 - - link: /em/ - name: 😉 extra_css: - css/termynal.css - css/custom.css diff --git a/docs/en/overrides/main.html b/docs/en/overrides/main.html index be31bd75c3..a37ebf0a4c 100644 --- a/docs/en/overrides/main.html +++ b/docs/en/overrides/main.html @@ -3,6 +3,13 @@ {% block announce %} {% endblock %} diff --git a/docs/es/docs/_llm-test.md b/docs/es/docs/_llm-test.md new file mode 100644 index 0000000000..591b1008bd --- /dev/null +++ b/docs/es/docs/_llm-test.md @@ -0,0 +1,503 @@ +# Archivo de prueba de LLM { #llm-test-file } + +Este documento prueba si el LLM, que traduce la documentación, entiende el `general_prompt` en `scripts/translate.py` y el prompt específico del idioma en `docs/{language code}/llm-prompt.md`. El prompt específico del idioma se agrega al final de `general_prompt`. + +Las pruebas añadidas aquí serán vistas por todas las personas que diseñan prompts específicos del idioma. + +Úsalo de la siguiente manera: + +* Ten un prompt específico del idioma - `docs/{language code}/llm-prompt.md`. +* Haz una traducción fresca de este documento a tu idioma destino (mira, por ejemplo, el comando `translate-page` de `translate.py`). Esto creará la traducción en `docs/{language code}/docs/_llm-test.md`. +* Revisa si las cosas están bien en la traducción. +* Si es necesario, mejora tu prompt específico del idioma, el prompt general, o el documento en inglés. +* Luego corrige manualmente los problemas restantes en la traducción para que sea una buena traducción. +* Vuelve a traducir, teniendo la buena traducción en su lugar. El resultado ideal sería que el LLM ya no hiciera cambios a la traducción. Eso significa que el prompt general y tu prompt específico del idioma están tan bien como pueden estar (A veces hará algunos cambios aparentemente aleatorios; la razón es que los LLMs no son algoritmos deterministas). + +Las pruebas: + +## Fragmentos de código { #code-snippets } + +//// tab | Prueba + +Este es un fragmento de código: `foo`. Y este es otro fragmento de código: `bar`. Y otro más: `baz quux`. + +//// + +//// tab | Info + +El contenido de los fragmentos de código debe dejarse tal cual. + +Consulta la sección `### Content of code snippets` en el prompt general en `scripts/translate.py`. + +//// + +## Comillas { #quotes } + +//// tab | Prueba + +Ayer, mi amigo escribió: "Si escribes 'incorrectly' correctamente, lo habrás escrito incorrectamente". A lo que respondí: "Correcto, pero 'incorrectly' está incorrecto, no '"incorrectly"'". + +/// note | Nota + +El LLM probablemente traducirá esto mal. Lo interesante es si mantiene la traducción corregida al volver a traducir. + +/// + +//// + +//// tab | Info + +La persona que diseña el prompt puede elegir si quiere convertir comillas neutras a comillas tipográficas. También está bien dejarlas como están. + +Consulta por ejemplo la sección `### Quotes` en `docs/de/llm-prompt.md`. + +//// + +## Comillas en fragmentos de código { #quotes-in-code-snippets } + +//// tab | Prueba + +`pip install "foo[bar]"` + +Ejemplos de literales de string en fragmentos de código: `"this"`, `'that'`. + +Un ejemplo difícil de literales de string en fragmentos de código: `f"I like {'oranges' if orange else "apples"}"` + +Hardcore: `Yesterday, my friend wrote: "If you spell incorrectly correctly, you have spelled it incorrectly". To which I answered: "Correct, but 'incorrectly' is incorrectly not '"incorrectly"'"` + +//// + +//// tab | Info + +... Sin embargo, las comillas dentro de fragmentos de código deben quedarse tal cual. + +//// + +## bloques de código { #code-blocks } + +//// tab | Prueba + +Un ejemplo de código Bash... + +```bash +# Imprime un saludo al universo +echo "Hello universe" +``` + +...y un ejemplo de código de consola... + +```console +$ fastapi run main.py + FastAPI Starting server + Searching for package file structure +``` + +...y otro ejemplo de código de consola... + +```console +// Crea un directorio "Code" +$ mkdir code +// Cambia a ese directorio +$ cd code +``` + +...y un ejemplo de código Python... + +```Python +wont_work() # Esto no va a funcionar 😱 +works(foo="bar") # Esto funciona 🎉 +``` + +...y eso es todo. + +//// + +//// tab | Info + +El código en bloques de código no debe modificarse, con la excepción de los comentarios. + +Consulta la sección `### Content of code blocks` en el prompt general en `scripts/translate.py`. + +//// + +## Pestañas y cajas coloreadas { #tabs-and-colored-boxes } + +//// tab | Prueba + +/// info | Información +Algo de texto +/// + +/// note | Nota +Algo de texto +/// + +/// note | Detalles técnicos +Algo de texto +/// + +/// check | Revisa +Algo de texto +/// + +/// tip | Consejo +Algo de texto +/// + +/// warning | Advertencia +Algo de texto +/// + +/// danger | Peligro +Algo de texto +/// + +//// + +//// tab | Info + +Las pestañas y los bloques `Info`/`Note`/`Warning`/etc. deben tener la traducción de su título añadida después de una barra vertical (`|`). + +Consulta las secciones `### Special blocks` y `### Tab blocks` en el prompt general en `scripts/translate.py`. + +//// + +## Enlaces web e internos { #web-and-internal-links } + +//// tab | Prueba + +El texto del enlace debe traducirse, la dirección del enlace debe permanecer sin cambios: + +* [Enlace al encabezado de arriba](#code-snippets) +* [Enlace interno](index.md#installation){.internal-link target=_blank} +* Enlace externo +* Enlace a un estilo +* Enlace a un script +* Enlace a una imagen + +El texto del enlace debe traducirse, la dirección del enlace debe apuntar a la traducción: + +* Enlace a FastAPI + +//// + +//// tab | Info + +Los enlaces deben traducirse, pero su dirección debe permanecer sin cambios. Una excepción son los enlaces absolutos a páginas de la documentación de FastAPI. En ese caso deben enlazar a la traducción. + +Consulta la sección `### Links` en el prompt general en `scripts/translate.py`. + +//// + +## Elementos HTML "abbr" { #html-abbr-elements } + +//// tab | Prueba + +Aquí algunas cosas envueltas en elementos HTML "abbr" (algunas son inventadas): + +### El abbr da una frase completa { #the-abbr-gives-a-full-phrase } + +* GTD +* lt +* XWT +* PSGI + +### El abbr da una explicación { #the-abbr-gives-an-explanation } + +* clúster +* Deep Learning + +### El abbr da una frase completa y una explicación { #the-abbr-gives-a-full-phrase-and-an-explanation } + +* MDN +* I/O. + +//// + +//// tab | Info + +Los atributos "title" de los elementos "abbr" se traducen siguiendo instrucciones específicas. + +Las traducciones pueden añadir sus propios elementos "abbr" que el LLM no debe eliminar. P. ej., para explicar palabras en inglés. + +Consulta la sección `### HTML abbr elements` en el prompt general en `scripts/translate.py`. + +//// + +## Encabezados { #headings } + +//// tab | Prueba + +### Desarrolla una webapp - un tutorial { #develop-a-webapp-a-tutorial } + +Hola. + +### Anotaciones de tipos y -anotaciones { #type-hints-and-annotations } + +Hola de nuevo. + +### Superclases y subclases { #super-and-subclasses } + +Hola de nuevo. + +//// + +//// tab | Info + +La única regla estricta para los encabezados es que el LLM deje la parte del hash dentro de llaves sin cambios, lo que asegura que los enlaces no se rompan. + +Consulta la sección `### Headings` en el prompt general en `scripts/translate.py`. + +Para instrucciones específicas del idioma, mira p. ej. la sección `### Headings` en `docs/de/llm-prompt.md`. + +//// + +## Términos usados en la documentación { #terms-used-in-the-docs } + +//// tab | Prueba + +* tú +* tu + +* p. ej. +* etc. + +* `foo` como un `int` +* `bar` como un `str` +* `baz` como una `list` + +* el Tutorial - Guía de usuario +* la Guía de usuario avanzada +* la documentación de SQLModel +* la documentación de la API +* la documentación automática + +* Ciencia de datos +* Deep Learning +* Machine Learning +* Inyección de dependencias +* autenticación HTTP Basic +* HTTP Digest +* formato ISO +* el estándar JSON Schema +* el JSON Schema +* la definición del esquema +* Flujo de contraseña +* Móvil + +* obsoleto +* diseñado +* inválido +* sobre la marcha +* estándar +* por defecto +* sensible a mayúsculas/minúsculas +* insensible a mayúsculas/minúsculas + +* servir la aplicación +* servir la página + +* la app +* la aplicación + +* la request +* la response +* la response de error + +* la path operation +* el decorador de path operation +* la path operation function + +* el body +* el request body +* el response body +* el body JSON +* el body del formulario +* el body de archivo +* el cuerpo de la función + +* el parámetro +* el parámetro del body +* el parámetro del path +* el parámetro de query +* el parámetro de cookie +* el parámetro de header +* el parámetro del formulario +* el parámetro de la función + +* el evento +* el evento de inicio +* el inicio del servidor +* el evento de apagado +* el evento de lifespan + +* el manejador +* el manejador de eventos +* el manejador de excepciones +* manejar + +* el modelo +* el modelo de Pydantic +* el modelo de datos +* el modelo de base de datos +* el modelo de formulario +* el objeto del modelo + +* la clase +* la clase base +* la clase padre +* la subclase +* la clase hija +* la clase hermana +* el método de clase + +* el header +* los headers +* el header de autorización +* el header `Authorization` +* el header forwarded + +* el sistema de inyección de dependencias +* la dependencia +* el dependable +* el dependiente + +* limitado por I/O +* limitado por CPU +* concurrencia +* paralelismo +* multiprocesamiento + +* la env var +* la variable de entorno +* el `PATH` +* la variable `PATH` + +* la autenticación +* el proveedor de autenticación +* la autorización +* el formulario de autorización +* el proveedor de autorización +* el usuario se autentica +* el sistema autentica al usuario + +* la CLI +* la interfaz de línea de comandos + +* el servidor +* el cliente + +* el proveedor en la nube +* el servicio en la nube + +* el desarrollo +* las etapas de desarrollo + +* el dict +* el diccionario +* la enumeración +* el enum +* el miembro del enum + +* el codificador +* el decodificador +* codificar +* decodificar + +* la excepción +* lanzar + +* la expresión +* el statement + +* el frontend +* el backend + +* la discusión de GitHub +* el issue de GitHub + +* el rendimiento +* la optimización de rendimiento + +* el tipo de retorno +* el valor de retorno + +* la seguridad +* el esquema de seguridad + +* la tarea +* la tarea en segundo plano +* la función de tarea + +* la plantilla +* el motor de plantillas + +* la anotación de tipos +* las anotaciones de tipos + +* el worker del servidor +* el worker de Uvicorn +* el Gunicorn Worker +* el worker process +* la worker class +* la carga de trabajo + +* el despliegue +* desplegar + +* el SDK +* el kit de desarrollo de software + +* el `APIRouter` +* el `requirements.txt` +* el Bearer Token +* el cambio incompatible +* el bug +* el botón +* el invocable +* el código +* el commit +* el context manager +* la corrutina +* la sesión de base de datos +* el disco +* el dominio +* el motor +* el X falso +* el método HTTP GET +* el ítem +* el paquete +* el lifespan +* el lock +* el middleware +* la aplicación móvil +* el módulo +* el montaje +* la red +* el origen +* el override +* el payload +* el procesador +* la propiedad +* el proxy +* el pull request +* la query +* la RAM +* la máquina remota +* el código de estado +* el string +* la etiqueta +* el framework web +* el comodín +* devolver +* validar + +//// + +//// tab | Info + +Esta es una lista no completa y no normativa de términos (mayormente) técnicos vistos en la documentación. Puede ayudar a la persona que diseña el prompt a identificar para qué términos el LLM necesita una mano. Por ejemplo cuando sigue revirtiendo una buena traducción a una traducción subóptima. O cuando tiene problemas conjugando/declinando un término en tu idioma. + +Mira p. ej. la sección `### List of English terms and their preferred German translations` en `docs/de/llm-prompt.md`. + +//// diff --git a/docs/es/docs/about/index.md b/docs/es/docs/about/index.md index e83400a8dc..fa152c62db 100644 --- a/docs/es/docs/about/index.md +++ b/docs/es/docs/about/index.md @@ -1,3 +1,3 @@ -# Acerca de +# Acerca de { #about } Acerca de FastAPI, su diseño, inspiración y más. 🤓 diff --git a/docs/es/docs/advanced/additional-responses.md b/docs/es/docs/advanced/additional-responses.md index 7788bccd97..d0baa97a45 100644 --- a/docs/es/docs/advanced/additional-responses.md +++ b/docs/es/docs/advanced/additional-responses.md @@ -1,4 +1,4 @@ -# Responses Adicionales en OpenAPI +# Responses Adicionales en OpenAPI { #additional-responses-in-openapi } /// warning | Advertencia @@ -14,7 +14,7 @@ Esos responses adicionales se incluirán en el esquema de OpenAPI, por lo que ta Pero para esos responses adicionales tienes que asegurarte de devolver un `Response` como `JSONResponse` directamente, con tu código de estado y contenido. -## Response Adicional con `model` +## Response Adicional con `model` { #additional-response-with-model } Puedes pasar a tus *decoradores de path operation* un parámetro `responses`. @@ -26,7 +26,7 @@ Cada uno de esos `dict`s de response puede tener una clave `model`, conteniendo Por ejemplo, para declarar otro response con un código de estado `404` y un modelo Pydantic `Message`, puedes escribir: -{* ../../docs_src/additional_responses/tutorial001.py hl[18,22] *} +{* ../../docs_src/additional_responses/tutorial001_py39.py hl[18,22] *} /// note | Nota @@ -169,13 +169,13 @@ Los esquemas se referencian a otro lugar dentro del esquema de OpenAPI: } ``` -## Media types adicionales para el response principal +## Media types adicionales para el response principal { #additional-media-types-for-the-main-response } Puedes usar este mismo parámetro `responses` para agregar diferentes media type para el mismo response principal. Por ejemplo, puedes agregar un media type adicional de `image/png`, declarando que tu *path operation* puede devolver un objeto JSON (con media type `application/json`) o una imagen PNG: -{* ../../docs_src/additional_responses/tutorial002.py hl[19:24,28] *} +{* ../../docs_src/additional_responses/tutorial002_py310.py hl[17:22,26] *} /// note | Nota @@ -191,25 +191,25 @@ Pero si has especificado una clase de response personalizada con `None` como su /// -## Combinando información +## Combinando información { #combining-information } También puedes combinar información de response de múltiples lugares, incluyendo los parámetros `response_model`, `status_code`, y `responses`. -Puedes declarar un `response_model`, usando el código de estado predeterminado `200` (o uno personalizado si lo necesitas), y luego declarar información adicional para ese mismo response en `responses`, directamente en el esquema de OpenAPI. +Puedes declarar un `response_model`, usando el código de estado por defecto `200` (o uno personalizado si lo necesitas), y luego declarar información adicional para ese mismo response en `responses`, directamente en el esquema de OpenAPI. -**FastAPI** manterá la información adicional de `responses` y la combinará con el JSON Schema de tu modelo. +**FastAPI** mantendrá la información adicional de `responses` y la combinará con el JSON Schema de tu modelo. Por ejemplo, puedes declarar un response con un código de estado `404` que usa un modelo Pydantic y tiene una `description` personalizada. Y un response con un código de estado `200` que usa tu `response_model`, pero incluye un `example` personalizado: -{* ../../docs_src/additional_responses/tutorial003.py hl[20:31] *} +{* ../../docs_src/additional_responses/tutorial003_py39.py hl[20:31] *} Todo se combinará e incluirá en tu OpenAPI, y se mostrará en la documentación de la API: -## Combina responses predefinidos y personalizados +## Combina responses predefinidos y personalizados { #combine-predefined-responses-and-custom-ones } Es posible que desees tener algunos responses predefinidos que se apliquen a muchas *path operations*, pero que quieras combinarlos con responses personalizados necesarios por cada *path operation*. @@ -237,9 +237,9 @@ Puedes usar esa técnica para reutilizar algunos responses predefinidos en tus * Por ejemplo: -{* ../../docs_src/additional_responses/tutorial004.py hl[13:17,26] *} +{* ../../docs_src/additional_responses/tutorial004_py310.py hl[11:15,24] *} -## Más información sobre responses OpenAPI +## Más información sobre responses OpenAPI { #more-information-about-openapi-responses } Para ver exactamente qué puedes incluir en los responses, puedes revisar estas secciones en la especificación OpenAPI: diff --git a/docs/es/docs/advanced/additional-status-codes.md b/docs/es/docs/advanced/additional-status-codes.md index df7737aacc..9adfa65cf3 100644 --- a/docs/es/docs/advanced/additional-status-codes.md +++ b/docs/es/docs/advanced/additional-status-codes.md @@ -1,10 +1,10 @@ -# Códigos de Estado Adicionales +# Códigos de Estado Adicionales { #additional-status-codes } Por defecto, **FastAPI** devolverá los responses usando un `JSONResponse`, colocando el contenido que devuelves desde tu *path operation* dentro de ese `JSONResponse`. Usará el código de estado por defecto o el que configures en tu *path operation*. -## Códigos de estado adicionales +## Códigos de estado adicionales { #additional-status-codes_1 } Si quieres devolver códigos de estado adicionales aparte del principal, puedes hacerlo devolviendo un `Response` directamente, como un `JSONResponse`, y configurando el código de estado adicional directamente. @@ -34,7 +34,7 @@ También podrías usar `from starlette.responses import JSONResponse`. /// -## OpenAPI y documentación de API +## OpenAPI y documentación de API { #openapi-and-api-docs } Si devuelves códigos de estado adicionales y responses directamente, no se incluirán en el esquema de OpenAPI (la documentación de la API), porque FastAPI no tiene una forma de saber de antemano qué vas a devolver. diff --git a/docs/es/docs/advanced/advanced-dependencies.md b/docs/es/docs/advanced/advanced-dependencies.md index dd3c63a37b..622a2caa26 100644 --- a/docs/es/docs/advanced/advanced-dependencies.md +++ b/docs/es/docs/advanced/advanced-dependencies.md @@ -1,6 +1,6 @@ -# Dependencias Avanzadas +# Dependencias Avanzadas { #advanced-dependencies } -## Dependencias con parámetros +## Dependencias con parámetros { #parameterized-dependencies } Todas las dependencias que hemos visto son una función o clase fija. @@ -10,7 +10,7 @@ Imaginemos que queremos tener una dependencia que revise si el parámetro de que Pero queremos poder parametrizar ese contenido fijo. -## Una *instance* "callable" +## Una *instance* "callable" { #a-callable-instance } En Python hay una forma de hacer que una instance de una clase sea un "callable". @@ -22,7 +22,7 @@ Para hacer eso, declaramos un método `__call__`: En este caso, este `__call__` es lo que **FastAPI** usará para comprobar parámetros adicionales y sub-dependencias, y es lo que llamará para pasar un valor al parámetro en tu *path operation function* más adelante. -## Parametrizar la instance +## Parametrizar la instance { #parameterize-the-instance } Y ahora, podemos usar `__init__` para declarar los parámetros de la instance que podemos usar para "parametrizar" la dependencia: @@ -30,7 +30,7 @@ Y ahora, podemos usar `__init__` para declarar los parámetros de la instance qu En este caso, **FastAPI** nunca tocará ni se preocupará por `__init__`, lo usaremos directamente en nuestro código. -## Crear una instance +## Crear una instance { #create-an-instance } Podríamos crear una instance de esta clase con: @@ -38,7 +38,7 @@ Podríamos crear una instance de esta clase con: Y de esa manera podemos "parametrizar" nuestra dependencia, que ahora tiene `"bar"` dentro de ella, como el atributo `checker.fixed_content`. -## Usar la instance como una dependencia +## Usar la instance como una dependencia { #use-the-instance-as-a-dependency } Luego, podríamos usar este `checker` en un `Depends(checker)`, en lugar de `Depends(FixedContentQueryChecker)`, porque la dependencia es la instance, `checker`, no la clase en sí. @@ -63,3 +63,101 @@ En los capítulos sobre seguridad, hay funciones utilitarias que se implementan Si entendiste todo esto, ya sabes cómo funcionan por debajo esas herramientas de utilidad para seguridad. /// + +## Dependencias con `yield`, `HTTPException`, `except` y Tareas en segundo plano { #dependencies-with-yield-httpexception-except-and-background-tasks } + +/// warning | Advertencia + +Muy probablemente no necesites estos detalles técnicos. + +Estos detalles son útiles principalmente si tenías una aplicación de FastAPI anterior a la 0.121.0 y estás enfrentando problemas con dependencias con `yield`. + +/// + +Las dependencias con `yield` han evolucionado con el tiempo para cubrir diferentes casos de uso y arreglar algunos problemas; aquí tienes un resumen de lo que ha cambiado. + +### Dependencias con `yield` y `scope` { #dependencies-with-yield-and-scope } + +En la versión 0.121.0, FastAPI agregó soporte para `Depends(scope="function")` para dependencias con `yield`. + +Usando `Depends(scope="function")`, el código de salida después de `yield` se ejecuta justo después de que la *path operation function* termina, antes de que la response se envíe de vuelta al cliente. + +Y al usar `Depends(scope="request")` (el valor por defecto), el código de salida después de `yield` se ejecuta después de que la response es enviada. + +Puedes leer más al respecto en la documentación de [Dependencias con `yield` - Salida temprana y `scope`](../tutorial/dependencies/dependencies-with-yield.md#early-exit-and-scope). + +### Dependencias con `yield` y `StreamingResponse`, detalles técnicos { #dependencies-with-yield-and-streamingresponse-technical-details } + +Antes de FastAPI 0.118.0, si usabas una dependencia con `yield`, ejecutaba el código de salida después de que la *path operation function* retornaba pero justo antes de enviar la response. + +La intención era evitar retener recursos por más tiempo del necesario, esperando a que la response viajara por la red. + +Este cambio también significaba que si retornabas un `StreamingResponse`, el código de salida de la dependencia con `yield` ya se habría ejecutado. + +Por ejemplo, si tenías una sesión de base de datos en una dependencia con `yield`, el `StreamingResponse` no podría usar esa sesión mientras hace streaming de datos porque la sesión ya se habría cerrado en el código de salida después de `yield`. + +Este comportamiento se revirtió en la 0.118.0, para hacer que el código de salida después de `yield` se ejecute después de que la response sea enviada. + +/// info | Información + +Como verás abajo, esto es muy similar al comportamiento anterior a la versión 0.106.0, pero con varias mejoras y arreglos de bugs para casos límite. + +/// + +#### Casos de uso con salida temprana del código { #use-cases-with-early-exit-code } + +Hay algunos casos de uso con condiciones específicas que podrían beneficiarse del comportamiento antiguo de ejecutar el código de salida de dependencias con `yield` antes de enviar la response. + +Por ejemplo, imagina que tienes código que usa una sesión de base de datos en una dependencia con `yield` solo para verificar un usuario, pero la sesión de base de datos no se vuelve a usar en la *path operation function*, solo en la dependencia, y la response tarda mucho en enviarse, como un `StreamingResponse` que envía datos lentamente, pero que por alguna razón no usa la base de datos. + +En este caso, la sesión de base de datos se mantendría hasta que la response termine de enviarse, pero si no la usas, entonces no sería necesario mantenerla. + +Así es como se vería: + +{* ../../docs_src/dependencies/tutorial013_an_py310.py *} + +El código de salida, el cierre automático de la `Session` en: + +{* ../../docs_src/dependencies/tutorial013_an_py310.py ln[19:21] *} + +...se ejecutaría después de que la response termine de enviar los datos lentos: + +{* ../../docs_src/dependencies/tutorial013_an_py310.py ln[30:38] hl[31:33] *} + +Pero como `generate_stream()` no usa la sesión de base de datos, no es realmente necesario mantener la sesión abierta mientras se envía la response. + +Si tienes este caso de uso específico usando SQLModel (o SQLAlchemy), podrías cerrar explícitamente la sesión después de que ya no la necesites: + +{* ../../docs_src/dependencies/tutorial014_an_py310.py ln[24:28] hl[28] *} + +De esa manera la sesión liberaría la conexión a la base de datos, para que otras requests puedan usarla. + +Si tienes un caso de uso diferente que necesite salir temprano desde una dependencia con `yield`, por favor crea una Pregunta de Discusión en GitHub con tu caso de uso específico y por qué te beneficiaría tener cierre temprano para dependencias con `yield`. + +Si hay casos de uso convincentes para el cierre temprano en dependencias con `yield`, consideraría agregar una nueva forma de optar por el cierre temprano. + +### Dependencias con `yield` y `except`, detalles técnicos { #dependencies-with-yield-and-except-technical-details } + +Antes de FastAPI 0.110.0, si usabas una dependencia con `yield`, y luego capturabas una excepción con `except` en esa dependencia, y no volvías a elevar la excepción, la excepción se elevaría/remitiría automáticamente a cualquier manejador de excepciones o al manejador de error interno del servidor. + +Esto cambió en la versión 0.110.0 para arreglar consumo de memoria no manejado por excepciones reenviadas sin un manejador (errores internos del servidor), y para hacerlo consistente con el comportamiento del código Python normal. + +### Tareas en segundo plano y dependencias con `yield`, detalles técnicos { #background-tasks-and-dependencies-with-yield-technical-details } + +Antes de FastAPI 0.106.0, elevar excepciones después de `yield` no era posible, el código de salida en dependencias con `yield` se ejecutaba después de que la response era enviada, por lo que [Manejadores de Excepciones](../tutorial/handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank} ya habrían corrido. + +Esto se diseñó así principalmente para permitir usar los mismos objetos devueltos con `yield` por las dependencias dentro de tareas en segundo plano, porque el código de salida se ejecutaría después de que las tareas en segundo plano terminaran. + +Esto cambió en FastAPI 0.106.0 con la intención de no retener recursos mientras se espera a que la response viaje por la red. + +/// tip | Consejo + +Adicionalmente, una tarea en segundo plano normalmente es un conjunto independiente de lógica que debería manejarse por separado, con sus propios recursos (por ejemplo, su propia conexión a la base de datos). + +Así, probablemente tendrás un código más limpio. + +/// + +Si solías depender de este comportamiento, ahora deberías crear los recursos para las tareas en segundo plano dentro de la propia tarea en segundo plano, y usar internamente solo datos que no dependan de los recursos de dependencias con `yield`. + +Por ejemplo, en lugar de usar la misma sesión de base de datos, crearías una nueva sesión de base de datos dentro de la tarea en segundo plano, y obtendrías los objetos de la base de datos usando esta nueva sesión. Y entonces, en lugar de pasar el objeto de la base de datos como parámetro a la función de la tarea en segundo plano, pasarías el ID de ese objeto y luego obtendrías el objeto de nuevo dentro de la función de la tarea en segundo plano. diff --git a/docs/es/docs/advanced/async-tests.md b/docs/es/docs/advanced/async-tests.md index f89db15335..4627e9bd18 100644 --- a/docs/es/docs/advanced/async-tests.md +++ b/docs/es/docs/advanced/async-tests.md @@ -1,4 +1,4 @@ -# Tests Asíncronos +# Tests Asíncronos { #async-tests } Ya has visto cómo probar tus aplicaciones de **FastAPI** usando el `TestClient` proporcionado. Hasta ahora, solo has visto cómo escribir tests sincrónicos, sin usar funciones `async`. @@ -6,11 +6,11 @@ Poder usar funciones asíncronas en tus tests puede ser útil, por ejemplo, cuan Veamos cómo podemos hacer que esto funcione. -## pytest.mark.anyio +## pytest.mark.anyio { #pytest-mark-anyio } Si queremos llamar funciones asíncronas en nuestros tests, nuestras funciones de test tienen que ser asíncronas. AnyIO proporciona un plugin útil para esto, que nos permite especificar que algunas funciones de test deben ser llamadas de manera asíncrona. -## HTTPX +## HTTPX { #httpx } Incluso si tu aplicación de **FastAPI** usa funciones `def` normales en lugar de `async def`, sigue siendo una aplicación `async` por debajo. @@ -18,7 +18,7 @@ El `TestClient` hace algo de magia interna para llamar a la aplicación FastAPI El `TestClient` está basado en HTTPX, y afortunadamente, podemos usarlo directamente para probar la API. -## Ejemplo +## Ejemplo { #example } Para un ejemplo simple, consideremos una estructura de archivos similar a la descrita en [Aplicaciones Más Grandes](../tutorial/bigger-applications.md){.internal-link target=_blank} y [Testing](../tutorial/testing.md){.internal-link target=_blank}: @@ -32,13 +32,13 @@ Para un ejemplo simple, consideremos una estructura de archivos similar a la des El archivo `main.py` tendría: -{* ../../docs_src/async_tests/main.py *} +{* ../../docs_src/async_tests/app_a_py39/main.py *} El archivo `test_main.py` tendría los tests para `main.py`, podría verse así ahora: -{* ../../docs_src/async_tests/test_main.py *} +{* ../../docs_src/async_tests/app_a_py39/test_main.py *} -## Ejecútalo +## Ejecútalo { #run-it } Puedes ejecutar tus tests como de costumbre vía: @@ -52,21 +52,21 @@ $ pytest -## En Detalle +## En Detalle { #in-detail } El marcador `@pytest.mark.anyio` le dice a pytest que esta función de test debe ser llamada asíncronamente: -{* ../../docs_src/async_tests/test_main.py hl[7] *} +{* ../../docs_src/async_tests/app_a_py39/test_main.py hl[7] *} /// tip | Consejo -Note que la función de test ahora es `async def` en lugar de solo `def` como antes al usar el `TestClient`. +Nota que la función de test ahora es `async def` en lugar de solo `def` como antes al usar el `TestClient`. /// Luego podemos crear un `AsyncClient` con la app y enviar requests asíncronos a ella, usando `await`. -{* ../../docs_src/async_tests/test_main.py hl[9:12] *} +{* ../../docs_src/async_tests/app_a_py39/test_main.py hl[9:12] *} Esto es equivalente a: @@ -88,12 +88,12 @@ Si tu aplicación depende de eventos de lifespan, el `AsyncClient` no activará /// -## Otras Llamadas a Funciones Asíncronas +## Otras Llamadas a Funciones Asíncronas { #other-asynchronous-function-calls } Al ser la función de test asíncrona, ahora también puedes llamar (y `await`) otras funciones `async` además de enviar requests a tu aplicación FastAPI en tus tests, exactamente como las llamarías en cualquier otro lugar de tu código. /// tip | Consejo -Si encuentras un `RuntimeError: Task attached to a different loop` al integrar llamadas a funciones asíncronas en tus tests (por ejemplo, cuando usas MotorClient de MongoDB), recuerda crear instances de objetos que necesiten un loop de eventos solo dentro de funciones async, por ejemplo, en un callback `'@app.on_event("startup")`. +Si encuentras un `RuntimeError: Task attached to a different loop` al integrar llamadas a funciones asíncronas en tus tests (por ejemplo, cuando usas MotorClient de MongoDB), recuerda crear instances de objetos que necesiten un loop de eventos solo dentro de funciones async, por ejemplo, en un callback `@app.on_event("startup")`. /// diff --git a/docs/es/docs/advanced/behind-a-proxy.md b/docs/es/docs/advanced/behind-a-proxy.md index 8c590cbe82..f81c16ee81 100644 --- a/docs/es/docs/advanced/behind-a-proxy.md +++ b/docs/es/docs/advanced/behind-a-proxy.md @@ -1,6 +1,105 @@ -# Detrás de un Proxy +# Detrás de un Proxy { #behind-a-proxy } -En algunas situaciones, podrías necesitar usar un **proxy** como Traefik o Nginx con una configuración que añade un prefijo de path extra que no es visto por tu aplicación. +En muchas situaciones, usarías un **proxy** como Traefik o Nginx delante de tu app de FastAPI. + +Estos proxies podrían manejar certificados HTTPS y otras cosas. + +## Headers reenviados por el Proxy { #proxy-forwarded-headers } + +Un **proxy** delante de tu aplicación normalmente establecería algunos headers sobre la marcha antes de enviar los requests a tu **server** para que el servidor sepa que el request fue **reenviado** por el proxy, informándole la URL original (pública), incluyendo el dominio, que está usando HTTPS, etc. + +El programa **server** (por ejemplo **Uvicorn** a través de **FastAPI CLI**) es capaz de interpretar esos headers, y luego pasar esa información a tu aplicación. + +Pero por seguridad, como el server no sabe que está detrás de un proxy confiable, no interpretará esos headers. + +/// note | Detalles Técnicos + +Los headers del proxy son: + +* X-Forwarded-For +* X-Forwarded-Proto +* X-Forwarded-Host + +/// + +### Habilitar headers reenviados por el Proxy { #enable-proxy-forwarded-headers } + +Puedes iniciar FastAPI CLI con la *Opción de CLI* `--forwarded-allow-ips` y pasar las direcciones IP que deberían ser confiables para leer esos headers reenviados. + +Si lo estableces a `--forwarded-allow-ips="*"`, confiaría en todas las IPs entrantes. + +Si tu **server** está detrás de un **proxy** confiable y solo el proxy le habla, esto haría que acepte cualquiera que sea la IP de ese **proxy**. + +
+ +```console +$ fastapi run --forwarded-allow-ips="*" + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +### Redirecciones con HTTPS { #redirects-with-https } + +Por ejemplo, digamos que defines una *path operation* `/items/`: + +{* ../../docs_src/behind_a_proxy/tutorial001_01_py39.py hl[6] *} + +Si el cliente intenta ir a `/items`, por defecto, sería redirigido a `/items/`. + +Pero antes de configurar la *Opción de CLI* `--forwarded-allow-ips` podría redirigir a `http://localhost:8000/items/`. + +Pero quizá tu aplicación está alojada en `https://mysuperapp.com`, y la redirección debería ser a `https://mysuperapp.com/items/`. + +Al configurar `--proxy-headers` ahora FastAPI podrá redirigir a la ubicación correcta. 😎 + +``` +https://mysuperapp.com/items/ +``` + +/// tip | Consejo + +Si quieres aprender más sobre HTTPS, revisa la guía [Acerca de HTTPS](../deployment/https.md){.internal-link target=_blank}. + +/// + +### Cómo funcionan los headers reenviados por el Proxy { #how-proxy-forwarded-headers-work } + +Aquí tienes una representación visual de cómo el **proxy** añade headers reenviados entre el cliente y el **application server**: + +```mermaid +sequenceDiagram + participant Client as Cliente + participant Proxy as Proxy/Load Balancer + participant Server as Servidor de FastAPI + + Client->>Proxy: HTTPS Request
Host: mysuperapp.com
Path: /items + + Note over Proxy: El proxy añade headers reenviados + + Proxy->>Server: HTTP Request
X-Forwarded-For: [client IP]
X-Forwarded-Proto: https
X-Forwarded-Host: mysuperapp.com
Path: /items + + Note over Server: El servidor interpreta los headers
(si --forwarded-allow-ips está configurado) + + Server->>Proxy: HTTP Response
con URLs HTTPS correctas + + Proxy->>Client: HTTPS Response +``` + +El **proxy** intercepta el request original del cliente y añade los *headers* especiales de reenvío (`X-Forwarded-*`) antes de pasar el request al **application server**. + +Estos headers preservan información sobre el request original que de otro modo se perdería: + +* **X-Forwarded-For**: La IP original del cliente +* **X-Forwarded-Proto**: El protocolo original (`https`) +* **X-Forwarded-Host**: El host original (`mysuperapp.com`) + +Cuando **FastAPI CLI** está configurado con `--forwarded-allow-ips`, confía en estos headers y los usa, por ejemplo para generar las URLs correctas en redirecciones. + +## Proxy con un prefijo de path eliminado { #proxy-with-a-stripped-path-prefix } + +Podrías tener un proxy que añada un prefijo de path a tu aplicación. En estos casos, puedes usar `root_path` para configurar tu aplicación. @@ -10,15 +109,13 @@ El `root_path` se usa para manejar estos casos específicos. Y también se usa internamente al montar subaplicaciones. -## Proxy con un prefijo de path eliminado - Tener un proxy con un prefijo de path eliminado, en este caso, significa que podrías declarar un path en `/app` en tu código, pero luego añades una capa encima (el proxy) que situaría tu aplicación **FastAPI** bajo un path como `/api/v1`. En este caso, el path original `/app` realmente sería servido en `/api/v1/app`. Aunque todo tu código esté escrito asumiendo que solo existe `/app`. -{* ../../docs_src/behind_a_proxy/tutorial001.py hl[6] *} +{* ../../docs_src/behind_a_proxy/tutorial001_py39.py hl[6] *} Y el proxy estaría **"eliminando"** el **prefijo del path** sobre la marcha antes de transmitir el request al servidor de aplicaciones (probablemente Uvicorn a través de FastAPI CLI), manteniendo a tu aplicación convencida de que está siendo servida en `/app`, así que no tienes que actualizar todo tu código para incluir el prefijo `/api/v1`. @@ -66,14 +163,14 @@ La UI de los docs también necesitaría el esquema de OpenAPI para declarar que En este ejemplo, el "Proxy" podría ser algo como **Traefik**. Y el servidor sería algo como FastAPI CLI con **Uvicorn**, ejecutando tu aplicación de FastAPI. -### Proporcionando el `root_path` +### Proporcionando el `root_path` { #providing-the-root-path } Para lograr esto, puedes usar la opción de línea de comandos `--root-path` como:
```console -$ fastapi run main.py --root-path /api/v1 +$ fastapi run main.py --forwarded-allow-ips="*" --root-path /api/v1 INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) ``` @@ -90,20 +187,20 @@ Y la opción de línea de comandos `--root-path` proporciona ese `root_path`. /// -### Revisar el `root_path` actual +### Revisar el `root_path` actual { #checking-the-current-root-path } Puedes obtener el `root_path` actual utilizado por tu aplicación para cada request, es parte del diccionario `scope` (que es parte de la especificación ASGI). Aquí lo estamos incluyendo en el mensaje solo con fines de demostración. -{* ../../docs_src/behind_a_proxy/tutorial001.py hl[8] *} +{* ../../docs_src/behind_a_proxy/tutorial001_py39.py hl[8] *} Luego, si inicias Uvicorn con:
```console -$ fastapi run main.py --root-path /api/v1 +$ fastapi run main.py --forwarded-allow-ips="*" --root-path /api/v1 INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) ``` @@ -119,19 +216,19 @@ El response sería algo como: } ``` -### Configurar el `root_path` en la app de FastAPI +### Configurar el `root_path` en la app de FastAPI { #setting-the-root-path-in-the-fastapi-app } Alternativamente, si no tienes una forma de proporcionar una opción de línea de comandos como `--root-path` o su equivalente, puedes configurar el parámetro `root_path` al crear tu app de FastAPI: -{* ../../docs_src/behind_a_proxy/tutorial002.py hl[3] *} +{* ../../docs_src/behind_a_proxy/tutorial002_py39.py hl[3] *} Pasar el `root_path` a `FastAPI` sería el equivalente a pasar la opción de línea de comandos `--root-path` a Uvicorn o Hypercorn. -### Acerca de `root_path` +### Acerca de `root_path` { #about-root-path } Ten en cuenta que el servidor (Uvicorn) no usará ese `root_path` para nada, a excepción de pasárselo a la app. -Pero si vas con tu navegador a http://127.0.0.1:8000/app verás el response normal: +Pero si vas con tu navegador a http://127.0.0.1:8000/app verás el response normal: ```JSON { @@ -144,15 +241,15 @@ Así que no se esperará que sea accedido en `http://127.0.0.1:8000/api/v1/app`. Uvicorn esperará que el proxy acceda a Uvicorn en `http://127.0.0.1:8000/app`, y luego será responsabilidad del proxy añadir el prefijo extra `/api/v1` encima. -## Sobre proxies con un prefijo de path eliminado +## Sobre proxies con un prefijo de path eliminado { #about-proxies-with-a-stripped-path-prefix } Ten en cuenta que un proxy con prefijo de path eliminado es solo una de las formas de configurarlo. -Probablemente en muchos casos, el valor predeterminado será que el proxy no tenga un prefijo de path eliminado. +Probablemente en muchos casos, el valor por defecto será que el proxy no tenga un prefijo de path eliminado. En un caso así (sin un prefijo de path eliminado), el proxy escucharía algo como `https://myawesomeapp.com`, y luego si el navegador va a `https://myawesomeapp.com/api/v1/app` y tu servidor (por ejemplo, Uvicorn) escucha en `http://127.0.0.1:8000`, el proxy (sin un prefijo de path eliminado) accedería a Uvicorn en el mismo path: `http://127.0.0.1:8000/api/v1/app`. -## Probando localmente con Traefik +## Probando localmente con Traefik { #testing-locally-with-traefik } Puedes ejecutar fácilmente el experimento localmente con un prefijo de path eliminado usando Traefik. @@ -224,14 +321,14 @@ Y ahora inicia tu app, utilizando la opción `--root-path`:
```console -$ fastapi run main.py --root-path /api/v1 +$ fastapi run main.py --forwarded-allow-ips="*" --root-path /api/v1 INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) ```
-### Revisa los responses +### Revisa los responses { #check-the-responses } Ahora, si vas a la URL con el puerto para Uvicorn: http://127.0.0.1:8000/app, verás el response normal: @@ -267,7 +364,7 @@ Y la versión sin el prefijo de path (`http://127.0.0.1:8000/app`), proporcionad Eso demuestra cómo el Proxy (Traefik) usa el prefijo de path y cómo el servidor (Uvicorn) usa el `root_path` de la opción `--root-path`. -### Revisa la UI de los docs +### Revisa la UI de los docs { #check-the-docs-ui } Pero aquí está la parte divertida. ✨ @@ -287,7 +384,7 @@ Justo como queríamos. ✔️ Esto es porque FastAPI usa este `root_path` para crear el `server` por defecto en OpenAPI con la URL proporcionada por `root_path`. -## Servidores adicionales +## Servidores adicionales { #additional-servers } /// warning | Advertencia @@ -303,7 +400,7 @@ Si pasas una lista personalizada de `servers` y hay un `root_path` (porque tu AP Por ejemplo: -{* ../../docs_src/behind_a_proxy/tutorial003.py hl[4:7] *} +{* ../../docs_src/behind_a_proxy/tutorial003_py39.py hl[4:7] *} Generará un esquema de OpenAPI como: @@ -317,11 +414,11 @@ Generará un esquema de OpenAPI como: }, { "url": "https://stag.example.com", - "description": "Entorno de pruebas" + "description": "Staging environment" }, { "url": "https://prod.example.com", - "description": "Entorno de producción" + "description": "Production environment" } ], "paths": { @@ -346,15 +443,23 @@ La UI de los docs interactuará con el server que selecciones. /// -### Desactivar el server automático de `root_path` +/// note | Detalles Técnicos + +La propiedad `servers` en la especificación de OpenAPI es opcional. + +Si no especificas el parámetro `servers` y `root_path` es igual a `/`, la propiedad `servers` en el esquema de OpenAPI generado se omitirá por completo por defecto, lo cual es equivalente a un único server con un valor `url` de `/`. + +/// + +### Desactivar el server automático de `root_path` { #disable-automatic-server-from-root-path } Si no quieres que **FastAPI** incluya un server automático usando el `root_path`, puedes usar el parámetro `root_path_in_servers=False`: -{* ../../docs_src/behind_a_proxy/tutorial004.py hl[9] *} +{* ../../docs_src/behind_a_proxy/tutorial004_py39.py hl[9] *} y entonces no lo incluirá en el esquema de OpenAPI. -## Montando una sub-aplicación +## Montando una sub-aplicación { #mounting-a-sub-application } Si necesitas montar una sub-aplicación (como se describe en [Aplicaciones secundarias - Monturas](sub-applications.md){.internal-link target=_blank}) mientras usas un proxy con `root_path`, puedes hacerlo normalmente, como esperarías. diff --git a/docs/es/docs/advanced/custom-response.md b/docs/es/docs/advanced/custom-response.md index f7bd81bcc7..0884c41a74 100644 --- a/docs/es/docs/advanced/custom-response.md +++ b/docs/es/docs/advanced/custom-response.md @@ -1,4 +1,4 @@ -# Response Personalizado - HTML, Stream, Archivo, otros +# Response Personalizado - HTML, Stream, Archivo, otros { #custom-response-html-stream-file-others } Por defecto, **FastAPI** devolverá los responses usando `JSONResponse`. @@ -18,7 +18,7 @@ Si usas una clase de response sin media type, FastAPI esperará que tu response /// -## Usa `ORJSONResponse` +## Usa `ORJSONResponse` { #use-orjsonresponse } Por ejemplo, si estás exprimendo el rendimiento, puedes instalar y usar `orjson` y establecer el response como `ORJSONResponse`. @@ -30,7 +30,7 @@ Esto se debe a que, por defecto, FastAPI inspeccionará cada elemento dentro y s Pero si estás seguro de que el contenido que estás devolviendo es **serializable con JSON**, puedes pasarlo directamente a la clase de response y evitar la sobrecarga extra que FastAPI tendría al pasar tu contenido de retorno a través de `jsonable_encoder` antes de pasarlo a la clase de response. -{* ../../docs_src/custom_response/tutorial001b.py hl[2,7] *} +{* ../../docs_src/custom_response/tutorial001b_py39.py hl[2,7] *} /// info | Información @@ -48,14 +48,14 @@ El `ORJSONResponse` solo está disponible en FastAPI, no en Starlette. /// -## Response HTML +## Response HTML { #html-response } Para devolver un response con HTML directamente desde **FastAPI**, usa `HTMLResponse`. * Importa `HTMLResponse`. * Pasa `HTMLResponse` como parámetro `response_class` de tu *path operation decorator*. -{* ../../docs_src/custom_response/tutorial002.py hl[2,7] *} +{* ../../docs_src/custom_response/tutorial002_py39.py hl[2,7] *} /// info | Información @@ -67,13 +67,13 @@ Y se documentará así en OpenAPI. /// -### Devuelve una `Response` +### Devuelve una `Response` { #return-a-response } Como se ve en [Devolver una Response directamente](response-directly.md){.internal-link target=_blank}, también puedes sobrescribir el response directamente en tu *path operation*, devolviéndolo. El mismo ejemplo de arriba, devolviendo una `HTMLResponse`, podría verse así: -{* ../../docs_src/custom_response/tutorial003.py hl[2,7,19] *} +{* ../../docs_src/custom_response/tutorial003_py39.py hl[2,7,19] *} /// warning | Advertencia @@ -87,27 +87,27 @@ Por supuesto, el `Content-Type` header real, el código de estado, etc., provend /// -### Documenta en OpenAPI y sobrescribe `Response` +### Documenta en OpenAPI y sobrescribe `Response` { #document-in-openapi-and-override-response } Si quieres sobrescribir el response desde dentro de la función pero al mismo tiempo documentar el "media type" en OpenAPI, puedes usar el parámetro `response_class` Y devolver un objeto `Response`. El `response_class` solo se usará para documentar el OpenAPI *path operation*, pero tu `Response` se usará tal cual. -#### Devuelve un `HTMLResponse` directamente +#### Devuelve un `HTMLResponse` directamente { #return-an-htmlresponse-directly } Por ejemplo, podría ser algo así: -{* ../../docs_src/custom_response/tutorial004.py hl[7,21,23] *} +{* ../../docs_src/custom_response/tutorial004_py39.py hl[7,21,23] *} En este ejemplo, la función `generate_html_response()` ya genera y devuelve una `Response` en lugar de devolver el HTML en un `str`. -Al devolver el resultado de llamar a `generate_html_response()`, ya estás devolviendo una `Response` que sobrescribirá el comportamiento predeterminado de **FastAPI**. +Al devolver el resultado de llamar a `generate_html_response()`, ya estás devolviendo una `Response` que sobrescribirá el comportamiento por defecto de **FastAPI**. Pero como pasaste `HTMLResponse` en el `response_class` también, **FastAPI** sabrá cómo documentarlo en OpenAPI y la documentación interactiva como HTML con `text/html`: -## Responses disponibles +## Responses disponibles { #available-responses } Aquí hay algunos de los responses disponibles. @@ -121,7 +121,7 @@ También podrías usar `from starlette.responses import HTMLResponse`. /// -### `Response` +### `Response` { #response } La clase principal `Response`, todos los otros responses heredan de ella. @@ -136,25 +136,25 @@ Acepta los siguientes parámetros: FastAPI (de hecho Starlette) incluirá automáticamente un header Content-Length. También incluirá un header Content-Type, basado en el `media_type` y añadiendo un conjunto de caracteres para tipos de texto. -{* ../../docs_src/response_directly/tutorial002.py hl[1,18] *} +{* ../../docs_src/response_directly/tutorial002_py39.py hl[1,18] *} -### `HTMLResponse` +### `HTMLResponse` { #htmlresponse } Toma algún texto o bytes y devuelve un response HTML, como leíste arriba. -### `PlainTextResponse` +### `PlainTextResponse` { #plaintextresponse } Toma algún texto o bytes y devuelve un response de texto plano. -{* ../../docs_src/custom_response/tutorial005.py hl[2,7,9] *} +{* ../../docs_src/custom_response/tutorial005_py39.py hl[2,7,9] *} -### `JSONResponse` +### `JSONResponse` { #jsonresponse } Toma algunos datos y devuelve un response codificado como `application/json`. -Este es el response predeterminado usado en **FastAPI**, como leíste arriba. +Este es el response usado por defecto en **FastAPI**, como leíste arriba. -### `ORJSONResponse` +### `ORJSONResponse` { #orjsonresponse } Un response JSON rápido alternativo usando `orjson`, como leíste arriba. @@ -164,7 +164,7 @@ Esto requiere instalar `orjson`, por ejemplo, con `pip install orjson`. /// -### `UJSONResponse` +### `UJSONResponse` { #ujsonresponse } Un response JSON alternativo usando `ujson`. @@ -180,7 +180,7 @@ Esto requiere instalar `ujson`, por ejemplo, con `pip install ujson`. /// -{* ../../docs_src/custom_response/tutorial001.py hl[2,7] *} +{* ../../docs_src/custom_response/tutorial001_py39.py hl[2,7] *} /// tip | Consejo @@ -188,45 +188,45 @@ Es posible que `ORJSONResponse` sea una alternativa más rápida. /// -### `RedirectResponse` +### `RedirectResponse` { #redirectresponse } Devuelve una redirección HTTP. Usa un código de estado 307 (Redirección Temporal) por defecto. Puedes devolver un `RedirectResponse` directamente: -{* ../../docs_src/custom_response/tutorial006.py hl[2,9] *} +{* ../../docs_src/custom_response/tutorial006_py39.py hl[2,9] *} --- O puedes usarlo en el parámetro `response_class`: -{* ../../docs_src/custom_response/tutorial006b.py hl[2,7,9] *} +{* ../../docs_src/custom_response/tutorial006b_py39.py hl[2,7,9] *} Si haces eso, entonces puedes devolver la URL directamente desde tu *path operation function*. -En este caso, el `status_code` utilizado será el predeterminado para `RedirectResponse`, que es `307`. +En este caso, el `status_code` utilizado será el por defecto para `RedirectResponse`, que es `307`. --- También puedes usar el parámetro `status_code` combinado con el parámetro `response_class`: -{* ../../docs_src/custom_response/tutorial006c.py hl[2,7,9] *} +{* ../../docs_src/custom_response/tutorial006c_py39.py hl[2,7,9] *} -### `StreamingResponse` +### `StreamingResponse` { #streamingresponse } Toma un generador `async` o un generador/iterador normal y transmite el cuerpo del response. -{* ../../docs_src/custom_response/tutorial007.py hl[2,14] *} +{* ../../docs_src/custom_response/tutorial007_py39.py hl[2,14] *} -#### Usando `StreamingResponse` con objetos similares a archivos +#### Usando `StreamingResponse` con objetos similares a archivos { #using-streamingresponse-with-file-like-objects } -Si tienes un objeto similar a un archivo (por ejemplo, el objeto devuelto por `open()`), puedes crear una función generadora para iterar sobre ese objeto similar a un archivo. +Si tienes un objeto similar a un archivo (por ejemplo, el objeto devuelto por `open()`), puedes crear una función generadora para iterar sobre ese objeto similar a un archivo. De esa manera, no tienes que leerlo todo primero en memoria, y puedes pasar esa función generadora al `StreamingResponse`, y devolverlo. Esto incluye muchos paquetes para interactuar con almacenamiento en la nube, procesamiento de video y otros. -{* ../../docs_src/custom_response/tutorial008.py hl[2,10:12,14] *} +{* ../../docs_src/custom_response/tutorial008_py39.py hl[2,10:12,14] *} 1. Esta es la función generadora. Es una "función generadora" porque contiene declaraciones `yield` dentro. 2. Al usar un bloque `with`, nos aseguramos de que el objeto similar a un archivo se cierre después de que la función generadora termine. Así, después de que termina de enviar el response. @@ -242,7 +242,7 @@ Nota que aquí como estamos usando `open()` estándar que no admite `async` y `a /// -### `FileResponse` +### `FileResponse` { #fileresponse } Transmite un archivo asincrónicamente como response. @@ -255,15 +255,15 @@ Toma un conjunto diferente de argumentos para crear un instance que los otros ti Los responses de archivos incluirán los headers apropiados `Content-Length`, `Last-Modified` y `ETag`. -{* ../../docs_src/custom_response/tutorial009.py hl[2,10] *} +{* ../../docs_src/custom_response/tutorial009_py39.py hl[2,10] *} También puedes usar el parámetro `response_class`: -{* ../../docs_src/custom_response/tutorial009b.py hl[2,8,10] *} +{* ../../docs_src/custom_response/tutorial009b_py39.py hl[2,8,10] *} En este caso, puedes devolver la path del archivo directamente desde tu *path operation* function. -## Clase de response personalizada +## Clase de response personalizada { #custom-response-class } Puedes crear tu propia clase de response personalizada, heredando de `Response` y usándola. @@ -273,7 +273,7 @@ Digamos que quieres que devuelva JSON con sangría y formato, por lo que quieres Podrías crear un `CustomORJSONResponse`. Lo principal que tienes que hacer es crear un método `Response.render(content)` que devuelva el contenido como `bytes`: -{* ../../docs_src/custom_response/tutorial009c.py hl[9:14,17] *} +{* ../../docs_src/custom_response/tutorial009c_py39.py hl[9:14,17] *} Ahora en lugar de devolver: @@ -291,7 +291,7 @@ Ahora en lugar de devolver: Por supuesto, probablemente encontrarás formas mucho mejores de aprovechar esto que formatear JSON. 😉 -## Clase de response predeterminada +## Clase de response por defecto { #default-response-class } Al crear una instance de la clase **FastAPI** o un `APIRouter`, puedes especificar qué clase de response usar por defecto. @@ -299,7 +299,7 @@ El parámetro que define esto es `default_response_class`. En el ejemplo a continuación, **FastAPI** usará `ORJSONResponse` por defecto, en todas las *path operations*, en lugar de `JSONResponse`. -{* ../../docs_src/custom_response/tutorial010.py hl[2,4] *} +{* ../../docs_src/custom_response/tutorial010_py39.py hl[2,4] *} /// tip | Consejo @@ -307,6 +307,6 @@ Todavía puedes sobrescribir `response_class` en *path operations* como antes. /// -## Documentación adicional +## Documentación adicional { #additional-documentation } También puedes declarar el media type y muchos otros detalles en OpenAPI usando `responses`: [Responses Adicionales en OpenAPI](additional-responses.md){.internal-link target=_blank}. diff --git a/docs/es/docs/advanced/dataclasses.md b/docs/es/docs/advanced/dataclasses.md index 0ca1fd3b62..3a07482ad1 100644 --- a/docs/es/docs/advanced/dataclasses.md +++ b/docs/es/docs/advanced/dataclasses.md @@ -1,10 +1,10 @@ -# Usando Dataclasses +# Usando Dataclasses { #using-dataclasses } FastAPI está construido sobre **Pydantic**, y te he estado mostrando cómo usar modelos de Pydantic para declarar requests y responses. Pero FastAPI también soporta el uso de `dataclasses` de la misma manera: -{* ../../docs_src/dataclasses/tutorial001.py hl[1,7:12,19:20] *} +{* ../../docs_src/dataclasses_/tutorial001_py310.py hl[1,6:11,18:19] *} Esto sigue siendo soportado gracias a **Pydantic**, ya que tiene soporte interno para `dataclasses`. @@ -28,11 +28,11 @@ Pero si tienes un montón de dataclasses por ahí, este es un buen truco para us /// -## Dataclasses en `response_model` +## Dataclasses en `response_model` { #dataclasses-in-response-model } También puedes usar `dataclasses` en el parámetro `response_model`: -{* ../../docs_src/dataclasses/tutorial002.py hl[1,7:13,19] *} +{* ../../docs_src/dataclasses_/tutorial002_py310.py hl[1,6:12,18] *} El dataclass será automáticamente convertido a un dataclass de Pydantic. @@ -40,7 +40,7 @@ De esta manera, su esquema aparecerá en la interfaz de usuario de la documentac -## Dataclasses en Estructuras de Datos Anidadas +## Dataclasses en Estructuras de Datos Anidadas { #dataclasses-in-nested-data-structures } También puedes combinar `dataclasses` con otras anotaciones de tipos para crear estructuras de datos anidadas. @@ -48,7 +48,7 @@ En algunos casos, todavía podrías tener que usar la versión de `dataclasses` En ese caso, simplemente puedes intercambiar los `dataclasses` estándar con `pydantic.dataclasses`, que es un reemplazo directo: -{* ../../docs_src/dataclasses/tutorial003.py hl[1,5,8:11,14:17,23:25,28] *} +{* ../../docs_src/dataclasses_/tutorial003_py310.py hl[1,4,7:10,13:16,22:24,27] *} 1. Todavía importamos `field` de los `dataclasses` estándar. @@ -64,7 +64,7 @@ En ese caso, simplemente puedes intercambiar los `dataclasses` estándar con `py 6. Aquí estamos regresando un diccionario que contiene `items`, que es una lista de dataclasses. - FastAPI todavía es capaz de serializar los datos a JSON. + FastAPI todavía es capaz de serializar los datos a JSON. 7. Aquí el `response_model` está usando una anotación de tipo de una lista de dataclasses `Author`. @@ -84,12 +84,12 @@ Puedes combinar `dataclasses` con otras anotaciones de tipos en muchas combinaci Revisa las anotaciones en el código arriba para ver más detalles específicos. -## Aprende Más +## Aprende Más { #learn-more } También puedes combinar `dataclasses` con otros modelos de Pydantic, heredar de ellos, incluirlos en tus propios modelos, etc. Para saber más, revisa la documentación de Pydantic sobre dataclasses. -## Versión +## Versión { #version } Esto está disponible desde la versión `0.67.0` de FastAPI. 🔖 diff --git a/docs/es/docs/advanced/events.md b/docs/es/docs/advanced/events.md index a33b517910..c2002a6f53 100644 --- a/docs/es/docs/advanced/events.md +++ b/docs/es/docs/advanced/events.md @@ -1,4 +1,4 @@ -# Eventos de Lifespan +# Eventos de Lifespan { #lifespan-events } Puedes definir lógica (código) que debería ser ejecutada antes de que la aplicación **inicie**. Esto significa que este código será ejecutado **una vez**, **antes** de que la aplicación **comience a recibir requests**. @@ -8,7 +8,7 @@ Debido a que este código se ejecuta antes de que la aplicación **comience** a Esto puede ser muy útil para configurar **recursos** que necesitas usar para toda la app, y que son **compartidos** entre requests, y/o que necesitas **limpiar** después. Por ejemplo, un pool de conexiones a una base de datos, o cargando un modelo de machine learning compartido. -## Caso de Uso +## Caso de Uso { #use-case } Empecemos con un ejemplo de **caso de uso** y luego veamos cómo resolverlo con esto. @@ -22,7 +22,7 @@ Podrías cargarlo en el nivel superior del módulo/archivo, pero eso también si Eso es lo que resolveremos, vamos a cargar el modelo antes de que los requests sean manejados, pero solo justo antes de que la aplicación comience a recibir requests, no mientras el código se está cargando. -## Lifespan +## Lifespan { #lifespan } Puedes definir esta lógica de *startup* y *shutdown* usando el parámetro `lifespan` de la app de `FastAPI`, y un "context manager" (te mostraré lo que es en un momento). @@ -30,7 +30,7 @@ Comencemos con un ejemplo y luego veámoslo en detalle. Creamos una función asíncrona `lifespan()` con `yield` así: -{* ../../docs_src/events/tutorial003.py hl[16,19] *} +{* ../../docs_src/events/tutorial003_py39.py hl[16,19] *} Aquí estamos simulando la operación costosa de *startup* de cargar el modelo poniendo la función del (falso) modelo en el diccionario con modelos de machine learning antes del `yield`. Este código será ejecutado **antes** de que la aplicación **comience a tomar requests**, durante el *startup*. @@ -44,25 +44,25 @@ Quizás necesites iniciar una nueva versión, o simplemente te cansaste de ejecu /// -### Función de Lifespan +### Función de Lifespan { #lifespan-function } Lo primero que hay que notar es que estamos definiendo una función asíncrona con `yield`. Esto es muy similar a las Dependencias con `yield`. -{* ../../docs_src/events/tutorial003.py hl[14:19] *} +{* ../../docs_src/events/tutorial003_py39.py hl[14:19] *} La primera parte de la función, antes del `yield`, será ejecutada **antes** de que la aplicación comience. Y la parte después del `yield` será ejecutada **después** de que la aplicación haya terminado. -### Async Context Manager +### Async Context Manager { #async-context-manager } Si revisas, la función está decorada con un `@asynccontextmanager`. Eso convierte a la función en algo llamado un "**async context manager**". -{* ../../docs_src/events/tutorial003.py hl[1,13] *} +{* ../../docs_src/events/tutorial003_py39.py hl[1,13] *} -Un **context manager** en Python es algo que puedes usar en una declaración `with`, por ejemplo, `open()` puede ser usado como un context manager: +Un **context manager** en Python es algo que puedes usar en un statement `with`, por ejemplo, `open()` puede ser usado como un context manager: ```Python with open("file.txt") as file: @@ -82,9 +82,9 @@ En nuestro ejemplo de código arriba, no lo usamos directamente, pero se lo pasa El parámetro `lifespan` de la app de `FastAPI` toma un **async context manager**, por lo que podemos pasar nuestro nuevo `lifespan` async context manager a él. -{* ../../docs_src/events/tutorial003.py hl[22] *} +{* ../../docs_src/events/tutorial003_py39.py hl[22] *} -## Eventos Alternativos (obsoleto) +## Eventos Alternativos (obsoleto) { #alternative-events-deprecated } /// warning | Advertencia @@ -100,11 +100,11 @@ Puedes definir manejadores de eventos (funciones) que necesitan ser ejecutadas a Estas funciones pueden ser declaradas con `async def` o `def` normal. -### Evento `startup` +### Evento `startup` { #startup-event } Para añadir una función que debería ejecutarse antes de que la aplicación inicie, declárala con el evento `"startup"`: -{* ../../docs_src/events/tutorial001.py hl[8] *} +{* ../../docs_src/events/tutorial001_py39.py hl[8] *} En este caso, la función manejadora del evento `startup` inicializará los ítems de la "base de datos" (solo un `dict`) con algunos valores. @@ -112,11 +112,11 @@ Puedes añadir más de un manejador de eventos. Y tu aplicación no comenzará a recibir requests hasta que todos los manejadores de eventos `startup` hayan completado. -### Evento `shutdown` +### Evento `shutdown` { #shutdown-event } Para añadir una función que debería ejecutarse cuando la aplicación se esté cerrando, declárala con el evento `"shutdown"`: -{* ../../docs_src/events/tutorial002.py hl[6] *} +{* ../../docs_src/events/tutorial002_py39.py hl[6] *} Aquí, la función manejadora del evento `shutdown` escribirá una línea de texto `"Application shutdown"` a un archivo `log.txt`. @@ -138,7 +138,7 @@ Por eso, declaramos la función manejadora del evento con `def` estándar en vez /// -### `startup` y `shutdown` juntos +### `startup` y `shutdown` juntos { #startup-and-shutdown-together } Hay una gran posibilidad de que la lógica para tu *startup* y *shutdown* esté conectada, podrías querer iniciar algo y luego finalizarlo, adquirir un recurso y luego liberarlo, etc. @@ -146,7 +146,7 @@ Hacer eso en funciones separadas que no comparten lógica o variables juntas es Debido a eso, ahora se recomienda en su lugar usar el `lifespan` como se explicó arriba. -## Detalles Técnicos +## Detalles Técnicos { #technical-details } Solo un detalle técnico para los nerds curiosos. 🤓 @@ -160,6 +160,6 @@ Incluyendo cómo manejar el estado de lifespan que puede ser usado en otras áre /// -## Sub Aplicaciones +## Sub Aplicaciones { #sub-applications } 🚨 Ten en cuenta que estos eventos de lifespan (startup y shutdown) solo serán ejecutados para la aplicación principal, no para [Sub Aplicaciones - Mounts](sub-applications.md){.internal-link target=_blank}. diff --git a/docs/es/docs/advanced/generate-clients.md b/docs/es/docs/advanced/generate-clients.md index b664bceacf..daf6cefed3 100644 --- a/docs/es/docs/advanced/generate-clients.md +++ b/docs/es/docs/advanced/generate-clients.md @@ -1,115 +1,76 @@ -# Genera Clientes +# Generando SDKs { #generating-sdks } -Como **FastAPI** está basado en la especificación OpenAPI, obtienes compatibilidad automática con muchas herramientas, incluyendo la documentación automática de la API (proporcionada por Swagger UI). +Como **FastAPI** está basado en la especificación **OpenAPI**, sus APIs se pueden describir en un formato estándar que muchas herramientas entienden. -Una ventaja particular que no es necesariamente obvia es que puedes **generar clientes** (a veces llamados **SDKs** ) para tu API, para muchos **lenguajes de programación** diferentes. +Esto facilita generar **documentación** actualizada, paquetes de cliente (**SDKs**) en múltiples lenguajes y **escribir pruebas** o **flujos de automatización** que se mantengan sincronizados con tu código. -## Generadores de Clientes OpenAPI +En esta guía, aprenderás a generar un **SDK de TypeScript** para tu backend con FastAPI. -Hay muchas herramientas para generar clientes desde **OpenAPI**. +## Generadores de SDKs de código abierto { #open-source-sdk-generators } -Una herramienta común es OpenAPI Generator. +Una opción versátil es el OpenAPI Generator, que soporta **muchos lenguajes de programación** y puede generar SDKs a partir de tu especificación OpenAPI. -Si estás construyendo un **frontend**, una alternativa muy interesante es openapi-ts. +Para **clientes de TypeScript**, Hey API es una solución diseñada específicamente, que ofrece una experiencia optimizada para el ecosistema de TypeScript. -## Generadores de Clientes y SDKs - Sponsor +Puedes descubrir más generadores de SDK en OpenAPI.Tools. -También hay algunos generadores de Clientes y SDKs **respaldados por empresas** basados en OpenAPI (FastAPI), en algunos casos pueden ofrecerte **funcionalidades adicionales** además de SDKs/clientes generados de alta calidad. +/// tip | Consejo -Algunos de ellos también ✨ [**sponsorean FastAPI**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨, esto asegura el **desarrollo** continuo y saludable de FastAPI y su **ecosistema**. +FastAPI genera automáticamente especificaciones **OpenAPI 3.1**, así que cualquier herramienta que uses debe soportar esta versión. -Y muestra su verdadero compromiso con FastAPI y su **comunidad** (tú), ya que no solo quieren proporcionarte un **buen servicio** sino también asegurarse de que tengas un **buen y saludable framework**, FastAPI. 🙇 +/// + +## Generadores de SDKs de sponsors de FastAPI { #sdk-generators-from-fastapi-sponsors } + +Esta sección destaca soluciones **respaldadas por empresas** y **venture-backed** de compañías que sponsorean FastAPI. Estos productos ofrecen **funcionalidades adicionales** e **integraciones** además de SDKs generados de alta calidad. + +Al ✨ [**sponsorear FastAPI**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨, estas compañías ayudan a asegurar que el framework y su **ecosistema** se mantengan saludables y **sustentables**. + +Su sponsorship también demuestra un fuerte compromiso con la **comunidad** de FastAPI (tú), mostrando que no solo les importa ofrecer un **gran servicio**, sino también apoyar un **framework robusto y próspero**, FastAPI. 🙇 Por ejemplo, podrías querer probar: * Speakeasy -* Stainless -* liblab +* Stainless +* liblab -También hay varias otras empresas que ofrecen servicios similares que puedes buscar y encontrar en línea. 🤓 +Algunas de estas soluciones también pueden ser open source u ofrecer niveles gratuitos, así que puedes probarlas sin un compromiso financiero. Hay otros generadores de SDK comerciales disponibles y se pueden encontrar en línea. 🤓 -## Genera un Cliente Frontend en TypeScript +## Crea un SDK de TypeScript { #create-a-typescript-sdk } Empecemos con una aplicación simple de FastAPI: {* ../../docs_src/generate_clients/tutorial001_py39.py hl[7:9,12:13,16:17,21] *} -Nota que las *path operations* definen los modelos que usan para el payload de la petición y el payload del response, usando los modelos `Item` y `ResponseMessage`. +Nota que las *path operations* definen los modelos que usan para el payload del request y el payload del response, usando los modelos `Item` y `ResponseMessage`. -### Documentación de la API +### Documentación de la API { #api-docs } -Si vas a la documentación de la API, verás que tiene los **esquemas** para los datos que se enviarán en las peticiones y se recibirán en los responses: +Si vas a `/docs`, verás que tiene los **esquemas** para los datos a enviar en requests y recibir en responses: -Puedes ver esos esquemas porque fueron declarados con los modelos en la aplicación. +Puedes ver esos esquemas porque fueron declarados con los modelos en la app. -Esa información está disponible en el **JSON Schema** de OpenAPI de la aplicación, y luego se muestra en la documentación de la API (por Swagger UI). +Esa información está disponible en el **OpenAPI schema** de la app, y luego se muestra en la documentación de la API. Y esa misma información de los modelos que está incluida en OpenAPI es lo que puede usarse para **generar el código del cliente**. -### Genera un Cliente en TypeScript +### Hey API { #hey-api } -Ahora que tenemos la aplicación con los modelos, podemos generar el código del cliente para el frontend. +Una vez que tenemos una app de FastAPI con los modelos, podemos usar Hey API para generar un cliente de TypeScript. La forma más rápida de hacerlo es con npx. -#### Instalar `openapi-ts` - -Puedes instalar `openapi-ts` en tu código de frontend con: - -
- -```console -$ npm install @hey-api/openapi-ts --save-dev - ----> 100% +```sh +npx @hey-api/openapi-ts -i http://localhost:8000/openapi.json -o src/client ``` -
+Esto generará un SDK de TypeScript en `./src/client`. -#### Generar el Código del Cliente +Puedes aprender cómo instalar `@hey-api/openapi-ts` y leer sobre el output generado en su sitio web. -Para generar el código del cliente puedes usar la aplicación de línea de comandos `openapi-ts` que ahora estaría instalada. +### Usar el SDK { #using-the-sdk } -Como está instalada en el proyecto local, probablemente no podrías llamar a ese comando directamente, pero podrías ponerlo en tu archivo `package.json`. - -Podría verse como esto: - -```JSON hl_lines="7" -{ - "name": "frontend-app", - "version": "1.0.0", - "description": "", - "main": "index.js", - "scripts": { - "generate-client": "openapi-ts --input http://localhost:8000/openapi.json --output ./src/client --client axios" - }, - "author": "", - "license": "", - "devDependencies": { - "@hey-api/openapi-ts": "^0.27.38", - "typescript": "^4.6.2" - } -} -``` - -Después de tener ese script de NPM `generate-client` allí, puedes ejecutarlo con: - -
- -```console -$ npm run generate-client - -frontend-app@1.0.0 generate-client /home/user/code/frontend-app -> openapi-ts --input http://localhost:8000/openapi.json --output ./src/client --client axios -``` - -
- -Ese comando generará código en `./src/client` y usará `axios` (el paquete HTTP de frontend) internamente. - -### Prueba el Código del Cliente - -Ahora puedes importar y usar el código del cliente, podría verse así, nota que tienes autocompletado para los métodos: +Ahora puedes importar y usar el código del cliente. Podría verse así, nota que tienes autocompletado para los métodos: @@ -131,17 +92,17 @@ El objeto de response también tendrá autocompletado: -## App de FastAPI con Tags +## App de FastAPI con tags { #fastapi-app-with-tags } -En muchos casos tu aplicación de FastAPI será más grande, y probablemente usarás tags para separar diferentes grupos de *path operations*. +En muchos casos tu app de FastAPI será más grande, y probablemente usarás tags para separar diferentes grupos de *path operations*. -Por ejemplo, podrías tener una sección para **items** y otra sección para **usuarios**, y podrían estar separadas por tags: +Por ejemplo, podrías tener una sección para **items** y otra sección para **users**, y podrían estar separadas por tags: {* ../../docs_src/generate_clients/tutorial002_py39.py hl[21,26,34] *} -### Genera un Cliente TypeScript con Tags +### Genera un Cliente TypeScript con tags { #generate-a-typescript-client-with-tags } -Si generas un cliente para una aplicación de FastAPI usando tags, normalmente también separará el código del cliente basándose en los tags. +Si generas un cliente para una app de FastAPI usando tags, normalmente también separará el código del cliente basándose en los tags. De esta manera podrás tener las cosas ordenadas y agrupadas correctamente para el código del cliente: @@ -152,7 +113,7 @@ En este caso tienes: * `ItemsService` * `UsersService` -### Nombres de los Métodos del Cliente +### Nombres de los métodos del cliente { #client-method-names } Ahora mismo los nombres de los métodos generados como `createItemItemsPost` no se ven muy limpios: @@ -166,15 +127,15 @@ OpenAPI requiere que cada operation ID sea único a través de todas las *path o Pero te mostraré cómo mejorar eso a continuación. 🤓 -## Operation IDs Personalizados y Mejores Nombres de Métodos +## Operation IDs personalizados y mejores nombres de métodos { #custom-operation-ids-and-better-method-names } Puedes **modificar** la forma en que estos operation IDs son **generados** para hacerlos más simples y tener **nombres de métodos más simples** en los clientes. En este caso tendrás que asegurarte de que cada operation ID sea **único** de alguna otra manera. -Por ejemplo, podrías asegurarte de que cada *path operation* tenga un tag, y luego generar el operation ID basado en el **tag** y el nombre de la *path operation* **name** (el nombre de la función). +Por ejemplo, podrías asegurarte de que cada *path operation* tenga un tag, y luego generar el operation ID basado en el **tag** y el **name** de la *path operation* (el nombre de la función). -### Función Personalizada para Generar ID Único +### Función personalizada para generar ID único { #custom-generate-unique-id-function } FastAPI usa un **ID único** para cada *path operation*, se usa para el **operation ID** y también para los nombres de cualquier modelo personalizado necesario, para requests o responses. @@ -186,15 +147,15 @@ Puedes entonces pasar esa función personalizada a **FastAPI** como el parámetr {* ../../docs_src/generate_clients/tutorial003_py39.py hl[6:7,10] *} -### Generar un Cliente TypeScript con Operation IDs Personalizados +### Genera un Cliente TypeScript con operation IDs personalizados { #generate-a-typescript-client-with-custom-operation-ids } -Ahora si generas el cliente de nuevo, verás que tiene los nombres de métodos mejorados: +Ahora, si generas el cliente de nuevo, verás que tiene los nombres de métodos mejorados: Como ves, los nombres de métodos ahora tienen el tag y luego el nombre de la función, ahora no incluyen información del path de la URL y la operación HTTP. -### Preprocesa la Especificación OpenAPI para el Generador de Clientes +### Preprocesa la especificación OpenAPI para el generador de clientes { #preprocess-the-openapi-specification-for-the-client-generator } El código generado aún tiene algo de **información duplicada**. @@ -206,7 +167,7 @@ Pero para el cliente generado podríamos **modificar** los operation IDs de Open Podríamos descargar el JSON de OpenAPI a un archivo `openapi.json` y luego podríamos **remover ese tag prefijado** con un script como este: -{* ../../docs_src/generate_clients/tutorial004.py *} +{* ../../docs_src/generate_clients/tutorial004_py39.py *} //// tab | Node.js @@ -218,44 +179,30 @@ Podríamos descargar el JSON de OpenAPI a un archivo `openapi.json` y luego podr Con eso, los operation IDs serían renombrados de cosas como `items-get_items` a solo `get_items`, de esa manera el generador del cliente puede generar nombres de métodos más simples. -### Generar un Cliente TypeScript con el OpenAPI Preprocesado +### Genera un Cliente TypeScript con el OpenAPI preprocesado { #generate-a-typescript-client-with-the-preprocessed-openapi } -Ahora como el resultado final está en un archivo `openapi.json`, modificarías el `package.json` para usar ese archivo local, por ejemplo: +Como el resultado final ahora está en un archivo `openapi.json`, necesitas actualizar la ubicación de la entrada: -```JSON hl_lines="7" -{ - "name": "frontend-app", - "version": "1.0.0", - "description": "", - "main": "index.js", - "scripts": { - "generate-client": "openapi-ts --input ./openapi.json --output ./src/client --client axios" - }, - "author": "", - "license": "", - "devDependencies": { - "@hey-api/openapi-ts": "^0.27.38", - "typescript": "^4.6.2" - } -} +```sh +npx @hey-api/openapi-ts -i ./openapi.json -o src/client ``` Después de generar el nuevo cliente, ahora tendrías nombres de métodos **limpios**, con todo el **autocompletado**, **errores en línea**, etc: -## Beneficios +## Beneficios { #benefits } -Cuando usas los clientes generados automáticamente obtendrás **autocompletado** para: +Cuando uses los clientes generados automáticamente obtendrás **autocompletado** para: * Métodos. -* Payloads de peticiones en el cuerpo, parámetros de query, etc. -* Payloads de responses. +* Payloads de request en el body, parámetros de query, etc. +* Payloads de response. También tendrás **errores en línea** para todo. Y cada vez que actualices el código del backend, y **regeneres** el frontend, tendrás las nuevas *path operations* disponibles como métodos, las antiguas eliminadas, y cualquier otro cambio se reflejará en el código generado. 🤓 -Esto también significa que si algo cambió será **reflejado** automáticamente en el código del cliente. Y si haces **build** del cliente, te dará error si tienes algún **desajuste** en los datos utilizados. +Esto también significa que si algo cambió será **reflejado** automáticamente en el código del cliente. Y si haces **build** del cliente, dará error si tienes algún **desajuste** en los datos utilizados. Así que, **detectarás muchos errores** muy temprano en el ciclo de desarrollo en lugar de tener que esperar a que los errores se muestren a tus usuarios finales en producción para luego intentar depurar dónde está el problema. ✨ diff --git a/docs/es/docs/advanced/index.md b/docs/es/docs/advanced/index.md index 0626a1563a..f3f4bb85ce 100644 --- a/docs/es/docs/advanced/index.md +++ b/docs/es/docs/advanced/index.md @@ -1,6 +1,6 @@ -# Guía avanzada del usuario +# Guía avanzada del usuario { #advanced-user-guide } -## Funcionalidades adicionales +## Funcionalidades adicionales { #additional-features } El [Tutorial - Guía del usuario](../tutorial/index.md){.internal-link target=_blank} principal debería ser suficiente para darte un recorrido por todas las funcionalidades principales de **FastAPI**. @@ -14,23 +14,8 @@ Y es posible que para tu caso de uso, la solución esté en una de ellas. /// -## Lee primero el Tutorial +## Lee primero el Tutorial { #read-the-tutorial-first } Aún podrías usar la mayoría de las funcionalidades en **FastAPI** con el conocimiento del [Tutorial - Guía del usuario](../tutorial/index.md){.internal-link target=_blank} principal. Y las siguientes secciones asumen que ya lo leíste y que conoces esas ideas principales. - -## Cursos externos - -Aunque el [Tutorial - Guía del usuario](../tutorial/index.md){.internal-link target=_blank} y esta **Guía avanzada del usuario** están escritos como un tutorial guiado (como un libro) y deberían ser suficientes para que **aprendas FastAPI**, podrías querer complementarlo con cursos adicionales. - -O podría ser que simplemente prefieras tomar otros cursos porque se adaptan mejor a tu estilo de aprendizaje. - -Algunos proveedores de cursos ✨ [**sponsorean FastAPI**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨, esto asegura el desarrollo continuo y saludable de FastAPI y su **ecosistema**. - -Y muestra su verdadero compromiso con FastAPI y su **comunidad** (tú), ya que no solo quieren brindarte una **buena experiencia de aprendizaje** sino que también quieren asegurarse de que tengas un **buen y saludable framework**, FastAPI. 🙇 - -Podrías querer probar sus cursos: - -* Talk Python Training -* Desarrollo guiado por pruebas diff --git a/docs/es/docs/advanced/middleware.md b/docs/es/docs/advanced/middleware.md index 0c8c44b882..7eead8ae14 100644 --- a/docs/es/docs/advanced/middleware.md +++ b/docs/es/docs/advanced/middleware.md @@ -1,4 +1,4 @@ -# Middleware Avanzado +# Middleware Avanzado { #advanced-middleware } En el tutorial principal leíste cómo agregar [Middleware Personalizado](../tutorial/middleware.md){.internal-link target=_blank} a tu aplicación. @@ -6,9 +6,9 @@ Y luego también leíste cómo manejar [CORS con el `CORSMiddleware`](../tutoria En esta sección veremos cómo usar otros middlewares. -## Agregando middlewares ASGI +## Agregando middlewares ASGI { #adding-asgi-middlewares } -Como **FastAPI** está basado en Starlette e implementa la especificación ASGI, puedes usar cualquier middleware ASGI. +Como **FastAPI** está basado en Starlette e implementa la especificación ASGI, puedes usar cualquier middleware ASGI. Un middleware no tiene que estar hecho para FastAPI o Starlette para funcionar, siempre que siga la especificación ASGI. @@ -39,7 +39,7 @@ app.add_middleware(UnicornMiddleware, some_config="rainbow") `app.add_middleware()` recibe una clase de middleware como primer argumento y cualquier argumento adicional que se le quiera pasar al middleware. -## Middlewares integrados +## Middlewares integrados { #integrated-middlewares } **FastAPI** incluye varios middlewares para casos de uso común, veremos a continuación cómo usarlos. @@ -51,40 +51,41 @@ Para los próximos ejemplos, también podrías usar `from starlette.middleware.s /// -## `HTTPSRedirectMiddleware` +## `HTTPSRedirectMiddleware` { #httpsredirectmiddleware } Impone que todas las requests entrantes deben ser `https` o `wss`. Cualquier request entrante a `http` o `ws` será redirigida al esquema seguro. -{* ../../docs_src/advanced_middleware/tutorial001.py hl[2,6] *} +{* ../../docs_src/advanced_middleware/tutorial001_py39.py hl[2,6] *} -## `TrustedHostMiddleware` +## `TrustedHostMiddleware` { #trustedhostmiddleware } Impone que todas las requests entrantes tengan correctamente configurado el header `Host`, para proteger contra ataques de HTTP Host Header. -{* ../../docs_src/advanced_middleware/tutorial002.py hl[2,6:8] *} +{* ../../docs_src/advanced_middleware/tutorial002_py39.py hl[2,6:8] *} Se soportan los siguientes argumentos: * `allowed_hosts` - Una list de nombres de dominio que deberían ser permitidos como nombres de host. Se soportan dominios comodín como `*.example.com` para hacer coincidir subdominios. Para permitir cualquier nombre de host, usa `allowed_hosts=["*"]` u omite el middleware. +* `www_redirect` - Si se establece en True, las requests a versiones sin www de los hosts permitidos serán redirigidas a sus equivalentes con www. Por defecto es `True`. Si una request entrante no se valida correctamente, se enviará un response `400`. -## `GZipMiddleware` +## `GZipMiddleware` { #gzipmiddleware } Maneja responses GZip para cualquier request que incluya `"gzip"` en el header `Accept-Encoding`. El middleware manejará tanto responses estándar como en streaming. -{* ../../docs_src/advanced_middleware/tutorial003.py hl[2,6] *} +{* ../../docs_src/advanced_middleware/tutorial003_py39.py hl[2,6] *} Se soportan los siguientes argumentos: * `minimum_size` - No comprimir con GZip responses que sean más pequeñas que este tamaño mínimo en bytes. Por defecto es `500`. * `compresslevel` - Usado durante la compresión GZip. Es un entero que varía de 1 a 9. Por defecto es `9`. Un valor más bajo resulta en una compresión más rápida pero archivos más grandes, mientras que un valor más alto resulta en una compresión más lenta pero archivos más pequeños. -## Otros middlewares +## Otros middlewares { #other-middlewares } Hay muchos otros middlewares ASGI. diff --git a/docs/es/docs/advanced/openapi-callbacks.md b/docs/es/docs/advanced/openapi-callbacks.md index 60d5cb3cc9..caaa70fa89 100644 --- a/docs/es/docs/advanced/openapi-callbacks.md +++ b/docs/es/docs/advanced/openapi-callbacks.md @@ -1,18 +1,18 @@ -# OpenAPI Callbacks +# Callbacks de OpenAPI { #openapi-callbacks } Podrías crear una API con una *path operation* que podría desencadenar un request a una *API externa* creada por alguien más (probablemente el mismo desarrollador que estaría *usando* tu API). -El proceso que ocurre cuando tu aplicación API llama a la *API externa* se llama un "callback". Porque el software que escribió el desarrollador externo envía un request a tu API y luego tu API *responde*, enviando un request a una *API externa* (que probablemente fue creada por el mismo desarrollador). +El proceso que ocurre cuando tu aplicación API llama a la *API externa* se llama un "callback". Porque el software que escribió el desarrollador externo envía un request a tu API y luego tu API hace un *callback*, enviando un request a una *API externa* (que probablemente fue creada por el mismo desarrollador). En este caso, podrías querer documentar cómo esa API externa *debería* verse. Qué *path operation* debería tener, qué cuerpo debería esperar, qué response debería devolver, etc. -## Una aplicación con callbacks +## Una aplicación con callbacks { #an-app-with-callbacks } Veamos todo esto con un ejemplo. Imagina que desarrollas una aplicación que permite crear facturas. -Estas facturas tendrán un `id`, `title` (opcional), `customer`, y `total`. +Estas facturas tendrán un `id`, `title` (opcional), `customer` y `total`. El usuario de tu API (un desarrollador externo) creará una factura en tu API con un request POST. @@ -23,15 +23,15 @@ Luego tu API (imaginemos): * Enviará una notificación de vuelta al usuario de la API (el desarrollador externo). * Esto se hará enviando un request POST (desde *tu API*) a alguna *API externa* proporcionada por ese desarrollador externo (este es el "callback"). -## La aplicación normal de **FastAPI** +## La aplicación normal de **FastAPI** { #the-normal-fastapi-app } -Primero veamos cómo sería la aplicación API normal antes de agregar el callback. +Primero veamos cómo se vería la aplicación API normal antes de agregar el callback. Tendrá una *path operation* que recibirá un cuerpo `Invoice`, y un parámetro de query `callback_url` que contendrá la URL para el callback. Esta parte es bastante normal, probablemente ya estés familiarizado con la mayor parte del código: -{* ../../docs_src/openapi_callbacks/tutorial001.py hl[9:13,36:53] *} +{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[7:11,34:51] *} /// tip | Consejo @@ -39,9 +39,9 @@ El parámetro de query `callback_url` utiliza un tipo expresión OpenAPI 3 (ver más abajo) donde puede usar variables con parámetros y partes del request original enviado a *tu API*. -### La expresión del path del callback +### La expresión del path del callback { #the-callback-path-expression } El *path* del callback puede tener una expresión OpenAPI 3 que puede contener partes del request original enviado a *tu API*. @@ -134,7 +134,7 @@ con un JSON body de: } ``` -luego *tu API* procesará la factura, y en algún momento después, enviará un request de callback al `callback_url` (la *API externa*): +luego *tu API* procesará la factura y, en algún momento después, enviará un request de callback al `callback_url` (la *API externa*): ``` https://www.external.org/events/invoices/2expen51ve @@ -163,13 +163,13 @@ Observa cómo la URL del callback utilizada contiene la URL recibida como parám /// -### Agregar el router de callback +### Agrega el router de callback { #add-the-callback-router } -En este punto tienes las *path operation(s)* del callback necesarias (las que el *desarrollador externo* debería implementar en la *API externa*) en el router de callback que creaste antes. +En este punto tienes las *path operation(s)* del callback necesarias (las que el *desarrollador externo* debería implementar en la *API externa*) en el router de callback que creaste arriba. Ahora usa el parámetro `callbacks` en el *decorador de path operation de tu API* para pasar el atributo `.routes` (que en realidad es solo un `list` de rutas/*path operations*) de ese router de callback: -{* ../../docs_src/openapi_callbacks/tutorial001.py hl[35] *} +{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[33] *} /// tip | Consejo @@ -177,7 +177,7 @@ Observa que no estás pasando el router en sí (`invoices_callback_router`) a `c /// -### Revisa la documentación +### Revisa la documentación { #check-the-docs } Ahora puedes iniciar tu aplicación e ir a http://127.0.0.1:8000/docs. diff --git a/docs/es/docs/advanced/openapi-webhooks.md b/docs/es/docs/advanced/openapi-webhooks.md index 01235b3ab9..c358a14008 100644 --- a/docs/es/docs/advanced/openapi-webhooks.md +++ b/docs/es/docs/advanced/openapi-webhooks.md @@ -1,4 +1,4 @@ -# Webhooks de OpenAPI +# Webhooks de OpenAPI { #openapi-webhooks } Hay casos donde quieres decirle a los **usuarios** de tu API que tu aplicación podría llamar a *su* aplicación (enviando una request) con algunos datos, normalmente para **notificar** de algún tipo de **evento**. @@ -6,7 +6,7 @@ Esto significa que en lugar del proceso normal de tus usuarios enviando requests Esto normalmente se llama un **webhook**. -## Pasos de los webhooks +## Pasos de los webhooks { #webhooks-steps } El proceso normalmente es que **tú defines** en tu código cuál es el mensaje que enviarás, el **body de la request**. @@ -16,7 +16,7 @@ Y **tus usuarios** definen de alguna manera (por ejemplo en un panel web en alg Toda la **lógica** sobre cómo registrar los URLs para webhooks y el código para realmente enviar esas requests depende de ti. Lo escribes como quieras en **tu propio código**. -## Documentando webhooks con **FastAPI** y OpenAPI +## Documentando webhooks con **FastAPI** y OpenAPI { #documenting-webhooks-with-fastapi-and-openapi } Con **FastAPI**, usando OpenAPI, puedes definir los nombres de estos webhooks, los tipos de operaciones HTTP que tu aplicación puede enviar (por ejemplo, `POST`, `PUT`, etc.) y los **bodies** de las requests que tu aplicación enviaría. @@ -28,11 +28,11 @@ Los webhooks están disponibles en OpenAPI 3.1.0 y superiores, soportados por Fa /// -## Una aplicación con webhooks +## Una aplicación con webhooks { #an-app-with-webhooks } Cuando creas una aplicación de **FastAPI**, hay un atributo `webhooks` que puedes usar para definir *webhooks*, de la misma manera que definirías *path operations*, por ejemplo con `@app.webhooks.post()`. -{* ../../docs_src/openapi_webhooks/tutorial001.py hl[9:13,36:53] *} +{* ../../docs_src/openapi_webhooks/tutorial001_py39.py hl[9:13,36:53] *} Los webhooks que defines terminarán en el esquema de **OpenAPI** y en la interfaz automática de **documentación**. @@ -46,7 +46,7 @@ Nota que con los webhooks en realidad no estás declarando un *path* (como `/ite Esto es porque se espera que **tus usuarios** definan el actual **URL path** donde quieren recibir la request del webhook de alguna otra manera (por ejemplo, un panel web). -### Revisa la documentación +### Revisa la documentación { #check-the-docs } Ahora puedes iniciar tu app e ir a http://127.0.0.1:8000/docs. diff --git a/docs/es/docs/advanced/path-operation-advanced-configuration.md b/docs/es/docs/advanced/path-operation-advanced-configuration.md index 2b20819aa3..ea58a300ad 100644 --- a/docs/es/docs/advanced/path-operation-advanced-configuration.md +++ b/docs/es/docs/advanced/path-operation-advanced-configuration.md @@ -1,6 +1,6 @@ -# Configuración Avanzada de Path Operation +# Configuración Avanzada de Path Operation { #path-operation-advanced-configuration } -## operationId de OpenAPI +## operationId de OpenAPI { #openapi-operationid } /// warning | Advertencia @@ -10,17 +10,17 @@ Si no eres un "experto" en OpenAPI, probablemente no necesites esto. Puedes establecer el `operationId` de OpenAPI para ser usado en tu *path operation* con el parámetro `operation_id`. -Tienes que asegurarte de que sea único para cada operación. +Tendrías que asegurarte de que sea único para cada operación. -{* ../../docs_src/path_operation_advanced_configuration/tutorial001.py hl[6] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial001_py39.py hl[6] *} -### Usar el nombre de la *función de path operation* como el operationId +### Usar el nombre de la *path operation function* como el operationId { #using-the-path-operation-function-name-as-the-operationid } Si quieres usar los nombres de las funciones de tus APIs como `operationId`s, puedes iterar sobre todas ellas y sobrescribir el `operation_id` de cada *path operation* usando su `APIRoute.name`. Deberías hacerlo después de agregar todas tus *path operations*. -{* ../../docs_src/path_operation_advanced_configuration/tutorial002.py hl[2, 12:21, 24] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial002_py39.py hl[2, 12:21, 24] *} /// tip | Consejo @@ -30,29 +30,29 @@ Si llamas manualmente a `app.openapi()`, deberías actualizar los `operationId`s /// warning | Advertencia -Si haces esto, tienes que asegurarte de que cada una de tus *funciones de path operation* tenga un nombre único. +Si haces esto, tienes que asegurarte de que cada una de tus *path operation functions* tenga un nombre único. Incluso si están en diferentes módulos (archivos de Python). /// -## Excluir de OpenAPI +## Excluir de OpenAPI { #exclude-from-openapi } Para excluir una *path operation* del esquema OpenAPI generado (y por lo tanto, de los sistemas de documentación automática), utiliza el parámetro `include_in_schema` y configúralo en `False`: -{* ../../docs_src/path_operation_advanced_configuration/tutorial003.py hl[6] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial003_py39.py hl[6] *} -## Descripción avanzada desde el docstring +## Descripción avanzada desde el docstring { #advanced-description-from-docstring } -Puedes limitar las líneas usadas del docstring de una *función de path operation* para OpenAPI. +Puedes limitar las líneas usadas del docstring de una *path operation function* para OpenAPI. -Añadir un `\f` (un carácter de separación de página escapado) hace que **FastAPI** trunque la salida usada para OpenAPI en este punto. +Añadir un `\f` (un carácter "form feed" escapado) hace que **FastAPI** trunque la salida usada para OpenAPI en este punto. No aparecerá en la documentación, pero otras herramientas (como Sphinx) podrán usar el resto. -{* ../../docs_src/path_operation_advanced_configuration/tutorial004.py hl[19:29] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial004_py310.py hl[17:27] *} -## Responses Adicionales +## Responses Adicionales { #additional-responses } Probablemente has visto cómo declarar el `response_model` y el `status_code` para una *path operation*. @@ -62,11 +62,11 @@ También puedes declarar responses adicionales con sus modelos, códigos de esta Hay un capítulo entero en la documentación sobre ello, puedes leerlo en [Responses Adicionales en OpenAPI](additional-responses.md){.internal-link target=_blank}. -## OpenAPI Extra +## OpenAPI Extra { #openapi-extra } Cuando declaras una *path operation* en tu aplicación, **FastAPI** genera automáticamente los metadatos relevantes sobre esa *path operation* para incluirlos en el esquema de OpenAPI. -/// note | Nota +/// note | Detalles técnicos En la especificación de OpenAPI se llama el Objeto de Operación. @@ -88,11 +88,11 @@ Si solo necesitas declarar responses adicionales, una forma más conveniente de Puedes extender el esquema de OpenAPI para una *path operation* usando el parámetro `openapi_extra`. -### Extensiones de OpenAPI +### Extensiones de OpenAPI { #openapi-extensions } Este `openapi_extra` puede ser útil, por ejemplo, para declarar [Extensiones de OpenAPI](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#specificationExtensions): -{* ../../docs_src/path_operation_advanced_configuration/tutorial005.py hl[6] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial005_py39.py hl[6] *} Si abres la documentación automática de la API, tu extensión aparecerá en la parte inferior de la *path operation* específica. @@ -129,7 +129,7 @@ Y si ves el OpenAPI resultante (en `/openapi.json` en tu API), verás tu extensi } ``` -### Esquema de *path operation* personalizada de OpenAPI +### Esquema de *path operation* personalizada de OpenAPI { #custom-openapi-path-operation-schema } El diccionario en `openapi_extra` se combinará profundamente con el esquema de OpenAPI generado automáticamente para la *path operation*. @@ -139,61 +139,29 @@ Por ejemplo, podrías decidir leer y validar el request con tu propio código, s Podrías hacer eso con `openapi_extra`: -{* ../../docs_src/path_operation_advanced_configuration/tutorial006.py hl[19:36, 39:40] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial006_py39.py hl[19:36, 39:40] *} -En este ejemplo, no declaramos ningún modelo Pydantic. De hecho, el cuerpo del request ni siquiera se parse como JSON, se lee directamente como `bytes`, y la función `magic_data_reader()` sería la encargada de parsearlo de alguna manera. +En este ejemplo, no declaramos ningún modelo Pydantic. De hecho, el request body ni siquiera se parse como JSON, se lee directamente como `bytes`, y la función `magic_data_reader()` sería la encargada de parsearlo de alguna manera. -Sin embargo, podemos declarar el esquema esperado para el cuerpo del request. +Sin embargo, podemos declarar el esquema esperado para el request body. -### Tipo de contenido personalizado de OpenAPI +### Tipo de contenido personalizado de OpenAPI { #custom-openapi-content-type } -Usando este mismo truco, podrías usar un modelo Pydantic para definir el esquema JSON que luego se incluye en la sección personalizada del esquema OpenAPI para la *path operation*. +Usando este mismo truco, podrías usar un modelo Pydantic para definir el JSON Schema que luego se incluye en la sección personalizada del esquema OpenAPI para la *path operation*. Y podrías hacer esto incluso si el tipo de datos en el request no es JSON. -Por ejemplo, en esta aplicación no usamos la funcionalidad integrada de FastAPI para extraer el esquema JSON de los modelos Pydantic ni la validación automática para JSON. De hecho, estamos declarando el tipo de contenido del request como YAML, no JSON: +Por ejemplo, en esta aplicación no usamos la funcionalidad integrada de FastAPI para extraer el JSON Schema de los modelos Pydantic ni la validación automática para JSON. De hecho, estamos declarando el tipo de contenido del request como YAML, no JSON: -//// tab | Pydantic v2 +{* ../../docs_src/path_operation_advanced_configuration/tutorial007_py39.py hl[15:20, 22] *} -{* ../../docs_src/path_operation_advanced_configuration/tutorial007.py hl[17:22, 24] *} - -//// - -//// tab | Pydantic v1 - -{* ../../docs_src/path_operation_advanced_configuration/tutorial007_pv1.py hl[17:22, 24] *} - -//// - -/// info | Información - -En la versión 1 de Pydantic el método para obtener el esquema JSON para un modelo se llamaba `Item.schema()`, en la versión 2 de Pydantic, el método se llama `Item.model_json_schema()`. - -/// - -Sin embargo, aunque no estamos usando la funcionalidad integrada por defecto, aún estamos usando un modelo Pydantic para generar manualmente el esquema JSON para los datos que queremos recibir en YAML. +Sin embargo, aunque no estamos usando la funcionalidad integrada por defecto, aún estamos usando un modelo Pydantic para generar manualmente el JSON Schema para los datos que queremos recibir en YAML. Luego usamos el request directamente, y extraemos el cuerpo como `bytes`. Esto significa que FastAPI ni siquiera intentará parsear la carga útil del request como JSON. Y luego en nuestro código, parseamos ese contenido YAML directamente, y nuevamente estamos usando el mismo modelo Pydantic para validar el contenido YAML: -//// tab | Pydantic v2 - -{* ../../docs_src/path_operation_advanced_configuration/tutorial007.py hl[26:33] *} - -//// - -//// tab | Pydantic v1 - -{* ../../docs_src/path_operation_advanced_configuration/tutorial007_pv1.py hl[26:33] *} - -//// - -/// info | Información - -En la versión 1 de Pydantic el método para parsear y validar un objeto era `Item.parse_obj()`, en la versión 2 de Pydantic, el método se llama `Item.model_validate()`. - -/// +{* ../../docs_src/path_operation_advanced_configuration/tutorial007_py39.py hl[24:31] *} /// tip | Consejo diff --git a/docs/es/docs/advanced/response-change-status-code.md b/docs/es/docs/advanced/response-change-status-code.md index e0889c4743..940f1dd3ff 100644 --- a/docs/es/docs/advanced/response-change-status-code.md +++ b/docs/es/docs/advanced/response-change-status-code.md @@ -1,10 +1,10 @@ -# Response - Cambiar Código de Estado +# Response - Cambiar Código de Estado { #response-change-status-code } Probablemente leíste antes que puedes establecer un [Código de Estado de Response](../tutorial/response-status-code.md){.internal-link target=_blank} por defecto. Pero en algunos casos necesitas devolver un código de estado diferente al predeterminado. -## Caso de uso +## Caso de uso { #use-case } Por ejemplo, imagina que quieres devolver un código de estado HTTP de "OK" `200` por defecto. @@ -14,13 +14,13 @@ Pero todavía quieres poder filtrar y convertir los datos que devuelves con un ` Para esos casos, puedes usar un parámetro `Response`. -## Usa un parámetro `Response` +## Usa un parámetro `Response` { #use-a-response-parameter } -Puedes declarar un parámetro de tipo `Response` en tu *función de path operation* (como puedes hacer para cookies y headers). +Puedes declarar un parámetro de tipo `Response` en tu *path operation function* (como puedes hacer para cookies y headers). Y luego puedes establecer el `status_code` en ese objeto de response *temporal*. -{* ../../docs_src/response_change_status_code/tutorial001.py hl[1,9,12] *} +{* ../../docs_src/response_change_status_code/tutorial001_py39.py hl[1,9,12] *} Y luego puedes devolver cualquier objeto que necesites, como lo harías normalmente (un `dict`, un modelo de base de datos, etc.). diff --git a/docs/es/docs/advanced/response-cookies.md b/docs/es/docs/advanced/response-cookies.md index 05b78528e0..550a5d97a7 100644 --- a/docs/es/docs/advanced/response-cookies.md +++ b/docs/es/docs/advanced/response-cookies.md @@ -1,12 +1,12 @@ -# Cookies de Response +# Cookies de Response { #response-cookies } -## Usar un parámetro `Response` +## Usar un parámetro `Response` { #use-a-response-parameter } Puedes declarar un parámetro de tipo `Response` en tu *path operation function*. Y luego puedes establecer cookies en ese objeto de response *temporal*. -{* ../../docs_src/response_cookies/tutorial002.py hl[1, 8:9] *} +{* ../../docs_src/response_cookies/tutorial002_py39.py hl[1, 8:9] *} Y entonces puedes devolver cualquier objeto que necesites, como normalmente lo harías (un `dict`, un modelo de base de datos, etc). @@ -16,7 +16,7 @@ Y si declaraste un `response_model`, todavía se utilizará para filtrar y conve También puedes declarar el parámetro `Response` en las dependencias, y establecer cookies (y headers) en ellas. -## Devolver una `Response` directamente +## Devolver una `Response` directamente { #return-a-response-directly } También puedes crear cookies al devolver una `Response` directamente en tu código. @@ -24,7 +24,7 @@ Para hacer eso, puedes crear un response como se describe en [Devolver un Respon Luego establece Cookies en ella, y luego devuélvela: -{* ../../docs_src/response_cookies/tutorial001.py hl[10:12] *} +{* ../../docs_src/response_cookies/tutorial001_py39.py hl[10:12] *} /// tip | Consejo @@ -36,7 +36,7 @@ Y también que no estés enviando ningún dato que debería haber sido filtrado /// -### Más información +### Más información { #more-info } /// note | Detalles Técnicos diff --git a/docs/es/docs/advanced/response-directly.md b/docs/es/docs/advanced/response-directly.md index 8594011d61..2da4e84e70 100644 --- a/docs/es/docs/advanced/response-directly.md +++ b/docs/es/docs/advanced/response-directly.md @@ -1,4 +1,4 @@ -# Devolver una Response Directamente +# Devolver una Response Directamente { #return-a-response-directly } Cuando creas una *path operation* en **FastAPI**, normalmente puedes devolver cualquier dato desde ella: un `dict`, una `list`, un modelo de Pydantic, un modelo de base de datos, etc. @@ -10,7 +10,7 @@ Pero puedes devolver un `JSONResponse` directamente desde tus *path operations*. Esto podría ser útil, por ejemplo, para devolver headers o cookies personalizados. -## Devolver una `Response` +## Devolver una `Response` { #return-a-response } De hecho, puedes devolver cualquier `Response` o cualquier subclase de ella. @@ -26,7 +26,7 @@ No hará ninguna conversión de datos con los modelos de Pydantic, no convertir Esto te da mucha flexibilidad. Puedes devolver cualquier tipo de datos, sobrescribir cualquier declaración o validación de datos, etc. -## Usar el `jsonable_encoder` en una `Response` +## Usar el `jsonable_encoder` en una `Response` { #using-the-jsonable-encoder-in-a-response } Como **FastAPI** no realiza cambios en una `Response` que devuelves, tienes que asegurarte de que sus contenidos estén listos para ello. @@ -34,9 +34,9 @@ Por ejemplo, no puedes poner un modelo de Pydantic en un `JSONResponse` sin prim Para esos casos, puedes usar el `jsonable_encoder` para convertir tus datos antes de pasarlos a un response: -{* ../../docs_src/response_directly/tutorial001.py hl[6:7,21:22] *} +{* ../../docs_src/response_directly/tutorial001_py310.py hl[5:6,20:21] *} -/// note | Nota +/// note | Detalles técnicos También podrías usar `from starlette.responses import JSONResponse`. @@ -44,7 +44,7 @@ También podrías usar `from starlette.responses import JSONResponse`. /// -## Devolver una `Response` personalizada +## Devolver una `Response` personalizada { #returning-a-custom-response } El ejemplo anterior muestra todas las partes que necesitas, pero aún no es muy útil, ya que podrías haber devuelto el `item` directamente, y **FastAPI** lo colocaría en un `JSONResponse` por ti, convirtiéndolo a un `dict`, etc. Todo eso por defecto. @@ -54,9 +54,9 @@ Digamos que quieres devolver un response en usando el prefijo 'X-'. +Ten en cuenta que los headers propietarios personalizados se pueden agregar usando el prefijo `X-`. Pero si tienes headers personalizados que quieres que un cliente en un navegador pueda ver, necesitas agregarlos a tus configuraciones de CORS (leer más en [CORS (Cross-Origin Resource Sharing)](../tutorial/cors.md){.internal-link target=_blank}), usando el parámetro `expose_headers` documentado en la documentación CORS de Starlette. diff --git a/docs/es/docs/advanced/security/http-basic-auth.md b/docs/es/docs/advanced/security/http-basic-auth.md index 629e6c50a0..440c081e06 100644 --- a/docs/es/docs/advanced/security/http-basic-auth.md +++ b/docs/es/docs/advanced/security/http-basic-auth.md @@ -1,4 +1,4 @@ -# HTTP Basic Auth +# HTTP Basic Auth { #http-basic-auth } Para los casos más simples, puedes usar HTTP Basic Auth. @@ -12,7 +12,7 @@ Eso le dice al navegador que muestre el prompt integrado para un nombre de usuar Luego, cuando escribes ese nombre de usuario y contraseña, el navegador los envía automáticamente en el header. -## Simple HTTP Basic Auth +## Simple HTTP Basic Auth { #simple-http-basic-auth } * Importa `HTTPBasic` y `HTTPBasicCredentials`. * Crea un "esquema de `security`" usando `HTTPBasic`. @@ -26,7 +26,7 @@ Cuando intentas abrir la URL por primera vez (o haces clic en el botón "Execute -## Revisa el nombre de usuario +## Revisa el nombre de usuario { #check-the-username } Aquí hay un ejemplo más completo. @@ -46,13 +46,13 @@ Esto sería similar a: ```Python if not (credentials.username == "stanleyjobson") or not (credentials.password == "swordfish"): - # Return some error + # Devuelve algún error ... ``` Pero al usar `secrets.compare_digest()` será seguro contra un tipo de ataques llamados "timing attacks". -### Timing Attacks +### Timing attacks { #timing-attacks } ¿Pero qué es un "timing attack"? @@ -80,19 +80,19 @@ if "stanleyjobsox" == "stanleyjobson" and "love123" == "swordfish": Python tendrá que comparar todo `stanleyjobso` en ambos `stanleyjobsox` y `stanleyjobson` antes de darse cuenta de que ambas strings no son las mismas. Así que tomará algunos microsegundos extra para responder "Nombre de usuario o contraseña incorrectos". -#### El tiempo de respuesta ayuda a los atacantes +#### El tiempo de respuesta ayuda a los atacantes { #the-time-to-answer-helps-the-attackers } En ese punto, al notar que el servidor tardó algunos microsegundos más en enviar el response "Nombre de usuario o contraseña incorrectos", los atacantes sabrán que acertaron en _algo_, algunas de las letras iniciales eran correctas. Y luego pueden intentar de nuevo sabiendo que probablemente es algo más similar a `stanleyjobsox` que a `johndoe`. -#### Un ataque "profesional" +#### Un ataque "profesional" { #a-professional-attack } Por supuesto, los atacantes no intentarían todo esto a mano, escribirían un programa para hacerlo, posiblemente con miles o millones de pruebas por segundo. Y obtendrían solo una letra correcta adicional a la vez. Pero haciendo eso, en algunos minutos u horas, los atacantes habrían adivinado el nombre de usuario y la contraseña correctos, con la "ayuda" de nuestra aplicación, solo usando el tiempo tomado para responder. -#### Arréglalo con `secrets.compare_digest()` +#### Arréglalo con `secrets.compare_digest()` { #fix-it-with-secrets-compare-digest } Pero en nuestro código estamos usando realmente `secrets.compare_digest()`. @@ -100,7 +100,7 @@ En resumen, tomará el mismo tiempo comparar `stanleyjobsox` con `stanleyjobson` De esa manera, usando `secrets.compare_digest()` en el código de tu aplicación, será seguro contra todo este rango de ataques de seguridad. -### Devuelve el error +### Devuelve el error { #return-the-error } Después de detectar que las credenciales son incorrectas, regresa un `HTTPException` con un código de estado 401 (el mismo que se devuelve cuando no se proporcionan credenciales) y agrega el header `WWW-Authenticate` para que el navegador muestre el prompt de inicio de sesión nuevamente: diff --git a/docs/es/docs/advanced/security/index.md b/docs/es/docs/advanced/security/index.md index e4ccb59781..8b3e67fac4 100644 --- a/docs/es/docs/advanced/security/index.md +++ b/docs/es/docs/advanced/security/index.md @@ -1,6 +1,6 @@ -# Seguridad Avanzada +# Seguridad Avanzada { #advanced-security } -## Funcionalidades Adicionales +## Funcionalidades Adicionales { #additional-features } Hay algunas funcionalidades extra para manejar la seguridad aparte de las cubiertas en el [Tutorial - Guía del Usuario: Seguridad](../../tutorial/security/index.md){.internal-link target=_blank}. @@ -12,8 +12,8 @@ Y es posible que para tu caso de uso, la solución esté en una de ellas. /// -## Lee primero el Tutorial +## Lee primero el Tutorial { #read-the-tutorial-first } -Las siguientes secciones asumen que ya leíste el [Tutorial - Guía del Usuario: Seguridad](../../tutorial/security/index.md){.internal-link target=_blank}. +Las siguientes secciones asumen que ya leíste el [Tutorial - Guía del Usuario: Seguridad](../../tutorial/security/index.md){.internal-link target=_blank} principal. Todas están basadas en los mismos conceptos, pero permiten algunas funcionalidades adicionales. diff --git a/docs/es/docs/advanced/security/oauth2-scopes.md b/docs/es/docs/advanced/security/oauth2-scopes.md index a8d2f20b68..4e4580fde3 100644 --- a/docs/es/docs/advanced/security/oauth2-scopes.md +++ b/docs/es/docs/advanced/security/oauth2-scopes.md @@ -1,4 +1,4 @@ -# Scopes de OAuth2 +# Scopes de OAuth2 { #oauth2-scopes } Puedes usar scopes de OAuth2 directamente con **FastAPI**, están integrados para funcionar de manera fluida. @@ -26,7 +26,7 @@ Pero si sabes que lo necesitas, o tienes curiosidad, sigue leyendo. /// -## Scopes de OAuth2 y OpenAPI +## Scopes de OAuth2 y OpenAPI { #oauth2-scopes-and-openapi } La especificación de OAuth2 define "scopes" como una lista de strings separados por espacios. @@ -58,15 +58,15 @@ Para OAuth2 son solo strings. /// -## Vista global +## Vista global { #global-view } Primero, echemos un vistazo rápido a las partes que cambian desde los ejemplos en el **Tutorial - User Guide** principal para [OAuth2 con Password (y hashing), Bearer con tokens JWT](../../tutorial/security/oauth2-jwt.md){.internal-link target=_blank}. Ahora usando scopes de OAuth2: -{* ../../docs_src/security/tutorial005_an_py310.py hl[5,9,13,47,65,106,108:116,122:125,129:135,140,156] *} +{* ../../docs_src/security/tutorial005_an_py310.py hl[5,9,13,47,65,106,108:116,122:126,130:136,141,157] *} Ahora revisemos esos cambios paso a paso. -## Esquema de seguridad OAuth2 +## Esquema de seguridad OAuth2 { #oauth2-security-scheme } El primer cambio es que ahora estamos declarando el esquema de seguridad OAuth2 con dos scopes disponibles, `me` y `items`. @@ -82,7 +82,7 @@ Este es el mismo mecanismo utilizado cuando das permisos al iniciar sesión con -## Token JWT con scopes +## Token JWT con scopes { #jwt-token-with-scopes } Ahora, modifica la *path operation* del token para devolver los scopes solicitados. @@ -98,9 +98,9 @@ Pero en tu aplicación, por seguridad, deberías asegurarte de añadir solo los /// -{* ../../docs_src/security/tutorial005_an_py310.py hl[156] *} +{* ../../docs_src/security/tutorial005_an_py310.py hl[157] *} -## Declarar scopes en *path operations* y dependencias +## Declarar scopes en *path operations* y dependencias { #declare-scopes-in-path-operations-and-dependencies } Ahora declaramos que la *path operation* para `/users/me/items/` requiere el scope `items`. @@ -124,7 +124,7 @@ Lo estamos haciendo aquí para demostrar cómo **FastAPI** maneja scopes declara /// -{* ../../docs_src/security/tutorial005_an_py310.py hl[5,140,171] *} +{* ../../docs_src/security/tutorial005_an_py310.py hl[5,141,172] *} /// info | Información Técnica @@ -136,7 +136,7 @@ Pero cuando importas `Query`, `Path`, `Depends`, `Security` y otros de `fastapi` /// -## Usar `SecurityScopes` +## Usar `SecurityScopes` { #use-securityscopes } Ahora actualiza la dependencia `get_current_user`. @@ -152,7 +152,7 @@ Esta clase `SecurityScopes` es similar a `Request` (`Request` se usó para obten {* ../../docs_src/security/tutorial005_an_py310.py hl[9,106] *} -## Usar los `scopes` +## Usar los `scopes` { #use-the-scopes } El parámetro `security_scopes` será del tipo `SecurityScopes`. @@ -166,7 +166,7 @@ En esta excepción, incluimos los scopes requeridos (si los hay) como un string {* ../../docs_src/security/tutorial005_an_py310.py hl[106,108:116] *} -## Verificar el `username` y la forma de los datos +## Verificar el `username` y la forma de los datos { #verify-the-username-and-data-shape } Verificamos que obtenemos un `username`, y extraemos los scopes. @@ -180,17 +180,17 @@ En lugar de, por ejemplo, un `dict`, o algo más, ya que podría romper la aplic También verificamos que tenemos un usuario con ese username, y si no, lanzamos esa misma excepción que creamos antes. -{* ../../docs_src/security/tutorial005_an_py310.py hl[47,117:128] *} +{* ../../docs_src/security/tutorial005_an_py310.py hl[47,117:129] *} -## Verificar los `scopes` +## Verificar los `scopes` { #verify-the-scopes } Ahora verificamos que todos los scopes requeridos, por esta dependencia y todos los dependientes (incluyendo *path operations*), estén incluidos en los scopes proporcionados en el token recibido, de lo contrario, lanzamos una `HTTPException`. Para esto, usamos `security_scopes.scopes`, que contiene una `list` con todos estos scopes como `str`. -{* ../../docs_src/security/tutorial005_an_py310.py hl[129:135] *} +{* ../../docs_src/security/tutorial005_an_py310.py hl[130:136] *} -## Árbol de dependencias y scopes +## Árbol de dependencias y scopes { #dependency-tree-and-scopes } Revisemos de nuevo este árbol de dependencias y los scopes. @@ -223,7 +223,7 @@ Todo depende de los `scopes` declarados en cada *path operation* y cada dependen /// -## Más detalles sobre `SecurityScopes` +## Más detalles sobre `SecurityScopes` { #more-details-about-securityscopes } Puedes usar `SecurityScopes` en cualquier punto, y en múltiples lugares, no tiene que ser en la dependencia "raíz". @@ -233,7 +233,7 @@ Debido a que `SecurityScopes` tendrá todos los scopes declarados por dependient Serán verificados independientemente para cada *path operation*. -## Revisa +## Revisa { #check-it } Si abres la documentación de la API, puedes autenticarte y especificar qué scopes deseas autorizar. @@ -245,7 +245,7 @@ Y si seleccionas el scope `me` pero no el scope `items`, podrás acceder a `/use Eso es lo que pasaría a una aplicación de terceros que intentara acceder a una de estas *path operations* con un token proporcionado por un usuario, dependiendo de cuántos permisos el usuario otorgó a la aplicación. -## Acerca de las integraciones de terceros +## Acerca de las integraciones de terceros { #about-third-party-integrations } En este ejemplo estamos usando el flujo de OAuth2 "password". @@ -269,6 +269,6 @@ Pero al final, están implementando el mismo estándar OAuth2. **FastAPI** incluye utilidades para todos estos flujos de autenticación OAuth2 en `fastapi.security.oauth2`. -## `Security` en `dependencies` del decorador +## `Security` en `dependencies` del decorador { #security-in-decorator-dependencies } De la misma manera que puedes definir una `list` de `Depends` en el parámetro `dependencies` del decorador (como se explica en [Dependencias en decoradores de path operation](../../tutorial/dependencies/dependencies-in-path-operation-decorators.md){.internal-link target=_blank}), también podrías usar `Security` con `scopes` allí. diff --git a/docs/es/docs/advanced/settings.md b/docs/es/docs/advanced/settings.md index 7e591cc01b..a2d749103e 100644 --- a/docs/es/docs/advanced/settings.md +++ b/docs/es/docs/advanced/settings.md @@ -1,4 +1,4 @@ -# Configuraciones y Variables de Entorno +# Configuraciones y Variables de Entorno { #settings-and-environment-variables } En muchos casos, tu aplicación podría necesitar algunas configuraciones o ajustes externos, por ejemplo, claves secretas, credenciales de base de datos, credenciales para servicios de correo electrónico, etc. @@ -12,17 +12,17 @@ Para entender las variables de entorno, puedes leer [Variables de Entorno](../en /// -## Tipos y validación +## Tipos y validación { #types-and-validation } Estas variables de entorno solo pueden manejar strings de texto, ya que son externas a Python y tienen que ser compatibles con otros programas y el resto del sistema (e incluso con diferentes sistemas operativos, como Linux, Windows, macOS). Eso significa que cualquier valor leído en Python desde una variable de entorno será un `str`, y cualquier conversión a un tipo diferente o cualquier validación tiene que hacerse en código. -## Pydantic `Settings` +## Pydantic `Settings` { #pydantic-settings } Afortunadamente, Pydantic proporciona una gran utilidad para manejar estas configuraciones provenientes de variables de entorno con Pydantic: Settings management. -### Instalar `pydantic-settings` +### Instalar `pydantic-settings` { #install-pydantic-settings } Primero, asegúrate de crear tu [entorno virtual](../virtual-environments.md){.internal-link target=_blank}, actívalo y luego instala el paquete `pydantic-settings`: @@ -46,13 +46,7 @@ $ pip install "fastapi[all]"
-/// info | Información - -En Pydantic v1 venía incluido con el paquete principal. Ahora se distribuye como este paquete independiente para que puedas elegir si instalarlo o no si no necesitas esa funcionalidad. - -/// - -### Crear el objeto `Settings` +### Crear el objeto `Settings` { #create-the-settings-object } Importa `BaseSettings` de Pydantic y crea una sub-clase, muy similar a un modelo de Pydantic. @@ -60,23 +54,7 @@ De la misma forma que con los modelos de Pydantic, declaras atributos de clase c Puedes usar todas las mismas funcionalidades de validación y herramientas que usas para los modelos de Pydantic, como diferentes tipos de datos y validaciones adicionales con `Field()`. -//// tab | Pydantic v2 - -{* ../../docs_src/settings/tutorial001.py hl[2,5:8,11] *} - -//// - -//// tab | Pydantic v1 - -/// info | Información - -En Pydantic v1 importarías `BaseSettings` directamente desde `pydantic` en lugar de desde `pydantic_settings`. - -/// - -{* ../../docs_src/settings/tutorial001_pv1.py hl[2,5:8,11] *} - -//// +{* ../../docs_src/settings/tutorial001_py39.py hl[2,5:8,11] *} /// tip | Consejo @@ -84,17 +62,17 @@ Si quieres algo rápido para copiar y pegar, no uses este ejemplo, usa el últim /// -Luego, cuando creas una instance de esa clase `Settings` (en este caso, en el objeto `settings`), Pydantic leerá las variables de entorno de una manera indiferente a mayúsculas y minúsculas, por lo que una variable en mayúsculas `APP_NAME` aún será leída para el atributo `app_name`. +Luego, cuando creas un instance de esa clase `Settings` (en este caso, en el objeto `settings`), Pydantic leerá las variables de entorno de una manera indiferente a mayúsculas y minúsculas, por lo que una variable en mayúsculas `APP_NAME` aún será leída para el atributo `app_name`. Luego convertirá y validará los datos. Así que, cuando uses ese objeto `settings`, tendrás datos de los tipos que declaraste (por ejemplo, `items_per_user` será un `int`). -### Usar el `settings` +### Usar el `settings` { #use-the-settings } Luego puedes usar el nuevo objeto `settings` en tu aplicación: -{* ../../docs_src/settings/tutorial001.py hl[18:20] *} +{* ../../docs_src/settings/tutorial001_py39.py hl[18:20] *} -### Ejecutar el servidor +### Ejecutar el servidor { #run-the-server } Luego, ejecutarías el servidor pasando las configuraciones como variables de entorno, por ejemplo, podrías establecer un `ADMIN_EMAIL` y `APP_NAME` con: @@ -110,7 +88,7 @@ $ ADMIN_EMAIL="deadpool@example.com" APP_NAME="ChimichangApp" fastapi run main.p /// tip | Consejo -Para establecer múltiples variables de entorno para un solo comando, simplemente sepáralas con un espacio y ponlas todas antes del comando. +Para establecer múltiples env vars para un solo comando, simplemente sepáralas con un espacio y ponlas todas antes del comando. /// @@ -120,17 +98,17 @@ El `app_name` sería `"ChimichangApp"`. Y el `items_per_user` mantendría su valor por defecto de `50`. -## Configuraciones en otro módulo +## Configuraciones en otro módulo { #settings-in-another-module } Podrías poner esas configuraciones en otro archivo de módulo como viste en [Aplicaciones Más Grandes - Múltiples Archivos](../tutorial/bigger-applications.md){.internal-link target=_blank}. Por ejemplo, podrías tener un archivo `config.py` con: -{* ../../docs_src/settings/app01/config.py *} +{* ../../docs_src/settings/app01_py39/config.py *} Y luego usarlo en un archivo `main.py`: -{* ../../docs_src/settings/app01/main.py hl[3,11:13] *} +{* ../../docs_src/settings/app01_py39/main.py hl[3,11:13] *} /// tip | Consejo @@ -138,21 +116,21 @@ También necesitarías un archivo `__init__.py` como viste en [Aplicaciones Más /// -## Configuraciones en una dependencia +## Configuraciones en una dependencia { #settings-in-a-dependency } En algunas ocasiones podría ser útil proporcionar las configuraciones desde una dependencia, en lugar de tener un objeto global con `settings` que se use en todas partes. Esto podría ser especialmente útil durante las pruebas, ya que es muy fácil sobrescribir una dependencia con tus propias configuraciones personalizadas. -### El archivo de configuración +### El archivo de configuración { #the-config-file } Proveniente del ejemplo anterior, tu archivo `config.py` podría verse como: -{* ../../docs_src/settings/app02/config.py hl[10] *} +{* ../../docs_src/settings/app02_an_py39/config.py hl[10] *} -Nota que ahora no creamos una instance por defecto `settings = Settings()`. +Nota que ahora no creamos un instance por defecto `settings = Settings()`. -### El archivo principal de la app +### El archivo principal de la app { #the-main-app-file } Ahora creamos una dependencia que devuelve un nuevo `config.Settings()`. @@ -170,17 +148,17 @@ Y luego podemos requerirlo desde la *path operation function* como una dependenc {* ../../docs_src/settings/app02_an_py39/main.py hl[17,19:21] *} -### Configuraciones y pruebas +### Configuraciones y pruebas { #settings-and-testing } -Luego sería muy fácil proporcionar un objeto de configuraciones diferente durante las pruebas al sobrescribir una dependencia para `get_settings`: +Luego sería muy fácil proporcionar un objeto de configuraciones diferente durante las pruebas al crear una sobrescritura de dependencia para `get_settings`: -{* ../../docs_src/settings/app02/test_main.py hl[9:10,13,21] *} +{* ../../docs_src/settings/app02_an_py39/test_main.py hl[9:10,13,21] *} -En la dependencia sobreescrita establecemos un nuevo valor para el `admin_email` al crear el nuevo objeto `Settings`, y luego devolvemos ese nuevo objeto. +En la sobrescritura de dependencia establecemos un nuevo valor para el `admin_email` al crear el nuevo objeto `Settings`, y luego devolvemos ese nuevo objeto. Luego podemos probar que se está usando. -## Leer un archivo `.env` +## Leer un archivo `.env` { #reading-a-env-file } Si tienes muchas configuraciones que posiblemente cambien mucho, tal vez en diferentes entornos, podría ser útil ponerlos en un archivo y luego leerlos desde allí como si fueran variables de entorno. @@ -202,7 +180,7 @@ Para que esto funcione, necesitas `pip install python-dotenv`. /// -### El archivo `.env` +### El archivo `.env` { #the-env-file } Podrías tener un archivo `.env` con: @@ -211,13 +189,11 @@ ADMIN_EMAIL="deadpool@example.com" APP_NAME="ChimichangApp" ``` -### Leer configuraciones desde `.env` +### Leer configuraciones desde `.env` { #read-settings-from-env } Y luego actualizar tu `config.py` con: -//// tab | Pydantic v2 - -{* ../../docs_src/settings/app03_an/config.py hl[9] *} +{* ../../docs_src/settings/app03_an_py39/config.py hl[9] *} /// tip | Consejo @@ -225,29 +201,9 @@ El atributo `model_config` se usa solo para configuración de Pydantic. Puedes l /// -//// - -//// tab | Pydantic v1 - -{* ../../docs_src/settings/app03_an/config_pv1.py hl[9:10] *} - -/// tip | Consejo - -La clase `Config` se usa solo para configuración de Pydantic. Puedes leer más en Pydantic Model Config. - -/// - -//// - -/// info | Información - -En la versión 1 de Pydantic la configuración se hacía en una clase interna `Config`, en la versión 2 de Pydantic se hace en un atributo `model_config`. Este atributo toma un `dict`, y para obtener autocompletado y errores en línea, puedes importar y usar `SettingsConfigDict` para definir ese `dict`. - -/// - Aquí definimos la configuración `env_file` dentro de tu clase Pydantic `Settings`, y establecemos el valor en el nombre del archivo con el archivo dotenv que queremos usar. -### Creando el `Settings` solo una vez con `lru_cache` +### Creando el `Settings` solo una vez con `lru_cache` { #creating-the-settings-only-once-with-lru-cache } Leer un archivo desde el disco es normalmente una operación costosa (lenta), por lo que probablemente quieras hacerlo solo una vez y luego reutilizar el mismo objeto de configuraciones, en lugar de leerlo para cada request. @@ -274,7 +230,7 @@ Pero como estamos usando el decorador `@lru_cache` encima, el objeto `Settings` Entonces, para cualquier llamada subsiguiente de `get_settings()` en las dependencias de los próximos requests, en lugar de ejecutar el código interno de `get_settings()` y crear un nuevo objeto `Settings`, devolverá el mismo objeto que fue devuelto en la primera llamada, una y otra vez. -#### Detalles Técnicos de `lru_cache` +#### Detalles Técnicos de `lru_cache` { #lru-cache-technical-details } `@lru_cache` modifica la función que decora para devolver el mismo valor que se devolvió la primera vez, en lugar de calcularlo nuevamente, ejecutando el código de la función cada vez. @@ -331,13 +287,13 @@ participant execute as Ejecutar función end ``` -En el caso de nuestra dependencia `get_settings()`, la función ni siquiera toma argumentos, por lo que siempre devolverá el mismo valor. +En el caso de nuestra dependencia `get_settings()`, la función ni siquiera toma argumentos, por lo que siempre devuelve el mismo valor. De esa manera, se comporta casi como si fuera solo una variable global. Pero como usa una función de dependencia, entonces podemos sobrescribirla fácilmente para las pruebas. -`@lru_cache` es parte de `functools`, que es parte del library estándar de Python, puedes leer más sobre él en las docs de Python para `@lru_cache`. +`@lru_cache` es parte de `functools`, que es parte del paquete estándar de Python, puedes leer más sobre él en las docs de Python para `@lru_cache`. -## Resumen +## Resumen { #recap } Puedes usar Pydantic Settings para manejar las configuraciones o ajustes de tu aplicación, con todo el poder de los modelos de Pydantic. diff --git a/docs/es/docs/advanced/sub-applications.md b/docs/es/docs/advanced/sub-applications.md index ccb31f1ea7..f604d18bad 100644 --- a/docs/es/docs/advanced/sub-applications.md +++ b/docs/es/docs/advanced/sub-applications.md @@ -1,34 +1,34 @@ -# Sub Aplicaciones - Mounts +# Sub Aplicaciones - Mounts { #sub-applications-mounts } Si necesitas tener dos aplicaciones de **FastAPI** independientes, cada una con su propio OpenAPI independiente y su propia interfaz de docs, puedes tener una aplicación principal y "montar" una (o más) sub-aplicación(es). -## Montar una aplicación **FastAPI** +## Montar una aplicación **FastAPI** { #mounting-a-fastapi-application } "Montar" significa añadir una aplicación completamente "independiente" en un path específico, que luego se encarga de manejar todo bajo ese path, con las _path operations_ declaradas en esa sub-aplicación. -### Aplicación de nivel superior +### Aplicación de nivel superior { #top-level-application } Primero, crea la aplicación principal de nivel superior de **FastAPI**, y sus *path operations*: -{* ../../docs_src/sub_applications/tutorial001.py hl[3, 6:8] *} +{* ../../docs_src/sub_applications/tutorial001_py39.py hl[3, 6:8] *} -### Sub-aplicación +### Sub-aplicación { #sub-application } Luego, crea tu sub-aplicación, y sus *path operations*. Esta sub-aplicación es solo otra aplicación estándar de FastAPI, pero es la que se "montará": -{* ../../docs_src/sub_applications/tutorial001.py hl[11, 14:16] *} +{* ../../docs_src/sub_applications/tutorial001_py39.py hl[11, 14:16] *} -### Montar la sub-aplicación +### Montar la sub-aplicación { #mount-the-sub-application } En tu aplicación de nivel superior, `app`, monta la sub-aplicación, `subapi`. En este caso, se montará en el path `/subapi`: -{* ../../docs_src/sub_applications/tutorial001.py hl[11, 19] *} +{* ../../docs_src/sub_applications/tutorial001_py39.py hl[11, 19] *} -### Revisa la documentación automática de la API +### Revisa la documentación automática de la API { #check-the-automatic-api-docs } Ahora, ejecuta el comando `fastapi` con tu archivo: @@ -56,7 +56,7 @@ Verás la documentación automática de la API para la sub-aplicación, incluyen Si intentas interactuar con cualquiera de las dos interfaces de usuario, funcionarán correctamente, porque el navegador podrá comunicarse con cada aplicación o sub-aplicación específica. -### Detalles Técnicos: `root_path` +### Detalles Técnicos: `root_path` { #technical-details-root-path } Cuando montas una sub-aplicación como se describe arriba, FastAPI se encargará de comunicar el path de montaje para la sub-aplicación usando un mecanismo de la especificación ASGI llamado `root_path`. diff --git a/docs/es/docs/advanced/templates.md b/docs/es/docs/advanced/templates.md index 1018197370..e5e8fe0613 100644 --- a/docs/es/docs/advanced/templates.md +++ b/docs/es/docs/advanced/templates.md @@ -1,4 +1,4 @@ -# Plantillas +# Plantillas { #templates } Puedes usar cualquier motor de plantillas que desees con **FastAPI**. @@ -6,7 +6,7 @@ Una elección común es Jinja2, el mismo que usa Flask y otras herramientas. Hay utilidades para configurarlo fácilmente que puedes usar directamente en tu aplicación de **FastAPI** (proporcionadas por Starlette). -## Instalar dependencias +## Instala dependencias { #install-dependencies } Asegúrate de crear un [entorno virtual](../virtual-environments.md){.internal-link target=_blank}, activarlo e instalar `jinja2`: @@ -20,14 +20,14 @@ $ pip install jinja2
-## Usando `Jinja2Templates` +## Usando `Jinja2Templates` { #using-jinja2templates } * Importa `Jinja2Templates`. * Crea un objeto `templates` que puedas reutilizar más tarde. * Declara un parámetro `Request` en la *path operation* que devolverá una plantilla. * Usa los `templates` que creaste para renderizar y devolver un `TemplateResponse`, pasa el nombre de la plantilla, el objeto de request, y un diccionario "context" con pares clave-valor que se usarán dentro de la plantilla Jinja2. -{* ../../docs_src/templates/tutorial001.py hl[4,11,15:18] *} +{* ../../docs_src/templates/tutorial001_py39.py hl[4,11,15:18] *} /// note | Nota @@ -51,7 +51,7 @@ También podrías usar `from starlette.templating import Jinja2Templates`. /// -## Escribiendo plantillas +## Escribiendo plantillas { #writing-templates } Luego puedes escribir una plantilla en `templates/item.html` con, por ejemplo: @@ -59,7 +59,7 @@ Luego puedes escribir una plantilla en `templates/item.html` con, por ejemplo: {!../../docs_src/templates/templates/item.html!} ``` -### Valores de Contexto de la Plantilla +### Valores de Contexto de la Plantilla { #template-context-values } En el HTML que contiene: @@ -83,7 +83,7 @@ Por ejemplo, con un ID de `42`, esto se renderizaría como: Item ID: 42 ``` -### Argumentos de la Plantilla `url_for` +### Argumentos de la Plantilla `url_for` { #template-url-for-arguments } También puedes usar `url_for()` dentro de la plantilla, toma como argumentos los mismos que usaría tu *path operation function*. @@ -105,7 +105,7 @@ Por ejemplo, con un ID de `42`, esto se renderizaría como: ``` -## Plantillas y archivos estáticos +## Plantillas y archivos estáticos { #templates-and-static-files } También puedes usar `url_for()` dentro de la plantilla, y usarlo, por ejemplo, con los `StaticFiles` que montaste con el `name="static"`. @@ -121,6 +121,6 @@ En este ejemplo, enlazaría a un archivo CSS en `static/styles.css` con: Y porque estás usando `StaticFiles`, ese archivo CSS sería servido automáticamente por tu aplicación de **FastAPI** en la URL `/static/styles.css`. -## Más detalles +## Más detalles { #more-details } Para más detalles, incluyendo cómo testear plantillas, revisa la documentación de Starlette sobre plantillas. diff --git a/docs/es/docs/advanced/testing-dependencies.md b/docs/es/docs/advanced/testing-dependencies.md index 14b90ea069..d209f2e401 100644 --- a/docs/es/docs/advanced/testing-dependencies.md +++ b/docs/es/docs/advanced/testing-dependencies.md @@ -1,6 +1,6 @@ -# Probando Dependencias con Overrides +# Probando Dependencias con Overrides { #testing-dependencies-with-overrides } -## Sobrescribir dependencias durante las pruebas +## Sobrescribir dependencias durante las pruebas { #overriding-dependencies-during-testing } Hay algunos escenarios donde podrías querer sobrescribir una dependencia durante las pruebas. @@ -8,7 +8,7 @@ No quieres que la dependencia original se ejecute (ni ninguna de las sub-depende En cambio, quieres proporcionar una dependencia diferente que se usará solo durante las pruebas (posiblemente solo algunas pruebas específicas), y que proporcionará un valor que pueda ser usado donde se usó el valor de la dependencia original. -### Casos de uso: servicio externo +### Casos de uso: servicio externo { #use-cases-external-service } Un ejemplo podría ser que tienes un proveedor de autenticación externo al que necesitas llamar. @@ -20,7 +20,7 @@ Probablemente quieras probar el proveedor externo una vez, pero no necesariament En este caso, puedes sobrescribir la dependencia que llama a ese proveedor y usar una dependencia personalizada que devuelva un usuario de prueba, solo para tus tests. -### Usa el atributo `app.dependency_overrides` +### Usa el atributo `app.dependency_overrides` { #use-the-app-dependency-overrides-attribute } Para estos casos, tu aplicación **FastAPI** tiene un atributo `app.dependency_overrides`, es un simple `dict`. diff --git a/docs/es/docs/advanced/testing-events.md b/docs/es/docs/advanced/testing-events.md index 9c2ec77b91..4f7bf03147 100644 --- a/docs/es/docs/advanced/testing-events.md +++ b/docs/es/docs/advanced/testing-events.md @@ -1,5 +1,12 @@ -# Testing Events: startup - shutdown +# Eventos de testing: lifespan y startup - shutdown { #testing-events-lifespan-and-startup-shutdown } -Cuando necesitas que tus manejadores de eventos (`startup` y `shutdown`) se ejecuten en tus tests, puedes usar el `TestClient` con un statement `with`: +Cuando necesitas que `lifespan` se ejecute en tus tests, puedes usar el `TestClient` con un statement `with`: -{* ../../docs_src/app_testing/tutorial003.py hl[9:12,20:24] *} +{* ../../docs_src/app_testing/tutorial004_py39.py hl[9:15,18,27:28,30:32,41:43] *} + + +Puedes leer más detalles sobre ["Ejecutar lifespan en tests en el sitio oficial de documentación de Starlette."](https://www.starlette.dev/lifespan/#running-lifespan-in-tests) + +Para los eventos obsoletos `startup` y `shutdown`, puedes usar el `TestClient` así: + +{* ../../docs_src/app_testing/tutorial003_py39.py hl[9:12,20:24] *} diff --git a/docs/es/docs/advanced/testing-websockets.md b/docs/es/docs/advanced/testing-websockets.md index 190e3a2242..3f60aa2caf 100644 --- a/docs/es/docs/advanced/testing-websockets.md +++ b/docs/es/docs/advanced/testing-websockets.md @@ -1,13 +1,13 @@ -# Probando WebSockets +# Probando WebSockets { #testing-websockets } Puedes usar el mismo `TestClient` para probar WebSockets. Para esto, usas el `TestClient` en un statement `with`, conectándote al WebSocket: -{* ../../docs_src/app_testing/tutorial002.py hl[27:31] *} +{* ../../docs_src/app_testing/tutorial002_py39.py hl[27:31] *} /// note | Nota -Para más detalles, revisa la documentación de Starlette sobre probando sesiones WebSocket. +Para más detalles, revisa la documentación de Starlette sobre probar WebSockets. /// diff --git a/docs/es/docs/advanced/using-request-directly.md b/docs/es/docs/advanced/using-request-directly.md index f61e498496..64feef81d7 100644 --- a/docs/es/docs/advanced/using-request-directly.md +++ b/docs/es/docs/advanced/using-request-directly.md @@ -1,4 +1,4 @@ -# Usar el Request Directamente +# Usar el Request Directamente { #using-the-request-directly } Hasta ahora, has estado declarando las partes del request que necesitas con sus tipos. @@ -13,7 +13,7 @@ Y al hacerlo, **FastAPI** está validando esos datos, convirtiéndolos y generan Pero hay situaciones donde podrías necesitar acceder al objeto `Request` directamente. -## Detalles sobre el objeto `Request` +## Detalles sobre el objeto `Request` { #details-about-the-request-object } Como **FastAPI** es en realidad **Starlette** por debajo, con una capa de varias herramientas encima, puedes usar el objeto `Request` de Starlette directamente cuando lo necesites. @@ -23,13 +23,13 @@ Aunque cualquier otro parámetro declarado normalmente (por ejemplo, el cuerpo c Pero hay casos específicos donde es útil obtener el objeto `Request`. -## Usa el objeto `Request` directamente +## Usa el objeto `Request` directamente { #use-the-request-object-directly } Imaginemos que quieres obtener la dirección IP/host del cliente dentro de tu *path operation function*. Para eso necesitas acceder al request directamente. -{* ../../docs_src/using_request_directly/tutorial001.py hl[1,7:8] *} +{* ../../docs_src/using_request_directly/tutorial001_py39.py hl[1,7:8] *} Al declarar un parámetro de *path operation function* con el tipo siendo `Request`, **FastAPI** sabrá pasar el `Request` en ese parámetro. @@ -43,7 +43,7 @@ De la misma manera, puedes declarar cualquier otro parámetro como normalmente, /// -## Documentación de `Request` +## Documentación de `Request` { #request-documentation } Puedes leer más detalles sobre el objeto `Request` en el sitio de documentación oficial de Starlette. diff --git a/docs/es/docs/advanced/websockets.md b/docs/es/docs/advanced/websockets.md index 1320f8bb79..39ddc12c4d 100644 --- a/docs/es/docs/advanced/websockets.md +++ b/docs/es/docs/advanced/websockets.md @@ -1,10 +1,10 @@ -# WebSockets +# WebSockets { #websockets } Puedes usar WebSockets con **FastAPI**. -## Instalar `WebSockets` +## Instalar `websockets` { #install-websockets } -Asegúrate de crear un [entorno virtual](../virtual-environments.md){.internal-link target=_blank}, activarlo e instalar `websockets`: +Asegúrate de crear un [entorno virtual](../virtual-environments.md){.internal-link target=_blank}, activarlo e instalar `websockets` (un paquete de Python que facilita usar el protocolo "WebSocket"):
@@ -16,9 +16,9 @@ $ pip install websockets
-## Cliente WebSockets +## Cliente WebSockets { #websockets-client } -### En producción +### En producción { #in-production } En tu sistema de producción, probablemente tengas un frontend creado con un framework moderno como React, Vue.js o Angular. @@ -38,13 +38,13 @@ En producción tendrías una de las opciones anteriores. Pero es la forma más sencilla de enfocarse en el lado del servidor de WebSockets y tener un ejemplo funcional: -{* ../../docs_src/websockets/tutorial001.py hl[2,6:38,41:43] *} +{* ../../docs_src/websockets/tutorial001_py39.py hl[2,6:38,41:43] *} -## Crear un `websocket` +## Crear un `websocket` { #create-a-websocket } En tu aplicación de **FastAPI**, crea un `websocket`: -{* ../../docs_src/websockets/tutorial001.py hl[1,46:47] *} +{* ../../docs_src/websockets/tutorial001_py39.py hl[1,46:47] *} /// note | Detalles Técnicos @@ -54,15 +54,15 @@ También podrías usar `from starlette.websockets import WebSocket`. /// -## Esperar mensajes y enviar mensajes +## Esperar mensajes y enviar mensajes { #await-for-messages-and-send-messages } En tu ruta de WebSocket puedes `await` para recibir mensajes y enviar mensajes. -{* ../../docs_src/websockets/tutorial001.py hl[48:52] *} +{* ../../docs_src/websockets/tutorial001_py39.py hl[48:52] *} Puedes recibir y enviar datos binarios, de texto y JSON. -## Pruébalo +## Pruébalo { #try-it } Si tu archivo se llama `main.py`, ejecuta tu aplicación con: @@ -96,7 +96,7 @@ Puedes enviar (y recibir) muchos mensajes: Y todos usarán la misma conexión WebSocket. -## Usando `Depends` y otros +## Usando `Depends` y otros { #using-depends-and-others } En endpoints de WebSocket puedes importar desde `fastapi` y usar: @@ -119,7 +119,7 @@ Puedes usar un código de cierre de los -## Manejar desconexiones y múltiples clientes +## Manejar desconexiones y múltiples clientes { #handling-disconnections-and-multiple-clients } Cuando una conexión de WebSocket se cierra, el `await websocket.receive_text()` lanzará una excepción `WebSocketDisconnect`, que puedes capturar y manejar como en este ejemplo. @@ -178,7 +178,7 @@ Si necesitas algo fácil de integrar con FastAPI pero que sea más robusto, sopo /// -## Más información +## Más información { #more-info } Para aprender más sobre las opciones, revisa la documentación de Starlette para: diff --git a/docs/es/docs/advanced/wsgi.md b/docs/es/docs/advanced/wsgi.md index 7df62fc9ae..d5113250ab 100644 --- a/docs/es/docs/advanced/wsgi.md +++ b/docs/es/docs/advanced/wsgi.md @@ -1,10 +1,10 @@ -# Incluyendo WSGI - Flask, Django, otros +# Incluyendo WSGI - Flask, Django, otros { #including-wsgi-flask-django-others } Puedes montar aplicaciones WSGI como viste con [Sub Aplicaciones - Mounts](sub-applications.md){.internal-link target=_blank}, [Detrás de un Proxy](behind-a-proxy.md){.internal-link target=_blank}. Para eso, puedes usar `WSGIMiddleware` y usarlo para envolver tu aplicación WSGI, por ejemplo, Flask, Django, etc. -## Usando `WSGIMiddleware` +## Usando `WSGIMiddleware` { #using-wsgimiddleware } Necesitas importar `WSGIMiddleware`. @@ -12,9 +12,9 @@ Luego envuelve la aplicación WSGI (p. ej., Flask) con el middleware. Y luego móntala bajo un path. -{* ../../docs_src/wsgi/tutorial001.py hl[2:3,3] *} +{* ../../docs_src/wsgi/tutorial001_py39.py hl[2:3,3] *} -## Revisa +## Revisa { #check-it } Ahora, cada request bajo el path `/v1/` será manejado por la aplicación Flask. diff --git a/docs/es/docs/alternatives.md b/docs/es/docs/alternatives.md index 6605b7bb08..6dd5933b03 100644 --- a/docs/es/docs/alternatives.md +++ b/docs/es/docs/alternatives.md @@ -1,8 +1,8 @@ -# Alternativas, Inspiración y Comparaciones +# Alternativas, Inspiración y Comparaciones { #alternatives-inspiration-and-comparisons } Lo que inspiró a **FastAPI**, cómo se compara con las alternativas y lo que aprendió de ellas. -## Introducción +## Introducción { #intro } **FastAPI** no existiría si no fuera por el trabajo previo de otros. @@ -12,17 +12,17 @@ He estado evitando la creación de un nuevo framework durante varios años. Prim Pero en algún punto, no hubo otra opción que crear algo que proporcionara todas estas funcionalidades, tomando las mejores ideas de herramientas previas y combinándolas de la mejor manera posible, usando funcionalidades del lenguaje que ni siquiera estaban disponibles antes (anotaciones de tipos de Python 3.6+). -## Herramientas previas +## Herramientas previas { #previous-tools } -### Django +### Django { #django } Es el framework más popular de Python y es ampliamente confiable. Se utiliza para construir sistemas como Instagram. Está relativamente acoplado con bases de datos relacionales (como MySQL o PostgreSQL), por lo que tener una base de datos NoSQL (como Couchbase, MongoDB, Cassandra, etc) como motor de almacenamiento principal no es muy fácil. -Fue creado para generar el HTML en el backend, no para crear APIs utilizadas por un frontend moderno (como React, Vue.js y Angular) o por otros sistemas (como dispositivos del IoT) comunicándose con él. +Fue creado para generar el HTML en el backend, no para crear APIs utilizadas por un frontend moderno (como React, Vue.js y Angular) o por otros sistemas (como dispositivos del IoT) comunicándose con él. -### Django REST Framework +### Django REST Framework { #django-rest-framework } El framework Django REST fue creado para ser un kit de herramientas flexible para construir APIs Web utilizando Django, mejorando sus capacidades API. @@ -42,7 +42,7 @@ Tener una interfaz de usuario web de documentación automática de APIs. /// -### Flask +### Flask { #flask } Flask es un "microframework", no incluye integraciones de bases de datos ni muchas de las cosas que vienen por defecto en Django. @@ -64,7 +64,7 @@ Tener un sistema de routing simple y fácil de usar. /// -### Requests +### Requests { #requests } **FastAPI** no es en realidad una alternativa a **Requests**. Su ámbito es muy diferente. @@ -106,7 +106,7 @@ Mira las similitudes entre `requests.get(...)` y `@app.get(...)`. /// -### Swagger / OpenAPI +### Swagger / OpenAPI { #swagger-openapi } La principal funcionalidad que quería de Django REST Framework era la documentación automática de la API. @@ -131,11 +131,11 @@ Estas dos fueron elegidas por ser bastante populares y estables, pero haciendo u /// -### Frameworks REST para Flask +### Frameworks REST para Flask { #flask-rest-frameworks } Existen varios frameworks REST para Flask, pero después de invertir tiempo y trabajo investigándolos, encontré que muchos son descontinuados o abandonados, con varios problemas existentes que los hacían inadecuados. -### Marshmallow +### Marshmallow { #marshmallow } Una de las principales funcionalidades necesitadas por los sistemas API es la "serialización" de datos, que consiste en tomar datos del código (Python) y convertirlos en algo que pueda ser enviado a través de la red. Por ejemplo, convertir un objeto que contiene datos de una base de datos en un objeto JSON. Convertir objetos `datetime` en strings, etc. @@ -153,7 +153,7 @@ Usar código para definir "esquemas" que proporcionen tipos de datos y validaci /// -### Webargs +### Webargs { #webargs } Otra gran funcionalidad requerida por las APIs es el parse de datos de las requests entrantes. @@ -175,7 +175,7 @@ Tener validación automática de datos entrantes en una request. /// -### APISpec +### APISpec { #apispec } Marshmallow y Webargs proporcionan validación, parse y serialización como plug-ins. @@ -205,7 +205,7 @@ Soportar el estándar abierto para APIs, OpenAPI. /// -### Flask-apispec +### Flask-apispec { #flask-apispec } Es un plug-in de Flask, que conecta juntos Webargs, Marshmallow y APISpec. @@ -237,7 +237,7 @@ Generar el esquema OpenAPI automáticamente, desde el mismo código que define l /// -### NestJS (y Angular) +### NestJS (y Angular) { #nestjs-and-angular } Esto ni siquiera es Python, NestJS es un framework de JavaScript (TypeScript) NodeJS inspirado por Angular. @@ -259,7 +259,7 @@ Tener un poderoso sistema de inyección de dependencias. Encontrar una forma de /// -### Sanic +### Sanic { #sanic } Fue uno de los primeros frameworks de Python extremadamente rápidos basados en `asyncio`. Fue hecho para ser muy similar a Flask. @@ -279,7 +279,7 @@ Por eso **FastAPI** se basa en Starlette, ya que es el framework más rápido di /// -### Falcon +### Falcon { #falcon } Falcon es otro framework de Python de alto rendimiento, está diseñado para ser minimalista y funcionar como la base de otros frameworks como Hug. @@ -297,7 +297,7 @@ Aunque en FastAPI es opcional, y se utiliza principalmente para configurar heade /// -### Molten +### Molten { #molten } Descubrí Molten en las primeras etapas de construcción de **FastAPI**. Y tiene ideas bastante similares: @@ -321,7 +321,7 @@ Esto en realidad inspiró la actualización de partes de Pydantic, para soportar /// -### Hug +### Hug { #hug } Hug fue uno de los primeros frameworks en implementar la declaración de tipos de parámetros API usando las anotaciones de tipos de Python. Esta fue una gran idea que inspiró a otras herramientas a hacer lo mismo. @@ -351,7 +351,7 @@ Hug inspiró a **FastAPI** a declarar un parámetro `response` en funciones para /// -### APIStar (<= 0.5) +### APIStar (<= 0.5) { #apistar-0-5 } Justo antes de decidir construir **FastAPI** encontré **APIStar** server. Tenía casi todo lo que estaba buscando y tenía un gran diseño. @@ -399,9 +399,9 @@ Considero a **FastAPI** un "sucesor espiritual" de APIStar, mientras mejora y au /// -## Usado por **FastAPI** +## Usado por **FastAPI** { #used-by-fastapi } -### Pydantic +### Pydantic { #pydantic } Pydantic es un paquete para definir validación de datos, serialización y documentación (usando JSON Schema) basándose en las anotaciones de tipos de Python. @@ -417,9 +417,9 @@ Manejar toda la validación de datos, serialización de datos y documentación a /// -### Starlette +### Starlette { #starlette } -Starlette es un framework/toolkit ASGI liviano, ideal para construir servicios asyncio de alto rendimiento. +Starlette es un framework/toolkit ASGI liviano, ideal para construir servicios asyncio de alto rendimiento. Es muy simple e intuitivo. Está diseñado para ser fácilmente extensible y tener componentes modulares. @@ -462,7 +462,7 @@ Por lo tanto, cualquier cosa que puedas hacer con Starlette, puedes hacerlo dire /// -### Uvicorn +### Uvicorn { #uvicorn } Uvicorn es un servidor ASGI extremadamente rápido, construido sobre uvloop y httptools. @@ -480,6 +480,6 @@ Revisa más detalles en la sección [Despliegue](deployment/index.md){.internal- /// -## Benchmarks y velocidad +## Benchmarks y velocidad { #benchmarks-and-speed } Para entender, comparar, y ver la diferencia entre Uvicorn, Starlette y FastAPI, revisa la sección sobre [Benchmarks](benchmarks.md){.internal-link target=_blank}. diff --git a/docs/es/docs/async.md b/docs/es/docs/async.md index 92a46ba9b5..a1c8f0fb9d 100644 --- a/docs/es/docs/async.md +++ b/docs/es/docs/async.md @@ -1,8 +1,8 @@ -# Concurrencia y async / await +# Concurrencia y async / await { #concurrency-and-async-await } Detalles sobre la sintaxis `async def` para *path operation functions* y algunos antecedentes sobre el código asíncrono, la concurrencia y el paralelismo. -## ¿Con prisa? +## ¿Con prisa? { #in-a-hurry } TL;DR: @@ -40,7 +40,7 @@ def results(): --- -Si tu aplicación (de alguna manera) no tiene que comunicarse con nada más y esperar a que responda, usa `async def`. +Si tu aplicación (de alguna manera) no tiene que comunicarse con nada más y esperar a que responda, usa `async def`, incluso si no necesitas usar `await` dentro. --- @@ -54,7 +54,7 @@ De todos modos, en cualquiera de los casos anteriores, FastAPI seguirá funciona Pero al seguir los pasos anteriores, podrá hacer algunas optimizaciones de rendimiento. -## Detalles Técnicos +## Detalles Técnicos { #technical-details } Las versiones modernas de Python tienen soporte para **"código asíncrono"** utilizando algo llamado **"coroutines"**, con la sintaxis **`async` y `await`**. @@ -64,7 +64,7 @@ Veamos esa frase por partes en las secciones a continuación: * **`async` y `await`** * **Coroutines** -## Código Asíncrono +## Código Asíncrono { #asynchronous-code } El código asíncrono simplemente significa que el lenguaje 💬 tiene una forma de decirle a la computadora / programa 🤖 que en algún momento del código, tendrá que esperar que *otra cosa* termine en otro lugar. Digamos que esa *otra cosa* se llama "archivo-lento" 📝. @@ -74,7 +74,7 @@ Luego la computadora / programa 🤖 volverá cada vez que tenga una oportunidad Después, 🤖 toma la primera tarea que termine (digamos, nuestro "archivo-lento" 📝) y continúa con lo que tenía que hacer con ella. -Ese "esperar otra cosa" normalmente se refiere a las operaciones de I/O que son relativamente "lentas" (comparadas con la velocidad del procesador y la memoria RAM), como esperar: +Ese "esperar otra cosa" normalmente se refiere a las operaciones de I/O que son relativamente "lentas" (comparadas con la velocidad del procesador y la memoria RAM), como esperar: * que los datos del cliente se envíen a través de la red * que los datos enviados por tu programa sean recibidos por el cliente a través de la red @@ -85,7 +85,7 @@ Ese "esperar otra cosa" normalmente se refiere a las operaciones de I/O, las llaman operaciones "I/O bound". +Como el tiempo de ejecución se consume principalmente esperando operaciones de I/O, las llaman operaciones "I/O bound". Se llama "asíncrono" porque la computadora / programa no tiene que estar "sincronizado" con la tarea lenta, esperando el momento exacto en que la tarea termine, sin hacer nada, para poder tomar el resultado de la tarea y continuar el trabajo. @@ -93,7 +93,7 @@ En lugar de eso, al ser un sistema "asíncrono", una vez terminado, la tarea pue Para el "sincrónico" (contrario al "asíncrono") comúnmente también usan el término "secuencial", porque la computadora / programa sigue todos los pasos en secuencia antes de cambiar a una tarea diferente, incluso si esos pasos implican esperar. -### Concurrencia y Hamburguesas +### Concurrencia y Hamburguesas { #concurrency-and-burgers } Esta idea de código **asíncrono** descrita anteriormente a veces también se llama **"concurrencia"**. Es diferente del **"paralelismo"**. @@ -103,7 +103,7 @@ Pero los detalles entre *concurrencia* y *paralelismo* son bastante diferentes. Para ver la diferencia, imagina la siguiente historia sobre hamburguesas: -### Hamburguesas Concurrentes +### Hamburguesas Concurrentes { #concurrent-burgers } Vas con tu crush a conseguir comida rápida, te pones en fila mientras el cajero toma los pedidos de las personas frente a ti. 😍 @@ -163,7 +163,7 @@ Así que esperas a que tu crush termine la historia (termine el trabajo ⏯ / ta Luego vas al mostrador 🔀, a la tarea inicial que ahora está terminada ⏯, recoges las hamburguesas, das las gracias y las llevas a la mesa. Eso termina ese paso / tarea de interacción con el mostrador ⏹. Eso a su vez, crea una nueva tarea, de "comer hamburguesas" 🔀 ⏯, pero la anterior de "obtener hamburguesas" ha terminado ⏹. -### Hamburguesas Paralelas +### Hamburguesas Paralelas { #parallel-burgers } Ahora imaginemos que estas no son "Hamburguesas Concurrentes", sino "Hamburguesas Paralelas". @@ -233,7 +233,7 @@ Y tienes que esperar 🕙 en la fila por mucho tiempo o pierdes tu turno. Probablemente no querrías llevar a tu crush 😍 contigo a hacer trámites en el banco 🏦. -### Conclusión de las Hamburguesas +### Conclusión de las Hamburguesas { #burger-conclusion } En este escenario de "hamburguesas de comida rápida con tu crush", como hay mucha espera 🕙, tiene mucho más sentido tener un sistema concurrente ⏸🔀⏯. @@ -253,7 +253,7 @@ Y ese es el mismo nivel de rendimiento que obtienes con **FastAPI**. Y como puedes tener paralelismo y asincronía al mismo tiempo, obtienes un mayor rendimiento que la mayoría de los frameworks de NodeJS probados y a la par con Go, que es un lenguaje compilado más cercano a C (todo gracias a Starlette). -### ¿Es la concurrencia mejor que el paralelismo? +### ¿Es la concurrencia mejor que el paralelismo? { #is-concurrency-better-than-parallelism } ¡No! Esa no es la moraleja de la historia. @@ -277,7 +277,7 @@ Pero en este caso, si pudieras traer a los 8 ex-cajeros/cocineros/ahora-limpiado En este escenario, cada uno de los limpiadores (incluyéndote) sería un procesador, haciendo su parte del trabajo. -Y como la mayor parte del tiempo de ejecución se dedica al trabajo real (en lugar de esperar), y el trabajo en una computadora lo realiza una CPU, llaman a estos problemas "CPU bound". +Y como la mayor parte del tiempo de ejecución se dedica al trabajo real (en lugar de esperar), y el trabajo en una computadora lo realiza una CPU, llaman a estos problemas "CPU bound". --- @@ -290,7 +290,7 @@ Por ejemplo: * **Machine Learning**: normalmente requiere muchas multiplicaciones de "matrices" y "vectores". Piensa en una enorme hoja de cálculo con números y multiplicando todos juntos al mismo tiempo. * **Deep Learning**: este es un subcampo de Machine Learning, por lo tanto, se aplica lo mismo. Es solo que no hay una sola hoja de cálculo de números para multiplicar, sino un enorme conjunto de ellas, y en muchos casos, usas un procesador especial para construir y / o usar esos modelos. -### Concurrencia + Paralelismo: Web + Machine Learning +### Concurrencia + Paralelismo: Web + Machine Learning { #concurrency-parallelism-web-machine-learning } Con **FastAPI** puedes aprovechar la concurrencia que es muy común para el desarrollo web (la misma atracción principal de NodeJS). @@ -300,7 +300,7 @@ Eso, más el simple hecho de que Python es el lenguaje principal para **Data Sci Para ver cómo lograr este paralelismo en producción, consulta la sección sobre [Deployment](deployment/index.md){.internal-link target=_blank}. -## `async` y `await` +## `async` y `await` { #async-and-await } Las versiones modernas de Python tienen una forma muy intuitiva de definir código asíncrono. Esto hace que se vea igual que el código "secuencial" normal y hace el "wait" por ti en los momentos adecuados. @@ -349,7 +349,7 @@ async def read_burgers(): return burgers ``` -### Más detalles técnicos +### Más detalles técnicos { #more-technical-details } Podrías haber notado que `await` solo se puede usar dentro de funciones definidas con `async def`. @@ -361,9 +361,9 @@ Si estás trabajando con **FastAPI** no tienes que preocuparte por eso, porque e Pero si deseas usar `async` / `await` sin FastAPI, también puedes hacerlo. -### Escribe tu propio código async +### Escribe tu propio código async { #write-your-own-async-code } -Starlette (y **FastAPI**) están basados en AnyIO, lo que lo hace compatible tanto con la librería estándar de Python asyncio como con Trio. +Starlette (y **FastAPI**) están basados en AnyIO, lo que lo hace compatible tanto con el asyncio del paquete estándar de Python como con Trio. En particular, puedes usar directamente AnyIO para tus casos de uso avanzados de concurrencia que requieran patrones más avanzados en tu propio código. @@ -371,7 +371,7 @@ E incluso si no estuvieras usando FastAPI, también podrías escribir tus propia Creé otro paquete sobre AnyIO, como una capa delgada, para mejorar un poco las anotaciones de tipos y obtener mejor **autocompletado**, **errores en línea**, etc. También tiene una introducción amigable y tutorial para ayudarte a **entender** y escribir **tu propio código async**: Asyncer. Sería particularmente útil si necesitas **combinar código async con regular** (bloqueante/sincrónico). -### Otras formas de código asíncrono +### Otras formas de código asíncrono { #other-forms-of-asynchronous-code } Este estilo de usar `async` y `await` es relativamente nuevo en el lenguaje. @@ -385,13 +385,13 @@ En versiones previas de Python, podrías haber usado hilos o I/O
de bloqueo. +Si vienes de otro framework async que no funciona de la manera descrita anteriormente y estás acostumbrado a definir funciones de *path operation* solo de cómputo trivial con `def` normal para una pequeña ganancia de rendimiento (alrededor de 100 nanosegundos), ten en cuenta que en **FastAPI** el efecto sería bastante opuesto. En estos casos, es mejor usar `async def` a menos que tus *path operation functions* usen código que realice I/O de bloqueo. Aun así, en ambas situaciones, es probable que **FastAPI** [siga siendo más rápida](index.md#performance){.internal-link target=_blank} que (o al menos comparable a) tu framework anterior. -### Dependencias +### Dependencias { #dependencies } Lo mismo aplica para las [dependencias](tutorial/dependencies/index.md){.internal-link target=_blank}. Si una dependencia es una función estándar `def` en lugar de `async def`, se ejecuta en el threadpool externo. -### Sub-dependencias +### Sub-dependencias { #sub-dependencies } Puedes tener múltiples dependencias y [sub-dependencias](tutorial/dependencies/sub-dependencies.md){.internal-link target=_blank} requiriéndose mutuamente (como parámetros de las definiciones de funciones), algunas de ellas podrían ser creadas con `async def` y algunas con `def` normal. Aun funcionará, y las que fueron creadas con `def` normal serían llamadas en un hilo externo (del threadpool) en lugar de ser "awaited". -### Otras funciones de utilidad +### Otras funciones de utilidad { #other-utility-functions } Cualquier otra función de utilidad que llames directamente puede ser creada con `def` normal o `async def` y FastAPI no afectará la forma en que la llames. diff --git a/docs/es/docs/benchmarks.md b/docs/es/docs/benchmarks.md index 49d65b6ba1..e6f8f99647 100644 --- a/docs/es/docs/benchmarks.md +++ b/docs/es/docs/benchmarks.md @@ -1,10 +1,10 @@ -# Benchmarks +# Benchmarks { #benchmarks } Los benchmarks independientes de TechEmpower muestran aplicaciones de **FastAPI** ejecutándose bajo Uvicorn como uno de los frameworks de Python más rápidos disponibles, solo por debajo de Starlette y Uvicorn en sí mismos (utilizados internamente por FastAPI). Pero al revisar benchmarks y comparaciones, debes tener en cuenta lo siguiente. -## Benchmarks y velocidad +## Benchmarks y velocidad { #benchmarks-and-speed } Cuando ves los benchmarks, es común ver varias herramientas de diferentes tipos comparadas como equivalentes. diff --git a/docs/es/docs/deployment/cloud.md b/docs/es/docs/deployment/cloud.md index f72b88c033..f3c951d989 100644 --- a/docs/es/docs/deployment/cloud.md +++ b/docs/es/docs/deployment/cloud.md @@ -1,15 +1,24 @@ -# Despliega FastAPI en Proveedores de Nube +# Despliega FastAPI en Proveedores de Nube { #deploy-fastapi-on-cloud-providers } Puedes usar prácticamente **cualquier proveedor de nube** para desplegar tu aplicación FastAPI. En la mayoría de los casos, los principales proveedores de nube tienen guías para desplegar FastAPI con ellos. -## Proveedores de Nube - Sponsors +## FastAPI Cloud { #fastapi-cloud } -Algunos proveedores de nube ✨ [**son sponsors de FastAPI**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨, esto asegura el desarrollo **continuado** y **saludable** de FastAPI y su **ecosistema**. +**FastAPI Cloud** está construido por el mismo autor y equipo detrás de **FastAPI**. -Y muestra su verdadero compromiso con FastAPI y su **comunidad** (tú), ya que no solo quieren proporcionarte un **buen servicio**, sino también asegurarse de que tengas un **framework bueno y saludable**, FastAPI. 🙇 +Simplifica el proceso de **construir**, **desplegar** y **acceder** a una API con un esfuerzo mínimo. -Podrías querer probar sus servicios y seguir sus guías: +Trae la misma experiencia de desarrollador de construir aplicaciones con FastAPI a desplegarlas en la nube. 🎉 + +FastAPI Cloud es el sponsor principal y proveedor de financiamiento de los proyectos open source *FastAPI and friends*. ✨ + +## Proveedores de Nube - Sponsors { #cloud-providers-sponsors } + +Otros proveedores de nube ✨ [**son sponsors de FastAPI**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨ también. 🙇 + +También podrías considerarlos para seguir sus guías y probar sus servicios: * Render +* Railway diff --git a/docs/es/docs/deployment/concepts.md b/docs/es/docs/deployment/concepts.md index bcc7948bc5..c42ced70bc 100644 --- a/docs/es/docs/deployment/concepts.md +++ b/docs/es/docs/deployment/concepts.md @@ -1,4 +1,4 @@ -# Conceptos de Implementación +# Conceptos de Implementación { #deployments-concepts } Cuando implementas una aplicación **FastAPI**, o en realidad, cualquier tipo de API web, hay varios conceptos que probablemente te importen, y al entenderlos, puedes encontrar la **forma más adecuada** de **implementar tu aplicación**. @@ -23,7 +23,7 @@ En los próximos capítulos, te daré más **recetas concretas** para implementa Pero por ahora, revisemos estas importantes **ideas conceptuales**. Estos conceptos también se aplican a cualquier otro tipo de API web. 💡 -## Seguridad - HTTPS +## Seguridad - HTTPS { #security-https } En el [capítulo anterior sobre HTTPS](https.md){.internal-link target=_blank} aprendimos sobre cómo HTTPS proporciona cifrado para tu API. @@ -31,7 +31,7 @@ También vimos que HTTPS es normalmente proporcionado por un componente **extern Y debe haber algo encargado de **renovar los certificados HTTPS**, podría ser el mismo componente o algo diferente. -### Herramientas de Ejemplo para HTTPS +### Herramientas de Ejemplo para HTTPS { #example-tools-for-https } Algunas de las herramientas que podrías usar como Proxy de Terminación TLS son: @@ -55,11 +55,11 @@ Te mostraré algunos ejemplos concretos en los próximos capítulos. Luego, los siguientes conceptos a considerar son todos acerca del programa que ejecuta tu API real (por ejemplo, Uvicorn). -## Programa y Proceso +## Programa y Proceso { #program-and-process } Hablaremos mucho sobre el "**proceso**" en ejecución, así que es útil tener claridad sobre lo que significa y cuál es la diferencia con la palabra "**programa**". -### Qué es un Programa +### Qué es un Programa { #what-is-a-program } La palabra **programa** se usa comúnmente para describir muchas cosas: @@ -67,7 +67,7 @@ La palabra **programa** se usa comúnmente para describir muchas cosas: * El **archivo** que puede ser **ejecutado** por el sistema operativo, por ejemplo: `python`, `python.exe` o `uvicorn`. * Un programa específico mientras está siendo **ejecutado** en el sistema operativo, usando la CPU y almacenando cosas en la memoria. Esto también se llama **proceso**. -### Qué es un Proceso +### Qué es un Proceso { #what-is-a-process } La palabra **proceso** se usa normalmente de una manera más específica, refiriéndose solo a lo que está ejecutándose en el sistema operativo (como en el último punto anterior): @@ -88,11 +88,11 @@ Y, por ejemplo, probablemente verás que hay múltiples procesos ejecutando el m Ahora que conocemos la diferencia entre los términos **proceso** y **programa**, sigamos hablando sobre implementaciones. -## Ejecución al Iniciar +## Ejecución al Iniciar { #running-on-startup } En la mayoría de los casos, cuando creas una API web, quieres que esté **siempre en ejecución**, ininterrumpida, para que tus clientes puedan acceder a ella en cualquier momento. Esto, por supuesto, a menos que tengas una razón específica para que se ejecute solo en ciertas situaciones, pero la mayoría de las veces quieres que esté constantemente en ejecución y **disponible**. -### En un Servidor Remoto +### En un Servidor Remoto { #in-a-remote-server } Cuando configuras un servidor remoto (un servidor en la nube, una máquina virtual, etc.) lo más sencillo que puedes hacer es usar `fastapi run` (que utiliza Uvicorn) o algo similar, manualmente, de la misma manera que lo haces al desarrollar localmente. @@ -102,15 +102,15 @@ Pero si pierdes la conexión con el servidor, el **proceso en ejecución** proba Y si el servidor se reinicia (por ejemplo, después de actualizaciones o migraciones del proveedor de la nube) probablemente **no lo notarás**. Y debido a eso, ni siquiera sabrás que tienes que reiniciar el proceso manualmente. Así, tu API simplemente quedará muerta. 😱 -### Ejecutar Automáticamente al Iniciar +### Ejecutar Automáticamente al Iniciar { #run-automatically-on-startup } En general, probablemente querrás que el programa del servidor (por ejemplo, Uvicorn) se inicie automáticamente al arrancar el servidor, y sin necesidad de ninguna **intervención humana**, para tener siempre un proceso en ejecución con tu API (por ejemplo, Uvicorn ejecutando tu aplicación FastAPI). -### Programa Separado +### Programa Separado { #separate-program } Para lograr esto, normalmente tendrás un **programa separado** que se asegurará de que tu aplicación se ejecute al iniciarse. Y en muchos casos, también se asegurará de que otros componentes o aplicaciones se ejecuten, por ejemplo, una base de datos. -### Herramientas de Ejemplo para Ejecutar al Iniciar +### Herramientas de Ejemplo para Ejecutar al Iniciar { #example-tools-to-run-at-startup } Algunos ejemplos de las herramientas que pueden hacer este trabajo son: @@ -125,29 +125,29 @@ Algunos ejemplos de las herramientas que pueden hacer este trabajo son: Te daré más ejemplos concretos en los próximos capítulos. -## Reinicios +## Reinicios { #restarts } De manera similar a asegurarte de que tu aplicación se ejecute al iniciar, probablemente también quieras asegurarte de que se **reinicie** después de fallos. -### Cometemos Errores +### Cometemos Errores { #we-make-mistakes } Nosotros, como humanos, cometemos **errores**, todo el tiempo. El software casi *siempre* tiene **bugs** ocultos en diferentes lugares. 🐛 Y nosotros, como desarrolladores, seguimos mejorando el código a medida que encontramos esos bugs y a medida que implementamos nuevas funcionalidades (posiblemente agregando nuevos bugs también 😅). -### Errores Pequeños Manejados Automáticamente +### Errores Pequeños Manejados Automáticamente { #small-errors-automatically-handled } -Al construir APIs web con FastAPI, si hay un error en nuestro código, FastAPI normalmente lo contiene a la solicitud única que desencadenó el error. 🛡 +Al construir APIs web con FastAPI, si hay un error en nuestro código, FastAPI normalmente lo contiene al request único que desencadenó el error. 🛡 -El cliente obtendrá un **500 Internal Server Error** para esa solicitud, pero la aplicación continuará funcionando para las siguientes solicitudes en lugar de simplemente colapsar por completo. +El cliente obtendrá un **500 Internal Server Error** para ese request, pero la aplicación continuará funcionando para los siguientes requests en lugar de simplemente colapsar por completo. -### Errores Mayores - Colapsos +### Errores Mayores - Colapsos { #bigger-errors-crashes } Sin embargo, puede haber casos en los que escribamos algún código que **colapse toda la aplicación** haciendo que Uvicorn y Python colapsen. 💥 Y aún así, probablemente no querrías que la aplicación quede muerta porque hubo un error en un lugar, probablemente querrás que **siga ejecutándose** al menos para las *path operations* que no estén rotas. -### Reiniciar Después del Colapso +### Reiniciar Después del Colapso { #restart-after-crash } Pero en esos casos con errores realmente malos que colapsan el **proceso en ejecución**, querrías un componente externo encargado de **reiniciar** el proceso, al menos un par de veces... @@ -161,7 +161,7 @@ Así que enfoquémonos en los casos principales, donde podría colapsar por comp Probablemente querrías que la cosa encargada de reiniciar tu aplicación sea un **componente externo**, porque para ese punto, la misma aplicación con Uvicorn y Python ya colapsó, así que no hay nada en el mismo código de la misma aplicación que pueda hacer algo al respecto. -### Herramientas de Ejemplo para Reiniciar Automáticamente +### Herramientas de Ejemplo para Reiniciar Automáticamente { #example-tools-to-restart-automatically } En la mayoría de los casos, la misma herramienta que se utiliza para **ejecutar el programa al iniciar** también se utiliza para manejar reinicios automáticos. @@ -176,19 +176,19 @@ Por ejemplo, esto podría ser manejado por: * Manejado internamente por un proveedor de nube como parte de sus servicios * Otros... -## Replicación - Procesos y Memoria +## Replicación - Procesos y Memoria { #replication-processes-and-memory } Con una aplicación FastAPI, usando un programa servidor como el comando `fastapi` que ejecuta Uvicorn, ejecutarlo una vez en **un proceso** puede servir a múltiples clientes concurrentemente. Pero en muchos casos, querrás ejecutar varios worker processes al mismo tiempo. -### Múltiples Procesos - Workers +### Múltiples Procesos - Workers { #multiple-processes-workers } Si tienes más clientes de los que un solo proceso puede manejar (por ejemplo, si la máquina virtual no es muy grande) y tienes **múltiples núcleos** en la CPU del servidor, entonces podrías tener **múltiples procesos** ejecutando la misma aplicación al mismo tiempo, y distribuir todas las requests entre ellos. Cuando ejecutas **múltiples procesos** del mismo programa de API, comúnmente se les llama **workers**. -### Worker Processes y Puertos +### Worker Processes y Puertos { #worker-processes-and-ports } Recuerda de la documentación [Sobre HTTPS](https.md){.internal-link target=_blank} que solo un proceso puede estar escuchando en una combinación de puerto y dirección IP en un servidor. @@ -196,19 +196,19 @@ Esto sigue siendo cierto. Así que, para poder tener **múltiples procesos** al mismo tiempo, tiene que haber un **solo proceso escuchando en un puerto** que luego transmita la comunicación a cada worker process de alguna forma. -### Memoria por Proceso +### Memoria por Proceso { #memory-per-process } -Ahora, cuando el programa carga cosas en memoria, por ejemplo, un modelo de machine learning en una variable, o el contenido de un archivo grande en una variable, todo eso **consume un poco de la memoria (RAM)** del servidor. +Ahora, cuando el programa carga cosas en memoria, por ejemplo, un modelo de Machine Learning en una variable, o el contenido de un archivo grande en una variable, todo eso **consume un poco de la memoria (RAM)** del servidor. Y múltiples procesos normalmente **no comparten ninguna memoria**. Esto significa que cada proceso en ejecución tiene sus propias cosas, variables y memoria. Y si estás consumiendo una gran cantidad de memoria en tu código, **cada proceso** consumirá una cantidad equivalente de memoria. -### Memoria del Servidor +### Memoria del Servidor { #server-memory } Por ejemplo, si tu código carga un modelo de Machine Learning con **1 GB de tamaño**, cuando ejecutas un proceso con tu API, consumirá al menos 1 GB de RAM. Y si inicias **4 procesos** (4 workers), cada uno consumirá 1 GB de RAM. Así que, en total, tu API consumirá **4 GB de RAM**. Y si tu servidor remoto o máquina virtual solo tiene 3 GB de RAM, intentar cargar más de 4 GB de RAM causará problemas. 🚨 -### Múltiples Procesos - Un Ejemplo +### Múltiples Procesos - Un Ejemplo { #multiple-processes-an-example } En este ejemplo, hay un **Proceso Administrador** que inicia y controla dos **Worker Processes**. @@ -224,7 +224,7 @@ Un detalle interesante es que el porcentaje de **CPU utilizado** por cada proces Si tienes una API que hace una cantidad comparable de cálculos cada vez y tienes muchos clientes, entonces la **utilización de CPU** probablemente *también sea estable* (en lugar de constantemente subir y bajar rápidamente). -### Ejemplos de Herramientas y Estrategias de Replicación +### Ejemplos de Herramientas y Estrategias de Replicación { #examples-of-replication-tools-and-strategies } Puede haber varios enfoques para lograr esto, y te contaré más sobre estrategias específicas en los próximos capítulos, por ejemplo, al hablar sobre Docker y contenedores. @@ -247,7 +247,7 @@ Te contaré más sobre imágenes de contenedores, Docker, Kubernetes, etc. en un /// -## Pasos Previos Antes de Iniciar +## Pasos Previos Antes de Iniciar { #previous-steps-before-starting } Hay muchos casos en los que quieres realizar algunos pasos **antes de iniciar** tu aplicación. @@ -269,7 +269,7 @@ En ese caso, no tendrías que preocuparte por nada de esto. 🤷 /// -### Ejemplos de Estrategias para Pasos Previos +### Ejemplos de Estrategias para Pasos Previos { #examples-of-previous-steps-strategies } Esto **dependerá mucho** de la forma en que **implementarás tu sistema**, y probablemente estará conectado con la forma en que inicias programas, manejas reinicios, etc. @@ -285,7 +285,7 @@ Te daré más ejemplos concretos para hacer esto con contenedores en un capítul /// -## Utilización de Recursos +## Utilización de Recursos { #resource-utilization } Tu(s) servidor(es) es(son) un **recurso** que puedes consumir o **utilizar**, con tus programas, el tiempo de cómputo en las CPUs y la memoria RAM disponible. @@ -305,7 +305,7 @@ Podrías establecer un **número arbitrario** para alcanzar, por ejemplo, algo * Puedes usar herramientas simples como `htop` para ver la CPU y RAM utilizadas en tu servidor o la cantidad utilizada por cada proceso. O puedes usar herramientas de monitoreo más complejas, que pueden estar distribuidas a través de servidores, etc. -## Resumen +## Resumen { #recap } Has estado leyendo aquí algunos de los conceptos principales que probablemente necesitarás tener en mente al decidir cómo implementar tu aplicación: diff --git a/docs/es/docs/deployment/docker.md b/docs/es/docs/deployment/docker.md index 3a39d3661a..114a62ec35 100644 --- a/docs/es/docs/deployment/docker.md +++ b/docs/es/docs/deployment/docker.md @@ -1,4 +1,4 @@ -# FastAPI en Contenedores - Docker +# FastAPI en Contenedores - Docker { #fastapi-in-containers-docker } Al desplegar aplicaciones de FastAPI, un enfoque común es construir una **imagen de contenedor de Linux**. Normalmente se realiza usando **Docker**. Luego puedes desplegar esa imagen de contenedor de varias formas. @@ -6,7 +6,7 @@ Usar contenedores de Linux tiene varias ventajas, incluyendo **seguridad**, **re /// tip | Consejo -¿Tienes prisa y ya conoces esto? Salta al [`Dockerfile` más abajo 👇](#construir-una-imagen-de-docker-para-fastapi). +¿Tienes prisa y ya conoces esto? Salta al [`Dockerfile` más abajo 👇](#build-a-docker-image-for-fastapi). /// @@ -32,7 +32,7 @@ CMD ["fastapi", "run", "app/main.py", "--port", "80"]
-## Qué es un Contenedor +## Qué es un Contenedor { #what-is-a-container } Los contenedores (principalmente contenedores de Linux) son una forma muy **ligera** de empaquetar aplicaciones incluyendo todas sus dependencias y archivos necesarios, manteniéndolos aislados de otros contenedores (otras aplicaciones o componentes) en el mismo sistema. @@ -42,11 +42,11 @@ De esta forma, los contenedores consumen **pocos recursos**, una cantidad compar Los contenedores también tienen sus propios procesos de ejecución **aislados** (normalmente solo un proceso), sistema de archivos y red, simplificando el despliegue, la seguridad, el desarrollo, etc. -## Qué es una Imagen de Contenedor +## Qué es una Imagen de Contenedor { #what-is-a-container-image } Un **contenedor** se ejecuta desde una **imagen de contenedor**. -Una imagen de contenedor es una versión **estática** de todos los archivos, variables de entorno y el comando/programa por defecto que debería estar presente en un contenedor. **Estático** aquí significa que la imagen de contenedor **no se está ejecutando**, no está siendo ejecutada, son solo los archivos empaquetados y los metadatos. +Una imagen de contenedor es una versión **estática** de todos los archivos, variables de entorno y el comando/programa por defecto que debería estar presente en un contenedor. **Estático** aquí significa que la **imagen** de contenedor no se está ejecutando, no está siendo ejecutada, son solo los archivos empaquetados y los metadatos. En contraste con una "**imagen de contenedor**" que son los contenidos estáticos almacenados, un "**contenedor**" normalmente se refiere a la instance en ejecución, lo que está siendo **ejecutado**. @@ -56,7 +56,7 @@ Una imagen de contenedor es comparable al archivo de **programa** y sus contenid Y el **contenedor** en sí (en contraste con la **imagen de contenedor**) es la instance real en ejecución de la imagen, comparable a un **proceso**. De hecho, un contenedor solo se está ejecutando cuando tiene un **proceso en ejecución** (y normalmente es solo un proceso). El contenedor se detiene cuando no hay un proceso en ejecución en él. -## Imágenes de Contenedor +## Imágenes de Contenedor { #container-images } Docker ha sido una de las herramientas principales para crear y gestionar **imágenes de contenedor** y **contenedores**. @@ -77,9 +77,9 @@ De esta manera, en muchos casos puedes aprender sobre contenedores y Docker y re Así, ejecutarías **múltiples contenedores** con diferentes cosas, como una base de datos, una aplicación de Python, un servidor web con una aplicación frontend en React, y conectarlos entre sí a través de su red interna. -Todos los sistemas de gestión de contenedores (como Docker o Kubernetes) tienen estas características de redes integradas en ellos. +Todos los sistemas de gestión de contenedores (como Docker o Kubernetes) tienen estas funcionalidades de redes integradas. -## Contenedores y Procesos +## Contenedores y Procesos { #containers-and-processes } Una **imagen de contenedor** normalmente incluye en sus metadatos el programa o comando por defecto que debería ser ejecutado cuando el **contenedor** se inicie y los parámetros que deben pasar a ese programa. Muy similar a lo que sería si estuviera en la línea de comandos. @@ -91,7 +91,7 @@ Un contenedor normalmente tiene un **proceso único**, pero también es posible Pero no es posible tener un contenedor en ejecución sin **al menos un proceso en ejecución**. Si el proceso principal se detiene, el contenedor se detiene. -## Construir una Imagen de Docker para FastAPI +## Construir una Imagen de Docker para FastAPI { #build-a-docker-image-for-fastapi } ¡Bien, construyamos algo ahora! 🚀 @@ -103,7 +103,7 @@ Esto es lo que querrías hacer en **la mayoría de los casos**, por ejemplo: * Al ejecutar en un **Raspberry Pi** * Usando un servicio en la nube que ejecutaría una imagen de contenedor por ti, etc. -### Requisitos del Paquete +### Requisitos del Paquete { #package-requirements } Normalmente tendrías los **requisitos del paquete** para tu aplicación en algún archivo. @@ -138,7 +138,7 @@ Existen otros formatos y herramientas para definir e instalar dependencias de pa /// -### Crear el Código de **FastAPI** +### Crear el Código de **FastAPI** { #create-the-fastapi-code } * Crea un directorio `app` y entra en él. * Crea un archivo vacío `__init__.py`. @@ -162,7 +162,7 @@ def read_item(item_id: int, q: Union[str, None] = None): return {"item_id": item_id, "q": q} ``` -### Dockerfile +### Dockerfile { #dockerfile } Ahora, en el mismo directorio del proyecto, crea un archivo `Dockerfile` con: @@ -238,7 +238,7 @@ Asegúrate de **siempre** usar la **forma exec** de la instrucción `CMD`, como /// -#### Usar `CMD` - Forma Exec +#### Usar `CMD` - Forma Exec { #use-cmd-exec-form } La instrucción Docker `CMD` se puede escribir usando dos formas: @@ -262,20 +262,20 @@ Puedes leer más sobre esto en las ¿Por qué mis servicios tardan 10 segundos en recrearse o detenerse?. -#### Estructura de Directorios +#### Estructura de Directorios { #directory-structure } Ahora deberías tener una estructura de directorios como: ``` . ├── app -│ ├── __init__.py +│   ├── __init__.py │ └── main.py ├── Dockerfile └── requirements.txt ``` -#### Detrás de un Proxy de Terminación TLS +#### Detrás de un Proxy de Terminación TLS { #behind-a-tls-termination-proxy } Si estás ejecutando tu contenedor detrás de un Proxy de Terminación TLS (load balancer) como Nginx o Traefik, añade la opción `--proxy-headers`, esto le dirá a Uvicorn (a través de la CLI de FastAPI) que confíe en los headers enviados por ese proxy indicando que la aplicación se está ejecutando detrás de HTTPS, etc. @@ -283,7 +283,7 @@ Si estás ejecutando tu contenedor detrás de un Proxy de Terminación TLS (load CMD ["fastapi", "run", "app/main.py", "--proxy-headers", "--port", "80"] ``` -#### Cache de Docker +#### Caché de Docker { #docker-cache } Hay un truco importante en este `Dockerfile`, primero copiamos **el archivo con las dependencias solo**, no el resto del código. Déjame decirte por qué es así. @@ -315,7 +315,7 @@ Luego, cerca del final del `Dockerfile`, copiamos todo el código. Como esto es COPY ./app /code/app ``` -### Construir la Imagen de Docker +### Construir la Imagen de Docker { #build-the-docker-image } Ahora que todos los archivos están en su lugar, vamos a construir la imagen del contenedor. @@ -340,7 +340,7 @@ En este caso, es el mismo directorio actual (`.`). /// -### Iniciar el Contenedor Docker +### Iniciar el Contenedor Docker { #start-the-docker-container } * Ejecuta un contenedor basado en tu imagen: @@ -352,7 +352,7 @@ $ docker run -d --name mycontainer -p 80:80 myimage -## Revísalo +## Revísalo { #check-it } Deberías poder revisarlo en la URL de tu contenedor de Docker, por ejemplo: http://192.168.99.100/items/5?q=somequery o http://127.0.0.1/items/5?q=somequery (o equivalente, usando tu host de Docker). @@ -362,7 +362,7 @@ Verás algo como: {"item_id": 5, "q": "somequery"} ``` -## Documentación Interactiva de la API +## Documentación Interactiva de la API { #interactive-api-docs } Ahora puedes ir a http://192.168.99.100/docs o http://127.0.0.1/docs (o equivalente, usando tu host de Docker). @@ -370,7 +370,7 @@ Verás la documentación interactiva automática de la API (proporcionada por http://192.168.99.100/redoc o http://127.0.0.1/redoc (o equivalente, usando tu host de Docker). @@ -378,7 +378,7 @@ Verás la documentación alternativa automática (proporcionada por cluster de máquinas con **Kubernetes**, Docker Swarm Mode, Nomad, u otro sistema complejo similar para gestionar contenedores distribuidos en varias máquinas, entonces probablemente querrás manejar la **replicación** a nivel de **cluster** en lugar de usar un **gestor de procesos** (como Uvicorn con workers) en cada contenedor. @@ -462,7 +462,7 @@ Uno de esos sistemas de gestión de contenedores distribuidos como Kubernetes no En esos casos, probablemente desearías construir una **imagen de Docker desde cero** como se [explica arriba](#dockerfile), instalando tus dependencias, y ejecutando **un solo proceso de Uvicorn** en lugar de usar múltiples workers de Uvicorn. -### Load Balancer +### Load Balancer { #load-balancer } Al usar contenedores, normalmente tendrías algún componente **escuchando en el puerto principal**. Podría posiblemente ser otro contenedor que es también un **Proxy de Terminación TLS** para manejar **HTTPS** o alguna herramienta similar. @@ -476,17 +476,17 @@ El mismo componente **Proxy de Terminación TLS** usado para HTTPS probablemente Y al trabajar con contenedores, el mismo sistema que usas para iniciarlos y gestionarlos ya tendría herramientas internas para transmitir la **comunicación en red** (e.g., requests HTTP) desde ese **load balancer** (que también podría ser un **Proxy de Terminación TLS**) a los contenedores con tu aplicación. -### Un Load Balancer - Múltiples Contenedores Worker +### Un Load Balancer - Múltiples Contenedores Worker { #one-load-balancer-multiple-worker-containers } Al trabajar con **Kubernetes** u otros sistemas de gestión de contenedores distribuidos similares, usar sus mecanismos de red internos permitiría que el único **load balancer** que está escuchando en el **puerto** principal transmita la comunicación (requests) a posiblemente **múltiples contenedores** ejecutando tu aplicación. Cada uno de estos contenedores ejecutando tu aplicación normalmente tendría **solo un proceso** (e.g., un proceso Uvicorn ejecutando tu aplicación FastAPI). Todos serían **contenedores idénticos**, ejecutando lo mismo, pero cada uno con su propio proceso, memoria, etc. De esa forma, aprovecharías la **paralelización** en **diferentes núcleos** de la CPU, o incluso en **diferentes máquinas**. -Y el sistema de contenedores distribuido con el **load balancer** **distribuiría las requests** a cada uno de los contenedores **replicados** que ejecutan tu aplicación **en turnos**. Así, cada request podría ser manejado por uno de los múltiples **contenedores replicados** ejecutando tu aplicación. +Y el sistema de contenedores distribuido con el **load balancer** **distribuiría las requests** a cada uno de los contenedores con tu aplicación **en turnos**. Así, cada request podría ser manejada por uno de los múltiples **contenedores replicados** ejecutando tu aplicación. -Y normalmente este **load balancer** podría manejar requests que vayan a *otras* aplicaciones en tu cluster (p. ej., a un dominio diferente, o bajo un prefijo de ruta de URL diferente), y transmitiría esa comunicación a los contenedores correctos para *esa otra* aplicación ejecutándose en tu cluster. +Y normalmente este **load balancer** podría manejar requests que vayan a *otras* aplicaciones en tu cluster (p. ej., a un dominio diferente, o bajo un prefijo de path de URL diferente), y transmitiría esa comunicación a los contenedores correctos para *esa otra* aplicación ejecutándose en tu cluster. -### Un Proceso por Contenedor +### Un Proceso por Contenedor { #one-process-per-container } En este tipo de escenario, probablemente querrías tener **un solo proceso (Uvicorn) por contenedor**, ya que ya estarías manejando la replicación a nivel de cluster. @@ -494,7 +494,7 @@ Así que, en este caso, **no** querrías tener múltiples workers en el contened Tener otro gestor de procesos dentro del contenedor (como sería con múltiples workers) solo añadiría **complejidad innecesaria** que probablemente ya estés manejando con tu sistema de cluster. -### Contenedores con Múltiples Procesos y Casos Especiales +### Contenedores con Múltiples Procesos y Casos Especiales { #containers-with-multiple-processes-and-special-cases } Por supuesto, hay **casos especiales** donde podrías querer tener **un contenedor** con varios **worker processes de Uvicorn** dentro. @@ -519,11 +519,11 @@ CMD ["fastapi", "run", "app/main.py", "--port", "80", "--workers", "4"] Aquí hay algunos ejemplos de cuándo eso podría tener sentido: -#### Una Aplicación Simple +#### Una Aplicación Simple { #a-simple-app } Podrías querer un gestor de procesos en el contenedor si tu aplicación es **lo suficientemente simple** que pueda ejecutarse en un **servidor único**, no un cluster. -#### Docker Compose +#### Docker Compose { #docker-compose } Podrías estar desplegando en un **servidor único** (no un cluster) con **Docker Compose**, por lo que no tendrías una forma fácil de gestionar la replicación de contenedores (con Docker Compose) mientras se preserva la red compartida y el **load balancing**. @@ -540,7 +540,7 @@ El punto principal es que, **ninguna** de estas son **reglas escritas en piedra* * Memoria * Pasos previos antes de comenzar -## Memoria +## Memoria { #memory } Si ejecutas **un solo proceso por contenedor**, tendrás una cantidad de memoria más o menos bien definida, estable y limitada consumida por cada uno de esos contenedores (más de uno si están replicados). @@ -550,11 +550,11 @@ Si tu aplicación es **simple**, probablemente esto **no será un problema**, y Si ejecutas **múltiples procesos por contenedor**, tendrás que asegurarte de que el número de procesos iniciados no **consuma más memoria** de la que está disponible. -## Pasos Previos Antes de Comenzar y Contenedores +## Pasos Previos Antes de Comenzar y Contenedores { #previous-steps-before-starting-and-containers } Si estás usando contenedores (por ejemplo, Docker, Kubernetes), entonces hay dos enfoques principales que puedes usar. -### Múltiples Contenedores +### Múltiples Contenedores { #multiple-containers } Si tienes **múltiples contenedores**, probablemente cada uno ejecutando un **proceso único** (por ejemplo, en un cluster de **Kubernetes**), entonces probablemente querrías tener un **contenedor separado** realizando el trabajo de los **pasos previos** en un solo contenedor, ejecutando un solo proceso, **antes** de ejecutar los contenedores worker replicados. @@ -566,13 +566,13 @@ Si estás usando Kubernetes, probablemente sería un tiangolo/uvicorn-gunicorn-fastapi. Pero ahora está obsoleta. ⛔️ +Solía haber una imagen official de Docker de FastAPI: tiangolo/uvicorn-gunicorn-fastapi-docker. Pero ahora está obsoleta. ⛔️ Probablemente **no** deberías usar esta imagen base de Docker (o cualquier otra similar). @@ -588,7 +588,7 @@ Pero ahora que Uvicorn (y el comando `fastapi`) soportan el uso de `--workers`, /// -## Desplegar la Imagen del Contenedor +## Desplegar la Imagen del Contenedor { #deploy-the-container-image } Después de tener una Imagen de Contenedor (Docker) hay varias maneras de desplegarla. @@ -600,11 +600,11 @@ Por ejemplo: * Con otra herramienta como Nomad * Con un servicio en la nube que tome tu imagen de contenedor y la despliegue -## Imagen de Docker con `uv` +## Imagen de Docker con `uv` { #docker-image-with-uv } Si estás usando uv para instalar y gestionar tu proyecto, puedes seguir su guía de Docker de uv. -## Resumen +## Resumen { #recap } Usando sistemas de contenedores (por ejemplo, con **Docker** y **Kubernetes**) se vuelve bastante sencillo manejar todos los **conceptos de despliegue**: diff --git a/docs/es/docs/deployment/fastapicloud.md b/docs/es/docs/deployment/fastapicloud.md new file mode 100644 index 0000000000..af3e7ce680 --- /dev/null +++ b/docs/es/docs/deployment/fastapicloud.md @@ -0,0 +1,65 @@ +# FastAPI Cloud { #fastapi-cloud } + +Puedes desplegar tu app de FastAPI en FastAPI Cloud con un solo comando; ve y únete a la lista de espera si aún no lo has hecho. 🚀 + +## Iniciar sesión { #login } + +Asegúrate de que ya tienes una cuenta de **FastAPI Cloud** (te invitamos desde la lista de espera 😉). + +Luego inicia sesión: + +
+ +```console +$ fastapi login + +You are logged in to FastAPI Cloud 🚀 +``` + +
+ +## Desplegar { #deploy } + +Ahora despliega tu app, con un solo comando: + +
+ +```console +$ fastapi deploy + +Deploying to FastAPI Cloud... + +✅ Deployment successful! + +🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev +``` + +
+ +¡Eso es todo! Ahora puedes acceder a tu app en esa URL. ✨ + +## Acerca de FastAPI Cloud { #about-fastapi-cloud } + +**FastAPI Cloud** está creado por el mismo autor y equipo detrás de **FastAPI**. + +Agiliza el proceso de **crear**, **desplegar** y **acceder** a una API con el mínimo esfuerzo. + +Aporta la misma **experiencia de desarrollador** de crear apps con FastAPI al **desplegarlas** en la nube. 🎉 + +También se encargará de la mayoría de las cosas que necesitas al desplegar una app, como: + +* HTTPS +* Replicación, con autoescalado basado en requests +* etc. + +FastAPI Cloud es el sponsor principal y proveedor de financiación de los proyectos open source de *FastAPI and friends*. ✨ + +## Desplegar en otros proveedores de la nube { #deploy-to-other-cloud-providers } + +FastAPI es open source y está basado en estándares. Puedes desplegar apps de FastAPI en cualquier proveedor de la nube que elijas. + +Sigue las guías de tu proveedor de la nube para desplegar apps de FastAPI con ellos. 🤓 + +## Despliega tu propio servidor { #deploy-your-own-server } + +También te enseñaré más adelante en esta guía de **Despliegue** todos los detalles, para que puedas entender qué está pasando, qué tiene que ocurrir o cómo desplegar apps de FastAPI por tu cuenta, también con tus propios servidores. 🤓 diff --git a/docs/es/docs/deployment/https.md b/docs/es/docs/deployment/https.md index 3ccb5dc474..e62bf8b153 100644 --- a/docs/es/docs/deployment/https.md +++ b/docs/es/docs/deployment/https.md @@ -1,4 +1,4 @@ -# Sobre HTTPS +# Sobre HTTPS { #about-https } Es fácil asumir que HTTPS es algo que simplemente está "activado" o no. @@ -28,7 +28,7 @@ Ahora, desde una **perspectiva de desarrollador**, aquí hay varias cosas a tene * **Por defecto**, eso significaría que solo puedes tener **un certificado HTTPS por dirección IP**. * No importa cuán grande sea tu servidor o qué tan pequeña pueda ser cada aplicación que tengas en él. * Sin embargo, hay una **solución** para esto. -* Hay una **extensión** para el protocolo **TLS** (el que maneja la encriptación a nivel de TCP, antes de HTTP) llamada **SNI**. +* Hay una **extensión** para el protocolo **TLS** (el que maneja la encriptación a nivel de TCP, antes de HTTP) llamada **SNI**. * Esta extensión SNI permite que un solo servidor (con una **sola dirección IP**) tenga **varios certificados HTTPS** y sirva **múltiples dominios/aplicaciones HTTPS**. * Para que esto funcione, un componente (programa) **único** que se ejecute en el servidor, escuchando en la **dirección IP pública**, debe tener **todos los certificados HTTPS** en el servidor. * **Después** de obtener una conexión segura, el protocolo de comunicación sigue siendo **HTTP**. @@ -43,7 +43,7 @@ Algunas de las opciones que podrías usar como un TLS Termination Proxy son: * Nginx * HAProxy -## Let's Encrypt +## Let's Encrypt { #lets-encrypt } Antes de Let's Encrypt, estos **certificados HTTPS** eran vendidos por terceros. @@ -51,17 +51,17 @@ El proceso para adquirir uno de estos certificados solía ser complicado, requer Pero luego se creó **Let's Encrypt**. -Es un proyecto de la Linux Foundation. Proporciona **certificados HTTPS de forma gratuita**, de manera automatizada. Estos certificados usan toda la seguridad criptográfica estándar, y tienen una corta duración (aproximadamente 3 meses), por lo que la **seguridad es en realidad mejor** debido a su corta vida útil. +Es un proyecto de la Linux Foundation. Proporciona **certificados HTTPS de forma gratuita**, de manera automatizada. Estos certificados usan toda la seguridad criptográfica estándar, y tienen una corta duración (aproximadamente 3 meses), por lo que la **seguridad es en realidad mejor** debido a su lifespan reducida. Los dominios son verificados de manera segura y los certificados se generan automáticamente. Esto también permite automatizar la renovación de estos certificados. La idea es automatizar la adquisición y renovación de estos certificados para que puedas tener **HTTPS seguro, gratuito, para siempre**. -## HTTPS para Desarrolladores +## HTTPS para Desarrolladores { #https-for-developers } Aquí tienes un ejemplo de cómo podría ser una API HTTPS, paso a paso, prestando atención principalmente a las ideas importantes para los desarrolladores. -### Nombre de Dominio +### Nombre de Dominio { #domain-name } Probablemente todo comenzaría adquiriendo un **nombre de dominio**. Luego, lo configurarías en un servidor DNS (posiblemente tu mismo proveedor de la nube). @@ -77,7 +77,7 @@ Esta parte del Nombre de Dominio es mucho antes de HTTPS, pero como todo depende /// -### DNS +### DNS { #dns } Ahora centrémonos en todas las partes realmente de HTTPS. @@ -87,7 +87,7 @@ Los servidores DNS le dirían al navegador que use una **dirección IP** especí -### Inicio del Handshake TLS +### Inicio del Handshake TLS { #tls-handshake-start } El navegador luego se comunicaría con esa dirección IP en el **puerto 443** (el puerto HTTPS). @@ -97,7 +97,7 @@ La primera parte de la comunicación es solo para establecer la conexión entre Esta interacción entre el cliente y el servidor para establecer la conexión TLS se llama **handshake TLS**. -### TLS con Extensión SNI +### TLS con Extensión SNI { #tls-with-sni-extension } **Solo un proceso** en el servidor puede estar escuchando en un **puerto** específico en una **dirección IP** específica. Podría haber otros procesos escuchando en otros puertos en la misma dirección IP, pero solo uno para cada combinación de dirección IP y puerto. @@ -127,7 +127,7 @@ Ten en cuenta que la encriptación de la comunicación ocurre a nivel de **TCP** /// -### Request HTTPS +### Request HTTPS { #https-request } Ahora que el cliente y el servidor (específicamente el navegador y el TLS Termination Proxy) tienen una **conexión TCP encriptada**, pueden iniciar la **comunicación HTTP**. @@ -135,19 +135,19 @@ Así que, el cliente envía un **request HTTPS**. Esto es simplemente un request -### Desencriptar el Request +### Desencriptar el Request { #decrypt-the-request } El TLS Termination Proxy usaría la encriptación acordada para **desencriptar el request**, y transmitiría el **request HTTP simple (desencriptado)** al proceso que ejecuta la aplicación (por ejemplo, un proceso con Uvicorn ejecutando la aplicación FastAPI). -### Response HTTP +### Response HTTP { #http-response } La aplicación procesaría el request y enviaría un **response HTTP simple (sin encriptar)** al TLS Termination Proxy. -### Response HTTPS +### Response HTTPS { #https-response } El TLS Termination Proxy entonces **encriptaría el response** usando la criptografía acordada antes (que comenzó con el certificado para `someapp.example.com`), y lo enviaría de vuelta al navegador. @@ -157,7 +157,7 @@ Luego, el navegador verificaría que el response sea válido y encriptado con la El cliente (navegador) sabrá que el response proviene del servidor correcto porque está utilizando la criptografía que acordaron usando el **certificado HTTPS** anteriormente. -### Múltiples Aplicaciones +### Múltiples Aplicaciones { #multiple-applications } En el mismo servidor (o servidores), podrían haber **múltiples aplicaciones**, por ejemplo, otros programas API o una base de datos. @@ -167,7 +167,7 @@ Solo un proceso puede estar gestionando la IP y puerto específica (el TLS Termi De esa manera, el TLS Termination Proxy podría gestionar HTTPS y certificados para **múltiples dominios**, para múltiples aplicaciones, y luego transmitir los requests a la aplicación correcta en cada caso. -### Renovación de Certificados +### Renovación de Certificados { #certificate-renewal } En algún momento en el futuro, cada certificado **expiraría** (alrededor de 3 meses después de haberlo adquirido). @@ -190,7 +190,39 @@ Para hacer eso, y para acomodar diferentes necesidades de aplicaciones, hay vari Todo este proceso de renovación, mientras aún se sirve la aplicación, es una de las principales razones por las que querrías tener un **sistema separado para gestionar el HTTPS** con un TLS Termination Proxy en lugar de simplemente usar los certificados TLS con el servidor de aplicaciones directamente (por ejemplo, Uvicorn). -## Resumen +## Headers reenviados por el proxy { #proxy-forwarded-headers } + +Al usar un proxy para gestionar HTTPS, tu **servidor de aplicaciones** (por ejemplo Uvicorn vía FastAPI CLI) no sabe nada sobre el proceso HTTPS, se comunica con HTTP simple con el **TLS Termination Proxy**. + +Este **proxy** normalmente configuraría algunos headers HTTP sobre la marcha antes de transmitir el request al **servidor de aplicaciones**, para hacerle saber al servidor de aplicaciones que el request está siendo **reenviado** por el proxy. + +/// note | Detalles técnicos + +Los headers del proxy son: + +* X-Forwarded-For +* X-Forwarded-Proto +* X-Forwarded-Host + +/// + +Aun así, como el **servidor de aplicaciones** no sabe que está detrás de un **proxy** de confianza, por defecto, no confiaría en esos headers. + +Pero puedes configurar el **servidor de aplicaciones** para confiar en los headers reenviados enviados por el **proxy**. Si estás usando FastAPI CLI, puedes usar la *Opción de la CLI* `--forwarded-allow-ips` para indicarle desde qué IPs debería confiar en esos headers reenviados. + +Por ejemplo, si el **servidor de aplicaciones** solo está recibiendo comunicación del **proxy** de confianza, puedes establecerlo en `--forwarded-allow-ips="*"` para hacer que confíe en todas las IPs entrantes, ya que solo recibirá requests desde la IP que sea utilizada por el **proxy**. + +De esta manera la aplicación podrá saber cuál es su propia URL pública, si está usando HTTPS, el dominio, etc. + +Esto sería útil, por ejemplo, para manejar correctamente redirecciones. + +/// tip | Consejo + +Puedes aprender más sobre esto en la documentación de [Detrás de un proxy - Habilitar headers reenviados por el proxy](../advanced/behind-a-proxy.md#enable-proxy-forwarded-headers){.internal-link target=_blank} + +/// + +## Resumen { #recap } Tener **HTTPS** es muy importante y bastante **crítico** en la mayoría de los casos. La mayor parte del esfuerzo que como desarrollador tienes que poner en torno a HTTPS es solo sobre **entender estos conceptos** y cómo funcionan. diff --git a/docs/es/docs/deployment/index.md b/docs/es/docs/deployment/index.md index 3b6dcc05d3..1260c68b98 100644 --- a/docs/es/docs/deployment/index.md +++ b/docs/es/docs/deployment/index.md @@ -1,8 +1,8 @@ -# Despliegue +# Despliegue { #deployment } Desplegar una aplicación **FastAPI** es relativamente fácil. -## Qué Significa Despliegue +## Qué Significa Despliegue { #what-does-deployment-mean } **Desplegar** una aplicación significa realizar los pasos necesarios para hacerla **disponible para los usuarios**. @@ -10,12 +10,14 @@ Para una **API web**, normalmente implica ponerla en una **máquina remota**, co Esto contrasta con las etapas de **desarrollo**, donde estás constantemente cambiando el código, rompiéndolo y arreglándolo, deteniendo y reiniciando el servidor de desarrollo, etc. -## Estrategias de Despliegue +## Estrategias de Despliegue { #deployment-strategies } Hay varias maneras de hacerlo dependiendo de tu caso de uso específico y las herramientas que utilices. Podrías **desplegar un servidor** tú mismo utilizando una combinación de herramientas, podrías usar un **servicio en la nube** que hace parte del trabajo por ti, u otras opciones posibles. +Por ejemplo, nosotros, el equipo detrás de FastAPI, construimos **FastAPI Cloud**, para hacer que desplegar aplicaciones de FastAPI en la nube sea lo más ágil posible, con la misma experiencia de desarrollador de trabajar con FastAPI. + Te mostraré algunos de los conceptos principales que probablemente deberías tener en cuenta al desplegar una aplicación **FastAPI** (aunque la mayoría se aplica a cualquier otro tipo de aplicación web). Verás más detalles a tener en cuenta y algunas de las técnicas para hacerlo en las siguientes secciones. ✨ diff --git a/docs/es/docs/deployment/manually.md b/docs/es/docs/deployment/manually.md index b56cf9514a..50ba79c228 100644 --- a/docs/es/docs/deployment/manually.md +++ b/docs/es/docs/deployment/manually.md @@ -1,51 +1,39 @@ -# Ejecutar un Servidor Manualmente +# Ejecutar un Servidor Manualmente { #run-a-server-manually } -## Usa el Comando `fastapi run` +## Usa el Comando `fastapi run` { #use-the-fastapi-run-command } En resumen, usa `fastapi run` para servir tu aplicación FastAPI:
```console -$ fastapi run main.py -INFO Usando path main.py -INFO Path absoluto resuelto /home/user/code/awesomeapp/main.py -INFO Buscando una estructura de archivos de paquete desde directorios con archivos __init__.py -INFO Importando desde /home/user/code/awesomeapp +$ fastapi run main.py - ╭─ Archivo de módulo de Python ─╮ - │ │ - │ 🐍 main.py │ - │ │ - ╰──────────────────────╯ + FastAPI Starting production server 🚀 -INFO Importando módulo main -INFO Encontrada aplicación FastAPI importable + Searching for package file structure from directories + with __init__.py files + Importing from /home/user/code/awesomeapp - ╭─ Aplicación FastAPI importable ─╮ - │ │ - │ from main import app │ - │ │ - ╰──────────────────────────╯ + module 🐍 main.py -INFO Usando la cadena de import main:app + code Importing the FastAPI app object from the module with + the following code: - ╭─────────── CLI de FastAPI - Modo Producción ───────────╮ - │ │ - │ Sirviendo en: http://0.0.0.0:8000 │ - │ │ - │ Docs de API: http://0.0.0.0:8000/docs │ - │ │ - │ Corriendo en modo producción, para desarrollo usa: │ - │ │ - fastapi dev - │ │ - ╰─────────────────────────────────────────────────────╯ + from main import app -INFO: Iniciado el proceso del servidor [2306215] -INFO: Esperando el inicio de la aplicación. -INFO: Inicio de la aplicación completado. -INFO: Uvicorn corriendo en http://0.0.0.0:8000 (Presiona CTRL+C para salir) + app Using import string: main:app + + server Server started at http://0.0.0.0:8000 + server Documentation at http://0.0.0.0:8000/docs + + Logs: + + INFO Started server process [2306215] + INFO Waiting for application startup. + INFO Application startup complete. + INFO Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C + to quit) ```
@@ -54,11 +42,11 @@ Eso funcionaría para la mayoría de los casos. 😎 Podrías usar ese comando, por ejemplo, para iniciar tu app **FastAPI** en un contenedor, en un servidor, etc. -## Servidores ASGI +## Servidores ASGI { #asgi-servers } Vamos a profundizar un poquito en los detalles. -FastAPI usa un estándar para construir frameworks de web y servidores de Python llamado ASGI. FastAPI es un framework web ASGI. +FastAPI usa un estándar para construir frameworks de web y servidores de Python llamado ASGI. FastAPI es un framework web ASGI. Lo principal que necesitas para ejecutar una aplicación **FastAPI** (o cualquier otra aplicación ASGI) en una máquina de servidor remota es un programa de servidor ASGI como **Uvicorn**, que es el que viene por defecto en el comando `fastapi`. @@ -70,7 +58,7 @@ Hay varias alternativas, incluyendo: * Granian: Un servidor HTTP Rust para aplicaciones en Python. * NGINX Unit: NGINX Unit es un runtime para aplicaciones web ligero y versátil. -## Máquina Servidor y Programa Servidor +## Máquina Servidor y Programa Servidor { #server-machine-and-server-program } Hay un pequeño detalle sobre los nombres que hay que tener en cuenta. 💡 @@ -80,7 +68,7 @@ Solo ten en cuenta que cuando leas "servidor" en general, podría referirse a un Al referirse a la máquina remota, es común llamarla **servidor**, pero también **máquina**, **VM** (máquina virtual), **nodo**. Todos esos se refieren a algún tipo de máquina remota, generalmente con Linux, donde ejecutas programas. -## Instala el Programa del Servidor +## Instala el Programa del Servidor { #install-the-server-program } Cuando instalas FastAPI, viene con un servidor de producción, Uvicorn, y puedes iniciarlo con el comando `fastapi run`. @@ -112,7 +100,7 @@ Cuando instalas FastAPI con algo como `pip install "fastapi[standard]"` ya obtie /// -## Ejecuta el Programa del Servidor +## Ejecuta el Programa del Servidor { #run-the-server-program } Si instalaste un servidor ASGI manualmente, normalmente necesitarías pasar una cadena de import en un formato especial para que importe tu aplicación FastAPI: @@ -121,7 +109,7 @@ Si instalaste un servidor ASGI manualmente, normalmente necesitarías pasar una ```console $ uvicorn main:app --host 0.0.0.0 --port 80 -INFO: Uvicorn corriendo en http://0.0.0.0:80 (Presiona CTRL+C para salir) +INFO: Uvicorn running on http://0.0.0.0:80 (Press CTRL+C to quit) ``` @@ -153,7 +141,7 @@ Ayuda mucho durante el **desarrollo**, pero **no** deberías usarla en **producc /// -## Conceptos de Despliegue +## Conceptos de Despliegue { #deployment-concepts } Estos ejemplos ejecutan el programa del servidor (por ejemplo, Uvicorn), iniciando **un solo proceso**, escuchando en todas las IPs (`0.0.0.0`) en un puerto predefinido (por ejemplo, `80`). diff --git a/docs/es/docs/deployment/server-workers.md b/docs/es/docs/deployment/server-workers.md index 1a1127ca6a..9cdd79bc0f 100644 --- a/docs/es/docs/deployment/server-workers.md +++ b/docs/es/docs/deployment/server-workers.md @@ -1,4 +1,4 @@ -# Servidores Workers - Uvicorn con Workers +# Servidores Workers - Uvicorn con Workers { #server-workers-uvicorn-with-workers } Vamos a revisar esos conceptos de despliegue de antes: @@ -25,7 +25,7 @@ En particular, cuando corras en **Kubernetes** probablemente **no** querrás usa /// -## Múltiples Workers +## Múltiples Workers { #multiple-workers } Puedes iniciar múltiples workers con la opción de línea de comando `--workers`: @@ -36,56 +36,43 @@ Si usas el comando `fastapi`:
```console -$
 fastapi run --workers 4 main.py
-INFO     Using path main.py
-INFO     Resolved absolute path /home/user/code/awesomeapp/main.py
-INFO     Searching for package file structure from directories with __init__.py files
-INFO     Importing from /home/user/code/awesomeapp
+$ fastapi run --workers 4 main.py
 
- ╭─ Python module file ─╮
- │                      │
- │  🐍 main.py          │
- │                      │
- ╰──────────────────────╯
+   FastAPI   Starting production server 🚀
 
-INFO     Importing module main
-INFO     Found importable FastAPI app
+             Searching for package file structure from directories with
+             __init__.py files
+             Importing from /home/user/code/awesomeapp
 
- ╭─ Importable FastAPI app ─╮
- │                          │
- │  from main import app    │
- │                          │
- ╰──────────────────────────╯
+    module   🐍 main.py
 
-INFO     Using import string main:app
+      code   Importing the FastAPI app object from the module with the
+             following code:
 
- ╭─────────── FastAPI CLI - Production mode ───────────╮
- │                                                     │
- │  Serving at: http://0.0.0.0:8000                    │
- │                                                     │
- │  API docs: http://0.0.0.0:8000/docs                 │
- │                                                     │
- │  Running in production mode, for development use:   │
- │                                                     │
- fastapi dev
- │                                                     │
- ╰─────────────────────────────────────────────────────╯
+             from main import app
 
-INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
-INFO:     Started parent process [27365]
-INFO:     Started server process [27368]
-INFO:     Waiting for application startup.
-INFO:     Application startup complete.
-INFO:     Started server process [27369]
-INFO:     Waiting for application startup.
-INFO:     Application startup complete.
-INFO:     Started server process [27370]
-INFO:     Waiting for application startup.
-INFO:     Application startup complete.
-INFO:     Started server process [27367]
-INFO:     Waiting for application startup.
-INFO:     Application startup complete.
-
+ app Using import string: main:app + + server Server started at http://0.0.0.0:8000 + server Documentation at http://0.0.0.0:8000/docs + + Logs: + + INFO Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to + quit) + INFO Started parent process [27365] + INFO Started server process [27368] + INFO Started server process [27369] + INFO Started server process [27370] + INFO Started server process [27367] + INFO Waiting for application startup. + INFO Waiting for application startup. + INFO Waiting for application startup. + INFO Waiting for application startup. + INFO Application startup complete. + INFO Application startup complete. + INFO Application startup complete. + INFO Application startup complete. ```
@@ -124,7 +111,7 @@ La única opción nueva aquí es `--workers` indicando a Uvicorn que inicie 4 wo También puedes ver que muestra el **PID** de cada proceso, `27365` para el proceso padre (este es el **gestor de procesos**) y uno para cada worker process: `27368`, `27369`, `27370`, y `27367`. -## Conceptos de Despliegue +## Conceptos de Despliegue { #deployment-concepts } Aquí viste cómo usar múltiples **workers** para **paralelizar** la ejecución de la aplicación, aprovechar los **múltiples núcleos** del CPU, y poder servir **más requests**. @@ -137,13 +124,13 @@ De la lista de conceptos de despliegue de antes, usar workers ayudaría principa * **Memoria** * **Pasos previos antes de empezar** -## Contenedores y Docker +## Contenedores y Docker { #containers-and-docker } En el próximo capítulo sobre [FastAPI en Contenedores - Docker](docker.md){.internal-link target=_blank} te explicaré algunas estrategias que podrías usar para manejar los otros **conceptos de despliegue**. Te mostraré cómo **construir tu propia imagen desde cero** para ejecutar un solo proceso de Uvicorn. Es un proceso sencillo y probablemente es lo que querrías hacer al usar un sistema de gestión de contenedores distribuido como **Kubernetes**. -## Resumen +## Resumen { #recap } Puedes usar múltiples worker processes con la opción CLI `--workers` con los comandos `fastapi` o `uvicorn` para aprovechar los **CPUs de múltiples núcleos**, para ejecutar **múltiples procesos en paralelo**. diff --git a/docs/es/docs/deployment/versions.md b/docs/es/docs/deployment/versions.md index d16ecf0a5b..193654b2d2 100644 --- a/docs/es/docs/deployment/versions.md +++ b/docs/es/docs/deployment/versions.md @@ -1,4 +1,4 @@ -# Sobre las versiones de FastAPI +# Sobre las versiones de FastAPI { #about-fastapi-versions } **FastAPI** ya se está utilizando en producción en muchas aplicaciones y sistemas. Y la cobertura de tests se mantiene al 100%. Pero su desarrollo sigue avanzando rápidamente. @@ -8,7 +8,7 @@ Por eso las versiones actuales siguen siendo `0.x.x`, esto refleja que cada vers Puedes crear aplicaciones de producción con **FastAPI** ahora mismo (y probablemente ya lo has estado haciendo desde hace algún tiempo), solo debes asegurarte de que utilizas una versión que funciona correctamente con el resto de tu código. -## Fijar tu versión de `fastapi` +## Fija tu versión de `fastapi` { #pin-your-fastapi-version } Lo primero que debes hacer es "fijar" la versión de **FastAPI** que estás usando a la versión específica más reciente que sabes que funciona correctamente para tu aplicación. @@ -32,11 +32,11 @@ eso significaría que usarías las versiones `0.112.0` o superiores, pero menore Si utilizas cualquier otra herramienta para gestionar tus instalaciones, como `uv`, Poetry, Pipenv, u otras, todas tienen una forma que puedes usar para definir versiones específicas para tus paquetes. -## Versiones disponibles +## Versiones disponibles { #available-versions } Puedes ver las versiones disponibles (por ejemplo, para revisar cuál es la más reciente) en las [Release Notes](../release-notes.md){.internal-link target=_blank}. -## Sobre las versiones +## Sobre las versiones { #about-versions } Siguiendo las convenciones del Semantic Versioning, cualquier versión por debajo de `1.0.0` podría potencialmente añadir cambios incompatibles. @@ -62,7 +62,7 @@ El "MINOR" es el número en el medio, por ejemplo, en `0.2.3`, la versión MINOR /// -## Actualizando las versiones de FastAPI +## Actualizando las versiones de FastAPI { #upgrading-the-fastapi-versions } Deberías añadir tests para tu aplicación. @@ -72,7 +72,7 @@ Después de tener tests, puedes actualizar la versión de **FastAPI** a una más Si todo está funcionando, o después de hacer los cambios necesarios, y todos tus tests pasan, entonces puedes fijar tu `fastapi` a esa nueva versión más reciente. -## Sobre Starlette +## Sobre Starlette { #about-starlette } No deberías fijar la versión de `starlette`. @@ -80,7 +80,7 @@ Diferentes versiones de **FastAPI** utilizarán una versión más reciente espec Así que, puedes simplemente dejar que **FastAPI** use la versión correcta de Starlette. -## Sobre Pydantic +## Sobre Pydantic { #about-pydantic } Pydantic incluye los tests para **FastAPI** con sus propios tests, así que nuevas versiones de Pydantic (por encima de `1.0.0`) siempre son compatibles con FastAPI. diff --git a/docs/es/docs/environment-variables.md b/docs/es/docs/environment-variables.md index 8e85b413cd..1b0941a7f5 100644 --- a/docs/es/docs/environment-variables.md +++ b/docs/es/docs/environment-variables.md @@ -1,4 +1,4 @@ -# Variables de Entorno +# Variables de Entorno { #environment-variables } /// tip | Consejo @@ -10,7 +10,7 @@ Una variable de entorno (también conocida como "**env var**") es una variable q Las variables de entorno pueden ser útiles para manejar **configuraciones** de aplicaciones, como parte de la **instalación** de Python, etc. -## Crear y Usar Variables de Entorno +## Crear y Usar Variables de Entorno { #create-and-use-env-vars } Puedes **crear** y usar variables de entorno en la **shell (terminal)**, sin necesidad de Python: @@ -50,7 +50,7 @@ Hello Wade Wilson //// -## Leer Variables de Entorno en Python +## Leer Variables de Entorno en Python { #read-env-vars-in-python } También podrías crear variables de entorno **fuera** de Python, en la terminal (o con cualquier otro método), y luego **leerlas en Python**. @@ -157,7 +157,7 @@ Puedes leer más al respecto en Uvicorn, un servidor ASGI de alto rendimiento y listo para producción. 😎 -## `fastapi dev` +## `fastapi dev` { #fastapi-dev } Ejecutar `fastapi dev` inicia el modo de desarrollo. Por defecto, **auto-reload** está habilitado, recargando automáticamente el servidor cuando realizas cambios en tu código. Esto consume muchos recursos y podría ser menos estable que cuando está deshabilitado. Deberías usarlo solo para desarrollo. También escucha en la dirección IP `127.0.0.1`, que es la IP para que tu máquina se comunique solo consigo misma (`localhost`). -## `fastapi run` +## `fastapi run` { #fastapi-run } Ejecutar `fastapi run` inicia FastAPI en modo de producción por defecto. diff --git a/docs/es/docs/features.md b/docs/es/docs/features.md index ac73bee164..9902b21fa5 100644 --- a/docs/es/docs/features.md +++ b/docs/es/docs/features.md @@ -1,17 +1,17 @@ -# Funcionalidades +# Funcionalidades { #features } -## Funcionalidades de FastAPI +## Funcionalidades de FastAPI { #fastapi-features } **FastAPI** te ofrece lo siguiente: -### Basado en estándares abiertos +### Basado en estándares abiertos { #based-on-open-standards } * OpenAPI para la creación de APIs, incluyendo declaraciones de path operations, parámetros, request bodies, seguridad, etc. * Documentación automática de modelos de datos con JSON Schema (ya que OpenAPI en sí mismo está basado en JSON Schema). * Diseñado alrededor de estos estándares, tras un estudio meticuloso. En lugar de ser una capa adicional. * Esto también permite el uso de **generación de código cliente automática** en muchos idiomas. -### Documentación automática +### Documentación automática { #automatic-docs } Interfaces web de documentación y exploración de APIs interactivas. Como el framework está basado en OpenAPI, hay múltiples opciones, 2 incluidas por defecto. @@ -23,7 +23,7 @@ Interfaces web de documentación y exploración de APIs interactivas. Como el fr ![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) -### Solo Python moderno +### Solo Python moderno { #just-modern-python } Todo está basado en declaraciones estándar de **tipos en Python** (gracias a Pydantic). Sin nueva sintaxis que aprender. Solo Python moderno estándar. @@ -71,7 +71,7 @@ Pasa las claves y valores del dict `second_user_data` directamente como argument /// -### Soporte del editor +### Soporte del editor { #editor-support } Todo el framework fue diseñado para ser fácil e intuitivo de usar, todas las decisiones fueron probadas en múltiples editores incluso antes de comenzar el desarrollo, para asegurar la mejor experiencia de desarrollo. @@ -95,13 +95,13 @@ Obtendrás autocompletado en código que podrías considerar imposible antes. Po No más escribir nombres de claves incorrectos, yendo de un lado a otro entre la documentación, o desplazándote hacia arriba y abajo para encontrar si finalmente usaste `username` o `user_name`. -### Breve +### Breve { #short } -Tiene **valores predeterminados** sensatos para todo, con configuraciones opcionales en todas partes. Todos los parámetros se pueden ajustar finamente para hacer lo que necesitas y para definir el API que necesitas. +Tiene **valores por defecto** sensatos para todo, con configuraciones opcionales en todas partes. Todos los parámetros se pueden ajustar finamente para hacer lo que necesitas y para definir el API que necesitas. Pero por defecto, todo **"simplemente funciona"**. -### Validación +### Validación { #validation } * Validación para la mayoría (¿o todas?) de los **tipos de datos** de Python, incluyendo: * Objetos JSON (`dict`). @@ -117,7 +117,7 @@ Pero por defecto, todo **"simplemente funciona"**. Toda la validación es manejada por **Pydantic**, una herramienta bien establecida y robusta. -### Seguridad y autenticación +### Seguridad y autenticación { #security-and-authentication } Seguridad y autenticación integradas. Sin ningún compromiso con bases de datos o modelos de datos. @@ -134,30 +134,30 @@ Además de todas las características de seguridad de Starlette (incluyendo **co Todo construido como herramientas y componentes reutilizables que son fáciles de integrar con tus sistemas, almacenes de datos, bases de datos relacionales y NoSQL, etc. -### Inyección de dependencias +### Inyección de dependencias { #dependency-injection } FastAPI incluye un sistema de Inyección de Dependencias extremadamente fácil de usar, pero extremadamente potente. -* Incluso las dependencias pueden tener dependencias, creando una jerarquía o **"gráfico de dependencias"**. +* Incluso las dependencias pueden tener dependencias, creando una jerarquía o **"grafo de dependencias"**. * Todo **manejado automáticamente** por el framework. * Todas las dependencias pueden requerir datos de los requests y **aumentar las restricciones de la path operation** y la documentación automática. * **Validación automática** incluso para los parámetros de *path operation* definidos en las dependencias. * Soporte para sistemas de autenticación de usuario complejos, **conexiones a bases de datos**, etc. * **Sin compromisos** con bases de datos, frontends, etc. Pero fácil integración con todos ellos. -### "Plug-ins" ilimitados +### "Plug-ins" ilimitados { #unlimited-plug-ins } O de otra manera, no hay necesidad de ellos, importa y usa el código que necesitas. Cualquier integración está diseñada para ser tan simple de usar (con dependencias) que puedes crear un "plug-in" para tu aplicación en 2 líneas de código usando la misma estructura y sintaxis utilizada para tus *path operations*. -### Probado +### Probado { #tested } * 100% de cobertura de tests. -* Código completamente anotado con tipos. +* 100% anotada con tipos code base. * Usado en aplicaciones en producción. -## Funcionalidades de Starlette +## Funcionalidades de Starlette { #starlette-features } **FastAPI** es totalmente compatible con (y está basado en) Starlette. Así que, cualquier código adicional de Starlette que tengas, también funcionará. @@ -173,13 +173,13 @@ Con **FastAPI** obtienes todas las funcionalidades de **Starlette** (ya que Fast * **CORS**, GZip, archivos estáticos, responses en streaming. * Soporte para **Session y Cookie**. * Cobertura de tests del 100%. -* Código completamente anotado con tipos. +* code base completamente anotada con tipos. -## Funcionalidades de Pydantic +## Funcionalidades de Pydantic { #pydantic-features } **FastAPI** es totalmente compatible con (y está basado en) Pydantic. Por lo tanto, cualquier código adicional de Pydantic que tengas, también funcionará. -Incluyendo paquetes externos también basados en Pydantic, como ORMs, ODMs para bases de datos. +Incluyendo paquetes externos también basados en Pydantic, como ORMs, ODMs para bases de datos. Esto también significa que, en muchos casos, puedes pasar el mismo objeto que obtienes de un request **directamente a la base de datos**, ya que todo se valida automáticamente. @@ -190,7 +190,7 @@ Con **FastAPI** obtienes todas las funcionalidades de **Pydantic** (ya que FastA * **Sin complicaciones**: * Sin micro-lenguaje de definición de esquemas nuevo que aprender. * Si conoces los tipos en Python sabes cómo usar Pydantic. -* Se lleva bien con tu **IDE/linter/cerebro**: +* Se lleva bien con tu **IDE/linter/cerebro**: * Porque las estructuras de datos de pydantic son solo instances de clases que defines; autocompletado, linting, mypy y tu intuición deberían funcionar correctamente con tus datos validados. * Valida **estructuras complejas**: * Uso de modelos jerárquicos de Pydantic, `List` y `Dict` de `typing` de Python, etc. diff --git a/docs/es/docs/help-fastapi.md b/docs/es/docs/help-fastapi.md index fa559f490e..cef9562249 100644 --- a/docs/es/docs/help-fastapi.md +++ b/docs/es/docs/help-fastapi.md @@ -1,4 +1,4 @@ -# Ayuda a FastAPI - Consigue Ayuda +# Ayuda a FastAPI - Consigue Ayuda { #help-fastapi-get-help } ¿Te gusta **FastAPI**? @@ -10,7 +10,7 @@ Hay formas muy sencillas de ayudar (varias implican solo uno o dos clics). Y también hay varias formas de conseguir ayuda. -## Suscríbete al boletín +## Suscríbete al boletín { #subscribe-to-the-newsletter } Puedes suscribirte al (esporádico) boletín [**FastAPI and friends**](newsletter.md){.internal-link target=_blank} para mantenerte al día sobre: @@ -20,17 +20,17 @@ Puedes suscribirte al (esporádico) boletín [**FastAPI and friends**](newslette * Cambios importantes 🚨 * Consejos y trucos ✅ -## Sigue a FastAPI en X (Twitter) +## Sigue a FastAPI en X (Twitter) { #follow-fastapi-on-x-twitter } Sigue a @fastapi en **X (Twitter)** para obtener las últimas noticias sobre **FastAPI**. 🐦 -## Dale una estrella a **FastAPI** en GitHub +## Dale una estrella a **FastAPI** en GitHub { #star-fastapi-in-github } Puedes "darle una estrella" a FastAPI en GitHub (haciendo clic en el botón de estrella en la parte superior derecha): https://github.com/fastapi/fastapi. ⭐️ Al agregar una estrella, otros usuarios podrán encontrarlo más fácilmente y ver que ya ha sido útil para otros. -## Observa el repositorio de GitHub para lanzamientos +## Observa el repositorio de GitHub para lanzamientos { #watch-the-github-repository-for-releases } Puedes "observar" FastAPI en GitHub (haciendo clic en el botón "watch" en la parte superior derecha): https://github.com/fastapi/fastapi. 👀 @@ -38,7 +38,7 @@ Allí puedes seleccionar "Releases only". Al hacerlo, recibirás notificaciones (en tu email) cada vez que haya un nuevo lanzamiento (una nueva versión) de **FastAPI** con correcciones de bugs y nuevas funcionalidades. -## Conéctate con el autor +## Conéctate con el autor { #connect-with-the-author } Puedes conectar conmigo (Sebastián Ramírez / `tiangolo`), el autor. @@ -57,19 +57,19 @@ Puedes: * Leer otras ideas, artículos, y leer sobre las herramientas que he creado. * Seguirme para leer lo que publico nuevo. -## Twittea sobre **FastAPI** +## Twittea sobre **FastAPI** { #tweet-about-fastapi } Twittea sobre **FastAPI** y dime a mí y a otros por qué te gusta. 🎉 Me encanta escuchar cómo se está utilizando **FastAPI**, qué te ha gustado, en qué proyecto/empresa lo estás usando, etc. -## Vota por FastAPI +## Vota por FastAPI { #vote-for-fastapi } * Vota por **FastAPI** en Slant. * Vota por **FastAPI** en AlternativeTo. * Di que usas **FastAPI** en StackShare. -## Ayuda a otros con preguntas en GitHub +## Ayuda a otros con preguntas en GitHub { #help-others-with-questions-in-github } Puedes intentar ayudar a otros con sus preguntas en: @@ -88,7 +88,7 @@ La idea es que la comunidad de **FastAPI** sea amable y acogedora. Al mismo tiem Aquí te explico cómo ayudar a otros con preguntas (en discusiones o issues): -### Entiende la pregunta +### Entiende la pregunta { #understand-the-question } * Revisa si puedes entender cuál es el **propósito** y el caso de uso de la persona que pregunta. @@ -98,7 +98,7 @@ Aquí te explico cómo ayudar a otros con preguntas (en discusiones o issues): * Si no puedes entender la pregunta, pide más **detalles**. -### Reproduce el problema +### Reproduce el problema { #reproduce-the-problem } En la mayoría de los casos y preguntas hay algo relacionado con el **código original** de la persona. @@ -108,13 +108,13 @@ En muchos casos solo copiarán un fragmento del código, pero eso no es suficien * Si te sientes muy generoso, puedes intentar **crear un ejemplo** así tú mismo, solo basado en la descripción del problema. Solo ten en cuenta que esto podría llevar mucho tiempo y podría ser mejor pedirles que aclaren el problema primero. -### Sugerir soluciones +### Sugerir soluciones { #suggest-solutions } * Después de poder entender la pregunta, puedes darles un posible **respuesta**. * En muchos casos, es mejor entender su **problema subyacente o caso de uso**, porque podría haber una mejor manera de resolverlo que lo que están intentando hacer. -### Pide cerrar +### Pide cerrar { #ask-to-close } Si responden, hay una alta probabilidad de que hayas resuelto su problema, felicidades, ¡**eres un héroe**! 🦸 @@ -123,7 +123,7 @@ Si responden, hay una alta probabilidad de que hayas resuelto su problema, felic * En GitHub Discussions: marquen el comentario como la **respuesta**. * En GitHub Issues: **cierren** el issue. -## Observa el repositorio de GitHub +## Observa el repositorio de GitHub { #watch-the-github-repository } Puedes "observar" FastAPI en GitHub (haciendo clic en el botón "watch" en la parte superior derecha): https://github.com/fastapi/fastapi. 👀 @@ -131,7 +131,7 @@ Si seleccionas "Watching" en lugar de "Releases only", recibirás notificaciones Luego puedes intentar ayudarlos a resolver esas preguntas. -## Haz preguntas +## Haz preguntas { #ask-questions } Puedes crear una nueva pregunta en el repositorio de GitHub, por ejemplo, para: @@ -140,7 +140,7 @@ Puedes Discord 👥 y charla con otros en la comunidad de FastAPI. @@ -237,7 +237,7 @@ Usa el chat solo para otras conversaciones generales. /// -### No uses el chat para preguntas +### No uses el chat para preguntas { #dont-use-the-chat-for-questions } Ten en cuenta que dado que los chats permiten una "conversación más libre", es fácil hacer preguntas que son demasiado generales y más difíciles de responder, por lo que es posible que no recibas respuestas. @@ -247,22 +247,9 @@ Las conversaciones en los sistemas de chat tampoco son tan fácilmente buscables Por otro lado, hay miles de usuarios en los sistemas de chat, por lo que hay muchas posibilidades de que encuentres a alguien con quien hablar allí, casi todo el tiempo. 😄 -## Patrocina al autor +## Patrocina al autor { #sponsor-the-author } -También puedes apoyar financieramente al autor (a mí) a través de GitHub sponsors. - -Allí podrías comprarme un café ☕️ para decir gracias. 😄 - -Y también puedes convertirte en un sponsor de Plata o de Oro para FastAPI. 🏅🎉 - -## Patrocina las herramientas que impulsan FastAPI - -Como habrás visto en la documentación, FastAPI se apoya en los hombros de gigantes, Starlette y Pydantic. - -También puedes patrocinar: - -* Samuel Colvin (Pydantic) -* Encode (Starlette, Uvicorn) +Si tu **producto/empresa** depende de o está relacionado con **FastAPI** y quieres llegar a sus usuarios, puedes patrocinar al autor (a mí) a través de GitHub sponsors. Según el nivel, podrías obtener algunos beneficios extra, como una insignia en la documentación. 🎁 --- diff --git a/docs/es/docs/history-design-future.md b/docs/es/docs/history-design-future.md index 7ed4fdf056..79835440bb 100644 --- a/docs/es/docs/history-design-future.md +++ b/docs/es/docs/history-design-future.md @@ -1,4 +1,4 @@ -# Historia, Diseño y Futuro +# Historia, Diseño y Futuro { #history-design-and-future } Hace algún tiempo, un usuario de **FastAPI** preguntó: @@ -6,7 +6,7 @@ Hace algún tiempo, **Pydantic** por sus ventajas. @@ -60,11 +60,11 @@ Luego contribuí a este, para hacerlo totalmente compatible con JSON Schema, par Durante el desarrollo, también contribuí a **Starlette**, el otro requisito clave. -## Desarrollo +## Desarrollo { #development } Para cuando comencé a crear el propio **FastAPI**, la mayoría de las piezas ya estaban en su lugar, el diseño estaba definido, los requisitos y herramientas estaban listos, y el conocimiento sobre los estándares y especificaciones estaba claro y fresco. -## Futuro +## Futuro { #future } A este punto, ya está claro que **FastAPI** con sus ideas está siendo útil para muchas personas. diff --git a/docs/es/docs/how-to/authentication-error-status-code.md b/docs/es/docs/how-to/authentication-error-status-code.md new file mode 100644 index 0000000000..9fff6c93d3 --- /dev/null +++ b/docs/es/docs/how-to/authentication-error-status-code.md @@ -0,0 +1,17 @@ +# Usar los códigos de estado antiguos 403 para errores de autenticación { #use-old-403-authentication-error-status-codes } + +Antes de FastAPI versión `0.122.0`, cuando las utilidades de seguridad integradas devolvían un error al cliente después de una autenticación fallida, usaban el código de estado HTTP `403 Forbidden`. + +A partir de FastAPI versión `0.122.0`, usan el código de estado HTTP `401 Unauthorized`, más apropiado, y devuelven un `WWW-Authenticate` header adecuado en la response, siguiendo las especificaciones HTTP, RFC 7235, RFC 9110. + +Pero si por alguna razón tus clientes dependen del comportamiento anterior, puedes volver a él sobrescribiendo el método `make_not_authenticated_error` en tus clases de seguridad. + +Por ejemplo, puedes crear una subclase de `HTTPBearer` que devuelva un error `403 Forbidden` en lugar del `401 Unauthorized` por defecto: + +{* ../../docs_src/authentication_error_status_code/tutorial001_an_py39.py hl[9:13] *} + +/// tip | Consejo + +Ten en cuenta que la función devuelve la instance de la excepción, no la lanza. El lanzamiento se hace en el resto del código interno. + +/// diff --git a/docs/es/docs/how-to/conditional-openapi.md b/docs/es/docs/how-to/conditional-openapi.md index 4f806ef6cd..a06ad95489 100644 --- a/docs/es/docs/how-to/conditional-openapi.md +++ b/docs/es/docs/how-to/conditional-openapi.md @@ -1,8 +1,8 @@ -# OpenAPI Condicional +# OpenAPI condicional { #conditional-openapi } Si lo necesitaras, podrías usar configuraciones y variables de entorno para configurar OpenAPI condicionalmente según el entorno, e incluso desactivarlo por completo. -## Sobre seguridad, APIs y documentación +## Sobre seguridad, APIs y documentación { #about-security-apis-and-docs } Ocultar las interfaces de usuario de la documentación en producción *no debería* ser la forma de proteger tu API. @@ -17,21 +17,21 @@ Si quieres asegurar tu API, hay varias cosas mejores que puedes hacer, por ejemp * Asegúrate de tener modelos Pydantic bien definidos para tus request bodies y responses. * Configura los permisos y roles necesarios usando dependencias. * Nunca guardes contraseñas en texto plano, solo hashes de contraseñas. -* Implementa y utiliza herramientas criptográficas bien conocidas, como Passlib y JWT tokens, etc. -* Añade controles de permisos más detallados con OAuth2 scopes donde sea necesario. +* Implementa y utiliza herramientas criptográficas bien conocidas, como pwdlib y JWT tokens, etc. +* Añade controles de permisos más detallados con Scopes de OAuth2 donde sea necesario. * ...etc. No obstante, podrías tener un caso de uso muy específico donde realmente necesites desactivar la documentación de la API para algún entorno (por ejemplo, para producción) o dependiendo de configuraciones de variables de entorno. -## OpenAPI condicional desde configuraciones y variables de entorno +## OpenAPI condicional desde configuraciones y variables de entorno { #conditional-openapi-from-settings-and-env-vars } Puedes usar fácilmente las mismas configuraciones de Pydantic para configurar tu OpenAPI generado y las interfaces de usuario de la documentación. Por ejemplo: -{* ../../docs_src/conditional_openapi/tutorial001.py hl[6,11] *} +{* ../../docs_src/conditional_openapi/tutorial001_py39.py hl[6,11] *} -Aquí declaramos la configuración `openapi_url` con el mismo valor predeterminado de `"/openapi.json"`. +Aquí declaramos la configuración `openapi_url` con el mismo valor por defecto de `"/openapi.json"`. Y luego la usamos al crear la app de `FastAPI`. diff --git a/docs/es/docs/how-to/configure-swagger-ui.md b/docs/es/docs/how-to/configure-swagger-ui.md index 4243c191cc..b2865d77d5 100644 --- a/docs/es/docs/how-to/configure-swagger-ui.md +++ b/docs/es/docs/how-to/configure-swagger-ui.md @@ -1,4 +1,4 @@ -# Configurar Swagger UI +# Configurar Swagger UI { #configure-swagger-ui } Puedes configurar algunos parámetros adicionales de Swagger UI. @@ -8,7 +8,7 @@ Para configurarlos, pasa el argumento `swagger_ui_parameters` al crear el objeto FastAPI convierte las configuraciones a **JSON** para hacerlas compatibles con JavaScript, ya que eso es lo que Swagger UI necesita. -## Desactivar el resaltado de sintaxis +## Desactivar el resaltado de sintaxis { #disable-syntax-highlighting } Por ejemplo, podrías desactivar el resaltado de sintaxis en Swagger UI. @@ -18,41 +18,41 @@ Sin cambiar la configuración, el resaltado de sintaxis está activado por defec Pero puedes desactivarlo estableciendo `syntaxHighlight` en `False`: -{* ../../docs_src/configure_swagger_ui/tutorial001.py hl[3] *} +{* ../../docs_src/configure_swagger_ui/tutorial001_py39.py hl[3] *} ...y entonces Swagger UI ya no mostrará el resaltado de sintaxis: -## Cambiar el tema +## Cambiar el tema { #change-the-theme } De la misma manera, podrías configurar el tema del resaltado de sintaxis con la clave `"syntaxHighlight.theme"` (ten en cuenta que tiene un punto en el medio): -{* ../../docs_src/configure_swagger_ui/tutorial002.py hl[3] *} +{* ../../docs_src/configure_swagger_ui/tutorial002_py39.py hl[3] *} Esa configuración cambiaría el tema de color del resaltado de sintaxis: -## Cambiar los parámetros predeterminados de Swagger UI +## Cambiar los parámetros predeterminados de Swagger UI { #change-default-swagger-ui-parameters } FastAPI incluye algunos parámetros de configuración predeterminados apropiados para la mayoría de los casos de uso. Incluye estas configuraciones predeterminadas: -{* ../../fastapi/openapi/docs.py ln[8:23] hl[17:23] *} +{* ../../fastapi/openapi/docs.py ln[9:24] hl[18:24] *} Puedes sobrescribir cualquiera de ellos estableciendo un valor diferente en el argumento `swagger_ui_parameters`. Por ejemplo, para desactivar `deepLinking` podrías pasar estas configuraciones a `swagger_ui_parameters`: -{* ../../docs_src/configure_swagger_ui/tutorial003.py hl[3] *} +{* ../../docs_src/configure_swagger_ui/tutorial003_py39.py hl[3] *} -## Otros parámetros de Swagger UI +## Otros parámetros de Swagger UI { #other-swagger-ui-parameters } Para ver todas las demás configuraciones posibles que puedes usar, lee la documentación oficial de los parámetros de Swagger UI. -## Configuraciones solo de JavaScript +## Configuraciones solo de JavaScript { #javascript-only-settings } Swagger UI también permite otras configuraciones que son objetos **solo de JavaScript** (por ejemplo, funciones de JavaScript). diff --git a/docs/es/docs/how-to/custom-docs-ui-assets.md b/docs/es/docs/how-to/custom-docs-ui-assets.md index 0a03ff330b..acd3f8d6d6 100644 --- a/docs/es/docs/how-to/custom-docs-ui-assets.md +++ b/docs/es/docs/how-to/custom-docs-ui-assets.md @@ -1,26 +1,26 @@ -# Recursos Estáticos Personalizados para la Docs UI (Self-Hosting) +# Recursos Estáticos Personalizados para la Docs UI (self hosting) { #custom-docs-ui-static-assets-self-hosting } La documentación de la API utiliza **Swagger UI** y **ReDoc**, y cada uno de estos necesita algunos archivos JavaScript y CSS. -Por defecto, esos archivos se sirven desde un CDN. +Por defecto, esos archivos se sirven desde un CDN. Pero es posible personalizarlo, puedes establecer un CDN específico, o servir los archivos tú mismo. -## CDN Personalizado para JavaScript y CSS +## CDN Personalizado para JavaScript y CSS { #custom-cdn-for-javascript-and-css } -Digamos que quieres usar un CDN diferente, por ejemplo, quieres usar `https://unpkg.com/`. +Digamos que quieres usar un CDN diferente, por ejemplo, quieres usar `https://unpkg.com/`. Esto podría ser útil si, por ejemplo, vives en un país que restringe algunas URLs. -### Desactiva la documentación automática +### Desactiva la documentación automática { #disable-the-automatic-docs } -El primer paso es desactivar la documentación automática, ya que por defecto, esos usan el CDN predeterminado. +El primer paso es desactivar la documentación automática, ya que por defecto, esos usan el CDN por defecto. Para desactivarlos, establece sus URLs en `None` cuando crees tu aplicación de `FastAPI`: -{* ../../docs_src/custom_docs_ui/tutorial001.py hl[8] *} +{* ../../docs_src/custom_docs_ui/tutorial001_py39.py hl[8] *} -### Incluye la documentación personalizada +### Incluye la documentación personalizada { #include-the-custom-docs } Ahora puedes crear las *path operations* para la documentación personalizada. @@ -28,13 +28,13 @@ Puedes reutilizar las funciones internas de FastAPI para crear las páginas HTML * `openapi_url`: la URL donde la página HTML para la documentación puede obtener el OpenAPI esquema de tu API. Puedes usar aquí el atributo `app.openapi_url`. * `title`: el título de tu API. -* `oauth2_redirect_url`: puedes usar `app.swagger_ui_oauth2_redirect_url` aquí para usar el valor predeterminado. +* `oauth2_redirect_url`: puedes usar `app.swagger_ui_oauth2_redirect_url` aquí para usar el valor por defecto. * `swagger_js_url`: la URL donde el HTML para tu documentación de Swagger UI puede obtener el archivo **JavaScript**. Esta es la URL personalizada del CDN. * `swagger_css_url`: la URL donde el HTML para tu documentación de Swagger UI puede obtener el archivo **CSS**. Esta es la URL personalizada del CDN. Y de manera similar para ReDoc... -{* ../../docs_src/custom_docs_ui/tutorial001.py hl[2:6,11:19,22:24,27:33] *} +{* ../../docs_src/custom_docs_ui/tutorial001_py39.py hl[2:6,11:19,22:24,27:33] *} /// tip | Consejo @@ -46,23 +46,23 @@ Swagger UI lo manejará detrás de escena para ti, pero necesita este auxiliar d /// -### Crea una *path operation* para probarlo +### Crea una *path operation* para probarlo { #create-a-path-operation-to-test-it } Ahora, para poder probar que todo funciona, crea una *path operation*: -{* ../../docs_src/custom_docs_ui/tutorial001.py hl[36:38] *} +{* ../../docs_src/custom_docs_ui/tutorial001_py39.py hl[36:38] *} -### Pruébalo +### Pruébalo { #test-it } Ahora, deberías poder ir a tu documentación en http://127.0.0.1:8000/docs, y recargar la página, cargará esos recursos desde el nuevo CDN. -## Self-hosting de JavaScript y CSS para la documentación +## self hosting de JavaScript y CSS para la documentación { #self-hosting-javascript-and-css-for-docs } -El self-hosting de JavaScript y CSS podría ser útil si, por ejemplo, necesitas que tu aplicación siga funcionando incluso offline, sin acceso a Internet, o en una red local. +El self hosting de JavaScript y CSS podría ser útil si, por ejemplo, necesitas que tu aplicación siga funcionando incluso offline, sin acceso a Internet, o en una red local. Aquí verás cómo servir esos archivos tú mismo, en la misma aplicación de FastAPI, y configurar la documentación para usarla. -### Estructura de archivos del proyecto +### Estructura de archivos del proyecto { #project-file-structure } Supongamos que la estructura de archivos de tu proyecto se ve así: @@ -85,7 +85,7 @@ Tu nueva estructura de archivos podría verse así: └── static/ ``` -### Descarga los archivos +### Descarga los archivos { #download-the-files } Descarga los archivos estáticos necesarios para la documentación y ponlos en ese directorio `static/`. @@ -113,14 +113,14 @@ Después de eso, tu estructura de archivos podría verse así: └── swagger-ui.css ``` -### Sirve los archivos estáticos +### Sirve los archivos estáticos { #serve-the-static-files } * Importa `StaticFiles`. * "Monta" una instance de `StaticFiles()` en un path específico. -{* ../../docs_src/custom_docs_ui/tutorial002.py hl[7,11] *} +{* ../../docs_src/custom_docs_ui/tutorial002_py39.py hl[7,11] *} -### Prueba los archivos estáticos +### Prueba los archivos estáticos { #test-the-static-files } Inicia tu aplicación y ve a http://127.0.0.1:8000/static/redoc.standalone.js. @@ -138,15 +138,15 @@ Eso confirma que puedes servir archivos estáticos desde tu aplicación, y que c Ahora podemos configurar la aplicación para usar esos archivos estáticos para la documentación. -### Desactiva la documentación automática para archivos estáticos +### Desactiva la documentación automática para archivos estáticos { #disable-the-automatic-docs-for-static-files } Igual que cuando usas un CDN personalizado, el primer paso es desactivar la documentación automática, ya que esos usan el CDN por defecto. Para desactivarlos, establece sus URLs en `None` cuando crees tu aplicación de `FastAPI`: -{* ../../docs_src/custom_docs_ui/tutorial002.py hl[9] *} +{* ../../docs_src/custom_docs_ui/tutorial002_py39.py hl[9] *} -### Incluye la documentación personalizada para archivos estáticos +### Incluye la documentación personalizada para archivos estáticos { #include-the-custom-docs-for-static-files } Y de la misma manera que con un CDN personalizado, ahora puedes crear las *path operations* para la documentación personalizada. @@ -154,13 +154,13 @@ Nuevamente, puedes reutilizar las funciones internas de FastAPI para crear las p * `openapi_url`: la URL donde la página HTML para la documentación puede obtener el OpenAPI esquema de tu API. Puedes usar aquí el atributo `app.openapi_url`. * `title`: el título de tu API. -* `oauth2_redirect_url`: puedes usar `app.swagger_ui_oauth2_redirect_url` aquí para usar el valor predeterminado. +* `oauth2_redirect_url`: puedes usar `app.swagger_ui_oauth2_redirect_url` aquí para usar el valor por defecto. * `swagger_js_url`: la URL donde el HTML para tu documentación de Swagger UI puede obtener el archivo **JavaScript**. **Este es el que tu propia aplicación está sirviendo ahora**. * `swagger_css_url`: la URL donde el HTML para tu documentación de Swagger UI puede obtener el archivo **CSS**. **Este es el que tu propia aplicación está sirviendo ahora**. Y de manera similar para ReDoc... -{* ../../docs_src/custom_docs_ui/tutorial002.py hl[2:6,14:22,25:27,30:36] *} +{* ../../docs_src/custom_docs_ui/tutorial002_py39.py hl[2:6,14:22,25:27,30:36] *} /// tip | Consejo @@ -172,13 +172,13 @@ Swagger UI lo manejará detrás de escena para ti, pero necesita este auxiliar d /// -### Crea una *path operation* para probar archivos estáticos +### Crea una *path operation* para probar archivos estáticos { #create-a-path-operation-to-test-static-files } Ahora, para poder probar que todo funciona, crea una *path operation*: -{* ../../docs_src/custom_docs_ui/tutorial002.py hl[39:41] *} +{* ../../docs_src/custom_docs_ui/tutorial002_py39.py hl[39:41] *} -### Prueba la UI de Archivos Estáticos +### Prueba la UI de Archivos Estáticos { #test-static-files-ui } Ahora, deberías poder desconectar tu WiFi, ir a tu documentación en http://127.0.0.1:8000/docs, y recargar la página. diff --git a/docs/es/docs/how-to/custom-request-and-route.md b/docs/es/docs/how-to/custom-request-and-route.md index a6ea657d15..ff13196f8a 100644 --- a/docs/es/docs/how-to/custom-request-and-route.md +++ b/docs/es/docs/how-to/custom-request-and-route.md @@ -1,4 +1,4 @@ -# Clase personalizada de Request y APIRoute +# Clase personalizada de Request y APIRoute { #custom-request-and-apiroute-class } En algunos casos, puede que quieras sobrescribir la lógica utilizada por las clases `Request` y `APIRoute`. @@ -14,7 +14,7 @@ Si apenas estás comenzando con **FastAPI**, quizás quieras saltar esta secció /// -## Casos de uso +## Casos de uso { #use-cases } Algunos casos de uso incluyen: @@ -22,13 +22,13 @@ Algunos casos de uso incluyen: * Descomprimir cuerpos de requests comprimidos con gzip. * Registrar automáticamente todos los request bodies. -## Manejo de codificaciones personalizadas de request body +## Manejo de codificaciones personalizadas de request body { #handling-custom-request-body-encodings } Veamos cómo hacer uso de una subclase personalizada de `Request` para descomprimir requests gzip. Y una subclase de `APIRoute` para usar esa clase de request personalizada. -### Crear una clase personalizada `GzipRequest` +### Crear una clase personalizada `GzipRequest` { #create-a-custom-gziprequest-class } /// tip | Consejo @@ -42,9 +42,9 @@ Si no hay `gzip` en el header, no intentará descomprimir el cuerpo. De esa manera, la misma clase de ruta puede manejar requests comprimidos con gzip o no comprimidos. -{* ../../docs_src/custom_request_and_route/tutorial001.py hl[8:15] *} +{* ../../docs_src/custom_request_and_route/tutorial001_an_py310.py hl[9:16] *} -### Crear una clase personalizada `GzipRoute` +### Crear una clase personalizada `GzipRoute` { #create-a-custom-gziproute-class } A continuación, creamos una subclase personalizada de `fastapi.routing.APIRoute` que hará uso de `GzipRequest`. @@ -54,7 +54,7 @@ Este método devuelve una función. Y esa función es la que recibirá un reques Aquí lo usamos para crear un `GzipRequest` a partir del request original. -{* ../../docs_src/custom_request_and_route/tutorial001.py hl[18:26] *} +{* ../../docs_src/custom_request_and_route/tutorial001_an_py310.py hl[19:27] *} /// note | Detalles técnicos @@ -78,7 +78,7 @@ Después de eso, toda la lógica de procesamiento es la misma. Pero debido a nuestros cambios en `GzipRequest.body`, el request body se descomprimirá automáticamente cuando sea cargado por **FastAPI** si es necesario. -## Accediendo al request body en un manejador de excepciones +## Accediendo al request body en un manejador de excepciones { #accessing-the-request-body-in-an-exception-handler } /// tip | Consejo @@ -92,18 +92,18 @@ También podemos usar este mismo enfoque para acceder al request body en un mane Todo lo que necesitamos hacer es manejar el request dentro de un bloque `try`/`except`: -{* ../../docs_src/custom_request_and_route/tutorial002.py hl[13,15] *} +{* ../../docs_src/custom_request_and_route/tutorial002_an_py310.py hl[14,16] *} Si ocurre una excepción, la `Request instance` aún estará en el alcance, así que podemos leer y hacer uso del request body cuando manejamos el error: -{* ../../docs_src/custom_request_and_route/tutorial002.py hl[16:18] *} +{* ../../docs_src/custom_request_and_route/tutorial002_an_py310.py hl[17:19] *} -## Clase personalizada `APIRoute` en un router +## Clase personalizada `APIRoute` en un router { #custom-apiroute-class-in-a-router } También puedes establecer el parámetro `route_class` de un `APIRouter`: -{* ../../docs_src/custom_request_and_route/tutorial003.py hl[26] *} +{* ../../docs_src/custom_request_and_route/tutorial003_py310.py hl[26] *} En este ejemplo, las *path operations* bajo el `router` usarán la clase personalizada `TimedRoute`, y tendrán un header `X-Response-Time` extra en el response con el tiempo que tomó generar el response: -{* ../../docs_src/custom_request_and_route/tutorial003.py hl[13:20] *} +{* ../../docs_src/custom_request_and_route/tutorial003_py310.py hl[13:20] *} diff --git a/docs/es/docs/how-to/extending-openapi.md b/docs/es/docs/how-to/extending-openapi.md index 3dbdd666be..2611b6e1b6 100644 --- a/docs/es/docs/how-to/extending-openapi.md +++ b/docs/es/docs/how-to/extending-openapi.md @@ -1,10 +1,10 @@ -# Extender OpenAPI +# Extender OpenAPI { #extending-openapi } Hay algunos casos en los que podrías necesitar modificar el esquema de OpenAPI generado. En esta sección verás cómo hacerlo. -## El proceso normal +## El proceso normal { #the-normal-process } El proceso normal (por defecto) es el siguiente. @@ -33,31 +33,31 @@ El parámetro `summary` está disponible en OpenAPI 3.1.0 y versiones superiores /// -## Sobrescribir los valores por defecto +## Sobrescribir los valores por defecto { #overriding-the-defaults } Usando la información anterior, puedes usar la misma función de utilidad para generar el esquema de OpenAPI y sobrescribir cada parte que necesites. Por ejemplo, vamos a añadir la extensión OpenAPI de ReDoc para incluir un logo personalizado. -### **FastAPI** normal +### **FastAPI** normal { #normal-fastapi } Primero, escribe toda tu aplicación **FastAPI** como normalmente: -{* ../../docs_src/extending_openapi/tutorial001.py hl[1,4,7:9] *} +{* ../../docs_src/extending_openapi/tutorial001_py39.py hl[1,4,7:9] *} -### Generar el esquema de OpenAPI +### Generar el esquema de OpenAPI { #generate-the-openapi-schema } Luego, usa la misma función de utilidad para generar el esquema de OpenAPI, dentro de una función `custom_openapi()`: -{* ../../docs_src/extending_openapi/tutorial001.py hl[2,15:21] *} +{* ../../docs_src/extending_openapi/tutorial001_py39.py hl[2,15:21] *} -### Modificar el esquema de OpenAPI +### Modificar el esquema de OpenAPI { #modify-the-openapi-schema } Ahora puedes añadir la extensión de ReDoc, agregando un `x-logo` personalizado al "objeto" `info` en el esquema de OpenAPI: -{* ../../docs_src/extending_openapi/tutorial001.py hl[22:24] *} +{* ../../docs_src/extending_openapi/tutorial001_py39.py hl[22:24] *} -### Cachear el esquema de OpenAPI +### Cachear el esquema de OpenAPI { #cache-the-openapi-schema } Puedes usar la propiedad `.openapi_schema` como un "cache", para almacenar tu esquema generado. @@ -65,15 +65,15 @@ De esa forma, tu aplicación no tendrá que generar el esquema cada vez que un u Se generará solo una vez, y luego se usará el mismo esquema cacheado para las siguientes requests. -{* ../../docs_src/extending_openapi/tutorial001.py hl[13:14,25:26] *} +{* ../../docs_src/extending_openapi/tutorial001_py39.py hl[13:14,25:26] *} -### Sobrescribir el método +### Sobrescribir el método { #override-the-method } Ahora puedes reemplazar el método `.openapi()` por tu nueva función. -{* ../../docs_src/extending_openapi/tutorial001.py hl[29] *} +{* ../../docs_src/extending_openapi/tutorial001_py39.py hl[29] *} -### Revisa +### Revisa { #check-it } Una vez que vayas a http://127.0.0.1:8000/redoc verás que estás usando tu logo personalizado (en este ejemplo, el logo de **FastAPI**): diff --git a/docs/es/docs/how-to/general.md b/docs/es/docs/how-to/general.md index e10621ce51..3a3dc82943 100644 --- a/docs/es/docs/how-to/general.md +++ b/docs/es/docs/how-to/general.md @@ -1,39 +1,39 @@ -# General - Cómo Hacer - Recetas +# General - Cómo Hacer - Recetas { #general-how-to-recipes } Aquí tienes varias indicaciones hacia otros lugares en la documentación, para preguntas generales o frecuentes. -## Filtrar Datos - Seguridad +## Filtrar Datos - Seguridad { #filter-data-security } Para asegurarte de que no devuelves más datos de los que deberías, lee la documentación para [Tutorial - Modelo de Response - Tipo de Retorno](../tutorial/response-model.md){.internal-link target=_blank}. -## Etiquetas de Documentación - OpenAPI +## Etiquetas de Documentación - OpenAPI { #documentation-tags-openapi } Para agregar etiquetas a tus *path operations*, y agruparlas en la interfaz de usuario de la documentación, lee la documentación para [Tutorial - Configuraciones de Path Operation - Etiquetas](../tutorial/path-operation-configuration.md#tags){.internal-link target=_blank}. -## Resumen y Descripción de Documentación - OpenAPI +## Resumen y Descripción de Documentación - OpenAPI { #documentation-summary-and-description-openapi } Para agregar un resumen y descripción a tus *path operations*, y mostrarlos en la interfaz de usuario de la documentación, lee la documentación para [Tutorial - Configuraciones de Path Operation - Resumen y Descripción](../tutorial/path-operation-configuration.md#summary-and-description){.internal-link target=_blank}. -## Documentación de Descripción de Response - OpenAPI +## Documentación de Descripción de Response - OpenAPI { #documentation-response-description-openapi } Para definir la descripción del response, mostrada en la interfaz de usuario de la documentación, lee la documentación para [Tutorial - Configuraciones de Path Operation - Descripción del Response](../tutorial/path-operation-configuration.md#response-description){.internal-link target=_blank}. -## Documentar la Deprecación de una *Path Operation* - OpenAPI +## Documentar la Deprecación de una *Path Operation* - OpenAPI { #documentation-deprecate-a-path-operation-openapi } Para deprecar una *path operation*, y mostrarla en la interfaz de usuario de la documentación, lee la documentación para [Tutorial - Configuraciones de Path Operation - Deprecación](../tutorial/path-operation-configuration.md#deprecate-a-path-operation){.internal-link target=_blank}. -## Convertir cualquier Dato a Compatible con JSON +## Convertir cualquier Dato a Compatible con JSON { #convert-any-data-to-json-compatible } Para convertir cualquier dato a compatible con JSON, lee la documentación para [Tutorial - Codificador Compatible con JSON](../tutorial/encoder.md){.internal-link target=_blank}. -## Metadatos OpenAPI - Documentación +## Metadatos OpenAPI - Documentación { #openapi-metadata-docs } Para agregar metadatos a tu esquema de OpenAPI, incluyendo una licencia, versión, contacto, etc, lee la documentación para [Tutorial - Metadatos y URLs de Documentación](../tutorial/metadata.md){.internal-link target=_blank}. -## URL Personalizada de OpenAPI +## URL Personalizada de OpenAPI { #openapi-custom-url } Para personalizar la URL de OpenAPI (o eliminarla), lee la documentación para [Tutorial - Metadatos y URLs de Documentación](../tutorial/metadata.md#openapi-url){.internal-link target=_blank}. -## URLs de Documentación de OpenAPI +## URLs de Documentación de OpenAPI { #openapi-docs-urls } Para actualizar las URLs usadas para las interfaces de usuario de documentación generadas automáticamente, lee la documentación para [Tutorial - Metadatos y URLs de Documentación](../tutorial/metadata.md#docs-urls){.internal-link target=_blank}. diff --git a/docs/es/docs/how-to/graphql.md b/docs/es/docs/how-to/graphql.md index 52f1638097..e50c1ae0ac 100644 --- a/docs/es/docs/how-to/graphql.md +++ b/docs/es/docs/how-to/graphql.md @@ -1,4 +1,4 @@ -# GraphQL +# GraphQL { #graphql } Como **FastAPI** se basa en el estándar **ASGI**, es muy fácil integrar cualquier paquete de **GraphQL** que también sea compatible con ASGI. @@ -14,7 +14,7 @@ Asegúrate de evaluar si los **beneficios** para tu caso de uso compensan los ** /// -## Paquetes de GraphQL +## Paquetes de GraphQL { #graphql-libraries } Aquí algunos de los paquetes de **GraphQL** que tienen soporte **ASGI**. Podrías usarlos con **FastAPI**: @@ -27,7 +27,7 @@ Aquí algunos de los paquetes de **GraphQL** que tienen soporte **ASGI**. Podrí * Graphene * Con starlette-graphene3 -## GraphQL con Strawberry +## GraphQL con Strawberry { #graphql-with-strawberry } Si necesitas o quieres trabajar con **GraphQL**, **Strawberry** es el paquete **recomendado** ya que tiene un diseño muy similar al diseño de **FastAPI**, todo basado en **anotaciones de tipos**. @@ -35,13 +35,13 @@ Dependiendo de tu caso de uso, podrías preferir usar un paquete diferente, pero Aquí tienes una pequeña vista previa de cómo podrías integrar Strawberry con FastAPI: -{* ../../docs_src/graphql/tutorial001.py hl[3,22,25:26] *} +{* ../../docs_src/graphql_/tutorial001_py39.py hl[3,22,25] *} Puedes aprender más sobre Strawberry en la documentación de Strawberry. Y también la documentación sobre Strawberry con FastAPI. -## `GraphQLApp` viejo de Starlette +## `GraphQLApp` viejo de Starlette { #older-graphqlapp-from-starlette } Las versiones anteriores de Starlette incluían una clase `GraphQLApp` para integrar con Graphene. @@ -53,7 +53,7 @@ Si necesitas GraphQL, aún te recomendaría revisar documentación oficial de GraphQL. diff --git a/docs/es/docs/how-to/index.md b/docs/es/docs/how-to/index.md index 152499af88..6f5988049a 100644 --- a/docs/es/docs/how-to/index.md +++ b/docs/es/docs/how-to/index.md @@ -1,4 +1,4 @@ -# How To - Recetas +# Cómo hacer - Recetas { #how-to-recipes } Aquí verás diferentes recetas o guías de "cómo hacer" para **varios temas**. @@ -8,6 +8,6 @@ Si algo parece interesante y útil para tu proyecto, adelante y revísalo, pero /// tip | Consejo -Si quieres **aprender FastAPI** de una manera estructurada (recomendado), ve y lee el [Tutorial - User Guide](../tutorial/index.md){.internal-link target=_blank} capítulo por capítulo. +Si quieres **aprender FastAPI** de una manera estructurada (recomendado), ve y lee el [Tutorial - Guía de Usuario](../tutorial/index.md){.internal-link target=_blank} capítulo por capítulo en su lugar. /// diff --git a/docs/es/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md b/docs/es/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md new file mode 100644 index 0000000000..c862ace902 --- /dev/null +++ b/docs/es/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md @@ -0,0 +1,135 @@ +# Migra de Pydantic v1 a Pydantic v2 { #migrate-from-pydantic-v1-to-pydantic-v2 } + +Si tienes una app de FastAPI antigua, podrías estar usando Pydantic versión 1. + +FastAPI versión 0.100.0 tenía compatibilidad con Pydantic v1 o v2. Usaba la que tuvieras instalada. + +FastAPI versión 0.119.0 introdujo compatibilidad parcial con Pydantic v1 desde dentro de Pydantic v2 (como `pydantic.v1`), para facilitar la migración a v2. + +FastAPI 0.126.0 eliminó la compatibilidad con Pydantic v1, aunque siguió soportando `pydantic.v1` por un poquito más de tiempo. + +/// warning | Advertencia + +El equipo de Pydantic dejó de dar soporte a Pydantic v1 para las versiones más recientes de Python, comenzando con **Python 3.14**. + +Esto incluye `pydantic.v1`, que ya no está soportado en Python 3.14 y superiores. + +Si quieres usar las funcionalidades más recientes de Python, tendrás que asegurarte de usar Pydantic v2. + +/// + +Si tienes una app de FastAPI antigua con Pydantic v1, aquí te muestro cómo migrarla a Pydantic v2, y las **funcionalidades en FastAPI 0.119.0** para ayudarte con una migración gradual. + +## Guía oficial { #official-guide } + +Pydantic tiene una Guía de migración oficial de v1 a v2. + +También incluye qué cambió, cómo las validaciones ahora son más correctas y estrictas, posibles consideraciones, etc. + +Puedes leerla para entender mejor qué cambió. + +## Tests { #tests } + +Asegúrate de tener [tests](../tutorial/testing.md){.internal-link target=_blank} para tu app y de ejecutarlos en integración continua (CI). + +Así podrás hacer la actualización y asegurarte de que todo sigue funcionando como esperas. + +## `bump-pydantic` { #bump-pydantic } + +En muchos casos, cuando usas modelos de Pydantic normales sin personalizaciones, podrás automatizar gran parte del proceso de migración de Pydantic v1 a Pydantic v2. + +Puedes usar `bump-pydantic` del mismo equipo de Pydantic. + +Esta herramienta te ayudará a cambiar automáticamente la mayor parte del código que necesita cambiarse. + +Después de esto, puedes ejecutar los tests y revisa si todo funciona. Si es así, ya terminaste. 😎 + +## Pydantic v1 en v2 { #pydantic-v1-in-v2 } + +Pydantic v2 incluye todo lo de Pydantic v1 como un submódulo `pydantic.v1`. Pero esto ya no está soportado en versiones por encima de Python 3.13. + +Esto significa que puedes instalar la versión más reciente de Pydantic v2 e importar y usar los componentes viejos de Pydantic v1 desde este submódulo, como si tuvieras instalado el Pydantic v1 antiguo. + +{* ../../docs_src/pydantic_v1_in_v2/tutorial001_an_py310.py hl[1,4] *} + +### Compatibilidad de FastAPI con Pydantic v1 en v2 { #fastapi-support-for-pydantic-v1-in-v2 } + +Desde FastAPI 0.119.0, también hay compatibilidad parcial para Pydantic v1 desde dentro de Pydantic v2, para facilitar la migración a v2. + +Así que podrías actualizar Pydantic a la última versión 2 y cambiar los imports para usar el submódulo `pydantic.v1`, y en muchos casos simplemente funcionaría. + +{* ../../docs_src/pydantic_v1_in_v2/tutorial002_an_py310.py hl[2,5,15] *} + +/// warning | Advertencia + +Ten en cuenta que, como el equipo de Pydantic ya no da soporte a Pydantic v1 en versiones recientes de Python, empezando por Python 3.14, usar `pydantic.v1` tampoco está soportado en Python 3.14 y superiores. + +/// + +### Pydantic v1 y v2 en la misma app { #pydantic-v1-and-v2-on-the-same-app } + +**No está soportado** por Pydantic tener un modelo de Pydantic v2 con sus propios campos definidos como modelos de Pydantic v1 o viceversa. + +```mermaid +graph TB + subgraph "❌ Not Supported" + direction TB + subgraph V2["Pydantic v2 Model"] + V1Field["Pydantic v1 Model"] + end + subgraph V1["Pydantic v1 Model"] + V2Field["Pydantic v2 Model"] + end + end + + style V2 fill:#f9fff3 + style V1 fill:#fff6f0 + style V1Field fill:#fff6f0 + style V2Field fill:#f9fff3 +``` + +...pero puedes tener modelos separados usando Pydantic v1 y v2 en la misma app. + +```mermaid +graph TB + subgraph "✅ Supported" + direction TB + subgraph V2["Pydantic v2 Model"] + V2Field["Pydantic v2 Model"] + end + subgraph V1["Pydantic v1 Model"] + V1Field["Pydantic v1 Model"] + end + end + + style V2 fill:#f9fff3 + style V1 fill:#fff6f0 + style V1Field fill:#fff6f0 + style V2Field fill:#f9fff3 +``` + +En algunos casos, incluso es posible tener modelos de Pydantic v1 y v2 en la misma **path operation** de tu app de FastAPI: + +{* ../../docs_src/pydantic_v1_in_v2/tutorial003_an_py310.py hl[2:3,6,12,21:22] *} + +En el ejemplo anterior, el modelo de entrada es un modelo de Pydantic v1 y el modelo de salida (definido en `response_model=ItemV2`) es un modelo de Pydantic v2. + +### Parámetros de Pydantic v1 { #pydantic-v1-parameters } + +Si necesitas usar algunas de las herramientas específicas de FastAPI para parámetros como `Body`, `Query`, `Form`, etc. con modelos de Pydantic v1, puedes importarlas de `fastapi.temp_pydantic_v1_params` mientras terminas la migración a Pydantic v2: + +{* ../../docs_src/pydantic_v1_in_v2/tutorial004_an_py310.py hl[4,18] *} + +### Migra por pasos { #migrate-in-steps } + +/// tip | Consejo + +Primero prueba con `bump-pydantic`, si tus tests pasan y eso funciona, entonces terminaste con un solo comando. ✨ + +/// + +Si `bump-pydantic` no funciona para tu caso de uso, puedes usar la compatibilidad de modelos Pydantic v1 y v2 en la misma app para hacer la migración a Pydantic v2 de forma gradual. + +Podrías primero actualizar Pydantic para usar la última versión 2, y cambiar los imports para usar `pydantic.v1` para todos tus modelos. + +Luego puedes empezar a migrar tus modelos de Pydantic v1 a v2 por grupos, en pasos graduales. 🚶 diff --git a/docs/es/docs/how-to/separate-openapi-schemas.md b/docs/es/docs/how-to/separate-openapi-schemas.md index b779158511..903313599d 100644 --- a/docs/es/docs/how-to/separate-openapi-schemas.md +++ b/docs/es/docs/how-to/separate-openapi-schemas.md @@ -1,18 +1,18 @@ -# Separación de Esquemas OpenAPI para Entrada y Salida o No +# Separación de Esquemas OpenAPI para Entrada y Salida o No { #separate-openapi-schemas-for-input-and-output-or-not } -Al usar **Pydantic v2**, el OpenAPI generado es un poco más exacto y **correcto** que antes. 😎 +Desde que se lanzó **Pydantic v2**, el OpenAPI generado es un poco más exacto y **correcto** que antes. 😎 De hecho, en algunos casos, incluso tendrá **dos JSON Schemas** en OpenAPI para el mismo modelo Pydantic, para entrada y salida, dependiendo de si tienen **valores por defecto**. Veamos cómo funciona eso y cómo cambiarlo si necesitas hacerlo. -## Modelos Pydantic para Entrada y Salida +## Modelos Pydantic para Entrada y Salida { #pydantic-models-for-input-and-output } Digamos que tienes un modelo Pydantic con valores por defecto, como este: {* ../../docs_src/separate_openapi_schemas/tutorial001_py310.py ln[1:7] hl[7] *} -### Modelo para Entrada +### Modelo para Entrada { #model-for-input } Si usas este modelo como entrada, como aquí: @@ -20,7 +20,7 @@ Si usas este modelo como entrada, como aquí: ...entonces el campo `description` **no será requerido**. Porque tiene un valor por defecto de `None`. -### Modelo de Entrada en la Documentación +### Modelo de Entrada en la Documentación { #input-model-in-docs } Puedes confirmar eso en la documentación, el campo `description` no tiene un **asterisco rojo**, no está marcado como requerido: @@ -28,7 +28,7 @@ Puedes confirmar eso en la documentación, el campo `description` no tiene un ** -### Modelo para Salida +### Modelo para Salida { #model-for-output } Pero si usas el mismo modelo como salida, como aquí: @@ -36,7 +36,7 @@ Pero si usas el mismo modelo como salida, como aquí: ...entonces, porque `description` tiene un valor por defecto, si **no devuelves nada** para ese campo, aún tendrá ese **valor por defecto**. -### Modelo para Datos de Response de Salida +### Modelo para Datos de Response de Salida { #model-for-output-response-data } Si interactúas con la documentación y revisas el response, aunque el código no agregó nada en uno de los campos `description`, el response JSON contiene el valor por defecto (`null`): @@ -46,7 +46,7 @@ Si interactúas con la documentación y revisas el response, aunque el código n Esto significa que **siempre tendrá un valor**, solo que a veces el valor podría ser `None` (o `null` en JSON). -Eso significa que, los clientes que usan tu API no tienen que comprobar si el valor existe o no, pueden **asumir que el campo siempre estará allí**, pero solo que en algunos casos tendrá el valor por defecto de `None`. +Eso significa que, los clientes que usan tu API no tienen que revisar si el valor existe o no, pueden **asumir que el campo siempre estará allí**, pero solo que en algunos casos tendrá el valor por defecto de `None`. La forma de describir esto en OpenAPI es marcar ese campo como **requerido**, porque siempre estará allí. @@ -55,7 +55,7 @@ Debido a eso, el JSON Schema para un modelo puede ser diferente dependiendo de s * para **entrada** el `description` **no será requerido** * para **salida** será **requerido** (y posiblemente `None`, o en términos de JSON, `null`) -### Modelo para Salida en la Documentación +### Modelo para Salida en la Documentación { #model-for-output-in-docs } También puedes revisar el modelo de salida en la documentación, **ambos** `name` y `description` están marcados como **requeridos** con un **asterisco rojo**: @@ -63,7 +63,7 @@ También puedes revisar el modelo de salida en la documentación, **ambos** `nam -### Modelo para Entrada y Salida en la Documentación +### Modelo para Entrada y Salida en la Documentación { #model-for-input-and-output-in-docs } Y si revisas todos los esquemas disponibles (JSON Schemas) en OpenAPI, verás que hay dos, uno `Item-Input` y uno `Item-Output`. @@ -77,7 +77,7 @@ Pero para `Item-Output`, `description` **es requerido**, tiene un asterisco rojo Con esta funcionalidad de **Pydantic v2**, la documentación de tu API es más **precisa**, y si tienes clientes y SDKs autogenerados, también serán más precisos, con una mejor **experiencia para desarrolladores** y consistencia. 🎉 -## No Separar Esquemas +## No Separar Esquemas { #do-not-separate-schemas } Ahora, hay algunos casos donde podrías querer tener el **mismo esquema para entrada y salida**. @@ -85,7 +85,7 @@ Probablemente el caso principal para esto es si ya tienes algún código cliente En ese caso, puedes desactivar esta funcionalidad en **FastAPI**, con el parámetro `separate_input_output_schemas=False`. -/// info | Información +/// info El soporte para `separate_input_output_schemas` fue agregado en FastAPI `0.102.0`. 🤓 @@ -93,12 +93,10 @@ El soporte para `separate_input_output_schemas` fue agregado en FastAPI `0.102.0 {* ../../docs_src/separate_openapi_schemas/tutorial002_py310.py hl[10] *} -### Mismo Esquema para Modelos de Entrada y Salida en la Documentación +### Mismo Esquema para Modelos de Entrada y Salida en la Documentación { #same-schema-for-input-and-output-models-in-docs } Y ahora habrá un único esquema para entrada y salida para el modelo, solo `Item`, y tendrá `description` como **no requerido**:
- +
- -Este es el mismo comportamiento que en Pydantic v1. 🤓 diff --git a/docs/es/docs/how-to/testing-database.md b/docs/es/docs/how-to/testing-database.md index b76f4c33a3..2fa2603123 100644 --- a/docs/es/docs/how-to/testing-database.md +++ b/docs/es/docs/how-to/testing-database.md @@ -1,4 +1,4 @@ -# Probando una Base de Datos +# Probando una Base de Datos { #testing-a-database } Puedes estudiar sobre bases de datos, SQL y SQLModel en la documentación de SQLModel. 🤓 diff --git a/docs/es/docs/index.md b/docs/es/docs/index.md index a965fa4b5f..14fa07e413 100644 --- a/docs/es/docs/index.md +++ b/docs/es/docs/index.md @@ -1,11 +1,11 @@ -# FastAPI +# FastAPI { #fastapi }

- FastAPI + FastAPI

FastAPI framework, alto rendimiento, fácil de aprender, rápido de programar, listo para producción @@ -27,7 +27,7 @@ --- -**Documentación**: https://fastapi.tiangolo.com +**Documentación**: https://fastapi.tiangolo.com **Código Fuente**: https://github.com/fastapi/fastapi @@ -35,12 +35,12 @@ FastAPI es un framework web moderno, rápido (de alto rendimiento), para construir APIs con Python basado en las anotaciones de tipos estándar de Python. -Las características clave son: +Las funcionalidades clave son: * **Rápido**: Muy alto rendimiento, a la par con **NodeJS** y **Go** (gracias a Starlette y Pydantic). [Uno de los frameworks Python más rápidos disponibles](#performance). * **Rápido de programar**: Aumenta la velocidad para desarrollar funcionalidades en aproximadamente un 200% a 300%. * * **Menos bugs**: Reduce en aproximadamente un 40% los errores inducidos por humanos (desarrolladores). * -* **Intuitivo**: Gran soporte para editores. Autocompletado en todas partes. Menos tiempo depurando. +* **Intuitivo**: Gran soporte para editores. Autocompletado en todas partes. Menos tiempo depurando. * **Fácil**: Diseñado para ser fácil de usar y aprender. Menos tiempo leyendo documentación. * **Corto**: Minimiza la duplicación de código. Múltiples funcionalidades desde cada declaración de parámetro. Menos bugs. * **Robusto**: Obtén código listo para producción. Con documentación interactiva automática. @@ -48,24 +48,30 @@ Las características clave son: * estimación basada en pruebas con un equipo de desarrollo interno, construyendo aplicaciones de producción. -## Sponsors +## Sponsors { #sponsors } -{% if sponsors %} +### Sponsor Keystone { #keystone-sponsor } + +{% for sponsor in sponsors.keystone -%} + +{% endfor -%} + +### Sponsors Oro y Plata { #gold-and-silver-sponsors } + {% for sponsor in sponsors.gold -%} {% endfor -%} {%- for sponsor in sponsors.silver -%} {% endfor %} -{% endif %} -Otros sponsors +Otros sponsors -## Opiniones +## Opiniones { #opinions } "_[...] Estoy usando **FastAPI** un montón estos días. [...] De hecho, estoy planeando usarlo para todos los servicios de **ML de mi equipo en Microsoft**. Algunos de ellos se están integrando en el núcleo del producto **Windows** y algunos productos de **Office**._" @@ -87,13 +93,13 @@ Las características clave son: "_Estoy súper emocionado con **FastAPI**. ¡Es tan divertido!_" -

Brian Okken - host del podcast Python Bytes (ref)
+
Brian Okken - Python Bytes host del podcast (ref)
--- "_Honestamente, lo que has construido parece súper sólido y pulido. En muchos aspectos, es lo que quería que **Hug** fuera; es realmente inspirador ver a alguien construir eso._" -
Timothy Crosley - creador de Hug (ref)
+
Timothy Crosley - Hug creador (ref)
--- @@ -111,24 +117,30 @@ Las características clave son: --- -## **Typer**, el FastAPI de las CLIs +## Mini documental de FastAPI { #fastapi-mini-documentary } + +Hay un mini documental de FastAPI lanzado a finales de 2025, puedes verlo online: + +FastAPI Mini Documentary + +## **Typer**, el FastAPI de las CLIs { #typer-the-fastapi-of-clis } -Si estás construyendo una aplicación de CLI para ser usada en el terminal en lugar de una API web, revisa **Typer**. +Si estás construyendo una aplicación de CLI para ser usada en la terminal en lugar de una API web, revisa **Typer**. **Typer** es el hermano pequeño de FastAPI. Y está destinado a ser el **FastAPI de las CLIs**. ⌨️ 🚀 -## Requisitos +## Requisitos { #requirements } FastAPI se apoya en hombros de gigantes: * Starlette para las partes web. * Pydantic para las partes de datos. -## Instalación +## Instalación { #installation } -Crea y activa un entorno virtual y luego instala FastAPI: +Crea y activa un entorno virtual y luego instala FastAPI:
@@ -142,11 +154,11 @@ $ pip install "fastapi[standard]" **Nota**: Asegúrate de poner `"fastapi[standard]"` entre comillas para asegurar que funcione en todas las terminales. -## Ejemplo +## Ejemplo { #example } -### Créalo +### Créalo { #create-it } -* Crea un archivo `main.py` con: +Crea un archivo `main.py` con: ```Python from typing import Union @@ -191,11 +203,11 @@ async def read_item(item_id: int, q: Union[str, None] = None): **Nota**: -Si no lo sabes, revisa la sección _"¿Con prisa?"_ sobre `async` y `await` en la documentación. +Si no lo sabes, revisa la sección _"¿Con prisa?"_ sobre `async` y `await` en la documentación. -### Córrelo +### Córrelo { #run-it } Corre el servidor con: @@ -233,11 +245,11 @@ El comando `fastapi dev` lee tu archivo `main.py`, detecta la app **FastAPI** en Por defecto, `fastapi dev` comenzará con auto-recarga habilitada para el desarrollo local. -Puedes leer más sobre esto en la documentación del CLI de FastAPI. +Puedes leer más sobre esto en la documentación del CLI de FastAPI. -### Revísalo +### Revísalo { #check-it } Abre tu navegador en http://127.0.0.1:8000/items/5?q=somequery. @@ -254,7 +266,7 @@ Ya creaste una API que: * El _path_ `/items/{item_id}` tiene un _parámetro de path_ `item_id` que debe ser un `int`. * El _path_ `/items/{item_id}` tiene un _parámetro de query_ `q` opcional que es un `str`. -### Documentación interactiva de la API +### Documentación interactiva de la API { #interactive-api-docs } Ahora ve a http://127.0.0.1:8000/docs. @@ -262,7 +274,7 @@ Verás la documentación interactiva automática de la API (proporcionada por http://127.0.0.1:8000/redoc. @@ -270,7 +282,7 @@ Verás la documentación alternativa automática (proporcionada por http://127.0.0.1:8000/docs. @@ -324,7 +336,7 @@ Ahora ve a http://127.0.0.1:8000/redoc. @@ -332,7 +344,7 @@ Y ahora, ve a Tutorial - Guía del Usuario. +Para un ejemplo más completo incluyendo más funcionalidades, ve al Tutorial - Guía del Usuario. **Alerta de spoilers**: el tutorial - guía del usuario incluye: @@ -444,17 +456,69 @@ Para un ejemplo más completo incluyendo más funcionalidades, ve al FastAPI Cloud, ve y únete a la lista de espera si no lo has hecho. 🚀 + +Si ya tienes una cuenta de **FastAPI Cloud** (te invitamos desde la lista de espera 😉), puedes desplegar tu aplicación con un solo comando. + +Antes de desplegar, asegúrate de haber iniciado sesión: + +
+ +```console +$ fastapi login + +You are logged in to FastAPI Cloud 🚀 +``` + +
+ +Luego despliega tu app: + +
+ +```console +$ fastapi deploy + +Deploying to FastAPI Cloud... + +✅ Deployment successful! + +🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev +``` + +
+ +¡Eso es todo! Ahora puedes acceder a tu app en esa URL. ✨ + +#### Acerca de FastAPI Cloud { #about-fastapi-cloud } + +**FastAPI Cloud** está construido por el mismo autor y equipo detrás de **FastAPI**. + +Optimiza el proceso de **construir**, **desplegar** y **acceder** a una API con un esfuerzo mínimo. + +Trae la misma **experiencia de desarrollador** de construir apps con FastAPI a **desplegarlas** en la nube. 🎉 + +FastAPI Cloud es el sponsor principal y proveedor de financiamiento para los proyectos open source *FastAPI and friends*. ✨ + +#### Despliega en otros proveedores de cloud { #deploy-to-other-cloud-providers } + +FastAPI es open source y está basado en estándares. Puedes desplegar apps de FastAPI en cualquier proveedor de cloud que elijas. + +Sigue las guías de tu proveedor de cloud para desplegar apps de FastAPI con ellos. 🤓 + +## Rendimiento { #performance } Benchmarks independientes de TechEmpower muestran aplicaciones **FastAPI** ejecutándose bajo Uvicorn como uno de los frameworks Python más rápidos disponibles, solo por debajo de Starlette y Uvicorn (usados internamente por FastAPI). (*) -Para entender más sobre esto, ve la sección Benchmarks. +Para entender más sobre esto, ve la sección Benchmarks. -## Dependencias +## Dependencias { #dependencies } FastAPI depende de Pydantic y Starlette. -### Dependencias `standard` +### Dependencias `standard` { #standard-dependencies } Cuando instalas FastAPI con `pip install "fastapi[standard]"` viene con el grupo `standard` de dependencias opcionales: @@ -465,19 +529,24 @@ Usadas por Pydantic: Usadas por Starlette: * httpx - Requerido si deseas usar el `TestClient`. -* jinja2 - Requerido si deseas usar la configuración de plantilla predeterminada. +* jinja2 - Requerido si deseas usar la configuración de plantilla por defecto. * python-multipart - Requerido si deseas soportar "parsing" de forms, con `request.form()`. -Usadas por FastAPI / Starlette: +Usadas por FastAPI: * uvicorn - para el servidor que carga y sirve tu aplicación. Esto incluye `uvicorn[standard]`, que incluye algunas dependencias (por ejemplo, `uvloop`) necesarias para servir con alto rendimiento. -* `fastapi-cli` - para proporcionar el comando `fastapi`. +* `fastapi-cli[standard]` - para proporcionar el comando `fastapi`. + * Esto incluye `fastapi-cloud-cli`, que te permite desplegar tu aplicación de FastAPI en FastAPI Cloud. -### Sin Dependencias `standard` +### Sin Dependencias `standard` { #without-standard-dependencies } Si no deseas incluir las dependencias opcionales `standard`, puedes instalar con `pip install fastapi` en lugar de `pip install "fastapi[standard]"`. -### Dependencias Opcionales Adicionales +### Sin `fastapi-cloud-cli` { #without-fastapi-cloud-cli } + +Si quieres instalar FastAPI con las dependencias standard pero sin `fastapi-cloud-cli`, puedes instalar con `pip install "fastapi[standard-no-fastapi-cloud-cli]"`. + +### Dependencias Opcionales Adicionales { #additional-optional-dependencies } Existen algunas dependencias adicionales que podrías querer instalar. @@ -491,6 +560,6 @@ Dependencias opcionales adicionales de FastAPI: * orjson - Requerido si deseas usar `ORJSONResponse`. * ujson - Requerido si deseas usar `UJSONResponse`. -## Licencia +## Licencia { #license } Este proyecto tiene licencia bajo los términos de la licencia MIT. diff --git a/docs/es/docs/learn/index.md b/docs/es/docs/learn/index.md index cc6c7cc3f0..4333bfcf6e 100644 --- a/docs/es/docs/learn/index.md +++ b/docs/es/docs/learn/index.md @@ -1,4 +1,4 @@ -# Aprende +# Aprende { #learn } Aquí están las secciones introductorias y los tutoriales para aprender **FastAPI**. diff --git a/docs/es/docs/project-generation.md b/docs/es/docs/project-generation.md index 5599951511..b4aa11d0dd 100644 --- a/docs/es/docs/project-generation.md +++ b/docs/es/docs/project-generation.md @@ -1,4 +1,4 @@ -# Plantilla Full Stack FastAPI +# Plantilla Full Stack FastAPI { #full-stack-fastapi-template } Las plantillas, aunque normalmente vienen con una configuración específica, están diseñadas para ser flexibles y personalizables. Esto te permite modificarlas y adaptarlas a los requisitos de tu proyecto, haciéndolas un excelente punto de partida. 🏁 @@ -6,23 +6,23 @@ Puedes usar esta plantilla para comenzar, ya que incluye gran parte de la config Repositorio de GitHub: Plantilla Full Stack FastAPI -## Plantilla Full Stack FastAPI - Tecnología y Funcionalidades +## Plantilla Full Stack FastAPI - Tecnología y Funcionalidades { #full-stack-fastapi-template-technology-stack-and-features } -- ⚡ [**FastAPI**](https://fastapi.tiangolo.com) para la API del backend en Python. +- ⚡ [**FastAPI**](https://fastapi.tiangolo.com/es) para la API del backend en Python. - 🧰 [SQLModel](https://sqlmodel.tiangolo.com) para las interacciones con bases de datos SQL en Python (ORM). - 🔍 [Pydantic](https://docs.pydantic.dev), utilizado por FastAPI, para la validación de datos y gestión de configuraciones. - 💾 [PostgreSQL](https://www.postgresql.org) como base de datos SQL. - 🚀 [React](https://react.dev) para el frontend. - - 💃 Usando TypeScript, hooks, [Vite](https://vitejs.dev), y otras partes de una stack moderna de frontend. - - 🎨 [Chakra UI](https://chakra-ui.com) para los componentes del frontend. + - 💃 Usando TypeScript, hooks, Vite, y otras partes de una stack moderna de frontend. + - 🎨 [Tailwind CSS](https://tailwindcss.com) y [shadcn/ui](https://ui.shadcn.com) para los componentes del frontend. - 🤖 Un cliente de frontend generado automáticamente. - - 🧪 [Playwright](https://playwright.dev) para pruebas End-to-End. + - 🧪 [Playwright](https://playwright.dev) para escribir pruebas End-to-End. - 🦇 Soporte para modo oscuro. - 🐋 [Docker Compose](https://www.docker.com) para desarrollo y producción. - 🔒 Hashing seguro de contraseñas por defecto. - 🔑 Autenticación con tokens JWT. - 📫 Recuperación de contraseñas basada en email. - ✅ Pruebas con [Pytest](https://pytest.org). -- 📞 [Traefik](https://traefik.io) como proxy inverso / balanceador de carga. +- 📞 [Traefik](https://traefik.io) como proxy inverso / load balancer. - 🚢 Instrucciones de despliegue usando Docker Compose, incluyendo cómo configurar un proxy Traefik frontend para manejar certificados HTTPS automáticos. - 🏭 CI (integración continua) y CD (despliegue continuo) basados en GitHub Actions. diff --git a/docs/es/docs/python-types.md b/docs/es/docs/python-types.md index 769204f8fd..60b50a08ff 100644 --- a/docs/es/docs/python-types.md +++ b/docs/es/docs/python-types.md @@ -1,4 +1,4 @@ -# Introducción a Tipos en Python +# Introducción a Tipos en Python { #python-types-intro } Python tiene soporte para "anotaciones de tipos" opcionales (también llamadas "type hints"). @@ -18,11 +18,11 @@ Si eres un experto en Python, y ya sabes todo sobre las anotaciones de tipos, sa /// -## Motivación +## Motivación { #motivation } Comencemos con un ejemplo simple: -{* ../../docs_src/python_types/tutorial001.py *} +{* ../../docs_src/python_types/tutorial001_py39.py *} Llamar a este programa genera: @@ -36,9 +36,9 @@ La función hace lo siguiente: * Convierte la primera letra de cada uno a mayúsculas con `title()`. * Concatena ambos con un espacio en el medio. -{* ../../docs_src/python_types/tutorial001.py hl[2] *} +{* ../../docs_src/python_types/tutorial001_py39.py hl[2] *} -### Edítalo +### Edítalo { #edit-it } Es un programa muy simple. @@ -58,7 +58,7 @@ Pero, tristemente, no obtienes nada útil: -### Añadir tipos +### Añadir tipos { #add-types } Modifiquemos una sola línea de la versión anterior. @@ -78,7 +78,7 @@ Eso es todo. Esas son las "anotaciones de tipos": -{* ../../docs_src/python_types/tutorial002.py hl[1] *} +{* ../../docs_src/python_types/tutorial002_py39.py hl[1] *} Eso no es lo mismo que declarar valores predeterminados como sería con: @@ -102,11 +102,11 @@ Con eso, puedes desplazarte, viendo las opciones, hasta que encuentres la que "t -## Más motivación +## Más motivación { #more-motivation } Revisa esta función, ya tiene anotaciones de tipos: -{* ../../docs_src/python_types/tutorial003.py hl[1] *} +{* ../../docs_src/python_types/tutorial003_py39.py hl[1] *} Porque el editor conoce los tipos de las variables, no solo obtienes autocompletado, también obtienes chequeo de errores: @@ -114,15 +114,15 @@ Porque el editor conoce los tipos de las variables, no solo obtienes autocomplet Ahora sabes que debes corregirlo, convertir `age` a un string con `str(age)`: -{* ../../docs_src/python_types/tutorial004.py hl[2] *} +{* ../../docs_src/python_types/tutorial004_py39.py hl[2] *} -## Declaración de tipos +## Declaración de tipos { #declaring-types } Acabas de ver el lugar principal para declarar anotaciones de tipos. Como parámetros de función. Este también es el lugar principal donde los utilizarías con **FastAPI**. -### Tipos simples +### Tipos simples { #simple-types } Puedes declarar todos los tipos estándar de Python, no solo `str`. @@ -133,9 +133,9 @@ Puedes usar, por ejemplo: * `bool` * `bytes` -{* ../../docs_src/python_types/tutorial005.py hl[1] *} +{* ../../docs_src/python_types/tutorial005_py39.py hl[1] *} -### Tipos genéricos con parámetros de tipo +### Tipos genéricos con parámetros de tipo { #generic-types-with-type-parameters } Hay algunas estructuras de datos que pueden contener otros valores, como `dict`, `list`, `set` y `tuple`. Y los valores internos también pueden tener su propio tipo. @@ -143,7 +143,7 @@ Estos tipos que tienen tipos internos se denominan tipos "**genéricos**". Y es Para declarar esos tipos y los tipos internos, puedes usar el módulo estándar de Python `typing`. Existe específicamente para soportar estas anotaciones de tipos. -#### Versiones más recientes de Python +#### Versiones más recientes de Python { #newer-versions-of-python } La sintaxis que utiliza `typing` es **compatible** con todas las versiones, desde Python 3.6 hasta las versiones más recientes, incluyendo Python 3.9, Python 3.10, etc. @@ -157,60 +157,28 @@ Por ejemplo, "**Python 3.6+**" significa que es compatible con Python 3.6 o supe Si puedes usar las **últimas versiones de Python**, utiliza los ejemplos para la última versión, esos tendrán la **mejor y más simple sintaxis**, por ejemplo, "**Python 3.10+**". -#### Lista +#### Lista { #list } Por ejemplo, vamos a definir una variable para ser una `list` de `str`. -//// tab | Python 3.9+ - Declara la variable, con la misma sintaxis de dos puntos (`:`). Como tipo, pon `list`. Como la lista es un tipo que contiene algunos tipos internos, los pones entre corchetes: -```Python hl_lines="1" -{!> ../../docs_src/python_types/tutorial006_py39.py!} -``` - -//// - -//// tab | Python 3.8+ - -De `typing`, importa `List` (con una `L` mayúscula): - -```Python hl_lines="1" -{!> ../../docs_src/python_types/tutorial006.py!} -``` - -Declara la variable, con la misma sintaxis de dos puntos (`:`). - -Como tipo, pon el `List` que importaste de `typing`. - -Como la lista es un tipo que contiene algunos tipos internos, los pones entre corchetes: - -```Python hl_lines="4" -{!> ../../docs_src/python_types/tutorial006.py!} -``` - -//// +{* ../../docs_src/python_types/tutorial006_py39.py hl[1] *} /// info | Información Esos tipos internos en los corchetes se denominan "parámetros de tipo". -En este caso, `str` es el parámetro de tipo pasado a `List` (o `list` en Python 3.9 y superior). +En este caso, `str` es el parámetro de tipo pasado a `list`. /// Eso significa: "la variable `items` es una `list`, y cada uno de los ítems en esta lista es un `str`". -/// tip | Consejo - -Si usas Python 3.9 o superior, no tienes que importar `List` de `typing`, puedes usar el mismo tipo `list` regular en su lugar. - -/// - Al hacer eso, tu editor puede proporcionar soporte incluso mientras procesa elementos de la lista: @@ -221,32 +189,18 @@ Nota que la variable `item` es uno de los elementos en la lista `items`. Y aún así, el editor sabe que es un `str` y proporciona soporte para eso. -#### Tuple y Set +#### Tuple y Set { #tuple-and-set } Harías lo mismo para declarar `tuple`s y `set`s: -//// tab | Python 3.9+ - -```Python hl_lines="1" -{!> ../../docs_src/python_types/tutorial007_py39.py!} -``` - -//// - -//// tab | Python 3.8+ - -```Python hl_lines="1 4" -{!> ../../docs_src/python_types/tutorial007.py!} -``` - -//// +{* ../../docs_src/python_types/tutorial007_py39.py hl[1] *} Esto significa: * La variable `items_t` es un `tuple` con 3 ítems, un `int`, otro `int`, y un `str`. * La variable `items_s` es un `set`, y cada uno de sus ítems es del tipo `bytes`. -#### Dict +#### Dict { #dict } Para definir un `dict`, pasas 2 parámetros de tipo, separados por comas. @@ -254,21 +208,7 @@ El primer parámetro de tipo es para las claves del `dict`. El segundo parámetro de tipo es para los valores del `dict`: -//// tab | Python 3.9+ - -```Python hl_lines="1" -{!> ../../docs_src/python_types/tutorial008_py39.py!} -``` - -//// - -//// tab | Python 3.8+ - -```Python hl_lines="1 4" -{!> ../../docs_src/python_types/tutorial008.py!} -``` - -//// +{* ../../docs_src/python_types/tutorial008_py39.py hl[1] *} Esto significa: @@ -276,7 +216,7 @@ Esto significa: * Las claves de este `dict` son del tipo `str` (digamos, el nombre de cada ítem). * Los valores de este `dict` son del tipo `float` (digamos, el precio de cada ítem). -#### Union +#### Union { #union } Puedes declarar que una variable puede ser cualquier de **varios tipos**, por ejemplo, un `int` o un `str`. @@ -292,24 +232,24 @@ En Python 3.10 también hay una **nueva sintaxis** donde puedes poner los posibl //// -//// tab | Python 3.8+ +//// tab | Python 3.9+ ```Python hl_lines="1 4" -{!> ../../docs_src/python_types/tutorial008b.py!} +{!> ../../docs_src/python_types/tutorial008b_py39.py!} ``` //// En ambos casos, esto significa que `item` podría ser un `int` o un `str`. -#### Posiblemente `None` +#### Posiblemente `None` { #possibly-none } Puedes declarar que un valor podría tener un tipo, como `str`, pero que también podría ser `None`. En Python 3.6 y posteriores (incluyendo Python 3.10) puedes declararlo importando y usando `Optional` del módulo `typing`. ```Python hl_lines="1 4" -{!../../docs_src/python_types/tutorial009.py!} +{!../../docs_src/python_types/tutorial009_py39.py!} ``` Usar `Optional[str]` en lugar de solo `str` te permitirá al editor ayudarte a detectar errores donde podrías estar asumiendo que un valor siempre es un `str`, cuando en realidad también podría ser `None`. @@ -326,23 +266,23 @@ Esto también significa que en Python 3.10, puedes usar `Something | None`: //// -//// tab | Python 3.8+ +//// tab | Python 3.9+ ```Python hl_lines="1 4" -{!> ../../docs_src/python_types/tutorial009.py!} +{!> ../../docs_src/python_types/tutorial009_py39.py!} ``` //// -//// tab | Python 3.8+ alternative +//// tab | Python 3.9+ alternativa ```Python hl_lines="1 4" -{!> ../../docs_src/python_types/tutorial009b.py!} +{!> ../../docs_src/python_types/tutorial009b_py39.py!} ``` //// -#### Uso de `Union` u `Optional` +#### Uso de `Union` u `Optional` { #using-union-or-optional } Si estás usando una versión de Python inferior a 3.10, aquí tienes un consejo desde mi punto de vista muy **subjetivo**: @@ -357,7 +297,7 @@ Se trata solo de las palabras y nombres. Pero esas palabras pueden afectar cómo Como ejemplo, tomemos esta función: -{* ../../docs_src/python_types/tutorial009c.py hl[1,4] *} +{* ../../docs_src/python_types/tutorial009c_py39.py hl[1,4] *} El parámetro `name` está definido como `Optional[str]`, pero **no es opcional**, no puedes llamar a la función sin el parámetro: @@ -377,7 +317,7 @@ La buena noticia es que, una vez que estés en Python 3.10, no tendrás que preo Y entonces no tendrás que preocuparte por nombres como `Optional` y `Union`. 😎 -#### Tipos genéricos +#### Tipos genéricos { #generic-types } Estos tipos que toman parámetros de tipo en corchetes se llaman **Tipos Genéricos** o **Genéricos**, por ejemplo: @@ -390,10 +330,10 @@ Puedes usar los mismos tipos integrados como genéricos (con corchetes y tipos d * `set` * `dict` -Y lo mismo que con Python 3.8, desde el módulo `typing`: +Y, como con versiones anteriores de Python, desde el módulo `typing`: * `Union` -* `Optional` (lo mismo que con Python 3.8) +* `Optional` * ...y otros. En Python 3.10, como alternativa a usar los genéricos `Union` y `Optional`, puedes usar la barra vertical (`|`) para declarar uniones de tipos, eso es mucho mejor y más simple. @@ -409,7 +349,7 @@ Puedes usar los mismos tipos integrados como genéricos (con corchetes y tipos d * `set` * `dict` -Y lo mismo que con Python 3.8, desde el módulo `typing`: +Y generics desde el módulo `typing`: * `Union` * `Optional` @@ -417,29 +357,17 @@ Y lo mismo que con Python 3.8, desde el módulo `typing`: //// -//// tab | Python 3.8+ - -* `List` -* `Tuple` -* `Set` -* `Dict` -* `Union` -* `Optional` -* ...y otros. - -//// - -### Clases como tipos +### Clases como tipos { #classes-as-types } También puedes declarar una clase como el tipo de una variable. Digamos que tienes una clase `Person`, con un nombre: -{* ../../docs_src/python_types/tutorial010.py hl[1:3] *} +{* ../../docs_src/python_types/tutorial010_py39.py hl[1:3] *} Luego puedes declarar una variable para que sea de tipo `Person`: -{* ../../docs_src/python_types/tutorial010.py hl[6] *} +{* ../../docs_src/python_types/tutorial010_py39.py hl[6] *} Y luego, nuevamente, obtienes todo el soporte del editor: @@ -449,7 +377,7 @@ Nota que esto significa "`one_person` es una **instance** de la clase `Person`". No significa "`one_person` es la **clase** llamada `Person`". -## Modelos Pydantic +## Modelos Pydantic { #pydantic-models } Pydantic es un paquete de Python para realizar la validación de datos. @@ -463,29 +391,7 @@ Y obtienes todo el soporte del editor con ese objeto resultante. Un ejemplo de la documentación oficial de Pydantic: -//// tab | Python 3.10+ - -```Python -{!> ../../docs_src/python_types/tutorial011_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!> ../../docs_src/python_types/tutorial011_py39.py!} -``` - -//// - -//// tab | Python 3.8+ - -```Python -{!> ../../docs_src/python_types/tutorial011.py!} -``` - -//// +{* ../../docs_src/python_types/tutorial011_py310.py *} /// info | Información @@ -503,31 +409,13 @@ Pydantic tiene un comportamiento especial cuando utilizas `Optional` o `Union[So /// -## Anotaciones de tipos con metadata +## Anotaciones de tipos con metadata { #type-hints-with-metadata-annotations } -Python también tiene una funcionalidad que permite poner **metadata adicional** en estas anotaciones de tipos usando `Annotated`. +Python también tiene una funcionalidad que permite poner **metadatos adicional** en estas anotaciones de tipos usando `Annotated`. -//// tab | Python 3.9+ +Desde Python 3.9, `Annotated` es parte de la standard library, así que puedes importarlo desde `typing`. -En Python 3.9, `Annotated` es parte de la librería estándar, así que puedes importarlo desde `typing`. - -```Python hl_lines="1 4" -{!> ../../docs_src/python_types/tutorial013_py39.py!} -``` - -//// - -//// tab | Python 3.8+ - -En versiones por debajo de Python 3.9, importas `Annotated` de `typing_extensions`. - -Ya estará instalado con **FastAPI**. - -```Python hl_lines="1 4" -{!> ../../docs_src/python_types/tutorial013.py!} -``` - -//// +{* ../../docs_src/python_types/tutorial013_py39.py hl[1,4] *} Python en sí no hace nada con este `Annotated`. Y para los editores y otras herramientas, el tipo sigue siendo `str`. @@ -547,7 +435,7 @@ Y también que tu código será muy compatible con muchas otras herramientas y p /// -## Anotaciones de tipos en **FastAPI** +## Anotaciones de tipos en **FastAPI** { #type-hints-in-fastapi } **FastAPI** aprovecha estas anotaciones de tipos para hacer varias cosas. diff --git a/docs/es/docs/resources/index.md b/docs/es/docs/resources/index.md index 92898d319e..3240095611 100644 --- a/docs/es/docs/resources/index.md +++ b/docs/es/docs/resources/index.md @@ -1,3 +1,3 @@ -# Recursos +# Recursos { #resources } -Recursos adicionales, enlaces externos, artículos y más. ✈️ +Recursos adicionales, enlaces externos y más. ✈️ diff --git a/docs/es/docs/tutorial/background-tasks.md b/docs/es/docs/tutorial/background-tasks.md index 783db20a4b..cc8a2c9cbe 100644 --- a/docs/es/docs/tutorial/background-tasks.md +++ b/docs/es/docs/tutorial/background-tasks.md @@ -1,4 +1,4 @@ -# Tareas en Segundo Plano +# Tareas en Segundo Plano { #background-tasks } Puedes definir tareas en segundo plano para que se ejecuten *después* de devolver un response. @@ -11,15 +11,15 @@ Esto incluye, por ejemplo: * Procesamiento de datos: * Por ejemplo, supongamos que recibes un archivo que debe pasar por un proceso lento, puedes devolver un response de "Accepted" (HTTP 202) y procesar el archivo en segundo plano. -## Usando `BackgroundTasks` +## Usando `BackgroundTasks` { #using-backgroundtasks } Primero, importa `BackgroundTasks` y define un parámetro en tu *path operation function* con una declaración de tipo de `BackgroundTasks`: -{* ../../docs_src/background_tasks/tutorial001.py hl[1,13] *} +{* ../../docs_src/background_tasks/tutorial001_py39.py hl[1,13] *} **FastAPI** creará el objeto de tipo `BackgroundTasks` por ti y lo pasará como ese parámetro. -## Crear una función de tarea +## Crear una función de tarea { #create-a-task-function } Crea una función para que se ejecute como la tarea en segundo plano. @@ -31,13 +31,13 @@ En este caso, la función de tarea escribirá en un archivo (simulando el envío Y como la operación de escritura no usa `async` y `await`, definimos la función con un `def` normal: -{* ../../docs_src/background_tasks/tutorial001.py hl[6:9] *} +{* ../../docs_src/background_tasks/tutorial001_py39.py hl[6:9] *} -## Agregar la tarea en segundo plano +## Agregar la tarea en segundo plano { #add-the-background-task } Dentro de tu *path operation function*, pasa tu función de tarea al objeto de *background tasks* con el método `.add_task()`: -{* ../../docs_src/background_tasks/tutorial001.py hl[14] *} +{* ../../docs_src/background_tasks/tutorial001_py39.py hl[14] *} `.add_task()` recibe como argumentos: @@ -45,7 +45,7 @@ Dentro de tu *path operation function*, pasa tu función de tarea al objeto de * * Cualquier secuencia de argumentos que deba pasarse a la función de tarea en orden (`email`). * Cualquier argumento de palabras clave que deba pasarse a la función de tarea (`message="some notification"`). -## Inyección de Dependencias +## Inyección de Dependencias { #dependency-injection } Usar `BackgroundTasks` también funciona con el sistema de inyección de dependencias, puedes declarar un parámetro de tipo `BackgroundTasks` en varios niveles: en una *path operation function*, en una dependencia (dependable), en una sub-dependencia, etc. @@ -59,7 +59,7 @@ Si hay un query en el request, se escribirá en el log en una tarea en segundo p Y luego otra tarea en segundo plano generada en la *path operation function* escribirá un mensaje usando el parámetro de path `email`. -## Detalles Técnicos +## Detalles Técnicos { #technical-details } La clase `BackgroundTasks` proviene directamente de `starlette.background`. @@ -71,7 +71,7 @@ Todavía es posible usar `BackgroundTask` solo en FastAPI, pero debes crear el o Puedes ver más detalles en la documentación oficial de Starlette sobre Background Tasks. -## Advertencia +## Advertencia { #caveat } Si necesitas realizar una computación intensa en segundo plano y no necesariamente necesitas que se ejecute por el mismo proceso (por ejemplo, no necesitas compartir memoria, variables, etc.), podrías beneficiarte del uso de otras herramientas más grandes como Celery. @@ -79,6 +79,6 @@ Tienden a requerir configuraciones más complejas, un gestor de cola de mensajes Pero si necesitas acceder a variables y objetos de la misma app de **FastAPI**, o necesitas realizar pequeñas tareas en segundo plano (como enviar una notificación por email), simplemente puedes usar `BackgroundTasks`. -## Resumen +## Resumen { #recap } Importa y usa `BackgroundTasks` con parámetros en *path operation functions* y dependencias para agregar tareas en segundo plano. diff --git a/docs/es/docs/tutorial/bigger-applications.md b/docs/es/docs/tutorial/bigger-applications.md index c3d8f06860..7938a12158 100644 --- a/docs/es/docs/tutorial/bigger-applications.md +++ b/docs/es/docs/tutorial/bigger-applications.md @@ -1,4 +1,4 @@ -# Aplicaciones más grandes - Múltiples archivos +# Aplicaciones más grandes - Múltiples archivos { #bigger-applications-multiple-files } Si estás construyendo una aplicación o una API web, rara vez podrás poner todo en un solo archivo. @@ -10,7 +10,7 @@ Si vienes de Flask, esto sería el equivalente a los Blueprints de Flask. /// -## Un ejemplo de estructura de archivos +## Un ejemplo de estructura de archivos { #an-example-file-structure } Digamos que tienes una estructura de archivos como esta: @@ -56,7 +56,7 @@ from app.routers import items La misma estructura de archivos con comentarios: -``` +```bash . ├── app # "app" es un paquete de Python │   ├── __init__.py # este archivo hace que "app" sea un "paquete de Python" @@ -71,7 +71,7 @@ La misma estructura de archivos con comentarios: │   └── admin.py # submódulo "admin", por ejemplo import app.internal.admin ``` -## `APIRouter` +## `APIRouter` { #apirouter } Digamos que el archivo dedicado solo a manejar usuarios es el submódulo en `/app/routers/users.py`. @@ -81,23 +81,19 @@ Pero todavía es parte de la misma aplicación/web API de **FastAPI** (es parte Puedes crear las *path operations* para ese módulo usando `APIRouter`. -### Importar `APIRouter` +### Importar `APIRouter` { #import-apirouter } Lo importas y creas una "instance" de la misma manera que lo harías con la clase `FastAPI`: -```Python hl_lines="1 3" title="app/routers/users.py" -{!../../docs_src/bigger_applications/app/routers/users.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/routers/users.py hl[1,3] title["app/routers/users.py"] *} -### *Path operations* con `APIRouter` +### *Path operations* con `APIRouter` { #path-operations-with-apirouter } Y luego lo usas para declarar tus *path operations*. Úsalo de la misma manera que usarías la clase `FastAPI`: -```Python hl_lines="6 11 16" title="app/routers/users.py" -{!../../docs_src/bigger_applications/app/routers/users.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/routers/users.py hl[6,11,16] title["app/routers/users.py"] *} Puedes pensar en `APIRouter` como una clase "mini `FastAPI`". @@ -113,53 +109,25 @@ En este ejemplo, la variable se llama `router`, pero puedes nombrarla como quier Vamos a incluir este `APIRouter` en la aplicación principal de `FastAPI`, pero primero, revisemos las dependencias y otro `APIRouter`. -## Dependencias +## Dependencias { #dependencies } Vemos que vamos a necesitar algunas dependencias usadas en varios lugares de la aplicación. Así que las ponemos en su propio módulo `dependencies` (`app/dependencies.py`). -Ahora utilizaremos una dependencia simple para leer un encabezado `X-Token` personalizado: +Ahora utilizaremos una dependencia simple para leer un header `X-Token` personalizado: -//// tab | Python 3.9+ - -```Python hl_lines="3 6-8" title="app/dependencies.py" -{!> ../../docs_src/bigger_applications/app_an_py39/dependencies.py!} -``` - -//// - -//// tab | Python 3.8+ - -```Python hl_lines="1 5-7" title="app/dependencies.py" -{!> ../../docs_src/bigger_applications/app_an/dependencies.py!} -``` - -//// - -//// tab | Python 3.8+ non-Annotated +{* ../../docs_src/bigger_applications/app_an_py39/dependencies.py hl[3,6:8] title["app/dependencies.py"] *} /// tip | Consejo -Preferiblemente usa la versión `Annotated` si es posible. - -/// - -```Python hl_lines="1 4-6" title="app/dependencies.py" -{!> ../../docs_src/bigger_applications/app/dependencies.py!} -``` - -//// - -/// tip | Consejo - -Estamos usando un encabezado inventado para simplificar este ejemplo. +Estamos usando un header inventado para simplificar este ejemplo. Pero en casos reales obtendrás mejores resultados usando las [utilidades de Seguridad](security/index.md){.internal-link target=_blank} integradas. /// -## Otro módulo con `APIRouter` +## Otro módulo con `APIRouter` { #another-module-with-apirouter } Digamos que también tienes los endpoints dedicados a manejar "items" de tu aplicación en el módulo `app/routers/items.py`. @@ -181,9 +149,7 @@ Sabemos que todas las *path operations* en este módulo tienen el mismo: Entonces, en lugar de agregar todo eso a cada *path operation*, podemos agregarlo al `APIRouter`. -```Python hl_lines="5-10 16 21" title="app/routers/items.py" -{!../../docs_src/bigger_applications/app/routers/items.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/routers/items.py hl[5:10,16,21] title["app/routers/items.py"] *} Como el path de cada *path operation* tiene que empezar con `/`, como en: @@ -219,7 +185,7 @@ El resultado final es que los paths de item son ahora: * Todos incluirán las `responses` predefinidas. * Todas estas *path operations* tendrán la lista de `dependencies` evaluadas/ejecutadas antes de ellas. * Si también declaras dependencias en una *path operation* específica, **también se ejecutarán**. - * Las dependencias del router se ejecutan primero, luego las [dependencias en el decorador](dependencies/dependencies-in-path-operation-decorators.md){.internal-link target=_blank}, y luego las dependencias de parámetros normales. + * Las dependencias del router se ejecutan primero, luego las [`dependencies` en el decorador](dependencies/dependencies-in-path-operation-decorators.md){.internal-link target=_blank}, y luego las dependencias de parámetros normales. * También puedes agregar [dependencias de `Security` con `scopes`](../advanced/security/oauth2-scopes.md){.internal-link target=_blank}. /// tip | Consejo @@ -234,7 +200,7 @@ Los parámetros `prefix`, `tags`, `responses`, y `dependencies` son (como en muc /// -### Importar las dependencias +### Importar las dependencias { #import-the-dependencies } Este código vive en el módulo `app.routers.items`, el archivo `app/routers/items.py`. @@ -242,15 +208,13 @@ Y necesitamos obtener la función de dependencia del módulo `app.dependencies`, Así que usamos un import relativo con `..` para las dependencias: -```Python hl_lines="3" title="app/routers/items.py" -{!../../docs_src/bigger_applications/app/routers/items.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/routers/items.py hl[3] title["app/routers/items.py"] *} -#### Cómo funcionan los imports relativos +#### Cómo funcionan los imports relativos { #how-relative-imports-work } /// tip | Consejo -Si sabes perfectamente cómo funcionan los imports, continúa a la siguiente sección. +Si sabes perfectamente cómo funcionan los imports, continúa a la siguiente sección abajo. /// @@ -307,27 +271,25 @@ eso significaría: Eso se referiría a algún paquete arriba de `app/`, con su propio archivo `__init__.py`, etc. Pero no tenemos eso. Así que, eso lanzaría un error en nuestro ejemplo. 🚨 -Pero ahora sabes cómo funciona, para que puedas usar imports relativos en tus propias aplicaciones sin importar cuán complejas sean. 🤓 +Pero ahora sabes cómo funciona, para que puedas usar imports relativos en tus propias apps sin importar cuán complejas sean. 🤓 -### Agregar algunos `tags`, `responses`, y `dependencies` personalizados +### Agregar algunos `tags`, `responses`, y `dependencies` personalizados { #add-some-custom-tags-responses-and-dependencies } No estamos agregando el prefijo `/items` ni los `tags=["items"]` a cada *path operation* porque los hemos añadido al `APIRouter`. Pero aún podemos agregar _más_ `tags` que se aplicarán a una *path operation* específica, y también algunas `responses` extra específicas para esa *path operation*: -```Python hl_lines="30-31" title="app/routers/items.py" -{!../../docs_src/bigger_applications/app/routers/items.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/routers/items.py hl[30:31] title["app/routers/items.py"] *} /// tip | Consejo -Esta última *path operation* tendrá la combinación de tags: `["items", "custom"]`. +Esta última path operation tendrá la combinación de tags: `["items", "custom"]`. Y también tendrá ambas responses en la documentación, una para `404` y otra para `403`. /// -## El `FastAPI` principal +## El `FastAPI` principal { #the-main-fastapi } Ahora, veamos el módulo en `app/main.py`. @@ -335,27 +297,25 @@ Aquí es donde importas y usas la clase `FastAPI`. Este será el archivo principal en tu aplicación que conecta todo. -### Importar `FastAPI` +Y como la mayor parte de tu lógica ahora vivirá en su propio módulo específico, el archivo principal será bastante simple. -Importas y creas una clase `FastAPI` como de costumbre. +### Importar `FastAPI` { #import-fastapi } + +Importas y creas una clase `FastAPI` como normalmente. Y podemos incluso declarar [dependencias globales](dependencies/global-dependencies.md){.internal-link target=_blank} que se combinarán con las dependencias para cada `APIRouter`: -```Python hl_lines="1 3 7" title="app/main.py" -{!../../docs_src/bigger_applications/app/main.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[1,3,7] title["app/main.py"] *} -### Importar el `APIRouter` +### Importar el `APIRouter` { #import-the-apirouter } Ahora importamos los otros submódulos que tienen `APIRouter`s: -```Python hl_lines="4-5" title="app/main.py" -{!../../docs_src/bigger_applications/app/main.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[4:5] title["app/main.py"] *} Como los archivos `app/routers/users.py` y `app/routers/items.py` son submódulos que son parte del mismo paquete de Python `app`, podemos usar un solo punto `.` para importarlos usando "imports relativos". -### Cómo funciona la importación +### Cómo funciona la importación { #how-the-importing-works } La sección: @@ -397,7 +357,7 @@ Para aprender más sobre Paquetes y Módulos de Python, lee @@ -531,11 +481,11 @@ $ fastapi dev app/main.py Y abre la documentación en http://127.0.0.1:8000/docs. -Verás la documentación automática de la API, incluyendo los paths de todos los submódulos, usando los paths correctos (y prefijos) y las tags correctas: +Verás la documentación automática de la API, incluyendo los paths de todos los submódulos, usando los paths correctos (y prefijos) y los tags correctos: -## Incluir el mismo router múltiples veces con diferentes `prefix` +## Incluir el mismo router múltiples veces con diferentes `prefix` { #include-the-same-router-multiple-times-with-different-prefix } También puedes usar `.include_router()` múltiples veces con el *mismo* router usando diferentes prefijos. @@ -543,7 +493,7 @@ Esto podría ser útil, por ejemplo, para exponer la misma API bajo diferentes p Este es un uso avanzado que quizás no necesites realmente, pero está allí en caso de que lo necesites. -## Incluir un `APIRouter` en otro +## Incluir un `APIRouter` en otro { #include-an-apirouter-in-another } De la misma manera que puedes incluir un `APIRouter` en una aplicación `FastAPI`, puedes incluir un `APIRouter` en otro `APIRouter` usando: @@ -551,4 +501,4 @@ De la misma manera que puedes incluir un `APIRouter` en una aplicación `FastAPI router.include_router(other_router) ``` -Asegúrate de hacerlo antes de incluir `router` en la aplicación de `FastAPI`, para que las *path operations* de `other_router` también se incluyan. +Asegúrate de hacerlo antes de incluir `router` en la app de `FastAPI`, para que las *path operations* de `other_router` también se incluyan. diff --git a/docs/es/docs/tutorial/body-fields.md b/docs/es/docs/tutorial/body-fields.md index d07d214ec8..8902ce4e94 100644 --- a/docs/es/docs/tutorial/body-fields.md +++ b/docs/es/docs/tutorial/body-fields.md @@ -1,8 +1,8 @@ -# Body - Campos +# Body - Campos { #body-fields } De la misma manera que puedes declarar validaciones adicionales y metadatos en los parámetros de las *path operation function* con `Query`, `Path` y `Body`, puedes declarar validaciones y metadatos dentro de los modelos de Pydantic usando `Field` de Pydantic. -## Importar `Field` +## Importar `Field` { #import-field } Primero, tienes que importarlo: @@ -14,7 +14,7 @@ Fíjate que `Field` se importa directamente desde `pydantic`, no desde `fastapi` /// -## Declarar atributos del modelo +## Declarar atributos del modelo { #declare-model-attributes } Después puedes utilizar `Field` con los atributos del modelo: @@ -40,7 +40,7 @@ Observa cómo cada atributo del modelo con un tipo, un valor por defecto y `Fiel /// -## Agregar información extra +## Agregar información extra { #add-extra-information } Puedes declarar información extra en `Field`, `Query`, `Body`, etc. Y será incluida en el JSON Schema generado. @@ -53,7 +53,7 @@ Como estas claves no necesariamente tienen que ser parte de la especificación d /// -## Resumen +## Resumen { #recap } Puedes utilizar `Field` de Pydantic para declarar validaciones adicionales y metadatos para los atributos del modelo. diff --git a/docs/es/docs/tutorial/body-multiple-params.md b/docs/es/docs/tutorial/body-multiple-params.md index df6560b620..57cec16743 100644 --- a/docs/es/docs/tutorial/body-multiple-params.md +++ b/docs/es/docs/tutorial/body-multiple-params.md @@ -1,24 +1,22 @@ -# Cuerpo - Múltiples Parámetros +# Cuerpo - Múltiples Parámetros { #body-multiple-parameters } Ahora que hemos visto cómo usar `Path` y `Query`, veamos usos más avanzados de las declaraciones del request body. -## Mezclar `Path`, `Query` y parámetros del cuerpo +## Mezclar `Path`, `Query` y parámetros del cuerpo { #mix-path-query-and-body-parameters } Primero, por supuesto, puedes mezclar las declaraciones de parámetros de `Path`, `Query` y del request body libremente y **FastAPI** sabrá qué hacer. -Y también puedes declarar parámetros del cuerpo como opcionales, estableciendo el valor predeterminado a `None`: +Y también puedes declarar parámetros del cuerpo como opcionales, estableciendo el valor por defecto a `None`: {* ../../docs_src/body_multiple_params/tutorial001_an_py310.py hl[18:20] *} -## Múltiples parámetros del cuerpo - /// note | Nota Ten en cuenta que, en este caso, el `item` que se tomaría del cuerpo es opcional. Ya que tiene un valor por defecto de `None`. /// -## Múltiples parámetros del cuerpo +## Múltiples parámetros del cuerpo { #multiple-body-parameters } En el ejemplo anterior, las *path operations* esperarían un cuerpo JSON con los atributos de un `Item`, como: @@ -64,7 +62,7 @@ Ten en cuenta que aunque el `item` se declaró de la misma manera que antes, aho Realizará la validación de los datos compuestos, y los documentará así para el esquema de OpenAPI y la documentación automática. -## Valores singulares en el cuerpo +## Valores singulares en el cuerpo { #singular-values-in-body } De la misma manera que hay un `Query` y `Path` para definir datos extra para parámetros de query y path, **FastAPI** proporciona un equivalente `Body`. @@ -96,7 +94,7 @@ En este caso, **FastAPI** esperará un cuerpo como: Nuevamente, convertirá los tipos de datos, validará, documentará, etc. -## Múltiples parámetros de cuerpo y query +## Múltiples parámetros de cuerpo y query { #multiple-body-params-and-query } Por supuesto, también puedes declarar parámetros adicionales de query siempre que lo necesites, además de cualquier parámetro del cuerpo. @@ -122,7 +120,7 @@ Por ejemplo: /// -## Embeber un solo parámetro de cuerpo +## Embeber un solo parámetro de cuerpo { #embed-a-single-body-parameter } Supongamos que solo tienes un único parámetro de cuerpo `item` de un modelo Pydantic `Item`. @@ -162,7 +160,7 @@ en lugar de: } ``` -## Resumen +## Resumen { #recap } Puedes añadir múltiples parámetros de cuerpo a tu *path operation function*, aunque un request solo puede tener un único cuerpo. diff --git a/docs/es/docs/tutorial/body-nested-models.md b/docs/es/docs/tutorial/body-nested-models.md index 5b4cfc14c2..0dfd6576f1 100644 --- a/docs/es/docs/tutorial/body-nested-models.md +++ b/docs/es/docs/tutorial/body-nested-models.md @@ -1,8 +1,8 @@ -# Cuerpo - Modelos Anidados +# Cuerpo - Modelos Anidados { #body-nested-models } Con **FastAPI**, puedes definir, validar, documentar y usar modelos anidados de manera arbitraria (gracias a Pydantic). -## Campos de lista +## Campos de lista { #list-fields } Puedes definir un atributo como un subtipo. Por ejemplo, una `list` en Python: @@ -10,39 +10,19 @@ Puedes definir un atributo como un subtipo. Por ejemplo, una `list` en Python: Esto hará que `tags` sea una lista, aunque no declare el tipo de los elementos de la lista. -## Campos de lista con parámetro de tipo +## Campos de lista con parámetro de tipo { #list-fields-with-type-parameter } Pero Python tiene una forma específica de declarar listas con tipos internos, o "parámetros de tipo": -### Importar `List` de typing +### Declarar una `list` con un parámetro de tipo { #declare-a-list-with-a-type-parameter } -En Python 3.9 y superior, puedes usar el `list` estándar para declarar estas anotaciones de tipo como veremos a continuación. 💡 - -Pero en versiones de Python anteriores a 3.9 (desde 3.6 en adelante), primero necesitas importar `List` del módulo `typing` estándar de Python: - -{* ../../docs_src/body_nested_models/tutorial002.py hl[1] *} - -### Declarar una `list` con un parámetro de tipo - -Para declarar tipos que tienen parámetros de tipo (tipos internos), como `list`, `dict`, `tuple`: - -* Si estás en una versión de Python inferior a 3.9, importa su versión equivalente del módulo `typing` -* Pasa el/los tipo(s) interno(s) como "parámetros de tipo" usando corchetes: `[` y `]` - -En Python 3.9 sería: +Para declarar tipos que tienen parámetros de tipo (tipos internos), como `list`, `dict`, `tuple`, +pasa el/los tipo(s) interno(s) como "parámetros de tipo" usando corchetes: `[` y `]` ```Python my_list: list[str] ``` -En versiones de Python anteriores a 3.9, sería: - -```Python -from typing import List - -my_list: List[str] -``` - Eso es toda la sintaxis estándar de Python para declaraciones de tipo. Usa esa misma sintaxis estándar para atributos de modelos con tipos internos. @@ -51,7 +31,7 @@ Así, en nuestro ejemplo, podemos hacer que `tags` sea específicamente una "lis {* ../../docs_src/body_nested_models/tutorial002_py310.py hl[12] *} -## Tipos de conjunto +## Tipos de conjunto { #set-types } Pero luego pensamos en ello, y nos damos cuenta de que los tags no deberían repetirse, probablemente serían strings únicos. @@ -67,7 +47,7 @@ Y siempre que emitas esos datos, incluso si la fuente tenía duplicados, se emit Y también se anotará/documentará en consecuencia. -## Modelos Anidados +## Modelos Anidados { #nested-models } Cada atributo de un modelo Pydantic tiene un tipo. @@ -77,13 +57,13 @@ Así que, puedes declarar "objetos" JSON anidados profundamente con nombres de a Todo eso, de manera arbitraria. -### Definir un submodelo +### Definir un submodelo { #define-a-submodel } Por ejemplo, podemos definir un modelo `Image`: {* ../../docs_src/body_nested_models/tutorial004_py310.py hl[7:9] *} -### Usar el submodelo como tipo +### Usar el submodelo como tipo { #use-the-submodel-as-a-type } Y luego podemos usarlo como el tipo de un atributo: @@ -112,7 +92,7 @@ Nuevamente, haciendo solo esa declaración, con **FastAPI** obtienes: * Validación de datos * Documentación automática -## Tipos especiales y validación +## Tipos especiales y validación { #special-types-and-validation } Además de tipos singulares normales como `str`, `int`, `float`, etc., puedes usar tipos singulares más complejos que heredan de `str`. @@ -124,7 +104,7 @@ Por ejemplo, como en el modelo `Image` tenemos un campo `url`, podemos declararl El string será verificado para ser una URL válida, y documentado en JSON Schema / OpenAPI como tal. -## Atributos con listas de submodelos +## Atributos con listas de submodelos { #attributes-with-lists-of-submodels } También puedes usar modelos Pydantic como subtipos de `list`, `set`, etc.: @@ -162,7 +142,7 @@ Nota cómo la clave `images` ahora tiene una lista de objetos de imagen. /// -## Modelos anidados profundamente +## Modelos anidados profundamente { #deeply-nested-models } Puedes definir modelos anidados tan profundamente como desees: @@ -174,16 +154,10 @@ Observa cómo `Offer` tiene una lista de `Item`s, que a su vez tienen una lista /// -## Cuerpos de listas puras +## Cuerpos de listas puras { #bodies-of-pure-lists } Si el valor superior del cuerpo JSON que esperas es un `array` JSON (una `list` en Python), puedes declarar el tipo en el parámetro de la función, al igual que en los modelos Pydantic: -```Python -images: List[Image] -``` - -o en Python 3.9 y superior: - ```Python images: list[Image] ``` @@ -192,7 +166,7 @@ como en: {* ../../docs_src/body_nested_models/tutorial008_py39.py hl[13] *} -## Soporte de editor en todas partes +## Soporte de editor en todas partes { #editor-support-everywhere } Y obtienes soporte de editor en todas partes. @@ -204,7 +178,7 @@ No podrías obtener este tipo de soporte de editor si estuvieras trabajando dire Pero tampoco tienes que preocuparte por ellos, los `dicts` entrantes se convierten automáticamente y tu salida se convierte automáticamente a JSON también. -## Cuerpos de `dict`s arbitrarios +## Cuerpos de `dict`s arbitrarios { #bodies-of-arbitrary-dicts } También puedes declarar un cuerpo como un `dict` con claves de algún tipo y valores de algún otro tipo. @@ -234,7 +208,7 @@ Y el `dict` que recibas como `weights` tendrá realmente claves `int` y valores /// -## Resumen +## Resumen { #recap } Con **FastAPI** tienes la máxima flexibilidad proporcionada por los modelos Pydantic, manteniendo tu código simple, corto y elegante. diff --git a/docs/es/docs/tutorial/body-updates.md b/docs/es/docs/tutorial/body-updates.md index 26cd3345fd..e75e29b54b 100644 --- a/docs/es/docs/tutorial/body-updates.md +++ b/docs/es/docs/tutorial/body-updates.md @@ -1,6 +1,6 @@ -# Cuerpo - Actualizaciones +# Body - Actualizaciones { #body-updates } -## Actualización reemplazando con `PUT` +## Actualización reemplazando con `PUT` { #update-replacing-with-put } Para actualizar un ítem puedes utilizar la operación de HTTP `PUT`. @@ -10,7 +10,7 @@ Puedes usar el `jsonable_encoder` para convertir los datos de entrada en datos q `PUT` se usa para recibir datos que deben reemplazar los datos existentes. -### Advertencia sobre el reemplazo +### Advertencia sobre el reemplazo { #warning-about-replacing } Esto significa que si quieres actualizar el ítem `bar` usando `PUT` con un body que contenga: @@ -26,7 +26,7 @@ debido a que no incluye el atributo ya almacenado `"tax": 20.2`, el modelo de en Y los datos se guardarían con ese "nuevo" `tax` de `10.5`. -## Actualizaciones parciales con `PATCH` +## Actualizaciones parciales con `PATCH` { #partial-updates-with-patch } También puedes usar la operación de HTTP `PATCH` para actualizar *parcialmente* datos. @@ -44,43 +44,27 @@ Pero esta guía te muestra, más o menos, cómo se pretende que se usen. /// -### Uso del parámetro `exclude_unset` de Pydantic +### Uso del parámetro `exclude_unset` de Pydantic { #using-pydantics-exclude-unset-parameter } Si quieres recibir actualizaciones parciales, es muy útil usar el parámetro `exclude_unset` en el `.model_dump()` del modelo de Pydantic. Como `item.model_dump(exclude_unset=True)`. -/// info | Información - -En Pydantic v1 el método se llamaba `.dict()`, fue deprecado (pero aún soportado) en Pydantic v2, y renombrado a `.model_dump()`. - -Los ejemplos aquí usan `.dict()` para compatibilidad con Pydantic v1, pero deberías usar `.model_dump()` si puedes usar Pydantic v2. - -/// - Eso generaría un `dict` solo con los datos que se establecieron al crear el modelo `item`, excluyendo los valores por defecto. Luego puedes usar esto para generar un `dict` solo con los datos que se establecieron (enviados en el request), omitiendo los valores por defecto: {* ../../docs_src/body_updates/tutorial002_py310.py hl[32] *} -### Uso del parámetro `update` de Pydantic +### Uso del parámetro `update` de Pydantic { #using-pydantics-update-parameter } Ahora, puedes crear una copia del modelo existente usando `.model_copy()`, y pasar el parámetro `update` con un `dict` que contenga los datos a actualizar. -/// info | Información - -En Pydantic v1 el método se llamaba `.copy()`, fue deprecado (pero aún soportado) en Pydantic v2, y renombrado a `.model_copy()`. - -Los ejemplos aquí usan `.copy()` para compatibilidad con Pydantic v1, pero deberías usar `.model_copy()` si puedes usar Pydantic v2. - -/// - Como `stored_item_model.model_copy(update=update_data)`: {* ../../docs_src/body_updates/tutorial002_py310.py hl[33] *} -### Resumen de actualizaciones parciales +### Resumen de actualizaciones parciales { #partial-updates-recap } En resumen, para aplicar actualizaciones parciales deberías: @@ -90,9 +74,9 @@ En resumen, para aplicar actualizaciones parciales deberías: * Generar un `dict` sin valores por defecto del modelo de entrada (usando `exclude_unset`). * De esta manera puedes actualizar solo los valores realmente establecidos por el usuario, en lugar de sobrescribir valores ya almacenados con valores por defecto en tu modelo. * Crear una copia del modelo almacenado, actualizando sus atributos con las actualizaciones parciales recibidas (usando el parámetro `update`). -* Convertir el modelo copiado en algo que pueda almacenarse en tu base de datos (por ejemplo, usando el `jsonable_encoder`). +* Convertir el modelo copiado en algo que pueda almacenarse en tu DB (por ejemplo, usando el `jsonable_encoder`). * Esto es comparable a usar el método `.model_dump()` del modelo de nuevo, pero asegura (y convierte) los valores a tipos de datos que pueden convertirse a JSON, por ejemplo, `datetime` a `str`. -* Guardar los datos en tu base de datos. +* Guardar los datos en tu DB. * Devolver el modelo actualizado. {* ../../docs_src/body_updates/tutorial002_py310.py hl[28:35] *} diff --git a/docs/es/docs/tutorial/body.md b/docs/es/docs/tutorial/body.md index 6d0aa7c60f..dde39f78c1 100644 --- a/docs/es/docs/tutorial/body.md +++ b/docs/es/docs/tutorial/body.md @@ -1,4 +1,4 @@ -# Request Body +# Request Body { #request-body } Cuando necesitas enviar datos desde un cliente (digamos, un navegador) a tu API, los envías como un **request body**. @@ -18,13 +18,13 @@ Como no se recomienda, la documentación interactiva con Swagger UI no mostrará /// -## Importar `BaseModel` de Pydantic +## Importar `BaseModel` de Pydantic { #import-pydantics-basemodel } Primero, necesitas importar `BaseModel` de `pydantic`: {* ../../docs_src/body/tutorial001_py310.py hl[2] *} -## Crea tu modelo de datos +## Crea tu modelo de datos { #create-your-data-model } Luego, declaras tu modelo de datos como una clase que hereda de `BaseModel`. @@ -32,7 +32,8 @@ Usa tipos estándar de Python para todos los atributos: {* ../../docs_src/body/tutorial001_py310.py hl[5:9] *} -Al igual que al declarar parámetros de query, cuando un atributo del modelo tiene un valor por defecto, no es obligatorio. De lo contrario, es obligatorio. Usa `None` para hacerlo opcional. + +Al igual que al declarar parámetros de query, cuando un atributo del modelo tiene un valor por defecto, no es obligatorio. De lo contrario, es obligatorio. Usa `None` para hacerlo solo opcional. Por ejemplo, el modelo anterior declara un “`object`” JSON (o `dict` en Python) como: @@ -54,7 +55,7 @@ Por ejemplo, el modelo anterior declara un “`object`” JSON (o `dict` en Pyth } ``` -## Decláralo como un parámetro +## Decláralo como un parámetro { #declare-it-as-a-parameter } Para añadirlo a tu *path operation*, decláralo de la misma manera que declaraste parámetros de path y query: @@ -62,7 +63,7 @@ Para añadirlo a tu *path operation*, decláralo de la misma manera que declaras ...y declara su tipo como el modelo que creaste, `Item`. -## Resultados +## Resultados { #results } Con solo esa declaración de tipo en Python, **FastAPI** hará lo siguiente: @@ -73,9 +74,9 @@ Con solo esa declaración de tipo en Python, **FastAPI** hará lo siguiente: * Proporcionar los datos recibidos en el parámetro `item`. * Como lo declaraste en la función como de tipo `Item`, también tendrás todo el soporte del editor (autocompletado, etc.) para todos los atributos y sus tipos. * Generar definiciones de JSON Schema para tu modelo, que también puedes usar en cualquier otro lugar si tiene sentido para tu proyecto. -* Esquemas que serán parte del esquema de OpenAPI generado y usados por la UIs de documentación automática. +* Esos esquemas serán parte del esquema de OpenAPI generado y usados por las UIs de documentación automática. -## Documentación automática +## Documentación automática { #automatic-docs } Los JSON Schemas de tus modelos serán parte del esquema OpenAPI generado y se mostrarán en la documentación API interactiva: @@ -85,7 +86,7 @@ Y también se utilizarán en la documentación API dentro de cada *path operatio -## Soporte del editor +## Soporte del editor { #editor-support } En tu editor, dentro de tu función, obtendrás anotaciones de tipos y autocompletado en todas partes (esto no sucedería si recibieras un `dict` en lugar de un modelo de Pydantic): @@ -121,13 +122,13 @@ Mejora el soporte del editor para modelos de Pydantic, con: /// -## Usa el modelo +## Usa el modelo { #use-the-model } Dentro de la función, puedes acceder a todos los atributos del objeto modelo directamente: {* ../../docs_src/body/tutorial002_py310.py *} -## Request body + parámetros de path +## Request body + parámetros de path { #request-body-path-parameters } Puedes declarar parámetros de path y request body al mismo tiempo. @@ -135,7 +136,8 @@ Puedes declarar parámetros de path y request body al mismo tiempo. {* ../../docs_src/body/tutorial003_py310.py hl[15:16] *} -## Request body + path + parámetros de query + +## Request body + path + parámetros de query { #request-body-path-query-parameters } También puedes declarar parámetros de **body**, **path** y **query**, todos al mismo tiempo. @@ -147,18 +149,18 @@ Los parámetros de la función se reconocerán de la siguiente manera: * Si el parámetro también se declara en el **path**, se utilizará como un parámetro de path. * Si el parámetro es de un **tipo singular** (como `int`, `float`, `str`, `bool`, etc.), se interpretará como un parámetro de **query**. -* Si el parámetro se declara como del tipo de un **modelo de Pydantic**, se interpretará como un **request body**. +* Si el parámetro se declara como del tipo de un **modelo de Pydantic**, se interpretará como un **body** de request. /// note | Nota FastAPI sabrá que el valor de `q` no es requerido debido al valor por defecto `= None`. -El `str | None` (Python 3.10+) o `Union` en `Union[str, None]` (Python 3.8+) no es utilizado por FastAPI para determinar que el valor no es requerido, sabrá que no es requerido porque tiene un valor por defecto de `= None`. +El `str | None` (Python 3.10+) o `Union` en `Union[str, None]` (Python 3.9+) no es utilizado por FastAPI para determinar que el valor no es requerido, sabrá que no es requerido porque tiene un valor por defecto de `= None`. Pero agregar las anotaciones de tipos permitirá que tu editor te brinde un mejor soporte y detecte errores. /// -## Sin Pydantic +## Sin Pydantic { #without-pydantic } -Si no quieres usar modelos de Pydantic, también puedes usar parámetros **Body**. Consulta la documentación para [Body - Multiples Parametros: Valores singulares en body](body-multiple-params.md#singular-values-in-body){.internal-link target=_blank}. +Si no quieres usar modelos de Pydantic, también puedes usar parámetros **Body**. Consulta la documentación para [Body - Multiple Parameters: Singular values in body](body-multiple-params.md#singular-values-in-body){.internal-link target=_blank}. diff --git a/docs/es/docs/tutorial/cookie-param-models.md b/docs/es/docs/tutorial/cookie-param-models.md index ebdb592651..dab7d8c0a2 100644 --- a/docs/es/docs/tutorial/cookie-param-models.md +++ b/docs/es/docs/tutorial/cookie-param-models.md @@ -1,4 +1,4 @@ -# Modelos de Cookies +# Modelos de Cookies { #cookie-parameter-models } Si tienes un grupo de **cookies** que están relacionadas, puedes crear un **modelo de Pydantic** para declararlas. 🍪 @@ -16,7 +16,7 @@ Esta misma técnica se aplica a `Query`, `Cookie`, y `Header`. 😎 /// -## Cookies con un Modelo de Pydantic +## Cookies con un Modelo de Pydantic { #cookies-with-a-pydantic-model } Declara los parámetros de **cookie** que necesites en un **modelo de Pydantic**, y luego declara el parámetro como `Cookie`: @@ -24,7 +24,7 @@ Declara los parámetros de **cookie** que necesites en un **modelo de Pydantic** **FastAPI** **extraerá** los datos para **cada campo** de las **cookies** recibidas en el request y te entregará el modelo de Pydantic que definiste. -## Revisa la Documentación +## Revisa la Documentación { #check-the-docs } Puedes ver las cookies definidas en la UI de la documentación en `/docs`: @@ -42,7 +42,7 @@ Pero incluso si **rellenas los datos** y haces clic en "Execute", como la UI de /// -## Prohibir Cookies Extra +## Prohibir Cookies Extra { #forbid-extra-cookies } En algunos casos de uso especiales (probablemente no muy comunes), podrías querer **restringir** las cookies que deseas recibir. @@ -50,7 +50,7 @@ Tu API ahora tiene el poder de controlar su propio **cookies** en **FastAPI**. 😎 diff --git a/docs/es/docs/tutorial/cookie-params.md b/docs/es/docs/tutorial/cookie-params.md index 45b113ff9c..598872c0a5 100644 --- a/docs/es/docs/tutorial/cookie-params.md +++ b/docs/es/docs/tutorial/cookie-params.md @@ -1,14 +1,14 @@ -# Parámetros de Cookie +# Parámetros de Cookie { #cookie-parameters } Puedes definir parámetros de Cookie de la misma manera que defines los parámetros `Query` y `Path`. -## Importar `Cookie` +## Importar `Cookie` { #import-cookie } Primero importa `Cookie`: {* ../../docs_src/cookie_params/tutorial001_an_py310.py hl[3] *} -## Declarar parámetros de `Cookie` +## Declarar parámetros de `Cookie` { #declare-cookie-parameters } Luego declara los parámetros de cookie usando la misma estructura que con `Path` y `Query`. @@ -30,6 +30,16 @@ Para declarar cookies, necesitas usar `Cookie`, porque de lo contrario los pará /// -## Resumen +/// info | Información + +Ten en cuenta que, como **los navegadores manejan las cookies** de formas especiales y por detrás, **no** permiten fácilmente que **JavaScript** las toque. + +Si vas a la **UI de la documentación de la API** en `/docs` podrás ver la **documentación** de cookies para tus *path operations*. + +Pero incluso si **rellenas los datos** y haces clic en "Execute", como la UI de la documentación funciona con **JavaScript**, las cookies no se enviarán y verás un mensaje de **error** como si no hubieras escrito ningún valor. + +/// + +## Resumen { #recap } Declara cookies con `Cookie`, usando el mismo patrón común que `Query` y `Path`. diff --git a/docs/es/docs/tutorial/cors.md b/docs/es/docs/tutorial/cors.md index e493d44312..c1a23295e1 100644 --- a/docs/es/docs/tutorial/cors.md +++ b/docs/es/docs/tutorial/cors.md @@ -1,8 +1,8 @@ -# CORS (Cross-Origin Resource Sharing) +# CORS (Cross-Origin Resource Sharing) { #cors-cross-origin-resource-sharing } CORS o "Cross-Origin Resource Sharing" se refiere a situaciones en las que un frontend que se ejecuta en un navegador tiene código JavaScript que se comunica con un backend, y el backend está en un "origen" diferente al frontend. -## Origen +## Origen { #origin } Un origen es la combinación de protocolo (`http`, `https`), dominio (`myapp.com`, `localhost`, `localhost.tiangolo.com`) y puerto (`80`, `443`, `8080`). @@ -14,7 +14,7 @@ Así que, todos estos son orígenes diferentes: Aunque todos están en `localhost`, usan protocolos o puertos diferentes, por lo tanto, son "orígenes" diferentes. -## Pasos +## Pasos { #steps } Entonces, digamos que tienes un frontend corriendo en tu navegador en `http://localhost:8080`, y su JavaScript está tratando de comunicarse con un backend corriendo en `http://localhost` (porque no especificamos un puerto, el navegador asumirá el puerto por defecto `80`). @@ -24,7 +24,7 @@ Para lograr esto, el backend `:80` debe tener una lista de "orígenes permitidos En este caso, la lista tendría que incluir `http://localhost:8080` para que el frontend `:8080` funcione correctamente. -## Comodines +## Comodines { #wildcards } También es posible declarar la lista como `"*"` (un "comodín") para decir que todos están permitidos. @@ -32,7 +32,7 @@ Pero eso solo permitirá ciertos tipos de comunicación, excluyendo todo lo que Así que, para que todo funcione correctamente, es mejor especificar explícitamente los orígenes permitidos. -## Usa `CORSMiddleware` +## Usa `CORSMiddleware` { #use-corsmiddleware } Puedes configurarlo en tu aplicación **FastAPI** usando el `CORSMiddleware`. @@ -46,7 +46,7 @@ También puedes especificar si tu backend permite: * Métodos HTTP específicos (`POST`, `PUT`) o todos ellos con el comodín `"*"`. * Headers HTTP específicos o todos ellos con el comodín `"*"`. -{* ../../docs_src/cors/tutorial001.py hl[2,6:11,13:19] *} +{* ../../docs_src/cors/tutorial001_py39.py hl[2,6:11,13:19] *} Los parámetros predeterminados utilizados por la implementación de `CORSMiddleware` son restrictivos por defecto, por lo que necesitarás habilitar explícitamente orígenes, métodos o headers particulares para que los navegadores estén permitidos de usarlos en un contexto de Cross-Domain. @@ -56,23 +56,26 @@ Se admiten los siguientes argumentos: * `allow_origin_regex` - Una cadena regex para coincidir con orígenes que deberían estar permitidos para hacer requests cross-origin. por ejemplo, `'https://.*\.example\.org'`. * `allow_methods` - Una lista de métodos HTTP que deberían estar permitidos para requests cross-origin. Por defecto es `['GET']`. Puedes usar `['*']` para permitir todos los métodos estándar. * `allow_headers` - Una lista de headers de request HTTP que deberían estar soportados para requests cross-origin. Por defecto es `[]`. Puedes usar `['*']` para permitir todos los headers. Los headers `Accept`, `Accept-Language`, `Content-Language` y `Content-Type` siempre están permitidos para requests CORS simples. -* `allow_credentials` - Indica que las cookies deberían estar soportadas para requests cross-origin. Por defecto es `False`. Además, `allow_origins` no puede ser configurado a `['*']` para que las credenciales estén permitidas, los orígenes deben ser especificados. +* `allow_credentials` - Indica que las cookies deberían estar soportadas para requests cross-origin. Por defecto es `False`. + + Ninguno de `allow_origins`, `allow_methods` y `allow_headers` puede establecerse a `['*']` si `allow_credentials` está configurado a `True`. Todos deben ser especificados explícitamente. + * `expose_headers` - Indica cualquier header de response que debería ser accesible para el navegador. Por defecto es `[]`. * `max_age` - Establece un tiempo máximo en segundos para que los navegadores almacenen en caché los responses CORS. Por defecto es `600`. El middleware responde a dos tipos particulares de request HTTP... -### Requests de preflight CORS +### Requests de preflight CORS { #cors-preflight-requests } Estos son cualquier request `OPTIONS` con headers `Origin` y `Access-Control-Request-Method`. En este caso, el middleware interceptará el request entrante y responderá con los headers CORS adecuados, y un response `200` o `400` con fines informativos. -### Requests simples +### Requests simples { #simple-requests } Cualquier request con un header `Origin`. En este caso, el middleware pasará el request a través de lo normal, pero incluirá los headers CORS adecuados en el response. -## Más info +## Más info { #more-info } Para más información sobre CORS, revisa la documentación de CORS de Mozilla. diff --git a/docs/es/docs/tutorial/debugging.md b/docs/es/docs/tutorial/debugging.md index 2a7544e830..c31daf40f4 100644 --- a/docs/es/docs/tutorial/debugging.md +++ b/docs/es/docs/tutorial/debugging.md @@ -1,14 +1,14 @@ -# Depuración +# Depuración { #debugging } Puedes conectar el depurador en tu editor, por ejemplo con Visual Studio Code o PyCharm. -## Llama a `uvicorn` +## Llama a `uvicorn` { #call-uvicorn } En tu aplicación de FastAPI, importa y ejecuta `uvicorn` directamente: -{* ../../docs_src/debugging/tutorial001.py hl[1,15] *} +{* ../../docs_src/debugging/tutorial001_py39.py hl[1,15] *} -### Acerca de `__name__ == "__main__"` +### Acerca de `__name__ == "__main__"` { #about-name-main } El objetivo principal de `__name__ == "__main__"` es tener algo de código que se ejecute cuando tu archivo es llamado con: @@ -26,7 +26,7 @@ pero no es llamado cuando otro archivo lo importa, como en: from myapp import app ``` -#### Más detalles +#### Más detalles { #more-details } Supongamos que tu archivo se llama `myapp.py`. @@ -78,7 +78,7 @@ Para más información, revisa -## Atajo +## Atajo { #shortcut } Pero ves que estamos teniendo algo de repetición de código aquí, escribiendo `CommonQueryParams` dos veces: -//// tab | Python 3.8+ +//// tab | Python 3.9+ ```Python commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] @@ -205,7 +205,7 @@ commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] //// -//// tab | Python 3.8+ sin `Annotated` +//// tab | Python 3.9+ sin `Annotated` /// tip | Consejo @@ -225,7 +225,7 @@ Para esos casos específicos, puedes hacer lo siguiente: En lugar de escribir: -//// tab | Python 3.8+ +//// tab | Python 3.9+ ```Python commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] @@ -233,7 +233,7 @@ commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] //// -//// tab | Python 3.8+ sin `Annotated` +//// tab | Python 3.9+ sin `Annotated` /// tip | Consejo @@ -249,7 +249,7 @@ commons: CommonQueryParams = Depends(CommonQueryParams) ...escribes: -//// tab | Python 3.8+ +//// tab | Python 3.9+ ```Python commons: Annotated[CommonQueryParams, Depends()] @@ -257,7 +257,7 @@ commons: Annotated[CommonQueryParams, Depends()] //// -//// tab | Python 3.8 sin `Annotated` +//// tab | Python 3.9+ sin `Annotated` /// tip | Consejo diff --git a/docs/es/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md b/docs/es/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md index fbe17c67a0..60baa93a90 100644 --- a/docs/es/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md +++ b/docs/es/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md @@ -1,4 +1,4 @@ -# Dependencias en decoradores de *path operation* +# Dependencias en decoradores de *path operation* { #dependencies-in-path-operation-decorators } En algunos casos realmente no necesitas el valor de retorno de una dependencia dentro de tu *path operation function*. @@ -8,7 +8,7 @@ Pero aún necesitas que sea ejecutada/resuelta. Para esos casos, en lugar de declarar un parámetro de *path operation function* con `Depends`, puedes añadir una `list` de `dependencies` al decorador de *path operation*. -## Agregar `dependencies` al decorador de *path operation* +## Agregar `dependencies` al decorador de *path operation* { #add-dependencies-to-the-path-operation-decorator } El decorador de *path operation* recibe un argumento opcional `dependencies`. @@ -36,23 +36,23 @@ Pero en casos reales, al implementar seguridad, obtendrías más beneficios usan /// -## Errores de dependencias y valores de retorno +## Errores de dependencias y valores de retorno { #dependencies-errors-and-return-values } Puedes usar las mismas *funciones* de dependencia que usas normalmente. -### Requisitos de dependencia +### Requisitos de dependencia { #dependency-requirements } Pueden declarar requisitos de request (como headers) u otras sub-dependencias: {* ../../docs_src/dependencies/tutorial006_an_py39.py hl[8,13] *} -### Lanzar excepciones +### Lanzar excepciones { #raise-exceptions } Estas dependencias pueden `raise` excepciones, igual que las dependencias normales: {* ../../docs_src/dependencies/tutorial006_an_py39.py hl[10,15] *} -### Valores de retorno +### Valores de retorno { #return-values } Y pueden devolver valores o no, los valores no serán usados. @@ -60,10 +60,10 @@ Así que, puedes reutilizar una dependencia normal (que devuelve un valor) que y {* ../../docs_src/dependencies/tutorial006_an_py39.py hl[11,16] *} -## Dependencias para un grupo de *path operations* +## Dependencias para un grupo de *path operations* { #dependencies-for-a-group-of-path-operations } Más adelante, cuando leas sobre cómo estructurar aplicaciones más grandes ([Aplicaciones Más Grandes - Múltiples Archivos](../../tutorial/bigger-applications.md){.internal-link target=_blank}), posiblemente con múltiples archivos, aprenderás cómo declarar un único parámetro `dependencies` para un grupo de *path operations*. -## Dependencias Globales +## Dependencias Globales { #global-dependencies } A continuación veremos cómo añadir dependencias a toda la aplicación `FastAPI`, de modo que se apliquen a cada *path operation*. diff --git a/docs/es/docs/tutorial/dependencies/dependencies-with-yield.md b/docs/es/docs/tutorial/dependencies/dependencies-with-yield.md index 94290443aa..aa645daa47 100644 --- a/docs/es/docs/tutorial/dependencies/dependencies-with-yield.md +++ b/docs/es/docs/tutorial/dependencies/dependencies-with-yield.md @@ -1,8 +1,8 @@ -# Dependencias con yield +# Dependencias con yield { #dependencies-with-yield } -FastAPI admite dependencias que realizan algunos pasos adicionales después de finalizar. +FastAPI admite dependencias que realizan algunos pasos adicionales después de finalizar. -Para hacer esto, usa `yield` en lugar de `return` y escribe los pasos adicionales (código) después. +Para hacer esto, usa `yield` en lugar de `return`, y escribe los pasos adicionales (código) después. /// tip | Consejo @@ -10,7 +10,7 @@ Asegúrate de usar `yield` una sola vez por dependencia. /// -/// note | Nota técnica +/// note | Detalles técnicos Cualquier función que sea válida para usar con: @@ -23,21 +23,21 @@ De hecho, FastAPI usa esos dos decoradores internamente. /// -## Una dependencia de base de datos con `yield` +## Una dependencia de base de datos con `yield` { #a-database-dependency-with-yield } Por ejemplo, podrías usar esto para crear una sesión de base de datos y cerrarla después de finalizar. Solo el código anterior e incluyendo la declaración `yield` se ejecuta antes de crear un response: -{* ../../docs_src/dependencies/tutorial007.py hl[2:4] *} +{* ../../docs_src/dependencies/tutorial007_py39.py hl[2:4] *} El valor generado es lo que se inyecta en *path operations* y otras dependencias: -{* ../../docs_src/dependencies/tutorial007.py hl[4] *} +{* ../../docs_src/dependencies/tutorial007_py39.py hl[4] *} -El código posterior a la declaración `yield` se ejecuta después de crear el response pero antes de enviarla: +El código posterior a la declaración `yield` se ejecuta después del response: -{* ../../docs_src/dependencies/tutorial007.py hl[5:6] *} +{* ../../docs_src/dependencies/tutorial007_py39.py hl[5:6] *} /// tip | Consejo @@ -47,7 +47,7 @@ Puedes usar funciones `async` o regulares. /// -## Una dependencia con `yield` y `try` +## Una dependencia con `yield` y `try` { #a-dependency-with-yield-and-try } Si usas un bloque `try` en una dependencia con `yield`, recibirás cualquier excepción que se haya lanzado al usar la dependencia. @@ -57,9 +57,9 @@ Por lo tanto, puedes buscar esa excepción específica dentro de la dependencia Del mismo modo, puedes usar `finally` para asegurarte de que los pasos de salida se ejecuten, sin importar si hubo una excepción o no. -{* ../../docs_src/dependencies/tutorial007.py hl[3,5] *} +{* ../../docs_src/dependencies/tutorial007_py39.py hl[3,5] *} -## Sub-dependencias con `yield` +## Sub-dependencias con `yield` { #sub-dependencies-with-yield } Puedes tener sub-dependencias y "árboles" de sub-dependencias de cualquier tamaño y forma, y cualquiera o todas ellas pueden usar `yield`. @@ -85,7 +85,7 @@ Puedes tener cualquier combinación de dependencias que quieras. **FastAPI** se asegurará de que todo se ejecute en el orden correcto. -/// note | Nota técnica +/// note | Detalles técnicos Esto funciona gracias a los Context Managers de Python. @@ -93,15 +93,17 @@ Esto funciona gracias a los > dep_req: Start request + Note over dep_req: Run code up to yield + dep_req ->> dep_func: Pass dependency + Note over dep_func: Run code up to yield + dep_func ->> operation: Run path operation with dependency + operation ->> dep_func: Return from path operation + Note over dep_func: Run code after yield + Note over dep_func: ✅ Dependency closed + dep_func ->> client: Send response to client + Note over client: Response sent + Note over dep_req: Run code after yield + Note over dep_req: ✅ Dependency closed +``` -De esta manera probablemente tendrás un código más limpio. +## Dependencias con `yield`, `HTTPException`, `except` y Tareas en Background { #dependencies-with-yield-httpexception-except-and-background-tasks } -/// +Las dependencias con `yield` han evolucionado con el tiempo para cubrir diferentes casos de uso y corregir algunos problemas. -Si solías depender de este comportamiento, ahora deberías crear los recursos para tareas en background dentro de la propia tarea en background, y usar internamente solo datos que no dependan de los recursos de las dependencias con `yield`. +Si quieres ver qué ha cambiado en diferentes versiones de FastAPI, puedes leer más al respecto en la guía avanzada, en [Dependencias avanzadas - Dependencias con `yield`, `HTTPException`, `except` y Tareas en Background](../../advanced/advanced-dependencies.md#dependencies-with-yield-httpexception-except-and-background-tasks){.internal-link target=_blank}. -Por ejemplo, en lugar de usar la misma sesión de base de datos, crearías una nueva sesión de base de datos dentro de la tarea en background, y obtendrías los objetos de la base de datos usando esta nueva sesión. Y luego, en lugar de pasar el objeto de la base de datos como parámetro a la función de tarea en background, pasarías el ID de ese objeto y luego obtendrías el objeto nuevamente dentro de la función de tarea en background. +## Context Managers { #context-managers } -## Context Managers - -### Qué son los "Context Managers" +### Qué son los "Context Managers" { #what-are-context-managers } Los "Context Managers" son aquellos objetos de Python que puedes usar en una declaración `with`. @@ -240,7 +255,7 @@ Cuando el bloque `with` termina, se asegura de cerrar el archivo, incluso si hub Cuando creas una dependencia con `yield`, **FastAPI** creará internamente un context manager para ella y lo combinará con algunas otras herramientas relacionadas. -### Usando context managers en dependencias con `yield` +### Usando context managers en dependencias con `yield` { #using-context-managers-in-dependencies-with-yield } /// warning | Advertencia @@ -255,7 +270,7 @@ En Python, puedes crear Context Managers Inyección de Dependencias** muy poderoso pero intuitivo. Está diseñado para ser muy simple de usar, y para hacer que cualquier desarrollador integre otros componentes con **FastAPI** de forma muy sencilla. -## Qué es la "Inyección de Dependencias" +## Qué es la "Inyección de Dependencias" { #what-is-dependency-injection } **"Inyección de Dependencias"** significa, en programación, que hay una manera para que tu código (en este caso, tus *path operation functions*) declare las cosas que necesita para funcionar y utilizar: "dependencias". @@ -19,13 +19,13 @@ Esto es muy útil cuando necesitas: Todo esto, mientras minimizas la repetición de código. -## Primeros Pasos +## Primeros Pasos { #first-steps } Veamos un ejemplo muy simple. Será tan simple que no es muy útil, por ahora. Pero de esta manera podemos enfocarnos en cómo funciona el sistema de **Inyección de Dependencias**. -### Crear una dependencia, o "dependable" +### Crear una dependencia, o "dependable" { #create-a-dependency-or-dependable } Primero enfoquémonos en la dependencia. @@ -61,11 +61,11 @@ Asegúrate de [Actualizar la versión de FastAPI](../../deployment/versions.md#u /// -### Importar `Depends` +### Importar `Depends` { #import-depends } {* ../../docs_src/dependencies/tutorial001_an_py310.py hl[3] *} -### Declarar la dependencia, en el "dependant" +### Declarar la dependencia, en el "dependant" { #declare-the-dependency-in-the-dependant } De la misma forma en que usas `Body`, `Query`, etc. con los parámetros de tu *path operation function*, usa `Depends` con un nuevo parámetro: @@ -114,7 +114,7 @@ Solo la pasas a `Depends` y **FastAPI** sabe cómo hacer el resto. /// -## Compartir dependencias `Annotated` +## Compartir dependencias `Annotated` { #share-annotated-dependencies } En los ejemplos anteriores, ves que hay un poquito de **duplicación de código**. @@ -138,9 +138,9 @@ Pero porque **FastAPI** está basado en los estándares de Python, incluido `Ann Las dependencias seguirán funcionando como se esperaba, y la **mejor parte** es que la **información de tipo se preservará**, lo que significa que tu editor podrá seguir proporcionándote **autocompletado**, **errores en línea**, etc. Lo mismo para otras herramientas como `mypy`. -Esto será especialmente útil cuando lo uses en una **gran base de código** donde uses **las mismas dependencias** una y otra vez en **muchas *path operations***. +Esto será especialmente útil cuando lo uses en una **gran code base** donde uses **las mismas dependencias** una y otra vez en **muchas *path operations***. -## Usar `async` o no usar `async` +## Usar `async` o no usar `async` { #to-async-or-not-to-async } Como las dependencias también serán llamadas por **FastAPI** (lo mismo que tus *path operation functions*), las mismas reglas aplican al definir tus funciones. @@ -156,7 +156,7 @@ Si no lo sabes, revisa la sección [Async: *"¿Con prisa?"*](../../async.md#in-a /// -## Integración con OpenAPI +## Integración con OpenAPI { #integrated-with-openapi } Todas las declaraciones de request, validaciones y requisitos de tus dependencias (y sub-dependencias) se integrarán en el mismo esquema de OpenAPI. @@ -164,7 +164,7 @@ Así, la documentación interactiva tendrá toda la información de estas depend -## Uso simple +## Uso simple { #simple-usage } Si lo ves, las *path operation functions* se declaran para ser usadas siempre que un *path* y una *operación* coincidan, y luego **FastAPI** se encarga de llamar la función con los parámetros correctos, extrayendo los datos del request. @@ -182,7 +182,7 @@ Otros términos comunes para esta misma idea de "inyección de dependencias" son * inyectables * componentes -## Plug-ins de **FastAPI** +## Plug-ins de **FastAPI** { #fastapi-plug-ins } Las integraciones y "plug-ins" pueden construirse usando el sistema de **Inyección de Dependencias**. Pero, de hecho, en realidad **no hay necesidad de crear "plug-ins"**, ya que al usar dependencias es posible declarar una cantidad infinita de integraciones e interacciones que se vuelven disponibles para tus *path operation functions*. @@ -190,7 +190,7 @@ Y las dependencias se pueden crear de una manera muy simple e intuitiva que te p Verás ejemplos de esto en los próximos capítulos, sobre bases de datos relacionales y NoSQL, seguridad, etc. -## Compatibilidad de **FastAPI** +## Compatibilidad de **FastAPI** { #fastapi-compatibility } La simplicidad del sistema de inyección de dependencias hace que **FastAPI** sea compatible con: @@ -203,7 +203,7 @@ La simplicidad del sistema de inyección de dependencias hace que **FastAPI** se * sistemas de inyección de datos de response * etc. -## Simple y Poderoso +## Simple y Poderoso { #simple-and-powerful } Aunque el sistema de inyección de dependencias jerárquico es muy simple de definir y usar, sigue siendo muy poderoso. @@ -243,7 +243,7 @@ admin_user --> activate_user paying_user --> pro_items ``` -## Integrado con **OpenAPI** +## Integrado con **OpenAPI** { #integrated-with-openapi_1 } Todas estas dependencias, al declarar sus requisitos, también añaden parámetros, validaciones, etc. a tus *path operations*. diff --git a/docs/es/docs/tutorial/dependencies/sub-dependencies.md b/docs/es/docs/tutorial/dependencies/sub-dependencies.md index bba5322072..e74d65d7e9 100644 --- a/docs/es/docs/tutorial/dependencies/sub-dependencies.md +++ b/docs/es/docs/tutorial/dependencies/sub-dependencies.md @@ -1,4 +1,4 @@ -# Sub-dependencias +# Sub-dependencias { #sub-dependencies } Puedes crear dependencias que tengan **sub-dependencias**. @@ -6,7 +6,7 @@ Pueden ser tan **profundas** como necesites. **FastAPI** se encargará de resolverlas. -## Primera dependencia "dependable" +## Primera dependencia "dependable" { #first-dependency-dependable } Podrías crear una primera dependencia ("dependable") así: @@ -16,7 +16,7 @@ Declara un parámetro de query opcional `q` como un `str`, y luego simplemente l Esto es bastante simple (no muy útil), pero nos ayudará a centrarnos en cómo funcionan las sub-dependencias. -## Segunda dependencia, "dependable" y "dependant" +## Segunda dependencia, "dependable" y "dependant" { #second-dependency-dependable-and-dependant } Luego puedes crear otra función de dependencia (un "dependable") que al mismo tiempo declare una dependencia propia (por lo que también es un "dependant"): @@ -29,7 +29,7 @@ Centrémonos en los parámetros declarados: * También declara una `last_query` cookie opcional, como un `str`. * Si el usuario no proporcionó ningún query `q`, usamos el último query utilizado, que guardamos previamente en una cookie. -## Usa la dependencia +## Usa la dependencia { #use-the-dependency } Entonces podemos usar la dependencia con: @@ -54,7 +54,7 @@ read_query["/items/"] query_extractor --> query_or_cookie_extractor --> read_query ``` -## Usando la misma dependencia múltiples veces +## Usando la misma dependencia múltiples veces { #using-the-same-dependency-multiple-times } Si una de tus dependencias se declara varias veces para la misma *path operation*, por ejemplo, múltiples dependencias tienen una sub-dependencia común, **FastAPI** sabrá llamar a esa sub-dependencia solo una vez por request. @@ -62,7 +62,7 @@ Y guardará el valor devuelto en un ISO. +Entonces, un objeto `datetime` tendría que ser convertido a un `str` que contenga los datos en formato ISO. De la misma manera, esta base de datos no recibiría un modelo de Pydantic (un objeto con atributos), solo un `dict`. diff --git a/docs/es/docs/tutorial/extra-data-types.md b/docs/es/docs/tutorial/extra-data-types.md index 28775780f0..e876921ba4 100644 --- a/docs/es/docs/tutorial/extra-data-types.md +++ b/docs/es/docs/tutorial/extra-data-types.md @@ -1,4 +1,4 @@ -# Tipos de Datos Extra +# Tipos de Datos Extra { #extra-data-types } Hasta ahora, has estado usando tipos de datos comunes, como: @@ -17,7 +17,7 @@ Y seguirás teniendo las mismas funcionalidades como hasta ahora: * Validación de datos. * Anotación y documentación automática. -## Otros tipos de datos +## Otros tipos de datos { #other-data-types } Aquí hay algunos de los tipos de datos adicionales que puedes usar: @@ -51,7 +51,7 @@ Aquí hay algunos de los tipos de datos adicionales que puedes usar: * En requests y responses, manejado igual que un `float`. * Puedes revisar todos los tipos de datos válidos de Pydantic aquí: Tipos de datos de Pydantic. -## Ejemplo +## Ejemplo { #example } Aquí tienes un ejemplo de una *path operation* con parámetros usando algunos de los tipos anteriores. diff --git a/docs/es/docs/tutorial/extra-models.md b/docs/es/docs/tutorial/extra-models.md index 0380b96904..d72c73e245 100644 --- a/docs/es/docs/tutorial/extra-models.md +++ b/docs/es/docs/tutorial/extra-models.md @@ -1,4 +1,4 @@ -# Modelos Extra +# Modelos Extra { #extra-models } Continuando con el ejemplo anterior, será común tener más de un modelo relacionado. @@ -16,27 +16,19 @@ Si no lo sabes, aprenderás qué es un "hash de contraseña" en los [capítulos /// -## Múltiples modelos +## Múltiples modelos { #multiple-models } Aquí tienes una idea general de cómo podrían ser los modelos con sus campos de contraseña y los lugares donde se utilizan: {* ../../docs_src/extra_models/tutorial001_py310.py hl[7,9,14,20,22,27:28,31:33,38:39] *} -/// info | Información +### Acerca de `**user_in.model_dump()` { #about-user-in-model-dump } -En Pydantic v1 el método se llamaba `.dict()`, fue deprecado (pero aún soportado) en Pydantic v2, y renombrado a `.model_dump()`. - -Los ejemplos aquí usan `.dict()` para compatibilidad con Pydantic v1, pero deberías usar `.model_dump()` en su lugar si puedes usar Pydantic v2. - -/// - -### Acerca de `**user_in.dict()` - -#### `.dict()` de Pydantic +#### `.model_dump()` de Pydantic { #pydantics-model-dump } `user_in` es un modelo Pydantic de la clase `UserIn`. -Los modelos Pydantic tienen un método `.dict()` que devuelve un `dict` con los datos del modelo. +Los modelos Pydantic tienen un método `.model_dump()` que devuelve un `dict` con los datos del modelo. Así que, si creamos un objeto Pydantic `user_in` como: @@ -47,7 +39,7 @@ user_in = UserIn(username="john", password="secret", email="john.doe@example.com y luego llamamos a: ```Python -user_dict = user_in.dict() +user_dict = user_in.model_dump() ``` ahora tenemos un `dict` con los datos en la variable `user_dict` (es un `dict` en lugar de un objeto modelo Pydantic). @@ -58,7 +50,7 @@ Y si llamamos a: print(user_dict) ``` -obtendremos un `dict` de Python con: +obtendríamos un `dict` de Python con: ```Python { @@ -69,7 +61,7 @@ obtendremos un `dict` de Python con: } ``` -#### Desempaquetando un `dict` +#### Desempaquetando un `dict` { #unpacking-a-dict } Si tomamos un `dict` como `user_dict` y lo pasamos a una función (o clase) con `**user_dict`, Python lo "desempaquetará". Pasará las claves y valores del `user_dict` directamente como argumentos clave-valor. @@ -101,31 +93,31 @@ UserInDB( ) ``` -#### Un modelo Pydantic a partir del contenido de otro +#### Un modelo Pydantic a partir del contenido de otro { #a-pydantic-model-from-the-contents-of-another } -Como en el ejemplo anterior obtuvimos `user_dict` de `user_in.dict()`, este código: +Como en el ejemplo anterior obtuvimos `user_dict` de `user_in.model_dump()`, este código: ```Python -user_dict = user_in.dict() +user_dict = user_in.model_dump() UserInDB(**user_dict) ``` sería equivalente a: ```Python -UserInDB(**user_in.dict()) +UserInDB(**user_in.model_dump()) ``` -...porque `user_in.dict()` es un `dict`, y luego hacemos que Python lo "desempaquete" al pasarlo a `UserInDB` con el prefijo `**`. +...porque `user_in.model_dump()` es un `dict`, y luego hacemos que Python lo "desempaquete" al pasarlo a `UserInDB` con el prefijo `**`. Así, obtenemos un modelo Pydantic a partir de los datos en otro modelo Pydantic. -#### Desempaquetando un `dict` y palabras clave adicionales +#### Desempaquetando un `dict` y palabras clave adicionales { #unpacking-a-dict-and-extra-keywords } Y luego agregando el argumento de palabra clave adicional `hashed_password=hashed_password`, como en: ```Python -UserInDB(**user_in.dict(), hashed_password=hashed_password) +UserInDB(**user_in.model_dump(), hashed_password=hashed_password) ``` ...termina siendo como: @@ -146,7 +138,7 @@ Las funciones adicionales de soporte `fake_password_hasher` y `fake_save_user` s /// -## Reducir duplicación +## Reducir duplicación { #reduce-duplication } Reducir la duplicación de código es una de las ideas centrales en **FastAPI**. @@ -156,7 +148,7 @@ Y estos modelos están compartiendo muchos de los datos y duplicando nombres y t Podríamos hacerlo mejor. -Podemos declarar un modelo `UserBase` que sirva como base para nuestros otros modelos. Y luego podemos hacer subclases de ese modelo que heredan sus atributos (declaraciones de tipo, validación, etc). +Podemos declarar un modelo `UserBase` que sirva como base para nuestros otros modelos. Y luego podemos hacer subclases de ese modelo que heredan sus atributos (declaraciones de tipos, validación, etc). Toda la conversión de datos, validación, documentación, etc. seguirá funcionando normalmente. @@ -164,13 +156,13 @@ De esa manera, podemos declarar solo las diferencias entre los modelos (con `pas {* ../../docs_src/extra_models/tutorial002_py310.py hl[7,13:14,17:18,21:22] *} -## `Union` o `anyOf` +## `Union` o `anyOf` { #union-or-anyof } Puedes declarar un response que sea la `Union` de dos o más tipos, eso significa que el response sería cualquiera de ellos. Se definirá en OpenAPI con `anyOf`. -Para hacerlo, usa el type hint estándar de Python `typing.Union`: +Para hacerlo, usa la anotación de tipos estándar de Python `typing.Union`: /// note | Nota @@ -180,22 +172,21 @@ Al definir una ```console -$ fastapi dev main.py -INFO Using path main.py -INFO Resolved absolute path /home/user/code/awesomeapp/main.py -INFO Searching for package file structure from directories with __init__.py files -INFO Importing from /home/user/code/awesomeapp +$ fastapi dev main.py - ╭─ Python module file ─╮ - │ │ - │ 🐍 main.py │ - │ │ - ╰──────────────────────╯ + FastAPI Starting development server 🚀 -INFO Importing module main -INFO Found importable FastAPI app + Searching for package file structure from directories + with __init__.py files + Importing from /home/user/code/awesomeapp - ╭─ Importable FastAPI app ─╮ - │ │ - │ from main import app │ - │ │ - ╰──────────────────────────╯ + module 🐍 main.py -INFO Using import string main:app + code Importing the FastAPI app object from the module with + the following code: - ╭────────── FastAPI CLI - Development mode ───────────╮ - │ │ - │ Serving at: http://127.0.0.1:8000 │ - │ │ - │ API docs: http://127.0.0.1:8000/docs │ - │ │ - │ Running in development mode, for production use: │ - │ │ - fastapi run - │ │ - ╰─────────────────────────────────────────────────────╯ + from main import app -INFO: Will watch for changes in these directories: ['/home/user/code/awesomeapp'] -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -INFO: Started reloader process [2265862] using WatchFiles -INFO: Started server process [2265873] -INFO: Waiting for application startup. -INFO: Application startup complete. + app Using import string: main:app + + server Server started at http://127.0.0.1:8000 + server Documentation at http://127.0.0.1:8000/docs + + tip Running in development mode, for production use: + fastapi run + + Logs: + + INFO Will watch for changes in these directories: + ['/home/user/code/awesomeapp'] + INFO Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C + to quit) + INFO Started reloader process [383138] using WatchFiles + INFO Started server process [383153] + INFO Waiting for application startup. + INFO Application startup complete. ```
@@ -64,7 +56,7 @@ INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) Esa línea muestra la URL donde tu aplicación está siendo servida, en tu máquina local. -### Compruébalo +### Compruébalo { #check-it } Abre tu navegador en http://127.0.0.1:8000. @@ -74,7 +66,7 @@ Verás el response JSON como: {"message": "Hello World"} ``` -### Documentación interactiva de la API +### Documentación interactiva de la API { #interactive-api-docs } Ahora ve a http://127.0.0.1:8000/docs. @@ -82,7 +74,7 @@ Verás la documentación interactiva automática de la API (proporcionada por http://127.0.0.1:8000/redoc. @@ -90,31 +82,31 @@ Verás la documentación alternativa automática (proporcionada por OpenAPI es una especificación que dicta cómo definir un esquema de tu API. Esta definición de esquema incluye los paths de tu API, los posibles parámetros que toman, etc. -#### Esquema de Datos +#### Esquema de Datos { #data-schema } El término "esquema" también podría referirse a la forma de algunos datos, como el contenido JSON. En ese caso, significaría los atributos del JSON, los tipos de datos que tienen, etc. -#### OpenAPI y JSON Schema +#### OpenAPI y JSON Schema { #openapi-and-json-schema } OpenAPI define un esquema de API para tu API. Y ese esquema incluye definiciones (o "esquemas") de los datos enviados y recibidos por tu API utilizando **JSON Schema**, el estándar para esquemas de datos JSON. -#### Revisa el `openapi.json` +#### Revisa el `openapi.json` { #check-the-openapi-json } Si tienes curiosidad por cómo se ve el esquema OpenAPI en bruto, FastAPI automáticamente genera un JSON (esquema) con las descripciones de toda tu API. @@ -143,7 +135,7 @@ Mostrará un JSON que empieza con algo como: ... ``` -#### Para qué sirve OpenAPI +#### Para qué sirve OpenAPI { #what-is-openapi-for } El esquema OpenAPI es lo que impulsa los dos sistemas de documentación interactiva incluidos. @@ -151,15 +143,51 @@ Y hay docenas de alternativas, todas basadas en OpenAPI. Podrías añadir fácil También podrías usarlo para generar código automáticamente, para clientes que se comuniquen con tu API. Por ejemplo, aplicaciones frontend, móviles o IoT. -## Recapitulación, paso a paso +### Despliega tu app (opcional) { #deploy-your-app-optional } -### Paso 1: importa `FastAPI` +Opcionalmente puedes desplegar tu app de FastAPI en FastAPI Cloud, ve y únete a la lista de espera si aún no lo has hecho. 🚀 -{* ../../docs_src/first_steps/tutorial001.py hl[1] *} +Si ya tienes una cuenta de **FastAPI Cloud** (te invitamos desde la lista de espera 😉), puedes desplegar tu aplicación con un solo comando. + +Antes de desplegar, asegúrate de haber iniciado sesión: + +
+ +```console +$ fastapi login + +You are logged in to FastAPI Cloud 🚀 +``` + +
+ +Luego despliega tu app: + +
+ +```console +$ fastapi deploy + +Deploying to FastAPI Cloud... + +✅ Deployment successful! + +🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev +``` + +
+ +¡Eso es todo! Ahora puedes acceder a tu app en esa URL. ✨ + +## Recapitulación, paso a paso { #recap-step-by-step } + +### Paso 1: importa `FastAPI` { #step-1-import-fastapi } + +{* ../../docs_src/first_steps/tutorial001_py39.py hl[1] *} `FastAPI` es una clase de Python que proporciona toda la funcionalidad para tu API. -/// note | Detalles Técnicos +/// note | Detalles técnicos `FastAPI` es una clase que hereda directamente de `Starlette`. @@ -167,17 +195,17 @@ Puedes usar toda la funcionalidad de FastAPI Cloud** con un solo comando: `fastapi deploy`. 🎉 + +#### Sobre FastAPI Cloud { #about-fastapi-cloud } + +**FastAPI Cloud** está construido por el mismo autor y equipo detrás de **FastAPI**. + +Agiliza el proceso de **construir**, **desplegar** y **acceder** a una API con el mínimo esfuerzo. + +Trae la misma **experiencia de desarrollador** de construir apps con FastAPI a **desplegarlas** en la nube. 🎉 + +FastAPI Cloud es el sponsor principal y proveedor de financiación para los proyectos open source de *FastAPI and friends*. ✨ + +#### Despliega en otros proveedores cloud { #deploy-to-other-cloud-providers } + +FastAPI es open source y basado en estándares. Puedes desplegar apps de FastAPI en cualquier proveedor cloud que elijas. + +Sigue las guías de tu proveedor cloud para desplegar apps de FastAPI con ellos. 🤓 + +## Recapitulación { #recap } * Importa `FastAPI`. -* Crea una instancia `app`. +* Crea una instance `app`. * Escribe un **path operation decorator** usando decoradores como `@app.get("/")`. * Define una **path operation function**; por ejemplo, `def root(): ...`. * Ejecuta el servidor de desarrollo usando el comando `fastapi dev`. +* Opcionalmente, despliega tu app con `fastapi deploy`. diff --git a/docs/es/docs/tutorial/handling-errors.md b/docs/es/docs/tutorial/handling-errors.md index 107af2a70a..71e0563203 100644 --- a/docs/es/docs/tutorial/handling-errors.md +++ b/docs/es/docs/tutorial/handling-errors.md @@ -1,4 +1,4 @@ -# Manejo de Errores +# Manejo de Errores { #handling-errors } Existen muchas situaciones en las que necesitas notificar un error a un cliente que está usando tu API. @@ -19,15 +19,15 @@ Los códigos de estado en el rango de 400 significan que hubo un error por parte ¿Recuerdas todos esos errores de **"404 Not Found"** (y chistes)? -## Usa `HTTPException` +## Usa `HTTPException` { #use-httpexception } Para devolver responses HTTP con errores al cliente, usa `HTTPException`. -### Importa `HTTPException` +### Importa `HTTPException` { #import-httpexception } -{* ../../docs_src/handling_errors/tutorial001.py hl[1] *} +{* ../../docs_src/handling_errors/tutorial001_py39.py hl[1] *} -### Lanza un `HTTPException` en tu código +### Lanza un `HTTPException` en tu código { #raise-an-httpexception-in-your-code } `HTTPException` es una excepción de Python normal con datos adicionales relevantes para APIs. @@ -39,9 +39,9 @@ El beneficio de lanzar una excepción en lugar de `return`ar un valor será más En este ejemplo, cuando el cliente solicita un ítem por un ID que no existe, lanza una excepción con un código de estado de `404`: -{* ../../docs_src/handling_errors/tutorial001.py hl[11] *} +{* ../../docs_src/handling_errors/tutorial001_py39.py hl[11] *} -### El response resultante +### El response resultante { #the-resulting-response } Si el cliente solicita `http://example.com/items/foo` (un `item_id` `"foo"`), ese cliente recibirá un código de estado HTTP de 200, y un response JSON de: @@ -69,7 +69,7 @@ Son manejados automáticamente por **FastAPI** y convertidos a JSON. /// -## Agrega headers personalizados +## Agrega headers personalizados { #add-custom-headers } Existen algunas situaciones en las que es útil poder agregar headers personalizados al error HTTP. Por ejemplo, para algunos tipos de seguridad. @@ -77,19 +77,19 @@ Probablemente no necesitarás usarlos directamente en tu código. Pero en caso de que los necesites para un escenario avanzado, puedes agregar headers personalizados: -{* ../../docs_src/handling_errors/tutorial002.py hl[14] *} +{* ../../docs_src/handling_errors/tutorial002_py39.py hl[14] *} -## Instalar manejadores de excepciones personalizados +## Instalar manejadores de excepciones personalizados { #install-custom-exception-handlers } Puedes agregar manejadores de excepciones personalizados con las mismas utilidades de excepciones de Starlette. -Supongamos que tienes una excepción personalizada `UnicornException` que tú (o un paquete que usas) podría lanzar. +Supongamos que tienes una excepción personalizada `UnicornException` que tú (o un paquete que usas) podrías lanzar. Y quieres manejar esta excepción globalmente con FastAPI. Podrías agregar un manejador de excepciones personalizado con `@app.exception_handler()`: -{* ../../docs_src/handling_errors/tutorial003.py hl[5:7,13:18,24] *} +{* ../../docs_src/handling_errors/tutorial003_py39.py hl[5:7,13:18,24] *} Aquí, si solicitas `/unicorns/yolo`, la *path operation* lanzará un `UnicornException`. @@ -109,7 +109,7 @@ También podrías usar `from starlette.requests import Request` y `from starlett /// -## Sobrescribir los manejadores de excepciones predeterminados +## Sobrescribir los manejadores de excepciones predeterminados { #override-the-default-exception-handlers } **FastAPI** tiene algunos manejadores de excepciones predeterminados. @@ -117,7 +117,7 @@ Estos manejadores se encargan de devolver los responses JSON predeterminadas cua Puedes sobrescribir estos manejadores de excepciones con los tuyos propios. -### Sobrescribir excepciones de validación de request +### Sobrescribir excepciones de validación de request { #override-request-validation-exceptions } Cuando un request contiene datos inválidos, **FastAPI** lanza internamente un `RequestValidationError`. @@ -127,7 +127,7 @@ Para sobrescribirlo, importa el `RequestValidationError` y úsalo con `@app.exce El manejador de excepciones recibirá un `Request` y la excepción. -{* ../../docs_src/handling_errors/tutorial004.py hl[2,14:16] *} +{* ../../docs_src/handling_errors/tutorial004_py39.py hl[2,14:19] *} Ahora, si vas a `/items/foo`, en lugar de obtener el error JSON por defecto con: @@ -149,36 +149,17 @@ Ahora, si vas a `/items/foo`, en lugar de obtener el error JSON por defecto con: obtendrás una versión en texto, con: ``` -1 validation error -path -> item_id - value is not a valid integer (type=type_error.integer) +Validation errors: +Field: ('path', 'item_id'), Error: Input should be a valid integer, unable to parse string as an integer ``` -#### `RequestValidationError` vs `ValidationError` - -/// warning | Advertencia - -Estos son detalles técnicos que podrías omitir si no es importante para ti en este momento. - -/// - -`RequestValidationError` es una subclase de `ValidationError` de Pydantic. - -**FastAPI** la usa para que, si usas un modelo Pydantic en `response_model`, y tus datos tienen un error, lo verás en tu log. - -Pero el cliente/usuario no lo verá. En su lugar, el cliente recibirá un "Error Interno del Servidor" con un código de estado HTTP `500`. - -Debería ser así porque si tienes un `ValidationError` de Pydantic en tu *response* o en cualquier lugar de tu código (no en el *request* del cliente), en realidad es un bug en tu código. - -Y mientras lo arreglas, tus clientes/usuarios no deberían tener acceso a información interna sobre el error, ya que eso podría exponer una vulnerabilidad de seguridad. - -### Sobrescribir el manejador de errores de `HTTPException` +### Sobrescribir el manejador de errores de `HTTPException` { #override-the-httpexception-error-handler } De la misma manera, puedes sobrescribir el manejador de `HTTPException`. Por ejemplo, podrías querer devolver un response de texto plano en lugar de JSON para estos errores: -{* ../../docs_src/handling_errors/tutorial004.py hl[3:4,9:11,22] *} +{* ../../docs_src/handling_errors/tutorial004_py39.py hl[3:4,9:11,25] *} /// note | Nota Técnica @@ -188,13 +169,21 @@ También podrías usar `from starlette.responses import PlainTextResponse`. /// -### Usar el body de `RequestValidationError` +/// warning | Advertencia + +Ten en cuenta que `RequestValidationError` contiene la información del nombre de archivo y la línea donde ocurre el error de validación, para que puedas mostrarla en tus logs con la información relevante si quieres. + +Pero eso significa que si simplemente lo conviertes a un string y devuelves esa información directamente, podrías estar filtrando un poquito de información sobre tu sistema, por eso aquí el código extrae y muestra cada error de forma independiente. + +/// + +### Usar el body de `RequestValidationError` { #use-the-requestvalidationerror-body } El `RequestValidationError` contiene el `body` que recibió con datos inválidos. Podrías usarlo mientras desarrollas tu aplicación para registrar el body y depurarlo, devolverlo al usuario, etc. -{* ../../docs_src/handling_errors/tutorial005.py hl[14] *} +{* ../../docs_src/handling_errors/tutorial005_py39.py hl[14] *} Ahora intenta enviar un ítem inválido como: @@ -226,7 +215,7 @@ Recibirás un response que te dirá que los datos son inválidos conteniendo el } ``` -#### `HTTPException` de FastAPI vs `HTTPException` de Starlette +#### `HTTPException` de FastAPI vs `HTTPException` de Starlette { #fastapis-httpexception-vs-starlettes-httpexception } **FastAPI** tiene su propio `HTTPException`. @@ -238,7 +227,7 @@ Así que puedes seguir lanzando un `HTTPException` de **FastAPI** como de costum Pero cuando registras un manejador de excepciones, deberías registrarlo para el `HTTPException` de Starlette. -De esta manera, si alguna parte del código interno de Starlette, o una extensión o complemento de Starlette, lanza un `HTTPException` de Starlette, tu manejador podrá capturarlo y manejarlo. +De esta manera, si alguna parte del código interno de Starlette, o una extensión o plug-in de Starlette, lanza un `HTTPException` de Starlette, tu manejador podrá capturarlo y manejarlo. En este ejemplo, para poder tener ambos `HTTPException` en el mismo código, las excepciones de Starlette son renombradas a `StarletteHTTPException`: @@ -246,10 +235,10 @@ En este ejemplo, para poder tener ambos `HTTPException` en el mismo código, las from starlette.exceptions import HTTPException as StarletteHTTPException ``` -### Reutilizar los manejadores de excepciones de **FastAPI** +### Reutilizar los manejadores de excepciones de **FastAPI** { #reuse-fastapis-exception-handlers } Si quieres usar la excepción junto con los mismos manejadores de excepciones predeterminados de **FastAPI**, puedes importar y reutilizar los manejadores de excepciones predeterminados de `fastapi.exception_handlers`: -{* ../../docs_src/handling_errors/tutorial006.py hl[2:5,15,21] *} +{* ../../docs_src/handling_errors/tutorial006_py39.py hl[2:5,15,21] *} En este ejemplo solo estás `print`eando el error con un mensaje muy expresivo, pero te haces una idea. Puedes usar la excepción y luego simplemente reutilizar los manejadores de excepciones predeterminados. diff --git a/docs/es/docs/tutorial/header-param-models.md b/docs/es/docs/tutorial/header-param-models.md index 3676231e67..546a8946d7 100644 --- a/docs/es/docs/tutorial/header-param-models.md +++ b/docs/es/docs/tutorial/header-param-models.md @@ -1,4 +1,4 @@ -# Modelos de Parámetros de Header +# Modelos de Parámetros de Header { #header-parameter-models } Si tienes un grupo de **parámetros de header** relacionados, puedes crear un **modelo Pydantic** para declararlos. @@ -10,7 +10,7 @@ Esto es compatible desde la versión `0.115.0` de FastAPI. 🤓 /// -## Parámetros de Header con un Modelo Pydantic +## Parámetros de Header con un Modelo Pydantic { #header-parameters-with-a-pydantic-model } Declara los **parámetros de header** que necesitas en un **modelo Pydantic**, y luego declara el parámetro como `Header`: @@ -18,7 +18,7 @@ Declara los **parámetros de header** que necesitas en un **modelo Pydantic**, y **FastAPI** **extraerá** los datos para **cada campo** de los **headers** en el request y te dará el modelo Pydantic que definiste. -## Revisa la Documentación +## Revisa la Documentación { #check-the-docs } Puedes ver los headers requeridos en la interfaz de documentación en `/docs`: @@ -26,7 +26,7 @@ Puedes ver los headers requeridos en la interfaz de documentación en `/docs`: -## Prohibir Headers Extra +## Prohibir Headers Extra { #forbid-extra-headers } En algunos casos de uso especiales (probablemente no muy comunes), podrías querer **restringir** los headers que deseas recibir. @@ -51,6 +51,22 @@ Por ejemplo, si el cliente intenta enviar un header `tool` con un valor de `plum } ``` -## Resumen +## Desactivar la conversión de guiones bajos { #disable-convert-underscores } + +De la misma forma que con los parámetros de header normales, cuando tienes caracteres de guion bajo en los nombres de los parámetros, se **convierten automáticamente en guiones**. + +Por ejemplo, si tienes un parámetro de header `save_data` en el código, el header HTTP esperado será `save-data`, y aparecerá así en la documentación. + +Si por alguna razón necesitas desactivar esta conversión automática, también puedes hacerlo para los modelos Pydantic de parámetros de header. + +{* ../../docs_src/header_param_models/tutorial003_an_py310.py hl[19] *} + +/// warning | Advertencia + +Antes de establecer `convert_underscores` a `False`, ten en cuenta que algunos proxies y servidores HTTP no permiten el uso de headers con guiones bajos. + +/// + +## Resumen { #summary } Puedes usar **modelos Pydantic** para declarar **headers** en **FastAPI**. 😎 diff --git a/docs/es/docs/tutorial/header-params.md b/docs/es/docs/tutorial/header-params.md index 9c0112bb2b..12b4d1002a 100644 --- a/docs/es/docs/tutorial/header-params.md +++ b/docs/es/docs/tutorial/header-params.md @@ -1,14 +1,14 @@ -# Parámetros de Header +# Parámetros de Header { #header-parameters } Puedes definir los parámetros de Header de la misma manera que defines los parámetros de `Query`, `Path` y `Cookie`. -## Importar `Header` +## Importar `Header` { #import-header } Primero importa `Header`: {* ../../docs_src/header_params/tutorial001_an_py310.py hl[3] *} -## Declarar parámetros de `Header` +## Declarar parámetros de `Header` { #declare-header-parameters } Luego declara los parámetros de header usando la misma estructura que con `Path`, `Query` y `Cookie`. @@ -30,7 +30,7 @@ Para declarar headers, necesitas usar `Header`, porque de otra forma los paráme /// -## Conversión automática +## Conversión automática { #automatic-conversion } `Header` tiene un poquito de funcionalidad extra además de lo que proporcionan `Path`, `Query` y `Cookie`. @@ -54,7 +54,7 @@ Antes de establecer `convert_underscores` a `False`, ten en cuenta que algunos p /// -## Headers duplicados +## Headers duplicados { #duplicate-headers } Es posible recibir headers duplicados. Eso significa, el mismo header con múltiples valores. @@ -84,7 +84,7 @@ El response sería como: } ``` -## Recapitulación +## Recapitulación { #recap } Declara headers con `Header`, usando el mismo patrón común que `Query`, `Path` y `Cookie`. diff --git a/docs/es/docs/tutorial/index.md b/docs/es/docs/tutorial/index.md index dcfc6cdfb7..7804b6854d 100644 --- a/docs/es/docs/tutorial/index.md +++ b/docs/es/docs/tutorial/index.md @@ -1,4 +1,4 @@ -# Tutorial - Guía del Usuario +# Tutorial - Guía del Usuario { #tutorial-user-guide } Este tutorial te muestra cómo usar **FastAPI** con la mayoría de sus funcionalidades, paso a paso. @@ -6,7 +6,7 @@ Cada sección se basa gradualmente en las anteriores, pero está estructurada pa También está diseñado para funcionar como una referencia futura para que puedas volver y ver exactamente lo que necesitas. -## Ejecuta el código +## Ejecuta el código { #run-the-code } Todos los bloques de código pueden ser copiados y usados directamente (de hecho, son archivos Python probados). @@ -15,48 +15,39 @@ Para ejecutar cualquiera de los ejemplos, copia el código a un archivo `main.py
```console -$ fastapi dev main.py -INFO Using path main.py -INFO Resolved absolute path /home/user/code/awesomeapp/main.py -INFO Searching for package file structure from directories with __init__.py files -INFO Importing from /home/user/code/awesomeapp +$ fastapi dev main.py - ╭─ Python module file ─╮ - │ │ - │ 🐍 main.py │ - │ │ - ╰──────────────────────╯ + FastAPI Starting development server 🚀 -INFO Importing module main -INFO Found importable FastAPI app + Searching for package file structure from directories + with __init__.py files + Importing from /home/user/code/awesomeapp - ╭─ Importable FastAPI app ─╮ - │ │ - │ from main import app │ - │ │ - ╰──────────────────────────╯ + module 🐍 main.py -INFO Using import string main:app + code Importing the FastAPI app object from the module with + the following code: - ╭────────── FastAPI CLI - Development mode ───────────╮ - │ │ - │ Serving at: http://127.0.0.1:8000 │ - │ │ - │ API docs: http://127.0.0.1:8000/docs │ - │ │ - │ Running in development mode, for production use: │ - │ │ - fastapi run - │ │ - ╰─────────────────────────────────────────────────────╯ + from main import app -INFO: Will watch for changes in these directories: ['/home/user/code/awesomeapp'] -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -INFO: Started reloader process [2265862] using WatchFiles -INFO: Started server process [2265873] -INFO: Waiting for application startup. -INFO: Application startup complete. - + app Using import string: main:app + + server Server started at http://127.0.0.1:8000 + server Documentation at http://127.0.0.1:8000/docs + + tip Running in development mode, for production use: + fastapi run + + Logs: + + INFO Will watch for changes in these directories: + ['/home/user/code/awesomeapp'] + INFO Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C + to quit) + INFO Started reloader process [383138] using WatchFiles + INFO Started server process [383153] + INFO Waiting for application startup. + INFO Application startup complete. ```
@@ -67,7 +58,7 @@ Usarlo en tu editor es lo que realmente te muestra los beneficios de FastAPI, al --- -## Instalar FastAPI +## Instalar FastAPI { #install-fastapi } El primer paso es instalar FastAPI. @@ -85,13 +76,15 @@ $ pip install "fastapi[standard]" /// note | Nota -Cuando instalas con `pip install "fastapi[standard]"` viene con algunas dependencias opcionales estándar por defecto. +Cuando instalas con `pip install "fastapi[standard]"` viene con algunas dependencias opcionales estándar por defecto, incluyendo `fastapi-cloud-cli`, que te permite hacer deploy a FastAPI Cloud. Si no quieres tener esas dependencias opcionales, en su lugar puedes instalar `pip install fastapi`. +Si quieres instalar las dependencias estándar pero sin `fastapi-cloud-cli`, puedes instalar con `pip install "fastapi[standard-no-fastapi-cloud-cli]"`. + /// -## Guía Avanzada del Usuario +## Guía Avanzada del Usuario { #advanced-user-guide } También hay una **Guía Avanzada del Usuario** que puedes leer después de esta **Tutorial - Guía del Usuario**. diff --git a/docs/es/docs/tutorial/metadata.md b/docs/es/docs/tutorial/metadata.md index 1561e4ae33..a5d9b5eadc 100644 --- a/docs/es/docs/tutorial/metadata.md +++ b/docs/es/docs/tutorial/metadata.md @@ -1,8 +1,8 @@ -# Metadata y URLs de Docs +# Metadata y URLs de Docs { #metadata-and-docs-urls } Puedes personalizar varias configuraciones de metadata en tu aplicación **FastAPI**. -## Metadata para la API +## Metadata para la API { #metadata-for-api } Puedes establecer los siguientes campos que se usan en la especificación OpenAPI y en las interfaces automáticas de documentación de la API: @@ -18,7 +18,7 @@ Puedes establecer los siguientes campos que se usan en la especificación OpenAP Puedes configurarlos de la siguiente manera: -{* ../../docs_src/metadata/tutorial001.py hl[3:16, 19:32] *} +{* ../../docs_src/metadata/tutorial001_py39.py hl[3:16, 19:32] *} /// tip | Consejo @@ -30,15 +30,15 @@ Con esta configuración, la documentación automática de la API se vería así: -## Identificador de licencia +## Identificador de licencia { #license-identifier } Desde OpenAPI 3.1.0 y FastAPI 0.99.0, también puedes establecer la `license_info` con un `identifier` en lugar de una `url`. Por ejemplo: -{* ../../docs_src/metadata/tutorial001_1.py hl[31] *} +{* ../../docs_src/metadata/tutorial001_1_py39.py hl[31] *} -## Metadata para etiquetas +## Metadata para etiquetas { #metadata-for-tags } También puedes agregar metadata adicional para las diferentes etiquetas usadas para agrupar tus path operations con el parámetro `openapi_tags`. @@ -52,13 +52,13 @@ Cada diccionario puede contener: * `description`: un `str` con una breve descripción para la documentación externa. * `url` (**requerido**): un `str` con la URL para la documentación externa. -### Crear metadata para etiquetas +### Crear metadata para etiquetas { #create-metadata-for-tags } Probemos eso en un ejemplo con etiquetas para `users` y `items`. Crea metadata para tus etiquetas y pásala al parámetro `openapi_tags`: -{* ../../docs_src/metadata/tutorial004.py hl[3:16,18] *} +{* ../../docs_src/metadata/tutorial004_py39.py hl[3:16,18] *} Nota que puedes utilizar Markdown dentro de las descripciones, por ejemplo "login" se mostrará en negrita (**login**) y "fancy" se mostrará en cursiva (_fancy_). @@ -68,11 +68,11 @@ No tienes que agregar metadata para todas las etiquetas que uses. /// -### Usar tus etiquetas +### Usar tus etiquetas { #use-your-tags } Usa el parámetro `tags` con tus *path operations* (y `APIRouter`s) para asignarlas a diferentes etiquetas: -{* ../../docs_src/metadata/tutorial004.py hl[21,26] *} +{* ../../docs_src/metadata/tutorial004_py39.py hl[21,26] *} /// info | Información @@ -80,19 +80,19 @@ Lee más sobre etiquetas en [Configuración de Path Operation](path-operation-co /// -### Revisa la documentación +### Revisa la documentación { #check-the-docs } Ahora, si revisas la documentación, mostrará toda la metadata adicional: -### Orden de las etiquetas +### Orden de las etiquetas { #order-of-tags } El orden de cada diccionario de metadata de etiqueta también define el orden mostrado en la interfaz de documentación. Por ejemplo, aunque `users` iría después de `items` en orden alfabético, se muestra antes porque agregamos su metadata como el primer diccionario en la list. -## URL de OpenAPI +## URL de OpenAPI { #openapi-url } Por defecto, el esquema OpenAPI se sirve en `/openapi.json`. @@ -100,11 +100,11 @@ Pero puedes configurarlo con el parámetro `openapi_url`. Por ejemplo, para configurarlo para que se sirva en `/api/v1/openapi.json`: -{* ../../docs_src/metadata/tutorial002.py hl[3] *} +{* ../../docs_src/metadata/tutorial002_py39.py hl[3] *} Si quieres deshabilitar el esquema OpenAPI completamente, puedes establecer `openapi_url=None`, eso también deshabilitará las interfaces de usuario de documentación que lo usan. -## URLs de Docs +## URLs de Docs { #docs-urls } Puedes configurar las dos interfaces de usuario de documentación incluidas: @@ -117,4 +117,4 @@ Puedes configurar las dos interfaces de usuario de documentación incluidas: Por ejemplo, para configurar Swagger UI para que se sirva en `/documentation` y deshabilitar ReDoc: -{* ../../docs_src/metadata/tutorial003.py hl[3] *} +{* ../../docs_src/metadata/tutorial003_py39.py hl[3] *} diff --git a/docs/es/docs/tutorial/middleware.md b/docs/es/docs/tutorial/middleware.md index c42e4eaa54..de636a4854 100644 --- a/docs/es/docs/tutorial/middleware.md +++ b/docs/es/docs/tutorial/middleware.md @@ -1,4 +1,4 @@ -# Middleware +# Middleware { #middleware } Puedes añadir middleware a las aplicaciones de **FastAPI**. @@ -15,11 +15,11 @@ Un "middleware" es una función que trabaja con cada **request** antes de que se Si tienes dependencias con `yield`, el código de salida se ejecutará *después* del middleware. -Si hubiera alguna tarea en segundo plano (documentada más adelante), se ejecutará *después* de todo el middleware. +Si hubiera tareas en segundo plano (cubiertas en la sección [Tareas en segundo plano](background-tasks.md){.internal-link target=_blank}, lo verás más adelante), se ejecutarán *después* de todo el middleware. /// -## Crear un middleware +## Crear un middleware { #create-a-middleware } Para crear un middleware usas el decorador `@app.middleware("http")` encima de una función. @@ -31,11 +31,11 @@ La función middleware recibe: * Luego devuelve la `response` generada por la correspondiente *path operation*. * Puedes entonces modificar aún más la `response` antes de devolverla. -{* ../../docs_src/middleware/tutorial001.py hl[8:9,11,14] *} +{* ../../docs_src/middleware/tutorial001_py39.py hl[8:9,11,14] *} /// tip | Consejo -Ten en cuenta que los custom proprietary headers se pueden añadir usando el prefijo 'X-'. +Ten en cuenta que los custom proprietary headers se pueden añadir usando el prefijo `X-`. Pero si tienes custom headers que deseas que un cliente en un navegador pueda ver, necesitas añadirlos a tus configuraciones de CORS ([CORS (Cross-Origin Resource Sharing)](cors.md){.internal-link target=_blank}) usando el parámetro `expose_headers` documentado en la documentación de CORS de Starlette. @@ -49,7 +49,7 @@ También podrías usar `from starlette.requests import Request`. /// -### Antes y después de la `response` +### Antes y después de la `response` { #before-and-after-the-response } Puedes añadir código que se ejecute con la `request`, antes de que cualquier *path operation* la reciba. @@ -57,7 +57,7 @@ Y también después de que se genere la `response`, antes de devolverla. Por ejemplo, podrías añadir un custom header `X-Process-Time` que contenga el tiempo en segundos que tomó procesar la request y generar una response: -{* ../../docs_src/middleware/tutorial001.py hl[10,12:13] *} +{* ../../docs_src/middleware/tutorial001_py39.py hl[10,12:13] *} /// tip | Consejo @@ -65,7 +65,30 @@ Aquí usamos -### Tags con Enums +### Tags con Enums { #tags-with-enums } Si tienes una gran aplicación, podrías terminar acumulando **varias tags**, y querrías asegurarte de que siempre uses la **misma tag** para *path operations* relacionadas. @@ -46,15 +46,15 @@ En estos casos, podría tener sentido almacenar las tags en un `Enum`. **FastAPI** soporta eso de la misma manera que con strings normales: -{* ../../docs_src/path_operation_configuration/tutorial002b.py hl[1,8:10,13,18] *} +{* ../../docs_src/path_operation_configuration/tutorial002b_py39.py hl[1,8:10,13,18] *} -## Resumen y Descripción +## Resumen y Descripción { #summary-and-description } Puedes añadir un `summary` y `description`: {* ../../docs_src/path_operation_configuration/tutorial003_py310.py hl[18:19] *} -## Descripción desde docstring +## Descripción desde docstring { #description-from-docstring } Como las descripciones tienden a ser largas y cubrir múltiples líneas, puedes declarar la descripción de la *path operation* en la docstring de la función y **FastAPI** la leerá desde allí. @@ -66,7 +66,7 @@ Será usado en la documentación interactiva: -## Descripción del Response +## Descripción del Response { #response-description } Puedes especificar la descripción del response con el parámetro `response_description`: @@ -88,11 +88,11 @@ Entonces, si no proporcionas una, **FastAPI** generará automáticamente una de -## Deprecar una *path operation* +## Deprecar una *path operation* { #deprecate-a-path-operation } Si necesitas marcar una *path operation* como deprecated, pero sin eliminarla, pasa el parámetro `deprecated`: -{* ../../docs_src/path_operation_configuration/tutorial006.py hl[16] *} +{* ../../docs_src/path_operation_configuration/tutorial006_py39.py hl[16] *} Se marcará claramente como deprecado en la documentación interactiva: @@ -102,6 +102,6 @@ Revisa cómo lucen las *path operations* deprecadas y no deprecadas: -## Resumen +## Resumen { #recap } Puedes configurar y añadir metadatos a tus *path operations* fácilmente pasando parámetros a los *path operation decorators*. diff --git a/docs/es/docs/tutorial/path-params-numeric-validations.md b/docs/es/docs/tutorial/path-params-numeric-validations.md index 4ea01753b4..569dd03dd0 100644 --- a/docs/es/docs/tutorial/path-params-numeric-validations.md +++ b/docs/es/docs/tutorial/path-params-numeric-validations.md @@ -1,8 +1,8 @@ -# Parámetros de Path y Validaciones Numéricas +# Parámetros de Path y Validaciones Numéricas { #path-parameters-and-numeric-validations } De la misma manera que puedes declarar más validaciones y metadatos para los parámetros de query con `Query`, puedes declarar el mismo tipo de validaciones y metadatos para los parámetros de path con `Path`. -## Importar Path +## Importar Path { #import-path } Primero, importa `Path` de `fastapi`, e importa `Annotated`: @@ -18,7 +18,7 @@ Asegúrate de [Actualizar la versión de FastAPI](../deployment/versions.md#upgr /// -## Declarar metadatos +## Declarar metadatos { #declare-metadata } Puedes declarar todos los mismos parámetros que para `Query`. @@ -32,7 +32,7 @@ Un parámetro de path siempre es requerido ya que tiene que formar parte del pat /// -## Ordena los parámetros como necesites +## Ordena los parámetros como necesites { #order-the-parameters-as-you-need } /// tip | Consejo @@ -54,13 +54,13 @@ No importa para **FastAPI**. Detectará los parámetros por sus nombres, tipos y Así que puedes declarar tu función como: -{* ../../docs_src/path_params_numeric_validations/tutorial002.py hl[7] *} +{* ../../docs_src/path_params_numeric_validations/tutorial002_py39.py hl[7] *} Pero ten en cuenta que si usas `Annotated`, no tendrás este problema, no importará ya que no estás usando los valores por defecto de los parámetros de la función para `Query()` o `Path()`. {* ../../docs_src/path_params_numeric_validations/tutorial002_an_py39.py *} -## Ordena los parámetros como necesites, trucos +## Ordena los parámetros como necesites, trucos { #order-the-parameters-as-you-need-tricks } /// tip | Consejo @@ -83,15 +83,15 @@ Pasa `*`, como el primer parámetro de la función. Python no hará nada con ese `*`, pero sabrá que todos los parámetros siguientes deben ser llamados como argumentos de palabras clave (parejas key-value), también conocidos como kwargs. Incluso si no tienen un valor por defecto. -{* ../../docs_src/path_params_numeric_validations/tutorial003.py hl[7] *} +{* ../../docs_src/path_params_numeric_validations/tutorial003_py39.py hl[7] *} -### Mejor con `Annotated` +### Mejor con `Annotated` { #better-with-annotated } Ten en cuenta que si usas `Annotated`, como no estás usando valores por defecto de los parámetros de la función, no tendrás este problema y probablemente no necesitarás usar `*`. {* ../../docs_src/path_params_numeric_validations/tutorial003_an_py39.py hl[10] *} -## Validaciones numéricas: mayor o igual +## Validaciones numéricas: mayor o igual { #number-validations-greater-than-or-equal } Con `Query` y `Path` (y otros que verás más adelante) puedes declarar restricciones numéricas. @@ -99,7 +99,7 @@ Aquí, con `ge=1`, `item_id` necesitará ser un número entero "`g`reater than o {* ../../docs_src/path_params_numeric_validations/tutorial004_an_py39.py hl[10] *} -## Validaciones numéricas: mayor que y menor o igual +## Validaciones numéricas: mayor que y menor o igual { #number-validations-greater-than-and-less-than-or-equal } Lo mismo aplica para: @@ -108,19 +108,19 @@ Lo mismo aplica para: {* ../../docs_src/path_params_numeric_validations/tutorial005_an_py39.py hl[10] *} -## Validaciones numéricas: flotantes, mayor y menor +## Validaciones numéricas: flotantes, mayor y menor { #number-validations-floats-greater-than-and-less-than } Las validaciones numéricas también funcionan para valores `float`. -Aquí es donde se convierte en importante poder declarar gt y no solo ge. Ya que con esto puedes requerir, por ejemplo, que un valor sea mayor que `0`, incluso si es menor que `1`. +Aquí es donde se convierte en importante poder declarar gt y no solo ge. Ya que con esto puedes requerir, por ejemplo, que un valor sea mayor que `0`, incluso si es menor que `1`. Así, `0.5` sería un valor válido. Pero `0.0` o `0` no lo serían. -Y lo mismo para lt. +Y lo mismo para lt. {* ../../docs_src/path_params_numeric_validations/tutorial006_an_py39.py hl[13] *} -## Resumen +## Resumen { #recap } Con `Query`, `Path` (y otros que aún no has visto) puedes declarar metadatos y validaciones de string de las mismas maneras que con [Parámetros de Query y Validaciones de String](query-params-str-validations.md){.internal-link target=_blank}. @@ -139,7 +139,7 @@ Todas ellas comparten los mismos parámetros para validación adicional y metada /// -/// note | Nota técnica +/// note | Detalles técnicos Cuando importas `Query`, `Path` y otros de `fastapi`, en realidad son funciones. diff --git a/docs/es/docs/tutorial/path-params.md b/docs/es/docs/tutorial/path-params.md index 4262809026..7ba49f3b0b 100644 --- a/docs/es/docs/tutorial/path-params.md +++ b/docs/es/docs/tutorial/path-params.md @@ -1,8 +1,8 @@ -# Parámetros de Path +# Parámetros de Path { #path-parameters } Puedes declarar "parámetros" o "variables" de path con la misma sintaxis que se usa en los format strings de Python: -{* ../../docs_src/path_params/tutorial001.py hl[6:7] *} +{* ../../docs_src/path_params/tutorial001_py39.py hl[6:7] *} El valor del parámetro de path `item_id` se pasará a tu función como el argumento `item_id`. @@ -12,11 +12,11 @@ Así que, si ejecutas este ejemplo y vas a Conversión de datos { #data-conversion } Si ejecutas este ejemplo y abres tu navegador en http://127.0.0.1:8000/items/3, verás un response de: @@ -38,11 +38,11 @@ Si ejecutas este ejemplo y abres tu navegador en "parsing" automático de requests. +Entonces, con esa declaración de tipo, **FastAPI** te ofrece "parsing" automático de request. /// -## Validación de datos +## Validación de datos { #data-validation } Pero si vas al navegador en http://127.0.0.1:8000/items/foo, verás un bonito error HTTP de: @@ -76,7 +76,7 @@ Esto es increíblemente útil mientras desarrollas y depuras código que interac /// -## Documentación +## Documentación { #documentation } Y cuando abras tu navegador en http://127.0.0.1:8000/docs, verás una documentación de API automática e interactiva como: @@ -90,7 +90,7 @@ Nota que el parámetro de path está declarado como un entero. /// -## Beneficios basados en estándares, documentación alternativa +## Beneficios basados en estándares, documentación alternativa { #standards-based-benefits-alternative-documentation } Y porque el esquema generado es del estándar OpenAPI, hay muchas herramientas compatibles. @@ -100,7 +100,7 @@ Debido a esto, el propio **FastAPI** proporciona una documentación de API alter De la misma manera, hay muchas herramientas compatibles. Incluyendo herramientas de generación de código para muchos lenguajes. -## Pydantic +## Pydantic { #pydantic } Toda la validación de datos se realiza internamente con Pydantic, así que obtienes todos los beneficios de esta. Y sabes que estás en buenas manos. @@ -108,7 +108,7 @@ Puedes usar las mismas declaraciones de tipo con `str`, `float`, `bool` y muchos Varios de estos se exploran en los siguientes capítulos del tutorial. -## El orden importa +## El orden importa { #order-matters } Al crear *path operations*, puedes encontrarte en situaciones donde tienes un path fijo. @@ -118,21 +118,21 @@ Y luego también puedes tener un path `/users/{user_id}` para obtener datos sobr Debido a que las *path operations* se evalúan en orden, necesitas asegurarte de que el path para `/users/me` se declara antes que el de `/users/{user_id}`: -{* ../../docs_src/path_params/tutorial003.py hl[6,11] *} +{* ../../docs_src/path_params/tutorial003_py39.py hl[6,11] *} De lo contrario, el path para `/users/{user_id}` también coincidiría para `/users/me`, "pensando" que está recibiendo un parámetro `user_id` con un valor de `"me"`. De manera similar, no puedes redefinir una path operation: -{* ../../docs_src/path_params/tutorial003b.py hl[6,11] *} +{* ../../docs_src/path_params/tutorial003b_py39.py hl[6,11] *} La primera siempre será utilizada ya que el path coincide primero. -## Valores predefinidos +## Valores predefinidos { #predefined-values } -Si tienes una *path operation* que recibe un *path parameter*, pero quieres que los valores posibles válidos del *path parameter* estén predefinidos, puedes usar un `Enum` estándar de Python. +Si tienes una *path operation* que recibe un *path parameter*, pero quieres que los valores posibles válidos del *path parameter* estén predefinidos, puedes usar un `Enum` estándar de Python. -### Crear una clase `Enum` +### Crear una clase `Enum` { #create-an-enum-class } Importa `Enum` y crea una subclase que herede de `str` y de `Enum`. @@ -140,13 +140,7 @@ Al heredar de `str`, la documentación de la API podrá saber que los valores de Luego crea atributos de clase con valores fijos, que serán los valores válidos disponibles: -{* ../../docs_src/path_params/tutorial005.py hl[1,6:9] *} - -/// info | Información - -Las enumeraciones (o enums) están disponibles en Python desde la versión 3.4. - -/// +{* ../../docs_src/path_params/tutorial005_py39.py hl[1,6:9] *} /// tip | Consejo @@ -154,33 +148,33 @@ Si te estás preguntando, "AlexNet", "ResNet" y "LeNet" son solo nombres de -### Trabajando con *enumeraciones* de Python +### Trabajando con *enumeraciones* de Python { #working-with-python-enumerations } El valor del *path parameter* será un *miembro* de enumeración. -#### Comparar *miembros* de enumeraciones +#### Comparar *miembros* de enumeraciones { #compare-enumeration-members } Puedes compararlo con el *miembro* de enumeración en tu enum creada `ModelName`: -{* ../../docs_src/path_params/tutorial005.py hl[17] *} +{* ../../docs_src/path_params/tutorial005_py39.py hl[17] *} -#### Obtener el valor de *enumeración* +#### Obtener el valor de *enumeración* { #get-the-enumeration-value } Puedes obtener el valor actual (un `str` en este caso) usando `model_name.value`, o en general, `your_enum_member.value`: -{* ../../docs_src/path_params/tutorial005.py hl[20] *} +{* ../../docs_src/path_params/tutorial005_py39.py hl[20] *} /// tip | Consejo @@ -188,13 +182,13 @@ También podrías acceder al valor `"lenet"` con `ModelName.lenet.value`. /// -#### Devolver *miembros* de enumeración +#### Devolver *miembros* de enumeración { #return-enumeration-members } Puedes devolver *miembros de enum* desde tu *path operation*, incluso anidados en un cuerpo JSON (por ejemplo, un `dict`). Serán convertidos a sus valores correspondientes (cadenas en este caso) antes de devolverlos al cliente: -{* ../../docs_src/path_params/tutorial005.py hl[18,21,23] *} +{* ../../docs_src/path_params/tutorial005_py39.py hl[18,21,23] *} En tu cliente recibirás un response JSON como: @@ -205,7 +199,7 @@ En tu cliente recibirás un response JSON como: } ``` -## Parámetros de path conteniendo paths +## Parámetros de path conteniendo paths { #path-parameters-containing-paths } Imaginemos que tienes una *path operation* con un path `/files/{file_path}`. @@ -213,7 +207,7 @@ Pero necesitas que `file_path` en sí mismo contenga un *path*, como `home/johnd Entonces, la URL para ese archivo sería algo como: `/files/home/johndoe/myfile.txt`. -### Soporte de OpenAPI +### Soporte de OpenAPI { #openapi-support } OpenAPI no soporta una manera de declarar un *path parameter* para que contenga un *path* dentro, ya que eso podría llevar a escenarios que son difíciles de probar y definir. @@ -221,7 +215,7 @@ Sin embargo, todavía puedes hacerlo en **FastAPI**, usando una de las herramien Y la documentación seguiría funcionando, aunque no agregue ninguna documentación indicando que el parámetro debe contener un path. -### Convertidor de Path +### Convertidor de Path { #path-convertor } Usando una opción directamente de Starlette puedes declarar un *path parameter* conteniendo un *path* usando una URL como: @@ -233,7 +227,7 @@ En este caso, el nombre del parámetro es `file_path`, y la última parte, `:pat Así que, puedes usarlo con: -{* ../../docs_src/path_params/tutorial004.py hl[6] *} +{* ../../docs_src/path_params/tutorial004_py39.py hl[6] *} /// tip | Consejo @@ -243,12 +237,12 @@ En ese caso, la URL sería: `/files//home/johndoe/myfile.txt`, con una doble bar /// -## Resumen +## Resumen { #recap } Con **FastAPI**, al usar declaraciones de tipo estándar de Python, cortas e intuitivas, obtienes: * Soporte del editor: chequeo de errores, autocompletado, etc. -* "parsing" de datos +* "parsing" de datos * Validación de datos * Anotación de API y documentación automática diff --git a/docs/es/docs/tutorial/query-param-models.md b/docs/es/docs/tutorial/query-param-models.md index 8338fc358c..e335cfe61e 100644 --- a/docs/es/docs/tutorial/query-param-models.md +++ b/docs/es/docs/tutorial/query-param-models.md @@ -1,4 +1,4 @@ -# Modelos de Parámetros Query +# Modelos de Parámetros Query { #query-parameter-models } Si tienes un grupo de **parámetros query** que están relacionados, puedes crear un **modelo de Pydantic** para declararlos. @@ -10,7 +10,7 @@ Esto es compatible desde la versión `0.115.0` de FastAPI. 🤓 /// -## Parámetros Query con un Modelo Pydantic +## Parámetros Query con un Modelo Pydantic { #query-parameters-with-a-pydantic-model } Declara los **parámetros query** que necesitas en un **modelo de Pydantic**, y luego declara el parámetro como `Query`: @@ -18,7 +18,7 @@ Declara los **parámetros query** que necesitas en un **modelo de Pydantic**, y **FastAPI** **extraerá** los datos para **cada campo** de los **parámetros query** en el request y te proporcionará el modelo de Pydantic que definiste. -## Revisa la Documentación +## Revisa la Documentación { #check-the-docs } Puedes ver los parámetros query en la UI de documentación en `/docs`: @@ -26,7 +26,7 @@ Puedes ver los parámetros query en la UI de documentación en `/docs`: -## Prohibir Parámetros Query Extras +## Prohibir Parámetros Query Extras { #forbid-extra-query-parameters } En algunos casos de uso especiales (probablemente no muy comunes), podrías querer **restringir** los parámetros query que deseas recibir. @@ -57,7 +57,7 @@ Recibirán un response de **error** que les indica que el parámetro query `tool } ``` -## Resumen +## Resumen { #summary } Puedes usar **modelos de Pydantic** para declarar **parámetros query** en **FastAPI**. 😎 diff --git a/docs/es/docs/tutorial/query-params-str-validations.md b/docs/es/docs/tutorial/query-params-str-validations.md index 9cb76156f0..4af4782f83 100644 --- a/docs/es/docs/tutorial/query-params-str-validations.md +++ b/docs/es/docs/tutorial/query-params-str-validations.md @@ -1,4 +1,4 @@ -# Parámetros de Query y Validaciones de String +# Parámetros de Query y Validaciones de String { #query-parameters-and-string-validations } **FastAPI** te permite declarar información adicional y validación para tus parámetros. @@ -6,48 +6,28 @@ Tomemos esta aplicación como ejemplo: {* ../../docs_src/query_params_str_validations/tutorial001_py310.py hl[7] *} -El parámetro de query `q` es del tipo `Union[str, None]` (o `str | None` en Python 3.10), lo que significa que es de tipo `str` pero también podría ser `None`, y de hecho, el valor por defecto es `None`, así que FastAPI sabrá que no es requerido. +El parámetro de query `q` es de tipo `str | None`, lo que significa que es de tipo `str` pero también podría ser `None`, y de hecho, el valor por defecto es `None`, así que FastAPI sabrá que no es requerido. /// note | Nota FastAPI sabrá que el valor de `q` no es requerido por el valor por defecto `= None`. -El `Union` en `Union[str, None]` permitirá a tu editor darte un mejor soporte y detectar errores. +Tener `str | None` permitirá que tu editor te dé un mejor soporte y detecte errores. /// -## Validaciones adicionales +## Validaciones adicionales { #additional-validation } Vamos a hacer que, aunque `q` sea opcional, siempre que se proporcione, **su longitud no exceda los 50 caracteres**. -### Importar `Query` y `Annotated` +### Importar `Query` y `Annotated` { #import-query-and-annotated } Para lograr eso, primero importa: * `Query` desde `fastapi` -* `Annotated` desde `typing` (o desde `typing_extensions` en Python por debajo de 3.9) +* `Annotated` desde `typing` -//// tab | Python 3.10+ - -En Python 3.9 o superior, `Annotated` es parte de la biblioteca estándar, así que puedes importarlo desde `typing`. - -```Python hl_lines="1 3" -{!> ../../docs_src/query_params_str_validations/tutorial002_an_py310.py!} -``` - -//// - -//// tab | Python 3.8+ - -En versiones de Python por debajo de 3.9 importas `Annotated` desde `typing_extensions`. - -Ya estará instalado con FastAPI. - -```Python hl_lines="3-4" -{!> ../../docs_src/query_params_str_validations/tutorial002_an.py!} -``` - -//// +{* ../../docs_src/query_params_str_validations/tutorial002_an_py310.py hl[1,3] *} /// info | Información @@ -59,9 +39,9 @@ Asegúrate de [Actualizar la versión de FastAPI](../deployment/versions.md#upgr /// -## Usar `Annotated` en el tipo del parámetro `q` +## Usar `Annotated` en el tipo del parámetro `q` { #use-annotated-in-the-type-for-the-q-parameter } -¿Recuerdas que te dije antes que `Annotated` puede ser usado para agregar metadatos a tus parámetros en la [Introducción a Tipos de Python](../python-types.md#type-hints-with-metadata-annotations){.internal-link target=_blank}? +¿Recuerdas que te dije antes que `Annotated` puede usarse para agregar metadatos a tus parámetros en la [Introducción a Tipos de Python](../python-types.md#type-hints-with-metadata-annotations){.internal-link target=_blank}? Ahora es el momento de usarlo con FastAPI. 🚀 @@ -75,7 +55,7 @@ q: str | None = None //// -//// tab | Python 3.8+ +//// tab | Python 3.9+ ```Python q: Union[str, None] = None @@ -93,7 +73,7 @@ q: Annotated[str | None] = None //// -//// tab | Python 3.8+ +//// tab | Python 3.9+ ```Python q: Annotated[Union[str, None]] = None @@ -105,7 +85,7 @@ Ambas versiones significan lo mismo, `q` es un parámetro que puede ser un `str` Ahora vamos a lo divertido. 🎉 -## Agregar `Query` a `Annotated` en el parámetro `q` +## Agregar `Query` a `Annotated` en el parámetro `q` { #add-query-to-annotated-in-the-q-parameter } Ahora que tenemos este `Annotated` donde podemos poner más información (en este caso algunas validaciones adicionales), agrega `Query` dentro de `Annotated`, y establece el parámetro `max_length` a `50`: @@ -127,9 +107,9 @@ FastAPI ahora: * Mostrará un **error claro** para el cliente cuando los datos no sean válidos * **Documentará** el parámetro en el OpenAPI esquema *path operation* (así aparecerá en la **UI de documentación automática**) -## Alternativa (antigua): `Query` como valor por defecto +## Alternativa (antigua): `Query` como valor por defecto { #alternative-old-query-as-the-default-value } -Versiones anteriores de FastAPI (antes de 0.95.0) requerían que usaras `Query` como el valor por defecto de tu parámetro, en lugar de ponerlo en `Annotated`. Hay una alta probabilidad de que veas código usándolo alrededor, así que te lo explicaré. +Versiones anteriores de FastAPI (antes de 0.95.0) requerían que usaras `Query` como el valor por defecto de tu parámetro, en lugar de ponerlo en `Annotated`, hay una alta probabilidad de que veas código usándolo alrededor, así que te lo explicaré. /// tip | Consejo @@ -141,63 +121,32 @@ Así es como usarías `Query()` como el valor por defecto de tu parámetro de fu {* ../../docs_src/query_params_str_validations/tutorial002_py310.py hl[7] *} -Ya que en este caso (sin usar `Annotated`) debemos reemplazar el valor por defecto `None` en la función con `Query()`, ahora necesitamos establecer el valor por defecto con el parámetro `Query(default=None)`, esto sirve al mismo propósito de definir ese valor por defecto (al menos para FastAPI). +Como en este caso (sin usar `Annotated`) debemos reemplazar el valor por defecto `None` en la función con `Query()`, ahora necesitamos establecer el valor por defecto con el parámetro `Query(default=None)`, esto sirve al mismo propósito de definir ese valor por defecto (al menos para FastAPI). Entonces: -```Python -q: Union[str, None] = Query(default=None) -``` - -...hace que el parámetro sea opcional, con un valor por defecto de `None`, lo mismo que: - -```Python -q: Union[str, None] = None -``` - -Y en Python 3.10 y superior: - ```Python q: str | None = Query(default=None) ``` ...hace que el parámetro sea opcional, con un valor por defecto de `None`, lo mismo que: + ```Python q: str | None = None ``` -Pero las versiones de `Query` lo declaran explícitamente como un parámetro de query. - -/// info | Información - -Ten en cuenta que la parte más importante para hacer un parámetro opcional es la parte: - -```Python -= None -``` - -o la parte: - -```Python -= Query(default=None) -``` - -ya que usará ese `None` como el valor por defecto, y de esa manera hará el parámetro **no requerido**. - -La parte `Union[str, None]` permite que tu editor brinde un mejor soporte, pero no es lo que le dice a FastAPI que este parámetro no es requerido. - -/// +Pero la versión con `Query` lo declara explícitamente como un parámetro de query. Luego, podemos pasar más parámetros a `Query`. En este caso, el parámetro `max_length` que se aplica a los strings: ```Python -q: Union[str, None] = Query(default=None, max_length=50) +q: str | None = Query(default=None, max_length=50) ``` -Esto validará los datos, mostrará un error claro cuando los datos no sean válidos, y documentará el parámetro en el esquema del *path operation* de OpenaPI. +Esto validará los datos, mostrará un error claro cuando los datos no sean válidos, y documentará el parámetro en el esquema del *path operation* de OpenAPI. -### `Query` como valor por defecto o en `Annotated` +### `Query` como valor por defecto o en `Annotated` { #query-as-the-default-value-or-in-annotated } Ten en cuenta que cuando uses `Query` dentro de `Annotated` no puedes usar el parámetro `default` para `Query`. @@ -217,13 +166,13 @@ Así que utilizarías (preferentemente): q: Annotated[str, Query()] = "rick" ``` -...o en code bases más antiguos encontrarás: +...o en code bases más antiguas encontrarás: ```Python q: str = Query(default="rick") ``` -### Ventajas de `Annotated` +### Ventajas de `Annotated` { #advantages-of-annotated } **Usar `Annotated` es recomendado** en lugar del valor por defecto en los parámetros de función, es **mejor** por múltiples razones. 🤓 @@ -235,13 +184,13 @@ Cuando no usas `Annotated` y en su lugar usas el estilo de valor por defecto **( Dado que `Annotated` puede tener más de una anotación de metadato, ahora podrías incluso usar la misma función con otras herramientas, como Typer. 🚀 -## Agregar más validaciones +## Agregar más validaciones { #add-more-validations } También puedes agregar un parámetro `min_length`: {* ../../docs_src/query_params_str_validations/tutorial003_an_py310.py hl[10] *} -## Agregar expresiones regulares +## Agregar expresiones regulares { #add-regular-expressions } Puedes definir una expresión regular `pattern` que el parámetro debe coincidir: @@ -249,29 +198,15 @@ Puedes definir una Campos requeridos. - -/// - -/// tip | Consejo - -Recuerda que en la mayoría de los casos, cuando algo es requerido, puedes simplemente omitir el default, así que normalmente no tienes que usar `...`. - -/// - -## Lista de parámetros de Query / múltiples valores +## Lista de parámetros de Query / múltiples valores { #query-parameter-list-multiple-values } Cuando defines un parámetro de query explícitamente con `Query` también puedes declararlo para recibir una lista de valores, o dicho de otra manera, para recibir múltiples valores. @@ -355,7 +266,7 @@ Entonces, con una URL como: http://localhost:8000/items/?q=foo&q=bar ``` -recibirías los múltiples valores del *query parameter* `q` (`foo` y `bar`) en una `list` de Python dentro de tu *path operation function*, en el *parámetro de función* `q`. +recibirías los múltiples valores de los *query parameters* `q` (`foo` y `bar`) en una `list` de Python dentro de tu *path operation function*, en el *parámetro de función* `q`. Entonces, el response a esa URL sería: @@ -378,9 +289,9 @@ La documentación interactiva de API se actualizará en consecuencia, para permi -### Lista de parámetros de Query / múltiples valores con valores por defecto +### Lista de parámetros de Query / múltiples valores con valores por defecto { #query-parameter-list-multiple-values-with-defaults } -Y también puedes definir un valor por defecto `list` de valores si no se proporcionan ninguno: +También puedes definir un valor por defecto `list` de valores si no se proporciona ninguno: {* ../../docs_src/query_params_str_validations/tutorial012_an_py39.py hl[9] *} @@ -401,9 +312,9 @@ el valor por defecto de `q` será: `["foo", "bar"]` y tu response será: } ``` -#### Usando solo `list` +#### Usando solo `list` { #using-just-list } -También puedes usar `list` directamente en lugar de `List[str]` (o `list[str]` en Python 3.9+): +También puedes usar `list` directamente en lugar de `list[str]`: {* ../../docs_src/query_params_str_validations/tutorial013_an_py39.py hl[9] *} @@ -411,11 +322,11 @@ También puedes usar `list` directamente en lugar de `List[str]` (o `list[str]` Ten en cuenta que en este caso, FastAPI no comprobará el contenido de la lista. -Por ejemplo, `List[int]` comprobaría (y documentaría) que el contenido de la lista son enteros. Pero `list` sola no lo haría. +Por ejemplo, `list[int]` comprobaría (y documentaría) que el contenido de la lista son enteros. Pero `list` sola no lo haría. /// -## Declarar más metadatos +## Declarar más metadatos { #declare-more-metadata } Puedes agregar más información sobre el parámetro. @@ -437,7 +348,7 @@ Y una `description`: {* ../../docs_src/query_params_str_validations/tutorial008_an_py310.py hl[14] *} -## Alias para parámetros +## Alias para parámetros { #alias-parameters } Imagina que quieres que el parámetro sea `item-query`. @@ -457,11 +368,11 @@ Entonces puedes declarar un `alias`, y ese alias será usado para encontrar el v {* ../../docs_src/query_params_str_validations/tutorial009_an_py310.py hl[9] *} -## Declarar parámetros obsoletos +## Declarar parámetros obsoletos { #deprecating-parameters } Ahora digamos que ya no te gusta este parámetro. -Tienes que dejarlo allí por un tiempo porque hay clientes usándolo, pero quieres que la documentación lo muestre claramente como deprecated. +Tienes que dejarlo allí por un tiempo porque hay clientes usándolo, pero quieres que la documentación lo muestre claramente como deprecated. Luego pasa el parámetro `deprecated=True` a `Query`: @@ -471,13 +382,75 @@ La documentación lo mostrará así: -## Excluir parámetros de OpenAPI +## Excluir parámetros de OpenAPI { #exclude-parameters-from-openapi } Para excluir un parámetro de query del esquema de OpenAPI generado (y por lo tanto, de los sistemas de documentación automática), establece el parámetro `include_in_schema` de `Query` a `False`: {* ../../docs_src/query_params_str_validations/tutorial014_an_py310.py hl[10] *} -## Recapitulación +## Validación personalizada { #custom-validation } + +Podría haber casos donde necesites hacer alguna **validación personalizada** que no puede hacerse con los parámetros mostrados arriba. + +En esos casos, puedes usar una **función validadora personalizada** que se aplique después de la validación normal (por ejemplo, después de validar que el valor es un `str`). + +Puedes lograr eso usando `AfterValidator` de Pydantic dentro de `Annotated`. + +/// tip | Consejo + +Pydantic también tiene `BeforeValidator` y otros. 🤓 + +/// + +Por ejemplo, este validador personalizado comprueba que el ID del ítem empiece con `isbn-` para un número de libro ISBN o con `imdb-` para un ID de URL de película de IMDB: + +{* ../../docs_src/query_params_str_validations/tutorial015_an_py310.py hl[5,16:19,24] *} + +/// info | Información + +Esto está disponible con Pydantic versión 2 o superior. 😎 + +/// + +/// tip | Consejo + +Si necesitas hacer cualquier tipo de validación que requiera comunicarte con algún **componente externo**, como una base de datos u otra API, deberías usar **Dependencias de FastAPI**, las aprenderás más adelante. + +Estos validadores personalizados son para cosas que pueden comprobarse **solo** con los **mismos datos** provistos en el request. + +/// + +### Entiende ese código { #understand-that-code } + +El punto importante es solo usar **`AfterValidator` con una función dentro de `Annotated`**. Si quieres, sáltate esta parte. 🤸 + +--- + +Pero si te da curiosidad este ejemplo de código específico y sigues entretenido, aquí tienes algunos detalles extra. + +#### String con `value.startswith()` { #string-with-value-startswith } + +¿Lo notaste? un string usando `value.startswith()` puede recibir una tupla, y comprobará cada valor en la tupla: + +{* ../../docs_src/query_params_str_validations/tutorial015_an_py310.py ln[16:19] hl[17] *} + +#### Un ítem aleatorio { #a-random-item } + +Con `data.items()` obtenemos un objeto iterable con tuplas que contienen la clave y el valor para cada elemento del diccionario. + +Convertimos este objeto iterable en una `list` propiamente dicha con `list(data.items())`. + +Luego con `random.choice()` podemos obtener un **valor aleatorio** de la lista, así que obtenemos una tupla con `(id, name)`. Será algo como `("imdb-tt0371724", "The Hitchhiker's Guide to the Galaxy")`. + +Luego **asignamos esos dos valores** de la tupla a las variables `id` y `name`. + +Así, si el usuario no proporcionó un ID de ítem, aún recibirá una sugerencia aleatoria. + +...hacemos todo esto en una **sola línea simple**. 🤯 ¿No te encanta Python? 🐍 + +{* ../../docs_src/query_params_str_validations/tutorial015_an_py310.py ln[22:30] hl[29] *} + +## Recapitulación { #recap } Puedes declarar validaciones y metadatos adicionales para tus parámetros. @@ -494,6 +467,8 @@ Validaciones específicas para strings: * `max_length` * `pattern` +Validaciones personalizadas usando `AfterValidator`. + En estos ejemplos viste cómo declarar validaciones para valores de tipo `str`. Mira los siguientes capítulos para aprender cómo declarar validaciones para otros tipos, como números. diff --git a/docs/es/docs/tutorial/query-params.md b/docs/es/docs/tutorial/query-params.md index 9bd47f8714..2b58a2b4b6 100644 --- a/docs/es/docs/tutorial/query-params.md +++ b/docs/es/docs/tutorial/query-params.md @@ -1,8 +1,8 @@ -# Parámetros de Query +# Parámetros de Query { #query-parameters } Cuando declaras otros parámetros de función que no son parte de los parámetros de path, son automáticamente interpretados como parámetros de "query". -{* ../../docs_src/query_params/tutorial001.py hl[9] *} +{* ../../docs_src/query_params/tutorial001_py39.py hl[9] *} La query es el conjunto de pares clave-valor que van después del `?` en una URL, separados por caracteres `&`. @@ -24,11 +24,11 @@ Pero cuando los declaras con tipos de Python (en el ejemplo anterior, como `int` Todo el mismo proceso que se aplica para los parámetros de path también se aplica para los parámetros de query: * Soporte del editor (obviamente) -* "Parsing" de datos +* "parsing" de datos * Validación de datos * Documentación automática -## Valores por defecto +## Valores por defecto { #defaults } Como los parámetros de query no son una parte fija de un path, pueden ser opcionales y pueden tener valores por defecto. @@ -57,19 +57,21 @@ Los valores de los parámetros en tu función serán: * `skip=20`: porque lo configuraste en la URL * `limit=10`: porque ese era el valor por defecto -## Parámetros opcionales +## Parámetros opcionales { #optional-parameters } De la misma manera, puedes declarar parámetros de query opcionales, estableciendo su valor por defecto en `None`: {* ../../docs_src/query_params/tutorial002_py310.py hl[7] *} +En este caso, el parámetro de función `q` será opcional y será `None` por defecto. + /// check | Revisa Además, nota que **FastAPI** es lo suficientemente inteligente para notar que el parámetro de path `item_id` es un parámetro de path y `q` no lo es, por lo tanto, es un parámetro de query. /// -## Conversión de tipos en parámetros de query +## Conversión de tipos en parámetros de query { #query-parameter-type-conversion } También puedes declarar tipos `bool`, y serán convertidos: @@ -107,7 +109,7 @@ http://127.0.0.1:8000/items/foo?short=yes o cualquier otra variación (mayúsculas, primera letra en mayúscula, etc.), tu función verá el parámetro `short` con un valor `bool` de `True`. De lo contrario, será `False`. -## Múltiples parámetros de path y de query +## Múltiples parámetros de path y de query { #multiple-path-and-query-parameters } Puedes declarar múltiples parámetros de path y de query al mismo tiempo, **FastAPI** sabe cuál es cuál. @@ -117,7 +119,7 @@ Serán detectados por nombre: {* ../../docs_src/query_params/tutorial004_py310.py hl[6,8] *} -## Parámetros de query requeridos +## Parámetros de query requeridos { #required-query-parameters } Cuando declaras un valor por defecto para parámetros que no son de path (por ahora, solo hemos visto parámetros de query), entonces no es requerido. @@ -125,7 +127,7 @@ Si no quieres agregar un valor específico pero solo hacer que sea opcional, est Pero cuando quieres hacer un parámetro de query requerido, simplemente no declares ningún valor por defecto: -{* ../../docs_src/query_params/tutorial005.py hl[6:7] *} +{* ../../docs_src/query_params/tutorial005_py39.py hl[6:7] *} Aquí el parámetro de query `needy` es un parámetro de query requerido de tipo `str`. diff --git a/docs/es/docs/tutorial/request-files.md b/docs/es/docs/tutorial/request-files.md index 330523c7ef..cc99deb2e3 100644 --- a/docs/es/docs/tutorial/request-files.md +++ b/docs/es/docs/tutorial/request-files.md @@ -1,4 +1,4 @@ -# Archivos de Request +# Archivos de Request { #request-files } Puedes definir archivos que serán subidos por el cliente utilizando `File`. @@ -16,13 +16,13 @@ Esto es porque los archivos subidos se envían como "form data". /// -## Importar `File` +## Importar `File` { #import-file } Importa `File` y `UploadFile` desde `fastapi`: {* ../../docs_src/request_files/tutorial001_an_py39.py hl[3] *} -## Definir Parámetros `File` +## Definir Parámetros `File` { #define-file-parameters } Crea parámetros de archivo de la misma manera que lo harías para `Body` o `Form`: @@ -50,7 +50,7 @@ Ten en cuenta que esto significa que todo el contenido se almacenará en memoria Pero hay varios casos en los que podrías beneficiarte de usar `UploadFile`. -## Parámetros de Archivo con `UploadFile` +## Parámetros de Archivo con `UploadFile` { #file-parameters-with-uploadfile } Define un parámetro de archivo con un tipo de `UploadFile`: @@ -66,7 +66,7 @@ Usar `UploadFile` tiene varias ventajas sobre `bytes`: * Tiene una interfaz `async` parecida a un archivo. * Expone un objeto Python real `SpooledTemporaryFile` que puedes pasar directamente a otros paquetes que esperan un objeto parecido a un archivo. -### `UploadFile` +### `UploadFile` { #uploadfile } `UploadFile` tiene los siguientes atributos: @@ -109,7 +109,7 @@ El `UploadFile` de **FastAPI** hereda directamente del `UploadFile` de **Starlet /// -## Qué es "Form Data" +## Qué es "Form Data" { #what-is-form-data } La manera en que los forms de HTML (`
`) envían los datos al servidor normalmente utiliza una codificación "especial" para esos datos, es diferente de JSON. @@ -121,7 +121,7 @@ Los datos de los forms normalmente se codifican usando el "media type" `applicat Pero cuando el formulario incluye archivos, se codifica como `multipart/form-data`. Si usas `File`, **FastAPI** sabrá que tiene que obtener los archivos de la parte correcta del cuerpo. -Si deseas leer más sobre estas codificaciones y campos de formularios, dirígete a la MDN web docs para POST. +Si deseas leer más sobre estas codificaciones y campos de formularios, dirígete a la MDN web docs para POST. /// @@ -133,19 +133,19 @@ Esto no es una limitación de **FastAPI**, es parte del protocolo HTTP. /// -## Subida de Archivos Opcional +## Subida de Archivos Opcional { #optional-file-upload } Puedes hacer un archivo opcional utilizando anotaciones de tipos estándar y estableciendo un valor por defecto de `None`: {* ../../docs_src/request_files/tutorial001_02_an_py310.py hl[9,17] *} -## `UploadFile` con Metadatos Adicionales +## `UploadFile` con Metadatos Adicionales { #uploadfile-with-additional-metadata } También puedes usar `File()` con `UploadFile`, por ejemplo, para establecer metadatos adicionales: {* ../../docs_src/request_files/tutorial001_03_an_py39.py hl[9,15] *} -## Subidas de Múltiples Archivos +## Subidas de Múltiples Archivos { #multiple-file-uploads } Es posible subir varios archivos al mismo tiempo. @@ -165,12 +165,12 @@ También podrías usar `from starlette.responses import HTMLResponse`. /// -### Subidas de Múltiples Archivos con Metadatos Adicionales +### Subidas de Múltiples Archivos con Metadatos Adicionales { #multiple-file-uploads-with-additional-metadata } Y de la misma manera que antes, puedes usar `File()` para establecer parámetros adicionales, incluso para `UploadFile`: {* ../../docs_src/request_files/tutorial003_an_py39.py hl[11,18:20] *} -## Recapitulación +## Recapitulación { #recap } Usa `File`, `bytes` y `UploadFile` para declarar archivos que se subirán en el request, enviados como form data. diff --git a/docs/es/docs/tutorial/request-form-models.md b/docs/es/docs/tutorial/request-form-models.md index 9d5d7495a0..1f4668c840 100644 --- a/docs/es/docs/tutorial/request-form-models.md +++ b/docs/es/docs/tutorial/request-form-models.md @@ -1,4 +1,4 @@ -# Modelos de Formulario +# Modelos de Formulario { #form-models } Puedes usar **modelos de Pydantic** para declarar **campos de formulario** en FastAPI. @@ -20,7 +20,7 @@ Esto es compatible desde la versión `0.113.0` de FastAPI. 🤓 /// -## Modelos de Pydantic para Formularios +## Modelos de Pydantic para Formularios { #pydantic-models-for-forms } Solo necesitas declarar un **modelo de Pydantic** con los campos que quieres recibir como **campos de formulario**, y luego declarar el parámetro como `Form`: @@ -28,7 +28,7 @@ Solo necesitas declarar un **modelo de Pydantic** con los campos que quieres rec **FastAPI** **extraerá** los datos de **cada campo** de los **form data** en el request y te dará el modelo de Pydantic que definiste. -## Revisa la Documentación +## Revisa la Documentación { #check-the-docs } Puedes verificarlo en la interfaz de documentación en `/docs`: @@ -36,7 +36,7 @@ Puedes verificarlo en la interfaz de documentación en `/docs`: -## Prohibir Campos de Formulario Extra +## Prohibir Campos de Formulario Extra { #forbid-extra-form-fields } En algunos casos de uso especiales (probablemente no muy comunes), podrías querer **restringir** los campos de formulario a solo aquellos declarados en el modelo de Pydantic. Y **prohibir** cualquier campo **extra**. @@ -73,6 +73,6 @@ Recibirá un response de error indicando que el campo `extra` no está permitido } ``` -## Resumen +## Resumen { #summary } Puedes usar modelos de Pydantic para declarar campos de formulario en FastAPI. 😎 diff --git a/docs/es/docs/tutorial/request-forms-and-files.md b/docs/es/docs/tutorial/request-forms-and-files.md index 51dfbb3578..363553e862 100644 --- a/docs/es/docs/tutorial/request-forms-and-files.md +++ b/docs/es/docs/tutorial/request-forms-and-files.md @@ -1,4 +1,4 @@ -# Request Forms and Files +# Formularios y archivos del request { #request-forms-and-files } Puedes definir archivos y campos de formulario al mismo tiempo usando `File` y `Form`. @@ -14,11 +14,11 @@ $ pip install python-multipart /// -## Importar `File` y `Form` +## Importa `File` y `Form` { #import-file-and-form } {* ../../docs_src/request_forms_and_files/tutorial001_an_py39.py hl[3] *} -## Definir parámetros `File` y `Form` +## Define parámetros `File` y `Form` { #define-file-and-form-parameters } Crea parámetros de archivo y formulario de la misma manera que lo harías para `Body` o `Query`: @@ -36,6 +36,6 @@ Esto no es una limitación de **FastAPI**, es parte del protocolo HTTP. /// -## Resumen +## Resumen { #recap } Usa `File` y `Form` juntos cuando necesites recibir datos y archivos en el mismo request. diff --git a/docs/es/docs/tutorial/request-forms.md b/docs/es/docs/tutorial/request-forms.md index a9d62e660b..33061e6a1b 100644 --- a/docs/es/docs/tutorial/request-forms.md +++ b/docs/es/docs/tutorial/request-forms.md @@ -1,10 +1,10 @@ -# Form Data +# Datos de formulario { #form-data } Cuando necesitas recibir campos de formulario en lugar de JSON, puedes usar `Form`. /// info | Información -Para usar forms, primero instala `python-multipart`. +Para usar formularios, primero instala `python-multipart`. Asegúrate de crear un [entorno virtual](../virtual-environments.md){.internal-link target=_blank}, activarlo, y luego instalarlo, por ejemplo: @@ -14,13 +14,13 @@ $ pip install python-multipart /// -## Importar `Form` +## Importar `Form` { #import-form } Importar `Form` desde `fastapi`: {* ../../docs_src/request_forms/tutorial001_an_py39.py hl[3] *} -## Definir parámetros de `Form` +## Definir parámetros de `Form` { #define-form-parameters } Crea parámetros de formulario de la misma manera que lo harías para `Body` o `Query`: @@ -28,7 +28,7 @@ Crea parámetros de formulario de la misma manera que lo harías para `Body` o ` Por ejemplo, en una de las formas en las que se puede usar la especificación OAuth2 (llamada "password flow") se requiere enviar un `username` y `password` como campos de formulario. -La especificación requiere que los campos se llamen exactamente `username` y `password`, y que se envíen como campos de formulario, no JSON. +La spec requiere que los campos se llamen exactamente `username` y `password`, y que se envíen como campos de formulario, no JSON. Con `Form` puedes declarar las mismas configuraciones que con `Body` (y `Query`, `Path`, `Cookie`), incluyendo validación, ejemplos, un alias (por ejemplo, `user-name` en lugar de `username`), etc. @@ -40,23 +40,23 @@ Con `Form` puedes declarar las mismas configuraciones que con `Body` (y `Query`, /// tip | Consejo -Para declarar bodies de forms, necesitas usar `Form` explícitamente, porque sin él, los parámetros se interpretarían como parámetros de query o como parámetros de body (JSON). +Para declarar bodies de formularios, necesitas usar `Form` explícitamente, porque sin él, los parámetros se interpretarían como parámetros de query o como parámetros de body (JSON). /// -## Sobre "Campos de Formulario" +## Sobre "Campos de formulario" { #about-form-fields } -La manera en que los forms HTML (`
`) envían los datos al servidor normalmente usa una codificación "especial" para esos datos, es diferente de JSON. +La manera en que los formularios HTML (`
`) envían los datos al servidor normalmente usa una codificación "especial" para esos datos, es diferente de JSON. **FastAPI** se encargará de leer esos datos del lugar correcto en lugar de JSON. /// note | Detalles técnicos -Los datos de forms normalmente se codifican usando el "media type" `application/x-www-form-urlencoded`. +Los datos de formularios normalmente se codifican usando el "media type" `application/x-www-form-urlencoded`. Pero cuando el formulario incluye archivos, se codifica como `multipart/form-data`. Leerás sobre la gestión de archivos en el próximo capítulo. -Si quieres leer más sobre estas codificaciones y campos de formulario, dirígete a la MDN web docs para POST. +Si quieres leer más sobre estas codificaciones y campos de formulario, dirígete a la MDN web docs para POST. /// @@ -68,6 +68,6 @@ Esto no es una limitación de **FastAPI**, es parte del protocolo HTTP. /// -## Recapitulación +## Recapitulación { #recap } Usa `Form` para declarar parámetros de entrada de datos de formulario. diff --git a/docs/es/docs/tutorial/response-model.md b/docs/es/docs/tutorial/response-model.md index 09682f51bc..8cfe69e782 100644 --- a/docs/es/docs/tutorial/response-model.md +++ b/docs/es/docs/tutorial/response-model.md @@ -1,8 +1,8 @@ -# Modelo de Response - Tipo de Retorno +# Modelo de Response - Tipo de Retorno { #response-model-return-type } Puedes declarar el tipo utilizado para el response anotando el **tipo de retorno** de la *path operation function*. -Puedes utilizar **anotaciones de tipos** de la misma manera que lo harías para datos de entrada en **parámetros** de función, puedes utilizar modelos de Pydantic, listas, diccionarios, valores escalares como enteros, booleanos, etc. +Puedes utilizar **anotaciones de tipos** de la misma manera que lo harías para datos de entrada en **parámetros** de función, puedes utilizar modelos de Pydantic, lists, diccionarios, valores escalares como enteros, booleanos, etc. {* ../../docs_src/response_model/tutorial001_01_py310.py hl[16,21] *} @@ -19,7 +19,7 @@ Pero lo más importante: * **Limitará y filtrará** los datos de salida a lo que se define en el tipo de retorno. * Esto es particularmente importante para la **seguridad**, veremos más sobre eso a continuación. -## Parámetro `response_model` +## Parámetro `response_model` { #response-model-parameter } Hay algunos casos en los que necesitas o quieres devolver algunos datos que no son exactamente lo que declara el tipo. @@ -27,7 +27,7 @@ Por ejemplo, podrías querer **devolver un diccionario** u objeto de base de dat Si añadiste la anotación del tipo de retorno, las herramientas y editores se quejarían con un error (correcto) diciéndote que tu función está devolviendo un tipo (por ejemplo, un dict) que es diferente de lo que declaraste (por ejemplo, un modelo de Pydantic). -En esos casos, puedes usar el parámetro del decorador de path operation `response_model` en lugar del tipo de retorno. +En esos casos, puedes usar el parámetro del *decorador de path operation* `response_model` en lugar del tipo de retorno. Puedes usar el parámetro `response_model` en cualquiera de las *path operations*: @@ -57,7 +57,7 @@ De esa manera le dices al editor que intencionalmente estás devolviendo cualqui /// -### Prioridad del `response_model` +### Prioridad del `response_model` { #response-model-priority } Si declaras tanto un tipo de retorno como un `response_model`, el `response_model` tomará prioridad y será utilizado por FastAPI. @@ -65,7 +65,7 @@ De esta manera puedes añadir anotaciones de tipos correctas a tus funciones inc También puedes usar `response_model=None` para desactivar la creación de un modelo de response para esa *path operation*, podrías necesitar hacerlo si estás añadiendo anotaciones de tipos para cosas que no son campos válidos de Pydantic, verás un ejemplo de eso en una de las secciones a continuación. -## Devolver los mismos datos de entrada +## Devolver los mismos datos de entrada { #return-the-same-input-data } Aquí estamos declarando un modelo `UserIn`, contendrá una contraseña en texto plano: @@ -105,7 +105,7 @@ Nunca almacenes la contraseña en texto plano de un usuario ni la envíes en un /// -## Añadir un modelo de salida +## Añadir un modelo de salida { #add-an-output-model } Podemos en cambio crear un modelo de entrada con la contraseña en texto plano y un modelo de salida sin ella: @@ -121,7 +121,7 @@ Aquí, aunque nuestra *path operation function* está devolviendo el mismo usuar Entonces, **FastAPI** se encargará de filtrar todos los datos que no estén declarados en el modelo de salida (usando Pydantic). -### `response_model` o Tipo de Retorno +### `response_model` o Tipo de Retorno { #response-model-or-return-type } En este caso, como los dos modelos son diferentes, si anotáramos el tipo de retorno de la función como `UserOut`, el editor y las herramientas se quejarían de que estamos devolviendo un tipo inválido, ya que son clases diferentes. @@ -129,7 +129,7 @@ Por eso en este ejemplo tenemos que declararlo en el parámetro `response_model` ...pero sigue leyendo abajo para ver cómo superar eso. -## Tipo de Retorno y Filtrado de Datos +## Tipo de Retorno y Filtrado de Datos { #return-type-and-data-filtering } Continuemos con el ejemplo anterior. Queríamos **anotar la función con un tipo**, pero queríamos poder devolver desde la función algo que en realidad incluya **más datos**. @@ -147,17 +147,17 @@ Con esto, obtenemos soporte de las herramientas, de los editores y mypy ya que e ¿Cómo funciona esto? Vamos a echarle un vistazo. 🤓 -### Anotaciones de Tipos y Herramientas +### Anotaciones de Tipos y Herramientas { #type-annotations-and-tooling } Primero vamos a ver cómo los editores, mypy y otras herramientas verían esto. `BaseUser` tiene los campos base. Luego `UserIn` hereda de `BaseUser` y añade el campo `password`, por lo que incluirá todos los campos de ambos modelos. -Anotamos el tipo de retorno de la función como `BaseUser`, pero en realidad estamos devolviendo una instancia de `UserIn`. +Anotamos el tipo de retorno de la función como `BaseUser`, pero en realidad estamos devolviendo un `UserIn` instance. El editor, mypy y otras herramientas no se quejarán de esto porque, en términos de tipificación, `UserIn` es una subclase de `BaseUser`, lo que significa que es un tipo *válido* cuando se espera algo que es un `BaseUser`. -### Filtrado de Datos en FastAPI +### Filtrado de Datos en FastAPI { #fastapi-data-filtering } Ahora, para FastAPI, verá el tipo de retorno y se asegurará de que lo que devuelves incluya **solo** los campos que están declarados en el tipo. @@ -165,7 +165,7 @@ FastAPI realiza varias cosas internamente con Pydantic para asegurarse de que es De esta manera, puedes obtener lo mejor de ambos mundos: anotaciones de tipos con **soporte de herramientas** y **filtrado de datos**. -## Verlo en la documentación +## Verlo en la documentación { #see-it-in-the-docs } Cuando veas la documentación automática, puedes verificar que el modelo de entrada y el modelo de salida tendrán cada uno su propio JSON Schema: @@ -175,29 +175,29 @@ Y ambos modelos se utilizarán para la documentación interactiva de la API: -## Otras Anotaciones de Tipos de Retorno +## Otras Anotaciones de Tipos de Retorno { #other-return-type-annotations } Podría haber casos en los que devuelvas algo que no es un campo válido de Pydantic y lo anotes en la función, solo para obtener el soporte proporcionado por las herramientas (el editor, mypy, etc). -### Devolver un Response Directamente +### Devolver un Response Directamente { #return-a-response-directly } El caso más común sería [devolver un Response directamente como se explica más adelante en la documentación avanzada](../advanced/response-directly.md){.internal-link target=_blank}. -{* ../../docs_src/response_model/tutorial003_02.py hl[8,10:11] *} +{* ../../docs_src/response_model/tutorial003_02_py39.py hl[8,10:11] *} Este caso simple es manejado automáticamente por FastAPI porque la anotación del tipo de retorno es la clase (o una subclase de) `Response`. Y las herramientas también estarán felices porque tanto `RedirectResponse` como `JSONResponse` son subclases de `Response`, por lo que la anotación del tipo es correcta. -### Anotar una Subclase de Response +### Anotar una Subclase de Response { #annotate-a-response-subclass } También puedes usar una subclase de `Response` en la anotación del tipo: -{* ../../docs_src/response_model/tutorial003_03.py hl[8:9] *} +{* ../../docs_src/response_model/tutorial003_03_py39.py hl[8:9] *} Esto también funcionará porque `RedirectResponse` es una subclase de `Response`, y FastAPI manejará automáticamente este caso simple. -### Anotaciones de Tipos de Retorno Inválidas +### Anotaciones de Tipos de Retorno Inválidas { #invalid-return-type-annotations } Pero cuando devuelves algún otro objeto arbitrario que no es un tipo válido de Pydantic (por ejemplo, un objeto de base de datos) y lo anotas así en la función, FastAPI intentará crear un modelo de response de Pydantic a partir de esa anotación de tipo, y fallará. @@ -207,7 +207,7 @@ Lo mismo sucedería si tuvieras algo como un I/O اشاره داره که نسبتاً "آروم" هستن (نسبت به سرعت پردازنده و حافظه RAM)، مثل منتظر موندن برای: - -* داده‌هایی که از کلاینت از طریق شبکه فرستاده می‌شن -* داده‌هایی که برنامه‌ات فرستاده تا از طریق شبکه به کلاینت برسه -* محتوای یه فایل توی دیسک که سیستم بخوندش و به برنامه‌ات بده -* محتوایی که برنامه‌ات به سیستم داده تا توی دیسک بنویسه -* یه عملیات API از راه دور -* یه عملیات دیتابیس که تموم بشه -* یه کوئری دیتابیس که نتایجش برگرده -* و غیره. - -چون زمان اجرا بیشتر صرف انتظار برای عملیات I/O می‌شه، بهشون می‌گن عملیات "I/O bound". - -بهش "ناهم‌زمان" می‌گن چون کامپیوتر / برنامه لازم نیست با کار آروم "هم‌زمان" باشه، منتظر لحظه دقیق تموم شدن کار بمونه، در حالی که هیچ کاری نمی‌کنه، تا نتیجه رو بگیره و کارش رو ادامه بده. - -به جاش، چون یه سیستم "ناهم‌زمان" هست، وقتی کار تموم شد، می‌تونه یه کم توی صف منتظر بمونه (چند میکروثانیه) تا کامپیوتر / برنامه هر کاری که رفته بکنه رو تموم کنه، و بعد برگرده نتیجه رو بگیره و باهاش کار کنه. - -برای "هم‌زمان" (برخلاف "ناهم‌زمان") معمولاً از اصطلاح "ترتیبی" هم استفاده می‌کنن، چون کامپیوتر / برنامه همه مراحل رو به ترتیب دنبال می‌کنه قبل از اینکه بره سراغ یه کار دیگه، حتی اگه اون مراحل شامل انتظار باشن. - -### هم‌زمانی و برگرها - -این ایده **ناهم‌زمان** که بالا توضیح دادم گاهی بهش **"هم‌زمانی"** هم می‌گن. با **"موازی‌سازی"** فرق داره. - -**هم‌زمانی** و **موازی‌سازی** هر دو به "اتفاق افتادن چیزای مختلف کم‌وبیش همزمان" ربط دارن. - -ولی جزئیات بین *هم‌زمانی* و *موازی‌سازی* خیلی متفاوته. - -برای دیدن فرقش، این داستان در مورد برگرها رو تصور کن: - -### برگرهای هم‌زمان - -با عشقت می‌ری فست‌فود بگیرین، توی صف وایمیستی در حالی که صندوقدار سفارش آدمای جلوی تو رو می‌گیره. 😍 - - - -بعد نوبت تو می‌شه، سفارش دو تا برگر خیلی شیک برای خودت و عشقت می‌دی. 🍔🍔 - - - -صندوقدار یه چیزی به آشپز توی آشپزخونه می‌گه تا بدونن باید برگرهای تو رو آماده کنن (گرچه الان دارن برگرهای مشتریای قبلی رو درست می‌کنن). - - - -پول رو می‌دی. 💸 - -صندوقدار شماره نوبتت رو بهت می‌ده. - - - -وقتی منتظری، با عشقت می‌ری یه میز انتخاب می‌کنی، می‌شینی و کلی با عشقت حرف می‌زنی (چون برگرهات خیلی شیکن و آماده کردنشون یه کم طول می‌کشه). - -وقتی پشت میز با عشقت نشستی، در حالی که منتظر برگرهایی، می‌تونی اون زمان رو صرف تحسین این کنی که عشقت چقدر باحال، ناز و باهوشه ✨😍✨. - - - -وقتی منتظری و با عشقت حرف می‌زنی، هر از گاهی شماره‌ای که رو پیشخون نشون داده می‌شه رو چک می‌کنی که ببینی نوبتت شده یا نه. - -بعد یه جایی بالاخره نوبتت می‌شه. می‌ری پیشخون، برگرهات رو می‌گیری و برمی‌گردی سر میز. - - - -تو و عشقت برگرها رو می‌خورین و یه وقت خوب باهم دارین. ✨ - - - -/// info - -تصاویر قشنگ از کترینا تامپسون. 🎨 - -/// - ---- - -تصور کن تو توی این داستان کامپیوتر / برنامه 🤖 هستی. - -وقتی توی صف هستی، فقط بیکاری 😴، منتظر نوبتت هستی، کار خیلی "مفیدی" نمی‌کنی. ولی صف سریع پیش می‌ره چون صندوقدار فقط سفارش می‌گیره (آمادشون نمی‌کنه)، پس این خوبه. - -بعد، وقتی نوبتت می‌شه، کار "مفید" واقعی می‌کنی، منو رو پردازش می‌کنی، تصمیم می‌گیری چی می‌خوای، انتخاب عشقت رو می‌گیری، پول می‌دی، چک می‌کنی اسکناس یا کارت درست رو دادی، چک می‌کنی درست حساب شده، چک می‌کنی سفارش آیتمای درست رو داره و غیره. - -ولی بعد، گرچه هنوز برگرهات رو نداری، کارت با صندوقدار "موقتاً متوقف" ⏸ می‌شه، چون باید منتظر بمونی 🕙 تا برگرهات آماده بشن. - -ولی وقتی از پیشخون دور می‌شی و با شماره نوبتت سر میز می‌شینی، می‌تونی توجهت رو 🔀 به عشقت بدی و "کار" ⏯ 🤓 رو اون بکنی. بعدش دوباره داری یه چیز خیلی "مفید" انجام می‌دی، مثل لاس زدن با عشقت 😍. - -بعد صندوقدار 💁 با گذاشتن شماره‌ات رو نمایشگر پیشخون می‌گه "من با درست کردن برگرها تموم کردم"، ولی تو مثل دیوونه‌ها وقتی شماره‌ات رو نمایشگر میاد فوری نمی‌پری. می‌دونی کسی برگرهات رو نمی‌دزده چون شماره نوبتت رو داری، و اونا هم مال خودشون رو دارن. - -پس منتظر می‌مونی تا عشقت داستانش رو تموم کنه (کار فعلی ⏯ / وظیفه‌ای که داره پردازش می‌شه 🤓)، آروم لبخند می‌زنی و می‌گی که می‌ری برگرها رو بیاری ⏸. - -بعد می‌ری پیشخون 🔀، به کار اولیه که حالا تموم شده ⏯، برگرها رو می‌گیری، تشکر می‌کنی و می‌برشون سر میز. این مرحله / وظیفه تعامل با پیشخون رو تموم می‌کنه ⏹. این به نوبه خودش یه وظیفه جدید، "خوردن برگرها" 🔀 ⏯، می‌سازه، ولی اون قبلی که "گرفتن برگرها" بود تموم شده ⏹. - -### برگرهای موازی - -حالا فرض کن اینا "برگرهای هم‌زمان" نیستن، بلکه "برگرهای موازی" هستن. - -با عشقت می‌ری فست‌فود موازی بگیری. - -توی صف وایمیستی در حالی که چند تا (مثلاً 8 تا) صندوقدار که همزمان آشپز هم هستن سفارش آدمای جلوی تو رو می‌گیرن. - -همه قبل تو منتظرن برگرهاشون آماده بشه قبل از اینکه پیشخون رو ترک کنن، چون هر کدوم از 8 تا صندوقدار می‌ره و برگر رو همون موقع درست می‌کنه قبل از اینکه سفارش بعدی رو بگیره. - - - -بالاخره نوبت تو می‌شه، سفارش دو تا برگر خیلی شیک برای خودت و عشقت می‌دی. - -پول رو می‌دی 💸. - - - -صندوقدار می‌ره آشپزخونه. - -منتظر می‌مونی، جلوی پیشخون وایستادی 🕙، که کسی قبل از تو برگرهات رو نگیره، چون شماره نوبت نیست. - - - -چون تو و عشقت مشغول این هستین که نذارین کسی جلوتون بیاد و هر وقت برگرها رسیدن اونا رو بگیره، نمی‌تونی به عشقت توجه کنی. 😞 - -این کار "هم‌زمان" هست، تو با صندوقدار/آشپز 👨‍🍳 "هم‌زمان" هستی. باید منتظر بمونی 🕙 و درست همون لحظه که صندوقدار/آشپز 👨‍🍳 برگرها رو تموم می‌کنه و بهت می‌ده اونجا باشی، وگرنه ممکنه یکی دیگه اونا رو بگیره. - - - -بعد صندوقدار/آشپزت 👨‍🍳 بالاخره بعد از یه مدت طولانی انتظار 🕙 جلوی پیشخون با برگرهات برمی‌گرده. - - - -برگرهات رو می‌گیری و با عشقت می‌ری سر میز. - -فقط می‌خورینشون، و تمومه. ⏹ - - - -حرف زدن یا لاس زدن زیاد نبود چون بیشتر وقت صرف انتظار 🕙 جلوی پیشخون شد. 😞 - -/// info - -تصاویر قشنگ از کترینا تامپسون. 🎨 - -/// - ---- - -توی این سناریوی برگرهای موازی، تو یه کامپیوتر / برنامه 🤖 با دو تا پردازنده (تو و عشقت) هستی، هر دو منتظر 🕙 و توجهشون ⏯ رو برای مدت طولانی "انتظار جلوی پیشخون" 🕙 گذاشتن. - -فست‌فود 8 تا پردازنده (صندوقدار/آشپز) داره. در حالی که فست‌فود برگرهای هم‌زمان شاید فقط 2 تا داشته (یه صندوقدار و یه آشپز). - -ولی با این حال، تجربه نهایی بهترین نیست. 😞 - ---- - -این معادل موازی داستان برگرها بود. 🍔 - -برای یه مثال "واقعی‌تر" از زندگی، یه بانک رو تصور کن. - -تا همین چند وقت پیش، بیشتر بانک‌ها چند تا صندوقدار 👨‍💼👨‍💼👨‍💼👨‍💼 داشتن و یه صف بزرگ 🕙🕙🕙🕙🕙🕙🕙🕙. - -همه صندوقدارها کار رو با یه مشتری بعد از اون یکی 👨‍💼⏯ انجام می‌دادن. - -و باید توی صف 🕙 مدت زیادی منتظر بمونی وگرنه نوبتت رو از دست می‌دی. - -احتمالاً نمی‌خوای عشقت 😍 رو با خودت ببری بانک 🏦 برای کارای روزمره. - -### نتیجه‌گیری برگرها - -توی این سناریوی "برگرهای فست‌فود با عشقت"، چون کلی انتظار 🕙 هست، خیلی منطقی‌تره که یه سیستم هم‌زمان ⏸🔀⏯ داشته باشی. - -این برای بیشتر برنامه‌های وب هم صدق می‌کنه. - -خیلی خیلی کاربر، ولی سرورت منتظر 🕙 اتصال نه‌چندان خوبشون هست تا درخواست‌هاشون رو بفرستن. - -و بعد دوباره منتظر 🕙 که جواب‌ها برگردن. - -این "انتظار" 🕙 توی میکروثانیه‌ها اندازه‌گیری می‌شه، ولی با این حال، جمعش که بکنی آخرش کلی انتظار می‌شه. - -برای همین استفاده از کد ناهم‌زمان ⏸🔀⏯ برای APIهای وب خیلی منطقیه. - -این نوع ناهم‌زمانی چیزیه که NodeJS رو محبوب کرد (گرچه NodeJS موازی نیست) و نقطه قوت Go به‌عنوان یه زبون برنامه‌نویسیه. - -و همون سطح عملکردی هست که با **FastAPI** می‌گیری. - -و چون می‌تونی هم‌زمانی و موازی‌سازی رو همزمان داشته باشی، عملکرد بالاتری از بیشتر فریم‌ورک‌های تست‌شده NodeJS می‌گیری و هم‌تراز با Go، که یه زبون کامپایل‌شده نزدیک به C هست (همه اینا به لطف Starlette). - -### آیا هم‌زمانی از موازی‌سازی بهتره؟ - -نه! این نتیجه داستان نیست. - -هم‌زمانی با موازی‌سازی فرق داره. و توی **سناریوهای خاص** که کلی انتظار دارن بهتره. به همین خاطر، معمولاً برای توسعه برنامه‌های وب خیلی از موازی‌سازی بهتره. ولی نه برای همه‌چیز. - -برای اینکه یه تعادل بذاریم، این داستان کوتاه رو تصور کن: - -> باید یه خونه بزرگ و کثیف رو تمیز کنی. - -*آره، کل داستان همینه*. - ---- - -هیچ انتظاری 🕙 اونجا نیست، فقط کلی کار برای انجام دادن توی جاهای مختلف خونه. - -می‌تونی مثل مثال برگرها نوبت بذاری، اول پذیرایی، بعد آشپزخونه، ولی چون منتظر چیزی نیستی 🕙، فقط داری تمیز می‌کنی و تمیز می‌کنی، نوبت‌ها هیچ تأثیری نداره. - -با نوبت یا بدون نوبت (هم‌زمانی) همون قدر طول می‌کشه تا تمومش کنی و همون مقدار کار رو کردی. - -ولی توی این موقعیت، اگه بتونی اون 8 تا صندوقدار/آشپز/حالا-تمیزکار رو بیاری، و هر کدومشون (به‌علاوه خودت) یه قسمت از خونه رو تمیز کنن، می‌تونی همه کار رو **موازی** انجام بدی، با کمک اضافی، و خیلی زودتر تمومش کنی. - -توی این سناریو، هر کدوم از تمیزکارها (از جمله خودت) یه پردازنده‌ست که کار خودش رو می‌کنه. - -و چون بیشتر زمان اجرا صرف کار واقعی می‌شه (به جای انتظار)، و کار توی کامپیوتر با CPU انجام می‌شه، به این مشکلات می‌گن "CPU bound". - ---- - -مثال‌های رایج عملیات CPU bound چیزایی هستن که نیاز به پردازش ریاضی پیچیده دارن. - -مثلاً: - -* پردازش **صدا** یا **تصویر**. -* **بینایی کامپیوتری**: یه تصویر از میلیون‌ها پیکسل تشکیل شده، هر پیکسل 3 تا مقدار / رنگ داره، پردازشش معمولاً نیاز داره چیزی رو رو اون پیکسل‌ها همزمان حساب کنی. -* **یادگیری ماشین**: معمولاً کلی ضرب "ماتریس" و "بردار" لازم داره. یه جدول بزرگ پر از عدد رو تصور کن که همه‌شون رو همزمان ضرب می‌کنی. -* **یادگیری عمیق**: این یه زیرشاخه از یادگیری ماشینه، پس همون قضیه صدق می‌کنه. فقط این که یه جدول عدد برای ضرب کردن نیست، بلکه یه مجموعه بزرگ از اونا هست، و توی خیلی موارد از یه پردازنده خاص برای ساخت و / یا استفاده از این مدل‌ها استفاده می‌کنی. - -### هم‌زمانی + موازی‌سازی: وب + یادگیری ماشین - -با **FastAPI** می‌تونی از هم‌زمانی که برای توسعه وب خیلی رایجه (همون جذابیت اصلی NodeJS) استفاده کنی. - -ولی می‌تونی از فواید موازی‌سازی و چندپردازشی (اجرای چند پروسه به‌صورت موازی) برای کارای **CPU bound** مثل سیستم‌های یادگیری ماشین هم بهره ببری. - -این، به‌علاوه این واقعیت ساده که پایتون زبون اصلی برای **علم داده**، یادگیری ماشین و به‌خصوص یادگیری عمیقه، باعث می‌شه FastAPI یه انتخاب خیلی خوب برای APIها و برنامه‌های وب علم داده / یادگیری ماشین باشه (بین خیلی چیزای دیگه). - -برای دیدن اینکه چطور توی محیط واقعی به این موازی‌سازی برسی، بخش [استقرار](deployment/index.md){.internal-link target=_blank} رو ببین. - -## `async` و `await` - -نسخه‌های مدرن پایتون یه راه خیلی ساده و قابل‌فهم برای تعریف کد ناهم‌زمان دارن. این باعث می‌شه مثل کد "ترتیبی" معمولی به نظر بیاد و توی لحظه‌های درست "انتظار" رو برات انجام بده. - -وقتی یه عملیاتی هست که قبل از دادن نتیجه‌ها نیاز به انتظار داره و از این قابلیت‌های جدید پایتون پشتیبانی می‌کنه، می‌تونی اینجوری کدنویسیش کنی: - -```Python -burgers = await get_burgers(2) -``` - -نکته کلیدی اینجا `await` هست. به پایتون می‌گه که باید ⏸ منتظر بمونه تا `get_burgers(2)` کارش 🕙 تموم بشه قبل از اینکه نتیجه‌ها رو توی `burgers` ذخیره کنه. با این، پایتون می‌دونه که می‌تونه بره یه کار دیگه 🔀 ⏯ توی این مدت بکنه (مثل گرفتن یه درخواست دیگه). - -برای اینکه `await` کار کنه، باید توی یه تابع باشه که از این ناهم‌زمانی پشتیبانی کنه. برای این کار، فقط با `async def` تعریفش می‌کنی: - -```Python hl_lines="1" -async def get_burgers(number: int): - # یه سری کار ناهم‌زمان برای ساختن برگرها انجام بده - return burgers -``` - -...به جای `def`: - -```Python hl_lines="2" -# این ناهم‌زمان نیست -def get_sequential_burgers(number: int): - # یه سری کار ترتیبی برای ساختن برگرها انجام بده - return burgers -``` - -با `async def`، پایتون می‌دونه که توی اون تابع باید حواسش به عبارت‌های `await` باشه، و می‌تونه اجرای اون تابع رو "موقتاً متوقف" ⏸ کنه و بره یه کار دیگه 🔀 قبل از برگشتن بکنه. - -وقتی می‌خوای یه تابع `async def` رو صدا کنی، باید "منتظرش" بمونی. پس این کار نمی‌کنه: - -```Python -# این کار نمی‌کنه، چون get_burgers با async def تعریف شده -burgers = get_burgers(2) -``` - ---- - -پس، اگه از یه کتابخونه استفاده می‌کنی که بهت می‌گه می‌تونی با `await` صداش کنی، باید *توابع عملیات مسیرت* که ازش استفاده می‌کنن رو با `async def` بسازی، مثل: - -```Python hl_lines="2-3" -@app.get('/burgers') -async def read_burgers(): - burgers = await get_burgers(2) - return burgers -``` - -### جزئیات فنی‌تر - -شاید متوجه شده باشی که `await` فقط توی توابعی که با `async def` تعریف شدن می‌تونه استفاده بشه. - -ولی در عین حال، توابعی که با `async def` تعریف شدن باید "منتظر"شون بمونی. پس توابع با `async def` فقط توی توابعی که با `async def` تعریف شدن می‌تونن صدا زده بشن. - -حالا، قضیه مرغ و تخم‌مرغ چیه، چطور اولین تابع `async` رو صدا می‌کنی؟ - -اگه با **FastAPI** کار می‌کنی، لازم نیست نگران این باشی، چون اون "اولین" تابع، *تابع عملیات مسیرت* هست، و FastAPI می‌دونه چطور کار درست رو بکنه. - -ولی اگه بخوای بدون FastAPI از `async` / `await` استفاده کنی، اینم ممکنه. - -### کد ناهم‌زمان خودت رو بنویس - -Starlette (و **FastAPI**) بر پایه AnyIO هستن، که باعث می‌شه با کتابخونه استاندارد پایتون asyncio و Trio سازگار باشه. - -به‌خصوص، می‌تونی مستقیماً از AnyIO برای موارد استفاده پیشرفته هم‌زمانی که نیاز به الگوهای پیچیده‌تر توی کد خودت دارن استفاده کنی. - -و حتی اگه از FastAPI استفاده نکنی، می‌تونی برنامه‌های ناهم‌زمان خودت رو با AnyIO بنویسی تا خیلی سازگار باشه و فوایدش رو بگیری (مثل *هم‌زمانی ساختاریافته*). - -من یه کتابخونه دیگه روی AnyIO ساختم، یه لایه نازک روش، تا یه کم annotationهای نوع رو بهتر کنم و **تکمیل خودکار** بهتر، **خطاهای درون‌خطی** و غیره بگیرم. یه مقدمه و آموزش ساده هم داره که بهت کمک می‌کنه **بفهمی** و **کد ناهم‌زمان خودت رو بنویسی**: Asyncer. اگه بخوای **کد ناهم‌زمان رو با کد معمولی** (بلاک‌کننده/هم‌زمان) ترکیب کنی خیلی به‌دردت می‌خوره. - -### شکل‌های دیگه کد ناهم‌زمان - -این سبک استفاده از `async` و `await` توی زبون نسبتاً جدیده. - -ولی کار با کد ناهم‌زمان رو خیلی ساده‌تر می‌کنه. - -همین سینتکس (یا تقریباً یکسان) اخیراً توی نسخه‌های مدرن جاوااسکریپت (توی مرورگر و NodeJS) هم اضافه شده. - -ولی قبل از اون، مدیریت کد ناهم‌زمان خیلی پیچیده‌تر و سخت‌تر بود. - -توی نسخه‌های قبلی پایتون، می‌تونستی از نخ‌ها یا Gevent استفاده کنی. ولی کد خیلی پیچیده‌تر می‌شه برای فهمیدن، دیباگ کردن و فکر کردن بهش. - -توی نسخه‌های قبلی NodeJS / جاوااسکریپت مرورگر، از "کال‌بک‌ها" استفاده می‌کردی. که می‌رسید به "جهان کال‌بک‌ها". - -## کروتین‌ها - -**کروتین** فقط یه اصطلاح خیلی شیک برای چیزیه که یه تابع `async def` برمی‌گردونه. پایتون می‌دونه که این یه چیزی مثل تابع هست، می‌تونه شروع بشه و یه جایی تموم بشه، ولی ممکنه داخلش هم موقف ⏸ بشه، هر وقت یه `await` توش باشه. - -ولی همه این قابلیت استفاده از کد ناهم‌زمان با `async` و `await` خیلی وقتا خلاصه می‌شه به استفاده از "کروتین‌ها". این قابل مقایسه با ویژگی اصلی Go، یعنی "Goroutineها" هست. - -## نتیجه‌گیری - -بیاید همون جمله از بالا رو ببینیم: - -> نسخه‌های مدرن پایتون از **"کد ناهم‌زمان"** با چیزی که بهش **"کروتین"** می‌گن پشتیبانی می‌کنن، با سینتکس **`async` و `await`**. - -حالا باید بیشتر برات معنی بده. ✨ - -همه اینا چیزیه که به FastAPI (از طریق Starlette) قدرت می‌ده و باعث می‌شه عملکرد چشمگیری داشته باشه. - -## جزئیات خیلی فنی - -/// warning - -احتمالاً می‌تونی اینو رد کنی. - -اینا جزئیات خیلی فنی از نحوه کار **FastAPI** زیر پوسته‌ست. - -اگه یه کم دانش فنی (کروتین‌ها، نخ‌ها، بلاک کردن و غیره) داری و کنجکاوی که FastAPI چطور `async def` رو در مقابل `def` معمولی مدیریت می‌کنه، ادامه بده. - -/// - -### توابع عملیات مسیر - -وقتی یه *تابع عملیات مسیر* رو با `def` معمولی به جای `async def` تعریف می‌کنی، توی یه استخر نخ خارجی اجرا می‌شه که بعدش منتظرش می‌مونن، به جای اینکه مستقیم صداش کنن (چون سرور رو بلاک می‌کنه). - -اگه از یه فریم‌ورک ناهم‌زمان دیگه میای که به روش بالا کار نمی‌کنه و عادت داری *توابع عملیات مسیر* ساده فقط محاسباتی رو با `def` معمولی برای یه سود کوچیک عملکرد (حدود 100 نانوثانیه) تعریف کنی، توجه کن که توی **FastAPI** اثرش کاملاً برعکسه. توی این موارد، بهتره از `async def` استفاده کنی مگه اینکه *توابع عملیات مسیرت* کدی داشته باشن که عملیات I/O بلاک‌کننده انجام بده. - -با این حال، توی هر دو موقعیت، احتمالش زیاده که **FastAPI** هنوز [سریع‌تر](index.md#performance){.internal-link target=_blank} از فریم‌ورک قبلی‌ات باشه (یا حداقل قابل مقایسه باهاش). - -### وابستگی‌ها - -همین برای [وابستگی‌ها](tutorial/dependencies/index.md){.internal-link target=_blank} هم صدق می‌کنه. اگه یه وابستگی یه تابع `def` معمولی به جای `async def` باشه، توی استخر نخ خارجی اجرا می‌شه. - -### زیروابستگی‌ها - -می‌تونی چند تا وابستگی و [زیروابستگی](tutorial/dependencies/sub-dependencies.md){.internal-link target=_blank} داشته باشی که همدیگه رو نیاز دارن (به‌عنوان پارامترهای تعریف تابع)، بعضی‌هاشون ممکنه با `async def` ساخته بشن و بعضی‌ها با `def` معمولی. بازم کار می‌کنه، و اونایی که با `def` معمولی ساخته شدن توی یه نخ خارجی (از استخر نخ) صدا زده می‌شن به جای اینکه "منتظرشون" بمونن. - -### توابع کاربردی دیگه - -هر تابع کاربردی دیگه‌ای که مستقیم خودت صداش می‌کنی می‌تونه با `def` معمولی یا `async def` ساخته بشه و FastAPI رو نحوه صدازدنش تأثیر نمی‌ذاره. - -این برخلاف توابعی هست که FastAPI برات صداشون می‌کنه: *توابع عملیات مسیر* و وابستگی‌ها. - -اگه تابع کاربردیت یه تابع معمولی با `def` باشه، مستقیم صداش می‌کنن (همون‌طور که توی کدت نوشتی)، نه توی استخر نخ، اگه تابع با `async def` ساخته شده باشه، باید وقتی توی کدت صداش می‌کنی `await`ش کنی. - ---- - -دوباره، اینا جزئیات خیلی فنی هستن که احتمالاً اگه دنبالشون اومده باشی برات مفید باشن. - -وگرنه، با راهنمایی‌های بخش بالا باید خوب باشی: عجله داری؟. diff --git a/docs/fa/docs/environment-variables.md b/docs/fa/docs/environment-variables.md deleted file mode 100644 index 75309ce1fe..0000000000 --- a/docs/fa/docs/environment-variables.md +++ /dev/null @@ -1,298 +0,0 @@ -# متغیرهای محیطی - -/// tip - -اگه از قبل می‌دونی متغیرهای محیطی چی هستن و چطور ازشون استفاده می‌شه، می‌تونی این بخش رو رد کنی. - -/// - -یه متغیر محیطی (که بهش "**env var**" هم می‌گن) یه متغیریه که **خارج** از کد پایتون، توی **سیستم‌عامل** زندگی می‌کنه و می‌تونه توسط کد پایتونت (یا برنامه‌های دیگه) خونده بشه. - -متغیرهای محیطی می‌تونن برای مدیریت **تنظیمات** برنامه، به‌عنوان بخشی از **نصب** پایتون و غیره مفید باشن. - -## ساخت و استفاده از متغیرهای محیطی - -می‌تونی متغیرهای محیطی رو توی **شل (ترمینال)** **بسازی** و ازشون استفاده کنی، بدون اینکه به پایتون نیاز داشته باشی: - -//// tab | لینوکس، مک‌اواس، ویندوز بش - -
- -```console -// می‌تونی یه متغیر محیطی به اسم MY_NAME بسازی با -$ export MY_NAME="Wade Wilson" - -// بعد می‌تونی با برنامه‌های دیگه ازش استفاده کنی، مثل -$ echo "Hello $MY_NAME" - -Hello Wade Wilson -``` - -
- -//// - -//// tab | ویندوز پاورشل - -
- -```console -// یه متغیر محیطی به اسم MY_NAME بساز -$ $Env:MY_NAME = "Wade Wilson" - -// با برنامه‌های دیگه ازش استفاده کن، مثل -$ echo "Hello $Env:MY_NAME" - -Hello Wade Wilson -``` - -
- -//// - -## خوندن متغیرهای محیطی توی پایتون - -می‌تونی متغیرهای محیطی رو **خارج** از پایتون، توی ترمینال (یا با هر روش دیگه) بسازی، و بعد توی **پایتون** اونا رو بخونی. - -مثلاً می‌تونی یه فایل `main.py` داشته باشی با: - -```Python hl_lines="3" -import os - -name = os.getenv("MY_NAME", "World") -print(f"Hello {name} from Python") -``` - -/// tip - -آرگومان دوم `os.getenv()` مقدار پیش‌فرضیه که برمی‌گردونه. - -اگه ندی، به‌صورت پیش‌فرض `None` هست، اینجا ما `"World"` رو به‌عنوان مقدار پیش‌فرض گذاشتیم. - -/// - -بعد می‌تونی اون برنامه پایتون رو صدا کنی: - -//// tab | لینوکس، مک‌اواس، ویندوز بش - -
- -```console -// اینجا هنوز متغیر محیطی رو تنظیم نکردیم -$ python main.py - -// چون متغیر محیطی رو تنظیم نکردیم، مقدار پیش‌فرض رو می‌گیریم - -Hello World from Python - -// ولی اگه اول یه متغیر محیطی بسازیم -$ export MY_NAME="Wade Wilson" - -// و بعد دوباره برنامه رو صدا کنیم -$ python main.py - -// حالا می‌تونه متغیر محیطی رو بخونه - -Hello Wade Wilson from Python -``` - -
- -//// - -//// tab | ویندوز پاورشل - -
- -```console -// اینجا هنوز متغیر محیطی رو تنظیم نکردیم -$ python main.py - -// چون متغیر محیطی رو تنظیم نکردیم، مقدار پیش‌فرض رو می‌گیریم - -Hello World from Python - -// ولی اگه اول یه متغیر محیطی بسازیم -$ $Env:MY_NAME = "Wade Wilson" - -// و بعد دوباره برنامه رو صدا کنیم -$ python main.py - -// حالا می‌تونه متغیر محیطی رو بخونه - -Hello Wade Wilson from Python -``` - -
- -//// - -چون متغیرهای محیطی می‌تونن خارج از کد تنظیم بشن، ولی کد می‌تونه اونا رو بخونه، و لازم نیست با بقیه فایل‌ها ذخیره (کمیتی به `git`) بشن، معمولاً برای پیکربندی یا **تنظیمات** استفاده می‌شن. - -همچنین می‌تونی یه متغیر محیطی رو فقط برای **یه اجرای خاص برنامه** بسازی، که فقط برای اون برنامه و فقط برای مدت زمان اجراش در دسترسه. - -برای این کار، درست قبل از خود برنامه، توی همون خط بسازش: - -
- -```console -// یه متغیر محیطی MY_NAME رو توی خط برای این اجرای برنامه بساز -$ MY_NAME="Wade Wilson" python main.py - -// حالا می‌تونه متغیر محیطی رو بخونه - -Hello Wade Wilson from Python - -// متغیر محیطی بعدش دیگه وجود نداره -$ python main.py - -Hello World from Python -``` - -
- -/// tip - -می‌تونی بیشتر در موردش توی برنامه دوازده‌فاکتوری: پیکربندی بخونی. - -/// - -## نوع‌ها و اعتبارسنجی - -این متغیرهای محیطی فقط می‌تونن **رشته‌های متنی** رو نگه دارن، چون خارج از پایتون هستن و باید با برنامه‌های دیگه و بقیه سیستم (و حتی سیستم‌عامل‌های مختلف مثل لینوکس، ویندوز، مک‌اواس) سازگار باشن. - -یعنی **هر مقداری** که توی پایتون از یه متغیر محیطی خونده می‌شه یه `str` هست، و هر تبدیل به نوع دیگه یا هر اعتبارسنجی باید توی کد انجام بشه. - -توی [راهنمای کاربر پیشرفته - تنظیمات و متغیرهای محیطی](./advanced/settings.md){.internal-link target=_blank} بیشتر در مورد استفاده از متغیرهای محیطی برای مدیریت **تنظیمات برنامه** یاد می‌گیری. - -## متغیر محیطی `PATH` - -یه متغیر محیطی **خاص** به اسم **`PATH`** وجود داره که سیستم‌عامل‌ها (لینوکس، مک‌اواس، ویندوز) ازش برای پیدا کردن برنامه‌هایی که قراره اجرا بشن استفاده می‌کنن. - -مقدار متغیر `PATH` یه رشته طولانی از پوشه‌هاست که توی لینوکس و مک‌اواس با دونقطه `:` و توی ویندوز با نقطه‌ویرگول `;` از هم جدا شدن. - -مثلاً، متغیر محیطی `PATH` می‌تونه اینجوری باشه: - -//// tab | لینوکس، مک‌اواس - -```plaintext -/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin -``` - -یعنی سیستم باید دنبال برنامه‌ها توی این پوشه‌ها بگرده: - -* `/usr/local/bin` -* `/usr/bin` -* `/bin` -* `/usr/sbin` -* `/sbin` - -//// - -//// tab | ویندوز - -```plaintext -C:\Program Files\Python312\Scripts;C:\Program Files\Python312;C:\Windows\System32 -``` - -یعنی سیستم باید دنبال برنامه‌ها توی این پوشه‌ها بگرده: - -* `C:\Program Files\Python312\Scripts` -* `C:\Program Files\Python312` -* `C:\Windows\System32` - -//// - -وقتی یه **دستور** توی ترمینال تایپ می‌کنی، سیستم‌عامل **دنبال** برنامه توی **هر کدوم از این پوشه‌ها** که توی متغیر محیطی `PATH` لیست شدن می‌گرده. - -مثلاً، وقتی توی ترمینال `python` تایپ می‌کنی، سیستم‌عامل دنبال یه برنامه به اسم `python` توی **اولین پوشه** توی اون لیست می‌گرده. - -اگه پیداش کنه، **استفاده‌ش می‌کنه**. وگرنه توی **پوشه‌های بعدی** دنبالش می‌گرده. - -### نصب پایتون و به‌روزرسانی `PATH` - -وقتی پایتون رو نصب می‌کنی، ممکنه ازت بپرسن آیا می‌خوای متغیر محیطی `PATH` رو به‌روزرسانی کنی. - -//// tab | لینوکس، مک‌اواس - -فرض کن پایتون رو نصب کردی و توی یه پوشه `/opt/custompython/bin` قرار گرفته. - -اگه بگی بله برای به‌روزرسانی متغیر محیطی `PATH`، نصاب `/opt/custompython/bin` رو به متغیر محیطی `PATH` اضافه می‌کنه. - -ممکنه اینجوری بشه: - -```plaintext -/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/custompython/bin -``` - -این‌جوری، وقتی توی ترمینال `python` تایپ می‌کنی، سیستم برنامه پایتون رو توی `/opt/custompython/bin` (آخرین پوشه) پیدا می‌کنه و از اون استفاده می‌کنه. - -//// - -//// tab | ویندوز - -فرض کن پایتون رو نصب کردی و توی یه پوشه `C:\opt\custompython\bin` قرار گرفته. - -اگه بگی بله برای به‌روزرسانی متغیر محیطی `PATH`، نصاب `C:\opt\custompython\bin` رو به متغیر محیطی `PATH` اضافه می‌کنه. - -```plaintext -C:\Program Files\Python312\Scripts;C:\Program Files\Python312;C:\Windows\System32;C:\opt\custompython\bin -``` - -این‌جوری، وقتی توی ترمینال `python` تایپ می‌کنی، سیستم برنامه پایتون رو توی `C:\opt\custompython\bin` (آخرین پوشه) پیدا می‌کنه و از اون استفاده می‌کنه. - -//// - -پس، اگه تایپ کنی: - -
- -```console -$ python -``` - -
- -//// tab | لینوکس، مک‌اواس - -سیستم برنامه `python` رو توی `/opt/custompython/bin` **پیدا** می‌کنه و اجراش می‌کنه. - -تقریباً معادل اینه که تایپ کنی: - -
- -```console -$ /opt/custompython/bin/python -``` - -
- -//// - -//// tab | ویندوز - -سیستم برنامه `python` رو توی `C:\opt\custompython\bin\python` **پیدا** می‌کنه و اجراش می‌کنه. - -تقریباً معادل اینه که تایپ کنی: - -
- -```console -$ C:\opt\custompython\bin\python -``` - -
- -//// - -این اطلاعات وقتی در مورد [محیط‌های مجازی](virtual-environments.md){.internal-link target=_blank} یاد می‌گیری به‌دردت می‌خوره. - -## نتیجه‌گیری - -با این باید یه درک پایه‌ای از **متغیرهای محیطی** و نحوه استفاده‌شون توی پایتون داشته باشی. - -می‌تونی بیشتر در موردشون توی ویکی‌پدیا برای متغیر محیطی بخونی. - -توی خیلی موارد مشخص نیست که متغیرهای محیطی چطور می‌تونن فوری مفید و کاربردی باشن. ولی توی موقعیت‌های مختلف توسعه مدام پیداشون می‌شه، پس خوبه که در موردشون بدونی. - -مثلاً، توی بخش بعدی در مورد [محیط‌های مجازی](virtual-environments.md) به این اطلاعات نیاز داری. diff --git a/docs/fa/docs/features.md b/docs/fa/docs/features.md deleted file mode 100644 index c265d29702..0000000000 --- a/docs/fa/docs/features.md +++ /dev/null @@ -1,209 +0,0 @@ -# ویژگی ها - -## ویژگی های FastAPI - -**FastAPI** موارد زیر را به شما ارائه میدهد: - -### برپایه استاندارد های باز - -* OpenAPI برای ساخت API, شامل مشخص سازی path operation ها, پارامترها, body request ها, امنیت و غیره. -* مستندسازی خودکار data model با JSON Schema (همانطور که OpenAPI خود نیز مبتنی بر JSON Schema است). -* طراحی شده بر اساس استاندارد هایی که پس از یک مطالعه دقیق بدست آمده اند بجای طرحی ناپخته و بدون فکر. -* همچنین به شما اجازه میدهد تا از تولید خودکار client code در بسیاری از زبان ها استفاده کنید. - -### مستندات خودکار - -مستندات API تعاملی و ایجاد رابط کاربری وب. از آنجایی که این فریم ورک برپایه OpenAPI میباشد، آپشن های متعددی وجود دارد که ۲ مورد بصورت پیش فرض گنجانده شده اند. - -* Swagger UI، با کاوش تعاملی، API خود را مستقیما از طریق مرورگر صدازده و تست کنید. - -![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) - -* مستندات API جایگزین با ReDoc. - -![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) - -### فقط پایتون مدرن - -همه اینها برپایه type declaration های **پایتون ۳.۶** استاندارد (به لطف Pydantic) میباشند. سینتکس جدیدی درکار نیست. تنها پایتون مدرن استاندارد. - -اگر به یک یادآوری ۲ دقیقه ای در مورد نحوه استفاده از تایپ های پایتون دارید (حتی اگر از FastAPI استفاده نمیکنید) این آموزش کوتاه را بررسی کنید: [Python Types](python-types.md){.internal-link target=\_blank}. - -شما پایتون استاندارد را با استفاده از تایپ ها مینویسید: - -```Python -from datetime import date - -from pydantic import BaseModel - -# Declare a variable as a str -# and get editor support inside the function -def main(user_id: str): - return user_id - - -# A Pydantic model -class User(BaseModel): - id: int - name: str - joined: date -``` - -که سپس میتوان به این شکل از آن استفاده کرد: - -```Python -my_user: User = User(id=3, name="John Doe", joined="2018-07-19") - -second_user_data = { - "id": 4, - "name": "Mary", - "joined": "2018-11-30", -} - -my_second_user: User = User(**second_user_data) -``` - -/// info - -`**second_user_data` یعنی: - -کلید ها و مقادیر دیکشنری `second_user_data` را مستقیما به عنوان ارگومان های key-value بفرست، که معادل است با : `User(id=4, name="Mary", joined="2018-11-30")` - -/// - -### پشتیبانی ویرایشگر - -تمام فریم ورک به گونه ای طراحی شده که استفاده از آن آسان و شهودی باشد، تمام تصمیمات حتی قبل از شروع توسعه بر روی چندین ویرایشگر آزمایش شده اند، تا از بهترین تجربه توسعه اطمینان حاصل شود. - -در آخرین نظرسنجی توسعه دهندگان پایتون کاملا مشخص بود که بیشترین ویژگی مورد استفاده از "تکمیل خودکار" است. - -تمام فریم ورک **FastAPI** برپایه ای برای براورده کردن این نیاز نیز ایجاد گشته است. تکمیل خودکار در همه جا کار میکند. - -شما به ندرت نیاز به بازگشت به مستندات را خواهید داشت. - -ببینید که چگونه ویرایشگر شما ممکن است به شما کمک کند: - -* در Visual Studio Code: - -![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) - -* در PyCharm: - -![editor support](https://fastapi.tiangolo.com/img/pycharm-completion.png) - -شما پیشنهاد های تکمیل خودکاری را خواهید گرفت که حتی ممکن است قبلا آن را غیرممکن تصور میکردید. به عنوان مثال کلید `price` در داخل بدنه JSON (که میتوانست تودرتو نیز باشد) که از یک درخواست آمده است. - -دیگر خبری از تایپ کلید اشتباهی، برگشتن به مستندات یا پایین بالا رفتن برای فهمیدن اینکه شما از `username` یا `user_name` استفاده کرده اید نیست. - -### مختصر - -FastAPI **پیش فرض** های معقولی برای همه چیز دارد، با قابلیت تنظیمات اختیاری در همه جا. تمام پارامترها را میتوانید برای انجام انچه نیاز دارید و برای تعریف API مورد نیاز خود به خوبی تنظیم کنید. - -اما به طور پیش فرض، همه چیز **کار میکند**. - -### اعتبارسنجی - -* اعتبارسنجی برای بیشتر (یا همه؟) **data type** های پایتون، شامل: - - * JSON objects (`dict`) - * آرایه های (‍‍‍‍`list`) JSON با قابلیت مشخص سازی تایپ ایتم های درون لیست. - * فیلد های رشته (`str`)، به همراه مشخص سازی حداقل و حداکثر طول رشته. - * اعداد (‍‍`int`,`float`) با حداقل و حداکثر مقدار و غیره. - -* اعتبارسنجی برای تایپ های عجیب تر، مثل: - * URL. - * Email. - * UUID. - * و غیره. - -تمام اعتبارسنجی ها توسط کتابخانه اثبات شده و قدرتمند **Pydantic** انجام میشود. - -### امنیت و احراز هویت - -امنیت و احرازهویت بدون هیچگونه ارتباط و مصالحه ای با پایگاه های داده یا مدل های داده ایجاد شده اند. - -تمام طرح های امنیتی در OpenAPI تعریف شده اند، از جمله: - -* . -* **OAuth2** (همچنین با **JWT tokens**). آموزش را در [OAuth2 with JWT](tutorial/security/oauth2-jwt.md){.internal-link target=\_blank} مشاهده کنید. -* کلید های API: - * Headers - * Query parameters - * Cookies، و غیره. - -به علاوه تمام ویژگی های امنیتی از **Statlette** (شامل **session cookies**) - -همه اینها به عنوان ابزارها و اجزای قابل استفاده ای ساخته شده اند که به راحتی با سیستم های شما، مخازن داده، پایگاه های داده رابطه ای و NoSQL و غیره ادغام میشوند. - -### Dependency Injection - -FastAPI شامل یک سیستم Dependency Injection بسیار آسان اما بسیار قدرتمند است. - -* حتی وابستگی ها نیز میتوانند وابستگی هایی داشته باشند و یک سلسله مراتب یا **"گرافی" از وابستگی ها** ایجاد کنند. - -* همه چیز توسط فریم ورک **به طور خودکار اداره میشود** - -* همه وابستگی ها میتوانند به داده های request ها نیاز داشته باشند و مستندات خودکار و محدودیت های path operation را **افزایش** دهند. - -* با قابلیت **اعتبارسنجی خودکار** حتی برای path operation parameter های تعریف شده در وابستگی ها. - -* پشتیبانی از سیستم های پیچیده احرازهویت کاربر، **اتصالات پایگاه داده** و غیره. - -* بدون هیچ ارتباطی با دیتابیس ها، فرانت اند و غیره. اما ادغام آسان و راحت با همه آنها. - -### پلاگین های نامحدود - -یا به عبارت دیگر، هیچ نیازی به آنها نیست، کد موردنیاز خود را وارد و استفاده کنید. - -هر یکپارچه سازی به گونه ای طراحی شده است که استفاده از آن بسیار ساده باشد (با وابستگی ها) که میتوانید با استفاده از همان ساختار و روشی که برای _path operation_ های خود استفاده کرده اید تنها در ۲ خط کد "پلاگین" برنامه خودتان را ایجاد کنید. - -### تست شده - -* 100% پوشش تست. - -* 100% کد بر اساس type annotate ها. - -* استفاده شده در اپلیکیشن های تولید - -## ویژگی های Starlette - -**FastAPI** کاملا (و براساس) با Starlette سازگار است. بنابراین، هرکد اضافی Starlette که دارید، نیز کار خواهد کرد. - -‍‍`FastAPI` در واقع یک زیرکلاس از `Starlette` است. بنابراین اگر از قبل Starlette را میشناسید یا با آن کار کرده اید، بیشتر قابلیت ها به همین روش کار خواهد کرد. - -با **FastAPI** شما تمام ویژگی های **Starlette** را خواهید داشت (زیرا FastAPI یک نسخه و نمونه به تمام معنا از Starlette است): - -* عملکرد به طورجدی چشمگیر. این یکی از سریعترین فریم ورک های موجود در پایتون است که همتراز با **نود جی اس** و **گو** است. -* پشتیبانی از **WebSocket**. -* تسک های درجریان در پس زمینه. -* رویداد های راه اندازی و متوفق شدن. -* تست کلاینت ساخته شده به روی HTTPX. -* **CORS**, GZip, فایل های استاتیک, پاسخ های جریانی. -* پشتیبانی از **نشست ها و کوکی ها**. -* 100% پوشش با تست. -* 100% کد براساس type annotate ها. - -## ویژگی های Pydantic - -**FastAPI** کاملا (و براساس) با Pydantic سازگار است. بنابراین هرکد Pydantic اضافی که داشته باشید، نیز کار خواهد کرد. - -از جمله کتابخانه های خارجی نیز مبتنی بر Pydantic میتوان به ORM و ODM ها برای دیتابیس ها اشاره کرد. - -این همچنین به این معناست که در خیلی از موارد میتوانید همان ابجکتی که از request میگیرید را **مستقیما به دیتابیس** بفرستید زیرا همه چیز به طور خودکار تأیید میشود. - -همین امر برعکس نیز صدق می‌کند، در بسیاری از موارد شما می‌توانید ابجکتی را که از پایگاه داده دریافت می‌کنید را **مستقیماً به کاربر** ارسال کنید. - -با FastAPI شما تمام ویژگی های Pydantic را دراختیار دارید (زیرا FastAPI برای تمام بخش مدیریت دیتا بر اساس Pydantic عمل میکند): - -* **خبری از گیج شدن نیست**: - * هیچ زبان خردی برای یادگیری تعریف طرحواره های جدید وجود ندارد. - * اگر تایپ های پایتون را میشناسید، نحوه استفاده از Pydantic را نیز میدانید. -* به خوبی با **IDE/linter/مغز** شما عمل میکند: - * به این دلیل که ساختار داده Pydantic فقط نمونه هایی از کلاس هایی هستند که شما تعریف میکنید، تکمیل خودکار، mypy، linting و مشاهده شما باید به درستی با داده های معتبر شما کار کنند. -* اعتبار سنجی **ساختارهای پیچیده**: - * استفاده از مدل های سلسله مراتبی Pydantic, `List` و `Dict` کتابخانه `typing` پایتون و غیره. - * و اعتبارسنج ها اجازه میدهند که طرحواره های داده پیچیده به طور واضح و آسان تعریف، بررسی و بر پایه JSON مستند شوند. - * شما میتوانید ابجکت های عمیقا تودرتو JSON را که همگی تایید شده و annotated شده اند را داشته باشید. -* **قابل توسعه**: - * Pydantic اجازه میدهد تا data type های سفارشی تعریف شوند یا میتوانید اعتبارسنجی را با روش هایی به روی مدل ها با validator decorator گسترش دهید. -* 100% پوشش با تست. diff --git a/docs/fa/docs/index.md b/docs/fa/docs/index.md deleted file mode 100644 index 0f1cbe8e60..0000000000 --- a/docs/fa/docs/index.md +++ /dev/null @@ -1,467 +0,0 @@ -# FastAPI - - - -

- FastAPI -

-

- فریم‌ورک FastAPI، کارایی بالا، یادگیری آسان، کدنویسی سریع، آماده برای استفاده در محیط پروداکشن -

-

- - Test - - - Coverage - - - Package version - - - Supported Python versions - -

- ---- - -**مستندات**: https://fastapi.tiangolo.com - -**کد منبع**: https://github.com/fastapi/fastapi - ---- -FastAPI یک وب فریم‌ورک مدرن و سریع (با کارایی بالا) برای ایجاد APIهای متنوع (وب، وب‌سوکت و غبره) با زبان پایتون نسخه +۳.۶ است. این فریم‌ورک با رعایت کامل راهنمای نوع داده (Type Hint) ایجاد شده است. - -ویژگی‌های کلیدی این فریم‌ورک عبارتند از: - -* **سرعت**: کارایی بسیار بالا و قابل مقایسه با **NodeJS** و **Go** (با تشکر از Starlette و Pydantic). [یکی از سریع‌ترین فریم‌ورک‌های پایتونی موجود](#_10). - -* **کدنویسی سریع**: افزایش ۲۰۰ تا ۳۰۰ درصدی سرعت توسعه قابلیت‌های جدید. * -* **باگ کمتر**: کاهش ۴۰ درصدی خطاهای انسانی (برنامه‌نویسی). * -* **هوشمندانه**: پشتیبانی فوق‌العاده در محیط‌های توسعه یکپارچه (IDE). تکمیل در همه بخش‌های کد. کاهش زمان رفع باگ. -* **آسان**: طراحی شده برای یادگیری و استفاده آسان. کاهش زمان مورد نیاز برای مراجعه به مستندات. -* **کوچک**: کاهش تکرار در کد. چندین قابلیت برای هر پارامتر (منظور پارامترهای ورودی تابع هندلر می‌باشد، به بخش خلاصه در همین صفحه مراجعه شود). باگ کمتر. -* **استوار**: ایجاد کدی آماده برای استفاده در محیط پروداکشن و تولید خودکار مستندات تعاملی -* **مبتنی بر استانداردها**: مبتنی بر (و منطبق با) استانداردهای متن باز مربوط به API: OpenAPI (سوگر سابق) و JSON Schema. - -* تخمین‌ها بر اساس تست‌های انجام شده در یک تیم توسعه داخلی که مشغول ایجاد برنامه‌های کاربردی واقعی بودند صورت گرفته است. - -## اسپانسرهای طلایی - - - -{% if sponsors %} -{% for sponsor in sponsors.gold -%} - -{% endfor %} -{% endif %} - - - -دیگر اسپانسرها - -## نظر دیگران در مورد FastAPI - -
[...] I'm using FastAPI a ton these days. [...] I'm actually planning to use it for all of my team's ML services at Microsoft. Some of them are getting integrated into the core Windows product and some Office products."
- -
Kabir Khan - Microsoft (ref)
- ---- - -
"We adopted the FastAPI library to spawn a RESTserver that can be queried to obtain predictions. [for Ludwig]"
- -
Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - Uber (ref)
- ---- - -
"Netflix is pleased to announce the open-source release of our crisis management orchestration framework: Dispatch! [built with FastAPI]"
- -
Kevin Glisson, Marc Vilanova, Forest Monsen - Netflix (ref)
- ---- - -
"I’m over the moon excited about FastAPI. It’s so fun!"
- -
Brian Okken - Python Bytes podcast host (ref)
- ---- - -
"Honestly, what you've built looks super solid and polished. In many ways, it's what I wanted Hug to be - it's really inspiring to see someone build that."
- -
Timothy Crosley - Hug creator (ref)
- ---- - -
"If you're looking to learn one modern framework for building REST APIs, check out FastAPI [...] It's fast, easy to use and easy to learn [...]"
- -
"We've switched over to FastAPI for our APIs [...] I think you'll like it [...]"
- -
Ines Montani - Matthew Honnibal - Explosion AI founders - spaCy creators (ref) - (ref)
- ---- - -## **Typer**, فریم‌ورکی معادل FastAPI برای کار با واسط خط فرمان - - - -اگر در حال ساختن برنامه‌ای برای استفاده در CLI (به جای استفاده در وب) هستید، می‌توانید از **Typer**. استفاده کنید. - -**Typer** دوقلوی کوچکتر FastAPI است و قرار است معادلی برای FastAPI در برنامه‌های CLI باشد.️ 🚀 - -## نیازمندی‌ها - -پایتون +۳.۶ - -FastAPI مبتنی بر ابزارهای قدرتمند زیر است: - -* فریم‌ورک Starlette برای بخش وب. -* کتابخانه Pydantic برای بخش داده‌. - -## نصب - -
- -```console -$ pip install fastapi - ----> 100% -``` - -
- -نصب یک سرور پروداکشن نظیر Uvicorn یا Hypercorn نیز جزء نیازمندی‌هاست. - -
- -```console -$ pip install "uvicorn[standard]" - ----> 100% -``` - -
- -## مثال - -### ایجاد کنید -* فایلی به نام `main.py` با محتوای زیر ایجاد کنید: - -```Python -from typing import Union - -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} -``` - -
-همچنین می‌توانید از async def... نیز استفاده کنید - -اگر در کدتان از `async` / `await` استفاده می‌کنید، از `async def` برای تعریف تابع خود استفاده کنید: - -```Python hl_lines="9 14" -from typing import Optional - -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -async def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -async def read_item(item_id: int, q: Optional[str] = None): - return {"item_id": item_id, "q": q} -``` - -**توجه**: - -اگر با `async / await` آشنا نیستید، به بخش _"عجله‌ دارید?"_ در صفحه درباره `async` و `await` در مستندات مراجعه کنید. - - -
- -### اجرا کنید - -با استفاده از دستور زیر سرور را اجرا کنید: - -
- -```console -$ uvicorn main:app --reload - -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -INFO: Started reloader process [28720] -INFO: Started server process [28722] -INFO: Waiting for application startup. -INFO: Application startup complete. -``` - -
- -
-درباره دستور uvicorn main:app --reload... - -دستور `uvicorn main:app` شامل موارد زیر است: - -* `main`: فایل `main.py` (ماژول پایتون ایجاد شده). -* `app`: شیء ایجاد شده در فایل `main.py` در خط `app = FastAPI()`. -* `--reload`: ریستارت کردن سرور با تغییر کد. تنها در هنگام توسعه از این گزینه استفاده شود.. - -
- -### بررسی کنید - -آدرس http://127.0.0.1:8000/items/5?q=somequery را در مرورگر خود باز کنید. - -پاسخ JSON زیر را مشاهده خواهید کرد: - -```JSON -{"item_id": 5, "q": "somequery"} -``` - -تا اینجا شما APIای ساختید که: - -* درخواست‌های HTTP به _مسیرهای_ `/` و `/items/{item_id}` را دریافت می‌کند. -* هردو _مسیر_ عملیات (یا HTTP _متد_) `GET` را پشتیبانی می‌کند. -* _مسیر_ `/items/{item_id}` شامل _پارامتر مسیر_ `item_id` از نوع `int` است. -* _مسیر_ `/items/{item_id}` شامل _پارامتر پرسمان_ اختیاری `q` از نوع `str` است. - -### مستندات API تعاملی - -حال به آدرس http://127.0.0.1:8000/docs بروید. - -مستندات API تعاملی (ایجاد شده به کمک Swagger UI) را مشاهده خواهید کرد: - -![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) - -### مستندات API جایگزین - -حال به آدرس http://127.0.0.1:8000/redoc بروید. - -مستندات خودکار دیگری را مشاهده خواهید کرد که به کمک ReDoc ایجاد می‌شود: - -![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) - -## تغییر مثال - -حال فایل `main.py` را مطابق زیر ویرایش کنید تا بتوانید بدنه یک درخواست `PUT` را دریافت کنید. - -به کمک Pydantic بدنه درخواست را با انواع استاندارد پایتون تعریف کنید. - -```Python hl_lines="4 9-12 25-27" -from typing import Optional - -from fastapi import FastAPI -from pydantic import BaseModel - -app = FastAPI() - - -class Item(BaseModel): - name: str - price: float - is_offer: Union[bool, None] = None - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} - - -@app.put("/items/{item_id}") -def update_item(item_id: int, item: Item): - return {"item_name": item.name, "item_id": item_id} -``` - -سرور به صورت خودکار ری‌استارت می‌شود (زیرا پیشتر از گزینه `--reload` در دستور `uvicorn` استفاده کردیم). - -### تغییر مستندات API تعاملی - -مجددا به آدرس http://127.0.0.1:8000/docs بروید. - -* مستندات API تعاملی به صورت خودکار به‌روز شده است و شامل بدنه تعریف شده در مرحله قبل است: - -![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) - -* روی دکمه "Try it out" کلیک کنید، اکنون می‌توانید پارامترهای مورد نیاز هر API را مشخص کرده و به صورت مستقیم با آنها تعامل کنید: - -![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) - -* سپس روی دکمه "Execute" کلیک کنید، خواهید دید که واسط کاربری با APIهای تعریف شده ارتباط برقرار کرده، پارامترهای مورد نیاز را به آن‌ها ارسال می‌کند، سپس نتایج را دریافت کرده و در صفحه نشان می‌دهد: - -![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) - -### تغییر مستندات API جایگزین - -حال به آدرس http://127.0.0.1:8000/redoc بروید. - -* خواهید دید که مستندات جایگزین نیز به‌روزرسانی شده و شامل پارامتر پرسمان و بدنه تعریف شده می‌باشد: - -![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) - -### خلاصه - -به طور خلاصه شما **یک بار** انواع پارامترها، بدنه و غیره را به عنوان پارامترهای ورودی تابع خود تعریف می‌کنید. - - این کار را با استفاده از انواع استاندارد و مدرن موجود در پایتون انجام می‌دهید. - -نیازی به یادگیری نحو جدید یا متدها و کلاس‌های یک کتابخانه بخصوص و غیره نیست. - -تنها **پایتون +۳.۶**. - -به عنوان مثال برای یک پارامتر از نوع `int`: - -```Python -item_id: int -``` - -یا برای یک مدل پیچیده‌تر مثل `Item`: - -```Python -item: Item -``` - -...و با همین اعلان تمامی قابلیت‌های زیر در دسترس قرار می‌گیرد: - -* پشتیبانی ویرایشگر متنی شامل: - * تکمیل کد. - * بررسی انواع داده. -* اعتبارسنجی داده: - * خطاهای خودکار و مشخص در هنگام نامعتبر بودن داده. - * اعتبارسنجی، حتی برای اشیاء JSON تو در تو. -* تبدیل داده ورودی: که از شبکه رسیده به انواع و داد‌ه‌ پایتونی. این داده‌ شامل: - * JSON. - * پارامترهای مسیر. - * پارامترهای پرسمان. - * کوکی‌ها. - * سرآیند‌ها (هدرها). - * فرم‌ها. - * فایل‌ها. -* تبدیل داده خروجی: تبدیل از انواع و داده‌ پایتون به داده شبکه (مانند JSON): - * تبدیل انواع داده پایتونی (`str`, `int`, `float`, `bool`, `list` و غیره). - * اشیاء `datetime`. - * اشیاء `UUID`. - * qمدل‌های پایگاه‌داده. - * و موارد بیشمار دیگر. -* دو مدل مستند API تعاملی خودکار : - * Swagger UI. - * ReDoc. - ---- - -به مثال قبلی باز می‌گردیم، در این مثال **FastAPI** موارد زیر را انجام می‌دهد: - -* اعتبارسنجی اینکه پارامتر `item_id` در مسیر درخواست‌های `GET` و `PUT` موجود است. -* اعتبارسنجی اینکه پارامتر `item_id` در درخواست‌های `GET` و `PUT` از نوع `int` است. - * اگر غیر از این موارد باشد، سرویس‌گیرنده خطای مفید و مشخصی دریافت خواهد کرد. -* بررسی وجود پارامتر پرسمان اختیاری `q` (مانند `http://127.0.0.1:8000/items/foo?q=somequery`) در درخواست‌های `GET`. - * از آنجا که پارامتر `q` با `= None` مقداردهی شده است، این پارامتر اختیاری است. - * اگر از مقدار اولیه `None` استفاده نکنیم، این پارامتر الزامی خواهد بود (همانند بدنه درخواست در درخواست `PUT`). -* برای درخواست‌های `PUT` به آدرس `/items/{item_id}`، بدنه درخواست باید از نوع JSON تعریف شده باشد: - * بررسی اینکه بدنه شامل فیلدی با نام `name` و از نوع `str` است. - * بررسی اینکه بدنه شامل فیلدی با نام `price` و از نوع `float` است. - * بررسی اینکه بدنه شامل فیلدی اختیاری با نام `is_offer` است، که در صورت وجود باید از نوع `bool` باشد. - * تمامی این موارد برای اشیاء JSON در هر عمقی قابل بررسی می‌باشد. -* تبدیل از/به JSON به صورت خودکار. -* مستندسازی همه چیز با استفاده از OpenAPI، که می‌توان از آن برای موارد زیر استفاده کرد: - * سیستم مستندات تعاملی. - * تولید خودکار کد سرویس‌گیرنده‌ در زبان‌های برنامه‌نویسی بیشمار. -* فراهم سازی ۲ مستند تعاملی مبتنی بر وب به صورت پیش‌فرض. - ---- - -موارد ذکر شده تنها پاره‌ای از ویژگی‌های بیشمار FastAPI است اما ایده‌ای کلی از طرز کار آن در اختیار قرار می‌دهد. - -خط زیر را به این صورت تغییر دهید: - -```Python - return {"item_name": item.name, "item_id": item_id} -``` - -از: - -```Python - ... "item_name": item.name ... -``` - -به: - -```Python - ... "item_price": item.price ... -``` - -در حین تایپ کردن توجه کنید که چگونه ویرایش‌گر، ویژگی‌های کلاس `Item` را تشخیص داده و به تکمیل خودکار آنها کمک می‌کند: - -![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) - -برای مشاهده مثال‌های کامل‌تر که شامل قابلیت‌های بیشتری از FastAPI باشد به بخش آموزش - راهنمای کاربر مراجعه کنید. - -**هشدار اسپویل**: بخش آموزش - راهنمای کاربر شامل موارد زیر است: - -* اعلان **پارامترهای** موجود در بخش‌های دیگر درخواست، شامل: **سرآیند‌ (هدر)ها**، **کوکی‌ها**، **فیلد‌های فرم** و **فایل‌ها**. -* چگونگی تنظیم **محدودیت‌های اعتبارسنجی** به عنوان مثال `maximum_length` یا `regex`. -* سیستم **Dependency Injection** قوی و کاربردی. -* امنیت و تایید هویت, شامل پشتیبانی از **OAuth2** مبتنی بر **JWT tokens** و **HTTP Basic**. -* تکنیک پیشرفته برای تعریف **مدل‌های چند سطحی JSON** (بر اساس Pydantic). -* قابلیت‌های اضافی دیگر (بر اساس Starlette) شامل: - * **وب‌سوکت** - * **GraphQL** - * تست‌های خودکار آسان مبتنی بر HTTPX و `pytest` - * **CORS** - * **Cookie Sessions** - * و موارد بیشمار دیگر. - -## کارایی - -معیار (بنچمارک‌)های مستقل TechEmpower حاکی از آن است که برنامه‌های **FastAPI** که تحت Uvicorn اجرا می‌شود، یکی از سریع‌ترین فریم‌ورک‌های مبتنی بر پایتون، است که کمی ضعیف‌تر از Starlette و Uvicorn عمل می‌کند (فریم‌ورک و سروری که FastAPI بر اساس آنها ایجاد شده است) (*) - -برای درک بهتری از این موضوع به بخش بنچ‌مارک‌ها مراجعه کنید. - -## نیازمندی‌های اختیاری - -استفاده شده توسط Pydantic: - -* email-validator - برای اعتبارسنجی آدرس‌های ایمیل. - -استفاده شده توسط Starlette: - -* HTTPX - در صورتی که می‌خواهید از `TestClient` استفاده کنید. -* aiofiles - در صورتی که می‌خواهید از `FileResponse` و `StaticFiles` استفاده کنید. -* jinja2 - در صورتی که بخواهید از پیکربندی پیش‌فرض برای قالب‌ها استفاده کنید. -* python-multipart - در صورتی که بخواهید با استفاده از `request.form()` از قابلیت "تجزیه (parse)" فرم استفاده کنید. -* itsdangerous - در صورتی که بخواید از `SessionMiddleware` پشتیبانی کنید. -* pyyaml - برای پشتیبانی `SchemaGenerator` در Starlet (به احتمال زیاد برای کار کردن با FastAPI به آن نیازی پیدا نمی‌کنید). -* graphene - در صورتی که از `GraphQLApp` پشتیبانی می‌کنید. - -استفاده شده توسط FastAPI / Starlette: - -* uvicorn - برای سرور اجرا کننده برنامه وب. -* orjson - در صورتی که بخواهید از `ORJSONResponse` استفاده کنید. -* ujson - در صورتی که بخواهید از `UJSONResponse` استفاده کنید. - -می‌توان همه این موارد را با استفاده از دستور `pip install fastapi[all]`. به صورت یکجا نصب کرد. - -## لایسنس - -این پروژه مشمول قوانین و مقررات لایسنس MIT است. diff --git a/docs/fa/docs/learn/index.md b/docs/fa/docs/learn/index.md deleted file mode 100644 index 06aa7f00e7..0000000000 --- a/docs/fa/docs/learn/index.md +++ /dev/null @@ -1,5 +0,0 @@ -# یادگیری - -اینجا بخش‌های مقدماتی و آموزش‌هایی هستن که برای یادگیری **FastAPI** بهت کمک می‌کنن. - -می‌تونی اینو یه **کتاب**، یه **دوره آموزشی**، یا راه **رسمی** و پیشنهادی برای یادگیری FastAPI در نظر بگیری. 😎 diff --git a/docs/fa/docs/python-types.md b/docs/fa/docs/python-types.md deleted file mode 100644 index c428acbf74..0000000000 --- a/docs/fa/docs/python-types.md +++ /dev/null @@ -1,578 +0,0 @@ -# مقدمه‌ای بر انواع نوع در پایتون - -پایتون از "نوع‌نما"های اختیاری (که بهشون "type hints" یا "type annotations" هم می‌گن) پشتیبانی می‌کنه. - -این **"نوع‌نماها"** یا annotationها یه سینتکس خاص هستن که بهت اجازه می‌دن نوع یه متغیر رو مشخص کنی. - -با مشخص کردن نوع متغیرها، ویرایشگرها و ابزارها می‌تونن پشتیبانی بهتری بهت بدن. - -این فقط یه **آموزش سریع / یادآوری** در مورد نوع‌نماهای پایتونه. فقط حداقل چیزایی که برای استفاده ازشون با **FastAPI** لازمه رو پوشش می‌ده... که در واقع خیلی کمه. - -**FastAPI** کاملاً بر پایه این نوع‌نماهاست و این بهش کلی مزیت و فایده می‌ده. - -ولی حتی اگه هیچ‌وقت از **FastAPI** استفاده نکنی، بازم یادگیری یه کم در موردشون به نفعته. - -/// note - -اگه حرفه‌ای پایتونی و همه‌چیز رو در مورد نوع‌نماها می‌دونی، برو سراغ فصل بعدی. - -/// - -## انگیزه - -بیاید با یه مثال ساده شروع کنیم: - -{* ../../docs_src/python_types/tutorial001.py *} - -وقتی این برنامه رو اجرا کنی، خروجی اینه: - -``` -John Doe -``` - -این تابع این کارا رو می‌کنه: - -* یه `first_name` و `last_name` می‌گیره. -* حرف اول هر کدوم رو با `title()` بزرگ می‌کنه. -* ترکیبشون می‌کنه با یه فاصله وسطشون. - -{* ../../docs_src/python_types/tutorial001.py hl[2] *} - -### ویرایشش کن - -این یه برنامه خیلی ساده‌ست. - -ولی حالا تصور کن داری از صفر می‌نویسیش. - -یه جایی شروع کردی به تعریف تابع، پارامترهات آماده‌ست... - -ولی بعد باید "اون متدی که حرف اول رو بزرگ می‌کنه" رو صدا کنی. - -آیا اسمش `upper` بود؟ یا `uppercase`؟ شاید `first_uppercase`؟ یا `capitalize`؟ - -بعد، با دوست قدیمی برنامه‌نویسا، تکمیل خودکار ویرایشگر، امتحان می‌کنی. - -پارامتر اول تابع، `first_name` رو تایپ می‌کنی، بعد یه نقطه (`.`) می‌ذاری و `Ctrl+Space` رو می‌زنی تا تکمیل خودکار بیاد. - -ولی متأسفانه، چیز مفیدی نمی‌گیری: - - - -### نوع اضافه کن - -بیا فقط یه خط از نسخه قبلی رو تغییر بدیم. - -دقیقاً این بخش، پارامترهای تابع رو، از: - -```Python - first_name, last_name -``` - -به: - -```Python - first_name: str, last_name: str -``` - -عوض می‌کنیم. - -همینه. - -اینا همون "نوع‌نماها" هستن: - -{* ../../docs_src/python_types/tutorial002.py hl[1] *} - -این با تعریف مقدار پیش‌فرض فرق داره، مثل: - -```Python - first_name="john", last_name="doe" -``` - -یه چیز متفاوته. - -ما از دونقطه (`:`) استفاده می‌کنیم، نه علامت مساوی (`=`)‌. - -و اضافه کردن نوع‌نماها معمولاً چیزی که اتفاق می‌افته رو از چیزی که بدون اونا می‌افتاد تغییر نمی‌ده. - -ولی حالا، دوباره تصور کن وسط ساختن اون تابع هستی، ولی این بار با نوع‌نماها. - -توی همون نقطه، سعی می‌کنی تکمیل خودکار رو با `Ctrl+Space` فعال کنی و اینو می‌بینی: - - - -با این، می‌تونی اسکرول کنی، گزینه‌ها رو ببینی، تا وقتی که اون چیزی که "به نظرت آشنا میاد" رو پیدا کنی: - - - -## انگیزه بیشتر - -این تابع رو چک کن، الان نوع‌نما داره: - -{* ../../docs_src/python_types/tutorial003.py hl[1] *} - -چون ویرایشگر نوع متغیرها رو می‌دونه، فقط تکمیل خودکار نمی‌گیری، بلکه چک خطاها هم داری: - - - -حالا می‌دونی که باید درستش کنی، `age` رو با `str(age)` به یه رشته تبدیل کنی: - -{* ../../docs_src/python_types/tutorial004.py hl[2] *} - -## تعریف نوع‌ها - -تازه اصلی‌ترین جا برای تعریف نوع‌نماها رو دیدی. به‌عنوان پارامترهای تابع. - -این هم اصلی‌ترین جاییه که با **FastAPI** ازشون استفاده می‌کنی. - -### نوع‌های ساده - -می‌تونی همه نوع‌های استاندارد پایتون رو تعریف کنی، نه فقط `str`. - -مثلاً می‌تونی از اینا استفاده کنی: - -* `int` -* `float` -* `bool` -* `bytes` - -{* ../../docs_src/python_types/tutorial005.py hl[1] *} - -### نوع‌های عمومی با پارامترهای نوع - -یه سری ساختار داده هستن که می‌تونن مقدارهای دیگه رو نگه دارن، مثل `dict`، `list`، `set` و `tuple`. و مقدارهای داخلیشون هم می‌تونن نوع خودشون رو داشته باشن. - -به این نوع‌ها که نوع‌های داخلی دارن می‌گن "**عمومی**" یا "generic". و می‌شه اونا رو تعریف کرد، حتی با نوع‌های داخلیشون. - -برای تعریف این نوع‌ها و نوع‌های داخلیشون، می‌تونی از ماژول استاندارد پایتون `typing` استفاده کنی. این ماژول مخصوص پشتیبانی از نوع‌نماهاست. - -#### نسخه‌های جدیدتر پایتون - -سینتکس با استفاده از `typing` با همه نسخه‌ها، از پایتون 3.6 تا جدیدترین‌ها، از جمله پایتون 3.9، 3.10 و غیره **سازگاره**. - -با پیشرفت پایتون، **نسخه‌های جدیدتر** پشتیبانی بهتری برای این نوع‌نماها دارن و توی خیلی موارد حتی لازم نیست ماژول `typing` رو وارد کنی و ازش برای تعریف نوع‌نماها استفاده کنی. - -اگه بتونی برای پروژه‌ات از یه نسخه جدیدتر پایتون استفاده کنی، می‌تونی از این سادگی اضافه بهره ببری. - -توی همه مستندات، مثال‌هایی هستن که با هر نسخه پایتون سازگارن (وقتی تفاوتی هست). - -مثلاً "**Python 3.6+**" یعنی با پایتون 3.6 یا بالاتر (مثل 3.7، 3.8، 3.9، 3.10 و غیره) سازگاره. و "**Python 3.9+**" یعنی با پایتون 3.9 یا بالاتر (مثل 3.10 و غیره) سازگاره. - -اگه بتونی از **جدیدترین نسخه‌های پایتون** استفاده کنی، از مثال‌های نسخه آخر استفاده کن، چون اونا **بهترین و ساده‌ترین سینتکس** رو دارن، مثلاً "**Python 3.10+**". - -#### لیست - -مثلاً، بیایم یه متغیر تعریف کنیم که یه `list` از `str` باشه. - -//// tab | Python 3.9+ - -متغیر رو با همون سینتکس دونقطه (`:`) تعریف کن. - -به‌عنوان نوع، `list` رو بذار. - -چون لیست یه نوعه که نوع‌های داخلی داره، اونا رو توی کروشه‌ها می‌ذاری: - -```Python hl_lines="1" -{!> ../../docs_src/python_types/tutorial006_py39.py!} -``` - -//// - -//// tab | Python 3.8+ - -از `typing`، `List` رو (با `L` بزرگ) وارد کن: - -```Python hl_lines="1" -{!> ../../docs_src/python_types/tutorial006.py!} -``` - -متغیر رو با همون سینتکس دونقطه (`:`) تعریف کن. - -به‌عنوان نوع، `List` رو که از `typing` وارد کردی بذار. - -چون لیست یه نوعه که نوع‌های داخلی داره، اونا رو توی کروشه‌ها می‌ذاری: - -```Python hl_lines="4" -{!> ../../docs_src/python_types/tutorial006.py!} -``` - -//// - -/// info - -اون نوع‌های داخلی توی کروشه‌ها بهشون "پارامترهای نوع" می‌گن. - -توی این مورد، `str` پارامتر نوعیه که به `List` (یا `list` توی پایتون 3.9 و بالاتر) پاس داده شده. - -/// - -یعنی: "متغیر `items` یه `list` هست، و هر کدوم از آیتم‌های این لیست یه `str` هستن". - -/// tip - -اگه از پایتون 3.9 یا بالاتر استفاده می‌کنی، لازم نیست `List` رو از `typing` وارد کنی، می‌تونی همون نوع معمولی `list` رو به جاش استفاده کنی. - -/// - -با این کار، ویرایشگرت حتی وقتی داری آیتم‌های لیست رو پردازش می‌کنی بهت کمک می‌کنه: - - - -بدون نوع‌ها، رسیدن به این تقریباً غیرممکنه. - -توجه کن که متغیر `item` یکی از عناصر توی لیست `items` هست. - -و با این حال، ویرایشگر می‌دونه که یه `str` هست و براش پشتیبانی می‌ده. - -#### تاپل و ست - -برای تعریف `tuple`ها و `set`ها هم همین کار رو می‌کنی: - -//// tab | Python 3.9+ - -```Python hl_lines="1" -{!> ../../docs_src/python_types/tutorial007_py39.py!} -``` - -//// - -//// tab | Python 3.8+ - -```Python hl_lines="1 4" -{!> ../../docs_src/python_types/tutorial007.py!} -``` - -//// - -یعنی: - -* متغیر `items_t` یه `tuple` با 3 تا آیتمه، یه `int`، یه `int` دیگه، و یه `str`. -* متغیر `items_s` یه `set` هست، و هر کدوم از آیتم‌هاش از نوع `bytes` هستن. - -#### دیکشنری - -برای تعریف یه `dict`، 2 تا پارامتر نوع می‌دی، که با کاما از هم جدا شدن. - -پارامتر نوع اول برای کلیدهای `dict` هست. - -پارامتر نوع دوم برای مقدارهای `dict` هست: - -//// tab | Python 3.9+ - -```Python hl_lines="1" -{!> ../../docs_src/python_types/tutorial008_py39.py!} -``` - -//// - -//// tab | Python 3.8+ - -```Python hl_lines="1 4" -{!> ../../docs_src/python_types/tutorial008.py!} -``` - -//// - -یعنی: - -* متغیر `prices` یه `dict` هست: - * کلیدهای این `dict` از نوع `str` هستن (مثلاً اسم هر آیتم). - * مقدارهای این `dict` از نوع `float` هستن (مثلاً قیمت هر آیتم). - -#### اتحادیه - -می‌تونی تعریف کنی که یه متغیر می‌تونه هر کدوم از **چند تا نوع** باشه، مثلاً یه `int` یا یه `str`. - -توی پایتون 3.6 و بالاتر (از جمله پایتون 3.10) می‌تونی از نوع `Union` توی `typing` استفاده کنی و نوع‌های ممکن رو توی کروشه‌ها بذاری. - -توی پایتون 3.10 یه **سینتکس جدید** هم هست که می‌تونی نوع‌های ممکن رو با یه خط عمودی (`|`) جدا کنی. - -//// tab | Python 3.10+ - -```Python hl_lines="1" -{!> ../../docs_src/python_types/tutorial008b_py310.py!} -``` - -//// - -//// tab | Python 3.8+ - -```Python hl_lines="1 4" -{!> ../../docs_src/python_types/tutorial008b.py!} -``` - -//// - -توی هر دو حالت یعنی `item` می‌تونه یه `int` یا یه `str` باشه. - -#### شاید `None` - -می‌تونی تعریف کنی که یه مقدار می‌تونه یه نوع باشه، مثلاً `str`، ولی می‌تونه `None` هم باشه. - -توی پایتون 3.6 و بالاتر (از جمله پایتون 3.10) می‌تونی با وارد کردن و استفاده از `Optional` از ماژول `typing` اینو تعریف کنی. - -```Python hl_lines="1 4" -{!../../docs_src/python_types/tutorial009.py!} -``` - -استفاده از `Optional[str]` به جای فقط `str` به ویرایشگر کمک می‌کنه خطاهایی که ممکنه فکر کنی یه مقدار همیشه `str` هست رو پیدا کنه، در حالی که می‌تونه `None` هم باشه. - -`Optional[Something]` در واقع میان‌بر برای `Union[Something, None]` هست، این دو تا معادلن. - -یعنی توی پایتون 3.10، می‌تونی از `Something | None` استفاده کنی: - -//// tab | Python 3.10+ - -```Python hl_lines="1" -{!> ../../docs_src/python_types/tutorial009_py310.py!} -``` - -//// - -//// tab | Python 3.8+ - -```Python hl_lines="1 4" -{!> ../../docs_src/python_types/tutorial009.py!} -``` - -//// - -//// tab | Python 3.8+ جایگزین - -```Python hl_lines="1 4" -{!> ../../docs_src/python_types/tutorial009b.py!} -``` - -//// - -#### استفاده از `Union` یا `Optional` - -اگه از نسخه پایتون زیر 3.10 استفاده می‌کنی، یه نکته از دید خیلی **شخصی** خودم: - -* 🚨 از `Optional[SomeType]` استفاده نکن -* به جاش ✨ **از `Union[SomeType, None]` استفاده کن** ✨. - -هر دو معادلن و زیر پوسته یکی‌ان، ولی من `Union` رو به `Optional` ترجیح می‌دم چون کلمه "**اختیاری**" انگار暗示 می‌کنه که مقدار اختیاریه، در حالی که در واقع یعنی "می‌تونه `None` باشه"، حتی اگه اختیاری نباشه و هنوز لازم باشه. - -فکر می‌کنم `Union[SomeType, None]` واضح‌تر نشون می‌ده چی معنی می‌ده. - -فقط بحث کلمات و اسم‌هاست. ولی این کلمات می‌تونن رو طرز فکر تو و تیمت نسبت به کد تأثیر بذارن. - -به‌عنوان مثال، این تابع رو ببین: - -{* ../../docs_src/python_types/tutorial009c.py hl[1,4] *} - -پارامتر `name` به‌عنوان `Optional[str]` تعریف شده، ولی **اختیاری نیست**، نمی‌تونی تابع رو بدون پارامتر صدا کنی: - -```Python -say_hi() # اوه نه، این خطا می‌ده! 😱 -``` - -پارامتر `name` **هنوز لازمه** (نه *اختیاری*) چون مقدار پیش‌فرض نداره. با این حال، `name` مقدار `None` رو قبول می‌کنه: - -```Python -say_hi(name=None) # این کار می‌کنه، None معتبره 🎉 -``` - -خبر خوب اینه که وقتی رو پایتون 3.10 باشی، لازم نیست نگران این باشی، چون می‌تونی به‌سادگی از `|` برای تعریف اتحادیه نوع‌ها استفاده کنی: - -{* ../../docs_src/python_types/tutorial009c_py310.py hl[1,4] *} - -اون موقع دیگه لازم نیست نگران اسم‌هایی مثل `Optional` و `Union` باشی. 😎 - -#### نوع‌های عمومی - -این نوع‌هایی که پارامترهای نوع رو توی کروشه‌ها می‌گیرن بهشون **نوع‌های عمومی** یا **Generics** می‌گن، مثلاً: - -//// tab | Python 3.10+ - -می‌تونی از همون نوع‌های داخلی به‌عنوان نوع‌های عمومی استفاده کنی (با کروشه‌ها و نوع‌ها داخلشون): - -* `list` -* `tuple` -* `set` -* `dict` - -و همون‌طور که توی پایتون 3.8 بود، از ماژول `typing`: - -* `Union` -* `Optional` (همون‌طور که توی پایتون 3.8 بود) -* ...و بقیه. - -توی پایتون 3.10، به‌عنوان جایگزین برای استفاده از نوع‌های عمومی `Union` و `Optional`، می‌تونی از خط عمودی (`|`) برای تعریف اتحادیه نوع‌ها استفاده کنی، که خیلی بهتر و ساده‌تره. - -//// - -//// tab | Python 3.9+ - -می‌تونی از همون نوع‌های داخلی به‌عنوان نوع‌های عمومی استفاده کنی (با کروشه‌ها و نوع‌ها داخلشون): - -* `list` -* `tuple` -* `set` -* `dict` - -و همون‌طور که توی پایتون 3.8 بود، از ماژول `typing`: - -* `Union` -* `Optional` -* ...و بقیه. - -//// - -//// tab | Python 3.8+ - -* `List` -* `Tuple` -* `Set` -* `Dict` -* `Union` -* `Optional` -* ...و بقیه. - -//// - -### کلاس‌ها به‌عنوان نوع - -می‌تونی یه کلاس رو هم به‌عنوان نوع یه متغیر تعریف کنی. - -فرض کن یه کلاس `Person` داری، با یه نام: - -{* ../../docs_src/python_types/tutorial010.py hl[1:3] *} - -بعد می‌تونی یه متغیر رو از نوع `Person` تعریف کنی: - -{* ../../docs_src/python_types/tutorial010.py hl[6] *} - -و بعد، دوباره، همه پشتیبانی ویرایشگر رو داری: - - - -توجه کن که این یعنی "`one_person` یه **نمونه** از کلاس `Person` هست". - -یعنی "`one_person` خود **کلاس** به اسم `Person` نیست". - -## مدل‌های Pydantic - -Pydantic یه کتابخونه پایتونه برای اعتبارسنجی داده‌ها. - -"شکل" داده‌ها رو به‌عنوان کلاس‌هایی با ویژگی‌ها تعریف می‌کنی. - -و هر ویژگی یه نوع داره. - -بعد یه نمونه از اون کلاس رو با یه سری مقدار می‌سازی و اون مقدارها رو اعتبارسنجی می‌کنه، به نوع مناسب تبدیلشون می‌کنه (اگه لازم باشه) و یه شیء با همه داده‌ها بهت می‌ده. - -و با اون شیء نهایی همه پشتیبانی ویرایشگر رو می‌گیری. - -یه مثال از مستندات رسمی Pydantic: - -//// tab | Python 3.10+ - -```Python -{!> ../../docs_src/python_types/tutorial011_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!> ../../docs_src/python_types/tutorial011_py39.py!} -``` - -//// - -//// tab | Python 3.8+ - -```Python -{!> ../../docs_src/python_types/tutorial011.py!} -``` - -//// - -/// info - -برای اطلاعات بیشتر در مورد Pydantic، مستنداتش رو چک کن. - -/// - -**FastAPI** کاملاً بر پایه Pydantic هست. - -توی [آموزش - راهنمای کاربر](tutorial/index.md){.internal-link target=_blank} خیلی بیشتر از اینا رو توی عمل می‌بینی. - -/// tip - -Pydantic یه رفتار خاص داره وقتی از `Optional` یا `Union[Something, None]` بدون مقدار پیش‌فرض استفاده می‌کنی، می‌تونی توی مستندات Pydantic در مورد فیلدهای اختیاری لازم بیشتر بخونی. - -/// - -## نوع‌نماها با Annotationهای متادیتا - -پایتون یه قابلیت هم داره که بهت اجازه می‌ده **متادیتا اضافی** رو توی این نوع‌نماها بذاری با استفاده از `Annotated`. - -//// tab | Python 3.9+ - -توی پایتون 3.9، `Annotated` بخشی از کتابخونه استاندارده، پس می‌تونی از `typing` واردش کنی. - -```Python hl_lines="1 4" -{!> ../../docs_src/python_types/tutorial013_py39.py!} -``` - -//// - -//// tab | Python 3.8+ - -توی نسخه‌های زیر پایتون 3.9، `Annotated` رو از `typing_extensions` وارد می‌کنی. - -با **FastAPI** از قبل نصب شده. - -```Python hl_lines="1 4" -{!> ../../docs_src/python_types/tutorial013.py!} -``` - -//// - -خود پایتون با این `Annotated` کاری نمی‌کنه. و برای ویرایشگرها و ابزارهای دیگه، نوع هنوز `str` هست. - -ولی می‌تونی از این فضا توی `Annotated` استفاده کنی تا به **FastAPI** متادیتای اضافی در مورد اینکه چطور می‌خوای برنامه‌ات رفتار کنه بدی. - -نکته مهم اینه که **اولین *پارامتر نوع*** که به `Annotated` می‌دی، **نوع واقعی** هست. بقیش فقط متادیتا برای ابزارهای دیگه‌ست. - -الان فقط باید بدونی که `Annotated` وجود داره، و اینکه پایتون استاندارده. 😎 - -بعداً می‌بینی که چقدر **قوی** می‌تونه باشه. - -/// tip - -اینکه این **پایتون استاندارده** یعنی هنوز **بهترین تجربه توسعه‌دهنده** رو توی ویرایشگرت، با ابزارهایی که برای تحلیل و بازسازی کدت استفاده می‌کنی و غیره می‌گیری. ✨ - -و همین‌طور کدت با خیلی از ابزارها و کتابخونه‌های دیگه پایتون خیلی سازگار می‌مونه. 🚀 - -/// - -## نوع‌نماها توی **FastAPI** - -**FastAPI** از این نوع‌نماها استفاده می‌کنه تا چند تا کار بکنه. - -با **FastAPI** پارامترها رو با نوع‌نماها تعریف می‌کنی و اینا رو می‌گیری: - -* **پشتیبانی ویرایشگر**. -* **چک نوع‌ها**. - -...و **FastAPI** از همون تعریف‌ها برای اینا استفاده می‌کنه: - -* **تعریف نیازها**: از پارامترهای مسیر درخواست، پارامترهای کوئری، هدرها، بدنه‌ها، وابستگی‌ها و غیره. -* **تبدیل داده**: از درخواست به نوع مورد نیاز. -* **اعتبارسنجی داده**: که از هر درخواست میاد: - * تولید **خطاهای خودکار** که به کلاینت برمی‌گرده وقتی داده نامعتبره. -* **مستندسازی** API با استفاده از OpenAPI: - * که بعدش توسط رابط‌های کاربری مستندات تعاملی خودکار استفاده می‌شه. - -اینا شاید همه‌ش انتزاعی به نظر بیاد. نگران نباش. همه اینا رو توی عمل توی [آموزش - راهنمای کاربر](tutorial/index.md){.internal-link target=_blank} می‌بینی. - -نکته مهم اینه که با استفاده از نوع‌های استاندارد پایتون، توی یه جا (به جای اضافه کردن کلاس‌های بیشتر، دکوراتورها و غیره)، **FastAPI** کلی از کار رو برات انجام می‌ده. - -/// info - -اگه همه آموزش رو گذروندی و برگشتی که بیشتر در مورد نوع‌ها ببینی، یه منبع خوب "تقلب‌نامه" از `mypy` هست. - -/// diff --git a/docs/fa/docs/tutorial/middleware.md b/docs/fa/docs/tutorial/middleware.md deleted file mode 100644 index d68c25d82b..0000000000 --- a/docs/fa/docs/tutorial/middleware.md +++ /dev/null @@ -1,63 +0,0 @@ -# میان‌افزار - middleware - -شما میتوانید میان‌افزارها را در **FastAPI** اضافه کنید. - -"میان‌افزار" یک تابع است که با هر درخواست(request) قبل از پردازش توسط هر path operation (عملیات مسیر) خاص کار می‌کند. همچنین با هر پاسخ(response) قبل از بازگشت آن نیز کار می‌کند. - -* هر **درخواستی (request)** که به برنامه شما می آید را می گیرد. -* سپس می تواند کاری برای آن **درخواست** انجام دهید یا هر کد مورد نیازتان را اجرا کنید. -* سپس **درخواست** را به بخش دیگری از برنامه (توسط یک path operation مشخص) برای پردازش ارسال می کند. -* سپس **پاسخ** تولید شده توسط برنامه را (توسط یک path operation مشخص) دریافت می‌کند. -* می تواند کاری با **پاسخ** انجام دهید یا هر کد مورد نیازتان را اجرا کند. -* سپس **پاسخ** را برمی گرداند. - -/// توجه | جزئیات فنی - -در صورت وجود وابستگی هایی با `yield`، کد خروجی **پس از** اجرای میان‌‌افزار اجرا خواهد شد. - -در صورت وجود هر گونه وظایف پس زمینه (که در ادامه توضیح داده می‌شوند)، تمام میان‌افزارها *پس از آن* اجرا خواهند شد. - -/// - -## ساخت یک میان افزار - -برای ایجاد یک میان‌افزار، از دکوریتور `@app.middleware("http")` در بالای یک تابع استفاده می‌شود. - -تابع میان افزار دریافت می کند: -* `درخواست` -* تابع `call_next` که `درخواست` را به عنوان پارامتر دریافت می کند - * این تابع `درخواست` را به *path operation* مربوطه ارسال می کند. - * سپس `پاسخ` تولید شده توسط *path operation* مربوطه را برمی‌گرداند. -* شما می‌توانید سپس `پاسخ` را تغییر داده و پس از آن را برگردانید. - -{* ../../docs_src/middleware/tutorial001.py hl[8:9,11,14] *} - -/// نکته | به خاطر داشته باشید که هدرهای اختصاصی سفارشی را می توان با استفاده از پیشوند "X-" اضافه کرد. - -اما اگر هدرهای سفارشی دارید که می‌خواهید مرورگر کاربر بتواند آنها را ببیند، باید آنها را با استفاده از پارامتر `expose_headers` که در مستندات CORS از Starlette توضیح داده شده است، به پیکربندی CORS خود اضافه کنید. - -/// - -/// توجه | جزئیات فنی - -شما همچنین می‌توانید از `from starlette.requests import Request` استفاده کنید. - -**FastAPI** این را به عنوان یک سهولت برای شما به عنوان برنامه‌نویس فراهم می‌کند. اما این مستقیما از Starlette به دست می‌آید. - -/// - -### قبل و بعد از `پاسخ` - -شما می‌توانید کدی را برای اجرا با `درخواست`، قبل از اینکه هر *path operation* آن را دریافت کند، اضافه کنید. - -همچنین پس از تولید `پاسخ`، قبل از بازگشت آن، می‌توانید کدی را اضافه کنید. - -به عنوان مثال، می‌توانید یک هدر سفارشی به نام `X-Process-Time` که شامل زمان پردازش درخواست و تولید پاسخ به صورت ثانیه است، اضافه کنید. - -{* ../../docs_src/middleware/tutorial001.py hl[10,12:13] *} - - ## سایر میان افزار - -شما می‌توانید بعداً در مورد میان‌افزارهای دیگر در [راهنمای کاربر پیشرفته: میان‌افزار پیشرفته](../advanced/middleware.md){.internal-link target=_blank} بیشتر بخوانید. - -شما در بخش بعدی در مورد این که چگونه با استفاده از یک میان‌افزار، CORS را مدیریت کنید، خواهید خواند. diff --git a/docs/fa/docs/tutorial/security/index.md b/docs/fa/docs/tutorial/security/index.md deleted file mode 100644 index c0827a8b33..0000000000 --- a/docs/fa/docs/tutorial/security/index.md +++ /dev/null @@ -1,106 +0,0 @@ -# امنیت - -روش‌های مختلفی برای مدیریت امنیت، تأیید هویت و اعتبارسنجی وجود دارد. - -عموماً این یک موضوع پیچیده و "سخت" است. - -در بسیاری از فریم ورک ها و سیستم‌ها، فقط مدیریت امنیت و تأیید هویت نیاز به تلاش و کد نویسی زیادی دارد (در بسیاری از موارد می‌تواند 50% یا بیشتر کل کد نوشته شده باشد). - - -فریم ورک **FastAPI** ابزارهای متعددی را در اختیار شما قرار می دهد تا به راحتی، با سرعت، به صورت استاندارد و بدون نیاز به مطالعه و یادگیری همه جزئیات امنیت، در مدیریت **امنیت** به شما کمک کند. - -اما قبل از آن، بیایید برخی از مفاهیم کوچک را بررسی کنیم. - -## عجله دارید؟ - -اگر به هیچ یک از این اصطلاحات اهمیت نمی دهید و فقط نیاز به افزودن امنیت با تأیید هویت بر اساس نام کاربری و رمز عبور دارید، *همین الان* به فصل های بعدی بروید. - -## پروتکل استاندارد OAuth2 - -پروتکل استاندارد OAuth2 یک مشخصه است که چندین روش برای مدیریت تأیید هویت و اعتبار سنجی تعریف می کند. - -این مشخصه بسیار گسترده است و چندین حالت استفاده پیچیده را پوشش می دهد. - -در آن روش هایی برای تأیید هویت با استفاده از "برنامه های شخص ثالث" وجود دارد. - -این همان چیزی است که تمامی سیستم های با "ورود با فیسبوک، گوگل، توییتر، گیت هاب" در پایین آن را استفاده می کنند. - -### پروتکل استاندارد OAuth 1 - -پروتکل استاندارد OAuth1 نیز وجود داشت که با OAuth2 خیلی متفاوت است و پیچیدگی بیشتری داشت، زیرا شامل مشخصات مستقیم در مورد رمزگذاری ارتباط بود. - -در حال حاضر OAuth1 بسیار محبوب یا استفاده شده نیست. - -پروتکل استاندارد OAuth2 روش رمزگذاری ارتباط را مشخص نمی کند، بلکه انتظار دارد که برنامه شما با HTTPS سرویس دهی شود. - -/// نکته - -در بخش در مورد **استقرار** ، شما یاد خواهید گرفت که چگونه با استفاده از Traefik و Let's Encrypt رایگان HTTPS را راه اندازی کنید. - -/// - -## استاندارد OpenID Connect - -استاندارد OpenID Connect، مشخصه‌ای دیگر است که بر پایه **OAuth2** ساخته شده است. - -این مشخصه، به گسترش OAuth2 می‌پردازد و برخی مواردی که در OAuth2 نسبتاً تردید برانگیز هستند را مشخص می‌کند تا سعی شود آن را با سایر سیستم‌ها قابل ارتباط کند. - -به عنوان مثال، ورود به سیستم گوگل از OpenID Connect استفاده می‌کند (که در زیر از OAuth2 استفاده می‌کند). - -اما ورود به سیستم فیسبوک، از OpenID Connect پشتیبانی نمی‌کند. به جای آن، نسخه خودش از OAuth2 را دارد. - -### استاندارد OpenID (نه "OpenID Connect" ) - -همچنین مشخصه "OpenID" نیز وجود داشت که سعی در حل مسائل مشابه OpenID Connect داشت، اما بر پایه OAuth2 ساخته نشده بود. - -بنابراین، یک سیستم جداگانه بود. - -اکنون این مشخصه کمتر استفاده می‌شود و محبوبیت زیادی ندارد. - -## استاندارد OpenAPI - -استاندارد OpenAPI (قبلاً با نام Swagger شناخته می‌شد) یک open specification برای ساخت APIs (که در حال حاضر جزئی از بنیاد لینوکس میباشد) است. - -فریم ورک **FastAPI** بر اساس **OpenAPI** است. - -این خاصیت، امکان دارد تا چندین رابط مستندات تعاملی خودکار(automatic interactive documentation interfaces)، تولید کد و غیره وجود داشته باشد. - -مشخصه OpenAPI روشی برای تعریف چندین "schemes" دارد. - -با استفاده از آن‌ها، شما می‌توانید از همه این ابزارهای مبتنی بر استاندارد استفاده کنید، از جمله این سیستم‌های مستندات تعاملی(interactive documentation systems). - -استاندارد OpenAPI شیوه‌های امنیتی زیر را تعریف می‌کند: - -* شیوه `apiKey`: یک کلید اختصاصی برای برنامه که می‌تواند از موارد زیر استفاده شود: - * پارامتر جستجو. - * هدر. - * کوکی. -* شیوه `http`: سیستم‌های استاندارد احراز هویت HTTP، از جمله: - * مقدار `bearer`: یک هدر `Authorization` با مقدار `Bearer` به همراه یک توکن. این از OAuth2 به ارث برده شده است. - * احراز هویت پایه HTTP. - * ویژگی HTTP Digest و غیره. -* شیوه `oauth2`: تمام روش‌های OAuth2 برای مدیریت امنیت (به نام "flows"). - * چندین از این flows برای ساخت یک ارائه‌دهنده احراز هویت OAuth 2.0 مناسب هستند (مانند گوگل، فیسبوک، توییتر، گیت‌هاب و غیره): - * ویژگی `implicit` - * ویژگی `clientCredentials` - * ویژگی `authorizationCode` - * اما یک "flow" خاص وجود دارد که می‌تواند به طور کامل برای مدیریت احراز هویت در همان برنامه به کار رود: - * بررسی `password`: چند فصل بعدی به مثال‌های این مورد خواهیم پرداخت. -* شیوه `openIdConnect`: یک روش برای تعریف نحوه کشف داده‌های احراز هویت OAuth2 به صورت خودکار. - * کشف خودکار این موضوع را که در مشخصه OpenID Connect تعریف شده است، مشخص می‌کند. - -/// نکته - -ادغام سایر ارائه‌دهندگان احراز هویت/اجازه‌دهی مانند گوگل، فیسبوک، توییتر، گیت‌هاب و غیره نیز امکان‌پذیر و نسبتاً آسان است. - -مشکل پیچیده‌ترین مسئله، ساخت یک ارائه‌دهنده احراز هویت/اجازه‌دهی مانند آن‌ها است، اما **FastAPI** ابزارهای لازم برای انجام این کار را با سهولت به شما می‌دهد و همه کارهای سنگین را برای شما انجام می‌دهد. - -/// - -## ابزارهای **FastAPI** - -فریم ورک FastAPI ابزارهایی برای هر یک از این شیوه‌های امنیتی در ماژول`fastapi.security` فراهم می‌کند که استفاده از این مکانیزم‌های امنیتی را ساده‌تر می‌کند. - -در فصل‌های بعدی، شما یاد خواهید گرفت که چگونه با استفاده از این ابزارهای ارائه شده توسط **FastAPI**، امنیت را به API خود اضافه کنید. - -همچنین، خواهید دید که چگونه به صورت خودکار در سیستم مستندات تعاملی ادغام می‌شود. diff --git a/docs/fa/mkdocs.yml b/docs/fa/mkdocs.yml deleted file mode 100644 index de18856f44..0000000000 --- a/docs/fa/mkdocs.yml +++ /dev/null @@ -1 +0,0 @@ -INHERIT: ../en/mkdocs.yml diff --git a/docs/fr/llm-prompt.md b/docs/fr/llm-prompt.md new file mode 100644 index 0000000000..5ff18c4e8b --- /dev/null +++ b/docs/fr/llm-prompt.md @@ -0,0 +1,128 @@ +### Target language + +Translate to French (français). + +Language code: fr. + +### Grammar to use when talking to the reader + +Use the formal grammar (use «vous» instead of «tu»). + +### Quotes + +1) Convert neutral double quotes («"») and English double typographic quotes («“» and «”») to French guillemets (««» and «»»). + +2) In the French docs, guillemets are written without extra spaces: use «texte», not « texte ». + +3) Do not convert quotes inside code blocks, inline code, paths, URLs, or anything wrapped in backticks. + +Examples: + + Source (English): + + ««« + "Hello world" + “Hello Universe” + "He said: 'Hello'" + "The module is `__main__`" + »»» + + Result (French): + + ««« + «Hello world» + «Hello Universe» + «He said: 'Hello'» + «The module is `__main__`» + »»» + +### Ellipsis + +1) Make sure there is a space between an ellipsis and a word following or preceding the ellipsis. + +Examples: + + Source (English): + + ««« + ...as we intended. + ...this would work: + ...etc. + others... + More to come... + »»» + + Result (French): + + ««« + ... comme prévu. + ... cela fonctionnerait : + ... etc. + D'autres ... + La suite ... + »»» + +2) This does not apply in URLs, code blocks, and code snippets. Do not remove or add spaces there. + +### Headings + +1) Prefer translating headings using the infinitive form (as is common in the existing French docs): «Créer…», «Utiliser…», «Ajouter…». + +2) For headings that are instructions written in imperative in English (e.g. “Go check …”), keep them in imperative in French, using the formal grammar (e.g. «Allez voir …»). + +3) Keep heading punctuation as in the source. In particular, keep occurrences of literal « - » (space-hyphen-space) as « - » (the existing French docs use a hyphen here). + +### French instructions about technical terms + +Do not try to translate everything. In particular, keep common programming terms when that is the established usage in the French docs (e.g. «framework», «endpoint», «plug-in», «payload»). Use French where the existing docs already consistently use French (e.g. «requête», «réponse»). + +Keep class names, function names, modules, file names, and CLI commands unchanged. + +### List of English terms and their preferred French translations + +Below is a list of English terms and their preferred French translations, separated by a colon («:»). Use these translations, do not use your own. If an existing translation does not use these terms, update it to use them. + +* «/// note | Technical Details»: «/// note | Détails techniques» +* «/// note»: «/// note | Remarque» +* «/// tip»: «/// tip | Astuce» +* «/// warning»: «/// warning | Attention» +* «/// check»: «/// check | vérifier» +* «/// info»: «/// info» + +* «the docs»: «les documents» +* «the documentation»: «la documentation» + +* «framework»: «framework» (do not translate to «cadre») +* «performance»: «performance» + +* «type hints»: «annotations de type» +* «type annotations»: «annotations de type» + +* «autocomplete»: «autocomplétion» +* «autocompletion»: «autocomplétion» + +* «the request» (what the client sends to the server): «la requête» +* «the response» (what the server sends back to the client): «la réponse» + +* «the request body»: «le corps de la requête» +* «the response body»: «le corps de la réponse» + +* «path operation»: «opération de chemin» +* «path operations» (plural): «opérations de chemin» +* «path operation function»: «fonction de chemin» +* «path operation decorator»: «décorateur d'opération de chemin» + +* «path parameter»: «paramètre de chemin» +* «query parameter»: «paramètre de requête» + +* «the `Request`»: «`Request`» (keep as code identifier) +* «the `Response`»: «`Response`» (keep as code identifier) + +* «deployment»: «déploiement» +* «to upgrade»: «mettre à niveau» + +* «deprecated»: «déprécié» +* «to deprecate»: «déprécier» + +* «cheat sheet»: «aide-mémoire» +* «plug-in»: «plug-in» diff --git a/docs/ja/llm-prompt.md b/docs/ja/llm-prompt.md new file mode 100644 index 0000000000..18909cd595 --- /dev/null +++ b/docs/ja/llm-prompt.md @@ -0,0 +1,47 @@ +### Target language + +Translate to Japanese (日本語). + +Language code: ja. + +### Grammar and tone + +- Use polite, instructional Japanese (です/ます調). +- Keep the tone concise and technical (match existing Japanese FastAPI docs). + +### Headings + +- Follow the existing Japanese style: short, descriptive headings (often noun phrases), e.g. 「チェック」. +- Do not add a trailing period at the end of headings. + +### Quotes + +- Prefer Japanese corner brackets 「」 in normal prose when quoting a term. +- Do not change quotes inside inline code, code blocks, URLs, or file paths. + +### Ellipsis + +- Keep ellipsis style consistent with existing Japanese docs (commonly `...`). +- Never change `...` in code, URLs, or CLI examples. + +### Preferred translations / glossary + +Use the following preferred translations when they apply in documentation prose: + +- request (HTTP): リクエスト +- response (HTTP): レスポンス +- path operation: パスオペレーション +- path operation function: パスオペレーション関数 + +### `///` admonitions + +1) Keep the admonition keyword in English (do not translate `note`, `tip`, etc.). +2) If a title is present, prefer these canonical titles: + +- `/// note | 備考` +- `/// note | 技術詳細` +- `/// tip | 豆知識` +- `/// warning | 注意` +- `/// info | 情報` +- `/// check | 確認` +- `/// danger | 警告` diff --git a/docs/ko/docs/about/index.md b/docs/ko/docs/about/index.md index ee7804d323..dc2c728740 100644 --- a/docs/ko/docs/about/index.md +++ b/docs/ko/docs/about/index.md @@ -1,3 +1,3 @@ -# 소개 +# 소개 { #about } -FastAPI에 대한 디자인, 영감 등에 대해 🤓 +FastAPI, 그 디자인, 영감 등에 대해 🤓 diff --git a/docs/ko/docs/advanced/additional-status-codes.md b/docs/ko/docs/advanced/additional-status-codes.md index da06cb778a..64a7eabd5e 100644 --- a/docs/ko/docs/advanced/additional-status-codes.md +++ b/docs/ko/docs/advanced/additional-status-codes.md @@ -1,16 +1,16 @@ -# 추가 상태 코드 +# 추가 상태 코드 { #additional-status-codes } 기본적으로 **FastAPI**는 응답을 `JSONResponse`를 사용하여 반환하며, *경로 작업(path operation)*에서 반환한 내용을 해당 `JSONResponse` 안에 넣어 반환합니다. 기본 상태 코드 또는 *경로 작업*에서 설정한 상태 코드를 사용합니다. -## 추가 상태 코드 +## 추가 상태 코드 { #additional-status-codes_1 } 기본 상태 코드와 별도로 추가 상태 코드를 반환하려면 `JSONResponse`와 같이 `Response`를 직접 반환하고 추가 상태 코드를 직접 설정할 수 있습니다. 예를 들어 항목을 업데이트할 수 있는 *경로 작업*이 있고 성공 시 200 “OK”의 HTTP 상태 코드를 반환한다고 가정해 보겠습니다. -하지만 새로운 항목을 허용하기를 원할 것입니다. 항목이 이전에 존재하지 않았다면 이를 생성하고 HTTP 상태 코드 201 "Created"를 반환합니다. +하지만 새로운 항목을 허용하기를 원할 것입니다. 그리고 항목이 이전에 존재하지 않았다면 이를 생성하고 HTTP 상태 코드 201 "Created"를 반환합니다. 이를 위해서는 `JSONResponse`를 가져와서 원하는 `status_code`를 설정하여 콘텐츠를 직접 반환합니다: @@ -26,7 +26,7 @@ /// -/// note | 기술적 세부 정보 +/// note | 기술 세부사항 `from starlette.responses import JSONResponse`를 사용할 수도 있습니다. @@ -34,7 +34,7 @@ /// -## OpenAPI 및 API 문서 +## OpenAPI 및 API 문서 { #openapi-and-api-docs } 추가 상태 코드와 응답을 직접 반환하는 경우, FastAPI는 반환할 내용을 미리 알 수 있는 방법이 없기 때문에 OpenAPI 스키마(API 문서)에 포함되지 않습니다. diff --git a/docs/ko/docs/advanced/advanced-dependencies.md b/docs/ko/docs/advanced/advanced-dependencies.md index 7fa043fa3a..04e557d15b 100644 --- a/docs/ko/docs/advanced/advanced-dependencies.md +++ b/docs/ko/docs/advanced/advanced-dependencies.md @@ -1,6 +1,6 @@ -# 고급 의존성 +# 고급 의존성 { #advanced-dependencies } -## 매개변수화된 의존성 +## 매개변수화된 의존성 { #parameterized-dependencies } 지금까지 본 모든 의존성은 고정된 함수 또는 클래스입니다. @@ -10,7 +10,7 @@ 이때 해당 고정된 내용을 매개변수화할 수 있길 바랍니다. -## "호출 가능한" 인스턴스 +## "호출 가능한" 인스턴스 { #a-callable-instance } Python에는 클래스의 인스턴스를 "호출 가능"하게 만드는 방법이 있습니다. @@ -21,9 +21,9 @@ Python에는 클래스의 인스턴스를 "호출 가능"하게 만드는 방법 {* ../../docs_src/dependencies/tutorial011_an_py39.py hl[12] *} 이 경우, **FastAPI**는 추가 매개변수와 하위 의존성을 확인하기 위해 `__call__`을 사용하게 되며, -나중에 *경로 연산 함수*에서 매개변수에 값을 전달할 때 이를 호출하게 됩니다. +나중에 *경로 처리 함수*에서 매개변수에 값을 전달할 때 이를 호출하게 됩니다. -## 인스턴스 매개변수화하기 +## 인스턴스 매개변수화하기 { #parameterize-the-instance } 이제 `__init__`을 사용하여 의존성을 "매개변수화"할 수 있는 인스턴스의 매개변수를 선언할 수 있습니다: @@ -31,7 +31,7 @@ Python에는 클래스의 인스턴스를 "호출 가능"하게 만드는 방법 이 경우, **FastAPI**는 `__init__`에 전혀 관여하지 않으며, 우리는 이 메서드를 코드에서 직접 사용하게 됩니다. -## 인스턴스 생성하기 +## 인스턴스 생성하기 { #create-an-instance } 다음과 같이 이 클래스의 인스턴스를 생성할 수 있습니다: @@ -39,10 +39,9 @@ Python에는 클래스의 인스턴스를 "호출 가능"하게 만드는 방법 이렇게 하면 `checker.fixed_content` 속성에 `"bar"`라는 값을 담아 의존성을 "매개변수화"할 수 있습니다. -## 인스턴스를 의존성으로 사용하기 +## 인스턴스를 의존성으로 사용하기 { #use-the-instance-as-a-dependency } -그런 다음, `Depends(FixedContentQueryChecker)` 대신 `Depends(checker)`에서 이 `checker` 인스턴스를 사용할 수 있으며, -클래스 자체가 아닌 인스턴스 `checker`가 의존성이 됩니다. +그런 다음, 클래스 자체가 아닌 인스턴스 `checker`가 의존성이 되므로, `Depends(FixedContentQueryChecker)` 대신 `Depends(checker)`에서 이 `checker` 인스턴스를 사용할 수 있습니다. 의존성을 해결할 때 **FastAPI**는 이 `checker`를 다음과 같이 호출합니다: @@ -50,18 +49,116 @@ Python에는 클래스의 인스턴스를 "호출 가능"하게 만드는 방법 checker(q="somequery") ``` -...그리고 이때 반환되는 값을 *경로 연산 함수*의 `fixed_content_included` 매개변수로 전달합니다: +...그리고 이때 반환되는 값을 *경로 처리 함수*의 의존성 값으로, `fixed_content_included` 매개변수에 전달합니다: {* ../../docs_src/dependencies/tutorial011_an_py39.py hl[22] *} -/// tip | 참고 +/// tip | 팁 이 모든 과정이 복잡하게 느껴질 수 있습니다. 그리고 지금은 이 방법이 얼마나 유용한지 명확하지 않을 수도 있습니다. 이 예시는 의도적으로 간단하게 만들었지만, 전체 구조가 어떻게 작동하는지 보여줍니다. -보안 관련 장에서는 이와 같은 방식으로 구현된 편의 함수들이 있습니다. +보안 관련 장에서는 이와 같은 방식으로 구현된 유틸리티 함수들이 있습니다. -이 모든 과정을 이해했다면, 이러한 보안 도구들이 내부적으로 어떻게 작동하는지 이미 파악한 것입니다. +이 모든 과정을 이해했다면, 이러한 보안용 유틸리티 도구들이 내부적으로 어떻게 작동하는지 이미 파악한 것입니다. /// + +## `yield`, `HTTPException`, `except`, 백그라운드 태스크가 있는 의존성 { #dependencies-with-yield-httpexception-except-and-background-tasks } + +/// warning | 경고 + +대부분의 경우 이러한 기술 세부사항이 필요하지 않을 것입니다. + +이 세부사항은 주로 0.121.0 이전의 FastAPI 애플리케이션이 있고 `yield`가 있는 의존성에서 문제가 발생하는 경우에 유용합니다. + +/// + +`yield`가 있는 의존성은 여러 사용 사례를 수용하고 일부 문제를 해결하기 위해 시간이 지나며 발전해 왔습니다. 다음은 변경된 내용의 요약입니다. + +### `yield`와 `scope`가 있는 의존성 { #dependencies-with-yield-and-scope } + +0.121.0 버전에서 FastAPI는 `yield`가 있는 의존성에 대해 `Depends(scope="function")` 지원을 추가했습니다. + +`Depends(scope="function")`를 사용하면, `yield` 이후의 종료 코드는 *경로 처리 함수*가 끝난 직후(클라이언트에 응답이 반환되기 전)에 실행됩니다. + +그리고 `Depends(scope="request")`(기본값)를 사용하면, `yield` 이후의 종료 코드는 응답이 전송된 후에 실행됩니다. + +자세한 내용은 [Dependencies with `yield` - Early exit and `scope`](../tutorial/dependencies/dependencies-with-yield.md#early-exit-and-scope) 문서를 참고하세요. + +### `yield`가 있는 의존성과 `StreamingResponse`, 기술 세부사항 { #dependencies-with-yield-and-streamingresponse-technical-details } + +FastAPI 0.118.0 이전에는 `yield`가 있는 의존성을 사용하면, *경로 처리 함수*가 반환된 뒤 응답을 보내기 직전에 `yield` 이후의 종료 코드가 실행되었습니다. + +의도는 응답이 네트워크를 통해 전달되기를 기다리면서 필요한 것보다 더 오래 리소스를 점유하지 않도록 하는 것이었습니다. + +이 변경은 `StreamingResponse`를 반환하는 경우에도 `yield`가 있는 의존성의 종료 코드가 이미 실행된다는 의미이기도 했습니다. + +예를 들어, `yield`가 있는 의존성에 데이터베이스 세션이 있다면, `StreamingResponse`는 데이터를 스트리밍하는 동안 해당 세션을 사용할 수 없게 됩니다. `yield` 이후의 종료 코드에서 세션이 이미 닫혔기 때문입니다. + +이 동작은 0.118.0에서 되돌려져, `yield` 이후의 종료 코드가 응답이 전송된 뒤 실행되도록 변경되었습니다. + +/// info | 정보 + +아래에서 보시겠지만, 이는 0.106.0 버전 이전의 동작과 매우 비슷하지만, 여러 개선 사항과 코너 케이스에 대한 버그 수정이 포함되어 있습니다. + +/// + +#### 종료 코드를 조기에 실행하는 사용 사례 { #use-cases-with-early-exit-code } + +특정 조건의 일부 사용 사례에서는 응답을 보내기 전에 `yield`가 있는 의존성의 종료 코드를 실행하던 예전 동작이 도움이 될 수 있습니다. + +예를 들어, `yield`가 있는 의존성에서 데이터베이스 세션을 사용해 사용자를 검증만 하고, *경로 처리 함수*에서는 그 데이터베이스 세션을 다시는 사용하지 않으며(의존성에서만 사용), **그리고** 응답을 전송하는 데 오랜 시간이 걸리는 경우를 생각해 봅시다. 예를 들어 데이터를 천천히 보내는 `StreamingResponse`인데, 어떤 이유로든 데이터베이스를 사용하지는 않는 경우입니다. + +이 경우 데이터베이스 세션은 응답 전송이 끝날 때까지 유지되지만, 사용하지 않는다면 굳이 유지할 필요가 없습니다. + +다음과 같이 보일 수 있습니다: + +{* ../../docs_src/dependencies/tutorial013_an_py310.py *} + +다음에서 `Session`을 자동으로 닫는 종료 코드는: + +{* ../../docs_src/dependencies/tutorial013_an_py310.py ln[19:21] *} + +...응답이 느린 데이터 전송을 마친 뒤에 실행됩니다: + +{* ../../docs_src/dependencies/tutorial013_an_py310.py ln[30:38] hl[31:33] *} + +하지만 `generate_stream()`는 데이터베이스 세션을 사용하지 않으므로, 응답을 전송하는 동안 세션을 열린 채로 유지할 필요는 없습니다. + +SQLModel(또는 SQLAlchemy)을 사용하면서 이런 특정 사용 사례가 있다면, 더 이상 필요하지 않을 때 세션을 명시적으로 닫을 수 있습니다: + +{* ../../docs_src/dependencies/tutorial014_an_py310.py ln[24:28] hl[28] *} + +그러면 세션이 데이터베이스 연결을 해제하여, 다른 요청들이 이를 사용할 수 있게 됩니다. + +`yield`가 있는 의존성에서 조기 종료가 필요한 다른 사용 사례가 있다면, 여러분의 구체적인 사용 사례와 `yield`가 있는 의존성에 대한 조기 종료가 어떤 점에서 이득이 되는지를 포함해 GitHub Discussion Question을 생성해 주세요. + +`yield`가 있는 의존성에서 조기 종료에 대한 설득력 있는 사용 사례가 있다면, 조기 종료를 선택적으로 활성화할 수 있는 새로운 방법을 추가하는 것을 고려하겠습니다. + +### `yield`가 있는 의존성과 `except`, 기술 세부사항 { #dependencies-with-yield-and-except-technical-details } + +FastAPI 0.110.0 이전에는 `yield`가 있는 의존성을 사용한 다음 그 의존성에서 `except`로 예외를 잡고, 예외를 다시 발생시키지 않으면, 예외가 자동으로 어떤 예외 핸들러 또는 내부 서버 오류 핸들러로 raise/forward 되었습니다. + +이는 핸들러 없이 전달된 예외(내부 서버 오류)로 인해 처리되지 않은 메모리 사용이 발생하는 문제를 수정하고, 일반적인 Python 코드의 동작과 일관되게 하기 위해 0.110.0 버전에서 변경되었습니다. + +### 백그라운드 태스크와 `yield`가 있는 의존성, 기술 세부사항 { #background-tasks-and-dependencies-with-yield-technical-details } + +FastAPI 0.106.0 이전에는 `yield` 이후에 예외를 발생시키는 것이 불가능했습니다. `yield`가 있는 의존성의 종료 코드는 응답이 전송된 *후에* 실행되었기 때문에, [Exception Handlers](../tutorial/handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank}가 이미 실행된 뒤였습니다. + +이는 주로 백그라운드 태스크 안에서 의존성이 "yield"한 동일한 객체들을 사용할 수 있게 하기 위한 설계였습니다. 백그라운드 태스크가 끝난 뒤에 종료 코드가 실행되었기 때문입니다. + +이는 응답이 네트워크를 통해 전달되기를 기다리는 동안 리소스를 점유하지 않기 위한 의도로 FastAPI 0.106.0에서 변경되었습니다. + +/// tip | 팁 + +추가로, 백그라운드 태스크는 보통 별도의 리소스(예: 자체 데이터베이스 연결)를 가지고 따로 처리되어야 하는 독립적인 로직 집합입니다. + +따라서 이 방식이 코드를 더 깔끔하게 만들어줄 가능성이 큽니다. + +/// + +이 동작에 의존하던 경우라면, 이제는 백그라운드 태스크를 위한 리소스를 백그라운드 태스크 내부에서 생성하고, 내부적으로는 `yield`가 있는 의존성의 리소스에 의존하지 않는 데이터만 사용해야 합니다. + +예를 들어, 동일한 데이터베이스 세션을 사용하는 대신, 백그라운드 태스크 내부에서 새 데이터베이스 세션을 생성하고, 이 새 세션을 사용해 데이터베이스에서 객체를 가져오면 됩니다. 그리고 데이터베이스에서 가져온 객체를 백그라운드 태스크 함수의 매개변수로 전달하는 대신, 해당 객체의 ID를 전달한 다음 백그라운드 태스크 함수 내부에서 객체를 다시 가져오면 됩니다. diff --git a/docs/ko/docs/advanced/async-tests.md b/docs/ko/docs/advanced/async-tests.md index 37dfe29792..6c85936812 100644 --- a/docs/ko/docs/advanced/async-tests.md +++ b/docs/ko/docs/advanced/async-tests.md @@ -1,31 +1,26 @@ -# 비동기 테스트 코드 작성 +# 비동기 테스트 { #async-tests } -이전 장에서 `TestClient` 를 이용해 **FastAPI** 어플리케이션 테스트를 작성하는 법을 배우셨을텐데요. -지금까지는 `async` 키워드 사용없이 동기 함수의 테스트 코드를 작성하는 법만 익혔습니다. +제공된 `TestClient`를 사용하여 **FastAPI** 애플리케이션을 테스트하는 방법을 이미 살펴보았습니다. 지금까지는 `async` 함수를 사용하지 않고, 동기 테스트를 작성하는 방법만 보았습니다. -하지만 비동기 함수를 사용하여 테스트 코드를 작성하는 것은 매우 유용할 수 있습니다. -예를 들면 데이터베이스에 비동기로 쿼리하는 경우를 생각해봅시다. -FastAPI 애플리케이션에 요청을 보내고, 비동기 데이터베이스 라이브러리를 사용하여 백엔드가 데이터베이스에 올바르게 데이터를 기록했는지 확인하고 싶을 때가 있을 겁니다. +테스트에서 비동기 함수를 사용할 수 있으면 유용할 수 있습니다. 예를 들어 데이터베이스를 비동기로 쿼리하는 경우를 생각해 보세요. FastAPI 애플리케이션에 요청을 보낸 다음, async 데이터베이스 라이브러리를 사용하면서 백엔드가 데이터베이스에 올바른 데이터를 성공적으로 기록했는지 검증하고 싶을 수 있습니다. -이런 경우의 테스트 코드를 어떻게 비동기로 작성하는지 알아봅시다. +어떻게 동작하게 만들 수 있는지 살펴보겠습니다. -## pytest.mark.anyio +## pytest.mark.anyio { #pytest-mark-anyio } -앞에서 작성한 테스트 함수에서 비동기 함수를 호출하고 싶다면, 테스트 코드도 비동기 함수여야합니다. -AnyIO는 특정 테스트 함수를 비동기 함수로 호출 할 수 있는 깔끔한 플러그인을 제공합니다. +테스트에서 비동기 함수를 호출하려면, 테스트 함수도 비동기여야 합니다. AnyIO는 이를 위한 깔끔한 플러그인을 제공하며, 일부 테스트 함수를 비동기로 호출하도록 지정할 수 있습니다. +## HTTPX { #httpx } -## HTTPX +**FastAPI** 애플리케이션이 `async def` 대신 일반 `def` 함수를 사용하더라도, 내부적으로는 여전히 `async` 애플리케이션입니다. -**FastAPI** 애플리케이션이 `async def` 대신 `def` 키워드로 선언된 함수를 사용하더라도, 내부적으로는 여전히 `비동기` 애플리케이션입니다. +`TestClient`는 표준 pytest를 사용하여, 일반 `def` 테스트 함수 안에서 비동기 FastAPI 애플리케이션을 호출하도록 내부에서 마법 같은 처리를 합니다. 하지만 비동기 함수 안에서 이를 사용하면 그 마법은 더 이상 동작하지 않습니다. 테스트를 비동기로 실행하면, 테스트 함수 안에서 `TestClient`를 더 이상 사용할 수 없습니다. -`TestClient`는 pytest 표준을 사용하여 비동기 FastAPI 애플리케이션을 일반적인 `def` 테스트 함수 내에서 호출할 수 있도록 내부에서 마술을 부립니다. 하지만 이 마술은 비동기 함수 내부에서 사용할 때는 더 이상 작동하지 않습니다. 테스트를 비동기로 실행하면, 더 이상 테스트 함수 내부에서 `TestClient`를 사용할 수 없습니다. +`TestClient`는 HTTPX를 기반으로 하며, 다행히 HTTPX를 직접 사용해 API를 테스트할 수 있습니다. -`TestClient`는 HTTPX를 기반으로 하고 있으며, 다행히 이를 직접 사용하여 API를 테스트할 수 있습니다. +## 예시 { #example } -## 예시 - -간단한 예시를 위해 [더 큰 어플리케이션 만들기](../ko/tutorial/bigger-applications.md){.internal-link target=_blank} 와 [테스트](../ko/tutorial/testing.md){.internal-link target=_blank}:에서 다룬 파일 구조와 비슷한 형태를 확인해봅시다: +간단한 예시로, [더 큰 애플리케이션](../tutorial/bigger-applications.md){.internal-link target=_blank}과 [테스트](../tutorial/testing.md){.internal-link target=_blank}에서 설명한 것과 비슷한 파일 구조를 살펴보겠습니다: ``` . @@ -35,17 +30,17 @@ AnyIO는 특정 테스트 함수를 비동기 함수로 호출 할 수 있는 │   └── test_main.py ``` - `main.py`는 아래와 같아야 합니다: +`main.py` 파일은 다음과 같습니다: -{* ../../docs_src/async_tests/main.py *} +{* ../../docs_src/async_tests/app_a_py39/main.py *} -`test_main.py` 파일은 `main.py`에 대한 테스트가 있을 텐데, 다음과 같을 수 있습니다: +`test_main.py` 파일에는 `main.py`에 대한 테스트가 있으며, 이제 다음과 같이 보일 수 있습니다: -{* ../../docs_src/async_tests/test_main.py *} +{* ../../docs_src/async_tests/app_a_py39/test_main.py *} -## 실행하기 +## 실행하기 { #run-it } -아래의 명령어로 테스트 코드를 실행합니다: +다음과 같이 평소처럼 테스트를 실행할 수 있습니다:
@@ -57,52 +52,48 @@ $ pytest
-## 자세히 보기 +## 자세히 보기 { #in-detail } -`@pytest.mark.anyio` 마커는 pytest에게 이 테스트 함수가 비동기로 호출되어야 함을 알려줍니다: +`@pytest.mark.anyio` 마커는 pytest에게 이 테스트 함수가 비동기로 호출되어야 한다고 알려줍니다: -{* ../../docs_src/async_tests/test_main.py hl[7] *} +{* ../../docs_src/async_tests/app_a_py39/test_main.py hl[7] *} /// tip | 팁 -테스트 함수가 이제 `TestClient`를 사용할 때처럼 단순히 `def`가 아니라 `async def`로 작성된 점에 주목해주세요. +`TestClient`를 사용할 때처럼 단순히 `def`가 아니라, 이제 테스트 함수가 `async def`라는 점에 주목하세요. /// -그 다음에 `AsyncClient` 로 앱을 만들고 비동기 요청을 `await` 키워드로 보낼 수 있습니다: +그 다음 앱으로 `AsyncClient`를 만들고, `await`를 사용해 비동기 요청을 보낼 수 있습니다. -{* ../../docs_src/async_tests/test_main.py hl[9:12] *} +{* ../../docs_src/async_tests/app_a_py39/test_main.py hl[9:12] *} -위의 코드는: +이는 다음과 동등합니다: ```Python response = client.get('/') ``` -`TestClient` 에 요청을 보내던 것과 동일합니다. +`TestClient`로 요청을 보내기 위해 사용하던 코드입니다. /// tip | 팁 -새로운 `AsyncClient`를 사용할 때 async/await를 사용하고 있다는 점에 주목하세요. 이 요청은 비동기적으로 처리됩니다. +새 `AsyncClient`와 함께 async/await를 사용하고 있다는 점에 주목하세요. 요청은 비동기입니다. /// /// warning | 경고 -만약의 어플리케이션이 Lifespan 이벤트에 의존성을 갖고 있다면 `AsyncClient` 가 이러한 이벤트를 실행시키지 않습니다. -`AsyncClient` 가 테스트를 실행시켰다는 것을 확인하기 위해 -`LifespanManager` from florimondmanca/asgi-lifespan.확인해주세요. - +애플리케이션이 lifespan 이벤트에 의존한다면, `AsyncClient`는 이러한 이벤트를 트리거하지 않습니다. 이벤트가 트리거되도록 하려면 florimondmanca/asgi-lifespan의 `LifespanManager`를 사용하세요. /// -## 그 외의 비동기 함수 호출 +## 기타 비동기 함수 호출 { #other-asynchronous-function-calls } -테스트 함수가 이제 비동기 함수이므로, FastAPI 애플리케이션에 요청을 보내는 것 외에도 다른 `async` 함수를 호출하고 `await` 키워드를 사용 할 수 있습니다. +테스트 함수가 이제 비동기이므로, 테스트에서 FastAPI 애플리케이션에 요청을 보내는 것 외에도 다른 `async` 함수를 코드의 다른 곳에서 호출하듯이 동일하게 호출하고 (`await`) 사용할 수도 있습니다. /// tip | 팁 -테스트에 비동기 함수 호출을 통합할 때 (예: MongoDB의 MotorClient를 사용할 때) `RuntimeError: Task attached to a different loop` 오류가 발생한다면, 이벤트 루프가 필요한 객체는 반드시 비동기 함수 내에서만 인스턴스화해야 한다는 점을 주의하세요! -예를 들어 `@app.on_event("startup")` 콜백 내에서 인스턴스화하는 것이 좋습니다. +테스트에 비동기 함수 호출을 통합할 때(예: MongoDB의 MotorClient를 사용할 때) `RuntimeError: Task attached to a different loop`를 마주친다면, 이벤트 루프가 필요한 객체는 async 함수 안에서만 인스턴스화해야 한다는 점을 기억하세요. 예를 들어 `@app.on_event("startup")` 콜백에서 인스턴스화할 수 있습니다. /// diff --git a/docs/ko/docs/advanced/custom-response.md b/docs/ko/docs/advanced/custom-response.md index 2001956fa2..55dc2a4be1 100644 --- a/docs/ko/docs/advanced/custom-response.md +++ b/docs/ko/docs/advanced/custom-response.md @@ -1,4 +1,4 @@ -# 사용자 정의 응답 - HTML, Stream, 파일, 기타 +# 사용자 정의 응답 - HTML, Stream, 파일, 기타 { #custom-response-html-stream-file-others } 기본적으로, **FastAPI** 응답을 `JSONResponse`를 사용하여 반환합니다. @@ -6,11 +6,11 @@ 그러나 `Response` (또는 `JSONResponse`와 같은 하위 클래스)를 직접 반환하면, 데이터가 자동으로 변환되지 않으며 (심지어 `response_model`을 선언했더라도), 문서화가 자동으로 생성되지 않습니다(예를 들어, 생성된 OpenAPI의 일부로 HTTP 헤더 `Content-Type`에 특정 "미디어 타입"을 포함하는 경우). -하지만 *경로 작업 데코레이터*에서 `response_class` 매개변수를 사용하여 원하는 `Response`(예: 모든 `Response` 하위 클래스)를 선언할 수도 있습니다. +하지만 *경로 처리 데코레이터*에서 `response_class` 매개변수를 사용하여 원하는 `Response`(예: 모든 `Response` 하위 클래스)를 선언할 수도 있습니다. -*경로 작업 함수*에서 반환하는 내용은 해당 `Response`안에 포함됩니다. +*경로 처리 함수*에서 반환하는 내용은 해당 `Response`안에 포함됩니다. -그리고 만약 그 `Response`가 `JSONResponse`와 `UJSONResponse`의 경우 처럼 JSON 미디어 타입(`application/json`)을 가지고 있다면, *경로 작업 데코레이터*에서 선언한 Pydantic의 `response_model`을 사용해 자동으로 변환(및 필터링) 됩니다. +그리고 만약 그 `Response`가 `JSONResponse`와 `UJSONResponse`의 경우 처럼 JSON 미디어 타입(`application/json`)을 가지고 있다면, *경로 처리 데코레이터*에서 선언한 Pydantic의 `response_model`을 사용해 자동으로 변환(및 필터링) 됩니다. /// note | 참고 @@ -18,11 +18,11 @@ /// -## `ORJSONResponse` 사용하기 +## `ORJSONResponse` 사용하기 { #use-orjsonresponse } -예를 들어, 성능을 극대화하려는 경우, orjson을 설치하여 사용하고 응답을 `ORJSONResponse`로 설정할 수 있습니다. +예를 들어, 성능을 극대화하려는 경우, `orjson`을 설치하여 사용하고 응답을 `ORJSONResponse`로 설정할 수 있습니다. -사용하고자 하는 `Response` 클래스(하위 클래스)를 임포트한 후, **경로 작업 데코레이터*에서 선언하세요. +사용하고자 하는 `Response` 클래스(하위 클래스)를 임포트한 후, *경로 처리 데코레이터*에서 선언하세요. 대규모 응답의 경우, 딕셔너리를 반환하는 것보다 `Response`를 반환하는 것이 훨씬 빠릅니다. @@ -30,7 +30,7 @@ 하지만 반환하는 내용이 **JSON으로 직렬화 가능**하다고 확신하는 경우, 해당 내용을 응답 클래스에 직접 전달할 수 있으며, FastAPI가 반환 내용을 `jsonable_encoder`를 통해 처리한 뒤 응답 클래스에 전달하는 오버헤드를 피할 수 있습니다. -{* ../../docs_src/custom_response/tutorial001b.py hl[2,7] *} +{* ../../docs_src/custom_response/tutorial001b_py39.py hl[2,7] *} /// info | 정보 @@ -48,14 +48,14 @@ /// -## HTML 응답 +## HTML 응답 { #html-response } **FastAPI**에서 HTML 응답을 직접 반환하려면 `HTMLResponse`를 사용하세요. * `HTMLResponse`를 임포트 합니다. -* *경로 작업 데코레이터*의 `response_class` 매개변수로 `HTMLResponse`를 전달합니다. +* *경로 처리 데코레이터*의 `response_class` 매개변수로 `HTMLResponse`를 전달합니다. -{* ../../docs_src/custom_response/tutorial002.py hl[2,7] *} +{* ../../docs_src/custom_response/tutorial002_py39.py hl[2,7] *} /// info | 정보 @@ -67,17 +67,17 @@ /// -### `Response` 반환하기 +### `Response` 반환하기 { #return-a-response } -[응답을 직접 반환하기](response-directly.md){.internal-link target=_blank}에서 본 것 처럼, *경로 작업*에서 응답을 직접 반환하여 재정의할 수도 있습니다. +[응답을 직접 반환하기](response-directly.md){.internal-link target=_blank}에서 본 것 처럼, *경로 처리*에서 응답을 직접 반환하여 재정의할 수도 있습니다. 위의 예제와 동일하게 `HTMLResponse`를 반환하는 코드는 다음과 같을 수 있습니다: -{* ../../docs_src/custom_response/tutorial003.py hl[2,7,19] *} +{* ../../docs_src/custom_response/tutorial003_py39.py hl[2,7,19] *} /// warning | 경고 -*경로 작업 함수*에서 직접 반환된 `Response`는 OpenAPI에 문서화되지 않습니다(예를들어, `Content-Type`이 문서화되지 않음) 자동 대화형 문서에서도 표시되지 않습니다. +*경로 처리 함수*에서 직접 반환된 `Response`는 OpenAPI에 문서화되지 않습니다(예를들어, `Content-Type`이 문서화되지 않음) 자동 대화형 문서에서도 표시되지 않습니다. /// @@ -87,27 +87,27 @@ /// -### OpenAPI에 문서화하고 `Response` 재정의 하기 +### OpenAPI에 문서화하고 `Response` 재정의 하기 { #document-in-openapi-and-override-response } 함수 내부에서 응답을 재정의하면서 동시에 OpenAPI에서 "미디어 타입"을 문서화하고 싶다면, `response_class` 매게변수를 사용하면서 `Response` 객체를 반환할 수 있습니다. -이 경우 `response_class`는 OpenAPI *경로 작업*을 문서화하는 데만 사용되고, 실제로는 여러분이 반환한 `Response`가 그대로 사용됩니다. +이 경우 `response_class`는 OpenAPI *경로 처리*를 문서화하는 데만 사용되고, 실제로는 여러분이 반환한 `Response`가 그대로 사용됩니다. -### `HTMLResponse`직접 반환하기 +#### `HTMLResponse`직접 반환하기 { #return-an-htmlresponse-directly } 예를 들어, 다음과 같이 작성할 수 있습니다: -{* ../../docs_src/custom_response/tutorial004.py hl[7,21,23] *} +{* ../../docs_src/custom_response/tutorial004_py39.py hl[7,21,23] *} 이 예제에서, `generate_html_response()` 함수는 HTML을 `str`로 반환하는 대신 이미 `Response`를 생성하고 반환합니다. `generate_html_response()`를 호출한 결과를 반환함으로써, 기본적인 **FastAPI** 기본 동작을 재정의 하는 `Response`를 이미 반환하고 있습니다. -하지만 `response_class`에 `HTMLResponse`를 함께 전달했기 때문에, FastAPI는 이를 OpenAPI 및 대화형 문서에서 `text/html`로 HTML을 문서화 하는 방법을 알 수 있습니다. +하지만 `response_class`에 `HTMLResponse`를 함께 전달했기 때문에, **FastAPI**는 이를 OpenAPI 및 대화형 문서에서 `text/html`로 HTML을 문서화 하는 방법을 알 수 있습니다. -## 사용 가능한 응답들 +## 사용 가능한 응답들 { #available-responses } 다음은 사용할 수 있는 몇가지 응답들 입니다. @@ -121,7 +121,7 @@ /// -### `Response` +### `Response` { #response } 기본 `Response` 클래스는 다른 모든 응답 클래스의 부모 클래스 입니다. @@ -134,27 +134,27 @@ * `headers` - 문자열로 이루어진 `dict`. * `media_type` - 미디어 타입을 나타내는 `str` 예: `"text/html"`. -FastAPI (실제로는 Starlette)가 자동으로 `Content-Length` 헤더를 포함시킵니다. 또한 `media_type`에 기반하여 `Content-Type` 헤더를 포함하며, 텍스트 타입의 경우 문자 집합을 추가 합니다. +FastAPI (실제로는 Starlette)가 자동으로 Content-Length 헤더를 포함시킵니다. 또한 `media_type`에 기반하여 Content-Type 헤더를 포함하며, 텍스트 타입의 경우 문자 집합을 추가 합니다. -{* ../../docs_src/response_directly/tutorial002.py hl[1,18] *} +{* ../../docs_src/response_directly/tutorial002_py39.py hl[1,18] *} -### `HTMLResponse` +### `HTMLResponse` { #htmlresponse } 텍스트 또는 바이트를 받아 HTML 응답을 반환합니다. 위에서 설명한 내용과 같습니다. -### `PlainTextResponse` +### `PlainTextResponse` { #plaintextresponse } 텍스트 또는 바이트를 받아 일반 텍스트 응답을 반환합니다. -{* ../../docs_src/custom_response/tutorial005.py hl[2,7,9] *} +{* ../../docs_src/custom_response/tutorial005_py39.py hl[2,7,9] *} -### `JSONResponse` +### `JSONResponse` { #jsonresponse } 데이터를 받아 `application/json`으로 인코딩된 응답을 반환합니다. 이는 위에서 설명했듯이 **FastAPI**에서 기본적으로 사용되는 응답 형식입니다. -### `ORJSONResponse` +### `ORJSONResponse` { #orjsonresponse } `orjson`을 사용하여 빠른 JSON 응답을 제공하는 대안입니다. 위에서 설명한 내용과 같습니다. @@ -164,13 +164,13 @@ FastAPI (실제로는 Starlette)가 자동으로 `Content-Length` 헤더를 포 /// -### `UJSONResponse` +### `UJSONResponse` { #ujsonresponse } `ujson`을 사용한 또 다른 JSON 응답 형식입니다. /// info | 정보 -이 응답을 사용하려면 `ujson`을 설치해야합니다. 예: 'pip install ujson`. +이 응답을 사용하려면 `ujson`을 설치해야합니다. 예: `pip install ujson`. /// @@ -180,7 +180,7 @@ FastAPI (실제로는 Starlette)가 자동으로 `Content-Length` 헤더를 포 /// -{* ../../docs_src/custom_response/tutorial001.py hl[2,7] *} +{* ../../docs_src/custom_response/tutorial001_py39.py hl[2,7] *} /// tip | 팁 @@ -188,22 +188,22 @@ FastAPI (실제로는 Starlette)가 자동으로 `Content-Length` 헤더를 포 /// -### `RedirectResponse` +### `RedirectResponse` { #redirectresponse } HTTP 리디렉션 응답을 반환합니다. 기본적으로 상태 코드는 307(임시 리디렉션)으로 설정됩니다. `RedirectResponse`를 직접 반환할 수 있습니다. -{* ../../docs_src/custom_response/tutorial006.py hl[2,9] *} +{* ../../docs_src/custom_response/tutorial006_py39.py hl[2,9] *} --- 또는 `response_class` 매개변수에서 사용할 수도 있습니다: -{* ../../docs_src/custom_response/tutorial006b.py hl[2,7,9] *} +{* ../../docs_src/custom_response/tutorial006b_py39.py hl[2,7,9] *} -이 경우, *경로 작업* 함수에서 URL을 직접 반환할 수 있습니다. +이 경우, *경로 처리* 함수에서 URL을 직접 반환할 수 있습니다. 이 경우, 사용되는 `status_code`는 `RedirectResponse`의 기본값인 `307` 입니다. @@ -211,23 +211,23 @@ HTTP 리디렉션 응답을 반환합니다. 기본적으로 상태 코드는 30 `status_code` 매개변수를 `response_class` 매개변수와 함께 사용할 수도 있습니다: -{* ../../docs_src/custom_response/tutorial006c.py hl[2,7,9] *} +{* ../../docs_src/custom_response/tutorial006c_py39.py hl[2,7,9] *} -### `StreamingResponse` +### `StreamingResponse` { #streamingresponse } 비동기 제너레이터 또는 일반 제너레이터/이터레이터를 받아 응답 본문을 스트리밍 합니다. -{* ../../docs_src/custom_response/tutorial007.py hl[2,14] *} +{* ../../docs_src/custom_response/tutorial007_py39.py hl[2,14] *} -#### 파일과 같은 객체를 사용한 `StreamingResponse` +#### 파일과 같은 객체를 사용한 `StreamingResponse` { #using-streamingresponse-with-file-like-objects } -파일과 같은 객체(예: `open()`으로 반환된 객체)가 있는 경우, 해당 파일과 같은 객체를 반복(iterate)하는 제너레이터 함수를 만들 수 있습니다. +file-like 객체(예: `open()`으로 반환된 객체)가 있는 경우, 해당 file-like 객체를 반복(iterate)하는 제너레이터 함수를 만들 수 있습니다. 이 방식으로, 파일 전체를 메모리에 먼저 읽어들일 필요 없이, 제너레이터 함수를 `StreamingResponse`에 전달하여 반환할 수 있습니다. 이 방식은 클라우드 스토리지, 비디오 처리 등의 다양한 라이브러리와 함께 사용할 수 있습니다. -{* ../../docs_src/custom_response/tutorial008.py hl[2,10:12,14] *} +{* ../../docs_src/custom_response/tutorial008_py39.py hl[2,10:12,14] *} 1. 이것이 제너레이터 함수입니다. `yield` 문을 포함하고 있으므로 "제너레이터 함수"입니다. 2. `with` 블록을 사용함으로써, 제너레이터 함수가 완료된 후 파일과 같은 객체가 닫히도록 합니다. 즉, 응답 전송이 끝난 후 닫힙니다. @@ -235,15 +235,15 @@ HTTP 리디렉션 응답을 반환합니다. 기본적으로 상태 코드는 30 이렇게 하면 "생성(generating)" 작업을 내부적으로 다른 무언가에 위임하는 제너레이터 함수가 됩니다. - 이 방식을 사용하면 `with` 블록 안에서 파일을 열 수 있어, 작업이 완료된 후 파일과 같은 객체가 닫히는 것을 보장할 수 있습니다. + 이 방식을 사용하면 `with` 블록 안에서 파일을 열 수 있어, 작업이 완료된 후 파일과 같은 객체가 닫히는 것을 보장할 수 있습니다. /// tip | 팁 -여기서 표준 `open()`을 사용하고 있기 때문에 `async`와 `await`를 지원하지 않습니다. 따라서 경로 작업은 일반 `def`로 선언합니다. +여기서 표준 `open()`을 사용하고 있기 때문에 `async`와 `await`를 지원하지 않습니다. 따라서 경로 처리는 일반 `def`로 선언합니다. /// -### `FileResponse` +### `FileResponse` { #fileresponse } 파일을 비동기로 스트리밍하여 응답합니다. @@ -256,25 +256,25 @@ HTTP 리디렉션 응답을 반환합니다. 기본적으로 상태 코드는 30 파일 응답에는 적절한 `Content-Length`, `Last-Modified`, 및 `ETag` 헤더가 포함됩니다. -{* ../../docs_src/custom_response/tutorial009.py hl[2,10] *} +{* ../../docs_src/custom_response/tutorial009_py39.py hl[2,10] *} 또한 `response_class` 매개변수를 사용할 수도 있습니다: -{* ../../docs_src/custom_response/tutorial009b.py hl[2,8,10] *} +{* ../../docs_src/custom_response/tutorial009b_py39.py hl[2,8,10] *} -이 경우, 경로 작업 함수에서 파일 경로를 직접 반환할 수 있습니다. +이 경우, 경로 처리 함수에서 파일 경로를 직접 반환할 수 있습니다. -## 사용자 정의 응답 클래스 +## 사용자 정의 응답 클래스 { #custom-response-class } `Response`를 상속받아 사용자 정의 응답 클래스를 생성하고 사용할 수 있습니다. -예를 들어, 포함된 `ORJSONResponse` 클래스에서 사용되지 않는 설정으로 orjson을 사용하고 싶다고 가정해봅시다. +예를 들어, 포함된 `ORJSONResponse` 클래스에서 사용되지 않는 설정으로 `orjson`을 사용하고 싶다고 가정해봅시다. 만약 들여쓰기 및 포맷된 JSON을 반환하고 싶다면, `orjson.OPT_INDENT_2` 옵션을 사용할 수 있습니다. `CustomORJSONResponse`를 생성할 수 있습니다. 여기서 핵심은 `Response.render(content)` 메서드를 생성하여 내용을 `bytes`로 반환하는 것입니다: -{* ../../docs_src/custom_response/tutorial009c.py hl[9:14,17] *} +{* ../../docs_src/custom_response/tutorial009c_py39.py hl[9:14,17] *} 이제 다음 대신: @@ -282,7 +282,7 @@ HTTP 리디렉션 응답을 반환합니다. 기본적으로 상태 코드는 30 {"message": "Hello World"} ``` -이 응답은 이렇게 반환됩니다: +...이 응답은 이렇게 반환됩니다: ```json { @@ -292,22 +292,22 @@ HTTP 리디렉션 응답을 반환합니다. 기본적으로 상태 코드는 30 물론 JSON 포맷팅보다 더 유용하게 활용할 방법을 찾을 수 있을 것입니다. 😉 -## 기본 응답 클래스 +## 기본 응답 클래스 { #default-response-class } **FastAPI** 클래스 객체 또는 `APIRouter`를 생성할 때 기본적으로 사용할 응답 클래스를 지정할 수 있습니다. 이를 정의하는 매개변수는 `default_response_class`입니다. -아래 예제에서 **FastAPI**는 모든 경로 작업에서 기본적으로 `JSONResponse` 대신 `ORJSONResponse`를 사용합니다. +아래 예제에서 **FastAPI**는 모든 *경로 처리*에서 기본적으로 `JSONResponse` 대신 `ORJSONResponse`를 사용합니다. -{* ../../docs_src/custom_response/tutorial010.py hl[2,4] *} +{* ../../docs_src/custom_response/tutorial010_py39.py hl[2,4] *} /// tip | 팁 -여전히 이전처럼 *경로 작업*에서 `response_class`를 재정의할 수 있습니다. +여전히 이전처럼 *경로 처리*에서 `response_class`를 재정의할 수 있습니다. /// -## 추가 문서화 +## 추가 문서화 { #additional-documentation } OpenAPI에서 `responses`를 사용하여 미디어 타입 및 기타 세부 정보를 선언할 수도 있습니다: [OpenAPI에서 추가 응답](additional-responses.md){.internal-link target=_blank}. diff --git a/docs/ko/docs/advanced/events.md b/docs/ko/docs/advanced/events.md index 4318ada54e..35223eaf39 100644 --- a/docs/ko/docs/advanced/events.md +++ b/docs/ko/docs/advanced/events.md @@ -1,67 +1,66 @@ -# Lifespan 이벤트 +# Lifespan 이벤트 { #lifespan-events } -애플리케이션 **시작 전**에 실행되어야 하는 로직(코드)을 정의할 수 있습니다. 이는 이 코드가 **한 번**만 실행되며, **애플리케이션이 요청을 받기 시작하기 전**에 실행된다는 의미입니다. +애플리케이션이 **시작**하기 전에 실행되어야 하는 로직(코드)을 정의할 수 있습니다. 이는 이 코드가 **한 번**만 실행되며, 애플리케이션이 **요청을 받기 시작하기 전**에 실행된다는 의미입니다. -마찬가지로, 애플리케이션이 **종료될 때** 실행되어야 하는 로직(코드)을 정의할 수 있습니다. 이 경우, 이 코드는 **한 번**만 실행되며, **여러 요청을 처리한 후**에 실행됩니다. +마찬가지로, 애플리케이션이 **종료**될 때 실행되어야 하는 로직(코드)을 정의할 수 있습니다. 이 경우, 이 코드는 **한 번**만 실행되며, **여러 요청을 처리한 후**에 실행됩니다. -이 코드가 애플리케이션이 **요청을 받기 시작하기 전에** 실행되고, 요청 처리가 끝난 후 **종료 직전에** 실행되기 때문에 전체 애플리케이션의 **수명(Lifespan)**을 다룹니다. (잠시 후 "수명"이라는 단어가 중요해집니다 😉) +이 코드는 애플리케이션이 요청을 받기 **시작**하기 전에 실행되고, 요청 처리를 **끝낸 직후**에 실행되기 때문에 전체 애플리케이션의 **수명(lifespan)**을 다룹니다(잠시 후 "lifespan"이라는 단어가 중요해집니다 😉). -이 방법은 전체 애플리케이션에서 사용해야 하는 **자원**을 설정하거나 요청 간에 **공유되는** 자원을 설정하고, 또는 그 후에 **정리**하는 데 매우 유용할 수 있습니다. 예를 들어, 데이터베이스 연결 풀 또는 공유되는 머신러닝 모델을 로드하는 경우입니다. +이는 전체 앱에서 사용해야 하는 **자원**을 설정하고, 요청 간에 **공유되는** 자원을 설정하고, 그리고/또는 이후에 **정리**하는 데 매우 유용할 수 있습니다. 예를 들어, 데이터베이스 연결 풀 또는 공유 머신러닝 모델을 로드하는 경우입니다. +## 사용 사례 { #use-case } -## 사용 사례 +먼저 **사용 사례** 예시로 시작한 다음, 이를 어떻게 해결할지 살펴보겠습니다. -먼저 **사용 사례**를 예로 들어보고, 이를 어떻게 해결할 수 있는지 살펴보겠습니다. +요청을 처리하는 데 사용하고 싶은 **머신러닝 모델**이 있다고 상상해 봅시다. 🤖 -우리가 요청을 처리하기 위해 사용하고 싶은 **머신러닝 모델**이 있다고 상상해 봅시다. 🤖 +동일한 모델이 요청 간에 공유되므로, 요청마다 모델이 하나씩 있거나 사용자마다 하나씩 있는 등의 방식이 아닙니다. -이 모델들은 요청 간에 공유되므로, 요청마다 모델이 하나씩 있는 것이 아니라, 여러 요청에서 동일한 모델을 사용합니다. +모델을 로드하는 데 **상당한 시간이 걸린다고 상상해 봅시다**, 왜냐하면 모델이 **디스크에서 많은 데이터를 읽어야** 하기 때문입니다. 그래서 모든 요청마다 이를 수행하고 싶지는 않습니다. -모델을 로드하는 데 **상당한 시간이 걸린다고 상상해 봅시다**, 왜냐하면 모델이 **디스크에서 많은 데이터를 읽어야** 하기 때문입니다. 따라서 모든 요청에 대해 모델을 매번 로드하고 싶지 않습니다. +모듈/파일의 최상위에서 로드할 수도 있지만, 그러면 단순한 자동화된 테스트를 실행하는 경우에도 **모델을 로드**하게 되고, 테스트가 코드의 독립적인 부분을 실행하기 전에 모델이 로드될 때까지 기다려야 하므로 **느려집니다**. -모듈/파일의 최상위에서 모델을 로드할 수도 있지만, 그러면 **모델을 로드하는데** 시간이 걸리기 때문에, 단순한 자동화된 테스트를 실행할 때도 모델이 로드될 때까지 기다려야 해서 **테스트 속도가 느려집니다**. +이것이 우리가 해결할 문제입니다. 요청을 처리하기 전에 모델을 로드하되, 코드가 로드되는 동안이 아니라 애플리케이션이 요청을 받기 시작하기 직전에만 로드하겠습니다. -이 문제를 해결하려고 하는 것입니다. 요청을 처리하기 전에 모델을 로드하되, 애플리케이션이 요청을 받기 시작하기 직전에만 로드하고, 코드가 로드되는 동안은 로드하지 않도록 하겠습니다. +## Lifespan { #lifespan } -## Lifespan +`FastAPI` 앱의 `lifespan` 매개변수와 "컨텍스트 매니저"를 사용하여 *시작*과 *종료* 로직을 정의할 수 있습니다(컨텍스트 매니저가 무엇인지 잠시 후에 보여드리겠습니다). -`FastAPI` 애플리케이션의 `lifespan` 매개변수와 "컨텍스트 매니저"를 사용하여 *시작*과 *종료* 로직을 정의할 수 있습니다. (컨텍스트 매니저가 무엇인지 잠시 후에 설명드리겠습니다.) +예제로 시작한 다음 자세히 살펴보겠습니다. -예제를 통해 시작하고, 그 후에 자세히 살펴보겠습니다. +`yield`를 사용해 비동기 함수 `lifespan()`을 다음과 같이 생성합니다: -우리는 `yield`를 사용하여 비동기 함수 `lifespan()`을 다음과 같이 생성합니다: +{* ../../docs_src/events/tutorial003_py39.py hl[16,19] *} -{* ../../docs_src/events/tutorial003.py hl[16,19] *} +여기서는 `yield` 이전에 (가짜) 모델 함수를 머신러닝 모델이 들어 있는 딕셔너리에 넣어 모델을 로드하는 비용이 큰 *시작* 작업을 시뮬레이션합니다. 이 코드는 애플리케이션이 **요청을 받기 시작하기 전**, *시작* 동안에 실행됩니다. -여기서 우리는 모델을 로드하는 비싼 *시작* 작업을 시뮬레이션하고 있습니다. `yield` 앞에서 (가짜) 모델 함수를 머신러닝 모델이 담긴 딕셔너리에 넣습니다. 이 코드는 **애플리케이션이 요청을 받기 시작하기 전**, *시작* 동안에 실행됩니다. - -그리고 `yield` 직후에는 모델을 언로드합니다. 이 코드는 **애플리케이션이 요청 처리 완료 후**, *종료* 직전에 실행됩니다. 예를 들어, 메모리나 GPU와 같은 자원을 해제하는 작업을 할 수 있습니다. +그리고 `yield` 직후에는 모델을 언로드합니다. 이 코드는 애플리케이션이 **요청 처리를 마친 후**, *종료* 직전에 실행됩니다. 예를 들어 메모리나 GPU 같은 자원을 해제할 수 있습니다. /// tip | 팁 -`shutdown`은 애플리케이션을 **종료**할 때 발생합니다. +`shutdown`은 애플리케이션을 **중지**할 때 발생합니다. -새로운 버전을 시작해야 하거나, 그냥 실행을 멈추고 싶을 수도 있습니다. 🤷 +새 버전을 시작해야 할 수도 있고, 그냥 실행하는 게 지겨워졌을 수도 있습니다. 🤷 /// -### Lifespan 함수 +### Lifespan 함수 { #lifespan-function } -먼저 주목할 점은, `yield`를 사용하여 비동기 함수(async function)를 정의하고 있다는 것입니다. 이는 `yield`를 사용한 의존성과 매우 유사합니다. +먼저 주목할 점은 `yield`를 사용하여 비동기 함수를 정의하고 있다는 것입니다. 이는 `yield`를 사용하는 의존성과 매우 유사합니다. -{* ../../docs_src/events/tutorial003.py hl[14:19] *} +{* ../../docs_src/events/tutorial003_py39.py hl[14:19] *} 함수의 첫 번째 부분, 즉 `yield` 이전의 코드는 애플리케이션이 시작되기 **전에** 실행됩니다. -그리고 `yield` 이후의 부분은 애플리케이션이 완료된 후 **나중에** 실행됩니다. +그리고 `yield` 이후의 부분은 애플리케이션이 종료된 **후에** 실행됩니다. -### 비동기 컨텍스트 매니저 +### 비동기 컨텍스트 매니저 { #async-context-manager } -함수를 확인해보면, `@asynccontextmanager`로 장식되어 있습니다. +확인해 보면, 함수는 `@asynccontextmanager`로 데코레이션되어 있습니다. -이것은 함수를 "**비동기 컨텍스트 매니저**"라고 불리는 것으로 변환시킵니다. +이는 함수를 "**비동기 컨텍스트 매니저**"라고 불리는 것으로 변환합니다. -{* ../../docs_src/events/tutorial003.py hl[1,13] *} +{* ../../docs_src/events/tutorial003_py39.py hl[1,13] *} 파이썬에서 **컨텍스트 매니저**는 `with` 문에서 사용할 수 있는 것입니다. 예를 들어, `open()`은 컨텍스트 매니저로 사용할 수 있습니다: @@ -69,97 +68,98 @@ with open("file.txt") as file: file.read() ``` -최근 버전의 파이썬에서는 **비동기 컨텍스트 매니저**도 있습니다. 이를 `async with`와 함께 사용합니다: + +최근 버전의 파이썬에는 **비동기 컨텍스트 매니저**도 있습니다. 이를 `async with`와 함께 사용합니다: ```Python async with lifespan(app): await do_stuff() ``` -컨텍스트 매니저나 위와 같은 비동기 컨텍스트 매니저를 만들면, `with` 블록에 들어가기 전에 `yield` 이전의 코드가 실행되고, `with` 블록을 벗어난 후에는 `yield` 이후의 코드가 실행됩니다. +위와 같은 컨텍스트 매니저 또는 비동기 컨텍스트 매니저를 만들면, `with` 블록에 들어가기 전에 `yield` 이전의 코드를 실행하고, `with` 블록을 벗어난 후에는 `yield` 이후의 코드를 실행합니다. -위의 코드 예제에서는 직접 사용하지 않고, FastAPI에 전달하여 사용하도록 합니다. +위의 코드 예제에서는 직접 사용하지 않고, FastAPI에 전달하여 FastAPI가 이를 사용하도록 합니다. -`FastAPI` 애플리케이션의 `lifespan` 매개변수는 **비동기 컨텍스트 매니저**를 받기 때문에, 새로운 `lifespan` 비동기 컨텍스트 매니저를 FastAPI에 전달할 수 있습니다. +`FastAPI` 앱의 `lifespan` 매개변수는 **비동기 컨텍스트 매니저**를 받으므로, 새 `lifespan` 비동기 컨텍스트 매니저를 전달할 수 있습니다. -{* ../../docs_src/events/tutorial003.py hl[22] *} +{* ../../docs_src/events/tutorial003_py39.py hl[22] *} -## 대체 이벤트 (사용 중단) +## 대체 이벤트(사용 중단) { #alternative-events-deprecated } /// warning | 경고 -*시작*과 *종료*를 처리하는 권장 방법은 위에서 설명한 대로 `FastAPI` 애플리케이션의 `lifespan` 매개변수를 사용하는 것입니다. `lifespan` 매개변수를 제공하면 `startup`과 `shutdown` 이벤트 핸들러는 더 이상 호출되지 않습니다. `lifespan`을 사용할지, 모든 이벤트를 사용할지 선택해야 하며 둘 다 사용할 수는 없습니다. +*시작*과 *종료*를 처리하는 권장 방법은 위에서 설명한 대로 `FastAPI` 앱의 `lifespan` 매개변수를 사용하는 것입니다. `lifespan` 매개변수를 제공하면 `startup`과 `shutdown` 이벤트 핸들러는 더 이상 호출되지 않습니다. `lifespan`만 쓰거나 이벤트만 쓰거나 둘 중 하나이지, 둘 다는 아닙니다. -이 부분은 건너뛰셔도 좋습니다. +이 부분은 아마 건너뛰셔도 됩니다. /// *시작*과 *종료* 동안 실행될 이 로직을 정의하는 대체 방법이 있습니다. -애플리케이션이 시작되기 전에 또는 종료될 때 실행해야 하는 이벤트 핸들러(함수)를 정의할 수 있습니다. +애플리케이션이 시작되기 전에 또는 애플리케이션이 종료될 때 실행되어야 하는 이벤트 핸들러(함수)를 정의할 수 있습니다. 이 함수들은 `async def` 또는 일반 `def`로 선언할 수 있습니다. -### `startup` 이벤트 +### `startup` 이벤트 { #startup-event } 애플리케이션이 시작되기 전에 실행되어야 하는 함수를 추가하려면, `"startup"` 이벤트로 선언합니다: -{* ../../docs_src/events/tutorial001.py hl[8] *} +{* ../../docs_src/events/tutorial001_py39.py hl[8] *} -이 경우, `startup` 이벤트 핸들러 함수는 "database"라는 항목(단지 `dict`)을 일부 값으로 초기화합니다. +이 경우, `startup` 이벤트 핸들러 함수는 "database"(그냥 `dict`) 항목을 일부 값으로 초기화합니다. 여러 개의 이벤트 핸들러 함수를 추가할 수 있습니다. -애플리케이션은 모든 `startup` 이벤트 핸들러가 완료될 때까지 요청을 받기 시작하지 않습니다. +그리고 모든 `startup` 이벤트 핸들러가 완료될 때까지 애플리케이션은 요청을 받기 시작하지 않습니다. -### `shutdown` 이벤트 +### `shutdown` 이벤트 { #shutdown-event } 애플리케이션이 종료될 때 실행되어야 하는 함수를 추가하려면, `"shutdown"` 이벤트로 선언합니다: -{* ../../docs_src/events/tutorial002.py hl[6] *} +{* ../../docs_src/events/tutorial002_py39.py hl[6] *} -여기서, `shutdown` 이벤트 핸들러 함수는 `"Application shutdown"`이라는 텍스트를 `log.txt` 파일에 기록합니다. +여기서 `shutdown` 이벤트 핸들러 함수는 텍스트 한 줄 `"Application shutdown"`을 `log.txt` 파일에 기록합니다. /// info | 정보 -`open()` 함수에서 `mode="a"`는 "추가"를 의미하므로, 파일에 있는 기존 내용은 덮어쓰지 않고 새로운 줄이 추가됩니다. +`open()` 함수에서 `mode="a"`는 "append"(추가)를 의미하므로, 기존 내용을 덮어쓰지 않고 파일에 있던 내용 뒤에 줄이 추가됩니다. /// /// tip | 팁 -이 경우, 우리는 표준 파이썬 `open()` 함수를 사용하여 파일과 상호작용하고 있습니다. +이 경우에는 파일과 상호작용하는 표준 파이썬 `open()` 함수를 사용하고 있습니다. -따라서 I/O(입출력) 작업이 포함되어 있어 디스크에 기록되는 것을 "기다리는" 과정이 필요합니다. +따라서 I/O(input/output)가 포함되어 있어 디스크에 기록되는 것을 "기다리는" 과정이 필요합니다. 하지만 `open()`은 `async`와 `await`를 사용하지 않습니다. -그래서 우리는 이벤트 핸들러 함수를 `async def` 대신 일반 `def`로 선언합니다. +그래서 이벤트 핸들러 함수는 `async def` 대신 표준 `def`로 선언합니다. /// -### `startup`과 `shutdown`을 함께 사용 +### `startup`과 `shutdown`을 함께 { #startup-and-shutdown-together } -*시작*과 *종료* 로직이 연결될 가능성이 높습니다. 예를 들어, 무언가를 시작한 후 끝내거나, 자원을 획득한 후 해제하는 등의 작업을 할 수 있습니다. +*시작*과 *종료* 로직은 연결되어 있을 가능성이 높습니다. 무언가를 시작했다가 끝내거나, 자원을 획득했다가 해제하는 등의 작업이 필요할 수 있습니다. -이러한 작업을 별도의 함수로 처리하면 서로 로직이나 변수를 공유하지 않기 때문에 더 어려워집니다. 값들을 전역 변수에 저장하거나 비슷한 트릭을 사용해야 할 수 있습니다. +로직이나 변수를 함께 공유하지 않는 분리된 함수에서 이를 처리하면, 전역 변수에 값을 저장하거나 비슷한 트릭이 필요해져 더 어렵습니다. -그렇기 때문에 위에서 설명한 대로 `lifespan`을 사용하는 것이 권장됩니다. +그 때문에, 이제는 위에서 설명한 대로 `lifespan`을 사용하는 것이 권장됩니다. -## 기술적 세부사항 +## 기술적 세부사항 { #technical-details } 호기심 많은 분들을 위한 기술적인 세부사항입니다. 🤓 -ASGI 기술 사양에 따르면, 이는 Lifespan Protocol의 일부이며, `startup`과 `shutdown`이라는 이벤트를 정의합니다. +내부적으로 ASGI 기술 사양에서는 이것이 Lifespan Protocol의 일부이며, `startup`과 `shutdown`이라는 이벤트를 정의합니다. /// info | 정보 -Starlette의 `lifespan` 핸들러에 대해 더 읽고 싶다면 Starlette의 Lifespan 문서에서 확인할 수 있습니다. +Starlette `lifespan` 핸들러에 대해서는 Starlette의 Lifespan 문서에서 더 읽어볼 수 있습니다. -이 문서에는 코드의 다른 영역에서 사용할 수 있는 lifespan 상태를 처리하는 방법도 포함되어 있습니다. +또한 코드의 다른 영역에서 사용할 수 있는 lifespan 상태를 처리하는 방법도 포함되어 있습니다. /// -## 서브 애플리케이션 +## 서브 애플리케이션 { #sub-applications } -🚨 이 lifespan 이벤트(`startup`과 `shutdown`)는 메인 애플리케이션에 대해서만 실행되며, [서브 애플리케이션 - Mounts](sub-applications.md){.internal-link target=_blank}에는 실행되지 않음을 유의하세요. +🚨 이 lifespan 이벤트(startup 및 shutdown)는 메인 애플리케이션에 대해서만 실행되며, [서브 애플리케이션 - Mounts](sub-applications.md){.internal-link target=_blank}에는 실행되지 않음을 유의하세요. diff --git a/docs/ko/docs/advanced/index.md b/docs/ko/docs/advanced/index.md index 31704727ca..78ef5ffece 100644 --- a/docs/ko/docs/advanced/index.md +++ b/docs/ko/docs/advanced/index.md @@ -1,6 +1,6 @@ -# 심화 사용자 안내서 - 도입부 +# 심화 사용자 안내서 - 도입부 { #advanced-user-guide } -## 추가 기능 +## 추가 기능 { #additional-features } 메인 [자습서 - 사용자 안내서](../tutorial/index.md){.internal-link target=_blank}는 여러분이 **FastAPI**의 모든 주요 기능을 둘러보시기에 충분할 것입니다. @@ -14,14 +14,8 @@ /// -## 자습서를 먼저 읽으십시오 +## 자습서를 먼저 읽으십시오 { #read-the-tutorial-first } 여러분은 메인 [자습서 - 사용자 안내서](../tutorial/index.md){.internal-link target=_blank}의 지식으로 **FastAPI**의 대부분의 기능을 사용하실 수 있습니다. 이어지는 장들은 여러분이 메인 자습서 - 사용자 안내서를 이미 읽으셨으며 주요 아이디어를 알고 계신다고 가정합니다. - -## TestDriven.io 강좌 - -여러분이 문서의 이 부분을 보완하시기 위해 심화-기초 강좌 수강을 희망하신다면 다음을 참고 하시기를 바랍니다: **TestDriven.io**의 FastAPI와 Docker를 사용한 테스트 주도 개발. - -그들은 현재 전체 수익의 10퍼센트를 **FastAPI** 개발에 기부하고 있습니다. 🎉 😄 diff --git a/docs/ko/docs/advanced/middlewares.md b/docs/ko/docs/advanced/middlewares.md deleted file mode 100644 index 5778528a8e..0000000000 --- a/docs/ko/docs/advanced/middlewares.md +++ /dev/null @@ -1,96 +0,0 @@ -# 고급 미들웨어 - -메인 튜토리얼에서 [Custom Middleware](../tutorial/middleware.md){.internal-link target=_blank}를 응용프로그램에 추가하는 방법을 읽으셨습니다. - -그리고 [CORS with the `CORSMiddleware`](){.internal-link target=_blank}하는 방법도 보셨습니다. - -이 섹션에서는 다른 미들웨어들을 사용하는 방법을 알아보겠습니다. - -## ASGI 미들웨어 추가하기 - -**FastAPI**는 Starlette을 기반으로 하고 있으며, ASGI 사양을 구현하므로 ASGI 미들웨어를 사용할 수 있습니다. - -미들웨어가 FastAPI나 Starlette용으로 만들어지지 않아도 ASGI 사양을 준수하는 한 동작할 수 있습니다. - -일반적으로 ASGI 미들웨어는 첫 번째 인수로 ASGI 앱을 받는 클래스들입니다. - -따라서 타사 ASGI 미들웨어 문서에서 일반적으로 다음과 같이 사용하도록 안내할 것입니다. - -```Python -from unicorn import UnicornMiddleware - -app = SomeASGIApp() - -new_app = UnicornMiddleware(app, some_config="rainbow") -``` - -하지만 내부 미들웨어가 서버 오류를 처리하고 사용자 정의 예외 처리기가 제대로 작동하도록 하는 더 간단한 방법을 제공하는 FastAPI(실제로는 Starlette)가 있습니다. - -이를 위해 `app.add_middleware()`를 사용합니다(CORS의 예에서와 같이). - -```Python -from fastapi import FastAPI -from unicorn import UnicornMiddleware - -app = FastAPI() - -app.add_middleware(UnicornMiddleware, some_config="rainbow") -``` - -`app.add_middleware()`는 첫 번째 인수로 미들웨어 클래스와 미들웨어에 전달할 추가 인수를 받습니다. - -## 통합 미들웨어 - -**FastAPI**에는 일반적인 사용 사례를 위한 여러 미들웨어가 포함되어 있으며, 사용 방법은 다음에서 살펴보겠습니다. - -/// note | 기술 세부 사항 - -다음 예제에서는 `from starlette.middleware.something import SomethingMiddleware`를 사용할 수도 있습니다. - -**FastAPI**는 개발자의 편의를 위해 `fastapi.middleware`에 여러 미들웨어를 제공합니다. 그러나 사용 가능한 대부분의 미들웨어는 Starlette에서 직접 제공합니다. - -/// - -## `HTTPSRedirectMiddleware` - -들어오는 모든 요청이 `https` 또는 `wss`여야 합니다. - -`http` 또는 `ws`로 들어오는 모든 요청은 대신 보안 체계로 리디렉션됩니다. - -{* ../../docs_src/advanced_middleware/tutorial001.py hl[2,6] *} - -## `TrustedHostMiddleware` - -HTTP 호스트 헤더 공격을 방지하기 위해 모든 수신 요청에 올바르게 설정된 `Host` 헤더를 갖도록 강제합니다. - -{* ../../docs_src/advanced_middleware/tutorial002.py hl[2,6:8] *} - -다음 인수가 지원됩니다: - -* `allowed_hosts` - 호스트 이름으로 허용해야 하는 도메인 이름 목록입니다. 일치하는 하위 도메인에 대해 `*.example.com`과 같은 와일드카드 도메인이 지원됩니다. 모든 호스트 이름을 허용하려면 `allowed_hosts=[“*”]`를 사용하거나 미들웨어를 생략하세요. - -수신 요청의 유효성이 올바르게 확인되지 않으면 `400`이라는 응답이 전송됩니다. - -## `GZipMiddleware` - -`Accept-Encoding` 헤더에 `“gzip”`이 포함된 모든 요청에 대해 GZip 응답을 처리합니다. - -미들웨어는 표준 응답과 스트리밍 응답을 모두 처리합니다. - -{* ../../docs_src/advanced_middleware/tutorial003.py hl[2,6] *} - -지원되는 인수는 다음과 같습니다: - -* `minimum_size` - 이 최소 크기(바이트)보다 작은 응답은 GZip하지 않습니다. 기본값은 `500`입니다. -* `compresslevel` - GZip 압축 중에 사용됩니다. 1에서 9 사이의 정수입니다. 기본값은 `9`입니다. 값이 낮을수록 압축 속도는 빨라지지만 파일 크기는 커지고, 값이 높을수록 압축 속도는 느려지지만 파일 크기는 작아집니다. - -## 기타 미들웨어 - -다른 많은 ASGI 미들웨어가 있습니다. - -예를 들어: - -유비콘의 `ProxyHeadersMiddleware`> -MessagePack - -사용 가능한 다른 미들웨어를 확인하려면 스타렛의 미들웨어 문서ASGI Awesome List를 참조하세요. diff --git a/docs/ko/docs/advanced/response-change-status-code.md b/docs/ko/docs/advanced/response-change-status-code.md index 1ba9aa3ccb..4dfadde9df 100644 --- a/docs/ko/docs/advanced/response-change-status-code.md +++ b/docs/ko/docs/advanced/response-change-status-code.md @@ -1,31 +1,31 @@ -# 응답 - 상태 코드 변경 +# 응답 - 상태 코드 변경 { #response-change-status-code } 기본 [응답 상태 코드 설정](../tutorial/response-status-code.md){.internal-link target=_blank}이 가능하다는 걸 이미 알고 계실 겁니다. 하지만 경우에 따라 기본 설정과 다른 상태 코드를 반환해야 할 때가 있습니다. -## 사용 예 +## 사용 예 { #use-case } 예를 들어 기본적으로 HTTP 상태 코드 "OK" `200`을 반환하고 싶다고 가정해 봅시다. 하지만 데이터가 존재하지 않으면 이를 새로 생성하고, HTTP 상태 코드 "CREATED" `201`을 반환하고자 할 때가 있을 수 있습니다. -이때도 여전히 `response_model`을 사용하여 반환하는 데이터를 필터링하고 변환하고 싶을 수 있습니다. +하지만 여전히 `response_model`을 사용하여 반환하는 데이터를 필터링하고 변환할 수 있기를 원합니다. 이런 경우에는 `Response` 파라미터를 사용할 수 있습니다. -## `Response` 파라미터 사용하기 +## `Response` 파라미터 사용하기 { #use-a-response-parameter } -*경로 작동 함수*에 `Response` 타입의 파라미터를 선언할 수 있습니다. (쿠키와 헤더에 대해 선언하는 것과 유사하게) +*경로 처리 함수*에 `Response` 타입의 파라미터를 선언할 수 있습니다. (쿠키와 헤더에 대해 선언하는 것과 유사하게) 그리고 이 *임시* 응답 객체에서 `status_code`를 설정할 수 있습니다. -{* ../../docs_src/response_change_status_code/tutorial001.py hl[1,9,12] *} +{* ../../docs_src/response_change_status_code/tutorial001_py39.py hl[1,9,12] *} -그리고 평소처럼 원하는 객체(`dict`, 데이터베이스 모델 등)를 반환할 수 있습니다. +그리고 평소처럼 필요한 어떤 객체든 반환할 수 있습니다(`dict`, 데이터베이스 모델 등). `response_model`을 선언했다면 반환된 객체는 여전히 필터링되고 변환됩니다. -**FastAPI**는 이 *임시* 응답 객체에서 상태 코드(쿠키와 헤더 포함)를 추출하여, `response_model`로 필터링된 반환 값을 최종 응답에 넣습니다. +**FastAPI**는 이 *임시* 응답 객체에서 상태 코드(쿠키와 헤더 포함)를 추출하여, `response_model`로 필터링된 반환 값을 포함하는 최종 응답에 넣습니다. 또한, 의존성에서도 `Response` 파라미터를 선언하고 그 안에서 상태 코드를 설정할 수 있습니다. 단, 마지막으로 설정된 상태 코드가 우선 적용된다는 점을 유의하세요. diff --git a/docs/ko/docs/advanced/response-cookies.md b/docs/ko/docs/advanced/response-cookies.md index 50da713fe6..eef74276fd 100644 --- a/docs/ko/docs/advanced/response-cookies.md +++ b/docs/ko/docs/advanced/response-cookies.md @@ -1,49 +1,51 @@ -# 응답 쿠키 +# 응답 쿠키 { #response-cookies } -## `Response` 매개변수 사용하기 +## `Response` 매개변수 사용하기 { #use-a-response-parameter } -*경로 작동 함수*에서 `Response` 타입의 매개변수를 선언할 수 있습니다. +*경로 처리 함수*에서 `Response` 타입의 매개변수를 선언할 수 있습니다. 그런 다음 해당 *임시* 응답 객체에서 쿠키를 설정할 수 있습니다. -{* ../../docs_src/response_cookies/tutorial002.py hl[1,8:9] *} +{* ../../docs_src/response_cookies/tutorial002_py39.py hl[1, 8:9] *} -그런 다음 필요한 객체(`dict`, 데이터베이스 모델 등)를 반환할 수 있습니다. +그런 다음 일반적으로 하듯이 필요한 어떤 객체든 반환할 수 있습니다(`dict`, 데이터베이스 모델 등). 그리고 `response_model`을 선언했다면 반환한 객체를 거르고 변환하는 데 여전히 사용됩니다. -**FastAPI**는 그 *임시* 응답에서 쿠키(또한 헤더 및 상태 코드)를 추출하고, 반환된 값이 포함된 최종 응답에 이를 넣습니다. 이 값은 `response_model`로 걸러지게 됩니다. +**FastAPI**는 그 *임시* 응답에서 쿠키(또한 헤더 및 상태 코드)를 추출하고, `response_model`로 필터링된 반환 값이 포함된 최종 응답에 이를 넣습니다. 또한 의존관계에서 `Response` 매개변수를 선언하고, 해당 의존성에서 쿠키(및 헤더)를 설정할 수도 있습니다. -## `Response`를 직접 반환하기 +## `Response`를 직접 반환하기 { #return-a-response-directly } 코드에서 `Response`를 직접 반환할 때도 쿠키를 생성할 수 있습니다. 이를 위해 [Response를 직접 반환하기](response-directly.md){.internal-link target=_blank}에서 설명한 대로 응답을 생성할 수 있습니다. 그런 다음 쿠키를 설정하고 반환하면 됩니다: -{* ../../docs_src/response_directly/tutorial002.py hl[1,18] *} -/// tip + +{* ../../docs_src/response_cookies/tutorial001_py39.py hl[10:12] *} + +/// tip | 팁 `Response` 매개변수를 사용하지 않고 응답을 직접 반환하는 경우, FastAPI는 이를 직접 반환한다는 점에 유의하세요. 따라서 데이터가 올바른 유형인지 확인해야 합니다. 예: `JSONResponse`를 반환하는 경우, JSON과 호환되는지 확인하세요. -또한 `response_model`로 걸러져야 할 데이터가 전달되지 않도록 확인하세요. +또한 `response_model`로 필터링되어야 했던 데이터를 전송하지 않도록 하세요. /// -### 추가 정보 +### 추가 정보 { #more-info } -/// note | 기술적 세부사항 +/// note | 기술 세부사항 `from starlette.responses import Response` 또는 `from starlette.responses import JSONResponse`를 사용할 수도 있습니다. -**FastAPI**는 개발자의 편의를 위해 `fastapi.responses`로 동일한 `starlette.responses`를 제공합니다. 그러나 대부분의 응답은 Starlette에서 직접 제공됩니다. +**FastAPI**는 개발자의 편의를 위해 `fastapi.responses`로 동일한 `starlette.responses`를 제공합니다. 하지만 사용 가능한 대부분의 응답은 Starlette에서 직접 제공됩니다. 또한 `Response`는 헤더와 쿠키를 설정하는 데 자주 사용되므로, **FastAPI**는 이를 `fastapi.Response`로도 제공합니다. /// -사용 가능한 모든 매개변수와 옵션은 Starlette 문서에서 확인할 수 있습니다. +사용 가능한 모든 매개변수와 옵션은 Starlette의 문서에서 확인할 수 있습니다. diff --git a/docs/ko/docs/advanced/response-directly.md b/docs/ko/docs/advanced/response-directly.md index 08d63c43ce..abf06bb18b 100644 --- a/docs/ko/docs/advanced/response-directly.md +++ b/docs/ko/docs/advanced/response-directly.md @@ -1,20 +1,20 @@ -# 응답을 직접 반환하기 +# 응답을 직접 반환하기 { #return-a-response-directly } -**FastAPI**에서 *경로 작업(path operation)*을 생성할 때, 일반적으로 `dict`, `list`, Pydantic 모델, 데이터베이스 모델 등의 데이터를 반환할 수 있습니다. +**FastAPI**에서 *경로 처리(path operation)*를 생성할 때, 일반적으로 `dict`, `list`, Pydantic 모델, 데이터베이스 모델 등의 데이터를 반환할 수 있습니다. -기본적으로 **FastAPI**는 [JSON 호환 가능 인코더](../tutorial/encoder.md){.internal-link target=_blank}에 설명된 `jsonable_encoder`를 사용해 해당 반환 값을 자동으로 `JSON`으로 변환합니다. +기본적으로 **FastAPI**는 [JSON 호환 가능 인코더](../tutorial/encoder.md){.internal-link target=_blank}에 설명된 `jsonable_encoder`를 사용해 해당 반환 값을 자동으로 JSON으로 변환합니다. -그런 다음, JSON 호환 데이터(예: `dict`)를 `JSONResponse`에 넣어 사용자의 응답을 전송하는 방식으로 처리됩니다. +그런 다음, 내부적으로는 JSON 호환 데이터(예: `dict`)를 `JSONResponse`에 넣어 클라이언트로 응답을 전송하는 데 사용합니다. -그러나 *경로 작업*에서 `JSONResponse`를 직접 반환할 수도 있습니다. +하지만 *경로 처리*에서 `JSONResponse`를 직접 반환할 수도 있습니다. 예를 들어, 사용자 정의 헤더나 쿠키를 반환해야 하는 경우에 유용할 수 있습니다. -## `Response` 반환하기 +## `Response` 반환하기 { #return-a-response } 사실, `Response` 또는 그 하위 클래스를 반환할 수 있습니다. -/// tip +/// tip | 팁 `JSONResponse` 자체도 `Response`의 하위 클래스입니다. @@ -26,38 +26,40 @@ Pydantic 모델로 데이터 변환을 수행하지 않으며, 내용을 다른 이로 인해 많은 유연성을 얻을 수 있습니다. 어떤 데이터 유형이든 반환할 수 있고, 데이터 선언이나 유효성 검사를 재정의할 수 있습니다. -## `Response`에서 `jsonable_encoder` 사용하기 +## `Response`에서 `jsonable_encoder` 사용하기 { #using-the-jsonable-encoder-in-a-response } -**FastAPI**는 반환하는 `Response`에 아무런 변환을 하지 않으므로, 그 내용이 준비되어 있어야 합니다. +**FastAPI**는 반환하는 `Response`에 아무런 변경도 하지 않으므로, 그 내용이 준비되어 있는지 확인해야 합니다. -예를 들어, Pydantic 모델을 `dict`로 변환해 `JSONResponse`에 넣지 않으면 JSON 호환 유형으로 변환된 데이터 유형(예: `datetime`, `UUID` 등)이 사용되지 않습니다. +예를 들어, Pydantic 모델을 먼저 `dict`로 변환하고 `datetime`, `UUID` 등의 모든 데이터 타입을 JSON 호환 타입으로 변환하지 않으면 Pydantic 모델을 `JSONResponse`에 넣을 수 없습니다. 이러한 경우, 데이터를 응답에 전달하기 전에 `jsonable_encoder`를 사용하여 변환할 수 있습니다: -{* ../../docs_src/response_directly/tutorial001.py hl[6:7,21:22] *} +{* ../../docs_src/response_directly/tutorial001_py310.py hl[5:6,20:21] *} -/// note | 기술적 세부 사항 +/// note | 기술 세부사항 `from starlette.responses import JSONResponse`를 사용할 수도 있습니다. -**FastAPI**는 개발자의 편의를 위해 `starlette.responses`를 `fastapi.responses`로 제공합니다. 그러나 대부분의 가능한 응답은 Starlette에서 직접 제공합니다. +**FastAPI**는 개발자의 편의를 위해 `starlette.responses`를 `fastapi.responses`로 제공합니다. 하지만 대부분의 사용 가능한 응답은 Starlette에서 직접 제공합니다. /// -## 사용자 정의 `Response` 반환하기 -위 예제는 필요한 모든 부분을 보여주지만, 아직 유용하지는 않습니다. 사실 데이터를 직접 반환하면 **FastAPI**가 이를 `JSONResponse`에 넣고 `dict`로 변환하는 등 모든 작업을 자동으로 처리합니다. +## 사용자 정의 `Response` 반환하기 { #returning-a-custom-response } -이제, 사용자 정의 응답을 반환하는 방법을 알아보겠습니다. +위 예제는 필요한 모든 부분을 보여주지만, 아직은 그다지 유용하지 않습니다. `item`을 그냥 직접 반환했어도 **FastAPI**가 기본으로 이를 `JSONResponse`에 넣고 `dict`로 변환하는 등의 작업을 모두 수행해 주었을 것이기 때문입니다. -예를 들어 XML 응답을 반환하고 싶다고 가정해보겠습니다. +이제, 이를 사용해 사용자 정의 응답을 반환하는 방법을 알아보겠습니다. + +예를 들어 XML 응답을 반환하고 싶다고 가정해 보겠습니다. XML 내용을 문자열에 넣고, 이를 `Response`에 넣어 반환할 수 있습니다: -{* ../../docs_src/response_directly/tutorial002.py hl[1,18] *} +{* ../../docs_src/response_directly/tutorial002_py39.py hl[1,18] *} + +## 참고 사항 { #notes } -## 참고 사항 `Response`를 직접 반환할 때, 그 데이터는 자동으로 유효성 검사되거나, 변환(직렬화)되거나, 문서화되지 않습니다. 그러나 [OpenAPI에서 추가 응답](additional-responses.md){.internal-link target=_blank}에서 설명된 대로 문서화할 수 있습니다. -이후 단락에서 자동 데이터 변환, 문서화 등을 사용하면서 사용자 정의 `Response`를 선언하는 방법을 확인할 수 있습니다. +이후 섹션에서 자동 데이터 변환, 문서화 등을 계속 사용하면서 이러한 사용자 정의 `Response`를 사용하는/선언하는 방법을 확인할 수 있습니다. diff --git a/docs/ko/docs/advanced/response-headers.md b/docs/ko/docs/advanced/response-headers.md index e4e022c9b7..1c36db9b95 100644 --- a/docs/ko/docs/advanced/response-headers.md +++ b/docs/ko/docs/advanced/response-headers.md @@ -1,12 +1,12 @@ -# 응답 헤더 +# 응답 헤더 { #response-headers } -## `Response` 매개변수 사용하기 +## `Response` 매개변수 사용하기 { #use-a-response-parameter } -여러분은 *경로 작동 함수*에서 `Response` 타입의 매개변수를 선언할 수 있습니다 (쿠키와 같이 사용할 수 있습니다). +여러분은 *경로 처리 함수*에서 `Response` 타입의 매개변수를 선언할 수 있습니다 (쿠키와 같이 사용할 수 있습니다). 그런 다음, 여러분은 해당 *임시* 응답 객체에서 헤더를 설정할 수 있습니다. -{* ../../docs_src/response_headers/tutorial002.py hl[1,7:8] *} +{* ../../docs_src/response_headers/tutorial002_py39.py hl[1, 7:8] *} 그 후, 일반적으로 사용하듯이 필요한 객체(`dict`, 데이터베이스 모델 등)를 반환할 수 있습니다. @@ -16,26 +16,26 @@ 또한, 종속성에서 `Response` 매개변수를 선언하고 그 안에서 헤더(및 쿠키)를 설정할 수 있습니다. -## `Response` 직접 반환하기 +## `Response` 직접 반환하기 { #return-a-response-directly } `Response`를 직접 반환할 때에도 헤더를 추가할 수 있습니다. [응답을 직접 반환하기](response-directly.md){.internal-link target=_blank}에서 설명한 대로 응답을 생성하고, 헤더를 추가 매개변수로 전달하세요. -{* ../../docs_src/response_headers/tutorial001.py hl[10:12] *} +{* ../../docs_src/response_headers/tutorial001_py39.py hl[10:12] *} -/// note | 기술적 세부사항 +/// note | 기술 세부사항 `from starlette.responses import Response`나 `from starlette.responses import JSONResponse`를 사용할 수도 있습니다. -**FastAPI**는 `starlette.responses`를 `fastapi.responses`로 개발자의 편의를 위해 직접 제공하지만, 대부분의 응답은 Starlette에서 직접 제공됩니다. +**FastAPI**는 해당 *임시* 응답에서 헤더(쿠키와 상태 코드도 포함)를 추출하여, 여러분이 반환한 값을 포함하는 최종 응답에 `response_model`로 필터링된 값을 넣습니다. 그리고 `Response`는 헤더와 쿠키를 설정하는 데 자주 사용될 수 있으므로, **FastAPI**는 `fastapi.Response`로도 이를 제공합니다. /// -## 커스텀 헤더 +## 커스텀 헤더 { #custom-headers } -‘X-’ 접두어를 사용하여 커스텀 사설 헤더를 추가할 수 있습니다. +`X-` 접두어를 사용하여 커스텀 사설 헤더를 추가할 수 있다는 점을 기억하세요. -하지만, 여러분이 브라우저에서 클라이언트가 볼 수 있기를 원하는 커스텀 헤더가 있는 경우, CORS 설정에 이를 추가해야 합니다([CORS (Cross-Origin Resource Sharing)](../tutorial/cors.md){.internal-link target=_blank}에서 자세히 알아보세요). `expose_headers` 매개변수를 사용하여 Starlette의 CORS 설명서에 문서화된 대로 설정할 수 있습니다. +하지만, 여러분이 브라우저에서 클라이언트가 볼 수 있기를 원하는 커스텀 헤더가 있는 경우, CORS 설정에 이를 추가해야 합니다([CORS (Cross-Origin Resource Sharing)](../tutorial/cors.md){.internal-link target=_blank}에서 자세히 알아보세요). Starlette의 CORS 문서에 문서화된 `expose_headers` 매개변수를 사용하세요. diff --git a/docs/ko/docs/advanced/sub-applications.md b/docs/ko/docs/advanced/sub-applications.md index c5835de15c..e1554ca5d6 100644 --- a/docs/ko/docs/advanced/sub-applications.md +++ b/docs/ko/docs/advanced/sub-applications.md @@ -1,67 +1,67 @@ -# 하위 응용프로그램 - 마운트 +# 하위 응용프로그램 - 마운트 { #sub-applications-mounts } -만약 각각의 독립적인 OpenAPI와 문서 UI를 갖는 두 개의 독립적인 FastAPI 응용프로그램이 필요하다면, 메인 어플리케이션에 하나 (또는 그 이상의) 하위-응용프로그램(들)을 “마운트"해서 사용할 수 있습니다. +각각의 독립적인 OpenAPI와 문서 UI를 갖는 두 개의 독립적인 FastAPI 애플리케이션이 필요하다면, 메인 앱을 두고 하나(또는 그 이상)의 하위 응용프로그램을 "마운트"할 수 있습니다. -## **FastAPI** 응용프로그램 마운트 +## **FastAPI** 애플리케이션 마운트 { #mounting-a-fastapi-application } -“마운트"이란 완전히 “독립적인" 응용프로그램을 특정 경로에 추가하여 해당 하위 응용프로그램에서 선언된 *경로 동작*을 통해 해당 경로 아래에 있는 모든 작업들을 처리할 수 있도록 하는 것을 의미합니다. +"마운트"란 완전히 "독립적인" 애플리케이션을 특정 경로에 추가하고, 그 하위 응용프로그램에 선언된 _경로 처리_로 해당 경로 아래의 모든 것을 처리하도록 하는 것을 의미합니다. -### 최상단 응용프로그램 +### 최상위 애플리케이션 { #top-level-application } -먼저, 메인, 최상단의 **FastAPI** 응용프로그램과 이것의 *경로 동작*을 생성합니다: +먼저, 메인 최상위 **FastAPI** 애플리케이션과 그 *경로 처리*를 생성합니다: -{* ../../docs_src/sub_applications/tutorial001.py hl[3, 6:8] *} +{* ../../docs_src/sub_applications/tutorial001_py39.py hl[3, 6:8] *} -### 하위 응용프로그램 +### 하위 응용프로그램 { #sub-application } -다음으로, 하위 응용프로그램과 이것의 *경로 동작*을 생성합니다: +그 다음, 하위 응용프로그램과 그 *경로 처리*를 생성합니다. -이 하위 응용프로그램은 또 다른 표준 FastAPI 응용프로그램입니다. 다만 이것은 “마운트”될 것입니다: +이 하위 응용프로그램은 또 다른 표준 FastAPI 애플리케이션이지만, "마운트"될 애플리케이션입니다: -{* ../../docs_src/sub_applications/tutorial001.py hl[11, 14:16] *} +{* ../../docs_src/sub_applications/tutorial001_py39.py hl[11, 14:16] *} -### 하위 응용프로그램 마운트 +### 하위 응용프로그램 마운트 { #mount-the-sub-application } -최상단 응용프로그램, `app`에 하위 응용프로그램, `subapi`를 마운트합니다. +최상위 애플리케이션 `app`에서 하위 응용프로그램 `subapi`를 마운트합니다. -이 예시에서, 하위 응용프로그램션은 `/subapi` 경로에 마운트 될 것입니다: +이 경우 `/subapi` 경로에 마운트됩니다: -{* ../../docs_src/sub_applications/tutorial001.py hl[11, 19] *} +{* ../../docs_src/sub_applications/tutorial001_py39.py hl[11, 19] *} -### 자동으로 생성된 API 문서 확인 +### 자동 API 문서 확인 { #check-the-automatic-api-docs } -이제, `uvicorn`으로 메인 응용프로그램을 실행하십시오. 당신의 파일이 `main.py`라면, 이렇게 실행합니다: +이제 파일과 함께 `fastapi` 명령을 실행하세요:
```console -$ uvicorn main:app --reload +$ fastapi dev main.py INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) ```
-그리고 http://127.0.0.1:8000/docs에서 문서를 여십시오. +그리고 http://127.0.0.1:8000/docs에서 문서를 여세요. -메인 응용프로그램의 *경로 동작*만을 포함하는, 메인 응용프로그램에 대한 자동 API 문서를 확인할 수 있습니다: +메인 앱의 자동 API 문서를 보게 될 것이며, 메인 앱 자체의 _경로 처리_만 포함됩니다: - + -다음으로, http://127.0.0.1:8000/subapi/docs에서 하위 응용프로그램의 문서를 여십시오. +그 다음, http://127.0.0.1:8000/subapi/docs에서 하위 응용프로그램의 문서를 여세요. -하위 경로 접두사 `/subapi` 아래에 선언된 *경로 동작* 을 포함하는, 하위 응용프로그램에 대한 자동 API 문서를 확인할 수 있습니다: +하위 응용프로그램의 자동 API 문서를 보게 될 것이며, 하위 경로 접두사 `/subapi` 아래에 올바르게 포함된 하위 응용프로그램 자체의 _경로 처리_만 포함됩니다: - + -두 사용자 인터페이스 중 어느 하나를 사용해야하는 경우, 브라우저는 특정 응용프로그램 또는 하위 응용프로그램과 각각 통신할 수 있기 때문에 올바르게 동작할 것입니다. +두 사용자 인터페이스 중 어느 것과 상호작용을 시도하더라도 올바르게 동작할 것입니다. 브라우저가 각 특정 앱 또는 하위 앱과 통신할 수 있기 때문입니다. -### 기술적 세부사항: `root_path` +### 기술적 세부사항: `root_path` { #technical-details-root-path } -위에 설명된 것과 같이 하위 응용프로그램을 마운트하는 경우, FastAPI는 `root_path`라고 하는 ASGI 명세의 매커니즘을 사용하여 하위 응용프로그램에 대한 마운트 경로 통신을 처리합니다. +위에서 설명한 대로 하위 응용프로그램을 마운트하면, FastAPI는 ASGI 명세의 메커니즘인 `root_path`를 사용해 하위 응용프로그램에 대한 마운트 경로를 전달하는 작업을 처리합니다. -이를 통해, 하위 응용프로그램은 문서 UI를 위해 경로 접두사를 사용해야 한다는 사실을 인지합니다. +이렇게 하면 하위 응용프로그램은 문서 UI를 위해 해당 경로 접두사를 사용해야 한다는 것을 알게 됩니다. -하위 응용프로그램에도 역시 다른 하위 응용프로그램을 마운트하는 것이 가능하며 FastAPI가 모든 `root_path` 들을 자동적으로 처리하기 때문에 모든 것은 올바르게 동작할 것입니다. +또한 하위 응용프로그램도 자체적으로 하위 앱을 마운트할 수 있으며, FastAPI가 이 모든 `root_path`를 자동으로 처리하기 때문에 모든 것이 올바르게 동작합니다. -`root_path`와 이것을 사용하는 방법에 대해서는 [프록시의 뒷단](./behind-a-proxy.md){.internal-link target=_blank} 섹션에서 배울 수 있습니다. +`root_path`와 이를 명시적으로 사용하는 방법에 대해서는 [프록시 뒤](behind-a-proxy.md){.internal-link target=_blank} 섹션에서 더 알아볼 수 있습니다. diff --git a/docs/ko/docs/advanced/templates.md b/docs/ko/docs/advanced/templates.md index 6126357132..fffffa6a54 100644 --- a/docs/ko/docs/advanced/templates.md +++ b/docs/ko/docs/advanced/templates.md @@ -1,4 +1,4 @@ -# 템플릿 +# 템플릿 { #templates } **FastAPI**와 함께 원하는 어떤 템플릿 엔진도 사용할 수 있습니다. @@ -6,10 +6,9 @@ 설정을 쉽게 할 수 있는 유틸리티가 있으며, 이를 **FastAPI** 애플리케이션에서 직접 사용할 수 있습니다(Starlette 제공). -## 의존성 설치 - -가상 환경을 생성하고(virtual environment{.internal-link target=_blank}), 활성화한 후 jinja2를 설치해야 합니다: +## 의존성 설치 { #install-dependencies } +[가상 환경](../virtual-environments.md){.internal-link target=_blank}을 생성하고, 활성화한 후 `jinja2`를 설치해야 합니다:
@@ -21,39 +20,38 @@ $ pip install jinja2
-## 사용하기 `Jinja2Templates` +## `Jinja2Templates` 사용하기 { #using-jinja2templates } * `Jinja2Templates`를 가져옵니다. * 나중에 재사용할 수 있는 `templates` 객체를 생성합니다. -* 템플릿을 반환할 경로 작업에 `Request` 매개변수를 선언합니다. +* 템플릿을 반환할 *경로 처리*에 `Request` 매개변수를 선언합니다. * 생성한 `templates`를 사용하여 `TemplateResponse`를 렌더링하고 반환합니다. 템플릿의 이름, 요청 객체 및 Jinja2 템플릿 내에서 사용될 키-값 쌍이 포함된 "컨텍스트" 딕셔너리도 전달합니다. - -```Python hl_lines="4 11 15-18" -{!../../docs_src/templates/tutorial001.py!} -``` +{* ../../docs_src/templates/tutorial001_py39.py hl[4,11,15:18] *} /// note | 참고 -FastAPI 0.108.0 이전과 Starlette 0.29.0에서는 `name`이 첫 번째 매개변수였습니다. +FastAPI 0.108.0 이전, Starlette 0.29.0에서는 `name`이 첫 번째 매개변수였습니다. -또한 이전 버전에서는 `request` 객체가 Jinja2의 컨텍스트에서 키-값 쌍의 일부로 전달되었습니다. +또한 그 이전 버전에서는 `request` 객체가 Jinja2의 컨텍스트에서 키-값 쌍의 일부로 전달되었습니다. /// /// tip | 팁 -`response_class=HTMLResponse`를 선언하면 문서 UI 응답이 HTML임을 알 수 있습니다. +`response_class=HTMLResponse`를 선언하면 문서 UI가 응답이 HTML임을 알 수 있습니다. /// -/// note | 기술 세부 사항 +/// note | 기술 세부사항 + `from starlette.templating import Jinja2Templates`를 사용할 수도 있습니다. -**FastAPI**는 개발자를 위한 편리함으로 `fastapi.templating` 대신 `starlette.templating`을 제공합니다. 하지만 대부분의 사용 가능한 응답은 Starlette에서 직접 옵니다. `Request` 및 `StaticFiles`도 마찬가지입니다. +**FastAPI**는 개발자를 위한 편리함으로 `fastapi.templating`과 동일하게 `starlette.templating`을 제공합니다. 하지만 대부분의 사용 가능한 응답은 Starlette에서 직접 옵니다. `Request` 및 `StaticFiles`도 마찬가지입니다. + /// -## 템플릿 작성하기 +## 템플릿 작성하기 { #writing-templates } 그런 다음 `templates/item.html`에 템플릿을 작성할 수 있습니다. 예를 들면: @@ -61,7 +59,7 @@ FastAPI 0.108.0 이전과 Starlette 0.29.0에서는 `name`이 첫 번째 매개 {!../../docs_src/templates/templates/item.html!} ``` -### 템플릿 컨텍스트 값 +### 템플릿 컨텍스트 값 { #template-context-values } 다음과 같은 HTML에서: @@ -85,9 +83,9 @@ Item ID: {{ id }} Item ID: 42 ``` -### 템플릿 `url_for` 인수 +### 템플릿 `url_for` 인수 { #template-url-for-arguments } -템플릿 내에서 `url_for()`를 사용할 수도 있으며, 이는 *경로 작업 함수*에서 사용될 인수와 동일한 인수를 받습니다. +템플릿 내에서 `url_for()`를 사용할 수도 있으며, 이는 *경로 처리 함수*에서 사용될 인수와 동일한 인수를 받습니다. 따라서 다음과 같은 부분에서: @@ -99,14 +97,15 @@ Item ID: 42 {% endraw %} -...이는 *경로 작업 함수* `read_item(id=id)`가 처리할 동일한 URL로 링크를 생성합니다. +...이는 *경로 처리 함수* `read_item(id=id)`가 처리할 동일한 URL로 링크를 생성합니다. 예를 들어, ID가 `42`일 경우, 이는 다음과 같이 렌더링됩니다: + ```html ``` -## 템플릿과 정적 파일 +## 템플릿과 정적 파일 { #templates-and-static-files } 템플릿 내에서 `url_for()`를 사용할 수 있으며, 예를 들어 `name="static"`으로 마운트한 `StaticFiles`와 함께 사용할 수 있습니다. @@ -114,7 +113,7 @@ Item ID: 42 {!../../docs_src/templates/templates/item.html!} ``` -이 예제에서는 `static/styles.css`에 있는 CSS 파일에 연결될 것입니다: +이 예제에서는 다음을 통해 `static/styles.css`에 있는 CSS 파일에 링크합니다: ```CSS hl_lines="4" {!../../docs_src/templates/static/styles.css!} @@ -122,6 +121,6 @@ Item ID: 42 그리고 `StaticFiles`를 사용하고 있으므로, 해당 CSS 파일은 **FastAPI** 애플리케이션에서 `/static/styles.css` URL로 자동 제공됩니다. -## 더 많은 세부 사항 +## 더 많은 세부 사항 { #more-details } 템플릿 테스트를 포함한 더 많은 세부 사항은 Starlette의 템플릿 문서를 확인하세요. diff --git a/docs/ko/docs/advanced/testing-dependencies.md b/docs/ko/docs/advanced/testing-dependencies.md index 780e19431f..ed90fe472d 100644 --- a/docs/ko/docs/advanced/testing-dependencies.md +++ b/docs/ko/docs/advanced/testing-dependencies.md @@ -1,14 +1,14 @@ -# 테스트 의존성 오버라이드 +# 오버라이드로 의존성 테스트하기 { #testing-dependencies-with-overrides } -## 테스트 중 의존성 오버라이드하기 +## 테스트 중 의존성 오버라이드하기 { #overriding-dependencies-during-testing } -테스트를 진행하다 보면 의존성을 오버라이드해야 하는 경우가 있습니다. +테스트를 진행하다 보면 테스트 중에 의존성을 오버라이드해야 하는 경우가 있습니다. 원래 의존성을 실행하고 싶지 않을 수도 있습니다(또는 그 의존성이 가지고 있는 하위 의존성까지도 실행되지 않길 원할 수 있습니다). 대신, 테스트 동안(특정 테스트에서만) 사용될 다른 의존성을 제공하고, 원래 의존성이 사용되던 곳에서 사용할 수 있는 값을 제공하기를 원할 수 있습니다. -### 사용 사례: 외부 서비스 +### 사용 사례: 외부 서비스 { #use-cases-external-service } 예를 들어, 외부 인증 제공자를 호출해야 하는 경우를 생각해봅시다. @@ -18,11 +18,11 @@ 외부 제공자를 한 번만 테스트하고 싶을 수도 있지만 테스트를 실행할 때마다 반드시 호출할 필요는 없습니다. -이 경우 해당 공급자를 호출하는 종속성을 오버라이드하고 테스트에 대해서만 모의 사용자를 반환하는 사용자 지정 종속성을 사용할 수 있습니다. +이 경우 해당 공급자를 호출하는 의존성을 오버라이드하고 테스트에 대해서만 모의 사용자를 반환하는 사용자 지정 의존성을 사용할 수 있습니다. -### `app.dependency_overrides` 속성 사용하기 +### `app.dependency_overrides` 속성 사용하기 { #use-the-app-dependency-overrides-attribute } -이런 경우를 위해 **FastAPI** 응용 프로그램에는 `app.dependency_overrides`라는 속성이 있습니다. 이는 간단한 `dict`입니다. +이런 경우를 위해 **FastAPI** 애플리케이션에는 `app.dependency_overrides`라는 속성이 있습니다. 이는 간단한 `dict`입니다. 테스트를 위해 의존성을 오버라이드하려면, 원래 의존성(함수)을 키로 설정하고 오버라이드할 의존성(다른 함수)을 값으로 설정합니다. @@ -34,7 +34,7 @@ **FastAPI** 애플리케이션 어디에서든 사용된 의존성에 대해 오버라이드를 설정할 수 있습니다. -원래 의존성은 *경로 동작 함수*, *경로 동작 데코레이터*(반환값을 사용하지 않는 경우), `.include_router()` 호출 등에서 사용될 수 있습니다. +원래 의존성은 *경로 처리 함수*, *경로 처리 데코레이터*(반환값을 사용하지 않는 경우), `.include_router()` 호출 등에서 사용될 수 있습니다. FastAPI는 여전히 이를 오버라이드할 수 있습니다. @@ -42,7 +42,7 @@ FastAPI는 여전히 이를 오버라이드할 수 있습니다. 그런 다음, `app.dependency_overrides`를 빈 `dict`로 설정하여 오버라이드를 재설정(제거)할 수 있습니다: -```python +```Python app.dependency_overrides = {} ``` diff --git a/docs/ko/docs/advanced/testing-events.md b/docs/ko/docs/advanced/testing-events.md index 502762f23b..8dbd4f6e66 100644 --- a/docs/ko/docs/advanced/testing-events.md +++ b/docs/ko/docs/advanced/testing-events.md @@ -1,5 +1,12 @@ -# 이벤트 테스트: 시작 - 종료 +# 이벤트 테스트: 라이프스팬 및 시작 - 종료 { #testing-events-lifespan-and-startup-shutdown } -테스트에서 이벤트 핸들러(`startup` 및 `shutdown`)를 실행해야 하는 경우, `with` 문과 함께 `TestClient`를 사용할 수 있습니다. +테스트에서 `lifespan`을 실행해야 하는 경우, `with` 문과 함께 `TestClient`를 사용할 수 있습니다: -{* ../../docs_src/app_testing/tutorial003.py hl[9:12,20:24] *} +{* ../../docs_src/app_testing/tutorial004_py39.py hl[9:15,18,27:28,30:32,41:43] *} + + +["공식 Starlette 문서 사이트에서 테스트에서 라이프스팬 실행하기."](https://www.starlette.dev/lifespan/#running-lifespan-in-tests)에 대한 자세한 내용을 더 읽을 수 있습니다. + +더 이상 권장되지 않는 `startup` 및 `shutdown` 이벤트의 경우, 다음과 같이 `TestClient`를 사용할 수 있습니다: + +{* ../../docs_src/app_testing/tutorial003_py39.py hl[9:12,20:24] *} diff --git a/docs/ko/docs/advanced/testing-websockets.md b/docs/ko/docs/advanced/testing-websockets.md index 9b67824295..1cb3cad67e 100644 --- a/docs/ko/docs/advanced/testing-websockets.md +++ b/docs/ko/docs/advanced/testing-websockets.md @@ -1,13 +1,13 @@ -# WebSocket 테스트하기 +# WebSocket 테스트하기 { #testing-websockets } -`TestClient`를 사용하여 WebSocket을 테스트할 수 있습니다. +같은 `TestClient`를 사용하여 WebSocket을 테스트할 수 있습니다. 이를 위해 `with` 문에서 `TestClient`를 사용하여 WebSocket에 연결합니다: -{* ../../docs_src/app_testing/tutorial002.py hl[27:31] *} +{* ../../docs_src/app_testing/tutorial002_py39.py hl[27:31] *} /// note | 참고 -자세한 내용은 Starlette의 WebSocket 테스트에 관한 설명서를 참고하시길 바랍니다. +자세한 내용은 Starlette의 testing WebSockets 문서를 확인하세요. /// diff --git a/docs/ko/docs/advanced/using-request-directly.md b/docs/ko/docs/advanced/using-request-directly.md index b88a83bf42..e0a5e99f88 100644 --- a/docs/ko/docs/advanced/using-request-directly.md +++ b/docs/ko/docs/advanced/using-request-directly.md @@ -1,10 +1,10 @@ -# `Request` 직접 사용하기 +# `Request` 직접 사용하기 { #using-the-request-directly } 지금까지 요청에서 필요한 부분을 각 타입으로 선언하여 사용해 왔습니다. 다음과 같은 곳에서 데이터를 가져왔습니다: -* 경로의 파라미터로부터. +* 경로를 매개변수로. * 헤더. * 쿠키. * 기타 등등. @@ -13,29 +13,29 @@ 하지만 `Request` 객체에 직접 접근해야 하는 상황이 있을 수 있습니다. -## `Request` 객체에 대한 세부 사항 +## `Request` 객체에 대한 세부 사항 { #details-about-the-request-object } **FastAPI**는 실제로 내부에 **Starlette**을 사용하며, 그 위에 여러 도구를 덧붙인 구조입니다. 따라서 여러분이 필요할 때 Starlette의 `Request` 객체를 직접 사용할 수 있습니다. -`Request` 객체에서 데이터를 직접 가져오는 경우(예: 본문을 읽기)에는 FastAPI가 해당 데이터를 검증하거나 변환하지 않으며, 문서화(OpenAPI를 통한 문서 자동화(로 생성된) API 사용자 인터페이스)도 되지 않습니다. +또한 이는 `Request` 객체에서 데이터를 직접 가져오는 경우(예: 본문을 읽기) FastAPI가 해당 데이터를 검증하거나 변환하지 않으며, 문서화(OpenAPI를 통한 자동 API 사용자 인터페이스용)도 되지 않는다는 의미이기도 합니다. 그러나 다른 매개변수(예: Pydantic 모델을 사용한 본문)는 여전히 검증, 변환, 주석 추가 등이 이루어집니다. -하지만 특정한 경우에는 `Request` 객체에 직접 접근하는 것이 유용할 수 있습니다. +하지만 특정한 경우에는 `Request` 객체를 가져오는 것이 유용할 수 있습니다. -## `Request` 객체를 직접 사용하기 +## `Request` 객체를 직접 사용하기 { #use-the-request-object-directly } -여러분이 클라이언트의 IP 주소/호스트 정보를 *경로 작동 함수* 내부에서 가져와야 한다고 가정해 보겠습니다. +여러분이 클라이언트의 IP 주소/호스트 정보를 *경로 처리 함수* 내부에서 가져와야 한다고 가정해 보겠습니다. 이를 위해서는 요청에 직접 접근해야 합니다. -{* ../../docs_src/using_request_directly/tutorial001.py hl[1,7:8] *} +{* ../../docs_src/using_request_directly/tutorial001_py39.py hl[1,7:8] *} -*경로 작동 함수* 매개변수를 `Request` 타입으로 선언하면 **FastAPI**가 해당 매개변수에 `Request` 객체를 전달하는 것을 알게 됩니다. +*경로 처리 함수* 매개변수를 `Request` 타입으로 선언하면 **FastAPI**가 해당 매개변수에 `Request`를 전달하는 것을 알게 됩니다. /// tip | 팁 -이 경우, 요청 매개변수와 함께 경로 매개변수를 선언한 것을 볼 수 있습니다. +이 경우, 요청 매개변수 옆에 경로 매개변수를 선언하고 있다는 점을 참고하세요. 따라서, 경로 매개변수는 추출되고 검증되며 지정된 타입으로 변환되고 OpenAPI로 주석이 추가됩니다. @@ -43,14 +43,14 @@ /// -## `Request` 설명서 +## `Request` 설명서 { #request-documentation } -여러분은 `Request` 객체에 대한 더 자세한 내용을 공식 Starlette 설명서 사이트에서 읽어볼 수 있습니다. +여러분은 공식 Starlette 설명서 사이트의 `Request` 객체에 대한 더 자세한 내용을 읽어볼 수 있습니다. /// note | 기술 세부사항 `from starlette.requests import Request`를 사용할 수도 있습니다. -**FastAPI**는 여러분(개발자)를 위한 편의를 위해 이를 직접 제공하지만, 실제로는 Starlette에서 가져온 것입니다. +**FastAPI**는 여러분(개발자)를 위한 편의를 위해 이를 직접 제공하지만, Starlette에서 직접 가져온 것입니다. /// diff --git a/docs/ko/docs/advanced/websockets.md b/docs/ko/docs/advanced/websockets.md index d9d0dd95c4..b6817870b7 100644 --- a/docs/ko/docs/advanced/websockets.md +++ b/docs/ko/docs/advanced/websockets.md @@ -1,10 +1,10 @@ -# WebSockets +# WebSockets { #websockets } 여러분은 **FastAPI**에서 WebSockets를 사용할 수 있습니다. -## `WebSockets` 설치 +## `websockets` 설치 { #install-websockets } -[가상 환경](../virtual-environments.md){.internal-link target=_blank)를 생성하고 활성화한 다음, `websockets`를 설치하세요: +[가상 환경](../virtual-environments.md){.internal-link target=_blank}을 생성하고 활성화한 다음, `websockets`("WebSocket" 프로토콜을 쉽게 사용할 수 있게 해주는 Python 라이브러리)를 설치하세요:
@@ -16,13 +16,13 @@ $ pip install websockets
-## WebSockets 클라이언트 +## WebSockets 클라이언트 { #websockets-client } -### 프로덕션 환경에서 +### 프로덕션 환경에서 { #in-production } 여러분의 프로덕션 시스템에서는 React, Vue.js 또는 Angular와 같은 최신 프레임워크로 생성된 프런트엔드를 사용하고 있을 가능성이 높습니다. -백엔드와 WebSockets을 사용해 통신하려면 아마도 프런트엔드의 유틸리티를 사용할 것입니다. +그리고 백엔드와 WebSockets을 사용해 통신하려면 아마도 프런트엔드의 유틸리티를 사용할 것입니다. 또는 네이티브 코드로 WebSocket 백엔드와 직접 통신하는 네이티브 모바일 응용 프로그램을 가질 수도 있습니다. @@ -30,23 +30,23 @@ $ pip install websockets --- -하지만 이번 예제에서는 일부 자바스크립트를 포함한 간단한 HTML 문서를 사용하겠습니다. 모든 것을 긴 문자열 안에 넣습니다. +하지만 이번 예제에서는 일부 자바스크립트를 포함한 매우 간단한 HTML 문서를 사용하겠습니다. 모든 것을 긴 문자열 안에 넣습니다. 물론, 이는 최적의 방법이 아니며 프로덕션 환경에서는 사용하지 않을 것입니다. -프로덕션 환경에서는 위에서 설명한 옵션 중 하나를 사용하는 것이 좋습니다. +프로덕션 환경에서는 위에서 설명한 옵션 중 하나를 사용할 것입니다. 그러나 이는 WebSockets의 서버 측에 집중하고 동작하는 예제를 제공하는 가장 간단한 방법입니다: -{* ../../docs_src/websockets/tutorial001.py hl[2,6:38,41:43] *} +{* ../../docs_src/websockets/tutorial001_py39.py hl[2,6:38,41:43] *} -## `websocket` 생성하기 +## `websocket` 생성하기 { #create-a-websocket } **FastAPI** 응용 프로그램에서 `websocket`을 생성합니다: -{* ../../docs_src/websockets/tutorial001.py hl[1,46:47] *} +{* ../../docs_src/websockets/tutorial001_py39.py hl[1,46:47] *} -/// note | 기술적 세부사항 +/// note | 기술 세부사항 `from starlette.websockets import WebSocket`을 사용할 수도 있습니다. @@ -54,17 +54,17 @@ $ pip install websockets /// -## 메시지를 대기하고 전송하기 +## 메시지를 대기하고 전송하기 { #await-for-messages-and-send-messages } WebSocket 경로에서 메시지를 대기(`await`)하고 전송할 수 있습니다. -{* ../../docs_src/websockets/tutorial001.py hl[48:52] *} +{* ../../docs_src/websockets/tutorial001_py39.py hl[48:52] *} 여러분은 이진 데이터, 텍스트, JSON 데이터를 받을 수 있고 전송할 수 있습니다. -## 시도해보기 +## 시도해보기 { #try-it } -파일 이름이 `main.py`라고 가정하고 응용 프로그램을 실행합니다: +파일 이름이 `main.py`라고 가정하고 다음으로 응용 프로그램을 실행합니다:
@@ -76,7 +76,7 @@ $ fastapi dev main.py
-브라우저에서 http://127.0.0.1:8000을 열어보세요. +브라우저에서 http://127.0.0.1:8000을 여세요. 간단한 페이지가 나타날 것입니다: @@ -86,7 +86,7 @@ $ fastapi dev main.py -**FastAPI** WebSocket 응용 프로그램이 응답을 돌려줄 것입니다: +그리고 WebSockets가 포함된 **FastAPI** 응용 프로그램이 응답을 돌려줄 것입니다: @@ -94,9 +94,9 @@ $ fastapi dev main.py -모든 메시지는 동일한 WebSocket 연결을 사용합니다. +그리고 모든 메시지는 동일한 WebSocket 연결을 사용합니다. -## `Depends` 및 기타 사용하기 +## `Depends` 및 기타 사용하기 { #using-depends-and-others } WebSocket 엔드포인트에서 `fastapi`에서 다음을 가져와 사용할 수 있습니다: @@ -107,21 +107,21 @@ WebSocket 엔드포인트에서 `fastapi`에서 다음을 가져와 사용할 * `Path` * `Query` -이들은 다른 FastAPI 엔드포인트/*경로 작동*과 동일하게 동작합니다: +이들은 다른 FastAPI 엔드포인트/*경로 처리*와 동일하게 동작합니다: {* ../../docs_src/websockets/tutorial002_an_py310.py hl[68:69,82] *} /// info | 정보 -WebSocket에서는 `HTTPException`을 발생시키는 것이 적합하지 않습니다. 대신 `WebSocketException`을 발생시킵니다. +WebSocket이기 때문에 `HTTPException`을 발생시키는 것은 적절하지 않습니다. 대신 `WebSocketException`을 발생시킵니다. 명세서에 정의된 유효한 코드를 사용하여 종료 코드를 설정할 수 있습니다. /// -### 종속성을 가진 WebSockets 테스트 +### 종속성을 가진 WebSockets 시도해보기 { #try-the-websockets-with-dependencies } -파일 이름이 `main.py`라고 가정하고 응용 프로그램을 실행합니다: +파일 이름이 `main.py`라고 가정하고 다음으로 응용 프로그램을 실행합니다:
@@ -133,9 +133,9 @@ $ fastapi dev main.py
-브라우저에서 http://127.0.0.1:8000을 열어보세요. +브라우저에서 http://127.0.0.1:8000을 여세요. -다음과 같은 값을 설정할 수 있습니다: +여기에서 다음을 설정할 수 있습니다: * 경로에 사용된 "Item ID". * 쿼리 매개변수로 사용된 "Token". @@ -146,13 +146,13 @@ $ fastapi dev main.py /// -이제 WebSocket에 연결하고 메시지를 전송 및 수신할 수 있습니다: +이렇게 하면 WebSocket에 연결하고 메시지를 전송 및 수신할 수 있습니다: -## 연결 해제 및 다중 클라이언트 처리 +## 연결 해제 및 다중 클라이언트 처리 { #handling-disconnections-and-multiple-clients } -WebSocket 연결이 닫히면, `await websocket.receive_text()`가 `WebSocketDisconnect` 예외를 발생시킵니다. 이를 잡아 처리할 수 있습니다: +WebSocket 연결이 닫히면, `await websocket.receive_text()`가 `WebSocketDisconnect` 예외를 발생시킵니다. 그러면 이 예제처럼 이를 잡아 처리할 수 있습니다. {* ../../docs_src/websockets/tutorial003_py39.py hl[79:81] *} @@ -160,7 +160,7 @@ WebSocket 연결이 닫히면, `await websocket.receive_text()`가 `WebSocketDis * 여러 브라우저 탭에서 앱을 엽니다. * 각 탭에서 메시지를 작성합니다. -* 한 탭을 닫아보세요. +* 그런 다음 탭 중 하나를 닫아보세요. `WebSocketDisconnect` 예외가 발생하며, 다른 모든 클라이언트가 다음과 같은 메시지를 수신합니다: @@ -170,17 +170,17 @@ Client #1596980209979 left the chat /// tip | 팁 -위 응용 프로그램은 여러 WebSocket 연결에 메시지를 브로드캐스트하는 방법을 보여주는 간단한 예제입니다. +위 앱은 여러 WebSocket 연결에 메시지를 처리하고 브로드캐스트하는 방법을 보여주는 최소한의 간단한 예제입니다. -그러나 모든 것을 메모리의 단일 리스트로 처리하므로, 프로세스가 실행 중인 동안만 동작하며 단일 프로세스에서만 작동합니다. +하지만 모든 것을 메모리의 단일 리스트로 처리하므로, 프로세스가 실행 중인 동안만 동작하며 단일 프로세스에서만 작동한다는 점을 기억하세요. -FastAPI와 쉽게 통합할 수 있으면서 더 견고하고 Redis, PostgreSQL 등을 지원하는 도구를 찾고 있다면, encode/broadcaster를 확인하세요. +FastAPI와 쉽게 통합할 수 있으면서 더 견고하고 Redis, PostgreSQL 등을 지원하는 도구가 필요하다면, encode/broadcaster를 확인하세요. /// -## 추가 정보 +## 추가 정보 { #more-info } -다음 옵션에 대한 자세한 내용을 보려면 Starlette의 문서를 확인하세요: +다음 옵션에 대해 더 알아보려면 Starlette의 문서를 확인하세요: * `WebSocket` 클래스. * 클래스 기반 WebSocket 처리. diff --git a/docs/ko/docs/advanced/wsgi.md b/docs/ko/docs/advanced/wsgi.md index 3e9de3e6ca..89cf57cfef 100644 --- a/docs/ko/docs/advanced/wsgi.md +++ b/docs/ko/docs/advanced/wsgi.md @@ -1,10 +1,10 @@ -# WSGI 포함하기 - Flask, Django 그 외 +# WSGI 포함하기 - Flask, Django 그 외 { #including-wsgi-flask-django-others } -[서브 응용 프로그램 - 마운트](sub-applications.md){.internal-link target=_blank}, [프록시 뒤편에서](behind-a-proxy.md){.internal-link target=_blank}에서 보았듯이 WSGI 응용 프로그램들을 다음과 같이 마운트 할 수 있습니다. +[서브 응용 프로그램 - 마운트](sub-applications.md){.internal-link target=_blank}, [프록시 뒤편에서](behind-a-proxy.md){.internal-link target=_blank}에서 보았듯이 WSGI 응용 프로그램들을 마운트 할 수 있습니다. -`WSGIMiddleware`를 사용하여 WSGI 응용 프로그램(예: Flask, Django 등)을 감쌀 수 있습니다. +이를 위해 `WSGIMiddleware`를 사용해 WSGI 응용 프로그램(예: Flask, Django 등)을 감쌀 수 있습니다. -## `WSGIMiddleware` 사용하기 +## `WSGIMiddleware` 사용하기 { #using-wsgimiddleware } `WSGIMiddleware`를 불러와야 합니다. @@ -12,9 +12,9 @@ 그 후, 해당 경로에 마운트합니다. -{* ../../docs_src/wsgi/tutorial001.py hl[2:3,23] *} +{* ../../docs_src/wsgi/tutorial001_py39.py hl[2:3,3] *} -## 확인하기 +## 확인하기 { #check-it } 이제 `/v1/` 경로에 있는 모든 요청은 Flask 응용 프로그램에서 처리됩니다. @@ -26,7 +26,7 @@ Hello, World from Flask! ``` -그리고 다음으로 이동하면 http://localhost:8000/v2 Flask의 응답을 볼 수 있습니다: +그리고 다음으로 이동하면 http://localhost:8000/v2 **FastAPI**의 응답을 볼 수 있습니다: ```JSON { diff --git a/docs/ko/docs/benchmarks.md b/docs/ko/docs/benchmarks.md index aff8ae70ef..2d4fdbeddb 100644 --- a/docs/ko/docs/benchmarks.md +++ b/docs/ko/docs/benchmarks.md @@ -1,10 +1,10 @@ -# 벤치마크 +# 벤치마크 { #benchmarks } -독립적인 TechEmpower 벤치마크에 따르면 **FastAPI** 애플리케이션이 Uvicorn을 사용하여 가장 빠른 Python 프레임워크 중 하나로 실행되며, Starlette와 Uvicorn 자체(내부적으로 FastAPI가 사용하는 도구)보다 조금 아래에 위치합니다. +독립적인 TechEmpower 벤치마크에 따르면 **FastAPI** 애플리케이션이 Uvicorn을 사용하여 사용 가능한 가장 빠른 Python 프레임워크 중 하나로 실행되며, Starlette와 Uvicorn 자체(내부적으로 FastAPI가 사용하는 도구)보다 조금 아래에 위치합니다. 그러나 벤치마크와 비교를 확인할 때 다음 사항을 염두에 두어야 합니다. -## 벤치마크와 속도 +## 벤치마크와 속도 { #benchmarks-and-speed } 벤치마크를 확인할 때, 일반적으로 여러 가지 유형의 도구가 동등한 것으로 비교되는 것을 볼 수 있습니다. @@ -16,7 +16,7 @@ * **Uvicorn**: ASGI 서버 * **Starlette**: (Uvicorn 사용) 웹 마이크로 프레임워크 - * **FastAPI**: (Starlette 사용) API 구축을 위한 데이터 검증 등 여러 추가 기능이 포함된 API 마이크로 프레임워크 + * **FastAPI**: (Starlette 사용) 데이터 검증 등 API를 구축하기 위한 여러 추가 기능이 포함된 API 마이크로 프레임워크 * **Uvicorn**: * 서버 자체 외에는 많은 추가 코드가 없기 때문에 최고의 성능을 발휘합니다. @@ -29,6 +29,6 @@ * **FastAPI**: * Starlette가 Uvicorn을 사용하므로 Uvicorn보다 빨라질 수 없는 것과 마찬가지로, **FastAPI**는 Starlette를 사용하므로 더 빠를 수 없습니다. * FastAPI는 Starlette에 추가적으로 더 많은 기능을 제공합니다. API를 구축할 때 거의 항상 필요한 데이터 검증 및 직렬화와 같은 기능들이 포함되어 있습니다. 그리고 이를 사용하면 문서 자동화 기능도 제공됩니다(문서 자동화는 응용 프로그램 실행 시 오버헤드를 추가하지 않고 시작 시 생성됩니다). - * FastAPI를 사용하지 않고 직접 Starlette(또는 Sanic, Flask, Responder 등)를 사용했다면 데이터 검증 및 직렬화를 직접 구현해야 합니다. 따라서 최종 응용 프로그램은 FastAPI를 사용한 것과 동일한 오버헤드를 가지게 될 것입니다. 많은 경우 데이터 검증 및 직렬화가 응용 프로그램에서 작성된 코드 중 가장 많은 부분을 차지합니다. - * 따라서 FastAPI를 사용함으로써 개발 시간, 버그, 코드 라인을 줄일 수 있으며, FastAPI를 사용하지 않았을 때와 동일하거나 더 나은 성능을 얻을 수 있습니다(코드에서 모두 구현해야 하기 때문에). - * FastAPI를 비교할 때는 Flask-apispec, NestJS, Molten 등 데이터 검증, 직렬화 및 문서화가 통합된 자동 데이터 검증, 직렬화 및 문서화를 제공하는 웹 응용 프로그램 프레임워크(또는 도구 집합)와 비교하세요. + * FastAPI를 사용하지 않고 직접 Starlette(또는 다른 도구, 예: Sanic, Flask, Responder 등)를 사용했다면 데이터 검증 및 직렬화를 직접 구현해야 합니다. 따라서 최종 응용 프로그램은 FastAPI를 사용한 것과 동일한 오버헤드를 가지게 될 것입니다. 많은 경우 데이터 검증 및 직렬화가 응용 프로그램에서 작성된 코드 중 가장 많은 부분을 차지합니다. + * 따라서 FastAPI를 사용함으로써 개발 시간, 버그, 코드 라인을 줄일 수 있으며, FastAPI를 사용하지 않았을 때와 동일한 성능(또는 더 나은 성능)을 얻을 수 있을 것입니다(코드에서 모두 구현해야 하기 때문에). + * FastAPI를 비교할 때는 Flask-apispec, NestJS, Molten 등 데이터 검증, 직렬화 및 문서화를 제공하는 웹 애플리케이션 프레임워크(또는 도구 집합)와 비교하세요. 통합된 자동 데이터 검증, 직렬화 및 문서화를 제공하는 프레임워크입니다. diff --git a/docs/ko/docs/deployment/cloud.md b/docs/ko/docs/deployment/cloud.md index dbc814bbdb..0705e120c4 100644 --- a/docs/ko/docs/deployment/cloud.md +++ b/docs/ko/docs/deployment/cloud.md @@ -1,13 +1,24 @@ -# FastAPI를 클라우드 제공업체에서 배포하기 +# 클라우드 제공업체에서 FastAPI 배포하기 { #deploy-fastapi-on-cloud-providers } 사실상 거의 **모든 클라우드 제공업체**를 사용하여 여러분의 FastAPI 애플리케이션을 배포할 수 있습니다. 대부분의 경우, 주요 클라우드 제공업체에서는 FastAPI를 배포할 수 있도록 가이드를 제공합니다. -## 클라우드 제공업체 - 후원자들 +## FastAPI Cloud { #fastapi-cloud } -몇몇 클라우드 제공업체들은 [**FastAPI를 후원하며**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨, 이를 통해 FastAPI와 FastAPI **생태계**가 지속적이고 건전한 **발전**을 할 수 있습니다. +**FastAPI Cloud**는 **FastAPI**를 만든 동일한 작성자와 팀이 구축했습니다. -이는 FastAPI와 **커뮤니티** (여러분)에 대한 진정한 헌신을 보여줍니다. 그들은 여러분에게 **좋은 서비스**를 제공할 뿐 만이 아니라 여러분이 **훌륭하고 건강한 프레임워크인** FastAPI 를 사용하길 원하기 때문입니다. 🙇 +최소한의 노력으로 API를 **구축**, **배포**, **접근**하는 과정을 간소화합니다. -아래와 같은 서비스를 사용해보고 각 서비스의 가이드를 따를 수도 있습니다. +FastAPI로 앱을 빌드할 때의 동일한 **개발자 경험**을 클라우드에 **배포**하는 데에도 제공합니다. 🎉 + +FastAPI Cloud는 *FastAPI and friends* 오픈 소스 프로젝트의 주요 후원자이자 자금 제공자입니다. ✨ + +## 클라우드 제공업체 - 후원자들 { #cloud-providers-sponsors } + +다른 몇몇 클라우드 제공업체들도 ✨ [**FastAPI를 후원합니다**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨. 🙇 + +가이드를 따라 하고 서비스를 사용해보기 위해 이들도 고려해볼 수 있습니다: + +* Render +* Railway diff --git a/docs/ko/docs/deployment/docker.md b/docs/ko/docs/deployment/docker.md index e8b2746c5f..be04c923a5 100644 --- a/docs/ko/docs/deployment/docker.md +++ b/docs/ko/docs/deployment/docker.md @@ -1,17 +1,17 @@ -# 컨테이너의 FastAPI - 도커 +# 컨테이너의 FastAPI - 도커 { #fastapi-in-containers-docker } -FastAPI 어플리케이션을 배포할 때 일반적인 접근 방법은 **리눅스 컨테이너 이미지**를 생성하는 것입니다. 이 방법은 주로 **도커**를 사용해 이루어집니다. 그런 다음 해당 컨테이너 이미지를 몇가지 방법으로 배포할 수 있습니다. +FastAPI 애플리케이션을 배포할 때 일반적인 접근 방법은 **리눅스 컨테이너 이미지**를 빌드하는 것입니다. 보통 **Docker**를 사용해 수행합니다. 그런 다음 해당 컨테이너 이미지를 몇 가지 가능한 방법 중 하나로 배포할 수 있습니다. -리눅스 컨테이너를 사용하는 데에는 **보안**, **반복 가능성**, **단순함** 등의 장점이 있습니다. +리눅스 컨테이너를 사용하면 **보안**, **재현 가능성**, **단순함** 등 여러 장점이 있습니다. /// tip | 팁 -시간에 쫓기고 있고 이미 이런것들을 알고 있다면 [`Dockerfile`👇](#build-a-docker-image-for-fastapi)로 점프할 수 있습니다. +시간이 없고 이미 이런 내용들을 알고 계신가요? 아래의 [`Dockerfile` 👇](#build-a-docker-image-for-fastapi)로 이동하세요. ///
-도커파일 미리보기 👀 +Dockerfile Preview 👀 ```Dockerfile FROM python:3.9 @@ -24,128 +24,125 @@ RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt COPY ./app /code/app -CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"] +CMD ["fastapi", "run", "app/main.py", "--port", "80"] # If running behind a proxy like Nginx or Traefik add --proxy-headers -# CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80", "--proxy-headers"] +# CMD ["fastapi", "run", "app/main.py", "--port", "80", "--proxy-headers"] ```
-## 컨테이너란 +## 컨테이너란 { #what-is-a-container } -컨테이너(주로 리눅스 컨테이너)는 어플리케이션의 의존성과 필요한 파일들을 모두 패키징하는 매우 **가벼운** 방법입니다. 컨테이너는 같은 시스템에 있는 다른 컨테이너(다른 어플리케이션이나 요소들)와 독립적으로 유지됩니다. +컨테이너(주로 리눅스 컨테이너)는 모든 의존성과 필요한 파일을 포함해 애플리케이션을 패키징하면서, 같은 시스템의 다른 컨테이너(다른 애플리케이션이나 컴포넌트)와는 분리된 상태로 유지할 수 있는 매우 **가벼운** 방법입니다. -리눅스 컨테이너는 호스트(머신, 가상 머신, 클라우드 서버 등)와 같은 리눅스 커널을 사용해 실행됩니다. 이말은 리눅스 컨테이너가 (전체 운영체제를 모방하는 다른 가상 머신과 비교했을 때) 매우 가볍다는 것을 의미합니다. +리눅스 컨테이너는 호스트(머신, 가상 머신, 클라우드 서버 등)와 같은 리눅스 커널을 사용해 실행됩니다. 즉, 전체 운영체제를 에뮬레이션하는 완전한 가상 머신에 비해 매우 가볍습니다. -이 방법을 통해, 컨테이너는 직접 프로세스를 실행하는 것과 비슷한 정도의 **적은 자원**을 소비합니다 (가상 머신은 훨씬 많은 자원을 소비할 것입니다). +이 방식으로 컨테이너는 프로세스를 직접 실행하는 것과 비슷한 수준의 **적은 자원**을 소비합니다(가상 머신은 훨씬 더 많은 자원을 소비합니다). -컨테이너는 또한 그들만의 **독립된** 실행 프로세스 (일반적으로 하나의 프로세스로 충분합니다), 파일 시스템, 그리고 네트워크를 가지므로 배포, 보안, 개발 및 기타 과정을 단순화 합니다. +또한 컨테이너는 자체적인 **격리된** 실행 프로세스(보통 하나의 프로세스), 파일 시스템, 네트워크를 가지므로 배포, 보안, 개발 등을 단순화합니다. -## 컨테이너 이미지란 +## 컨테이너 이미지란 { #what-is-a-container-image } -**컨테이너**는 **컨테이너 이미지**를 실행한 것 입니다. +**컨테이너**는 **컨테이너 이미지**에서 실행됩니다. -컨테이너 이미지란 컨테이너에 필요한 모든 파일, 환경 변수 그리고 디폴트 명령/프로그램의 **정적** 버전입니다. 여기서 **정적**이란 말은 컨테이너 **이미지**가 작동되거나 실행되지 않으며, 단지 패키지 파일과 메타 데이터라는 것을 의미합니다. +컨테이너 이미지는 컨테이너에 있어야 하는 모든 파일, 환경 변수, 기본 명령/프로그램의 **정적** 버전입니다. 여기서 **정적**이라는 것은 컨테이너 **이미지**가 실행 중이거나 수행되는 것이 아니라, 패키징된 파일과 메타데이터일 뿐이라는 뜻입니다. -저장된 정적 컨텐츠인 **컨테이너 이미지**와 대조되게, **컨테이너**란 보통 실행될 수 있는 작동 인스턴스를 의미합니다. +저장된 정적 콘텐츠인 "**컨테이너 이미지**"와 달리, "**컨테이너**"는 보통 실행 중인 인스턴스, 즉 **실행되는** 대상을 의미합니다. -**컨테이너**가 (**컨테이너 이미지**로 부터) 시작되고 실행되면, 컨테이너는 파일이나 환경 변수를 생성하거나 변경할 수 있습니다. 이러한 변화는 오직 컨테이너에서만 존재하며, 그 기반이 되는 컨테이너 이미지에는 지속되지 않습니다 (즉 디스크에는 저장되지 않습니다). +**컨테이너**가 시작되어 실행 중이면(**컨테이너 이미지**로부터 시작됨) 파일, 환경 변수 등을 생성하거나 변경할 수 있습니다. 이러한 변경은 해당 컨테이너에만 존재하며, 기반이 되는 컨테이너 이미지에는 지속되지 않습니다(디스크에 저장되지 않습니다). -컨테이너 이미지는 **프로그램** 파일과 컨텐츠, 즉 `python`과 어떤 파일 `main.py`에 비교할 수 있습니다. +컨테이너 이미지는 **프로그램** 파일과 그 콘텐츠, 예를 들어 `python`과 어떤 파일 `main.py`에 비유할 수 있습니다. -그리고 (**컨테이너 이미지**와 대비해서) **컨테이너**는 이미지의 실제 실행 인스턴스로 **프로세스**에 비교할 수 있습니다. 사실, 컨테이너는 **프로세스 러닝**이 있을 때만 실행됩니다 (그리고 보통 하나의 프로세스 입니다). 컨테이너는 내부에서 실행되는 프로세스가 없으면 종료됩니다. +그리고 **컨테이너** 자체는(**컨테이너 이미지**와 달리) 이미지의 실제 실행 인스턴스로서 **프로세스**에 비유할 수 있습니다. 실제로 컨테이너는 **실행 중인 프로세스**가 있을 때만 실행됩니다(보통 단일 프로세스입니다). 컨테이너 내부에 실행 중인 프로세스가 없으면 컨테이너는 중지됩니다. -## 컨테이너 이미지 +## 컨테이너 이미지 { #container-images } -도커는 **컨테이너 이미지**와 **컨테이너**를 생성하고 관리하는데 주요 도구 중 하나가 되어왔습니다. +Docker는 **컨테이너 이미지**와 **컨테이너**를 생성하고 관리하는 주요 도구 중 하나입니다. -그리고 도커 허브에 다양한 도구, 환경, 데이터베이스, 그리고 어플리케이션에 대해 미리 만들어진 **공식 컨테이너 이미지**가 공개되어 있습니다. +또한 Docker Hub에는 다양한 도구, 환경, 데이터베이스, 애플리케이션을 위한 미리 만들어진 **공식 컨테이너 이미지**가 공개되어 있습니다. -예를 들어, 공식 파이썬 이미지가 있습니다. +예를 들어, 공식 Python Image가 있습니다. -또한 다른 대상, 예를 들면 데이터베이스를 위한 이미지들도 있습니다: +그리고 데이터베이스 등 다양한 용도의 다른 이미지도 많이 있습니다. 예를 들면: * PostgreSQL * MySQL * MongoDB * Redis 등 -미리 만들어진 컨테이너 이미지를 사용하면 서로 다른 도구들을 **결합**하기 쉽습니다. 대부분의 경우에, **공식 이미지들**을 사용하고 환경 변수를 통해 설정할 수 있습니다. +미리 만들어진 컨테이너 이미지를 사용하면 서로 다른 도구를 **결합**하고 사용하기가 매우 쉽습니다. 예를 들어 새로운 데이터베이스를 시험해 볼 때도 그렇습니다. 대부분의 경우 **공식 이미지**를 사용하고, 환경 변수로 설정만 하면 됩니다. -이런 방법으로 대부분의 경우에 컨테이너와 도커에 대해 배울 수 있으며 다양한 도구와 요소들에 대한 지식을 재사용할 수 있습니다. +이렇게 하면 많은 경우 컨테이너와 Docker를 학습하고, 그 지식을 여러 다른 도구와 컴포넌트에 재사용할 수 있습니다. -따라서, 서로 다른 **다중 컨테이너**를 생성한 다음 이들을 연결할 수 있습니다. 예를 들어 데이터베이스, 파이썬 어플리케이션, 리액트 프론트엔드 어플리케이션을 사용하는 웹 서버에 대한 컨테이너를 만들어 이들의 내부 네트워크로 각 컨테이너를 연결할 수 있습니다. +따라서 데이터베이스, Python 애플리케이션, React 프론트엔드 애플리케이션이 있는 웹 서버 등 서로 다른 것들을 담은 **여러 컨테이너**를 실행하고 내부 네트워크를 통해 연결할 수 있습니다. -모든 컨테이너 관리 시스템(도커나 쿠버네티스)은 이러한 네트워킹 특성을 포함하고 있습니다. +Docker나 Kubernetes 같은 모든 컨테이너 관리 시스템에는 이러한 네트워킹 기능이 통합되어 있습니다. -## 컨테이너와 프로세스 +## 컨테이너와 프로세스 { #containers-and-processes } -**컨테이너 이미지**는 보통 **컨테이너**를 시작하기 위해 필요한 메타데이터와 디폴트 커맨드/프로그램과 그 프로그램에 전달하기 위한 파라미터들을 포함합니다. 이는 커맨드 라인에서 프로그램을 실행할 때 필요한 값들과 유사합니다. +**컨테이너 이미지**는 보통 **컨테이너**가 시작될 때 실행되어야 하는 기본 프로그램/명령과 해당 프로그램에 전달할 매개변수를 메타데이터에 포함합니다. 커맨드 라인에서 실행할 때와 매우 유사합니다. -**컨테이너**가 시작되면, 해당 커맨드/프로그램이 실행됩니다 (그러나 다른 커맨드/프로그램을 실행하도록 오버라이드 할 수 있습니다). +**컨테이너**가 시작되면 해당 명령/프로그램을 실행합니다(다만 오버라이드하여 다른 명령/프로그램을 실행하게 할 수도 있습니다). -컨테이너는 **메인 프로세스**(커맨드 또는 프로그램)이 실행되는 동안 실행됩니다. +컨테이너는 **메인 프로세스**(명령 또는 프로그램)가 실행되는 동안 실행됩니다. -컨테이너는 일반적으로 **단일 프로세스**를 가지고 있지만, 메인 프로세스의 서브 프로세스를 시작하는 것도 가능하며, 이 방법으로 하나의 컨테이너에 **다중 프로세스**를 가질 수 있습니다. +컨테이너는 보통 **단일 프로세스**를 가지지만, 메인 프로세스에서 서브프로세스를 시작할 수도 있으며, 그러면 같은 컨테이너에 **여러 프로세스**가 존재하게 됩니다. -그러나 **최소한 하나의 실행중인 프로세스**를 가지지 않고서는 실행중인 컨테이너를 가질 수 없습니다. 만약 메인 프로세스가 중단되면, 컨테이너도 중단됩니다. +하지만 **최소 하나의 실행 중인 프로세스** 없이 실행 중인 컨테이너를 가질 수는 없습니다. 메인 프로세스가 중지되면 컨테이너도 중지됩니다. -## FastAPI를 위한 도커 이미지 빌드하기 +## FastAPI를 위한 도커 이미지 빌드하기 { #build-a-docker-image-for-fastapi } -이제 무언가를 만들어 봅시다! 🚀 +좋습니다, 이제 무언가를 만들어 봅시다! 🚀 -**공식 파이썬** 이미지에 기반하여, FastAPI를 위한 **도커 이미지**를 **맨 처음부터** 생성하는 방법을 보이겠습니다. +**공식 Python** 이미지에 기반하여 FastAPI용 **Docker 이미지**를 **처음부터** 빌드하는 방법을 보여드리겠습니다. -**대부분의 경우**에 다음과 같은 것들을 하게 됩니다. 예를 들면: +이는 **대부분의 경우**에 하고 싶은 방식입니다. 예를 들면: -* **쿠버네티스** 또는 유사한 도구 사용하기 -* **라즈베리 파이**로 실행하기 -* 컨테이너 이미지를 실행할 클라우드 서비스 사용하기 등 +* **Kubernetes** 또는 유사한 도구를 사용할 때 +* **Raspberry Pi**에서 실행할 때 +* 컨테이너 이미지를 대신 실행해주는 클라우드 서비스를 사용할 때 등 -### 요구 패키지 +### 패키지 요구사항 { #package-requirements } -일반적으로는 어플리케이션의 특정 파일을 위한 **패키지 요구 조건**이 있을 것입니다. +보통 애플리케이션의 **패키지 요구사항**을 어떤 파일에 적어 둡니다. -그 요구 조건을 **설치**하는 방법은 여러분이 사용하는 도구에 따라 다를 것입니다. +이는 주로 그 요구사항을 **설치**하는 데 사용하는 도구에 따라 달라집니다. -가장 일반적인 방법은 패키지 이름과 버전이 줄 별로 기록된 `requirements.txt` 파일을 만드는 것입니다. +가장 일반적인 방법은 패키지 이름과 버전을 한 줄에 하나씩 적어 둔 `requirements.txt` 파일을 사용하는 것입니다. -버전의 범위를 설정하기 위해서는 [FastAPI 버전들에 대하여](versions.md){.internal-link target=_blank}에 쓰여진 것과 같은 아이디어를 사용합니다. +버전 범위를 설정할 때는 [FastAPI 버전들에 대하여](versions.md){.internal-link target=_blank}에서 읽은 것과 같은 아이디어를 사용하면 됩니다. -예를 들어, `requirements.txt` 파일은 다음과 같을 수 있습니다: +예를 들어 `requirements.txt`는 다음과 같을 수 있습니다: ``` -fastapi>=0.68.0,<0.69.0 -pydantic>=1.8.0,<2.0.0 -uvicorn>=0.15.0,<0.16.0 +fastapi[standard]>=0.113.0,<0.114.0 +pydantic>=2.7.0,<3.0.0 ``` -그리고 일반적으로 패키지 종속성은 `pip`로 설치합니다. 예를 들어: +그리고 보통 `pip`로 패키지 의존성을 설치합니다. 예를 들면:
```console $ pip install -r requirements.txt ---> 100% -Successfully installed fastapi pydantic uvicorn +Successfully installed fastapi pydantic ```
/// info | 정보 -패키지 종속성을 정의하고 설치하기 위한 방법과 도구는 다양합니다. - -나중에 아래 세션에서 Poetry를 사용한 예시를 보이겠습니다. 👇 +패키지 의존성을 정의하고 설치하는 다른 형식과 도구도 있습니다. /// -### **FastAPI** 코드 생성하기 +### **FastAPI** 코드 생성하기 { #create-the-fastapi-code } -* `app` 디렉터리를 생성하고 이동합니다. -* 빈 파일 `__init__.py`을 생성합니다. -* 다음과 같은 `main.py`을 생성합니다: +* `app` 디렉터리를 만들고 들어갑니다. +* 빈 파일 `__init__.py`를 만듭니다. +* 다음 내용으로 `main.py` 파일을 만듭니다: ```Python from typing import Union @@ -165,79 +162,109 @@ def read_item(item_id: int, q: Union[str, None] = None): return {"item_id": item_id, "q": q} ``` -### 도커파일 +### Dockerfile { #dockerfile } -이제 같은 프로젝트 디렉터리에 다음과 같은 파일 `Dockerfile`을 생성합니다: +이제 같은 프로젝트 디렉터리에 다음 내용으로 `Dockerfile` 파일을 만듭니다: ```{ .dockerfile .annotate } -# (1) +# (1)! FROM python:3.9 -# (2) +# (2)! WORKDIR /code -# (3) +# (3)! COPY ./requirements.txt /code/requirements.txt -# (4) +# (4)! RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt -# (5) +# (5)! COPY ./app /code/app -# (6) -CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"] +# (6)! +CMD ["fastapi", "run", "app/main.py", "--port", "80"] ``` -1. 공식 파이썬 베이스 이미지에서 시작합니다. +1. 공식 Python 베이스 이미지에서 시작합니다. -2. 현재 워킹 디렉터리를 `/code`로 설정합니다. +2. 현재 작업 디렉터리를 `/code`로 설정합니다. - 여기에 `requirements.txt` 파일과 `app` 디렉터리를 위치시킬 것입니다. + 여기에 `requirements.txt` 파일과 `app` 디렉터리를 둘 것입니다. -3. 요구 조건과 파일을 `/code` 디렉터리로 복사합니다. +3. 요구사항 파일을 `/code` 디렉터리로 복사합니다. - 처음에는 **오직** 요구 조건이 필요한 파일만 복사하고, 이외의 코드는 그대로 둡니다. + 처음에는 요구사항 파일만 **단독으로** 복사하고, 나머지 코드는 복사하지 않습니다. - 이 파일이 **자주 바뀌지 않기 때문에**, 도커는 파일을 탐지하여 이 단계의 **캐시**를 사용하여 다음 단계에서도 캐시를 사용할 수 있도록 합니다. + 이 파일은 **자주 바뀌지 않기** 때문에 Docker는 이를 감지하여 이 단계에서 **캐시**를 사용하고, 다음 단계에서도 캐시를 사용할 수 있게 해줍니다. -4. 요구 조건 파일에 있는 패키지 종속성을 설치합니다. +4. 요구사항 파일에 있는 패키지 의존성을 설치합니다. - `--no-cache-dir` 옵션은 `pip`에게 다운로드한 패키지들을 로컬 환경에 저장하지 않도록 전달합니다. 이는 마치 같은 패키지를 설치하기 위해 오직 `pip`만 다시 실행하면 될 것 같지만, 컨테이너로 작업하는 경우 그렇지는 않습니다. + `--no-cache-dir` 옵션은 `pip`가 다운로드한 패키지를 로컬에 저장하지 않도록 합니다. 이는 `pip`가 같은 패키지를 설치하기 위해 다시 실행될 때만 의미가 있지만, 컨테이너 작업에서는 그렇지 않기 때문입니다. - /// note | 노트 + /// note | 참고 - `--no-cache-dir` 는 오직 `pip`와 관련되어 있으며, 도커나 컨테이너와는 무관합니다. + `--no-cache-dir`는 `pip`에만 관련되어 있으며 Docker나 컨테이너와는 관련이 없습니다. /// - `--upgrade` 옵션은 `pip`에게 설치된 패키지들을 업데이트하도록 합니다. + `--upgrade` 옵션은 이미 설치된 패키지가 있다면 `pip`가 이를 업그레이드하도록 합니다. - 이전 단계에서 파일을 복사한 것이 **도커 캐시**에 의해 탐지되기 때문에, 이 단계에서도 가능한 한 **도커 캐시**를 사용하게 됩니다. + 이전 단계에서 파일을 복사한 것이 **Docker 캐시**에 의해 감지될 수 있으므로, 이 단계에서도 가능하면 **Docker 캐시를 사용**합니다. - 이 단계에서 캐시를 사용하면 **매번** 모든 종속성을 다운로드 받고 설치할 필요가 없어, 개발 과정에서 이미지를 지속적으로 생성하는 데에 드는 **시간**을 많이 **절약**할 수 있습니다. + 이 단계에서 캐시를 사용하면 개발 중에 이미지를 반복해서 빌드할 때, 의존성을 **매번 다운로드하고 설치하는** 대신 많은 **시간**을 **절약**할 수 있습니다. -5. `/code` 디렉터리에 `./app` 디렉터리를 복사합니다. +5. `./app` 디렉터리를 `/code` 디렉터리 안으로 복사합니다. - **자주 변경되는** 모든 코드를 포함하고 있기 때문에, 도커 **캐시**는 이 단계나 **이후의 단계에서** 잘 사용되지 않습니다. + 이 디렉터리에는 **가장 자주 변경되는** 코드가 모두 포함되어 있으므로, Docker **캐시**는 이 단계나 **이후 단계들**에서는 쉽게 사용되지 않습니다. - 그러므로 컨테이너 이미지 빌드 시간을 최적화하기 위해 `Dockerfile`의 **거의 끝 부분**에 입력하는 것이 중요합니다. + 따라서 컨테이너 이미지 빌드 시간을 최적화하려면 `Dockerfile`의 **끝부분 근처**에 두는 것이 중요합니다. -6. `uvicorn` 서버를 실행하기 위해 **커맨드**를 설정합니다. +6. 내부적으로 Uvicorn을 사용하는 `fastapi run`을 사용하도록 **명령**을 설정합니다. - `CMD`는 문자열 리스트를 입력받고, 각 문자열은 커맨드 라인의 각 줄에 입력할 문자열입니다. + `CMD`는 문자열 리스트를 받으며, 각 문자열은 커맨드 라인에서 공백으로 구분해 입력하는 항목들입니다. - 이 커맨드는 **현재 워킹 디렉터리**에서 실행되며, 이는 위에서 `WORKDIR /code`로 설정한 `/code` 디렉터리와 같습니다. - - 프로그램이 `/code`에서 시작하고 그 속에 `./app` 디렉터리가 여러분의 코드와 함께 들어있기 때문에, **Uvicorn**은 이를 보고 `app`을 `app.main`으로부터 **불러 올** 것입니다. + 이 명령은 **현재 작업 디렉터리**에서 실행되며, 이는 위에서 `WORKDIR /code`로 설정한 `/code` 디렉터리와 같습니다. /// tip | 팁 -각 코드 라인을 코드의 숫자 버블을 클릭하여 리뷰할 수 있습니다. 👆 +코드의 각 숫자 버블을 클릭해 각 줄이 하는 일을 확인하세요. 👆 /// -이제 여러분은 다음과 같은 디렉터리 구조를 가지고 있을 것입니다: +/// warning | 경고 + +아래에서 설명하는 것처럼 `CMD` 지시어는 **항상** **exec form**을 사용해야 합니다. + +/// + +#### `CMD` 사용하기 - Exec Form { #use-cmd-exec-form } + +Docker 지시어 `CMD`는 두 가지 형식으로 작성할 수 있습니다: + +✅ **Exec** form: + +```Dockerfile +# ✅ Do this +CMD ["fastapi", "run", "app/main.py", "--port", "80"] +``` + +⛔️ **Shell** form: + +```Dockerfile +# ⛔️ Don't do this +CMD fastapi run app/main.py --port 80 +``` + +FastAPI가 정상적으로 종료(graceful shutdown)되고 [lifespan 이벤트](../advanced/events.md){.internal-link target=_blank}가 트리거되도록 하려면, 항상 **exec** form을 사용하세요. + +자세한 내용은 shell and exec form에 대한 Docker 문서를 참고하세요. + +이는 `docker compose`를 사용할 때 꽤 눈에 띌 수 있습니다. 좀 더 기술적인 상세 내용은 Docker Compose FAQ 섹션을 참고하세요: Why do my services take 10 seconds to recreate or stop?. + +#### 디렉터리 구조 { #directory-structure } + +이제 다음과 같은 디렉터리 구조가 되어야 합니다: ``` . @@ -248,51 +275,51 @@ CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"] └── requirements.txt ``` -#### TLS 종료 프록시의 배후 +#### TLS 종료 프록시의 배후 { #behind-a-tls-termination-proxy } -만약 여러분이 컨테이너를 Nginx 또는 Traefik과 같은 TLS 종료 프록시 (로드 밸런서) 뒤에서 실행하고 있다면, `--proxy-headers` 옵션을 더하는 것이 좋습니다. 이 옵션은 Uvicorn에게 어플리케이션이 HTTPS 등의 뒤에서 실행되고 있으므로 프록시에서 전송된 헤더를 신뢰할 수 있다고 알립니다. +Nginx나 Traefik 같은 TLS 종료 프록시(로드 밸런서) 뒤에서 컨테이너를 실행하고 있다면 `--proxy-headers` 옵션을 추가하세요. 이 옵션은 (FastAPI CLI를 통해) Uvicorn에게 해당 프록시가 보낸 헤더를 신뢰하도록 하여, 애플리케이션이 HTTPS 뒤에서 실행 중임을 알게 합니다. ```Dockerfile -CMD ["uvicorn", "app.main:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "80"] +CMD ["fastapi", "run", "app/main.py", "--proxy-headers", "--port", "80"] ``` -#### 도커 캐시 +#### 도커 캐시 { #docker-cache } -이 `Dockerfile`에는 중요한 트릭이 있는데, 처음에는 **의존성이 있는 파일만** 복사하고, 나머지 코드는 그대로 둡니다. 왜 이런 방법을 써야하는지 설명하겠습니다. +이 `Dockerfile`에는 중요한 트릭이 있습니다. 먼저 **의존성 파일만** 복사하고, 나머지 코드는 복사하지 않는 것입니다. 왜 그런지 설명하겠습니다. ```Dockerfile COPY ./requirements.txt /code/requirements.txt ``` -도커와 다른 도구들은 컨테이너 이미지를 **증가하는 방식으로 빌드**합니다. `Dockerfile`의 맨 윗 부분부터 시작해, 레이어 위에 새로운 레이어를 더하는 방식으로, `Dockerfile`의 각 지시 사항으로 부터 생성된 어떤 파일이든 더해갑니다. +Docker와 다른 도구들은 `Dockerfile`의 위에서부터 시작해, 각 지시어가 만든 파일을 포함하며 **레이어를 하나씩 위에 쌓는 방식으로** 컨테이너 이미지를 **점진적으로** 빌드합니다. -도커 그리고 이와 유사한 도구들은 이미지 생성 시에 **내부 캐시**를 사용합니다. 만약 어떤 파일이 마지막으로 컨테이너 이미지를 빌드한 때로부터 바뀌지 않았다면, 파일을 다시 복사하여 새로운 레이어를 처음부터 생성하는 것이 아니라, 마지막에 생성했던 **같은 레이어를 재사용**합니다. +Docker와 유사한 도구들은 이미지를 빌드할 때 **내부 캐시**도 사용합니다. 어떤 파일이 마지막으로 컨테이너 이미지를 빌드했을 때부터 바뀌지 않았다면, 파일을 다시 복사하고 새 레이어를 처음부터 만드는 대신, 이전에 만든 **같은 레이어를 재사용**합니다. -단지 파일 복사를 지양하는 것으로 효율이 많이 향상되는 것은 아니지만, 그 단계에서 캐시를 사용했기 때문에, **다음 단계에서도 마찬가지로 캐시를 사용**할 수 있습니다. 예를 들어, 다음과 같은 의존성을 설치하는 지시 사항을 위한 캐시를 사용할 수 있습니다: +파일 복사를 피하는 것만으로 큰 개선이 생기지는 않을 수 있지만, 해당 단계에서 캐시를 사용했기 때문에 **다음 단계에서도 캐시를 사용할 수** 있습니다. 예를 들어 다음과 같이 의존성을 설치하는 지시어에서 캐시를 사용할 수 있습니다: ```Dockerfile RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt ``` -패키지를 포함하는 파일은 **자주 변경되지 않습니다**. 따라서 해당 파일만 복사하므로서, 도커는 그 단계의 **캐시를 사용**할 수 있습니다. +패키지 요구사항 파일은 **자주 변경되지 않습니다**. 따라서 그 파일만 복사하면 Docker는 그 단계에서 **캐시를 사용할 수** 있습니다. -그 다음으로, 도커는 **다음 단계에서** 의존성을 다운로드하고 설치하는 **캐시를 사용**할 수 있게 됩니다. 바로 이 과정에서 우리는 **많은 시간을 절약**하게 됩니다. ✨ ...그리고 기다리는 지루함도 피할 수 있습니다. 😪😆 +그리고 Docker는 그 다음 단계에서 의존성을 다운로드하고 설치할 때도 **캐시를 사용할 수** 있습니다. 바로 여기에서 **많은 시간을 절약**하게 됩니다. ✨ ...그리고 기다리며 지루해지는 것도 피할 수 있습니다. 😪😆 -패키지 의존성을 다운로드 받고 설치하는 데이는 **수 분이 걸릴 수 있지만**, **캐시**를 사용하면 최대 **수 초만에** 끝낼 수 있습니다. +패키지 의존성을 다운로드하고 설치하는 데에는 **몇 분**이 걸릴 수 있지만, **캐시**를 사용하면 많아야 **몇 초**면 끝납니다. -또한 여러분이 개발 과정에서 코드의 변경 사항이 반영되었는지 확인하기 위해 컨테이너 이미지를 계속해서 빌드하면, 절약된 시간은 축적되어 더욱 커질 것입니다. +또한 개발 중에 코드 변경 사항이 동작하는지 확인하기 위해 컨테이너 이미지를 계속 빌드하게 되므로, 이렇게 절약되는 시간은 누적되어 상당히 커집니다. -그리고 나서 `Dockerfile`의 거의 끝 부분에서, 모든 코드를 복사합니다. 이것이 **가장 빈번하게 변경**되는 부분이며, 대부분의 경우에 이 다음 단계에서는 캐시를 사용할 수 없기 때문에 가장 마지막에 둡니다. +그 다음 `Dockerfile`의 끝부분 근처에서 모든 코드를 복사합니다. 이 부분은 **가장 자주 변경되는** 부분이므로, 거의 항상 이 단계 이후에는 캐시를 사용할 수 없기 때문에 끝부분에 둡니다. ```Dockerfile COPY ./app /code/app ``` -### 도커 이미지 생성하기 +### 도커 이미지 생성하기 { #build-the-docker-image } -이제 모든 파일이 제자리에 있으니, 컨테이너 이미지를 빌드합니다. +이제 모든 파일이 제자리에 있으니 컨테이너 이미지를 빌드해봅시다. -* (여러분의 `Dockerfile`과 `app` 디렉터리가 위치한) 프로젝트 디렉터리로 이동합니다. +* 프로젝트 디렉터리로 이동합니다(`Dockerfile`이 있고 `app` 디렉터리를 포함하는 위치). * FastAPI 이미지를 빌드합니다:
@@ -307,13 +334,13 @@ $ docker build -t myimage . /// tip | 팁 -맨 끝에 있는 `.` 에 주목합시다. 이는 `./`와 동등하며, 도커에게 컨테이너 이미지를 빌드하기 위한 디렉터리를 알려줍니다. +끝에 있는 `.`에 주목하세요. 이는 `./`와 동일하며, Docker에게 컨테이너 이미지를 빌드할 때 사용할 디렉터리를 알려줍니다. -이 경우에는 현재 디렉터리(`.`)와 같습니다. +이 경우 현재 디렉터리(`.`)입니다. /// -### 도커 컨테이너 시작하기 +### 도커 컨테이너 시작하기 { #start-the-docker-container } * 여러분의 이미지에 기반하여 컨테이너를 실행합니다: @@ -325,35 +352,35 @@ $ docker run -d --name mycontainer -p 80:80 myimage
-## 체크하기 +## 확인하기 { #check-it } -여러분의 도커 컨테이너 URL에서 실행 사항을 체크할 수 있습니다. 예를 들어: http://192.168.99.100/items/5?q=somequery 또는 http://127.0.0.1/items/5?q=somequery (또는 동일하게, 여러분의 도커 호스트를 이용해서 체크할 수도 있습니다). +Docker 컨테이너의 URL에서 확인할 수 있어야 합니다. 예를 들어: http://192.168.99.100/items/5?q=somequery 또는 http://127.0.0.1/items/5?q=somequery(또는 Docker 호스트를 사용해 동등하게 확인할 수 있습니다). -아래와 비슷한 것을 보게 될 것입니다: +아래와 같은 것을 보게 될 것입니다: ```JSON {"item_id": 5, "q": "somequery"} ``` -## 인터랙티브 API 문서 +## 인터랙티브 API 문서 { #interactive-api-docs } -이제 여러분은 http://192.168.99.100/docs 또는 http://127.0.0.1/docs로 이동할 수 있습니다(또는, 여러분의 도커 호스트를 이용할 수 있습니다). +이제 http://192.168.99.100/docs 또는 http://127.0.0.1/docs(또는 Docker 호스트를 사용해 동등하게 접근)로 이동할 수 있습니다. -여러분은 자동으로 생성된 인터랙티브 API(Swagger UI에서 제공된)를 볼 수 있습니다: +자동으로 생성된 인터랙티브 API 문서(Swagger UI 제공)를 볼 수 있습니다: ![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) -## 대안 API 문서 +## 대안 API 문서 { #alternative-api-docs } -또한 여러분은 http://192.168.99.100/redoc 또는 http://127.0.0.1/redoc으로 이동할 수 있습니다(또는, 여러분의 도커 호스트를 이용할 수 있습니다). +또한 http://192.168.99.100/redoc 또는 http://127.0.0.1/redoc(또는 Docker 호스트를 사용해 동등하게 접근)로 이동할 수도 있습니다. -여러분은 자동으로 생성된 대안 문서(ReDoc에서 제공된)를 볼 수 있습니다: +대안 자동 문서(ReDoc 제공)를 볼 수 있습니다: ![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) -## 단일 파일 FastAPI로 도커 이미지 생성하기 +## 단일 파일 FastAPI로 도커 이미지 빌드하기 { #build-a-docker-image-with-a-single-file-fastapi } -만약 여러분의 FastAPI가 하나의 파일이라면, 예를 들어 `./app` 디렉터리 없이 `main.py` 파일만으로 이루어져 있다면, 파일 구조는 다음과 유사할 것입니다: +FastAPI가 단일 파일(예: `./app` 디렉터리 없이 `main.py`만 있는 경우)이라면, 파일 구조는 다음과 같을 수 있습니다: ``` . @@ -362,7 +389,7 @@ $ docker run -d --name mycontainer -p 80:80 myimage └── requirements.txt ``` -그러면 여러분들은 `Dockerfile` 내에 있는 파일을 복사하기 위해 그저 상응하는 경로를 바꾸기만 하면 됩니다: +그런 다음 `Dockerfile`에서 해당 파일을 복사하도록 경로만 맞게 변경하면 됩니다: ```{ .dockerfile .annotate hl_lines="10 13" } FROM python:3.9 @@ -373,359 +400,221 @@ COPY ./requirements.txt /code/requirements.txt RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt -# (1) +# (1)! COPY ./main.py /code/ -# (2) -CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "80"] +# (2)! +CMD ["fastapi", "run", "main.py", "--port", "80"] ``` -1. `main.py` 파일을 `/code` 디렉터리로 곧바로 복사합니다(`./app` 디렉터리는 고려하지 않습니다). +1. `main.py` 파일을 `/code` 디렉터리로 직접 복사합니다(`./app` 디렉터리 없이). -2. Uvicorn을 실행해 `app` 객체를 (`app.main` 대신) `main`으로 부터 불러오도록 합니다. +2. 단일 파일 `main.py`에 있는 애플리케이션을 제공(serve)하기 위해 `fastapi run`을 사용합니다. -그 다음 Uvicorn 커맨드를 조정해서 FastAPI 객체를 불러오는데 `app.main` 대신에 새로운 모듈 `main`을 사용하도록 합니다. +`fastapi run`에 파일을 전달하면, 이것이 패키지의 일부가 아닌 단일 파일이라는 것을 자동으로 감지하고, 어떻게 임포트해서 FastAPI 앱을 제공할지 알아냅니다. 😎 -## 배포 개념 +## 배포 개념 { #deployment-concepts } -이제 컨테이너의 측면에서 [배포 개념](concepts.md){.internal-link target=_blank}에서 다루었던 것과 같은 배포 개념에 대해 이야기해 보겠습니다. +컨테이너 관점에서 같은 [배포 개념](concepts.md){.internal-link target=_blank}들을 다시 이야기해 봅시다. -컨테이너는 주로 어플리케이션을 빌드하고 배포하기 위한 과정을 단순화하는 도구이지만, **배포 개념**에 대한 특정한 접근법을 강요하지 않기 때문에 가능한 배포 전략에는 여러가지가 있습니다. +컨테이너는 주로 애플리케이션의 **빌드 및 배포** 과정을 단순화하는 도구이지만, 이러한 **배포 개념**을 처리하는 특정 접근 방식을 강제하지는 않으며, 가능한 전략은 여러 가지입니다. -**좋은 소식**은 서로 다른 전략들을 포괄하는 배포 개념이 있다는 점입니다. 🎉 +**좋은 소식**은 각 전략마다 모든 배포 개념을 다룰 수 있는 방법이 있다는 점입니다. 🎉 -컨테이너 측면에서 **배포 개념**을 리뷰해 보겠습니다: +컨테이너 관점에서 이 **배포 개념**들을 살펴봅시다: * HTTPS -* 구동하기 +* 시작 시 자동 실행 * 재시작 -* 복제 (실행 중인 프로세스 개수) +* 복제(실행 중인 프로세스 수) * 메모리 -* 시작하기 전 단계들 +* 시작 전 사전 단계 -## HTTPS +## HTTPS { #https } -만약 우리가 FastAPI 어플리케이션을 위한 **컨테이너 이미지**에만 집중한다면 (그리고 나중에 실행될 **컨테이너**에), HTTPS는 일반적으로 다른 도구에 의해 **외부적으로** 다루어질 것 입니다. +FastAPI 애플리케이션의 **컨테이너 이미지**(그리고 나중에 실행 중인 **컨테이너**)에만 집중한다면, HTTPS는 보통 다른 도구에 의해 **외부적으로** 처리됩니다. -**HTTPS**와 **인증서**의 **자동** 취득을 다루는 것은 다른 컨테이너가 될 수 있는데, 예를 들어 Traefik을 사용하는 것입니다. +예를 들어 Traefik을 사용하는 다른 컨테이너가 **HTTPS**와 **인증서**의 **자동** 획득을 처리할 수 있습니다. /// tip | 팁 -Traefik은 도커, 쿠버네티스, 그리고 다른 도구와 통합되어 있어 여러분의 컨테이너를 포함하는 HTTPS를 셋업하고 설정하는 것이 매우 쉽습니다. +Traefik은 Docker, Kubernetes 등과 통합되어 있어, 이를 사용해 컨테이너에 HTTPS를 설정하고 구성하기가 매우 쉽습니다. /// -대안적으로, HTTPS는 클라우드 제공자에 의해 서비스의 일환으로 다루어질 수도 있습니다 (이때도 어플리케이션은 여전히 컨테이너에서 실행될 것입니다). +또는 HTTPS를 클라우드 제공자가 서비스의 일부로 처리할 수도 있습니다(애플리케이션은 여전히 컨테이너에서 실행됩니다). -## 구동과 재시작 +## 시작 시 자동 실행과 재시작 { #running-on-startup-and-restarts } -여러분의 컨테이너를 **시작하고 실행하는** 데에 일반적으로 사용되는 도구는 따로 있습니다. +보통 컨테이너를 **시작하고 실행**하는 역할을 담당하는 다른 도구가 있습니다. -이는 **도커** 자체일 수도 있고, **도커 컴포즈**, **쿠버네티스**, **클라우드 서비스** 등이 될 수 있습니다. +직접 **Docker**일 수도 있고, **Docker Compose**, **Kubernetes**, **클라우드 서비스** 등일 수도 있습니다. -대부분 (또는 전체) 경우에, 컨테이너를 구동하거나 고장시에 재시작하도록 하는 간단한 옵션이 있습니다. 예를 들어, 도커에서는, 커맨드 라인 옵션 `--restart` 입니다. +대부분(또는 전부)의 경우, 시작 시 컨테이너를 실행하고 실패 시 재시작을 활성화하는 간단한 옵션이 있습니다. 예를 들어 Docker에서는 커맨드 라인 옵션 `--restart`입니다. -컨테이너를 사용하지 않고서는, 어플리케이션을 구동하고 재시작하는 것이 매우 번거롭고 어려울 수 있습니다. 하지만 **컨테이너를 사용한다면** 대부분의 경우에 이런 기능은 기본적으로 포함되어 있습니다. ✨ +컨테이너를 사용하지 않으면 애플리케이션을 시작 시 자동 실행하고 재시작까지 구성하는 것이 번거롭고 어렵습니다. 하지만 **컨테이너로 작업할 때**는 대부분의 경우 그 기능이 기본으로 포함되어 있습니다. ✨ -## 복제 - 프로세스 개수 +## 복제 - 프로세스 개수 { #replication-number-of-processes } -만약 여러분이 **쿠버네티스**와 머신 클러스터, 도커 스왐 모드, 노마드, 또는 다른 여러 머신 위에 분산 컨테이너를 관리하는 복잡한 시스템을 다루고 있다면, 여러분은 각 컨테이너에서 (워커와 함께 사용하는 Gunicorn 같은) **프로세스 매니저** 대신 **클러스터 레벨**에서 **복제를 다루**고 싶을 것입니다. +**Kubernetes**, Docker Swarm Mode, Nomad 등의 복잡한 시스템으로 여러 머신에 분산된 컨테이너를 관리하는 cluster를 사용한다면, 각 컨테이너에서(**워커를 사용하는 Uvicorn** 같은) **프로세스 매니저**를 쓰는 대신, **클러스터 레벨**에서 **복제를 처리**하고 싶을 가능성이 큽니다. -쿠버네티스와 같은 분산 컨테이너 관리 시스템 중 일부는 일반적으로 들어오는 요청에 대한 **로드 밸런싱**을 지원하면서 **컨테이너 복제**를 다루는 통합된 방법을 가지고 있습니다. 모두 **클러스터 레벨**에서 말이죠. +Kubernetes 같은 분산 컨테이너 관리 시스템은 보통 들어오는 요청에 대한 **로드 밸런싱**을 지원하면서도, **컨테이너 복제**를 처리하는 통합된 방법을 가지고 있습니다. 모두 **클러스터 레벨**에서요. -이런 경우에, 여러분은 [위에서 묘사된 것](#dockerfile)처럼 **처음부터 도커 이미지를** 빌드해서, 의존성을 설치하고, Uvicorn 워커를 관리하는 Gunicorn 대신 **단일 Uvicorn 프로세스**를 실행하고 싶을 것입니다. +그런 경우에는 [위에서 설명한 대로](#dockerfile) 의존성을 설치하고, 여러 Uvicorn 워커를 사용하는 대신 **단일 Uvicorn 프로세스**를 실행하는 **처음부터 만든 Docker 이미지**를 사용하는 것이 좋을 것입니다. -### 로드 밸런서 +### 로드 밸런서 { #load-balancer } -컨테이너로 작업할 때, 여러분은 일반적으로 **메인 포트의 상황을 감지하는** 요소를 가지고 있을 것입니다. 이는 **HTTPS**를 다루는 **TLS 종료 프록시**와 같은 다른 컨테이너일 수도 있고, 유사한 다른 도구일 수도 있습니다. +컨테이너를 사용할 때는 보통 **메인 포트에서 대기(listening)하는** 컴포넌트가 있습니다. **HTTPS**를 처리하기 위한 **TLS 종료 프록시** 역할을 하는 다른 컨테이너일 수도 있고, 유사한 도구일 수도 있습니다. -이 요소가 요청들의 **로드**를 읽어들이고 각 워커에게 (바라건대) **균형적으로** 분배한다면, 이 요소는 일반적으로 **로드 밸런서**라고 불립니다. +이 컴포넌트가 요청의 **부하(load)**를 받아 워커들에 (가능하면) **균형 있게** 분산한다면, 보통 **로드 밸런서**라고 부릅니다. /// tip | 팁 -HTTPS를 위해 사용된 **TLS 종료 프록시** 요소 또한 **로드 밸런서**가 될 수 있습니다. +HTTPS에 사용되는 동일한 **TLS 종료 프록시** 컴포넌트가 **로드 밸런서**이기도 한 경우가 많습니다. /// -또한 컨테이너로 작업할 때, 컨테이너를 시작하고 관리하기 위해 사용한 것과 동일한 시스템은 이미 해당 **로드 밸런서**로 부터 여러분의 앱에 해당하는 컨테이너로 **네트워크 통신**(예를 들어, HTTP 요청)을 전송하는 내부적인 도구를 가지고 있을 것입니다 (여기서도 로드 밸런서는 **TLS 종료 프록시**일 수 있습니다). +또한 컨테이너로 작업할 때, 이를 시작하고 관리하는 시스템은 이미 해당 **로드 밸런서**(또는 **TLS 종료 프록시**)에서 여러분의 앱이 있는 컨테이너로 **네트워크 통신**(예: HTTP 요청)을 전달하는 내부 도구를 가지고 있습니다. -### 하나의 로드 밸런서 - 다중 워커 컨테이너 +### 하나의 로드 밸런서 - 여러 워커 컨테이너 { #one-load-balancer-multiple-worker-containers } -**쿠버네티스**나 또는 다른 분산 컨테이너 관리 시스템으로 작업할 때, 시스템 내부의 네트워킹 메커니즘을 이용함으로써 메인 **포트**를 감지하고 있는 단일 **로드 밸런서**는 여러분의 앱에서 실행되고 있는 **여러개의 컨테이너**에 통신(요청들)을 전송할 수 있게 됩니다. +**Kubernetes** 같은 분산 컨테이너 관리 시스템에서는 내부 네트워킹 메커니즘을 통해, 메인 **포트**에서 대기하는 단일 **로드 밸런서**가 여러분의 앱을 실행하는 **여러 컨테이너**로 통신(요청)을 전달할 수 있습니다. -여러분의 앱에서 실행되고 있는 각각의 컨테이너는 일반적으로 **하나의 프로세스**만 가질 것입니다 (예를 들어, FastAPI 어플리케이션에서 실행되는 하나의 Uvicorn 프로세스처럼). 이 컨테이너들은 모두 같은 것을 실행하는 점에서 **동일한 컨테이너**이지만, 프로세스, 메모리 등은 공유하지 않습니다. 이 방식으로 여러분은 CPU의 **서로 다른 코어들** 또는 **서로 다른 머신들**을 **병렬화**하는 이점을 얻을 수 있습니다. +앱을 실행하는 각 컨테이너는 보통 **프로세스 하나만** 가집니다(예: FastAPI 애플리케이션을 실행하는 Uvicorn 프로세스). 모두 같은 것을 실행하는 **동일한 컨테이너**이지만, 각자 고유한 프로세스, 메모리 등을 가집니다. 이렇게 하면 CPU의 **서로 다른 코어** 또는 **서로 다른 머신**에서 **병렬화**의 이점을 얻을 수 있습니다. -또한 **로드 밸런서**가 있는 분산 컨테이너 시스템은 여러분의 앱에 있는 컨테이너 각각에 **차례대로 요청을 분산**시킬 것 입니다. 따라서 각 요청은 여러분의 앱에서 실행되는 여러개의 **복제된 컨테이너들** 중 하나에 의해 다루어질 것 입니다. +그리고 **로드 밸런서**가 있는 분산 컨테이너 시스템은 여러분의 앱을 실행하는 각 컨테이너에 **번갈아가며** 요청을 **분산**합니다. 따라서 각 요청은 여러분의 앱을 실행하는 여러 **복제된 컨테이너** 중 하나에서 처리될 수 있습니다. -그리고 일반적으로 **로드 밸런서**는 여러분의 클러스터에 있는 *다른* 앱으로 가는 요청들도 다룰 수 있으며 (예를 들어, 다른 도메인으로 가거나 다른 URL 경로 접두사를 가지는 경우), 이 통신들을 클러스터에 있는 *바로 그 다른* 어플리케이션으로 제대로 전송할 수 있습니다. +또한 보통 이 **로드 밸런서**는 클러스터 내 *다른* 앱으로 가는 요청(예: 다른 도메인, 또는 다른 URL 경로 접두사 아래로 가는 요청)도 처리할 수 있으며, 그 통신을 클러스터에서 실행 중인 *그 다른* 애플리케이션의 올바른 컨테이너로 전달할 수 있습니다. -### 단일 프로세스를 가지는 컨테이너 +### 컨테이너당 하나의 프로세스 { #one-process-per-container } -이 시나리오의 경우, 여러분은 이미 클러스터 레벨에서 복제를 다루고 있을 것이므로 **컨테이너 당 단일 (Uvicorn) 프로세스**를 가지고자 할 것입니다. +이 시나리오에서는 이미 클러스터 레벨에서 복제를 처리하고 있으므로, **컨테이너당 단일 (Uvicorn) 프로세스**를 두는 것이 좋을 가능성이 큽니다. -따라서, 여러분은 Gunicorn 이나 Uvicorn 워커, 또는 Uvicorn 워커를 사용하는 Uvicorn 매니저와 같은 프로세스 매니저를 가지고 싶어하지 **않을** 것입니다. 여러분은 컨테이너 당 **단일 Uvicorn 프로세스**를 가지고 싶어할 것입니다 (그러나 아마도 다중 컨테이너를 가질 것입니다). +따라서 이 경우 컨테이너에서 `--workers` 커맨드 라인 옵션 같은 방식으로 여러 워커를 두고 싶지는 **않을** 것입니다. 컨테이너당 **단일 Uvicorn 프로세스**만 두고(하지만 컨테이너는 여러 개일 수 있습니다) 싶을 것입니다. -이미 여러분이 클러스터 시스템을 관리하고 있으므로, (Uvicorn 워커를 관리하는 Gunicorn 이나 Uvicorn 처럼) 컨테이너 내에 다른 프로세스 매니저를 가지는 것은 **불필요한 복잡성**만 더하게 될 것입니다. +컨테이너 내부에 (여러 워커를 위한) 또 다른 프로세스 매니저를 두는 것은, 이미 클러스터 시스템에서 처리하고 있는 **불필요한 복잡성**만 추가할 가능성이 큽니다. -### 다중 프로세스를 가지는 컨테이너와 특수한 경우들 +### 여러 프로세스를 가진 컨테이너와 특수한 경우 { #containers-with-multiple-processes-and-special-cases } -당연한 말이지만, 여러분이 내부적으로 **Uvicorn 워커 프로세스들**를 시작하는 **Gunicorn 프로세스 매니저**를 가지는 단일 컨테이너를 원하는 **특수한 경우**도 있을 것입니다. +물론 컨테이너 하나에 여러 **Uvicorn 워커 프로세스**를 두고 싶을 수 있는 **특수한 경우**도 있습니다. -그런 경우에, 여러분들은 **Gunicorn**을 프로세스 매니저로 포함하는 **공식 도커 이미지**를 사용할 수 있습니다. 이 프로세스 매니저는 다중 **Uvicorn 워커 프로세스들**을 실행하며, 디폴트 세팅으로 현재 CPU 코어에 기반하여 자동으로 워커 개수를 조정합니다. 이 사항에 대해서는 아래의 [Gunicorn과 함께하는 공식 도커 이미지 - Uvicorn](#official-docker-image-with-gunicorn-uvicorn)에서 더 다루겠습니다. +그런 경우에는 `--workers` 커맨드 라인 옵션을 사용해 실행할 워커 수를 설정할 수 있습니다: -이런 경우에 해당하는 몇가지 예시가 있습니다: +```{ .dockerfile .annotate } +FROM python:3.9 -#### 단순한 앱 +WORKDIR /code -만약 여러분의 어플리케이션이 **충분히 단순**해서 (적어도 아직은) 프로세스 개수를 파인-튠 할 필요가 없거나 클러스터가 아닌 **단일 서버**에서 실행하고 있다면, 여러분은 컨테이너 내에 프로세스 매니저를 사용하거나 (공식 도커 이미지에서) 자동으로 설정되는 디폴트 값을 사용할 수 있습니다. +COPY ./requirements.txt /code/requirements.txt -#### 도커 구성 +RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt -여러분은 **도커 컴포즈**로 (클러스터가 아닌) **단일 서버로** 배포할 수 있으며, 이 경우에 공유된 네트워크와 **로드 밸런싱**을 포함하는 (도커 컴포즈로) 컨테이너의 복제를 관리하는 단순한 방법이 없을 수도 있습니다. +COPY ./app /code/app -그렇다면 여러분은 **프로세스 매니저**와 함께 내부에 **몇개의 워커 프로세스들**을 시작하는 **단일 컨테이너**를 필요로 할 수 있습니다. +# (1)! +CMD ["fastapi", "run", "app/main.py", "--port", "80", "--workers", "4"] +``` -#### Prometheus와 다른 이유들 +1. 여기서는 `--workers` 커맨드 라인 옵션으로 워커 수를 4로 설정합니다. -여러분은 **단일 프로세스**를 가지는 **다중 컨테이너** 대신 **다중 프로세스**를 가지는 **단일 컨테이너**를 채택하는 **다른 이유**가 있을 수 있습니다. +이런 방식이 의미가 있을 수 있는 예시는 다음과 같습니다: -예를 들어 (여러분의 장치 설정에 따라) Prometheus 익스포터와 같이 같은 컨테이너에 들어오는 **각 요청에 대해** 접근권한을 가지는 도구를 사용할 수 있습니다. +#### 단순한 앱 { #a-simple-app } -이 경우에 여러분이 **여러개의 컨테이너들**을 가지고 있다면, Prometheus가 **메트릭을 읽어 들일 때**, 디폴트로 **매번 하나의 컨테이너**(특정 리퀘스트를 관리하는 바로 그 컨테이너)로 부터 읽어들일 것입니다. 이는 모든 복제된 컨테이너에 대해 **축적된 메트릭들**을 읽어들이는 것과 대비됩니다. +애플리케이션이 **충분히 단순**해서 클러스터가 아닌 **단일 서버**에서 실행할 수 있다면, 컨테이너에 프로세스 매니저를 두고 싶을 수 있습니다. -그렇다면 이 경우에는 **다중 프로세스**를 가지는 **하나의 컨테이너**를 두어서 같은 컨테이너에서 모든 내부 프로세스에 대한 Prometheus 메트릭을 수집하는 로컬 도구(예를 들어 Prometheus 익스포터 같은)를 두어서 이 메그릭들을 하나의 컨테이너에 내에서 공유하는 방법이 더 단순할 것입니다. +#### Docker Compose { #docker-compose } + +**Docker Compose**로 클러스터가 아닌 **단일 서버**에 배포하는 경우, 공유 네트워크와 **로드 밸런싱**을 유지하면서(Docker Compose로) 컨테이너 복제를 관리하는 쉬운 방법이 없을 수 있습니다. + +그렇다면 **프로세스 매니저**가 컨테이너 내부에서 **여러 워커 프로세스**를 시작하는 **단일 컨테이너**를 원할 수 있습니다. --- -요점은, 이 중의 **어느것도** 여러분들이 반드시 따라야하는 **확정된 사실**이 아니라는 것입니다. 여러분은 이 아이디어들을 **여러분의 고유한 이용 사례를 평가**하는데 사용하고, 여러분의 시스템에 가장 적합한 접근법이 어떤 것인지 결정하며, 다음의 개념들을 관리하는 방법을 확인할 수 있습니다: +핵심은, 이것들 중 **어느 것도** 무조건 따라야 하는 **절대적인 규칙**은 아니라는 것입니다. 이 아이디어들을 사용해 **여러분의 사용 사례를 평가**하고, 여러분의 시스템에 가장 적합한 접근 방식을 결정하면서 다음 개념을 어떻게 관리할지 확인할 수 있습니다: * 보안 - HTTPS -* 구동하기 +* 시작 시 자동 실행 * 재시작 -* 복제 (실행 중인 프로세스 개수) +* 복제(실행 중인 프로세스 수) * 메모리 -* 시작하기 전 단계들 +* 시작 전 사전 단계 -## 메모리 +## 메모리 { #memory } -만약 여러분이 **컨테이너 당 단일 프로세스**를 실행한다면, 여러분은 각 컨테이너(복제된 경우에는 여러개의 컨테이너들)에 대해 잘 정의되고, 안정적이며, 제한된 용량의 메모리 소비량을 가지고 있을 것입니다. +**컨테이너당 단일 프로세스**를 실행하면, 각 컨테이너(복제된 경우 여러 개)마다 소비하는 메모리 양이 대체로 잘 정의되고 안정적이며 제한된 값이 됩니다. -그러면 여러분의 컨테이너 관리 시스템(예를 들어 **쿠버네티스**) 설정에서 앞서 정의된 것과 같은 메모리 제한과 요구사항을 설정할 수 있습니다. 이런 방법으로 **가용 머신**이 필요로하는 메모리와 클러스터에 있는 가용 머신들을 염두에 두고 **컨테이너를 복제**할 수 있습니다. +그런 다음 컨테이너 관리 시스템(예: **Kubernetes**) 설정에서 동일하게 메모리 제한과 요구사항을 설정할 수 있습니다. 그러면 클러스터에서 사용 가능한 머신에 있는 메모리와 컨테이너가 필요로 하는 메모리 양을 고려해 **컨테이너를 복제**할 수 있습니다. -만약 여러분의 어플리케이션이 **단순**하다면, 이것은 **문제가 되지 않을** 것이고, 고정된 메모리 제한을 구체화할 필요도 없을 것입니다. 하지만 여러분의 어플리케이션이 (예를 들어 **머신 러닝** 모델같이) **많은 메모리를 소요한다면**, 어플리케이션이 얼마나 많은 양의 메모리를 사용하는지 확인하고 **각 머신에서** 사용하는 **컨테이너의 수**를 조정할 필요가 있습니다 (그리고 필요에 따라 여러분의 클러스터에 머신을 추가할 수 있습니다). +애플리케이션이 **단순**하다면 이는 아마도 **문제가 되지 않을** 것이고, 엄격한 메모리 제한을 지정할 필요가 없을 수도 있습니다. 하지만 **많은 메모리를 사용한다면**(예: **머신 러닝** 모델), 얼마나 많은 메모리를 소비하는지 확인하고, **각 머신**에서 실행되는 **컨테이너 수**를 조정해야 합니다(필요하다면 클러스터에 머신을 더 추가할 수도 있습니다). -만약 여러분이 **컨테이너 당 여러개의 프로세스**를 실행한다면 (예를 들어 공식 도커 이미지 처럼), 여러분은 시작된 프로세스 개수가 가용한 것 보다 **더 많은 메모리를 소비**하지 않는지 확인해야 합니다. +**컨테이너당 여러 프로세스**를 실행한다면, 시작되는 프로세스 수가 사용 가능한 것보다 **더 많은 메모리를 소비하지** 않는지 확인해야 합니다. -## 시작하기 전 단계들과 컨테이너 +## 시작 전 단계와 컨테이너 { #previous-steps-before-starting-and-containers } -만약 여러분이 컨테이너(예를 들어 도커, 쿠버네티스)를 사용한다면, 여러분이 접근할 수 있는 주요 방법은 크게 두가지가 있습니다. +컨테이너(예: Docker, Kubernetes)를 사용한다면, 사용할 수 있는 주요 접근 방식은 두 가지입니다. -### 다중 컨테이너 +### 여러 컨테이너 { #multiple-containers } -만약 여러분이 **여러개의 컨테이너**를 가지고 있다면, 아마도 각각의 컨테이너는 **하나의 프로세스**를 가지고 있을 것입니다(예를 들어, **쿠버네티스** 클러스터에서). 그러면 여러분은 복제된 워커 컨테이너를 실행하기 **이전에**, 하나의 컨테이너에 있는 **이전의 단계들을** 수행하는 단일 프로세스를 가지는 **별도의 컨테이너들**을 가지고 싶을 것입니다. +**여러 컨테이너**가 있고 각 컨테이너가 보통 **단일 프로세스**를 실행한다면(예: **Kubernetes** 클러스터), 복제된 워커 컨테이너를 실행하기 **전에**, 단일 컨테이너에서 단일 프로세스로 **시작 전 사전 단계**를 수행하는 **별도의 컨테이너**를 두고 싶을 가능성이 큽니다. /// info | 정보 -만약 여러분이 쿠버네티스를 사용하고 있다면, 아마도 이는 Init Container일 것입니다. +Kubernetes를 사용한다면, 이는 아마도 Init Container일 것입니다. /// -만약 여러분의 이용 사례에서 이전 단계들을 **병렬적으로 여러번** 수행하는데에 문제가 없다면 (예를 들어 데이터베이스 이전을 실행하지 않고 데이터베이스가 준비되었는지 확인만 하는 경우), 메인 프로세스를 시작하기 전에 이 단계들을 각 컨테이너에 넣을 수 있습니다. +사용 사례에서 시작 전 사전 단계를 **여러 번 병렬로 실행**해도 문제가 없다면(예: 데이터베이스 마이그레이션을 실행하는 것이 아니라, 데이터베이스가 준비되었는지 확인만 하는 경우), 메인 프로세스를 시작하기 직전에 각 컨테이너에 그 단계를 넣을 수도 있습니다. -### 단일 컨테이너 +### 단일 컨테이너 { #single-container } -만약 여러분의 셋업이 **다중 프로세스**(또는 하나의 프로세스)를 시작하는 **하나의 컨테이너**를 가지는 단순한 셋업이라면, 사전 단계들을 앱을 포함하는 프로세스를 시작하기 직전에 같은 컨테이너에서 실행할 수 있습니다. 공식 도커 이미지는 이를 내부적으로 지원합니다. +**단일 컨테이너**에서 여러 **워커 프로세스**(또는 단일 프로세스)를 시작하는 단순한 셋업이라면, 앱이 있는 프로세스를 시작하기 직전에 같은 컨테이너에서 시작 전 사전 단계를 실행할 수 있습니다. -## Gunicorn과 함께하는 공식 도커 이미지 - Uvicorn +### 베이스 도커 이미지 { #base-docker-image } -앞 챕터에서 자세하게 설명된 것 처럼, Uvicorn 워커와 같이 실행되는 Gunicorn을 포함하는 공식 도커 이미지가 있습니다: [서버 워커 - Uvicorn과 함께하는 Gunicorn](server-workers.md){.internal-link target=_blank}. +과거에는 공식 FastAPI Docker 이미지가 있었습니다: tiangolo/uvicorn-gunicorn-fastapi. 하지만 이제는 deprecated되었습니다. ⛔️ -이 이미지는 주로 위에서 설명된 상황에서 유용할 것입니다: [다중 프로세스를 가지는 컨테이너와 특수한 경우들](#containers-with-multiple-processes-and-special-cases). +아마도 이 베이스 도커 이미지(또는 유사한 다른 이미지)는 **사용하지 않는** 것이 좋습니다. -* tiangolo/uvicorn-gunicorn-fastapi. +**Kubernetes**(또는 다른 도구)를 사용하고, 클러스터 레벨에서 여러 **컨테이너**로 **복제**를 이미 설정해 둔 경우라면, 위에서 설명한 대로 **처음부터 이미지를 빌드하는 것**이 더 낫습니다: [FastAPI를 위한 도커 이미지 빌드하기](#build-a-docker-image-for-fastapi). -/// warning | 경고 +그리고 여러 워커가 필요하다면, `--workers` 커맨드 라인 옵션을 간단히 사용하면 됩니다. -여러분이 이 베이스 이미지 또는 다른 유사한 이미지를 필요로 하지 **않을** 높은 가능성이 있으며, [위에서 설명된 것처럼: FastAPI를 위한 도커 이미지 빌드하기](#build-a-docker-image-for-fastapi) 처음부터 이미지를 빌드하는 것이 더 나을 수 있습니다. +/// note Technical Details | 기술 세부사항 + +이 Docker 이미지는 Uvicorn이 죽은 워커를 관리하고 재시작하는 기능을 지원하지 않던 시기에 만들어졌습니다. 그래서 Gunicorn과 Uvicorn을 함께 사용해야 했고, Gunicorn이 Uvicorn 워커 프로세스를 관리하고 재시작하도록 하기 위해 상당한 복잡성이 추가되었습니다. + +하지만 이제 Uvicorn(그리고 `fastapi` 명령)은 `--workers`를 지원하므로, 베이스 도커 이미지를 사용하는 대신 직접 이미지를 빌드하지 않을 이유가 없습니다(코드 양도 사실상 거의 같습니다 😅). /// -이 이미지는 가능한 CPU 코어에 기반한 **몇개의 워커 프로세스**를 설정하는 **자동-튜닝** 메커니즘을 포함하고 있습니다. +## 컨테이너 이미지 배포하기 { #deploy-the-container-image } -이 이미지는 **민감한 디폴트** 값을 가지고 있지만, 여러분들은 여전히 **환경 변수** 또는 설정 파일을 통해 설정값을 수정하고 업데이트 할 수 있습니다. - -또한 스크립트를 통해 **시작하기 전 사전 단계**를 실행하는 것을 지원합니다. - -/// tip | 팁 - -모든 설정과 옵션을 보려면, 도커 이미지 페이지로 이동합니다: tiangolo/uvicorn-gunicorn-fastapi. - -/// - -### 공식 도커 이미지에 있는 프로세스 개수 - -이 이미지에 있는 **프로세스 개수**는 가용한 CPU **코어들**로 부터 **자동으로 계산**됩니다. - -이것이 의미하는 바는 이미지가 CPU로부터 **최대한의 성능**을 **쥐어짜낸다**는 것입니다. - -여러분은 이 설정 값을 **환경 변수**나 기타 방법들로 조정할 수 있습니다. - -그러나 프로세스의 개수가 컨테이너가 실행되고 있는 CPU에 의존한다는 것은 또한 **소요되는 메모리의 크기** 또한 이에 의존한다는 것을 의미합니다. - -그렇기 때문에, 만약 여러분의 어플리케이션이 많은 메모리를 요구하고 (예를 들어 머신러닝 모델처럼), 여러분의 서버가 CPU 코어 수는 많지만 **적은 메모리**를 가지고 있다면, 여러분의 컨테이너는 가용한 메모리보다 많은 메모리를 사용하려고 시도할 수 있으며, 결국 퍼포먼스를 크게 떨어뜨릴 수 있습니다(심지어 고장이 날 수도 있습니다). 🚨 - -### `Dockerfile` 생성하기 - -이 이미지에 기반해 `Dockerfile`을 생성하는 방법은 다음과 같습니다: - -```Dockerfile -FROM tiangolo/uvicorn-gunicorn-fastapi:python3.9 - -COPY ./requirements.txt /app/requirements.txt - -RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt - -COPY ./app /app -``` - -### 더 큰 어플리케이션 - -만약 여러분이 [다중 파일을 가지는 더 큰 어플리케이션](../tutorial/bigger-applications.md){.internal-link target=_blank}을 생성하는 섹션을 따랐다면, 여러분의 `Dockerfile`은 대신 이렇게 생겼을 것입니다: - -```Dockerfile hl_lines="7" -FROM tiangolo/uvicorn-gunicorn-fastapi:python3.9 - -COPY ./requirements.txt /app/requirements.txt - -RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt - -COPY ./app /app/app -``` - -### 언제 사용할까 - -여러분들이 **쿠버네티스**(또는 유사한 다른 도구) 사용하거나 클러스터 레벨에서 다중 컨테이너를 이용해 이미 **사본**을 설정하고 있다면, 공식 베이스 이미지(또는 유사한 다른 이미지)를 사용하지 **않는** 것 좋습니다. 그런 경우에 여러분은 다음에 설명된 것 처럼 **처음부터 이미지를 빌드하는 것**이 더 낫습니다: [FastAPI를 위한 도커 이미지 빌드하기](#build-a-docker-image-for-fastapi). - -이 이미지는 위의 [다중 프로세스를 가지는 컨테이너와 특수한 경우들](#containers-with-multiple-processes-and-special-cases)에서 설명된 특수한 경우에 대해서만 주로 유용할 것입니다. 예를 들어, 만약 여러분의 어플리케이션이 **충분히 단순**해서 CPU에 기반한 디폴트 프로세스 개수를 설정하는 것이 잘 작동한다면, 클러스터 레벨에서 수동으로 사본을 설정할 필요가 없을 것이고, 여러분의 앱에서 하나 이상의 컨테이너를 실행하지도 않을 것입니다. 또는 만약에 여러분이 **도커 컴포즈**로 배포하거나, 단일 서버에서 실행하거나 하는 경우에도 마찬가지입니다. - -## 컨테이너 이미지 배포하기 - -컨테이너 (도커) 이미지를 완성한 뒤에 이를 배포하는 방법에는 여러가지 방법이 있습니다. +컨테이너(Docker) 이미지를 만든 후에는 이를 배포하는 여러 방법이 있습니다. 예를 들어: -* 단일 서버에서 **도커 컴포즈**로 배포하기 -* **쿠버네티스** 클러스터로 배포하기 -* 도커 스왐 모드 클러스터로 배포하기 -* 노마드 같은 다른 도구로 배포하기 -* 여러분의 컨테이너 이미지를 배포해주는 클라우드 서비스로 배포하기 +* 단일 서버에서 **Docker Compose**로 +* **Kubernetes** 클러스터로 +* Docker Swarm Mode 클러스터로 +* Nomad 같은 다른 도구로 +* 컨테이너 이미지를 받아 배포해주는 클라우드 서비스로 -## Poetry의 도커 이미지 +## `uv`를 사용하는 도커 이미지 { #docker-image-with-uv } -만약 여러분들이 프로젝트 의존성을 관리하기 위해 Poetry를 사용한다면, 도커의 멀티-스테이지 빌딩을 사용할 수 있습니다: +프로젝트를 설치하고 관리하기 위해 uv를 사용한다면, uv Docker guide를 따를 수 있습니다. -```{ .dockerfile .annotate } -# (1) -FROM python:3.9 as requirements-stage +## 요약 { #recap } -# (2) -WORKDIR /tmp - -# (3) -RUN pip install poetry - -# (4) -COPY ./pyproject.toml ./poetry.lock* /tmp/ - -# (5) -RUN poetry export -f requirements.txt --output requirements.txt --without-hashes - -# (6) -FROM python:3.9 - -# (7) -WORKDIR /code - -# (8) -COPY --from=requirements-stage /tmp/requirements.txt /code/requirements.txt - -# (9) -RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt - -# (10) -COPY ./app /code/app - -# (11) -CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"] -``` - -1. 첫 스테이지로, `requirements-stage`라고 이름 붙였습니다. - -2. `/tmp`를 현재의 워킹 디렉터리로 설정합니다. - - 이 위치에 우리는 `requirements.txt` 파일을 생성할 것입니다. - -3. 이 도커 스테이지에서 Poetry를 설치합니다. - -4. 파일 `pyproject.toml`와 `poetry.lock`를 `/tmp` 디렉터리로 복사합니다. - - `./poetry.lock*` (`*`로 끝나는) 파일을 사용하기 때문에, 파일이 아직 사용가능하지 않더라도 고장나지 않을 것입니다. - -5. `requirements.txt` 파일을 생성합니다. - -6. 이것이 마지막 스테이지로, 여기에 위치한 모든 것이 마지막 컨테이너 이미지에 포함될 것입니다. - -7. 현재의 워킹 디렉터리를 `/code`로 설정합니다. - -8. 파일 `requirements.txt`를 `/code` 디렉터리로 복사합니다. - - 이 파일은 오직 이전의 도커 스테이지에만 존재하며, 때문에 복사하기 위해서 `--from-requirements-stage` 옵션이 필요합니다. - -9. 생성된 `requirements.txt` 파일에 패키지 의존성을 설치합니다. - -10. `app` 디렉터리를 `/code` 디렉터리로 복사합니다. - -11. `uvicorn` 커맨드를 실행하여, `app.main`에서 불러온 `app` 객체를 사용하도록 합니다. - -/// tip | 팁 - -버블 숫자를 클릭해 각 줄이 하는 일을 알아볼 수 있습니다. - -/// - -**도커 스테이지**란 `Dockefile`의 일부로서 나중에 사용하기 위한 파일들을 생성하기 위한 **일시적인 컨테이너 이미지**로 작동합니다. - -첫 스테이지는 오직 **Poetry를 설치**하고 Poetry의 `pyproject.toml` 파일로부터 프로젝트 의존성을 위한 **`requirements.txt`를 생성**하기 위해 사용됩니다. - -이 `requirements.txt` 파일은 **다음 스테이지**에서 `pip`로 사용될 것입니다. - -마지막 컨테이너 이미지에는 **오직 마지막 스테이지만** 보존됩니다. 이전 스테이지(들)은 버려집니다. - -Poetry를 사용할 때 **도커 멀티-스테이지 빌드**를 사용하는 것이 좋은데, 여러분들의 프로젝트 의존성을 설치하기 위해 마지막 컨테이너 이미지에 **오직** `requirements.txt` 파일만 필요하지, Poetry와 그 의존성은 있을 필요가 없기 때문입니다. - -이 다음 (또한 마지막) 스테이지에서 여러분들은 이전에 설명된 것과 비슷한 방식으로 방식으로 이미지를 빌드할 수 있습니다. - -### TLS 종료 프록시의 배후 - Poetry - -이전에 언급한 것과 같이, 만약 여러분이 컨테이너를 Nginx 또는 Traefik과 같은 TLS 종료 프록시 (로드 밸런서) 뒤에서 실행하고 있다면, 커맨드에 `--proxy-headers` 옵션을 추가합니다: - -```Dockerfile -CMD ["uvicorn", "app.main:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "80"] -``` - -## 요약 - -컨테이너 시스템(예를 들어 **도커**나 **쿠버네티스**)을 사용하여 모든 **배포 개념**을 다루는 것은 꽤 간단합니다: +컨테이너 시스템(예: **Docker**, **Kubernetes**)을 사용하면 모든 **배포 개념**을 다루는 것이 상당히 단순해집니다: * HTTPS -* 구동하기 +* 시작 시 자동 실행 * 재시작 -* 복제 (실행 중인 프로세스 개수) +* 복제(실행 중인 프로세스 수) * 메모리 -* 시작하기 전 단계들 +* 시작 전 사전 단계 -대부분의 경우에서 여러분은 어떤 베이스 이미지도 사용하지 않고 공식 파이썬 도커 이미지에 기반해 **처음부터 컨테이너 이미지를 빌드**할 것입니다. +대부분의 경우 베이스 이미지는 사용하지 않고, 공식 Python Docker 이미지에 기반해 **처음부터 컨테이너 이미지를 빌드**하는 것이 좋습니다. -`Dockerfile`에 있는 지시 사항을 **순서대로** 다루고 **도커 캐시**를 사용하는 것으로 여러분은 **빌드 시간을 최소화**할 수 있으며, 이로써 생산성을 최대화할 수 있습니다 (그리고 지루함을 피할 수 있죠) 😎 - -특별한 경우에는, FastAPI를 위한 공식 도커 이미지를 사용할 수도 있습니다. 🤓 +`Dockerfile`에서 지시어의 **순서**와 **Docker 캐시**를 신경 쓰면 **빌드 시간을 최소화**해 생산성을 최대화할 수 있습니다(그리고 지루함도 피할 수 있습니다). 😎 diff --git a/docs/ko/docs/deployment/index.md b/docs/ko/docs/deployment/index.md index 87b05b68f1..e0d2375344 100644 --- a/docs/ko/docs/deployment/index.md +++ b/docs/ko/docs/deployment/index.md @@ -1,21 +1,23 @@ -# 배포하기 - 들어가면서 +# 배포 { #deployment } -**FastAPI**을 배포하는 것은 비교적 쉽습니다. +**FastAPI** 애플리케이션을 배포하는 것은 비교적 쉽습니다. -## 배포의 의미 +## 배포의 의미 { #what-does-deployment-mean } -**배포**란 애플리케이션을 **사용자가 사용**할 수 있도록 하는 데 필요한 단계를 수행하는 것을 의미합니다. +애플리케이션을 **배포**한다는 것은 **사용자가 사용**할 수 있도록 하는 데 필요한 단계를 수행하는 것을 의미합니다. -**웹 API**의 경우, 일반적으로 **사용자**가 중단이나 오류 없이 애플리케이션에 효율적으로 **접근**할 수 있도록 좋은 성능, 안정성 등을 제공하는 **서버 프로그램과** 함께 **원격 시스템**에 이를 설치하는 작업을 의미합니다. +**웹 API**의 경우, 일반적으로 **원격 머신**에 이를 설치하고, 좋은 성능, 안정성 등을 제공하는 **서버 프로그램**과 함께 구성하여 **사용자**가 중단이나 문제 없이 애플리케이션에 효율적으로 **접근**할 수 있게 하는 것을 포함합니다. -이는 지속적으로 코드를 변경하고, 지우고, 수정하고, 개발 서버를 중지했다가 다시 시작하는 등의 **개발** 단계와 대조됩니다. +이는 지속적으로 코드를 변경하고, 망가뜨리고 고치고, 개발 서버를 중지했다가 다시 시작하는 등의 **개발** 단계와 대조됩니다. -## 배포 전략 +## 배포 전략 { #deployment-strategies } -사용하는 도구나 특정 사례에 따라 여러 가지 방법이 있습니다. +구체적인 사용 사례와 사용하는 도구에 따라 여러 가지 방법이 있습니다. -배포도구들을 사용하여 직접 **서버에 배포**하거나, 배포작업의 일부를 수행하는 **클라우드 서비스** 또는 다른 방법을 사용할 수도 있습니다. +여러 도구를 조합해 직접 **서버를 배포**할 수도 있고, 작업의 일부를 대신해 주는 **클라우드 서비스**를 사용할 수도 있으며, 다른 가능한 선택지도 있습니다. -**FastAPI** 애플리케이션을 배포할 때 선택할 수 있는 몇 가지 주요 방법을 보여 드리겠습니다 (대부분 다른 유형의 웹 애플리케이션에도 적용됩니다). +예를 들어, FastAPI 뒤에 있는 저희 팀은 FastAPI로 작업하는 것과 같은 개발자 경험을 유지하면서, FastAPI 앱을 클라우드에 가능한 한 간소화된 방식으로 배포할 수 있도록 **FastAPI Cloud**를 만들었습니다. -다음 차례에 자세한 내용과 이를 위한 몇 가지 기술을 볼 수 있습니다. ✨ +**FastAPI** 애플리케이션을 배포할 때 아마 염두에 두어야 할 몇 가지 주요 개념을 보여드리겠습니다(대부분은 다른 유형의 웹 애플리케이션에도 적용됩니다). + +다음 섹션에서 염두에 둘 더 많은 세부사항과 이를 위한 몇 가지 기술을 볼 수 있습니다. ✨ diff --git a/docs/ko/docs/deployment/server-workers.md b/docs/ko/docs/deployment/server-workers.md index b40b25cd8f..e98cfd1142 100644 --- a/docs/ko/docs/deployment/server-workers.md +++ b/docs/ko/docs/deployment/server-workers.md @@ -1,130 +1,87 @@ -# 서버 워커 - 구니콘과 유비콘 +# 서버 워커 - 워커와 함께 사용하는 Uvicorn { #server-workers-uvicorn-with-workers } -전단계에서의 배포 개념들을 다시 확인해보겠습니다: +이전의 배포 개념들을 다시 확인해보겠습니다: * 보안 - HTTPS -* 서버 시작과 동시에 실행하기 +* 서버 시작 시 실행 * 재시작 -* **복제본 (실행 중인 프로세스의 숫자)** +* **복제(실행 중인 프로세스 수)** * 메모리 -* 시작하기 전의 여러 단계들 +* 시작하기 전의 이전 단계 -지금까지 문서의 모든 튜토리얼을 참고하여 **단일 프로세스**로 Uvicorn과 같은 **서버 프로그램**을 실행했을 것입니다. +지금까지 문서의 모든 튜토리얼을 참고하면서, `fastapi` 명령처럼 Uvicorn을 실행하는 **서버 프로그램**을 사용해 **단일 프로세스**로 실행해 왔을 가능성이 큽니다. -애플리케이션을 배포할 때 **다중 코어**를 활용하고 더 많은 요청을 처리할 수 있도록 **프로세스 복제본**이 필요합니다. +애플리케이션을 배포할 때는 **다중 코어**를 활용하고 더 많은 요청을 처리할 수 있도록 **프로세스 복제**를 하고 싶을 가능성이 큽니다. -전 과정이었던 [배포 개념들](concepts.md){.internal-link target=_blank}에서 본 것처럼 여러가지 방법이 존재합니다. +이전 장의 [배포 개념들](concepts.md){.internal-link target=_blank}에서 본 것처럼, 사용할 수 있는 전략이 여러 가지 있습니다. -지금부터 **구니콘**을 **유비콘 워커 프로세스**와 함께 사용하는 방법을 알려드리겠습니다. +여기서는 `fastapi` 명령을 사용하거나 `uvicorn` 명령을 직접 사용해서, **워커 프로세스**와 함께 **Uvicorn**을 사용하는 방법을 보여드리겠습니다. /// info | 정보 -만약 도커와 쿠버네티스 같은 컨테이너를 사용하고 있다면 다음 챕터 [FastAPI와 컨테이너 - 도커](docker.md){.internal-link target=_blank}에서 더 많은 정보를 얻을 수 있습니다. +Docker나 Kubernetes 같은 컨테이너를 사용하고 있다면, 다음 장인 [컨테이너에서의 FastAPI - 도커](docker.md){.internal-link target=_blank}에서 더 자세히 설명하겠습니다. -특히, 쿠버네티스에서 실행할 때는 구니콘을 사용하지 않고 대신 컨테이너당 하나의 유비콘 프로세스를 실행하는 것이 좋습니다. 이 장의 뒷부분에서 설명하겠습니다. +특히 **Kubernetes**에서 실행할 때는 워커를 사용하기보다는, 대신 **컨테이너당 단일 Uvicorn 프로세스 하나**를 실행하고 싶을 가능성이 크지만, 해당 내용은 그 장의 뒤에서 설명하겠습니다. /// -## 구니콘과 유비콘 워커 +## 여러 워커 { #multiple-workers } -**Gunicorn**은 **WSGI 표준**을 주로 사용하는 애플리케이션 서버입니다. 이것은 구니콘이 플라스크와 쟝고와 같은 애플리케이션을 제공할 수 있다는 것을 의미합니다. 구니콘 자체는 최신 **ASGI 표준**을 사용하기 때문에 FastAPI와 호환되지 않습니다. +`--workers` 커맨드라인 옵션으로 여러 워커를 시작할 수 있습니다: -하지만 구니콘은 **프로세스 관리자**역할을 하고 사용자에게 특정 **워커 프로세스 클래스**를 알려줍니다. 그런 다음 구니콘은 해당 클래스를 사용하여 하나 이상의 **워커 프로세스**를 시작합니다. +//// tab | `fastapi` -그리고 **유비콘**은 **구니콘과 호환되는 워커 클래스**가 있습니다. - -이 조합을 사용하여 구니콘은 **프로세스 관리자** 역할을 하며 **포트**와 **IP**를 관찰하고, **유비콘 클래스**를 실행하는 워커 프로세스로 통신 정보를 **전송**합니다. - -그리고 나서 구니콘과 호환되는 **유비콘 워커** 클래스는 구니콘이 보낸 데이터를 FastAPI에서 사용하기 위한 ASGI 표준으로 변환하는 일을 담당합니다. - -## 구니콘과 유비콘 설치하기 +`fastapi` 명령을 사용한다면:
```console -$ pip install "uvicorn[standard]" gunicorn +$ fastapi run --workers 4 main.py ----> 100% + FastAPI Starting production server 🚀 + + Searching for package file structure from directories with + __init__.py files + Importing from /home/user/code/awesomeapp + + module 🐍 main.py + + code Importing the FastAPI app object from the module with the + following code: + + from main import app + + app Using import string: main:app + + server Server started at http://0.0.0.0:8000 + server Documentation at http://0.0.0.0:8000/docs + + Logs: + + INFO Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to + quit) + INFO Started parent process [27365] + INFO Started server process [27368] + INFO Started server process [27369] + INFO Started server process [27370] + INFO Started server process [27367] + INFO Waiting for application startup. + INFO Waiting for application startup. + INFO Waiting for application startup. + INFO Waiting for application startup. + INFO Application startup complete. + INFO Application startup complete. + INFO Application startup complete. + INFO Application startup complete. ```
-이 명령어는 유비콘 `standard` 추가 패키지(좋은 성능을 위한)와 구니콘을 설치할 것입니다. +//// -## 구니콘을 유비콘 워커와 함께 실행하기 +//// tab | `uvicorn` -설치 후 구니콘 실행하기: - -
- -```console -$ gunicorn main:app --workers 4 --worker-class uvicorn.workers.UvicornWorker --bind 0.0.0.0:80 - -[19499] [INFO] Starting gunicorn 20.1.0 -[19499] [INFO] Listening at: http://0.0.0.0:80 (19499) -[19499] [INFO] Using worker: uvicorn.workers.UvicornWorker -[19511] [INFO] Booting worker with pid: 19511 -[19513] [INFO] Booting worker with pid: 19513 -[19514] [INFO] Booting worker with pid: 19514 -[19515] [INFO] Booting worker with pid: 19515 -[19511] [INFO] Started server process [19511] -[19511] [INFO] Waiting for application startup. -[19511] [INFO] Application startup complete. -[19513] [INFO] Started server process [19513] -[19513] [INFO] Waiting for application startup. -[19513] [INFO] Application startup complete. -[19514] [INFO] Started server process [19514] -[19514] [INFO] Waiting for application startup. -[19514] [INFO] Application startup complete. -[19515] [INFO] Started server process [19515] -[19515] [INFO] Waiting for application startup. -[19515] [INFO] Application startup complete. -``` - -
- -각 옵션이 무엇을 의미하는지 살펴봅시다: - -* 이것은 유비콘과 똑같은 문법입니다. `main`은 파이썬 모듈 네임 "`main`"을 의미하므로 `main.py`파일을 뜻합니다. 그리고 `app`은 **FastAPI** 어플리케이션이 들어 있는 변수의 이름입니다. - * `main:app`이 파이썬의 `import` 문법과 흡사한 면이 있다는 걸 알 수 있습니다: - - ```Python - from main import app - ``` - - * 곧, `main:app`안에 있는 콜론의 의미는 파이썬에서 `from main import app`에서의 `import`와 같습니다. -* `--workers`: 사용할 워커 프로세스의 개수이며 숫자만큼의 유비콘 워커를 실행합니다. 이 예제에서는 4개의 워커를 실행합니다. -* `--worker-class`: 워커 프로세스에서 사용하기 위한 구니콘과 호환되는 워커클래스. - * 이런식으로 구니콘이 import하여 사용할 수 있는 클래스를 전달해줍니다: - - ```Python - import uvicorn.workers.UvicornWorker - ``` - -* `--bind`: 구니콘이 관찰할 IP와 포트를 의미합니다. 콜론 (`:`)을 사용하여 IP와 포트를 구분합니다. - * 만약에 `--bind 0.0.0.0:80` (구니콘 옵션) 대신 유비콘을 직접 실행하고 싶다면 `--host 0.0.0.0`과 `--port 80`을 사용해야 합니다. - -출력에서 각 프로세스에 대한 **PID** (process ID)를 확인할 수 있습니다. (단순한 숫자입니다) - -출력 내용: - -* 구니콘 **프로세스 매니저**는 PID `19499`로 실행됩니다. (직접 실행할 경우 숫자가 다를 수 있습니다) -* 다음으로 `Listening at: http://0.0.0.0:80`을 시작합니다. -* 그런 다음 사용해야할 `uvicorn.workers.UvicornWorker`의 워커클래스를 탐지합니다. -* 그리고 PID `19511`, `19513`, `19514`, 그리고 `19515`를 가진 **4개의 워커**를 실행합니다. - - -또한 구니콘은 워커의 수를 유지하기 위해 **죽은 프로세스**를 관리하고 **재시작**하는 작업을 책임집니다. 이것은 이번 장 상단 목록의 **재시작** 개념을 부분적으로 도와주는 것입니다. - -그럼에도 불구하고 필요할 경우 외부에서 **구니콘을 재시작**하고, 혹은 **서버를 시작할 때 실행**할 수 있도록 하고 싶어할 것입니다. - -## 유비콘과 워커 - -유비콘은 몇 개의 **워커 프로세스**와 함께 실행할 수 있는 선택지가 있습니다. - -그럼에도 불구하고, 유비콘은 워커 프로세스를 다루는 데에 있어서 구니콘보다 더 제한적입니다. 따라서 이 수준(파이썬 수준)의 프로세스 관리자를 사용하려면 구니콘을 프로세스 관리자로 사용하는 것이 좋습니다. - -보통 이렇게 실행할 수 있습니다: +`uvicorn` 명령을 직접 사용하는 편이 좋다면:
@@ -148,36 +105,35 @@ $ uvicorn main:app --host 0.0.0.0 --port 8080 --workers 4
-새로운 옵션인 `--workers`은 유비콘에게 4개의 워커 프로세스를 사용한다고 알려줍니다. +//// -각 프로세스의 **PID**를 확인할 수 있습니다. `27365`는 상위 프로세스(**프로세스 매니저**), 그리고 각각의 워커프로세스는 `27368`, `27369`, `27370`, 그리고 `27367`입니다. +여기서 새로운 옵션은 `--workers`뿐이며, Uvicorn에게 워커 프로세스 4개를 시작하라고 알려줍니다. -## 배포 개념들 +또한 각 프로세스의 **PID**도 확인할 수 있는데, 상위 프로세스(이것이 **프로세스 관리자**)의 PID는 `27365`이고, 각 워커 프로세스의 PID는 `27368`, `27369`, `27370`, `27367`입니다. -여기에서는 **유비콘 워커 프로세스**를 관리하는 **구니콘**(또는 유비콘)을 사용하여 애플리케이션을 **병렬화**하고, CPU **멀티 코어**의 장점을 활용하고, **더 많은 요청**을 처리할 수 있는 방법을 살펴보았습니다. +## 배포 개념들 { #deployment-concepts } -워커를 사용하는 것은 배포 개념 목록에서 주로 **복제본** 부분과 **재시작**에 약간 도움이 되지만 다른 배포 개념들도 다루어야 합니다: +여기서는 여러 **워커**를 사용해 애플리케이션 실행을 **병렬화**하고, CPU의 **다중 코어**를 활용하며, **더 많은 요청**을 제공할 수 있는 방법을 살펴봤습니다. + +위의 배포 개념 목록에서 워커를 사용하는 것은 주로 **복제** 부분에 도움이 되고, **재시작**에도 약간 도움이 되지만, 나머지 항목들도 여전히 신경 써야 합니다: * **보안 - HTTPS** -* **서버 시작과 동시에 실행하기** +* **서버 시작 시 실행** * ***재시작*** -* 복제본 (실행 중인 프로세스의 숫자) +* 복제(실행 중인 프로세스 수) * **메모리** -* **시작하기 전의 여러 단계들** +* **시작하기 전의 이전 단계** +## 컨테이너와 도커 { #containers-and-docker } -## 컨테이너와 도커 +다음 장인 [컨테이너에서의 FastAPI - 도커](docker.md){.internal-link target=_blank}에서는 다른 **배포 개념들**을 처리하기 위해 사용할 수 있는 몇 가지 전략을 설명하겠습니다. -다음 장인 [FastAPI와 컨테이너 - 도커](docker.md){.internal-link target=_blank}에서 다른 **배포 개념들**을 다루는 전략들을 알려드리겠습니다. +단일 Uvicorn 프로세스를 실행하기 위해, **처음부터 여러분만의 이미지를 직접 빌드**하는 방법을 보여드리겠습니다. 이는 간단한 과정이며, **Kubernetes** 같은 분산 컨테이너 관리 시스템을 사용할 때 아마도 이렇게 하고 싶을 것입니다. -또한 간단한 케이스에서 사용할 수 있는, **구니콘과 유비콘 워커**가 포함돼 있는 **공식 도커 이미지**와 함께 몇 가지 기본 구성을 보여드리겠습니다. +## 요약 { #recap } -그리고 단일 유비콘 프로세스(구니콘 없이)를 실행할 수 있도록 **사용자 자신의 이미지를 처음부터 구축**하는 방법도 보여드리겠습니다. 이는 간단한 과정이며, **쿠버네티스**와 같은 분산 컨테이너 관리 시스템을 사용할 때 수행할 작업입니다. +`fastapi` 또는 `uvicorn` 명령에서 `--workers` CLI 옵션을 사용해 여러 워커 프로세스를 실행하면, **멀티 코어 CPU**를 활용해 **여러 프로세스를 병렬로 실행**할 수 있습니다. -## 요약 +다른 배포 개념들을 직접 처리하면서 **자체 배포 시스템**을 구축하는 경우, 이러한 도구와 아이디어를 활용할 수 있습니다. -당신은 **구니콘**(또는 유비콘)을 유비콘 워커와 함께 프로세스 관리자로 사용하여 **멀티-코어 CPU**를 활용하는 **멀티 프로세스를 병렬로 실행**할 수 있습니다. - -다른 배포 개념을 직접 다루면서 **자신만의 배포 시스템**을 구성하는 경우 이러한 도구와 개념들을 활용할 수 있습니다. - -다음 장에서 컨테이너(예: 도커 및 쿠버네티스)와 함께하는 **FastAPI**에 대해 배워보세요. 이러한 툴에는 다른 **배포 개념**들을 간단히 해결할 수 있는 방법이 있습니다. ✨ +다음 장에서 컨테이너(예: Docker 및 Kubernetes)와 함께 사용하는 **FastAPI**에 대해 알아보세요. 해당 도구들이 다른 **배포 개념들**도 간단히 해결하는 방법이 있다는 것을 확인할 수 있습니다. ✨ diff --git a/docs/ko/docs/deployment/versions.md b/docs/ko/docs/deployment/versions.md index 559a892ab9..173ba925cf 100644 --- a/docs/ko/docs/deployment/versions.md +++ b/docs/ko/docs/deployment/versions.md @@ -1,94 +1,93 @@ -# FastAPI 버전들에 대하여 +# FastAPI 버전들에 대하여 { #about-fastapi-versions } -**FastAPI** 는 이미 많은 응용 프로그램과 시스템들을 만드는데 사용되고 있습니다. 그리고 100%의 테스트 정확성을 가지고 있습니다. 하지만 이것은 아직까지도 빠르게 발전하고 있습니다. +**FastAPI**는 이미 많은 애플리케이션과 시스템에서 프로덕션으로 사용되고 있습니다. 그리고 테스트 커버리지는 100%로 유지됩니다. 하지만 개발은 여전히 빠르게 진행되고 있습니다. -새로운 특징들이 빈번하게 추가되고, 오류들이 지속적으로 수정되고 있습니다. 그리고 코드가 계속적으로 향상되고 있습니다. +새로운 기능이 자주 추가되고, 버그가 규칙적으로 수정되며, 코드는 계속해서 지속적으로 개선되고 있습니다. -이것이 아직도 최신 버전이 `0.x.x`인 이유입니다. 이것은 각각의 버전들이 잠재적으로 변할 수 있다는 것을 보여줍니다. 이는 유의적 버전 관습을 따릅니다. +그래서 현재 버전이 아직 `0.x.x`인 것입니다. 이는 각 버전이 잠재적으로 하위 호환성이 깨지는 변경을 포함할 수 있음을 반영합니다. 이는 Semantic Versioning 관례를 따릅니다. -지금 바로 **FastAPI**로 응용 프로그램을 만들 수 있습니다. 이때 (아마 지금까지 그래 왔던 것처럼), 사용하는 버전이 코드와 잘 맞는지 확인해야합니다. +지금 바로 **FastAPI**로 프로덕션 애플리케이션을 만들 수 있습니다(그리고 아마도 한동안 그렇게 해오셨을 것입니다). 다만 나머지 코드와 함께 올바르게 동작하는 버전을 사용하고 있는지 확인하기만 하면 됩니다. -## `fastapi` 버전을 표시 +## `fastapi` 버전을 고정하기 { #pin-your-fastapi-version } -가장 먼저 해야할 것은 응용 프로그램이 잘 작동하는 가장 최신의 구체적인 **FastAPI** 버전을 표시하는 것입니다. +가장 먼저 해야 할 일은 여러분의 애플리케이션에서 올바르게 동작하는 것으로 알고 있는 **FastAPI**의 최신 구체 버전에 맞춰 사용 중인 버전을 "고정(pin)"하는 것입니다. -예를 들어, 응용 프로그램에 `0.45.0` 버전을 사용했다고 가정합니다. +예를 들어, 앱에서 `0.112.0` 버전을 사용하고 있다고 가정해 보겠습니다. -만약에 `requirements.txt` 파일을 사용했다면, 다음과 같이 버전을 명세할 수 있습니다: +`requirements.txt` 파일을 사용한다면 다음과 같이 버전을 지정할 수 있습니다: ```txt -fastapi==0.45.0 +fastapi[standard]==0.112.0 ``` -이것은 `0.45.0` 버전을 사용했다는 것을 의미합니다. +이는 정확히 `0.112.0` 버전을 사용한다는 의미입니다. -또는 다음과 같이 표시할 수 있습니다: +또는 다음과 같이 고정할 수도 있습니다: + +```txt +fastapi[standard]>=0.112.0,<0.113.0 +``` + +이는 `0.112.0` 이상이면서 `0.113.0` 미만의 버전을 사용한다는 의미입니다. 예를 들어 `0.112.2` 버전도 허용됩니다. + +`uv`, Poetry, Pipenv 등 다른 도구로 설치를 관리한다면, 모두 패키지의 특정 버전을 정의할 수 있는 방법을 제공합니다. + +## 이용 가능한 버전들 { #available-versions } + +사용 가능한 버전(예: 현재 최신 버전이 무엇인지 확인하기 위해)은 [Release Notes](../release-notes.md){.internal-link target=_blank}에서 확인할 수 있습니다. + +## 버전들에 대해 { #about-versions } + +Semantic Versioning 관례에 따르면, `1.0.0` 미만의 어떤 버전이든 잠재적으로 하위 호환성이 깨지는 변경을 추가할 수 있습니다. + +FastAPI는 또한 "PATCH" 버전 변경은 버그 수정과 하위 호환성이 깨지지 않는 변경을 위한 것이라는 관례를 따릅니다. + +/// tip | 팁 + +"PATCH"는 마지막 숫자입니다. 예를 들어 `0.2.3`에서 PATCH 버전은 `3`입니다. + +/// + +따라서 다음과 같이 버전을 고정할 수 있어야 합니다: ```txt fastapi>=0.45.0,<0.46.0 ``` -이것은 `0.45.0` 버전과 같거나 높으면서 `0.46.0` 버전 보다는 낮은 버전을 사용했다는 것을 의미합니다. 예를 들어, `0.45.2` 버전과 같은 경우는 해당 조건을 만족합니다. - -만약에 Poetry, Pipenv, 또는 그밖의 다양한 설치 도구를 사용한다면, 패키지에 구체적인 버전을 정의할 수 있는 방법을 가지고 있을 것입니다. - -## 이용가능한 버전들 - -[Release Notes](../release-notes.md){.internal-link target=_blank}를 통해 사용할 수 있는 버전들을 확인할 수 있습니다.(예를 들어, 가장 최신의 버전을 확인할 수 있습니다.) - - -## 버전들에 대해 - -유의적 버전 관습을 따라서, `1.0.0` 이하의 모든 버전들은 잠재적으로 급변할 수 있습니다. - -FastAPI는 오류를 수정하고, 일반적인 변경사항을 위해 "패치"버전의 관습을 따릅니다. +하위 호환성이 깨지는 변경과 새로운 기능은 "MINOR" 버전에 추가됩니다. /// tip | 팁 -여기서 말하는 "패치"란 버전의 마지막 숫자로, 예를 들어 `0.2.3` 버전에서 "패치"는 `3`을 의미합니다. +"MINOR"는 가운데 숫자입니다. 예를 들어 `0.2.3`에서 MINOR 버전은 `2`입니다. /// -따라서 다음과 같이 버전을 표시할 수 있습니다: +## FastAPI 버전 업그레이드하기 { #upgrading-the-fastapi-versions } + +앱에 테스트를 추가해야 합니다. + +**FastAPI**에서는 매우 쉽습니다(Starlette 덕분에). 문서를 확인해 보세요: [Testing](../tutorial/testing.md){.internal-link target=_blank} + +테스트를 갖춘 뒤에는 **FastAPI** 버전을 더 최신 버전으로 업그레이드하고, 테스트를 실행하여 모든 코드가 올바르게 동작하는지 확인하세요. + +모든 것이 동작하거나 필요한 변경을 한 뒤 모든 테스트가 통과한다면, `fastapi`를 그 새로운 최신 버전으로 고정할 수 있습니다. + +## Starlette에 대해 { #about-starlette } + +`starlette`의 버전은 고정하지 않는 것이 좋습니다. + +서로 다른 **FastAPI** 버전은 Starlette의 특정한 더 새로운 버전을 사용하게 됩니다. + +따라서 **FastAPI**가 올바른 Starlette 버전을 사용하도록 그냥 두면 됩니다. + +## Pydantic에 대해 { #about-pydantic } + +Pydantic은 자체 테스트에 **FastAPI**에 대한 테스트도 포함하고 있으므로, Pydantic의 새 버전(`1.0.0` 초과)은 항상 FastAPI와 호환됩니다. + +여러분에게 맞는 `1.0.0` 초과의 어떤 Pydantic 버전으로든 고정할 수 있습니다. + +예를 들어: ```txt -fastapi>=0.45.0,<0.46.0 -``` - -수정된 사항과 새로운 요소들이 "마이너" 버전에 추가되었습니다. - -/// tip | 팁 - -"마이너"란 버전 넘버의 가운데 숫자로, 예를 들어서 `0.2.3`의 "마이너" 버전은 `2`입니다. - -/// - -## FastAPI 버전의 업그레이드 - -응용 프로그램을 검사해야합니다. - -(Starlette 덕분에), **FastAPI** 를 이용하여 굉장히 쉽게 할 수 있습니다. [Testing](../tutorial/testing.md){.internal-link target=_blank}문서를 확인해 보십시오: - -검사를 해보고 난 후에, **FastAPI** 버전을 더 최신으로 업그레이드 할 수 있습니다. 그리고 코드들이 테스트에 정상적으로 작동하는지 확인을 해야합니다. - -만약에 모든 것이 정상 작동하거나 필요한 부분을 변경하고, 모든 검사를 통과한다면, 새로운 버전의 `fastapi`를 표시할 수 있습니다. - -## Starlette에 대해 - -`starlette`의 버전은 표시할 수 없습니다. - -서로다른 버전의 **FastAPI**가 구체적이고 새로운 버전의 Starlette을 사용할 것입니다. - -그러므로 **FastAPI**가 알맞은 Starlette 버전을 사용하도록 하십시오. - -## Pydantic에 대해 - -Pydantic은 **FastAPI** 를 위한 검사를 포함하고 있습니다. 따라서, 새로운 버전의 Pydantic(`1.0.0`이상)은 항상 FastAPI와 호환됩니다. - -작업을 하고 있는 `1.0.0` 이상의 모든 버전과 `2.0.0` 이하의 Pydantic 버전을 표시할 수 있습니다. - -예를 들어 다음과 같습니다: - -```txt -pydantic>=1.2.0,<2.0.0 +pydantic>=2.7.0,<3.0.0 ``` diff --git a/docs/ko/docs/environment-variables.md b/docs/ko/docs/environment-variables.md index 1e6af3ceba..dc231acb64 100644 --- a/docs/ko/docs/environment-variables.md +++ b/docs/ko/docs/environment-variables.md @@ -1,4 +1,4 @@ -# 환경 변수 +# 환경 변수 { #environment-variables } /// tip | 팁 @@ -6,11 +6,11 @@ /// -환경 변수는 파이썬 코드의 **바깥**인, **운영 체제**에 존재하는 변수입니다. 파이썬 코드나 다른 프로그램에서 읽을 수 있습니다. +환경 변수(또는 "**env var**"라고도 합니다)는 파이썬 코드의 **바깥**인, **운영 체제**에 존재하는 변수이며, 파이썬 코드(또는 다른 프로그램에서도)에서 읽을 수 있습니다. -환경 변수는 애플리케이션 **설정**을 처리하거나, 파이썬의 **설치** 과정의 일부로 유용합니다. +환경 변수는 애플리케이션 **설정**을 처리하거나, 파이썬의 **설치** 과정의 일부로 유용할 수 있습니다. -## 환경 변수를 만들고 사용하기 +## 환경 변수를 만들고 사용하기 { #create-and-use-env-vars } 파이썬 없이도, **셸 (터미널)** 에서 환경 변수를 **생성** 하고 사용할 수 있습니다. @@ -50,9 +50,9 @@ Hello Wade Wilson //// -## 파이썬에서 환경 변수 읽기 +## 파이썬에서 env var 읽기 { #read-env-vars-in-python } -파이썬 **바깥**인 터미널에서(다른 도구로도 가능) 환경 변수를 생성도 할 수도 있고, 이를 **파이썬에서 읽을 수 있습니다.** +파이썬 **바깥**인 터미널에서(또는 다른 어떤 방법으로든) 환경 변수를 만들고, 그런 다음 **파이썬에서 읽을 수 있습니다**. 예를 들어 다음과 같은 `main.py` 파일이 있다고 합시다: @@ -67,7 +67,7 @@ print(f"Hello {name} from Python") `os.getenv()` 의 두 번째 인자는 반환할 기본값입니다. -여기서는 `"World"`를 넣었기에 기본값으로써 사용됩니다. 넣지 않으면 `None` 이 기본값으로 사용됩니다. +제공하지 않으면 기본값은 `None`이며, 여기서는 사용할 기본값으로 `"World"`를 제공합니다. /// @@ -129,7 +129,7 @@ Hello Wade Wilson from Python 환경변수는 코드 바깥에서 설정될 수 있지만, 코드에서 읽을 수 있고, 나머지 파일과 함께 저장(`git`에 커밋)할 필요가 없으므로, 구성이나 **설정** 에 사용하는 것이 일반적입니다. -**특정 프로그램 호출**에 대해서만 사용할 수 있는 환경 변수를 만들 수도 있습니다. 해당 프로그램에서만 사용할 수 있고, 해당 프로그램이 실행되는 동안만 사용할 수 있습니다. +또한 **특정 프로그램 호출**에 대해서만 사용할 수 있는 환경 변수를 만들 수도 있는데, 해당 프로그램에서만 사용할 수 있고, 해당 프로그램이 실행되는 동안만 사용할 수 있습니다. 그렇게 하려면 프로그램 바로 앞, 같은 줄에 환경 변수를 만들어야 합니다: @@ -157,17 +157,17 @@ Hello World from Python /// -## 타입과 검증 +## 타입과 검증 { #types-and-validation } -이 환경변수들은 오직 **텍스트 문자열**로만 처리할 수 있습니다. 텍스트 문자열은 파이썬 외부에 있으며 다른 프로그램 및 나머지 시스템(Linux, Windows, macOS 등 다른 운영 체제)과 호환되어야 합니다. +이 환경변수들은 오직 **텍스트 문자열**로만 처리할 수 있습니다. 텍스트 문자열은 파이썬 외부에 있으며 다른 프로그램 및 나머지 시스템(그리고 Linux, Windows, macOS 같은 서로 다른 운영 체제에서도)과 호환되어야 합니다. 즉, 파이썬에서 환경 변수로부터 읽은 **모든 값**은 **`str`**이 되고, 다른 타입으로의 변환이나 검증은 코드에서 수행해야 합니다. -**애플리케이션 설정**을 처리하기 위한 환경 변수 사용에 대한 자세한 내용은 [고급 사용자 가이드 - 설정 및 환경 변수](./advanced/settings.md){.internal-link target=\_blank} 에서 확인할 수 있습니다. +**애플리케이션 설정**을 처리하기 위한 환경 변수 사용에 대한 자세한 내용은 [고급 사용자 가이드 - 설정 및 환경 변수](./advanced/settings.md){.internal-link target=_blank} 에서 확인할 수 있습니다. -## `PATH` 환경 변수 +## `PATH` 환경 변수 { #path-environment-variable } -**`PATH`**라고 불리는, **특별한** 환경변수가 있습니다. 운영체제(Linux, Windows, macOS 등)에서 실행할 프로그램을 찾기위해 사용됩니다. +**`PATH`**라고 불리는, **특별한** 환경변수가 있습니다. 운영체제(Linux, macOS, Windows)에서 실행할 프로그램을 찾기위해 사용됩니다. 변수 `PATH`의 값은 Linux와 macOS에서는 콜론 `:`, Windows에서는 세미콜론 `;`으로 구분된 디렉토리로 구성된 긴 문자열입니다. @@ -181,11 +181,11 @@ Hello World from Python 이는 시스템이 다음 디렉토리에서 프로그램을 찾아야 함을 의미합니다: -- `/usr/local/bin` -- `/usr/bin` -- `/bin` -- `/usr/sbin` -- `/sbin` +* `/usr/local/bin` +* `/usr/bin` +* `/bin` +* `/usr/sbin` +* `/sbin` //// @@ -197,9 +197,9 @@ C:\Program Files\Python312\Scripts;C:\Program Files\Python312;C:\Windows\System3 이는 시스템이 다음 디렉토리에서 프로그램을 찾아야 함을 의미합니다: -- `C:\Program Files\Python312\Scripts` -- `C:\Program Files\Python312` -- `C:\Windows\System32` +* `C:\Program Files\Python312\Scripts` +* `C:\Program Files\Python312` +* `C:\Windows\System32` //// @@ -209,7 +209,7 @@ C:\Program Files\Python312\Scripts;C:\Program Files\Python312;C:\Windows\System3 찾으면 **사용합니다**. 그렇지 않으면 **다른 디렉토리**에서 계속 찾습니다. -### 파이썬 설치와 `PATH` 업데이트 +### 파이썬 설치와 `PATH` 업데이트 { #installing-python-and-updating-the-path } 파이썬을 설치할 때, 아마 `PATH` 환경 변수를 업데이트 할 것이냐고 물어봤을 겁니다. @@ -285,13 +285,13 @@ $ C:\opt\custompython\bin\python //// -이 정보는 [가상 환경](virtual-environments.md){.internal-link target=\_blank} 에 대해 알아볼 때 유용할 것입니다. +이 정보는 [가상 환경](virtual-environments.md){.internal-link target=_blank} 에 대해 알아볼 때 유용할 것입니다. -## 결론 +## 결론 { #conclusion } -이 문서를 읽고 **환경 변수**가 무엇이고 파이썬에서 어떻게 사용하는지 기본적으로 이해하셨을 겁니다. +이 문서를 통해 **환경 변수**가 무엇이고 파이썬에서 어떻게 사용하는지 기본적으로 이해하셨을 겁니다. -또한 환경 변수에 대한 위키피디아(한국어)에서 이에 대해 자세히 알아볼 수 있습니다. +또한 환경 변수에 대한 위키피디아에서 이에 대해 자세히 알아볼 수 있습니다. 많은 경우에서, 환경 변수가 어떻게 유용하고 적용 가능한지 바로 명확하게 알 수는 없습니다. 하지만 개발할 때 다양한 시나리오에서 계속 나타나므로 이에 대해 아는 것이 좋습니다. diff --git a/docs/ko/docs/how-to/conditional-openapi.md b/docs/ko/docs/how-to/conditional-openapi.md index 79c7f0dd2d..16e6833661 100644 --- a/docs/ko/docs/how-to/conditional-openapi.md +++ b/docs/ko/docs/how-to/conditional-openapi.md @@ -1,46 +1,41 @@ -# 조건부적인 OpenAPI +# 조건부 OpenAPI { #conditional-openapi } -필요한 경우, 설정 및 환경 변수를 사용하여 환경에 따라 조건부로 OpenAPI를 구성하고 완전히 비활성화할 수도 있습니다. +필요한 경우, 설정 및 환경 변수를 사용하여 환경에 따라 OpenAPI를 조건부로 구성하고 완전히 비활성화할 수도 있습니다. -## 보안, API 및 docs에 대해서 +## 보안, API 및 docs에 대해서 { #about-security-apis-and-docs } 프로덕션에서, 문서화된 사용자 인터페이스(UI)를 숨기는 것이 API를 보호하는 방법이 *되어서는 안 됩니다*. -이는 API에 추가적인 보안을 제공하지 않으며, *경로 작업*은 여전히 동일한 위치에서 사용 할 수 있습니다. +이는 API에 추가적인 보안을 제공하지 않으며, *경로 처리*는 여전히 동일한 위치에서 사용 할 수 있습니다. 코드에 보안 결함이 있다면, 그 결함은 여전히 존재할 것입니다. -문서를 숨기는 것은 API와 상호작용하는 방법을 이해하기 어렵게 만들며, 프로덕션에서 디버깅을 더 어렵게 만들 수 있습니다. 이는 단순히 '모호성에 의한 보안'의 한 형태로 간주될 수 있습니다. +문서를 숨기는 것은 API와 상호작용하는 방법을 이해하기 어렵게 만들며, 프로덕션에서 디버깅을 더 어렵게 만들 수 있습니다. 이는 단순히 Security through obscurity의 한 형태로 간주될 수 있습니다. API를 보호하고 싶다면, 예를 들어 다음과 같은 더 나은 방법들이 있습니다: -* 요청 본문과 응답에 대해 잘 정의된 Pydantic 모델을 사용하도록 하세요. - +* 요청 본문과 응답에 대해 잘 정의된 Pydantic 모델이 있는지 확인하세요. * 종속성을 사용하여 필요한 권한과 역할을 구성하세요. - -* 평문 비밀번호를 절대 저장하지 말고, 오직 암호화된 비밀번호만 저장하세요. - -* Passlib과 JWT 토큰과 같은 잘 알려진 암호화 도구들을 구현하고 사용하세요. - +* 평문 비밀번호를 절대 저장하지 말고, 비밀번호 해시만 저장하세요. +* pwdlib와 JWT 토큰 등과 같은 잘 알려진 암호화 도구들을 구현하고 사용하세요. * 필요한 곳에 OAuth2 범위를 사용하여 더 세분화된 권한 제어를 추가하세요. +* ...등등. -* 등등.... +그럼에도 불구하고, 특정 환경(예: 프로덕션)에서 또는 환경 변수의 설정에 따라 API docs를 비활성화해야 하는 매우 특정한 사용 사례가 있을 수 있습니다. -그럼에도 불구하고, 특정 환경(예: 프로덕션)에서 또는 환경 변수의 설정에 따라 API 문서를 비활성화해야 하는 매우 특정한 사용 사례가 있을 수 있습니다. +## 설정 및 환경변수의 조건부 OpenAPI { #conditional-openapi-from-settings-and-env-vars } -## 설정 및 환경변수의 조건부 OpenAPI - -동일한 Pydantic 설정을 사용하여 생성된 OpenAPI 및 문서 UI를 쉽게 구성할 수 있습니다. +동일한 Pydantic 설정을 사용하여 생성된 OpenAPI 및 docs UI를 쉽게 구성할 수 있습니다. 예를 들어: -{* ../../docs_src/conditional_openapi/tutorial001.py hl[6,11] *} +{* ../../docs_src/conditional_openapi/tutorial001_py39.py hl[6,11] *} 여기서 `openapi_url` 설정을 기본값인 `"/openapi.json"`으로 선언합니다. 그런 뒤, 우리는 `FastAPI` 앱을 만들 때 그것을 사용합니다. -환경 변수 `OPENAPI_URL`을 빈 문자열로 설정하여 OpenAPI(문서 UI 포함)를 비활성화할 수도 있습니다. 예를 들어: +그런 다음 환경 변수 `OPENAPI_URL`을 빈 문자열로 설정하여 OpenAPI(UI docs 포함)를 비활성화할 수도 있습니다. 예를 들어:
diff --git a/docs/ko/docs/how-to/configure-swagger-ui.md b/docs/ko/docs/how-to/configure-swagger-ui.md index 5a57342cf2..174f976f67 100644 --- a/docs/ko/docs/how-to/configure-swagger-ui.md +++ b/docs/ko/docs/how-to/configure-swagger-ui.md @@ -1,4 +1,4 @@ -# Swagger UI 구성 +# Swagger UI 구성 { #configure-swagger-ui } 추가적인 Swagger UI 매개변수를 구성할 수 있습니다. @@ -8,7 +8,7 @@ FastAPI는 이 구성을 **JSON** 형식으로 변환하여 JavaScript와 호환되도록 합니다. 이는 Swagger UI에서 필요로 하는 형식입니다. -## 구문 강조 비활성화 +## 구문 강조 비활성화 { #disable-syntax-highlighting } 예를 들어, Swagger UI에서 구문 강조 기능을 비활성화할 수 있습니다. @@ -18,41 +18,41 @@ FastAPI는 이 구성을 **JSON** 형식으로 변환하여 JavaScript와 호환 그러나 `syntaxHighlight`를 `False`로 설정하여 구문 강조 기능을 비활성화할 수 있습니다: -{* ../../docs_src/configure_swagger_ui/tutorial001.py hl[3] *} +{* ../../docs_src/configure_swagger_ui/tutorial001_py39.py hl[3] *} ...그럼 Swagger UI에서 더 이상 구문 강조 기능이 표시되지 않습니다: -## 테마 변경 +## 테마 변경 { #change-the-theme } 동일한 방식으로 `"syntaxHighlight.theme"` 키를 사용하여 구문 강조 테마를 설정할 수 있습니다 (중간에 점이 포함된 것을 참고하십시오). -{* ../../docs_src/configure_swagger_ui/tutorial002.py hl[3] *} +{* ../../docs_src/configure_swagger_ui/tutorial002_py39.py hl[3] *} 이 설정은 구문 강조 색상 테마를 변경합니다: -## 기본 Swagger UI 매개변수 변경 +## 기본 Swagger UI 매개변수 변경 { #change-default-swagger-ui-parameters } FastAPI는 대부분의 사용 사례에 적합한 몇 가지 기본 구성 매개변수를 포함하고 있습니다. 기본 구성에는 다음이 포함됩니다: -{* ../../fastapi/openapi/docs.py ln[8:23] hl[17:23] *} +{* ../../fastapi/openapi/docs.py ln[9:24] hl[18:24] *} `swagger_ui_parameters` 인수에 다른 값을 설정하여 이러한 기본값 중 일부를 재정의할 수 있습니다. 예를 들어, `deepLinking`을 비활성화하려면 `swagger_ui_parameters`에 다음 설정을 전달할 수 있습니다: -{* ../../docs_src/configure_swagger_ui/tutorial003.py hl[3] *} +{* ../../docs_src/configure_swagger_ui/tutorial003_py39.py hl[3] *} -## 기타 Swagger UI 매개변수 +## 기타 Swagger UI 매개변수 { #other-swagger-ui-parameters } -사용할 수 있는 다른 모든 구성 옵션을 확인하려면, Swagger UI 매개변수에 대한 공식 문서를 참조하십시오. +사용할 수 있는 다른 모든 구성 옵션을 확인하려면, Swagger UI 매개변수에 대한 공식 Swagger UI 매개변수 문서를 참조하십시오. -## JavaScript 전용 설정 +## JavaScript 전용 설정 { #javascript-only-settings } Swagger UI는 **JavaScript 전용** 객체(예: JavaScript 함수)로 다른 구성을 허용하기도 합니다. @@ -67,4 +67,4 @@ presets: [ 이들은 문자열이 아닌 **JavaScript** 객체이므로 Python 코드에서 직접 전달할 수 없습니다. -이와 같은 JavaScript 전용 구성을 사용해야 하는 경우, 위의 방법 중 하나를 사용하여 모든 Swagger UI 경로 작업을 재정의하고 필요한 JavaScript를 수동으로 작성할 수 있습니다. +이와 같은 JavaScript 전용 구성을 사용해야 하는 경우, 위의 방법 중 하나를 사용할 수 있습니다. Swagger UI *경로 처리*를 모두 재정의하고 필요한 JavaScript를 수동으로 작성하세요. diff --git a/docs/ko/docs/index.md b/docs/ko/docs/index.md index b6b4765dad..0c317f296d 100644 --- a/docs/ko/docs/index.md +++ b/docs/ko/docs/index.md @@ -1,11 +1,11 @@ -# FastAPI +# FastAPI { #fastapi }

- FastAPI + FastAPI

FastAPI 프레임워크, 고성능, 간편한 학습, 빠른 코드 작성, 준비된 프로덕션 @@ -27,7 +27,7 @@ --- -**문서**: https://fastapi.tiangolo.com +**문서**: https://fastapi.tiangolo.com **소스 코드**: https://github.com/fastapi/fastapi @@ -37,36 +37,41 @@ FastAPI는 현대적이고, 빠르며(고성능), 파이썬 표준 타입 힌트 주요 특징으로: -* **빠름**: (Starlette과 Pydantic 덕분에) **NodeJS** 및 **Go**와 대등할 정도로 매우 높은 성능. [사용 가능한 가장 빠른 파이썬 프레임워크 중 하나](#_11). - +* **빠름**: (Starlette과 Pydantic 덕분에) **NodeJS** 및 **Go**와 대등할 정도로 매우 높은 성능. [사용 가능한 가장 빠른 파이썬 프레임워크 중 하나](#performance). * **빠른 코드 작성**: 약 200%에서 300%까지 기능 개발 속도 증가. * * **적은 버그**: 사람(개발자)에 의한 에러 약 40% 감소. * * **직관적**: 훌륭한 편집기 지원. 모든 곳에서 자동완성. 적은 디버깅 시간. * **쉬움**: 쉽게 사용하고 배우도록 설계. 적은 문서 읽기 시간. * **짧음**: 코드 중복 최소화. 각 매개변수 선언의 여러 기능. 적은 버그. * **견고함**: 준비된 프로덕션 용 코드를 얻으십시오. 자동 대화형 문서와 함께. -* **표준 기반**: API에 대한 (완전히 호환되는) 개방형 표준 기반: OpenAPI (이전에 Swagger로 알려졌던) 및 JSON 스키마. +* **표준 기반**: API에 대한 (완전히 호환되는) 개방형 표준 기반: OpenAPI (이전에 Swagger로 알려졌던) 및 JSON Schema. * 내부 개발팀의 프로덕션 애플리케이션을 빌드한 테스트에 근거한 측정 -## 골드 스폰서 +## 스폰서 { #sponsors } -{% if sponsors %} +### 키스톤 스폰서 { #keystone-sponsor } + +{% for sponsor in sponsors.keystone -%} + +{% endfor -%} + +### 골드 및 실버 스폰서 { #gold-and-silver-sponsors } + {% for sponsor in sponsors.gold -%} {% endfor -%} {%- for sponsor in sponsors.silver -%} {% endfor %} -{% endif %} -다른 스폰서 +다른 스폰서 -## 의견들 +## 의견들 { #opinions } "_[...] 저는 요즘 **FastAPI**를 많이 사용하고 있습니다. [...] 사실 우리 팀의 **마이크로소프트 ML 서비스** 전부를 바꿀 계획입니다. 그중 일부는 핵심 **Windows**와 몇몇의 **Office** 제품들이 통합되고 있습니다._" @@ -94,7 +99,7 @@ FastAPI는 현대적이고, 빠르며(고성능), 파이썬 표준 타입 힌트 "_솔직히, 당신이 만든 것은 매우 견고하고 세련되어 보입니다. 여러 면에서 **Hug**가 이렇게 되었으면 합니다 - 그걸 만든 누군가를 보는 것은 많은 영감을 줍니다._" -

Timothy Crosley - Hug 제작자 (ref)
+
Timothy Crosley - Hug 제작자 (ref)
--- @@ -106,50 +111,54 @@ FastAPI는 현대적이고, 빠르며(고성능), 파이썬 표준 타입 힌트 --- -## **Typer**, FastAPI의 CLI +"_프로덕션 Python API를 만들고자 한다면, 저는 **FastAPI**를 강력히 추천합니다. **아름답게 설계**되었고, **사용이 간단**하며, **확장성이 매우 뛰어나**고, 우리의 API 우선 개발 전략에서 **핵심 구성 요소**가 되었으며 Virtual TAC Engineer 같은 많은 자동화와 서비스를 이끌고 있습니다._" + +
Deon Pillsbury - Cisco (ref)
+ +--- + +## FastAPI 미니 다큐멘터리 { #fastapi-mini-documentary } + +2025년 말에 공개된 FastAPI 미니 다큐멘터리가 있습니다. 온라인에서 시청할 수 있습니다: + +FastAPI Mini Documentary + +## **Typer**, CLI를 위한 FastAPI { #typer-the-fastapi-of-clis } 웹 API 대신 터미널에서 사용할 CLI 앱을 만들고 있다면, **Typer**를 확인해 보십시오. -**Typer**는 FastAPI의 동생입니다. 그리고 **FastAPI의 CLI**가 되기 위해 생겼습니다. ⌨️ 🚀 +**Typer**는 FastAPI의 동생입니다. 그리고 **CLI를 위한 FastAPI**가 되기 위해 생겼습니다. ⌨️ 🚀 -## 요구사항 +## 요구사항 { #requirements } FastAPI는 거인들의 어깨 위에 서 있습니다: * 웹 부분을 위한 Starlette. * 데이터 부분을 위한 Pydantic. -## 설치 +## 설치 { #installation } + +가상 환경을 생성하고 활성화한 다음 FastAPI를 설치하세요:
```console -$ pip install fastapi +$ pip install "fastapi[standard]" ---> 100% ```
-프로덕션을 위해 Uvicorn 또는 Hypercorn과 같은 ASGI 서버도 필요할 겁니다. +**Note**: 모든 터미널에서 동작하도록 `"fastapi[standard]"`를 따옴표로 감싸 넣었는지 확인하세요. -
+## 예제 { #example } -```console -$ pip install "uvicorn[standard]" +### 만들기 { #create-it } ----> 100% -``` - -
- -## 예제 - -### 만들기 - -* `main.py` 파일을 만드십시오: +다음 내용으로 `main.py` 파일을 만드십시오: ```Python from typing import Union @@ -172,9 +181,9 @@ def read_item(item_id: int, q: Union[str, None] = None):
또는 async def 사용하기... -여러분의 코드가 `async` / `await`을 사용한다면, `async def`를 사용하십시오. +여러분의 코드가 `async` / `await`을 사용한다면, `async def`를 사용하십시오: -```Python hl_lines="9 14" +```Python hl_lines="9 14" from typing import Union from fastapi import FastAPI @@ -194,22 +203,35 @@ async def read_item(item_id: int, q: Union[str, None] = None): **Note**: -잘 모르겠다면, 문서에서 `async`와 `await`에 관한 _"급하세요?"_ 섹션을 확인해 보십시오. +잘 모르겠다면, 문서에서 `async`와 `await`에 관한 _"급하세요?"_ 섹션을 확인해 보십시오.
-### 실행하기 +### 실행하기 { #run-it } -서버를 실행하십시오: +다음 명령으로 서버를 실행하십시오:
```console -$ uvicorn main:app --reload +$ fastapi dev main.py + ╭────────── FastAPI CLI - Development mode ───────────╮ + │ │ + │ Serving at: http://127.0.0.1:8000 │ + │ │ + │ API docs: http://127.0.0.1:8000/docs │ + │ │ + │ Running in development mode, for production use: │ + │ │ + │ fastapi run │ + │ │ + ╰─────────────────────────────────────────────────────╯ + +INFO: Will watch for changes in these directories: ['/home/user/code/awesomeapp'] INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -INFO: Started reloader process [28720] -INFO: Started server process [28722] +INFO: Started reloader process [2248755] using WatchFiles +INFO: Started server process [2248757] INFO: Waiting for application startup. INFO: Application startup complete. ``` @@ -217,17 +239,17 @@ INFO: Application startup complete.
-uvicorn main:app --reload 명령에 관하여... +fastapi dev main.py 명령에 관하여... -명령 `uvicorn main:app`은 다음을 나타냅니다: +`fastapi dev` 명령은 `main.py` 파일을 읽고, 그 안의 **FastAPI** 앱을 감지한 다음, Uvicorn을 사용해 서버를 시작합니다. -* `main`: `main.py` 파일 (파이썬 "모듈"). -* `app`: the object created inside of `main.py` with the line `app = FastAPI()`. -* `--reload`: 코드가 변경된 후 서버 재시작하기. 개발환경에서만 사용하십시오. +기본적으로 `fastapi dev`는 로컬 개발을 위해 auto-reload가 활성화된 상태로 시작됩니다. + +자세한 내용은 FastAPI CLI 문서에서 확인할 수 있습니다.
-### 확인하기 +### 확인하기 { #check-it } 브라우저로 http://127.0.0.1:8000/items/5?q=somequery를 열어보십시오. @@ -241,10 +263,10 @@ INFO: Application startup complete. * _경로_ `/` 및 `/items/{item_id}`에서 HTTP 요청 받기. * 두 _경로_ 모두 `GET` 연산(HTTP _메소드_ 로 알려진)을 받습니다. -* _경로_ `/items/{item_id}`는 _경로 매개변수_ `int`형 이어야 하는 `item_id`를 가지고 있습니다. -* _경로_ `/items/{item_id}`는 선택적인 `str`형 이어야 하는 _경로 매개변수_ `q`를 가지고 있습니다. +* _경로_ `/items/{item_id}`는 `int`형 이어야 하는 _경로 매개변수_ `item_id`를 가지고 있습니다. +* _경로_ `/items/{item_id}`는 선택적인 `str`형 _쿼리 매개변수_ `q`를 가지고 있습니다. -### 대화형 API 문서 +### 대화형 API 문서 { #interactive-api-docs } 이제 http://127.0.0.1:8000/docs로 가보십시오. @@ -252,7 +274,7 @@ INFO: Application startup complete. ![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) -### 대안 API 문서 +### 대안 API 문서 { #alternative-api-docs } 그리고 이제 http://127.0.0.1:8000/redoc로 가봅시다. @@ -260,13 +282,13 @@ INFO: Application startup complete. ![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) -## 예제 심화 +## 예제 업그레이드 { #example-upgrade } -이제 `PUT` 요청에 있는 본문(Body)을 받기 위해 `main.py`를 수정해봅시다. +이제 `PUT` 요청에서 본문을 받기 위해 `main.py` 파일을 수정해봅시다. -Pydantic을 이용해 파이썬 표준 타입으로 본문을 선언합니다. +Pydantic 덕분에 표준 Python 타입을 사용해 본문을 선언합니다. -```Python hl_lines="4 9 10 11 12 25 26 27" +```Python hl_lines="4 9-12 25-27" from typing import Union from fastapi import FastAPI @@ -296,25 +318,25 @@ def update_item(item_id: int, item: Item): return {"item_name": item.name, "item_id": item_id} ``` -서버가 자동으로 리로딩 할 수 있어야 합니다 (위에서 `uvicorn` 명령에 `--reload`을 추가 했기 때문입니다). +`fastapi dev` 서버는 자동으로 리로딩되어야 합니다. -### 대화형 API 문서 업그레이드 +### 대화형 API 문서 업그레이드 { #interactive-api-docs-upgrade } 이제 http://127.0.0.1:8000/docs로 이동합니다. -* 대화형 API 문서가 새 본문과 함께 자동으로 업데이트 합니다: +* 대화형 API 문서는 새 본문을 포함해 자동으로 업데이트됩니다: ![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) -* "Try it out" 버튼을 클릭하면, 매개변수를 채울 수 있게 해주고 직접 API와 상호작용 할 수 있습니다: +* "Try it out" 버튼을 클릭하면, 매개변수를 채우고 API와 직접 상호작용할 수 있습니다: ![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) -* 그러고 나서 "Execute" 버튼을 누르면, 사용자 인터페이스는 API와 통신하고 매개변수를 전송하며 그 결과를 가져와서 화면에 표시합니다: +* 그런 다음 "Execute" 버튼을 클릭하면, 사용자 인터페이스가 API와 통신하고 매개변수를 전송한 뒤 결과를 받아 화면에 표시합니다: ![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) -### 대안 API 문서 업그레이드 +### 대안 API 문서 업그레이드 { #alternative-api-docs-upgrade } 그리고 이제, http://127.0.0.1:8000/redoc로 이동합니다. @@ -322,7 +344,7 @@ def update_item(item_id: int, item: Item): ![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) -### 요약 +### 요약 { #recap } 요약하면, 여러분은 매개변수의 타입, 본문 등을 함수 매개변수로서 **한번에** 선언했습니다. @@ -351,8 +373,8 @@ item: Item * 타입 검사. * 데이터 검증: * 데이터가 유효하지 않을 때 자동으로 생성하는 명확한 에러. - * 중첩된 JSON 객체에 대한 유효성 검사. -* 입력 데이터 변환: 네트워크에서 파이썬 데이터 및 타입으로 전송. 읽을 수 있는 것들: + * 깊이 중첩된 JSON 객체에 대한 유효성 검사. +* 입력 데이터 변환: 네트워크에서 파이썬 데이터 및 타입으로 전송. 읽을 수 있는 것들: * JSON. * 경로 매개변수. * 쿼리 매개변수. @@ -360,7 +382,7 @@ item: Item * 헤더. * 폼(Forms). * 파일. -* 출력 데이터 변환: 파이썬 데이터 및 타입을 네트워크 데이터로 전환(JSON 형식으로): +* 출력 데이터 변환: 파이썬 데이터 및 타입을 네트워크 데이터로 전환(JSON 형식으로): * 파이썬 타입 변환 (`str`, `int`, `float`, `bool`, `list`, 등). * `datetime` 객체. * `UUID` 객체. @@ -377,13 +399,13 @@ item: Item * `GET` 및 `PUT` 요청에 `item_id`가 경로에 있는지 검증. * `GET` 및 `PUT` 요청에 `item_id`가 `int` 타입인지 검증. * 그렇지 않다면 클라이언트는 유용하고 명확한 에러를 볼 수 있습니다. -* `GET` 요청에 `q`라는 선택적인 쿼리 매개변수가 검사(`http://127.0.0.1:8000/items/foo?q=somequery`처럼). +* `GET` 요청에 `q`라는 선택적인 쿼리 매개변수가 있는지 검사(`http://127.0.0.1:8000/items/foo?q=somequery`처럼). * `q` 매개변수는 `= None`으로 선언되었기 때문에 선택사항입니다. * `None`이 없다면 필수사항입니다(`PUT`의 경우와 마찬가지로). * `/items/{item_id}`으로의 `PUT` 요청은 본문을 JSON으로 읽음: * `name`을 필수 속성으로 갖고 `str` 형인지 검사. - * `price`을 필수 속성으로 갖고 `float` 형인지 검사. - * 만약 주어진다면, `is_offer`를 선택 속성으로 갖고 `bool` 형인지 검사. + * `price`를 필수 속성으로 갖고 `float` 형이어야 하는지 검사. + * 만약 주어진다면, `is_offer`를 선택 속성으로 갖고 `bool` 형이어야 하는지 검사. * 이 모든 것은 깊이 중첩된 JSON 객체에도 적용됩니다. * JSON을 변환하거나 JSON으로 변환하는 것을 자동화. * 다음에서 사용할 수 있는 모든 것을 OpenAPI로 문서화: @@ -417,30 +439,88 @@ item: Item ![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) -더 많은 기능을 포함한 보다 완전한 예제의 경우, 튜토리얼 - 사용자 가이드를 보십시오. +더 많은 기능을 포함한 보다 완전한 예제의 경우, 튜토리얼 - 사용자 가이드를 보십시오. **스포일러 주의**: 튜토리얼 - 사용자 가이드는: * 서로 다른 장소에서 **매개변수** 선언: **헤더**, **쿠키**, **폼 필드** 그리고 **파일**. * `maximum_length` 또는 `regex`처럼 **유효성 제약**하는 방법. -* 강력하고 사용하기 쉬운 **의존성 주입** 시스템. +* 강력하고 사용하기 쉬운 **의존성 주입** 시스템. * **OAuth2** 지원을 포함한 **JWT tokens** 및 **HTTP Basic**을 갖는 보안과 인증. * (Pydantic 덕분에) **깊은 중첩 JSON 모델**을 선언하는데 더 진보한 (하지만 마찬가지로 쉬운) 기술. +* Strawberry 및 기타 라이브러리와의 **GraphQL** 통합. * (Starlette 덕분에) 많은 추가 기능: * **웹 소켓** - * **GraphQL** * HTTPX 및 `pytest`에 기반한 극히 쉬운 테스트 * **CORS** * **쿠키 세션** * ...기타 등등. -## 성능 +### 앱 배포하기(선택 사항) { #deploy-your-app-optional } -독립된 TechEmpower 벤치마크에서 Uvicorn에서 작동하는 FastAPI 어플리케이션이 사용 가능한 가장 빠른 프레임워크 중 하나로 Starlette와 Uvicorn(FastAPI에서 내부적으로 사용)에만 밑돌고 있습니다. (*) +선택적으로 FastAPI 앱을 FastAPI Cloud에 배포할 수 있습니다. 아직이라면 대기자 명단에 등록해 보세요. 🚀 -자세한 내용은 벤치마크 섹션을 보십시오. +이미 **FastAPI Cloud** 계정이 있다면(대기자 명단에서 초대해 드렸습니다 😉), 한 번의 명령으로 애플리케이션을 배포할 수 있습니다. -## 선택가능한 의존성 +배포하기 전에, 로그인되어 있는지 확인하세요: + +
+ +```console +$ fastapi login + +You are logged in to FastAPI Cloud 🚀 +``` + +
+ +그런 다음 앱을 배포하세요: + +
+ +```console +$ fastapi deploy + +Deploying to FastAPI Cloud... + +✅ Deployment successful! + +🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev +``` + +
+ +이게 전부입니다! 이제 해당 URL에서 앱에 접근할 수 있습니다. ✨ + +#### FastAPI Cloud 소개 { #about-fastapi-cloud } + +**FastAPI Cloud**는 **FastAPI** 뒤에 있는 동일한 작성자와 팀이 만들었습니다. + +최소한의 노력으로 API를 **빌드**, **배포**, **접근**하는 과정을 간소화합니다. + +FastAPI로 앱을 빌드할 때의 동일한 **개발자 경험**을 클라우드에 **배포**하는 데까지 확장해 줍니다. 🎉 + +FastAPI Cloud는 *FastAPI and friends* 오픈 소스 프로젝트의 주요 스폰서이자 자금 제공자입니다. ✨ + +#### 다른 클라우드 제공자에 배포하기 { #deploy-to-other-cloud-providers } + +FastAPI는 오픈 소스이며 표준을 기반으로 합니다. 선택한 어떤 클라우드 제공자에도 FastAPI 앱을 배포할 수 있습니다. + +클라우드 제공자의 가이드를 따라 FastAPI 앱을 배포하세요. 🤓 + +## 성능 { #performance } + +독립된 TechEmpower 벤치마크에서 Uvicorn에서 작동하는 FastAPI 애플리케이션이 사용 가능한 가장 빠른 Python 프레임워크 중 하나로 Starlette와 Uvicorn(FastAPI에서 내부적으로 사용)에만 밑돌고 있습니다. (*) + +자세한 내용은 벤치마크 섹션을 보십시오. + +## 의존성 { #dependencies } + +FastAPI는 Pydantic과 Starlette에 의존합니다. + +### `standard` 의존성 { #standard-dependencies } + +FastAPI를 `pip install "fastapi[standard]"`로 설치하면 `standard` 그룹의 선택적 의존성이 함께 설치됩니다. Pydantic이 사용하는: @@ -448,21 +528,38 @@ Pydantic이 사용하는: Starlette이 사용하는: -* HTTPX - `TestClient`를 사용하려면 필요. -* jinja2 - 기본 템플릿 설정을 사용하려면 필요. -* python-multipart - `request.form()`과 함께 "parsing"의 지원을 원하면 필요. -* itsdangerous - `SessionMiddleware` 지원을 위해 필요. -* pyyaml - Starlette의 `SchemaGenerator` 지원을 위해 필요 (FastAPI와 쓸때는 필요 없을 것입니다). -* graphene - `GraphQLApp` 지원을 위해 필요. +* httpx - `TestClient`를 사용하려면 필요. +* jinja2 - 기본 템플릿 설정을 사용하려면 필요. +* python-multipart - `request.form()`과 함께 form "parsing" 지원을 원하면 필요. -FastAPI / Starlette이 사용하는: +FastAPI가 사용하는: -* uvicorn - 애플리케이션을 로드하고 제공하는 서버. -* orjson - `ORJSONResponse`을 사용하려면 필요. +* uvicorn - 애플리케이션을 로드하고 제공하는 서버를 위한 것입니다. 여기에는 고성능 서빙에 필요한 일부 의존성(예: `uvloop`)이 포함된 `uvicorn[standard]`가 포함됩니다. +* `fastapi-cli[standard]` - `fastapi` 명령을 제공하기 위한 것입니다. + * 여기에는 FastAPI 애플리케이션을 FastAPI Cloud에 배포할 수 있게 해주는 `fastapi-cloud-cli`가 포함됩니다. + +### `standard` 의존성 없이 { #without-standard-dependencies } + +`standard` 선택적 의존성을 포함하고 싶지 않다면, `pip install "fastapi[standard]"` 대신 `pip install fastapi`로 설치할 수 있습니다. + +### `fastapi-cloud-cli` 없이 { #without-fastapi-cloud-cli } + +표준 의존성과 함께 FastAPI를 설치하되 `fastapi-cloud-cli` 없이 설치하고 싶다면, `pip install "fastapi[standard-no-fastapi-cloud-cli]"`로 설치할 수 있습니다. + +### 추가 선택적 의존성 { #additional-optional-dependencies } + +추가로 설치하고 싶을 수 있는 의존성도 있습니다. + +추가 선택적 Pydantic 의존성: + +* pydantic-settings - 설정 관리를 위한 것입니다. +* pydantic-extra-types - Pydantic에서 사용할 추가 타입을 위한 것입니다. + +추가 선택적 FastAPI 의존성: + +* orjson - `ORJSONResponse`를 사용하려면 필요. * ujson - `UJSONResponse`를 사용하려면 필요. -`pip install fastapi[all]`를 통해 이 모두를 설치 할 수 있습니다. - -## 라이센스 +## 라이센스 { #license } 이 프로젝트는 MIT 라이센스 조약에 따라 라이센스가 부여됩니다. diff --git a/docs/ko/docs/learn/index.md b/docs/ko/docs/learn/index.md index 7ac3a99b64..0b4d14ff47 100644 --- a/docs/ko/docs/learn/index.md +++ b/docs/ko/docs/learn/index.md @@ -1,5 +1,5 @@ -# 배우기 +# 배우기 { #learn } -여기 **FastAPI**를 배우기 위한 입문 자료와 자습서가 있습니다. +여기 **FastAPI**를 배우기 위한 입문 섹션과 자습서가 있습니다. -여러분은 FastAPI를 배우기 위해 **책**, **강의**, **공식 자료** 그리고 추천받은 방법을 고려할 수 있습니다. 😎 +여러분은 이것을 FastAPI를 배우기 위한 **책**, **강의**, **공식**이자 권장되는 방법으로 생각할 수 있습니다. 😎 diff --git a/docs/ko/docs/openapi-webhooks.md b/docs/ko/docs/openapi-webhooks.md deleted file mode 100644 index 96339aa961..0000000000 --- a/docs/ko/docs/openapi-webhooks.md +++ /dev/null @@ -1,55 +0,0 @@ -# OpenAPI 웹훅(Webhooks) - -API **사용자**에게 특정 **이벤트**가 발생할 때 *그들*의 앱(시스템)에 요청을 보내 **알림**을 전달할 수 있다는 것을 알리고 싶은 경우가 있습니다. - -즉, 일반적으로 사용자가 API에 요청을 보내는 것과는 반대로, **API**(또는 앱)가 **사용자의 시스템**(그들의 API나 앱)으로 **요청을 보내는** 상황을 의미합니다. - -이를 흔히 **웹훅(Webhook)**이라고 부릅니다. - -## 웹훅 스텝 - -**코드에서** 웹훅으로 보낼 메시지, 즉 요청의 **바디(body)**를 정의하는 것이 일반적인 프로세스입니다. - -앱에서 해당 요청이나 이벤트를 전송할 **시점**을 정의합니다. - -**사용자**는 앱이 해당 요청을 보낼 **URL**을 정의합니다. (예: 웹 대시보드에서 설정) - -웹훅의 URL을 등록하는 방법과 이러한 요청을 실제로 전송하는 코드에 대한 모든 로직은 여러분에게 달려 있습니다. 원하는대로 **고유의 코드**를 작성하면 됩니다. - -## **FastAPI**와 OpenAPI로 웹훅 문서화하기 - -**FastAPI**를 사용하여 OpenAPI와 함께 웹훅의 이름, 앱이 보낼 수 있는 HTTP 작업 유형(예: `POST`, `PUT` 등), 그리고 보낼 요청의 **바디**를 정의할 수 있습니다. - -이를 통해 사용자가 **웹훅** 요청을 수신할 **API 구현**을 훨씬 쉽게 할 수 있으며, 경우에 따라 사용자 API 코드의 일부를 자동 생성할 수도 있습니다. - -/// info - -웹훅은 OpenAPI 3.1.0 이상에서 지원되며, FastAPI `0.99.0` 이상 버전에서 사용할 수 있습니다. - -/// - -## 웹훅이 포함된 앱 만들기 - -**FastAPI** 애플리케이션을 만들 때, `webhooks` 속성을 사용하여 *웹훅*을 정의할 수 있습니다. 이는 `@app.webhooks.post()`와 같은 방식으로 *경로(path) 작업*을 정의하는 것과 비슷합니다. - -{* ../../docs_src/openapi_webhooks/tutorial001.py hl[9:13,36:53] *} - -이렇게 정의한 웹훅은 **OpenAPI** 스키마와 자동 **문서화 UI**에 표시됩니다. - -/// info - -`app.webhooks` 객체는 사실 `APIRouter`일 뿐이며, 여러 파일로 앱을 구성할 때 사용하는 것과 동일한 타입입니다. - -/// - -웹훅에서는 실제 **경로(path)** (예: `/items/`)를 선언하지 않는 점에 유의해야 합니다. 여기서 전달하는 텍스트는 **식별자**로, 웹훅의 이름(이벤트 이름)입니다. 예를 들어, `@app.webhooks.post("new-subscription")`에서 웹훅 이름은 `new-subscription`입니다. - -이는 실제 **URL 경로**는 **사용자**가 다른 방법(예: 웹 대시보드)을 통해 지정하도록 기대되기 때문입니다. - -### 문서 확인하기 - -이제 앱을 시작하고 http://127.0.0.1:8000/docs로 이동해 봅시다. - -문서에서 기존 *경로 작업*뿐만 아니라 **웹훅**도 표시된 것을 확인할 수 있습니다: - - diff --git a/docs/ko/docs/project-generation.md b/docs/ko/docs/project-generation.md index dd11fca703..73ea67d3e5 100644 --- a/docs/ko/docs/project-generation.md +++ b/docs/ko/docs/project-generation.md @@ -1,4 +1,4 @@ -# Full Stack FastAPI 템플릿 +# Full Stack FastAPI 템플릿 { #full-stack-fastapi-template } 템플릿은 일반적으로 특정 설정과 함께 제공되지만, 유연하고 커스터마이징이 가능하게 디자인 되었습니다. 이 특성들은 여러분이 프로젝트의 요구사항에 맞춰 수정, 적용을 할 수 있게 해주고, 템플릿이 완벽한 시작점이 되게 해줍니다. 🏁 @@ -6,23 +6,23 @@ GitHub 저장소: Full Stack FastAPI 템플릿 -## Full Stack FastAPI 템플릿 - 기술 스택과 기능들 +## Full Stack FastAPI 템플릿 - 기술 스택과 기능들 { #full-stack-fastapi-template-technology-stack-and-features } -- ⚡ [**FastAPI**](https://fastapi.tiangolo.com): Python 백엔드 API. - - 🧰 [SQLModel](https://sqlmodel.tiangolo.com): Python SQL 데이터 상호작용을 위한 (ORM). - - 🔍 [Pydantic](https://docs.pydantic.dev): FastAPI에 의해 사용되는, 데이터 검증과 설정관리. - - 💾 [PostgreSQL](https://www.postgresql.org): SQL 데이터베이스. -- 🚀 [React](https://react.dev): 프론트엔드. - - 💃 TypeScript, hooks, [Vite](https://vitejs.dev) 및 기타 현대적인 프론트엔드 스택을 사용. - - 🎨 [Chakra UI](https://chakra-ui.com): 프론트엔드 컴포넌트. +- ⚡ Python 백엔드 API를 위한 [**FastAPI**](https://fastapi.tiangolo.com/ko). + - 🧰 Python SQL 데이터베이스 상호작용을 위한 [SQLModel](https://sqlmodel.tiangolo.com) (ORM). + - 🔍 FastAPI에 의해 사용되는, 데이터 검증과 설정 관리를 위한 [Pydantic](https://docs.pydantic.dev). + - 💾 SQL 데이터베이스로서의 [PostgreSQL](https://www.postgresql.org). +- 🚀 프론트엔드를 위한 [React](https://react.dev). + - 💃 TypeScript, hooks, Vite 및 기타 현대적인 프론트엔드 스택을 사용. + - 🎨 프론트엔드 컴포넌트를 위한 [Tailwind CSS](https://tailwindcss.com) 및 [shadcn/ui](https://ui.shadcn.com). - 🤖 자동으로 생성된 프론트엔드 클라이언트. - - 🧪 E2E 테스트를 위한 [Playwright](https://playwright.dev). + - 🧪 End-to-End 테스트를 위한 [Playwright](https://playwright.dev). - 🦇 다크 모드 지원. -- 🐋 [Docker Compose](https://www.docker.com): 개발 환경과 프로덕션(운영). +- 🐋 개발 환경과 프로덕션(운영)을 위한 [Docker Compose](https://www.docker.com). - 🔒 기본으로 지원되는 안전한 비밀번호 해싱. -- 🔑 JWT 토큰 인증. +- 🔑 JWT (JSON Web Token) 인증. - 📫 이메일 기반 비밀번호 복구. -- ✅ [Pytest]를 이용한 테스트(https://pytest.org). -- 📞 [Traefik](https://traefik.io): 리버스 프록시 / 로드 밸런서. +- ✅ [Pytest](https://pytest.org)를 이용한 테스트. +- 📞 리버스 프록시 / 로드 밸런서로서의 [Traefik](https://traefik.io). - 🚢 Docker Compose를 이용한 배포 지침: 자동 HTTPS 인증서를 처리하기 위한 프론트엔드 Traefik 프록시 설정 방법을 포함. - 🏭 GitHub Actions를 기반으로 CI (지속적인 통합) 및 CD (지속적인 배포). diff --git a/docs/ko/docs/python-types.md b/docs/ko/docs/python-types.md index 18d4b341e4..dc264df805 100644 --- a/docs/ko/docs/python-types.md +++ b/docs/ko/docs/python-types.md @@ -1,313 +1,466 @@ -# 파이썬 타입 소개 +# 파이썬 타입 소개 { #python-types-intro } -파이썬은 선택적으로 "타입 힌트(type hints)"를 지원합니다. +파이썬은 선택적으로 "타입 힌트(type hints)"(“type annotations”라고도 함)를 지원합니다. -이러한 **타입 힌트**들은 변수의 타입을 선언할 수 있게 해주는 특수한 구문입니다. +이러한 **"타입 힌트"** 또는 애너테이션은 변수의 타입을 선언할 수 있게 해주는 특수한 구문입니다. -변수의 타입을 지정하면 에디터와 툴이 더 많은 도움을 줄 수 있게 됩니다. +변수의 타입을 선언하면 에디터와 도구가 더 나은 지원을 제공할 수 있습니다. -이 문서는 파이썬 타입 힌트에 대한 **빠른 자습서 / 내용환기** 수준의 문서입니다. 여기서는 **FastAPI**를 쓰기 위한 최소한의 내용만을 다룹니다. +이 문서는 파이썬 타입 힌트에 대한 **빠른 자습서 / 내용 환기**입니다. **FastAPI**와 함께 사용하기 위해 필요한 최소한만 다룹니다... 실제로는 아주 조금만 있으면 됩니다. -**FastAPI**는 타입 힌트에 기반을 두고 있으며, 이는 많은 장점과 이익이 있습니다. +**FastAPI**는 모두 이러한 타입 힌트에 기반을 두고 있으며, 이는 많은 장점과 이점을 제공합니다. -비록 **FastAPI**를 쓰지 않는다고 하더라도, 조금이라도 알아두면 도움이 될 것입니다. +하지만 **FastAPI**를 전혀 사용하지 않더라도, 타입 힌트를 조금만 배워도 도움이 됩니다. /// note | 참고 -파이썬에 능숙하셔서 타입 힌트에 대해 모두 아신다면, 다음 챕터로 건너뛰세요. +파이썬에 능숙하고 타입 힌트에 대해 이미 모두 알고 있다면, 다음 장으로 건너뛰세요. /// -## 동기 부여 +## 동기 부여 { #motivation } -간단한 예제부터 시작해봅시다: +간단한 예제로 시작해봅시다: -{* ../../docs_src/python_types/tutorial001.py *} +{* ../../docs_src/python_types/tutorial001_py39.py *} - -이 프로그램을 실행한 결과값: +이 프로그램을 호출하면 다음이 출력됩니다: ``` John Doe ``` -함수는 아래와 같이 실행됩니다: +이 함수는 다음을 수행합니다: * `first_name`과 `last_name`를 받습니다. -* `title()`로 각 첫 문자를 대문자로 변환시킵니다. -* 두 단어를 중간에 공백을 두고 연결합니다. +* `title()`로 각각의 첫 글자를 대문자로 변환합니다. +* 가운데에 공백을 두고 연결합니다. -{* ../../docs_src/python_types/tutorial001.py hl[2] *} +{* ../../docs_src/python_types/tutorial001_py39.py hl[2] *} +### 수정하기 { #edit-it } -### 코드 수정 +매우 간단한 프로그램입니다. -이건 매우 간단한 프로그램입니다. +하지만 이제, 이것을 처음부터 작성한다고 상상해봅시다. -그런데 처음부터 작성한다고 생각을 해봅시다. +어느 시점엔 함수를 정의하기 시작했고, 매개변수도 준비해두었을 겁니다... -여러분은 매개변수를 준비했고, 함수를 정의하기 시작했을 겁니다. +그런데 "첫 글자를 대문자로 변환하는 그 메서드"를 호출해야 합니다. -이때 "첫 글자를 대문자로 바꾸는 함수"를 호출해야 합니다. +`upper`였나요? `uppercase`였나요? `first_uppercase`? `capitalize`? -`upper`였나? 아니면 `uppercase`? `first_uppercase`? `capitalize`? +그 다음, 개발자들의 오랜 친구인 에디터 자동완성을 시도합니다. -그때 개발자들의 오랜 친구, 에디터 자동완성을 시도해봅니다. +함수의 첫 번째 매개변수인 `first_name`을 입력하고, 점(`.`)을 찍은 다음, 완성을 트리거하기 위해 `Ctrl+Space`를 누릅니다. -당신은 `first_name`를 입력한 뒤 점(`.`)을 입력하고 자동완성을 켜기 위해서 `Ctrl+Space`를 눌렀습니다. - -하지만 슬프게도 아무런 도움이 되지 않습니다: +하지만, 슬프게도 쓸만한 게 아무것도 없습니다: -### 타입 추가하기 +### 타입 추가하기 { #add-types } 이전 버전에서 한 줄만 수정해봅시다. -저희는 이 함수의 매개변수 부분: +함수의 매개변수인 정확히 이 부분을: ```Python first_name, last_name ``` -을 아래와 같이 바꿀 겁니다: +에서: ```Python first_name: str, last_name: str ``` +로 바꾸겠습니다. + 이게 다입니다. -이게 "타입 힌트"입니다: +이것들이 "타입 힌트"입니다: -{* ../../docs_src/python_types/tutorial002.py hl[1] *} +{* ../../docs_src/python_types/tutorial002_py39.py hl[1] *} - -타입힌트는 다음과 같이 기본 값을 선언하는 것과는 다릅니다: +이것은 다음처럼 기본값을 선언하는 것과는 다릅니다: ```Python first_name="john", last_name="doe" ``` -이는 다른 것입니다. +다른 것입니다. -등호(`=`) 대신 콜론(`:`)을 쓰고 있습니다. +등호(`=`)가 아니라 콜론(`:`)을 사용합니다. -일반적으로 타입힌트를 추가한다고 해서 특별하게 어떤 일이 일어나지도 않습니다. +그리고 보통 타입 힌트를 추가해도, 타입 힌트 없이 일어나는 일과 비교해 특별히 달라지는 것은 없습니다. -그렇지만 이제, 다시 함수를 만드는 도중이라고 생각해봅시다. 다만 이번엔 타입 힌트가 있습니다. +하지만 이제, 타입 힌트를 포함해 그 함수를 다시 만드는 중이라고 상상해봅시다. -같은 상황에서 `Ctrl+Space`로 자동완성을 작동시키면, +같은 지점에서 `Ctrl+Space`로 자동완성을 트리거하면 다음이 보입니다: -아래와 같이 "그렇지!"하는 옵션이 나올때까지 스크롤을 내려서 볼 수 있습니다: +그러면 스크롤하며 옵션을 보다가, "기억나는" 것을 찾을 수 있습니다: -## 더 큰 동기부여 +## 더 큰 동기부여 { #more-motivation } -아래 함수를 보면, 이미 타입 힌트가 적용되어 있는 걸 볼 수 있습니다: +이 함수를 확인해보세요. 이미 타입 힌트가 있습니다: -{* ../../docs_src/python_types/tutorial003.py hl[1] *} +{* ../../docs_src/python_types/tutorial003_py39.py hl[1] *} - -편집기가 변수의 타입을 알고 있기 때문에, 자동완성 뿐 아니라 에러도 확인할 수 있습니다: +에디터가 변수의 타입을 알고 있기 때문에, 자동완성만 되는 게 아니라 오류 검사도 할 수 있습니다: -이제 고쳐야하는 걸 알기 때문에, `age`를 `str(age)`과 같이 문자열로 바꾸게 됩니다: +이제 고쳐야 한다는 것을 알고, `age`를 `str(age)`로 문자열로 바꿉니다: -{* ../../docs_src/python_types/tutorial004.py hl[2] *} +{* ../../docs_src/python_types/tutorial004_py39.py hl[2] *} +## 타입 선언 { #declaring-types } -## 타입 선언 +방금 타입 힌트를 선언하는 주요 위치를 보았습니다. 함수 매개변수입니다. -방금 함수의 매개변수로써 타입 힌트를 선언하는 주요 장소를 보았습니다. +이것은 **FastAPI**와 함께 사용할 때도 주요 위치입니다. -이 위치는 여러분이 **FastAPI**와 함께 이를 사용하는 주요 장소입니다. - -### Simple 타입 +### Simple 타입 { #simple-types } `str`뿐 아니라 모든 파이썬 표준 타입을 선언할 수 있습니다. -예를 들면: +예를 들어 다음을 사용할 수 있습니다: * `int` * `float` * `bool` * `bytes` -{* ../../docs_src/python_types/tutorial005.py hl[1] *} +{* ../../docs_src/python_types/tutorial005_py39.py hl[1] *} +### 타입 매개변수가 있는 Generic(제네릭) 타입 { #generic-types-with-type-parameters } -### 타입 매개변수를 활용한 Generic(제네릭) 타입 +`dict`, `list`, `set`, `tuple`처럼 다른 값을 담을 수 있는 데이터 구조가 있습니다. 그리고 내부 값에도 각자의 타입이 있을 수 있습니다. -`dict`, `list`, `set`, `tuple`과 같은 값을 저장할 수 있는 데이터 구조가 있고, 내부의 값은 각자의 타입을 가질 수도 있습니다. +이렇게 내부 타입을 가지는 타입을 "**generic**" 타입이라고 부릅니다. 그리고 내부 타입까지 포함해 선언할 수도 있습니다. -타입과 내부 타입을 선언하기 위해서는 파이썬 표준 모듈인 `typing`을 이용해야 합니다. +이런 타입과 내부 타입을 선언하려면 표준 파이썬 모듈 `typing`을 사용할 수 있습니다. 이 모듈은 이러한 타입 힌트를 지원하기 위해 존재합니다. -구체적으로는 아래 타입 힌트를 지원합니다. +#### 더 최신 버전의 Python { #newer-versions-of-python } -#### `List` +`typing`을 사용하는 문법은 Python 3.6부터 최신 버전까지, Python 3.9, Python 3.10 등을 포함한 모든 버전과 **호환**됩니다. -예를 들면, `str`의 `list`인 변수를 정의해봅시다. +파이썬이 발전함에 따라 **더 최신 버전**에서는 이러한 타입 애너테이션 지원이 개선되며, 많은 경우 타입 애너테이션을 선언하기 위해 `typing` 모듈을 import해서 사용할 필요조차 없게 됩니다. -`typing`에서 `List`(대문자 `L`)를 import 합니다. +프로젝트에서 더 최신 버전의 파이썬을 선택할 수 있다면, 그 추가적인 단순함을 활용할 수 있습니다. -{* ../../docs_src/python_types/tutorial006.py hl[1] *} +이 문서 전체에는 각 파이썬 버전과 호환되는 예제가 있습니다(차이가 있을 때). +예를 들어 "**Python 3.6+**"는 Python 3.6 이상(3.7, 3.8, 3.9, 3.10 등 포함)과 호환된다는 뜻입니다. 그리고 "**Python 3.9+**"는 Python 3.9 이상(3.10 등 포함)과 호환된다는 뜻입니다. -콜론(`:`) 문법을 이용하여 변수를 선언합니다. +**최신 버전의 Python**을 사용할 수 있다면, 최신 버전용 예제를 사용하세요. 예를 들어 "**Python 3.10+**"처럼, 가장 **좋고 가장 단순한 문법**을 갖게 됩니다. -타입으로는 `List`를 넣어줍니다. +#### List { #list } -이때 배열은 내부 타입을 포함하는 타입이기 때문에 대괄호 안에 넣어줍니다. +예를 들어, `str`의 `list`인 변수를 정의해봅시다. -{* ../../docs_src/python_types/tutorial006.py hl[4] *} +같은 콜론(`:`) 문법으로 변수를 선언합니다. +타입으로 `list`를 넣습니다. -/// tip | 팁 - -대괄호 안의 내부 타입은 "타입 매개변수(type paramters)"라고 합니다. - -이번 예제에서는 `str`이 `List`에 들어간 타입 매개변수 입니다. - -/// - -이는 "`items`은 `list`인데, 배열에 들어있는 아이템 각각은 `str`이다"라는 뜻입니다. - -이렇게 함으로써, 에디터는 배열에 들어있는 아이템을 처리할때도 도움을 줄 수 있게 됩니다: - - - -타입이 없으면 이건 거의 불가능이나 다름 없습니다. - -변수 `item`은 `items`의 개별 요소라는 사실을 알아두세요. - -그리고 에디터는 계속 `str`라는 사실을 알고 도와줍니다. - -#### `Tuple`과 `Set` - -`tuple`과 `set`도 동일하게 선언할 수 있습니다. - -{* ../../docs_src/python_types/tutorial007.py hl[1,4] *} - - -이 뜻은 아래와 같습니다: - -* 변수 `items_t`는, 차례대로 `int`, `int`, `str`인 `tuple`이다. -* 변수 `items_s`는, 각 아이템이 `bytes`인 `set`이다. - -#### `Dict` - -`dict`를 선언하려면 컴마로 구분된 2개의 파라미터가 필요합니다. - -첫 번째 매개변수는 `dict`의 키(key)이고, - -두 번째 매개변수는 `dict`의 값(value)입니다. - -{* ../../docs_src/python_types/tutorial008.py hl[1,4] *} - - -이 뜻은 아래와 같습니다: - -* 변수 `prices`는 `dict`이다: - * `dict`의 키(key)는 `str`타입이다. (각 아이템의 이름(name)) - * `dict`의 값(value)는 `float`타입이다. (각 아이템의 가격(price)) - -#### `Optional` - -`str`과 같이 타입을 선언할 때 `Optional`을 쓸 수도 있는데, "선택적(Optional)"이기때문에 `None`도 될 수 있습니다: - -```Python hl_lines="1 4" -{!../../docs_src/python_types/tutorial009.py!} -``` - -`Optional[str]`을 `str` 대신 쓰게 되면, 특정 값이 실제로는 `None`이 될 수도 있는데 항상 `str`이라고 가정하는 상황에서 에디터가 에러를 찾게 도와줄 수 있습니다. - -#### Generic(제네릭) 타입 - -이 타입은 대괄호 안에 매개변수를 가지며, 종류는: - -* `List` -* `Tuple` -* `Set` -* `Dict` -* `Optional` -* ...등등 - -위와 같은 타입은 **Generic(제네릭) 타입** 혹은 **Generics(제네릭스)**라고 불립니다. - -### 타입으로서의 클래스 - -변수의 타입으로 클래스를 선언할 수도 있습니다. - -이름(name)을 가진 `Person` 클래스가 있다고 해봅시다. - -{* ../../docs_src/python_types/tutorial010.py hl[1:3] *} - - -그렇게 하면 변수를 `Person`이라고 선언할 수 있게 됩니다. - -{* ../../docs_src/python_types/tutorial010.py hl[6] *} - - -그리고 역시나 모든 에디터 도움을 받게 되겠죠. - - - -## Pydantic 모델 - -Pydantic은 데이터 검증(Validation)을 위한 파이썬 라이브러리입니다. - -당신은 속성들을 포함한 클래스 형태로 "모양(shape)"을 선언할 수 있습니다. - -그리고 각 속성은 타입을 가지고 있습니다. - -이 클래스를 활용하여서 값을 가지고 있는 인스턴스를 만들게 되면, 필요한 경우에는 적당한 타입으로 변환까지 시키기도 하여 데이터가 포함된 객체를 반환합니다. - -그리고 결과 객체에 대해서는 에디터의 도움을 받을 수 있게 됩니다. - -Pydantic 공식 문서 예시: - -{* ../../docs_src/python_types/tutorial011.py *} +`list`는 내부 타입을 포함하는 타입이므로, 그 타입들을 대괄호 안에 넣습니다: +{* ../../docs_src/python_types/tutorial006_py39.py hl[1] *} /// info | 정보 -Pydantic<에 대해 더 배우고 싶다면 공식 문서를 참고하세요. +대괄호 안의 내부 타입은 "type parameters"라고 부릅니다. + +이 경우 `str`이 `list`에 전달된 타입 매개변수입니다. /// -**FastAPI**는 모두 Pydantic을 기반으로 되어 있습니다. +이는 "변수 `items`는 `list`이고, 이 `list`의 각 아이템은 `str`이다"라는 뜻입니다. -이 모든 것이 실제로 어떻게 사용되는지에 대해서는 [자습서 - 사용자 안내서](tutorial/index.md){.internal-link target=_blank} 에서 더 많이 확인하실 수 있습니다. +이렇게 하면, 에디터는 리스트의 아이템을 처리하는 동안에도 지원을 제공할 수 있습니다: -## **FastAPI**에서의 타입 힌트 + -**FastAPI**는 여러 부분에서 타입 힌트의 장점을 취하고 있습니다. +타입이 없으면, 이는 거의 불가능합니다. -**FastAPI**에서 타입 힌트와 함께 매개변수를 선언하면 장점은: +변수 `item`이 리스트 `items`의 요소 중 하나라는 점에 주목하세요. + +그리고 에디터는 여전히 이것이 `str`임을 알고, 그에 대한 지원을 제공합니다. + +#### Tuple과 Set { #tuple-and-set } + +`tuple`과 `set`도 동일하게 선언할 수 있습니다: + +{* ../../docs_src/python_types/tutorial007_py39.py hl[1] *} + +이는 다음을 의미합니다: + +* 변수 `items_t`는 3개의 아이템을 가진 `tuple`이며, `int`, 또 다른 `int`, 그리고 `str`입니다. +* 변수 `items_s`는 `set`이며, 각 아이템의 타입은 `bytes`입니다. + +#### Dict { #dict } + +`dict`를 정의하려면, 쉼표로 구분된 2개의 타입 매개변수를 전달합니다. + +첫 번째 타입 매개변수는 `dict`의 키를 위한 것입니다. + +두 번째 타입 매개변수는 `dict`의 값을 위한 것입니다: + +{* ../../docs_src/python_types/tutorial008_py39.py hl[1] *} + +이는 다음을 의미합니다: + +* 변수 `prices`는 `dict`입니다: + * 이 `dict`의 키는 `str` 타입입니다(예: 각 아이템의 이름). + * 이 `dict`의 값은 `float` 타입입니다(예: 각 아이템의 가격). + +#### Union { #union } + +변수가 **여러 타입 중 어떤 것이든** 될 수 있다고 선언할 수 있습니다. 예를 들어 `int` 또는 `str`입니다. + +Python 3.6 이상(3.10 포함)에서는 `typing`의 `Union` 타입을 사용하고, 대괄호 안에 허용할 수 있는 타입들을 넣을 수 있습니다. + +Python 3.10에는 가능한 타입들을 세로 막대(`|`)로 구분해 넣을 수 있는 **새 문법**도 있습니다. + +//// tab | Python 3.10+ + +```Python hl_lines="1" +{!> ../../docs_src/python_types/tutorial008b_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="1 4" +{!> ../../docs_src/python_types/tutorial008b_py39.py!} +``` + +//// + +두 경우 모두 이는 `item`이 `int` 또는 `str`일 수 있다는 뜻입니다. + +#### `None`일 수도 있음 { #possibly-none } + +값이 `str` 같은 타입일 수도 있지만, `None`일 수도 있다고 선언할 수 있습니다. + +Python 3.6 이상(3.10 포함)에서는 `typing` 모듈에서 `Optional`을 import해서 사용하여 선언할 수 있습니다. + +```Python hl_lines="1 4" +{!../../docs_src/python_types/tutorial009_py39.py!} +``` + +그냥 `str` 대신 `Optional[str]`을 사용하면, 값이 항상 `str`이라고 가정하고 있지만 실제로는 `None`일 수도 있는 상황에서 에디터가 오류를 감지하도록 도와줍니다. + +`Optional[Something]`은 사실 `Union[Something, None]`의 축약이며, 서로 동등합니다. + +또한 이는 Python 3.10에서 `Something | None`을 사용할 수 있다는 의미이기도 합니다: + +//// tab | Python 3.10+ + +```Python hl_lines="1" +{!> ../../docs_src/python_types/tutorial009_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="1 4" +{!> ../../docs_src/python_types/tutorial009_py39.py!} +``` + +//// + +//// tab | Python 3.9+ alternative + +```Python hl_lines="1 4" +{!> ../../docs_src/python_types/tutorial009b_py39.py!} +``` + +//// + +#### `Union` 또는 `Optional` 사용하기 { #using-union-or-optional } + +Python 3.10 미만 버전을 사용한다면, 아주 **주관적인** 관점에서의 팁입니다: + +* 🚨 `Optional[SomeType]` 사용을 피하세요 +* 대신 ✨ **`Union[SomeType, None]`을 사용하세요** ✨. + +둘은 동등하고 내부적으로는 같은 것이지만, `Optional`이라는 단어가 값이 선택 사항인 것처럼 보일 수 있기 때문에 `Optional` 대신 `Union`을 권장합니다. 실제 의미는 값이 선택 사항이라는 뜻이 아니라, "값이 `None`일 수 있다"는 뜻이기 때문입니다. 선택 사항이 아니고 여전히 필수인 경우에도요. + +`Union[SomeType, None]`이 의미를 더 명확하게 드러낸다고 생각합니다. + +이건 단지 단어와 이름의 문제입니다. 하지만 그런 단어들이 여러분과 팀원이 코드에 대해 생각하는 방식에 영향을 줄 수 있습니다. + +예로, 이 함수를 봅시다: + +{* ../../docs_src/python_types/tutorial009c_py39.py hl[1,4] *} + +매개변수 `name`은 `Optional[str]`로 정의되어 있지만, **선택 사항이 아닙니다**. 매개변수 없이 함수를 호출할 수 없습니다: + +```Python +say_hi() # Oh, no, this throws an error! 😱 +``` + +기본값이 없기 때문에 `name` 매개변수는 **여전히 필수입니다**(*optional*이 아님). 그럼에도 `name`은 값으로 `None`을 허용합니다: + +```Python +say_hi(name=None) # This works, None is valid 🎉 +``` + +좋은 소식은 Python 3.10을 사용하면, 타입의 유니온을 정의하기 위해 간단히 `|`를 사용할 수 있어서 이런 걱정을 할 필요가 없다는 점입니다: + +{* ../../docs_src/python_types/tutorial009c_py310.py hl[1,4] *} + +그러면 `Optional`이나 `Union` 같은 이름에 대해 걱정할 필요도 없습니다. 😎 + +#### Generic(제네릭) 타입 { #generic-types } + +대괄호 안에 타입 매개변수를 받는 이러한 타입들은 **Generic types** 또는 **Generics**라고 부릅니다. 예를 들면: + +//// tab | Python 3.10+ + +대괄호와 내부 타입을 사용해, 동일한 내장 타입들을 제네릭으로 사용할 수 있습니다: + +* `list` +* `tuple` +* `set` +* `dict` + +그리고 이전 파이썬 버전과 마찬가지로 `typing` 모듈의 다음도 사용할 수 있습니다: + +* `Union` +* `Optional` +* ...그 밖의 것들. + +Python 3.10에서는 제네릭 `Union`과 `Optional`을 사용하는 대안으로, 타입 유니온을 선언하기 위해 세로 막대(`|`)를 사용할 수 있는데, 훨씬 더 좋고 단순합니다. + +//// + +//// tab | Python 3.9+ + +대괄호와 내부 타입을 사용해, 동일한 내장 타입들을 제네릭으로 사용할 수 있습니다: + +* `list` +* `tuple` +* `set` +* `dict` + +그리고 `typing` 모듈의 제네릭들: + +* `Union` +* `Optional` +* ...그 밖의 것들. + +//// + +### 타입으로서의 클래스 { #classes-as-types } + +변수의 타입으로 클래스를 선언할 수도 있습니다. + +이름을 가진 `Person` 클래스가 있다고 해봅시다: + +{* ../../docs_src/python_types/tutorial010_py39.py hl[1:3] *} + +그러면 `Person` 타입의 변수를 선언할 수 있습니다: + +{* ../../docs_src/python_types/tutorial010_py39.py hl[6] *} + +그리고 다시, 에디터의 모든 지원을 받을 수 있습니다: + + + +이는 "`one_person`은 `Person` 클래스의 **인스턴스**"라는 뜻입니다. + +"`one_person`은 `Person`이라는 **클래스**다"라는 뜻이 아닙니다. + +## Pydantic 모델 { #pydantic-models } + +Pydantic은 데이터 검증을 수행하는 파이썬 라이브러리입니다. + +속성을 가진 클래스 형태로 데이터의 "모양(shape)"을 선언합니다. + +그리고 각 속성은 타입을 가집니다. + +그 다음 그 클래스의 인스턴스를 몇 가지 값으로 생성하면, 값들을 검증하고, (그런 경우라면) 적절한 타입으로 변환한 뒤, 모든 데이터를 가진 객체를 제공합니다. + +그리고 그 결과 객체에 대해 에디터의 모든 지원을 받을 수 있습니다. + +Pydantic 공식 문서의 예시: + +{* ../../docs_src/python_types/tutorial011_py310.py *} + +/// info | 정보 + +Pydantic에 대해 더 알아보려면 문서를 확인하세요. + +/// + +**FastAPI**는 모두 Pydantic에 기반을 두고 있습니다. + +이 모든 것은 [자습서 - 사용자 안내서](tutorial/index.md){.internal-link target=_blank}에서 실제로 많이 보게 될 것입니다. + +/// tip | 팁 + +Pydantic은 기본값 없이 `Optional` 또는 `Union[Something, None]`을 사용할 때 특별한 동작이 있습니다. 이에 대해서는 Pydantic 문서의 Required Optional fields에서 더 읽을 수 있습니다. + +/// + +## 메타데이터 애너테이션이 있는 타입 힌트 { #type-hints-with-metadata-annotations } + +파이썬에는 `Annotated`를 사용해 이러한 타입 힌트에 **추가 메타데이터**를 넣을 수 있는 기능도 있습니다. + +Python 3.9부터 `Annotated`는 표준 라이브러리의 일부이므로, `typing`에서 import할 수 있습니다. + +{* ../../docs_src/python_types/tutorial013_py39.py hl[1,4] *} + +파이썬 자체는 이 `Annotated`로 아무것도 하지 않습니다. 그리고 에디터와 다른 도구들에게는 타입이 여전히 `str`입니다. + +하지만 `Annotated`의 이 공간을 사용해, 애플리케이션이 어떻게 동작하길 원하는지에 대한 추가 메타데이터를 **FastAPI**에 제공할 수 있습니다. + +기억해야 할 중요한 점은 `Annotated`에 전달하는 **첫 번째 *타입 매개변수***가 **실제 타입**이라는 것입니다. 나머지는 다른 도구를 위한 메타데이터일 뿐입니다. + +지금은 `Annotated`가 존재하며, 표준 파이썬이라는 것만 알면 됩니다. 😎 + +나중에 이것이 얼마나 **강력**할 수 있는지 보게 될 것입니다. + +/// tip | 팁 + +이것이 **표준 파이썬**이라는 사실은, 에디터에서 가능한 **최고의 개발자 경험**을 계속 얻을 수 있다는 뜻이기도 합니다. 사용하는 도구로 코드를 분석하고 리팩터링하는 등에서도요. ✨ + +또한 코드가 많은 다른 파이썬 도구 및 라이브러리와 매우 호환된다는 뜻이기도 합니다. 🚀 + +/// + +## **FastAPI**에서의 타입 힌트 { #type-hints-in-fastapi } + +**FastAPI**는 이러한 타입 힌트를 활용해 여러 가지를 합니다. + +**FastAPI**에서는 타입 힌트로 매개변수를 선언하면 다음을 얻습니다: * **에디터 도움**. * **타입 확인**. -...그리고 **FastAPI**는 같은 정의를 아래에도 적용합니다: +...그리고 **FastAPI**는 같은 선언을 다음에도 사용합니다: -* **요구사항 정의**: 요청 경로 매개변수, 쿼리 매개변수, 헤더, 바디, 의존성 등. -* **데이터 변환**: 요청에서 요구한 타입으로. -* **데이터 검증**: 각 요청마다: - * 데이터가 유효하지 않은 경우에는 **자동으로 에러**를 발생합니다. -* OpenAPI를 활용한 **API 문서화**: - * 자동으로 상호작용하는 유저 인터페이스에 쓰이게 됩니다. +* **요구사항 정의**: 요청 경로 매개변수, 쿼리 매개변수, 헤더, 바디, 의존성 등에서. +* **데이터 변환**: 요청에서 필요한 타입으로. +* **데이터 검증**: 각 요청에서: + * 데이터가 유효하지 않을 때 클라이언트에 반환되는 **자동 오류**를 생성합니다. +* OpenAPI를 사용해 API를 **문서화**: + * 자동 상호작용 문서 UI에서 사용됩니다. -위 내용이 다소 추상적일 수도 있지만, 걱정마세요. [자습서 - 사용자 안내서](tutorial/index.md){.internal-link target=_blank}에서 전부 확인 가능합니다. +이 모든 것이 다소 추상적으로 들릴 수도 있습니다. 걱정하지 마세요. [자습서 - 사용자 안내서](tutorial/index.md){.internal-link target=_blank}에서 실제로 확인하게 될 것입니다. -가장 중요한 건, 표준 파이썬 타입을 한 곳에서(클래스를 더하거나, 데코레이터 사용하는 대신) 사용함으로써 **FastAPI**가 당신을 위해 많은 일을 해준다는 사실이죠. +가장 중요한 점은 표준 파이썬 타입을 한 곳에서 사용함으로써(더 많은 클래스, 데코레이터 등을 추가하는 대신) **FastAPI**가 여러분을 위해 많은 일을 해준다는 사실입니다. /// info | 정보 -만약 모든 자습서를 다 보았음에도 타입에 대해서 더 보고자 방문한 경우에는 `mypy`에서 제공하는 "cheat sheet"이 좋은 자료가 될 겁니다. +자습서를 모두 끝내고 타입에 대해 더 알아보기 위해 다시 돌아왔다면, 좋은 자료로 `mypy`의 "cheat sheet"가 있습니다. /// diff --git a/docs/ko/docs/resources/index.md b/docs/ko/docs/resources/index.md index e804dd4d56..477b93a584 100644 --- a/docs/ko/docs/resources/index.md +++ b/docs/ko/docs/resources/index.md @@ -1,3 +1,3 @@ -# 리소스 +# 리소스 { #resources } -추가 리소스, 외부 링크, 기사 등. ✈️ +추가 리소스, 외부 링크 등. ✈️ diff --git a/docs/ko/docs/security/index.md b/docs/ko/docs/security/index.md deleted file mode 100644 index 5a6c733f05..0000000000 --- a/docs/ko/docs/security/index.md +++ /dev/null @@ -1,19 +0,0 @@ -# 고급 보안 - -## 추가 기능 - -[자습서 - 사용자 가이드: 보안](../../tutorial/security/index.md){.internal-link target=_blank} 문서에서 다룬 내용 외에도 보안 처리를 위한 몇 가지 추가 기능이 있습니다. - -/// tip - -다음 섹션은 **반드시 "고급"** 기능은 아닙니다. - -그리고 여러분의 사용 사례에 따라, 적합한 해결책이 그 중 하나에 있을 가능성이 있습니다. - -/// - -## 먼저 자습서 읽기 - -다음 섹션은 이미 [자습서 - 사용자 가이드: 보안](../../tutorial/security/index.md){.internal-link target=_blank} 문서를 읽었다고 가정합니다. - -이 섹션들은 모두 동일한 개념을 바탕으로 하며, 추가 기능을 제공합니다. diff --git a/docs/ko/docs/tutorial/background-tasks.md b/docs/ko/docs/tutorial/background-tasks.md index 9c4d57481f..9e868f2fa7 100644 --- a/docs/ko/docs/tutorial/background-tasks.md +++ b/docs/ko/docs/tutorial/background-tasks.md @@ -1,84 +1,86 @@ -# 백그라운드 작업 +# 백그라운드 작업 { #background-tasks } -FastAPI에서는 응답을 반환한 후에 실행할 백그라운드 작업을 정의할 수 있습니다. +FastAPI에서는 응답을 반환한 *후에* 실행할 백그라운드 작업을 정의할 수 있습니다. -백그라운드 작업은 클라이언트가 응답을 받기 위해 작업이 완료될 때까지 기다릴 필요가 없기 때문에 요청 후에 발생해야하는 작업에 매우 유용합니다. +백그라운드 작업은 요청 후에 발생해야 하지만, 클라이언트가 응답을 받기 전에 작업이 완료될 때까지 기다릴 필요가 없는 작업에 유용합니다. -이러한 작업에는 다음이 포함됩니다. +예를 들면 다음과 같습니다. -* 작업을 수행한 후 전송되는 이메일 알림 - * 이메일 서버에 연결하고 이메일을 전송하는 것은 (몇 초 정도) "느린" 경향이 있으므로, 응답은 즉시 반환하고 이메일 알림은 백그라운드에서 전송하는 게 가능합니다. +* 작업을 수행한 후 전송되는 이메일 알림: + * 이메일 서버에 연결하고 이메일을 전송하는 것은 (몇 초 정도) "느린" 경향이 있으므로, 응답은 즉시 반환하고 이메일 알림은 백그라운드에서 전송할 수 있습니다. * 데이터 처리: - * 예를 들어 처리에 오랜 시간이 걸리는 데이터를 받았을 때 "Accepted" (HTTP 202)을 반환하고, 백그라운드에서 데이터를 처리할 수 있습니다. + * 예를 들어 처리에 오랜 시간이 걸리는 프로세스를 거쳐야 하는 파일을 받았다면, "Accepted"(HTTP 202) 응답을 반환하고 백그라운드에서 파일을 처리할 수 있습니다. -## `백그라운드 작업` 사용 +## `BackgroundTasks` 사용 { #using-backgroundtasks } -먼저 아래와 같이 `BackgroundTasks`를 임포트하고, `BackgroundTasks`를 _경로 작동 함수_ 에서 매개변수로 가져오고 정의합니다. +먼저 `BackgroundTasks`를 임포트하고, `BackgroundTasks` 타입 선언으로 *경로 처리 함수*에 매개변수를 정의합니다: -{* ../../docs_src/background_tasks/tutorial001.py hl[1,13] *} +{* ../../docs_src/background_tasks/tutorial001_py39.py hl[1,13] *} -**FastAPI** 는 `BackgroundTasks` 개체를 생성하고, 매개 변수로 전달합니다. +**FastAPI**가 `BackgroundTasks` 타입의 객체를 생성하고 해당 매개변수로 전달합니다. -## 작업 함수 생성 +## 작업 함수 생성 { #create-a-task-function } -백그라운드 작업으로 실행할 함수를 정의합니다. +백그라운드 작업으로 실행할 함수를 생성합니다. -이것은 단순히 매개변수를 받을 수 있는 표준 함수일 뿐입니다. +이는 매개변수를 받을 수 있는 표준 함수일 뿐입니다. -**FastAPI**는 이것이 `async def` 함수이든, 일반 `def` 함수이든 내부적으로 이를 올바르게 처리합니다. +`async def` 함수일 수도, 일반 `def` 함수일 수도 있으며, **FastAPI**가 이를 올바르게 처리하는 방법을 알고 있습니다. -이 경우, 아래 작업은 파일에 쓰는 함수입니다. (이메일 보내기 시물레이션) +이 경우 작업 함수는 파일에 쓰기를 수행합니다(이메일 전송을 시뮬레이션). -그리고 이 작업은 `async`와 `await`를 사용하지 않으므로 일반 `def` 함수로 선언합니다. +그리고 쓰기 작업은 `async`와 `await`를 사용하지 않으므로, 일반 `def`로 함수를 정의합니다: -{* ../../docs_src/background_tasks/tutorial001.py hl[6:9] *} +{* ../../docs_src/background_tasks/tutorial001_py39.py hl[6:9] *} -## 백그라운드 작업 추가 +## 백그라운드 작업 추가 { #add-the-background-task } -_경로 작동 함수_ 내에서 작업 함수를 `.add_task()` 함수 통해 _백그라운드 작업_ 개체에 전달합니다. +*경로 처리 함수* 내부에서 `.add_task()` 메서드로 작업 함수를 *백그라운드 작업* 객체에 전달합니다: -{* ../../docs_src/background_tasks/tutorial001.py hl[14] *} +{* ../../docs_src/background_tasks/tutorial001_py39.py hl[14] *} -`.add_task()` 함수는 다음과 같은 인자를 받습니다 : +`.add_task()`는 다음 인자를 받습니다: -- 백그라운드에서 실행되는 작업 함수 (`write_notification`). -- 작업 함수에 순서대로 전달되어야 하는 일련의 인자 (`email`). -- 작업 함수에 전달되어야하는 모든 키워드 인자 (`message="some notification"`). +* 백그라운드에서 실행될 작업 함수(`write_notification`). +* 작업 함수에 순서대로 전달되어야 하는 인자 시퀀스(`email`). +* 작업 함수에 전달되어야 하는 키워드 인자(`message="some notification"`). -## 의존성 주입 +## 의존성 주입 { #dependency-injection } -`BackgroundTasks`를 의존성 주입 시스템과 함께 사용하면 _경로 작동 함수_, 종속성, 하위 종속성 등 여러 수준에서 BackgroundTasks 유형의 매개변수를 선언할 수 있습니다. +`BackgroundTasks`는 의존성 주입 시스템에서도 동작하며, *경로 처리 함수*, 의존성(dependable), 하위 의존성 등 여러 수준에서 `BackgroundTasks` 타입의 매개변수를 선언할 수 있습니다. -**FastAPI**는 각 경우에 수행할 작업과 동일한 개체를 내부적으로 재사용하기에, 모든 백그라운드 작업이 함께 병합되고 나중에 백그라운드에서 실행됩니다. +**FastAPI**는 각 경우에 무엇을 해야 하는지와 동일한 객체를 어떻게 재사용해야 하는지를 알고 있으므로, 모든 백그라운드 작업이 함께 병합되어 이후 백그라운드에서 실행됩니다: -{* ../../docs_src/background_tasks/tutorial002.py hl[13,15,22,25] *} -이 예제에서는 응답이 반환된 후에 `log.txt` 파일에 메시지가 기록됩니다. +{* ../../docs_src/background_tasks/tutorial002_an_py310.py hl[13,15,22,25] *} -요청에 쿼리가 있는 경우 백그라운드 작업의 로그에 기록됩니다. -그리고 _경로 작동 함수_ 에서 생성된 또 다른 백그라운드 작업은 경로 매개 변수를 활용하여 사용하여 메시지를 작성합니다. +이 예제에서는 응답이 전송된 *후에* 메시지가 `log.txt` 파일에 작성됩니다. -## 기술적 세부사항 +요청에 쿼리가 있었다면, 백그라운드 작업으로 로그에 작성됩니다. + +그 다음 *경로 처리 함수*에서 생성된 또 다른 백그라운드 작업이 `email` 경로 매개변수를 사용해 메시지를 작성합니다. + +## 기술적 세부사항 { #technical-details } `BackgroundTasks` 클래스는 `starlette.background`에서 직접 가져옵니다. -`BackgroundTasks` 클래스는 FastAPI에서 직접 임포트하거나 포함하기 때문에 실수로 `BackgroundTask` (끝에 `s`가 없음)을 임포트하더라도 starlette.background에서 `BackgroundTask`를 가져오는 것을 방지할 수 있습니다. +FastAPI에 직접 임포트/포함되어 있으므로 `fastapi`에서 임포트할 수 있고, 실수로 `starlette.background`에서 대안인 `BackgroundTask`(끝에 `s`가 없음)를 임포트하는 것을 피할 수 있습니다. -(`BackgroundTask`가 아닌) `BackgroundTasks`를 사용하면, _경로 작동 함수_ 매개변수로 사용할 수 있게 되고 나머지는 **FastAPI**가 대신 처리하도록 할 수 있습니다. 이것은 `Request` 객체를 직접 사용하는 것과 같은 방식입니다. +`BackgroundTask`가 아닌 `BackgroundTasks`만 사용하면, 이를 *경로 처리 함수*의 매개변수로 사용할 수 있고 나머지는 **FastAPI**가 `Request` 객체를 직접 사용할 때처럼 대신 처리해 줍니다. -FastAPI에서 `BackgroundTask`를 단독으로 사용하는 것은 여전히 가능합니다. 하지만 객체를 코드에서 생성하고, 이 객체를 포함하는 Starlette `Response`를 반환해야 합니다. +FastAPI에서 `BackgroundTask`만 단독으로 사용하는 것도 가능하지만, 코드에서 객체를 생성하고 이를 포함하는 Starlette `Response`를 반환해야 합니다. -`Starlette의 공식 문서`에서 백그라운드 작업에 대한 자세한 내용을 확인할 수 있습니다. +더 자세한 내용은 Starlette의 Background Tasks 공식 문서에서 확인할 수 있습니다. -## 경고 +## 주의사항 { #caveat } -만약 무거운 백그라운드 작업을 수행해야하고 동일한 프로세스에서 실행할 필요가 없는 경우 (예: 메모리, 변수 등을 공유할 필요가 없음) `Celery`와 같은 큰 도구를 사용하면 도움이 될 수 있습니다. +무거운 백그라운드 계산을 수행해야 하고, 반드시 동일한 프로세스에서 실행할 필요가 없다면(예: 메모리, 변수 등을 공유할 필요가 없음) Celery 같은 더 큰 도구를 사용하는 것이 도움이 될 수 있습니다. -RabbitMQ 또는 Redis와 같은 메시지/작업 큐 시스템 보다 복잡한 구성이 필요한 경향이 있지만, 여러 작업 프로세스를 특히 여러 서버의 백그라운드에서 실행할 수 있습니다. +이들은 RabbitMQ나 Redis 같은 메시지/작업 큐 관리자 등 더 복잡한 설정을 필요로 하는 경향이 있지만, 여러 프로세스에서, 특히 여러 서버에서 백그라운드 작업을 실행할 수 있습니다. -그러나 동일한 FastAPI 앱에서 변수 및 개체에 접근해야햐는 작은 백그라운드 수행이 필요한 경우 (예 : 알림 이메일 보내기) 간단하게 `BackgroundTasks`를 사용해보세요. +하지만 동일한 **FastAPI** 앱의 변수와 객체에 접근해야 하거나(또는 이메일 알림 전송처럼) 작은 백그라운드 작업을 수행해야 한다면, `BackgroundTasks`를 간단히 사용하면 됩니다. -## 요약 +## 요약 { #recap } -백그라운드 작업을 추가하기 위해 _경로 작동 함수_ 에 매개변수로 `BackgroundTasks`를 가져오고 사용합니다. +*경로 처리 함수*와 의존성에서 매개변수로 `BackgroundTasks`를 임포트해 사용하여 백그라운드 작업을 추가합니다. diff --git a/docs/ko/docs/tutorial/body-fields.md b/docs/ko/docs/tutorial/body-fields.md index 4708e7099f..c98734ab37 100644 --- a/docs/ko/docs/tutorial/body-fields.md +++ b/docs/ko/docs/tutorial/body-fields.md @@ -1,8 +1,8 @@ -# 본문 - 필드 +# 본문 - 필드 { #body-fields } -`Query`, `Path`와 `Body`를 사용해 *경로 작동 함수* 매개변수 내에서 추가적인 검증이나 메타데이터를 선언한 것처럼 Pydantic의 `Field`를 사용하여 모델 내에서 검증과 메타데이터를 선언할 수 있습니다. +`Query`, `Path`와 `Body`를 사용해 *경로 처리 함수* 매개변수 내에서 추가적인 검증이나 메타데이터를 선언한 것처럼 Pydantic의 `Field`를 사용하여 모델 내에서 검증과 메타데이터를 선언할 수 있습니다. -## `Field` 임포트 +## `Field` 임포트 { #import-field } 먼저 이를 임포트해야 합니다: @@ -14,7 +14,7 @@ /// -## 모델 어트리뷰트 선언 +## 모델 어트리뷰트 선언 { #declare-model-attributes } 그 다음 모델 어트리뷰트와 함께 `Field`를 사용할 수 있습니다: @@ -22,7 +22,7 @@ `Field`는 `Query`, `Path`와 `Body`와 같은 방식으로 동작하며, 모두 같은 매개변수들 등을 가집니다. -/// note | 기술적 세부사항 +/// note | 기술 세부사항 실제로 `Query`, `Path`등, 여러분이 앞으로 볼 다른 것들은 공통 클래스인 `Param` 클래스의 서브클래스 객체를 만드는데, 그 자체로 Pydantic의 `FieldInfo` 클래스의 서브클래스입니다. @@ -36,11 +36,11 @@ /// tip | 팁 -주목할 점은 타입, 기본 값 및 `Field`로 이루어진 각 모델 어트리뷰트가 `Path`, `Query`와 `Body`대신 `Field`를 사용하는 *경로 작동 함수*의 매개변수와 같은 구조를 가진다는 점 입니다. +주목할 점은 타입, 기본 값 및 `Field`로 이루어진 각 모델 어트리뷰트가 `Path`, `Query`와 `Body`대신 `Field`를 사용하는 *경로 처리 함수*의 매개변수와 같은 구조를 가진다는 점 입니다. /// -## 별도 정보 추가 +## 별도 정보 추가 { #add-extra-information } `Field`, `Query`, `Body`, 그 외 안에 별도 정보를 선언할 수 있습니다. 이는 생성된 JSON 스키마에 포함됩니다. @@ -53,7 +53,7 @@ /// -## 요약 +## 요약 { #recap } 모델 어트리뷰트를 위한 추가 검증과 메타데이터 선언하기 위해 Pydantic의 `Field` 를 사용할 수 있습니다. diff --git a/docs/ko/docs/tutorial/body-multiple-params.md b/docs/ko/docs/tutorial/body-multiple-params.md index edf892dfab..701351e637 100644 --- a/docs/ko/docs/tutorial/body-multiple-params.md +++ b/docs/ko/docs/tutorial/body-multiple-params.md @@ -1,26 +1,24 @@ -# 본문 - 다중 매개변수 +# 본문 - 다중 매개변수 { #body-multiple-parameters } -지금부터 `Path`와 `Query`를 어떻게 사용하는지 확인하겠습니다. +이제 `Path`와 `Query`를 어떻게 사용하는지 확인했으니, 요청 본문 선언에 대한 더 고급 사용법을 살펴보겠습니다. -요청 본문 선언에 대한 심화 사용법을 알아보겠습니다. +## `Path`, `Query` 및 본문 매개변수 혼합 { #mix-path-query-and-body-parameters } -## `Path`, `Query` 및 본문 매개변수 혼합 +먼저, 물론 `Path`, `Query` 및 요청 본문 매개변수 선언을 자유롭게 혼합해서 사용할 수 있고, **FastAPI**는 어떤 동작을 할지 압니다. -당연하게 `Path`, `Query` 및 요청 본문 매개변수 선언을 자유롭게 혼합해서 사용할 수 있고, **FastAPI**는 어떤 동작을 할지 압니다. +또한 기본 값을 `None`으로 설정해 본문 매개변수를 선택사항으로 선언할 수 있습니다: -또한, 기본 값을 `None`으로 설정해 본문 매개변수를 선택사항으로 선언할 수 있습니다. - -{* ../../docs_src/body_multiple_params/tutorial001.py hl[19:21] *} +{* ../../docs_src/body_multiple_params/tutorial001_an_py310.py hl[18:20] *} /// note | 참고 -이 경우에는 본문으로 부터 가져온 ` item`은 기본값이 `None`이기 때문에, 선택사항이라는 점을 유의해야 합니다. +이 경우에는 본문에서 가져올 `item`이 선택사항이라는 점을 유의하세요. 기본값이 `None`이기 때문입니다. /// -## 다중 본문 매개변수 +## 다중 본문 매개변수 { #multiple-body-parameters } -이전 예제에서 보듯이, *경로 작동*은 아래와 같이 `Item` 속성을 가진 JSON 본문을 예상합니다: +이전 예제에서, *경로 처리*는 아래처럼 `Item`의 속성을 가진 JSON 본문을 예상합니다: ```JSON { @@ -33,11 +31,12 @@ 하지만, 다중 본문 매개변수 역시 선언할 수 있습니다. 예. `item`과 `user`: -{* ../../docs_src/body_multiple_params/tutorial002.py hl[22] *} +{* ../../docs_src/body_multiple_params/tutorial002_py310.py hl[20] *} -이 경우에, **FastAPI**는 이 함수 안에 한 개 이상의 본문 매개변수(Pydantic 모델인 두 매개변수)가 있다고 알 것입니다. -그래서, 본문의 매개변수 이름을 키(필드 명)로 사용할 수 있고, 다음과 같은 본문을 예측합니다: +이 경우에, **FastAPI**는 이 함수에 본문 매개변수가 1개보다 많다는 것을 알아챌 것입니다(두 매개변수가 Pydantic 모델입니다). + +그래서, 본문에서 매개변수 이름을 키(필드 이름)로 사용하고, 다음과 같은 본문을 예상합니다: ```JSON { @@ -56,29 +55,28 @@ /// note | 참고 -이전과 같이 `item`이 선언 되었더라도, 본문 내의 `item` 키가 있을 것이라고 예측합니다. +`item`이 이전과 같은 방식으로 선언되었더라도, 이제는 본문에서 `item` 키 안에 있을 것으로 예상된다는 점을 유의하세요. /// -FastAPI는 요청을 자동으로 변환해, 매개변수의 `item`과 `user`를 특별한 내용으로 받도록 할 것입니다. +**FastAPI**는 요청에서 자동으로 변환을 수행하여, 매개변수 `item`이 해당하는 내용을 받고 `user`도 마찬가지로 받도록 합니다. -복합 데이터의 검증을 수행하고 OpenAPI 스키마 및 자동 문서를 문서화합니다. +복합 데이터의 검증을 수행하고, OpenAPI 스키마 및 자동 문서에도 그에 맞게 문서화합니다. -## 본문 내의 단일 값 +## 본문 내의 단일 값 { #singular-values-in-body } -쿼리 및 경로 매개변수에 대한 추가 데이터를 정의하는 `Query`와 `Path`와 같이, **FastAPI**는 동등한 `Body`를 제공합니다. +쿼리 및 경로 매개변수에 대한 추가 데이터를 정의하는 `Query`와 `Path`가 있는 것과 같은 방식으로, **FastAPI**는 동등한 `Body`를 제공합니다. -예를 들어 이전의 모델을 확장하면, `item`과 `user`와 동일한 본문에 또 다른 `importance`라는 키를 갖도록 할 수있습니다. +예를 들어 이전 모델을 확장해서, `item`과 `user` 외에도 같은 본문에 `importance`라는 다른 키를 두고 싶을 수 있습니다. -단일 값을 그대로 선언한다면, **FastAPI**는 쿼리 매개변수로 가정할 것입니다. +단일 값이므로 그대로 선언하면, **FastAPI**는 이를 쿼리 매개변수라고 가정할 것입니다. -하지만, **FastAPI**의 `Body`를 사용해 다른 본문 키로 처리하도록 제어할 수 있습니다: +하지만 `Body`를 사용하여 다른 본문 키로 처리하도록 **FastAPI**에 지시할 수 있습니다: + +{* ../../docs_src/body_multiple_params/tutorial003_an_py310.py hl[23] *} -{* ../../docs_src/body_multiple_params/tutorial003.py hl[23] *} - -이 경우에는 **FastAPI**는 본문을 이와 같이 예측할 것입니다: - +이 경우에는 **FastAPI**가 다음과 같은 본문을 예상할 것입니다: ```JSON { @@ -96,58 +94,55 @@ FastAPI는 요청을 자동으로 변환해, 매개변수의 `item`과 `user`를 } ``` -다시 말해, 데이터 타입, 검증, 문서 등을 변환합니다. +다시 말해, 데이터 타입을 변환하고, 검증하고, 문서화하는 등의 작업을 수행합니다. -## 다중 본문 매개변수와 쿼리 +## 다중 본문 매개변수와 쿼리 { #multiple-body-params-and-query } -당연히, 필요할 때마다 추가적인 쿼리 매개변수를 선언할 수 있고, 이는 본문 매개변수에 추가됩니다. +물론, 필요할 때마다 어떤 본문 매개변수에 추가로 쿼리 매개변수도 선언할 수 있습니다. -기본적으로 단일 값은 쿼리 매개변수로 해석되므로, 명시적으로 `Query`를 추가할 필요가 없고, 아래처럼 할 수 있습니다: - -{* ../../docs_src/body_multiple_params/tutorial004.py hl[27] *} - -이렇게: +기본적으로 단일 값은 쿼리 매개변수로 해석되므로, 명시적으로 `Query`를 추가할 필요 없이 이렇게 하면 됩니다: ```Python -q: Optional[str] = None +q: Union[str, None] = None ``` +또는 Python 3.10 이상에서는: + +```Python +q: str | None = None +``` + +예를 들어: + +{* ../../docs_src/body_multiple_params/tutorial004_an_py310.py hl[28] *} + + /// info | 정보 -`Body` 또한 `Query`, `Path` 그리고 이후에 볼 다른 것들처럼 동일한 추가 검증과 메타데이터 매개변수를 갖고 있습니다. +`Body` 또한 `Query`, `Path` 그리고 이후에 볼 다른 것들과 마찬가지로 동일한 추가 검증과 메타데이터 매개변수를 모두 갖고 있습니다. /// -## 단일 본문 매개변수 삽입하기 +## 단일 본문 매개변수 삽입하기 { #embed-a-single-body-parameter } -Pydantic 모델 `Item`의 `item`을 본문 매개변수로 오직 한개만 갖고있다고 하겠습니다. +Pydantic 모델 `Item`에서 가져온 단일 `item` 본문 매개변수만 있다고 하겠습니다. -기본적으로 **FastAPI**는 직접 본문으로 예측할 것입니다. +기본적으로 **FastAPI**는 그 본문을 직접 예상합니다. -하지만, 만약 모델 내용에 `item `키를 가진 JSON으로 예측하길 원한다면, 추가적인 본문 매개변수를 선언한 것처럼 `Body`의 특별한 매개변수인 `embed`를 사용할 수 있습니다: - -{* ../../docs_src/body_multiple_params/tutorial005.py hl[17] *} - -아래 처럼: +하지만 추가 본문 매개변수를 선언할 때처럼, `item` 키를 가지고 그 안에 모델 내용이 들어 있는 JSON을 예상하게 하려면, `Body`의 특별한 매개변수 `embed`를 사용할 수 있습니다: ```Python -item: Item = Body(..., embed=True) +item: Item = Body(embed=True) ``` -이 경우에 **FastAPI**는 본문을 아래 대신에: +다음과 같이요: + +{* ../../docs_src/body_multiple_params/tutorial005_an_py310.py hl[17] *} + + +이 경우 **FastAPI**는 다음과 같은 본문을 예상합니다: ```JSON hl_lines="2" -{ - "name": "Foo", - "description": "The pretender", - "price": 42.0, - "tax": 3.2 -} -``` - -아래 처럼 예측할 것 입니다: - -```JSON { "item": { "name": "Foo", @@ -158,12 +153,23 @@ item: Item = Body(..., embed=True) } ``` -## 정리 +다음 대신에: -요청이 단 한개의 본문을 가지고 있더라도, *경로 작동 함수*로 다중 본문 매개변수를 추가할 수 있습니다. +```JSON +{ + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2 +} +``` -하지만, **FastAPI**는 이를 처리하고, 함수에 올바른 데이터를 제공하며, *경로 작동*으로 올바른 스키마를 검증하고 문서화 합니다. +## 정리 { #recap } -또한, 단일 값을 본문의 일부로 받도록 선언할 수 있습니다. +요청은 본문을 하나만 가질 수 있지만, *경로 처리 함수*에 다중 본문 매개변수를 추가할 수 있습니다. -그리고 **FastAPI**는 단 한개의 매개변수가 선언 되더라도, 본문 내의 키로 삽입 시킬 수 있습니다. +하지만 **FastAPI**는 이를 처리하고, 함수에 올바른 데이터를 제공하며, *경로 처리*에서 올바른 스키마를 검증하고 문서화합니다. + +또한 단일 값을 본문의 일부로 받도록 선언할 수 있습니다. + +그리고 단 하나의 매개변수만 선언되어 있더라도, **FastAPI**에 본문을 키 안에 삽입하도록 지시할 수 있습니다. diff --git a/docs/ko/docs/tutorial/body-nested-models.md b/docs/ko/docs/tutorial/body-nested-models.md index ebd7b3ba6a..4a8c1afc13 100644 --- a/docs/ko/docs/tutorial/body-nested-models.md +++ b/docs/ko/docs/tutorial/body-nested-models.md @@ -1,35 +1,26 @@ -# 본문 - 중첩 모델 +# 본문 - 중첩 모델 { #body-nested-models } -**FastAPI**를 이용하면 (Pydantic 덕분에) 단독으로 깊이 중첩된 모델을 정의, 검증, 문서화하며 사용할 수 있습니다. -## 리스트 필드 +**FastAPI**를 사용하면 (Pydantic 덕분에) 임의로 깊게 중첩된 모델을 정의, 검증, 문서화하고 사용할 수 있습니다. + +## 리스트 필드 { #list-fields } 어트리뷰트를 서브타입으로 정의할 수 있습니다. 예를 들어 파이썬 `list`는: -{* ../../docs_src/body_nested_models/tutorial001.py hl[14] *} +{* ../../docs_src/body_nested_models/tutorial001_py310.py hl[12] *} -이는 `tags`를 항목 리스트로 만듭니다. 각 항목의 타입을 선언하지 않더라도요. +이는 `tags`를 리스트로 만들지만, 리스트 요소의 타입을 선언하지는 않습니다. -## 타입 매개변수가 있는 리스트 필드 +## 타입 매개변수가 있는 리스트 필드 { #list-fields-with-type-parameter } -하지만 파이썬은 내부의 타입이나 "타입 매개변수"를 선언할 수 있는 특정 방법이 있습니다: +하지만 파이썬에는 내부 타입, 즉 "타입 매개변수"를 사용해 리스트를 선언하는 특정한 방법이 있습니다: -### typing의 `List` 임포트 +### 타입 매개변수로 `list` 선언 { #declare-a-list-with-a-type-parameter } -먼저, 파이썬 표준 `typing` 모듈에서 `List`를 임포트합니다: - -{* ../../docs_src/body_nested_models/tutorial002.py hl[1] *} - -### 타입 매개변수로 `List` 선언 - -`list`, `dict`, `tuple`과 같은 타입 매개변수(내부 타입)를 갖는 타입을 선언하려면: - -* `typing` 모듈에서 임포트 -* 대괄호를 사용하여 "타입 매개변수"로 내부 타입 전달: `[` 및 `]` +`list`, `dict`, `tuple`처럼 타입 매개변수(내부 타입)를 갖는 타입을 선언하려면, +대괄호 `[` 및 `]`를 사용해 내부 타입(들)을 "타입 매개변수"로 전달하세요. ```Python -from typing import List - -my_list: List[str] +my_list: list[str] ``` 이 모든 것은 타입 선언을 위한 표준 파이썬 문법입니다. @@ -38,45 +29,45 @@ my_list: List[str] 마찬가지로 예제에서 `tags`를 구체적으로 "문자열의 리스트"로 만들 수 있습니다: -{* ../../docs_src/body_nested_models/tutorial002.py hl[14] *} +{* ../../docs_src/body_nested_models/tutorial002_py310.py hl[12] *} -## 집합 타입 +## 집합 타입 { #set-types } -그런데 생각해보니 태그는 반복되면 안 되고, 고유한(Unique) 문자열이어야 할 것 같습니다. +그런데 생각해보니 태그는 반복되면 안 되고, 아마 고유한 문자열이어야 할 것입니다. -그리고 파이썬은 집합을 위한 특별한 데이터 타입 `set`이 있습니다. +그리고 파이썬에는 고유한 항목들의 집합을 위한 특별한 데이터 타입 `set`이 있습니다. -그렇다면 `Set`을 임포트 하고 `tags`를 `str`의 `set`으로 선언할 수 있습니다: +그렇다면 `tags`를 문자열의 집합으로 선언할 수 있습니다: -{* ../../docs_src/body_nested_models/tutorial003.py hl[1,14] *} +{* ../../docs_src/body_nested_models/tutorial003_py310.py hl[12] *} -덕분에 중복 데이터가 있는 요청을 수신하더라도 고유한 항목들의 집합으로 변환됩니다. +이렇게 하면 중복 데이터가 있는 요청을 받더라도 고유한 항목들의 집합으로 변환됩니다. -그리고 해당 데이터를 출력 할 때마다 소스에 중복이 있더라도 고유한 항목들의 집합으로 출력됩니다. +그리고 해당 데이터를 출력할 때마다, 소스에 중복이 있더라도 고유한 항목들의 집합으로 출력됩니다. 또한 그에 따라 주석이 생기고 문서화됩니다. -## 중첩 모델 +## 중첩 모델 { #nested-models } Pydantic 모델의 각 어트리뷰트는 타입을 갖습니다. -그런데 해당 타입 자체로 또다른 Pydantic 모델의 타입이 될 수 있습니다. +그런데 그 타입 자체가 또 다른 Pydantic 모델일 수 있습니다. -그러므로 특정한 어트리뷰트의 이름, 타입, 검증을 사용하여 깊게 중첩된 JSON "객체"를 선언할 수 있습니다. +따라서 특정한 어트리뷰트 이름, 타입, 검증을 사용하여 깊게 중첩된 JSON "객체"를 선언할 수 있습니다. -모든 것이 단독으로 중첩됩니다. +모든 것이 임의의 깊이로 중첩됩니다. -### 서브모델 정의 +### 서브모델 정의 { #define-a-submodel } -예를 들어, `Image` 모델을 선언할 수 있습니다: +예를 들어, `Image` 모델을 정의할 수 있습니다: -{* ../../docs_src/body_nested_models/tutorial004.py hl[9:11] *} +{* ../../docs_src/body_nested_models/tutorial004_py310.py hl[7:9] *} -### 서브모듈을 타입으로 사용 +### 서브모델을 타입으로 사용 { #use-the-submodel-as-a-type } -그리고 어트리뷰트의 타입으로 사용할 수 있습니다: +그리고 이를 어트리뷰트의 타입으로 사용할 수 있습니다: -{* ../../docs_src/body_nested_models/tutorial004.py hl[20] *} +{* ../../docs_src/body_nested_models/tutorial004_py310.py hl[18] *} 이는 **FastAPI**가 다음과 유사한 본문을 기대한다는 것을 의미합니다: @@ -94,32 +85,32 @@ Pydantic 모델의 각 어트리뷰트는 타입을 갖습니다. } ``` -다시 한번, **FastAPI**를 사용하여 해당 선언을 함으로써 얻는 것은: +다시 한번, **FastAPI**로 그 선언만 해도 얻는 것은: * 중첩 모델도 편집기 지원(자동완성 등) * 데이터 변환 * 데이터 검증 * 자동 문서화 -## 특별한 타입과 검증 +## 특별한 타입과 검증 { #special-types-and-validation } -`str`, `int`, `float` 등과 같은 단일 타입과는 별개로, `str`을 상속하는 더 복잡한 단일 타입을 사용할 수 있습니다. +`str`, `int`, `float` 등과 같은 일반적인 단일 타입과는 별개로, `str`을 상속하는 더 복잡한 단일 타입을 사용할 수 있습니다. -모든 옵션을 보려면, Pydantic's exotic types 문서를 확인하세요. 다음 장에서 몇가지 예제를 볼 수 있습니다. +사용할 수 있는 모든 옵션을 보려면 Pydantic의 Type Overview를 확인하세요. 다음 장에서 몇 가지 예제를 볼 수 있습니다. -예를 들어 `Image` 모델 안에 `url` 필드를 `str` 대신 Pydantic의 `HttpUrl`로 선언할 수 있습니다: +예를 들어 `Image` 모델에는 `url` 필드가 있으므로, 이를 `str` 대신 Pydantic의 `HttpUrl` 인스턴스로 선언할 수 있습니다: -{* ../../docs_src/body_nested_models/tutorial005.py hl[4,10] *} +{* ../../docs_src/body_nested_models/tutorial005_py310.py hl[2,8] *} -이 문자열이 유효한 URL인지 검사하고 JSON 스키마/OpenAPI로 문서화 됩니다. +이 문자열은 유효한 URL인지 검사되며, JSON Schema / OpenAPI에도 그에 맞게 문서화됩니다. -## 서브모델 리스트를 갖는 어트리뷰트 +## 서브모델 리스트를 갖는 어트리뷰트 { #attributes-with-lists-of-submodels } `list`, `set` 등의 서브타입으로 Pydantic 모델을 사용할 수도 있습니다: -{* ../../docs_src/body_nested_models/tutorial006.py hl[20] *} +{* ../../docs_src/body_nested_models/tutorial006_py310.py hl[18] *} -아래와 같은 JSON 본문으로 예상(변환, 검증, 문서화 등을)합니다: +아래와 같은 JSON 본문을 예상(변환, 검증, 문서화 등)합니다: ```JSON hl_lines="11" { @@ -147,84 +138,84 @@ Pydantic 모델의 각 어트리뷰트는 타입을 갖습니다. /// info | 정보 -`images` 키가 어떻게 이미지 객체 리스트를 갖는지 주목하세요. +`images` 키가 이제 이미지 객체 리스트를 갖는지 주목하세요. /// -## 깊게 중첩된 모델 +## 깊게 중첩된 모델 { #deeply-nested-models } -단독으로 깊게 중첩된 모델을 정의할 수 있습니다: +임의로 깊게 중첩된 모델을 정의할 수 있습니다: -{* ../../docs_src/body_nested_models/tutorial007.py hl[9,14,20,23,27] *} +{* ../../docs_src/body_nested_models/tutorial007_py310.py hl[7,12,18,21,25] *} /// info | 정보 -`Offer`가 선택사항 `Image` 리스트를 차례로 갖는 `Item` 리스트를 어떻게 가지고 있는지 주목하세요 +`Offer`가 `Item`의 리스트를 가지고, 그 `Item`이 다시 선택 사항인 `Image` 리스트를 갖는지 주목하세요 /// -## 순수 리스트의 본문 +## 순수 리스트의 본문 { #bodies-of-pure-lists } -예상되는 JSON 본문의 최상위 값이 JSON `array`(파이썬 `list`)면, Pydantic 모델에서와 마찬가지로 함수의 매개변수에서 타입을 선언할 수 있습니다: +예상되는 JSON 본문의 최상위 값이 JSON `array`(파이썬 `list`)라면, Pydantic 모델에서와 마찬가지로 함수의 매개변수에서 타입을 선언할 수 있습니다: ```Python -images: List[Image] +images: list[Image] ``` 이를 아래처럼: -{* ../../docs_src/body_nested_models/tutorial008.py hl[15] *} +{* ../../docs_src/body_nested_models/tutorial008_py39.py hl[13] *} -## 어디서나 편집기 지원 +## 어디서나 편집기 지원 { #editor-support-everywhere } -그리고 어디서나 편집기 지원을 받을수 있습니다. +그리고 어디서나 편집기 지원을 받을 수 있습니다. 리스트 내부 항목의 경우에도: -Pydantic 모델 대신에 `dict`를 직접 사용하여 작업할 경우, 이러한 편집기 지원을 받을수 없습니다. +Pydantic 모델 대신 `dict`로 직접 작업한다면 이런 종류의 편집기 지원을 받을 수 없습니다. -하지만 수신한 딕셔너리가 자동으로 변환되고 출력도 자동으로 JSON으로 변환되므로 걱정할 필요는 없습니다. +하지만 그 부분에 대해서도 걱정할 필요는 없습니다. 들어오는 dict는 자동으로 변환되고, 출력도 자동으로 JSON으로 변환됩니다. -## 단독 `dict`의 본문 +## 임의의 `dict` 본문 { #bodies-of-arbitrary-dicts } -일부 타입의 키와 다른 타입의 값을 사용하여 `dict`로 본문을 선언할 수 있습니다. +또한 키는 어떤 타입이고 값은 다른 타입인 `dict`로 본문을 선언할 수 있습니다. -(Pydantic을 사용한 경우처럼) 유효한 필드/어트리뷰트 이름이 무엇인지 알 필요가 없습니다. +이렇게 하면 (Pydantic 모델을 사용하는 경우처럼) 유효한 필드/어트리뷰트 이름이 무엇인지 미리 알 필요가 없습니다. -아직 모르는 키를 받으려는 경우 유용합니다. +아직 모르는 키를 받으려는 경우에 유용합니다. --- -다른 유용한 경우는 다른 타입의 키를 가질 때입니다. 예. `int`. +또 다른 유용한 경우는 다른 타입(예: `int`)의 키를 갖고 싶을 때입니다. 여기서 그 경우를 볼 것입니다. -이 경우, `float` 값을 가진 `int` 키가 있는 모든 `dict`를 받아들입니다: +이 경우, `int` 키와 `float` 값을 가진 한 어떤 `dict`든 받아들입니다: -{* ../../docs_src/body_nested_models/tutorial009.py hl[15] *} +{* ../../docs_src/body_nested_models/tutorial009_py39.py hl[7] *} /// tip | 팁 -JSON은 오직 `str`형 키만 지원한다는 것을 염두에 두세요. +JSON은 키로 `str`만 지원한다는 것을 염두에 두세요. -하지만 Pydantic은 자동 데이터 변환이 있습니다. +하지만 Pydantic은 자동 데이터 변환 기능이 있습니다. -즉, API 클라이언트가 문자열을 키로 보내더라도 해당 문자열이 순수한 정수를 포함하는한 Pydantic은 이를 변환하고 검증합니다. +즉, API 클라이언트는 키로 문자열만 보낼 수 있더라도, 해당 문자열이 순수한 정수를 포함하기만 하면 Pydantic이 이를 변환하고 검증합니다. -그러므로 `weights`로 받은 `dict`는 실제로 `int` 키와 `float` 값을 가집니다. +그리고 `weights`로 받는 `dict`는 실제로 `int` 키와 `float` 값을 갖게 됩니다. /// -## 요약 +## 요약 { #recap } -**FastAPI**를 사용하면 Pydantic 모델이 제공하는 최대 유연성을 확보하면서 코드를 간단하고 짧게, 그리고 우아하게 유지할 수 있습니다. +**FastAPI**를 사용하면 Pydantic 모델이 제공하는 최대 유연성을 확보하면서 코드를 간단하고 짧고 우아하게 유지할 수 있습니다. -물론 아래의 이점도 있습니다: +하지만 아래의 모든 이점도 있습니다: -* 편집기 지원 (자동완성이 어디서나!) -* 데이터 변환 (일명 파싱/직렬화) +* 편집기 지원(어디서나 자동완성!) +* 데이터 변환(일명 파싱/직렬화) * 데이터 검증 * 스키마 문서화 * 자동 문서 diff --git a/docs/ko/docs/tutorial/body.md b/docs/ko/docs/tutorial/body.md index b3914fa4b4..1e66c60c22 100644 --- a/docs/ko/docs/tutorial/body.md +++ b/docs/ko/docs/tutorial/body.md @@ -1,10 +1,10 @@ -# 요청 본문 +# 요청 본문 { #request-body } 클라이언트(브라우저라고 해봅시다)로부터 여러분의 API로 데이터를 보내야 할 때, **요청 본문**으로 보냅니다. **요청** 본문은 클라이언트에서 API로 보내지는 데이터입니다. **응답** 본문은 API가 클라이언트로 보내는 데이터입니다. -여러분의 API는 대부분의 경우 **응답** 본문을 보내야 합니다. 하지만 클라이언트는 **요청** 본문을 매 번 보낼 필요가 없습니다. +여러분의 API는 대부분의 경우 **응답** 본문을 보내야 합니다. 하지만 클라이언트는 항상 **요청 본문**을 보낼 필요는 없고, 때로는 (쿼리 매개변수와 함께) 어떤 경로만 요청하고 본문은 보내지 않을 수도 있습니다. **요청** 본문을 선언하기 위해서 모든 강력함과 이점을 갖춘 Pydantic 모델을 사용합니다. @@ -18,13 +18,13 @@ /// -## Pydantic의 `BaseModel` 임포트 +## Pydantic의 `BaseModel` 임포트 { #import-pydantics-basemodel } 먼저 `pydantic`에서 `BaseModel`를 임포트해야 합니다: {* ../../docs_src/body/tutorial001_py310.py hl[2] *} -## 여러분의 데이터 모델 만들기 +## 여러분의 데이터 모델 만들기 { #create-your-data-model } `BaseModel`를 상속받은 클래스로 여러분의 데이터 모델을 선언합니다. @@ -32,6 +32,7 @@ {* ../../docs_src/body/tutorial001_py310.py hl[5:9] *} + 쿼리 매개변수를 선언할 때와 같이, 모델 어트리뷰트가 기본 값을 가지고 있어도 이는 필수가 아닙니다. 그외에는 필수입니다. 그저 `None`을 사용하여 선택적으로 만들 수 있습니다. 예를 들면, 위의 이 모델은 JSON "`object`" (혹은 파이썬 `dict`)을 다음과 같이 선언합니다: @@ -39,7 +40,7 @@ ```JSON { "name": "Foo", - "description": "선택적인 설명란", + "description": "An optional description", "price": 45.2, "tax": 3.5 } @@ -54,15 +55,15 @@ } ``` -## 매개변수로서 선언하기 +## 매개변수로서 선언하기 { #declare-it-as-a-parameter } -여러분의 *경로 작동*에 추가하기 위해, 경로 매개변수 그리고 쿼리 매개변수에서 선언했던 것과 같은 방식으로 선언하면 됩니다. +여러분의 *경로 처리*에 추가하기 위해, 경로 매개변수 그리고 쿼리 매개변수에서 선언했던 것과 같은 방식으로 선언하면 됩니다. {* ../../docs_src/body/tutorial001_py310.py hl[16] *} ...그리고 만들어낸 모델인 `Item`으로 타입을 선언합니다. -## 결과 +## 결과 { #results } 위에서의 단순한 파이썬 타입 선언으로, **FastAPI**는 다음과 같이 동작합니다: @@ -72,20 +73,20 @@ * 만약 데이터가 유효하지 않다면, 정확히 어떤 것이 그리고 어디에서 데이터가 잘 못 되었는지 지시하는 친절하고 명료한 에러를 반환할 것입니다. * 매개변수 `item`에 포함된 수신 데이터를 제공합니다. * 함수 내에서 매개변수를 `Item` 타입으로 선언했기 때문에, 모든 어트리뷰트와 그에 대한 타입에 대한 편집기 지원(완성 등)을 또한 받을 수 있습니다. -* 여러분의 모델을 위한 JSON 스키마 정의를 생성합니다. 여러분의 프로젝트에 적합하다면 여러분이 사용하고 싶은 곳 어디에서나 사용할 수 있습니다. -* 이러한 스키마는, 생성된 OpenAPI 스키마 일부가 될 것이며, 자동 문서화 UI에 사용됩니다. +* 여러분의 모델을 위한 JSON Schema 정의를 생성합니다. 여러분의 프로젝트에 적합하다면 여러분이 사용하고 싶은 곳 어디에서나 사용할 수 있습니다. +* 이러한 스키마는, 생성된 OpenAPI 스키마 일부가 될 것이며, 자동 문서화 UIs에 사용됩니다. -## 자동 문서화 +## 자동 문서화 { #automatic-docs } 모델의 JSON 스키마는 생성된 OpenAPI 스키마에 포함되며 대화형 API 문서에 표시됩니다: -이를 필요로 하는 각각의 *경로 작동*내부의 API 문서에도 사용됩니다: +이를 필요로 하는 각각의 *경로 처리* 내부의 API 문서에도 사용됩니다: -## 편집기 지원 +## 편집기 지원 { #editor-support } 편집기에서, 함수 내에서 타입 힌트와 완성을 어디서나 (만약 Pydantic model 대신에 `dict`을 받을 경우 나타나지 않을 수 있습니다) 받을 수 있습니다: @@ -97,13 +98,13 @@ 단순한 우연이 아닙니다. 프레임워크 전체가 이러한 디자인을 중심으로 설계되었습니다. -그 어떤 실행 전에, 모든 편집기에서 작동할 수 있도록 보장하기 위해 설계 단계에서 혹독하게 테스트되었습니다. +그 어떤 구현 전에, 모든 편집기에서 작동할 수 있도록 보장하기 위해 설계 단계에서 혹독하게 테스트되었습니다. 이를 지원하기 위해 Pydantic 자체에서 몇몇 변경점이 있었습니다. 이전 스크린샷은 Visual Studio Code를 찍은 것입니다. -하지만 똑같은 편집기 지원을 PyCharm에서 받을 수 있거나, 대부분의 다른 편집기에서도 받을 수 있습니다: +하지만 똑같은 편집기 지원을 PyCharm와 대부분의 다른 파이썬 편집기에서도 받을 수 있습니다: @@ -113,21 +114,21 @@ 다음 사항을 포함해 Pydantic 모델에 대한 편집기 지원을 향상시킵니다: -* 자동 완성 -* 타입 확인 -* 리팩토링 -* 검색 -* 점검 +* auto-completion +* type checks +* refactoring +* searching +* inspections /// -## 모델 사용하기 +## 모델 사용하기 { #use-the-model } 함수 안에서 모델 객체의 모든 어트리뷰트에 직접 접근 가능합니다: -{* ../../docs_src/body/tutorial002_py310.py hl[19] *} +{* ../../docs_src/body/tutorial002_py310.py *} -## 요청 본문 + 경로 매개변수 +## 요청 본문 + 경로 매개변수 { #request-body-path-parameters } 경로 매개변수와 요청 본문을 동시에 선언할 수 있습니다. @@ -135,7 +136,8 @@ {* ../../docs_src/body/tutorial003_py310.py hl[15:16] *} -## 요청 본문 + 경로 + 쿼리 매개변수 + +## 요청 본문 + 경로 + 쿼리 매개변수 { #request-body-path-query-parameters } **본문**, **경로** 그리고 **쿼리** 매개변수 모두 동시에 선언할 수도 있습니다. @@ -153,10 +155,12 @@ FastAPI는 `q`의 값이 필요없음을 알게 될 것입니다. 기본 값이 `= None`이기 때문입니다. -`Union[str, None]`에 있는 `Union`은 FastAPI에 의해 사용된 것이 아니지만, 편집기로 하여금 더 나은 지원과 에러 탐지를 지원할 것입니다. +Python 3.10+의 `str | None` 또는 Python 3.9+의 `Union[str, None]`에 있는 `Union`은 FastAPI가 `q` 값이 필수가 아님을 판단하기 위해 사용하지 않습니다. 기본 값이 `= None`이기 때문에 필수가 아님을 알게 됩니다. + +하지만 타입 어노테이션을 추가하면 편집기가 더 나은 지원을 제공하고 오류를 감지할 수 있습니다. /// -## Pydantic없이 +## Pydantic없이 { #without-pydantic } -만약 Pydantic 모델을 사용하고 싶지 않다면, **Body** 매개변수를 사용할 수도 있습니다. [Body - 다중 매개변수: 본문에 있는 유일한 값](body-multiple-params.md#_2){.internal-link target=_blank} 문서를 확인하세요. +만약 Pydantic 모델을 사용하고 싶지 않다면, **Body** 매개변수를 사용할 수도 있습니다. [Body - Multiple Parameters: Singular values in body](body-multiple-params.md#singular-values-in-body){.internal-link target=_blank} 문서를 확인하세요. diff --git a/docs/ko/docs/tutorial/cookie-param-models.md b/docs/ko/docs/tutorial/cookie-param-models.md index e7eef0b1de..00238d1b79 100644 --- a/docs/ko/docs/tutorial/cookie-param-models.md +++ b/docs/ko/docs/tutorial/cookie-param-models.md @@ -1,8 +1,8 @@ -# 쿠키 매개변수 모델 +# 쿠키 매개변수 모델 { #cookie-parameter-models } 관련있는 **쿠키**들의 그룹이 있는 경우, **Pydantic 모델**을 생성하여 선언할 수 있습니다. 🍪 -이를 통해 **여러 위치**에서 **모델을 재사용** 할 수 있고 모든 매개변수에 대한 유효성 검사 및 메타데이터를 한 번에 선언할 수도 있습니다. 😍 +이를 통해 **여러 위치**에서 **모델을 재사용** 할 수 있고 모든 매개변수에 대한 유효성 검사 및 메타데이터를 한 번에 선언할 수도 있습니다. 😎 /// note | 참고 @@ -16,7 +16,7 @@ /// -## Pydantic 모델을 사용한 쿠키 +## Pydantic 모델을 사용한 쿠키 { #cookies-with-a-pydantic-model } **Pydantic 모델**에 필요한 **쿠키** 매개변수를 선언한 다음, 해당 매개변수를 `Cookie`로 선언합니다: @@ -24,7 +24,7 @@ **FastAPI**는 요청에서 받은 **쿠키**에서 **각 필드**에 대한 데이터를 **추출**하고 정의한 Pydantic 모델을 줍니다. -## 문서 확인하기 +## 문서 확인하기 { #check-the-docs } 문서 UI `/docs`에서 정의한 쿠키를 볼 수 있습니다: @@ -36,27 +36,27 @@ 명심하세요, 내부적으로 **브라우저는 쿠키를 특별한 방식으로 처리**하기 때문에 **자바스크립트**가 쉽게 쿠키를 건드릴 수 **없습니다**. -`/docs`에서 **API 문서 UI**로 이동하면 *경로 작업*에 대한 쿠키의 **문서**를 볼 수 있습니다. +`/docs`에서 **API 문서 UI**로 이동하면 *경로 처리*에 대한 쿠키의 **문서**를 볼 수 있습니다. 하지만 아무리 **데이터를 입력**하고 "실행(Execute)"을 클릭해도, 문서 UI는 **자바스크립트**로 작동하기 때문에 쿠키는 전송되지 않고, 아무 값도 쓰지 않은 것처럼 **오류** 메시지를 보게 됩니다. /// -## 추가 쿠키 금지하기 +## 추가 쿠키 금지하기 { #forbid-extra-cookies } 일부 특별한 사용 사례(흔하지는 않겠지만)에서는 수신하려는 쿠키를 **제한**할 수 있습니다. -이제 API는 자신의 쿠키 동의를 제어할 수 있는 권한을 갖게 되었습니다. 🤪🍪 +이제 API는 자신의 cookie consent를 제어할 수 있는 권한을 갖게 되었습니다. 🤪🍪 Pydantic의 모델 구성을 사용하여 추가(`extra`) 필드를 금지(`forbid`)할 수 있습니다: -{* ../../docs_src/cookie_param_models/tutorial002_an_py39.py hl[10] *} +{* ../../docs_src/cookie_param_models/tutorial002_an_py310.py hl[10] *} 클라이언트가 **추가 쿠키**를 보내려고 시도하면, **오류** 응답을 받게 됩니다. -API가 거부하는데도 동의를 얻기 위해 애쓰는 불쌍한 쿠키 배너(팝업)들. 🍪 +동의를 얻기 위해 애쓰는 불쌍한 쿠키 배너(팝업)들, API가 거부하는데도. 🍪 -예를 들어, 클라이언트가 `good-list-please` 값으로 `santa_tracker` 쿠키를 보내려고 하면 클라이언트는 `santa_tracker` 쿠키가 허용되지 않는다는 **오류** 응답을 받게 됩니다: +예를 들어, 클라이언트가 `good-list-please` 값으로 `santa_tracker` 쿠키를 보내려고 하면 클라이언트는 `santa_tracker` 쿠키가 허용되지 않는다는 **오류** 응답을 받게 됩니다: ```json { @@ -71,6 +71,6 @@ Pydantic의 모델 구성을 사용하여 추가(`extra`) 필드를 금지(`forb } ``` -## 요약 +## 요약 { #summary } -**Pydantic 모델**을 사용하여 **FastAPI**에서 **쿠키**를 선언할 수 있습니다. 😍 +**Pydantic 모델**을 사용하여 **FastAPI**에서 **쿠키**를 선언할 수 있습니다. 😎 diff --git a/docs/ko/docs/tutorial/cookie-params.md b/docs/ko/docs/tutorial/cookie-params.md index fba756d498..0591a5e96b 100644 --- a/docs/ko/docs/tutorial/cookie-params.md +++ b/docs/ko/docs/tutorial/cookie-params.md @@ -1,14 +1,14 @@ -# 쿠키 매개변수 +# 쿠키 매개변수 { #cookie-parameters } 쿠키 매개변수를 `Query`와 `Path` 매개변수들과 같은 방식으로 정의할 수 있습니다. -## `Cookie` 임포트 +## `Cookie` 임포트 { #import-cookie } 먼저 `Cookie`를 임포트합니다: {* ../../docs_src/cookie_params/tutorial001_an_py310.py hl[3] *} -## `Cookie` 매개변수 선언 +## `Cookie` 매개변수 선언 { #declare-cookie-parameters } 그런 다음, `Path`와 `Query`처럼 동일한 구조를 사용하는 쿠키 매개변수를 선언합니다. @@ -20,7 +20,7 @@ `Cookie`는 `Path` 및 `Query`의 "자매"클래스입니다. 이 역시 동일한 공통 `Param` 클래스를 상속합니다. -`Query`, `Path`, `Cookie` 그리고 다른 것들은 `fastapi`에서 임포트 할 때, 실제로는 특별한 클래스를 반환하는 함수임을 기억하세요. +하지만 `fastapi`에서 `Query`, `Path`, `Cookie` 그리고 다른 것들을 임포트할 때, 실제로는 특별한 클래스를 반환하는 함수임을 기억하세요. /// @@ -30,6 +30,16 @@ /// -## 요약 +/// info | 정보 -`Cookie`는 `Query`, `Path`와 동일한 패턴을 사용하여 선언합니다. +**브라우저는 쿠키를** 내부적으로 특별한 방식으로 처리하기 때문에, **JavaScript**가 쉽게 쿠키를 다루도록 허용하지 않는다는 점을 염두에 두세요. + +`/docs`의 **API docs UI**로 이동하면 *경로 처리*에 대한 쿠키 **문서**를 확인할 수 있습니다. + +하지만 **데이터를 채우고** "Execute"를 클릭하더라도, docs UI는 **JavaScript**로 동작하기 때문에 쿠키가 전송되지 않고, 아무 값도 입력하지 않은 것처럼 **오류** 메시지를 보게 될 것입니다. + +/// + +## 요약 { #recap } + +`Query`와 `Path`에서 사용하는 것과 동일한 공통 패턴으로, `Cookie`를 사용해 쿠키를 선언합니다. diff --git a/docs/ko/docs/tutorial/cors.md b/docs/ko/docs/tutorial/cors.md index 1ef5a74803..0f3948a3dc 100644 --- a/docs/ko/docs/tutorial/cors.md +++ b/docs/ko/docs/tutorial/cors.md @@ -1,10 +1,10 @@ -# 교차 출처 리소스 공유 +# CORS (교차-출처 리소스 공유) { #cors-cross-origin-resource-sharing } -CORS 또는 "교차-출처 리소스 공유"란, 브라우저에서 동작하는 프론트엔드가 자바스크립트로 코드로 백엔드와 통신하고, 백엔드는 해당 프론트엔드와 다른 "출처"에 존재하는 상황을 의미합니다. +CORS 또는 "Cross-Origin Resource Sharing"란, 브라우저에서 실행되는 프론트엔드에 백엔드와 통신하는 JavaScript 코드가 있고, 백엔드가 프론트엔드와 다른 "출처(origin)"에 있는 상황을 의미합니다. -## 출처 +## 출처 { #origin } -출처란 프로토콜(`http` , `https`), 도메인(`myapp.com`, `localhost`, `localhost.tiangolo.com` ), 그리고 포트(`80`, `443`, `8080` )의 조합을 의미합니다. +출처란 프로토콜(`http`, `https`), 도메인(`myapp.com`, `localhost`, `localhost.tiangolo.com`), 그리고 포트(`80`, `443`, `8080`)의 조합을 의미합니다. 따라서, 아래는 모두 상이한 출처입니다: @@ -12,74 +12,78 @@ * `https://localhost` * `http://localhost:8080` -모두 `localhost` 에 있지만, 서로 다른 프로토콜과 포트를 사용하고 있으므로 다른 "출처"입니다. +모두 `localhost`에 있더라도, 서로 다른 프로토콜이나 포트를 사용하므로 서로 다른 "출처"입니다. -## 단계 +## 단계 { #steps } -브라우저 내 `http://localhost:8080`에서 동작하는 프론트엔드가 있고, 자바스크립트는 `http://localhost`를 통해 백엔드와 통신한다고 가정해봅시다(포트를 명시하지 않는 경우, 브라우저는 `80` 을 기본 포트로 간주합니다). +그러면 브라우저에서 `http://localhost:8080`으로 실행되는 프론트엔드가 있고, 그 JavaScript가 `http://localhost`에서 실행되는 백엔드와 통신하려고 한다고 해봅시다(포트를 명시하지 않았기 때문에, 브라우저는 기본 포트 `80`을 가정합니다). -그러면 브라우저는 백엔드에 HTTP `OPTIONS` 요청을 보내고, 백엔드에서 이 다른 출처(`http://localhost:8080`)와의 통신을 허가하는 적절한 헤더를 보내면, 브라우저는 프론트엔드의 자바스크립트가 백엔드에 요청을 보낼 수 있도록 합니다. +그러면 브라우저는 `:80`-백엔드에 HTTP `OPTIONS` 요청을 보내고, 백엔드가 이 다른 출처(`http://localhost:8080`)로부터의 통신을 허가하는 적절한 헤더를 보내면, `:8080`-브라우저는 프론트엔드의 JavaScript가 `:80`-백엔드에 요청을 보낼 수 있도록 합니다. -이를 위해, 백엔드는 "허용된 출처(allowed origins)" 목록을 가지고 있어야만 합니다. +이를 위해, `:80`-백엔드는 "허용된 출처(allowed origins)" 목록을 가지고 있어야 합니다. -이 경우, 프론트엔드가 제대로 동작하기 위해 `http://localhost:8080`을 목록에 포함해야 합니다. +이 경우, `:8080`-프론트엔드가 올바르게 동작하려면 목록에 `http://localhost:8080`이 포함되어야 합니다. -## 와일드카드 +## 와일드카드 { #wildcards } -모든 출처를 허용하기 위해 목록을 `"*"` ("와일드카드")로 선언하는 것도 가능합니다. +또한 목록을 `"*"`("와일드카드")로 선언해 모두 허용된다고 말할 수도 있습니다. -하지만 이것은 특정한 유형의 통신만을 허용하며, 쿠키 및 액세스 토큰과 사용되는 인증 헤더(Authoriztion header) 등이 포함된 경우와 같이 자격 증명(credentials)이 포함된 통신은 허용되지 않습니다. +하지만 그러면 자격 증명(credentials)이 포함된 모든 것을 제외하고 특정 유형의 통신만 허용하게 됩니다. 예: 쿠키, Bearer Token에 사용되는 것과 같은 Authorization 헤더 등. -따라서 모든 작업을 의도한대로 실행하기 위해, 허용되는 출처를 명시적으로 지정하는 것이 좋습니다. +따라서 모든 것이 올바르게 동작하게 하려면, 허용된 출처를 명시적으로 지정하는 것이 더 좋습니다. -## `CORSMiddleware` 사용 +## `CORSMiddleware` 사용 { #use-corsmiddleware } -`CORSMiddleware` 을 사용하여 **FastAPI** 응용 프로그램의 교차 출처 리소스 공유 환경을 설정할 수 있습니다. +`CORSMiddleware`를 사용하여 **FastAPI** 애플리케이션에서 이를 설정할 수 있습니다. -* `CORSMiddleware` 임포트. -* 허용되는 출처(문자열 형식)의 리스트 생성. -* FastAPI 응용 프로그램에 "미들웨어(middleware)"로 추가. +* `CORSMiddleware`를 임포트합니다. +* 허용된 출처(문자열)의 리스트를 생성합니다. +* **FastAPI** 애플리케이션에 "미들웨어(middleware)"로 추가합니다. -백엔드에서 다음의 사항을 허용할지에 대해 설정할 수도 있습니다: +또한 백엔드가 다음을 허용할지 여부도 지정할 수 있습니다: -* 자격증명 (인증 헤더, 쿠키 등). -* 특정한 HTTP 메소드(`POST`, `PUT`) 또는 와일드카드 `"*"` 를 사용한 모든 HTTP 메소드. -* 특정한 HTTP 헤더 또는 와일드카드 `"*"` 를 사용한 모든 HTTP 헤더. +* 자격 증명(Authorization 헤더, 쿠키 등). +* 특정 HTTP 메서드(`POST`, `PUT`) 또는 와일드카드 `"*"`를 사용한 모든 메서드. +* 특정 HTTP 헤더 또는 와일드카드 `"*"`를 사용한 모든 헤더. -{* ../../docs_src/cors/tutorial001.py hl[2,6:11,13:19] *} +{* ../../docs_src/cors/tutorial001_py39.py hl[2,6:11,13:19] *} -`CORSMiddleware` 에서 사용하는 기본 매개변수는 제한적이므로, 브라우저가 교차-도메인 상황에서 특정한 출처, 메소드, 헤더 등을 사용할 수 있도록 하려면 이들을 명시적으로 허용해야 합니다. -다음의 인자들이 지원됩니다: +`CORSMiddleware` 구현에서 사용하는 기본 매개변수는 기본적으로 제한적이므로, 브라우저가 Cross-Domain 컨텍스트에서 특정 출처, 메서드 또는 헤더를 사용할 수 있도록 하려면 이를 명시적으로 활성화해야 합니다. -* `allow_origins` - 교차-출처 요청을 보낼 수 있는 출처의 리스트입니다. 예) `['https://example.org', 'https://www.example.org']`. 모든 출처를 허용하기 위해 `['*']` 를 사용할 수 있습니다. -* `allow_origin_regex` - 교차-출처 요청을 보낼 수 있는 출처를 정규표현식 문자열로 나타냅니다. `'https://.*\.example\.org'`. -* `allow_methods` - 교차-출처 요청을 허용하는 HTTP 메소드의 리스트입니다. 기본값은 `['GET']` 입니다. `['*']` 을 사용하여 모든 표준 메소드들을 허용할 수 있습니다. -* `allow_headers` - 교차-출처를 지원하는 HTTP 요청 헤더의 리스트입니다. 기본값은 `[]` 입니다. 모든 헤더들을 허용하기 위해 `['*']` 를 사용할 수 있습니다. `Accept`, `Accept-Language`, `Content-Language` 그리고 `Content-Type` 헤더는 CORS 요청시 언제나 허용됩니다. -* `allow_credentials` - 교차-출처 요청시 쿠키 지원 여부를 설정합니다. 기본값은 `False` 입니다. 또한 해당 항목을 허용할 경우 `allow_origins` 는 `['*']` 로 설정할 수 없으며, 출처를 반드시 특정해야 합니다. -* `expose_headers` - 브라우저에 접근할 수 있어야 하는 모든 응답 헤더를 가리킵니다. 기본값은 `[]` 입니다. -* `max_age` - 브라우저가 CORS 응답을 캐시에 저장하는 최대 시간을 초 단위로 설정합니다. 기본값은 `600` 입니다. +다음 인자들이 지원됩니다: -미들웨어는 두가지 특정한 종류의 HTTP 요청에 응답합니다... +* `allow_origins` - 교차-출처 요청을 보낼 수 있도록 허용해야 하는 출처의 리스트입니다. 예: `['https://example.org', 'https://www.example.org']`. 모든 출처를 허용하려면 `['*']`를 사용할 수 있습니다. +* `allow_origin_regex` - 교차-출처 요청을 보낼 수 있도록 허용해야 하는 출처와 매칭할 정규표현식 문자열입니다. 예: `'https://.*\.example\.org'`. +* `allow_methods` - 교차-출처 요청에 허용되어야 하는 HTTP 메서드의 리스트입니다. 기본값은 `['GET']`입니다. 모든 표준 메서드를 허용하려면 `['*']`를 사용할 수 있습니다. +* `allow_headers` - 교차-출처 요청에 대해 지원되어야 하는 HTTP 요청 헤더의 리스트입니다. 기본값은 `[]`입니다. 모든 헤더를 허용하려면 `['*']`를 사용할 수 있습니다. `Accept`, `Accept-Language`, `Content-Language`, `Content-Type` 헤더는 단순 CORS 요청에 대해 항상 허용됩니다. +* `allow_credentials` - 교차-출처 요청에 대해 쿠키를 지원해야 함을 나타냅니다. 기본값은 `False`입니다. -### CORS 사전 요청 + `allow_credentials`가 `True`로 설정된 경우 `allow_origins`, `allow_methods`, `allow_headers` 중 어느 것도 `['*']`로 설정할 수 없습니다. 모두 명시적으로 지정되어야 합니다. -`Origin` 및 `Access-Control-Request-Method` 헤더와 함께 전송하는 모든 `OPTIONS` 요청입니다. +* `expose_headers` - 브라우저에서 접근 가능해야 하는 모든 응답 헤더를 나타냅니다. 기본값은 `[]`입니다. +* `max_age` - 브라우저가 CORS 응답을 캐시하는 최대 시간을 초 단위로 설정합니다. 기본값은 `600`입니다. -이 경우 미들웨어는 들어오는 요청을 가로채 적절한 CORS 헤더와, 정보 제공을 위한 `200` 또는 `400` 응답으로 응답합니다. +미들웨어는 두 가지 특정한 종류의 HTTP 요청에 응답합니다... -### 단순한 요청 +### CORS 사전 요청 { #cors-preflight-requests } -`Origin` 헤더를 가진 모든 요청. 이 경우 미들웨어는 요청을 정상적으로 전달하지만, 적절한 CORS 헤더를 응답에 포함시킵니다. +`Origin` 및 `Access-Control-Request-Method` 헤더가 있는 모든 `OPTIONS` 요청입니다. -## 더 많은 정보 +이 경우 미들웨어는 들어오는 요청을 가로채 적절한 CORS 헤더와 함께, 정보 제공 목적으로 `200` 또는 `400` 응답을 반환합니다. -CORS에 대한 더 많은 정보를 알고싶다면, Mozilla CORS 문서를 참고하기 바랍니다. +### 단순한 요청 { #simple-requests } -/// note | 기술적 세부 사항 +`Origin` 헤더가 있는 모든 요청입니다. 이 경우 미들웨어는 요청을 정상적으로 통과시키지만, 응답에 적절한 CORS 헤더를 포함합니다. -`from starlette.middleware.cors import CORSMiddleware` 역시 사용할 수 있습니다. +## 더 많은 정보 { #more-info } -**FastAPI**는 개발자인 당신의 편의를 위해 `fastapi.middleware` 에서 몇가지의 미들웨어를 제공합니다. 하지만 대부분의 미들웨어가 Stralette으로부터 직접 제공됩니다. +CORS에 대한 더 많은 정보는 Mozilla CORS 문서를 참고하세요. + +/// note | 기술 세부사항 + +`from starlette.middleware.cors import CORSMiddleware`도 사용할 수 있습니다. + +**FastAPI**는 개발자인 여러분의 편의를 위해 `fastapi.middleware`에 여러 미들웨어를 제공합니다. 하지만 사용 가능한 미들웨어 대부분은 Starlette에서 직접 제공됩니다. /// diff --git a/docs/ko/docs/tutorial/debugging.md b/docs/ko/docs/tutorial/debugging.md index e42f1ba880..ca20acff64 100644 --- a/docs/ko/docs/tutorial/debugging.md +++ b/docs/ko/docs/tutorial/debugging.md @@ -1,14 +1,14 @@ -# 디버깅 +# 디버깅 { #debugging } 예를 들면 Visual Studio Code 또는 PyCharm을 사용하여 편집기에서 디버거를 연결할 수 있습니다. -## `uvicorn` 호출 +## `uvicorn` 호출 { #call-uvicorn } -FastAPI 애플리케이션에서 `uvicorn`을 직접 임포트하여 실행합니다 +FastAPI 애플리케이션에서 `uvicorn`을 직접 임포트하여 실행합니다: -{* ../../docs_src/debugging/tutorial001.py hl[1,15] *} +{* ../../docs_src/debugging/tutorial001_py39.py hl[1,15] *} -### `__name__ == "__main__"` 에 대하여 +### `__name__ == "__main__"` 에 대하여 { #about-name-main } `__name__ == "__main__"`의 주요 목적은 다음과 같이 파일이 호출될 때 실행되는 일부 코드를 갖는 것입니다. @@ -26,7 +26,7 @@ $ python myapp.py from myapp import app ``` -#### 추가 세부사항 +#### 추가 세부사항 { #more-details } 파일 이름이 `myapp.py`라고 가정해 보겠습니다. @@ -62,7 +62,7 @@ from myapp import app # Some more code ``` -이 경우 `myapp.py` 내부의 자동 변수에는 값이 `"__main__"`인 변수 `__name__`이 없습니다. +이 경우 `myapp.py` 내부의 자동 변수 `__name__`에는 값이 `"__main__"`이 들어가지 않습니다. 따라서 다음 행 @@ -74,11 +74,11 @@ from myapp import app /// info | 정보 -자세한 내용은 공식 Python 문서를 확인하세요 +자세한 내용은 공식 Python 문서를 확인하세요. /// -## 디버거로 코드 실행 +## 디버거로 코드 실행 { #run-your-code-with-your-debugger } 코드에서 직접 Uvicorn 서버를 실행하고 있기 때문에 디버거에서 직접 Python 프로그램(FastAPI 애플리케이션)을 호출할 수 있습니다. @@ -101,7 +101,7 @@ from myapp import app Pycharm을 사용하는 경우 다음을 수행할 수 있습니다 -* "Run" 메뉴를 엽니다 +* "Run" 메뉴를 엽니다. * "Debug..." 옵션을 선택합니다. * 그러면 상황에 맞는 메뉴가 나타납니다. * 디버그할 파일을 선택합니다(이 경우 `main.py`). diff --git a/docs/ko/docs/tutorial/dependencies/classes-as-dependencies.md b/docs/ko/docs/tutorial/dependencies/classes-as-dependencies.md index 3e5cdcc8c8..68bba669ae 100644 --- a/docs/ko/docs/tutorial/dependencies/classes-as-dependencies.md +++ b/docs/ko/docs/tutorial/dependencies/classes-as-dependencies.md @@ -1,30 +1,30 @@ -# 의존성으로서의 클래스 +# 의존성으로서의 클래스 { #classes-as-dependencies } -**의존성 주입** 시스템에 대해 자세히 살펴보기 전에 이전 예제를 업그레이드 해보겠습니다. +**의존성 주입** 시스템에 대해 더 깊이 살펴보기 전에, 이전 예제를 업그레이드해 보겠습니다. -## 이전 예제의 `딕셔너리` +## 이전 예제의 `dict` { #a-dict-from-the-previous-example } -이전 예제에서, 우리는 의존성(의존 가능한) 함수에서 `딕셔너리`객체를 반환하고 있었습니다: +이전 예제에서는 의존성("dependable")에서 `dict`를 반환하고 있었습니다: -{* ../../docs_src/dependencies/tutorial001.py hl[9] *} +{* ../../docs_src/dependencies/tutorial001_an_py310.py hl[9] *} -우리는 *경로 작동 함수*의 매개변수 `commons`에서 `딕셔너리` 객체를 얻습니다. +하지만 그러면 *경로 처리 함수*의 매개변수 `commons`에서 `dict`를 받게 됩니다. -그리고 우리는 에디터들이 `딕셔너리` 객체의 키나 밸류의 자료형을 알 수 없기 때문에 자동 완성과 같은 기능을 제공해 줄 수 없다는 것을 알고 있습니다. +그리고 에디터는 `dict`의 키와 값 타입을 알 수 없기 때문에 `dict`에 대해서는 (완성 기능 같은) 많은 지원을 제공할 수 없다는 것을 알고 있습니다. -더 나은 방법이 있을 것 같습니다... +더 나은 방법이 있습니다... -## 의존성으로 사용 가능한 것 +## 의존성이 되기 위한 조건 { #what-makes-a-dependency } -지금까지 함수로 선언된 의존성을 봐왔습니다. +지금까지는 함수로 선언된 의존성을 봤습니다. -아마도 더 일반적이기는 하겠지만 의존성을 선언하는 유일한 방법은 아닙니다. +하지만 그것만이 의존성을 선언하는 유일한 방법은 아닙니다(아마도 더 일반적이긴 하겠지만요). -핵심 요소는 의존성이 "호출 가능"해야 한다는 것입니다 +핵심 요소는 의존성이 "호출 가능(callable)"해야 한다는 것입니다. -파이썬에서의 "**호출 가능**"은 파이썬이 함수처럼 "호출"할 수 있는 모든 것입니다. +파이썬에서 "**호출 가능(callable)**"이란 파이썬이 함수처럼 "호출"할 수 있는 모든 것입니다. -따라서, 만약 당신이 `something`(함수가 아닐 수도 있음) 객체를 가지고 있고, +따라서 `something`(함수가 _아닐_ 수도 있습니다)이라는 객체가 있고, 다음처럼 "호출"(실행)할 수 있다면: ```Python something() @@ -36,11 +36,11 @@ something() something(some_argument, some_keyword_argument="foo") ``` -상기와 같은 방식으로 "호출(실행)" 할 수 있다면 "호출 가능"이 됩니다. +그것은 "호출 가능(callable)"입니다. -## 의존성으로서의 클래스 +## 의존성으로서의 클래스 { #classes-as-dependencies_1 } -파이썬 클래스의 인스턴스를 생성하기 위해 사용하는 것과 동일한 문법을 사용한다는 걸 알 수 있습니다. +파이썬 클래스의 인스턴스를 만들 때도 같은 문법을 사용한다는 것을 알 수 있을 겁니다. 예를 들어: @@ -53,125 +53,236 @@ class Cat: fluffy = Cat(name="Mr Fluffy") ``` -이 경우에 `fluffy`는 클래스 `Cat`의 인스턴스입니다. 그리고 우리는 `fluffy`를 만들기 위해서 `Cat`을 "호출"했습니다. +이 경우 `fluffy`는 클래스 `Cat`의 인스턴스입니다. -따라서, 파이썬 클래스는 **호출 가능**합니다. +그리고 `fluffy`를 만들기 위해 `Cat`을 "호출"하고 있습니다. -그래서 **FastAPI**에서는 파이썬 클래스를 의존성으로 사용할 수 있습니다. +따라서 파이썬 클래스도 **호출 가능(callable)**합니다. -FastAPI가 실질적으로 확인하는 것은 "호출 가능성"(함수, 클래스 또는 다른 모든 것)과 정의된 매개변수들입니다. +그러면 **FastAPI**에서는 파이썬 클래스를 의존성으로 사용할 수 있습니다. -"호출 가능"한 것을 의존성으로서 **FastAPI**에 전달하면, 그 "호출 가능"한 것의 매개변수들을 분석한 후 이를 *경로 작동 함수*를 위한 매개변수와 동일한 방식으로 처리합니다. 하위-의존성 또한 같은 방식으로 처리합니다. +FastAPI가 실제로 확인하는 것은 그것이 "호출 가능(callable)"(함수, 클래스, 또는 다른 무엇이든)한지와 정의된 매개변수들입니다. -매개변수가 없는 "호출 가능"한 것 역시 매개변수가 없는 *경로 작동 함수*와 동일한 방식으로 적용됩니다. +**FastAPI**에서 "호출 가능(callable)"한 것을 의존성으로 넘기면, 그 "호출 가능(callable)"한 것의 매개변수들을 분석하고 *경로 처리 함수*의 매개변수와 동일한 방식으로 처리합니다. 하위 의존성도 포함해서요. -그래서, 우리는 위 예제에서의 `common_paramenters` 의존성을 클래스 `CommonQueryParams`로 바꿀 수 있습니다. +이것은 매개변수가 전혀 없는 callable에도 적용됩니다. 매개변수가 없는 *경로 처리 함수*에 적용되는 것과 동일합니다. -{* ../../docs_src/dependencies/tutorial002.py hl[11:15] *} +그러면 위의 의존성("dependable") `common_parameters`를 클래스 `CommonQueryParams`로 바꿀 수 있습니다: -클래스의 인스턴스를 생성하는 데 사용되는 `__init__` 메서드에 주목하기 바랍니다: +{* ../../docs_src/dependencies/tutorial002_an_py310.py hl[11:15] *} -{* ../../docs_src/dependencies/tutorial002.py hl[12] *} +클래스의 인스턴스를 만들 때 사용하는 `__init__` 메서드에 주목하세요: -...이전 `common_parameters`와 동일한 매개변수를 가집니다: +{* ../../docs_src/dependencies/tutorial002_an_py310.py hl[12] *} -{* ../../docs_src/dependencies/tutorial001.py hl[9] *} +...이전의 `common_parameters`와 동일한 매개변수를 가지고 있습니다: -이 매개변수들은 **FastAPI**가 의존성을 "해결"하기 위해 사용할 것입니다 +{* ../../docs_src/dependencies/tutorial001_an_py310.py hl[8] *} -함수와 클래스 두 가지 방식 모두, 아래 요소를 갖습니다: +이 매개변수들이 **FastAPI**가 의존성을 "해결"하는 데 사용할 것들입니다. -* `문자열`이면서 선택사항인 쿼리 매개변수 `q`. -* 기본값이 `0`이면서 `정수형`인 쿼리 매개변수 `skip` -* 기본값이 `100`이면서 `정수형`인 쿼리 매개변수 `limit` +두 경우 모두 다음을 갖게 됩니다: -두 가지 방식 모두, 데이터는 변환, 검증되고 OpenAPI 스키마에 문서화됩니다. +* `str`인 선택적 쿼리 매개변수 `q`. +* 기본값이 `0`인 `int` 쿼리 매개변수 `skip`. +* 기본값이 `100`인 `int` 쿼리 매개변수 `limit`. -## 사용해봅시다! +두 경우 모두 데이터는 변환되고, 검증되며, OpenAPI 스키마에 문서화되는 등 여러 처리가 적용됩니다. -이제 아래의 클래스를 이용해서 의존성을 정의할 수 있습니다. +## 사용하기 { #use-it } -{* ../../docs_src/dependencies/tutorial002.py hl[19] *} +이제 이 클래스를 사용해 의존성을 선언할 수 있습니다. -**FastAPI**는 `CommonQueryParams` 클래스를 호출합니다. 이것은 해당 클래스의 "인스턴스"를 생성하고 그 인스턴스는 함수의 매개변수 `commons`로 전달됩니다. +{* ../../docs_src/dependencies/tutorial002_an_py310.py hl[19] *} -## 타입 힌팅 vs `Depends` +**FastAPI**는 `CommonQueryParams` 클래스를 호출합니다. 그러면 해당 클래스의 "인스턴스"가 생성되고, 그 인스턴스가 함수의 매개변수 `commons`로 전달됩니다. -위 코드에서 `CommonQueryParams`를 두 번 작성한 방식에 주목하십시오: +## 타입 어노테이션 vs `Depends` { #type-annotation-vs-depends } + +위 코드에서 `CommonQueryParams`를 두 번 작성하는 방식에 주목하세요: + +//// tab | Python 3.9+ + +```Python +commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] +``` + +//// + +//// tab | Python 3.9+ non-Annotated + +/// tip | 팁 + +가능하다면 `Annotated` 버전을 사용하는 것을 권장합니다. + +/// ```Python commons: CommonQueryParams = Depends(CommonQueryParams) ``` -마지막 `CommonQueryParams` 변수를 보면: +//// + +마지막 `CommonQueryParams`는, 다음에서: ```Python -... = Depends(CommonQueryParams) +... Depends(CommonQueryParams) ``` -... **FastAPI**가 실제로 어떤 것이 의존성인지 알기 위해서 사용하는 방법입니다. -FastAPI는 선언된 매개변수들을 추출할 것이고 실제로 이 변수들을 호출할 것입니다. +...**FastAPI**가 실제로 무엇이 의존성인지 알기 위해 사용하는 것입니다. + +FastAPI는 여기에서 선언된 매개변수들을 추출하고, 실제로 이것을 호출합니다. --- -이 경우에, 첫번째 `CommonQueryParams` 변수를 보면: +이 경우 첫 번째 `CommonQueryParams`는 다음에서: + +//// tab | Python 3.9+ + +```Python +commons: Annotated[CommonQueryParams, ... +``` + +//// + +//// tab | Python 3.9+ non-Annotated + +/// tip | 팁 + +가능하다면 `Annotated` 버전을 사용하는 것을 권장합니다. + +/// ```Python commons: CommonQueryParams ... ``` -... **FastAPI**는 `CommonQueryParams` 변수에 어떠한 특별한 의미도 부여하지 않습니다. FastAPI는 이 변수를 데이터 변환, 검증 등에 활용하지 않습니다. (활용하려면 `= Depends(CommonQueryParams)`를 사용해야 합니다.) +//// -사실 아래와 같이 작성해도 무관합니다: +...**FastAPI**에 특별한 의미가 없습니다. FastAPI는 이를 데이터 변환, 검증 등에 사용하지 않습니다(그런 용도로는 `Depends(CommonQueryParams)`를 사용하고 있기 때문입니다). + +실제로는 이렇게만 작성해도 됩니다: + +//// tab | Python 3.9+ + +```Python +commons: Annotated[Any, Depends(CommonQueryParams)] +``` + +//// + +//// tab | Python 3.9+ non-Annotated + +/// tip | 팁 + +가능하다면 `Annotated` 버전을 사용하는 것을 권장합니다. + +/// ```Python commons = Depends(CommonQueryParams) ``` -..전체적인 코드는 아래와 같습니다: +//// -{* ../../docs_src/dependencies/tutorial003.py hl[19] *} +...다음과 같이요: -그러나 자료형을 선언하면 에디터가 매개변수 `commons`로 전달될 것이 무엇인지 알게 되고, 이를 통해 코드 완성, 자료형 확인 등에 도움이 될 수 있으므로 권장됩니다. +{* ../../docs_src/dependencies/tutorial003_an_py310.py hl[19] *} - +하지만 타입을 선언하는 것을 권장합니다. 그러면 에디터가 매개변수 `commons`에 무엇이 전달되는지 알 수 있고, 코드 완성, 타입 체크 등에서 도움을 받을 수 있습니다: -## 코드 단축 + -그러나 여기 `CommonQueryParams`를 두 번이나 작성하는, 코드 반복이 있다는 것을 알 수 있습니다: +## 단축 { #shortcut } + +하지만 `CommonQueryParams`를 두 번 작성하는 코드 반복이 있다는 것을 볼 수 있습니다: + +//// tab | Python 3.9+ + +```Python +commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] +``` + +//// + +//// tab | Python 3.9+ non-Annotated + +/// tip | 팁 + +가능하다면 `Annotated` 버전을 사용하는 것을 권장합니다. + +/// ```Python commons: CommonQueryParams = Depends(CommonQueryParams) ``` -**FastAPI**는 *특히* 의존성이 **FastAPI**가 클래스 자체의 인스턴스를 생성하기 위해 "호출"하는 클래스인 경우, 조금 더 쉬운 방법을 제공합니다. +//// -이러한 특정한 경우에는 아래처럼 사용할 수 있습니다: +**FastAPI**는 이런 경우를 위한 단축 방법을 제공합니다. 이때 의존성은 *특히* **FastAPI**가 "호출"해서 클래스 자체의 인스턴스를 만들도록 하는 클래스입니다. -이렇게 쓰는 것 대신: +이 특정한 경우에는 다음과 같이 할 수 있습니다: + +다음처럼 작성하는 대신: + +//// tab | Python 3.9+ + +```Python +commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] +``` + +//// + +//// tab | Python 3.9+ non-Annotated + +/// tip | 팁 + +가능하다면 `Annotated` 버전을 사용하는 것을 권장합니다. + +/// ```Python commons: CommonQueryParams = Depends(CommonQueryParams) ``` -...이렇게 쓸 수 있습니다.: +//// + +...이렇게 작성합니다: + +//// tab | Python 3.9+ + +```Python +commons: Annotated[CommonQueryParams, Depends()] +``` + +//// + +//// tab | Python 3.9+ non-Annotated + +/// tip | 팁 + +가능하다면 `Annotated` 버전을 사용하는 것을 권장합니다. + +/// ```Python commons: CommonQueryParams = Depends() ``` -의존성을 매개변수의 타입으로 선언하는 경우 `Depends(CommonQueryParams)`처럼 클래스 이름 전체를 *다시* 작성하는 대신, 매개변수를 넣지 않은 `Depends()`의 형태로 사용할 수 있습니다. +//// -아래에 같은 예제가 있습니다: +의존성을 매개변수의 타입으로 선언하고, `Depends(CommonQueryParams)` 안에 클래스 전체를 *다시* 작성하는 대신 매개변수 없이 `Depends()`를 사용합니다. -{* ../../docs_src/dependencies/tutorial004.py hl[19] *} +그러면 같은 예제는 다음처럼 보일 겁니다: -...이렇게 코드를 단축하여도 **FastAPI**는 무엇을 해야하는지 알고 있습니다. +{* ../../docs_src/dependencies/tutorial004_an_py310.py hl[19] *} + +...그리고 **FastAPI**는 무엇을 해야 하는지 알게 됩니다. /// tip | 팁 -만약 이것이 도움이 되기보다 더 헷갈리게 만든다면, 잊어버리십시오. 이것이 반드시 필요한 것은 아닙니다. +도움이 되기보다 더 헷갈린다면, 무시하세요. 이건 *필수*가 아닙니다. -이것은 단지 손쉬운 방법일 뿐입니다. 왜냐하면 **FastAPI**는 코드 반복을 최소화할 수 있는 방법을 고민하기 때문입니다. +그저 단축 방법일 뿐입니다. **FastAPI**는 코드 반복을 최소화하도록 도와주는 것을 중요하게 생각하기 때문입니다. /// diff --git a/docs/ko/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md b/docs/ko/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md index 4a3854cefa..39c78c0786 100644 --- a/docs/ko/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md +++ b/docs/ko/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md @@ -1,4 +1,4 @@ -# 경로 작동 데코레이터에서의 의존성 +# 경로 작동 데코레이터에서의 의존성 { #dependencies-in-path-operation-decorators } 몇몇 경우에는, *경로 작동 함수* 안에서 의존성의 반환 값이 필요하지 않습니다. @@ -8,7 +8,7 @@ 그런 경우에, `Depends`를 사용하여 *경로 작동 함수*의 매개변수로 선언하는 것보다 *경로 작동 데코레이터*에 `dependencies`의 `list`를 추가할 수 있습니다. -## *경로 작동 데코레이터*에 `dependencies` 추가하기 +## *경로 작동 데코레이터*에 `dependencies` 추가하기 { #add-dependencies-to-the-path-operation-decorator } *경로 작동 데코레이터*는 `dependencies`라는 선택적인 인자를 받습니다. @@ -22,7 +22,7 @@ 일부 편집기에서는 사용되지 않는 함수 매개변수를 검사하고 오류로 표시합니다. -*경로 작동 데코레이터*에서 `dependencies`를 사용하면 편집기/도구 오류를 피하며 실행되도록 할 수 있습니다. +*경로 작동 데코레이터*에서 이러한 `dependencies`를 사용하면 편집기/도구 오류를 피하면서도 실행되도록 할 수 있습니다. 또한 코드에서 사용되지 않는 매개변수를 보고 불필요하다고 생각할 수 있는 새로운 개발자의 혼란을 방지하는데 도움이 될 수 있습니다. @@ -36,23 +36,23 @@ /// -## 의존성 오류와 값 반환하기 +## 의존성 오류와 값 반환하기 { #dependencies-errors-and-return-values } 평소에 사용하던대로 같은 의존성 *함수*를 사용할 수 있습니다. -### 의존성 요구사항 +### 의존성 요구사항 { #dependency-requirements } (헤더같은) 요청 요구사항이나 하위-의존성을 선언할 수 있습니다: {* ../../docs_src/dependencies/tutorial006_an_py39.py hl[8,13] *} -### 오류 발생시키기 +### 오류 발생시키기 { #raise-exceptions } -다음 의존성은 기존 의존성과 동일하게 예외를 `raise`를 일으킬 수 있습니다: +다음 의존성은 기존 의존성과 동일하게 예외를 `raise`할 수 있습니다: {* ../../docs_src/dependencies/tutorial006_an_py39.py hl[10,15] *} -### 값 반환하기 +### 값 반환하기 { #return-values } 값을 반환하거나, 그러지 않을 수 있으며 값은 사용되지 않습니다. @@ -60,10 +60,10 @@ {* ../../docs_src/dependencies/tutorial006_an_py39.py hl[11,16] *} -## *경로 작동* 모음에 대한 의존성 +## *경로 작동* 모음에 대한 의존성 { #dependencies-for-a-group-of-path-operations } 나중에 여러 파일을 가지고 있을 수 있는 더 큰 애플리케이션을 구조화하는 법([더 큰 애플리케이션 - 여러 파일들](../../tutorial/bigger-applications.md){.internal-link target=_blank})을 읽을 때, *경로 작동* 모음에 대한 단일 `dependencies` 매개변수를 선언하는 법에 대해서 배우게 될 것입니다. -## 전역 의존성 +## 전역 의존성 { #global-dependencies } 다음으로 각 *경로 작동*에 적용되도록 `FastAPI` 애플리케이션 전체에 의존성을 추가하는 법을 볼 것입니다. diff --git a/docs/ko/docs/tutorial/dependencies/dependencies-with-yield.md b/docs/ko/docs/tutorial/dependencies/dependencies-with-yield.md index ff174937de..9bf6c0693a 100644 --- a/docs/ko/docs/tutorial/dependencies/dependencies-with-yield.md +++ b/docs/ko/docs/tutorial/dependencies/dependencies-with-yield.md @@ -1,6 +1,6 @@ -# yield를 사용하는 의존성 +# `yield`를 사용하는 의존성 { #dependencies-with-yield } -FastAPI는 작업 완료 후 추가 단계를 수행하는 의존성을 지원합니다. +FastAPI는 작업 완료 후 추가 단계를 수행하는 의존성을 지원합니다. 이를 구현하려면 `return` 대신 `yield`를 사용하고, 추가로 실행할 단계 (코드)를 그 뒤에 작성하세요. @@ -23,21 +23,21 @@ FastAPI는 하여 추가할 수 있습니다. +사용자 정의 독점 헤더는 `X-` 접두사를 사용하여 추가할 수 있다는 점을 기억하세요. -그러나 만약 클라이언트의 브라우저에서 볼 수 있는 사용자 정의 헤더를 가지고 있다면, 그것들을 CORS 설정([CORS (Cross-Origin Resource Sharing)](cors.md){.internal-link target=_blank})에 Starlette CORS 문서에 명시된 `expose_headers` 매개변수를 이용하여 헤더들을 추가하여야합니다. +하지만 브라우저에서 클라이언트가 볼 수 있게 하려는 사용자 정의 헤더가 있다면, Starlette의 CORS 문서에 문서화된 `expose_headers` 매개변수를 사용해 CORS 설정([CORS (Cross-Origin Resource Sharing)](cors.md){.internal-link target=_blank})에 추가해야 합니다. /// -/// note | 기술적 세부사항 +/// note | 기술 세부사항 -`from starlette.requests import request`를 사용할 수도 있습니다. +`from starlette.requests import Request`를 사용할 수도 있습니다. -**FastAPI**는 개발자에게 편의를 위해 이를 제공합니다. 그러나 Starlette에서 직접 파생되었습니다. +**FastAPI**는 개발자인 여러분의 편의를 위해 이를 제공합니다. 하지만 이는 Starlette에서 직접 가져온 것입니다. /// -### `response`의 전과 후 +### `response`의 전과 후 { #before-and-after-the-response } -*경로 작동*을 받기 전 `request`와 함께 작동할 수 있는 코드를 추가할 수 있습니다. +어떤 *경로 처리*가 받기 전에, `request`와 함께 실행될 코드를 추가할 수 있습니다. -그리고 `response` 또한 생성된 후 반환되기 전에 코드를 추가 할 수 있습니다. +또한 `response`가 생성된 후, 반환하기 전에 코드를 추가할 수도 있습니다. -예를 들어, 요청을 수행하고 응답을 생성하는데 까지 걸린 시간 값을 가지고 있는 `X-Process-Time` 같은 사용자 정의 헤더를 추가할 수 있습니다. +예를 들어, 요청을 처리하고 응답을 생성하는 데 걸린 시간을 초 단위로 담는 사용자 정의 헤더 `X-Process-Time`을 추가할 수 있습니다: -{* ../../docs_src/middleware/tutorial001.py hl[10,12:13] *} +{* ../../docs_src/middleware/tutorial001_py39.py hl[10,12:13] *} -## 다른 미들웨어 +/// tip | 팁 -미들웨어에 대한 더 많은 정보는 [숙련된 사용자 안내서: 향상된 미들웨어](../advanced/middleware.md){.internal-link target=\_blank}에서 확인할 수 있습니다. +여기서는 이러한 사용 사례에서 더 정확할 수 있기 때문에 `time.time()` 대신 `time.perf_counter()`를 사용합니다. 🤓 -다음 부분에서 미들웨어와 함께 CORS를 어떻게 다루는지에 대해 확인할 것입니다. +/// + +## 여러 미들웨어 실행 순서 { #multiple-middleware-execution-order } + +`@app.middleware()` 데코레이터 또는 `app.add_middleware()` 메서드를 사용해 여러 미들웨어를 추가하면, 새로 추가된 각 미들웨어가 애플리케이션을 감싸 스택을 형성합니다. 마지막에 추가된 미들웨어가 *가장 바깥쪽*이고, 처음에 추가된 미들웨어가 *가장 안쪽*입니다. + +요청 경로에서는 *가장 바깥쪽* 미들웨어가 먼저 실행됩니다. + +응답 경로에서는 마지막에 실행됩니다. + +예를 들어: + +```Python +app.add_middleware(MiddlewareA) +app.add_middleware(MiddlewareB) +``` + +이 경우 실행 순서는 다음과 같습니다: + +* **요청**: MiddlewareB → MiddlewareA → route + +* **응답**: route → MiddlewareA → MiddlewareB + +이러한 스태킹 동작은 미들웨어가 예측 가능하고 제어 가능한 순서로 실행되도록 보장합니다. + +## 다른 미들웨어 { #other-middlewares } + +다른 미들웨어에 대한 더 많은 정보는 나중에 [숙련된 사용자 안내서: 향상된 미들웨어](../advanced/middleware.md){.internal-link target=_blank}에서 확인할 수 있습니다. + +다음 섹션에서 미들웨어로 CORS를 처리하는 방법을 보게 될 것입니다. diff --git a/docs/ko/docs/tutorial/path-operation-configuration.md b/docs/ko/docs/tutorial/path-operation-configuration.md index 81914182af..b8a87667a0 100644 --- a/docs/ko/docs/tutorial/path-operation-configuration.md +++ b/docs/ko/docs/tutorial/path-operation-configuration.md @@ -1,97 +1,107 @@ -# 경로 작동 설정 +# 경로 처리 설정 { #path-operation-configuration } -*경로 작동 데코레이터*를 설정하기 위해서 전달할수 있는 몇 가지 매개변수가 있습니다. +*경로 처리 데코레이터*를 설정하기 위해 전달할 수 있는 몇 가지 매개변수가 있습니다. /// warning | 경고 -아래 매개변수들은 *경로 작동 함수*가 아닌 *경로 작동 데코레이터*에 직접 전달된다는 사실을 기억하십시오. +아래 매개변수들은 *경로 처리 함수*가 아닌 *경로 처리 데코레이터*에 직접 전달된다는 사실을 기억하세요. /// -## 응답 상태 코드 +## 응답 상태 코드 { #response-status-code } -*경로 작동*의 응답에 사용될 (HTTP) `status_code`를 정의할수 있습니다. +*경로 처리*의 응답에 사용될 (HTTP) `status_code`를 정의할 수 있습니다. -`404`와 같은 `int`형 코드를 직접 전달할수 있습니다. +`404`와 같은 `int`형 코드를 직접 전달할 수 있습니다. -하지만 각 코드의 의미를 모른다면, `status`에 있는 단축 상수들을 사용할수 있습니다: +하지만 각 숫자 코드가 무엇을 의미하는지 기억하지 못한다면, `status`에 있는 단축 상수들을 사용할 수 있습니다: -{* ../../docs_src/path_operation_configuration/tutorial001.py hl[3,17] *} +{* ../../docs_src/path_operation_configuration/tutorial001_py310.py hl[1,15] *} -각 상태 코드들은 응답에 사용되며, OpenAPI 스키마에 추가됩니다. +해당 상태 코드는 응답에 사용되며, OpenAPI 스키마에 추가됩니다. -/// note | 기술적 세부사항 +/// note | 기술 세부사항 다음과 같이 임포트하셔도 좋습니다. `from starlette import status`. -**FastAPI**는 개발자 여러분의 편의를 위해서 `starlette.status`와 동일한 `fastapi.status`를 제공합니다. 하지만 Starlette에서 직접 온 것입니다. +**FastAPI**는 개발자 여러분의 편의를 위해 `starlette.status`와 동일한 `fastapi.status`를 제공합니다. 하지만 이는 Starlette에서 직접 온 것입니다. /// -## 태그 +## 태그 { #tags } -(보통 단일 `str`인) `str`로 구성된 `list`와 함께 매개변수 `tags`를 전달하여, `경로 작동`에 태그를 추가할 수 있습니다: +(보통 단일 `str`인) `str`로 구성된 `list`와 함께 매개변수 `tags`를 전달하여, *경로 처리*에 태그를 추가할 수 있습니다: -{* ../../docs_src/path_operation_configuration/tutorial002.py hl[17,22,27] *} +{* ../../docs_src/path_operation_configuration/tutorial002_py310.py hl[15,20,25] *} 전달된 태그들은 OpenAPI의 스키마에 추가되며, 자동 문서 인터페이스에서 사용됩니다: -## 요약과 기술 +### Enum을 사용한 태그 { #tags-with-enums } + +큰 애플리케이션이 있다면, **여러 태그**가 쌓이게 될 수 있고, 관련된 *경로 처리*에 항상 **같은 태그**를 사용하는지 확인하고 싶을 것입니다. + +이런 경우에는 태그를 `Enum`에 저장하는 것이 합리적일 수 있습니다. + +**FastAPI**는 일반 문자열과 동일한 방식으로 이를 지원합니다: + +{* ../../docs_src/path_operation_configuration/tutorial002b_py39.py hl[1,8:10,13,18] *} + +## 요약과 설명 { #summary-and-description } `summary`와 `description`을 추가할 수 있습니다: -{* ../../docs_src/path_operation_configuration/tutorial003.py hl[20:21] *} +{* ../../docs_src/path_operation_configuration/tutorial003_py310.py hl[18:19] *} -## 독스트링으로 만든 기술 +## 독스트링으로 만든 설명 { #description-from-docstring } -설명은 보통 길어지고 여러 줄에 걸쳐있기 때문에, *경로 작동* 기술을 함수 독스트링 에 선언할 수 있습니다, 이를 **FastAPI**가 독스트링으로부터 읽습니다. +설명은 보통 길어지고 여러 줄에 걸쳐있기 때문에, *경로 처리* 설명을 함수 docstring에 선언할 수 있으며, **FastAPI**는 그곳에서 이를 읽습니다. -마크다운 문법으로 독스트링을 작성할 수 있습니다, 작성된 마크다운 형식의 독스트링은 (마크다운의 들여쓰기를 고려하여) 올바르게 화면에 출력됩니다. +독스트링에는 Markdown을 작성할 수 있으며, (독스트링의 들여쓰기를 고려하여) 올바르게 해석되고 표시됩니다. -{* ../../docs_src/path_operation_configuration/tutorial004.py hl[19:27] *} +{* ../../docs_src/path_operation_configuration/tutorial004_py310.py hl[17:25] *} 이는 대화형 문서에서 사용됩니다: -## 응답 기술 +## 응답 설명 { #response-description } `response_description` 매개변수로 응답에 관한 설명을 명시할 수 있습니다: -{* ../../docs_src/path_operation_configuration/tutorial005.py hl[21] *} +{* ../../docs_src/path_operation_configuration/tutorial005_py310.py hl[19] *} /// info | 정보 -`response_description`은 구체적으로 응답을 지칭하며, `description`은 일반적인 *경로 작동*을 지칭합니다. +`response_description`은 구체적으로 응답을 지칭하며, `description`은 일반적인 *경로 처리*를 지칭합니다. /// /// check | 확인 -OpenAPI는 각 *경로 작동*이 응답에 관한 설명을 요구할 것을 명시합니다. +OpenAPI는 각 *경로 처리*가 응답에 관한 설명을 요구할 것을 명시합니다. -따라서, 응답에 관한 설명이 없을경우, **FastAPI**가 자동으로 "성공 응답" 중 하나를 생성합니다. +따라서, 응답에 관한 설명을 제공하지 않으면, **FastAPI**가 "Successful response" 중 하나를 자동으로 생성합니다. /// -## 단일 *경로 작동* 지원중단 +## *경로 처리* 지원중단하기 { #deprecate-a-path-operation } -단일 *경로 작동*을 없애지 않고 지원중단을 해야한다면, `deprecated` 매개변수를 전달하면 됩니다. +*경로 처리*를 제거하지 않고 deprecated로 표시해야 한다면, `deprecated` 매개변수를 전달하면 됩니다: -{* ../../docs_src/path_operation_configuration/tutorial006.py hl[16] *} +{* ../../docs_src/path_operation_configuration/tutorial006_py39.py hl[16] *} -대화형 문서에 지원중단이라고 표시됩니다. +대화형 문서에서 지원중단으로 명확하게 표시됩니다: -지원중단된 경우와 지원중단 되지 않은 경우에 대한 *경로 작동*이 어떻게 보이는 지 확인하십시오. +지원중단된 *경로 처리*와 지원중단되지 않은 *경로 처리*가 어떻게 보이는지 확인해 보세요: -## 정리 +## 정리 { #recap } -*경로 작동 데코레이터*에 매개변수(들)를 전달함으로 *경로 작동*을 설정하고 메타데이터를 추가할수 있습니다. +*경로 처리 데코레이터*에 매개변수(들)를 전달하여 *경로 처리*를 설정하고 메타데이터를 쉽게 추가할 수 있습니다. diff --git a/docs/ko/docs/tutorial/path-params-numeric-validations.md b/docs/ko/docs/tutorial/path-params-numeric-validations.md index f21c9290ed..f2c52d4aac 100644 --- a/docs/ko/docs/tutorial/path-params-numeric-validations.md +++ b/docs/ko/docs/tutorial/path-params-numeric-validations.md @@ -1,116 +1,153 @@ -# 경로 매개변수와 숫자 검증 +# 경로 매개변수와 숫자 검증 { #path-parameters-and-numeric-validations } `Query`를 사용하여 쿼리 매개변수에 더 많은 검증과 메타데이터를 선언하는 방법과 동일하게 `Path`를 사용하여 경로 매개변수에 검증과 메타데이터를 같은 타입으로 선언할 수 있습니다. -## 경로 임포트 +## `Path` 임포트 { #import-path } -먼저 `fastapi`에서 `Path`를 임포트합니다: +먼저 `fastapi`에서 `Path`를 임포트하고, `Annotated`도 임포트합니다: -{* ../../docs_src/path_params_numeric_validations/tutorial001.py hl[3] *} +{* ../../docs_src/path_params_numeric_validations/tutorial001_an_py310.py hl[1,3] *} -## 메타데이터 선언 +/// info | 정보 -`Query`에 동일한 매개변수를 선언할 수 있습니다. +FastAPI는 0.95.0 버전에서 `Annotated` 지원을 추가했고(그리고 이를 권장하기 시작했습니다). -예를 들어, `title` 메타데이터 값을 경로 매개변수 `item_id`에 선언하려면 다음과 같이 입력할 수 있습니다: +더 오래된 버전이 있다면 `Annotated`를 사용하려고 할 때 오류가 발생합니다. -{* ../../docs_src/path_params_numeric_validations/tutorial001.py hl[10] *} - -/// note | 참고 - -경로 매개변수는 경로의 일부여야 하므로 언제나 필수적입니다. - -즉, `...`로 선언해서 필수임을 나타내는게 좋습니다. - -그럼에도 `None`으로 선언하거나 기본값을 지정할지라도 아무 영향을 끼치지 않으며 언제나 필수입니다. +`Annotated`를 사용하기 전에 최소 0.95.1까지 [FastAPI 버전 업그레이드](../deployment/versions.md#upgrading-the-fastapi-versions){.internal-link target=_blank}를 꼭 하세요. /// -## 필요한 경우 매개변수 정렬하기 +## 메타데이터 선언 { #declare-metadata } + +`Query`에 동일한 매개변수를 선언할 수 있습니다. + +예를 들어, 경로 매개변수 `item_id`에 `title` 메타데이터 값을 선언하려면 다음과 같이 입력할 수 있습니다: + +{* ../../docs_src/path_params_numeric_validations/tutorial001_an_py310.py hl[10] *} + +/// note | 참고 + +경로 매개변수는 경로의 일부여야 하므로 언제나 필수입니다. `None`으로 선언하거나 기본값을 지정하더라도 아무 영향이 없으며, 항상 필수입니다. + +/// + +## 필요한 대로 매개변수 정렬하기 { #order-the-parameters-as-you-need } + +/// tip | 팁 + +`Annotated`를 사용한다면 이것은 아마 그렇게 중요하지 않거나 필요하지 않을 수 있습니다. + +/// `str` 형인 쿼리 매개변수 `q`를 필수로 선언하고 싶다고 해봅시다. -해당 매개변수에 대해 아무런 선언을 할 필요가 없으므로 `Query`를 정말로 써야할 필요는 없습니다. +해당 매개변수에 대해 아무런 선언을 할 필요가 없으므로 `Query`를 정말로 써야 할 필요는 없습니다. -하지만 `item_id` 경로 매개변수는 여전히 `Path`를 사용해야 합니다. +하지만 `item_id` 경로 매개변수는 여전히 `Path`를 사용해야 합니다. 그리고 어떤 이유로 `Annotated`를 사용하고 싶지 않다고 해봅시다. -파이썬은 "기본값"이 없는 값 앞에 "기본값"이 있는 값을 입력하면 불평합니다. +파이썬은 "기본값"이 있는 값을 "기본값"이 없는 값 앞에 두면 불평합니다. -그러나 매개변수들을 재정렬함으로써 기본값(쿼리 매개변수 `q`)이 없는 값을 처음 부분에 위치 할 수 있습니다. +하지만 순서를 재정렬해서 기본값이 없는 값(쿼리 매개변수 `q`)을 앞에 둘 수 있습니다. -**FastAPI**에서는 중요하지 않습니다. 이름, 타입 그리고 선언구(`Query`, `Path` 등)로 매개변수를 감지하며 순서는 신경 쓰지 않습니다. +**FastAPI**에서는 중요하지 않습니다. 이름, 타입 그리고 기본값 선언(`Query`, `Path` 등)로 매개변수를 감지하며 순서는 신경 쓰지 않습니다. -따라서 함수를 다음과 같이 선언 할 수 있습니다: +따라서 함수를 다음과 같이 선언할 수 있습니다: -{* ../../docs_src/path_params_numeric_validations/tutorial002.py hl[7] *} +{* ../../docs_src/path_params_numeric_validations/tutorial002_py39.py hl[7] *} -## 필요한 경우 매개변수 정렬하기, 트릭 +하지만 `Annotated`를 사용하면 이 문제가 없다는 점을 기억하세요. `Query()`나 `Path()`에 함수 매개변수 기본값을 사용하지 않기 때문에, 순서는 중요하지 않습니다. -`Query`나 아무런 기본값으로도 `q` 경로 매개변수를 선언하고 싶지 않지만 `Path`를 사용하여 경로 매개변수를 `item_id` 다른 순서로 선언하고 싶다면, 파이썬은 이를 위한 작고 특별한 문법이 있습니다. +{* ../../docs_src/path_params_numeric_validations/tutorial002_an_py39.py *} -`*`를 함수의 첫 번째 매개변수로 전달하세요. +## 필요한 대로 매개변수 정렬하기, 트릭 { #order-the-parameters-as-you-need-tricks } -파이썬은 `*`으로 아무런 행동도 하지 않지만, 따르는 매개변수들은 kwargs로도 알려진 키워드 인자(키-값 쌍)여야 함을 인지합니다. 기본값을 가지고 있지 않더라도 그렇습니다. +/// tip | 팁 -{* ../../docs_src/path_params_numeric_validations/tutorial003.py hl[7] *} +`Annotated`를 사용한다면 이것은 아마 그렇게 중요하지 않거나 필요하지 않을 수 있습니다. -## 숫자 검증: 크거나 같음 +/// -`Query`와 `Path`(나중에 볼 다른 것들도)를 사용하여 문자열 뿐만 아니라 숫자의 제약을 선언할 수 있습니다. +유용할 수 있는 **작은 트릭**이 하나 있지만, 자주 필요하진 않을 겁니다. -여기서 `ge=1`인 경우, `item_id`는 `1`보다 "크거나(`g`reater) 같은(`e`qual)" 정수형 숫자여야 합니다. +만약 다음을 원한다면: -{* ../../docs_src/path_params_numeric_validations/tutorial004.py hl[8] *} +* `Query`나 어떤 기본값 없이 쿼리 매개변수 `q`를 선언하기 +* `Path`를 사용해서 경로 매개변수 `item_id`를 선언하기 +* 이들을 다른 순서로 두기 +* `Annotated`를 사용하지 않기 -## 숫자 검증: 크거나 같음 및 작거나 같음 +...이를 위해 파이썬에는 작은 특별한 문법이 있습니다. + +함수의 첫 번째 매개변수로 `*`를 전달하세요. + +파이썬은 `*`으로 아무것도 하지 않지만, 뒤따르는 모든 매개변수는 키워드 인자(키-값 쌍)로 호출되어야 함을 알게 됩니다. 이는 kwargs로도 알려져 있습니다. 기본값이 없더라도 마찬가지입니다. + +{* ../../docs_src/path_params_numeric_validations/tutorial003_py39.py hl[7] *} + +### `Annotated`를 쓰면 더 좋습니다 { #better-with-annotated } + +`Annotated`를 사용하면 함수 매개변수 기본값을 사용하지 않기 때문에 이 문제가 발생하지 않으며, 아마 `*`도 사용할 필요가 없다는 점을 기억하세요. + +{* ../../docs_src/path_params_numeric_validations/tutorial003_an_py39.py hl[10] *} + +## 숫자 검증: 크거나 같음 { #number-validations-greater-than-or-equal } + +`Query`와 `Path`(그리고 나중에 볼 다른 것들)를 사용하여 숫자 제약을 선언할 수 있습니다. + +여기서 `ge=1`인 경우, `item_id`는 `1`보다 "`g`reater than or `e`qual"(크거나 같은) 정수형 숫자여야 합니다. + +{* ../../docs_src/path_params_numeric_validations/tutorial004_an_py39.py hl[10] *} + +## 숫자 검증: 크거나 및 작거나 같음 { #number-validations-greater-than-and-less-than-or-equal } 동일하게 적용됩니다: -* `gt`: 크거나(`g`reater `t`han) -* `le`: 작거나 같은(`l`ess than or `e`qual) +* `gt`: `g`reater `t`han +* `le`: `l`ess than or `e`qual -{* ../../docs_src/path_params_numeric_validations/tutorial005.py hl[9] *} +{* ../../docs_src/path_params_numeric_validations/tutorial005_an_py39.py hl[10] *} -## 숫자 검증: 부동소수, 크거나 및 작거나 +## 숫자 검증: 부동소수, 크거나 및 작거나 { #number-validations-floats-greater-than-and-less-than } 숫자 검증은 `float` 값에도 동작합니다. -여기에서 ge뿐만 아니라 gt를 선언 할 수있는 것이 중요해집니다. 예를 들어 필요한 경우, 값이 `1`보다 작더라도 반드시 `0`보다 커야합니다. +여기에서 gt를, ge뿐만 아니라 선언할 수 있다는 점이 중요해집니다. 예를 들어 값이 `1`보다 작더라도, 반드시 `0`보다 커야 한다고 요구할 수 있습니다. 즉, `0.5`는 유효한 값입니다. 그러나 `0.0` 또는 `0`은 그렇지 않습니다. lt 역시 마찬가지입니다. -{* ../../docs_src/path_params_numeric_validations/tutorial006.py hl[11] *} +{* ../../docs_src/path_params_numeric_validations/tutorial006_an_py39.py hl[13] *} -## 요약 +## 요약 { #recap } `Query`, `Path`(아직 보지 못한 다른 것들도)를 사용하면 [쿼리 매개변수와 문자열 검증](query-params-str-validations.md){.internal-link target=_blank}에서와 마찬가지로 메타데이터와 문자열 검증을 선언할 수 있습니다. 그리고 숫자 검증 또한 선언할 수 있습니다: -* `gt`: 크거나(`g`reater `t`han) -* `ge`: 크거나 같은(`g`reater than or `e`qual) -* `lt`: 작거나(`l`ess `t`han) -* `le`: 작거나 같은(`l`ess than or `e`qual) +* `gt`: `g`reater `t`han +* `ge`: `g`reater than or `e`qual +* `lt`: `l`ess `t`han +* `le`: `l`ess than or `e`qual /// info | 정보 -`Query`, `Path`, 그리고 나중에게 보게될 것들은 (여러분이 사용할 필요가 없는) 공통 `Param` 클래스의 서브 클래스입니다. +`Query`, `Path`, 그리고 나중에 보게 될 다른 클래스들은 공통 `Param` 클래스의 서브클래스입니다. -그리고 이들 모두는 여태까지 본 추가 검증과 메타데이터의 동일한 모든 매개변수를 공유합니다. +이들 모두는 여러분이 본 추가 검증과 메타데이터에 대한 동일한 매개변수를 공유합니다. /// /// note | 기술 세부사항 -`fastapi`에서 `Query`, `Path` 등을 임포트 할 때, 이것들은 실제로 함수입니다. +`fastapi`에서 `Query`, `Path` 등을 임포트할 때, 이것들은 실제로 함수입니다. 호출되면 동일한 이름의 클래스의 인스턴스를 반환합니다. 즉, 함수인 `Query`를 임포트한 겁니다. 그리고 호출하면 `Query`라는 이름을 가진 클래스의 인스턴스를 반환합니다. -편집기에서 타입에 대한 오류를 표시하지 않도록 하기 위해 (클래스를 직접 사용하는 대신) 이러한 함수들이 있습니다. +이 함수들이 있는 이유는(클래스를 직접 사용하는 대신) 편집기에서 타입에 대한 오류를 표시하지 않도록 하기 위해서입니다. 이렇게 하면 오류를 무시하기 위한 사용자 설정을 추가하지 않고도 일반 편집기와 코딩 도구를 사용할 수 있습니다. diff --git a/docs/ko/docs/tutorial/path-params.md b/docs/ko/docs/tutorial/path-params.md index b72787e0b2..ea5170eccb 100644 --- a/docs/ko/docs/tutorial/path-params.md +++ b/docs/ko/docs/tutorial/path-params.md @@ -1,8 +1,8 @@ -# 경로 매개변수 +# 경로 매개변수 { #path-parameters } 파이썬의 포맷 문자열 리터럴에서 사용되는 문법을 이용하여 경로 "매개변수" 또는 "변수"를 선언할 수 있습니다: -{* ../../docs_src/path_params/tutorial001.py hl[6:7] *} +{* ../../docs_src/path_params/tutorial001_py39.py hl[6:7] *} 경로 매개변수 `item_id`의 값은 함수의 `item_id` 인자로 전달됩니다. @@ -12,11 +12,11 @@ {"item_id":"foo"} ``` -## 타입이 있는 매개변수 +## 타입이 있는 경로 매개변수 { #path-parameters-with-types } 파이썬 표준 타입 어노테이션을 사용하여 함수에 있는 경로 매개변수의 타입을 선언할 수 있습니다: -{* ../../docs_src/path_params/tutorial002.py hl[7] *} +{* ../../docs_src/path_params/tutorial002_py39.py hl[7] *} 위의 예시에서, `item_id`는 `int`로 선언되었습니다. @@ -26,7 +26,7 @@ /// -## 데이터 변환 +## 데이터 변환 { #data-conversion } 이 예제를 실행하고 http://127.0.0.1:8000/items/3을 열면, 다음 응답을 볼 수 있습니다: @@ -42,40 +42,41 @@ /// -## 데이터 검증 +## 데이터 검증 { #data-validation } -하지만 브라우저에서 http://127.0.0.1:8000/items/foo로 이동하면, HTTP 오류가 잘 뜨는 것을 확인할 수 있습니다: +하지만 브라우저에서 http://127.0.0.1:8000/items/foo로 이동하면, 다음과 같은 HTTP 오류를 볼 수 있습니다: ```JSON { - "detail": [ - { - "loc": [ - "path", - "item_id" - ], - "msg": "value is not a valid integer", - "type": "type_error.integer" - } - ] + "detail": [ + { + "type": "int_parsing", + "loc": [ + "path", + "item_id" + ], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "foo" + } + ] } ``` -경로 매개변수 `item_id`는 `int`가 아닌 `"foo"` 값이기 때문입니다. +경로 매개변수 `item_id`가 `int`가 아닌 `"foo"` 값을 가졌기 때문입니다. -`int`가 아닌 `float`을 전달하는 경우에도 동일한 오류가 나타납니다: http://127.0.0.1:8000/items/4.2 +`int` 대신 `float`을 제공하면(예: http://127.0.0.1:8000/items/4.2) 동일한 오류가 나타납니다. /// check | 확인 즉, 파이썬 타입 선언을 하면 **FastAPI**는 데이터 검증을 합니다. -오류에는 정확히 어느 지점에서 검증을 통과하지 못했는지 명시됩니다. +또한 오류에는 검증을 통과하지 못한 지점이 정확히 명시됩니다. 이는 API와 상호 작용하는 코드를 개발하고 디버깅하는 데 매우 유용합니다. /// -## 문서화 +## 문서화 { #documentation } 그리고 브라우저에서 http://127.0.0.1:8000/docs를 열면, 다음과 같이 자동 대화식 API 문서를 볼 수 있습니다: @@ -83,23 +84,23 @@ /// check | 확인 -그저 파이썬 타입 선언을 하기만 하면 **FastAPI**는 자동 대화형 API 문서(Swagger UI)를 제공합니다. +다시 한 번, 동일한 파이썬 타입 선언만으로 **FastAPI**는 자동 대화형 문서(Swagger UI 통합)를 제공합니다. -경로 매개변수가 정수형으로 명시된 것을 확인할 수 있습니다. +경로 매개변수가 정수형으로 선언된 것을 확인할 수 있습니다. /// -## 표준 기반의 이점, 대체 문서 +## 표준 기반의 이점, 대체 문서 { #standards-based-benefits-alternative-documentation } -그리고 생성된 스키마는 OpenAPI 표준에서 나온 것이기 때문에 호환되는 도구가 많이 있습니다. +그리고 생성된 스키마는 OpenAPI 표준에서 나온 것이기 때문에 호환되는 도구가 많이 있습니다. -이 덕분에 **FastAPI**는 http://127.0.0.1:8000/redoc로 접속할 수 있는 (ReDoc을 사용하는) 대체 API 문서를 제공합니다: +이 덕분에 **FastAPI** 자체에서 http://127.0.0.1:8000/redoc로 접속할 수 있는 (ReDoc을 사용하는) 대체 API 문서를 제공합니다: 이와 마찬가지로 다양한 언어에 대한 코드 생성 도구를 포함하여 여러 호환되는 도구가 있습니다. -## Pydantic +## Pydantic { #pydantic } 모든 데이터 검증은 Pydantic에 의해 내부적으로 수행되므로 이로 인한 이점을 모두 얻을 수 있습니다. 여러분은 관리를 잘 받고 있음을 느낄 수 있습니다. @@ -107,25 +108,31 @@ 이 중 몇 가지는 자습서의 다음 장에 설명되어 있습니다. -## 순서 문제 +## 순서 문제 { #order-matters } -*경로 작동*을 만들때 고정 경로를 갖고 있는 상황들을 맞닥뜨릴 수 있습니다. +*경로 처리*를 만들 때 고정 경로를 갖고 있는 상황들을 맞닥뜨릴 수 있습니다. `/users/me`처럼, 현재 사용자의 데이터를 가져온다고 합시다. 사용자 ID를 이용해 특정 사용자의 정보를 가져오는 경로 `/users/{user_id}`도 있습니다. -*경로 작동*은 순차적으로 실행되기 때문에 `/users/{user_id}` 이전에 `/users/me`를 먼저 선언해야 합니다: +*경로 처리*는 순차적으로 평가되기 때문에 `/users/{user_id}` 이전에 `/users/me`에 대한 경로가 먼저 선언되었는지 확인해야 합니다: -{* ../../docs_src/path_params/tutorial003.py hl[6,11] *} +{* ../../docs_src/path_params/tutorial003_py39.py hl[6,11] *} -그렇지 않으면 `/users/{user_id}`는 `/users/me` 요청 또한 매개변수 `user_id`의 값이 `"me"`인 것으로 "생각하게" 됩니다. +그렇지 않으면 `/users/{user_id}`에 대한 경로가 `/users/me`에도 매칭되어, 매개변수 `user_id`에 `"me"` 값이 들어왔다고 "생각하게" 됩니다. -## 사전정의 값 +마찬가지로, 경로 처리를 재정의할 수는 없습니다: -만약 *경로 매개변수*를 받는 *경로 작동*이 있지만, *경로 매개변수*로 가능한 값들을 미리 정의하고 싶다면 파이썬 표준 `Enum`을 사용할 수 있습니다. +{* ../../docs_src/path_params/tutorial003b_py39.py hl[6,11] *} -### `Enum` 클래스 생성 +경로가 먼저 매칭되기 때문에 첫 번째 것이 항상 사용됩니다. + +## 사전정의 값 { #predefined-values } + +만약 *경로 매개변수*를 받는 *경로 처리*가 있지만, 가능한 유효한 *경로 매개변수* 값들을 미리 정의하고 싶다면 파이썬 표준 `Enum`을 사용할 수 있습니다. + +### `Enum` 클래스 생성 { #create-an-enum-class } `Enum`을 임포트하고 `str`과 `Enum`을 상속하는 서브 클래스를 만듭니다. @@ -133,47 +140,41 @@ 가능한 값들에 해당하는 고정된 값의 클래스 어트리뷰트들을 만듭니다: -{* ../../docs_src/path_params/tutorial005.py hl[1,6:9] *} - -/// info | 정보 - -열거형(또는 enums)은 파이썬 버전 3.4 이후로 사용 가능합니다. - -/// +{* ../../docs_src/path_params/tutorial005_py39.py hl[1,6:9] *} /// tip | 팁 -혹시 궁금하다면, "AlexNet", "ResNet", 그리고 "LeNet"은 그저 기계 학습 모델들의 이름입니다. +혹시 궁금하다면, "AlexNet", "ResNet", 그리고 "LeNet"은 그저 머신 러닝 모델들의 이름입니다. /// -### *경로 매개변수* 선언 +### *경로 매개변수* 선언 { #declare-a-path-parameter } 생성한 열거형 클래스(`ModelName`)를 사용하는 타입 어노테이션으로 *경로 매개변수*를 만듭니다: -{* ../../docs_src/path_params/tutorial005.py hl[16] *} +{* ../../docs_src/path_params/tutorial005_py39.py hl[16] *} -### 문서 확인 +### 문서 확인 { #check-the-docs } *경로 매개변수*에 사용할 수 있는 값은 미리 정의되어 있으므로 대화형 문서에서 잘 표시됩니다: -### 파이썬 *열거형*으로 작업하기 +### 파이썬 *열거형*으로 작업하기 { #working-with-python-enumerations } *경로 매개변수*의 값은 *열거형 멤버*가 됩니다. -#### *열거형 멤버* 비교 +#### *열거형 멤버* 비교 { #compare-enumeration-members } -열거형 `ModelName`의 *열거형 멤버*를 비교할 수 있습니다: +생성한 열거형 `ModelName`의 *열거형 멤버*와 비교할 수 있습니다: -{* ../../docs_src/path_params/tutorial005.py hl[17] *} +{* ../../docs_src/path_params/tutorial005_py39.py hl[17] *} -#### *열거형 값* 가져오기 +#### *열거형 값* 가져오기 { #get-the-enumeration-value } `model_name.value` 또는 일반적으로 `your_enum_member.value`를 이용하여 실제 값(위 예시의 경우 `str`)을 가져올 수 있습니다: -{* ../../docs_src/path_params/tutorial005.py hl[20] *} +{* ../../docs_src/path_params/tutorial005_py39.py hl[20] *} /// tip | 팁 @@ -181,15 +182,15 @@ /// -#### *열거형 멤버* 반환 +#### *열거형 멤버* 반환 { #return-enumeration-members } -*경로 작동*에서 *열거형 멤버*를 반환할 수 있습니다. 이는 중첩 JSON 본문(예: `dict`)내의 값으로도 가능합니다. +*경로 처리*에서 *enum 멤버*를 반환할 수 있습니다. 이는 JSON 본문(예: `dict`) 내에 중첩된 형태로도 가능합니다. 클라이언트에 반환하기 전에 해당 값(이 경우 문자열)으로 변환됩니다: -{* ../../docs_src/path_params/tutorial005.py hl[18,21,23] *} +{* ../../docs_src/path_params/tutorial005_py39.py hl[18,21,23] *} -클라이언트는 아래의 JSON 응답을 얻습니다: +클라이언트는 아래와 같은 JSON 응답을 얻게 됩니다: ```JSON { @@ -198,53 +199,53 @@ } ``` -## 경로를 포함하는 경로 매개변수 +## 경로를 포함하는 경로 매개변수 { #path-parameters-containing-paths } -경로를 포함하는 *경로 작동* `/files/{file_path}`이 있다고 해봅시다. +경로 `/files/{file_path}`를 가진 *경로 처리*가 있다고 해봅시다. -그런데 이 경우 `file_path` 자체가 `home/johndoe/myfile.txt`와 같은 경로를 포함해야 합니다. +하지만 `file_path` 자체가 `home/johndoe/myfile.txt`와 같은 *경로*를 포함해야 합니다. 이때 해당 파일의 URL은 다음처럼 됩니다: `/files/home/johndoe/myfile.txt`. -### OpenAPI 지원 +### OpenAPI 지원 { #openapi-support } 테스트와 정의가 어려운 시나리오로 이어질 수 있으므로 OpenAPI는 *경로*를 포함하는 *경로 매개변수*를 내부에 선언하는 방법을 지원하지 않습니다. -그럼에도 Starlette의 내부 도구중 하나를 사용하여 **FastAPI**에서는 이가 가능합니다. +그럼에도 Starlette의 내부 도구 중 하나를 사용하여 **FastAPI**에서는 이가 가능합니다. -문서에 매개변수에 경로가 포함되어야 한다는 정보가 명시되지는 않지만 여전히 작동합니다. +또한 문서가 여전히 동작하긴 하지만, 매개변수에 경로가 포함되어야 한다는 내용을 추가로 문서화하지는 않습니다. -### 경로 변환기 +### 경로 변환기 { #path-convertor } -Starlette의 옵션을 직접 이용하여 다음과 같은 URL을 사용함으로써 *path*를 포함하는 *경로 매개변수*를 선언할 수 있습니다: +Starlette의 옵션을 직접 이용하여 다음과 같은 URL을 사용함으로써 *경로*를 포함하는 *경로 매개변수*를 선언할 수 있습니다: ``` /files/{file_path:path} ``` -이러한 경우 매개변수의 이름은 `file_path`이며, 마지막 부분 `:path`는 매개변수가 *경로*와 일치해야 함을 명시합니다. +이러한 경우 매개변수의 이름은 `file_path`이며, 마지막 부분 `:path`는 매개변수가 어떤 *경로*와도 매칭되어야 함을 의미합니다. 따라서 다음과 같이 사용할 수 있습니다: -{* ../../docs_src/path_params/tutorial004.py hl[6] *} +{* ../../docs_src/path_params/tutorial004_py39.py hl[6] *} /// tip | 팁 -매개변수가 가져야 하는 값이 `/home/johndoe/myfile.txt`와 같이 슬래시로 시작(`/`)해야 할 수 있습니다. +매개변수가 선행 슬래시(`/`)가 있는 `/home/johndoe/myfile.txt`를 포함해야 할 수도 있습니다. -이 경우 URL은: `/files//home/johndoe/myfile.txt`이며 `files`과 `home` 사이에 이중 슬래시(`//`)가 생깁니다. +그 경우 URL은: `/files//home/johndoe/myfile.txt`이며 `files`와 `home` 사이에 이중 슬래시(`//`)가 생깁니다. /// -## 요약 +## 요약 { #recap } **FastAPI**를 이용하면 짧고 직관적인 표준 파이썬 타입 선언을 사용하여 다음을 얻을 수 있습니다: * 편집기 지원: 오류 검사, 자동완성 등 -* 데이터 "파싱" +* 데이터 "parsing" * 데이터 검증 * API 주석(Annotation)과 자동 문서 -단 한번의 선언만으로 위 사항들을 모두 선언할 수 있습니다. +그리고 한 번만 선언하면 됩니다. 이는 대체 프레임워크와 비교했을 때 (엄청나게 빠른 성능 외에도) **FastAPI**의 주요한 장점일 것입니다. diff --git a/docs/ko/docs/tutorial/query-param-models.md b/docs/ko/docs/tutorial/query-param-models.md index 2ca65a3319..d354c56c3d 100644 --- a/docs/ko/docs/tutorial/query-param-models.md +++ b/docs/ko/docs/tutorial/query-param-models.md @@ -1,48 +1,48 @@ -# 쿼리 매개변수 모델 +# 쿼리 매개변수 모델 { #query-parameter-models } -연관된 쿼리 **매개변수** 그룹이 있다면 **Pydantic 모델** 을 사용해 선언할 수 있습니다. +연관된 **쿼리 매개변수** 그룹이 있다면 이를 선언하기 위해 **Pydantic 모델**을 생성할 수 있습니다. 이렇게 하면 **여러 곳**에서 **모델을 재사용**할 수 있을 뿐만 아니라, 매개변수에 대한 검증 및 메타데이터도 한 번에 선언할 수 있습니다. 😎 /// note | 참고 -이 기능은 FastAPI 버전 `0.115.0`부터 제공됩니다. 🤓 +이 기능은 FastAPI 버전 `0.115.0`부터 지원됩니다. 🤓 /// -## 쿼리 매개변수와 Pydantic 모델 +## Pydantic 모델과 쿼리 매개변수 { #query-parameters-with-a-pydantic-model } -필요한 **쿼리 매개변수**를 **Pydantic 모델** 안에 선언한 다음, 모델을 `Query`로 선언합니다. +필요한 **쿼리 매개변수**를 **Pydantic 모델** 안에 선언한 다음, 매개변수를 `Query`로 선언합니다: {* ../../docs_src/query_param_models/tutorial001_an_py310.py hl[9:13,17] *} -**FastAPI**는 요청의 **쿼리 매개변수**에서 **각 필드**의 데이터를 **추출**해 정의한 Pydantic 모델로 제공합니다. +**FastAPI**는 요청의 **쿼리 매개변수**에서 **각 필드**의 데이터를 **추출**해 정의한 Pydantic 모델을 제공합니다. -## 문서 확인하기 +## 문서 확인하기 { #check-the-docs } -`/docs` 경로의 API 문서에서 매개변수를 확인할 수 있습니다. +`/docs`의 문서 UI에서 쿼리 매개변수를 확인할 수 있습니다:
-## 추가 쿼리 매개변수 금지 +## 추가 쿼리 매개변수 금지 { #forbid-extra-query-parameters } -몇몇의 특이한 경우에 (흔치 않지만), 허용할 쿼리 매개변수를 **제한**해야할 수 있습니다. +몇몇의 특이한 경우에 (흔치 않지만), 받으려는 쿼리 매개변수를 **제한**하고 싶을 수 있습니다. -Pydantic 모델 설정에서 `extra` 필드를 `forbid` 로 설정할 수 있습니다. +Pydantic의 모델 설정을 사용해 어떤 `extra` 필드도 `forbid`할 수 있습니다: {* ../../docs_src/query_param_models/tutorial002_an_py310.py hl[10] *} -만약 클라이언트가 쿼리 매개변수로 **추가적인** 데이터를 보내려고 하면, 클라이언트는 **에러** 응답을 받게 됩니다. +클라이언트가 **쿼리 매개변수**로 **추가적인** 데이터를 보내려고 하면 **에러** 응답을 받게 됩니다. -예를 들어, 아래와 같이 만약 클라이언트가 `tool` 쿼리 매개변수에 `plumbus` 라는 값을 추가해서 보내려고 하면, +예를 들어, 아래와 같이 클라이언트가 `tool` 쿼리 매개변수에 `plumbus` 값을 보내려고 하면: ```http https://example.com/items/?limit=10&tool=plumbus ``` -클라이언트는 쿼리 매개변수 `tool` 이 허용되지 않는다는 **에러** 응답을 받게 됩니다. +쿼리 매개변수 `tool`이 허용되지 않는다는 **에러** 응답을 받게 됩니다: ```json { @@ -57,12 +57,12 @@ https://example.com/items/?limit=10&tool=plumbus } ``` -## 요약 +## 요약 { #summary } -**FastAPI** 에서 **쿼리 매개변수** 를 선언할 때 **Pydantic 모델** 을 사용할 수 있습니다. 😎 +**FastAPI**에서 **쿼리 매개변수**를 선언할 때 **Pydantic 모델**을 사용할 수 있습니다. 😎 /// tip | 팁 -스포일러 경고: Pydantic 모델을 쿠키와 헤더에도 적용할 수 있습니다. 이에 대해서는 이후 튜토리얼에서 다룰 예정입니다. 🤫 +스포일러 경고: Pydantic 모델을 쿠키와 헤더에도 사용할 수 있지만, 이에 대해서는 이후 튜토리얼에서 읽게 될 것입니다. 🤫 /// diff --git a/docs/ko/docs/tutorial/query-params-str-validations.md b/docs/ko/docs/tutorial/query-params-str-validations.md index f2ca453ac5..68824932ee 100644 --- a/docs/ko/docs/tutorial/query-params-str-validations.md +++ b/docs/ko/docs/tutorial/query-params-str-validations.md @@ -1,118 +1,226 @@ -# 쿼리 매개변수와 문자열 검증 +# 쿼리 매개변수와 문자열 검증 { #query-parameters-and-string-validations } **FastAPI**를 사용하면 매개변수에 대한 추가 정보 및 검증을 선언할 수 있습니다. 이 응용 프로그램을 예로 들어보겠습니다: -{* ../../docs_src/query_params_str_validations/tutorial001.py hl[9] *} +{* ../../docs_src/query_params_str_validations/tutorial001_py310.py hl[7] *} -쿼리 매개변수 `q`는 `Optional[str]` 자료형입니다. 즉, `str` 자료형이지만 `None` 역시 될 수 있음을 뜻하고, 실제로 기본값은 `None`이기 때문에 FastAPI는 이 매개변수가 필수가 아니라는 것을 압니다. +쿼리 매개변수 `q`는 `str | None` 자료형입니다. 즉, `str` 자료형이지만 `None` 역시 될 수 있음을 뜻하고, 실제로 기본값은 `None`이기 때문에 FastAPI는 이 매개변수가 필수가 아니라는 것을 압니다. /// note | 참고 FastAPI는 `q`의 기본값이 `= None`이기 때문에 필수가 아님을 압니다. -`Optional[str]`에 있는 `Optional`은 FastAPI가 사용하는게 아니지만, 편집기에게 더 나은 지원과 오류 탐지를 제공하게 해줍니다. +`str | None`을 사용하면 편집기가 더 나은 지원과 오류 탐지를 제공하게 해줍니다. /// -## 추가 검증 +## 추가 검증 { #additional-validation } -`q`가 선택적이지만 값이 주어질 때마다 **값이 50 글자를 초과하지 않게** 강제하려 합니다. +`q`가 선택적이지만 값이 주어질 때마다 **길이가 50자를 초과하지 않게** 강제하려 합니다. -### `Query` 임포트 +### `Query`와 `Annotated` 임포트 { #import-query-and-annotated } -이를 위해 먼저 `fastapi`에서 `Query`를 임포트합니다: +이를 위해 먼저 다음을 임포트합니다: -{* ../../docs_src/query_params_str_validations/tutorial002.py hl[3] *} +* `fastapi`에서 `Query` +* `typing`에서 `Annotated` -## 기본값으로 `Query` 사용 +{* ../../docs_src/query_params_str_validations/tutorial002_an_py310.py hl[1,3] *} -이제 `Query`를 매개변수의 기본값으로 사용하여 `max_length` 매개변수를 50으로 설정합니다: +/// info | 정보 -{* ../../docs_src/query_params_str_validations/tutorial002.py hl[9] *} +FastAPI는 0.95.0 버전에서 `Annotated` 지원을 추가했고(그리고 이를 권장하기 시작했습니다). -기본값 `None`을 `Query(None)`으로 바꿔야 하므로, `Query`의 첫 번째 매개변수는 기본값을 정의하는 것과 같은 목적으로 사용됩니다. +이전 버전을 사용하면 `Annotated`를 사용하려고 할 때 오류가 발생합니다. + +`Annotated`를 사용하기 전에 최소 0.95.1 버전으로 [FastAPI 버전 업그레이드](../deployment/versions.md#upgrading-the-fastapi-versions){.internal-link target=_blank}를 진행하세요. + +/// + +## `q` 매개변수의 타입에 `Annotated` 사용하기 { #use-annotated-in-the-type-for-the-q-parameter } + +이전에 [Python Types Intro](../python-types.md#type-hints-with-metadata-annotations){.internal-link target=_blank}에서 `Annotated`를 사용해 매개변수에 메타데이터를 추가할 수 있다고 말씀드린 것을 기억하시나요? + +이제 FastAPI에서 사용할 차례입니다. 🚀 + +다음과 같은 타입 어노테이션이 있었습니다: + +//// tab | Python 3.10+ + +```Python +q: str | None = None +``` + +//// + +//// tab | Python 3.9+ + +```Python +q: Union[str, None] = None +``` + +//// + +여기서 `Annotated`로 감싸서 다음과 같이 만듭니다: + +//// tab | Python 3.10+ + +```Python +q: Annotated[str | None] = None +``` + +//// + +//// tab | Python 3.9+ + +```Python +q: Annotated[Union[str, None]] = None +``` + +//// + +두 버전 모두 같은 의미로, `q`는 `str` 또는 `None`이 될 수 있는 매개변수이며 기본값은 `None`입니다. + +이제 재미있는 부분으로 넘어가 봅시다. 🎉 + +## `q` 매개변수의 `Annotated`에 `Query` 추가하기 { #add-query-to-annotated-in-the-q-parameter } + +이제 이 `Annotated`에 더 많은 정보를 넣을 수 있으므로(이 경우에는 추가 검증), `Annotated` 안에 `Query`를 추가하고 `max_length` 매개변수를 `50`으로 설정합니다: + +{* ../../docs_src/query_params_str_validations/tutorial002_an_py310.py hl[9] *} + +기본값은 여전히 `None`이므로, 매개변수는 여전히 선택적입니다. + +하지만 `Annotated` 안에 `Query(max_length=50)`를 넣음으로써, 이 값에 대해 **추가 검증**을 적용하고 최대 50자까지만 허용하도록 FastAPI에 알려줍니다. 😎 + +/// tip | 팁 + +여기서는 **쿼리 매개변수**이기 때문에 `Query()`를 사용합니다. 나중에 `Path()`, `Body()`, `Header()`, `Cookie()`와 같이 `Query()`와 동일한 인자를 받는 것들도 보게 될 것입니다. + +/// + +이제 FastAPI는 다음을 수행합니다: + +* 최대 길이가 50자인지 확인하도록 데이터를 **검증**합니다 +* 데이터가 유효하지 않을 때 클라이언트에게 **명확한 오류**를 보여줍니다 +* OpenAPI 스키마 *경로 처리*에 매개변수를 **문서화**합니다(따라서 **자동 문서 UI**에 표시됩니다) + +## 대안(이전 방식): 기본값으로 `Query` 사용 { #alternative-old-query-as-the-default-value } + +이전 FastAPI 버전(0.95.0 이전)에서는 `Annotated`에 넣는 대신, 매개변수의 기본값으로 `Query`를 사용해야 했습니다. 주변에서 이 방식을 사용하는 코드를 볼 가능성이 높기 때문에 설명해 드리겠습니다. + +/// tip | 팁 + +새 코드를 작성할 때와 가능할 때는 위에서 설명한 대로 `Annotated`를 사용하세요. 여러 장점이 있고(아래에서 설명합니다) 단점은 없습니다. 🍰 + +/// + +다음은 함수 매개변수의 기본값으로 `Query()`를 사용하면서 `max_length`를 50으로 설정하는 방법입니다: + +{* ../../docs_src/query_params_str_validations/tutorial002_py310.py hl[7] *} + +이 경우(`Annotated`를 사용하지 않는 경우) 함수에서 기본값 `None`을 `Query()`로 바꿔야 하므로, 이제 `Query(default=None)`로 기본값을 설정해야 합니다. (최소한 FastAPI 입장에서는) 이 인자는 해당 기본값을 정의하는 것과 같은 목적을 수행합니다. 그러므로: ```Python -q: Optional[str] = Query(None) +q: str | None = Query(default=None) ``` -...위 코드는 아래와 동일하게 매개변수를 선택적으로 만듭니다: +...위 코드는 기본값이 `None`인 선택적 매개변수를 만들며, 아래와 동일합니다: + ```Python -q: Optional[str] = None +q: str | None = None ``` -하지만 명시적으로 쿼리 매개변수를 선언합니다. +하지만 `Query` 버전은 이것이 쿼리 매개변수임을 명시적으로 선언합니다. -/// info | 정보 - -FastAPI는 다음 부분에 관심이 있습니다: +그 다음, `Query`로 더 많은 매개변수를 전달할 수 있습니다. 지금의 경우 문자열에 적용되는 `max_length` 매개변수입니다: ```Python -= None +q: str | None = Query(default=None, max_length=50) ``` -또는: +이는 데이터를 검증할 것이고, 데이터가 유효하지 않다면 명백한 오류를 보여주며, OpenAPI 스키마 *경로 처리*에 매개변수를 문서화 합니다. + +### 기본값으로 `Query` 사용 또는 `Annotated`에 넣기 { #query-as-the-default-value-or-in-annotated } + +`Annotated` 안에서 `Query`를 사용할 때는 `Query`에 `default` 매개변수를 사용할 수 없다는 점을 기억하세요. + +대신 함수 매개변수의 실제 기본값을 사용하세요. 그렇지 않으면 일관성이 깨집니다. + +예를 들어, 다음은 허용되지 않습니다: ```Python -= Query(None) +q: Annotated[str, Query(default="rick")] = "morty" ``` -그리고 `None`을 사용하여 쿼라 매개변수가 필수적이지 않다는 것을 파악합니다. +...왜냐하면 기본값이 `"rick"`인지 `"morty"`인지 명확하지 않기 때문입니다. -`Optional` 부분은 편집기에게 더 나은 지원을 제공하기 위해서만 사용됩니다. - -/// - -또한 `Query`로 더 많은 매개변수를 전달할 수 있습니다. 지금의 경우 문자열에 적용되는 `max_length` 매개변수입니다: +따라서 (가능하면) 다음과 같이 사용합니다: ```Python -q: str = Query(None, max_length=50) +q: Annotated[str, Query()] = "rick" ``` -이는 데이터를 검증할 것이고, 데이터가 유효하지 않다면 명백한 오류를 보여주며, OpenAPI 스키마 *경로 작동*에 매개변수를 문서화 합니다. +...또는 오래된 코드베이스에서는 다음과 같은 코드를 찾게 될 것입니다: -## 검증 추가 +```Python +q: str = Query(default="rick") +``` -매개변수 `min_length` 또한 추가할 수 있습니다: +### `Annotated`의 장점 { #advantages-of-annotated } -{* ../../docs_src/query_params_str_validations/tutorial003.py hl[9] *} +함수 매개변수의 기본값 방식 대신 **`Annotated`를 사용하는 것을 권장**합니다. 여러 이유로 **더 좋기** 때문입니다. 🤓 -## 정규식 추가 +**함수 매개변수**의 **기본값**이 **실제 기본값**이 되므로, 전반적으로 Python에 더 직관적입니다. 😌 -매개변수와 일치해야 하는 정규표현식을 정의할 수 있습니다: +FastAPI 없이도 **다른 곳에서** 같은 함수를 **호출**할 수 있고, **예상대로 동작**합니다. **필수** 매개변수(기본값이 없는 경우)가 있다면 **편집기**가 오류로 알려줄 것이고, 필수 매개변수를 전달하지 않고 실행하면 **Python**도 오류를 냅니다. -{* ../../docs_src/query_params_str_validations/tutorial004.py hl[10] *} +`Annotated`를 사용하지 않고 **(이전) 기본값 스타일**을 사용하면, FastAPI 없이 **다른 곳에서** 함수를 호출할 때도 제대로 동작하도록 함수에 인자를 전달해야 한다는 것을 **기억**해야 합니다. 그렇지 않으면 값이 기대와 다르게 됩니다(예: `str` 대신 `QueryInfo` 같은 것). 그리고 편집기도 경고하지 않고 Python도 그 함수를 실행할 때는 불평하지 않으며, 오직 내부 동작에서 오류가 발생할 때만 문제가 드러납니다. -이 특정 정규표현식은 전달 받은 매개변수 값을 검사합니다: +`Annotated`는 하나 이상의 메타데이터 어노테이션을 가질 수 있기 때문에, 이제 Typer 같은 다른 도구에서도 같은 함수를 사용할 수 있습니다. 🚀 -* `^`: 이전에 문자가 없고 뒤따르는 문자로 시작합니다. -* `fixedquery`: 정확히 `fixedquery` 값을 갖습니다. -* `$`: 여기서 끝나고 `fixedquery` 이후로 아무 문자도 갖지 않습니다. +## 검증 더 추가하기 { #add-more-validations } -**"정규표현식"** 개념에 대해 상실감을 느꼈다면 걱정하지 않아도 됩니다. 많은 사람에게 어려운 주제입니다. 아직은 정규표현식 없이도 많은 작업들을 할 수 있습니다. +`min_length` 매개변수도 추가할 수 있습니다: -하지만 언제든지 가서 배울수 있고, **FastAPI**에서 직접 사용할 수 있다는 사실을 알고 있어야 합니다. +{* ../../docs_src/query_params_str_validations/tutorial003_an_py310.py hl[10] *} -## 기본값 +## 정규식 추가 { #add-regular-expressions } -기본값으로 사용하는 첫 번째 인자로 `None`을 전달하듯이, 다른 값을 전달할 수 있습니다. +매개변수와 일치해야 하는 정규 표현식 `pattern`을 정의할 수 있습니다: -`min_length`가 `3`이고, 기본값이 `"fixedquery"`인 쿼리 매개변수 `q`를 선언해봅시다: +{* ../../docs_src/query_params_str_validations/tutorial004_an_py310.py hl[11] *} -{* ../../docs_src/query_params_str_validations/tutorial005.py hl[7] *} +이 특정 정규표현식 패턴은 전달 받은 매개변수 값이 다음을 만족하는지 검사합니다: + +* `^`: 뒤따르는 문자로 시작하며, 앞에는 문자가 없습니다. +* `fixedquery`: 정확히 `fixedquery` 값을 가집니다. +* `$`: 여기서 끝나며, `fixedquery` 이후로 더 이상 문자가 없습니다. + +**"정규 표현식"** 개념에 대해 상실감을 느꼈다면 걱정하지 않아도 됩니다. 많은 사람에게 어려운 주제입니다. 아직은 정규 표현식 없이도 많은 작업들을 할 수 있습니다. + +이제 필요할 때 언제든지 **FastAPI**에서 직접 사용할 수 있다는 사실을 알게 되었습니다. + +## 기본값 { #default-values } + +물론 `None`이 아닌 다른 기본값을 사용할 수도 있습니다. + +`q` 쿼리 매개변수에 `min_length`를 `3`으로 설정하고, 기본값을 `"fixedquery"`로 선언하고 싶다고 해봅시다: + +{* ../../docs_src/query_params_str_validations/tutorial005_an_py39.py hl[9] *} /// note | 참고 -기본값을 갖는 것만으로 매개변수는 선택적이 됩니다. +`None`을 포함해 어떤 타입이든 기본값을 가지면 매개변수는 선택적(필수 아님)이 됩니다. /// -## 필수로 만들기 +## 필수 매개변수 { #required-parameters } 더 많은 검증이나 메타데이터를 선언할 필요가 없는 경우, 다음과 같이 기본값을 선언하지 않고 쿼리 매개변수 `q`를 필수로 만들 수 있습니다: @@ -123,42 +231,42 @@ q: str 아래 대신: ```Python -q: Optional[str] = None +q: str | None = None ``` -그러나 이제 다음과 같이 `Query`로 선언합니다: +하지만 이제는 예를 들어 다음과 같이 `Query`로 선언합니다: ```Python -q: Optional[str] = Query(None, min_length=3) +q: Annotated[str | None, Query(min_length=3)] = None ``` -그래서 `Query`를 필수값으로 만들어야 할 때면, 첫 번째 인자로 `...`를 사용할 수 있습니다: +따라서 `Query`를 사용하면서 값을 필수로 선언해야 할 때는, 기본값을 선언하지 않으면 됩니다: -{* ../../docs_src/query_params_str_validations/tutorial006.py hl[7] *} +{* ../../docs_src/query_params_str_validations/tutorial006_an_py39.py hl[9] *} -/// info | 정보 +### 필수지만 `None` 가능 { #required-can-be-none } -이전에 `...`를 본적이 없다면: 특별한 단일값으로, 파이썬의 일부이며 "Ellipsis"라 부릅니다. +매개변수가 `None`을 허용하지만 여전히 필수라고 선언할 수 있습니다. 이렇게 하면 값이 `None`이더라도 클라이언트는 값을 반드시 전송해야 합니다. -/// +이를 위해 `None`이 유효한 타입이라고 선언하되, 기본값은 선언하지 않으면 됩니다: -이렇게 하면 **FastAPI**가 이 매개변수는 필수임을 알 수 있습니다. +{* ../../docs_src/query_params_str_validations/tutorial006c_an_py310.py hl[9] *} -## 쿼리 매개변수 리스트 / 다중값 +## 쿼리 매개변수 리스트 / 다중값 { #query-parameter-list-multiple-values } -쿼리 매개변수를 `Query`와 함께 명시적으로 선언할 때, 값들의 리스트나 다른 방법으로 여러 값을 받도록 선언 할 수도 있습니다. +`Query`로 쿼리 매개변수를 명시적으로 정의할 때 값들의 리스트를 받도록 선언할 수도 있고, 다른 말로 하면 여러 값을 받도록 선언할 수도 있습니다. -예를 들어, URL에서 여러번 나오는 `q` 쿼리 매개변수를 선언하려면 다음과 같이 작성할 수 있습니다: +예를 들어, URL에서 여러 번 나타날 수 있는 `q` 쿼리 매개변수를 선언하려면 다음과 같이 작성할 수 있습니다: -{* ../../docs_src/query_params_str_validations/tutorial011.py hl[9] *} +{* ../../docs_src/query_params_str_validations/tutorial011_an_py310.py hl[9] *} -아래와 같은 URL을 사용합니다: +그 다음, 아래와 같은 URL로: ``` http://localhost:8000/items/?q=foo&q=bar ``` -여러 `q` *쿼리 매개변수* 값들을 (`foo` 및 `bar`) 파이썬 `list`로 *경로 작동 함수* 내 *함수 매개변수* `q`로 전달 받습니다. +여러 `q` *쿼리 매개변수* 값들(`foo` 및 `bar`)을 파이썬 `list`로 *경로 처리 함수*의 *함수 매개변수* `q`에서 받게 됩니다. 따라서 해당 URL에 대한 응답은 다음과 같습니다: @@ -173,7 +281,7 @@ http://localhost:8000/items/?q=foo&q=bar /// tip | 팁 -위의 예와 같이 `list` 자료형으로 쿼리 매개변수를 선언하려면 `Query`를 명시적으로 사용해야 합니다. 그렇지 않으면 요청 본문으로 해석됩니다. +위의 예와 같이 `list` 타입으로 쿼리 매개변수를 선언하려면 `Query`를 명시적으로 사용해야 합니다. 그렇지 않으면 요청 본문으로 해석됩니다. /// @@ -181,19 +289,19 @@ http://localhost:8000/items/?q=foo&q=bar -### 쿼리 매개변수 리스트 / 기본값을 사용하는 다중값 +### 쿼리 매개변수 리스트 / 기본값이 있는 다중값 { #query-parameter-list-multiple-values-with-defaults } -그리고 제공된 값이 없으면 기본 `list` 값을 정의할 수도 있습니다: +제공된 값이 없으면 기본 `list` 값을 정의할 수도 있습니다: -{* ../../docs_src/query_params_str_validations/tutorial012.py hl[9] *} +{* ../../docs_src/query_params_str_validations/tutorial012_an_py39.py hl[9] *} -아래로 이동한다면: +다음으로 이동하면: ``` http://localhost:8000/items/ ``` -`q`의 기본값은: `["foo", "bar"]`이며 응답은 다음이 됩니다: +`q`의 기본값은 `["foo", "bar"]`가 되고, 응답은 다음이 됩니다: ```JSON { @@ -204,21 +312,21 @@ http://localhost:8000/items/ } ``` -#### `list` 사용하기 +#### `list`만 사용하기 { #using-just-list } -`List[str]` 대신 `list`를 직접 사용할 수도 있습니다: +`list[str]` 대신 `list`를 직접 사용할 수도 있습니다: -{* ../../docs_src/query_params_str_validations/tutorial013.py hl[7] *} +{* ../../docs_src/query_params_str_validations/tutorial013_an_py39.py hl[9] *} /// note | 참고 -이 경우 FastAPI는 리스트의 내용을 검사하지 않음을 명심하기 바랍니다. +이 경우 FastAPI는 리스트의 내용을 검사하지 않음을 명심하세요. -예를 들어, `List[int]`는 리스트 내용이 정수인지 검사(및 문서화)합니다. 하지만 `list` 단독일 경우는 아닙니다. +예를 들어, `list[int]`는 리스트 내용이 정수인지 검사(및 문서화)합니다. 하지만 `list` 단독일 경우는 아닙니다. /// -## 더 많은 메타데이터 선언 +## 더 많은 메타데이터 선언 { #declare-more-metadata } 매개변수에 대한 정보를 추가할 수 있습니다. @@ -226,7 +334,7 @@ http://localhost:8000/items/ /// note | 참고 -도구에 따라 OpenAPI 지원 수준이 다를 수 있음을 명심하기 바랍니다. +도구에 따라 OpenAPI 지원 수준이 다를 수 있음을 명심하세요. 일부는 아직 선언된 추가 정보를 모두 표시하지 않을 수 있지만, 대부분의 경우 누락된 기능은 이미 개발 계획이 있습니다. @@ -234,13 +342,13 @@ http://localhost:8000/items/ `title`을 추가할 수 있습니다: -{* ../../docs_src/query_params_str_validations/tutorial007.py hl[10] *} +{* ../../docs_src/query_params_str_validations/tutorial007_an_py310.py hl[10] *} 그리고 `description`도 추가할 수 있습니다: -{* ../../docs_src/query_params_str_validations/tutorial008.py hl[13] *} +{* ../../docs_src/query_params_str_validations/tutorial008_an_py310.py hl[14] *} -## 별칭 매개변수 +## 별칭 매개변수 { #alias-parameters } 매개변수가 `item-query`이길 원한다고 가정해 봅시다. @@ -250,31 +358,99 @@ http://localhost:8000/items/ http://127.0.0.1:8000/items/?item-query=foobaritems ``` -그러나 `item-query`은 유효한 파이썬 변수 이름이 아닙니다. +그러나 `item-query`는 유효한 파이썬 변수 이름이 아닙니다. 가장 가까운 것은 `item_query`일 겁니다. -하지만 정확히`item-query`이길 원합니다... +하지만 정확히 `item-query`이길 원합니다... 이럴 경우 `alias`를 선언할 수 있으며, 해당 별칭은 매개변수 값을 찾는 데 사용됩니다: -{* ../../docs_src/query_params_str_validations/tutorial009.py hl[9] *} +{* ../../docs_src/query_params_str_validations/tutorial009_an_py310.py hl[9] *} -## 매개변수 사용하지 않게 하기 +## 매개변수 사용 중단하기 { #deprecating-parameters } -이제는 더이상 이 매개변수를 마음에 들어하지 않는다고 가정해 봅시다. +이제는 더 이상 이 매개변수를 마음에 들어하지 않는다고 가정해 봅시다. -이 매개변수를 사용하는 클라이언트가 있기 때문에 한동안은 남겨둬야 하지만, 사용되지 않는다(deprecated)고 확실하게 문서에서 보여주고 싶습니다. +이 매개변수를 사용하는 클라이언트가 있기 때문에 한동안은 남겨둬야 하지만, 문서에서 deprecated로 명확하게 보여주고 싶습니다. 그렇다면 `deprecated=True` 매개변수를 `Query`로 전달합니다: -{* ../../docs_src/query_params_str_validations/tutorial010.py hl[18] *} +{* ../../docs_src/query_params_str_validations/tutorial010_an_py310.py hl[19] *} 문서가 아래와 같이 보일겁니다: -## 요약 +## OpenAPI에서 매개변수 제외 { #exclude-parameters-from-openapi } + +생성된 OpenAPI 스키마(따라서 자동 문서화 시스템)에서 쿼리 매개변수를 제외하려면 `Query`의 `include_in_schema` 매개변수를 `False`로 설정하세요: + +{* ../../docs_src/query_params_str_validations/tutorial014_an_py310.py hl[10] *} + +## 커스텀 검증 { #custom-validation } + +위에 나온 매개변수들로는 할 수 없는 **커스텀 검증**이 필요한 경우가 있을 수 있습니다. + +그런 경우에는 일반적인 검증(예: 값이 `str`인지 검증한 뒤) 이후에 적용되는 **커스텀 검증 함수**를 사용할 수 있습니다. + +`Annotated` 안에서 Pydantic의 `AfterValidator`를 사용하면 이를 구현할 수 있습니다. + +/// tip | 팁 + +Pydantic에는 `BeforeValidator`와 같은 다른 것들도 있습니다. 🤓 + +/// + +예를 들어, 이 커스텀 validator는 ISBN 도서 번호의 경우 아이템 ID가 `isbn-`으로 시작하고, IMDB 영화 URL ID의 경우 `imdb-`로 시작하는지 확인합니다: + +{* ../../docs_src/query_params_str_validations/tutorial015_an_py310.py hl[5,16:19,24] *} + +/// info | 정보 + +이는 Pydantic 2 이상 버전에서 사용할 수 있습니다. 😎 + +/// + +/// tip | 팁 + +데이터베이스나 다른 API 같은 **외부 구성요소**와 통신이 필요한 어떤 종류의 검증이든 해야 한다면, 대신 **FastAPI Dependencies**를 사용해야 합니다. 이에 대해서는 나중에 배우게 됩니다. + +이 커스텀 validator는 요청에서 제공된 **같은 데이터만**으로 확인할 수 있는 것들을 위한 것입니다. + +/// + +### 코드 이해하기 { #understand-that-code } + +중요한 부분은 **`Annotated` 안에서 함수와 함께 `AfterValidator`를 사용한다는 것**뿐입니다. 이 부분은 건너뛰셔도 됩니다. 🤸 + +--- + +하지만 이 특정 코드 예제가 궁금하고 계속 보고 싶다면, 추가 세부사항은 다음과 같습니다. + +#### `value.startswith()`를 사용한 문자열 { #string-with-value-startswith } + +알고 계셨나요? `value.startswith()`를 사용하는 문자열은 튜플을 받을 수 있으며, 튜플에 있는 각 값을 확인합니다: + +{* ../../docs_src/query_params_str_validations/tutorial015_an_py310.py ln[16:19] hl[17] *} + +#### 임의의 항목 { #a-random-item } + +`data.items()`를 사용하면 각 딕셔너리 항목의 키와 값을 담은 튜플로 구성된 iterable object를 얻습니다. + +이 iterable object를 `list(data.items())`로 적절한 `list`로 변환합니다. + +그 다음 `random.choice()`로 리스트에서 **무작위 값**을 얻어 `(id, name)` 형태의 튜플을 얻습니다. 예를 들어 `("imdb-tt0371724", "The Hitchhiker's Guide to the Galaxy")` 같은 값이 될 것입니다. + +그 다음 이 튜플의 **두 값을** 변수 `id`와 `name`에 **할당**합니다. + +따라서 사용자가 아이템 ID를 제공하지 않더라도, 무작위 추천을 받게 됩니다. + +...이 모든 것을 **단 하나의 간단한 줄**로 합니다. 🤯 Python 정말 좋지 않나요? 🐍 + +{* ../../docs_src/query_params_str_validations/tutorial015_an_py310.py ln[22:30] hl[29] *} + +## 요약 { #recap } 매개변수에 검증과 메타데이터를 추가 선언할 수 있습니다. @@ -285,12 +461,14 @@ http://127.0.0.1:8000/items/?item-query=foobaritems * `description` * `deprecated` -특정 문자열 검증: +문자열에 특화된 검증: * `min_length` * `max_length` -* `regex` +* `pattern` + +`AfterValidator`를 사용하는 커스텀 검증. 예제에서 `str` 값의 검증을 어떻게 추가하는지 살펴보았습니다. -숫자와 같은 다른 자료형에 대한 검증을 어떻게 선언하는지 확인하려면 다음 장을 확인하기 바랍니다. +숫자와 같은 다른 타입에 대한 검증을 어떻게 선언하는지 확인하려면 다음 장을 확인하기 바랍니다. diff --git a/docs/ko/docs/tutorial/query-params.md b/docs/ko/docs/tutorial/query-params.md index d5b9837c4e..5124f73bf5 100644 --- a/docs/ko/docs/tutorial/query-params.md +++ b/docs/ko/docs/tutorial/query-params.md @@ -1,8 +1,8 @@ -# 쿼리 매개변수 +# 쿼리 매개변수 { #query-parameters } 경로 매개변수의 일부가 아닌 다른 함수 매개변수를 선언하면 "쿼리" 매개변수로 자동 해석합니다. -{* ../../docs_src/query_params/tutorial001.py hl[9] *} +{* ../../docs_src/query_params/tutorial001_py39.py hl[9] *} 쿼리는 URL에서 `?` 후에 나오고 `&`으로 구분되는 키-값 쌍의 집합입니다. @@ -24,11 +24,11 @@ URL의 일부이므로 "자연스럽게" 문자열입니다. 경로 매개변수에 적용된 동일한 프로세스가 쿼리 매개변수에도 적용됩니다: * (당연히) 편집기 지원 -* 데이터 "파싱" +* 데이터 "파싱" * 데이터 검증 * 자동 문서화 -## 기본값 +## 기본값 { #defaults } 쿼리 매개변수는 경로에서 고정된 부분이 아니기 때문에 선택적일 수 있고 기본값을 가질 수 있습니다. @@ -57,33 +57,25 @@ http://127.0.0.1:8000/items/?skip=20 * `skip=20`: URL에서 지정했기 때문입니다 * `limit=10`: 기본값이기 때문입니다 -## 선택적 매개변수 +## 선택적 매개변수 { #optional-parameters } 같은 방법으로 기본값을 `None`으로 설정하여 선택적 매개변수를 선언할 수 있습니다: -{* ../../docs_src/query_params/tutorial002.py hl[9] *} +{* ../../docs_src/query_params/tutorial002_py310.py hl[7] *} 이 경우 함수 매개변수 `q`는 선택적이며 기본값으로 `None` 값이 됩니다. -/// check | 확인 +/// check -**FastAPI**는 `item_id`가 경로 매개변수이고 `q`는 경로 매개변수가 아닌 쿼리 매개변수라는 것을 알 정도로 충분히 똑똑합니다. +또한 **FastAPI**는 `item_id`가 경로 매개변수이고 `q`는 경로 매개변수가 아니라서 쿼리 매개변수라는 것을 알 정도로 충분히 똑똑하다는 점도 확인하세요. /// -/// note | 참고 - -FastAPI는 `q`가 `= None`이므로 선택적이라는 것을 인지합니다. - -`Union[str, None]`에 있는 `Union`은 FastAPI(FastAPI는 `str` 부분만 사용합니다)가 사용하는게 아니지만, `Union[str, None]`은 편집기에게 코드에서 오류를 찾아낼 수 있게 도와줍니다. - -/// - -## 쿼리 매개변수 형변환 +## 쿼리 매개변수 형변환 { #query-parameter-type-conversion } `bool` 형으로 선언할 수도 있고, 아래처럼 변환됩니다: -{* ../../docs_src/query_params/tutorial003.py hl[9] *} +{* ../../docs_src/query_params/tutorial003_py310.py hl[7] *} 이 경우, 아래로 이동하면: @@ -115,10 +107,10 @@ http://127.0.0.1:8000/items/foo?short=on http://127.0.0.1:8000/items/foo?short=yes ``` -또는 다른 어떤 변형(대문자, 첫글자만 대문자 등)이더라도 함수는 매개변수 `bool`형을 가진 `short`의 값이 `True`임을 압니다. 그렇지 않은 경우 `False`입니다. +또는 다른 어떤 변형(대문자, 첫글자만 대문자 등)이더라도 함수는 `bool` 값이 `True`인 매개변수 `short`를 보게 됩니다. 그렇지 않은 경우 `False`입니다. -## 여러 경로/쿼리 매개변수 +## 여러 경로/쿼리 매개변수 { #multiple-path-and-query-parameters } 여러 경로 매개변수와 쿼리 매개변수를 동시에 선언할 수 있으며 **FastAPI**는 어느 것이 무엇인지 알고 있습니다. @@ -126,9 +118,9 @@ http://127.0.0.1:8000/items/foo?short=yes 매개변수들은 이름으로 감지됩니다: -{* ../../docs_src/query_params/tutorial004.py hl[8,10] *} +{* ../../docs_src/query_params/tutorial004_py310.py hl[6,8] *} -## 필수 쿼리 매개변수 +## 필수 쿼리 매개변수 { #required-query-parameters } 경로가 아닌 매개변수에 대한 기본값을 선언할 때(지금은 쿼리 매개변수만 보았습니다), 해당 매개변수는 필수적(Required)이지 않았습니다. @@ -136,7 +128,7 @@ http://127.0.0.1:8000/items/foo?short=yes 그러나 쿼리 매개변수를 필수로 만들려면 단순히 기본값을 선언하지 않으면 됩니다: -{* ../../docs_src/query_params/tutorial005.py hl[6:7] *} +{* ../../docs_src/query_params/tutorial005_py39.py hl[6:7] *} 여기 쿼리 매개변수 `needy`는 `str`형인 필수 쿼리 매개변수입니다. @@ -150,16 +142,17 @@ http://127.0.0.1:8000/items/foo-item ```JSON { - "detail": [ - { - "loc": [ - "query", - "needy" - ], - "msg": "field required", - "type": "value_error.missing" - } - ] + "detail": [ + { + "type": "missing", + "loc": [ + "query", + "needy" + ], + "msg": "Field required", + "input": null + } + ] } ``` @@ -180,7 +173,7 @@ http://127.0.0.1:8000/items/foo-item?needy=sooooneedy 그리고 물론, 일부 매개변수는 필수로, 다른 일부는 기본값을, 또 다른 일부는 선택적으로 선언할 수 있습니다: -{* ../../docs_src/query_params/tutorial006.py hl[10] *} +{* ../../docs_src/query_params/tutorial006_py310.py hl[8] *} 위 예시에서는 3가지 쿼리 매개변수가 있습니다: @@ -188,8 +181,8 @@ http://127.0.0.1:8000/items/foo-item?needy=sooooneedy * `skip`, 기본값이 `0`인 `int`. * `limit`, 선택적인 `int`. -/// tip | 팁 +/// tip -[경로 매개변수](path-params.md#_8){.internal-link target=_blank}와 마찬가지로 `Enum`을 사용할 수 있습니다. +[경로 매개변수](path-params.md#predefined-values){.internal-link target=_blank}와 마찬가지로 `Enum`을 사용할 수 있습니다. /// diff --git a/docs/ko/docs/tutorial/request-files.md b/docs/ko/docs/tutorial/request-files.md index 9162b353cd..cc00009211 100644 --- a/docs/ko/docs/tutorial/request-files.md +++ b/docs/ko/docs/tutorial/request-files.md @@ -1,4 +1,4 @@ -# 파일 요청 +# 파일 요청 { #request-files } `File`을 사용하여 클라이언트가 업로드할 파일들을 정의할 수 있습니다. @@ -6,23 +6,27 @@ 업로드된 파일을 전달받기 위해 먼저 `python-multipart`를 설치해야합니다. -예시) `pip install python-multipart`. +[가상 환경](../virtual-environments.md){.internal-link target=_blank}을 생성하고, 활성화한 다음, 예를 들어 다음과 같이 설치하세요: + +```console +$ pip install python-multipart +``` 업로드된 파일들은 "폼 데이터"의 형태로 전송되기 때문에 이 작업이 필요합니다. /// -## `File` 임포트 +## `File` 임포트 { #import-file } `fastapi` 에서 `File` 과 `UploadFile` 을 임포트 합니다: -{* ../../docs_src/request_files/tutorial001.py hl[1] *} +{* ../../docs_src/request_files/tutorial001_an_py39.py hl[3] *} -## `File` 매개변수 정의 +## `File` 매개변수 정의 { #define-file-parameters } `Body` 및 `Form` 과 동일한 방식으로 파일의 매개변수를 생성합니다: -{* ../../docs_src/request_files/tutorial001.py hl[7] *} +{* ../../docs_src/request_files/tutorial001_an_py39.py hl[9] *} /// info | 정보 @@ -40,20 +44,21 @@ File의 본문을 선언할 때, 매개변수가 쿼리 매개변수 또는 본 파일들은 "폼 데이터"의 형태로 업로드 됩니다. -*경로 작동 함수*의 매개변수를 `bytes` 로 선언하는 경우 **FastAPI**는 파일을 읽고 `bytes` 형태의 내용을 전달합니다. +*경로 처리 함수*의 매개변수를 `bytes` 로 선언하는 경우 **FastAPI**는 파일을 읽고 `bytes` 형태의 내용을 전달합니다. 이것은 전체 내용이 메모리에 저장된다는 것을 의미한다는 걸 염두하기 바랍니다. 이는 작은 크기의 파일들에 적합합니다. 어떤 경우에는 `UploadFile` 을 사용하는 것이 더 유리합니다. -## `File` 매개변수와 `UploadFile` +## `UploadFile`을 사용하는 `File` 매개변수 { #file-parameters-with-uploadfile } `File` 매개변수를 `UploadFile` 타입으로 정의합니다: -{* ../../docs_src/request_files/tutorial001.py hl[12] *} +{* ../../docs_src/request_files/tutorial001_an_py39.py hl[14] *} `UploadFile` 을 사용하는 것은 `bytes` 과 비교해 다음과 같은 장점이 있습니다: +* 매개변수의 기본값에서 `File()`을 사용할 필요가 없습니다. * "스풀 파일"을 사용합니다. * 최대 크기 제한까지만 메모리에 저장되며, 이를 초과하는 경우 디스크에 저장됩니다. * 따라서 이미지, 동영상, 큰 이진코드와 같은 대용량 파일들을 많은 메모리를 소모하지 않고 처리하기에 적합합니다. @@ -61,13 +66,13 @@ File의 본문을 선언할 때, 매개변수가 쿼리 매개변수 또는 본 * file-like `async` 인터페이스를 갖고 있습니다. * file-like object를 필요로하는 다른 라이브러리에 직접적으로 전달할 수 있는 파이썬 `SpooledTemporaryFile` 객체를 반환합니다. -### `UploadFile` +### `UploadFile` { #uploadfile } `UploadFile` 은 다음과 같은 어트리뷰트가 있습니다: * `filename` : 문자열(`str`)로 된 업로드된 파일의 파일명입니다 (예: `myimage.jpg`). * `content_type` : 문자열(`str`)로 된 파일 형식(MIME type / media type)입니다 (예: `image/jpeg`). -* `file` : `SpooledTemporaryFile` (파일류 객체)입니다. 이것은 "파일류" 객체를 필요로하는 다른 라이브러리에 직접적으로 전달할 수 있는 실질적인 파이썬 파일입니다. +* `file` : `SpooledTemporaryFile` (a file-like object)입니다. 이것은 "file-like" 객체를 필요로하는 다른 함수나 라이브러리에 직접적으로 전달할 수 있는 실질적인 파이썬 파일 객체입니다. `UploadFile` 에는 다음의 `async` 메소드들이 있습니다. 이들은 내부적인 `SpooledTemporaryFile` 을 사용하여 해당하는 파일 메소드를 호출합니다. @@ -80,55 +85,67 @@ File의 본문을 선언할 때, 매개변수가 쿼리 매개변수 또는 본 상기 모든 메소드들이 `async` 메소드이기 때문에 “await”을 사용하여야 합니다. -예를들어, `async` *경로 작동 함수*의 내부에서 다음과 같은 방식으로 내용을 가져올 수 있습니다: +예를들어, `async` *경로 처리 함수*의 내부에서 다음과 같은 방식으로 내용을 가져올 수 있습니다: ```Python contents = await myfile.read() ``` -만약 일반적인 `def` *경로 작동 함수*의 내부라면, 다음과 같이 `UploadFile.file` 에 직접 접근할 수 있습니다: +만약 일반적인 `def` *경로 처리 함수*의 내부라면, 다음과 같이 `UploadFile.file` 에 직접 접근할 수 있습니다: ```Python contents = myfile.file.read() ``` -/// note | "`async` 기술적 세부사항" +/// note | `async` 기술 세부사항 `async` 메소드들을 사용할 때 **FastAPI**는 스레드풀에서 파일 메소드들을 실행하고 그들을 기다립니다. /// -/// note | Starlette 기술적 세부사항 +/// note | Starlette 기술 세부사항 **FastAPI**의 `UploadFile` 은 **Starlette**의 `UploadFile` 을 직접적으로 상속받지만, **Pydantic** 및 FastAPI의 다른 부분들과의 호환성을 위해 필요한 부분들이 추가되었습니다. /// -## "폼 데이터"란 +## "폼 데이터"란 { #what-is-form-data } HTML의 폼들(`
`)이 서버에 데이터를 전송하는 방식은 대개 데이터에 JSON과는 다른 "특별한" 인코딩을 사용합니다. **FastAPI**는 JSON 대신 올바른 위치에서 데이터를 읽을 수 있도록 합니다. -/// note | 기술적 세부사항 +/// note | 기술 세부사항 폼의 데이터는 파일이 포함되지 않은 경우 일반적으로 "미디어 유형" `application/x-www-form-urlencoded` 을 사용해 인코딩 됩니다. 하지만 파일이 포함된 경우, `multipart/form-data`로 인코딩됩니다. `File`을 사용하였다면, **FastAPI**는 본문의 적합한 부분에서 파일을 가져와야 한다는 것을 인지합니다. -인코딩과 폼 필드에 대해 더 알고싶다면, POST에 관한MDN웹 문서 를 참고하기 바랍니다,. +인코딩과 폼 필드에 대해 더 알고싶다면, MDN web docs for POST를 참고하기 바랍니다. /// /// warning | 경고 -다수의 `File` 과 `Form` 매개변수를 한 *경로 작동*에 선언하는 것이 가능하지만, 요청의 본문이 `application/json` 가 아닌 `multipart/form-data` 로 인코딩 되기 때문에 JSON으로 받아야하는 `Body` 필드를 함께 선언할 수는 없습니다. +다수의 `File` 과 `Form` 매개변수를 한 *경로 처리*에 선언하는 것이 가능하지만, 요청의 본문이 `application/json` 가 아닌 `multipart/form-data` 로 인코딩 되기 때문에 JSON으로 받아야하는 `Body` 필드를 함께 선언할 수는 없습니다. 이는 **FastAPI**의 한계가 아니라, HTTP 프로토콜에 의한 것입니다. /// -## 다중 파일 업로드 +## 선택적 파일 업로드 { #optional-file-upload } + +표준 타입 애너테이션을 사용하고 기본값을 `None`으로 설정하여 파일을 선택적으로 만들 수 있습니다: + +{* ../../docs_src/request_files/tutorial001_02_an_py310.py hl[9,17] *} + +## 추가 메타데이터를 포함한 `UploadFile` { #uploadfile-with-additional-metadata } + +추가 메타데이터를 설정하기 위해 예를 들어 `UploadFile`과 함께 `File()`을 사용할 수도 있습니다: + +{* ../../docs_src/request_files/tutorial001_03_an_py39.py hl[9,15] *} + +## 다중 파일 업로드 { #multiple-file-uploads } 여러 파일을 동시에 업로드 할 수 있습니다. @@ -136,21 +153,11 @@ HTML의 폼들(`
`)이 서버에 데이터를 전송하는 방식은 이 기능을 사용하기 위해 , `bytes` 의 `List` 또는 `UploadFile` 를 선언하기 바랍니다: -{* ../../docs_src/request_files/tutorial002.py hl[10,15] *} +{* ../../docs_src/request_files/tutorial002_an_py39.py hl[10,15] *} 선언한대로, `bytes` 의 `list` 또는 `UploadFile` 들을 전송받을 것입니다. -/// note | 참고 - -2019년 4월 14일부터 Swagger UI가 하나의 폼 필드로 다수의 파일을 업로드하는 것을 지원하지 않습니다. 더 많은 정보를 원하면, #4276#3641을 참고하세요. - -그럼에도, **FastAPI**는 표준 Open API를 사용해 이미 호환이 가능합니다. - -따라서 Swagger UI 또는 기타 그 외의 OpenAPI를 지원하는 툴이 다중 파일 업로드를 지원하는 경우, 이들은 **FastAPI**와 호환됩니다. - -/// - -/// note | 기술적 세부사항 +/// note | 기술 세부사항 `from starlette.responses import HTMLResponse` 역시 사용할 수 있습니다. @@ -158,6 +165,12 @@ HTML의 폼들(`
`)이 서버에 데이터를 전송하는 방식은 /// -## 요약 +### 추가 메타데이터를 포함한 다중 파일 업로드 { #multiple-file-uploads-with-additional-metadata } -폼 데이터로써 입력 매개변수로 업로드할 파일을 선언할 경우 `File` 을 사용하기 바랍니다. +이전과 같은 방식으로 `UploadFile`에 대해서도 `File()`을 사용해 추가 매개변수를 설정할 수 있습니다: + +{* ../../docs_src/request_files/tutorial003_an_py39.py hl[11,18:20] *} + +## 요약 { #recap } + +`File`, `bytes`, `UploadFile`을 사용하여 폼 데이터로 전송되는 요청에서 업로드할 파일을 선언하세요. diff --git a/docs/ko/docs/tutorial/request-form-models.md b/docs/ko/docs/tutorial/request-form-models.md index 3316a93d52..b37186dfb8 100644 --- a/docs/ko/docs/tutorial/request-form-models.md +++ b/docs/ko/docs/tutorial/request-form-models.md @@ -1,12 +1,12 @@ -# 폼 모델 +# 폼 모델 { #form-models } FastAPI에서 **Pydantic 모델**을 이용하여 **폼 필드**를 선언할 수 있습니다. /// info | 정보 -폼(Form)을 사용하려면, 먼저 `python-multipart`를 설치하세요. +폼을 사용하려면, 먼저 `python-multipart`를 설치하세요. -[가상 환경](../virtual-environments.md){.internal-link target=_blank}을 생성하고 활성화한 다음, 아래와 같이 설치할 수 있습니다: +[가상 환경](../virtual-environments.md){.internal-link target=_blank}을 생성하고 활성화한 다음, 예를 들어 아래와 같이 설치하세요: ```console $ pip install python-multipart @@ -20,7 +20,7 @@ $ pip install python-multipart /// -## Pydantic 모델을 사용한 폼 +## 폼을 위한 Pydantic 모델 { #pydantic-models-for-forms } **폼 필드**로 받고 싶은 필드를 **Pydantic 모델**로 선언한 다음, 매개변수를 `Form`으로 선언하면 됩니다: @@ -28,7 +28,7 @@ $ pip install python-multipart **FastAPI**는 요청에서 받은 **폼 데이터**에서 **각 필드**에 대한 데이터를 **추출**하고 정의한 Pydantic 모델을 줍니다. -## 문서 확인하기 +## 문서 확인하기 { #check-the-docs } 문서 UI `/docs`에서 확인할 수 있습니다: @@ -36,9 +36,9 @@ $ pip install python-multipart
-## 추가 폼 필드 금지하기 +## 추가 폼 필드 금지하기 { #forbid-extra-form-fields } -일부 특별한 사용 사례(흔하지는 않겠지만)에서는 Pydantic 모델에서 정의한 폼 필드를 **제한**하길 원할 수도 있습니다. 그리고 **추가** 필드를 **금지**할 수도 있습니다. +일부 특별한 사용 사례(아마도 흔하지는 않겠지만)에서는 Pydantic 모델에서 선언된 폼 필드로만 **제한**하길 원할 수도 있습니다. 그리고 **추가** 필드를 **금지**할 수도 있습니다. /// note | 참고 @@ -46,7 +46,7 @@ $ pip install python-multipart /// -Pydantic의 모델 구성을 사용하여 추가(`extra`) 필드를 금지(`forbid`)할 수 있습니다: +Pydantic의 모델 구성을 사용하여 `extra` 필드를 `forbid`할 수 있습니다: {* ../../docs_src/request_form_models/tutorial002_an_py39.py hl[12] *} @@ -73,6 +73,6 @@ Pydantic의 모델 구성을 사용하여 추가(`extra`) 필드를 금지(`forb } ``` -## 요약 +## 요약 { #summary } Pydantic 모델을 사용하여 FastAPI에서 폼 필드를 선언할 수 있습니다. 😎 diff --git a/docs/ko/docs/tutorial/request-forms-and-files.md b/docs/ko/docs/tutorial/request-forms-and-files.md index dc1bda21a8..a5309b5c07 100644 --- a/docs/ko/docs/tutorial/request-forms-and-files.md +++ b/docs/ko/docs/tutorial/request-forms-and-files.md @@ -1,37 +1,41 @@ -# 폼 및 파일 요청 +# 폼 및 파일 요청 { #request-forms-and-files } -`File` 과 `Form` 을 사용하여 파일과 폼을 함께 정의할 수 있습니다. +`File` 과 `Form` 을 사용하여 파일과 폼 필드를 동시에 정의할 수 있습니다. /// info | 정보 -파일과 폼 데이터를 함께, 또는 각각 업로드하기 위해 먼저 `python-multipart`를 설치해야합니다. +업로드된 파일 및/또는 폼 데이터를 받으려면 먼저 `python-multipart`를 설치해야 합니다. -예 ) `pip install python-multipart`. +[가상 환경](../virtual-environments.md){.internal-link target=_blank}을 생성하고, 활성화한 다음 설치해야 합니다. 예: + +```console +$ pip install python-multipart +``` /// -## `File` 및 `Form` 업로드 +## `File` 및 `Form` 임포트 { #import-file-and-form } -{* ../../docs_src/request_forms_and_files/tutorial001.py hl[1] *} +{* ../../docs_src/request_forms_and_files/tutorial001_an_py39.py hl[3] *} -## `File` 및 `Form` 매개변수 정의 +## `File` 및 `Form` 매개변수 정의 { #define-file-and-form-parameters } `Body` 및 `Query`와 동일한 방식으로 파일과 폼의 매개변수를 생성합니다: -{* ../../docs_src/request_forms_and_files/tutorial001.py hl[8] *} +{* ../../docs_src/request_forms_and_files/tutorial001_an_py39.py hl[10:12] *} -파일과 폼 필드는 폼 데이터 형식으로 업로드되어 파일과 폼 필드로 전달됩니다. +파일과 폼 필드는 폼 데이터로 업로드되며, 파일과 폼 필드를 받게 됩니다. -어떤 파일들은 `bytes`로, 또 어떤 파일들은 `UploadFile`로 선언할 수 있습니다. +또한 일부 파일은 `bytes`로, 일부 파일은 `UploadFile`로 선언할 수 있습니다. /// warning | 경고 -다수의 `File`과 `Form` 매개변수를 한 *경로 작동*에 선언하는 것이 가능하지만, 요청의 본문이 `application/json`가 아닌 `multipart/form-data`로 인코딩 되기 때문에 JSON으로 받아야하는 `Body` 필드를 함께 선언할 수는 없습니다. +다수의 `File`과 `Form` 매개변수를 한 *경로 처리*에 선언하는 것이 가능하지만, 요청의 본문이 `application/json`가 아닌 `multipart/form-data`로 인코딩되기 때문에 JSON으로 받기를 기대하는 `Body` 필드를 함께 선언할 수는 없습니다. -이는 **FastAPI**의 한계가 아니라, HTTP 프로토콜에 의한 것입니다. +이는 **FastAPI**의 한계가 아니라, HTTP 프로토콜의 일부입니다. /// -## 요약 +## 요약 { #recap } 하나의 요청으로 데이터와 파일들을 받아야 할 경우 `File`과 `Form`을 함께 사용하기 바랍니다. diff --git a/docs/ko/docs/tutorial/request-forms.md b/docs/ko/docs/tutorial/request-forms.md index 5ca17b0d68..584cbba35c 100644 --- a/docs/ko/docs/tutorial/request-forms.md +++ b/docs/ko/docs/tutorial/request-forms.md @@ -1,4 +1,4 @@ -# 폼 데이터 +# 폼 데이터 { #form-data } JSON 대신 폼 필드를 받아야 하는 경우 `Form`을 사용할 수 있습니다. @@ -14,13 +14,13 @@ $ pip install python-multipart /// -## `Form` 임포트하기 +## `Form` 임포트하기 { #import-form } `fastapi`에서 `Form`을 임포트합니다: {* ../../docs_src/request_forms/tutorial001_an_py39.py hl[3] *} -## `Form` 매개변수 정의하기 +## `Form` 매개변수 정의하기 { #define-form-parameters } `Body` 또는 `Query`와 동일한 방식으로 폼 매개변수를 만듭니다: @@ -28,7 +28,7 @@ $ pip install python-multipart 예를 들어, OAuth2 사양을 사용할 수 있는 방법 중 하나("패스워드 플로우"라고 함)로 `username`과 `password`를 폼 필드로 보내야 합니다. -사양에서는 필드 이름이 `username` 및 `password`로 정확하게 명명되어야 하고, JSON이 아닌 폼 필드로 전송해야 합니다. +spec에서는 필드 이름이 `username` 및 `password`로 정확하게 명명되어야 하고, JSON이 아닌 폼 필드로 전송해야 합니다. `Form`을 사용하면 유효성 검사, 예제, 별칭(예: `username` 대신 `user-name`) 등을 포함하여 `Body`(및 `Query`, `Path`, `Cookie`)와 동일한 구성을 선언할 수 있습니다. @@ -44,7 +44,7 @@ $ pip install python-multipart /// -## "폼 필드"에 대해 +## "폼 필드"에 대해 { #about-form-fields } HTML 폼(`
`)이 데이터를 서버로 보내는 방식은 일반적으로 해당 데이터에 대해 "특수" 인코딩을 사용하며, 이는 JSON과 다릅니다. @@ -56,19 +56,18 @@ HTML 폼(`
`)이 데이터를 서버로 보내는 방식은 일반 그러나 폼에 파일이 포함된 경우, `multipart/form-data`로 인코딩합니다. 다음 장에서 파일 처리에 대해 읽을 겁니다. - -이러한 인코딩 및 폼 필드에 대해 더 읽고 싶다면, POST에 대한 MDN 웹 문서를 참조하세요. +이러한 인코딩 및 폼 필드에 대해 더 읽고 싶다면, POST에 대한 MDN 웹 문서를 참조하세요. /// /// warning | 경고 -*경로 작업*에서 여러 `Form` 매개변수를 선언할 수 있지만, JSON으로 수신할 것으로 예상되는 `Body` 필드와 함께 선언할 수 없습니다. 요청 본문은 `application/json` 대신에 `application/x-www-form-urlencoded`를 사용하여 인코딩되기 때문입니다. +*경로 처리*에서 여러 `Form` 매개변수를 선언할 수 있지만, JSON으로 수신할 것으로 예상되는 `Body` 필드와 함께 선언할 수 없습니다. 요청 본문은 `application/json` 대신에 `application/x-www-form-urlencoded`를 사용하여 인코딩되기 때문입니다. 이는 **FastAPI**의 제한 사항이 아니며 HTTP 프로토콜의 일부입니다. /// -## 요약 +## 요약 { #recap } 폼 데이터 입력 매개변수를 선언하려면 `Form`을 사용하세요. diff --git a/docs/ko/docs/tutorial/response-model.md b/docs/ko/docs/tutorial/response-model.md index a71d649f90..6246ed9ad1 100644 --- a/docs/ko/docs/tutorial/response-model.md +++ b/docs/ko/docs/tutorial/response-model.md @@ -1,81 +1,173 @@ -# 응답 모델 +# 응답 모델 - 반환 타입 { #response-model-return-type } -어떤 *경로 작동*이든 매개변수 `response_model`를 사용하여 응답을 위한 모델을 선언할 수 있습니다: +*경로 처리 함수*의 **반환 타입**을 어노테이션하여 응답에 사용될 타입을 선언할 수 있습니다. + +함수 **매개변수**에서 입력 데이터를 위해 사용하는 것과 동일하게 **타입 어노테이션**을 사용할 수 있으며, Pydantic 모델, 리스트, 딕셔너리, 정수/불리언 같은 스칼라 값 등을 사용할 수 있습니다. + +{* ../../docs_src/response_model/tutorial001_01_py310.py hl[16,21] *} + +FastAPI는 이 반환 타입을 사용하여: + +* 반환된 데이터를 **검증**합니다. + * 데이터가 유효하지 않다면(예: 필드가 누락된 경우), 이는 *여러분의* 앱 코드가 깨져서 의도한 값을 반환하지 못한다는 의미이며, 잘못된 데이터를 반환하는 대신 서버 오류를 반환합니다. 이렇게 하면 여러분과 클라이언트는 기대한 데이터와 데이터 형태를 받는다는 것을 확실히 할 수 있습니다. +* OpenAPI *경로 처리*의 응답에 **JSON Schema**를 추가합니다. + * 이는 **자동 문서**에서 사용됩니다. + * 또한 자동 클라이언트 코드 생성 도구에서도 사용됩니다. + +하지만 가장 중요한 것은: + +* 반환 타입에 정의된 내용으로 출력 데이터를 **제한하고 필터링**합니다. + * 이는 특히 **보안**에 매우 중요합니다. 아래에서 더 자세히 살펴보겠습니다. + +## `response_model` 매개변수 { #response-model-parameter } + +타입 선언이 말하는 것과 정확히 일치하지 않는 데이터를 반환해야 하거나 반환하고 싶은 경우가 있습니다. + +예를 들어, **딕셔너리**나 데이터베이스 객체를 **반환**하고 싶지만, **Pydantic 모델로 선언**하고 싶을 수 있습니다. 이렇게 하면 Pydantic 모델이 반환한 객체(예: 딕셔너리나 데이터베이스 객체)에 대해 데이터 문서화, 검증 등 모든 작업을 수행합니다. + +반환 타입 어노테이션을 추가했다면, 도구와 에디터가 함수가 선언한 타입(예: Pydantic 모델)과 다른 타입(예: dict)을 반환하고 있다는 (올바른) 오류로 불평할 것입니다. + +그런 경우에는 반환 타입 대신 *경로 처리 데코레이터*의 매개변수 `response_model`을 사용할 수 있습니다. + +`response_model` 매개변수는 모든 *경로 처리*에서 사용할 수 있습니다: * `@app.get()` * `@app.post()` * `@app.put()` * `@app.delete()` -* 기타. +* 등. -{* ../../docs_src/response_model/tutorial001.py hl[17] *} +{* ../../docs_src/response_model/tutorial001_py310.py hl[17,22,24:27] *} /// note | 참고 -`response_model`은 "데코레이터" 메소드(`get`, `post`, 등)의 매개변수입니다. 모든 매개변수들과 본문(body)처럼 *경로 작동 함수*가 아닙니다. +`response_model`은 "데코레이터" 메서드(`get`, `post` 등)의 매개변수입니다. 모든 매개변수와 body처럼, *경로 처리 함수*의 매개변수가 아닙니다. /// -Pydantic 모델 어트리뷰트를 선언한 것과 동일한 타입을 수신하므로 Pydantic 모델이 될 수 있지만, `List[Item]`과 같이 Pydantic 모델들의 `list`일 수도 있습니다. +`response_model`은 Pydantic 모델 필드에 선언하는 것과 동일한 타입을 받습니다. 따라서 Pydantic 모델이 될 수도 있고, `List[Item]`처럼 Pydantic 모델의 `list`가 될 수도 있습니다. -FastAPI는 이 `response_model`를 사용하여: +FastAPI는 이 `response_model`을 사용해 데이터 문서화, 검증 등을 수행하고, 또한 출력 데이터를 타입 선언에 맞게 **변환하고 필터링**합니다. -* 출력 데이터를 타입 선언으로 변환. -* 데이터 검증. -* OpenAPI *경로 작동*의 응답에 JSON 스키마 추가. -* 자동 생성 문서 시스템에 사용. +/// tip | 팁 -하지만 가장 중요한 것은: +에디터, mypy 등에서 엄격한 타입 체크를 사용하고 있다면, 함수 반환 타입을 `Any`로 선언할 수 있습니다. -* 해당 모델의 출력 데이터 제한. 이것이 얼마나 중요한지 아래에서 볼 것입니다. - -/// note | 기술 세부사항 - -응답 모델은 함수의 타입 어노테이션 대신 이 매개변수로 선언하는데, 경로 함수가 실제 응답 모델을 반환하지 않고 `dict`, 데이터베이스 객체나 기타 다른 모델을 `response_model`을 사용하여 필드 제한과 직렬화를 수행하고 반환할 수 있기 때문입니다 +이렇게 하면 에디터에 의도적으로 어떤 값이든 반환한다고 알려줍니다. 하지만 FastAPI는 여전히 `response_model`을 사용하여 데이터 문서화, 검증, 필터링 등을 수행합니다. /// -## 동일한 입력 데이터 반환 +### `response_model` 우선순위 { #response-model-priority } -여기서 우리는 평문 비밀번호를 포함하는 `UserIn` 모델을 선언합니다: +반환 타입과 `response_model`을 둘 다 선언하면, `response_model`이 우선순위를 가지며 FastAPI에서 사용됩니다. -{* ../../docs_src/response_model/tutorial002.py hl[9,11] *} +이렇게 하면 응답 모델과 다른 타입을 반환하는 경우에도 에디터와 mypy 같은 도구에서 사용할 올바른 타입 어노테이션을 함수에 추가할 수 있습니다. 그리고 동시에 FastAPI가 `response_model`을 사용하여 데이터 검증, 문서화 등을 수행하게 할 수도 있습니다. + +또한 `response_model=None`을 사용해 해당 *경로 처리*에 대한 응답 모델 생성을 비활성화할 수도 있습니다. 이는 유효한 Pydantic 필드가 아닌 것들에 대해 타입 어노테이션을 추가하는 경우에 필요할 수 있으며, 아래 섹션 중 하나에서 예시를 볼 수 있습니다. + +## 동일한 입력 데이터 반환 { #return-the-same-input-data } + +여기서는 평문 비밀번호를 포함하는 `UserIn` 모델을 선언합니다: + +{* ../../docs_src/response_model/tutorial002_py310.py hl[7,9] *} + +/// info | 정보 + +`EmailStr`을 사용하려면 먼저 `email-validator`를 설치하세요. + +[가상 환경](../virtual-environments.md){.internal-link target=_blank}을 생성하고, 활성화한 다음 설치해야 합니다. 예를 들어: + +```console +$ pip install email-validator +``` + +또는 다음과 같이: + +```console +$ pip install "pydantic[email]" +``` + +/// 그리고 이 모델을 사용하여 입력을 선언하고 같은 모델로 출력을 선언합니다: -{* ../../docs_src/response_model/tutorial002.py hl[17:18] *} +{* ../../docs_src/response_model/tutorial002_py310.py hl[16] *} 이제 브라우저가 비밀번호로 사용자를 만들 때마다 API는 응답으로 동일한 비밀번호를 반환합니다. -이 경우, 사용자가 스스로 비밀번호를 발신했기 때문에 문제가 되지 않을 수 있습니다. +이 경우, 동일한 사용자가 비밀번호를 보내는 것이므로 문제가 되지 않을 수도 있습니다. -그러나 동일한 모델을 다른 *경로 작동*에서 사용할 경우, 모든 클라이언트에게 사용자의 비밀번호를 발신할 수 있습니다. +하지만 동일한 모델을 다른 *경로 처리*에서 사용하면, 모든 클라이언트에게 사용자의 비밀번호를 보내게 될 수도 있습니다. /// danger | 위험 -절대로 사용자의 평문 비밀번호를 저장하거나 응답으로 발신하지 마십시오. +모든 주의사항을 알고 있으며 무엇을 하는지 정확히 알고 있지 않다면, 이런 방식으로 사용자의 평문 비밀번호를 저장하거나 응답으로 보내지 마세요. /// -## 출력 모델 추가 +## 출력 모델 추가 { #add-an-output-model } -대신 평문 비밀번호로 입력 모델을 만들고 해당 비밀번호 없이 출력 모델을 만들 수 있습니다: +대신 평문 비밀번호를 포함하는 입력 모델과, 비밀번호가 없는 출력 모델을 만들 수 있습니다: -{* ../../docs_src/response_model/tutorial003.py hl[9,11,16] *} +{* ../../docs_src/response_model/tutorial003_py310.py hl[9,11,16] *} -여기서 *경로 작동 함수*가 비밀번호를 포함하는 동일한 입력 사용자를 반환할지라도: +여기서 *경로 처리 함수*가 비밀번호를 포함하는 동일한 입력 사용자를 반환하더라도: -{* ../../docs_src/response_model/tutorial003.py hl[24] *} +{* ../../docs_src/response_model/tutorial003_py310.py hl[24] *} -...`response_model`을 `UserOut` 모델로 선언했기 때문에 비밀번호를 포함하지 않습니다: +...비밀번호를 포함하지 않는 `UserOut` 모델로 `response_model`을 선언했습니다: -{* ../../docs_src/response_model/tutorial003.py hl[22] *} +{* ../../docs_src/response_model/tutorial003_py310.py hl[22] *} -따라서 **FastAPI**는 출력 모델에서 선언하지 않은 모든 데이터를 (Pydantic을 사용하여) 필터링합니다. +따라서 **FastAPI**는 출력 모델에 선언되지 않은 모든 데이터를 (Pydantic을 사용하여) 필터링합니다. -## 문서에서 보기 +### `response_model` 또는 반환 타입 { #response-model-or-return-type } -자동 생성 문서를 보면 입력 모델과 출력 모델이 각자의 JSON 스키마를 가지고 있음을 확인할 수 있습니다: +이 경우 두 모델이 서로 다르기 때문에, 함수 반환 타입을 `UserOut`으로 어노테이션하면 에디터와 도구는 서로 다른 클래스인데 잘못된 타입을 반환하고 있다고 불평할 것입니다. + +그래서 이 예제에서는 `response_model` 매개변수로 선언해야 합니다. + +...하지만 아래를 계속 읽으면 이를 극복하는 방법을 볼 수 있습니다. + +## 반환 타입과 데이터 필터링 { #return-type-and-data-filtering } + +이전 예제에서 계속해 봅시다. 함수에 **하나의 타입으로 어노테이션**을 하고 싶지만, 함수에서 실제로는 **더 많은 데이터**를 포함하는 것을 반환할 수 있길 원했습니다. + +FastAPI가 응답 모델을 사용해 데이터를 계속 **필터링**하길 원합니다. 그래서 함수가 더 많은 데이터를 반환하더라도, 응답에는 응답 모델에 선언된 필드만 포함되게 합니다. + +이전 예제에서는 클래스가 달랐기 때문에 `response_model` 매개변수를 써야 했습니다. 하지만 이는 에디터와 도구가 함수 반환 타입을 체크해 주는 지원을 받지 못한다는 뜻이기도 합니다. + +하지만 대부분 이런 작업이 필요한 경우에는, 이 예제처럼 모델로 일부 데이터를 **필터링/제거**하길 원하는 경우가 많습니다. + +그리고 그런 경우에는 클래스와 상속을 사용하여 함수 **타입 어노테이션**을 활용해 에디터/도구에서 더 나은 지원을 받으면서도 FastAPI의 **데이터 필터링**을 유지할 수 있습니다. + +{* ../../docs_src/response_model/tutorial003_01_py310.py hl[7:10,13:14,18] *} + +이를 통해 이 코드는 타입 관점에서 올바르므로 에디터와 mypy 등의 도구 지원을 받을 수 있고, 동시에 FastAPI의 데이터 필터링도 받을 수 있습니다. + +이게 어떻게 동작할까요? 확인해 봅시다. 🤓 + +### 타입 어노테이션과 도구 지원 { #type-annotations-and-tooling } + +먼저 에디터, mypy 및 기타 도구가 이를 어떻게 보는지 살펴봅시다. + +`BaseUser`는 기본 필드를 가집니다. 그리고 `UserIn`은 `BaseUser`를 상속하고 `password` 필드를 추가하므로, 두 모델의 모든 필드를 포함하게 됩니다. + +함수 반환 타입을 `BaseUser`로 어노테이션하지만, 실제로는 `UserIn` 인스턴스를 반환합니다. + +에디터, mypy 및 기타 도구는 이에 대해 불평하지 않습니다. 타이핑 관점에서 `UserIn`은 `BaseUser`의 서브클래스이므로, `BaseUser`인 어떤 것이 기대되는 곳에서는 *유효한* 타입이기 때문입니다. + +### FastAPI 데이터 필터링 { #fastapi-data-filtering } + +이제 FastAPI는 반환 타입을 보고, 여러분이 반환하는 값이 해당 타입에 선언된 필드 **만** 포함하도록 보장합니다. + +FastAPI는 Pydantic을 내부적으로 여러 방식으로 사용하여, 클래스 상속의 동일한 규칙이 반환 데이터 필터링에는 적용되지 않도록 합니다. 그렇지 않으면 기대한 것보다 훨씬 더 많은 데이터를 반환하게 될 수도 있습니다. + +이렇게 하면 **도구 지원**이 있는 타입 어노테이션과 **데이터 필터링**이라는 두 가지 장점을 모두 얻을 수 있습니다. + +## 문서에서 보기 { #see-it-in-the-docs } + +자동 생성 문서를 보면 입력 모델과 출력 모델이 각자의 JSON Schema를 가지고 있음을 확인할 수 있습니다: @@ -83,29 +175,73 @@ FastAPI는 이 `response_model`를 사용하여: -## 응답 모델 인코딩 매개변수 +## 기타 반환 타입 어노테이션 { #other-return-type-annotations } + +유효한 Pydantic 필드가 아닌 것을 반환하면서도, 도구(에디터, mypy 등)가 제공하는 지원을 받기 위해 함수에 어노테이션을 달아두는 경우가 있을 수 있습니다. + +### 응답을 직접 반환하기 { #return-a-response-directly } + +가장 흔한 경우는 [고급 문서에서 나중에 설명하는 대로 Response를 직접 반환하는 것](../advanced/response-directly.md){.internal-link target=_blank}입니다. + +{* ../../docs_src/response_model/tutorial003_02_py39.py hl[8,10:11] *} + +이 간단한 경우는 반환 타입 어노테이션이 `Response` 클래스(또는 그 서브클래스)이기 때문에 FastAPI에서 자동으로 처리됩니다. + +그리고 `RedirectResponse`와 `JSONResponse`는 모두 `Response`의 서브클래스이므로, 타입 어노테이션이 올바르기 때문에 도구들도 만족합니다. + +### Response 서브클래스 어노테이션 { #annotate-a-response-subclass } + +타입 어노테이션에 `Response`의 서브클래스를 사용할 수도 있습니다: + +{* ../../docs_src/response_model/tutorial003_03_py39.py hl[8:9] *} + +이는 `RedirectResponse`가 `Response`의 서브클래스이기 때문에 동작하며, FastAPI가 이 간단한 경우를 자동으로 처리합니다. + +### 유효하지 않은 반환 타입 어노테이션 { #invalid-return-type-annotations } + +하지만 유효한 Pydantic 타입이 아닌 다른 임의의 객체(예: 데이터베이스 객체)를 반환하고, 함수에서 그렇게 어노테이션하면, FastAPI는 그 타입 어노테이션으로부터 Pydantic 응답 모델을 만들려고 시도하다가 실패합니다. + +또한, 유효한 Pydantic 타입이 아닌 타입이 하나 이상 포함된 여러 타입 간의 union이 있는 경우에도 동일합니다. 예를 들어, 아래는 실패합니다 💥: + +{* ../../docs_src/response_model/tutorial003_04_py310.py hl[8] *} + +...이는 타입 어노테이션이 Pydantic 타입이 아니고, 단일 `Response` 클래스/서브클래스도 아니며, `Response`와 `dict` 간 union(둘 중 아무거나)이기 때문에 실패합니다. + +### 응답 모델 비활성화 { #disable-response-model } + +위 예제에서 이어서, FastAPI가 수행하는 기본 데이터 검증, 문서화, 필터링 등을 원하지 않을 수 있습니다. + +하지만 에디터나 타입 체커(예: mypy) 같은 도구 지원을 받기 위해 함수에 반환 타입 어노테이션은 유지하고 싶을 수도 있습니다. + +이 경우 `response_model=None`으로 설정하여 응답 모델 생성을 비활성화할 수 있습니다: + +{* ../../docs_src/response_model/tutorial003_05_py310.py hl[7] *} + +그러면 FastAPI는 응답 모델 생성을 건너뛰며, FastAPI 애플리케이션에 영향을 주지 않고 필요한 반환 타입 어노테이션을 어떤 것이든 사용할 수 있습니다. 🤓 + +## 응답 모델 인코딩 매개변수 { #response-model-encoding-parameters } 응답 모델은 아래와 같이 기본값을 가질 수 있습니다: -{* ../../docs_src/response_model/tutorial004.py hl[11,13:14] *} +{* ../../docs_src/response_model/tutorial004_py310.py hl[9,11:12] *} -* `description: Optional[str] = None`은 기본값으로 `None`을 갖습니다. +* `description: Union[str, None] = None` (또는 Python 3.10에서 `str | None = None`)은 기본값으로 `None`을 갖습니다. * `tax: float = 10.5`는 기본값으로 `10.5`를 갖습니다. -* `tags: List[str] = []` 빈 리스트의 기본값으로: `[]`. +* `tags: List[str] = []`는 기본값으로 빈 리스트 `[]`를 갖습니다. -그러나 실제로 저장되지 않았을 경우 결과에서 값을 생략하고 싶을 수 있습니다. +하지만 실제로 저장되지 않았을 경우 결과에서 이를 생략하고 싶을 수 있습니다. 예를 들어, NoSQL 데이터베이스에 많은 선택적 속성이 있는 모델이 있지만, 기본값으로 가득 찬 매우 긴 JSON 응답을 보내고 싶지 않습니다. -### `response_model_exclude_unset` 매개변수 사용 +### `response_model_exclude_unset` 매개변수 사용 { #use-the-response-model-exclude-unset-parameter } -*경로 작동 데코레이터* 매개변수를 `response_model_exclude_unset=True`로 설정 할 수 있습니다: +*경로 처리 데코레이터* 매개변수 `response_model_exclude_unset=True`로 설정할 수 있습니다: -{* ../../docs_src/response_model/tutorial004.py hl[24] *} +{* ../../docs_src/response_model/tutorial004_py310.py hl[22] *} -이러한 기본값은 응답에 포함되지 않고 실제로 설정된 값만 포함됩니다. +그러면 이러한 기본값은 응답에 포함되지 않고, 실제로 설정된 값만 포함됩니다. -따라서 해당 *경로 작동*에 ID가 `foo`인 항목(items)을 요청으로 보내면 (기본값을 제외한) 응답은 다음과 같습니다: +따라서 ID가 `foo`인 항목에 대해 해당 *경로 처리*로 요청을 보내면, (기본값을 제외한) 응답은 다음과 같습니다: ```JSON { @@ -116,24 +252,18 @@ FastAPI는 이 `response_model`를 사용하여: /// info | 정보 -FastAPI는 이를 위해 Pydantic 모델의 `.dict()`의 `exclude_unset` 매개변수를 사용합니다. - -/// - -/// info | 정보 - -아래 또한 사용할 수 있습니다: +다음도 사용할 수 있습니다: * `response_model_exclude_defaults=True` * `response_model_exclude_none=True` -Pydantic 문서에서 `exclude_defaults` 및 `exclude_none`에 대해 설명한 대로 사용할 수 있습니다. +`exclude_defaults` 및 `exclude_none`에 대해 Pydantic 문서에 설명된 대로 사용할 수 있습니다. /// -#### 기본값이 있는 필드를 갖는 값의 데이터 +#### 기본값이 있는 필드에 값이 있는 데이터 { #data-with-values-for-fields-with-defaults } -하지만 모델의 필드가 기본값이 있어도 ID가 `bar`인 항목(items)처럼 데이터가 값을 갖는다면: +하지만 ID가 `bar`인 항목처럼, 기본값이 있는 모델의 필드에 값이 있다면: ```Python hl_lines="3 5" { @@ -144,12 +274,11 @@ FastAPI는 이를 위해 Pydantic 모델의 `.dict()`의 `http.HTTPStatus` 와 같은 `IntEnum` 을 입력받을 수도 있습니다. /// `status_code` 매개변수는: * 응답에서 해당 상태 코드를 반환합니다. -* 상태 코드를 OpenAPI 스키마(및 사용자 인터페이스)에 문서화 합니다. +* 상태 코드를 OpenAPI 스키마(따라서, 사용자 인터페이스에도)에 문서화합니다: - + /// note | 참고 -어떤 응답 코드들은 해당 응답에 본문이 없다는 것을 의미하기도 합니다 (다음 항목 참고). +일부 응답 코드(다음 섹션 참고)는 응답에 본문이 없다는 것을 나타냅니다. -이에 따라 FastAPI는 응답 본문이 없음을 명시하는 OpenAPI를 생성합니다. +FastAPI는 이를 알고 있으며, 응답 본문이 없다고 명시하는 OpenAPI 문서를 생성합니다. /// -## HTTP 상태 코드에 대하여 +## HTTP 상태 코드에 대하여 { #about-http-status-codes } /// note | 참고 -만약 HTTP 상태 코드에 대하여 이미 알고있다면, 다음 항목으로 넘어가십시오. +만약 HTTP 상태 코드가 무엇인지 이미 알고 있다면, 다음 섹션으로 넘어가세요. /// -HTTP는 세자리의 숫자 상태 코드를 응답의 일부로 전송합니다. +HTTP에서는 응답의 일부로 3자리 숫자 상태 코드를 보냅니다. -이 상태 코드들은 각자를 식별할 수 있도록 지정된 이름이 있으나, 중요한 것은 숫자 코드입니다. +이 상태 코드들은 이를 식별할 수 있도록 이름이 연결되어 있지만, 중요한 부분은 숫자입니다. 요약하자면: -* `1xx` 상태 코드는 "정보"용입니다. 이들은 직접적으로는 잘 사용되지는 않습니다. 이 상태 코드를 갖는 응답들은 본문을 가질 수 없습니다. -* **`2xx`** 상태 코드는 "성공적인" 응답을 위해 사용됩니다. 가장 많이 사용되는 유형입니다. - * `200` 은 디폴트 상태 코드로, 모든 것이 "성공적임"을 의미합니다. - * 다른 예로는 `201` "생성됨"이 있습니다. 일반적으로 데이터베이스에 새로운 레코드를 생성한 후 사용합니다. - * 단, `204` "내용 없음"은 특별한 경우입니다. 이것은 클라이언트에게 반환할 내용이 없는 경우 사용합니다. 따라서 응답은 본문을 가질 수 없습니다. -* **`3xx`** 상태 코드는 "리다이렉션"용입니다. 본문을 가질 수 없는 `304` "수정되지 않음"을 제외하고, 이 상태 코드를 갖는 응답에는 본문이 있을 수도, 없을 수도 있습니다. -* **`4xx`** 상태 코드는 "클라이언트 오류" 응답을 위해 사용됩니다. 이것은 아마 가장 많이 사용하게 될 두번째 유형입니다. - * 일례로 `404` 는 "찾을 수 없음" 응답을 위해 사용합니다. - * 일반적인 클라이언트 오류의 경우 `400` 을 사용할 수 있습니다. -* `5xx` 상태 코드는 서버 오류에 사용됩니다. 이것들을 직접 사용할 일은 거의 없습니다. 응용 프로그램 코드나 서버의 일부에서 문제가 발생하면 자동으로 이들 상태 코드 중 하나를 반환합니다. +* `100 - 199` 는 "정보"용입니다. 직접 사용할 일은 거의 없습니다. 이 상태 코드를 갖는 응답은 본문을 가질 수 없습니다. +* **`200 - 299`** 는 "성공적인" 응답을 위한 것입니다. 가장 많이 사용하게 될 유형입니다. + * `200` 은 기본 상태 코드로, 모든 것이 "OK"임을 의미합니다. + * 다른 예로는 `201` "생성됨"이 있습니다. 일반적으로 데이터베이스에 새 레코드를 생성한 후 사용합니다. + * 특별한 경우로 `204` "내용 없음"이 있습니다. 이 응답은 클라이언트에게 반환할 내용이 없을 때 사용되며, 따라서 응답은 본문을 가지면 안 됩니다. +* **`300 - 399`** 는 "리다이렉션"용입니다. 이 상태 코드를 갖는 응답은 본문이 있을 수도 없을 수도 있으며, 본문이 없어야 하는 `304` "수정되지 않음"을 제외합니다. +* **`400 - 499`** 는 "클라이언트 오류" 응답을 위한 것입니다. 아마 두 번째로 가장 많이 사용하게 될 유형입니다. + * 예를 들어 `404` 는 "찾을 수 없음" 응답을 위해 사용합니다. + * 클라이언트의 일반적인 오류에는 `400` 을 그냥 사용할 수 있습니다. +* `500 - 599` 는 서버 오류에 사용됩니다. 직접 사용할 일은 거의 없습니다. 애플리케이션 코드의 일부나 서버에서 문제가 발생하면 자동으로 이들 상태 코드 중 하나를 반환합니다. /// tip | 팁 -각각의 상태 코드와 이들이 의미하는 내용에 대해 더 알고싶다면 MDN HTTP 상태 코드에 관한 문서 를 확인하십시오. +각 상태 코드와 어떤 코드가 어떤 용도인지 더 알고 싶다면 MDN의 HTTP 상태 코드에 관한 문서를 확인하세요. /// -## 이름을 기억하는 쉬운 방법 +## 이름을 기억하는 쉬운 방법 { #shortcut-to-remember-the-names } -상기 예시 참고: +이전 예시를 다시 확인해보겠습니다: -{* ../../docs_src/response_status_code/tutorial001.py hl[6] *} +{* ../../docs_src/response_status_code/tutorial001_py39.py hl[6] *} -`201` 은 "생성됨"를 의미하는 상태 코드입니다. +`201` 은 "생성됨"을 위한 상태 코드입니다. -하지만 모든 상태 코드들이 무엇을 의미하는지 외울 필요는 없습니다. +하지만 각각의 코드가 무엇을 의미하는지 외울 필요는 없습니다. `fastapi.status` 의 편의 변수를 사용할 수 있습니다. -{* ../../docs_src/response_status_code/tutorial002.py hl[1,6] *} +{* ../../docs_src/response_status_code/tutorial002_py39.py hl[1,6] *} -이것은 단순히 작업을 편리하게 하기 위한 것으로, HTTP 상태 코드와 동일한 번호를 갖고있지만, 이를 사용하면 편집기의 자동완성 기능을 사용할 수 있습니다: +이것들은 단지 편의를 위한 것으로, 동일한 숫자를 갖고 있지만, 이를 통해 편집기의 자동완성 기능으로 찾을 수 있습니다: - + -/// note | 기술적 세부사항 +/// note | 기술 세부사항 `from starlette import status` 역시 사용할 수 있습니다. @@ -96,6 +96,6 @@ HTTP는 세자리의 숫자 상태 코드를 응답의 일부로 전송합니다 /// -## 기본값 변경 +## 기본값 변경 { #changing-the-default } -추후 여기서 선언하는 기본 상태 코드가 아닌 다른 상태 코드를 반환하는 방법을 [숙련된 사용자 지침서](https://fastapi.tiangolo.com/ko/advanced/response-change-status-code/){.internal-link target=_blank}에서 확인할 수 있습니다. +나중에 [고급 사용자 지침서](../advanced/response-change-status-code.md){.internal-link target=_blank}에서, 여기서 선언하는 기본값과 다른 상태 코드를 반환하는 방법을 확인할 수 있습니다. diff --git a/docs/ko/docs/tutorial/schema-extra-example.md b/docs/ko/docs/tutorial/schema-extra-example.md index 77e94db720..b2b54836ac 100644 --- a/docs/ko/docs/tutorial/schema-extra-example.md +++ b/docs/ko/docs/tutorial/schema-extra-example.md @@ -1,43 +1,21 @@ -# 요청 예제 데이터 선언 +# 요청 예제 데이터 선언 { #declare-request-example-data } 여러분의 앱이 받을 수 있는 데이터 예제를 선언할 수 있습니다. 여기 이를 위한 몇가지 방식이 있습니다. -## Pydantic 모델 속 추가 JSON 스키마 데이터 +## Pydantic 모델 속 추가 JSON 스키마 데이터 { #extra-json-schema-data-in-pydantic-models } 생성된 JSON 스키마에 추가될 Pydantic 모델을 위한 `examples`을 선언할 수 있습니다. -//// tab | Pydantic v2 - {* ../../docs_src/schema_extra_example/tutorial001_py310.py hl[13:24] *} -//// - -//// tab | Pydantic v1 - -{* ../../docs_src/schema_extra_example/tutorial001_pv1_py310.py hl[13:23] *} - -//// - 추가 정보는 있는 그대로 해당 모델의 **JSON 스키마** 결과에 추가되고, API 문서에서 사용합니다. -//// tab | Pydantic v2 - -Pydantic 버전 2에서 Pydantic 공식 문서: Model Config에 나와 있는 것처럼 `dict`를 받는 `model_config` 어트리뷰트를 사용할 것입니다. +Pydantic 문서: Configuration에 설명된 것처럼 `dict`를 받는 `model_config` 어트리뷰트를 사용할 수 있습니다. `"json_schema_extra"`를 생성된 JSON 스키마에서 보여주고 싶은 별도의 데이터와 `examples`를 포함하는 `dict`으로 설정할 수 있습니다. -//// - -//// tab | Pydantic v1 - -Pydantic v1에서 Pydantic 공식 문서: Schema customization에서 설명하는 것처럼, 내부 클래스인 `Config`와 `schema_extra`를 사용할 것입니다. - -`schema_extra`를 생성된 JSON 스키마에서 보여주고 싶은 별도의 데이터와 `examples`를 포함하는 `dict`으로 설정할 수 있습니다. - -//// - /// tip | 팁 JSON 스키마를 확장하고 여러분의 별도의 자체 데이터를 추가하기 위해 같은 기술을 사용할 수 있습니다. @@ -52,19 +30,19 @@ JSON 스키마를 확장하고 여러분의 별도의 자체 데이터를 추가 그 전에는, 하나의 예제만 가능한 `example` 키워드만 지원했습니다. 이는 아직 OpenAPI 3.1.0에서 지원하지만, 지원이 종료될 것이며 JSON 스키마 표준에 포함되지 않습니다. 그렇기에 `example`을 `examples`으로 이전하는 것을 추천합니다. 🤓 -이 문서 끝에 더 많은 읽을거리가 있습니다. +이 페이지 끝에서 더 많은 내용을 읽을 수 있습니다. /// -## `Field` 추가 인자 +## `Field` 추가 인자 { #field-additional-arguments } Pydantic 모델과 같이 `Field()`를 사용할 때 추가적인 `examples`를 선언할 수 있습니다: {* ../../docs_src/schema_extra_example/tutorial002_py310.py hl[2,8:11] *} -## JSON Schema에서의 `examples` - OpenAPI +## JSON Schema에서의 `examples` - OpenAPI { #examples-in-json-schema-openapi } -이들 중에서 사용합니다: +다음 중 하나를 사용할 때: * `Path()` * `Query()` @@ -74,45 +52,45 @@ Pydantic 모델과 같이 `Field()`를 사용할 때 추가적인 `examples`를 * `Form()` * `File()` -**OpenAPI**의 **JSON 스키마**에 추가될 부가적인 정보를 포함한 `examples` 모음을 선언할 수 있습니다. +**OpenAPI** 안의 **JSON 스키마**에 추가될 부가적인 정보를 포함한 `examples` 모음을 선언할 수도 있습니다. -### `examples`를 포함한 `Body` +### `examples`를 포함한 `Body` { #body-with-examples } 여기, `Body()`에 예상되는 예제 데이터 하나를 포함한 `examples`를 넘겼습니다: {* ../../docs_src/schema_extra_example/tutorial003_an_py310.py hl[22:29] *} -### 문서 UI 예시 +### 문서 UI 예시 { #example-in-the-docs-ui } 위의 어느 방법과 함께라면 `/docs`에서 다음과 같이 보일 것입니다: -### 다중 `examples`를 포함한 `Body` +### 다중 `examples`를 포함한 `Body` { #body-with-multiple-examples } 물론 여러 `examples`를 넘길 수 있습니다: {* ../../docs_src/schema_extra_example/tutorial004_an_py310.py hl[23:38] *} -이와 같이 하면 이 예제는 그 본문 데이터를 위한 내부 **JSON 스키마**의 일부가 될 것입니다. +이와 같이 하면 예제들은 그 본문 데이터의 내부 **JSON 스키마**의 일부가 될 것입니다. 그럼에도 불구하고, 지금 이 문서를 작성하는 시간에, 문서 UI를 보여주는 역할을 맡은 Swagger UI는 **JSON 스키마** 속 데이터를 위한 여러 예제의 표현을 지원하지 않습니다. 하지만 해결 방안을 밑에서 읽어보세요. -### OpenAPI-특화 `examples` +### OpenAPI-특화 `examples` { #openapi-specific-examples } -**JSON 스키마**가 `examples`를 지원하기 전 부터, OpenAPI는 `examples`이라 불리는 다른 필드를 지원해 왔습니다. +**JSON 스키마**가 `examples`를 지원하기 전부터 OpenAPI는 `examples`이라 불리는 다른 필드를 지원해 왔습니다. -이 **OpenAPI-특화** `examples`는 OpenAPI 명세서의 다른 구역으로 들어갑니다. 각 JSON 스키마 내부가 아니라 **각 *경로 작동* 세부 정보**에 포함됩니다. +이 **OpenAPI-특화** `examples`는 OpenAPI 명세서의 다른 구역으로 들어갑니다. 각 JSON 스키마 내부가 아니라 **각 *경로 처리* 세부 정보**에 포함됩니다. 그리고 Swagger UI는 이 특정한 `examples` 필드를 한동안 지원했습니다. 그래서, 이를 다른 **문서 UI에 있는 예제**를 **표시**하기 위해 사용할 수 있습니다. -이 OpenAPI-특화 필드인 `examples`의 형태는 (`list`대신에) **다중 예제**가 포함된 `dict`이며, 각각의 별도 정보 또한 **OpenAPI**에 추가될 것입니다. +이 OpenAPI-특화 필드인 `examples`의 형태는 (`list` 대신에) **다중 예제**가 포함된 `dict`이며, 각각의 별도 정보 또한 **OpenAPI**에 추가될 것입니다. -이는 OpenAPI에 포함된 JSON 스키마 안으로 포함되지 않으며, *경로 작동*에 직접적으로 포함됩니다. +이는 OpenAPI에 포함된 각 JSON 스키마 안으로 포함되지 않으며, *경로 처리*에 직접적으로 포함됩니다. -### `openapi_examples` 매개변수 사용하기 +### `openapi_examples` 매개변수 사용하기 { #using-the-openapi-examples-parameter } -다음 예시 속에 OpenAPI-특화 `examples`를 FastAPI 안에서 매개변수 `openapi_examples` 매개변수와 함께 선언할 수 있습니다: +다음에 대해 FastAPI에서 매개변수 `openapi_examples`로 OpenAPI-특화 `examples`를 선언할 수 있습니다: * `Path()` * `Query()` @@ -122,26 +100,26 @@ Pydantic 모델과 같이 `Field()`를 사용할 때 추가적인 `examples`를 * `Form()` * `File()` -`dict`의 키가 또 다른 `dict`인 각 예제와 값을 구별합니다. +`dict`의 키는 각 예제를 식별하고, 각 값은 또 다른 `dict`입니다. -각각의 특정 `examples` 속 `dict` 예제는 다음을 포함할 수 있습니다: +`examples` 안의 각 특정 예제 `dict`에는 다음이 포함될 수 있습니다: * `summary`: 예제에 대한 짧은 설명문. * `description`: 마크다운 텍스트를 포함할 수 있는 긴 설명문. * `value`: 실제로 보여지는 예시, 예를 들면 `dict`. -* `externalValue`: `value`의 대안이며 예제를 가르키는 URL. 비록 `value`처럼 많은 도구를 지원하지 못할 수 있습니다. +* `externalValue`: `value`의 대안이며 예제를 가리키는 URL. 비록 `value`처럼 많은 도구를 지원하지 못할 수 있습니다. 이를 다음과 같이 사용할 수 있습니다: {* ../../docs_src/schema_extra_example/tutorial005_an_py310.py hl[23:49] *} -### 문서 UI에서의 OpenAPI 예시 +### 문서 UI에서의 OpenAPI 예시 { #openapi-examples-in-the-docs-ui } -`Body()`에 추가된 `openapi_examples`를 포함한 `/docs`는 다음과 같이 보일 것입니다: +`Body()`에 `openapi_examples`가 추가되면 `/docs`는 다음과 같이 보일 것입니다: -## 기술적 세부 사항 +## 기술적 세부 사항 { #technical-details } /// tip | 팁 @@ -167,12 +145,12 @@ JSON 스키마는 `examples`를 가지고 있지 않았고, 따라서 OpenAPI는 OpenAPI는 또한 `example`과 `examples` 필드를 명세서의 다른 부분에 추가했습니다: -* `(명세서에 있는) Parameter Object`는 FastAPI의 다음 기능에서 쓰였습니다: +* `Parameter Object` (명세서에 있는)는 FastAPI의 다음 기능에서 쓰였습니다: * `Path()` * `Query()` * `Header()` * `Cookie()` -* (명세서에 있는)`Media Type Object`속 `content`에 있는 `Request Body Object`는 FastAPI의 다음 기능에서 쓰였습니다: +* `Request Body Object`, `Media Type Object` (명세서에 있는)의 `content` 필드에 있는는 FastAPI의 다음 기능에서 쓰였습니다: * `Body()` * `File()` * `Form()` @@ -183,15 +161,15 @@ OpenAPI는 또한 `example`과 `examples` 필드를 명세서의 다른 부분 /// -### JSON 스키마의 `examples` 필드 +### JSON 스키마의 `examples` 필드 { #json-schemas-examples-field } -하지만, 후에 JSON 스키마는 `examples`필드를 명세서의 새 버전에 추가했습니다. +하지만, 후에 JSON 스키마는 `examples` 필드를 명세서의 새 버전에 추가했습니다. 그리고 새로운 OpenAPI 3.1.0은 이 새로운 `examples` 필드가 포함된 최신 버전 (JSON 스키마 2020-12)을 기반으로 했습니다. -이제 새로운 `examples` 필드는 이전의 단일 (그리고 커스텀) `example` 필드보다 우선되며, `example`은 사용하지 않는 것이 좋습니다. +그리고 이제 이 새로운 `examples` 필드는 이제 지원 중단된, 예전의 단일 (그리고 커스텀) `example` 필드보다 우선됩니다. -JSON 스키마의 새로운 `examples` 필드는 예제 속 **단순한 `list`**이며, (위에서 상술한 것처럼) OpenAPI의 다른 곳에 존재하는 dict으로 된 추가적인 메타데이터가 아닙니다. +JSON 스키마의 새로운 `examples` 필드는 예제의 **단순한 `list`**일 뿐이며, (위에서 상술한 것처럼) OpenAPI의 다른 곳에 존재하는 추가 메타데이터가 있는 dict가 아닙니다. /// info | 정보 @@ -201,24 +179,24 @@ JSON 스키마의 새로운 `examples` 필드는 예제 속 **단순한 `list`** /// -### Pydantic과 FastAPI `examples` +### Pydantic과 FastAPI `examples` { #pydantic-and-fastapi-examples } -`examples`를 Pydantic 모델 속에 추가할 때, `schema_extra` 혹은 `Field(examples=["something"])`를 사용하면 Pydantic 모델의 **JSON 스키마**에 해당 예시가 추가됩니다. +Pydantic 모델 안에 `examples`를 추가할 때, `schema_extra` 또는 `Field(examples=["something"])`를 사용하면 그 예제는 해당 Pydantic 모델의 **JSON 스키마**에 추가됩니다. 그리고 Pydantic 모델의 **JSON 스키마**는 API의 **OpenAPI**에 포함되고, 그 후 문서 UI 속에서 사용됩니다. -FastAPI 0.99.0 이전 버전에서 (0.99.0 이상 버전은 새로운 OpenAPI 3.1.0을 사용합니다), `example` 혹은 `examples`를 다른 유틸리티(`Query()`, `Body()` 등)와 함께 사용했을 때, 저러한 예시는 데이터를 설명하는 JSON 스키마에 추가되지 않으며 (심지어 OpenAPI의 자체 JSON 스키마에도 포함되지 않습니다), OpenAPI의 *경로 작동* 선언에 직접적으로 추가됩니다 (JSON 스키마를 사용하는 OpenAPI 부분 외에도). +FastAPI 0.99.0 이전 버전에서 (0.99.0 이상 버전은 새로운 OpenAPI 3.1.0을 사용합니다), 다른 유틸리티(`Query()`, `Body()` 등)와 함께 `example` 또는 `examples`를 사용했을 때, 그러한 예제는 그 데이터를 설명하는 JSON 스키마에 추가되지 않고 (OpenAPI 자체의 JSON 스키마에도 포함되지 않습니다), OpenAPI의 *경로 처리* 선언에 직접적으로 추가됩니다 (JSON 스키마를 사용하는 OpenAPI 부분 밖에서). -하지만 지금은 FastAPI 0.99.0 및 이후 버전에서는 JSON 스키마 2020-12를 사용하는 OpenAPI 3.1.0과 Swagger UI 5.0.0 및 이후 버전을 사용하며, 모든 것이 더 일관성을 띄고 예시는 JSON 스키마에 포함됩니다. +하지만 이제 FastAPI 0.99.0 및 이후 버전에서는 JSON 스키마 2020-12를 사용하는 OpenAPI 3.1.0과 Swagger UI 5.0.0 및 이후 버전을 사용하기 때문에, 모든 것이 더 일관성을 띄고 예제도 JSON 스키마에 포함됩니다. -### Swagger UI와 OpenAPI-특화 `examples` +### Swagger UI와 OpenAPI-특화 `examples` { #swagger-ui-and-openapi-specific-examples } -현재 (2023-08-26), Swagger UI가 다중 JSON 스키마 예시를 지원하지 않으며, 사용자는 다중 예시를 문서에 표시하는 방법이 없었습니다. +Swagger UI는 다중 JSON 스키마 예제를 지원하지 않았기 때문에(2023-08-26 기준), 사용자는 문서에 여러 예제를 표시할 방법이 없었습니다. -이를 해결하기 위해, FastAPI `0.103.0`은 새로운 매개변수인 `openapi_examples`를 포함하는 예전 **OpenAPI-특화** `examples` 필드를 선언하기 위한 **지원을 추가**했습니다. 🤓 +이를 해결하기 위해, FastAPI `0.103.0`은 새로운 매개변수인 `openapi_examples`로 동일한 예전 **OpenAPI-특화** `examples` 필드를 선언하는 **지원**을 추가했습니다. 🤓 -### 요약 +### 요약 { #summary } -저는 역사를 그다지 좋아하는 편이 아니라고 말하고는 했지만... "기술 역사" 강의를 가르치는 지금의 저를 보세요. +저는 역사를 그다지 좋아하는 편이 아니라고 말하고는 했지만... "기술 역사" 강의를 하는 지금의 저를 보세요. 😅 -요약하자면 **FastAPI 0.99.0 혹은 그 이상의 버전**으로 업그레이드하는 것은 많은 것들이 더 **쉽고, 일관적이며 직관적이게** 되며, 여러분은 이 모든 역사적 세부 사항을 알 필요가 없습니다. 😎 +요약하자면 **FastAPI 0.99.0 혹은 그 이상의 버전**으로 업그레이드하면, 많은 것들이 훨씬 더 **단순하고, 일관적이며 직관적**이 되며, 여러분은 이 모든 역사적 세부 사항을 알 필요가 없습니다. 😎 diff --git a/docs/ko/docs/tutorial/security/get-current-user.md b/docs/ko/docs/tutorial/security/get-current-user.md index 98ef3885e4..f21a22b7a0 100644 --- a/docs/ko/docs/tutorial/security/get-current-user.md +++ b/docs/ko/docs/tutorial/security/get-current-user.md @@ -1,105 +1,105 @@ -# 현재 사용자 가져오기 +# 현재 사용자 가져오기 { #get-current-user } -이전 장에서 (의존성 주입 시스템을 기반으로 한)보안 시스템은 *경로 작동 함수*에서 `str`로 `token`을 제공했습니다: +이전 장에서 (의존성 주입 시스템을 기반으로 한) 보안 시스템은 *경로 처리 함수*에 `str`로 `token`을 제공했습니다: -{* ../../docs_src/security/tutorial001.py hl[10] *} +{* ../../docs_src/security/tutorial001_an_py39.py hl[12] *} -그러나 아직도 유용하지 않습니다. +하지만 이는 여전히 그다지 유용하지 않습니다. -현재 사용자를 제공하도록 합시다. +현재 사용자를 제공하도록 해봅시다. -## 유저 모델 생성하기 +## 사용자 모델 생성하기 { #create-a-user-model } -먼저 Pydantic 유저 모델을 만들어 보겠습니다. +먼저 Pydantic 사용자 모델을 만들어 봅시다. -Pydantic을 사용하여 본문을 선언하는 것과 같은 방식으로 다른 곳에서 사용할 수 있습니다. +Pydantic을 사용해 본문을 선언하는 것과 같은 방식으로, 다른 곳에서도 어디서든 사용할 수 있습니다: -{* ../../docs_src/security/tutorial002.py hl[5,12:16] *} +{* ../../docs_src/security/tutorial002_an_py310.py hl[5,12:6] *} -## `get_current_user` 의존성 생성하기 +## `get_current_user` 의존성 생성하기 { #create-a-get-current-user-dependency } 의존성 `get_current_user`를 만들어 봅시다. -의존성이 하위 의존성을 가질 수 있다는 것을 기억하십니까? +의존성이 하위 의존성을 가질 수 있다는 것을 기억하시나요? -`get_current_user`는 이전에 생성한 것과 동일한 `oauth2_scheme`과 종속성을 갖게 됩니다. +`get_current_user`는 이전에 생성한 것과 동일한 `oauth2_scheme`에 대한 의존성을 갖게 됩니다. -이전에 *경로 작동*에서 직접 수행했던 것과 동일하게 새 종속성 `get_current_user`는 하위 종속성 `oauth2_scheme`에서 `str`로 `token`을 수신합니다. +이전에 *경로 처리*에서 직접 수행했던 것과 동일하게, 새 의존성 `get_current_user`는 하위 의존성 `oauth2_scheme`로부터 `str`로 `token`을 받게 됩니다: -{* ../../docs_src/security/tutorial002.py hl[25] *} +{* ../../docs_src/security/tutorial002_an_py310.py hl[25] *} -## 유저 가져오기 +## 사용자 가져오기 { #get-the-user } -`get_current_user`는 토큰을 `str`로 취하고 Pydantic `User` 모델을 반환하는 우리가 만든 (가짜) 유틸리티 함수를 사용합니다. +`get_current_user`는 우리가 만든 (가짜) 유틸리티 함수를 사용합니다. 이 함수는 `str`로 토큰을 받아 Pydantic `User` 모델을 반환합니다: -{* ../../docs_src/security/tutorial002.py hl[19:22,26:27] *} +{* ../../docs_src/security/tutorial002_an_py310.py hl[19:22,26:27] *} -## 현재 유저 주입하기 +## 현재 사용자 주입하기 { #inject-the-current-user } -이제 *경로 작동*에서 `get_current_user`와 동일한 `Depends`를 사용할 수 있습니다. +이제 *경로 처리*에서 `get_current_user`와 함께 같은 `Depends`를 사용할 수 있습니다: -{* ../../docs_src/security/tutorial002.py hl[31] *} +{* ../../docs_src/security/tutorial002_an_py310.py hl[31] *} -Pydantic 모델인 `User`로 `current_user`의 타입을 선언하는 것을 알아야 합니다. +`current_user`의 타입을 Pydantic 모델 `User`로 선언한다는 점에 주목하세요. -이것은 모든 완료 및 타입 검사를 통해 함수 내부에서 우리를 도울 것입니다. +이는 함수 내부에서 자동 완성과 타입 체크에 도움을 줍니다. /// tip | 팁 -요청 본문도 Pydantic 모델로 선언된다는 것을 기억할 것입니다. +요청 본문도 Pydantic 모델로 선언된다는 것을 기억하실지도 모릅니다. -여기서 **FastAPI**는 `Depends`를 사용하고 있기 때문에 혼동되지 않습니다. +여기서 **FastAPI**는 `Depends`를 사용하고 있기 때문에 혼동하지 않습니다. /// /// check | 확인 -이 의존성 시스템이 설계된 방식은 모두 `User` 모델을 반환하는 다양한 의존성(다른 "의존적인")을 가질 수 있도록 합니다. +이 의존성 시스템이 설계된 방식은 모두 `User` 모델을 반환하는 서로 다른 의존성(서로 다른 "dependables")을 가질 수 있도록 합니다. -해당 타입의 데이터를 반환할 수 있는 의존성이 하나만 있는 것으로 제한되지 않습니다. +해당 타입의 데이터를 반환할 수 있는 의존성이 하나만 있어야 하는 것으로 제한되지 않습니다. /// -## 다른 모델 +## 다른 모델 { #other-models } -이제 *경로 작동 함수*에서 현재 사용자를 직접 가져올 수 있으며 `Depends`를 사용하여 **의존성 주입** 수준에서 보안 메커니즘을 처리할 수 있습니다. +이제 *경로 처리 함수*에서 현재 사용자를 직접 가져올 수 있으며, `Depends`를 사용해 **의존성 주입** 수준에서 보안 메커니즘을 처리할 수 있습니다. -그리고 보안 요구 사항에 대한 모든 모델 또는 데이터를 사용할 수 있습니다(이 경우 Pydantic 모델 `User`). +그리고 보안 요구 사항을 위해 어떤 모델이나 데이터든 사용할 수 있습니다(이 경우 Pydantic 모델 `User`). -그러나 일부 특정 데이터 모델, 클래스 또는 타입을 사용하도록 제한되지 않습니다. +하지만 특정 데이터 모델, 클래스 또는 타입만 사용해야 하는 것은 아닙니다. -모델에 `id`와 `email`이 있고 `username`이 없길 원하십니까? 맞습니다. 이들은 동일한 도구를 사용할 수 있습니다. +모델에 `id`와 `email`이 있고 `username`은 없게 하고 싶으신가요? 물론입니다. 같은 도구를 사용할 수 있습니다. -`str`만 갖고 싶습니까? 아니면 그냥 `dict`를 갖고 싶습니까? 아니면 데이터베이스 클래스 모델 인스턴스를 직접 갖고 싶습니까? 그들은 모두 같은 방식으로 작동합니다. +`str`만 갖고 싶으신가요? 아니면 `dict`만요? 또는 데이터베이스 클래스 모델 인스턴스를 직접 쓰고 싶으신가요? 모두 같은 방식으로 동작합니다. -실제로 애플리케이션에 로그인하는 사용자가 없지만 액세스 토큰만 있는 로봇, 봇 또는 기타 시스템이 있습니까? 다시 말하지만 모두 동일하게 작동합니다. +애플리케이션에 로그인하는 사용자는 없고, 액세스 토큰만 가진 로봇, 봇 또는 다른 시스템만 있나요? 이것도 마찬가지로 모두 동일하게 동작합니다. -애플리케이션에 필요한 모든 종류의 모델, 모든 종류의 클래스, 모든 종류의 데이터베이스를 사용하십시오. **FastAPI**는 의존성 주입 시스템을 다루었습니다. +애플리케이션에 필요한 어떤 종류의 모델, 어떤 종류의 클래스, 어떤 종류의 데이터베이스든 사용하세요. **FastAPI**는 의존성 주입 시스템으로 이를 지원합니다. -## 코드 사이즈 +## 코드 크기 { #code-size } -이 예는 장황해 보일 수 있습니다. 동일한 파일에서 보안, 데이터 모델, 유틸리티 기능 및 *경로 작동*을 혼합하고 있음을 염두에 두십시오. +이 예시는 장황해 보일 수 있습니다. 동일한 파일에서 보안, 데이터 모델, 유틸리티 함수 및 *경로 처리*를 섞어서 사용하고 있다는 점을 기억하세요. -그러나 이게 키포인트입니다. +하지만 여기 핵심이 있습니다. -보안과 종속성 주입 항목을 한 번만 작성하면 됩니다. +보안과 의존성 주입 관련 코드는 한 번만 작성합니다. -그리고 원하는 만큼 복잡하게 만들 수 있습니다. 그래도 유연성과 함께 한 곳에 한 번에 작성할 수 있습니다. +그리고 원하는 만큼 복잡하게 만들 수 있습니다. 그럼에도 여전히 한 번만, 한 곳에만 작성하면 됩니다. 유연성을 모두 유지하면서요. -그러나 동일한 보안 시스템을 사용하여 수천 개의 엔드포인트(*경로 작동*)를 가질 수 있습니다. +하지만 같은 보안 시스템을 사용해 수천 개의 엔드포인트(*경로 처리*)를 가질 수 있습니다. -그리고 그들 모두(또는 원하는 부분)는 이러한 의존성 또는 생성한 다른 의존성을 재사용하는 이점을 얻을 수 있습니다. +그리고 그들 모두(또는 원하는 일부)는 이러한 의존성 또는 여러분이 생성한 다른 의존성을 재사용하는 이점을 얻을 수 있습니다. -그리고 이 수천 개의 *경로 작동*은 모두 3줄 정도로 줄일 수 있습니다. +그리고 이 수천 개의 *경로 처리*는 3줄 정도로도 만들 수 있습니다: -{* ../../docs_src/security/tutorial002.py hl[30:32] *} +{* ../../docs_src/security/tutorial002_an_py310.py hl[30:32] *} -## 요약 +## 요약 { #recap } -이제 *경로 작동 함수*에서 현재 사용자를 직접 가져올 수 있습니다. +이제 *경로 처리 함수*에서 현재 사용자를 직접 가져올 수 있습니다. -우리는 이미 이들 사이에 있습니다. +우리는 이미 절반은 왔습니다. -사용자/클라이언트가 실제로 `username`과 `password`를 보내려면 *경로 작동*을 추가하기만 하면 됩니다. +사용자/클라이언트가 실제로 `username`과 `password`를 보내도록 하는 *경로 처리*만 추가하면 됩니다. -다음 장을 확인해 봅시다. +다음에 이어집니다. diff --git a/docs/ko/docs/tutorial/security/oauth2-jwt.md b/docs/ko/docs/tutorial/security/oauth2-jwt.md index 8d27856e84..907795ca4b 100644 --- a/docs/ko/docs/tutorial/security/oauth2-jwt.md +++ b/docs/ko/docs/tutorial/security/oauth2-jwt.md @@ -1,36 +1,36 @@ -# 패스워드 해싱을 이용한 OAuth2, JWT 토큰을 사용하는 Bearer 인증 +# 패스워드(해싱 포함)를 사용하는 OAuth2, JWT 토큰을 사용하는 Bearer { #oauth2-with-password-and-hashing-bearer-with-jwt-tokens } -모든 보안 흐름을 구성했으므로, 이제 JWT 토큰과 패스워드 해싱을 사용해 애플리케이션을 안전하게 만들 것입니다. +모든 보안 흐름을 구성했으므로, 이제 JWT 토큰과 안전한 패스워드 해싱을 사용해 애플리케이션을 실제로 안전하게 만들겠습니다. -이 코드는 실제로 애플리케이션에서 패스워드를 해싱하여 DB에 저장하는 등의 작업에 활용할 수 있습니다. +이 코드는 실제로 애플리케이션에서 사용할 수 있으며, 패스워드 해시를 데이터베이스에 저장하는 등의 작업에 활용할 수 있습니다. -이전 장에 이어서 시작해 봅시다. +이전 장에서 멈춘 지점부터 시작해 내용을 확장해 나가겠습니다. -## JWT +## JWT 알아보기 { #about-jwt } -JWT 는 "JSON Web Tokens" 을 의미합니다. +JWT는 "JSON Web Tokens"를 의미합니다. -JSON 객체를 공백이 없는 긴 문자열로 인코딩하는 표준이며, 다음과 같은 형태입니다: +JSON 객체를 공백이 없는 길고 밀집된 문자열로 부호화하는 표준입니다. 다음과 같은 형태입니다: ``` eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c ``` -JWT는 암호화되지 않아 누구든지 토큰에서 정보를 복원할 수 있습니다. +암호화된 것이 아니므로, 누구나 내용에서 정보를 복원할 수 있습니다. -하지만 JWT는 서명되어 있습니다. 그래서 자신이 발급한 토큰을 받았을 때, 실제로 자신이 발급한게 맞는지 검증할 수 있습니다. +하지만 서명되어 있습니다. 따라서 자신이 발급한 토큰을 받았을 때, 실제로 자신이 발급한 것이 맞는지 검증할 수 있습니다. -만료 기간이 일주일인 토큰을 발행했다고 가정해 봅시다. 다음 날 사용자가 토큰을 가져왔을 때, 그 사용자가 시스템에 여전히 로그인되어 있다는 것을 알 수 있습니다. +예를 들어 만료 기간이 1주일인 토큰을 생성할 수 있습니다. 그리고 사용자가 다음 날 토큰을 가지고 돌아오면, 그 사용자가 시스템에 여전히 로그인되어 있다는 것을 알 수 있습니다. -일주일 뒤에는 토큰이 만료될 것이고, 사용자는 인가되지 않아 새 토큰을 받기 위해 다시 로그인해야 할 것입니다. 만약 사용자(또는 제3자)가 토큰을 수정하거나 만료일을 변경하면, 서명이 일치하지 않기 때문에 알아챌 수 있을 것입니다. +1주일 뒤에는 토큰이 만료되고 사용자는 인가되지 않으므로 새 토큰을 받기 위해 다시 로그인해야 합니다. 그리고 사용자(또는 제3자)가 만료 시간을 바꾸기 위해 토큰을 수정하려고 하면, 서명이 일치하지 않기 때문에 이를 알아챌 수 있습니다. -만약 JWT 토큰을 다뤄보고, 작동 방식도 알아보고 싶다면 https://jwt.io 을 확인하십시오. +JWT 토큰을 직접 다뤄보고 동작 방식을 확인해보고 싶다면 https://jwt.io를 확인하십시오. -## `PyJWT` 설치 +## `PyJWT` 설치 { #install-pyjwt } -파이썬으로 JWT 토큰을 생성하고 검증하려면 `PyJWT` 를 설치해야 합니다. +Python에서 JWT 토큰을 생성하고 검증하려면 `PyJWT`를 설치해야 합니다. -[가상환경](../../virtual-environments.md){.internal-link target=_blank} 을 만들고 활성화한 다음 `pyjwt` 를 설치하십시오: +[가상환경](../../virtual-environments.md){.internal-link target=_blank}을 만들고 활성화한 다음 `pyjwt`를 설치하십시오:
@@ -42,77 +42,77 @@ $ pip install pyjwt
-/// info | 참고 +/// info -RSA나 ECDSA 같은 전자 서명 알고리즘을 사용하려면, `pyjwt[crypto]`라는 암호화 라이브러리 의존성을 설치해야 합니다. +RSA나 ECDSA 같은 전자 서명 알고리즘을 사용할 계획이라면, cryptography 라이브러리 의존성인 `pyjwt[crypto]`를 설치해야 합니다. -더 자세한 내용은 PyJWT 설치 에서 확인할 수 있습니다. +자세한 내용은 PyJWT Installation docs에서 확인할 수 있습니다. /// -## 패스워드 해싱 +## 패스워드 해싱 { #password-hashing } -"해싱(Hashing)"은 어떤 내용(여기서는 패스워드)을 해석할 수 없는 일련의 바이트 집합(단순 문자열)으로 변환하는 것을 의미합니다. +"해싱(Hashing)"은 어떤 내용(여기서는 패스워드)을 알아볼 수 없는 바이트 시퀀스(그냥 문자열)로 변환하는 것을 의미합니다. -동일한 내용(똑같은 패스워드)을 해싱하면 동일한 문자열을 얻습니다. +정확히 같은 내용(정확히 같은 패스워드)을 넣으면 정확히 같은 알아볼 수 없는 문자열이 나옵니다. -하지만 그 문자열을 다시 패스워드로 되돌릴 수는 없습니다. +하지만 그 알아볼 수 없는 문자열에서 다시 패스워드로 되돌릴 수는 없습니다. -### 패스워드를 해싱하는 이유 +### 패스워드 해싱을 사용하는 이유 { #why-use-password-hashing } -데이터베이스를 탈취당하더라도, 침입자는 사용자의 평문 패스워드 대신 해시 값만 얻을 수 있습니다. +데이터베이스를 탈취당하더라도, 침입자는 사용자의 평문 패스워드 대신 해시만 얻게 됩니다. -따라서 침입자는 훔친 사용자 패스워드를 다른 시스템에서 활용할 수 없습니다. (대다수 사용자가 여러 시스템에서 동일한 패스워드를 사용하기 때문에 평문 패스워드가 유출되면 위험합니다.) +따라서 침입자는 그 패스워드를 다른 시스템에서 사용해 보려고 시도할 수 없습니다(많은 사용자가 어디서나 같은 패스워드를 사용하므로, 이는 위험합니다). -## `passlib` 설치 +## `pwdlib` 설치 { #install-pwdlib } -PassLib는 패스워드 해시를 다루는 훌륭한 파이썬 패키지입니다. +pwdlib는 패스워드 해시를 다루기 위한 훌륭한 Python 패키지입니다. -많은 안전한 해시 알고리즘과 도구들을 지원합니다. +많은 안전한 해싱 알고리즘과 이를 다루기 위한 유틸리티를 지원합니다. -추천하는 알고리즘은 "Bcrypt"입니다. +추천 알고리즘은 "Argon2"입니다. -[가상환경](../../virtual-environments.md){.internal-link target=_blank} 을 만들고 활성화한 다음 PassLib와 Bcrypt를 설치하십시오: +[가상환경](../../virtual-environments.md){.internal-link target=_blank}을 만들고 활성화한 다음 Argon2와 함께 pwdlib를 설치하십시오:
```console -$ pip install "passlib[bcrypt]" +$ pip install "pwdlib[argon2]" ---> 100% ```
-/// tip | 팁 +/// tip -`passlib`를 사용하여, **Django**, **Flask** 의 보안 플러그인이나 다른 도구로 생성한 패스워드를 읽을 수 있도록 설정할 수도 있습니다. +`pwdlib`를 사용하면 **Django**, **Flask** 보안 플러그인 또는 다른 여러 도구로 생성한 패스워드를 읽을 수 있도록 설정할 수도 있습니다. -예를 들자면, FastAPI 애플리케이션과 Django 애플리케이션이 같은 데이터베이스에서 데이터를 공유할 수 있습니다. 또는 같은 데이터베이스를 사용하여 Django 애플리케이션을 점진적으로 마이그레이션 할 수도 있습니다. +따라서 예를 들어, FastAPI 애플리케이션과 Django 애플리케이션이 같은 데이터베이스에서 동일한 데이터를 공유할 수 있습니다. 또는 같은 데이터베이스를 사용하면서 Django 애플리케이션을 점진적으로 마이그레이션할 수도 있습니다. -그리고 사용자는 FastAPI 애플리케이션과 Django 애플리케이션에 동시에 로그인할 수 있습니다. +그리고 사용자는 Django 앱 또는 **FastAPI** 앱에서 동시에 로그인할 수 있습니다. /// -## 패스워드의 해시와 검증 +## 패스워드 해시 및 검증 { #hash-and-verify-the-passwords } -필요한 도구를 `passlib`에서 임포트합니다. +`pwdlib`에서 필요한 도구를 임포트합니다. -PassLib "컨텍스트(context)"를 생성합니다. 이것은 패스워드를 해싱하고 검증하는데 사용합니다. +권장 설정으로 PasswordHash 인스턴스를 생성합니다. 이는 패스워드를 해싱하고 검증하는 데 사용됩니다. -/// tip | 팁 +/// tip -PassLib 컨텍스트는 다양한 해싱 알고리즘을 사용할 수 있는 기능을 제공하며, 더 이상 사용이 권장되지 않는 오래된 해싱 알고리즘을 검증하는 기능도 포함되어 있습니다. +pwdlib는 bcrypt 해싱 알고리즘도 지원하지만 레거시 알고리즘은 포함하지 않습니다. 오래된 해시로 작업해야 한다면 passlib 라이브러리를 사용하는 것을 권장합니다. -예를 들어, 다른 시스템(Django 같은)에서 생성한 패스워드를 읽고 검증할 수 있으며, 새로운 패스워드를 Bcrypt 같은 다른 알고리즘으로 해싱할 수도 있습니다. +예를 들어, 다른 시스템(Django 같은)에서 생성한 패스워드를 읽고 검증하되, 새 패스워드는 Argon2나 Bcrypt 같은 다른 알고리즘으로 해싱하도록 할 수 있습니다. -그리고 동시에 그런 모든 알고리즘과 호환성을 유지합니다. +그리고 동시에 그 모든 것과 호환되게 만들 수 있습니다. /// 사용자로부터 받은 패스워드를 해싱하는 유틸리티 함수를 생성합니다. -그리고 받은 패스워드가 저장된 해시와 일치하는지 검증하는 또 다른 유틸리티 함수도 생성합니다. +그리고 받은 패스워드가 저장된 해시와 일치하는지 검증하는 또 다른 유틸리티도 생성합니다. 그리고 사용자를 인증하고 반환하는 또 다른 함수도 생성합니다. @@ -120,17 +120,17 @@ PassLib 컨텍스트는 다양한 해싱 알고리즘을 사용할 수 있는 /// note -새로운 (가짜) 데이터베이스 `fake_users_db`를 확인하면, 해시 처리된 패스워드가 어떻게 생겼는지 볼 수 있습니다: `"$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW"`. +새로운 (가짜) 데이터베이스 `fake_users_db`를 확인하면, 이제 해시 처리된 패스워드가 어떻게 생겼는지 볼 수 있습니다: `"$argon2id$v=19$m=65536,t=3,p=4$wagCPXjifgvUFBzq4hqe3w$CYaIb8sB+wtD+Vu/P4uod1+Qof8h+1g7bbDlBID48Rc"`. /// -## JWT 토큰 처리 +## JWT 토큰 처리 { #handle-jwt-tokens } -설치된 모듈을 임포트 합니다. +설치된 모듈을 임포트합니다. -JWT 토큰 서명에 사용될 임의의 비밀키를 생성합니다. +JWT 토큰을 서명하는 데 사용할 임의의 비밀 키를 생성합니다. -안전한 임의의 비밀키를 생성하려면 다음 명령어를 사용하십시오: +안전한 임의의 비밀 키를 생성하려면 다음 명령을 사용하십시오:
@@ -142,67 +142,67 @@ $ openssl rand -hex 32
-그리고 생성한 비밀키를 복사해 변수 `SECRET_KEY`에 대입합니다. (이 예제의 변수 값을 그대로 사용하지 마십시오.) +그리고 출력 결과를 변수 `SECRET_KEY`에 복사합니다(예제의 값을 사용하지 마십시오). -JWT 토큰을 서명하는 데 사용될 알고리즘을 위한 변수 `ALGORITHM` 을 생성하고 `"HS256"` 으로 설정합니다. +JWT 토큰을 서명하는 데 사용될 알고리즘을 위한 변수 `ALGORITHM`을 생성하고 `"HS256"`으로 설정합니다. -토큰 만료 기간을 위한 변수를 생성합니다. +토큰 만료를 위한 변수를 생성합니다. -응답을 위한 토큰 엔드포인트에 사용될 Pydantic 모델을 정의합니다. +응답을 위해 토큰 엔드포인트에서 사용될 Pydantic 모델을 정의합니다. 새 액세스 토큰을 생성하기 위한 유틸리티 함수를 생성합니다. {* ../../docs_src/security/tutorial004_an_py310.py hl[4,7,13:15,29:31,79:87] *} -## 의존성 수정 +## 의존성 업데이트 { #update-the-dependencies } -`get_current_user` 함수를 이전과 동일한 토큰을 받도록 수정하되, 이번에는 JWT 토큰을 사용하도록 합니다. +`get_current_user`가 이전과 동일한 토큰을 받도록 업데이트하되, 이번에는 JWT 토큰을 사용하도록 합니다. -받은 토큰을 디코딩하여 검증한 후 현재 사용자를 반환합니다. +받은 토큰을 디코딩하고 검증한 뒤 현재 사용자를 반환합니다. -토큰이 유효하지 않다면 HTTP 오류를 반환합니다. +토큰이 유효하지 않다면 즉시 HTTP 오류를 반환합니다. {* ../../docs_src/security/tutorial004_an_py310.py hl[90:107] *} -## `/token` 경로 작업 수정 +## `/token` *경로 처리* 업데이트 { #update-the-token-path-operation } -토큰의 만료 시각을 설정하기 위해 `timedelta` 를 생성합니다. +토큰의 만료 시간으로 `timedelta`를 생성합니다. 실제 JWT 액세스 토큰을 생성하여 반환합니다. {* ../../docs_src/security/tutorial004_an_py310.py hl[118:133] *} -### JWT "주체(subject)" `sub`에 대한 기술 세부 사항 +### JWT "주체(subject)" `sub`에 대한 기술 세부사항 { #technical-details-about-the-jwt-subject-sub } -JWT 명세에 따르면 토큰의 주체를 포함하는 `sub`라는 키가 있습니다. +JWT 명세에 따르면 토큰의 주체를 담는 `sub` 키가 있습니다. -사용 여부는 선택사항이지만, 사용자의 식별 정보를 저장할 수 있으므로 여기서는 이를 사용합니다. +선택적으로 사용할 수 있지만, 여기에 사용자 식별 정보를 넣게 되므로 여기서는 이를 사용합니다. -JWT는 사용자를 식별하고 사용자가 API를 직접 사용할 수 있도록 허용하는 것 외에도 다른 용도로 사용될 수도 있습니다. +JWT는 사용자를 식별하고 사용자가 API에서 직접 작업을 수행할 수 있도록 허용하는 것 외에도 다른 용도로 사용될 수 있습니다. -예를 들어 "자동차"나 "블로그 게시물"을 식별하는 데 사용할 수 있습니다. +예를 들어 "자동차"나 "블로그 게시물"을 식별할 수 있습니다. -그리고 "자동차를 운전하다"나 "블로그 게시물을 수정하다"처럼 해당 엔터티에 대한 권한을 추가할 수 있습니다. +그런 다음 해당 엔터티에 대한 권한(자동차의 경우 "drive", 블로그의 경우 "edit" 등)을 추가할 수 있습니다. -그 후 이 JWT 토큰을 사용자(또는 봇)에게 제공하면, 그들은 계정을 따로 만들 필요 없이 API가 생성한 JWT 토큰만으로 작업(자동차 운전 또는 블로그 게시물 편집)을 수행할 수 있습니다. +그리고 그 JWT 토큰을 사용자(또는 봇)에게 제공하면, 계정이 없어도 API가 생성한 JWT 토큰만으로 그 동작들(자동차 운전, 블로그 편집)을 수행할 수 있습니다. -이러한 개념을 활용하면 JWT는 훨씬 더 복잡한 시나리오에도 사용할 수 있습니다. +이러한 아이디어를 활용하면 JWT는 훨씬 더 정교한 시나리오에도 사용될 수 있습니다. -이 경우 여러 엔터티가 동일한 ID를 가질 수 있습니다. 예를 들어 foo라는 ID를 가진 사용자, 자동차, 블로그 게시물이 있을 수 있습니다. +그런 경우 여러 엔터티가 동일한 ID(예: `foo`)를 가질 수도 있습니다(사용자 `foo`, 자동차 `foo`, 블로그 게시물 `foo`). -그래서 ID 충돌을 방지하기 위해, 사용자의 JWT 토큰을 생성할 때 접두사로 `sub` 키를 추가할 수 있습니다. 예를 들어 `username:` 을 붙이는 방식입니다. 이 예제에서는 `sub` 값이 `username:johndoe`이 될 수 있습니다. +따라서 ID 충돌을 방지하기 위해, 사용자에 대한 JWT 토큰을 생성할 때 `sub` 키의 값에 접두사를 붙일 수 있습니다. 예를 들어 `username:` 같은 것입니다. 그러면 이 예제에서 `sub` 값은 `username:johndoe`가 될 수 있습니다. -가장 중요한 점은 `sub` 키는 전체 애플리케이션에서 고유한 식별자가 되어야 하며 문자열이어야 한다는 점입니다. +기억해야 할 중요한 점은 `sub` 키가 전체 애플리케이션에서 고유한 식별자여야 하고, 문자열이어야 한다는 것입니다. -## 확인해봅시다 +## 확인하기 { #check-it } 서버를 실행하고 문서로 이동하십시오: http://127.0.0.1:8000/docs. -다음과 같은 사용자 인터페이스를 볼 수 있습니다: +다음과 같은 사용자 인터페이스가 보일 것입니다: -이전과 같은 방법으로 애플리케이션에 인증하십시오. +이전과 같은 방법으로 애플리케이션을 인가하십시오. 다음 인증 정보를 사용하십시오: @@ -211,13 +211,13 @@ Password: `secret` /// check -코드 어디에도 평문 패스워드 "`secret`" 이 없다는 점에 유의하십시오. 해시된 버전만 있습니다. +코드 어디에도 평문 패스워드 "`secret`"은 없고, 해시된 버전만 있다는 점에 유의하십시오. /// -`/users/me/` 를 호출하면 다음과 같은 응답을 얻을 수 있습니다: +엔드포인트 `/users/me/`를 호출하면 다음과 같은 응답을 받게 됩니다: ```JSON { @@ -230,44 +230,44 @@ Password: `secret` -개발자 도구를 열어보면 전송된 데이터에 토큰만 포함된 것을 확인할 수 있습니다. 패스워드는 사용자를 인증하고 액세스 토큰을 받기 위한 첫 번째 요청에만 전송되며, 이후에는 전송되지 않습니다: +개발자 도구를 열어보면 전송된 데이터에는 토큰만 포함되어 있고, 패스워드는 사용자를 인증하고 해당 액세스 토큰을 얻기 위한 첫 번째 요청에서만 전송되며 이후에는 전송되지 않는 것을 확인할 수 있습니다: /// note -`Bearer `로 시작하는 `Authorization` 헤더에 주목하십시오. +`Bearer `로 시작하는 값을 가진 `Authorization` 헤더에 주목하십시오. /// -## `scopes` 의 고급 사용법 +## `scopes`의 고급 사용법 { #advanced-usage-with-scopes } -OAuth2는 "스코프(scopes)" 라는 개념을 갖고 있습니다. +OAuth2에는 "scopes"라는 개념이 있습니다. -이를 사용하여 JWT 토큰에 특정 권한 집합을 추가할 수 있습니다. +이를 사용해 JWT 토큰에 특정 권한 집합을 추가할 수 있습니다. -그 후 이 토큰을 사용자에게 직접 제공하거나 제3자에게 제공하여, 특정 제한사항 하에있는 API와 통신하도록 할 수 있습니다. +그런 다음 이 토큰을 사용자에게 직접 제공하거나 제3자에게 제공하여, 특정 제한사항 하에서 API와 상호작용하도록 할 수 있습니다. -**FastAPI** 에서의 사용 방법과 통합 방식은 **심화 사용자 안내서** 에서 자세히 배울 수 있습니다. +어떻게 사용하는지, 그리고 **FastAPI**에 어떻게 통합되는지는 이후 **심화 사용자 안내서**에서 배울 수 있습니다. -## 요약 +## 요약 { #recap } -지금까지 살펴본 내용을 바탕으로, OAuth2와 JWT 같은 표준을 사용하여 안전한 **FastAPI** 애플리케이션을 만들 수 있습니다. +지금까지 살펴본 내용을 바탕으로, OAuth2와 JWT 같은 표준을 사용해 안전한 **FastAPI** 애플리케이션을 설정할 수 있습니다. -거의 모든 프레임워크에서 보안 처리는 상당히 복잡한 주제입니다. +거의 모든 프레임워크에서 보안 처리는 꽤 빠르게 복잡한 주제가 됩니다. -이를 단순화하는 많은 패키지는 데이터 모델, 데이터베이스, 사용 가능한 기능들에 대해 여러 제약이 있습니다. 그리고 지나치게 단순화하는 일부 패키지들은 심각한 보안 결함을 가질 수도 있습니다. +이를 크게 단순화하는 많은 패키지들은 데이터 모델, 데이터베이스, 사용 가능한 기능들에 대해 많은 타협을 해야 합니다. 그리고 지나치게 단순화하는 일부 패키지들은 실제로 내부에 보안 결함이 있기도 합니다. --- -**FastAPI** 는 어떤 데이터베이스, 데이터 모델, 도구도 강요하지 않습니다. +**FastAPI**는 어떤 데이터베이스, 데이터 모델, 도구에도 타협하지 않습니다. -프로젝트에 가장 적합한 것을 선택할 수 있는 유연성을 제공합니다. +프로젝트에 가장 잘 맞는 것들을 선택할 수 있는 모든 유연성을 제공합니다. -그리고 `passlib` 와 `PyJWT` 처럼 잘 관리되고 널리 사용되는 패키지들을 바로 사용할 수 있습니다. **FastAPI** 는 외부 패키지 통합을 위해 복잡한 메커니즘이 필요하지 않기 때문입니다. +그리고 **FastAPI**는 외부 패키지를 통합하기 위해 복잡한 메커니즘을 요구하지 않기 때문에 `pwdlib`와 `PyJWT` 같은 잘 관리되고 널리 사용되는 패키지들을 바로 사용할 수 있습니다. -그러나 유연성, 견고성, 보안성을 해치지 않으면서 과정을 단순화할 수 있는 도구들을 제공합니다. +하지만 유연성, 견고성, 보안성을 해치지 않으면서 과정을 가능한 한 단순화할 수 있도록 도구들을 제공합니다. -그리고 OAuth2와 같은 표준 프로토콜을 비교적 간단한 방법으로 구현하고 사용할 수 있습니다. +또한 OAuth2 같은 안전한 표준 프로토콜을 비교적 간단한 방식으로 사용하고 구현할 수 있습니다. -더 세분화된 권한 체계를 위해 OAuth2의 "스코프"를 사용하는 방법은 **심화 사용자 안내서**에서 더 자세히 배울 수 있습니다. OAuth2의 스코프는 제3자 애플리케이션이 사용자를 대신해 그들의 API와 상호작용하도록 권한을 부여하기 위해, Facebook, Google, GitHub, Microsoft, X (Twitter) 등의 많은 대형 인증 제공업체들이 사용하는 메커니즘입니다. +더 세분화된 권한 시스템을 위해 OAuth2 "scopes"를 사용하는 방법은, 같은 표준을 따르는 방식으로 **심화 사용자 안내서**에서 더 자세히 배울 수 있습니다. 스코프를 사용하는 OAuth2는 Facebook, Google, GitHub, Microsoft, X (Twitter) 등 많은 대형 인증 제공업체들이 제3자 애플리케이션이 사용자 대신 그들의 API와 상호작용할 수 있도록 인가하는 데 사용하는 메커니즘입니다. diff --git a/docs/ko/docs/tutorial/security/simple-oauth2.md b/docs/ko/docs/tutorial/security/simple-oauth2.md index f10c4f588d..189dd89f2a 100644 --- a/docs/ko/docs/tutorial/security/simple-oauth2.md +++ b/docs/ko/docs/tutorial/security/simple-oauth2.md @@ -1,8 +1,8 @@ -# 패스워드와 Bearer를 이용한 간단한 OAuth2 +# 패스워드와 Bearer를 이용한 간단한 OAuth2 { #simple-oauth2-with-password-and-bearer } 이제 이전 장에서 빌드하고 누락된 부분을 추가하여 완전한 보안 흐름을 갖도록 하겠습니다. -## `username`와 `password` 얻기 +## `username`와 `password` 얻기 { #get-the-username-and-password } **FastAPI** 보안 유틸리티를 사용하여 `username` 및 `password`를 가져올 것입니다. @@ -14,11 +14,11 @@ OAuth2는 (우리가 사용하고 있는) "패스워드 플로우"을 사용할 그리고 데이터베이스 모델은 원하는 다른 이름을 사용할 수 있습니다. -그러나 로그인 *경로 작동*의 경우 사양과 호환되도록 이러한 이름을 사용해야 합니다(예를 들어 통합 API 문서 시스템을 사용할 수 있어야 합니다). +그러나 로그인 *경로 처리*의 경우 사양과 호환되도록 이러한 이름을 사용해야 합니다(예를 들어 통합 API 문서 시스템을 사용할 수 있어야 합니다). 사양에는 또한 `username`과 `password`가 폼 데이터로 전송되어야 한다고 명시되어 있습니다(따라서 여기에는 JSON이 없습니다). -### `scope` +### `scope` { #scope } 사양에는 클라이언트가 다른 폼 필드 "`scope`"를 보낼 수 있다고 나와 있습니다. @@ -44,15 +44,15 @@ OAuth2의 경우 문자열일 뿐입니다. /// -## `username`과 `password`를 가져오는 코드 +## `username`과 `password`를 가져오는 코드 { #code-to-get-the-username-and-password } 이제 **FastAPI**에서 제공하는 유틸리티를 사용하여 이를 처리해 보겠습니다. -### `OAuth2PasswordRequestForm` +### `OAuth2PasswordRequestForm` { #oauth2passwordrequestform } -먼저 `OAuth2PasswordRequestForm`을 가져와 `/token`에 대한 *경로 작동*에서 `Depends`의 의존성으로 사용합니다. +먼저 `OAuth2PasswordRequestForm`을 가져와 `/token`에 대한 *경로 처리*에서 `Depends`의 의존성으로 사용합니다. -{* ../../docs_src/security/tutorial003.py hl[4,76] *} +{* ../../docs_src/security/tutorial003_an_py310.py hl[4,78] *} `OAuth2PasswordRequestForm`은 다음을 사용하여 폼 본문을 선언하는 클래스 의존성입니다: @@ -84,7 +84,7 @@ OAuth2 사양은 실제로 `password`라는 고정 값이 있는 `grant_type` /// -### 폼 데이터 사용하기 +### 폼 데이터 사용하기 { #use-the-form-data } /// tip | 팁 @@ -100,9 +100,9 @@ OAuth2 사양은 실제로 `password`라는 고정 값이 있는 `grant_type` 오류의 경우 `HTTPException` 예외를 사용합니다: -{* ../../docs_src/security/tutorial003.py hl[3,77:79] *} +{* ../../docs_src/security/tutorial003_an_py310.py hl[3,79:81] *} -### 패스워드 확인하기 +### 패스워드 확인하기 { #check-the-password } 이 시점에서 데이터베이스의 사용자 데이터 형식을 확인했지만 암호를 확인하지 않았습니다. @@ -112,7 +112,7 @@ OAuth2 사양은 실제로 `password`라는 고정 값이 있는 `grant_type` 두 패스워드가 일치하지 않으면 동일한 오류가 반환됩니다. -#### 패스워드 해싱 +#### 패스워드 해싱 { #password-hashing } "해싱"은 일부 콘텐츠(이 경우 패스워드)를 횡설수설하는 것처럼 보이는 일련의 바이트(문자열)로 변환하는 것을 의미합니다. @@ -120,21 +120,15 @@ OAuth2 사양은 실제로 `password`라는 고정 값이 있는 `grant_type` 그러나 횡설수설에서 암호로 다시 변환할 수는 없습니다. -##### 패스워드 해싱을 사용해야 하는 이유 +##### 패스워드 해싱을 사용해야 하는 이유 { #why-use-password-hashing } 데이터베이스가 유출된 경우 해커는 사용자의 일반 텍스트 암호가 아니라 해시만 갖게 됩니다. 따라서 해커는 다른 시스템에서 동일한 암호를 사용하려고 시도할 수 없습니다(많은 사용자가 모든 곳에서 동일한 암호를 사용하므로 이는 위험할 수 있습니다). -//// tab | 파이썬 3.7 이상 +{* ../../docs_src/security/tutorial003_an_py310.py hl[82:85] *} -{* ../../docs_src/security/tutorial003.py hl[80:83] *} - -//// - -{* ../../docs_src/security/tutorial003_py310.py hl[78:81] *} - -#### `**user_dict`에 대해 +#### `**user_dict`에 대해 { #about-user-dict } `UserInDB(**user_dict)`는 다음을 의미한다: @@ -152,11 +146,11 @@ UserInDB( /// info | 정보 -`**user_dict`에 대한 자세한 설명은 [**추가 모델** 문서](../extra-models.md#about-user_indict){.internal-link target=_blank}를 다시 읽어봅시다. +`**user_dict`에 대한 자세한 설명은 [**추가 모델** 문서](../extra-models.md#about-user-in-dict){.internal-link target=_blank}를 다시 확인해보세요. /// -## 토큰 반환하기 +## 토큰 반환하기 { #return-the-token } `token` 엔드포인트의 응답은 JSON 객체여야 합니다. @@ -174,7 +168,7 @@ UserInDB( /// -{* ../../docs_src/security/tutorial003.py hl[85] *} +{* ../../docs_src/security/tutorial003_an_py310.py hl[87] *} /// tip | 팁 @@ -188,19 +182,19 @@ UserInDB( /// -## 의존성 업데이트하기 +## 의존성 업데이트하기 { #update-the-dependencies } 이제 의존성을 업데이트를 할 겁니다. 이 사용자가 활성화되어 있는 *경우에만* `current_user`를 가져올 겁니다. -따라서 `get_current_user`를 의존성으로 사용하는 추가 종속성 `get_current_active_user`를 만듭니다. +따라서 `get_current_user`를 의존성으로 사용하는 추가 의존성 `get_current_active_user`를 만듭니다. 이러한 의존성 모두, 사용자가 존재하지 않거나 비활성인 경우 HTTP 오류를 반환합니다. 따라서 엔드포인트에서는 사용자가 존재하고 올바르게 인증되었으며 활성 상태인 경우에만 사용자를 얻습니다: -{* ../../docs_src/security/tutorial003.py hl[58:66,69:72,90] *} +{* ../../docs_src/security/tutorial003_an_py310.py hl[58:66,69:74,94] *} /// info | 정보 @@ -220,11 +214,11 @@ UserInDB( /// -## 확인하기 +## 확인하기 { #see-it-in-action } 대화형 문서 열기: http://127.0.0.1:8000/docs. -### 인증하기 +### 인증하기 { #authenticate } "Authorize" 버튼을 눌러봅시다. @@ -240,7 +234,7 @@ UserInDB( -### 자신의 유저 데이터 가져오기 +### 자신의 유저 데이터 가져오기 { #get-your-own-user-data } 이제 `/users/me` 경로에 `GET` 작업을 진행합시다. @@ -266,7 +260,7 @@ UserInDB( } ``` -### 비활성된 유저 +### 비활성된 유저 { #inactive-user } 이제 비활성된 사용자로 시도하고, 인증해봅시다: @@ -284,7 +278,7 @@ UserInDB( } ``` -## 요약 +## 요약 { #recap } 이제 API에 대한 `username` 및 `password`를 기반으로 완전한 보안 시스템을 구현할 수 있는 도구가 있습니다. diff --git a/docs/ko/docs/tutorial/sql-databases.md b/docs/ko/docs/tutorial/sql-databases.md index 58c7017d6a..3d64cf627a 100644 --- a/docs/ko/docs/tutorial/sql-databases.md +++ b/docs/ko/docs/tutorial/sql-databases.md @@ -1,18 +1,18 @@ -# SQL (관계형) 데이터베이스 +# SQL (관계형) 데이터베이스 { #sql-relational-databases } -**FastAPI**에서 SQL(관계형) 데이터베이스 사용은 필수가 아닙니다. 여러분이 원하는 **어떤 데이터베이스든** 사용할 수 있습니다. +**FastAPI**에서 SQL(관계형) 데이터베이스 사용은 필수가 아닙니다. 하지만 여러분이 원하는 **어떤 데이터베이스든** 사용할 수 있습니다. 여기서는 SQLModel을 사용하는 예제를 살펴보겠습니다. -**SQLModel**은 SQLAlchemy와 Pydantic을 기반으로 구축되었습니다.SQLModel은 **SQL 데이터베이스**를 사용하는 FastAPI 애플리케이션에 완벽히 어울리도록 **FastAPI**의 제작자가 설계한 도구입니다. +**SQLModel**은 SQLAlchemy와 Pydantic을 기반으로 구축되었습니다. **SQL 데이터베이스**를 사용해야 하는 FastAPI 애플리케이션에 완벽히 어울리도록 **FastAPI**와 같은 제작자가 만든 도구입니다. /// tip | 팁 -다른 SQL 또는 NoSQL 데이터베이스 라이브러리를 사용할 수도 있습니다 (일부는 "ORM"이라고도 불립니다), FastAPI는 특정 라이브러리의 사용을 강요하지 않습니다. 😎 +다른 SQL 또는 NoSQL 데이터베이스 라이브러리를 사용할 수도 있습니다 (일부는 "ORMs"이라고도 불립니다), FastAPI는 특정 라이브러리의 사용을 강요하지 않습니다. 😎 /// -SQLModel은 SQLAlchemy를 기반으로 하므로, SQLAlchemy에서 **지원하는 모든 데이터베이스**를 손쉽게 사용할 수 있습니다(SQLModel에서도 동일하게 지원됩니다). 예를 들면: +SQLModel은 SQLAlchemy를 기반으로 하므로, SQLAlchemy에서 **지원하는 모든 데이터베이스**를 손쉽게 사용할 수 있습니다(이것들은 SQLModel에서도 지원됩니다). 예를 들면: * PostgreSQL * MySQL @@ -20,19 +20,19 @@ SQLModel은 SQLAlchemy를 기반으로 하므로, SQLAlchemy에서 **지원하 * Oracle * Microsoft SQL Server 등. -이 예제에서는 **SQLite**를 사용합니다. SQLite는 단일 파일을 사용하고 파이썬에서 기본적으로 지원하기 때문입니다. 따라서 이 예제를 그대로 복사하여 실행할 수 있습니다. +이 예제에서는 **SQLite**를 사용합니다. SQLite는 단일 파일을 사용하고 Python에서 통합 지원하기 때문입니다. 따라서 이 예제를 그대로 복사하여 실행할 수 있습니다. -나중에 실제 프로덕션 애플리케이션에서는 **PostgreSQL**과 같은 데이터베이스 서버를 사용하는 것이 좋습니다. +나중에 프로덕션 애플리케이션에서는 **PostgreSQL**과 같은 데이터베이스 서버를 사용하는 것이 좋습니다. /// tip | 팁 -**FastAPI**와 **PostgreSQL**를 포함하여 프론트엔드와 다양한 도구를 제공하는 공식 프로젝트 생성기가 있습니다: https://github.com/fastapi/full-stack-fastapi-template +프론트엔드와 더 많은 도구를 포함하여 **FastAPI**와 **PostgreSQL**을 포함한 공식 프로젝트 생성기가 있습니다: https://github.com/fastapi/full-stack-fastapi-template /// -이 튜토리얼은 매우 간단하고 짧습니다. 데이터베이스 기본 개념, SQL, 또는 더 복잡한 기능에 대해 배우고 싶다면, SQLModel 문서를 참고하세요. +이 튜토리얼은 매우 간단하고 짧습니다. 데이터베이스 기본 개념, SQL, 또는 더 고급 기능에 대해 배우고 싶다면, SQLModel 문서를 참고하세요. -## `SQLModel` 설치하기 +## `SQLModel` 설치하기 { #install-sqlmodel } 먼저, [가상 환경](../virtual-environments.md){.internal-link target=_blank}을 생성하고 활성화한 다음, `sqlmodel`을 설치하세요: @@ -45,13 +45,13 @@ $ pip install sqlmodel -## 단일 모델로 애플리케이션 생성하기 +## 단일 모델로 애플리케이션 생성하기 { #create-the-app-with-a-single-model } 우선 단일 **SQLModel** 모델을 사용하여 애플리케이션의 가장 간단한 첫 번째 버전을 생성해보겠습니다. -이후 **다중 모델**을 추가하여 보안과 유연성을 강화할 것입니다. 🤓 +이후 아래에서 **여러 모델**로 보안과 유연성을 강화하며 개선하겠습니다. 🤓 -### 모델 생성하기 +### 모델 생성하기 { #create-models } `SQLModel`을 가져오고 데이터베이스 모델을 생성합니다: @@ -61,45 +61,45 @@ $ pip install sqlmodel 몇 가지 차이점이 있습니다: -* `table=True`는 SQLModel에 이 모델이 *테이블 모델*이며, 단순한 데이터 모델이 아니라 SQL 데이터베이스의 **테이블**을 나타낸다는 것을 알려줍니다. (다른 일반적인 Pydantic 클래스처럼) 단순한 *데이터 모델*이 아닙니다. +* `table=True`는 SQLModel에 이 모델이 *테이블 모델*이며, SQL 데이터베이스의 **테이블**을 나타내야 한다는 것을 알려줍니다. (다른 일반적인 Pydantic 클래스처럼) 단순한 *데이터 모델*이 아닙니다. * `Field(primary_key=True)`는 SQLModel에 `id`가 SQL 데이터베이스의 **기본 키**임을 알려줍니다 (SQL 기본 키에 대한 자세한 내용은 SQLModel 문서를 참고하세요). - `int | None` 유형으로 설정하면, SQLModel은 해당 열이 SQL 데이터베이스에서 `INTEGER` 유형이며 `NULLABLE` 값이어야 한다는 것을 알 수 있습니다. + **참고:** 기본 키 필드에 `int | None`을 사용하는 이유는, Python 코드에서 *`id` 없이 객체를 생성*할 수 있게 하기 위해서입니다(`id=None`). 데이터베이스가 *저장할 때 생성해 줄 것*이라고 가정합니다. SQLModel은 데이터베이스가 `id`를 제공한다는 것을 이해하고, 데이터베이스 스키마에서 *해당 열을 null이 아닌 `INTEGER`*로 정의합니다. 자세한 내용은 기본 키에 대한 SQLModel 문서를 참고하세요. -* `Field(index=True)`는 SQLModel에 해당 열에 대해 **SQL 인덱스**를 생성하도록 지시합니다. 이를 통해 데이터베이스에서 이 열으로 필터링된 데이터를 읽을 때 더 빠르게 조회할 수 있습니다. +* `Field(index=True)`는 SQLModel에 해당 열에 대해 **SQL 인덱스**를 생성하도록 지시합니다. 이를 통해 데이터베이스에서 이 열로 필터링된 데이터를 읽을 때 더 빠르게 조회할 수 있습니다. SQLModel은 `str`으로 선언된 항목이 SQL 데이터베이스에서 `TEXT` (또는 데이터베이스에 따라 `VARCHAR`) 유형의 열로 저장된다는 것을 인식합니다. -### 엔진 생성하기 +### 엔진 생성하기 { #create-an-engine } SQLModel의 `engine` (내부적으로는 SQLAlchemy `engine`)은 데이터베이스에 대한 **연결을 유지**하는 역할을 합니다. -**하나의 단일 engine 객체**를 통해 코드 전체에서 동일한 데이터베이스에 연결할 수 있습니다. +코드 전체에서 동일한 데이터베이스에 연결하기 위해 **하나의 단일 `engine` 객체**를 사용합니다. {* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[14:18] hl[14:15,17:18] *} -`check_same_thread=False`를 사용하면 FastAPI에서 여러 스레드에서 동일한 SQLite 데이터베이스를 사용할 수 있습니다. 이는 **하나의 단일 요청**이 **여러 스레드**를 사용할 수 있기 때문에 필요합니다(예: 의존성에서 사용되는 경우). +`check_same_thread=False`를 사용하면 FastAPI에서 여러 스레드에서 동일한 SQLite 데이터베이스를 사용할 수 있습니다. 이는 **하나의 단일 요청**이 **둘 이상의 스레드**를 사용할 수 있기 때문에 필요합니다(예: 의존성에서 사용되는 경우). -걱정하지 마세요. 코드가 구조화된 방식으로 인해, 이후에 **각 요청마다 단일 SQLModel *세션*을 사용**하도록 보장할 것입니다. 실제로 그것이 `check_same_thread`가 하려는 것입니다. +걱정하지 마세요. 코드가 구조화된 방식으로 인해, 이후에 **각 요청마다 단일 SQLModel *세션*을 사용**하도록 보장할 것입니다. 실제로 이것이 `check_same_thread`가 하려는 것입니다. -### 테이블 생성하기 +### 테이블 생성하기 { #create-the-tables } 그 다음 `SQLModel.metadata.create_all(engine)`을 사용하여 모든 *테이블 모델*의 **테이블을 생성**하는 함수를 추가합니다. {* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[21:22] hl[21:22] *} -### 세션 의존성 생성하기 +### 세션 의존성 생성하기 { #create-a-session-dependency } **`Session`**은 **메모리에 객체**를 저장하고 데이터에 필요한 모든 변경 사항을 추적한 후, **`engine`을 통해** 데이터베이스와 통신합니다. `yield`를 사용해 FastAPI의 **의존성**을 생성하여 각 요청마다 새로운 `Session`을 제공합니다. 이는 요청당 하나의 세션만 사용되도록 보장합니다. 🤓 -그런 다음 이 의존성을 사용하는 코드를 간소화하기 위해 `Annotated` 의존성 `SessionDep`을 생성합니다. +그런 다음 이 의존성을 사용하는 나머지 코드를 간소화하기 위해 `Annotated` 의존성 `SessionDep`을 생성합니다. {* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[25:30] hl[25:27,30] *} -### 시작 시 데이터베이스 테이블 생성하기 +### 시작 시 데이터베이스 테이블 생성하기 { #create-database-tables-on-startup } 애플리케이션 시작 시 데이터베이스 테이블을 생성합니다. @@ -115,9 +115,9 @@ SQLModel은 Alembic을 감싸는 마이그레이션 유틸리티를 제공할 /// -### Hero 생성하기 +### Hero 생성하기 { #create-a-hero } -각 SQLModel 모델은 Pydantic 모델이기도 하므로, Pydantic 모델을 사용할 수 있는 **타입 어노테이**션에서 동일하게 사용할 수 있습니다. +각 SQLModel 모델은 Pydantic 모델이기도 하므로, Pydantic 모델을 사용할 수 있는 동일한 **타입 어노테이션**에서 사용할 수 있습니다. 예를 들어, 파라미터를 `Hero` 타입으로 선언하면 **JSON 본문**에서 값을 읽어옵니다. @@ -125,31 +125,29 @@ SQLModel은 Alembic을 감싸는 마이그레이션 유틸리티를 제공할 {* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[40:45] hl[40:45] *} - +여기서는 `SessionDep` 의존성(`Session`)을 사용하여 새로운 `Hero`를 `Session` 인스턴스에 추가하고, 데이터베이스에 변경 사항을 커밋하고, `hero` 데이터의 최신 상태를 갱신한 다음 이를 반환합니다. -여기서 `SessionDep` 의존성 (즉, `Session`)을 사용하여 새로운 `Hero`를 `Session` 인스턴스에 추가하고, 데이터베이스에 변경 사항을 커밋하고, `hero` 데이터의 최신 상태를 갱신한 다음 이를 반환합니다. - -### Heroes 조회하기 +### Heroes 조회하기 { #read-heroes } `select()`를 사용하여 데이터베이스에서 `Hero`를 **조회**할 수 있습니다. 결과에 페이지네이션을 적용하기 위해 `limit`와 `offset`을 포함할 수 있습니다. {* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[48:55] hl[51:52,54] *} -### 단일 Hero 조회하기 +### 단일 Hero 조회하기 { #read-one-hero } 단일 `Hero`를 **조회**할 수도 있습니다. {* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[58:63] hl[60] *} -### Hero 삭제하기 +### Hero 삭제하기 { #delete-a-hero } `Hero`를 **삭제**하는 것도 가능합니다. {* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[66:73] hl[71] *} -### 애플리케이션 실행하기 +### 애플리케이션 실행하기 { #run-the-app } -애플리케이션을 실행하려면 다음 명령을 사용합니다: +애플리케이션을 실행할 수 있습니다:
@@ -161,33 +159,33 @@ $ fastapi dev main.py
-그런 다음 `/docs` UI로 이동하면, **FastAPI**가 해당 **model들**을 사용하여 API **문서를 생성**하는 것으르 확인할 수 있습니다. 또한 이 모델들은 데이터를 직렬화하고 검증하는 데에도 사용됩니다. +그런 다음 `/docs` UI로 이동하면, **FastAPI**가 이 **모델**들을 사용해 API를 **문서화**하고, 데이터를 **직렬화**하고 **검증**하는 데에도 사용하는 것을 확인할 수 있습니다.
-## 여러 모델로 애플리케이션 업데이트 +## 여러 모델로 애플리케이션 업데이트 { #update-the-app-with-multiple-models } -이제 애플리케이션을 약간 **리팩토링**하여 **보안**과 **유연성**을 개선해 보겠습니다. +이제 이 애플리케이션을 약간 **리팩터링**하여 **보안**과 **유연성**을 개선해 보겠습니다. -이전 애플리케이션의 UI를 보면, 지금까지는 클라이언트가 생성할 `Hero`의 `id`를 직접 지정할 수 있다는 것을 알 수 있습니다. 😱 +이전 애플리케이션을 확인해 보면, 지금까지는 UI에서 클라이언트가 생성할 `Hero`의 `id`를 결정할 수 있게 되어 있는 것을 볼 수 있습니다. 😱 -이는 허용되어선 안 됩니다. 클라이언트가 이미 데이터베이스에 저장된 `id`를 덮어쓸 위험이 있기 때문입니다. `id`는 **백엔드** 또는 **데이터베이스**가 결정해야 하며, **클라이언트**가 결정해서는 안 됩니다. +이렇게 해서는 안 됩니다. 클라이언트가 DB에 이미 할당되어 있는 `id`를 덮어쓸 수 있기 때문입니다. `id`를 결정하는 것은 **백엔드** 또는 **데이터베이스**가 해야 하며, **클라이언트**가 해서는 안 됩니다. -또한 hero의 `secret_name`을 생성하긴 했지만, 지금까지는 이 값을 어디에서나 반환하고 있습니다. 이는 그다지 **비밀스럽지** 않습니다... 😅 +또한 hero에 대한 `secret_name`을 생성하지만, 지금까지는 이 값을 어디에서나 반환하고 있습니다. 이는 그다지 **비밀스럽지** 않습니다... 😅 -이러한 문제를 해결하기 위해 몇 가지 **추가 모델**을 추가할 것입니다. 바로 여기서 SQLModel이 빛을 발하게 됩니다. ✨ +이러한 문제는 몇 가지 **추가 모델**을 추가해 해결하겠습니다. 바로 여기서 SQLModel이 빛을 발하게 됩니다. ✨ -### 여러 모델 생성하기 +### 여러 모델 생성하기 { #create-multiple-models } **SQLModel**에서 `table=True`가 설정된 모델 클래스는 **테이블 모델**입니다. -`table=True`가 없는 모델 클래스는 **데이터 모델**로, 이는 실제로 몇 가지 추가 기능이 포함된 Pydantic 모델에 불과합니다. 🤓 +그리고 `table=True`가 없는 모델 클래스는 **데이터 모델**인데, 이것들은 실제로는 (몇 가지 작은 추가 기능이 있는) Pydantic 모델일 뿐입니다. 🤓 SQLModel을 사용하면 **상속**을 통해 모든 경우에 필드를 **중복 선언하지 않아도** 됩니다. -#### `HeroBase` - 기본 클래스 +#### `HeroBase` - 기본 클래스 { #herobase-the-base-class } 모든 모델에서 **공유되는 필드**를 가진 `HeroBase` 모델을 시작해 봅시다: @@ -196,14 +194,14 @@ SQLModel을 사용하면 **상속**을 통해 모든 경우에 필드를 **중 {* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:9] hl[7:9] *} -#### `Hero` - *테이블 모델* +#### `Hero` - *테이블 모델* { #hero-the-table-model } 다음으로 실제 *테이블 모델*인 `Hero`를 생성합니다. 이 모델은 다른 모델에는 항상 포함되는 건 아닌 **추가 필드**를 포함합니다: * `id` * `secret_name` -`Hero`는 `HeroBase`를 상속하므로 `HeroBase`에 선언된 필드도 포함합니다. 따라서 `Hero`는 다음 **필드들도** 가지게 됩니다: +`Hero`는 `HeroBase`를 상속하므로 `HeroBase`에 선언된 필드도 **또한** 포함합니다. 따라서 `Hero`의 모든 필드는 다음과 같습니다: * `id` * `name` @@ -212,11 +210,11 @@ SQLModel을 사용하면 **상속**을 통해 모든 경우에 필드를 **중 {* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:14] hl[12:14] *} -#### `HeroPublic` - 공개 *데이터 모델* +#### `HeroPublic` - 공개 *데이터 모델* { #heropublic-the-public-data-model } 다음으로 `HeroPublic` 모델을 생성합니다. 이 모델은 API 클라이언트에 **반환**되는 모델입니다. -`HeroPublic`은 `HeroBase`와 동일한 필드를 가지며, `secret_name`은 포함하지 않습니다. +`HeroPublic`은 `HeroBase`와 동일한 필드를 가지므로, `secret_name`은 포함하지 않습니다. 마침내 우리의 heroes의 정체가 보호됩니다! 🥷 @@ -224,9 +222,9 @@ SQLModel을 사용하면 **상속**을 통해 모든 경우에 필드를 **중 /// tip | 팁 -반환 모델이 값이 항상 존재하고 항상 `int`(`None`이 아님)를 보장하는 것은 API 클라이언트에게 매우 유용합니다. 이를 통해 API와 통신하는 개발자가 훨씬 더 간단한 코드를 작성할 수 있습니다. +반환 모델이 값이 항상 존재하고 항상 `int`(`None`이 아님)를 보장하는 것은 API 클라이언트에게 매우 유용합니다. 이를 통해 API 클라이언트는 이런 확신을 바탕으로 훨씬 더 간단한 코드를 작성할 수 있습니다. -또한 **자동으로 생성된 클라이언트**는 더 단순한 인터페이스를 제공하므로, API와 소통하는 개발자들이 훨씬 수월하게 작업할 수 있습니다. 😎 +또한 **자동으로 생성된 클라이언트**는 더 단순한 인터페이스를 제공하므로, API와 소통하는 개발자들이 API를 사용하면서 훨씬 더 좋은 경험을 할 수 있습니다. 😎 /// @@ -235,23 +233,22 @@ SQLModel을 사용하면 **상속**을 통해 모든 경우에 필드를 **중 * `id` * `name` * `age` -* `secret_name` {* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:18] hl[17:18] *} -#### `HeroCreate` - hero 생성용 *데이터 모델* +#### `HeroCreate` - hero 생성용 *데이터 모델* { #herocreate-the-data-model-to-create-a-hero } 이제 `HeroCreate` 모델을 생성합니다. 이 모델은 클라이언트로부터 받은 데이터를 **검증**하는 역할을 합니다. -`HeroCreate`는 `HeroBase와` 동일한 필드를 가지며, 추가로 `secret_name을` 포함합니다. +`HeroCreate`는 `HeroBase`와 동일한 필드를 가지며, `secret_name`도 포함합니다. -클라이언트가 **새 hero을 생성**할 때 `secret_name`을 보내고, 이는 데이터베이스에 저장되지만, 해당 비밀 이름은 API를 통해 클라이언트에게 반환되지 않습니다. +이제 클라이언트가 **새 hero를 생성**할 때 `secret_name`을 보내면, 데이터베이스에 저장되지만, 그 비밀 이름은 API를 통해 클라이언트에게 반환되지 않습니다. /// tip | 팁 이 방식은 **비밀번호**를 처리하는 방법과 동일합니다. 비밀번호를 받지만, 이를 API에서 반환하지는 않습니다. -비밀번호 값을 저장하기 전에 **해싱**하여 저장하고, **평문으로 저장하지 마십시오**. +또한 비밀번호 값을 저장하기 전에 **해싱**하여 저장하고, **평문으로 저장하지 마십시오**. /// @@ -263,15 +260,15 @@ SQLModel을 사용하면 **상속**을 통해 모든 경우에 필드를 **중 {* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:22] hl[21:22] *} -#### `HeroUpdate` - hero 수정용 *데이터 모델* +#### `HeroUpdate` - hero 수정용 *데이터 모델* { #heroupdate-the-data-model-to-update-a-hero } -이전 애플리케이션에서는 **hero를 수정**할 방법이 없었지만, 이제 **다중 모델**을 통해 수정 기능을 추가할 수 있습니다. 🎉 +이전 버전의 애플리케이션에서는 **hero를 수정**할 방법이 없었지만, 이제 **여러 모델**로 이를 할 수 있습니다. 🎉 -`HeroUpdate` *데이터 모델*은 약간 특별한데, 새 hero을 생성할 때 필요한 **모든 동일한 필드**를 가지지만, 모든 필드가 **선택적**(기본값이 있음)입니다. 이렇게 하면 hero을 수정할 때 수정하려는 필드만 보낼 수 있습니다. +`HeroUpdate` *데이터 모델*은 약간 특별한데, 새 hero를 생성할 때 필요한 **모든 동일한 필드**를 가지지만, 모든 필드가 **선택적**(기본값이 있음)입니다. 이렇게 하면 hero를 수정할 때 수정하려는 필드만 보낼 수 있습니다. -모든 **필드가 변경되기** 때문에(타입이 `None`을 포함하고, 기본값이 `None`으로 설정됨), 모든 필드를 **다시 선언**해야 합니다. +모든 **필드가 실제로 변경**되기 때문에(타입이 이제 `None`을 포함하고, 기본값도 이제 `None`이 됨), 우리는 필드를 **다시 선언**해야 합니다. -엄밀히 말하면 `HeroBase`를 상속할 필요는 없습니다. 모든 필드를 다시 선언하기 때문입니다. 일관성을 위해 상속을 유지하긴 했지만, 필수는 아닙니다. 이는 개인적인 취향의 문제입니다. 🤷 +`HeroBase`를 상속할 필요는 없습니다. 모든 필드를 다시 선언하기 때문입니다. 일관성을 위해 상속을 유지하긴 했지만, 필수는 아닙니다. 이는 개인적인 취향의 문제입니다. 🤷 `HeroUpdate`의 필드는 다음과 같습니다: @@ -281,61 +278,61 @@ SQLModel을 사용하면 **상속**을 통해 모든 경우에 필드를 **중 {* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:28] hl[25:28] *} -### `HeroCreate`로 생성하고 `HeroPublic` 반환하기 +### `HeroCreate`로 생성하고 `HeroPublic` 반환하기 { #create-with-herocreate-and-return-a-heropublic } -이제 **다중 모델**을 사용하므로 애플리케이션의 관련 부분을 업데이트할 수 있습니다. +이제 **여러 모델**을 사용하므로 애플리케이션의 관련 부분을 업데이트할 수 있습니다. 요청에서 `HeroCreate` *데이터 모델*을 받아 이를 기반으로 `Hero` *테이블 모델*을 생성합니다. 이 새 *테이블 모델* `Hero`는 클라이언트에서 보낸 필드를 가지며, 데이터베이스에서 생성된 `id`도 포함합니다. -그런 다음 함수를 통해 동일한 *테이블 모델* `Hero`를 반환합니다. 하지만 `response_model`로 `HeroPublic` *데이터 모델*을 선언했기 때문에, **FastAPI**는 `HeroPublic`을 사용하여 데이터를 검증하고 직렬화합니다. +그런 다음 함수를 통해 동일한 *테이블 모델* `Hero`를 그대로 반환합니다. 하지만 `response_model`로 `HeroPublic` *데이터 모델*을 선언했기 때문에, **FastAPI**는 `HeroPublic`을 사용하여 데이터를 검증하고 직렬화합니다. {* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[56:62] hl[56:58] *} /// tip | 팁 -이제 **반환 타입 주석** `-> HeroPublic` 대신 `response_model=HeroPublic`을 사용합니다. 반환하는 값이 실제로 `HeroPublic`이 *아니기* 때문입니다. +이제 **반환 타입 어노테이션** `-> HeroPublic` 대신 `response_model=HeroPublic`을 사용합니다. 반환하는 값이 실제로 `HeroPublic`이 *아니기* 때문입니다. -만약 `-> HeroPublic`으로 선언했다면, 에디터와 린터에서 반환값이 `HeroPublic`이 아니라 `Hero`라고 경고했을 것입니다. 이는 적절한 경고입니다. +만약 `-> HeroPublic`으로 선언했다면, 에디터와 린터에서 `HeroPublic` 대신 `Hero`를 반환한다고 (당연히) 불평할 것입니다. -`response_model`에 선언함으로써 **FastAPI**가 이를 처리하도록 하고, 타입 어노테이션과 에디터 및 다른 도구의 도움에는 영향을 미치지 않도록 설정합니다. +`response_model`에 선언함으로써 **FastAPI**가 처리하도록 하고, 타입 어노테이션과 에디터 및 다른 도구의 도움에는 영향을 미치지 않도록 합니다. /// -### `HeroPublic`으로 Heroes 조회하기 +### `HeroPublic`으로 Heroes 조회하기 { #read-heroes-with-heropublic } 이전과 동일하게 `Hero`를 **조회**할 수 있습니다. 이번에도 `response_model=list[HeroPublic]`을 사용하여 데이터가 올바르게 검증되고 직렬화되도록 보장합니다. {* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[65:72] hl[65] *} -### `HeroPublic`으로 단일 Hero 조회하기 +### `HeroPublic`으로 단일 Hero 조회하기 { #read-one-hero-with-heropublic } -단일 hero을 **조회**할 수도 있습니다: +단일 hero를 **조회**할 수도 있습니다: {* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[75:80] hl[77] *} -### `HeroUpdate`로 Hero 수정하기 +### `HeroUpdate`로 Hero 수정하기 { #update-a-hero-with-heroupdate } **hero를 수정**할 수도 있습니다. 이를 위해 HTTP `PATCH` 작업을 사용합니다. -코드에서는 클라이언트가 보낸 데이터를 딕셔너리 형태(`dict`)로 가져옵니다. 이는 **클라이언트가 보낸 데이터만 포함**하며, 기본값으로 들어가는 값은 제외합니다. 이를 위해 `exclude_unset=True`를 사용합니다. 이것이 주요 핵심입니다. 🪄 +그리고 코드에서는 클라이언트가 보낸 모든 데이터가 담긴 `dict`를 가져오는데, **클라이언트가 보낸 데이터만** 포함하고, 기본값이어서 들어가 있는 값은 제외합니다. 이를 위해 `exclude_unset=True`를 사용합니다. 이것이 주요 핵심입니다. 🪄 -그런 다음, `hero_db.sqlmodel_update(hero_data)`를 사용하여 `hero_data`의 데이터를 `hero_db`에 업데이트합니다. +그런 다음, `hero_db.sqlmodel_update(hero_data)`를 사용하여 `hero_data`의 데이터로 `hero_db`를 업데이트합니다. {* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[83:93] hl[83:84,88:89] *} -### Hero 다시 삭제하기 +### Hero 다시 삭제하기 { #delete-a-hero-again } hero **삭제**는 이전과 거의 동일합니다. -이번에는 모든 것을 리팩토링하고 싶은 욕구를 만족시키지 못할 것 같습니다. 😅 +이번에는 모든 것을 리팩터링하고 싶은 욕구를 만족시키지 못하겠습니다. 😅 {* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[96:103] hl[101] *} -### 애플리케이션 다시 실행하기 +### 애플리케이션 다시 실행하기 { #run-the-app-again } -다음 명령을 사용해 애플리케이션을 다시 실행할 수 있습니다: +애플리케이션을 다시 실행할 수 있습니다:
@@ -347,14 +344,14 @@ $ fastapi dev main.py
-`/docs` API UI로 이동하면 업데이트된 것을 확인할 수 있습니다. 클라이언트가 영웅을 생성할 때 `id`를 제공할 필요가 없게 되는 등의 변화도 보입니다. +`/docs` API UI로 이동하면 이제 업데이트되어 있고, hero를 생성할 때 클라이언트가 `id`를 보낼 것이라고 기대하지 않는 것 등을 확인할 수 있습니다.
-## 요약 +## 요약 { #recap } **SQLModel**을 사용하여 SQL 데이터베이스와 상호작용하고, *데이터 모델* 및 *테이블 모델*로 코드를 간소화할 수 있습니다. -더 많은 내용을 배우고 싶다면, **SQLModel** 문서를 참고하세요. SQLModel을 **FastAPI**와 함께 사용하는 것에 대한 더 긴 미니 튜토리얼도 제공합니다. 🚀 +더 많은 내용을 배우려면 **SQLModel** 문서를 참고하세요. **FastAPI**와 함께 SQLModel을 사용하는 더 긴 미니 튜토리얼도 있습니다. 🚀 diff --git a/docs/ko/docs/tutorial/static-files.md b/docs/ko/docs/tutorial/static-files.md index 4f3e3ab286..aa4c571793 100644 --- a/docs/ko/docs/tutorial/static-files.md +++ b/docs/ko/docs/tutorial/static-files.md @@ -1,41 +1,40 @@ -# 정적 파일 +# 정적 파일 { #static-files } -'StaticFiles'를 사용하여 디렉토리에서 정적 파일을 자동으로 제공할 수 있습니다. +`StaticFiles`를 사용하면 디렉터리에서 정적 파일을 자동으로 제공할 수 있습니다. -## `StaticFiles` 사용 +## `StaticFiles` 사용 { #use-staticfiles } -* `StaticFiles` 임포트합니다. -* 특정 경로에 `StaticFiles()` 인스턴스를 "마운트" 합니다. +* `StaticFiles`를 임포트합니다. +* 특정 경로에 `StaticFiles()` 인스턴스를 "마운트"합니다. -{* ../../docs_src/static_files/tutorial001.py hl[2,6] *} +{* ../../docs_src/static_files/tutorial001_py39.py hl[2,6] *} -/// note | 기술적 세부사항 +/// note | 기술 세부사항 -`from starlette.staticfiles import StaticFiles` 를 사용할 수도 있습니다. +`from starlette.staticfiles import StaticFiles`를 사용할 수도 있습니다. -**FastAPI**는 단지 개발자인, 당신에게 편의를 제공하기 위해 `fastapi.static files` 와 동일한 `starlett.static files`를 제공합니다. 하지만 사실 이것은 Starlett에서 직접 온 것입니다. +**FastAPI**는 개발자인 여러분의 편의를 위해 `fastapi.staticfiles`로 `starlette.staticfiles`와 동일한 것을 제공합니다. 하지만 실제로는 Starlette에서 직접 가져온 것입니다. /// -### "마운팅" 이란 +### "마운팅"이란 { #what-is-mounting } -"마운팅"은 특정 경로에 완전히 "독립적인" 애플리케이션을 추가하는 것을 의미하는데, 그 후 모든 하위 경로에 대해서도 적용됩니다. +"마운팅"은 특정 경로에 완전한 "독립적인" 애플리케이션을 추가하고, 그 애플리케이션이 모든 하위 경로를 처리하도록 하는 것을 의미합니다. -마운트된 응용 프로그램은 완전히 독립적이기 때문에 `APIRouter`를 사용하는 것과는 다릅니다. OpenAPI 및 응용 프로그램의 문서는 마운트된 응용 프로그램 등에서 어떤 것도 포함하지 않습니다. +마운트된 애플리케이션은 완전히 독립적이므로 `APIRouter`를 사용하는 것과는 다릅니다. 메인 애플리케이션의 OpenAPI 및 문서에는 마운트된 애플리케이션의 내용 등이 포함되지 않습니다. -자세한 내용은 **숙련된 사용자 안내서**에서 확인할 수 있습니다. +자세한 내용은 [고급 사용자 가이드](../advanced/index.md){.internal-link target=_blank}에서 확인할 수 있습니다. -## 세부사항 +## 세부사항 { #details } -첫 번째 `"/static"`은 이 "하위 응용 프로그램"이 "마운트"될 하위 경로를 가리킵니다. 따라서 `"/static"`으로 시작하는 모든 경로는 `"/static"`으로 처리됩니다. +첫 번째 `"/static"`은 이 "하위 애플리케이션"이 "마운트"될 하위 경로를 가리킵니다. 따라서 `"/static"`으로 시작하는 모든 경로는 이 애플리케이션이 처리합니다. -`'directory="static"`은 정적 파일이 들어 있는 디렉토리의 이름을 나타냅니다. +`directory="static"`은 정적 파일이 들어 있는 디렉터리의 이름을 나타냅니다. `name="static"`은 **FastAPI**에서 내부적으로 사용할 수 있는 이름을 제공합니다. -이 모든 매개변수는 "`static`"과 다를 수 있으며, 사용자 응용 프로그램의 요구 사항 및 구체적인 세부 정보에 따라 매개변수를 조정할 수 있습니다. +이 모든 매개변수는 "`static`"과 다를 수 있으며, 여러분의 애플리케이션 요구 사항 및 구체적인 세부 정보에 맞게 조정하세요. +## 추가 정보 { #more-info } -## 추가 정보 - -자세한 내용과 선택 사항을 보려면 Starlette의 정적 파일에 관한 문서를 확인하십시오. +자세한 내용과 옵션은 Starlette의 정적 파일 문서를 확인하세요. diff --git a/docs/ko/docs/tutorial/testing.md b/docs/ko/docs/tutorial/testing.md index 915ff6d22d..db7fb17ead 100644 --- a/docs/ko/docs/tutorial/testing.md +++ b/docs/ko/docs/tutorial/testing.md @@ -1,18 +1,18 @@ -# 테스팅 +# 테스팅 { #testing } -Starlette 덕분에 **FastAPI** 를 테스트하는 일은 쉽고 즐거운 일이 되었습니다. +Starlette 덕분에 **FastAPI** 애플리케이션을 테스트하는 일은 쉽고 즐거운 일이 되었습니다. -Starlette는 HTTPX를 기반으로 하며, 이는 Requests를 기반으로 설계되었기 때문에 매우 친숙하고 직관적입니다. +Starlette는 HTTPX를 기반으로 하며, 이는 Requests를 기반으로 설계되었기 때문에 매우 친숙하고 직관적입니다. -이를 사용하면 FastAPI에서 pytest를 직접 사용할 수 있습니다. +이를 사용하면 **FastAPI**에서 pytest를 직접 사용할 수 있습니다. -## `TestClient` 사용하기 +## `TestClient` 사용하기 { #using-testclient } /// info | 정보 -`TestClient` 사용하려면, 우선 `httpx` 를 설치해야 합니다. +`TestClient` 사용하려면, 우선 `httpx`를 설치해야 합니다. -[virtual environment](../virtual-environments.md){.internal-link target=_blank} 를 만들고, 활성화 시킨 뒤에 설치하세요. 예시: +[virtual environment](../virtual-environments.md){.internal-link target=_blank}를 만들고, 활성화 시킨 뒤에 설치하세요. 예시: ```console $ pip install httpx @@ -20,52 +20,51 @@ $ pip install httpx /// -`TestClient` 를 임포트하세요. +`TestClient`를 임포트하세요. -**FastAPI** 어플리케이션을 전달하여 `TestClient` 를 만드세요. +**FastAPI** 애플리케이션을 전달하여 `TestClient`를 만드세요. -이름이 `test_` 로 시작하는 함수를 만드세요(`pytest` 의 표준적인 관례입니다). +이름이 `test_`로 시작하는 함수를 만드세요(`pytest`의 표준적인 관례입니다). -`httpx` 를 사용하는 것과 같은 방식으로 `TestClient` 객체를 사용하세요. +`httpx`를 사용하는 것과 같은 방식으로 `TestClient` 객체를 사용하세요. -표준적인 파이썬 문법을 이용하여 확인이 필요한 곳에 간단한 `assert` 문장을 작성하세요(역시 표준적인 `pytest` 관례입니다). +표준적인 파이썬 표현식으로 확인이 필요한 곳에 간단한 `assert` 문장을 작성하세요(역시 표준적인 `pytest` 관례입니다). -{* ../../docs_src/app_testing/tutorial001.py hl[2,12,15:18] *} +{* ../../docs_src/app_testing/tutorial001_py39.py hl[2,12,15:18] *} /// tip | 팁 -테스트를 위한 함수는 `async def` 가 아니라 `def` 로 작성됨에 주의하세요. +테스트를 위한 함수는 `async def`가 아니라 `def`로 작성됨에 주의하세요. -그리고 클라이언트에 대한 호출도 `await` 를 사용하지 않는 일반 호출입니다. +그리고 클라이언트에 대한 호출도 `await`를 사용하지 않는 일반 호출입니다. -이렇게 하여 복잡한 과정 없이 `pytest` 를 직접적으로 사용할 수 있습니다. +이렇게 하여 복잡한 과정 없이 `pytest`를 직접적으로 사용할 수 있습니다. /// -/// note | 기술 세부사항 +/// note Technical Details | 기술 세부사항 `from starlette.testclient import TestClient` 역시 사용할 수 있습니다. -**FastAPI** 는 개발자의 편의를 위해 `starlette.testclient` 를 `fastapi.testclient` 로도 제공할 뿐입니다. 이는 단지 `Starlette` 에서 직접 가져오는지의 차이일 뿐입니다. +**FastAPI**는 개발자의 편의를 위해 `starlette.testclient`를 `fastapi.testclient`로도 제공할 뿐입니다. 하지만 이는 Starlette에서 직접 가져옵니다. /// /// tip | 팁 -FastAPI 애플리케이션에 요청을 보내는 것 외에도 테스트에서 `async` 함수를 호출하고 싶다면 (예: 비동기 데이터베이스 함수), 심화 튜토리얼의 [Async Tests](../advanced/async-tests.md){.internal-link target=_blank} 를 참조하세요. +FastAPI 애플리케이션에 요청을 보내는 것 외에도 테스트에서 `async` 함수를 호출하고 싶다면 (예: 비동기 데이터베이스 함수), 심화 튜토리얼의 [Async Tests](../advanced/async-tests.md){.internal-link target=_blank}를 참조하세요. /// -## 테스트 분리하기 +## 테스트 분리하기 { #separating-tests } 실제 애플리케이션에서는 테스트를 별도의 파일로 나누는 경우가 많습니다. - 그리고 **FastAPI** 애플리케이션도 여러 파일이나 모듈 등으로 구성될 수 있습니다. -### **FastAPI** app 파일 +### **FastAPI** app 파일 { #fastapi-app-file } -[Bigger Applications](bigger-applications.md){.internal-link target=_blank} 에 묘사된 파일 구조를 가지고 있는 것으로 가정해봅시다. +[Bigger Applications](bigger-applications.md){.internal-link target=_blank}에 묘사된 파일 구조를 가지고 있는 것으로 가정해봅시다. ``` . @@ -76,11 +75,12 @@ FastAPI 애플리케이션에 요청을 보내는 것 외에도 테스트에서 `main.py` 파일 안에 **FastAPI** app 을 만들었습니다: -{* ../../docs_src/app_testing/main.py *} -### 테스트 파일 +{* ../../docs_src/app_testing/app_a_py39/main.py *} -테스트를 위해 `test_main.py` 라는 파일을 생성할 수 있습니다. 이 파일은 동일한 Python 패키지(즉, `__init__.py` 파일이 있는 동일한 디렉터리)에 위치할 수 있습니다. +### 테스트 파일 { #testing-file } + +테스트를 위해 `test_main.py`라는 파일을 생성할 수 있습니다. 이 파일은 동일한 Python 패키지(즉, `__init__.py` 파일이 있는 동일한 디렉터리)에 위치할 수 있습니다. ``` hl_lines="5" . @@ -90,18 +90,18 @@ FastAPI 애플리케이션에 요청을 보내는 것 외에도 테스트에서 │   └── test_main.py ``` -파일들이 동일한 패키지에 위치해 있으므로, 상대 참조를 사용하여 `main` 에서 `app` 객체를 임포트 해올 수 있습니다. +파일들이 동일한 패키지에 위치해 있으므로, 상대 임포트를 사용하여 `main` 모듈(`main.py`)에서 `app` 객체를 임포트 해올 수 있습니다. -{* ../../docs_src/app_testing/test_main.py hl[3] *} +{* ../../docs_src/app_testing/app_a_py39/test_main.py hl[3] *} ...그리고 이전에 작성했던 것과 같은 테스트 코드를 작성할 수 있습니다. -## 테스트: 확장된 예시 +## 테스트: 확장된 예시 { #testing-extended-example } 이제 위의 예시를 확장하고 더 많은 세부 사항을 추가하여 다양한 부분을 어떻게 테스트하는지 살펴보겠습니다. -### 확장된 FastAPI 애플리케이션 파일 +### 확장된 **FastAPI** app 파일 { #extended-fastapi-app-file } 이전과 같은 파일 구조를 계속 사용해 보겠습니다. @@ -113,100 +113,50 @@ FastAPI 애플리케이션에 요청을 보내는 것 외에도 테스트에서 │   └── test_main.py ``` -이제 **FastAPI** 앱이 있는 `main.py` 파일에 몇 가지 다른 **경로 작업** 이 추가된 경우를 생각해봅시다. +이제 **FastAPI** 앱이 있는 `main.py` 파일에 몇 가지 다른 **경로 처리**가 추가된 경우를 생각해봅시다. 단일 오류를 반환할 수 있는 `GET` 작업이 있습니다. 여러 다른 오류를 반환할 수 있는 `POST` 작업이 있습니다. -두 *경로 작업* 모두 `X-Token` 헤더를 요구합니다. +두 *경로 처리* 모두 `X-Token` 헤더를 요구합니다. -//// tab | Python 3.10+ +{* ../../docs_src/app_testing/app_b_an_py310/main.py *} -```Python -{!> ../../docs_src/app_testing/app_b_an_py310/main.py!} -``` +### 확장된 테스트 파일 { #extended-testing-file } -//// +이제는 `test_main.py`를 확장된 테스트들로 수정할 수 있습니다: -//// tab | Python 3.9+ - -```Python -{!> ../../docs_src/app_testing/app_b_an_py39/main.py!} -``` - -//// - -//// tab | Python 3.8+ - -```Python -{!> ../../docs_src/app_testing/app_b_an/main.py!} -``` - -//// - -//// tab | Python 3.10+ non-Annotated - -/// tip | 팁 - -될 수 있으면 `Annotated` 버전 사용을 권장합나다. - -/// - -```Python -{!> ../../docs_src/app_testing/app_b_py310/main.py!} -``` - -//// - -//// tab | Python 3.8+ non-Annotated - -/// tip | 팁 - -될 수 있으면 `Annotated` 버전 사용을 권장합나다. - -/// - -```Python -{!> ../../docs_src/app_testing/app_b/main.py!} -``` - -//// - -### 확장된 테스트 파일 - -이제는 `test_main.py` 를 확장된 테스트들로 수정할 수 있습니다: - -{* ../../docs_src/app_testing/app_b/test_main.py *} +{* ../../docs_src/app_testing/app_b_an_py310/test_main.py *} -클라이언트가 요청에 정보를 전달해야 하는데 방법을 모르겠다면, `httpx`에서 해당 작업을 수행하는 방법을 검색(Google)하거나, `requests`에서의 방법을 검색해보세요. HTTPX는 Requests의 디자인을 기반으로 설계되었습니다. +클라이언트가 요청에 정보를 전달해야 하는데 방법을 모르겠다면, Requests의 디자인을 기반으로 설계된 HTTPX처럼 `httpx`에서 해당 작업을 수행하는 방법을 검색(Google)하거나, `requests`에서의 방법을 검색해보세요. 그 후, 테스트에서도 동일하게 적용하면 됩니다. 예시: * *경로* 혹은 *쿼리* 매개변수를 전달하려면, URL 자체에 추가한다. -* JSON 본문을 전달하려면, 파이썬 객체 (예를들면 `dict`) 를 `json` 파라미터로 전달한다. -* JSON 대신 *폼 데이터* 를 보내야한다면, `data` 파라미터를 대신 전달한다. -* *헤더* 를 전달하려면, `headers` 파라미터에 `dict` 를 전달한다. -* *쿠키* 를 전달하려면, `cookies` 파라미터에 `dict` 를 전달한다. +* JSON 본문을 전달하려면, 파이썬 객체 (예를들면 `dict`)를 `json` 파라미터로 전달한다. +* JSON 대신 *폼 데이터*를 보내야한다면, `data` 파라미터를 대신 전달한다. +* *헤더*를 전달하려면, `headers` 파라미터에 `dict`를 전달한다. +* *쿠키*를 전달하려면, `cookies` 파라미터에 `dict`를 전달한다. -백엔드로 데이터를 어떻게 보내는지 정보를 더 얻으려면 (`httpx` 혹은 `TestClient` 를 이용해서) HTTPX documentation 를 확인하세요. +백엔드로 데이터를 어떻게 보내는지 정보를 더 얻으려면 (`httpx` 혹은 `TestClient`를 이용해서) HTTPX documentation를 확인하세요. /// info | 정보 -`TestClient` 는 Pydantic 모델이 아니라 JSON 으로 변환될 수 있는 데이터를 받습니다. +`TestClient`는 Pydantic 모델이 아니라 JSON으로 변환될 수 있는 데이터를 받습니다. -만약 테스트중 Pydantic 모델을 어플리케이션으로에 보내고 싶다면, [JSON 호환 가능 인코더](encoder.md){.internal-link target=_blank} 에 설명되어 있는 `jsonable_encoder` 를 사용할 수 있습니다. +만약 테스트 중 Pydantic 모델을 가지고 있고 테스트 중에 애플리케이션으로 해당 데이터를 보내고 싶다면, [JSON Compatible Encoder](encoder.md){.internal-link target=_blank}에 설명되어 있는 `jsonable_encoder`를 사용할 수 있습니다. /// -## 실행하기 +## 실행하기 { #run-it } -테스트 코드를 작성하고, `pytest` 를 설치해야합니다. +그 후에는 `pytest`를 설치하기만 하면 됩니다. -[virtual environment](../virtual-environments.md){.internal-link target=_blank} 를 만들고, 활성화 시킨 뒤에 설치하세요. 예시: +[virtual environment](../virtual-environments.md){.internal-link target=_blank}를 만들고, 활성화 시킨 뒤에 설치하세요. 예시:
@@ -218,7 +168,7 @@ $ pip install pytest
-`pytest` 파일과 테스트를 자동으로 감지하고 실행한 다음, 결과를 보고할 것입니다. +`pytest`는 파일과 테스트를 자동으로 감지하고 실행한 다음, 결과를 보고할 것입니다. 테스트를 다음 명령어로 실행하세요. diff --git a/docs/ko/docs/virtual-environments.md b/docs/ko/docs/virtual-environments.md index 0d10c3200d..b639f8a3e4 100644 --- a/docs/ko/docs/virtual-environments.md +++ b/docs/ko/docs/virtual-environments.md @@ -1,16 +1,16 @@ -# 가상 환경 +# 가상 환경 { #virtual-environments } -Python 프로젝트를 작업할 때는 **가상 환경** (또는 이와 유사한 도구)을 사용하는 것이 좋습니다. 각 프로젝트 마다 설치하는 패키지를 분리하여 관리할 수 있습니다. +Python 프로젝트를 작업할 때는 **가상 환경**(또는 이와 유사한 메커니즘)을 사용해 각 프로젝트마다 설치하는 패키지를 분리하는 것이 좋습니다. -/// info | 정보 +/// info -이미 가상 환경에 대해 잘 알고 있다면, 이 섹션은 건너 뛰어도 괜찮습니다. 🤓 +이미 가상 환경에 대해 알고 있고, 어떻게 생성하고 사용하는지도 알고 있다면, 이 섹션은 건너뛰어도 괜찮습니다. 🤓 /// -/// tip | 팁 +/// tip -**가상 환경(Virtual Environment)** 은 **환경 변수(Environment Variable)** 와 다릅니다. +**가상 환경**은 **환경 변수**와 다릅니다. **환경 변수**는 시스템에 존재하며, 프로그램이 사용할 수 있는 변수입니다. @@ -18,50 +18,52 @@ Python 프로젝트를 작업할 때는 **가상 환경** (또는 이와 유사 /// -/// info | 정보 +/// info -이 페이지에서는 **가상 환경**의 사용 방법과 작동 방식을 설명합니다. +이 페이지에서는 **가상 환경**을 사용하는 방법과 작동 방식을 알려드립니다. -만약 **모든 것을 관리해주는 도구** (Python 설치까지 포함)를 사용하고 싶다면 uv를 사용해보세요. +Python 설치까지 포함해 **모든 것을 관리해주는 도구**를 도입할 준비가 되었다면 uv를 사용해 보세요. /// -## 프로젝트 생성 +## 프로젝트 생성 { #create-a-project } 먼저, 프로젝트를 위한 디렉터리를 하나 생성합니다. -보통 사용자 홈 디렉터리 안에 `code`라는 디렉터리를 만들고, 그 안에 프로젝트마다 하나씩 디렉터리를 만들어 관리합니다. +제가 보통 하는 방법은 사용자 홈/유저 디렉터리 안에 `code`라는 디렉터리를 만드는 것입니다. + +그리고 그 안에 프로젝트마다 디렉터리를 하나씩 만듭니다.
```console -// 홈 디렉터리로 이동 +// Go to the home directory $ cd -// 모든 코드 프로젝트를 위한 디렉터리 생성 +// Create a directory for all your code projects $ mkdir code -// code 디렉터리로 이동 +// Enter into that code directory $ cd code -// 이번 프로젝트를 위한 디렉터리 생성 +// Create a directory for this project $ mkdir awesome-project -// 해당 프로젝트 디렉터리로 이동 +// Enter into that project directory $ cd awesome-project ```
-## 가상 환경 생성 +## 가상 환경 생성 { #create-a-virtual-environment } -Python 프로젝트를 **처음 시작할 때**, 가상 환경을 **프로젝트 내부**에 생성합니다. +Python 프로젝트를 **처음 시작할 때**, **프로젝트 내부**에 가상 환경을 생성하세요. -/// tip | 팁 +/// tip -이 작업은 **프로젝트를 처음 설정할 때 한번만** 해주면 됩니다. 이후 작업할 때 반복할 필요는 없습니다. +이 작업은 **프로젝트당 한 번만** 하면 되며, 작업할 때마다 할 필요는 없습니다. /// //// tab | `venv` -Python 표준 라이브러리에 포함된 venv 모듈을 사용해 가상 환경을 생성할 수 있습니다. +가상 환경을 만들려면 Python에 포함된 `venv` 모듈을 사용할 수 있습니다.
@@ -71,12 +73,12 @@ $ python -m venv .venv
-/// details | 명령어 상세 설명 +/// details | 명령어 의미 -* `python`: `python` 프로그램을 실행합니다. -* `-m`: 특정 모듈을 스크립트처럼 실행합니다. 대상 모듈을 바로 뒤에 지정합니다. -* `venv`: Python 표준 라이브러리에 포함된 `venv` 모듈을 실행합니다. -* `.venv`: 가상 환경을 `.venv` 디렉터리에 생성합니다. +* `python`: `python`이라는 프로그램을 사용합니다 +* `-m`: 모듈을 스크립트로 호출합니다. 다음에 어떤 모듈인지 지정합니다 +* `venv`: 보통 Python에 기본으로 설치되어 있는 `venv` 모듈을 사용합니다 +* `.venv`: 새 디렉터리인 `.venv`에 가상 환경을 생성합니다 /// @@ -84,7 +86,7 @@ $ python -m venv .venv //// tab | `uv` -`uv`가 설치되어 있다면, uv를 통해 가상 환경을 생성할 수 있습니다. +`uv`가 설치되어 있다면, 이를 사용해 가상 환경을 생성할 수 있습니다.
@@ -94,31 +96,31 @@ $ uv venv
-/// tip | 팁 +/// tip -`uv`는 기본적으로 `.venv` 디렉터리에 가상 환경을 생성합니다. +기본적으로 `uv`는 `.venv`라는 디렉터리에 가상 환경을 생성합니다. -별도로 디렉터리 이름을 추가 인자로 넘겨 주면 경로를 지정 할 수 있습니다. +하지만 디렉터리 이름을 추가 인자로 전달해 이를 커스터마이즈할 수 있습니다. /// //// -해당 명령어는 `.venv` 디렉터리에 새로운 가상 환경을 생성합니다. +해당 명령어는 `.venv`라는 디렉터리에 새로운 가상 환경을 생성합니다. /// details | `.venv` 또는 다른 이름 -가상 환경을 다른 디렉터리에 생성할 수도 있지만, 관례적으로 `.venv` 디렉터리 이름을 사용합니다. +가상 환경을 다른 디렉터리에 생성할 수도 있지만, 관례적으로 `.venv`라는 이름을 사용합니다. /// -## 가상 환경 활성화 +## 가상 환경 활성화 { #activate-the-virtual-environment } -이후 실행하는 Python 명령어와 패키지 설치가 가상 환경을 따르도록, 가상 환경을 활성화하세요. +이후 실행하는 Python 명령어와 설치하는 패키지가 새 가상 환경을 사용하도록, 새 가상 환경을 활성화하세요. -/// tip | 팁 +/// tip -**터미널을 새로 열고** 프로젝트 작업을 시작할 때는, **항상 이 작업을** 해주세요. +프로젝트 작업을 위해 **새 터미널 세션**을 시작할 때마다 **매번** 이 작업을 하세요. /// @@ -148,7 +150,7 @@ $ .venv\Scripts\Activate.ps1 //// tab | Windows Bash -Windows에서 Bash(예: Git Bash)를 사용하는 경우: +또는 Windows에서 Bash(예: Git Bash)를 사용하는 경우:
@@ -160,21 +162,21 @@ $ source .venv/Scripts/activate //// -/// tip | 팁 +/// tip -가상 환경에 새로운 패키지를 설치할 때마다, 해당 환경을 다시 활성화하세요. +해당 환경에 **새 패키지**를 설치할 때마다, 환경을 다시 **활성화**하세요. -이렇게 하면 해당 패키지로 설치된 **터미널(CLI) 프로그램**을 사용할 때, 전역에 설치된 다른 버전이 아니라, 가상 환경 안에 설치된 정확한 버전을 사용합니다. +이렇게 하면 해당 패키지가 설치한 **터미널(CLI) 프로그램**을 사용할 때, 전역으로 설치되어 있을 수도 있는(아마 필요한 버전과는 다른 버전인) 다른 프로그램이 아니라 가상 환경에 있는 것을 사용하게 됩니다. /// -## 가상 환경이 활성화 여부 확인 +## 가상 환경 활성화 여부 확인 { #check-the-virtual-environment-is-active } -가상 환경이 활성화되었는지 확인합니다. (이전 명령어가 제대로 작동했는지 확인합니다). +가상 환경이 활성화되어 있는지(이전 명령어가 작동했는지) 확인합니다. -/// tip | 팁 +/// tip -이 단계는 **선택 사항**이지만, 모든 것이 예상대로 작동하고 있는지, 그리고 의도한 가상 환경이 활성화 되었는 지 **확인**하는 좋은 방법입니다. +이 단계는 **선택 사항**이지만, 모든 것이 예상대로 작동하고 있는지, 그리고 의도한 가상 환경을 사용하고 있는지 **확인**하는 좋은 방법입니다. /// @@ -190,7 +192,7 @@ $ which python
-`python` 위치가 프로젝트 내부(이 예시에서는 `awesome-project`)의 `.venv/bin/python` 경로로 표시된다면 성공입니다. 🎉 +프로젝트 내부(이 경우 `awesome-project`)의 `.venv/bin/python`에 있는 `python` 바이너리가 표시된다면, 정상적으로 작동한 것입니다. 🎉 //// @@ -206,29 +208,29 @@ C:\Users\user\code\awesome-project\.venv\Scripts\python -`python` 위치가 프로젝트 내부(이 예시에서는 `awesome-project`)의 `.venv\bin\python` 경로로 표시된다면 성공입니다. 🎉 +프로젝트 내부(이 경우 `awesome-project`)의 `.venv\Scripts\python`에 있는 `python` 바이너리가 표시된다면, 정상적으로 작동한 것입니다. 🎉 //// -## pip 업그레이드 +## `pip` 업그레이드 { #upgrade-pip } -/// tip | 팁 +/// tip -`uv`를 사용한다면, `pip` 대신 `uv`로 패키지를 설치하게 되므로 `pip`을 업그레이드할 필요가 없습니다. 😎 +`uv`를 사용한다면, `pip` 대신 `uv`로 설치하게 되므로 `pip`을 업그레이드할 필요가 없습니다. 😎 /// -`pip`을 사용하여 패키지를 설치하는 경우 (Python 표준 라이브러리에 포함되어 있습니다), **최신 버전으로 업그레이드**하는 것이 좋습니다. +`pip`로 패키지를 설치한다면(Python에 기본으로 포함되어 있습니다) 최신 버전으로 **업그레이드**하는 것이 좋습니다. -패키지 설치 중 발생하는 다양하고 특이한 에러들은 `pip` 업그레이드로 쉽게 해결되는 경우가 많습니다. +패키지 설치 중 발생하는 다양한 특이한 오류는 먼저 `pip`를 업그레이드하는 것만으로 해결되는 경우가 많습니다. -/// tip | 팁 +/// tip -이 작업은 보통 가상 환경을 생성한 **직후 한 번만** 하면 됩니다. +보통 이 작업은 가상 환경을 만든 직후 **한 번만** 하면 됩니다. /// -가상 환경이 활성화된 상태인지 확인한 후(앞서 설명한 명령어 사용), 아래 명령어를 실행하세요: +가상 환경이 활성화된 상태인지 확인한 다음(위의 명령어 사용) 아래를 실행하세요:
@@ -240,19 +242,39 @@ $ python -m pip install --upgrade pip
-## `.gitignore` 추가하기 +/// tip -**Git**을 사용하고 있다면 (사용하는 것이 좋습니다), `.gitignore` 파일을 추가해서 `.venv` 디렉터리 전체를 Git에서 제외하세요. +때로는 pip를 업그레이드하려고 할 때 **`No module named pip`** 오류가 발생할 수 있습니다. -/// tip | 팁 +이 경우 아래 명령어로 pip를 설치하고 업그레이드하세요: -`uv`를 사용해 가상 환경을 생성했다면, 이미 이 작업이 자동으로 처리되어 있으므로 이 단계는 건너뛰어도 됩니다. 😎 +
+ +```console +$ python -m ensurepip --upgrade + +---> 100% +``` + +
+ +이 명령어는 pip가 아직 설치되어 있지 않다면 설치하며, 설치된 pip 버전이 `ensurepip`에서 제공 가능한 버전만큼 최신임을 보장합니다. /// -/// tip | 팁 +## `.gitignore` 추가하기 { #add-gitignore } -이 작업도 마찬가지로, 가상 환경을 생성한 **직후 한 번만** 하면 됩니다. +**Git**을 사용하고 있다면(사용하는 것이 좋습니다), `.venv`의 모든 내용을 Git에서 제외하도록 `.gitignore` 파일을 추가하세요. + +/// tip + +`uv`로 가상 환경을 만들었다면, 이미 자동으로 처리되어 있으므로 이 단계는 건너뛰어도 됩니다. 😎 + +/// + +/// tip + +가상 환경을 만든 직후 **한 번만** 하면 됩니다. /// @@ -264,16 +286,15 @@ $ echo "*" > .venv/.gitignore -/// details | 명령어 상세 설명 +/// details | 명령어 의미 -* `echo "*"`: 터미널에 `*` 텍스트를 "출력"합니다 (다음 설명에서 조금 바뀝니다) -* `>`: 왼쪽 명령어의 출력 내용을 터미널에 출력하지 않고, 오른쪽에 지정된 파일로 **기록(write)** 하라는 의미입니다. -* `.gitignore`: 출력된 텍스트가 기록될 파일 이름입니다. +* `echo "*"`: 터미널에 `*` 텍스트를 "출력"합니다(다음 부분이 이를 약간 변경합니다) +* `>`: `>` 왼쪽 명령어가 터미널에 출력한 내용을 터미널에 출력하지 않고, `>` 오른쪽에 있는 파일에 기록하라는 의미입니다 +* `.gitignore`: 텍스트가 기록될 파일 이름입니다 -그리고 Git에서 `*`는 "모든 것"을 의미합니다. 따라서 `.venv` 디렉터리 안의 모든 것을 무시하게 됩니다. - -이 명령어는 다음과 같은 내용을 가진 `.gitignore` 파일을 생성합니다: +그리고 Git에서 `*`는 "모든 것"을 의미합니다. 따라서 `.venv` 디렉터리 안의 모든 것을 무시합니다. +이 명령어는 다음 내용을 가진 `.gitignore` 파일을 생성합니다: ```gitignore * @@ -281,25 +302,25 @@ $ echo "*" > .venv/.gitignore /// -## 패키지 설치 +## 패키지 설치 { #install-packages } -가상 환경을 활성화한 후, 그 안에 필요한 패키지들을 설치할 수 있습니다. +환경을 활성화한 뒤, 그 안에 패키지를 설치할 수 있습니다. -/// tip | 팁 +/// tip -프로젝트에서 필요한 패키지를 설치하거나 업그레이드할 때는 이 작업을 **한 번만** 하면 됩니다. +프로젝트에 필요한 패키지를 설치하거나 업그레이드할 때는 **한 번**만 하면 됩니다. -만약 특정 패키지의 버전을 업그레이드하거나, 새로운 패키지를 추가할 필요가 생기면 **다시 이 작업을 반복**하면 됩니다. +버전을 업그레이드하거나 새 패키지를 추가해야 한다면 **다시 이 작업을** 하게 됩니다. /// -### 패키지 직접 설치 +### 패키지 직접 설치 { #install-packages-directly } -급하게 작업하거나, 프로젝트에 필요한 패키지 목록을 따로 파일로 관리하고 싶지 않은 경우, 패키지를 직접 설치할 수도 있습니다. +급하게 작업 중이고 프로젝트의 패키지 요구사항을 선언하는 파일을 사용하고 싶지 않다면, 패키지를 직접 설치할 수 있습니다. -/// tip | 팁 +/// tip -패키지 이름과 버전 정보를 파일에 정리해두는 것(예: `requirements.txt` 또는 `pyproject.toml`)은 (매우) 좋은 생각입니다. +프로그램에 필요한 패키지와 버전을 파일(예: `requirements.txt` 또는 `pyproject.toml`)에 적어두는 것은 (매우) 좋은 생각입니다. /// @@ -319,7 +340,7 @@ $ pip install "fastapi[standard]" //// tab | `uv` -`uv`를 사용하는 경우: +`uv`가 있다면:
@@ -332,9 +353,9 @@ $ uv pip install "fastapi[standard]" //// -### `requirements.txt`에서 설치 +### `requirements.txt`에서 설치 { #install-from-requirements-txt } -`requirements.txt` 파일이 있다면, 그 안에 명시된 패키지들을 한 번에 설치할 수 있습니다. +`requirements.txt`가 있다면, 이제 이를 사용해 그 안의 패키지를 설치할 수 있습니다. //// tab | `pip` @@ -351,7 +372,7 @@ $ pip install -r requirements.txt //// tab | `uv` -`uv`를 사용하는 경우: +`uv`가 있다면:
@@ -366,7 +387,7 @@ $ uv pip install -r requirements.txt /// details | `requirements.txt` -다음은 몇 가지 패키지를 포함한 `requirements.txt`의 예시입니다: +일부 패키지가 있는 `requirements.txt`는 다음과 같이 생겼을 수 있습니다: ```requirements.txt fastapi[standard]==0.113.0 @@ -375,9 +396,9 @@ pydantic==2.8.0 /// -## 프로그램 실행 +## 프로그램 실행 { #run-your-program } -가상 환경을 활성화한 후에는 프로그램을 실행할 수 있습니다. 이때 해당 가상 환경에 설치된 Python과 패키지들이 사용됩니다. +가상 환경을 활성화한 뒤에는 프로그램을 실행할 수 있으며, 설치한 패키지가 들어있는 가상 환경 내부의 Python을 사용하게 됩니다.
@@ -389,25 +410,24 @@ Hello World
-## 에디터 설정 +## 에디터 설정 { #configure-your-editor } -에디터를 사용할 경우, 앞서 만든 가상 환경을 사용하도록 설정하는 것이 좋습니다. (대부분의 에디터는 자동으로 감지하기도 합니다.) -이렇게 하면 자동 완성 기능이나 코드 내 오류 표시 기능을 제대로 사용할 수 있습니다. +아마 에디터를 사용할 텐데, 자동 완성과 인라인 오류 표시를 받을 수 있도록 생성한 가상 환경을 사용하도록 설정하세요(대부분 자동 감지합니다). -예시: +예를 들면: * VS Code * PyCharm -/// tip | 팁 +/// tip -이 설정은 보통 가상 환경을 **처음 만들었을 때 한 번만** 해주면 됩니다. +보통 이 설정은 가상 환경을 만들 때 **한 번만** 하면 됩니다. /// -## 가상 환경 비활성화 +## 가상 환경 비활성화 { #deactivate-the-virtual-environment } -프로젝트 작업이 끝났다면, 가상 환경을 **비활성화**할 수 있습니다. +프로젝트 작업을 마쳤다면 가상 환경을 **비활성화**할 수 있습니다.
@@ -417,54 +437,55 @@ $ deactivate
-이렇게 하면 이후에 `python` 명령어를 실행했을 때, 가상 환경의 Python이나 그 안에 설치된 패키지들을 사용하지 않게 됩니다. +이렇게 하면 `python`을 실행할 때, 해당 가상 환경과 그 안에 설치된 패키지에서 실행하려고 하지 않습니다. -## 이제 작업할 준비가 되었습니다 +## 작업할 준비 완료 { #ready-to-work } -이제 프로젝트 작업을 시작할 준비가 완료되었습니다. +이제 프로젝트 작업을 시작할 준비가 되었습니다. -/// tip | 팁 -위 내용을 더 깊이 이해하고 싶으신가요? +/// tip -그렇다면 계속 읽어 주세요. 👇🤓 +위의 내용이 무엇인지 더 이해하고 싶으신가요? + +계속 읽어보세요. 👇🤓 /// -## 가상 환경을 왜 사용하는가 +## 가상 환경을 왜 사용하나요 { #why-virtual-environments } -FastAPI를 사용하려면 먼저 Python을 설치해야 합니다. +FastAPI로 작업하려면 Python을 설치해야 합니다. -그 후에는 FastAPI와 함께 사용할 **기타 패키지들**을 **설치**해야 합니다. +그 다음 FastAPI와 사용하려는 다른 **패키지**를 **설치**해야 합니다. -패키지를 설치할 때 보통 Python에 기본 포함된 `pip` 명령어(또는 유사한 도구)를 사용합니다. +패키지를 설치할 때는 보통 Python에 포함된 `pip` 명령어(또는 유사한 대안)를 사용합니다. -하지만 `pip`을 그냥 직접 사용하면, 해당 패키지들은 **전역 Python 환경**(시스템 전체에 설치된 Python)에 설치됩니다. +하지만 `pip`를 그대로 직접 사용하면, 패키지는 **전역 Python 환경**(전역 Python 설치)에 설치됩니다. -### 문제점 +### 문제점 { #the-problem } -그렇다면, 전역 Python 환경에 패키지를 설치하면 어떤 문제가 발생할까요? +그렇다면, 전역 Python 환경에 패키지를 설치하면 어떤 문제가 있을까요? -어느 시점이 되면, **서로 다른 패키지들**에 의존하는 여러 개의 프로그램을 작성하게 될 것입니다. 그리고 이들 중 일부는 **같은 패키지의 서로 다른 버전**을 필요로 할 수 있습니다. 😱 +어느 시점이 되면 **서로 다른 패키지**에 의존하는 다양한 프로그램을 작성하게 될 것입니다. 그리고 작업하는 프로젝트 중 일부는 같은 패키지의 **서로 다른 버전**에 의존할 수도 있습니다. 😱 -예를 들어, `마법사의 돌(philosophers-stone)` 프로젝트를 만들었다고 가정해봅시다. 이 프로그램은 `해리 포터(harry)`라는 패키지의 `v1` 버전을 **의존**합니다. 따라서 `harry`를 설치해야 합니다. +예를 들어 `philosophers-stone`이라는 프로젝트를 만들 수 있습니다. 이 프로그램은 **`harry`라는 다른 패키지의 버전 `1`**에 의존합니다. 그래서 `harry`를 설치해야 합니다. ```mermaid flowchart LR stone(philosophers-stone) -->|requires| harry-1[harry v1] ``` -그런데 나중에 `아즈카반의 죄수(prisoner-of-azkaban)`이라는 또 다른 프로젝트를 만들게 되었고, 이 프로젝트도 역시 `harry` 패키지를 사용합니다. 그런데 이 프로젝트는 `harry`의 `v3` 버전이 필요합니다. +그다음, 나중에 `prisoner-of-azkaban`이라는 또 다른 프로젝트를 만들고, 이 프로젝트도 `harry`에 의존하지만, 이 프로젝트는 **`harry` 버전 `3`**이 필요합니다. ```mermaid flowchart LR azkaban(prisoner-of-azkaban) --> |requires| harry-3[harry v3] ``` -하지만 이제 문제가 생깁니다. 로컬 가상 환경 대신에 전역 환경에 패키지를 설치하게 되면, 어떤 버전의 `harry`를 설치할지를 선택해야 하기 때문입니다. +하지만 이제 문제가 생깁니다. 로컬 **가상 환경**이 아니라 전역(전역 환경)에 패키지를 설치한다면, 어떤 버전의 `harry`를 설치할지 선택해야 합니다. -예를 들어, `마법사의 돌(philosophers-stone)`을 실행하고 싶다면 먼저 `harry` `v1` 버전을 다음과 같이 설치 해야 합니다: +`philosophers-stone`을 실행하고 싶다면, 먼저 `harry` 버전 `1`을 다음과 같이 설치해야 합니다:
@@ -474,7 +495,7 @@ $ pip install "harry==1"
-그러면 결국 전역 Python 환경에는 `harry` `v1`버전이 설치된 상태가 됩니다. +그리고 전역 Python 환경에 `harry` 버전 `1`이 설치된 상태가 됩니다. ```mermaid flowchart LR @@ -486,7 +507,7 @@ flowchart LR end ``` -하지만 이제 `아즈카반의 죄수(prisoner-of-azkaban)`을 실행하고 싶다면, `harry` `v1`버전을 제거하고 `harry` `v3`버전을 설치해야 합니다. (또는 단순히 `v3`버전을 설치하는 것만으로도 기존의 `v1`버전이 자동으로 제거됩니다.) +하지만 `prisoner-of-azkaban`을 실행하려면 `harry` 버전 `1`을 제거하고 `harry` 버전 `3`을 설치해야 합니다(또는 버전 `3`을 설치하기만 해도 버전 `1`이 자동으로 제거됩니다).
@@ -496,9 +517,9 @@ $ pip install "harry==3"
-그렇게 하면 이제 전역 Python 환경에는 `harry` `v3`버전이 설치된 상태가 됩니다. +그러면 전역 Python 환경에 `harry` 버전 `3`이 설치된 상태가 됩니다. -그리고 다시 `마법사의 돌(philosophers-stone)`을 실행하려고 하면, **작동하지** 않을 수 있습니다. 왜냐하면 이 프로그램은 `harry` `v1`버전을 필요로 하기 때문입니다. +그리고 `philosophers-stone`을 다시 실행하려고 하면, `harry` 버전 `1`이 필요하기 때문에 **작동하지 않을** 가능성이 있습니다. ```mermaid flowchart LR @@ -515,50 +536,49 @@ flowchart LR end ``` -/// tip | 팁 +/// tip -Python 패키지들은 **새 버전**에서 **호환성 문제(breaking changes)**가 발생하지 않도록 최대한 노력하는 것이 일반적입니다. 하지만 그래도 안전하게 작업하려면, 테스트를 실행해보면서 새 버전을 의도적으로 설치하는 것이 좋습니다. +Python 패키지에서는 **새 버전**에서 **호환성을 깨뜨리는 변경(breaking changes)**을 **피하려고** 최선을 다하는 것이 매우 일반적이지만, 안전을 위해 더 최신 버전은 의도적으로 설치하고, 테스트를 실행해 모든 것이 올바르게 작동하는지 확인할 수 있을 때 설치하는 것이 좋습니다. /// -이제, 이런 일이 여러분의 **모든 프로젝트**가 사용하는 **수많은 패키지들**에서 동시에 발생한다고 상상해보세요. 이는 매우 관리하기 어려우며, 결국 **서로 호환되지 않는 버전**의 패키지로 프로젝트를 실행하게 될 가능성이 높고, 그로 인해 어떤 문제가 왜 발생하는지 알 수 없게 될 수 있습니다. +이제 이런 일이 여러분의 **모든 프로젝트가 의존하는** **많은** 다른 **패키지**에서도 일어난다고 상상해 보세요. 이는 관리하기가 매우 어렵습니다. 그리고 결국 일부 프로젝트는 패키지의 **호환되지 않는 버전**으로 실행하게 될 가능성이 높으며, 왜 무언가가 작동하지 않는지 알지 못하게 될 수 있습니다. -또한 사용하는 운영체제(Linux, Windows, macOS 등)에 따라 Python이 **미리 설치되어 있을 수도** 있습니다. 이런 경우에는 운영체제의 동작에 필요한 특정 버전의 패키지들이 함께 설치되어 있을 수 있습니다. 이 상태에서 전역 Python 환경에 임의의 패키지를 설치하면, 운영체제에 포함된 프로그램 일부가 **깨질 위험**도 있습니다. +또한 운영체제(Linux, Windows, macOS 등)에 따라 Python이 이미 설치되어 있을 수도 있습니다. 그런 경우에는 시스템에 **필요한 특정 버전**의 패키지가 일부 미리 설치되어 있을 가능성이 큽니다. 전역 Python 환경에 패키지를 설치하면, 운영체제에 포함된 프로그램 일부가 **깨질** 수 있습니다. -## 패키지들은 어디에 설치되는가 +## 패키지는 어디에 설치되나요 { #where-are-packages-installed } -Python을 설치하면, 컴퓨터에 여러 디렉터리와 파일들이 생성됩니다. +Python을 설치하면 컴퓨터에 몇몇 파일이 들어 있는 디렉터리가 생성됩니다. -이 중 일부 디렉터리는 사용자가 설치한 패키지들을 보관하는 역할을 합니다. +이 디렉터리 중 일부는 설치한 모든 패키지를 담는 역할을 합니다. -예를 들어, 아래 명령어를 실행하면: +다음을 실행하면:
```console -// 지금 실행하지 않아도 됩니다, 그냥 예제일 뿐이에요 🤓 +// Don't run this now, it's just an example 🤓 $ pip install "fastapi[standard]" ---> 100% ```
-해당 명령어는 FastAPI 코드를 포함한 압축 파일을 다운로드합니다. 이 파일은 보통 PyPI에서 받아옵니다. +FastAPI 코드를 담은 압축 파일을 다운로드합니다. 보통 PyPI에서 받습니다. -또한 FastAPI가 의존하는 다른 패키지들도 함께 **다운로드**됩니다. +또한 FastAPI가 의존하는 다른 패키지들의 파일도 **다운로드**합니다. -그리고 그 모든 파일들을 **압축 해제**한 뒤, 컴퓨터의 특정 디렉터리에 저장합니다. +그 다음 모든 파일을 **압축 해제**하고 컴퓨터의 한 디렉터리에 넣습니다. -기본적으로 이 파일들은 Python이 설치된 디렉터리 안, 즉 **전역 환경**에 내의 디렉터리에 저장됩니다. +기본적으로, 다운로드하고 압축 해제한 파일들은 Python 설치와 함께 제공되는 디렉터리, 즉 **전역 환경**에 저장됩니다. -## 가상 환경이란 +## 가상 환경이란 무엇인가요 { #what-are-virtual-environments } -전역 환경에 모든 패키지를 설치하면서 발생하는 문제에 대한 해결책은, 작업하는 **각 프로젝트마다 가상 환경**을 사용하는 것입니다. +전역 환경에 모든 패키지를 두는 문제에 대한 해결책은 작업하는 **각 프로젝트마다 가상 환경**을 사용하는 것입니다. -가상 환경은 전역 환경과 매우 유사한 하나의 **디렉터리**이며, 그 안에 해당 프로젝트를 위한 패키지들을 설치할 수 있습니다. - -이렇게 하면 각 프로젝트는 자체적인 가상 환경(`.venv` 디렉터리)을 가지게 되며, 그 안에 해당 프로젝트 전용 패키지들을 보유하게 됩니다. +가상 환경은 전역 환경과 매우 유사한 하나의 **디렉터리**이며, 프로젝트의 패키지를 설치할 수 있습니다. +이렇게 하면 각 프로젝트는 자체 가상 환경(`.venv` 디렉터리)과 자체 패키지를 갖게 됩니다. ```mermaid flowchart TB @@ -577,9 +597,9 @@ flowchart TB stone-project ~~~ azkaban-project ``` -## 가상 환경 활성화 의미 +## 가상 환경을 활성화한다는 것은 무엇을 의미하나요 { #what-does-activating-a-virtual-environment-mean } -가상 환경을 활성화한다는 것은, 예를 들어 다음과 같은 명령어를 실행하는 것을 의미합니다: +가상 환경을 활성화한다는 것은, 예를 들어 다음과 같은 명령어로: //// tab | Linux, macOS @@ -607,7 +627,7 @@ $ .venv\Scripts\Activate.ps1 //// tab | Windows Bash -Windows에서 Bash(예: Git Bash)를 사용하는 경우: +또는 Windows에서 Bash(예: Git Bash)를 사용하는 경우:
@@ -619,19 +639,19 @@ $ source .venv/Scripts/activate //// -이 명령어는 이후에 실행될 명령어에서 사용될 [환경 변수](environment-variables.md){.internal-link target=_blank} 몇 개를 생성하거나 수정합니다. +다음 명령어들에서 사용할 수 있는 몇몇 [환경 변수](environment-variables.md){.internal-link target=_blank}를 생성하거나 수정하는 것을 의미합니다. -이 변수들 중 하나가 바로 `PATH` 변수입니다. +그 변수 중 하나가 `PATH` 변수입니다. -/// tip | 팁 +/// tip -`PATH` 환경 변수에 대해 더 알고 싶다면 [환경 변수 문서의 PATH 환경 변수 섹션](environment-variables.md#path-environment-variable){.internal-link target=_blank}을 참고하세요. +`PATH` 환경 변수에 대해 더 알아보려면 [환경 변수](environment-variables.md#path-environment-variable){.internal-link target=_blank} 섹션을 참고하세요. /// -가상 환경을 활성화하면, 가상 환경의 경로인 `.venv/bin` (Linux와 macOS) 또는 `.venv\Scripts`(Windows)를 `PATH` 환경 변수에 추가됩니다. +가상 환경을 활성화하면 가상 환경의 경로인 `.venv/bin`(Linux와 macOS) 또는 `.venv\Scripts`(Windows)를 `PATH` 환경 변수에 추가합니다. -예를 들어, 가상 환경을 활성화하기 전의 `PATH` 변수는 다음과 같았다고 가정해봅시다: +가령 환경을 활성화하기 전에는 `PATH` 변수가 다음과 같았다고 해보겠습니다: //// tab | Linux, macOS @@ -639,7 +659,7 @@ $ source .venv/Scripts/activate /usr/bin:/bin:/usr/sbin:/sbin ``` -시스템은 다음 경로들에서 프로그램을 찾게 됩니다: +이는 시스템이 다음 위치에서 프로그램을 찾는다는 뜻입니다: * `/usr/bin` * `/bin` @@ -654,13 +674,13 @@ $ source .venv/Scripts/activate C:\Windows\System32 ``` -시스템은 다음 경로들에서 프로그램을 찾게 됩니다: +이는 시스템이 다음 위치에서 프로그램을 찾는다는 뜻입니다: * `C:\Windows\System32` //// -가상 환경을 활성화한 후에는, `PATH` 변수는 다음과 같은 형태가 됩니다: +가상 환경을 활성화한 뒤에는 `PATH` 변수가 다음과 같이 보일 수 있습니다: //// tab | Linux, macOS @@ -668,21 +688,21 @@ C:\Windows\System32 /home/user/code/awesome-project/.venv/bin:/usr/bin:/bin:/usr/sbin:/sbin ``` -시스템은 가장 먼저 다음 경로에서 프로그램을 찾기 시작합니다: +이는 시스템이 이제 다음 위치에서 프로그램을 가장 먼저 찾기 시작한다는 뜻입니다: ```plaintext /home/user/code/awesome-project/.venv/bin ``` -그 후에 다른 디렉터리들을 탐색합니다. +그리고 나서 다른 디렉터리들을 탐색합니다. -따라서 터미널에 `python`을 입력하면, 시스템은 다음 위치에 있는 Python 프로그램을 찾게 됩니다: +따라서 터미널에 `python`을 입력하면, 시스템은 다음 위치에서 Python 프로그램을 찾고: ```plaintext /home/user/code/awesome-project/.venv/bin/python ``` -그리고 해당 Python을 사용하게 됩니다. +그것을 사용하게 됩니다. //// @@ -692,31 +712,31 @@ C:\Windows\System32 C:\Users\user\code\awesome-project\.venv\Scripts;C:\Windows\System32 ``` -시스템은 가장 먼저 다음 경로에서 프로그램을 찾기 시작합니다: +이는 시스템이 이제 다음 위치에서 프로그램을 가장 먼저 찾기 시작한다는 뜻입니다: ```plaintext C:\Users\user\code\awesome-project\.venv\Scripts ``` -그 후에 다른 디렉터리들을 탐색합니다. +그리고 나서 다른 디렉터리들을 탐색합니다. -따라서 터미널에 `python`을 입력하면, 시스템은 다음 경로에 있는 Python 프로그램을 찾게 됩니다: +따라서 터미널에 `python`을 입력하면, 시스템은 다음 위치에서 Python 프로그램을 찾고: ```plaintext C:\Users\user\code\awesome-project\.venv\Scripts\python ``` -그리고 해당 Python을 사용하게 됩니다. +그것을 사용하게 됩니다. //// -중요한 세부 사항 중 하나는, 가상 환경의 경로가 `PATH` 변수의 가장 **앞**에 추가된다는 점입니다. 시스템은 사용 가능한 다른 Python들보다 **먼저** 이 경로를 찾습니다. 그래서 터미널에서 `python`을 실행하면, 전역 환경의 Python이 아닌 **가상 환경에 있는** Python이 사용됩니다. (예: 전역 환경에 설치된 `python`이 있더라도 그보다 우선합니다.) +중요한 세부 사항은 가상 환경 경로가 `PATH` 변수의 **맨 앞**에 들어간다는 점입니다. 시스템은 다른 어떤 Python보다도 **먼저** 이를 찾게 됩니다. 이렇게 하면 `python`을 실행할 때, 다른 어떤 `python`(예: 전역 환경의 `python`)이 아니라 **가상 환경의 Python**을 사용하게 됩니다. -가상 환경을 활성화하면 이 외에도 몇 가지 다른 것들이 변경되지만, 이는 그중에서도 가장 중요한 변화 중 하나입니다. +가상 환경을 활성화하면 다른 몇 가지도 변경되지만, 이것이 그중 가장 중요한 것 중 하나입니다. -## 가상 환경 확인하기 +## 가상 환경 확인하기 { #checking-a-virtual-environment } -가상 환경이 활성화 되었는지 확인하려면, 아래 명령어를 사용할 수 있습니다: +가상 환경이 활성화되어 있는지 확인할 때는, 예를 들어 다음을 사용합니다: //// tab | Linux, macOS, Windows Bash @@ -746,34 +766,33 @@ C:\Users\user\code\awesome-project\.venv\Scripts\python //// -즉, 현재 사용되는 `python` 프로그램은 **가상 환경 내부에 있는 것**입니다. +이는 사용될 `python` 프로그램이 **가상 환경 내부에 있는 것**이라는 뜻입니다. -Linux와 macOS에서는 `which`, Windows PowerShell에서는 `Get-Command` 명령어를 사용합니다. +Linux와 macOS에서는 `which`, Windows PowerShell에서는 `Get-Command`를 사용합니다. -이 명령어는 `PATH` 환경 변수에 지정된 경로들을 **순서대로 탐색**하면서 `python`이라는 이름의 프로그램을 찾습니다. -찾는 즉시, 해당 프로그램의 **경로를 출력**합니다. +이 명령어는 `PATH` 환경 변수에 있는 경로를 **순서대로** 확인하면서 `python`이라는 프로그램을 찾습니다. 찾는 즉시, 그 프로그램의 **경로를 보여줍니다**. -중요한 점은 터미널에서 `python`을 실행했을 때, 실제로 실행되는 "`python`"이 어떤 것인지 정확히 알 수 있다는 것입니다. +가장 중요한 부분은 `python`을 호출했을 때, 실행될 정확한 "`python`"이 무엇인지 알 수 있다는 점입니다. -따라서 현재 올바른 가상 환경에 있는지 확인할 수 있습니다. +따라서 올바른 가상 환경에 있는지 확인할 수 있습니다. -/// tip | 팁 +/// tip -하나의 가상 환경을 활성화한 뒤, 해당 Python을 가진 상태에서 **또 다른 프로젝트**로 이동하는 것은 흔히 발생합니다. +가상 환경을 하나 활성화해서 Python을 사용한 다음, **다른 프로젝트로 이동**하기 쉽습니다. -하지만 이때 이전 프로젝트의 가상 환경에 있는 **잘못된 Python 실행 파일**을 사용하게 되어 새 프로젝트가 **정상 작동하지 않을 수 있습니다.** +그리고 두 번째 프로젝트는 다른 프로젝트의 가상 환경에서 온 **잘못된 Python**을 사용하고 있기 때문에 **작동하지 않을** 수 있습니다. -그래서 현재 어떤 `python`이 사용되고 있는지 확인할 수 있는 능력은 매우 유용합니다. 🤓 +어떤 `python`이 사용되고 있는지 확인할 수 있으면 유용합니다. 🤓 /// -## 가상 환경을 비활성화하는 이유 +## 가상 환경을 왜 비활성화하나요 { #why-deactivate-a-virtual-environment } -예를 들어 `마법사의 돌(philosophers-stone)`이라는 프로젝트에서 작업 중이라고 해보겠습니다. 이때 해당 **가상 환경을 활성화**하고, 필요한 패키지를 설치하며 작업을 진행합니다. +예를 들어 `philosophers-stone` 프로젝트에서 작업하면서, **그 가상 환경을 활성화**하고, 패키지를 설치하고, 그 환경으로 작업하고 있다고 해보겠습니다. -그런데 이제는 **다른 프로젝트**인 `아즈카반의 죄수(prisoner-of-azkaban)`을 작업하고 싶어졌습니다. +그런데 이제 **다른 프로젝트**인 `prisoner-of-azkaban`에서 작업하고 싶습니다. -그래서 그 프로젝트 디렉터리로 이동합니다: +해당 프로젝트로 이동합니다:
@@ -783,7 +802,7 @@ $ cd ~/code/prisoner-of-azkaban
-만약 `마법사의 돌(philosophers-stone)`의 가상 환경을 비활성화하지 않았다면, 터미널에서 `python`을 실행할 때 여전히 `마법사의 돌(philosophers-stone)` 가상 환경의 Python을 사용하게 됩니다. +`philosophers-stone`의 가상 환경을 비활성화하지 않으면, 터미널에서 `python`을 실행할 때 `philosophers-stone`의 Python을 사용하려고 할 것입니다.
@@ -792,7 +811,7 @@ $ cd ~/code/prisoner-of-azkaban $ python main.py -// sirius를 임포트하는 데 실패했습니다. 설치되어 있지 않아요 😱 +// Error importing sirius, it's not installed 😱 Traceback (most recent call last): File "main.py", line 1, in import sirius @@ -800,47 +819,46 @@ Traceback (most recent call last):
-하지만 `마법사의 돌(philosophers-stone)`의 가상 환경을 비활성화한 다음, `아즈카반의 죄수(prisoner-of-azkaban)` 프로젝트의 가상 환경을 활성화하면, 이제 `python` 명령어는 `아즈카반의 죄수(prisoner-of-azkaban)` 가상 환경의 Python을 사용하게 됩니다. +하지만 가상 환경을 비활성화하고 `prisoner-of-askaban`에 대한 새 가상 환경을 활성화하면, `python`을 실행할 때 `prisoner-of-azkaban`의 가상 환경에 있는 Python을 사용하게 됩니다.
```console $ cd ~/code/prisoner-of-azkaban -// 이전 디렉터리에 있을 필요 없이, 어디서든 가상 환경을 비활성화할 수 있습니다. 다른 프로젝트 디렉터리로 이동한 후에도 괜찮아요 😎 +// You don't need to be in the old directory to deactivate, you can do it wherever you are, even after going to the other project 😎 $ deactivate -// prisoner-of-azkaban/.venv 가상 환경을 활성화합니다 🚀 +// Activate the virtual environment in prisoner-of-azkaban/.venv 🚀 $ source .venv/bin/activate -// 이제 python을 실행하면, 이 가상 환경에 설치된 sirius 패키지를 찾게 됩니다 ✨ +// Now when you run python, it will find the package sirius installed in this virtual environment ✨ $ python main.py -못된 짓을 꾸미고 있음을 엄숙히 맹세합니다.🧙 -ImportError는 이제 없습니다. 🐺 +I solemnly swear 🐺 ```
-## 대안들 +## 대안들 { #alternatives } -이 문서는 여러분이 Python 프로젝트를 시작하고, **그 내부에서** 어떻게 돌아가는지 알려주는 간단한 가이드입니다. +이 문서는 시작을 돕고, 내부에서 모든 것이 어떻게 작동하는지 알려주는 간단한 가이드입니다. -가상 환경, 패키지 의존성(Requirements), 프로젝트를 관리하는 방법에는 이 외에도 다양한 **대안**들이 존재합니다. +가상 환경, 패키지 의존성(requirements), 프로젝트를 관리하는 방법에는 많은 **대안**이 있습니다. -만약 준비가 되었다면, **프로젝트 전체**, 패키지 의존성, 가상 환경 등을 통합적으로 **관리**할 수 있는 도구를 써보는 것도 좋습니다. 그럴 때 추천하는 도구가 바로 uv입니다. +준비가 되었고 **프로젝트 전체**, 패키지 의존성, 가상 환경 등을 **관리**하는 도구를 사용하고 싶다면 uv를 사용해 보시길 권합니다. -`uv`는 다양한 기능을 지원합니다: +`uv`는 많은 일을 할 수 있습니다. 예를 들어: -* 다양한 버전의 **Python 설치** -* 각 프로젝트 별 **가상 환경 관리** -* **패키지 설치** -* 프로젝트의 **의존성과 버전** 관리 -* 설치된 패키지들과 그 버전을 **정확히 고정(lock)**해서,개발 환경과 운영 환경이 완전히 동일하게 작동할 수 있도록 보장 -* 이 외에도 다양한 기능을 지원 +* 여러 버전을 포함해 **Python을 설치** +* 프로젝트의 **가상 환경** 관리 +* **패키지** 설치 +* 프로젝트의 패키지 **의존성과 버전** 관리 +* 의존성을 포함해 설치할 패키지와 버전의 **정확한** 세트를 보장하여, 개발 중인 컴퓨터와 동일하게 프로덕션에서 실행할 수 있도록 합니다. 이를 **locking**이라고 합니다 +* 그 외에도 많은 기능이 있습니다 -## 결론 +## 결론 { #conclusion } -여기까지 모두 읽고 이해했다면, 이제 많은 개발자들보다 가상 환경을 **훨씬 더 깊이 있게 이해**하게 되셨습니다. 🤓 +여기까지 모두 읽고 이해했다면, 이제 많은 개발자들보다 가상 환경에 대해 **훨씬 더 많이** 알게 된 것입니다. 🤓 -이런 세부적인 내용을 알고 있으면, 언젠가 복잡해 보이는 문제를 디버깅할 때 분명히 큰 도움이 될 것입니다. 이제는 **이 모든 것들이 내부에서 어떻게 작동하는지** 알고 있기 때문입니다. 😎 +이 세부 사항을 알고 있으면, 나중에 복잡해 보이는 무언가를 디버깅할 때 아마도 도움이 될 것입니다. **내부에서 어떻게 작동하는지** 알고 있기 때문입니다. 😎 diff --git a/docs/ko/llm-prompt.md b/docs/ko/llm-prompt.md new file mode 100644 index 0000000000..df807c9496 --- /dev/null +++ b/docs/ko/llm-prompt.md @@ -0,0 +1,51 @@ +### Target language + +Translate to Korean (한국어). + +Language code: ko. + +### Grammar and tone + +- Use polite, instructional Korean (e.g. 합니다/하세요 style). +- Keep the tone consistent with the existing Korean FastAPI docs. + +### Headings + +- Follow existing Korean heading style (short, action-oriented headings like “확인하기”). +- Do not add trailing punctuation to headings. + +### Quotes + +- Keep quote style consistent with the existing Korean docs. +- Never change quotes inside inline code, code blocks, URLs, or file paths. + +### Ellipsis + +- Keep ellipsis style consistent with existing Korean docs (often `...`). +- Never change `...` in code, URLs, or CLI examples. + +### Preferred translations / glossary + +Use the following preferred translations when they apply in documentation prose: + +- request (HTTP): 요청 +- response (HTTP): 응답 +- path operation: 경로 처리 +- path operation function: 경로 처리 함수 + +### `///` admonitions + +1) Keep the admonition keyword in English (do not translate `note`, `tip`, etc.). +2) If a title is present, prefer these canonical titles: + +- `/// note | 참고` +- `/// tip | 팁` +- `/// warning | 경고` +- `/// info | 정보` +- `/// danger | 위험` +- `/// note Technical Details | 기술 세부사항` +- `/// check | 확인` +Notes: + +- `details` blocks exist in Korean docs; keep `/// details` as-is and translate only the title after `|`. +- Example canonical title used: `/// details | 상세 설명` diff --git a/docs/missing-translation.md b/docs/missing-translation.md index bfff847669..71c0925c5c 100644 --- a/docs/missing-translation.md +++ b/docs/missing-translation.md @@ -4,6 +4,6 @@ This page hasn’t been translated into your language yet. 🌍 We’re currently switching to an automated translation system 🤖, which will help keep all translations complete and up to date. -Learn more: [Contributing – Translations](https://fastapi.tiangolo.com/contributing/#translations){.internal-link target=_blank} +Learn more: [Contributing - Translations](https://fastapi.tiangolo.com/contributing/#translations){.internal-link target=_blank} /// diff --git a/docs/pt/docs/_llm-test.md b/docs/pt/docs/_llm-test.md new file mode 100644 index 0000000000..3da5e8a71d --- /dev/null +++ b/docs/pt/docs/_llm-test.md @@ -0,0 +1,503 @@ +# Arquivo de teste de LLM { #llm-test-file } + +Este documento testa se o LLM, que traduz a documentação, entende o `general_prompt` em `scripts/translate.py` e o prompt específico do idioma em `docs/{language code}/llm-prompt.md`. O prompt específico do idioma é anexado ao `general_prompt`. + +Os testes adicionados aqui serão vistos por todos os autores dos prompts específicos de idioma. + +Use da seguinte forma: + +* Tenha um prompt específico do idioma – `docs/{language code}/llm-prompt.md`. +* Faça uma tradução nova deste documento para o seu idioma de destino (veja, por exemplo, o comando `translate-page` do `translate.py`). Isso criará a tradução em `docs/{language code}/docs/_llm-test.md`. +* Verifique se está tudo certo na tradução. +* Se necessário, melhore seu prompt específico do idioma, o prompt geral ou o documento em inglês. +* Em seguida, corrija manualmente os problemas restantes na tradução, para que fique uma boa tradução. +* Retraduzir, tendo a boa tradução no lugar. O resultado ideal seria que o LLM não fizesse mais mudanças na tradução. Isso significa que o prompt geral e o seu prompt específico do idioma estão tão bons quanto possível (às vezes fará algumas mudanças aparentemente aleatórias, a razão é que LLMs não são algoritmos determinísticos). + +Os testes: + +## Trechos de código { #code-snippets } + +//// tab | Teste + +Este é um trecho de código: `foo`. E este é outro trecho de código: `bar`. E mais um: `baz quux`. + +//// + +//// tab | Informações + +O conteúdo dos trechos de código deve ser deixado como está. + +Veja a seção `### Content of code snippets` no prompt geral em `scripts/translate.py`. + +//// + +## Citações { #quotes } + +//// tab | Teste + +Ontem, meu amigo escreveu: "Se você soletrar incorretamente corretamente, você a soletrou incorretamente". Ao que respondi: "Correto, mas 'incorrectly' está incorretamente não '"incorrectly"'". + +/// note | Nota + +O LLM provavelmente vai traduzir isso errado. O interessante é apenas se ele mantém a tradução corrigida ao retraduzir. + +/// + +//// + +//// tab | Informações + +O autor do prompt pode escolher se deseja converter aspas neutras em aspas tipográficas. Também é aceitável deixá-las como estão. + +Veja, por exemplo, a seção `### Quotes` em `docs/de/llm-prompt.md`. + +//// + +## Citações em trechos de código { #quotes-in-code-snippets } + +//// tab | Teste + +`pip install "foo[bar]"` + +Exemplos de literais de string em trechos de código: `"this"`, `'that'`. + +Um exemplo difícil de literais de string em trechos de código: `f"I like {'oranges' if orange else "apples"}"` + +Pesado: `Yesterday, my friend wrote: "If you spell incorrectly correctly, you have spelled it incorrectly". To which I answered: "Correct, but 'incorrectly' is incorrectly not '"incorrectly"'"` + +//// + +//// tab | Informações + +... No entanto, as aspas dentro de trechos de código devem permanecer como estão. + +//// + +## Blocos de código { #code-blocks } + +//// tab | Teste + +Um exemplo de código Bash... + +```bash +# Imprimir uma saudação ao universo +echo "Hello universe" +``` + +...e um exemplo de código de console... + +```console +$ fastapi run main.py + FastAPI Starting server + Searching for package file structure +``` + +...e outro exemplo de código de console... + +```console +// Crie um diretório "Code" +$ mkdir code +// Entre nesse diretório +$ cd code +``` + +...e um exemplo de código Python... + +```Python +wont_work() # Isto não vai funcionar 😱 +works(foo="bar") # Isto funciona 🎉 +``` + +...e é isso. + +//// + +//// tab | Informações + +O código em blocos de código não deve ser modificado, com exceção dos comentários. + +Veja a seção `### Content of code blocks` no prompt geral em `scripts/translate.py`. + +//// + +## Abas e caixas coloridas { #tabs-and-colored-boxes } + +//// tab | Teste + +/// info | Informação +Algum texto +/// + +/// note | Nota +Algum texto +/// + +/// note | Detalhes Técnicos +Algum texto +/// + +/// check | Verifique +Algum texto +/// + +/// tip | Dica +Algum texto +/// + +/// warning | Atenção +Algum texto +/// + +/// danger | Cuidado +Algum texto +/// + +//// + +//// tab | Informações + +Abas e blocos `Info`/`Note`/`Warning`/etc. devem ter a tradução do seu título adicionada após uma barra vertical (`|`). + +Veja as seções `### Special blocks` e `### Tab blocks` no prompt geral em `scripts/translate.py`. + +//// + +## Links da Web e internos { #web-and-internal-links } + +//// tab | Teste + +O texto do link deve ser traduzido, o endereço do link deve permanecer inalterado: + +* [Link para o título acima](#code-snippets) +* [Link interno](index.md#installation){.internal-link target=_blank} +* Link externo +* Link para um estilo +* Link para um script +* Link para uma imagem + +O texto do link deve ser traduzido, o endereço do link deve apontar para a tradução: + +* Link do FastAPI + +//// + +//// tab | Informações + +Os links devem ser traduzidos, mas seus endereços devem permanecer inalterados. Uma exceção são links absolutos para páginas da documentação do FastAPI. Nesse caso, devem apontar para a tradução. + +Veja a seção `### Links` no prompt geral em `scripts/translate.py`. + +//// + +## Elementos HTML "abbr" { #html-abbr-elements } + +//// tab | Teste + +Aqui estão algumas coisas envolvidas em elementos HTML "abbr" (algumas são inventadas): + +### O abbr fornece uma frase completa { #the-abbr-gives-a-full-phrase } + +* GTD +* lt +* XWT +* PSGI + +### O abbr fornece uma explicação { #the-abbr-gives-an-explanation } + +* cluster +* Deep Learning + +### O abbr fornece uma frase completa e uma explicação { #the-abbr-gives-a-full-phrase-and-an-explanation } + +* MDN +* I/O. + +//// + +//// tab | Informações + +Os atributos "title" dos elementos "abbr" são traduzidos seguindo algumas instruções específicas. + +As traduções podem adicionar seus próprios elementos "abbr" que o LLM não deve remover. Por exemplo, para explicar palavras em inglês. + +Veja a seção `### HTML abbr elements` no prompt geral em `scripts/translate.py`. + +//// + +## Títulos { #headings } + +//// tab | Teste + +### Desenvolver uma aplicação web - um tutorial { #develop-a-webapp-a-tutorial } + +Olá. + +### Anotações de tipo e -anotações { #type-hints-and-annotations } + +Olá novamente. + +### Super- e subclasses { #super-and-subclasses } + +Olá novamente. + +//// + +//// tab | Informações + +A única regra rígida para títulos é que o LLM deixe a parte do hash dentro de chaves inalterada, o que garante que os links não quebrem. + +Veja a seção `### Headings` no prompt geral em `scripts/translate.py`. + +Para algumas instruções específicas do idioma, veja, por exemplo, a seção `### Headings` em `docs/de/llm-prompt.md`. + +//// + +## Termos usados na documentação { #terms-used-in-the-docs } + +//// tab | Teste + +* você +* seu + +* por exemplo +* etc. + +* `foo` como um `int` +* `bar` como uma `str` +* `baz` como uma `list` + +* o Tutorial - Guia do Usuário +* o Guia do Usuário Avançado +* a documentação do SQLModel +* a documentação da API +* a documentação automática + +* Ciência de Dados +* Deep Learning +* Aprendizado de Máquina +* Injeção de Dependências +* autenticação HTTP Basic +* HTTP Digest +* formato ISO +* o padrão JSON Schema +* o JSON schema +* a definição do schema +* Fluxo de Senha +* Mobile + +* descontinuado +* projetado +* inválido +* dinamicamente +* padrão +* padrão predefinido +* sensível a maiúsculas e minúsculas +* não sensível a maiúsculas e minúsculas + +* servir a aplicação +* servir a página + +* o app +* a aplicação + +* a requisição +* a resposta +* a resposta de erro + +* a operação de rota +* o decorador de operação de rota +* a função de operação de rota + +* o corpo +* o corpo da requisição +* o corpo da resposta +* o corpo JSON +* o corpo do formulário +* o corpo do arquivo +* o corpo da função + +* o parâmetro +* o parâmetro de corpo +* o parâmetro de path +* o parâmetro de query +* o parâmetro de cookie +* o parâmetro de header +* o parâmetro de formulário +* o parâmetro da função + +* o evento +* o evento de inicialização +* a inicialização do servidor +* o evento de encerramento +* o evento de lifespan + +* o manipulador +* o manipulador de eventos +* o manipulador de exceções +* tratar + +* o modelo +* o modelo Pydantic +* o modelo de dados +* o modelo de banco de dados +* o modelo de formulário +* o objeto de modelo + +* a classe +* a classe base +* a classe pai +* a subclasse +* a classe filha +* a classe irmã +* o método de classe + +* o cabeçalho +* os cabeçalhos +* o cabeçalho de autorização +* o cabeçalho `Authorization` +* o cabeçalho encaminhado + +* o sistema de injeção de dependências +* a dependência +* o dependable +* o dependant + +* limitado por I/O +* limitado por CPU +* concorrência +* paralelismo +* multiprocessamento + +* a env var +* a variável de ambiente +* o `PATH` +* a variável `PATH` + +* a autenticação +* o provedor de autenticação +* a autorização +* o formulário de autorização +* o provedor de autorização +* o usuário se autentica +* o sistema autentica o usuário + +* a CLI +* a interface de linha de comando + +* o servidor +* o cliente + +* o provedor de nuvem +* o serviço de nuvem + +* o desenvolvimento +* as etapas de desenvolvimento + +* o dict +* o dicionário +* a enumeração +* o enum +* o membro do enum + +* o codificador +* o decodificador +* codificar +* decodificar + +* a exceção +* lançar + +* a expressão +* a instrução + +* o frontend +* o backend + +* a discussão do GitHub +* a issue do GitHub + +* o desempenho +* a otimização de desempenho + +* o tipo de retorno +* o valor de retorno + +* a segurança +* o esquema de segurança + +* a tarefa +* a tarefa em segundo plano +* a função da tarefa + +* o template +* o mecanismo de template + +* a anotação de tipo +* a anotação de tipo + +* o worker de servidor +* o worker do Uvicorn +* o Worker do Gunicorn +* o processo worker +* a classe de worker +* a carga de trabalho + +* a implantação +* implantar + +* o SDK +* o kit de desenvolvimento de software + +* o `APIRouter` +* o `requirements.txt` +* o Bearer Token +* a alteração com quebra de compatibilidade +* o bug +* o botão +* o chamável +* o código +* o commit +* o gerenciador de contexto +* a corrotina +* a sessão do banco de dados +* o disco +* o domínio +* o mecanismo +* o X falso +* o método HTTP GET +* o item +* a biblioteca +* o lifespan +* o bloqueio +* o middleware +* a aplicação mobile +* o módulo +* a montagem +* a rede +* a origem +* a sobrescrita +* a carga útil +* o processador +* a propriedade +* o proxy +* o pull request +* a consulta +* a RAM +* a máquina remota +* o código de status +* a string +* a tag +* o framework web +* o curinga +* retornar +* validar + +//// + +//// tab | Informações + +Esta é uma lista não completa e não normativa de termos (principalmente) técnicos vistos na documentação. Pode ser útil para o autor do prompt descobrir para quais termos o LLM precisa de uma ajudinha. Por exemplo, quando ele continua revertendo uma boa tradução para uma tradução subótima. Ou quando tem problemas para conjugar/declinar um termo no seu idioma. + +Veja, por exemplo, a seção `### List of English terms and their preferred German translations` em `docs/de/llm-prompt.md`. + +//// diff --git a/docs/pt/docs/about/index.md b/docs/pt/docs/about/index.md index 1f42e88318..39e160741b 100644 --- a/docs/pt/docs/about/index.md +++ b/docs/pt/docs/about/index.md @@ -1,3 +1,3 @@ -# Sobre +# Sobre { #about } -Sobre o FastAPI, seus padrões, inspirações e muito mais. 🤓 +Sobre o FastAPI, seu design, inspiração e mais. 🤓 diff --git a/docs/pt/docs/advanced/additional-responses.md b/docs/pt/docs/advanced/additional-responses.md index 1060d18afc..688bc16967 100644 --- a/docs/pt/docs/advanced/additional-responses.md +++ b/docs/pt/docs/advanced/additional-responses.md @@ -1,6 +1,6 @@ -# Retornos Adicionais no OpenAPI +# Retornos Adicionais no OpenAPI { #additional-responses-in-openapi } -/// warning | Aviso +/// warning | Atenção Este é um tema bem avançado. @@ -14,7 +14,7 @@ Essas respostas adicionais serão incluídas no esquema do OpenAPI, e também ap Porém para as respostas adicionais, você deve garantir que está retornando um `Response` como por exemplo o `JSONResponse` diretamente, junto com o código de status e o conteúdo. -## Retorno Adicional com `model` +## Retorno Adicional com `model` { #additional-response-with-model } Você pode fornecer o parâmetro `responses` aos seus *decoradores de caminho*. @@ -26,7 +26,7 @@ O **FastAPI** pegará este modelo, gerará o esquema JSON dele e incluirá no lo Por exemplo, para declarar um outro retorno com o status code `404` e um modelo do Pydantic chamado `Message`, você pode escrever: -{* ../../docs_src/additional_responses/tutorial001.py hl[18,22] *} +{* ../../docs_src/additional_responses/tutorial001_py39.py hl[18,22] *} /// note | Nota @@ -49,7 +49,7 @@ O local correto é: /// -O retorno gerado no OpenAI para esta *operação de caminho* será: +O retorno gerado no OpenAPI para esta *operação de rota* será: ```JSON hl_lines="3-12" { @@ -169,13 +169,13 @@ Os esquemas são referenciados em outro local dentro do esquema OpenAPI: } ``` -## Media types adicionais para o retorno principal +## Media types adicionais para o retorno principal { #additional-media-types-for-the-main-response } Você pode utilizar o mesmo parâmetro `responses` para adicionar diferentes media types para o mesmo retorno principal. -Por exemplo, você pode adicionar um media type adicional de `image/png`, declarando que a sua *operação de caminho* pode retornar um objeto JSON (com o media type `application/json`) ou uma imagem PNG: +Por exemplo, você pode adicionar um media type adicional de `image/png`, declarando que a sua *operação de rota* pode retornar um objeto JSON (com o media type `application/json`) ou uma imagem PNG: -{* ../../docs_src/additional_responses/tutorial002.py hl[19:24,28] *} +{* ../../docs_src/additional_responses/tutorial002_py310.py hl[17:22,26] *} /// note | Nota @@ -191,7 +191,7 @@ Porém se você especificou uma classe de retorno com o valor `None` como media /// -## Combinando informações +## Combinando informações { #combining-information } Você também pode combinar informações de diferentes lugares, incluindo os parâmetros `response_model`, `status_code`, e `responses`. @@ -203,15 +203,15 @@ Por exemplo, você pode declarar um retorno com o código de status `404` que ut E um retorno com o código de status `200` que utiliza o seu `response_model`, porém inclui um `example` customizado: -{* ../../docs_src/additional_responses/tutorial003.py hl[20:31] *} +{* ../../docs_src/additional_responses/tutorial003_py39.py hl[20:31] *} Isso será combinado e incluído em seu OpenAPI, e disponibilizado na documentação da sua API: -## Combinar retornos predefinidos e personalizados +## Combinar retornos predefinidos e personalizados { #combine-predefined-responses-and-custom-ones } -Você pode querer possuir alguns retornos predefinidos que são aplicados para diversas *operações de caminho*, porém você deseja combinar com retornos personalizados que são necessários para cada *operação de caminho*. +Você pode querer possuir alguns retornos predefinidos que são aplicados para diversas *operações de rota*, porém você deseja combinar com retornos personalizados que são necessários para cada *operação de rota*. Para estes casos, você pode utilizar a técnica do Python de "desempacotamento" de um `dict` utilizando `**dict_to_unpack`: @@ -233,15 +233,15 @@ Aqui, o `new_dict` terá todos os pares de chave-valor do `old_dict` mais o novo } ``` -Você pode utilizar essa técnica para reutilizar alguns retornos predefinidos nas suas *operações de caminho* e combiná-las com personalizações adicionais. +Você pode utilizar essa técnica para reutilizar alguns retornos predefinidos nas suas *operações de rota* e combiná-las com personalizações adicionais. Por exemplo: -{* ../../docs_src/additional_responses/tutorial004.py hl[13:17,26] *} +{* ../../docs_src/additional_responses/tutorial004_py310.py hl[11:15,24] *} -## Mais informações sobre retornos OpenAPI +## Mais informações sobre retornos OpenAPI { #more-information-about-openapi-responses } Para verificar exatamente o que você pode incluir nos retornos, você pode conferir estas seções na especificação do OpenAPI: -* Objeto de Retorno OpenAPI, inclui o `Response Object`. -* Objeto de Retorno OpenAPI, você pode incluir qualquer coisa dele diretamente em cada retorno dentro do seu parâmetro `responses`. Incluindo `description`, `headers`, `content` (dentro dele que você declara diferentes media types e esquemas JSON), e `links`. +* Objeto de Retorno OpenAPI, inclui o `Response Object`. +* Objeto de Retorno OpenAPI, você pode incluir qualquer coisa dele diretamente em cada retorno dentro do seu parâmetro `responses`. Incluindo `description`, `headers`, `content` (dentro dele que você declara diferentes media types e esquemas JSON), e `links`. diff --git a/docs/pt/docs/advanced/additional-status-codes.md b/docs/pt/docs/advanced/additional-status-codes.md index 06d6191519..fd90b1795d 100644 --- a/docs/pt/docs/advanced/additional-status-codes.md +++ b/docs/pt/docs/advanced/additional-status-codes.md @@ -1,22 +1,22 @@ -# Códigos de status adicionais +# Códigos de status adicionais { #additional-status-codes } -Por padrão, o **FastAPI** retornará as respostas utilizando o `JSONResponse`, adicionando o conteúdo do retorno da sua *operação de caminho* dentro do `JSONResponse`. +Por padrão, o **FastAPI** retornará as respostas utilizando o `JSONResponse`, adicionando o conteúdo do retorno da sua *operação de rota* dentro do `JSONResponse`. -Ele usará o código de status padrão ou o que você definir na sua *operação de caminho*. +Ele usará o código de status padrão ou o que você definir na sua *operação de rota*. -## Códigos de status adicionais +## Códigos de status adicionais { #additional-status-codes_1 } Caso você queira retornar códigos de status adicionais além do código principal, você pode fazer isso retornando um `Response` diretamente, como por exemplo um `JSONResponse`, e definir os códigos de status adicionais diretamente. -Por exemplo, vamos dizer que você deseja ter uma *operação de caminho* que permita atualizar itens, e retornar um código de status HTTP 200 "OK" quando for bem sucedido. +Por exemplo, vamos dizer que você deseja ter uma *operação de rota* que permita atualizar itens, e retornar um código de status HTTP 200 "OK" quando for bem sucedido. -Mas você também deseja aceitar novos itens. E quando os itens não existiam, ele os cria, e retorna o código de status HTTP 201 "Created. +Mas você também deseja aceitar novos itens. E quando os itens não existiam, ele os cria, e retorna o código de status HTTP 201 "Created". Para conseguir isso, importe `JSONResponse` e retorne o seu conteúdo diretamente, definindo o `status_code` que você deseja: {* ../../docs_src/additional_status_codes/tutorial001_an_py310.py hl[4,25] *} -/// warning | Aviso +/// warning | Atenção Quando você retorna um `Response` diretamente, como no exemplo acima, ele será retornado diretamente. @@ -26,7 +26,7 @@ Garanta que ele tenha toda informação que você deseja, e que os valores sejam /// -/// note | Detalhes técnicos +/// note | Detalhes Técnicos Você também pode utilizar `from starlette.responses import JSONResponse`. @@ -34,7 +34,7 @@ O **FastAPI** disponibiliza o `starlette.responses` como `fastapi.responses` ape /// -## OpenAPI e documentação da API +## OpenAPI e documentação da API { #openapi-and-api-docs } Se você retorna códigos de status adicionais e retornos diretamente, eles não serão incluídos no esquema do OpenAPI (a documentação da API), porque o FastAPI não tem como saber de antemão o que será retornado. diff --git a/docs/pt/docs/advanced/advanced-dependencies.md b/docs/pt/docs/advanced/advanced-dependencies.md index f57abba618..1ad9ea617f 100644 --- a/docs/pt/docs/advanced/advanced-dependencies.md +++ b/docs/pt/docs/advanced/advanced-dependencies.md @@ -1,6 +1,6 @@ -# Dependências avançadas +# Dependências avançadas { #advanced-dependencies } -## Dependências parametrizadas +## Dependências parametrizadas { #parameterized-dependencies } Todas as dependências que vimos até agora são funções ou classes fixas. @@ -10,7 +10,7 @@ Vamos imaginar que queremos ter uma dependência que verifica se o parâmetro de Porém nós queremos poder parametrizar o conteúdo fixo. -## Uma instância "chamável" +## Uma instância "chamável" { #a-callable-instance } Em Python existe uma maneira de fazer com que uma instância de uma classe seja um "chamável". @@ -22,7 +22,7 @@ Para fazer isso, nós declaramos o método `__call__`: Neste caso, o `__call__` é o que o **FastAPI** utilizará para verificar parâmetros adicionais e sub dependências, e isso é o que será chamado para passar o valor ao parâmetro na sua *função de operação de rota* posteriormente. -## Parametrizar a instância +## Parametrizar a instância { #parameterize-the-instance } E agora, nós podemos utilizar o `__init__` para declarar os parâmetros da instância que podemos utilizar para "parametrizar" a dependência: @@ -30,7 +30,7 @@ E agora, nós podemos utilizar o `__init__` para declarar os parâmetros da inst Neste caso, o **FastAPI** nunca tocará ou se importará com o `__init__`, nós vamos utilizar diretamente em nosso código. -## Crie uma instância +## Crie uma instância { #create-an-instance } Nós poderíamos criar uma instância desta classe com: @@ -38,7 +38,7 @@ Nós poderíamos criar uma instância desta classe com: E deste modo nós podemos "parametrizar" a nossa dependência, que agora possui `"bar"` dentro dele, como o atributo `checker.fixed_content`. -## Utilize a instância como dependência +## Utilize a instância como dependência { #use-the-instance-as-a-dependency } Então, nós podemos utilizar este `checker` em um `Depends(checker)`, no lugar de `Depends(FixedContentQueryChecker)`, porque a dependência é a instância, `checker`, e não a própria classe. @@ -63,3 +63,101 @@ Nos capítulos sobre segurança, existem funções utilitárias que são impleme Se você entendeu tudo isso, você já sabe como essas funções utilitárias para segurança funcionam por debaixo dos panos. /// + +## Dependências com `yield`, `HTTPException`, `except` e Tarefas em Segundo Plano { #dependencies-with-yield-httpexception-except-and-background-tasks } + +/// warning | Atenção + +Muito provavelmente você não precisa desses detalhes técnicos. + +Esses detalhes são úteis principalmente se você tinha uma aplicação FastAPI anterior à versão 0.121.0 e está enfrentando problemas com dependências com `yield`. + +/// + +Dependências com `yield` evoluíram ao longo do tempo para contemplar diferentes casos de uso e corrigir alguns problemas, aqui está um resumo do que mudou. + +### Dependências com `yield` e `scope` { #dependencies-with-yield-and-scope } + +Na versão 0.121.0, o FastAPI adicionou suporte a `Depends(scope="function")` para dependências com `yield`. + +Usando `Depends(scope="function")`, o código de saída após o `yield` é executado logo depois que a *função de operação de rota* termina, antes de a response ser enviada de volta ao cliente. + +E ao usar `Depends(scope="request")` (o padrão), o código de saída após o `yield` é executado depois que a response é enviada. + +Você pode ler mais na documentação em [Dependências com `yield` - Saída antecipada e `scope`](../tutorial/dependencies/dependencies-with-yield.md#early-exit-and-scope). + +### Dependências com `yield` e `StreamingResponse`, Detalhes Técnicos { #dependencies-with-yield-and-streamingresponse-technical-details } + +Antes do FastAPI 0.118.0, se você usasse uma dependência com `yield`, o código de saída (após o `yield`) rodaria depois que a *função de operação de rota* retornasse, mas logo antes de enviar a resposta. + +A intenção era evitar manter recursos por mais tempo que o necessário, esperando a resposta percorrer a rede. + +Essa mudança também significava que, se você retornasse um `StreamingResponse`, o código de saída da dependência com `yield` já teria sido executado. + +Por exemplo, se você tivesse uma sessão de banco de dados em uma dependência com `yield`, o `StreamingResponse` não conseguiria usar essa sessão enquanto transmite dados, porque a sessão já teria sido fechada no código de saída após o `yield`. + +Esse comportamento foi revertido na versão 0.118.0, para que o código de saída após o `yield` seja executado depois que a resposta for enviada. + +/// info | Informação + +Como você verá abaixo, isso é muito semelhante ao comportamento antes da versão 0.106.0, mas com várias melhorias e correções de bugs para casos extremos. + +/// + +#### Casos de uso com código de saída antecipado { #use-cases-with-early-exit-code } + +Há alguns casos de uso, com condições específicas, que poderiam se beneficiar do comportamento antigo de executar o código de saída das dependências com `yield` antes de enviar a resposta. + +Por exemplo, imagine que você tem código que usa uma sessão de banco de dados em uma dependência com `yield` apenas para verificar um usuário, mas a sessão de banco de dados nunca é usada novamente na *função de operação de rota*, somente na dependência, e a resposta demora a ser enviada, como um `StreamingResponse` que envia dados lentamente, mas por algum motivo não usa o banco de dados. + +Nesse caso, a sessão de banco de dados seria mantida até que a resposta termine de ser enviada, mas se você não a usa, então não seria necessário mantê-la. + +Veja como poderia ser: + +{* ../../docs_src/dependencies/tutorial013_an_py310.py *} + +O código de saída, o fechamento automático da `Session` em: + +{* ../../docs_src/dependencies/tutorial013_an_py310.py ln[19:21] *} + +...seria executado depois que a resposta terminar de enviar os dados lentos: + +{* ../../docs_src/dependencies/tutorial013_an_py310.py ln[30:38] hl[31:33] *} + +Mas como `generate_stream()` não usa a sessão do banco de dados, não é realmente necessário manter a sessão aberta enquanto envia a resposta. + +Se você tiver esse caso específico usando SQLModel (ou SQLAlchemy), você poderia fechar explicitamente a sessão depois que não precisar mais dela: + +{* ../../docs_src/dependencies/tutorial014_an_py310.py ln[24:28] hl[28] *} + +Dessa forma a sessão liberaria a conexão com o banco de dados, para que outras requisições pudessem usá-la. + +Se você tiver um caso diferente que precise sair antecipadamente de uma dependência com `yield`, por favor crie uma Pergunta no GitHub Discussions com o seu caso específico e por que você se beneficiaria de ter o fechamento antecipado para dependências com `yield`. + +Se houver casos de uso convincentes para fechamento antecipado em dependências com `yield`, considerarei adicionar uma nova forma de optar por esse fechamento antecipado. + +### Dependências com `yield` e `except`, Detalhes Técnicos { #dependencies-with-yield-and-except-technical-details } + +Antes do FastAPI 0.110.0, se você usasse uma dependência com `yield`, e então capturasse uma exceção com `except` nessa dependência, e você não relançasse a exceção, a exceção seria automaticamente levantada/encaminhada para quaisquer tratadores de exceção ou para o tratador de erro interno do servidor. + +Isso foi alterado na versão 0.110.0 para corrigir consumo de memória não tratado decorrente de exceções encaminhadas sem um tratador (erros internos do servidor), e para torná-lo consistente com o comportamento do código Python regular. + +### Tarefas em Segundo Plano e Dependências com `yield`, Detalhes Técnicos { #background-tasks-and-dependencies-with-yield-technical-details } + +Antes do FastAPI 0.106.0, lançar exceções após o `yield` não era possível, o código de saída em dependências com `yield` era executado depois que a resposta era enviada, então [Tratadores de Exceções](../tutorial/handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank} já teriam sido executados. + +Isso foi projetado assim principalmente para permitir o uso dos mesmos objetos "yielded" por dependências dentro de tarefas em segundo plano, porque o código de saída seria executado depois que as tarefas em segundo plano fossem concluídas. + +Isso foi alterado no FastAPI 0.106.0 com a intenção de não manter recursos enquanto se espera a resposta percorrer a rede. + +/// tip | Dica + +Além disso, uma tarefa em segundo plano normalmente é um conjunto de lógica independente que deve ser tratado separadamente, com seus próprios recursos (por exemplo, sua própria conexão de banco de dados). + +Assim, desta forma você provavelmente terá um código mais limpo. + +/// + +Se você costumava depender desse comportamento, agora você deve criar os recursos para tarefas em segundo plano dentro da própria tarefa em segundo plano, e usar internamente apenas dados que não dependam dos recursos de dependências com `yield`. + +Por exemplo, em vez de usar a mesma sessão de banco de dados, você criaria uma nova sessão de banco de dados dentro da tarefa em segundo plano, e obteria os objetos do banco de dados usando essa nova sessão. E então, em vez de passar o objeto do banco de dados como parâmetro para a função da tarefa em segundo plano, você passaria o ID desse objeto e então obteria o objeto novamente dentro da função da tarefa em segundo plano. diff --git a/docs/pt/docs/advanced/async-tests.md b/docs/pt/docs/advanced/async-tests.md index a2b79426c6..953ebedd9c 100644 --- a/docs/pt/docs/advanced/async-tests.md +++ b/docs/pt/docs/advanced/async-tests.md @@ -1,4 +1,4 @@ -# Testes Assíncronos +# Testes Assíncronos { #async-tests } Você já viu como testar as suas aplicações **FastAPI** utilizando o `TestClient` que é fornecido. Até agora, você viu apenas como escrever testes síncronos, sem utilizar funções `async`. @@ -6,11 +6,11 @@ Ser capaz de utilizar funções assíncronas em seus testes pode ser útil, por Vamos ver como nós podemos fazer isso funcionar. -## pytest.mark.anyio +## pytest.mark.anyio { #pytest-mark-anyio } Se quisermos chamar funções assíncronas em nossos testes, as nossas funções de teste precisam ser assíncronas. O AnyIO oferece um plugin bem legal para isso, que nos permite especificar que algumas das nossas funções de teste precisam ser chamadas de forma assíncrona. -## HTTPX +## HTTPX { #httpx } Mesmo que a sua aplicação **FastAPI** utilize funções normais com `def` no lugar de `async def`, ela ainda é uma aplicação `async` por baixo dos panos. @@ -18,7 +18,7 @@ O `TestClient` faz algumas mágicas para invocar a aplicação FastAPI assíncro O `TestClient` é baseado no HTTPX, e felizmente nós podemos utilizá-lo diretamente para testar a API. -## Exemplo +## Exemplo { #example } Para um exemplos simples, vamos considerar uma estrutura de arquivos semelhante ao descrito em [Bigger Applications](../tutorial/bigger-applications.md){.internal-link target=_blank} e [Testing](../tutorial/testing.md){.internal-link target=_blank}: @@ -32,13 +32,13 @@ Para um exemplos simples, vamos considerar uma estrutura de arquivos semelhante O arquivo `main.py` teria: -{* ../../docs_src/async_tests/main.py *} +{* ../../docs_src/async_tests/app_a_py39/main.py *} O arquivo `test_main.py` teria os testes para para o arquivo `main.py`, ele poderia ficar assim: -{* ../../docs_src/async_tests/test_main.py *} +{* ../../docs_src/async_tests/app_a_py39/test_main.py *} -## Executá-lo +## Executá-lo { #run-it } Você pode executar os seus testes normalmente via: @@ -52,11 +52,11 @@ $ pytest
-## Em Detalhes +## Em Detalhes { #in-detail } O marcador `@pytest.mark.anyio` informa ao pytest que esta função de teste deve ser invocada de maneira assíncrona: -{* ../../docs_src/async_tests/test_main.py hl[7] *} +{* ../../docs_src/async_tests/app_a_py39/test_main.py hl[7] *} /// tip | Dica @@ -66,7 +66,7 @@ Note que a função de teste é `async def` agora, no lugar de apenas `def` como Então podemos criar um `AsyncClient` com a aplicação, e enviar requisições assíncronas para ela utilizando `await`. -{* ../../docs_src/async_tests/test_main.py hl[9:12] *} +{* ../../docs_src/async_tests/app_a_py39/test_main.py hl[9:12] *} Isso é equivalente a: @@ -82,18 +82,18 @@ Note que nós estamos utilizando async/await com o novo `AsyncClient` - a requis /// -/// warning | Aviso +/// warning | Atenção -Se a sua aplicação depende dos eventos de vida útil (*lifespan*), o `AsyncClient` não acionará estes eventos. Para garantir que eles são acionados, utilize o `LifespanManager` do florimondmanca/asgi-lifespan. +Se a sua aplicação depende de eventos de lifespan, o `AsyncClient` não acionará estes eventos. Para garantir que eles são acionados, utilize o `LifespanManager` do florimondmanca/asgi-lifespan. /// -## Outras Chamadas de Funções Assíncronas +## Outras Chamadas de Funções Assíncronas { #other-asynchronous-function-calls } -Como a função de teste agora é assíncrona, você pode chamar (e `esperar`) outras funções `async` além de enviar requisições para a sua aplicação FastAPI em seus testes, exatamente como você as chamaria em qualquer outro lugar do seu código. +Como a função de teste agora é assíncrona, você pode chamar (e `await`) outras funções `async` além de enviar requisições para a sua aplicação FastAPI em seus testes, exatamente como você as chamaria em qualquer outro lugar do seu código. /// tip | Dica -Se você se deparar com um `RuntimeError: Task attached to a different loop` ao integrar funções assíncronas em seus testes (e.g. ao utilizar o MotorClient do MongoDB) Lembre-se de instanciar objetos que precisam de um loop de eventos (*event loop*) apenas em funções assíncronas, e.g. um *"callback"* `'@app.on_event("startup")`. +Se você se deparar com um `RuntimeError: Task attached to a different loop` ao integrar funções assíncronas em seus testes (e.g. ao utilizar o MotorClient do MongoDB) Lembre-se de instanciar objetos que precisam de um loop de eventos (*event loop*) apenas em funções assíncronas, e.g. um callback `@app.on_event("startup")`. /// diff --git a/docs/pt/docs/advanced/behind-a-proxy.md b/docs/pt/docs/advanced/behind-a-proxy.md index 6837c95426..bf0d124289 100644 --- a/docs/pt/docs/advanced/behind-a-proxy.md +++ b/docs/pt/docs/advanced/behind-a-proxy.md @@ -1,34 +1,131 @@ -# Atrás de um Proxy +# Atrás de um Proxy { #behind-a-proxy } -Em algumas situações, você pode precisar usar um servidor **proxy** como Traefik ou Nginx com uma configuração que adiciona um prefixo de caminho extra que não é visto pela sua aplicação. +Em muitas situações, você usaria um **proxy** como Traefik ou Nginx na frente da sua aplicação FastAPI. + +Esses proxies podem lidar com certificados HTTPS e outras coisas. + +## Headers Encaminhados pelo Proxy { #proxy-forwarded-headers } + +Um **proxy** na frente da sua aplicação normalmente definiria alguns headers dinamicamente antes de enviar as requisições para o seu **servidor**, para informar ao servidor que a requisição foi **encaminhada** pelo proxy, informando a URL original (pública), incluindo o domínio, que está usando HTTPS, etc. + +O programa do **servidor** (por exemplo, **Uvicorn** via **CLI do FastAPI**) é capaz de interpretar esses headers e então repassar essas informações para a sua aplicação. + +Mas, por segurança, como o servidor não sabe que está atrás de um proxy confiável, ele não interpretará esses headers. + +/// note | Detalhes Técnicos + +Os headers do proxy são: + +* X-Forwarded-For +* X-Forwarded-Proto +* X-Forwarded-Host + +/// + +### Ativar headers encaminhados pelo proxy { #enable-proxy-forwarded-headers } + +Você pode iniciar a CLI do FastAPI com a opção de linha de comando `--forwarded-allow-ips` e informar os endereços IP que devem ser confiáveis para ler esses headers encaminhados. + +Se você definir como `--forwarded-allow-ips="*"`, ele confiará em todos os IPs de entrada. + +Se o seu **servidor** estiver atrás de um **proxy** confiável e somente o proxy falar com ele, isso fará com que ele aceite seja qual for o IP desse **proxy**. + +
+ +```console +$ fastapi run --forwarded-allow-ips="*" + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +### Redirecionamentos com HTTPS { #redirects-with-https } + +Por exemplo, suponha que você defina uma *operação de rota* `/items/`: + +{* ../../docs_src/behind_a_proxy/tutorial001_01_py39.py hl[6] *} + +Se o cliente tentar ir para `/items`, por padrão, ele seria redirecionado para `/items/`. + +Mas antes de definir a opção de linha de comando `--forwarded-allow-ips`, poderia redirecionar para `http://localhost:8000/items/`. + +Mas talvez sua aplicação esteja hospedada em `https://mysuperapp.com`, e o redirecionamento deveria ser para `https://mysuperapp.com/items/`. + +Ao definir `--proxy-headers`, agora o FastAPI conseguirá redirecionar para o local correto. 😎 + +``` +https://mysuperapp.com/items/ +``` + +/// tip | Dica + +Se você quiser saber mais sobre HTTPS, confira o tutorial [Sobre HTTPS](../deployment/https.md){.internal-link target=_blank}. + +/// + +### Como funcionam os headers encaminhados pelo proxy { #how-proxy-forwarded-headers-work } + +Aqui está uma representação visual de como o **proxy** adiciona headers encaminhados entre o cliente e o **servidor da aplicação**: + +```mermaid +sequenceDiagram + participant Client + participant Proxy as Proxy/Load Balancer + participant Server as FastAPI Server + + Client->>Proxy: HTTPS Request
Host: mysuperapp.com
Path: /items + + Note over Proxy: Proxy adds forwarded headers + + Proxy->>Server: HTTP Request
X-Forwarded-For: [client IP]
X-Forwarded-Proto: https
X-Forwarded-Host: mysuperapp.com
Path: /items + + Note over Server: Server interprets headers
(if --forwarded-allow-ips is set) + + Server->>Proxy: HTTP Response
with correct HTTPS URLs + + Proxy->>Client: HTTPS Response +``` + +O **proxy** intercepta a requisição original do cliente e adiciona os headers especiais de encaminhamento (`X-Forwarded-*`) antes de repassar a requisição para o **servidor da aplicação**. + +Esses headers preservam informações sobre a requisição original que, de outra forma, seriam perdidas: + +* X-Forwarded-For: o endereço IP original do cliente +* X-Forwarded-Proto: o protocolo original (`https`) +* X-Forwarded-Host: o host original (`mysuperapp.com`) + +Quando a **CLI do FastAPI** é configurada com `--forwarded-allow-ips`, ela confia nesses headers e os utiliza, por exemplo, para gerar as URLs corretas em redirecionamentos. + +## Proxy com um prefixo de path removido { #proxy-with-a-stripped-path-prefix } + +Você pode ter um proxy que adiciona um prefixo de path à sua aplicação. Nesses casos, você pode usar `root_path` para configurar sua aplicação. -O `root_path` é um mecanismo fornecido pela especificação ASGI (que o FastAPI utiliza, através do Starlette). +O `root_path` é um mecanismo fornecido pela especificação ASGI (na qual o FastAPI é construído, através do Starlette). O `root_path` é usado para lidar com esses casos específicos. E também é usado internamente ao montar sub-aplicações. -## Proxy com um prefixo de caminho removido +Ter um proxy com um prefixo de path removido, nesse caso, significa que você poderia declarar um path em `/app` no seu código, mas então você adiciona uma camada no topo (o proxy) que colocaria sua aplicação **FastAPI** sob um path como `/api/v1`. -Ter um proxy com um prefixo de caminho removido, nesse caso, significa que você poderia declarar um caminho em `/app` no seu código, mas então, você adiciona uma camada no topo (o proxy) que colocaria sua aplicação **FastAPI** sob um caminho como `/api/v1`. - -Nesse caso, o caminho original `/app` seria servido em `/api/v1/app`. +Nesse caso, o path original `/app` seria servido em `/api/v1/app`. Embora todo o seu código esteja escrito assumindo que existe apenas `/app`. -{* ../../docs_src/behind_a_proxy/tutorial001.py hl[6] *} +{* ../../docs_src/behind_a_proxy/tutorial001_py39.py hl[6] *} -E o proxy estaria **"removendo"** o **prefixo do caminho** dinamicamente antes de transmitir a solicitação para o servidor da aplicação (provavelmente Uvicorn via CLI do FastAPI), mantendo sua aplicação convencida de que está sendo servida em `/app`, para que você não precise atualizar todo o seu código para incluir o prefixo `/api/v1`. +E o proxy estaria **"removendo"** o **prefixo de path** dinamicamente antes de transmitir a solicitação para o servidor da aplicação (provavelmente Uvicorn via CLI do FastAPI), mantendo sua aplicação convencida de que está sendo servida em `/app`, para que você não precise atualizar todo o seu código para incluir o prefixo `/api/v1`. Até aqui, tudo funcionaria normalmente. -Mas então, quando você abre a interface de documentação integrada (o frontend), ele esperaria obter o OpenAPI schema em `/openapi.json`, em vez de `/api/v1/openapi.json`. +Mas então, quando você abre a interface de documentação integrada (o frontend), ela esperaria obter o OpenAPI schema em `/openapi.json`, em vez de `/api/v1/openapi.json`. Então, o frontend (que roda no navegador) tentaria acessar `/openapi.json` e não conseguiria obter o OpenAPI schema. -Como temos um proxy com um prefixo de caminho de `/api/v1` para nossa aplicação, o frontend precisa buscar o OpenAPI schema em `/api/v1/openapi.json`. +Como temos um proxy com um prefixo de path de `/api/v1` para nossa aplicação, o frontend precisa buscar o OpenAPI schema em `/api/v1/openapi.json`. ```mermaid graph LR @@ -47,7 +144,7 @@ O IP `0.0.0.0` é comumente usado para significar que o programa escuta em todos /// -A interface de documentação também precisaria do OpenAPI schema para declarar que API `server` está localizado em `/api/v1` (atrás do proxy). Por exemplo: +A interface de documentação também precisaria do OpenAPI schema para declarar que este `server` da API está localizado em `/api/v1` (atrás do proxy). Por exemplo: ```JSON hl_lines="4-8" { @@ -64,16 +161,16 @@ A interface de documentação também precisaria do OpenAPI schema para declarar } ``` -Neste exemplo, o "Proxy" poderia ser algo como **Traefik**. E o servidor seria algo como CLI do FastAPI com **Uvicorn**, executando sua aplicação FastAPI. +Neste exemplo, o "Proxy" poderia ser algo como **Traefik**. E o servidor seria algo como a CLI do FastAPI com **Uvicorn**, executando sua aplicação FastAPI. -### Fornecendo o `root_path` +### Fornecendo o `root_path` { #providing-the-root-path } Para conseguir isso, você pode usar a opção de linha de comando `--root-path` assim:
```console -$ fastapi run main.py --root-path /api/v1 +$ fastapi run main.py --forwarded-allow-ips="*" --root-path /api/v1 INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) ``` @@ -90,20 +187,20 @@ E a opção de linha de comando `--root-path` fornece esse `root_path`. /// -### Verificando o `root_path` atual +### Verificando o `root_path` atual { #checking-the-current-root-path } Você pode obter o `root_path` atual usado pela sua aplicação para cada solicitação, ele faz parte do dicionário `scope` (que faz parte da especificação ASGI). -Aqui estamos incluindo ele na mensagem apenas para fins de demonstração. +Aqui estamos incluindo-o na mensagem apenas para fins de demonstração. -{* ../../docs_src/behind_a_proxy/tutorial001.py hl[8] *} +{* ../../docs_src/behind_a_proxy/tutorial001_py39.py hl[8] *} Então, se você iniciar o Uvicorn com:
```console -$ fastapi run main.py --root-path /api/v1 +$ fastapi run main.py --forwarded-allow-ips="*" --root-path /api/v1 INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) ``` @@ -119,19 +216,19 @@ A resposta seria algo como: } ``` -### Configurando o `root_path` na aplicação FastAPI +### Configurando o `root_path` na aplicação FastAPI { #setting-the-root-path-in-the-fastapi-app } -Alternativamente, se você não tiver uma maneira de fornecer uma opção de linha de comando como `--root-path` ou equivalente, você pode definir o parâmetro `--root-path` ao criar sua aplicação FastAPI: +Alternativamente, se você não tiver uma maneira de fornecer uma opção de linha de comando como `--root-path` ou equivalente, você pode definir o parâmetro `root_path` ao criar sua aplicação FastAPI: -{* ../../docs_src/behind_a_proxy/tutorial002.py hl[3] *} +{* ../../docs_src/behind_a_proxy/tutorial002_py39.py hl[3] *} -Passar o `root_path`h para `FastAPI` seria o equivalente a passar a opção de linha de comando `--root-path` para Uvicorn ou Hypercorn. +Passar o `root_path` para `FastAPI` seria o equivalente a passar a opção de linha de comando `--root-path` para Uvicorn ou Hypercorn. -### Sobre `root_path` +### Sobre `root_path` { #about-root-path } Tenha em mente que o servidor (Uvicorn) não usará esse `root_path` para nada além de passá-lo para a aplicação. -Mas se você acessar com seu navegador http://127.0.0.1:8000/app você verá a resposta normal: +Mas se você acessar com seu navegador http://127.0.0.1:8000/app você verá a resposta normal: ```JSON { @@ -144,19 +241,19 @@ Portanto, ele não esperará ser acessado em `http://127.0.0.1:8000/api/v1/app`. O Uvicorn esperará que o proxy acesse o Uvicorn em `http://127.0.0.1:8000/app`, e então seria responsabilidade do proxy adicionar o prefixo extra `/api/v1` no topo. -## Sobre proxies com um prefixo de caminho removido +## Sobre proxies com um prefixo de path removido { #about-proxies-with-a-stripped-path-prefix } -Tenha em mente que um proxy com prefixo de caminho removido é apenas uma das maneiras de configurá-lo. +Tenha em mente que um proxy com prefixo de path removido é apenas uma das maneiras de configurá-lo. -Provavelmente, em muitos casos, o padrão será que o proxy não tenha um prefixo de caminho removido. +Provavelmente, em muitos casos, o padrão será que o proxy não tenha um prefixo de path removido. -Em um caso como esse (sem um prefixo de caminho removido), o proxy escutaria em algo como `https://myawesomeapp.com`, e então se o navegador acessar `https://myawesomeapp.com/api/v1/app` e seu servidor (por exemplo, Uvicorn) escutar em `http://127.0.0.1:8000` o proxy (sem um prefixo de caminho removido) acessaria o Uvicorn no mesmo caminho: `http://127.0.0.1:8000/api/v1/app`. +Em um caso como esse (sem um prefixo de path removido), o proxy escutaria em algo como `https://myawesomeapp.com`, e então, se o navegador acessar `https://myawesomeapp.com/api/v1/app` e seu servidor (por exemplo, Uvicorn) escutar em `http://127.0.0.1:8000`, o proxy (sem um prefixo de path removido) acessaria o Uvicorn no mesmo path: `http://127.0.0.1:8000/api/v1/app`. -## Testando localmente com Traefik +## Testando localmente com Traefik { #testing-locally-with-traefik } -Você pode facilmente executar o experimento localmente com um prefixo de caminho removido usando Traefik. +Você pode facilmente executar o experimento localmente com um prefixo de path removido usando Traefik. -Faça o download do Traefik., Ele é um único binário e você pode extrair o arquivo compactado e executá-lo diretamente do terminal. +Faça o download do Traefik, ele é um único binário, você pode extrair o arquivo compactado e executá-lo diretamente do terminal. Então, crie um arquivo `traefik.toml` com: @@ -203,9 +300,9 @@ Agora crie esse outro arquivo `routes.toml`: url = "http://127.0.0.1:8000" ``` -Esse arquivo configura o Traefik para usar o prefixo de caminho `/api/v1`. +Esse arquivo configura o Traefik para usar o prefixo de path `/api/v1`. -E então ele redirecionará suas solicitações para seu Uvicorn rodando em `http://127.0.0.1:8000`. +E então o Traefik redirecionará suas solicitações para seu Uvicorn rodando em `http://127.0.0.1:8000`. Agora inicie o Traefik: @@ -224,14 +321,14 @@ E agora inicie sua aplicação, usando a opção `--root-path`:
```console -$ fastapi run main.py --root-path /api/v1 +$ fastapi run main.py --forwarded-allow-ips="*" --root-path /api/v1 INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) ```
-### Verifique as respostas +### Verifique as respostas { #check-the-responses } Agora, se você for ao URL com a porta para o Uvicorn: http://127.0.0.1:8000/app, você verá a resposta normal: @@ -248,7 +345,7 @@ Perceba que, mesmo acessando em `http://127.0.0.1:8000/app`, ele mostra o `root_ /// -E agora abra o URL com a porta para o Traefik, incluindo o prefixo de caminho: http://127.0.0.1:9999/api/v1/app. +E agora abra o URL com a porta para o Traefik, incluindo o prefixo de path: http://127.0.0.1:9999/api/v1/app. Obtemos a mesma resposta: @@ -259,19 +356,19 @@ Obtemos a mesma resposta: } ``` -mas desta vez no URL com o prefixo de caminho fornecido pelo proxy: `/api/v1`. +mas desta vez no URL com o prefixo de path fornecido pelo proxy: `/api/v1`. -Claro, a ideia aqui é que todos acessariam a aplicação através do proxy, então a versão com o prefixo de caminho `/api/v1` é a "correta". +Claro, a ideia aqui é que todos acessariam a aplicação através do proxy, então a versão com o prefixo de path `/api/v1` é a "correta". -E a versão sem o prefixo de caminho (`http://127.0.0.1:8000/app`), fornecida diretamente pelo Uvicorn, seria exclusivamente para o _proxy_ (Traefik) acessá-la. +E a versão sem o prefixo de path (`http://127.0.0.1:8000/app`), fornecida diretamente pelo Uvicorn, seria exclusivamente para o _proxy_ (Traefik) acessá-la. -Isso demonstra como o Proxy (Traefik) usa o prefixo de caminho e como o servidor (Uvicorn) usa o `root_path` da opção `--root-path`. +Isso demonstra como o Proxy (Traefik) usa o prefixo de path e como o servidor (Uvicorn) usa o `root_path` da opção `--root-path`. -### Verifique a interface de documentação +### Verifique a interface de documentação { #check-the-docs-ui } Mas aqui está a parte divertida. ✨ -A maneira "oficial" de acessar a aplicação seria através do proxy com o prefixo de caminho que definimos. Então, como esperaríamos, se você tentar a interface de documentação servida diretamente pelo Uvicorn, sem o prefixo de caminho no URL, ela não funcionará, porque espera ser acessada através do proxy. +A maneira "oficial" de acessar a aplicação seria através do proxy com o prefixo de path que definimos. Então, como esperaríamos, se você tentar a interface de documentação servida diretamente pelo Uvicorn, sem o prefixo de path no URL, ela não funcionará, porque espera ser acessada através do proxy. Você pode verificar em http://127.0.0.1:8000/docs: @@ -287,9 +384,9 @@ Exatamente como queríamos. ✔️ Isso porque o FastAPI usa esse `root_path` para criar o `server` padrão no OpenAPI com o URL fornecido pelo `root_path`. -## Servidores adicionais +## Servidores adicionais { #additional-servers } -/// warning | Aviso +/// warning | Atenção Este é um caso de uso mais avançado. Sinta-se à vontade para pular. @@ -297,13 +394,13 @@ Este é um caso de uso mais avançado. Sinta-se à vontade para pular. Por padrão, o **FastAPI** criará um `server` no OpenAPI schema com o URL para o `root_path`. -Mas você também pode fornecer outros `servers` alternativos, por exemplo, se quiser que a *mesma* interface de documentação interaja com ambientes de staging e produção. +Mas você também pode fornecer outros `servers` alternativos, por exemplo, se quiser que a mesma interface de documentação interaja com ambientes de staging e produção. Se você passar uma lista personalizada de `servers` e houver um `root_path` (porque sua API está atrás de um proxy), o **FastAPI** inserirá um "server" com esse `root_path` no início da lista. Por exemplo: -{* ../../docs_src/behind_a_proxy/tutorial003.py hl[4:7] *} +{* ../../docs_src/behind_a_proxy/tutorial003_py39.py hl[4:7] *} Gerará um OpenAPI schema como: @@ -346,16 +443,24 @@ A interface de documentação interagirá com o servidor que você selecionar. /// -### Desabilitar servidor automático de `root_path` +/// note | Detalhes Técnicos + +A propriedade `servers` na especificação OpenAPI é opcional. + +Se você não especificar o parâmetro `servers` e `root_path` for igual a `/`, a propriedade `servers` no OpenAPI gerado será totalmente omitida por padrão, o que equivale a um único servidor com valor de `url` igual a `/`. + +/// + +### Desabilitar servidor automático de `root_path` { #disable-automatic-server-from-root-path } Se você não quiser que o **FastAPI** inclua um servidor automático usando o `root_path`, você pode usar o parâmetro `root_path_in_servers=False`: -{* ../../docs_src/behind_a_proxy/tutorial004.py hl[9] *} +{* ../../docs_src/behind_a_proxy/tutorial004_py39.py hl[9] *} e então ele não será incluído no OpenAPI schema. -## Montando uma sub-aplicação +## Montando uma sub-aplicação { #mounting-a-sub-application } -Se você precisar montar uma sub-aplicação (como descrito em [Sub Aplicações - Montagens](sub-applications.md){.internal-link target=_blank}) enquanto também usa um proxy com `root_path`, você pode fazer isso normalmente, como esperaria. +Se você precisar montar uma sub-aplicação (como descrito em [Sub-aplicações - Montagens](sub-applications.md){.internal-link target=_blank}) enquanto também usa um proxy com `root_path`, você pode fazer isso normalmente, como esperaria. O FastAPI usará internamente o `root_path` de forma inteligente, então tudo funcionará. ✨ diff --git a/docs/pt/docs/advanced/custom-response.md b/docs/pt/docs/advanced/custom-response.md index a0bcc2b978..5f26390c28 100644 --- a/docs/pt/docs/advanced/custom-response.md +++ b/docs/pt/docs/advanced/custom-response.md @@ -1,4 +1,4 @@ -# Resposta Personalizada - HTML, Stream, File e outras +# Resposta Personalizada - HTML, Stream, File e outras { #custom-response-html-stream-file-others } Por padrão, o **FastAPI** irá retornar respostas utilizando `JSONResponse`. @@ -8,9 +8,9 @@ Mas se você retornar uma `Response` diretamente (ou qualquer subclasse, como `J Mas você também pode declarar a `Response` que você deseja utilizar (e.g. qualquer subclasse de `Response`), em um *decorador de operação de rota* utilizando o parâmetro `response_class`. -Os conteúdos que você retorna em sua *função de operador de rota* serão colocados dentro dessa `Response`. +Os conteúdos que você retorna em sua *função de operação de rota* serão colocados dentro dessa `Response`. -E se a `Response` tiver um media type JSON (`application/json`), como é o caso com `JSONResponse` e `UJSONResponse`, os dados que você retornar serão automaticamente convertidos (e filtrados) com qualquer `response_model` do Pydantic que for declarado em sua *função de operador de rota*. +E se a `Response` tiver um media type JSON (`application/json`), como é o caso com `JSONResponse` e `UJSONResponse`, os dados que você retornar serão automaticamente convertidos (e filtrados) com qualquer `response_model` do Pydantic que for declarado no decorador de operação de rota. /// note | Nota @@ -18,7 +18,7 @@ Se você utilizar uma classe de Resposta sem media type, o FastAPI esperará que /// -## Utilizando `ORJSONResponse` +## Utilizando `ORJSONResponse` { #use-orjsonresponse } Por exemplo, se você precisa bastante de performance, você pode instalar e utilizar o `orjson` e definir a resposta para ser uma `ORJSONResponse`. @@ -30,7 +30,7 @@ Isso ocorre por que, por padrão, o FastAPI irá verificar cada item dentro do d Mas se você tem certeza que o conteúdo que você está retornando é **serializável com JSON**, você pode passá-lo diretamente para a classe de resposta e evitar o trabalho extra que o FastAPI teria ao passar o conteúdo pelo `jsonable_encoder` antes de passar para a classe de resposta. -{* ../../docs_src/custom_response/tutorial001b.py hl[2,7] *} +{* ../../docs_src/custom_response/tutorial001b_py39.py hl[2,7] *} /// info | Informação @@ -48,14 +48,14 @@ A `ORJSONResponse` está disponível apenas no FastAPI, e não no Starlette. /// -## Resposta HTML +## Resposta HTML { #html-response } Para retornar uma resposta com HTML diretamente do **FastAPI**, utilize `HTMLResponse`. * Importe `HTMLResponse` * Passe `HTMLResponse` como o parâmetro de `response_class` do seu *decorador de operação de rota*. -{* ../../docs_src/custom_response/tutorial002.py hl[2,7] *} +{* ../../docs_src/custom_response/tutorial002_py39.py hl[2,7] *} /// info | Informação @@ -67,15 +67,15 @@ E será documentado como tal no OpenAPI. /// -### Retornando uma `Response` +### Retornando uma `Response` { #return-a-response } Como visto em [Retornando uma Resposta Diretamente](response-directly.md){.internal-link target=_blank}, você também pode sobrescrever a resposta diretamente na sua *operação de rota*, ao retornar ela. O mesmo exemplo de antes, retornando uma `HTMLResponse`, poderia parecer com: -{* ../../docs_src/custom_response/tutorial003.py hl[2,7,19] *} +{* ../../docs_src/custom_response/tutorial003_py39.py hl[2,7,19] *} -/// warning | Aviso +/// warning | Atenção Uma `Response` retornada diretamente em sua *função de operação de rota* não será documentada no OpenAPI (por exemplo, o `Content-Type` não será documentado) e não será visível na documentação interativa automática. @@ -87,17 +87,17 @@ Obviamente, o cabeçalho `Content-Type`, o código de status, etc, virão do obj /// -### Documentar no OpenAPI e sobrescrever `Response` +### Documentar no OpenAPI e sobrescrever `Response` { #document-in-openapi-and-override-response } Se você deseja sobrescrever a resposta dentro de uma função, mas ao mesmo tempo documentar o "media type" no OpenAPI, você pode utilizar o parâmetro `response_class` E retornar um objeto `Response`. A `response_class` será usada apenas para documentar o OpenAPI da *operação de rota*, mas sua `Response` será usada como foi definida. -##### Retornando uma `HTMLResponse` diretamente +#### Retornando uma `HTMLResponse` diretamente { #return-an-htmlresponse-directly } Por exemplo, poderia ser algo como: -{* ../../docs_src/custom_response/tutorial004.py hl[7,21,23] *} +{* ../../docs_src/custom_response/tutorial004_py39.py hl[7,21,23] *} Neste exemplo, a função `generate_html_response()` já cria e retorna uma `Response` em vez de retornar o HTML em uma `str`. @@ -107,7 +107,7 @@ Mas se você passasse uma `HTMLResponse` em `response_class` também, o **FastAP -## Respostas disponíveis +## Respostas disponíveis { #available-responses } Aqui estão algumas dos tipos de resposta disponíveis. @@ -121,7 +121,7 @@ O **FastAPI** provê a mesma `starlette.responses` como `fastapi.responses` apen /// -### `Response` +### `Response` { #response } A classe principal de respostas, todas as outras respostas herdam dela. @@ -136,25 +136,25 @@ Ela aceita os seguintes parâmetros: O FastAPI (Starlette, na verdade) irá incluir o cabeçalho Content-Length automaticamente. Ele também irá incluir o cabeçalho Content-Type, baseado no `media_type` e acrescentando uma codificação para tipos textuais. -{* ../../docs_src/response_directly/tutorial002.py hl[1,18] *} +{* ../../docs_src/response_directly/tutorial002_py39.py hl[1,18] *} -### `HTMLResponse` +### `HTMLResponse` { #htmlresponse } Usa algum texto ou sequência de bytes e retorna uma resposta HTML. Como você leu acima. -### `PlainTextResponse` +### `PlainTextResponse` { #plaintextresponse } Usa algum texto ou sequência de bytes para retornar uma resposta de texto não formatado. -{* ../../docs_src/custom_response/tutorial005.py hl[2,7,9] *} +{* ../../docs_src/custom_response/tutorial005_py39.py hl[2,7,9] *} -### `JSONResponse` +### `JSONResponse` { #jsonresponse } Pega alguns dados e retorna uma resposta com codificação `application/json`. É a resposta padrão utilizada no **FastAPI**, como você leu acima. -### `ORJSONResponse` +### `ORJSONResponse` { #orjsonresponse } Uma alternativa mais rápida de resposta JSON utilizando o `orjson`, como você leu acima. @@ -164,7 +164,7 @@ Essa resposta requer a instalação do pacote `orjson`, com o comando `pip insta /// -### `UJSONResponse` +### `UJSONResponse` { #ujsonresponse } Uma alternativa de resposta JSON utilizando a biblioteca `ujson`. @@ -174,13 +174,13 @@ Essa resposta requer a instalação do pacote `ujson`, com o comando `pip instal /// -/// warning | Aviso +/// warning | Atenção `ujson` é menos cauteloso que a implementação nativa do Python na forma que os casos especiais são tratados /// -{* ../../docs_src/custom_response/tutorial001.py hl[2,7] *} +{* ../../docs_src/custom_response/tutorial001_py39.py hl[2,7] *} /// tip | Dica @@ -188,19 +188,19 @@ Essa resposta requer a instalação do pacote `ujson`, com o comando `pip instal /// -### `RedirectResponse` +### `RedirectResponse` { #redirectresponse } Retorna um redirecionamento HTTP. Utiliza o código de status 307 (Redirecionamento Temporário) por padrão. Você pode retornar uma `RedirectResponse` diretamente: -{* ../../docs_src/custom_response/tutorial006.py hl[2,9] *} +{* ../../docs_src/custom_response/tutorial006_py39.py hl[2,9] *} --- Ou você pode utilizá-la no parâmetro `response_class`: -{* ../../docs_src/custom_response/tutorial006b.py hl[2,7,9] *} +{* ../../docs_src/custom_response/tutorial006b_py39.py hl[2,7,9] *} Se você fizer isso, então você pode retornar a URL diretamente da sua *função de operação de rota* @@ -210,28 +210,26 @@ Neste caso, o `status_code` utilizada será o padrão de `RedirectResponse`, que Você também pode utilizar o parâmetro `status_code` combinado com o parâmetro `response_class`: -{* ../../docs_src/custom_response/tutorial006c.py hl[2,7,9] *} +{* ../../docs_src/custom_response/tutorial006c_py39.py hl[2,7,9] *} -### `StreamingResponse` +### `StreamingResponse` { #streamingresponse } -Recebe uma gerador assíncrono ou um gerador/iterador comum e retorna o corpo da requisição continuamente (stream). +Recebe um gerador assíncrono ou um gerador/iterador comum e retorna o corpo da resposta de forma contínua (stream). -{* ../../docs_src/custom_response/tutorial007.py hl[2,14] *} +{* ../../docs_src/custom_response/tutorial007_py39.py hl[2,14] *} -#### Utilizando `StreamingResponse` com objetos semelhantes a arquivos +#### Utilizando `StreamingResponse` com objetos semelhantes a arquivos { #using-streamingresponse-with-file-like-objects } -Se você tiver um objeto semelhante a um arquivo (e.g. o objeto retornado por `open()`), você pode criar uma função geradora para iterar sobre esse objeto. +Se você tiver um objeto semelhante a um arquivo (e.g. o objeto retornado por `open()`), você pode criar uma função geradora para iterar sobre esse objeto. Dessa forma, você não precisa ler todo o arquivo na memória primeiro, e você pode passar essa função geradora para `StreamingResponse` e retorná-la. Isso inclui muitas bibliotecas que interagem com armazenamento em nuvem, processamento de vídeos, entre outras. -```{ .python .annotate hl_lines="2 10-12 14" } -{!../../docs_src/custom_response/tutorial008.py!} -``` +{* ../../docs_src/custom_response/tutorial008_py39.py hl[2,10:12,14] *} 1. Essa é a função geradora. É definida como "função geradora" porque contém declarações `yield` nela. -2. Ao utilizar o bloco `with`, nós garantimos que o objeto semelhante a um arquivo é fechado após a função geradora ser finalizada. Isto é, após a resposta terminar de ser enivada. +2. Ao utilizar o bloco `with`, nós garantimos que o objeto semelhante a um arquivo é fechado após a função geradora ser finalizada. Isto é, após a resposta terminar de ser enviada. 3. Essa declaração `yield from` informa a função para iterar sobre essa coisa nomeada de `file_like`. E então, para cada parte iterada, fornece essa parte como se viesse dessa função geradora (`iterfile`). Então, é uma função geradora que transfere o trabalho de "geração" para alguma outra coisa interna. @@ -244,10 +242,10 @@ Perceba que aqui estamos utilizando o `open()` da biblioteca padrão que não su /// -### `FileResponse` +### `FileResponse` { #fileresponse } Envia um arquivo de forma assíncrona e contínua (stream). -* + Recebe um conjunto de argumentos do construtor diferente dos outros tipos de resposta: * `path` - O caminho do arquivo que será transmitido @@ -257,15 +255,15 @@ Recebe um conjunto de argumentos do construtor diferente dos outros tipos de res Respostas de Arquivos incluem o tamanho do arquivo, data da última modificação e ETags apropriados, nos cabeçalhos `Content-Length`, `Last-Modified` e `ETag`, respectivamente. -{* ../../docs_src/custom_response/tutorial009.py hl[2,10] *} +{* ../../docs_src/custom_response/tutorial009_py39.py hl[2,10] *} Você também pode usar o parâmetro `response_class`: -{* ../../docs_src/custom_response/tutorial009b.py hl[2,8,10] *} +{* ../../docs_src/custom_response/tutorial009b_py39.py hl[2,8,10] *} Nesse caso, você pode retornar o caminho do arquivo diretamente da sua *função de operação de rota*. -## Classe de resposta personalizada +## Classe de resposta personalizada { #custom-response-class } Você pode criar sua própria classe de resposta, herdando de `Response` e usando essa nova classe. @@ -273,9 +271,9 @@ Por exemplo, vamos supor que você queira utilizar o `dataclasses` da mesma forma: -{* ../../docs_src/dataclasses/tutorial001.py hl[1,7:12,19:20] *} +{* ../../docs_src/dataclasses_/tutorial001_py310.py hl[1,6:11,18:19] *} Isso ainda é suportado graças ao **Pydantic**, pois ele tem suporte interno para `dataclasses`. @@ -28,11 +28,11 @@ Mas se você tem um monte de dataclasses por aí, este é um truque legal para u /// -## Dataclasses em `response_model` +## Dataclasses em `response_model` { #dataclasses-in-response-model } Você também pode usar `dataclasses` no parâmetro `response_model`: -{* ../../docs_src/dataclasses/tutorial002.py hl[1,7:13,19] *} +{* ../../docs_src/dataclasses_/tutorial002_py310.py hl[1,6:12,18] *} A dataclass será automaticamente convertida para uma dataclass Pydantic. @@ -40,7 +40,7 @@ Dessa forma, seu esquema aparecerá na interface de documentação da API: -## Dataclasses em Estruturas de Dados Aninhadas +## Dataclasses em Estruturas de Dados Aninhadas { #dataclasses-in-nested-data-structures } Você também pode combinar `dataclasses` com outras anotações de tipo para criar estruturas de dados aninhadas. @@ -48,9 +48,7 @@ Em alguns casos, você ainda pode ter que usar a versão do Pydantic das `datacl Nesse caso, você pode simplesmente trocar as `dataclasses` padrão por `pydantic.dataclasses`, que é um substituto direto: -```{ .python .annotate hl_lines="1 5 8-11 14-17 23-25 28" } -{!../../docs_src/dataclasses/tutorial003.py!} -``` +{* ../../docs_src/dataclasses_/tutorial003_py310.py hl[1,4,7:10,13:16,22:24,27] *} 1. Ainda importamos `field` das `dataclasses` padrão. @@ -86,12 +84,12 @@ Você pode combinar `dataclasses` com outras anotações de tipo em muitas combi Confira as dicas de anotação no código acima para ver mais detalhes específicos. -## Saiba Mais +## Saiba Mais { #learn-more } Você também pode combinar `dataclasses` com outros modelos Pydantic, herdar deles, incluí-los em seus próprios modelos, etc. Para saber mais, confira a documentação do Pydantic sobre dataclasses. -## Versão +## Versão { #version } Isso está disponível desde a versão `0.67.0` do FastAPI. 🔖 diff --git a/docs/pt/docs/advanced/events.md b/docs/pt/docs/advanced/events.md index 2d38e0899c..8cdc358289 100644 --- a/docs/pt/docs/advanced/events.md +++ b/docs/pt/docs/advanced/events.md @@ -1,67 +1,66 @@ -# Eventos de vida útil +# Eventos de lifespan { #lifespan-events } -Você pode definir a lógica (código) que poderia ser executada antes da aplicação **inicializar**. Isso significa que esse código será executado **uma vez**, **antes** da aplicação **começar a receber requisições**. +Você pode definir a lógica (código) que deve ser executada antes da aplicação **inicializar**. Isso significa que esse código será executado **uma vez**, **antes** de a aplicação **começar a receber requisições**. -Do mesmo modo, você pode definir a lógica (código) que será executada quando a aplicação estiver sendo **encerrada**. Nesse caso, este código será executado **uma vez**, **depois** de ter possivelmente tratado **várias requisições**. +Da mesma forma, você pode definir a lógica (código) que deve ser executada quando a aplicação estiver **encerrando**. Nesse caso, esse código será executado **uma vez**, **depois** de possivelmente ter tratado **várias requisições**. -Por conta desse código ser executado antes da aplicação **começar** a receber requisições, e logo após **terminar** de lidar com as requisições, ele cobre toda a **vida útil** (_lifespan_) da aplicação (o termo "vida útil" será importante em um segundo 😉). +Como esse código é executado antes de a aplicação **começar** a receber requisições e logo depois que ela **termina** de lidar com as requisições, ele cobre todo o **lifespan** da aplicação (a palavra "lifespan" será importante em um segundo 😉). -Pode ser muito útil para configurar **recursos** que você precisa usar por toda aplicação, e que são **compartilhados** entre as requisições, e/ou que você precisa **limpar** depois. Por exemplo, o pool de uma conexão com o banco de dados ou carregamento de um modelo compartilhado de aprendizado de máquina (_machine learning_). +Isso pode ser muito útil para configurar **recursos** que você precisa usar por toda a aplicação, e que são **compartilhados** entre as requisições e/ou que você precisa **limpar** depois. Por exemplo, um pool de conexões com o banco de dados ou o carregamento de um modelo de machine learning compartilhado. -## Caso de uso +## Caso de uso { #use-case } -Vamos iniciar com um exemplo de **caso de uso** e então ver como resolvê-lo com isso. +Vamos começar com um exemplo de **caso de uso** e então ver como resolvê-lo com isso. -Vamos imaginar que você tem alguns **modelos de _machine learning_** que deseja usar para lidar com as requisições. 🤖 +Vamos imaginar que você tem alguns **modelos de machine learning** que deseja usar para lidar com as requisições. 🤖 -Os mesmos modelos são compartilhados entre as requisições, então não é um modelo por requisição, ou um por usuário ou algo parecido. +Os mesmos modelos são compartilhados entre as requisições, então não é um modelo por requisição, ou um por usuário, ou algo parecido. -Vamos imaginar que o carregamento do modelo pode **demorar bastante tempo**, porque ele tem que ler muitos **dados do disco**. Então você não quer fazer isso a cada requisição. +Vamos imaginar que o carregamento do modelo pode **demorar bastante tempo**, porque ele precisa ler muitos **dados do disco**. Então você não quer fazer isso a cada requisição. -Você poderia carregá-lo no nível mais alto do módulo/arquivo, mas isso também poderia significaria **carregar o modelo** mesmo se você estiver executando um simples teste automatizado, então esse teste poderia ser **lento** porque teria que esperar o carregamento do modelo antes de ser capaz de executar uma parte independente do código. +Você poderia carregá-lo no nível mais alto do módulo/arquivo, mas isso também significaria **carregar o modelo** mesmo se você estivesse executando um teste automatizado simples; então esse teste poderia ser **lento** porque teria que esperar o carregamento do modelo antes de conseguir executar uma parte independente do código. +É isso que vamos resolver: vamos carregar o modelo antes de as requisições serem tratadas, mas apenas um pouco antes de a aplicação começar a receber requisições, não enquanto o código estiver sendo carregado. -Isso é que nós iremos resolver, vamos carregar o modelo antes das requisições serem manuseadas, mas apenas um pouco antes da aplicação começar a receber requisições, não enquanto o código estiver sendo carregado. +## Lifespan { #lifespan } -## Vida útil (_Lifespan_) +Você pode definir essa lógica de *inicialização* e *encerramento* usando o parâmetro `lifespan` da aplicação `FastAPI`, e um "gerenciador de contexto" (vou mostrar o que é isso em um segundo). -Você pode definir essa lógica de *inicialização* e *encerramento* usando os parâmetros de `lifespan` da aplicação `FastAPI`, e um "gerenciador de contexto" (te mostrarei o que é isso a seguir). +Vamos começar com um exemplo e depois ver em detalhes. -Vamos iniciar com um exemplo e ver isso detalhadamente. +Nós criamos uma função assíncrona `lifespan()` com `yield` assim: -Nós criamos uma função assíncrona chamada `lifespan()` com `yield` como este: +{* ../../docs_src/events/tutorial003_py39.py hl[16,19] *} -{* ../../docs_src/events/tutorial003.py hl[16,19] *} +Aqui estamos simulando a operação de *inicialização* custosa de carregar o modelo, colocando a (falsa) função do modelo no dicionário com modelos de machine learning antes do `yield`. Esse código será executado **antes** de a aplicação **começar a receber requisições**, durante a *inicialização*. -Aqui nós estamos simulando a *inicialização* custosa do carregamento do modelo colocando a (falsa) função de modelo no dicionário com modelos de _machine learning_ antes do `yield`. Este código será executado **antes** da aplicação **começar a receber requisições**, durante a *inicialização*. - -E então, logo após o `yield`, descarregaremos o modelo. Esse código será executado **após** a aplicação **terminar de lidar com as requisições**, pouco antes do *encerramento*. Isso poderia, por exemplo, liberar recursos como memória ou GPU. +E então, logo após o `yield`, descarregamos o modelo. Esse código será executado **depois** de a aplicação **terminar de lidar com as requisições**, pouco antes do *encerramento*. Isso poderia, por exemplo, liberar recursos como memória ou uma GPU. /// tip | Dica O `shutdown` aconteceria quando você estivesse **encerrando** a aplicação. -Talvez você precise inicializar uma nova versão, ou apenas cansou de executá-la. 🤷 +Talvez você precise iniciar uma nova versão, ou apenas cansou de executá-la. 🤷 /// -### Função _lifespan_ +### Função lifespan { #lifespan-function } -A primeira coisa a notar, é que estamos definindo uma função assíncrona com `yield`. Isso é muito semelhante à Dependências com `yield`. +A primeira coisa a notar é que estamos definindo uma função assíncrona com `yield`. Isso é muito semelhante a Dependências com `yield`. -{* ../../docs_src/events/tutorial003.py hl[14:19] *} +{* ../../docs_src/events/tutorial003_py39.py hl[14:19] *} -A primeira parte da função, antes do `yield`, será executada **antes** da aplicação inicializar. +A primeira parte da função, antes do `yield`, será executada **antes** de a aplicação iniciar. -E a parte posterior do `yield` irá executar **após** a aplicação ser encerrada. +E a parte posterior ao `yield` será executada **depois** de a aplicação ter terminado. -### Gerenciador de Contexto Assíncrono +### Gerenciador de contexto assíncrono { #async-context-manager } Se você verificar, a função está decorada com um `@asynccontextmanager`. -Que converte a função em algo chamado de "**Gerenciador de Contexto Assíncrono**". +Isso converte a função em algo chamado "**gerenciador de contexto assíncrono**". -{* ../../docs_src/events/tutorial003.py hl[1,13] *} +{* ../../docs_src/events/tutorial003_py39.py hl[1,13] *} Um **gerenciador de contexto** em Python é algo que você pode usar em uma declaração `with`, por exemplo, `open()` pode ser usado como um gerenciador de contexto: @@ -70,97 +69,97 @@ with open("file.txt") as file: file.read() ``` -Nas versões mais recentes de Python, há também um **gerenciador de contexto assíncrono**. Você o usaria com `async with`: +Em versões mais recentes do Python, há também um **gerenciador de contexto assíncrono**. Você o usaria com `async with`: ```Python async with lifespan(app): await do_stuff() ``` -Quando você cria um gerenciador de contexto ou um gerenciador de contexto assíncrono como mencionado acima, o que ele faz é que, antes de entrar no bloco `with`, ele irá executar o código anterior ao `yield`, e depois de sair do bloco `with`, ele irá executar o código depois do `yield`. +Quando você cria um gerenciador de contexto ou um gerenciador de contexto assíncrono como acima, o que ele faz é: antes de entrar no bloco `with`, ele executa o código antes do `yield`, e após sair do bloco `with`, ele executa o código depois do `yield`. -No nosso exemplo de código acima, nós não usamos ele diretamente, mas nós passamos para o FastAPI para ele usá-lo. +No nosso exemplo de código acima, não o usamos diretamente, mas passamos para o FastAPI para que ele o use. -O parâmetro `lifespan` da aplicação `FastAPI` usa um **Gerenciador de Contexto Assíncrono**, então nós podemos passar nosso novo gerenciador de contexto assíncrono do `lifespan` para ele. +O parâmetro `lifespan` da aplicação `FastAPI` aceita um **gerenciador de contexto assíncrono**, então podemos passar para ele nosso novo gerenciador de contexto assíncrono `lifespan`. -{* ../../docs_src/events/tutorial003.py hl[22] *} +{* ../../docs_src/events/tutorial003_py39.py hl[22] *} -## Eventos alternativos (deprecados) +## Eventos alternativos (descontinuados) { #alternative-events-deprecated } -/// warning | Aviso +/// warning | Atenção -A maneira recomendada para lidar com a *inicialização* e o *encerramento* é usando o parâmetro `lifespan` da aplicação `FastAPI` como descrito acima. +A forma recomendada de lidar com a *inicialização* e o *encerramento* é usando o parâmetro `lifespan` da aplicação `FastAPI`, como descrito acima. Se você fornecer um parâmetro `lifespan`, os manipuladores de eventos `startup` e `shutdown` não serão mais chamados. É tudo `lifespan` ou tudo por eventos, não ambos. -Você provavelmente pode pular essa parte. +Você provavelmente pode pular esta parte. /// -Existe uma forma alternativa para definir a execução dessa lógica durante *inicialização* e durante *encerramento*. +Existe uma forma alternativa de definir essa lógica para ser executada durante a *inicialização* e durante o *encerramento*. -Você pode definir manipuladores de eventos (funções) que precisam ser executadas antes da aplicação inicializar, ou quando a aplicação estiver encerrando. +Você pode definir manipuladores de eventos (funções) que precisam ser executados antes de a aplicação iniciar ou quando a aplicação estiver encerrando. Essas funções podem ser declaradas com `async def` ou `def` normal. -### Evento `startup` +### Evento `startup` { #startup-event } -Para adicionar uma função que deve rodar antes da aplicação iniciar, declare-a com o evento `"startup"`: +Para adicionar uma função que deve rodar antes de a aplicação iniciar, declare-a com o evento `"startup"`: -{* ../../docs_src/events/tutorial001.py hl[8] *} +{* ../../docs_src/events/tutorial001_py39.py hl[8] *} -Nesse caso, a função de manipulação de evento `startup` irá inicializar os itens do "banco de dados" (só um `dict`) com alguns valores. +Nesse caso, a função de manipulador do evento `startup` inicializará os itens do "banco de dados" (apenas um `dict`) com alguns valores. -Você pode adicionar mais que uma função de manipulação de evento. +Você pode adicionar mais de uma função de manipulador de eventos. -E sua aplicação não irá começar a receber requisições até que todos os manipuladores de eventos de `startup` sejam concluídos. +E sua aplicação não começará a receber requisições até que todos os manipuladores de eventos `startup` sejam concluídos. -### Evento `shutdown` +### Evento `shutdown` { #shutdown-event } -Para adicionar uma função que deve ser executada quando a aplicação estiver encerrando, declare ela com o evento `"shutdown"`: +Para adicionar uma função que deve ser executada quando a aplicação estiver encerrando, declare-a com o evento `"shutdown"`: -{* ../../docs_src/events/tutorial002.py hl[6] *} +{* ../../docs_src/events/tutorial002_py39.py hl[6] *} -Aqui, a função de manipulação de evento `shutdown` irá escrever uma linha de texto `"Application shutdown"` no arquivo `log.txt`. +Aqui, a função de manipulador do evento `shutdown` escreverá uma linha de texto `"Application shutdown"` no arquivo `log.txt`. /// info | Informação -Na função `open()`, o `mode="a"` significa "acrescentar", então, a linha irá ser adicionada depois de qualquer coisa que esteja naquele arquivo, sem sobrescrever o conteúdo anterior. +Na função `open()`, o `mode="a"` significa "acrescentar", então a linha será adicionada depois do que já estiver naquele arquivo, sem sobrescrever o conteúdo anterior. /// /// tip | Dica -Perceba que nesse caso nós estamos usando a função padrão do Python `open()` que interage com um arquivo. +Perceba que, nesse caso, estamos usando a função padrão do Python `open()` que interage com um arquivo. -Então, isso envolve I/O (input/output), que exige "esperar" que coisas sejam escritas em disco. +Então, isso envolve I/O (input/output), que requer "esperar" que as coisas sejam escritas em disco. Mas `open()` não usa `async` e `await`. -Então, nós declaramos uma função de manipulação de evento com o padrão `def` ao invés de `async def`. +Assim, declaramos a função de manipulador de evento com `def` padrão em vez de `async def`. /// -### `startup` e `shutdown` juntos +### `startup` e `shutdown` juntos { #startup-and-shutdown-together } -Há uma grande chance que a lógica para sua *inicialização* e *encerramento* esteja conectada, você pode querer iniciar alguma coisa e então finalizá-la, adquirir um recurso e então liberá-lo, etc. +Há uma grande chance de que a lógica para sua *inicialização* e *encerramento* esteja conectada, você pode querer iniciar alguma coisa e então finalizá-la, adquirir um recurso e então liberá-lo, etc. -Fazendo isso em funções separadas que não compartilham lógica ou variáveis entre elas é mais difícil já que você precisa armazenar os valores em variáveis globais ou truques parecidos. +Fazer isso em funções separadas que não compartilham lógica ou variáveis entre si é mais difícil, pois você precisaria armazenar valores em variáveis globais ou truques semelhantes. -Por causa disso, agora é recomendado em vez disso usar o `lifespan` como explicado acima. +Por causa disso, agora é recomendado usar o `lifespan`, como explicado acima. -## Detalhes técnicos +## Detalhes técnicos { #technical-details } -Só um detalhe técnico para nerds curiosos. 🤓 +Apenas um detalhe técnico para nerds curiosos. 🤓 -Por baixo, na especificação técnica ASGI, essa é a parte do Protocolo Lifespan, e define eventos chamados `startup` e `shutdown`. +Por baixo, na especificação técnica do ASGI, isso é parte do Protocolo Lifespan, e define eventos chamados `startup` e `shutdown`. /// info | Informação -Você pode ler mais sobre o manipulador `lifespan` do Starlette na Documentação do Lifespan Starlette. +Você pode ler mais sobre os manipuladores de `lifespan` do Starlette na Documentação do Lifespan do Starlette. -Incluindo como manipular estado do lifespan que pode ser usado em outras áreas do seu código. +Incluindo como lidar com estado do lifespan que pode ser usado em outras áreas do seu código. /// -## Sub Aplicações +## Sub Aplicações { #sub-applications } -🚨 Tenha em mente que esses eventos de lifespan (de inicialização e desligamento) irão somente ser executados para a aplicação principal, não para [Sub Aplicações - Montagem](sub-applications.md){.internal-link target=_blank}. +🚨 Tenha em mente que esses eventos de lifespan (inicialização e encerramento) serão executados apenas para a aplicação principal, não para [Sub Aplicações - Montagem](sub-applications.md){.internal-link target=_blank}. diff --git a/docs/pt/docs/advanced/generate-clients.md b/docs/pt/docs/advanced/generate-clients.md index dc6b29511a..5134bc7cb5 100644 --- a/docs/pt/docs/advanced/generate-clients.md +++ b/docs/pt/docs/advanced/generate-clients.md @@ -1,115 +1,76 @@ -# Generate Clients +# Gerando SDKs { #generating-sdks } -Como o **FastAPI** é baseado na especificação **OpenAPI**, você obtém compatibilidade automática com muitas ferramentas, incluindo a documentação automática da API (fornecida pelo Swagger UI). +Como o **FastAPI** é baseado na especificação **OpenAPI**, suas APIs podem ser descritas em um formato padrão que muitas ferramentas entendem. -Uma vantagem particular que nem sempre é óbvia é que você pode **gerar clientes** (às vezes chamados de **SDKs**) para a sua API, para muitas **linguagens de programação** diferentes. +Isso facilita gerar **documentação** atualizada, bibliotecas clientes (**SDKs**) em várias linguagens e **testes** ou **fluxos de trabalho de automação** que permanecem em sincronia com o seu código. -## Geradores de Clientes OpenAPI +Neste guia, você aprenderá como gerar um **SDK em TypeScript** para o seu backend FastAPI. -Existem muitas ferramentas para gerar clientes a partir do **OpenAPI**. +## Geradores de SDK de código aberto { #open-source-sdk-generators } -Uma ferramenta comum é o OpenAPI Generator. +Uma opção versátil é o OpenAPI Generator, que suporta **muitas linguagens de programação** e pode gerar SDKs a partir da sua especificação OpenAPI. -Se voce está construindo um **frontend**, uma alternativa muito interessante é o openapi-ts. +Para **clientes TypeScript**, o Hey API é uma solução feita sob medida, oferecendo uma experiência otimizada para o ecossistema TypeScript. -## Geradores de Clientes e SDKs - Patrocinadores +Você pode descobrir mais geradores de SDK em OpenAPI.Tools. -Existem também alguns geradores de clientes e SDKs baseados na OpenAPI (FastAPI) **patrocinados por empresas**, em alguns casos eles podem oferecer **recursos adicionais** além de SDKs/clientes gerados de alta qualidade. +/// tip | Dica -Alguns deles também ✨ [**patrocinam o FastAPI**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨, isso garante o **desenvolvimento** contínuo e saudável do FastAPI e seu **ecossistema**. +O FastAPI gera automaticamente especificações **OpenAPI 3.1**, então qualquer ferramenta que você usar deve suportar essa versão. -E isso mostra o verdadeiro compromisso deles com o FastAPI e sua **comunidade** (você), pois eles não apenas querem fornecer um **bom serviço**, mas também querem garantir que você tenha um **framework bom e saudável**, o FastAPI. 🙇 +/// + +## Geradores de SDK dos patrocinadores do FastAPI { #sdk-generators-from-fastapi-sponsors } + +Esta seção destaca soluções **financiadas por investimento** e **com suporte de empresas** que patrocinam o FastAPI. Esses produtos fornecem **funcionalidades adicionais** e **integrações** além de SDKs gerados com alta qualidade. + +Ao ✨ [**patrocinar o FastAPI**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨, essas empresas ajudam a garantir que o framework e seu **ecossistema** continuem saudáveis e **sustentáveis**. + +O patrocínio também demonstra um forte compromisso com a **comunidade** FastAPI (você), mostrando que elas se importam não apenas em oferecer um **ótimo serviço**, mas também em apoiar um **framework robusto e próspero**, o FastAPI. 🙇 Por exemplo, você pode querer experimentar: * Speakeasy -* Stainless -* liblab +* Stainless +* liblab -Existem também várias outras empresas que oferecem serviços semelhantes que você pode pesquisar e encontrar online. 🤓 +Algumas dessas soluções também podem ser open source ou oferecer planos gratuitos, para que você possa testá-las sem compromisso financeiro. Outros geradores comerciais de SDK estão disponíveis e podem ser encontrados online. 🤓 -## Gerar um Cliente Frontend TypeScript +## Crie um SDK em TypeScript { #create-a-typescript-sdk } -Vamos começar com um aplicativo **FastAPI** simples: +Vamos começar com uma aplicação FastAPI simples: {* ../../docs_src/generate_clients/tutorial001_py39.py hl[7:9,12:13,16:17,21] *} Note que as *operações de rota* definem os modelos que usam para o corpo da requisição e o corpo da resposta, usando os modelos `Item` e `ResponseMessage`. -### Documentação da API +### Documentação da API { #api-docs } -Se você acessar a documentação da API, verá que ela tem os **schemas** para os dados a serem enviados nas requisições e recebidos nas respostas: +Se você for para `/docs`, verá que ela tem os **schemas** para os dados a serem enviados nas requisições e recebidos nas respostas: Você pode ver esses schemas porque eles foram declarados com os modelos no app. -Essas informações estão disponíveis no **OpenAPI schema** do app e são mostradas na documentação da API (pelo Swagger UI). +Essas informações estão disponíveis no **schema OpenAPI** do app e são mostradas na documentação da API. E essas mesmas informações dos modelos que estão incluídas no OpenAPI são o que pode ser usado para **gerar o código do cliente**. -### Gerar um Cliente TypeScript +### Hey API { #hey-api } -Agora que temos o app com os modelos, podemos gerar o código do cliente para o frontend. +Depois que tivermos uma aplicação FastAPI com os modelos, podemos usar o Hey API para gerar um cliente TypeScript. A forma mais rápida é via npx. -#### Instalar o `openapi-ts` - -Você pode instalar o `openapi-ts` no seu código frontend com: - -
- -```console -$ npm install @hey-api/openapi-ts --save-dev - ----> 100% +```sh +npx @hey-api/openapi-ts -i http://localhost:8000/openapi.json -o src/client ``` -
+Isso gerará um SDK TypeScript em `./src/client`. -#### Gerar o Código do Cliente +Você pode aprender como instalar `@hey-api/openapi-ts` e ler sobre o resultado gerado no site deles. -Para gerar o código do cliente, você pode usar a aplicação de linha de comando `openapi-ts` que agora está instalada. +### Usando o SDK { #using-the-sdk } -Como ela está instalada no projeto local, você provavelmente não conseguiria chamar esse comando diretamente, mas você o colocaria no seu arquivo `package.json`. - -Poderia ser assim: - -```JSON hl_lines="7" -{ - "name": "frontend-app", - "version": "1.0.0", - "description": "", - "main": "index.js", - "scripts": { - "generate-client": "openapi-ts --input http://localhost:8000/openapi.json --output ./src/client --client axios" - }, - "author": "", - "license": "", - "devDependencies": { - "@hey-api/openapi-ts": "^0.27.38", - "typescript": "^4.6.2" - } -} -``` - -Depois de ter esse script NPM `generate-client` lá, você pode executá-lo com: - -
- -```console -$ npm run generate-client - -frontend-app@1.0.0 generate-client /home/user/code/frontend-app -> openapi-ts --input http://localhost:8000/openapi.json --output ./src/client --client axios -``` - -
- -Esse comando gerará o código em `./src/client` e usará o `axios` (a biblioteca HTTP frontend) internamente. - -### Experimente o Código do Cliente - -Agora você pode importar e usar o código do cliente, ele poderia ser assim, observe que você obtém preenchimento automático para os métodos: +Agora você pode importar e usar o código do cliente. Poderia ser assim, observe que você obtém preenchimento automático para os métodos: @@ -119,7 +80,7 @@ Você também obterá preenchimento automático para o corpo a ser enviado: /// tip | Dica -Observe o preenchimento automático para `name` e `price`, que foi definido no aplicativo FastAPI, no modelo `Item`. +Observe o preenchimento automático para `name` e `price`, que foi definido na aplicação FastAPI, no modelo `Item`. /// @@ -131,17 +92,17 @@ O objeto de resposta também terá preenchimento automático: -## App FastAPI com Tags +## Aplicação FastAPI com Tags { #fastapi-app-with-tags } -Em muitos casos seu app FastAPI será maior, e você provavelmente usará tags para separar diferentes grupos de *operações de rota*. +Em muitos casos, sua aplicação FastAPI será maior, e você provavelmente usará tags para separar diferentes grupos de *operações de rota*. Por exemplo, você poderia ter uma seção para **items** e outra seção para **users**, e elas poderiam ser separadas por tags: {* ../../docs_src/generate_clients/tutorial002_py39.py hl[21,26,34] *} -### Gerar um Cliente TypeScript com Tags +### Gere um cliente TypeScript com Tags { #generate-a-typescript-client-with-tags } -Se você gerar um cliente para um app FastAPI usando tags, normalmente também separará o código do cliente com base nas tags. +Se você gerar um cliente para uma aplicação FastAPI usando tags, normalmente também separará o código do cliente com base nas tags. Dessa forma, você poderá ter as coisas ordenadas e agrupadas corretamente para o código do cliente: @@ -152,33 +113,33 @@ Nesse caso você tem: * `ItemsService` * `UsersService` -### Nomes dos Métodos do Cliente +### Nomes dos métodos do cliente { #client-method-names } -Agora os nomes dos métodos gerados como `createItemItemsPost` não parecem muito "limpos": +Agora os nomes dos métodos gerados como `createItemItemsPost` não parecem muito “limpos”: ```TypeScript ItemsService.createItemItemsPost({name: "Plumbus", price: 5}) ``` -...isto ocorre porque o gerador de clientes usa o **operation ID** interno do OpenAPI para cada *operação de rota*. +...isso ocorre porque o gerador de clientes usa o **ID de operação** interno do OpenAPI para cada *operação de rota*. -O OpenAPI exige que cada operation ID seja único em todas as *operações de rota*, então o FastAPI usa o **nome da função**, o **caminho** e o **método/operacao HTTP** para gerar esse operation ID, porque dessa forma ele pode garantir que os operation IDs sejam únicos. +O OpenAPI exige que cada ID de operação seja único em todas as *operações de rota*, então o FastAPI usa o **nome da função**, o **path** e o **método/operação HTTP** para gerar esse ID de operação, porque dessa forma ele pode garantir que os IDs de operação sejam únicos. Mas eu vou te mostrar como melhorar isso a seguir. 🤓 -### IDs de Operação Personalizados e Melhores Nomes de Método +## IDs de operação personalizados e nomes de métodos melhores { #custom-operation-ids-and-better-method-names } Você pode **modificar** a maneira como esses IDs de operação são **gerados** para torná-los mais simples e ter **nomes de método mais simples** nos clientes. Neste caso, você terá que garantir que cada ID de operação seja **único** de alguma outra maneira. -Por exemplo, você poderia garantir que cada *operação de rota* tenha uma tag, e então gerar o ID da operação com base na **tag** e no **nome** da *operação de rota* (o nome da função). +Por exemplo, você poderia garantir que cada *operação de rota* tenha uma tag, e então gerar o ID de operação com base na **tag** e no **nome** da *operação de rota* (o nome da função). -### Função Personalizada para Gerar IDs de Operação Únicos +### Função personalizada para gerar IDs exclusivos { #custom-generate-unique-id-function } -O FastAPI usa um **ID único** para cada *operação de rota*, ele é usado para o **ID da operação** e também para os nomes de quaisquer modelos personalizados necessários, para requisições ou respostas. +O FastAPI usa um **ID exclusivo** para cada *operação de rota*, ele é usado para o **ID de operação** e também para os nomes de quaisquer modelos personalizados necessários, para requisições ou respostas. -Você pode personalizar essa função. Ela recebe uma `APIRoute` e gera uma string. +Você pode personalizar essa função. Ela recebe uma `APIRoute` e retorna uma string. Por exemplo, aqui está usando a primeira tag (você provavelmente terá apenas uma tag) e o nome da *operação de rota* (o nome da função). @@ -186,15 +147,15 @@ Você pode então passar essa função personalizada para o **FastAPI** como o p {* ../../docs_src/generate_clients/tutorial003_py39.py hl[6:7,10] *} -### Gerar um Cliente TypeScript com IDs de Operação Personalizados +### Gere um cliente TypeScript com IDs de operação personalizados { #generate-a-typescript-client-with-custom-operation-ids } Agora, se você gerar o cliente novamente, verá que ele tem os nomes dos métodos melhorados: -Como você pode ver, os nomes dos métodos agora têm a tag e, em seguida, o nome da função. Agora eles não incluem informações do caminho da URL e da operação HTTP. +Como você pode ver, os nomes dos métodos agora têm a tag e, em seguida, o nome da função. Agora eles não incluem informações do path da URL e da operação HTTP. -### Pré-processar a Especificação OpenAPI para o Gerador de Clientes +### Pré-processar a especificação OpenAPI para o gerador de clientes { #preprocess-the-openapi-specification-for-the-client-generator } O código gerado ainda tem algumas **informações duplicadas**. @@ -202,11 +163,11 @@ Nós já sabemos que esse método está relacionado aos **items** porque essa pa Provavelmente ainda queremos mantê-lo para o OpenAPI em geral, pois isso garantirá que os IDs de operação sejam **únicos**. -Mas para o cliente gerado, poderíamos **modificar** os IDs de operação do OpenAPI logo antes de gerar os clientes, apenas para tornar esses nomes de método mais **simples**. +Mas para o cliente gerado, poderíamos **modificar** os IDs de operação do OpenAPI logo antes de gerar os clientes, apenas para tornar esses nomes de método mais agradáveis e **limpos**. Poderíamos baixar o JSON do OpenAPI para um arquivo `openapi.json` e então poderíamos **remover essa tag prefixada** com um script como este: -{* ../../docs_src/generate_clients/tutorial004.py *} +{* ../../docs_src/generate_clients/tutorial004_py39.py *} //// tab | Node.js @@ -218,44 +179,30 @@ Poderíamos baixar o JSON do OpenAPI para um arquivo `openapi.json` e então pod Com isso, os IDs de operação seriam renomeados de coisas como `items-get_items` para apenas `get_items`, dessa forma o gerador de clientes pode gerar nomes de métodos mais simples. -### Gerar um Cliente TypeScript com o OpenAPI Pré-processado +### Gere um cliente TypeScript com o OpenAPI pré-processado { #generate-a-typescript-client-with-the-preprocessed-openapi } -Agora, como o resultado final está em um arquivo `openapi.json`, você modificaria o `package.json` para usar esse arquivo local, por exemplo: +Como o resultado final está agora em um arquivo `openapi.json`, você precisa atualizar o local de entrada: -```JSON hl_lines="7" -{ - "name": "frontend-app", - "version": "1.0.0", - "description": "", - "main": "index.js", - "scripts": { - "generate-client": "openapi-ts --input ./openapi.json --output ./src/client --client axios" - }, - "author": "", - "license": "", - "devDependencies": { - "@hey-api/openapi-ts": "^0.27.38", - "typescript": "^4.6.2" - } -} +```sh +npx @hey-api/openapi-ts -i ./openapi.json -o src/client ``` -Depois de gerar o novo cliente, você teria agora **nomes de métodos "limpos"**, com todo o **preenchimento automático**, **erros em linha**, etc: +Depois de gerar o novo cliente, você terá agora **nomes de métodos “limpos”**, com todo o **preenchimento automático**, **erros em linha**, etc: -## Benefícios +## Benefícios { #benefits } -Ao usar os clientes gerados automaticamente, você teria **preenchimento automático** para: +Ao usar os clientes gerados automaticamente, você terá **preenchimento automático** para: * Métodos. -* Corpo de requisições, parâmetros da query, etc. -* Corpo de respostas. +* Corpos de requisições, parâmetros de query, etc. +* Corpos de respostas. -Você também teria **erros em linha** para tudo. +Você também terá **erros em linha** para tudo. -E sempre que você atualizar o código do backend, e **regenerar** o frontend, ele teria quaisquer novas *operações de rota* disponíveis como métodos, as antigas removidas, e qualquer outra alteração seria refletida no código gerado. 🤓 +E sempre que você atualizar o código do backend e **regenerar** o frontend, ele terá quaisquer novas *operações de rota* disponíveis como métodos, as antigas removidas, e qualquer outra alteração será refletida no código gerado. 🤓 -Isso também significa que se algo mudar, será **refletido** no código do cliente automaticamente. E se você **construir** o cliente, ele dará erro se houver alguma **incompatibilidade** nos dados usados. +Isso também significa que, se algo mudou, será **refletido** no código do cliente automaticamente. E se você **construir** o cliente, ele falhará caso haja qualquer **incompatibilidade** nos dados usados. -Então, você **detectaria vários erros** muito cedo no ciclo de desenvolvimento, em vez de ter que esperar que os erros apareçam para seus usuários finais em produção e então tentar depurar onde está o problema. ✨ +Assim, você **detectará muitos erros** muito cedo no ciclo de desenvolvimento, em vez de ter que esperar que os erros apareçam para seus usuários finais em produção e então tentar depurar onde está o problema. ✨ diff --git a/docs/pt/docs/advanced/index.md b/docs/pt/docs/advanced/index.md index 22ba2bf4a4..23e5510743 100644 --- a/docs/pt/docs/advanced/index.md +++ b/docs/pt/docs/advanced/index.md @@ -1,10 +1,10 @@ -# Guia de Usuário Avançado +# Guia de Usuário Avançado { #advanced-user-guide } -## Recursos Adicionais +## Recursos Adicionais { #additional-features } O [Tutorial - Guia de Usuário](../tutorial/index.md){.internal-link target=_blank} deve ser o suficiente para dar a você um tour por todos os principais recursos do **FastAPI**. -Na próxima seção você verá outras opções, configurações, e recursos adicionais. +Nas próximas seções você verá outras opções, configurações, e recursos adicionais. /// tip | Dica @@ -14,14 +14,8 @@ E é possível que para seu caso de uso, a solução esteja em uma delas. /// -## Leia o Tutorial primeiro +## Leia o Tutorial primeiro { #read-the-tutorial-first } Você ainda pode usar a maior parte dos recursos no **FastAPI** com o conhecimento do [Tutorial - Guia de Usuário](../tutorial/index.md){.internal-link target=_blank}. E as próximas seções assumem que você já leu ele, e que você conhece suas ideias principais. - -## Curso TestDriven.io - -Se você gostaria de fazer um curso avançado-iniciante para complementar essa seção da documentação, você pode querer conferir: Test-Driven Development com FastAPI e Docker por **TestDriven.io**. - -Eles estão atualmente doando 10% de todos os lucros para o desenvolvimento do **FastAPI**. 🎉 😄 diff --git a/docs/pt/docs/advanced/middleware.md b/docs/pt/docs/advanced/middleware.md index 7700939f06..30c1834790 100644 --- a/docs/pt/docs/advanced/middleware.md +++ b/docs/pt/docs/advanced/middleware.md @@ -1,4 +1,4 @@ -# Middleware Avançado +# Middleware Avançado { #advanced-middleware } No tutorial principal você leu como adicionar [Middleware Personalizado](../tutorial/middleware.md){.internal-link target=_blank} à sua aplicação. @@ -6,9 +6,9 @@ E então você também leu como lidar com [CORS com o `CORSMiddleware`](../tutor Nesta seção, veremos como usar outros middlewares. -## Adicionando middlewares ASGI +## Adicionando middlewares ASGI { #adding-asgi-middlewares } -Como o **FastAPI** é baseado no Starlette e implementa a especificação ASGI, você pode usar qualquer middleware ASGI. +Como o **FastAPI** é baseado no Starlette e implementa a especificação ASGI, você pode usar qualquer middleware ASGI. O middleware não precisa ser feito para o FastAPI ou Starlette para funcionar, desde que siga a especificação ASGI. @@ -39,52 +39,53 @@ app.add_middleware(UnicornMiddleware, some_config="rainbow") `app.add_middleware()` recebe uma classe de middleware como o primeiro argumento e quaisquer argumentos adicionais a serem passados para o middleware. -## Middlewares Integrados +## Middlewares Integrados { #integrated-middlewares } **FastAPI** inclui vários middlewares para casos de uso comuns, veremos a seguir como usá-los. /// note | Detalhes Técnicos -Para o próximo exemplo, você também poderia usar `from starlette.middleware.something import SomethingMiddleware`. +Para os próximos exemplos, você também poderia usar `from starlette.middleware.something import SomethingMiddleware`. **FastAPI** fornece vários middlewares em `fastapi.middleware` apenas como uma conveniência para você, o desenvolvedor. Mas a maioria dos middlewares disponíveis vem diretamente do Starlette. /// -## `HTTPSRedirectMiddleware` +## `HTTPSRedirectMiddleware` { #httpsredirectmiddleware } Garante que todas as requisições devem ser `https` ou `wss`. Qualquer requisição para `http` ou `ws` será redirecionada para o esquema seguro. -{* ../../docs_src/advanced_middleware/tutorial001.py hl[2,6] *} +{* ../../docs_src/advanced_middleware/tutorial001_py39.py hl[2,6] *} -## `TrustedHostMiddleware` +## `TrustedHostMiddleware` { #trustedhostmiddleware } Garante que todas as requisições recebidas tenham um cabeçalho `Host` corretamente configurado, a fim de proteger contra ataques de cabeçalho de host HTTP. -{* ../../docs_src/advanced_middleware/tutorial002.py hl[2,6:8] *} +{* ../../docs_src/advanced_middleware/tutorial002_py39.py hl[2,6:8] *} Os seguintes argumentos são suportados: * `allowed_hosts` - Uma lista de nomes de domínio que são permitidos como nomes de host. Domínios com coringa, como `*.example.com`, são suportados para corresponder a subdomínios. Para permitir qualquer nome de host, use `allowed_hosts=["*"]` ou omita o middleware. +* `www_redirect` - Se definido como True, as requisições para versões sem www dos hosts permitidos serão redirecionadas para suas versões com www. O padrão é `True`. Se uma requisição recebida não for validada corretamente, uma resposta `400` será enviada. -## `GZipMiddleware` +## `GZipMiddleware` { #gzipmiddleware } Gerencia respostas GZip para qualquer requisição que inclua `"gzip"` no cabeçalho `Accept-Encoding`. O middleware lidará com respostas padrão e de streaming. -{* ../../docs_src/advanced_middleware/tutorial003.py hl[2,6] *} +{* ../../docs_src/advanced_middleware/tutorial003_py39.py hl[2,6] *} Os seguintes argumentos são suportados: * `minimum_size` - Não comprima respostas menores que este tamanho mínimo em bytes. O padrão é `500`. * `compresslevel` - Usado durante a compressão GZip. É um inteiro variando de 1 a 9. O padrão é `9`. Um valor menor resulta em uma compressão mais rápida, mas em arquivos maiores, enquanto um valor maior resulta em uma compressão mais lenta, mas em arquivos menores. -## Outros middlewares +## Outros middlewares { #other-middlewares } Há muitos outros middlewares ASGI. diff --git a/docs/pt/docs/advanced/openapi-callbacks.md b/docs/pt/docs/advanced/openapi-callbacks.md index b0659d3d6a..57c8c5e81d 100644 --- a/docs/pt/docs/advanced/openapi-callbacks.md +++ b/docs/pt/docs/advanced/openapi-callbacks.md @@ -1,29 +1,29 @@ -# Callbacks na OpenAPI +# Callbacks na OpenAPI { #openapi-callbacks } -Você poderia criar uma API com uma *operação de rota* que poderia acionar uma solicitação a uma *API externa* criada por outra pessoa (provavelmente o mesmo desenvolvedor que estaria *usando* sua API). +Você poderia criar uma API com uma *operação de rota* que poderia acionar um request a uma *API externa* criada por outra pessoa (provavelmente o mesmo desenvolvedor que estaria *usando* sua API). -O processo que acontece quando seu aplicativo de API chama a *API externa* é chamado de "callback". Porque o software que o desenvolvedor externo escreveu envia uma solicitação para sua API e então sua API *chama de volta*, enviando uma solicitação para uma *API externa* (que provavelmente foi criada pelo mesmo desenvolvedor). +O processo que acontece quando sua aplicação de API chama a *API externa* é chamado de "callback". Porque o software que o desenvolvedor externo escreveu envia um request para sua API e então sua API *chama de volta*, enviando um request para uma *API externa* (que provavelmente foi criada pelo mesmo desenvolvedor). Nesse caso, você poderia querer documentar como essa API externa *deveria* ser. Que *operação de rota* ela deveria ter, que corpo ela deveria esperar, que resposta ela deveria retornar, etc. -## Um aplicativo com callbacks +## Um aplicativo com callbacks { #an-app-with-callbacks } Vamos ver tudo isso com um exemplo. -Imagine que você tem um aplicativo que permite criar faturas. +Imagine que você desenvolve um aplicativo que permite criar faturas. Essas faturas terão um `id`, `title` (opcional), `customer` e `total`. -O usuário da sua API (um desenvolvedor externo) criará uma fatura em sua API com uma solicitação POST. +O usuário da sua API (um desenvolvedor externo) criará uma fatura na sua API com um request POST. Então sua API irá (vamos imaginar): -* Enviar uma solicitação de pagamento para o desenvolvedor externo. +* Enviar a fatura para algum cliente do desenvolvedor externo. * Coletar o dinheiro. * Enviar a notificação de volta para o usuário da API (o desenvolvedor externo). -* Isso será feito enviando uma solicitação POST (de *sua API*) para alguma *API externa* fornecida por esse desenvolvedor externo (este é o "callback"). + * Isso será feito enviando um request POST (de *sua API*) para alguma *API externa* fornecida por esse desenvolvedor externo (este é o "callback"). -## O aplicativo **FastAPI** normal +## O aplicativo **FastAPI** normal { #the-normal-fastapi-app } Vamos primeiro ver como o aplicativo da API normal se pareceria antes de adicionar o callback. @@ -31,7 +31,7 @@ Ele terá uma *operação de rota* que receberá um corpo `Invoice`, e um parâm Essa parte é bastante normal, a maior parte do código provavelmente já é familiar para você: -{* ../../docs_src/openapi_callbacks/tutorial001.py hl[9:13,36:53] *} +{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[7:11,34:51] *} /// tip | Dica @@ -39,11 +39,11 @@ O parâmetro de consulta `callback_url` usa um tipo Pydantic HTTPX ou Requisições. +Ao implementar o callback por conta própria, você pode usar algo como HTTPX ou Requests. /// -## Escrevendo o código de documentação do callback +## Escreva o código de documentação do callback { #write-the-callback-documentation-code } Esse código não será executado em seu aplicativo, nós só precisamos dele para *documentar* como essa *API externa* deveria ser. @@ -80,37 +80,37 @@ Então vamos usar esse mesmo conhecimento para documentar como a *API externa* d /// tip | Dica -Quando escrever o código para documentar um callback, pode ser útil imaginar que você é aquele *desenvolvedor externo*. E que você está atualmente implementando a *API externa*, não *sua API*. +Ao escrever o código para documentar um callback, pode ser útil imaginar que você é aquele *desenvolvedor externo*. E que você está atualmente implementando a *API externa*, não *sua API*. -Adotar temporariamente esse ponto de vista (do *desenvolvedor externo*) pode ajudar a sentir que é mais óbvio onde colocar os parâmetros, o modelo Pydantic para o corpo, para a resposta, etc. para essa *API externa*. +Adotar temporariamente esse ponto de vista (do *desenvolvedor externo*) pode ajudar a perceber mais facilmente onde colocar os parâmetros, o modelo Pydantic para o corpo, para a resposta, etc. para essa *API externa*. /// -### Criar um `APIRouter` para o callback +### Crie um `APIRouter` de callback { #create-a-callback-apirouter } -Primeiramente crie um novo `APIRouter` que conterá um ou mais callbacks. +Primeiro crie um novo `APIRouter` que conterá um ou mais callbacks. -{* ../../docs_src/openapi_callbacks/tutorial001.py hl[3,25] *} +{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[1,23] *} -### Crie a *operação de rota* do callback +### Crie a *operação de rota* do callback { #create-the-callback-path-operation } Para criar a *operação de rota* do callback, use o mesmo `APIRouter` que você criou acima. -Ele deve parecer exatamente como uma *operação de rota* normal do FastAPI: +Ela deve parecer exatamente como uma *operação de rota* normal do FastAPI: -* Ele provavelmente deveria ter uma declaração do corpo que deveria receber, por exemplo. `body: InvoiceEvent`. -* E também deveria ter uma declaração de um código de status de resposta, por exemplo. `response_model=InvoiceEventReceived`. +* Ela provavelmente deveria ter uma declaração do corpo que deveria receber, por exemplo, `body: InvoiceEvent`. +* E também poderia ter uma declaração da resposta que deveria retornar, por exemplo, `response_model=InvoiceEventReceived`. -{* ../../docs_src/openapi_callbacks/tutorial001.py hl[16:18,21:22,28:32] *} +{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[14:16,19:20,26:30] *} Há 2 diferenças principais de uma *operação de rota* normal: * Ela não necessita ter nenhum código real, porque seu aplicativo nunca chamará esse código. Ele é usado apenas para documentar a *API externa*. Então, a função poderia ter apenas `pass`. -* A *rota* pode conter uma expressão OpenAPI 3 (veja mais abaixo) onde pode usar variáveis com parâmetros e partes da solicitação original enviada para *sua API*. +* O *path* pode conter uma expressão OpenAPI 3 (veja mais abaixo) em que pode usar variáveis com parâmetros e partes da solicitação original enviada para *sua API*. -### A expressão do caminho do callback +### A expressão do path do callback { #the-callback-path-expression } -A *rota* do callback pode ter uma expressão OpenAPI 3 que pode conter partes da solicitação original enviada para *sua API*. +O *path* do callback pode ter uma expressão OpenAPI 3 que pode conter partes da solicitação original enviada para *sua API*. Nesse caso, é a `str`: @@ -118,7 +118,7 @@ Nesse caso, é a `str`: "{$callback_url}/invoices/{$request.body.id}" ``` -Então, se o usuário da sua API (o desenvolvedor externo) enviar uma solicitação para *sua API* para: +Então, se o usuário da sua API (o desenvolvedor externo) enviar um request para *sua API* para: ``` https://yourapi.com/invoices/?callback_url=https://www.external.org/events @@ -134,7 +134,7 @@ com um corpo JSON de: } ``` -então *sua API* processará a fatura e, em algum momento posterior, enviará uma solicitação de callback para o `callback_url` (a *API externa*): +então *sua API* processará a fatura e, em algum momento posterior, enviará um request de callback para o `callback_url` (a *API externa*): ``` https://www.external.org/events/invoices/2expen51ve @@ -163,13 +163,13 @@ Perceba como a URL de callback usada contém a URL recebida como um parâmetro d /// -### Adicionar o roteador de callback +### Adicione o roteador de callback { #add-the-callback-router } -Nesse ponto você tem a(s) *operação de rota de callback* necessária(s) (a(s) que o *desenvolvedor externo* deveria implementar na *API externa*) no roteador de callback que você criou acima. +Nesse ponto você tem a(s) *operação(ões) de rota de callback* necessária(s) (a(s) que o *desenvolvedor externo* deveria implementar na *API externa*) no roteador de callback que você criou acima. -Agora use o parâmetro `callbacks` no decorador da *operação de rota de sua API* para passar o atributo `.routes` (que é na verdade apenas uma `list` de rotas/*operações de rota*) do roteador de callback que você criou acima: +Agora use o parâmetro `callbacks` no decorador da *operação de rota da sua API* para passar o atributo `.routes` (que é na verdade apenas uma `list` de rotas/*operações de path*) do roteador de callback: -{* ../../docs_src/openapi_callbacks/tutorial001.py hl[35] *} +{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[33] *} /// tip | Dica @@ -177,7 +177,7 @@ Perceba que você não está passando o roteador em si (`invoices_callback_route /// -### Verifique a documentação +### Verifique a documentação { #check-the-docs } Agora você pode iniciar seu aplicativo e ir para http://127.0.0.1:8000/docs. diff --git a/docs/pt/docs/advanced/openapi-webhooks.md b/docs/pt/docs/advanced/openapi-webhooks.md index f35922234f..011898e8cd 100644 --- a/docs/pt/docs/advanced/openapi-webhooks.md +++ b/docs/pt/docs/advanced/openapi-webhooks.md @@ -1,4 +1,4 @@ -# Webhooks OpenAPI +# Webhooks OpenAPI { #openapi-webhooks } Existem situações onde você deseja informar os **usuários** da sua API que a sua aplicação pode chamar a aplicação *deles* (enviando uma requisição) com alguns dados, normalmente para **notificar** algum tipo de **evento**. @@ -6,7 +6,7 @@ Isso significa que no lugar do processo normal de seus usuários enviarem requis Isso normalmente é chamado de **webhook**. -## Etapas dos Webhooks +## Etapas dos webhooks { #webhooks-steps } Normalmente, o processo é que **você define** em seu código qual é a mensagem que você irá mandar, o **corpo da sua requisição**. @@ -16,7 +16,7 @@ E os **seus usuários** definem de alguma forma (em algum painel por exemplo) a Toda a **lógica** sobre como cadastrar as URLs para os webhooks e o código para enviar de fato as requisições cabe a você definir. Você escreve da maneira que você desejar no **seu próprio código**. -## Documentando webhooks com o FastAPI e OpenAPI +## Documentando webhooks com o FastAPI e OpenAPI { #documenting-webhooks-with-fastapi-and-openapi } Com o **FastAPI**, utilizando o OpenAPI, você pode definir os nomes destes webhooks, os tipos das operações HTTP que a sua aplicação pode enviar (e.g. `POST`, `PUT`, etc.) e os **corpos** da requisição que a sua aplicação enviaria. @@ -28,11 +28,11 @@ Webhooks estão disponíveis a partir do OpenAPI 3.1.0, e possui suporte do Fast /// -## Uma aplicação com webhooks +## Uma aplicação com webhooks { #an-app-with-webhooks } Quando você cria uma aplicação com o **FastAPI**, existe um atributo chamado `webhooks`, que você utilizar para defini-los da mesma maneira que você definiria as suas **operações de rotas**, utilizando por exemplo `@app.webhooks.post()`. -{* ../../docs_src/openapi_webhooks/tutorial001.py hl[9:13,36:53] *} +{* ../../docs_src/openapi_webhooks/tutorial001_py39.py hl[9:13,36:53] *} Os webhooks que você define aparecerão no esquema do **OpenAPI** e na **página de documentação** gerada automaticamente. @@ -42,11 +42,11 @@ O objeto `app.webhooks` é na verdade apenas um `APIRouter`, o mesmo tipo que vo /// -Note que utilizando webhooks você não está de fato declarando uma **rota** (como `/items/`), o texto que informa é apenas um **identificador** do webhook (o nome do evento), por exemplo em `@app.webhooks.post("new-subscription")`, o nome do webhook é `new-subscription`. +Note que utilizando webhooks você não está de fato declarando um *path* (como `/items/`), o texto que informa é apenas um **identificador** do webhook (o nome do evento), por exemplo em `@app.webhooks.post("new-subscription")`, o nome do webhook é `new-subscription`. -Isto porque espera-se que os **seus usuários** definam o verdadeiro **caminho da URL** onde eles desejam receber a requisição do webhook de algum outra maneira. (e.g. um painel). +Isto porque espera-se que os **seus usuários** definam o verdadeiro **URL path** onde eles desejam receber a requisição do webhook de algum outra maneira. (e.g. um painel). -### Confira a documentação +### Confira a documentação { #check-the-docs } Agora você pode iniciar a sua aplicação e ir até http://127.0.0.1:8000/docs. diff --git a/docs/pt/docs/advanced/path-operation-advanced-configuration.md b/docs/pt/docs/advanced/path-operation-advanced-configuration.md index 411d0f9a7a..e1c3e5ab89 100644 --- a/docs/pt/docs/advanced/path-operation-advanced-configuration.md +++ b/docs/pt/docs/advanced/path-operation-advanced-configuration.md @@ -1,8 +1,8 @@ -# Configuração Avançada da Operação de Rota +# Configuração Avançada da Operação de Rota { #path-operation-advanced-configuration } -## operationId do OpenAPI +## operationId do OpenAPI { #openapi-operationid } -/// warning | Aviso +/// warning | Atenção Se você não é um "especialista" no OpenAPI, você provavelmente não precisa disso. @@ -12,15 +12,15 @@ Você pode definir o `operationId` do OpenAPI que será utilizado na sua *opera Você precisa ter certeza que ele é único para cada operação. -{* ../../docs_src/path_operation_advanced_configuration/tutorial001.py hl[6] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial001_py39.py hl[6] *} -### Utilizando o nome da *função de operação de rota* como o operationId +### Utilizando o nome da *função de operação de rota* como o operationId { #using-the-path-operation-function-name-as-the-operationid } -Se você quiser utilizar o nome das funções da sua API como `operationId`s, você pode iterar sobre todos esses nomes e sobrescrever o `operationId` em cada *operação de rota* utilizando o `APIRoute.name` dela. +Se você quiser utilizar o nome das funções da sua API como `operationId`s, você pode iterar sobre todos esses nomes e sobrescrever o `operation_id` em cada *operação de rota* utilizando o `APIRoute.name` dela. Você deve fazer isso depois de adicionar todas as suas *operações de rota*. -{* ../../docs_src/path_operation_advanced_configuration/tutorial002.py hl[2,12:21,24] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial002_py39.py hl[2, 12:21, 24] *} /// tip | Dica @@ -28,7 +28,7 @@ Se você chamar `app.openapi()` manualmente, os `operationId`s devem ser atualiz /// -/// warning | Aviso +/// warning | Atenção Se você fizer isso, você tem que ter certeza de que cada uma das suas *funções de operação de rota* tem um nome único. @@ -36,13 +36,13 @@ Mesmo que elas estejam em módulos (arquivos Python) diferentes. /// -## Excluir do OpenAPI +## Excluir do OpenAPI { #exclude-from-openapi } Para excluir uma *operação de rota* do esquema OpenAPI gerado (e por consequência, dos sistemas de documentação automáticos), utilize o parâmetro `include_in_schema` e defina ele como `False`: -{* ../../docs_src/path_operation_advanced_configuration/tutorial003.py hl[6] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial003_py39.py hl[6] *} -## Descrição avançada a partir de docstring +## Descrição avançada a partir de docstring { #advanced-description-from-docstring } Você pode limitar as linhas utilizadas a partir de uma docstring de uma *função de operação de rota* para o OpenAPI. @@ -50,9 +50,9 @@ Adicionar um `\f` (um caractere de escape para alimentação de formulário) faz Ele não será mostrado na documentação, mas outras ferramentas (como o Sphinx) serão capazes de utilizar o resto do texto. -{* ../../docs_src/path_operation_advanced_configuration/tutorial004.py hl[19:29] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial004_py310.py hl[17:27] *} -## Respostas Adicionais +## Respostas Adicionais { #additional-responses } Você provavelmente já viu como declarar o `response_model` e `status_code` para uma *operação de rota*. @@ -62,11 +62,11 @@ Você também pode declarar respostas adicionais, com seus modelos, códigos de Existe um capítulo inteiro da nossa documentação sobre isso, você pode ler em [Retornos Adicionais no OpenAPI](additional-responses.md){.internal-link target=_blank}. -## Extras do OpenAPI +## Extras do OpenAPI { #openapi-extra } Quando você declara uma *operação de rota* na sua aplicação, o **FastAPI** irá gerar os metadados relevantes da *operação de rota* automaticamente para serem incluídos no esquema do OpenAPI. -/// note | Nota +/// note | Detalhes Técnicos Na especificação do OpenAPI, isso é chamado de um Objeto de Operação. @@ -88,11 +88,11 @@ Caso você só precise declarar respostas adicionais, uma forma conveniente de f Você pode estender o esquema do OpenAPI para uma *operação de rota* utilizando o parâmetro `openapi_extra`. -### Extensões do OpenAPI +### Extensões do OpenAPI { #openapi-extensions } Esse parâmetro `openapi_extra` pode ser útil, por exemplo, para declarar [Extensões do OpenAPI](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#specificationExtensions): -{* ../../docs_src/path_operation_advanced_configuration/tutorial005.py hl[6] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial005_py39.py hl[6] *} Se você abrir os documentos criados automaticamente para a API, sua extensão aparecerá no final da *operação de rota* específica. @@ -129,7 +129,7 @@ E se você olhar o esquema OpenAPI resultante (na rota `/openapi.json` da sua AP } ``` -### Esquema de *operação de rota* do OpenAPI personalizado +### Esquema de *operação de rota* do OpenAPI personalizado { #custom-openapi-path-operation-schema } O dicionário em `openapi_extra` vai ter todos os seus níveis mesclados dentro do esquema OpenAPI gerado automaticamente para a *operação de rota*. @@ -139,39 +139,39 @@ Por exemplo, você poderia optar por ler e validar a requisição com seu própr Você pode fazer isso com `openapi_extra`: -{* ../../docs_src/path_operation_advanced_configuration/tutorial006.py hl[19:36,39:40] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial006_py39.py hl[19:36, 39:40] *} Nesse exemplo, nós não declaramos nenhum modelo do Pydantic. Na verdade, o corpo da requisição não está nem mesmo analisado como JSON, ele é lido diretamente como `bytes` e a função `magic_data_reader()` seria a responsável por analisar ele de alguma forma. De toda forma, nós podemos declarar o esquema esperado para o corpo da requisição. -### Tipo de conteúdo do OpenAPI personalizado +### Tipo de conteúdo do OpenAPI personalizado { #custom-openapi-content-type } -Utilizando esse mesmo truque, você pode utilizar um modelo Pydantic para definir o esquema JSON que é então incluído na seção do esquema personalizado do OpenAPI na *operação de rota*. +Utilizando esse mesmo truque, você pode utilizar um modelo Pydantic para definir o JSON Schema que é então incluído na seção do esquema personalizado do OpenAPI na *operação de rota*. E você pode fazer isso até mesmo quando os dados da requisição não seguem o formato JSON. -Por exemplo, nesta aplicação nós não usamos a funcionalidade integrada ao FastAPI de extrair o esquema JSON dos modelos Pydantic nem a validação automática do JSON. Na verdade, estamos declarando o tipo do conteúdo da requisição como YAML, em vez de JSON: +Por exemplo, nesta aplicação nós não usamos a funcionalidade integrada ao FastAPI de extrair o JSON Schema dos modelos Pydantic nem a validação automática do JSON. Na verdade, estamos declarando o tipo do conteúdo da requisição como YAML, em vez de JSON: //// tab | Pydantic v2 -{* ../../docs_src/path_operation_advanced_configuration/tutorial007.py hl[17:22,24] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial007_py39.py hl[15:20, 22] *} //// //// tab | Pydantic v1 -{* ../../docs_src/path_operation_advanced_configuration/tutorial007_pv1.py hl[17:22,24] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial007_pv1_py39.py hl[15:20, 22] *} //// /// info | Informação -Na versão 1 do Pydantic, o método para obter o esquema JSON de um modelo é `Item.schema()`, na versão 2 do Pydantic, o método é `Item.model_json_schema()` +Na versão 1 do Pydantic, o método para obter o JSON Schema de um modelo é `Item.schema()`, na versão 2 do Pydantic, o método é `Item.model_json_schema()`. /// -Entretanto, mesmo que não utilizemos a funcionalidade integrada por padrão, ainda estamos usando um modelo Pydantic para gerar um esquema JSON manualmente para os dados que queremos receber no formato YAML. +Entretanto, mesmo que não utilizemos a funcionalidade integrada por padrão, ainda estamos usando um modelo Pydantic para gerar um JSON Schema manualmente para os dados que queremos receber no formato YAML. Então utilizamos a requisição diretamente, e extraímos o corpo como `bytes`. Isso significa que o FastAPI não vai sequer tentar analisar o corpo da requisição como JSON. @@ -179,13 +179,13 @@ E então no nosso código, nós analisamos o conteúdo YAML diretamente, e estam //// tab | Pydantic v2 -{* ../../docs_src/path_operation_advanced_configuration/tutorial007.py hl[26:33] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial007_py39.py hl[24:31] *} //// //// tab | Pydantic v1 -{* ../../docs_src/path_operation_advanced_configuration/tutorial007_pv1.py hl[26:33] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial007_pv1_py39.py hl[24:31] *} //// @@ -195,7 +195,7 @@ Na versão 1 do Pydantic, o método para analisar e validar um objeto era `Item. /// -///tip | Dica +/// tip | Dica Aqui reutilizamos o mesmo modelo do Pydantic. diff --git a/docs/pt/docs/advanced/response-change-status-code.md b/docs/pt/docs/advanced/response-change-status-code.md index 358c12d549..ee81f0bfc8 100644 --- a/docs/pt/docs/advanced/response-change-status-code.md +++ b/docs/pt/docs/advanced/response-change-status-code.md @@ -1,10 +1,10 @@ -# Retorno - Altere o Código de Status +# Retorno - Altere o Código de Status { #response-change-status-code } Você provavelmente leu anteriormente que você pode definir um [Código de Status do Retorno](../tutorial/response-status-code.md){.internal-link target=_blank} padrão. Porém em alguns casos você precisa retornar um código de status diferente do padrão. -## Caso de uso +## Caso de uso { #use-case } Por exemplo, imagine que você deseja retornar um código de status HTTP de "OK" `200` por padrão. @@ -14,13 +14,13 @@ Mas você ainda quer ser capaz de filtrar e converter o dado que você retornar Para estes casos, você pode utilizar um parâmetro `Response`. -## Use um parâmetro `Response` +## Use um parâmetro `Response` { #use-a-response-parameter } Você pode declarar um parâmetro do tipo `Response` em sua *função de operação de rota* (assim como você pode fazer para cookies e headers). E então você pode definir o `status_code` neste objeto de retorno temporal. -{* ../../docs_src/response_change_status_code/tutorial001.py hl[1,9,12] *} +{* ../../docs_src/response_change_status_code/tutorial001_py39.py hl[1,9,12] *} E então você pode retornar qualquer objeto que você precise, como você faria normalmente (um `dict`, um modelo de banco de dados, etc.). diff --git a/docs/pt/docs/advanced/response-cookies.md b/docs/pt/docs/advanced/response-cookies.md index f005f0b9b9..67820b433b 100644 --- a/docs/pt/docs/advanced/response-cookies.md +++ b/docs/pt/docs/advanced/response-cookies.md @@ -1,12 +1,12 @@ -# Cookies de Resposta +# Cookies de Resposta { #response-cookies } -## Usando um parâmetro `Response` +## Use um parâmetro `Response` { #use-a-response-parameter } Você pode declarar um parâmetro do tipo `Response` na sua *função de operação de rota*. E então você pode definir cookies nesse objeto de resposta *temporário*. -{* ../../docs_src/response_cookies/tutorial002.py hl[1,8:9] *} +{* ../../docs_src/response_cookies/tutorial002_py39.py hl[1, 8:9] *} Em seguida, você pode retornar qualquer objeto que precise, como normalmente faria (um `dict`, um modelo de banco de dados, etc). @@ -16,15 +16,15 @@ E se você declarou um `response_model`, ele ainda será usado para filtrar e co Você também pode declarar o parâmetro `Response` em dependências e definir cookies (e cabeçalhos) nelas. -## Retornando uma `Response` diretamente +## Retorne uma `Response` diretamente { #return-a-response-directly } Você também pode criar cookies ao retornar uma `Response` diretamente no seu código. -Para fazer isso, você pode criar uma resposta como descrito em [Retornando uma Resposta Diretamente](response-directly.md){.internal-link target=_blank}. +Para fazer isso, você pode criar uma resposta como descrito em [Retorne uma Resposta Diretamente](response-directly.md){.internal-link target=_blank}. Então, defina os cookies nela e a retorne: -{* ../../docs_src/response_cookies/tutorial001.py hl[10:12] *} +{* ../../docs_src/response_cookies/tutorial001_py39.py hl[10:12] *} /// tip | Dica @@ -36,7 +36,7 @@ E também que você não esteja enviando nenhum dado que deveria ter sido filtra /// -### Mais informações +### Mais informações { #more-info } /// note | Detalhes Técnicos diff --git a/docs/pt/docs/advanced/response-directly.md b/docs/pt/docs/advanced/response-directly.md index ea717be008..bbbef2f913 100644 --- a/docs/pt/docs/advanced/response-directly.md +++ b/docs/pt/docs/advanced/response-directly.md @@ -1,4 +1,4 @@ -# Retornando uma Resposta Diretamente +# Retornando uma Resposta Diretamente { #return-a-response-directly } Quando você cria uma *operação de rota* no **FastAPI** você pode retornar qualquer dado nela: um dicionário (`dict`), uma lista (`list`), um modelo do Pydantic ou do seu banco de dados, etc. @@ -10,7 +10,7 @@ Mas você pode retornar a `JSONResponse` diretamente nas suas *operações de ro Pode ser útil para retornar cabeçalhos e cookies personalizados, por exemplo. -## Retornando uma `Response` +## Retornando uma `Response` { #return-a-response } Na verdade, você pode retornar qualquer `Response` ou subclasse dela. @@ -26,7 +26,7 @@ Ele não vai fazer conversões de dados com modelos do Pydantic, não irá conve Isso te dá bastante flexibilidade. Você pode retornar qualquer tipo de dado, sobrescrever qualquer declaração e validação nos dados, etc. -## Utilizando o `jsonable_encoder` em uma `Response` +## Utilizando o `jsonable_encoder` em uma `Response` { #using-the-jsonable-encoder-in-a-response } Como o **FastAPI** não realiza nenhuma mudança na `Response` que você retorna, você precisa garantir que o conteúdo dela está pronto para uso. @@ -34,7 +34,7 @@ Por exemplo, você não pode colocar um modelo do Pydantic em uma `JSONResponse` Para esses casos, você pode usar o `jsonable_encoder` para converter seus dados antes de repassá-los para a resposta: -{* ../../docs_src/response_directly/tutorial001.py hl[6:7,21:22] *} +{* ../../docs_src/response_directly/tutorial001_py310.py hl[5:6,20:21] *} /// note | Detalhes Técnicos @@ -44,23 +44,22 @@ Você também pode utilizar `from starlette.responses import JSONResponse`. /// -## Retornando uma `Response` +## Retornando uma `Response` personalizada { #returning-a-custom-response } O exemplo acima mostra todas as partes que você precisa, mas ainda não é muito útil, já que você poderia ter retornado o `item` diretamente, e o **FastAPI** colocaria em uma `JSONResponse` para você, convertendo em um `dict`, etc. Tudo isso por padrão. Agora, vamos ver como você pode usar isso para retornar uma resposta personalizada. -Vamos dizer quer retornar uma resposta XML. +Vamos dizer que você quer retornar uma resposta XML. Você pode colocar o seu conteúdo XML em uma string, colocar em uma `Response`, e retorná-lo: -{* ../../docs_src/response_directly/tutorial002.py hl[1,18] *} +{* ../../docs_src/response_directly/tutorial002_py39.py hl[1,18] *} -## Notas +## Notas { #notes } Quando você retorna uma `Response` diretamente os dados não são validados, convertidos (serializados) ou documentados automaticamente. -Mas você ainda pode documentar como descrito em [Retornos Adicionais no OpenAPI -](additional-responses.md){.internal-link target=_blank}. +Mas você ainda pode documentar como descrito em [Retornos Adicionais no OpenAPI](additional-responses.md){.internal-link target=_blank}. Você pode ver nas próximas seções como usar/declarar essas `Responses` customizadas enquanto mantém a conversão e documentação automática dos dados. diff --git a/docs/pt/docs/advanced/response-headers.md b/docs/pt/docs/advanced/response-headers.md index a1fc84cc0e..14c3fb1869 100644 --- a/docs/pt/docs/advanced/response-headers.md +++ b/docs/pt/docs/advanced/response-headers.md @@ -1,12 +1,12 @@ -# Cabeçalhos de resposta +# Cabeçalhos de resposta { #response-headers } -## Usando um parâmetro `Response` +## Use um parâmetro `Response` { #use-a-response-parameter } Você pode declarar um parâmetro do tipo `Response` na sua *função de operação de rota* (assim como você pode fazer para cookies). Então você pode definir os cabeçalhos nesse objeto de resposta *temporário*. -{* ../../docs_src/response_headers/tutorial002.py hl[1,7:8] *} +{* ../../docs_src/response_headers/tutorial002_py39.py hl[1, 7:8] *} Em seguida você pode retornar qualquer objeto que precisar, da maneira que faria normalmente (um `dict`, um modelo de banco de dados, etc.). @@ -16,15 +16,15 @@ Se você declarou um `response_model`, ele ainda será utilizado para filtrar e Você também pode declarar o parâmetro `Response` em dependências e definir cabeçalhos (e cookies) nelas. -## Retornar uma `Response` diretamente +## Retorne uma `Response` diretamente { #return-a-response-directly } Você também pode adicionar cabeçalhos quando retornar uma `Response` diretamente. Crie uma resposta conforme descrito em [Retornar uma resposta diretamente](response-directly.md){.internal-link target=_blank} e passe os cabeçalhos como um parâmetro adicional: -{* ../../docs_src/response_headers/tutorial001.py hl[10:12] *} +{* ../../docs_src/response_headers/tutorial001_py39.py hl[10:12] *} -/// note | Detalhes técnicos +/// note | Detalhes Técnicos Você também pode usar `from starlette.responses import Response` ou `from starlette.responses import JSONResponse`. @@ -34,8 +34,8 @@ E como a `Response` pode ser usada frequentemente para definir cabeçalhos e coo /// -## Cabeçalhos personalizados +## Cabeçalhos personalizados { #custom-headers } -Tenha em mente que cabeçalhos personalizados proprietários podem ser adicionados usando o prefixo 'X-'. +Tenha em mente que cabeçalhos personalizados proprietários podem ser adicionados usando o prefixo `X-`. Porém, se voce tiver cabeçalhos personalizados que deseja que um cliente no navegador possa ver, você precisa adicioná-los às suas configurações de CORS (saiba mais em [CORS (Cross-Origin Resource Sharing)](../tutorial/cors.md){.internal-link target=_blank}), usando o parâmetro `expose_headers` descrito na documentação de CORS do Starlette. diff --git a/docs/pt/docs/advanced/security/http-basic-auth.md b/docs/pt/docs/advanced/security/http-basic-auth.md index 3315139273..bd572217b2 100644 --- a/docs/pt/docs/advanced/security/http-basic-auth.md +++ b/docs/pt/docs/advanced/security/http-basic-auth.md @@ -1,10 +1,10 @@ -# HTTP Basic Auth +# HTTP Basic Auth { #http-basic-auth } Para os casos mais simples, você pode utilizar o HTTP Basic Auth. No HTTP Basic Auth, a aplicação espera um cabeçalho que contém um usuário e uma senha. -Caso ela não receba, ela retorna um erro HTTP 401 "Unauthorized" (*Não Autorizado*). +Caso ela não receba, ela retorna um erro HTTP 401 "Unauthorized". E retorna um cabeçalho `WWW-Authenticate` com o valor `Basic`, e um parâmetro opcional `realm`. @@ -12,7 +12,7 @@ Isso sinaliza ao navegador para mostrar o prompt integrado para um usuário e se Então, quando você digitar o usuário e senha, o navegador os envia automaticamente no cabeçalho. -## HTTP Basic Auth Simples +## HTTP Basic Auth Simples { #simple-http-basic-auth } * Importe `HTTPBasic` e `HTTPBasicCredentials`. * Crie um "esquema `security`" utilizando `HTTPBasic`. @@ -22,11 +22,11 @@ Então, quando você digitar o usuário e senha, o navegador os envia automatica {* ../../docs_src/security/tutorial006_an_py39.py hl[4,8,12] *} -Quando você tentar abrir a URL pela primeira vez (ou clicar no botão "Executar" nos documentos) o navegador vai pedir pelo seu usuário e senha: +Quando você tentar abrir a URL pela primeira vez (ou clicar no botão "Executar" na documentação) o navegador vai pedir pelo seu usuário e senha: -## Verifique o usuário +## Verifique o usuário { #check-the-username } Aqui está um exemplo mais completo. @@ -52,7 +52,7 @@ if not (credentials.username == "stanleyjobson") or not (credentials.password == Porém, ao utilizar o `secrets.compare_digest()`, isso estará seguro contra um tipo de ataque chamado "timing attacks" (ataques de temporização). -### Ataques de Temporização +### Ataques de Temporização { #timing-attacks } Mas o que é um "timing attack" (ataque de temporização)? @@ -80,19 +80,19 @@ if "stanleyjobsox" == "stanleyjobson" and "love123" == "swordfish": O Python terá que comparar todo o `stanleyjobso` tanto em `stanleyjobsox` como em `stanleyjobson` antes de perceber que as strings não são a mesma. Então isso levará alguns microssegundos a mais para retornar "Usuário ou senha incorretos". -#### O tempo para responder ajuda os invasores +#### O tempo para responder ajuda os invasores { #the-time-to-answer-helps-the-attackers } Neste ponto, ao perceber que o servidor demorou alguns microssegundos a mais para enviar o retorno "Usuário ou senha incorretos", os invasores irão saber que eles acertaram _alguma coisa_, algumas das letras iniciais estavam certas. E eles podem tentar de novo sabendo que provavelmente é algo mais parecido com `stanleyjobsox` do que com `johndoe`. -#### Um ataque "profissional" +#### Um ataque "profissional" { #a-professional-attack } Claro, os invasores não tentariam tudo isso de forma manual, eles escreveriam um programa para fazer isso, possivelmente com milhares ou milhões de testes por segundo. E obteriam apenas uma letra a mais por vez. Mas fazendo isso, em alguns minutos ou horas os invasores teriam adivinhado o usuário e senha corretos, com a "ajuda" da nossa aplicação, apenas usando o tempo levado para responder. -#### Corrija com o `secrets.compare_digest()` +#### Corrija com o `secrets.compare_digest()` { #fix-it-with-secrets-compare-digest } Mas em nosso código já estamos utilizando o `secrets.compare_digest()`. @@ -100,8 +100,7 @@ Resumindo, levará o mesmo tempo para comparar `stanleyjobsox` com `stanleyjobso Deste modo, ao utilizar `secrets.compare_digest()` no código de sua aplicação, ela estará a salvo contra toda essa gama de ataques de segurança. - -### Retorne o erro +### Retorne o erro { #return-the-error } Após detectar que as credenciais estão incorretas, retorne um `HTTPException` com o status 401 (o mesmo retornado quando nenhuma credencial foi informada) e adicione o cabeçalho `WWW-Authenticate` para fazer com que o navegador mostre o prompt de login novamente: diff --git a/docs/pt/docs/advanced/security/index.md b/docs/pt/docs/advanced/security/index.md index 6c7becb67a..70fb999d0e 100644 --- a/docs/pt/docs/advanced/security/index.md +++ b/docs/pt/docs/advanced/security/index.md @@ -1,6 +1,6 @@ -# Segurança Avançada +# Segurança Avançada { #advanced-security } -## Funcionalidades Adicionais +## Funcionalidades Adicionais { #additional-features } Existem algumas funcionalidades adicionais para lidar com segurança além das cobertas em [Tutorial - Guia de Usuário: Segurança](../../tutorial/security/index.md){.internal-link target=_blank}. @@ -12,7 +12,7 @@ E é possível que para o seu caso de uso, a solução está em uma delas. /// -## Leia o Tutorial primeiro +## Leia o Tutorial primeiro { #read-the-tutorial-first } As próximas seções pressupõem que você já leu o principal [Tutorial - Guia de Usuário: Segurança](../../tutorial/security/index.md){.internal-link target=_blank}. diff --git a/docs/pt/docs/advanced/security/oauth2-scopes.md b/docs/pt/docs/advanced/security/oauth2-scopes.md index 07b3459452..591ac9b4aa 100644 --- a/docs/pt/docs/advanced/security/oauth2-scopes.md +++ b/docs/pt/docs/advanced/security/oauth2-scopes.md @@ -1,4 +1,4 @@ -# Escopos OAuth2 +# Escopos OAuth2 { #oauth2-scopes } Você pode utilizar escopos do OAuth2 diretamente com o **FastAPI**, eles são integrados para funcionar perfeitamente. @@ -10,7 +10,7 @@ Toda vez que você "se autentica com" Facebook, Google, GitHub, Microsoft, X (Tw Nesta seção, você verá como gerenciar a autenticação e autorização com os mesmos escopos do OAuth2 em sua aplicação **FastAPI**. -/// warning | Aviso +/// warning | Atenção Isso é uma seção mais ou menos avançada. Se você está apenas começando, você pode pular. @@ -18,7 +18,7 @@ Você não necessariamente precisa de escopos do OAuth2, e você pode lidar com Mas o OAuth2 com escopos pode ser integrado de maneira fácil em sua API (com OpenAPI) e a sua documentação de API. -No entando, você ainda aplica estes escopos, ou qualquer outro requisito de segurança/autorização, conforme necessário, em seu código. +No entanto, você ainda aplica estes escopos, ou qualquer outro requisito de segurança/autorização, conforme necessário, em seu código. Em muitos casos, OAuth2 com escopos pode ser um exagero. @@ -26,7 +26,7 @@ Mas se você sabe que precisa, ou está curioso, continue lendo. /// -## Escopos OAuth2 e OpenAPI +## Escopos OAuth2 e OpenAPI { #oauth2-scopes-and-openapi } A especificação OAuth2 define "escopos" como uma lista de strings separadas por espaços. @@ -58,15 +58,15 @@ Para o OAuth2, eles são apenas strings. /// -## Visão global +## Visão global { #global-view } Primeiro, vamos olhar rapidamente as partes que mudam dos exemplos do **Tutorial - Guia de Usuário** para [OAuth2 com Senha (e hash), Bearer com tokens JWT](../../tutorial/security/oauth2-jwt.md){.internal-link target=_blank}. Agora utilizando escopos OAuth2: -{* ../../docs_src/security/tutorial005_an_py310.py hl[5,9,13,47,65,106,108:116,122:125,129:135,140,156] *} +{* ../../docs_src/security/tutorial005_an_py310.py hl[5,9,13,47,65,106,108:116,122:126,130:136,141,157] *} Agora vamos revisar essas mudanças passo a passo. -## Esquema de segurança OAuth2 +## Esquema de segurança OAuth2 { #oauth2-security-scheme } A primeira mudança é que agora nós estamos declarando o esquema de segurança OAuth2 com dois escopos disponíveis, `me` e `items`. @@ -82,9 +82,9 @@ Este é o mesmo mecanismo utilizado quando você adiciona permissões enquanto s -## Token JWT com escopos +## Token JWT com escopos { #jwt-token-with-scopes } -Agora, modifique o *caminho de rota* para retornar os escopos solicitados. +Agora, modifique a *operação de rota* do token para retornar os escopos solicitados. Nós ainda estamos utilizando o mesmo `OAuth2PasswordRequestForm`. Ele inclui a propriedade `scopes` com uma `list` de `str`, com cada escopo que ele recebeu na requisição. @@ -98,15 +98,15 @@ Porém em sua aplicação, por segurança, você deve garantir que você apenas /// -{* ../../docs_src/security/tutorial005_an_py310.py hl[156] *} +{* ../../docs_src/security/tutorial005_an_py310.py hl[157] *} -## Declare escopos em *operações de rota* e dependências +## Declare escopos em *operações de rota* e dependências { #declare-scopes-in-path-operations-and-dependencies } Agora nós declaramos que a *operação de rota* para `/users/me/items/` exige o escopo `items`. Para isso, nós importamos e utilizamos `Security` de `fastapi`. -Você pode utilizar `Security` para declarar dependências (assim como `Depends`), porém o `Security` também recebe o parâmetros `scopes` com uma lista de escopos (strings). +Você pode utilizar `Security` para declarar dependências (assim como `Depends`), porém o `Security` também recebe o parâmetro `scopes` com uma lista de escopos (strings). Neste caso, nós passamos a função `get_current_active_user` como dependência para `Security` (da mesma forma que nós faríamos com `Depends`). @@ -124,9 +124,9 @@ Nós estamos fazendo isso aqui para demonstrar como o **FastAPI** lida com escop /// -{* ../../docs_src/security/tutorial005_an_py310.py hl[5,140,171] *} +{* ../../docs_src/security/tutorial005_an_py310.py hl[5,141,172] *} -/// info | Informações Técnicas +/// info | Detalhes Técnicos `Security` é na verdade uma subclasse de `Depends`, e ele possui apenas um parâmetro extra que veremos depois. @@ -136,7 +136,7 @@ Mas quando você importa `Query`, `Path`, `Depends`, `Security` entre outros de /// -## Utilize `SecurityScopes` +## Utilize `SecurityScopes` { #use-securityscopes } Agora atualize a dependência `get_current_user`. @@ -152,7 +152,7 @@ A classe `SecurityScopes` é semelhante à classe `Request` (`Request` foi utili {* ../../docs_src/security/tutorial005_an_py310.py hl[9,106] *} -## Utilize os `scopes` +## Utilize os `scopes` { #use-the-scopes } O parâmetro `security_scopes` será do tipo `SecurityScopes`. @@ -166,7 +166,7 @@ Nesta exceção, nós incluímos os escopos necessários (se houver algum) como {* ../../docs_src/security/tutorial005_an_py310.py hl[106,108:116] *} -## Verifique o `username` e o formato dos dados +## Verifique o `username` e o formato dos dados { #verify-the-username-and-data-shape } Nós verificamos que nós obtemos um `username`, e extraímos os escopos. @@ -180,17 +180,17 @@ No lugar de, por exemplo, um `dict`, ou alguma outra coisa, que poderia quebrar Nós também verificamos que nós temos um usuário com o "*username*", e caso contrário, nós levantamos a mesma exceção que criamos anteriormente. -{* ../../docs_src/security/tutorial005_an_py310.py hl[47,117:128] *} +{* ../../docs_src/security/tutorial005_an_py310.py hl[47,117:129] *} -## Verifique os `scopes` +## Verifique os `scopes` { #verify-the-scopes } Nós verificamos agora que todos os escopos necessários, por essa dependência e todos os dependentes (incluindo as *operações de rota*) estão incluídas nos escopos fornecidos pelo token recebido, caso contrário, levantamos uma `HTTPException`. Para isso, nós utilizamos `security_scopes.scopes`, que contém uma `list` com todos esses escopos como uma `str`. -{* ../../docs_src/security/tutorial005_an_py310.py hl[129:135] *} +{* ../../docs_src/security/tutorial005_an_py310.py hl[130:136] *} -## Árvore de dependência e escopos +## Árvore de dependência e escopos { #dependency-tree-and-scopes } Vamos rever novamente essa árvore de dependência e os escopos. @@ -223,7 +223,7 @@ Tudo depende dos `scopes` declarados em cada *operação de rota* e cada depend /// -## Mais detalhes sobre `SecurityScopes` +## Mais detalhes sobre `SecurityScopes` { #more-details-about-securityscopes } Você pode utilizar `SecurityScopes` em qualquer lugar, e em diversos lugares. Ele não precisa estar na dependência "raiz". @@ -233,9 +233,9 @@ Porque o `SecurityScopes` terá todos os escopos declarados por dependentes, voc Todos eles serão validados independentemente para cada *operação de rota*. -## Verifique +## Verifique { #check-it } -Se você abrir os documentos da API, você pode antenticar e especificar quais escopos você quer autorizar. +Se você abrir os documentos da API, você pode autenticar e especificar quais escopos você quer autorizar. @@ -245,9 +245,9 @@ E se você selecionar o escopo `me`, mas não o escopo `items`, você poderá ac Isso é o que aconteceria se uma aplicação terceira que tentou acessar uma dessas *operações de rota* com um token fornecido por um usuário, dependendo de quantas permissões o usuário forneceu para a aplicação. -## Sobre integrações de terceiros +## Sobre integrações de terceiros { #about-third-party-integrations } -Neste exemplos nós estamos utilizando o fluxo de senha do OAuth2. +Neste exemplo nós estamos utilizando o fluxo de senha do OAuth2. Isso é apropriado quando nós estamos autenticando em nossa própria aplicação, provavelmente com o nosso próprio "*frontend*". @@ -269,6 +269,6 @@ Mas no final, eles estão implementando o mesmo padrão OAuth2. O **FastAPI** inclui utilitários para todos esses fluxos de autenticação OAuth2 em `fastapi.security.oauth2`. -## `Security` em docoradores de `dependências` +## `Security` em decoradores de `dependencies` { #security-in-decorator-dependencies } -Da mesma forma que você pode definir uma `list` de `Depends` no parâmetro de `dependencias` do decorador (como explicado em [Dependências em decoradores de operações de rota](../../tutorial/dependencies/dependencies-in-path-operation-decorators.md){.internal-link target=_blank}), você também pode utilizar `Security` com escopos lá. +Da mesma forma que você pode definir uma `list` de `Depends` no parâmetro `dependencies` do decorador (como explicado em [Dependências em decoradores de operações de rota](../../tutorial/dependencies/dependencies-in-path-operation-decorators.md){.internal-link target=_blank}), você também pode utilizar `Security` com escopos lá. diff --git a/docs/pt/docs/advanced/settings.md b/docs/pt/docs/advanced/settings.md index cdc6400adf..6f5b7feae7 100644 --- a/docs/pt/docs/advanced/settings.md +++ b/docs/pt/docs/advanced/settings.md @@ -1,148 +1,30 @@ -# Configurações e Variáveis de Ambiente +# Configurações e Variáveis de Ambiente { #settings-and-environment-variables } -Em muitos casos a sua aplicação pode precisar de configurações externas, como chaves secretas, credenciais de banco de dados, credenciais para serviços de email, etc. +Em muitos casos, sua aplicação pode precisar de configurações externas, por exemplo chaves secretas, credenciais de banco de dados, credenciais para serviços de e-mail, etc. -A maioria dessas configurações é variável (podem mudar), como URLs de bancos de dados. E muitas delas podem conter dados sensíveis, como tokens secretos. +A maioria dessas configurações é variável (pode mudar), como URLs de banco de dados. E muitas podem ser sensíveis, como segredos. -Por isso é comum prover essas configurações como variáveis de ambiente que são utilizidas pela aplicação. - -## Variáveis de Ambiente +Por esse motivo, é comum fornecê-las em variáveis de ambiente lidas pela aplicação. /// tip | Dica -Se você já sabe o que são variáveis de ambiente e como utilizá-las, sinta-se livre para avançar para o próximo tópico. +Para entender variáveis de ambiente, você pode ler [Variáveis de Ambiente](../environment-variables.md){.internal-link target=_blank}. /// -Uma variável de ambiente (abreviada em inglês para "env var") é uma variável definida fora do código Python, no sistema operacional, e pode ser lida pelo seu código Python (ou por outros programas). +## Tipagem e validação { #types-and-validation } -Você pode criar e utilizar variáveis de ambiente no terminal, sem precisar utilizar Python: +Essas variáveis de ambiente só conseguem lidar com strings de texto, pois são externas ao Python e precisam ser compatíveis com outros programas e com o resto do sistema (e até com diferentes sistemas operacionais, como Linux, Windows, macOS). -//// tab | Linux, macOS, Windows Bash +Isso significa que qualquer valor lido em Python a partir de uma variável de ambiente será uma `str`, e qualquer conversão para um tipo diferente ou validação precisa ser feita em código. -
+## Pydantic `Settings` { #pydantic-settings } -```console -// Você pode criar uma env var MY_NAME usando -$ export MY_NAME="Wade Wilson" +Felizmente, o Pydantic fornece uma ótima utilidade para lidar com essas configurações vindas de variáveis de ambiente com Pydantic: Settings management. -// E utilizá-la em outros programas, como -$ echo "Hello $MY_NAME" +### Instalar `pydantic-settings` { #install-pydantic-settings } -Hello Wade Wilson -``` - -
- -//// - -//// tab | Windows PowerShell - -
- -```console -// Criando env var MY_NAME -$ $Env:MY_NAME = "Wade Wilson" - -// Usando em outros programas, como -$ echo "Hello $Env:MY_NAME" - -Hello Wade Wilson -``` - -
- -//// - -### Lendo variáveis de ambiente com Python - -Você também pode criar variáveis de ambiente fora do Python, no terminal (ou com qualquer outro método), e realizar a leitura delas no Python. - -Por exemplo, você pode definir um arquivo `main.py` com o seguinte código: - -```Python hl_lines="3" -import os - -name = os.getenv("MY_NAME", "World") -print(f"Hello {name} from Python") -``` - -/// tip | Dica - -O segundo parâmetro em `os.getenv()` é o valor padrão para o retorno. - -Se nenhum valor for informado, `None` é utilizado por padrão, aqui definimos `"World"` como o valor padrão a ser utilizado. - -/// - -E depois você pode executar esse arquivo: - -
- -```console -// Aqui ainda não definimos a env var -$ python main.py - -// Por isso obtemos o valor padrão - -Hello World from Python - -// Mas se definirmos uma variável de ambiente primeiro -$ export MY_NAME="Wade Wilson" - -// E executarmos o programa novamente -$ python main.py - -// Agora ele pode ler a variável de ambiente - -Hello Wade Wilson from Python -``` - -
- -Como variáveis de ambiente podem ser definidas fora do código da aplicação, mas acessadas pela aplicação, e não precisam ser armazenadas (versionadas com `git`) junto dos outros arquivos, é comum utilizá-las para guardar configurações. - -Você também pode criar uma variável de ambiente específica para uma invocação de um programa, que é acessível somente para esse programa, e somente enquanto ele estiver executando. - -Para fazer isso, crie a variável imediatamente antes de iniciar o programa, na mesma linha: - -
- -```console -// Criando uma env var MY_NAME na mesma linha da execução do programa -$ MY_NAME="Wade Wilson" python main.py - -// Agora a aplicação consegue ler a variável de ambiente - -Hello Wade Wilson from Python - -// E a variável deixa de existir após isso -$ python main.py - -Hello World from Python -``` - -
- -/// tip | Dica - -Você pode ler mais sobre isso em: The Twelve-Factor App: Configurações. - -/// - -### Tipagem e Validação - -Essas variáveis de ambiente suportam apenas strings, por serem externas ao Python e por que precisam ser compatíveis com outros programas e o resto do sistema (e até mesmo com outros sistemas operacionais, como Linux, Windows e macOS). - -Isso significa que qualquer valor obtido de uma variável de ambiente em Python terá o tipo `str`, e qualquer conversão para um tipo diferente ou validação deve ser realizada no código. - -## Pydantic `Settings` - -Por sorte, o Pydantic possui uma funcionalidade para lidar com essas configurações vindas de variáveis de ambiente utilizando Pydantic: Settings management. - -### Instalando `pydantic-settings` - -Primeiro, instale o pacote `pydantic-settings`: +Primeiro, certifique-se de criar seu [ambiente virtual](../virtual-environments.md){.internal-link target=_blank}, ativá-lo e então instalar o pacote `pydantic-settings`:
@@ -153,7 +35,7 @@ $ pip install pydantic-settings
-Ele também está incluído no fastapi quando você instala com a opção `all`: +Ele também vem incluído quando você instala os extras `all` com:
@@ -164,57 +46,57 @@ $ pip install "fastapi[all]"
-/// info +/// info | Informação -Na v1 do Pydantic ele estava incluído no pacote principal. Agora ele está distribuido como um pacote independente para que você possa optar por instalar ou não caso você não precise dessa funcionalidade. +No Pydantic v1 ele vinha incluído no pacote principal. Agora é distribuído como um pacote independente para que você possa optar por instalá-lo ou não, caso não precise dessa funcionalidade. /// -### Criando o objeto `Settings` +### Criar o objeto `Settings` { #create-the-settings-object } -Importe a classe `BaseSettings` do Pydantic e crie uma nova subclasse, de forma parecida com um modelo do Pydantic. +Importe `BaseSettings` do Pydantic e crie uma subclasse, muito parecido com um modelo do Pydantic. -Os atributos da classe são declarados com anotações de tipo, e possíveis valores padrão, da mesma maneira que os modelos do Pydantic. +Da mesma forma que com modelos do Pydantic, você declara atributos de classe com anotações de tipo e, possivelmente, valores padrão. -Você pode utilizar todas as ferramentas e funcionalidades de validação que são utilizadas nos modelos do Pydantic, como tipos de dados diferentes e validações adicionei com `Field()`. +Você pode usar as mesmas funcionalidades e ferramentas de validação que usa em modelos do Pydantic, como diferentes tipos de dados e validações adicionais com `Field()`. //// tab | Pydantic v2 -{* ../../docs_src/settings/tutorial001.py hl[2,5:8,11] *} +{* ../../docs_src/settings/tutorial001_py39.py hl[2,5:8,11] *} //// //// tab | Pydantic v1 -/// info +/// info | Informação -Na versão 1 do Pydantic você importaria `BaseSettings` diretamente do módulo `pydantic` em vez do módulo `pydantic_settings`. +No Pydantic v1 você importaria `BaseSettings` diretamente de `pydantic` em vez de `pydantic_settings`. /// -{* ../../docs_src/settings/tutorial001_pv1.py hl[2,5:8,11] *} +{* ../../docs_src/settings/tutorial001_pv1_py39.py hl[2,5:8,11] *} //// /// tip | Dica -Se você quiser algo pronto para copiar e colar na sua aplicação, não use esse exemplo, mas sim o exemplo abaixo. +Se você quer algo rápido para copiar e colar, não use este exemplo, use o último abaixo. /// -Portanto, quando você cria uma instância da classe `Settings` (nesse caso, o objeto `settings`), o Pydantic lê as variáveis de ambiente sem diferenciar maiúsculas e minúsculas, por isso, uma variável maiúscula `APP_NAME` será usada para o atributo `app_name`. +Então, quando você cria uma instância dessa classe `Settings` (neste caso, no objeto `settings`), o Pydantic vai ler as variáveis de ambiente sem diferenciar maiúsculas de minúsculas; assim, uma variável em maiúsculas `APP_NAME` ainda será lida para o atributo `app_name`. -Depois ele irá converter e validar os dados. Assim, quando você utilizar aquele objeto `settings`, os dados terão o tipo que você declarou (e.g. `items_per_user` será do tipo `int`). +Em seguida, ele converterá e validará os dados. Assim, quando você usar esse objeto `settings`, terá dados dos tipos que declarou (por exemplo, `items_per_user` será um `int`). -### Usando o objeto `settings` +### Usar o `settings` { #use-the-settings } -Depois, Você pode utilizar o novo objeto `settings` na sua aplicação: +Depois você pode usar o novo objeto `settings` na sua aplicação: -{* ../../docs_src/settings/tutorial001.py hl[18:20] *} +{* ../../docs_src/settings/tutorial001_py39.py hl[18:20] *} -### Executando o servidor +### Executar o servidor { #run-the-server } -No próximo passo, você pode inicializar o servidor passando as configurações em forma de variáveis de ambiente, por exemplo, você poderia definir `ADMIN_EMAIL` e `APP_NAME` da seguinte forma: +Em seguida, você executaria o servidor passando as configurações como variáveis de ambiente, por exemplo, você poderia definir `ADMIN_EMAIL` e `APP_NAME` com:
@@ -228,118 +110,118 @@ $ ADMIN_EMAIL="deadpool@example.com" APP_NAME="ChimichangApp" fastapi run main.p /// tip | Dica -Para definir múltiplas variáveis de ambiente para um único comando basta separá-las utilizando espaços, e incluir todas elas antes do comando. +Para definir várias variáveis de ambiente para um único comando, basta separá-las com espaço e colocá-las todas antes do comando. /// -Assim, o atributo `admin_email` seria definido como `"deadpool@example.com"`. +Então a configuração `admin_email` seria definida como `"deadpool@example.com"`. -`app_name` seria `"ChimichangApp"`. +O `app_name` seria `"ChimichangApp"`. -E `items_per_user` manteria o valor padrão de `50`. +E `items_per_user` manteria seu valor padrão de `50`. -## Configurações em um módulo separado +## Configurações em outro módulo { #settings-in-another-module } -Você também pode incluir essas configurações em um arquivo de um módulo separado como visto em [Bigger Applications - Multiple Files](../tutorial/bigger-applications.md){.internal-link target=\_blank}. +Você pode colocar essas configurações em outro arquivo de módulo como visto em [Aplicações Maiores - Múltiplos Arquivos](../tutorial/bigger-applications.md){.internal-link target=_blank}. -Por exemplo, você pode adicionar um arquivo `config.py` com: +Por exemplo, você poderia ter um arquivo `config.py` com: -{* ../../docs_src/settings/app01/config.py *} +{* ../../docs_src/settings/app01_py39/config.py *} -E utilizar essa configuração em `main.py`: +E então usá-lo em um arquivo `main.py`: -{* ../../docs_src/settings/app01/main.py hl[3,11:13] *} +{* ../../docs_src/settings/app01_py39/main.py hl[3,11:13] *} /// tip | Dica -Você também precisa incluir um arquivo `__init__.py` como visto em [Bigger Applications - Multiple Files](../tutorial/bigger-applications.md){.internal-link target=\_blank}. +Você também precisaria de um arquivo `__init__.py` como visto em [Aplicações Maiores - Múltiplos Arquivos](../tutorial/bigger-applications.md){.internal-link target=_blank}. /// -## Configurações em uma dependência +## Configurações em uma dependência { #settings-in-a-dependency } -Em certas ocasiões, pode ser útil fornecer essas configurações a partir de uma dependência, em vez de definir um objeto global `settings` que é utilizado em toda a aplicação. +Em algumas ocasiões, pode ser útil fornecer as configurações a partir de uma dependência, em vez de ter um objeto global `settings` usado em todos os lugares. -Isso é especialmente útil durante os testes, já que é bastante simples sobrescrever uma dependência com suas configurações personalizadas. +Isso pode ser especialmente útil durante os testes, pois é muito fácil sobrescrever uma dependência com suas próprias configurações personalizadas. -### O arquivo de configuração +### O arquivo de configuração { #the-config-file } -Baseando-se no exemplo anterior, seu arquivo `config.py` seria parecido com isso: +Vindo do exemplo anterior, seu arquivo `config.py` poderia ser assim: -{* ../../docs_src/settings/app02/config.py hl[10] *} +{* ../../docs_src/settings/app02_an_py39/config.py hl[10] *} -Perceba que dessa vez não criamos uma instância padrão `settings = Settings()`. +Perceba que agora não criamos uma instância padrão `settings = Settings()`. -### O arquivo principal da aplicação +### O arquivo principal da aplicação { #the-main-app-file } -Agora criamos a dependência que retorna um novo objeto `config.Settings()`. +Agora criamos uma dependência que retorna um novo `config.Settings()`. {* ../../docs_src/settings/app02_an_py39/main.py hl[6,12:13] *} /// tip | Dica -Vamos discutir sobre `@lru_cache` logo mais. +Vamos discutir o `@lru_cache` em breve. -Por enquanto, você pode considerar `get_settings()` como uma função normal. +Por enquanto, você pode assumir que `get_settings()` é uma função normal. /// -E então podemos declarar essas configurações como uma dependência na função de operação da rota e utilizar onde for necessário. +E então podemos exigi-la na *função de operação de rota* como dependência e usá-la onde for necessário. {* ../../docs_src/settings/app02_an_py39/main.py hl[17,19:21] *} -### Configurações e testes +### Configurações e testes { #settings-and-testing } -Então seria muito fácil fornecer uma configuração diferente durante a execução dos testes sobrescrevendo a dependência de `get_settings`: +Então seria muito fácil fornecer um objeto de configurações diferente durante os testes criando uma sobrescrita de dependência para `get_settings`: -{* ../../docs_src/settings/app02/test_main.py hl[9:10,13,21] *} +{* ../../docs_src/settings/app02_an_py39/test_main.py hl[9:10,13,21] *} -Na sobrescrita da dependência, definimos um novo valor para `admin_email` quando instanciamos um novo objeto `Settings`, e então retornamos esse novo objeto. +Na sobrescrita da dependência definimos um novo valor para `admin_email` ao criar o novo objeto `Settings`, e então retornamos esse novo objeto. -Após isso, podemos testar se o valor está sendo utilizado. +Depois podemos testar que ele é usado. -## Lendo um arquivo `.env` +## Lendo um arquivo `.env` { #reading-a-env-file } -Se você tiver muitas configurações que variem bastante, talvez em ambientes distintos, pode ser útil colocá-las em um arquivo e depois lê-las como se fossem variáveis de ambiente. +Se você tiver muitas configurações que possivelmente mudam bastante, talvez em diferentes ambientes, pode ser útil colocá-las em um arquivo e então lê-las como se fossem variáveis de ambiente. -Essa prática é tão comum que possui um nome, essas variáveis de ambiente normalmente são colocadas em um arquivo `.env`, e esse arquivo é chamado de "dotenv". +Essa prática é tão comum que tem um nome: essas variáveis de ambiente são comumente colocadas em um arquivo `.env`, e o arquivo é chamado de "dotenv". /// tip | Dica -Um arquivo iniciando com um ponto final (`.`) é um arquivo oculto em sistemas baseados em Unix, como Linux e MacOS. +Um arquivo começando com um ponto (`.`) é um arquivo oculto em sistemas tipo Unix, como Linux e macOS. -Mas um arquivo dotenv não precisa ter esse nome exato. +Mas um arquivo dotenv não precisa ter exatamente esse nome de arquivo. /// -Pydantic suporta a leitura desses tipos de arquivos utilizando uma biblioteca externa. Você pode ler mais em Pydantic Settings: Dotenv (.env) support. +O Pydantic tem suporte para leitura desses tipos de arquivos usando uma biblioteca externa. Você pode ler mais em Pydantic Settings: Dotenv (.env) support. /// tip | Dica -Para que isso funcione você precisa executar `pip install python-dotenv`. +Para isso funcionar, você precisa executar `pip install python-dotenv`. /// -### O arquivo `.env` +### O arquivo `.env` { #the-env-file } -Você pode definir um arquivo `.env` com o seguinte conteúdo: +Você poderia ter um arquivo `.env` com: ```bash ADMIN_EMAIL="deadpool@example.com" APP_NAME="ChimichangApp" ``` -### Obtendo configurações do `.env` +### Ler configurações do `.env` { #read-settings-from-env } -E então adicionar o seguinte código em `config.py`: +E então atualizar seu `config.py` com: //// tab | Pydantic v2 -{* ../../docs_src/settings/app03_an/config.py hl[9] *} +{* ../../docs_src/settings/app03_an_py39/config.py hl[9] *} /// tip | Dica -O atributo `model_config` é usado apenas para configuração do Pydantic. Você pode ler mais em Pydantic Model Config. +O atributo `model_config` é usado apenas para configuração do Pydantic. Você pode ler mais em Pydantic: Concepts: Configuration. /// @@ -347,7 +229,7 @@ O atributo `model_config` é usado apenas para configuração do Pydantic. Você //// tab | Pydantic v1 -{* ../../docs_src/settings/app03_an/config_pv1.py hl[9:10] *} +{* ../../docs_src/settings/app03_an_py39/config_pv1.py hl[9:10] *} /// tip | Dica @@ -357,48 +239,48 @@ A classe `Config` é usada apenas para configuração do Pydantic. Você pode le //// -/// info +/// info | Informação -Na versão 1 do Pydantic a configuração é realizada por uma classe interna `Config`, na versão 2 do Pydantic isso é feito com o atributo `model_config`. Esse atributo recebe um `dict`, para utilizar o autocomplete e checagem de erros do seu editor de texto você pode importar e utilizar `SettingsConfigDict` para definir esse `dict`. +Na versão 1 do Pydantic a configuração era feita em uma classe interna `Config`, na versão 2 do Pydantic é feita em um atributo `model_config`. Esse atributo recebe um `dict`, e para ter autocompletar e erros inline você pode importar e usar `SettingsConfigDict` para definir esse `dict`. /// -Aqui definimos a configuração `env_file` dentro da classe `Settings` do Pydantic, e definimos o valor como o nome do arquivo dotenv que queremos utilizar. +Aqui definimos a configuração `env_file` dentro da sua classe `Settings` do Pydantic e definimos o valor como o nome do arquivo dotenv que queremos usar. -### Declarando `Settings` apenas uma vez com `lru_cache` +### Criando o `Settings` apenas uma vez com `lru_cache` { #creating-the-settings-only-once-with-lru-cache } -Ler o conteúdo de um arquivo em disco normalmente é uma operação custosa (lenta), então você provavelmente quer fazer isso apenas um vez e reutilizar o mesmo objeto settings depois, em vez de ler os valores a cada requisição. +Ler um arquivo do disco normalmente é uma operação custosa (lenta), então você provavelmente vai querer fazer isso apenas uma vez e depois reutilizar o mesmo objeto de configurações, em vez de lê-lo a cada requisição. -Mas cada vez que fazemos: +Mas toda vez que fizermos: ```Python Settings() ``` -um novo objeto `Settings` é instanciado, e durante a instanciação, o arquivo `.env` é lido novamente. +um novo objeto `Settings` seria criado e, na criação, ele leria o arquivo `.env` novamente. -Se a função da dependência fosse apenas: +Se a função de dependência fosse assim: ```Python def get_settings(): return Settings() ``` -Iriamos criar um novo objeto a cada requisição, e estaríamos lendo o arquivo `.env` a cada requisição. ⚠️ +criaríamos esse objeto para cada requisição e leríamos o arquivo `.env` para cada requisição. ⚠️ -Mas como estamos utilizando o decorador `@lru_cache` acima, o objeto `Settings` é criado apenas uma vez, na primeira vez que a função é chamada. ✔️ +Mas como estamos usando o decorador `@lru_cache` por cima, o objeto `Settings` será criado apenas uma vez, na primeira vez em que for chamado. ✔️ {* ../../docs_src/settings/app03_an_py39/main.py hl[1,11] *} -Dessa forma, todas as chamadas da função `get_settings()` nas dependências das próximas requisições, em vez de executar o código interno de `get_settings()` e instanciar um novo objeto `Settings`, irão retornar o mesmo objeto que foi retornado na primeira chamada, de novo e de novo. +Em qualquer chamada subsequente de `get_settings()` nas dependências das próximas requisições, em vez de executar o código interno de `get_settings()` e criar um novo objeto `Settings`, ele retornará o mesmo objeto que foi retornado na primeira chamada, repetidamente. -#### Detalhes Técnicos de `lru_cache` +#### Detalhes Técnicos do `lru_cache` { #lru-cache-technical-details } -`@lru_cache` modifica a função decorada para retornar o mesmo valor que foi retornado na primeira vez, em vez de calculá-lo novamente, executando o código da função toda vez. +`@lru_cache` modifica a função que decora para retornar o mesmo valor que foi retornado na primeira vez, em vez de calculá-lo novamente executando o código da função todas as vezes. -Assim, a função abaixo do decorador é executada uma única vez para cada combinação dos argumentos passados. E os valores retornados para cada combinação de argumentos são sempre reutilizados para cada nova chamada da função com a mesma combinação de argumentos. +Assim, a função abaixo dele será executada uma vez para cada combinação de argumentos. E então os valores retornados para cada uma dessas combinações de argumentos serão usados repetidamente sempre que a função for chamada com exatamente a mesma combinação de argumentos. -Por exemplo, se você definir uma função: +Por exemplo, se você tiver uma função: ```Python @lru_cache @@ -406,59 +288,59 @@ def say_hi(name: str, salutation: str = "Ms."): return f"Hello {salutation} {name}" ``` -Seu programa poderia executar dessa forma: +seu programa poderia executar assim: ```mermaid sequenceDiagram -participant code as Código +participant code as Code participant function as say_hi() -participant execute as Executar Função +participant execute as Execute function rect rgba(0, 255, 0, .1) code ->> function: say_hi(name="Camila") - function ->> execute: executar código da função - execute ->> code: retornar o resultado + function ->> execute: execute function code + execute ->> code: return the result end rect rgba(0, 255, 255, .1) code ->> function: say_hi(name="Camila") - function ->> code: retornar resultado armazenado + function ->> code: return stored result end rect rgba(0, 255, 0, .1) code ->> function: say_hi(name="Rick") - function ->> execute: executar código da função - execute ->> code: retornar o resultado + function ->> execute: execute function code + execute ->> code: return the result end rect rgba(0, 255, 0, .1) code ->> function: say_hi(name="Rick", salutation="Mr.") - function ->> execute: executar código da função - execute ->> code: retornar o resultado + function ->> execute: execute function code + execute ->> code: return the result end rect rgba(0, 255, 255, .1) code ->> function: say_hi(name="Rick") - function ->> code: retornar resultado armazenado + function ->> code: return stored result end rect rgba(0, 255, 255, .1) code ->> function: say_hi(name="Camila") - function ->> code: retornar resultado armazenado + function ->> code: return stored result end ``` -No caso da nossa dependência `get_settings()`, a função não recebe nenhum argumento, então ela sempre retorna o mesmo valor. +No caso da nossa dependência `get_settings()`, a função nem recebe argumentos, então ela sempre retorna o mesmo valor. -Dessa forma, ela se comporta praticamente como uma variável global, mas ao ser utilizada como uma função de uma dependência, pode facilmente ser sobrescrita durante os testes. +Dessa forma, ela se comporta quase como se fosse apenas uma variável global. Mas como usa uma função de dependência, podemos sobrescrevê-la facilmente para testes. -`@lru_cache` é definido no módulo `functools` que faz parte da biblioteca padrão do Python, você pode ler mais sobre esse decorador no link Python Docs sobre `@lru_cache`. +`@lru_cache` faz parte de `functools`, que faz parte da biblioteca padrão do Python; você pode ler mais sobre isso na documentação do Python para `@lru_cache`. -## Recapitulando +## Recapitulando { #recap } -Você pode usar o módulo Pydantic Settings para gerenciar as configurações de sua aplicação, utilizando todo o poder dos modelos Pydantic. +Você pode usar Pydantic Settings para lidar com as configurações da sua aplicação, com todo o poder dos modelos Pydantic. -- Utilizar dependências simplifica os testes. -- Você pode utilizar arquivos .env junto das configurações do Pydantic. -- Utilizar o decorador `@lru_cache` evita que o arquivo .env seja lido de novo e de novo para cada requisição, enquanto permite que você sobrescreva durante os testes. +* Usando uma dependência você pode simplificar os testes. +* Você pode usar arquivos `.env` com ele. +* Usar `@lru_cache` permite evitar ler o arquivo dotenv repetidamente a cada requisição, enquanto permite sobrescrevê-lo durante os testes. diff --git a/docs/pt/docs/advanced/sub-applications.md b/docs/pt/docs/advanced/sub-applications.md index efc6bef647..c61d1e92a0 100644 --- a/docs/pt/docs/advanced/sub-applications.md +++ b/docs/pt/docs/advanced/sub-applications.md @@ -1,41 +1,41 @@ -# Sub Aplicações - Montagens +# Sub Aplicações - Montagens { #sub-applications-mounts } Se você precisar ter duas aplicações FastAPI independentes, cada uma com seu próprio OpenAPI e suas próprias interfaces de documentação, você pode ter um aplicativo principal e "montar" uma (ou mais) sub-aplicações. -## Montando uma aplicação **FastAPI** +## Montando uma aplicação **FastAPI** { #mounting-a-fastapi-application } -"Montar" significa adicionar uma aplicação completamente "independente" em um caminho específico, que então se encarrega de lidar com tudo sob esse caminho, com as operações de rota declaradas nessa sub-aplicação. +"Montar" significa adicionar uma aplicação completamente "independente" em um path específico, que então se encarrega de lidar com tudo sob esse path, com as _operações de rota_ declaradas nessa sub-aplicação. -### Aplicação de nível superior +### Aplicação de nível superior { #top-level-application } Primeiro, crie a aplicação principal, de nível superior, **FastAPI**, e suas *operações de rota*: -{* ../../docs_src/sub_applications/tutorial001.py hl[3,6:8] *} +{* ../../docs_src/sub_applications/tutorial001_py39.py hl[3, 6:8] *} -### Sub-aplicação +### Sub-aplicação { #sub-application } Em seguida, crie sua sub-aplicação e suas *operações de rota*. Essa sub-aplicação é apenas outra aplicação FastAPI padrão, mas esta é a que será "montada": -{* ../../docs_src/sub_applications/tutorial001.py hl[11,14:16] *} +{* ../../docs_src/sub_applications/tutorial001_py39.py hl[11, 14:16] *} -### Monte a sub-aplicação +### Monte a sub-aplicação { #mount-the-sub-application } Na sua aplicação de nível superior, `app`, monte a sub-aplicação, `subapi`. -Neste caso, ela será montada no caminho `/subapi`: +Neste caso, ela será montada no path `/subapi`: -{* ../../docs_src/sub_applications/tutorial001.py hl[11,19] *} +{* ../../docs_src/sub_applications/tutorial001_py39.py hl[11, 19] *} -### Verifique a documentação automática da API +### Verifique a documentação automática da API { #check-the-automatic-api-docs } -Agora, execute `uvicorn` com a aplicação principal, se o seu arquivo for `main.py`, seria: +Agora, execute o comando `fastapi` com o seu arquivo:
```console -$ uvicorn main:app --reload +$ fastapi dev main.py INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) ``` @@ -50,17 +50,17 @@ Você verá a documentação automática da API para a aplicação principal, in E então, abra a documentação para a sub-aplicação, em http://127.0.0.1:8000/subapi/docs. -Você verá a documentação automática da API para a sub-aplicação, incluindo apenas suas próprias _operações de rota_, todas sob o prefixo de sub-caminho correto `/subapi`: +Você verá a documentação automática da API para a sub-aplicação, incluindo apenas suas próprias _operações de rota_, todas sob o prefixo de sub-path correto `/subapi`: Se você tentar interagir com qualquer uma das duas interfaces de usuário, elas funcionarão corretamente, porque o navegador será capaz de se comunicar com cada aplicação ou sub-aplicação específica. -### Detalhes Técnicos: `root_path` +### Detalhes Técnicos: `root_path` { #technical-details-root-path } -Quando você monta uma sub-aplicação como descrito acima, o FastAPI se encarrega de comunicar o caminho de montagem para a sub-aplicação usando um mecanismo da especificação ASGI chamado `root_path`. +Quando você monta uma sub-aplicação como descrito acima, o FastAPI se encarrega de comunicar o path de montagem para a sub-aplicação usando um mecanismo da especificação ASGI chamado `root_path`. -Dessa forma, a sub-aplicação saberá usar esse prefixo de caminho para a interface de documentação. +Dessa forma, a sub-aplicação saberá usar esse prefixo de path para a interface de documentação. E a sub-aplicação também poderia ter suas próprias sub-aplicações montadas e tudo funcionaria corretamente, porque o FastAPI lida com todos esses `root_path`s automaticamente. diff --git a/docs/pt/docs/advanced/templates.md b/docs/pt/docs/advanced/templates.md index 65ff89faee..eb64e72bb8 100644 --- a/docs/pt/docs/advanced/templates.md +++ b/docs/pt/docs/advanced/templates.md @@ -1,4 +1,4 @@ -# Templates +# Templates { #templates } Você pode usar qualquer template engine com o **FastAPI**. @@ -6,28 +6,30 @@ Uma escolha comum é o Jinja2, o mesmo usado pelo Flask e outras ferramentas. Existem utilitários para configurá-lo facilmente que você pode usar diretamente em sua aplicação **FastAPI** (fornecidos pelo Starlette). -## Instalação de dependências +## Instalar dependências { #install-dependencies } -Para instalar o `jinja2`, siga o código abaixo: +Certifique-se de criar um [ambiente virtual](../virtual-environments.md){.internal-link target=_blank}, ativá-lo e instalar `jinja2`:
```console $ pip install jinja2 + +---> 100% ```
-## Usando `Jinja2Templates` +## Usando `Jinja2Templates` { #using-jinja2templates } * Importe `Jinja2Templates`. -* Crie um `templates` que você possa reutilizar posteriormente. +* Crie um objeto `templates` que você possa reutilizar posteriormente. * Declare um parâmetro `Request` no *path operation* que retornará um template. -* Use o `template` que você criou para renderizar e retornar uma `TemplateResponse`, passe o nome do template, o request object, e um "context" dict com pares chave-valor a serem usados dentro do template do Jinja2. +* Use o `templates` que você criou para renderizar e retornar uma `TemplateResponse`, passe o nome do template, o objeto `request` e um dicionário "context" com pares chave-valor a serem usados dentro do template do Jinja2. -{* ../../docs_src/templates/tutorial001.py hl[4,11,15:18] *} +{* ../../docs_src/templates/tutorial001_py39.py hl[4,11,15:18] *} -/// note +/// note | Nota Antes do FastAPI 0.108.0, Starlette 0.29.0, `name` era o primeiro parâmetro. @@ -49,7 +51,7 @@ Você também poderia usar `from starlette.templating import Jinja2Templates`. /// -## Escrevendo Templates +## Escrevendo templates { #writing-templates } Então você pode escrever um template em `templates/item.html`, por exemplo: @@ -57,7 +59,7 @@ Então você pode escrever um template em `templates/item.html`, por exemplo: {!../../docs_src/templates/templates/item.html!} ``` -### Interpolação de Valores no Template +### Valores de contexto do template { #template-context-values } No código HTML que contém: @@ -81,7 +83,7 @@ Por exemplo, dado um ID de valor `42`, aparecerá: Item ID: 42 ``` -### Argumentos do `url_for` +### Argumentos do `url_for` no template { #template-url-for-arguments } Você também pode usar `url_for()` dentro do template, ele recebe como argumentos os mesmos argumentos que seriam usados pela sua *path operation function*. @@ -103,9 +105,9 @@ Por exemplo, com um ID de `42`, isso renderizará: ``` -## Templates e Arquivos Estáticos +## Templates e arquivos estáticos { #templates-and-static-files } -Você também pode usar `url_for()` dentro do template e usá-lo, por examplo, com o `StaticFiles` que você montou com o `name="static"`. +Você também pode usar `url_for()` dentro do template e usá-lo, por exemplo, com o `StaticFiles` que você montou com o `name="static"`. ```jinja hl_lines="4" {!../../docs_src/templates/templates/item.html!} @@ -117,8 +119,8 @@ Neste exemplo, ele seria vinculado a um arquivo CSS em `static/styles.css` com: {!../../docs_src/templates/static/styles.css!} ``` -E como você está usando `StaticFiles`, este arquivo CSS será automaticamente servido pela sua aplicação FastAPI na URL `/static/styles.css`. +E como você está usando `StaticFiles`, este arquivo CSS será automaticamente servido pela sua aplicação **FastAPI** na URL `/static/styles.css`. -## Mais detalhes +## Mais detalhes { #more-details } Para obter mais detalhes, incluindo como testar templates, consulte a documentação da Starlette sobre templates. diff --git a/docs/pt/docs/advanced/testing-dependencies.md b/docs/pt/docs/advanced/testing-dependencies.md index 3ede4741d9..52b12dddbc 100644 --- a/docs/pt/docs/advanced/testing-dependencies.md +++ b/docs/pt/docs/advanced/testing-dependencies.md @@ -1,6 +1,6 @@ -# Testando Dependências com Sobreposição (Overrides) +# Testando Dependências com Sobreposições { #testing-dependencies-with-overrides } -## Sobrepondo dependências durante os testes +## Sobrepondo dependências durante os testes { #overriding-dependencies-during-testing } Existem alguns cenários onde você deseje sobrepor uma dependência durante os testes. @@ -8,7 +8,7 @@ Você não quer que a dependência original execute (e nenhuma das subdependênc Em vez disso, você deseja fornecer uma dependência diferente que será usada somente durante os testes (possivelmente apenas para alguns testes específicos) e fornecerá um valor que pode ser usado onde o valor da dependência original foi usado. -### Casos de uso: serviço externo +### Casos de uso: serviço externo { #use-cases-external-service } Um exemplo pode ser que você possua um provedor de autenticação externo que você precisa chamar. @@ -20,7 +20,7 @@ Você provavelmente quer testar o provedor externo uma vez, mas não necessariam Neste caso, você pode sobrepor (*override*) a dependência que chama o provedor, e utilizar uma dependência customizada que retorna um *mock* do usuário, apenas para os seus testes. -### Utilize o atributo `app.dependency_overrides` +### Utilize o atributo `app.dependency_overrides` { #use-the-app-dependency-overrides-attribute } Para estes casos, a sua aplicação **FastAPI** possui o atributo `app.dependency_overrides`. Ele é um simples `dict`. @@ -34,7 +34,7 @@ E então o **FastAPI** chamará a sobreposição no lugar da dependência origin Você pode definir uma sobreposição de dependência para uma dependência que é utilizada em qualquer lugar da sua aplicação **FastAPI**. -A dependência original pode estar sendo utilizada em uma *função de operação de rota*, um *docorador de operação de rota* (quando você não utiliza o valor retornado), uma chamada ao `.include_router()`, etc. +A dependência original pode estar sendo utilizada em uma *função de operação de rota*, um *decorador de operação de rota* (quando você não utiliza o valor retornado), uma chamada ao `.include_router()`, etc. O FastAPI ainda poderá sobrescrevê-lo. @@ -48,6 +48,6 @@ app.dependency_overrides = {} /// tip | Dica -Se você quer sobrepor uma dependência apenas para alguns testes, você pode definir a sobreposição no início do testes (dentro da função de teste) e reiniciá-la ao final (no final da função de teste). +Se você quer sobrepor uma dependência apenas para alguns testes, você pode definir a sobreposição no início do teste (dentro da função de teste) e reiniciá-la ao final (no final da função de teste). /// diff --git a/docs/pt/docs/advanced/testing-events.md b/docs/pt/docs/advanced/testing-events.md index 6113c99136..971b437630 100644 --- a/docs/pt/docs/advanced/testing-events.md +++ b/docs/pt/docs/advanced/testing-events.md @@ -1,5 +1,11 @@ -# Testando Eventos: inicialização - encerramento +# Testando eventos: lifespan e inicialização - encerramento { #testing-events-lifespan-and-startup-shutdown } -Quando você precisa que os seus manipuladores de eventos (`startup` e `shutdown`) sejam executados em seus testes, você pode utilizar o `TestClient` usando a instrução `with`: +Quando você precisa que o `lifespan` seja executado em seus testes, você pode utilizar o `TestClient` com a instrução `with`: -{* ../../docs_src/app_testing/tutorial003.py hl[9:12,20:24] *} +{* ../../docs_src/app_testing/tutorial004_py39.py hl[9:15,18,27:28,30:32,41:43] *} + +Você pode ler mais detalhes sobre o ["Executando lifespan em testes no site oficial da documentação do Starlette."](https://www.starlette.dev/lifespan/#running-lifespan-in-tests) + +Para os eventos `startup` e `shutdown` descontinuados, você pode usar o `TestClient` da seguinte forma: + +{* ../../docs_src/app_testing/tutorial003_py39.py hl[9:12,20:24] *} diff --git a/docs/pt/docs/advanced/testing-websockets.md b/docs/pt/docs/advanced/testing-websockets.md index 9b81936552..d9d1723a64 100644 --- a/docs/pt/docs/advanced/testing-websockets.md +++ b/docs/pt/docs/advanced/testing-websockets.md @@ -1,10 +1,10 @@ -# Testando WebSockets +# Testando WebSockets { #testing-websockets } Você pode usar o mesmo `TestClient` para testar WebSockets. Para isso, você utiliza o `TestClient` dentro de uma instrução `with`, conectando com o WebSocket: -{* ../../docs_src/app_testing/tutorial002.py hl[27:31] *} +{* ../../docs_src/app_testing/tutorial002_py39.py hl[27:31] *} /// note | Nota diff --git a/docs/pt/docs/advanced/using-request-directly.md b/docs/pt/docs/advanced/using-request-directly.md index f4fb0ed8f3..ab1ef9ff4d 100644 --- a/docs/pt/docs/advanced/using-request-directly.md +++ b/docs/pt/docs/advanced/using-request-directly.md @@ -1,10 +1,10 @@ -# Utilizando o Request diretamente +# Utilizando o Request diretamente { #using-the-request-directly } Até agora você declarou as partes da requisição que você precisa utilizando os seus tipos. Obtendo dados de: -* Os parâmetros das rotas. +* O path como parâmetros. * Cabeçalhos (*Headers*). * Cookies. * etc. @@ -13,7 +13,7 @@ E ao fazer isso, o **FastAPI** está validando as informações, convertendo-as Porém há situações em que você possa precisar acessar o objeto `Request` diretamente. -## Detalhes sobre o objeto `Request` +## Detalhes sobre o objeto `Request` { #details-about-the-request-object } Como o **FastAPI** é na verdade o **Starlette** por baixo, com camadas de diversas funcionalidades por cima, você pode utilizar o objeto `Request` do Starlette diretamente quando precisar. @@ -23,27 +23,27 @@ Embora qualquer outro parâmetro declarado normalmente (o corpo da requisição Mas há situações específicas onde é útil utilizar o objeto `Request`. -## Utilize o objeto `Request` diretamente +## Utilize o objeto `Request` diretamente { #use-the-request-object-directly } Vamos imaginar que você deseja obter o endereço de IP/host do cliente dentro da sua *função de operação de rota*. Para isso você precisa acessar a requisição diretamente. -{* ../../docs_src/using_request_directly/tutorial001.py hl[1,7:8] *} +{* ../../docs_src/using_request_directly/tutorial001_py39.py hl[1,7:8] *} Ao declarar o parâmetro com o tipo sendo um `Request` em sua *função de operação de rota*, o **FastAPI** saberá como passar o `Request` neste parâmetro. /// tip | Dica -Note que neste caso, nós estamos declarando o parâmetro da rota ao lado do parâmetro da requisição. +Note que neste caso, nós estamos declarando o parâmetro de path ao lado do parâmetro da requisição. -Assim, o parâmetro da rota será extraído, validado, convertido para o tipo especificado e anotado com OpenAPI. +Assim, o parâmetro de path será extraído, validado, convertido para o tipo especificado e anotado com OpenAPI. Do mesmo jeito, você pode declarar qualquer outro parâmetro normalmente, e além disso, obter o `Request` também. /// -## Documentação do `Request` +## Documentação do `Request` { #request-documentation } Você pode ler mais sobre os detalhes do objeto `Request` no site da documentação oficial do Starlette.. diff --git a/docs/pt/docs/advanced/websockets.md b/docs/pt/docs/advanced/websockets.md index 721c0b403a..021a73bed3 100644 --- a/docs/pt/docs/advanced/websockets.md +++ b/docs/pt/docs/advanced/websockets.md @@ -1,10 +1,10 @@ -# WebSockets +# WebSockets { #websockets } Você pode usar WebSockets com **FastAPI**. -## Instalando `WebSockets` +## Instale `websockets` { #install-websockets } -Garanta que você criou um [ambiente virtual](../virtual-environments.md){.internal-link target=_blank}, o ativou e instalou o `websockets`: +Garanta que você criou um [ambiente virtual](../virtual-environments.md){.internal-link target=_blank}, o ativou e instalou o `websockets` (uma biblioteca Python que facilita o uso do protocolo "WebSocket"):
@@ -16,9 +16,9 @@ $ pip install websockets
-## Cliente WebSockets +## Cliente WebSockets { #websockets-client } -### Em produção +### Em produção { #in-production } Em seu sistema de produção, você provavelmente tem um frontend criado com um framework moderno como React, Vue.js ou Angular. @@ -38,13 +38,13 @@ Na produção, você teria uma das opções acima. Mas é a maneira mais simples de focar no lado do servidor de WebSockets e ter um exemplo funcional: -{* ../../docs_src/websockets/tutorial001.py hl[2,6:38,41:43] *} +{* ../../docs_src/websockets/tutorial001_py39.py hl[2,6:38,41:43] *} -## Criando um `websocket` +## Crie um `websocket` { #create-a-websocket } Em sua aplicação **FastAPI**, crie um `websocket`: -{*../../docs_src/websockets/tutorial001.py hl[46:47]*} +{* ../../docs_src/websockets/tutorial001_py39.py hl[1,46:47] *} /// note | Detalhes Técnicos @@ -54,15 +54,15 @@ A **FastAPI** fornece o mesmo `WebSocket` diretamente apenas como uma conveniên /// -## Aguardar por mensagens e enviar mensagens +## Aguarde mensagens e envie mensagens { #await-for-messages-and-send-messages } Em sua rota WebSocket você pode esperar (`await`) por mensagens e enviar mensagens. -{*../../docs_src/websockets/tutorial001.py hl[48:52]*} +{* ../../docs_src/websockets/tutorial001_py39.py hl[48:52] *} Você pode receber e enviar dados binários, de texto e JSON. -## Tente você mesmo +## Tente { #try-it } Se seu arquivo for nomeado `main.py`, execute sua aplicação com: @@ -96,7 +96,7 @@ Você pode enviar (e receber) muitas mensagens: E todas elas usarão a mesma conexão WebSocket. -## Usando `Depends` e outros +## Usando `Depends` e outros { #using-depends-and-others } Nos endpoints WebSocket você pode importar do `fastapi` e usar: @@ -109,7 +109,7 @@ Nos endpoints WebSocket você pode importar do `fastapi` e usar: Eles funcionam da mesma forma que para outros endpoints FastAPI/*operações de rota*: -{*../../docs_src/websockets/tutorial002_an_py310.py hl[68:69,82]*} +{* ../../docs_src/websockets/tutorial002_an_py310.py hl[68:69,82] *} /// info | Informação @@ -119,7 +119,7 @@ Você pode usar um código de fechamento dos http://127.0.0.1:8000. +Abra seu navegador em: http://127.0.0.1:8000. Lá você pode definir: -* O "Item ID", usado na rota. +* O "Item ID", usado no path. * O "Token" usado como um parâmetro de consulta. /// tip | Dica @@ -150,15 +150,15 @@ Com isso você pode conectar o WebSocket e então enviar e receber mensagens: -## Lidando com desconexões e múltiplos clientes +## Lidando com desconexões e múltiplos clientes { #handling-disconnections-and-multiple-clients } Quando uma conexão WebSocket é fechada, o `await websocket.receive_text()` levantará uma exceção `WebSocketDisconnect`, que você pode então capturar e lidar como neste exemplo. -{*../../docs_src/websockets/tutorial003_py39.py hl[79:81]*} +{* ../../docs_src/websockets/tutorial003_py39.py hl[79:81] *} Para testar: -* Abrar o aplicativo com várias abas do navegador. +* Abra o aplicativo com várias abas do navegador. * Escreva mensagens a partir delas. * Então feche uma das abas. @@ -172,13 +172,13 @@ Client #1596980209979 left the chat O app acima é um exemplo mínimo e simples para demonstrar como lidar e transmitir mensagens para várias conexões WebSocket. -Mas tenha em mente que, como tudo é manipulado na memória, em uma única lista, ele só funcionará enquanto o processo estiver em execução e só funcionará com um único processo. +Mas tenha em mente que, como tudo é manipulado na memória, em uma única list, ele só funcionará enquanto o processo estiver em execução e só funcionará com um único processo. Se você precisa de algo fácil de integrar com o FastAPI, mas que seja mais robusto, suportado por Redis, PostgreSQL ou outros, verifique o encode/broadcaster. /// -## Mais informações +## Mais informações { #more-info } Para aprender mais sobre as opções, verifique a documentação do Starlette para: diff --git a/docs/pt/docs/advanced/wsgi.md b/docs/pt/docs/advanced/wsgi.md index a36261e5ef..c3c21d430a 100644 --- a/docs/pt/docs/advanced/wsgi.md +++ b/docs/pt/docs/advanced/wsgi.md @@ -1,22 +1,22 @@ -# Adicionando WSGI - Flask, Django, entre outros +# Adicionando WSGI - Flask, Django, entre outros { #including-wsgi-flask-django-others } -Como você viu em [Sub Applications - Mounts](sub-applications.md){.internal-link target=_blank} e [Behind a Proxy](behind-a-proxy.md){.internal-link target=_blank}, você pode **"montar"** aplicações WSGI. +Como você viu em [Subaplicações - Montagens](sub-applications.md){.internal-link target=_blank} e [Atrás de um Proxy](behind-a-proxy.md){.internal-link target=_blank}, você pode montar aplicações WSGI. Para isso, você pode utilizar o `WSGIMiddleware` para encapsular a sua aplicação WSGI, como por exemplo Flask, Django, etc. -## Usando o `WSGIMiddleware` +## Usando `WSGIMiddleware` { #using-wsgimiddleware } Você precisa importar o `WSGIMiddleware`. -Em seguinda, encapsular a aplicação WSGI (e.g. Flask) com o middleware. +Em seguida, encapsule a aplicação WSGI (e.g. Flask) com o middleware. -E então **"montar"** em um caminho de rota. +E então monte isso sob um path. -{* ../../docs_src/wsgi/tutorial001.py hl[2:3,23] *} +{* ../../docs_src/wsgi/tutorial001_py39.py hl[2:3,3] *} -## Conferindo +## Confira { #check-it } -Agora todas as requisições sob o caminho `/v1/` serão manipuladas pela aplicação utilizando Flask. +Agora, todas as requisições sob o path `/v1/` serão manipuladas pela aplicação Flask. E o resto será manipulado pelo **FastAPI**. diff --git a/docs/pt/docs/alternatives.md b/docs/pt/docs/alternatives.md index 66cf3fe127..fd992ec951 100644 --- a/docs/pt/docs/alternatives.md +++ b/docs/pt/docs/alternatives.md @@ -1,34 +1,34 @@ -# Alternativas, Inspiração e Comparações +# Alternativas, Inspiração e Comparações { #alternatives-inspiration-and-comparisons } -O que inspirou o **FastAPI**, como ele se compara às alternativas e o que FastAPI aprendeu delas. +O que inspirou o **FastAPI**, como ele se compara às alternativas e o que ele aprendeu com elas. -## Introdução +## Introdução { #intro } -**FastAPI** não poderia existir se não fosse pelos trabalhos anteriores de outras pessoas. +**FastAPI** não existiria se não fosse pelo trabalho anterior de outras pessoas. -Houveram tantas ferramentas criadas que ajudaram a inspirar sua criação. +Houve muitas ferramentas criadas antes que ajudaram a inspirar sua criação. -Tenho evitado criar um novo framework por anos. Primeiramente tentei resolver todos os recursos cobertos pelo **FastAPI** utilizando muitos frameworks diferentes, plug-ins e ferramentas. +Tenho evitado criar um novo framework por vários anos. Primeiro tentei resolver todas as funcionalidades cobertas pelo **FastAPI** utilizando muitos frameworks, plug-ins e ferramentas diferentes. -Mas em algum ponto, não houve outra opção senão criar algo que fornecesse todos esses recursos, pegando as melhores idéias de ferramentas anteriores, e combinando eles da melhor forma possível, utilizando recursos da linguagem que não estavam disponíveis antes (_Type Hints_ no Python 3.6+). +Mas em algum momento, não havia outra opção senão criar algo que fornecesse todos esses recursos, pegando as melhores ideias de ferramentas anteriores e combinando-as da melhor maneira possível, usando funcionalidades da linguagem que nem sequer estavam disponíveis antes (anotações de tipo no Python 3.6+). -## Ferramentas anteriores +## Ferramentas anteriores { #previous-tools } -### Django +### Django { #django } -É o framework mais popular e largamente confiável. É utilizado para construir sistemas como o _Instagram_. +É o framework Python mais popular e amplamente confiável. É utilizado para construir sistemas como o Instagram. -É bem acoplado com banco de dados relacional (como MySQL ou PostgreSQL), então, tendo um banco de dados NoSQL (como Couchbase, MongoDB, Cassandra etc) como a principal ferramenta de armazenamento não é muito fácil. +É relativamente bem acoplado com bancos de dados relacionais (como MySQL ou PostgreSQL), então, ter um banco de dados NoSQL (como Couchbase, MongoDB, Cassandra, etc.) como mecanismo principal de armazenamento não é muito fácil. -Foi criado para gerar HTML no _backend_, não para criar APIs utilizando um _frontend_ moderno (como React, Vue.js e Angular) ou por outros sistemas (como dispositivos IoT) comunicando com ele. +Foi criado para gerar o HTML no backend, não para criar APIs usadas por um frontend moderno (como React, Vue.js e Angular) ou por outros sistemas (como dispositivos IoT) comunicando com ele. -### Django REST Framework +### Django REST Framework { #django-rest-framework } -Django REST framework foi criado para ser uma caixa de ferramentas flexível para construção de APIs web utilizando Django por baixo, para melhorar suas capacidades de API. +Django REST framework foi criado para ser uma caixa de ferramentas flexível para construção de APIs Web utilizando Django por baixo, para melhorar suas capacidades de API. -Ele é utilizado por muitas companhias incluindo Mozilla, Red Hat e Eventbrite. +Ele é utilizado por muitas empresas incluindo Mozilla, Red Hat e Eventbrite. -Ele foi um dos primeiros exemplos de **documentação automática de API**, e essa foi especificamente uma das primeiras idéias que inspirou "a busca por" **FastAPI**. +Foi um dos primeiros exemplos de **documentação automática de API**, e essa foi especificamente uma das primeiras ideias que inspirou "a busca por" **FastAPI**. /// note | Nota @@ -38,57 +38,57 @@ Django REST Framework foi criado por Tom Christie. O mesmo criador de Starlette /// check | **FastAPI** inspirado para -Ter uma documentação automática da API em interface web. +Ter uma interface web de documentação automática da API. /// -### Flask +### Flask { #flask } -Flask é um "microframework", não inclui integração com banco de dados nem muitas das coisas que vêm por padrão no Django. +Flask é um "microframework", não inclui integrações com banco de dados nem muitas das coisas que vêm por padrão no Django. -Sua simplicidade e flexibilidade permitem fazer coisas como utilizar bancos de dados NoSQL como principal sistema de armazenamento de dados. +Essa simplicidade e flexibilidade permitem fazer coisas como utilizar bancos de dados NoSQL como o principal sistema de armazenamento de dados. -Por ser tão simples, é relativamente intuitivo de aprender, embora a documentação esteja de forma mais técnica em alguns pontos. +Por ser muito simples, é relativamente intuitivo de aprender, embora a documentação se torne um pouco técnica em alguns pontos. -Ele é comumente utilizado por outras aplicações que não necessariamente precisam de banco de dados, gerenciamento de usuários, ou algum dos muitos recursos que já vem instalados no Django. Embora muitos desses recursos possam ser adicionados com plug-ins. +Ele também é comumente utilizado por outras aplicações que não necessariamente precisam de banco de dados, gerenciamento de usuários, ou qualquer uma das muitas funcionalidades que já vêm prontas no Django. Embora muitas dessas funcionalidades possam ser adicionadas com plug-ins. -Esse desacoplamento de partes, e sendo um "microframework" que pode ser extendido para cobrir exatamente o que é necessário era um recurso chave que eu queria manter. +Esse desacoplamento de partes, e ser um "microframework" que pode ser estendido para cobrir exatamente o que é necessário era uma funcionalidade chave que eu queria manter. -Dada a simplicidade do Flask, parecia uma ótima opção para construção de APIs. A próxima coisa a procurar era um "Django REST Framework" para Flask. +Dada a simplicidade do Flask, ele parecia uma boa opção para construção de APIs. A próxima coisa a encontrar era um "Django REST Framework" para Flask. /// check | **FastAPI** inspirado para -Ser um microframework. Fazer ele fácil para misturar e combinar com ferramentas e partes necessárias. +Ser um microframework. Tornar fácil misturar e combinar as ferramentas e partes necessárias. -Ser simples e com sistema de roteamento fácil de usar. +Ter um sistema de roteamento simples e fácil de usar. /// -### Requests +### Requests { #requests } -**FastAPI** não é uma alternativa para **Requests**. O escopo deles é muito diferente. +**FastAPI** na verdade não é uma alternativa ao **Requests**. O escopo deles é muito diferente. -Na verdade é comum utilizar Requests *dentro* de uma aplicação FastAPI. +Na verdade, é comum utilizar Requests dentro de uma aplicação FastAPI. -Ainda assim, FastAPI pegou alguma inspiração do Requests. +Ainda assim, o FastAPI tirou bastante inspiração do Requests. -**Requests** é uma biblioteca para interagir com APIs (como um cliente), enquanto **FastAPI** é uma biblioteca para *construir* APIs (como um servidor). +**Requests** é uma biblioteca para interagir com APIs (como um cliente), enquanto **FastAPI** é uma biblioteca para construir APIs (como um servidor). -Eles estão, mais ou menos, em pontas opostas, um complementando o outro. +Eles estão, mais ou menos, em pontas opostas, complementando-se. -Requests tem um projeto muito simples e intuitivo, fácil de usar, com padrões sensíveis. Mas ao mesmo tempo, é muito poderoso e customizável. +Requests tem um design muito simples e intuitivo, é muito fácil de usar, com padrões sensatos. Mas ao mesmo tempo, é muito poderoso e personalizável. É por isso que, como dito no site oficial: > Requests é um dos pacotes Python mais baixados de todos os tempos -O jeito de usar é muito simples. Por exemplo, para fazer uma requisição `GET`, você deveria escrever: +O jeito de usar é muito simples. Por exemplo, para fazer uma requisição `GET`, você escreveria: ```Python response = requests.get("http://example.com/some/url") ``` -A contra-parte da aplicação FastAPI, *rota de operação*, poderia parecer como: +A contra-parte na aplicação FastAPI, a operação de rota, poderia ficar assim: ```Python hl_lines="1" @app.get("/some/url") @@ -102,50 +102,50 @@ Veja as similaridades em `requests.get(...)` e `@app.get(...)`. * Ter uma API simples e intuitiva. * Utilizar nomes de métodos HTTP (operações) diretamente, de um jeito direto e intuitivo. -* Ter padrões sensíveis, mas customizações poderosas. +* Ter padrões sensatos, mas customizações poderosas. /// -### Swagger / OpenAPI +### Swagger / OpenAPI { #swagger-openapi } -O principal recurso que eu queria do Django REST Framework era a documentação automática da API. +A principal funcionalidade que eu queria do Django REST Framework era a documentação automática da API. -Então eu descobri que existia um padrão para documentar APIs, utilizando JSON (ou YAML, uma extensão do JSON) chamado Swagger. +Então descobri que existia um padrão para documentar APIs, utilizando JSON (ou YAML, uma extensão do JSON) chamado Swagger. -E tinha uma interface web para APIs Swagger já criada. Então, sendo capaz de gerar documentação Swagger para uma API poderia permitir utilizar essa interface web automaticamente. +E havia uma interface web para APIs Swagger já criada. Então, ser capaz de gerar documentação Swagger para uma API permitiria usar essa interface web automaticamente. -Em algum ponto, Swagger foi dado para a Fundação Linux, e foi renomeado OpenAPI. +Em algum ponto, Swagger foi doado para a Fundação Linux, para ser renomeado OpenAPI. -Isso acontece porquê quando alguém fala sobre a versão 2.0 é comum dizer "Swagger", e para a versão 3+, "OpenAPI". +É por isso que ao falar sobre a versão 2.0 é comum dizer "Swagger", e para a versão 3+ "OpenAPI". /// check | **FastAPI** inspirado para -Adotar e usar um padrão aberto para especificações API, ao invés de algum esquema customizado. +Adotar e usar um padrão aberto para especificações de API, em vez de um schema personalizado. -E integrar ferramentas de interface para usuários baseado nos padrões: +E integrar ferramentas de interface para usuários baseadas nos padrões: * Swagger UI * ReDoc -Esses dois foram escolhidos por serem bem populares e estáveis, mas fazendo uma pesquisa rápida, você pode encontrar dúzias de interfaces alternativas adicionais para OpenAPI (assim você poderá utilizar com **FastAPI**). +Essas duas foram escolhidas por serem bem populares e estáveis, mas fazendo uma pesquisa rápida, você pode encontrar dúzias de interfaces alternativas adicionais para OpenAPI (que você pode utilizar com **FastAPI**). /// -### Flask REST frameworks +### Flask REST frameworks { #flask-rest-frameworks } -Existem vários Flask REST frameworks, mas depois de investir tempo e trabalho investigando eles, eu descobri que muitos estão descontinuados ou abandonados, com alguns tendo questões que fizeram eles inadequados. +Existem vários Flask REST frameworks, mas depois de investir tempo e trabalho investigando-os, descobri que muitos estão descontinuados ou abandonados, com diversas questões em aberto que os tornaram inadequados. -### Marshmallow +### Marshmallow { #marshmallow } -Um dos principais recursos necessários em sistemas API é "serialização" de dados, que é pegar dados do código (Python) e converter eles em alguma coisa que possa ser enviado através da rede. Por exemplo, converter um objeto contendo dados de um banco de dados em um objeto JSON. Converter objetos `datetime` em strings etc. +Uma das principais funcionalidades necessárias em sistemas de API é a "serialização" de dados, que é pegar dados do código (Python) e convertê-los em algo que possa ser enviado pela rede. Por exemplo, converter um objeto contendo dados de um banco de dados em um objeto JSON. Converter objetos `datetime` em strings, etc. -Outro grande recurso necessário nas APIs é validação de dados, certificando que os dados são válidos, dados certos parâmetros. Por exemplo, algum campo é `int`, e não alguma string aleatória. Isso é especialmente útil para dados que estão chegando. +Outra grande funcionalidade necessária pelas APIs é a validação de dados, garantindo que os dados são válidos, dados certos parâmetros. Por exemplo, que algum campo seja `int`, e não alguma string aleatória. Isso é especialmente útil para dados de entrada. Sem um sistema de validação de dados, você teria que realizar todas as verificações manualmente, no código. -Esses recursos são o que Marshmallow foi construído para fornecer. Ele é uma ótima biblioteca, e eu já utilizei muito antes. +Essas funcionalidades são o que o Marshmallow foi construído para fornecer. É uma ótima biblioteca, e eu a utilizei bastante antes. -Mas ele foi criado antes da existência do _type hints_ do Python. Então, para definir todo o _schema_ você precisa utilizar específicas ferramentas e classes fornecidas pelo Marshmallow. +Mas ele foi criado antes de existirem as anotações de tipo do Python. Então, para definir cada schema você precisa utilizar utilitários e classes específicos fornecidos pelo Marshmallow. /// check | **FastAPI** inspirado para @@ -153,17 +153,17 @@ Usar código para definir "schemas" que forneçam, automaticamente, tipos de dad /// -### Webargs +### Webargs { #webargs } -Outro grande recurso necessário pelas APIs é a análise de dados vindos de requisições. +Outra grande funcionalidade requerida pelas APIs é o parsing de dados vindos de requisições de entrada. -Webargs é uma ferramente feita para fornecer o que está no topo de vários frameworks, inclusive Flask. +Webargs é uma ferramenta feita para fornecer isso no topo de vários frameworks, inclusive Flask. -Ele utiliza Marshmallow por baixo para validação de dados. E ele foi criado pelos mesmos desenvolvedores. +Ele utiliza Marshmallow por baixo para a validação de dados. E foi criado pelos mesmos desenvolvedores. -Ele é uma grande ferramenta e eu também a utilizei muito, antes de ter o **FastAPI**. +É uma grande ferramenta e eu também a utilizei bastante, antes de ter o **FastAPI**. -/// info +/// info | Informação Webargs foi criado pelos mesmos desenvolvedores do Marshmallow. @@ -171,29 +171,29 @@ Webargs foi criado pelos mesmos desenvolvedores do Marshmallow. /// check | **FastAPI** inspirado para -Ter validação automática de dados vindos de requisições. +Ter validação automática dos dados de requisições de entrada. /// -### APISpec +### APISpec { #apispec } -Marshmallow e Webargs fornecem validação, análise e serialização como plug-ins. +Marshmallow e Webargs fornecem validação, parsing e serialização como plug-ins. -Mas a documentação ainda está faltando. Então APISpec foi criado. +Mas a documentação ainda estava faltando. Então APISpec foi criado. -APISpec tem plug-ins para muitos frameworks (e tem um plug-in para Starlette também). +É um plug-in para muitos frameworks (e há um plug-in para Starlette também). -O jeito como ele funciona é que você escreve a definição do _schema_ usando formato YAML dentro da _docstring_ de cada função controlando uma rota. +O jeito como ele funciona é que você escreve a definição do schema usando formato YAML dentro da docstring de cada função que lida com uma rota. -E ele gera _schemas_ OpenAPI. +E ele gera schemas OpenAPI. -É assim como funciona no Flask, Starlette, Responder etc. +É assim como funciona no Flask, Starlette, Responder, etc. -Mas então, nós temos novamente o problema de ter uma micro-sintaxe, dentro de uma string Python (um grande YAML). +Mas então, temos novamente o problema de ter uma micro-sintaxe, dentro de uma string Python (um grande YAML). -O editor não poderá ajudar muito com isso. E se nós modificarmos os parâmetros dos _schemas_ do Marshmallow e esquecer de modificar também aquela _docstring_ YAML, o _schema_ gerado pode ficar obsoleto. +O editor não pode ajudar muito com isso. E se modificarmos parâmetros ou schemas do Marshmallow e esquecermos de também modificar aquela docstring em YAML, o schema gerado ficaria obsoleto. -/// info +/// info | Informação APISpec foi criado pelos mesmos desenvolvedores do Marshmallow. @@ -201,31 +201,31 @@ APISpec foi criado pelos mesmos desenvolvedores do Marshmallow. /// check | **FastAPI** inspirado para -Dar suporte a padrões abertos para APIs, OpenAPI. +Dar suporte ao padrão aberto para APIs, OpenAPI. /// -### Flask-apispec +### Flask-apispec { #flask-apispec } -É um plug-in Flask, que amarra junto Webargs, Marshmallow e APISpec. +É um plug-in Flask, que amarra juntos Webargs, Marshmallow e APISpec. -Ele utiliza a informação do Webargs e Marshmallow para gerar automaticamente _schemas_ OpenAPI, usando APISpec. +Ele utiliza a informação do Webargs e Marshmallow para gerar automaticamente schemas OpenAPI, usando APISpec. -É uma grande ferramenta, mas muito subestimada. Ela deveria ser um pouco mais popular do que muitos outros plug-ins Flask. É de ser esperado que sua documentação seja bem concisa e abstrata. +É uma grande ferramenta, muito subestimada. Deveria ser bem mais popular do que muitos plug-ins Flask por aí. Pode ser devido à sua documentação ser concisa e abstrata demais. -Isso resolveu o problema de ter que escrever YAML (outra sintaxe) dentro das _docstrings_ Python. +Isso resolveu ter que escrever YAML (outra sintaxe) dentro das docstrings do Python. -Essa combinação de Flask, Flask-apispec com Marshmallow e Webargs foi meu _backend stack_ favorito até construir o **FastAPI**. +Essa combinação de Flask, Flask-apispec com Marshmallow e Webargs foi a minha stack de backend favorita até construir o **FastAPI**. -Usando essa combinação levou a criação de vários geradores Flask _full-stack_. Há muitas _stacks_ que eu (e vários times externos) estou utilizando até agora: +Usá-la levou à criação de vários geradores Flask full-stack. Estas são as principais stacks que eu (e várias equipes externas) tenho utilizado até agora: * https://github.com/tiangolo/full-stack * https://github.com/tiangolo/full-stack-flask-couchbase * https://github.com/tiangolo/full-stack-flask-couchdb -E esses mesmos geradores _full-stack_ foram a base dos [Geradores de Projetos **FastAPI**](project-generation.md){.internal-link target=_blank}. +E esses mesmos geradores full-stack foram a base dos [Geradores de Projetos **FastAPI**](project-generation.md){.internal-link target=_blank}. -/// info +/// info | Informação Flask-apispec foi criado pelos mesmos desenvolvedores do Marshmallow. @@ -233,151 +233,149 @@ Flask-apispec foi criado pelos mesmos desenvolvedores do Marshmallow. /// check | **FastAPI** inspirado para -Gerar _schema_ OpenAPI automaticamente, a partir do mesmo código que define serialização e validação. +Gerar o schema OpenAPI automaticamente, a partir do mesmo código que define serialização e validação. /// -### NestJS (and Angular) +### NestJS (e Angular) { #nestjs-and-angular } -NestJS, que não é nem Python, é um framework NodeJS JavaScript (TypeScript) inspirado pelo Angular. +Isso nem é Python, NestJS é um framework NodeJS em JavaScript (TypeScript) inspirado pelo Angular. -Ele alcança de uma forma similar ao que pode ser feito com o Flask-apispec. +Ele alcança algo um tanto similar ao que pode ser feito com Flask-apispec. -Ele tem um sistema de injeção de dependência integrado, inspirado pelo Angular 2. É necessário fazer o pré-registro dos "injetáveis" (como todos os sistemas de injeção de dependência que conheço), então, adicionando verbosidade e repetição de código. +Ele tem um sistema de injeção de dependência integrado, inspirado pelo Angular 2. É necessário fazer o pré-registro dos "injetáveis" (como todos os sistemas de injeção de dependência que conheço), então, adiciona verbosidade e repetição de código. -Como os parâmetros são descritos com tipos TypeScript (similar aos _type hints_ do Python), o suporte ao editor é muito bom. +Como os parâmetros são descritos com tipos do TypeScript (similares às anotações de tipo do Python), o suporte do editor é muito bom. -Mas como os dados TypeScript não são preservados após a compilação para o JavaScript, ele não pode depender dos tipos para definir a validação, serialização e documentação ao mesmo tempo. Devido a isso e a algumas decisões de projeto, para pegar a validação, serialização e geração automática do _schema_, é necessário adicionar decoradores em muitos lugares. Então, ele se torna muito verboso. +Mas como os dados do TypeScript não são preservados após a compilação para JavaScript, ele não pode depender dos tipos para definir validação, serialização e documentação ao mesmo tempo. Devido a isso e a algumas decisões de projeto, para obter validação, serialização e geração automática de schema, é necessário adicionar decorators em muitos lugares. Então, ele se torna bastante verboso. -Ele também não controla modelos aninhados muito bem. Então, se o corpo JSON na requisição for um objeto JSON que contém campos internos que contém objetos JSON aninhados, ele não consegue ser validado e documentado apropriadamente. +Ele não consegue lidar muito bem com modelos aninhados. Então, se o corpo JSON na requisição for um objeto JSON que contém campos internos que por sua vez são objetos JSON aninhados, ele não consegue ser documentado e validado apropriadamente. /// check | **FastAPI** inspirado para -Usar tipos Python para ter um ótimo suporte do editor. +Usar tipos do Python para ter um ótimo suporte do editor. -Ter um sistema de injeção de dependência poderoso. Achar um jeito de minimizar repetição de código. +Ter um sistema de injeção de dependência poderoso. Encontrar um jeito de minimizar repetição de código. /// -### Sanic +### Sanic { #sanic } -Ele foi um dos primeiros frameworks Python extremamente rápido baseado em `asyncio`. Ele foi feito para ser muito similar ao Flask. +Ele foi um dos primeiros frameworks Python extremamente rápidos baseados em `asyncio`. Ele foi feito para ser muito similar ao Flask. -/// note | Detalhes técnicos +/// note | Detalhes Técnicos -Ele utiliza `uvloop` ao invés do '_loop_' `asyncio` padrão do Python. É isso que deixa ele tão rápido. +Ele utilizava `uvloop` em vez do loop `asyncio` padrão do Python. É isso que o deixava tão rápido. -Ele claramente inspirou Uvicorn e Starlette, que são atualmente mais rápidos que o Sanic em testes de performance abertos. +Ele claramente inspirou Uvicorn e Starlette, que atualmente são mais rápidos que o Sanic em benchmarks abertos. /// /// check | **FastAPI** inspirado para -Achar um jeito de ter uma performance insana. +Encontrar um jeito de ter uma performance insana. -É por isso que o **FastAPI** é baseado em Starlette, para que ele seja o framework mais rápido disponível (performance testada por terceiros). +É por isso que o **FastAPI** é baseado em Starlette, pois ela é o framework mais rápido disponível (testado por benchmarks de terceiros). /// -### Falcon +### Falcon { #falcon } -Falcon é outro framework Python de alta performance, e é projetado para ser minimalista, e funciona como fundação de outros frameworks como Hug. +Falcon é outro framework Python de alta performance, projetado para ser minimalista, e servir como base para outros frameworks como Hug. -Ele usa o padrão anterior para frameworks web Python (WSGI) que é síncrono, então ele não pode controlar _WebSockets_ e outros casos de uso. No entanto, ele também tem uma boa performance. +Ele é projetado para ter funções que recebem dois parâmetros, uma "request" e uma "response". Então você "lê" partes da requisição, e "escreve" partes para a resposta. Por causa desse design, não é possível declarar parâmetros de requisição e corpos com as anotações de tipo padrão do Python como parâmetros de função. -Ele é projetado para ter funções que recebem dois parâmetros, uma "requisição" e uma "resposta". Então você "lê" as partes da requisição, e "escreve" partes para a resposta. Devido ao seu design, não é possível declarar parâmetros de requisição e corpos com _type hints_ Python padrão como parâmetros de funções. - -Então, validação de dados, serialização e documentação tem que ser feitos no código, não automaticamente. Ou eles terão que ser implementados como um framework acima do Falcon, como o Hug. Essa mesma distinção acontece em outros frameworks que são inspirados pelo design do Falcon, tendo um objeto de requisição e um objeto de resposta como parâmetros. +Então, validação de dados, serialização e documentação têm que ser feitos no código, não automaticamente. Ou eles têm que ser implementados como um framework acima do Falcon, como o Hug. Essa mesma distinção acontece em outros frameworks inspirados pelo design do Falcon, de ter um objeto de request e um objeto de response como parâmetros. /// check | **FastAPI** inspirado para -Achar jeitos de conseguir melhor performance. +Encontrar maneiras de obter uma ótima performance. -Juntamente com Hug (como Hug é baseado no Falcon) inspirou **FastAPI** para declarar um parâmetro de `resposta` nas funções. +Juntamente com Hug (como Hug é baseado no Falcon) inspirou **FastAPI** a declarar um parâmetro de `response` nas funções. Embora no FastAPI seja opcional, é utilizado principalmente para configurar cabeçalhos, cookies e códigos de status alternativos. /// -### Molten +### Molten { #molten } -Eu descobri Molten nos primeiros estágios da construção do **FastAPI**. E ele tem umas idéias bem similares: +Eu descobri Molten nos primeiros estágios da construção do **FastAPI**. E ele tem ideias bastante similares: -* Baseado em _type hints_ Python. -* Validação e documentação desses tipos. -* Sistema de injeção de dependência. +* Baseado nas anotações de tipo do Python. +* Validação e documentação a partir desses tipos. +* Sistema de Injeção de Dependência. -Ele não utiliza validação de dados, seriallização e documentação de bibliotecas de terceiros como o Pydantic, ele tem seu prórpio. Então, essas definições de tipo de dados não podem ser reutilizados tão facilmente. +Ele não utiliza uma biblioteca de terceiros para validação de dados, serialização e documentação como o Pydantic, ele tem a sua própria. Então, essas definições de tipos de dados não seriam reutilizáveis tão facilmente. -Ele exige um pouco mais de verbosidade nas configurações. E como é baseado no WSGI (ao invés de ASGI), ele não é projetado para ter a vantagem da alta performance fornecida por ferramentas como Uvicorn, Starlette e Sanic. +Ele exige configurações um pouco mais verbosas. E como é baseado em WSGI (em vez de ASGI), ele não é projetado para tirar vantagem da alta performance fornecida por ferramentas como Uvicorn, Starlette e Sanic. -O sistema de injeção de dependência exige pré-registro das dependências e as dependências são resolvidas baseadas nos tipos declarados. Então, não é possível declarar mais do que um "componente" que fornece um certo tipo. +O sistema de injeção de dependência exige pré-registro das dependências e elas são resolvidas com base nos tipos declarados. Então, não é possível declarar mais de um "componente" que forneça um certo tipo. -Rotas são declaradas em um único lugar, usando funções declaradas em outros lugares (ao invés de usar decoradores que possam ser colocados diretamente acima da função que controla o _endpoint_). Isso é mais perto de como o Django faz isso do que como Flask (e Starlette) faz. Ele separa no código coisas que são relativamente amarradas. +As rotas são declaradas em um único lugar, usando funções declaradas em outros lugares (em vez de usar decorators que possam ser colocados diretamente acima da função que lida com o endpoint). Isso é mais próximo de como o Django faz do que de como o Flask (e o Starlette) fazem. Separa no código coisas que são relativamente bem acopladas. /// check | **FastAPI** inspirado para -Definir validações extras para tipos de dados usando valores "padrão" de atributos dos modelos. Isso melhora o suporte do editor, e não estava disponível no Pydantic antes. +Definir validações extras para tipos de dados usando o valor "padrão" de atributos dos modelos. Isso melhora o suporte do editor, e não estava disponível no Pydantic antes. Isso na verdade inspirou a atualização de partes do Pydantic, para dar suporte ao mesmo estilo de declaração da validação (toda essa funcionalidade já está disponível no Pydantic). /// -### Hug +### Hug { #hug } -Hug foi um dos primeiros frameworks a implementar a declaração de tipos de parâmetros usando Python _type hints_. Isso foi uma ótima idéia que inspirou outras ferramentas a fazer o mesmo. +Hug foi um dos primeiros frameworks a implementar a declaração de tipos de parâmetros de API usando anotações de tipo do Python. Isso foi uma ótima ideia que inspirou outras ferramentas a fazer o mesmo. -Ele usou tipos customizados em suas declarações ao invés dos tipos padrão Python, mas mesmo assim foi um grande passo. +Ele usou tipos personalizados em suas declarações em vez dos tipos padrão do Python, mas mesmo assim foi um grande passo adiante. -Ele também foi um dos primeiros frameworks a gerar um _schema_ customizado declarando a API inteira em JSON. +Ele também foi um dos primeiros frameworks a gerar um schema personalizado declarando a API inteira em JSON. -Ele não era baseado em um padrão como OpenAPI e JSON Schema. Então não poderia ter interação direta com outras ferramentas, como Swagger UI. Mas novamente, era uma idéia muito inovadora. +Ele não era baseado em um padrão como OpenAPI e JSON Schema. Então não seria simples integrá-lo com outras ferramentas, como Swagger UI. Mas novamente, era uma ideia muito inovadora. -Hug tinha um incomum, interessante recurso: usando o mesmo framework, é possível criar tanto APIs como CLIs. +Ele tem um recurso interessante e incomum: usando o mesmo framework, é possível criar APIs e também CLIs. -Como é baseado nos padrões anteriores de frameworks web síncronos (WSGI), ele não pode controlar _Websockets_ e outras coisas, embora ele ainda tenha uma alta performance também. +Como é baseado no padrão anterior para frameworks web Python síncronos (WSGI), ele não consegue lidar com Websockets e outras coisas, embora ainda tenha alta performance também. -/// info +/// info | Informação -Hug foi criado por Timothy Crosley, o mesmo criador do `isort`, uma grande ferramenta para ordenação automática de _imports_ em arquivos Python. +Hug foi criado por Timothy Crosley, o mesmo criador do `isort`, uma ótima ferramenta para ordenar automaticamente imports em arquivos Python. /// -/// check | Idéias inspiradas para o **FastAPI** +/// check | Ideias que inspiraram o **FastAPI** -Hug inspirou partes do APIStar, e foi uma das ferramentas que eu achei mais promissora, ao lado do APIStar. +Hug inspirou partes do APIStar, e foi uma das ferramentas que achei mais promissoras, ao lado do APIStar. -Hug ajudou a inspirar o **FastAPI** a usar _type hints_ do Python para declarar parâmetros, e para gerar um _schema_ definindo a API automaticamente. +Hug ajudou a inspirar o **FastAPI** a usar anotações de tipo do Python para declarar parâmetros e para gerar um schema definindo a API automaticamente. -Hug inspirou **FastAPI** a declarar um parâmetro de `resposta` em funções para definir cabeçalhos e cookies. +Hug inspirou **FastAPI** a declarar um parâmetro de `response` em funções para definir cabeçalhos e cookies. /// -### APIStar (<= 0.5) +### APIStar (<= 0.5) { #apistar-0-5 } -Antes de decidir construir **FastAPI** eu encontrei o servidor **APIStar**. Tinha quase tudo que eu estava procurando e tinha um grande projeto. +Pouco antes de decidir construir o **FastAPI** eu encontrei o servidor **APIStar**. Ele tinha quase tudo o que eu estava procurando e tinha um ótimo design. -Ele foi uma das primeiras implementações de um framework usando Python _type hints_ para declarar parâmetros e requisições que eu nunca vi (antes no NestJS e Molten). Eu encontrei ele mais ou menos na mesma época que o Hug. Mas o APIStar utilizava o padrão OpenAPI. +Foi uma das primeiras implementações de um framework usando anotações de tipo do Python para declarar parâmetros e requisições que eu já vi (antes do NestJS e Molten). Eu o encontrei mais ou menos na mesma época que o Hug. Mas o APIStar utilizava o padrão OpenAPI. -Ele tinha validação de dados automática, serialização de dados e geração de _schema_ OpenAPI baseado nos mesmos _type hints_ em vários locais. +Ele tinha validação de dados automática, serialização de dados e geração de schema OpenAPI baseadas nas mesmas anotações de tipo em vários locais. -Definições de _schema_ de corpo não utilizavam os mesmos Python _type hints_ como Pydantic, ele era um pouco mais similar ao Marshmallow, então, o suporte ao editor não seria tão bom, ainda assim, APIStar era a melhor opção disponível. +As definições de schema de corpo não utilizavam as mesmas anotações de tipo do Python como o Pydantic, eram um pouco mais similares ao Marshmallow, então o suporte do editor não seria tão bom, ainda assim, APIStar era a melhor opção disponível. -Ele obteve as melhores performances em testes na época (somente batido por Starlette). +Ele obteve os melhores benchmarks de performance na época (somente ultrapassado por Starlette). A princípio, ele não tinha uma interface web com documentação automática da API, mas eu sabia que poderia adicionar o Swagger UI a ele. -Ele tinha um sistema de injeção de dependência. Ele exigia pré-registro dos componentes, como outras ferramentas já discutidas acima. Mas ainda era um grande recurso. +Ele tinha um sistema de injeção de dependência. Exigia pré-registro dos componentes, como outras ferramentas já discutidas acima. Mas ainda assim era um grande recurso. -Eu nunca fui capaz de usar ele num projeto inteiro, por não ter integração de segurança, então, eu não pude substituir todos os recursos que eu tinha com os geradores _full-stack_ baseados no Flask-apispec. Eu tive em minha gaveta de projetos a idéia de criar um _pull request_ adicionando essa funcionalidade. +Eu nunca fui capaz de usá-lo em um projeto completo, pois ele não tinha integração de segurança, então, eu não pude substituir todos os recursos que eu tinha com os geradores full-stack baseados no Flask-apispec. Eu tinha no meu backlog de projetos criar um pull request adicionando essa funcionalidade. Mas então, o foco do projeto mudou. -Ele não era mais um framework web API, como o criador precisava focar no Starlette. +Ele não era mais um framework web de API, pois o criador precisava focar no Starlette. Agora APIStar é um conjunto de ferramentas para validar especificações OpenAPI, não um framework web. -/// info +/// info | Informação APIStar foi criado por Tom Christie. O mesmo cara que criou: @@ -391,98 +389,97 @@ APIStar foi criado por Tom Christie. O mesmo cara que criou: Existir. -A idéia de declarar múltiplas coisas (validação de dados, serialização e documentação) com os mesmos tipos Python, que ao mesmo tempo fornecesse grande suporte ao editor, era algo que eu considerava uma brilhante idéia. +A ideia de declarar múltiplas coisas (validação de dados, serialização e documentação) com os mesmos tipos do Python, que ao mesmo tempo fornecessem grande suporte ao editor, era algo que eu considerava uma ideia brilhante. -E após procurar por um logo tempo por um framework similar e testar muitas alternativas diferentes, APIStar foi a melhor opção disponível. +E após procurar por muito tempo por um framework similar e testar muitas alternativas diferentes, APIStar foi a melhor opção disponível. -Então APIStar parou de existir como um servidor e Starlette foi criado, e foi uma nova melhor fundação para tal sistema. Essa foi a inspiração final para construir **FastAPI**. +Então APIStar deixou de existir como servidor e o Starlette foi criado, sendo uma nova e melhor fundação para tal sistema. Essa foi a inspiração final para construir o **FastAPI**. -Eu considero **FastAPI** um "sucessor espiritual" para o APIStar, evoluindo e melhorando os recursos, sistema de tipagem e outras partes, baseado na aprendizagem de todas essas ferramentas acima. +Eu considero o **FastAPI** um "sucessor espiritual" do APIStar, enquanto aprimora e amplia as funcionalidades, o sistema de tipagem e outras partes, baseado nos aprendizados de todas essas ferramentas anteriores. /// -## Usados por **FastAPI** +## Usados por **FastAPI** { #used-by-fastapi } -### Pydantic +### Pydantic { #pydantic } -Pydantic é uma biblioteca para definir validação de dados, serialização e documentação (usando JSON Schema) baseado nos Python _type hints_. +Pydantic é uma biblioteca para definir validação de dados, serialização e documentação (usando JSON Schema) com base nas anotações de tipo do Python. -Isso faz dele extremamente intuitivo. +Isso o torna extremamente intuitivo. -Ele é comparável ao Marshmallow. Embora ele seja mais rápido que Marshmallow em testes de performance. E ele é baseado nos mesmos Python _type hints_, o suporte ao editor é ótimo. +Ele é comparável ao Marshmallow. Embora seja mais rápido que o Marshmallow em benchmarks. E como é baseado nas mesmas anotações de tipo do Python, o suporte do editor é ótimo. /// check | **FastAPI** usa isso para -Controlar toda a validação de dados, serialização de dados e modelo de documentação automática (baseado no JSON Schema). +Controlar toda a validação de dados, serialização de dados e documentação automática de modelos (baseada no JSON Schema). -**FastAPI** então pega dados do JSON Schema e coloca eles no OpenAPI, à parte de todas as outras coisas que ele faz. +**FastAPI** então pega esses dados do JSON Schema e os coloca no OpenAPI, além de todas as outras coisas que faz. /// -### Starlette +### Starlette { #starlette } -Starlette é um framework/caixa de ferramentas ASGI peso leve, o que é ideal para construir serviços assíncronos de alta performance. +Starlette é um framework/caixa de ferramentas ASGI leve, o que é ideal para construir serviços asyncio de alta performance. -Ele é muito simples e intuitivo. É projetado para ser extensível facilmente, e ter componentes modulares. +Ele é muito simples e intuitivo. É projetado para ser facilmente extensível, e ter componentes modulares. Ele tem: * Performance seriamente impressionante. * Suporte a WebSocket. -* Suporte a GraphQL. -* Tarefas de processamento interno por trás dos panos. +* Tarefas em segundo plano dentro do processo. * Eventos de inicialização e encerramento. * Cliente de testes construído com HTTPX. -* Respostas CORS, GZip, Arquivos Estáticos, Streaming. +* CORS, GZip, Arquivos Estáticos, respostas Streaming. * Suporte para Sessão e Cookie. * 100% coberto por testes. * Código base 100% anotado com tipagem. -* Dependências complexas Zero. +* Poucas dependências obrigatórias. -Starlette é atualmente o mais rápido framework Python testado. Somente ultrapassado pelo Uvicorn, que não é um framework, mas um servidor. +Starlette é atualmente o framework Python mais rápido testado. Somente ultrapassado pelo Uvicorn, que não é um framework, mas um servidor. Starlette fornece toda a funcionalidade básica de um microframework web. -Mas ele não fornece validação de dados automática, serialização e documentação. +Mas ele não fornece validação de dados automática, serialização ou documentação. -Essa é uma das principais coisas que **FastAPI** adiciona no topo, tudo baseado em Python _type hints_ (usando Pydantic). Isso, mais o sistema de injeção de dependência, utilidades de segurança, geração de _schema_ OpenAPI, etc. +Essa é uma das principais coisas que o **FastAPI** adiciona por cima, tudo baseado nas anotações de tipo do Python (usando Pydantic). Isso, mais o sistema de injeção de dependência, utilidades de segurança, geração de schema OpenAPI, etc. /// note | Detalhes Técnicos -ASGI é um novo "padrão" sendo desenvolvido pelos membros do time central do Django. Ele ainda não está como "Padrão Python" (PEP), embora eles estejam em processo de fazer isso. +ASGI é um novo "padrão" sendo desenvolvido por membros do time central do Django. Ele ainda não é um "padrão Python" (uma PEP), embora eles estejam no processo de fazer isso. -No entanto, ele já está sendo utilizado como "padrão" por diversas ferramentas. Isso melhora enormemente a interoperabilidade, como você poderia trocar Uvicorn por qualquer outro servidor ASGI (como Daphne ou Hypercorn), ou você poderia adicionar ferramentas compatíveis com ASGI, como `python-socketio`. +No entanto, ele já está sendo utilizado como "padrão" por diversas ferramentas. Isso melhora enormemente a interoperabilidade, pois você poderia trocar Uvicorn por qualquer outro servidor ASGI (como Daphne ou Hypercorn), ou você poderia adicionar ferramentas compatíveis com ASGI, como `python-socketio`. /// /// check | **FastAPI** usa isso para -Controlar todas as partes web centrais. Adiciona recursos no topo. +Controlar todas as partes web centrais. Adiciona funcionalidades por cima. -A classe `FastAPI` em si herda `Starlette`. +A classe `FastAPI` em si herda diretamente da classe `Starlette`. -Então, qualquer coisa que você faz com Starlette, você pode fazer diretamente com **FastAPI**, pois ele é basicamente um Starlette com esteróides. +Então, qualquer coisa que você pode fazer com Starlette, você pode fazer diretamente com o **FastAPI**, pois ele é basicamente um Starlette com esteróides. /// -### Uvicorn +### Uvicorn { #uvicorn } -Uvicorn é um servidor ASGI peso leve, construído com uvloop e httptools. +Uvicorn é um servidor ASGI extremamente rápido, construído com uvloop e httptools. -Ele não é um framework web, mas sim um servidor. Por exemplo, ele não fornece ferramentas para roteamento por rotas. Isso é algo que um framework como Starlette (ou **FastAPI**) poderia fornecer por cima. +Ele não é um framework web, mas sim um servidor. Por exemplo, ele não fornece ferramentas para roteamento por paths. Isso é algo que um framework como Starlette (ou **FastAPI**) forneceria por cima. Ele é o servidor recomendado para Starlette e **FastAPI**. -/// check | **FastAPI** recomenda isso para +/// check | **FastAPI** o recomenda como O principal servidor web para rodar aplicações **FastAPI**. -Você pode combinar ele com o Gunicorn, para ter um servidor multi-processos assíncrono. +Você também pode usar a opção de linha de comando `--workers` para ter um servidor assíncrono multi-processos. Verifique mais detalhes na seção [Deployment](deployment/index.md){.internal-link target=_blank}. /// -## Performance e velocidade +## Benchmarks e velocidade { #benchmarks-and-speed } Para entender, comparar e ver a diferença entre Uvicorn, Starlette e FastAPI, verifique a seção sobre [Benchmarks](benchmarks.md){.internal-link target=_blank}. diff --git a/docs/pt/docs/async.md b/docs/pt/docs/async.md index c70924ea52..f01ff23159 100644 --- a/docs/pt/docs/async.md +++ b/docs/pt/docs/async.md @@ -1,10 +1,10 @@ -# Concorrência e async / await +# Concorrência e async / await { #concurrency-and-async-await } Detalhes sobre a sintaxe `async def` para *funções de operação de rota* e alguns conceitos de código assíncrono, concorrência e paralelismo. -## Com pressa? +## Com pressa? { #in-a-hurry } -TL;DR: +TL;DR: Se você estiver utilizando bibliotecas de terceiros que dizem para você chamar as funções com `await`, como: @@ -12,7 +12,7 @@ Se você estiver utilizando bibliotecas de terceiros que dizem para você chamar results = await some_library() ``` -Então, declare sua *função de operação de rota* com `async def` como: +Então, declare suas *funções de operação de rota* com `async def` como: ```Python hl_lines="2" @app.get('/') @@ -21,7 +21,7 @@ async def read_results(): return results ``` -/// note +/// note | Nota Você só pode usar `await` dentro de funções criadas com `async def`. @@ -29,7 +29,7 @@ Você só pode usar `await` dentro de funções criadas com `async def`. --- -Se você está usando biblioteca de terceiros que se comunica com alguma coisa (um banco de dados, uma API, sistema de arquivos etc) e não tem suporte para utilizar `await` (esse é atualmente o caso para a maioria das bibliotecas de banco de dados), então declare suas *funções de operação de rota* normalmente, com apenas `def`, como: +Se você está usando uma biblioteca de terceiros que se comunica com alguma coisa (um banco de dados, uma API, o sistema de arquivos etc.) e não tem suporte para utilizar `await` (esse é atualmente o caso para a maioria das bibliotecas de banco de dados), então declare suas *funções de operação de rota* normalmente, com apenas `def`, como: ```Python hl_lines="2" @app.get('/') @@ -40,7 +40,7 @@ def results(): --- -Se sua aplicação (de alguma forma) não tem que se comunicar com nada mais e esperar que o respondam, use `async def`. +Se sua aplicação (de alguma forma) não tem que se comunicar com nada mais e esperar que o respondam, use `async def`, mesmo que você não precise usar `await` dentro dela. --- @@ -54,17 +54,17 @@ De qualquer forma, em ambos os casos acima, FastAPI irá trabalhar assincronamen Mas, seguindo os passos acima, ele será capaz de fazer algumas otimizações de performance. -## Detalhes Técnicos +## Detalhes Técnicos { #technical-details } -Versões modernas de Python tem suporte para **"código assíncrono"** usando algo chamado **"corrotinas"**, com sintaxe **`async` e `await`**. +Versões modernas de Python têm suporte para **"código assíncrono"** usando algo chamado **"corrotinas"**, com sintaxe **`async` e `await`**. -Vamos ver aquela frase por partes na seção abaixo: +Vamos ver aquela frase por partes nas seções abaixo: * **Código assíncrono** * **`async` e `await`** * **Corrotinas** -## Código assíncrono +## Código assíncrono { #asynchronous-code } Código assíncrono apenas significa que a linguagem 💬 tem um jeito de dizer para o computador / programa 🤖 que em certo ponto do código, ele 🤖 terá que esperar *algo* finalizar em outro lugar. Vamos dizer que esse *algo* seja chamado "arquivo lento" 📝. @@ -74,10 +74,10 @@ Então o computador / programa 🤖 irá voltar sempre que tiver uma chance, sej Depois, ele 🤖 pega a primeira tarefa para finalizar (vamos dizer, nosso "arquivo lento" 📝) e continua o que tem que fazer com ela. -Esse "esperar por algo" normalmente se refere a operações I/O que são relativamente "lentas" (comparadas à velocidade do processador e da memória RAM), como esperar por: +Esse "esperar por algo" normalmente se refere a operações I/O que são relativamente "lentas" (comparadas à velocidade do processador e da memória RAM), como esperar por: * dados do cliente para serem enviados através da rede -* dados enviados pelo seu programa serem recebidos pelo clente através da rede +* dados enviados pelo seu programa serem recebidos pelo cliente através da rede * conteúdo de um arquivo no disco ser lido pelo sistema e entregue ao seu programa * conteúdo que seu programa deu ao sistema para ser escrito no disco * uma operação em uma API remota @@ -85,7 +85,7 @@ Esse "esperar por algo" normalmente se refere a operações I/O, essas operações são chamadas operações "limitadas por I/O". +Quanto o tempo de execução é consumido majoritariamente pela espera de operações I/O, essas operações são chamadas operações "limitadas por I/O". Isso é chamado de "assíncrono" porque o computador / programa não tem que ser "sincronizado" com a tarefa lenta, esperando pelo momento exato em que a tarefa finaliza, enquanto não faz nada, para ser capaz de pegar o resultado da tarefa e dar continuidade ao trabalho. @@ -93,9 +93,9 @@ Ao invés disso, sendo um sistema "assíncrono", uma vez finalizada, a tarefa po Para "síncrono" (contrário de "assíncrono") também é utilizado o termo "sequencial", porquê o computador / programa segue todos os passos, em sequência, antes de trocar para uma tarefa diferente, mesmo se alguns passos envolvam esperar. -### Concorrência e hambúrgueres +### Concorrência e hambúrgueres { #concurrency-and-burgers } -Essa idéia de código **assíncrono** descrita acima é às vezes chamado de **"concorrência"**. Isso é diferente de **"paralelismo"**. +Essa ideia de código **assíncrono** descrita acima é às vezes chamada de **"concorrência"**. Isso é diferente de **"paralelismo"**. **Concorrência** e **paralelismo** ambos são relacionados a "diferentes coisas acontecendo mais ou menos ao mesmo tempo". @@ -103,31 +103,51 @@ Mas os detalhes entre *concorrência* e *paralelismo* são bem diferentes. Para ver essa diferença, imagine a seguinte história sobre hambúrgueres: -### Hambúrgueres concorrentes +### Hambúrgueres concorrentes { #concurrent-burgers } Você vai com seu _crush_ na lanchonete, e fica na fila enquanto o caixa pega os pedidos das pessoas na sua frente. 😍 + + Então chega a sua vez, você pede dois saborosos hambúrgueres para você e seu _crush_. 🍔🍔 -O caixa diz alguma coisa para o cozinheiro na cozinha para que eles saivam que têm que preparar seus hambúrgueres (mesmo que ele esteja atualmente preparando os lanches dos outros clientes). + + +O caixa diz alguma coisa para o cozinheiro na cozinha para que eles saibam que têm que preparar seus hambúrgueres (mesmo que ele esteja atualmente preparando os lanches dos outros clientes). + + Você paga. 💸 O caixa te entrega seu número de chamada. + + Enquanto você espera, você vai com seu _crush_ e pega uma mesa, senta e conversa com seu _crush_ por um bom tempo (já que seus hambúrgueres são muito saborosos, e leva um tempo para serem preparados). Já que você está sentado na mesa com seu _crush_, esperando os hambúrgueres, você pode passar esse tempo admirando o quão lindo, maravilhoso e esperto é seu _crush_ ✨😍✨. + + Enquanto espera e conversa com seu _crush_, de tempos em tempos, você verifica o número da chamada exibido no balcão para ver se já é sua vez. Então em algum momento, é finalmente sua vez. Você vai ao balcão, pega seus hambúrgueres e volta para a mesa. + + Você e seu _crush_ comem os hambúrgueres e aproveitam o tempo. ✨ + + +/// info | Informação + +Belas ilustrações de Ketrina Thompson. 🎨 + +/// + --- -Imagine que você seja o computador / programa nessa história. +Imagine que você seja o computador / programa 🤖 nessa história. Enquanto você está na fila, você está somente ocioso 😴, esperando por sua vez, sem fazer nada muito "produtivo". Mas a fila é rápida porque o caixa só está pegando os pedidos (não os preparando), então está tudo bem. @@ -139,13 +159,13 @@ Contudo, à medida que você se afasta do balcão e senta na mesa, com um númer Então o caixa 💁 diz que "seus hambúrgueres estão prontos" colocando seu número no balcão, mas você não corre que nem um maluco imediatamente quando o número exibido é o seu. Você sabe que ninguém irá roubar seus hambúrgueres porque você tem o seu número da chamada, e os outros têm os deles. -Então você espera seu _crush_ terminar a história que estava contando (terminar o trabalho atual ⏯ / tarefa sendo processada 🤓), sorri gentilmente e diz que você está indo buscar os hambúrgueres. +Então você espera seu _crush_ terminar a história que estava contando (terminar o trabalho atual ⏯ / tarefa sendo processada 🤓), sorri gentilmente e diz que você está indo buscar os hambúrgueres ⏸. -Então você vai ao balcão 🔀, para a tarefa inicial que agora está finalizada⏯, pega os hambúrgueres, agradece, e leva-os para a mesa. Isso finaliza esse passo / tarefa da interação com o balcão ⏹. Isso, por sua vez, cria uma nova tarefa, a de "comer hambúrgueres" 🔀 ⏯, mas a tarefa anterior de "pegar os hambúrgueres" já está finalizada ⏹. +Então você vai ao balcão 🔀, para a tarefa inicial que agora está finalizada ⏯, pega os hambúrgueres, agradece, e leva-os para a mesa. Isso finaliza esse passo / tarefa da interação com o balcão ⏹. Isso, por sua vez, cria uma nova tarefa, a de "comer hambúrgueres" 🔀 ⏯, mas a tarefa anterior de "pegar os hambúrgueres" já está finalizada ⏹. -### Hambúrgueres paralelos +### Hambúrgueres paralelos { #parallel-burgers } -Agora vamos imaginar que esses não são "Hambúrgueres Concorrentes", e sim "Hambúrgueres Paralelos" +Agora vamos imaginar que esses não são "Hambúrgueres Concorrentes", e sim "Hambúrgueres Paralelos". Você vai com seu _crush_ na lanchonete paralela. @@ -153,29 +173,47 @@ Você fica na fila enquanto vários (vamos dizer 8) caixas que também são cozi Todo mundo na sua frente está esperando seus hambúrgueres ficarem prontos antes de deixar o caixa porque cada um dos 8 caixas vai e prepara o hambúrguer logo após receber o pedido, antes de pegar o próximo pedido. + + Então é finalmente sua vez, você pede 2 hambúrgueres muito saborosos para você e seu _crush_. Você paga 💸. + + O caixa vai para a cozinha. Você espera, na frente do balcão 🕙, para que ninguém pegue seus hambúrgueres antes de você, já que não tem números de chamadas. + + Como você e seu _crush_ estão ocupados não permitindo que ninguém passe na frente e pegue seus hambúrgueres assim que estiverem prontos, você não pode dar atenção ao seu _crush_. 😞 -Isso é trabalho "síncrono", você está "sincronizado" com o caixa / cozinheiro👨‍🍳. Você tem que esperar 🕙 e estar lá no exato momento que o caixa / cozinheiro 👨‍🍳 terminar os hambúrgueres e os der a você, ou então, outro alguém pode pegá-los. +Isso é trabalho "síncrono", você está "sincronizado" com o caixa / cozinheiro 👨‍🍳. Você tem que esperar 🕙 e estar lá no exato momento que o caixa / cozinheiro 👨‍🍳 terminar os hambúrgueres e os der a você, ou então, outro alguém pode pegá-los. + + Então seu caixa / cozinheiro 👨‍🍳 finalmente volta com seus hambúrgueres, depois de um longo tempo esperando 🕙 por eles em frente ao balcão. + + Você pega seus hambúrgueres e vai para a mesa com seu _crush_. Vocês comem os hambúrgueres, e o trabalho está terminado. ⏹ + + Não houve muita conversa ou flerte já que a maior parte do tempo foi gasto esperando 🕙 na frente do balcão. 😞 +/// info | Informação + +Belas ilustrações de Ketrina Thompson. 🎨 + +/// + --- -Nesse cenário dos hambúrgueres paralelos, você é um computador / programa com dois processadores (você e seu _crush_), ambos esperando 🕙 e dedicando sua atenção ⏯ "esperando no balcão" 🕙 por um bom tempo. +Nesse cenário dos hambúrgueres paralelos, você é um computador / programa 🤖 com dois processadores (você e seu _crush_), ambos esperando 🕙 e dedicando sua atenção ⏯ "esperando no balcão" 🕙 por um bom tempo. A lanchonete paralela tem 8 processadores (caixas / cozinheiros), enquanto a lanchonete dos hambúrgueres concorrentes tinha apenas 2 (um caixa e um cozinheiro). @@ -183,7 +221,7 @@ Ainda assim, a experiência final não foi a melhor. 😞 --- -Essa seria o equivalente paralelo à histório dos hambúrgueres. 🍔 +Essa seria o equivalente paralelo à história dos hambúrgueres. 🍔 Para um exemplo "mais real", imagine um banco. @@ -195,7 +233,7 @@ E você tinha que esperar 🕙 na fila por um longo tempo ou poderia perder a ve Você provavelmente não gostaria de levar seu _crush_ 😍 com você para um rolezinho no banco 🏦. -### Conclusão dos hambúrgueres +### Conclusão dos hambúrgueres { #burger-conclusion } Nesse cenário dos "hambúrgueres com seu _crush_", como tem muita espera, faz mais sentido ter um sistema concorrente ⏸🔀⏯. @@ -215,7 +253,7 @@ E esse é o mesmo nível de performance que você tem com o **FastAPI**. E como você pode ter paralelismo e assincronicidade ao mesmo tempo, você tem uma maior performance do que a maioria dos frameworks NodeJS testados e lado a lado com Go, que é uma linguagem compilada, mais próxima ao C (tudo graças ao Starlette). -### Concorrência é melhor que paralelismo? +### Concorrência é melhor que paralelismo? { #is-concurrency-better-than-parallelism } Não! Essa não é a moral da história. @@ -239,7 +277,7 @@ Mas nesse caso, se você trouxesse os 8 ex-caixas / cozinheiros / agora-faxineir Nesse cenário, cada um dos faxineiros (incluindo você) poderia ser um processador, fazendo a sua parte do trabalho. -E a maior parte do tempo de execução é tomada por trabalho real (ao invés de ficar esperando), e o trabalho em um computador é feito pela CPU. Eles chamam esses problemas de "limitados por CPU". +E a maior parte do tempo de execução é tomada por trabalho real (ao invés de ficar esperando), e o trabalho em um computador é feito pela CPU. Eles chamam esses problemas de "limitados por CPU". --- @@ -249,22 +287,20 @@ Por exemplo: * **Processamento de áudio** ou **imagem** * **Visão Computacional**: uma imagem é composta por milhões de pixels, cada pixel tem 3 valores / cores, processar isso normalmente exige alguma computação em todos esses pixels ao mesmo tempo +* **Aprendizado de Máquina**: Normalmente exige muita multiplicação de matrizes e vetores. Pense numa grande planilha com números e em multiplicar todos eles juntos e ao mesmo tempo. +* **Deep Learning**: Esse é um subcampo do Aprendizado de Máquina, então, o mesmo se aplica. A diferença é que não há apenas uma grande planilha com números para multiplicar, mas um grande conjunto delas, e em muitos casos, você utiliza um processador especial para construir e/ou usar esses modelos. -* **Machine Learning**: Normalmente exige muita multiplicação de matrizes e vetores. Pense numa grande planilha com números e em multiplicar todos eles juntos e ao mesmo tempo. - -* **Deep Learning**: Esse é um subcampo do Machine Learning, então, o mesmo se aplica. A diferença é que não há apenas uma grande planilha com números para multiplicar, mas um grande conjunto delas, e em muitos casos, você utiliza um processador especial para construir e/ou usar esses modelos. - -### Concorrência + Paralelismo: Web + Machine learning +### Concorrência + Paralelismo: Web + Aprendizado de Máquina { #concurrency-parallelism-web-machine-learning } Com **FastAPI** você pode levar a vantagem da concorrência que é muito comum para desenvolvimento web (o mesmo atrativo de NodeJS). -Mas você também pode explorar os benefícios do paralelismo e multiprocessamento (tendo múltiplos processadores rodando em paralelo) para trabalhos **limitados por CPU** como aqueles em sistemas de Machine Learning. +Mas você também pode explorar os benefícios do paralelismo e multiprocessamento (tendo múltiplos processadores rodando em paralelo) para trabalhos **limitados por CPU** como aqueles em sistemas de Aprendizado de Máquina. -Isso, somado ao simples fato que Python é a principal linguagem para **Data Science**, Machine Learning e especialmente Deep Learning, faz do FastAPI uma ótima escolha para APIs web e aplicações com Data Science / Machine Learning (entre muitas outras). +Isso, somado ao simples fato que Python é a principal linguagem para **Data Science**, Aprendizado de Máquina e especialmente Deep Learning, faz do FastAPI uma ótima escolha para APIs web e aplicações com Data Science / Aprendizado de Máquina (entre muitas outras). -Para ver como alcançar esse paralelismo em produção veja a seção sobre [Deployment](deployment/index.md){.internal-link target=_blank}. +Para ver como alcançar esse paralelismo em produção veja a seção sobre [Implantação](deployment/index.md){.internal-link target=_blank}. -## `async` e `await` +## `async` e `await` { #async-and-await } Versões modernas do Python têm um modo muito intuitivo para definir código assíncrono. Isso faz parecer do mesmo jeito do código normal "sequencial" e fazer a "espera" para você nos momentos certos. @@ -274,7 +310,7 @@ Quando tem uma operação que exigirá espera antes de dar os resultados e tem s burgers = await get_burgers(2) ``` -A chave aqui é o `await`. Ele diz ao Python que ele tem que esperar por `get_burgers(2)` finalizar suas coisas 🕙 antes de armazenar os resultados em `burgers`. Com isso, o Python saberá que ele pode ir e fazer outras coisas 🔀 ⏯ nesse meio tempo (como receber outra requisição). +A chave aqui é o `await`. Ele diz ao Python que ele tem que esperar ⏸ por `get_burgers(2)` finalizar suas coisas 🕙 antes de armazenar os resultados em `burgers`. Com isso, o Python saberá que ele pode ir e fazer outras coisas 🔀 ⏯ nesse meio tempo (como receber outra requisição). Para o `await` funcionar, tem que estar dentro de uma função que suporte essa assincronicidade. Para fazer isso, apenas declare a função com `async def`: @@ -306,18 +342,18 @@ burgers = get_burgers(2) Então, se você está usando uma biblioteca que diz que você pode chamá-la com `await`, você precisa criar as *funções de operação de rota* com `async def`, como em: -```Python hl_lines="2 3" +```Python hl_lines="2-3" @app.get('/burgers') async def read_burgers(): burgers = await get_burgers(2) return burgers ``` -### Mais detalhes técnicos +### Mais detalhes técnicos { #more-technical-details } Você deve ter observado que `await` pode ser usado somente dentro de funções definidas com `async def`. -Mas ao mesmo tempo, funções definidas com `async def` têm que ser "aguardadas". Então, funções com `async def` pdem ser chamadas somente dentro de funções definidas com `async def` também. +Mas ao mesmo tempo, funções definidas com `async def` têm que ser "aguardadas". Então, funções com `async def` podem ser chamadas somente dentro de funções definidas com `async def` também. Então, sobre o ovo e a galinha, como você chama a primeira função async? @@ -325,7 +361,7 @@ Se você estivar trabalhando com **FastAPI** não terá que se preocupar com iss Mas se você quiser usar `async` / `await` sem FastAPI, você também pode fazê-lo. -### Escreva seu próprio código assíncrono +### Escreva seu próprio código assíncrono { #write-your-own-async-code } Starlette (e **FastAPI**) são baseados no AnyIO, o que o torna compatível com ambos o asyncio da biblioteca padrão do Python, e o Trio. @@ -333,10 +369,9 @@ Em particular, você pode usar diretamente o AnyIO por ser altamente compatível e ganhar seus benefícios (e.g. *concorrência estruturada*). -Eu criei outra biblioteca em cima do AnyIO, como uma fina camada acima, para melhorar um pouco as anotações de tipo e obter melhor **autocompletar**, **erros de linha**, etc. Ela também possui uma introdução amigável e um tutorial para ajudar você a **entender** e escrever **seu próprio código async**: Asyncer. Seria particularmente útil se você precisar **combinar código async com código regular** (bloqueador/síncrono). +Eu criei outra biblioteca em cima do AnyIO, como uma fina camada acima, para melhorar um pouco as anotações de tipo e obter melhor **preenchimento automático**, **erros inline**, etc. Ela também possui uma introdução amigável e um tutorial para ajudar você a **entender** e escrever **seu próprio código async**: Asyncer. Seria particularmente útil se você precisar **combinar código async com código regular** (bloqueador/síncrono). - -### Outras formas de código assíncrono +### Outras formas de código assíncrono { #other-forms-of-asynchronous-code } Esse estilo de usar `async` e `await` é relativamente novo na linguagem. @@ -346,17 +381,17 @@ Essa mesma sintaxe (ou quase a mesma) foi também incluída recentemente em vers Mas antes disso, controlar código assíncrono era bem mais complexo e difícil. -Nas versões anteriores do Python, você poderia utilizar threads ou Gevent. Mas o código é bem mais complexo de entender, debugar, e pensar sobre. +Nas versões anteriores do Python, você poderia utilizar threads ou Gevent. Mas o código é bem mais complexo de entender, debugar, e pensar sobre. Nas versões anteriores do NodeJS / Navegador JavaScript, você utilizaria "callbacks". O que leva ao "inferno do callback". -## Corrotinas +## Corrotinas { #coroutines } **Corrotina** é apenas um jeito bonitinho para a coisa que é retornada de uma função `async def`. O Python sabe que é algo como uma função, que pode começar e que vai terminar em algum ponto, mas que pode ser pausada ⏸ internamente também, sempre que tiver um `await` dentro dela. Mas toda essa funcionalidade de código assíncrono com `async` e `await` é muitas vezes resumida como usando "corrotinas". É comparável ao principal recurso chave do Go, a "Gorrotina". -## Conclusão +## Conclusão { #conclusion } Vamos ver a mesma frase de cima: @@ -366,9 +401,9 @@ Isso pode fazer mais sentido agora. ✨ Tudo isso é o que empodera o FastAPI (através do Starlette) e que o faz ter uma performance tão impressionante. -## Detalhes muito técnicos +## Detalhes muito técnicos { #very-technical-details } -/// warning +/// warning | Atenção Você pode provavelmente pular isso. @@ -378,23 +413,23 @@ Se você tem certo conhecimento técnico (corrotinas, threads, blocking etc) e e /// -### Funções de operação de rota +### Funções de operação de rota { #path-operation-functions } Quando você declara uma *função de operação de rota* com `def` normal ao invés de `async def`, ela é rodada em uma threadpool externa que é então aguardada, ao invés de ser chamada diretamente (já que ela bloquearia o servidor). -Se você está chegando de outro framework assíncrono que não funciona como descrito acima e você está acostumado a definir *funções de operação de rota* triviais somente de computação com simples `def` para ter um mínimo ganho de performance (cerca de 100 nanosegundos), por favor observe que no **FastAPI** o efeito pode ser bem o oposto. Nesses casos, é melhor usar `async def` a menos que suas *funções de operação de rota* utilizem código que performe bloqueamento IO. +Se você está chegando de outro framework assíncrono que não funciona como descrito acima e você está acostumado a definir *funções de operação de rota* triviais somente de computação com simples `def` para ter um mínimo ganho de performance (cerca de 100 nanosegundos), por favor observe que no **FastAPI** o efeito pode ser bem o oposto. Nesses casos, é melhor usar `async def` a menos que suas *funções de operação de rota* utilizem código que performe bloqueamento I/O. Ainda, em ambas as situações, as chances são que o **FastAPI** [ainda será mais rápido](index.md#performance){.internal-link target=_blank} do que (ou ao menos comparável a) seu framework anterior. -### Dependências +### Dependências { #dependencies } O mesmo se aplica para as [dependências](tutorial/dependencies/index.md){.internal-link target=_blank}. Se uma dependência tem as funções com padrão `def` ao invés de `async def`, ela é rodada no threadpool externo. -### Sub-dependências +### Sub-dependências { #sub-dependencies } Você pode ter múltiplas dependências e [sub-dependências](tutorial/dependencies/sub-dependencies.md){.internal-link target=_blank} requisitando uma à outra (como parâmetros de definições de funções), algumas delas podem ser criadas com `async def` e algumas com `def` normal. Isso ainda funcionaria, e aquelas criadas com `def` normal seriam chamadas em uma thread externa (do threadpool) ao invés de serem "aguardadas". -### Outras funções de utilidade +### Outras funções de utilidade { #other-utility-functions } Qualquer outra função de utilidade que você chame diretamente pode ser criada com `def` normal ou `async def` e o FastAPI não irá afetar o modo como você a chama. diff --git a/docs/pt/docs/benchmarks.md b/docs/pt/docs/benchmarks.md index 07461ce463..c0b0c4c466 100644 --- a/docs/pt/docs/benchmarks.md +++ b/docs/pt/docs/benchmarks.md @@ -1,10 +1,10 @@ -# Comparações +# Benchmarks { #benchmarks } -As comparações independentes da TechEmpower mostram as aplicações **FastAPI** rodando com Uvicorn como um dos _frameworks_ Python mais rápidos disponíveis, somente atrás dos próprios Starlette e Uvicorn (utilizados internamente pelo FastAPI). (*) +Benchmarks independentes da TechEmpower mostram as aplicações **FastAPI** rodando com Uvicorn como um dos frameworks Python mais rápidos disponíveis, somente atrás dos próprios Starlette e Uvicorn (utilizados internamente pelo FastAPI). Mas quando se checa _benchmarks_ e comparações você deveria ter o seguinte em mente. -## Comparações e velocidade +## Benchmarks e velocidade { #benchmarks-and-speed } Ao verificar os _benchmarks_, é comum observar algumas ferramentas de diferentes tipos comparadas como equivalentes. diff --git a/docs/pt/docs/deployment/cloud.md b/docs/pt/docs/deployment/cloud.md index fc490db4dc..419fd76265 100644 --- a/docs/pt/docs/deployment/cloud.md +++ b/docs/pt/docs/deployment/cloud.md @@ -1,13 +1,24 @@ -# Implantar FastAPI em provedores de nuvem +# Implantar FastAPI em provedores de nuvem { #deploy-fastapi-on-cloud-providers } Você pode usar praticamente **qualquer provedor de nuvem** para implantar seu aplicativo FastAPI. -Na maioria dos casos, os principais provedores de nuvem têm guias para implantar o FastAPI com eles. +Na maioria dos casos, os principais provedores de nuvem têm tutoriais para implantar o FastAPI com eles. -## Provedores de Nuvem - Patrocinadores +## FastAPI Cloud { #fastapi-cloud } -Alguns provedores de nuvem ✨ [**patrocinam o FastAPI**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨, o que garante o **desenvolvimento** contínuo e saudável do FastAPI e seu **ecossistema**. +**FastAPI Cloud** é desenvolvido pelo mesmo autor e equipe por trás do **FastAPI**. -E isso mostra seu verdadeiro comprometimento com o FastAPI e sua **comunidade** (você), pois eles não querem apenas fornecer a você um **bom serviço**, mas também querem ter certeza de que você tenha uma **estrutura boa e saudável**, o FastAPI. 🙇 +Ele simplifica o processo de **criar**, **implantar** e **acessar** uma API com o mínimo de esforço. -Talvez você queira experimentar os serviços deles e seguir os guias. +Traz a mesma **experiência do desenvolvedor** de criar aplicações com FastAPI para **implantá-las** na nuvem. 🎉 + +FastAPI Cloud é o patrocinador principal e provedor de financiamento dos projetos de código aberto *FastAPI and friends*. ✨ + +## Provedores de Nuvem - Patrocinadores { #cloud-providers-sponsors } + +Alguns outros provedores de nuvem ✨ [**patrocinam o FastAPI**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨ também. 🙇 + +Você também pode considerá-los para seguir seus tutoriais e experimentar seus serviços: + +* Render +* Railway diff --git a/docs/pt/docs/deployment/concepts.md b/docs/pt/docs/deployment/concepts.md index 014ca3797a..6af4b177a3 100644 --- a/docs/pt/docs/deployment/concepts.md +++ b/docs/pt/docs/deployment/concepts.md @@ -1,4 +1,4 @@ -# Conceitos de Implantações +# Conceitos de Implantações { #deployments-concepts } Ao implantar um aplicativo **FastAPI**, ou na verdade, qualquer tipo de API da web, há vários conceitos com os quais você provavelmente se importa e, usando-os, você pode encontrar a maneira **mais apropriada** de **implantar seu aplicativo**. @@ -23,7 +23,7 @@ Nos próximos capítulos, darei a você mais **receitas concretas** para implant Mas por enquanto, vamos verificar essas importantes **ideias conceituais**. Esses conceitos também se aplicam a qualquer outro tipo de API da web. 💡 -## Segurança - HTTPS +## Segurança - HTTPS { #security-https } No [capítulo anterior sobre HTTPS](https.md){.internal-link target=_blank} aprendemos como o HTTPS fornece criptografia para sua API. @@ -31,7 +31,7 @@ Também vimos que o HTTPS normalmente é fornecido por um componente **externo** E tem que haver algo responsável por **renovar os certificados HTTPS**, pode ser o mesmo componente ou pode ser algo diferente. -### Ferramentas de exemplo para HTTPS +### Ferramentas de exemplo para HTTPS { #example-tools-for-https } Algumas das ferramentas que você pode usar como um proxy de terminação TLS são: @@ -55,11 +55,11 @@ Mostrarei alguns exemplos concretos nos próximos capítulos. Os próximos conceitos a serem considerados são todos sobre o programa que executa sua API real (por exemplo, Uvicorn). -## Programa e Processo +## Programa e Processo { #program-and-process } Falaremos muito sobre o "**processo**" em execução, então é útil ter clareza sobre o que ele significa e qual é a diferença com a palavra "**programa**". -### O que é um Programa +### O que é um Programa { #what-is-a-program } A palavra **programa** é comumente usada para descrever muitas coisas: @@ -67,7 +67,7 @@ A palavra **programa** é comumente usada para descrever muitas coisas: * O **arquivo** que pode ser **executado** pelo sistema operacional, por exemplo: `python`, `python.exe` ou `uvicorn`. * Um programa específico enquanto está **em execução** no sistema operacional, usando a CPU e armazenando coisas na memória. Isso também é chamado de **processo**. -### O que é um Processo +### O que é um Processo { #what-is-a-process } A palavra **processo** normalmente é usada de forma mais específica, referindo-se apenas ao que está sendo executado no sistema operacional (como no último ponto acima): @@ -88,11 +88,11 @@ E, por exemplo, você provavelmente verá que há vários processos executando o Agora que sabemos a diferença entre os termos **processo** e **programa**, vamos continuar falando sobre implantações. -## Executando na inicialização +## Executando na inicialização { #running-on-startup } Na maioria dos casos, quando você cria uma API web, você quer que ela esteja **sempre em execução**, ininterrupta, para que seus clientes possam sempre acessá-la. Isso é claro, a menos que você tenha um motivo específico para querer que ela seja executada somente em certas situações, mas na maioria das vezes você quer que ela esteja constantemente em execução e **disponível**. -### Em um servidor remoto +### Em um servidor remoto { #in-a-remote-server } Ao configurar um servidor remoto (um servidor em nuvem, uma máquina virtual, etc.), a coisa mais simples que você pode fazer é usar `fastapi run` (que usa Uvicorn) ou algo semelhante, manualmente, da mesma forma que você faz ao desenvolver localmente. @@ -102,15 +102,15 @@ Mas se sua conexão com o servidor for perdida, o **processo em execução** pro E se o servidor for reiniciado (por exemplo, após atualizações ou migrações do provedor de nuvem), você provavelmente **não notará**. E por causa disso, você nem saberá que precisa reiniciar o processo manualmente. Então, sua API simplesmente permanecerá inativa. 😱 -### Executar automaticamente na inicialização +### Executar automaticamente na inicialização { #run-automatically-on-startup } Em geral, você provavelmente desejará que o programa do servidor (por exemplo, Uvicorn) seja iniciado automaticamente na inicialização do servidor e, sem precisar de nenhuma **intervenção humana**, tenha um processo sempre em execução com sua API (por exemplo, Uvicorn executando seu aplicativo FastAPI). -### Programa separado +### Programa separado { #separate-program } Para conseguir isso, você normalmente terá um **programa separado** que garantiria que seu aplicativo fosse executado na inicialização. E em muitos casos, ele também garantiria que outros componentes ou aplicativos também fossem executados, por exemplo, um banco de dados. -### Ferramentas de exemplo para executar na inicialização +### Ferramentas de exemplo para executar na inicialização { #example-tools-to-run-at-startup } Alguns exemplos de ferramentas que podem fazer esse trabalho são: @@ -125,29 +125,29 @@ Alguns exemplos de ferramentas que podem fazer esse trabalho são: Darei exemplos mais concretos nos próximos capítulos. -## Reinicializações +## Reinicializações { #restarts } Semelhante a garantir que seu aplicativo seja executado na inicialização, você provavelmente também deseja garantir que ele seja **reiniciado** após falhas. -### Nós cometemos erros +### Nós cometemos erros { #we-make-mistakes } Nós, como humanos, cometemos **erros** o tempo todo. O software quase *sempre* tem **bugs** escondidos em lugares diferentes. 🐛 E nós, como desenvolvedores, continuamos aprimorando o código à medida que encontramos esses bugs e implementamos novos recursos (possivelmente adicionando novos bugs também 😅). -### Pequenos erros são tratados automaticamente +### Pequenos erros são tratados automaticamente { #small-errors-automatically-handled } Ao criar APIs da web com FastAPI, se houver um erro em nosso código, o FastAPI normalmente o conterá na única solicitação que acionou o erro. 🛡 O cliente receberá um **Erro Interno do Servidor 500** para essa solicitação, mas o aplicativo continuará funcionando para as próximas solicitações em vez de travar completamente. -### Erros maiores - Travamentos +### Erros maiores - Travamentos { #bigger-errors-crashes } No entanto, pode haver casos em que escrevemos algum código que **trava todo o aplicativo**, fazendo com que o Uvicorn e o Python travem. 💥 E ainda assim, você provavelmente não gostaria que o aplicativo permanecesse inativo porque houve um erro em um lugar, você provavelmente quer que ele **continue em execução** pelo menos para as *operações de caminho* que não estão quebradas. -### Reiniciar após falha +### Reiniciar após falha { #restart-after-crash } Mas nos casos com erros realmente graves que travam o **processo** em execução, você vai querer um componente externo que seja responsável por **reiniciar** o processo, pelo menos algumas vezes... @@ -161,7 +161,7 @@ Então, vamos nos concentrar nos casos principais, onde ele pode travar completa Você provavelmente gostaria de ter a coisa responsável por reiniciar seu aplicativo como um **componente externo**, porque a essa altura, o mesmo aplicativo com Uvicorn e Python já havia travado, então não há nada no mesmo código do mesmo aplicativo que possa fazer algo a respeito. -### Ferramentas de exemplo para reiniciar automaticamente +### Ferramentas de exemplo para reiniciar automaticamente { #example-tools-to-restart-automatically } Na maioria dos casos, a mesma ferramenta usada para **executar o programa na inicialização** também é usada para lidar com **reinicializações** automáticas. @@ -176,19 +176,19 @@ Por exemplo, isso poderia ser resolvido por: * Gerenciado internamente por um provedor de nuvem como parte de seus serviços * Outros... -## Replicação - Processos e Memória +## Replicação - Processos e Memória { #replication-processes-and-memory } Com um aplicativo FastAPI, usando um programa de servidor como o comando `fastapi` que executa o Uvicorn, executá-lo uma vez em **um processo** pode atender a vários clientes simultaneamente. Mas em muitos casos, você desejará executar vários processos de trabalho ao mesmo tempo. -### Processos Múltiplos - Trabalhadores +### Processos Múltiplos - Trabalhadores { #multiple-processes-workers } Se você tiver mais clientes do que um único processo pode manipular (por exemplo, se a máquina virtual não for muito grande) e tiver **vários núcleos** na CPU do servidor, você poderá ter **vários processos** em execução com o mesmo aplicativo ao mesmo tempo e distribuir todas as solicitações entre eles. Quando você executa **vários processos** do mesmo programa de API, eles são comumente chamados de **trabalhadores**. -### Processos do Trabalhador e Portas +### Processos do Trabalhador e Portas { #worker-processes-and-ports } Lembra da documentação [Sobre HTTPS](https.md){.internal-link target=_blank} que diz que apenas um processo pode escutar em uma combinação de porta e endereço IP em um servidor? @@ -196,19 +196,19 @@ Isso ainda é verdade. Então, para poder ter **vários processos** ao mesmo tempo, tem que haver um **único processo escutando em uma porta** que então transmite a comunicação para cada processo de trabalho de alguma forma. -### Memória por Processo +### Memória por Processo { #memory-per-process } Agora, quando o programa carrega coisas na memória, por exemplo, um modelo de aprendizado de máquina em uma variável, ou o conteúdo de um arquivo grande em uma variável, tudo isso **consome um pouco da memória (RAM)** do servidor. E vários processos normalmente **não compartilham nenhuma memória**. Isso significa que cada processo em execução tem suas próprias coisas, variáveis ​​e memória. E se você estiver consumindo uma grande quantidade de memória em seu código, **cada processo** consumirá uma quantidade equivalente de memória. -### Memória do servidor +### Memória do servidor { #server-memory } Por exemplo, se seu código carrega um modelo de Machine Learning com **1 GB de tamanho**, quando você executa um processo com sua API, ele consumirá pelo menos 1 GB de RAM. E se você iniciar **4 processos** (4 trabalhadores), cada um consumirá 1 GB de RAM. Então, no total, sua API consumirá **4 GB de RAM**. E se o seu servidor remoto ou máquina virtual tiver apenas 3 GB de RAM, tentar carregar mais de 4 GB de RAM causará problemas. 🚨 -### Processos Múltiplos - Um Exemplo +### Processos Múltiplos - Um Exemplo { #multiple-processes-an-example } Neste exemplo, há um **Processo Gerenciador** que inicia e controla dois **Processos de Trabalhadores**. @@ -224,7 +224,7 @@ Um detalhe interessante é que a porcentagem da **CPU usada** por cada processo Se você tiver uma API que faz uma quantidade comparável de cálculos todas as vezes e tiver muitos clientes, então a **utilização da CPU** provavelmente *também será estável* (em vez de ficar constantemente subindo e descendo rapidamente). -### Exemplos de ferramentas e estratégias de replicação +### Exemplos de ferramentas e estratégias de replicação { #examples-of-replication-tools-and-strategies } Pode haver várias abordagens para conseguir isso, e falarei mais sobre estratégias específicas nos próximos capítulos, por exemplo, ao falar sobre Docker e contêineres. @@ -247,7 +247,7 @@ Falarei mais sobre imagens de contêiner, Docker, Kubernetes, etc. em um capítu /// -## Etapas anteriores antes de começar +## Etapas anteriores antes de começar { #previous-steps-before-starting } Há muitos casos em que você deseja executar algumas etapas **antes de iniciar** sua aplicação. @@ -269,7 +269,7 @@ Nesse caso, você não precisaria se preocupar com nada disso. 🤷 /// -### Exemplos de estratégias de etapas anteriores +### Exemplos de estratégias de etapas anteriores { #examples-of-previous-steps-strategies } Isso **dependerá muito** da maneira como você **implanta seu sistema** e provavelmente estará conectado à maneira como você inicia programas, lida com reinicializações, etc. @@ -285,7 +285,7 @@ Darei exemplos mais concretos de como fazer isso com contêineres em um capítul /// -## Utilização de recursos +## Utilização de recursos { #resource-utilization } Seu(s) servidor(es) é(são) um **recurso** que você pode consumir ou **utilizar**, com seus programas, o tempo de computação nas CPUs e a memória RAM disponível. @@ -305,7 +305,7 @@ Você poderia colocar um **número arbitrário** para atingir, por exemplo, algo Você pode usar ferramentas simples como `htop` para ver a CPU e a RAM usadas no seu servidor ou a quantidade usada por cada processo. Ou você pode usar ferramentas de monitoramento mais complexas, que podem ser distribuídas entre servidores, etc. -## Recapitular +## Recapitular { #recap } Você leu aqui alguns dos principais conceitos que provavelmente precisa ter em mente ao decidir como implantar seu aplicativo: diff --git a/docs/pt/docs/deployment/docker.md b/docs/pt/docs/deployment/docker.md index cf18bb1532..b26a69b54f 100644 --- a/docs/pt/docs/deployment/docker.md +++ b/docs/pt/docs/deployment/docker.md @@ -1,4 +1,4 @@ -# FastAPI em contêineres - Docker +# FastAPI em contêineres - Docker { #fastapi-in-containers-docker } Ao fazer o deploy de aplicações FastAPI uma abordagem comum é construir uma **imagem de contêiner Linux**. Isso normalmente é feito usando o **Docker**. Você pode a partir disso fazer o deploy dessa imagem de algumas maneiras. @@ -6,7 +6,7 @@ Usando contêineres Linux você tem diversas vantagens incluindo **segurança**, /// tip | Dica -Está com pressa e já sabe dessas coisas? Pode ir direto para [`Dockerfile` abaixo 👇](#construindo-uma-imagem-docker-para-fastapi). +Está com pressa e já sabe dessas coisas? Pode ir direto para o [`Dockerfile` abaixo 👇](#build-a-docker-image-for-fastapi). /// @@ -24,25 +24,25 @@ RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt COPY ./app /code/app -CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"] +CMD ["fastapi", "run", "app/main.py", "--port", "80"] -# If running behind a proxy like Nginx or Traefik add --proxy-headers -# CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80", "--proxy-headers"] +# Se estiver executando atrás de um proxy como Nginx ou Traefik, adicione --proxy-headers +# CMD ["fastapi", "run", "app/main.py", "--port", "80", "--proxy-headers"] ``` -## O que é um Contêiner +## O que é um Contêiner { #what-is-a-container } -Contêineres (especificamente contêineres Linux) são um jeito muito **leve** de empacotar aplicações contendo todas as dependências e arquivos necessários enquanto os mantém isolados de outros contêineres (outras aplicações ou componentes) no mesmo sistema. +Contêineres (principalmente contêineres Linux) são um jeito muito **leve** de empacotar aplicações contendo todas as dependências e arquivos necessários enquanto os mantém isolados de outros contêineres (outras aplicações ou componentes) no mesmo sistema. Contêineres Linux rodam usando o mesmo kernel Linux do hospedeiro (máquina, máquina virtual, servidor na nuvem, etc). Isso simplesmente significa que eles são muito leves (comparados com máquinas virtuais emulando um sistema operacional completo). Dessa forma, contêineres consomem **poucos recursos**, uma quantidade comparável com rodar os processos diretamente (uma máquina virtual consumiria muito mais). -Contêineres também possuem seus próprios processos (comumente um único processo), sistema de arquivos e rede **isolados** simplificando deploy, segurança, desenvolvimento, etc. +Contêineres também possuem seus próprios processos em execução (comumente **um único processo**), sistema de arquivos e rede **isolados**, simplificando deploy, segurança, desenvolvimento, etc. -## O que é uma Imagem de Contêiner +## O que é uma Imagem de Contêiner { #what-is-a-container-image } Um **contêiner** roda a partir de uma **imagem de contêiner**. @@ -56,7 +56,7 @@ Uma imagem de contêiner é comparável ao arquivo de **programa** e seus conte E o **contêiner** em si (em contraste à **imagem de contêiner**) é a própria instância da imagem rodando, comparável a um **processo**. Na verdade, um contêiner está rodando somente quando há um **processo rodando** (e normalmente é somente um processo). O contêiner finaliza quando não há um processo rodando nele. -## Imagens de contêiner +## Imagens de contêiner { #container-images } Docker tem sido uma das principais ferramentas para criar e gerenciar **imagens de contêiner** e **contêineres**. @@ -71,15 +71,15 @@ E existe muitas outras imagens para diferentes coisas, como bancos de dados, por * MongoDB * Redis, etc. -Usando imagens de contêiner pré-prontas é muito fácil **combinar** e usar diferentes ferramentas. Por exemplo, para testar um novo banco de dados. Em muitos casos, você pode usar as **imagens oficiais** precisando somente de variáveis de ambiente para configurá-las. +Usando imagens de contêiner pré-prontas é muito fácil **combinar** e usar diferentes ferramentas. Por exemplo, para testar um novo banco de dados. Em muitos casos, você pode usar as **imagens oficiais**, precisando somente de variáveis de ambiente para configurá-las. -Dessa forma, em muitos casos você pode aprender sobre contêineres e Docker e re-usar essa experiência com diversos componentes e ferramentas. +Dessa forma, em muitos casos você pode aprender sobre contêineres e Docker e reusar essa experiência com diversos componentes e ferramentas. Então, você rodaria **vários contêineres** com coisas diferentes, como um banco de dados, uma aplicação Python, um servidor web com uma aplicação frontend React, e conectá-los juntos via sua rede interna. Todos os sistemas de gerenciamento de contêineres (como Docker ou Kubernetes) possuem essas funcionalidades de rede integradas a eles. -## Contêineres e Processos +## Contêineres e Processos { #containers-and-processes } Uma **imagem de contêiner** normalmente inclui em seus metadados o programa padrão ou comando que deve ser executado quando o **contêiner** é iniciado e os parâmetros a serem passados para esse programa. Muito similar ao que seria se estivesse na linha de comando. @@ -91,11 +91,11 @@ Um contêiner normalmente tem um **único processo**, mas também é possível i Mas não é possível ter um contêiner rodando sem **pelo menos um processo rodando**. Se o processo principal parar, o contêiner também para. -## Construindo uma Imagem Docker para FastAPI +## Construir uma Imagem Docker para FastAPI { #build-a-docker-image-for-fastapi } Okay, vamos construir algo agora! 🚀 -Eu vou mostrar como construir uma **imagem Docker** para FastAPI **do zero**, baseado na **imagem oficial do Python**. +Eu vou mostrar como construir uma **imagem Docker** para FastAPI **do zero**, baseada na imagem **oficial do Python**. Isso é o que você quer fazer na **maioria dos casos**, por exemplo: @@ -103,22 +103,21 @@ Isso é o que você quer fazer na **maioria dos casos**, por exemplo: * Quando rodando em uma **Raspberry Pi** * Usando um serviço em nuvem que irá rodar uma imagem de contêiner para você, etc. -### O Pacote Requirements +### Requisitos de Pacotes { #package-requirements } -Você normalmente teria os **requisitos do pacote** para sua aplicação em algum arquivo. +Você normalmente teria os **requisitos de pacotes** da sua aplicação em algum arquivo. Isso pode depender principalmente da ferramenta que você usa para **instalar** esses requisitos. -O caminho mais comum de fazer isso é ter um arquivo `requirements.txt` com os nomes dos pacotes e suas versões, um por linha. +A forma mais comum de fazer isso é ter um arquivo `requirements.txt` com os nomes dos pacotes e suas versões, um por linha. -Você, naturalmente, usaria as mesmas ideias que você leu em [Sobre Versões do FastAPI](versions.md){.internal-link target=_blank} para definir os intervalos de versões. +Você, naturalmente, usaria as mesmas ideias que você leu em [Sobre versões do FastAPI](versions.md){.internal-link target=_blank} para definir os intervalos de versões. Por exemplo, seu `requirements.txt` poderia parecer com: ``` -fastapi>=0.68.0,<0.69.0 -pydantic>=1.8.0,<2.0.0 -uvicorn>=0.15.0,<0.16.0 +fastapi[standard]>=0.113.0,<0.114.0 +pydantic>=2.7.0,<3.0.0 ``` E você normalmente instalaria essas dependências de pacote com `pip`, por exemplo: @@ -128,27 +127,25 @@ E você normalmente instalaria essas dependências de pacote com `pip`, por exem ```console $ pip install -r requirements.txt ---> 100% -Successfully installed fastapi pydantic uvicorn +Successfully installed fastapi pydantic ```
-/// info +/// info | Informação -Há outros formatos e ferramentas para definir e instalar dependências de pacote. - -Eu vou mostrar um exemplo depois usando Poetry em uma seção abaixo. 👇 +Há outros formatos e ferramentas para definir e instalar dependências de pacotes. /// -### Criando o Código do **FastAPI** +### Crie o código do **FastAPI** { #create-the-fastapi-code } * Crie um diretório `app` e entre nele. * Crie um arquivo vazio `__init__.py`. * Crie um arquivo `main.py` com: ```Python -from typing import Optional +from typing import Union from fastapi import FastAPI @@ -165,28 +162,28 @@ def read_item(item_id: int, q: Union[str, None] = None): return {"item_id": item_id, "q": q} ``` -### Dockerfile +### Dockerfile { #dockerfile } Agora, no mesmo diretório do projeto, crie um arquivo `Dockerfile` com: ```{ .dockerfile .annotate } -# (1) +# (1)! FROM python:3.9 -# (2) +# (2)! WORKDIR /code -# (3) +# (3)! COPY ./requirements.txt /code/requirements.txt -# (4) +# (4)! RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt -# (5) +# (5)! COPY ./app /code/app -# (6) -CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"] +# (6)! +CMD ["fastapi", "run", "app/main.py", "--port", "80"] ``` 1. Inicie a partir da imagem base oficial do Python. @@ -205,7 +202,7 @@ CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"] A opção `--no-cache-dir` diz ao `pip` para não salvar os pacotes baixados localmente, pois isso só aconteceria se `pip` fosse executado novamente para instalar os mesmos pacotes, mas esse não é o caso quando trabalhamos com contêineres. - /// note + /// note | Nota `--no-cache-dir` é apenas relacionado ao `pip`, não tem nada a ver com Docker ou contêineres. @@ -223,21 +220,51 @@ CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"] Então, é importante colocar isso **perto do final** do `Dockerfile`, para otimizar o tempo de construção da imagem do contêiner. -6. Defina o **comando** para rodar o servidor `uvicorn`. +6. Defina o **comando** para usar `fastapi run`, que utiliza o Uvicorn por baixo dos panos. `CMD` recebe uma lista de strings, cada uma dessas strings é o que você digitaria na linha de comando separado por espaços. Esse comando será executado a partir do **diretório de trabalho atual**, o mesmo diretório `/code` que você definiu acima com `WORKDIR /code`. - Porque o programa será iniciado em `/code` e dentro dele está o diretório `./app` com seu código, o **Uvicorn** será capaz de ver e **importar** `app` de `app.main`. - -/// tip +/// tip | Dica Revise o que cada linha faz clicando em cada bolha com o número no código. 👆 /// -Agora você deve ter uma estrutura de diretório como: +/// warning | Atenção + +Certifique-se de **sempre** usar a **forma exec** da instrução `CMD`, como explicado abaixo. + +/// + +#### Use `CMD` - Forma Exec { #use-cmd-exec-form } + +A instrução `CMD` no Docker pode ser escrita de duas formas: + +✅ Forma **Exec**: + +```Dockerfile +# ✅ Faça assim +CMD ["fastapi", "run", "app/main.py", "--port", "80"] +``` + +⛔️ Forma **Shell**: + +```Dockerfile +# ⛔️ Não faça assim +CMD fastapi run app/main.py --port 80 +``` + +Garanta que você sempre use a forma **exec** para assegurar que o FastAPI consiga encerrar graciosamente e que os [eventos de lifespan](../advanced/events.md){.internal-link target=_blank} sejam disparados. + +Você pode ler mais na documentação do Docker sobre as formas shell e exec. + +Isso pode ser bem perceptível ao usar `docker compose`. Veja esta seção de FAQ do Docker Compose para mais detalhes técnicos: Por que meus serviços demoram 10 segundos para recriar ou parar?. + +#### Estrutura de diretórios { #directory-structure } + +Agora você deve haver uma estrutura de diretório como: ``` . @@ -248,15 +275,15 @@ Agora você deve ter uma estrutura de diretório como: └── requirements.txt ``` -#### Por Trás de um Proxy de Terminação TLS +#### Por trás de um Proxy de Terminação TLS { #behind-a-tls-termination-proxy } -Se você está executando seu contêiner atrás de um Proxy de Terminação TLS (load balancer) como Nginx ou Traefik, adicione a opção `--proxy-headers`, isso fará com que o Uvicorn confie nos cabeçalhos enviados por esse proxy, informando que o aplicativo está sendo executado atrás do HTTPS, etc. +Se você está executando seu contêiner atrás de um Proxy de Terminação TLS (load balancer) como Nginx ou Traefik, adicione a opção `--proxy-headers`, isso fará com que o Uvicorn (pela CLI do FastAPI) confie nos cabeçalhos enviados por esse proxy, informando que o aplicativo está sendo executado atrás do HTTPS, etc. ```Dockerfile -CMD ["uvicorn", "app.main:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "80"] +CMD ["fastapi", "run", "app/main.py", "--proxy-headers", "--port", "80"] ``` -#### Cache Docker +#### Cache Docker { #docker-cache } Existe um truque importante nesse `Dockerfile`, primeiro copiamos o **arquivo com as dependências sozinho**, não o resto do código. Deixe-me te contar o porquê disso. @@ -288,7 +315,7 @@ A partir daí, perto do final do `Dockerfile`, copiamos todo o código. Como iss COPY ./app /code/app ``` -### Construindo a Imagem Docker +### Construa a Imagem Docker { #build-the-docker-image } Agora que todos os arquivos estão no lugar, vamos construir a imagem do contêiner. @@ -305,7 +332,7 @@ $ docker build -t myimage .
-/// tip +/// tip | Dica Note o `.` no final, é equivalente a `./`, ele diz ao Docker o diretório a ser usado para construir a imagem do contêiner. @@ -313,19 +340,19 @@ Nesse caso, é o mesmo diretório atual (`.`). /// -### Inicie o contêiner Docker +### Inicie o Contêiner Docker { #start-the-docker-container } * Execute um contêiner baseado na sua imagem:
```console -$ docker run -d --name mycontêiner -p 80:80 myimage +$ docker run -d --name mycontainer -p 80:80 myimage ```
-## Verifique +## Verifique { #check-it } Você deve ser capaz de verificar isso no URL do seu contêiner Docker, por exemplo: http://192.168.99.100/items/5?q=somequery ou http://127.0.0.1/items/5?q=somequery (ou equivalente, usando seu host Docker). @@ -335,7 +362,7 @@ Você verá algo como: {"item_id": 5, "q": "somequery"} ``` -## Documentação interativa da API +## Documentação interativa da API { #interactive-api-docs } Agora você pode ir para http://192.168.99.100/docs ou http://127.0.0.1/docs (ou equivalente, usando seu host Docker). @@ -343,7 +370,7 @@ Você verá a documentação interativa automática da API (fornecida pelo http://192.168.99.100/redoc ou http://127.0.0.1/redoc (ou equivalente, usando seu host Docker). @@ -351,7 +378,7 @@ Você verá a documentação alternativa automática (fornecida pela Traefik, lidando com **HTTPS** e aquisição **automática** de **certificados**. -/// tip +/// tip | Dica -Traefik tem integrações com Docker, Kubernetes e outros, portanto, é muito fácil configurar e configurar o HTTPS para seus contêineres com ele. +Traefik tem integrações com Docker, Kubernetes e outros, portanto, é muito fácil configurar o HTTPS para seus contêineres com ele. /// Alternativamente, o HTTPS poderia ser tratado por um provedor de nuvem como um de seus serviços (enquanto ainda executasse o aplicativo em um contêiner). -## Executando na inicialização e reinicializações +## Executando na inicialização e reinicializações { #running-on-startup-and-restarts } Normalmente, outra ferramenta é responsável por **iniciar e executar** seu contêiner. @@ -427,21 +454,21 @@ Na maioria (ou em todos) os casos, há uma opção simples para habilitar a exec Sem usar contêineres, fazer aplicativos executarem na inicialização e com reinicializações pode ser trabalhoso e difícil. Mas quando **trabalhando com contêineres** em muitos casos essa funcionalidade é incluída por padrão. ✨ -## Replicação - Número de Processos +## Replicação - Número de Processos { #replication-number-of-processes } -Se você tiver um cluster de máquinas com **Kubernetes**, Docker Swarm Mode, Nomad ou outro sistema complexo semelhante para gerenciar contêineres distribuídos em várias máquinas, então provavelmente desejará **lidar com a replicação** no **nível do cluster** em vez de usar um **gerenciador de processos** (como o Gunicorn com workers) em cada contêiner. +Se você tiver um cluster de máquinas com **Kubernetes**, Docker Swarm Mode, Nomad ou outro sistema complexo semelhante para gerenciar contêineres distribuídos em várias máquinas, então provavelmente desejará **lidar com a replicação** no **nível do cluster** em vez de usar um **gerenciador de processos** (como Uvicorn com workers) em cada contêiner. Um desses sistemas de gerenciamento de contêineres distribuídos como o Kubernetes normalmente tem alguma maneira integrada de lidar com a **replicação de contêineres** enquanto ainda oferece **balanceamento de carga** para as solicitações recebidas. Tudo no **nível do cluster**. -Nesses casos, você provavelmente desejará criar uma **imagem do contêiner do zero** como [explicado acima](#dockerfile), instalando suas dependências e executando **um único processo Uvicorn** em vez de executar algo como Gunicorn com trabalhadores Uvicorn. +Nesses casos, você provavelmente desejará criar uma **imagem Docker do zero** como [explicado acima](#dockerfile), instalando suas dependências e executando **um único processo Uvicorn** em vez de usar múltiplos workers do Uvicorn. -### Balanceamento de Carga +### Balanceador de Carga { #load-balancer } Quando usando contêineres, normalmente você terá algum componente **escutando na porta principal**. Poderia ser outro contêiner que também é um **Proxy de Terminação TLS** para lidar com **HTTPS** ou alguma ferramenta semelhante. -Como esse componente assumiria a **carga** de solicitações e distribuiria isso entre os trabalhadores de uma maneira (esperançosamente) **balanceada**, ele também é comumente chamado de **Balanceador de Carga**. +Como esse componente assumiria a **carga** de solicitações e distribuiria isso entre os workers de uma maneira (esperançosamente) **balanceada**, ele também é comumente chamado de **Balanceador de Carga**. -/// tip +/// tip | Dica O mesmo componente **Proxy de Terminação TLS** usado para HTTPS provavelmente também seria um **Balanceador de Carga**. @@ -449,9 +476,9 @@ O mesmo componente **Proxy de Terminação TLS** usado para HTTPS provavelmente E quando trabalhar com contêineres, o mesmo sistema que você usa para iniciar e gerenciá-los já terá ferramentas internas para transmitir a **comunicação de rede** (por exemplo, solicitações HTTP) do **balanceador de carga** (que também pode ser um **Proxy de Terminação TLS**) para o(s) contêiner(es) com seu aplicativo. -### Um Balanceador de Carga - Múltiplos Contêineres de Workers +### Um Balanceador de Carga - Múltiplos Contêineres de Workers { #one-load-balancer-multiple-worker-containers } -Quando trabalhando com **Kubernetes** ou sistemas similares de gerenciamento de contêiner distribuído, usando seus mecanismos de rede internos permitiria que o único **balanceador de carga** que estivesse escutando na **porta principal** transmitisse comunicação (solicitações) para possivelmente **múltiplos contêineres** executando seu aplicativo. +Quando trabalhando com **Kubernetes** ou sistemas similares de gerenciamento de contêiner distribuído, usar seus mecanismos de rede internos permite que o único **balanceador de carga** que está escutando na **porta principal** transmita a comunicação (solicitações) para possivelmente **múltiplos contêineres** executando seu aplicativo. Cada um desses contêineres executando seu aplicativo normalmente teria **apenas um processo** (ex.: um processo Uvicorn executando seu aplicativo FastAPI). Todos seriam **contêineres idênticos**, executando a mesma coisa, mas cada um com seu próprio processo, memória, etc. Dessa forma, você aproveitaria a **paralelização** em **núcleos diferentes** da CPU, ou até mesmo em **máquinas diferentes**. @@ -459,54 +486,61 @@ E o sistema de contêiner com o **balanceador de carga** iria **distribuir as so E normalmente esse **balanceador de carga** seria capaz de lidar com solicitações que vão para *outros* aplicativos em seu cluster (por exemplo, para um domínio diferente, ou sob um prefixo de URL diferente), e transmitiria essa comunicação para os contêineres certos para *esse outro* aplicativo em execução em seu cluster. -### Um Processo por Contêiner +### Um Processo por Contêiner { #one-process-per-container } Nesse tipo de cenário, provavelmente você desejará ter **um único processo (Uvicorn) por contêiner**, pois já estaria lidando com a replicação no nível do cluster. -Então, nesse caso, você **não** desejará ter um gerenciador de processos como o Gunicorn com trabalhadores Uvicorn, ou o Uvicorn usando seus próprios trabalhadores Uvicorn. Você desejará ter apenas um **único processo Uvicorn** por contêiner (mas provavelmente vários contêineres). +Então, nesse caso, você **não** desejará ter múltiplos workers no contêiner, por exemplo com a opção de linha de comando `--workers`. Você desejará ter apenas um **único processo Uvicorn** por contêiner (mas provavelmente vários contêineres). -Tendo outro gerenciador de processos dentro do contêiner (como seria com o Gunicorn ou o Uvicorn gerenciando trabalhadores Uvicorn) só adicionaria **complexidade desnecessária** que você provavelmente já está cuidando com seu sistema de cluster. +Ter outro gerenciador de processos dentro do contêiner (como seria com múltiplos workers) só adicionaria **complexidade desnecessária** que você provavelmente já está cuidando com seu sistema de cluster. -### Contêineres com Múltiplos Processos e Casos Especiais +### Contêineres com Múltiplos Processos e Casos Especiais { #containers-with-multiple-processes-and-special-cases } -Claro, existem **casos especiais** em que você pode querer ter um **contêiner** com um **gerenciador de processos Gunicorn** iniciando vários **processos trabalhadores Uvicorn** dentro. +Claro, existem **casos especiais** em que você pode querer ter **um contêiner** com vários **processos workers do Uvicorn** dentro. -Nesses casos, você pode usar a **imagem oficial do Docker** que inclui o **Gunicorn** como um gerenciador de processos executando vários **processos trabalhadores Uvicorn**, e algumas configurações padrão para ajustar o número de trabalhadores com base nos atuais núcleos da CPU automaticamente. Eu vou te contar mais sobre isso abaixo em [Imagem Oficial do Docker com Gunicorn - Uvicorn](#imagem-oficial-do-docker-com-gunicorn-uvicorn). +Nesses casos, você pode usar a opção de linha de comando `--workers` para definir o número de workers que deseja executar: + +```{ .dockerfile .annotate } +FROM python:3.9 + +WORKDIR /code + +COPY ./requirements.txt /code/requirements.txt + +RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt + +COPY ./app /code/app + +# (1)! +CMD ["fastapi", "run", "app/main.py", "--port", "80", "--workers", "4"] +``` + +1. Aqui usamos a opção de linha de comando `--workers` para definir o número de workers como 4. Aqui estão alguns exemplos de quando isso pode fazer sentido: -#### Um Aplicativo Simples +#### Um Aplicativo Simples { #a-simple-app } -Você pode querer um gerenciador de processos no contêiner se seu aplicativo for **simples o suficiente** para que você não precise (pelo menos não agora) ajustar muito o número de processos, e você pode simplesmente usar um padrão automatizado (com a imagem oficial do Docker), e você está executando em um **único servidor**, não em um cluster. +Você pode querer um gerenciador de processos no contêiner se seu aplicativo for **simples o suficiente** para rodar em um **único servidor**, não em um cluster. -#### Docker Compose +#### Docker Compose { #docker-compose } Você pode estar implantando em um **único servidor** (não em um cluster) com o **Docker Compose**, então você não teria uma maneira fácil de gerenciar a replicação de contêineres (com o Docker Compose) enquanto preserva a rede compartilhada e o **balanceamento de carga**. -Então você pode querer ter **um único contêiner** com um **gerenciador de processos** iniciando **vários processos trabalhadores** dentro. - -#### Prometheus and Outros Motivos - -Você também pode ter **outros motivos** que tornariam mais fácil ter um **único contêiner** com **múltiplos processos** em vez de ter **múltiplos contêineres** com **um único processo** em cada um deles. - -Por exemplo (dependendo de sua configuração), você poderia ter alguma ferramenta como um exportador do Prometheus no mesmo contêiner que deve ter acesso a **cada uma das solicitações** que chegam. - -Nesse caso, se você tivesse **múltiplos contêineres**, por padrão, quando o Prometheus fosse **ler as métricas**, ele receberia as métricas de **um único contêiner cada vez** (para o contêiner que tratou essa solicitação específica), em vez de receber as **métricas acumuladas** de todos os contêineres replicados. - -Então, nesse caso, poderia ser mais simples ter **um único contêiner** com **múltiplos processos**, e uma ferramenta local (por exemplo, um exportador do Prometheus) no mesmo contêiner coletando métricas do Prometheus para todos os processos internos e expor essas métricas no único contêiner. +Então você pode querer ter **um único contêiner** com um **gerenciador de processos** iniciando **vários processos workers** dentro. --- -O ponto principal é que **nenhum** desses são **regras escritas em pedra** que você deve seguir cegamente. Você pode usar essas idéias para **avaliar seu próprio caso de uso** e decidir qual é a melhor abordagem para seu sistema, verificando como gerenciar os conceitos de: +O ponto principal é que **nenhum** desses são **regras escritas em pedra** que você deve seguir cegamente. Você pode usar essas ideias para **avaliar seu próprio caso de uso** e decidir qual é a melhor abordagem para seu sistema, verificando como gerenciar os conceitos de: * Segurança - HTTPS * Executando na inicialização * Reinicializações * Replicação (o número de processos em execução) * Memória -* Passos anteriores antes de inicializar +* Passos anteriores antes de iniciar -## Memória +## Memória { #memory } Se você executar **um único processo por contêiner**, terá uma quantidade mais ou menos bem definida, estável e limitada de memória consumida por cada um desses contêineres (mais de um se eles forem replicados). @@ -514,17 +548,17 @@ E então você pode definir esses mesmos limites e requisitos de memória em sua Se sua aplicação for **simples**, isso provavelmente **não será um problema**, e você pode não precisar especificar limites de memória rígidos. Mas se você estiver **usando muita memória** (por exemplo, com **modelos de aprendizado de máquina**), deve verificar quanta memória está consumindo e ajustar o **número de contêineres** que executa em **cada máquina** (e talvez adicionar mais máquinas ao seu cluster). -Se você executar **múltiplos processos por contêiner** (por exemplo, com a imagem oficial do Docker), deve garantir que o número de processos iniciados não **consuma mais memória** do que o disponível. +Se você executar **múltiplos processos por contêiner**, deve garantir que o número de processos iniciados não **consuma mais memória** do que o disponível. -## Passos anteriores antes de inicializar e contêineres +## Passos anteriores antes de iniciar e contêineres { #previous-steps-before-starting-and-containers } Se você estiver usando contêineres (por exemplo, Docker, Kubernetes), existem duas abordagens principais que você pode usar. -### Contêineres Múltiplos +### Contêineres Múltiplos { #multiple-containers } -Se você tiver **múltiplos contêineres**, provavelmente cada um executando um **único processo** (por exemplo, em um cluster do **Kubernetes**), então provavelmente você gostaria de ter um **contêiner separado** fazendo o trabalho dos **passos anteriores** em um único contêiner, executando um único processo, **antes** de executar os contêineres trabalhadores replicados. +Se você tiver **múltiplos contêineres**, provavelmente cada um executando um **único processo** (por exemplo, em um cluster do **Kubernetes**), então provavelmente você gostaria de ter um **contêiner separado** fazendo o trabalho dos **passos anteriores** em um único contêiner, executando um único processo, **antes** de executar os contêineres workers replicados. -/// info +/// info | Informação Se você estiver usando o Kubernetes, provavelmente será um Init Container. @@ -532,85 +566,29 @@ Se você estiver usando o Kubernetes, provavelmente será um tiangolo/uvicorn-gunicorn-fastapi. Mas agora ela está descontinuada. ⛔️ -Essa imagem seria útil principalmente nas situações descritas acima em: [Contêineres com Múltiplos Processos e Casos Especiais](#conteineres-com-multiplos-processos-e-casos-especiais). +Você provavelmente **não** deve usar essa imagem base do Docker (ou qualquer outra semelhante). -* tiangolo/uvicorn-gunicorn-fastapi. +Se você está usando **Kubernetes** (ou outros) e já está definindo a **replicação** no nível do cluster, com vários **contêineres**. Nesses casos, é melhor **construir uma imagem do zero** como descrito acima: [Construir uma Imagem Docker para FastAPI](#build-a-docker-image-for-fastapi). -/// warning +E se você precisar ter múltiplos workers, você pode simplesmente usar a opção de linha de comando `--workers`. -Existe uma grande chance de que você **não** precise dessa imagem base ou de qualquer outra semelhante, e seria melhor construir a imagem do zero, como [descrito acima em: Construa uma Imagem Docker para o FastAPI](#construindo-uma-imagem-docker-para-fastapi). +/// note | Detalhes Técnicos + +A imagem Docker foi criada quando o Uvicorn não suportava gerenciar e reiniciar workers mortos, então era necessário usar o Gunicorn com o Uvicorn, o que adicionava bastante complexidade, apenas para que o Gunicorn gerenciasse e reiniciasse os processos workers do Uvicorn. + +Mas agora que o Uvicorn (e o comando `fastapi`) suportam o uso de `--workers`, não há razão para usar uma imagem base do Docker em vez de construir a sua própria (é praticamente a mesma quantidade de código 😅). /// -Essa imagem tem um mecanismo de **auto-ajuste** incluído para definir o **número de processos trabalhadores** com base nos núcleos de CPU disponíveis. - -Isso tem **padrões sensíveis**, mas você ainda pode alterar e atualizar todas as configurações com **variáveis de ambiente** ou arquivos de configuração. - -Há também suporte para executar **passos anteriores antes de iniciar** com um script. - -/// tip - -Para ver todas as configurações e opções, vá para a página da imagem Docker: tiangolo/uvicorn-gunicorn-fastapi. - -/// - -### Número de Processos na Imagem Oficial do Docker - -O **número de processos** nesta imagem é **calculado automaticamente** a partir dos **núcleos de CPU** disponíveis. - -Isso significa que ele tentará **aproveitar** o máximo de **desempenho** da CPU possível. - -Você também pode ajustá-lo com as configurações usando **variáveis de ambiente**, etc. - -Mas isso também significa que, como o número de processos depende da CPU do contêiner em execução, a **quantidade de memória consumida** também dependerá disso. - -Então, se seu aplicativo consumir muito memória (por exemplo, com modelos de aprendizado de máquina), e seu servidor tiver muitos núcleos de CPU **mas pouca memória**, então seu contêiner pode acabar tentando usar mais memória do que está disponível e degradar o desempenho muito (ou até mesmo travar). 🚨 - -### Criando um `Dockerfile` - -Aqui está como você criaria um `Dockerfile` baseado nessa imagem: - -```Dockerfile -FROM tiangolo/uvicorn-gunicorn-fastapi:python3.9 - -COPY ./requirements.txt /app/requirements.txt - -RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt - -COPY ./app /app -``` - -### Aplicações Maiores - -Se você seguiu a seção sobre a criação de [Aplicações Maiores com Múltiplos Arquivos](../tutorial/bigger-applications.md){.internal-link target=_blank}, seu `Dockerfile` pode parecer com isso: - -```Dockerfile - -```Dockerfile hl_lines="7" -FROM tiangolo/uvicorn-gunicorn-fastapi:python3.9 - -COPY ./requirements.txt /app/requirements.txt - -RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt - -COPY ./app /app/app -``` - -### Quando Usar - -Você provavelmente **não** deve usar essa imagem base oficial (ou qualquer outra semelhante) se estiver usando **Kubernetes** (ou outros) e já estiver definindo **replicação** no nível do cluster, com vários **contêineres**. Nesses casos, é melhor **construir uma imagem do zero** conforme descrito acima: [Construindo uma Imagem Docker para FastAPI](#construindo-uma-imagem-docker-para-fastapi). - -Essa imagem seria útil principalmente nos casos especiais descritos acima em [Contêineres com Múltiplos Processos e Casos Especiais](#conteineres-com-multiplos-processos-e-casos-especiais). Por exemplo, se sua aplicação for **simples o suficiente** para que a configuração padrão de número de processos com base na CPU funcione bem, você não quer se preocupar com a configuração manual da replicação no nível do cluster e não está executando mais de um contêiner com seu aplicativo. Ou se você estiver implantando com **Docker Compose**, executando em um único servidor, etc. - -## Deploy da Imagem do Contêiner +## Deploy da Imagem do Contêiner { #deploy-the-container-image } Depois de ter uma imagem de contêiner (Docker), existem várias maneiras de implantá-la. @@ -622,100 +600,11 @@ Por exemplo: * Com outra ferramenta como o Nomad * Com um serviço de nuvem que pega sua imagem de contêiner e a implanta -## Imagem Docker com Poetry +## Imagem Docker com `uv` { #docker-image-with-uv } -Se você usa Poetry para gerenciar as dependências do seu projeto, pode usar a construção multi-estágio do Docker: +Se você está usando o uv para instalar e gerenciar seu projeto, você pode seguir o guia de Docker do uv. -```{ .dockerfile .annotate } -# (1) -FROM python:3.9 as requirements-stage - -# (2) -WORKDIR /tmp - -# (3) -RUN pip install poetry - -# (4) -COPY ./pyproject.toml ./poetry.lock* /tmp/ - -# (5) -RUN poetry export -f requirements.txt --output requirements.txt --without-hashes - -# (6) -FROM python:3.9 - -# (7) -WORKDIR /code - -# (8) -COPY --from=requirements-stage /tmp/requirements.txt /code/requirements.txt - -# (9) -RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt - -# (10) -COPY ./app /code/app - -# (11) -CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"] -``` - -1. Esse é o primeiro estágio, ele é chamado `requirements-stage`. - -2. Defina `/tmp` como o diretório de trabalho atual. - - Aqui é onde geraremos o arquivo `requirements.txt` - -3. Instale o Poetry nesse estágio do Docker. - -4. Copie os arquivos `pyproject.toml` e `poetry.lock` para o diretório `/tmp`. - - Porque está usando `./poetry.lock*` (terminando com um `*`), não irá falhar se esse arquivo ainda não estiver disponível. - -5. Gere o arquivo `requirements.txt`. - -6. Este é o estágio final, tudo aqui será preservado na imagem final do contêiner. - -7. Defina o diretório de trabalho atual como `/code`. - -8. Copie o arquivo `requirements.txt` para o diretório `/code`. - - Essse arquivo só existe no estágio anterior do Docker, é por isso que usamos `--from-requirements-stage` para copiá-lo. - -9. Instale as dependências de pacote do arquivo `requirements.txt` gerado. - -10. Copie o diretório `app` para o diretório `/code`. - -11. Execute o comando `uvicorn`, informando-o para usar o objeto `app` importado de `app.main`. - -/// tip - -Clique nos números das bolhas para ver o que cada linha faz. - -/// - -Um **estágio do Docker** é uma parte de um `Dockerfile` que funciona como uma **imagem temporária do contêiner** que só é usada para gerar alguns arquivos para serem usados posteriormente. - -O primeiro estágio será usado apenas para **instalar Poetry** e para **gerar o `requirements.txt`** com as dependências do seu projeto a partir do arquivo `pyproject.toml` do Poetry. - -Esse arquivo `requirements.txt` será usado com `pip` mais tarde no **próximo estágio**. - -Na imagem final do contêiner, **somente o estágio final** é preservado. Os estágios anteriores serão descartados. - -Quando usar Poetry, faz sentido usar **construções multi-estágio do Docker** porque você realmente não precisa ter o Poetry e suas dependências instaladas na imagem final do contêiner, você **apenas precisa** ter o arquivo `requirements.txt` gerado para instalar as dependências do seu projeto. - -Então, no próximo (e último) estágio, você construiria a imagem mais ou menos da mesma maneira descrita anteriormente. - -### Por trás de um proxy de terminação TLS - Poetry - -Novamente, se você estiver executando seu contêiner atrás de um proxy de terminação TLS (balanceador de carga) como Nginx ou Traefik, adicione a opção `--proxy-headers` ao comando: - -```Dockerfile -CMD ["uvicorn", "app.main:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "80"] -``` - -## Recapitulando +## Recapitulando { #recap } Usando sistemas de contêiner (por exemplo, com **Docker** e **Kubernetes**), torna-se bastante simples lidar com todos os **conceitos de implantação**: @@ -724,10 +613,8 @@ Usando sistemas de contêiner (por exemplo, com **Docker** e **Kubernetes**), to * Reinícios * Replicação (o número de processos rodando) * Memória -* Passos anteriores antes de inicializar +* Passos anteriores antes de iniciar Na maioria dos casos, você provavelmente não desejará usar nenhuma imagem base e, em vez disso, **construir uma imagem de contêiner do zero** baseada na imagem oficial do Docker Python. -Tendo cuidado com a **ordem** das instruções no `Dockerfile` e o **cache do Docker**, você pode **minimizar os tempos de construção**, para maximizar sua produtividade (e evitar a tédio). 😎 - -Em alguns casos especiais, você pode querer usar a imagem oficial do Docker para o FastAPI. 🤓 +Tendo cuidado com a **ordem** das instruções no `Dockerfile` e o **cache do Docker**, você pode **minimizar os tempos de construção**, para maximizar sua produtividade (e evitar o tédio). 😎 diff --git a/docs/pt/docs/deployment/fastapicloud.md b/docs/pt/docs/deployment/fastapicloud.md new file mode 100644 index 0000000000..03d3bd03be --- /dev/null +++ b/docs/pt/docs/deployment/fastapicloud.md @@ -0,0 +1,65 @@ +# FastAPI Cloud { #fastapi-cloud } + +Você pode implantar sua aplicação FastAPI no FastAPI Cloud com um **único comando**; entre na lista de espera, caso ainda não tenha feito isso. 🚀 + +## Login { #login } + +Certifique-se de que você já tem uma conta no **FastAPI Cloud** (nós convidamos você a partir da lista de espera 😉). + +Depois, faça login: + +
+ +```console +$ fastapi login + +You are logged in to FastAPI Cloud 🚀 +``` + +
+ +## Implantar { #deploy } + +Agora, implante sua aplicação, com **um único comando**: + +
+ +```console +$ fastapi deploy + +Deploying to FastAPI Cloud... + +✅ Deployment successful! + +🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev +``` + +
+ +É isso! Agora você pode acessar sua aplicação nesse URL. ✨ + +## Sobre o FastAPI Cloud { #about-fastapi-cloud } + +O **FastAPI Cloud** é desenvolvido pelo mesmo autor e equipe por trás do **FastAPI**. + +Ele simplifica o processo de **criar**, **implantar** e **acessar** uma API com esforço mínimo. + +Traz a mesma **experiência do desenvolvedor** de criar aplicações com FastAPI para **implantá-las** na nuvem. 🎉 + +Ele também cuidará da maioria das coisas de que você precisaria ao implantar uma aplicação, como: + +* HTTPS +* Replicação, com dimensionamento automático baseado em requests +* etc. + +O FastAPI Cloud é o patrocinador principal e provedor de financiamento dos projetos open source do ecossistema *FastAPI and friends*. ✨ + +## Implantar em outros provedores de nuvem { #deploy-to-other-cloud-providers } + +FastAPI é open source e baseado em padrões. Você pode implantar aplicações FastAPI em qualquer provedor de nuvem que escolher. + +Siga os tutoriais do seu provedor de nuvem para implantar aplicações FastAPI com esse provedor. 🤓 + +## Implantar no seu próprio servidor { #deploy-your-own-server } + +Também vou lhe ensinar, mais adiante neste guia de **Implantação**, todos os detalhes, para que você possa entender o que está acontecendo, o que precisa acontecer, ou como implantar aplicações FastAPI por conta própria, inclusive nos seus próprios servidores. 🤓 diff --git a/docs/pt/docs/deployment/https.md b/docs/pt/docs/deployment/https.md index 904d04eaa5..6195cefab2 100644 --- a/docs/pt/docs/deployment/https.md +++ b/docs/pt/docs/deployment/https.md @@ -1,4 +1,4 @@ -# Sobre HTTPS +# Sobre HTTPS { #about-https } É fácil assumir que HTTPS é algo que é apenas "habilitado" ou não. @@ -10,31 +10,31 @@ Se você está com pressa ou não se importa, continue com as seções seguintes /// -Para aprender o básico de HTTPS de uma perspectiva do usuário, verifique https://howhttps.works/pt-br/. +Para aprender o básico de HTTPS do ponto de vista do consumidor, verifique https://howhttps.works/. Agora, a partir de uma perspectiva do desenvolvedor, aqui estão algumas coisas para ter em mente ao pensar em HTTPS: -* Para HTTPS, **o servidor** precisa ter certificados gerados por **um terceiro**. - * Esses certificados são na verdade **adquiridos** de um terceiro, eles não são simplesmente "gerados". -* Certificados têm um **tempo de vida**. - * Eles **expiram**. - * E então eles precisam ser **renovados**, **adquirindo-os novamente** de um terceiro. -* A criptografia da conexão acontece no **nível TCP**. - * Essa é uma camada **abaixo do HTTP**. - * Portanto, o manuseio do **certificado e da criptografia** é feito **antes do HTTP**. -* **O TCP não sabe sobre "domínios"**. Apenas sobre endereços IP. - * As informações sobre o **domínio solicitado** vão nos **dados HTTP**. -* Os **certificados HTTPS** “certificam” um **determinado domínio**, mas o protocolo e a encriptação acontecem ao nível do TCP, **antes de sabermos** de que domínio se trata. -* **Por padrão**, isso significa que você só pode ter **um certificado HTTPS por endereço IP**. +* Para HTTPS, o servidor precisa ter "certificados" gerados por um terceiro. + * Esses certificados são na verdade adquiridos de um terceiro, eles não são simplesmente "gerados". +* Certificados têm um tempo de vida. + * Eles expiram. + * E então eles precisam ser renovados, adquirindo-os novamente de um terceiro. +* A criptografia da conexão acontece no nível TCP. + * Essa é uma camada abaixo do HTTP. + * Portanto, o manuseio do certificado e da criptografia é feito antes do HTTP. +* O TCP não sabe sobre "domínios". Apenas sobre endereços IP. + * As informações sobre o domínio específico solicitado vão nos dados HTTP. +* Os certificados HTTPS “certificam” um determinado domínio, mas o protocolo e a encriptação acontecem ao nível do TCP, antes de sabermos de que domínio se trata. +* Por padrão, isso significa que você só pode ter um certificado HTTPS por endereço IP. * Não importa o tamanho do seu servidor ou quão pequeno cada aplicativo que você tem nele possa ser. - * No entanto, existe uma **solução** para isso. -* Há uma **extensão** para o protocolo **TLS** (aquele que lida com a criptografia no nível TCP, antes do HTTP) chamado **SNI**. - * Esta extensão SNI permite que um único servidor (com um **único endereço IP**) tenha **vários certificados HTTPS** e atenda a **vários domínios / aplicativos HTTPS**. - * Para que isso funcione, um **único** componente (programa) em execução no servidor, ouvindo no **endereço IP público**, deve ter **todos os certificados HTTPS** no servidor. -* **Depois** de obter uma conexão segura, o protocolo de comunicação **ainda é HTTP**. - * Os conteúdos são **criptografados**, embora sejam enviados com o **protocolo HTTP**. + * No entanto, existe uma solução para isso. +* Há uma extensão para o protocolo TLS (aquele que lida com a criptografia no nível TCP, antes do HTTP) chamada SNI. + * Esta extensão SNI permite que um único servidor (com um único endereço IP) tenha vários certificados HTTPS e atenda a vários domínios / aplicativos HTTPS. + * Para que isso funcione, um único componente (programa) em execução no servidor, ouvindo no endereço IP público, deve ter todos os certificados HTTPS no servidor. +* Depois de obter uma conexão segura, o protocolo de comunicação ainda é HTTP. + * Os conteúdos são criptografados, embora sejam enviados com o protocolo HTTP. -É uma prática comum ter um **programa/servidor HTTP** em execução no servidor (máquina, host, etc.) e **gerenciar todas as partes HTTPS**: **recebendo as requisições encriptadas**, enviando as **solicitações HTTP descriptografadas** para o aplicativo HTTP real em execução no mesmo servidor (a aplicação **FastAPI**, neste caso), pegue a **resposta HTTP** do aplicativo, **criptografe-a** usando o **certificado HTTPS** apropriado e envie-a de volta ao cliente usando **HTTPS**. Este servidor é frequentemente chamado de **Proxy de Terminação TLS**. +É uma prática comum ter um programa/servidor HTTP em execução no servidor (máquina, host, etc.) e gerenciar todas as partes HTTPS: recebendo as requisições HTTPS encriptadas, enviando as solicitações HTTP descriptografadas para o aplicativo HTTP real em execução no mesmo servidor (a aplicação FastAPI, neste caso), pegar a resposta HTTP do aplicativo, criptografá-la usando o certificado HTTPS apropriado e enviá-la de volta ao cliente usando HTTPS. Este servidor é frequentemente chamado de Proxy de Terminação TLS. Algumas das opções que você pode usar como Proxy de Terminação TLS são: @@ -43,31 +43,31 @@ Algumas das opções que você pode usar como Proxy de Terminação TLS são: * Nginx * HAProxy -## Let's Encrypt +## Let's Encrypt { #lets-encrypt } -Antes de Let's Encrypt, esses **certificados HTTPS** eram vendidos por terceiros confiáveis. +Antes de Let's Encrypt, esses certificados HTTPS eram vendidos por terceiros confiáveis. O processo de aquisição de um desses certificados costumava ser complicado, exigia bastante papelada e os certificados eram bastante caros. -Mas então o **Let's Encrypt** foi criado. +Mas então o Let's Encrypt foi criado. -Ele é um projeto da Linux Foundation que fornece **certificados HTTPS gratuitamente** . De forma automatizada. Esses certificados usam toda a segurança criptográfica padrão e têm vida curta (cerca de 3 meses), então a **segurança é, na verdade, melhor** por causa de sua vida útil reduzida. +Ele é um projeto da Linux Foundation que fornece certificados HTTPS gratuitamente. De forma automatizada. Esses certificados usam toda a segurança criptográfica padrão e têm vida curta (cerca de 3 meses), então a segurança é, na verdade, melhor por causa do seu lifespan reduzido. Os domínios são verificados com segurança e os certificados são gerados automaticamente. Isso também permite automatizar a renovação desses certificados. -A ideia é automatizar a aquisição e renovação desses certificados, para que você tenha **HTTPS seguro, de graça e para sempre**. +A ideia é automatizar a aquisição e renovação desses certificados, para que você tenha HTTPS seguro, de graça e para sempre. -## HTTPS para Desenvolvedores +## HTTPS para Desenvolvedores { #https-for-developers } Aqui está um exemplo de como uma API HTTPS poderia ser estruturada, passo a passo, com foco principal nas ideias relevantes para desenvolvedores. -### Nome do domínio +### Nome do domínio { #domain-name } -A etapa inicial provavelmente seria **adquirir** algum **nome de domínio**. Então, você iria configurá-lo em um servidor DNS (possivelmente no mesmo provedor em nuvem). +A etapa inicial provavelmente seria adquirir algum nome de domínio. Então, você iria configurá-lo em um servidor DNS (possivelmente no mesmo provedor em nuvem). -Você provavelmente usaria um servidor em nuvem (máquina virtual) ou algo parecido, e ele teria fixed **Endereço IP público**. +Você provavelmente usaria um servidor em nuvem (máquina virtual) ou algo parecido, e ele teria um fixo Endereço IP público. -No(s) servidor(es) DNS, você configuraria um registro (`registro A`) para apontar **seu domínio** para o **endereço IP público do seu servidor**. +No(s) servidor(es) DNS, você configuraria um registro (um `A record`) para apontar seu domínio para o endereço IP público do seu servidor. Você provavelmente fará isso apenas uma vez, na primeira vez em que tudo estiver sendo configurado. @@ -77,123 +77,155 @@ Essa parte do Nome do Domínio se dá muito antes do HTTPS, mas como tudo depend /// -### DNS +### DNS { #dns } Agora vamos focar em todas as partes que realmente fazem parte do HTTPS. -Primeiro, o navegador iria verificar com os **servidores DNS** qual o **IP do domínio**, nesse caso, `someapp.example.com`. +Primeiro, o navegador iria verificar com os servidores DNS qual o IP do domínio, nesse caso, `someapp.example.com`. -Os servidores DNS iriam informar o navegador para utilizar algum **endereço IP** específico. Esse seria o endereço IP público em uso no seu servidor, que você configurou nos servidores DNS. +Os servidores DNS iriam informar o navegador para utilizar algum endereço IP específico. Esse seria o endereço IP público em uso no seu servidor, que você configurou nos servidores DNS. -### Início do Handshake TLS +### Início do Handshake TLS { #tls-handshake-start } -O navegador então irá comunicar-se com esse endereço IP na **porta 443** (a porta HTTPS). +O navegador então irá comunicar-se com esse endereço IP na porta 443 (a porta HTTPS). A primeira parte dessa comunicação é apenas para estabelecer a conexão entre o cliente e o servidor e para decidir as chaves criptográficas a serem utilizadas, etc. -Esse interação entre o cliente e o servidor para estabelecer uma conexão TLS é chamada de **Handshake TLS**. +Esse interação entre o cliente e o servidor para estabelecer uma conexão TLS é chamada de Handshake TLS. -### TLS com a Extensão SNI +### TLS com a Extensão SNI { #tls-with-sni-extension } -**Apenas um processo** no servidor pode se conectar a uma **porta** em um **endereço IP**. Poderiam existir outros processos conectados em outras portas desse mesmo endereço IP, mas apenas um para cada combinação de endereço IP e porta. +Apenas um processo no servidor pode se conectar a uma porta em um endereço IP. Poderiam existir outros processos conectados em outras portas desse mesmo endereço IP, mas apenas um para cada combinação de endereço IP e porta. TLS (HTTPS) usa a porta `443` por padrão. Então essa é a porta que precisamos. -Como apenas um único processo pode se comunicar com essa porta, o processo que faria isso seria o **Proxy de Terminação TLS**. +Como apenas um único processo pode se comunicar com essa porta, o processo que faria isso seria o Proxy de Terminação TLS. -O Proxy de Terminação TLS teria acesso a um ou mais **certificados TLS** (certificados HTTPS). +O Proxy de Terminação TLS teria acesso a um ou mais certificados TLS (certificados HTTPS). -Utilizando a **extensão SNI** discutida acima, o Proxy de Terminação TLS iria checar qual dos certificados TLS (HTTPS) disponíveis deve ser usado para essa conexão, utilizando o que corresponda ao domínio esperado pelo cliente. +Utilizando a extensão SNI discutida acima, o Proxy de Terminação TLS iria checar qual dos certificados TLS (HTTPS) disponíveis deve ser usado para essa conexão, utilizando o que corresponda ao domínio esperado pelo cliente. Nesse caso, ele usaria o certificado para `someapp.example.com`. -O cliente já **confia** na entidade que gerou o certificado TLS (nesse caso, o Let's Encrypt, mas veremos sobre isso mais tarde), então ele pode **verificar** que o certificado é válido. +O cliente já confia na entidade que gerou o certificado TLS (nesse caso, o Let's Encrypt, mas veremos sobre isso mais tarde), então ele pode verificar que o certificado é válido. -Então, utilizando o certificado, o cliente e o Proxy de Terminação TLS **decidem como encriptar** o resto da **comunicação TCP**. Isso completa a parte do **Handshake TLS**. +Então, utilizando o certificado, o cliente e o Proxy de Terminação TLS decidem como encriptar o resto da comunicação TCP. Isso completa a parte do Handshake TLS. -Após isso, o cliente e o servidor possuem uma **conexão TCP encriptada**, que é provida pelo TLS. E então eles podem usar essa conexão para começar a **comunicação HTTP** propriamente dita. +Após isso, o cliente e o servidor possuem uma conexão TCP encriptada, que é provida pelo TLS. E então eles podem usar essa conexão para começar a comunicação HTTP propriamente dita. -E isso resume o que é **HTTPS**, apenas **HTTP** simples dentro de uma **conexão TLS segura** em vez de uma conexão TCP pura (não encriptada). +E isso resume o que é HTTPS, apenas HTTP simples dentro de uma conexão TLS segura em vez de uma conexão TCP pura (não encriptada). /// tip | Dica -Percebe que a encriptação da comunicação acontece no **nível do TCP**, não no nível do HTTP. +Percebe que a encriptação da comunicação acontece no nível do TCP, não no nível do HTTP. /// -### Solicitação HTTPS +### Solicitação HTTPS { #https-request } -Agora que o cliente e servidor (especialmente o navegador e o Proxy de Terminação TLS) possuem uma **conexão TCP encriptada**, eles podem iniciar a **comunicação HTTP**. +Agora que o cliente e servidor (especialmente o navegador e o Proxy de Terminação TLS) possuem uma conexão TCP encriptada, eles podem iniciar a comunicação HTTP. -Então, o cliente envia uma **solicitação HTTPS**. Que é apenas uma solicitação HTTP sobre uma conexão TLS encriptada. +Então, o cliente envia uma solicitação HTTPS. Que é apenas uma solicitação HTTP sobre uma conexão TLS encriptada. -### Desencriptando a Solicitação +### Desencriptando a Solicitação { #decrypt-the-request } -O Proxy de Terminação TLS então usaria a encriptação combinada para **desencriptar a solicitação**, e transmitiria a **solicitação básica (desencriptada)** para o processo executando a aplicação (por exemplo, um processo com Uvicorn executando a aplicação FastAPI). +O Proxy de Terminação TLS então usaria a encriptação combinada para desencriptar a solicitação, e transmitiria a solicitação básica (desencriptada) para o processo executando a aplicação (por exemplo, um processo com Uvicorn executando a aplicação FastAPI). -### Resposta HTTP +### Resposta HTTP { #http-response } -A aplicação processaria a solicitação e retornaria uma **resposta HTTP básica (não encriptada)** para o Proxy de Terminação TLS. +A aplicação processaria a solicitação e retornaria uma resposta HTTP básica (não encriptada) para o Proxy de Terminação TLS. -### Resposta HTTPS +### Resposta HTTPS { #https-response } -O Proxy de Terminação TLS iria **encriptar a resposta** utilizando a criptografia combinada anteriormente (que foi definida com o certificado para `someapp.example.com`), e devolveria para o navegador. +O Proxy de Terminação TLS iria encriptar a resposta utilizando a criptografia combinada anteriormente (que foi definida com o certificado para `someapp.example.com`), e devolveria para o navegador. -No próximo passo, o navegador verifica que a resposta é válida e encriptada com a chave criptográfica correta, etc. E depois **desencripta a resposta** e a processa. +No próximo passo, o navegador verifica que a resposta é válida e encriptada com a chave criptográfica correta, etc. E depois desencripta a resposta e a processa. -O cliente (navegador) saberá que a resposta vem do servidor correto por que ela usa a criptografia que foi combinada entre eles usando o **certificado HTTPS** anterior. +O cliente (navegador) saberá que a resposta vem do servidor correto por que ela usa a criptografia que foi combinada entre eles usando o certificado HTTPS anterior. -### Múltiplas Aplicações +### Múltiplas Aplicações { #multiple-applications } -Podem existir **múltiplas aplicações** em execução no mesmo servidor (ou servidores), por exemplo: outras APIs ou um banco de dados. +Podem existir múltiplas aplicações em execução no mesmo servidor (ou servidores), por exemplo: outras APIs ou um banco de dados. -Apenas um processo pode estar vinculado a um IP e porta (o Proxy de Terminação TLS, por exemplo), mas outras aplicações/processos também podem estar em execução no(s) servidor(es), desde que não tentem usar a mesma **combinação de IP público e porta**. +Apenas um processo pode estar vinculado a um IP e porta (o Proxy de Terminação TLS, por exemplo), mas outras aplicações/processos também podem estar em execução no(s) servidor(es), desde que não tentem usar a mesma combinação de IP público e porta. -Dessa forma, o Proxy de Terminação TLS pode gerenciar o HTTPS e os certificados de **múltiplos domínios**, para múltiplas aplicações, e então transmitir as requisições para a aplicação correta em cada caso. +Dessa forma, o Proxy de Terminação TLS pode gerenciar o HTTPS e os certificados de múltiplos domínios, para múltiplas aplicações, e então transmitir as requisições para a aplicação correta em cada caso. -### Renovação de Certificados +### Renovação de Certificados { #certificate-renewal } -Em algum momento futuro, cada certificado irá **expirar** (aproximadamente 3 meses após a aquisição). +Em algum momento futuro, cada certificado irá expirar (aproximadamente 3 meses após a aquisição). E então, haverá outro programa (em alguns casos pode ser o próprio Proxy de Terminação TLS) que irá interagir com o Let's Encrypt e renovar o(s) certificado(s). -Os **certificados TLS** são **associados com um nome de domínio**, e não a um endereço IP. +Os certificados TLS são associados com um nome de domínio, e não a um endereço IP. -Então para renovar os certificados, o programa de renovação precisa **provar** para a autoridade (Let's Encrypt) que ele realmente **possui e controla esse domínio**> +Então para renovar os certificados, o programa de renovação precisa provar para a autoridade (Let's Encrypt) que ele realmente "possui" e controla esse domínio. Para fazer isso, e acomodar as necessidades de diferentes aplicações, existem diferentes opções para esse programa. Algumas escolhas populares são: -* **Modificar alguns registros DNS** - * Para isso, o programa de renovação precisa ter suporte as APIs do provedor DNS, então, dependendo do provedor DNS que você utilize, isso pode ou não ser uma opção viável. -* **Executar como um servidor** (ao menos durante o processo de aquisição do certificado) no endereço IP público associado com o domínio. +* Modificar alguns registros DNS + * Para isso, o programa de renovação precisa ter suporte às APIs do provedor DNS, então, dependendo do provedor DNS que você utilize, isso pode ou não ser uma opção viável. +* Executar como um servidor (ao menos durante o processo de aquisição do certificado) no endereço IP público associado com o domínio. * Como dito anteriormente, apenas um processo pode estar ligado a uma porta e IP específicos. * Essa é uma dos motivos que fazem utilizar o mesmo Proxy de Terminação TLS para gerenciar a renovação de certificados ser tão útil. * Caso contrário, você pode ter que parar a execução do Proxy de Terminação TLS momentaneamente, inicializar o programa de renovação para renovar os certificados, e então reiniciar o Proxy de Terminação TLS. Isso não é o ideal, já que sua(s) aplicação(ões) não vão estar disponíveis enquanto o Proxy de Terminação TLS estiver desligado. -Todo esse processo de renovação, enquanto o aplicativo ainda funciona, é uma das principais razões para preferir um **sistema separado para gerenciar HTTPS** com um Proxy de Terminação TLS em vez de usar os certificados TLS no servidor da aplicação diretamente (e.g. com o Uvicorn). +Todo esse processo de renovação, enquanto o aplicativo ainda funciona, é uma das principais razões para preferir um sistema separado para gerenciar HTTPS com um Proxy de Terminação TLS em vez de usar os certificados TLS no servidor da aplicação diretamente (e.g. com o Uvicorn). -## Recapitulando +## Cabeçalhos encaminhados por Proxy { #proxy-forwarded-headers } -Possuir **HTTPS** habilitado na sua aplicação é bastante importante, e até **crítico** na maioria dos casos. A maior parte do esforço que você tem que colocar sobre o HTTPS como desenvolvedor está em **entender esses conceitos** e como eles funcionam. +Ao usar um proxy para lidar com HTTPS, seu servidor de aplicação (por exemplo, Uvicorn via FastAPI CLI) não sabe nada sobre o processo de HTTPS; ele se comunica com HTTP simples com o Proxy de Terminação TLS. -Mas uma vez que você saiba o básico de **HTTPS para desenvolvedores**, você pode combinar e configurar diferentes ferramentas facilmente para gerenciar tudo de uma forma simples. +Esse proxy normalmente define alguns cabeçalhos HTTP dinamicamente antes de transmitir a requisição para o servidor de aplicação, para informar ao servidor de aplicação que a requisição está sendo encaminhada pelo proxy. -Em alguns dos próximos capítulos, eu mostrarei para você vários exemplos concretos de como configurar o **HTTPS** para aplicações **FastAPI**. 🔒 +/// note | Detalhes Técnicos + +Os cabeçalhos do proxy são: + +* X-Forwarded-For +* X-Forwarded-Proto +* X-Forwarded-Host + +/// + +No entanto, como o servidor de aplicação não sabe que está atrás de um proxy confiável, por padrão ele não confiaria nesses cabeçalhos. + +Mas você pode configurar o servidor de aplicação para confiar nos cabeçalhos encaminhados enviados pelo proxy. Se você estiver usando o FastAPI CLI, pode usar a opção de CLI `--forwarded-allow-ips` para dizer de quais IPs ele deve confiar nesses cabeçalhos encaminhados. + +Por exemplo, se o servidor de aplicação só estiver recebendo comunicação do proxy confiável, você pode defini-lo como `--forwarded-allow-ips="*"` para fazê-lo confiar em todos os IPs de entrada, já que ele só receberá requisições de seja lá qual for o IP usado pelo proxy. + +Dessa forma, a aplicação seria capaz de saber qual é sua própria URL pública, se está usando HTTPS, o domínio, etc. + +Isso seria útil, por exemplo, para lidar corretamente com redirecionamentos. + +/// tip | Dica + +Você pode saber mais sobre isso na documentação em [Atrás de um Proxy - Habilitar cabeçalhos encaminhados pelo proxy](../advanced/behind-a-proxy.md#enable-proxy-forwarded-headers){.internal-link target=_blank} + +/// + +## Recapitulando { #recap } + +Possuir HTTPS habilitado na sua aplicação é bastante importante, e até crítico na maioria dos casos. A maior parte do esforço que você tem que colocar sobre o HTTPS como desenvolvedor está em entender esses conceitos e como eles funcionam. + +Mas uma vez que você saiba o básico de HTTPS para desenvolvedores, você pode combinar e configurar diferentes ferramentas facilmente para gerenciar tudo de uma forma simples. + +Em alguns dos próximos capítulos, eu mostrarei para você vários exemplos concretos de como configurar o HTTPS para aplicações FastAPI. 🔒 diff --git a/docs/pt/docs/deployment/index.md b/docs/pt/docs/deployment/index.md index 6b4290d1d7..6a6c21804d 100644 --- a/docs/pt/docs/deployment/index.md +++ b/docs/pt/docs/deployment/index.md @@ -1,7 +1,23 @@ -# Implantação +# Implantação { #deployment } -A implantação de uma aplicação **FastAPI** é relativamente simples. +Implantar uma aplicação **FastAPI** é relativamente fácil. -Existem várias maneiras para fazer isso, dependendo do seu caso específico e das ferramentas que você utiliza. +## O que significa Implantação { #what-does-deployment-mean } -Você verá mais detalhes para se ter em mente e algumas das técnicas para a implantação nas próximas seções. +Implantar uma aplicação significa executar as etapas necessárias para torná-la disponível para os usuários. + +Para uma **web API**, isso normalmente envolve colocá-la em uma **máquina remota**, com um **programa de servidor** que ofereça bom desempenho, estabilidade, etc., de modo que seus **usuários** possam **acessar** a aplicação com eficiência e sem interrupções ou problemas. + +Isso contrasta com as fases de **desenvolvimento**, em que você está constantemente alterando o código, quebrando e consertando, parando e reiniciando o servidor de desenvolvimento, etc. + +## Estratégias de Implantação { #deployment-strategies } + +Há várias maneiras de fazer isso, dependendo do seu caso de uso específico e das ferramentas que você utiliza. + +Você pode **implantar um servidor** por conta própria usando uma combinação de ferramentas, pode usar um **serviço em nuvem** que faça parte do trabalho por você, entre outras opções. + +Por exemplo, nós, a equipe por trás do FastAPI, criamos **FastAPI Cloud**, para tornar a implantação de aplicações FastAPI na nuvem o mais simples possível, com a mesma experiência de desenvolvimento de trabalhar com o FastAPI. + +Vou mostrar alguns dos principais conceitos que você provavelmente deve ter em mente ao implantar uma aplicação **FastAPI** (embora a maior parte se aplique a qualquer outro tipo de aplicação web). + +Você verá mais detalhes para ter em mente e algumas das técnicas para fazer isso nas próximas seções. ✨ diff --git a/docs/pt/docs/deployment/manually.md b/docs/pt/docs/deployment/manually.md index c7caabbcd0..21d0f44cdf 100644 --- a/docs/pt/docs/deployment/manually.md +++ b/docs/pt/docs/deployment/manually.md @@ -1,6 +1,6 @@ -# Execute um Servidor Manualmente +# Execute um Servidor Manualmente { #run-a-server-manually } -## Utilize o comando `fastapi run` +## Utilize o comando `fastapi run` { #use-the-fastapi-run-command } Em resumo, utilize o comando `fastapi run` para inicializar sua aplicação FastAPI: @@ -42,23 +42,23 @@ Isto deve funcionar para a maioria dos casos. 😎 Você pode utilizar esse comando, por exemplo, para iniciar sua aplicação **FastAPI** em um contêiner, em um servidor, etc. -## Servidores ASGI +## Servidores ASGI { #asgi-servers } Vamos nos aprofundar um pouco mais em detalhes. -FastAPI utiliza um padrão para construir frameworks e servidores web em Python chamado ASGI. FastAPI é um framework web ASGI. +FastAPI utiliza um padrão para construir frameworks e servidores web em Python chamado ASGI. FastAPI é um framework web ASGI. A principal coisa que você precisa para executar uma aplicação **FastAPI** (ou qualquer outra aplicação ASGI) em uma máquina de servidor remoto é um programa de servidor ASGI como o **Uvicorn**, que é o que vem por padrão no comando `fastapi`. Existem diversas alternativas, incluindo: * Uvicorn: um servidor ASGI de alta performance. -* Hypercorn: um servidor ASGI compátivel com HTTP/2, Trio e outros recursos. +* Hypercorn: um servidor ASGI compatível com HTTP/2, Trio e outros recursos. * Daphne: servidor ASGI construído para Django Channels. * Granian: um servidor HTTP Rust para aplicações Python. * NGINX Unit: NGINX Unit é um runtime de aplicação web leve e versátil. -## Máquina Servidora e Programa Servidor +## Máquina Servidora e Programa Servidor { #server-machine-and-server-program } Existe um pequeno detalhe sobre estes nomes para se manter em mente. 💡 @@ -68,7 +68,7 @@ Apenas tenha em mente que quando você ler "servidor" em geral, isso pode se ref Quando se refere à máquina remota, é comum chamá-la de **servidor**, mas também de **máquina**, **VM** (máquina virtual), **nó**. Todos esses termos se referem a algum tipo de máquina remota, normalmente executando Linux, onde você executa programas. -## Instale o Programa Servidor +## Instale o Programa Servidor { #install-the-server-program } Quando você instala o FastAPI, ele vem com um servidor de produção, o Uvicorn, e você pode iniciá-lo com o comando `fastapi run`. @@ -100,7 +100,7 @@ Quando você instala o FastAPI com algo como `pip install "fastapi[standard]"`, /// -## Execute o Programa Servidor +## Execute o Programa Servidor { #run-the-server-program } Se você instalou um servidor ASGI manualmente, normalmente precisará passar uma string de importação em um formato especial para que ele importe sua aplicação FastAPI: @@ -131,7 +131,7 @@ from main import app Cada programa de servidor ASGI alternativo teria um comando semelhante, você pode ler mais na documentação respectiva. -/// warning | Aviso +/// warning | Atenção Uvicorn e outros servidores suportam a opção `--reload` que é útil durante o desenvolvimento. @@ -141,7 +141,7 @@ Ela ajuda muito durante o **desenvolvimento**, mas você **não deve** usá-la e /// -## Conceitos de Implantação +## Conceitos de Implantação { #deployment-concepts } Esses exemplos executam o programa do servidor (por exemplo, Uvicorn), iniciando **um único processo**, ouvindo em todos os IPs (`0.0.0.0`) em uma porta predefinida (por exemplo, `80`). diff --git a/docs/pt/docs/deployment/server-workers.md b/docs/pt/docs/deployment/server-workers.md index a0db1bea42..bfb1e66873 100644 --- a/docs/pt/docs/deployment/server-workers.md +++ b/docs/pt/docs/deployment/server-workers.md @@ -1,4 +1,4 @@ -# Trabalhadores do Servidor - Uvicorn com Trabalhadores +# Trabalhadores do Servidor - Uvicorn com Trabalhadores { #server-workers-uvicorn-with-workers } Vamos rever os conceitos de implantação anteriores: @@ -25,7 +25,7 @@ Em particular, ao executar no **Kubernetes** você provavelmente **não** vai qu /// -## Vários trabalhadores +## Vários trabalhadores { #multiple-workers } Você pode iniciar vários trabalhadores com a opção de linha de comando `--workers`: @@ -111,7 +111,7 @@ A única opção nova aqui é `--workers` informando ao Uvicorn para iniciar 4 p Você também pode ver que ele mostra o **PID** de cada processo, `27365` para o processo pai (este é o **gerenciador de processos**) e um para cada processo de trabalho: `27368`, `27369`, `27370` e `27367`. -## Conceitos de Implantação +## Conceitos de Implantação { #deployment-concepts } Aqui você viu como usar vários **trabalhadores** para **paralelizar** a execução do aplicativo, aproveitar **vários núcleos** na CPU e conseguir atender **mais solicitações**. @@ -124,13 +124,13 @@ Da lista de conceitos de implantação acima, o uso de trabalhadores ajudaria pr * **Memória** * **Etapas anteriores antes de iniciar** -## Contêineres e Docker +## Contêineres e Docker { #containers-and-docker } No próximo capítulo sobre [FastAPI em contêineres - Docker](docker.md){.internal-link target=_blank}, explicarei algumas estratégias que você pode usar para lidar com os outros **conceitos de implantação**. Vou mostrar como **construir sua própria imagem do zero** para executar um único processo Uvicorn. É um processo simples e provavelmente é o que você gostaria de fazer ao usar um sistema de gerenciamento de contêineres distribuídos como o **Kubernetes**. -## Recapitular +## Recapitular { #recap } Você pode usar vários processos de trabalho com a opção CLI `--workers` com os comandos `fastapi` ou `uvicorn` para aproveitar as vantagens de **CPUs multi-core** e executar **vários processos em paralelo**. diff --git a/docs/pt/docs/deployment/versions.md b/docs/pt/docs/deployment/versions.md index 323ddbd450..a2aca5a17e 100644 --- a/docs/pt/docs/deployment/versions.md +++ b/docs/pt/docs/deployment/versions.md @@ -1,46 +1,46 @@ -# Sobre as versões do FastAPI +# Sobre as versões do FastAPI { #about-fastapi-versions } -**FastAPI** já está sendo usado em produção em diversas aplicações e sistemas, a cobertura de testes é mantida em 100%, mas seu desenvolvimento está avançando rapidamente. +**FastAPI** já está sendo usado em produção em muitas aplicações e sistemas. E a cobertura de testes é mantida em 100%. Mas seu desenvolvimento ainda está avançando rapidamente. -Novos recursos são adicionados com frequência, bugs são corrigidos regularmente e o código está sempre melhorando. +Novas funcionalidades são adicionadas com frequência, bugs são corrigidos regularmente e o código continua melhorando continuamente. -Esse é o motivo das versões atuais estarem em `0.x.x`, significando que em cada versão pode haver mudanças significativas, tudo isso seguindo as convenções de controle de versão semântica. +É por isso que as versões atuais ainda são `0.x.x`, isso reflete que cada versão pode potencialmente ter mudanças significativas. Isso segue as convenções de Versionamento Semântico. -Já é possível criar aplicativos de produção com **FastAPI** (e provavelmente você já faz isso há algum tempo), apenas precisando ter certeza de usar uma versão que funcione corretamente com o resto do seu código. +Você pode criar aplicações de produção com **FastAPI** agora mesmo (e provavelmente já vem fazendo isso há algum tempo), apenas certifique-se de usar uma versão que funcione corretamente com o resto do seu código. -## Fixe a sua versão de `fastapi` +## Fixe a sua versão de `fastapi` { #pin-your-fastapi-version } -A primeira coisa que você deve fazer é "fixar" a versão do **FastAPI** que você está utilizando na mais atual, na qual você sabe que funciona corretamente para o seu aplicativo. +A primeira coisa que você deve fazer é "fixar" a versão do **FastAPI** que você está utilizando na versão mais recente específica que você sabe que funciona corretamente para a sua aplicação. -Por exemplo, supondo que você está usando a versão `0.45.0` em sua aplicação. +Por exemplo, suponha que você esteja usando a versão `0.112.0` em sua aplicação. -Caso você utilize o arquivo `requirements.txt`, você poderia especificar a versão com: +Se você usa um arquivo `requirements.txt`, você poderia especificar a versão com: ```txt -fastapi==0.45.0 +fastapi[standard]==0.112.0 ``` -Isso significa que você conseguiria utilizar a versão exata `0.45.0`. +isso significaria que você usaria exatamente a versão `0.112.0`. -Ou, você poderia fixá-la com: +Ou você também poderia fixá-la com: ```txt -fastapi>=0.45.0,<0.46.0 +fastapi[standard]>=0.112.0,<0.113.0 ``` -isso significa que você iria usar as versões `0.45.0` ou acima, mas inferiores à `0.46.0`, por exemplo, a versão `0.45.2` ainda seria aceita. +isso significaria que você usaria as versões `0.112.0` ou superiores, mas menores que `0.113.0`, por exemplo, a versão `0.112.2` ainda seria aceita. -Se você usar qualquer outra ferramenta para gerenciar suas instalações, como Poetry, Pipenv ou outras, todas elas têm uma maneira que você pode usar para definir as versões específicas dos seus pacotes. +Se você usa qualquer outra ferramenta para gerenciar suas instalações, como `uv`, Poetry, Pipenv ou outras, todas elas têm uma forma de definir versões específicas para seus pacotes. -## Versões disponíveis +## Versões disponíveis { #available-versions } -Você pode ver as versões disponíveis (por exemplo, para verificar qual é a versão atual) em [Release Notes](../release-notes.md){.internal-link target=\_blank}. +Você pode ver as versões disponíveis (por exemplo, para verificar qual é a mais recente) nas [Release Notes](../release-notes.md){.internal-link target=_blank}. -## Sobre versões +## Sobre versões { #about-versions } -Seguindo as convenções de controle de versão semântica, qualquer versão abaixo de `1.0.0` pode adicionar mudanças significativas. +Seguindo as convenções de Versionamento Semântico, qualquer versão abaixo de `1.0.0` pode potencialmente adicionar mudanças significativas. -FastAPI também segue a convenção de que qualquer alteração de versão "PATCH" é para correção de bugs e alterações não significativas. +FastAPI também segue a convenção de que qualquer alteração de versão "PATCH" é para correções de bugs e mudanças que não quebram compatibilidade. /// tip | Dica @@ -54,40 +54,40 @@ Logo, você deveria conseguir fixar a versão, como: fastapi>=0.45.0,<0.46.0 ``` -Mudanças significativas e novos recursos são adicionados em versões "MINOR". +Mudanças significativas e novas funcionalidades são adicionadas em versões "MINOR". /// tip | Dica -O "MINOR" é o número que está no meio, por exemplo, em `0.2.3`, a versão MINOR é `2`. +O "MINOR" é o número do meio, por exemplo, em `0.2.3`, a versão MINOR é `2`. /// -## Atualizando as versões do FastAPI +## Atualizando as versões do FastAPI { #upgrading-the-fastapi-versions } Você deve adicionar testes para a sua aplicação. -Com **FastAPI** isso é muito fácil (graças a Starlette), verifique a documentação: [Testing](../tutorial/testing.md){.internal-link target=\_blank} +Com **FastAPI** isso é muito fácil (graças ao Starlette), veja a documentação: [Testing](../tutorial/testing.md){.internal-link target=_blank} -Após a criação dos testes, você pode atualizar a sua versão do **FastAPI** para uma mais recente, execute os testes para se certificar de que todo o seu código está funcionando corretamente. +Depois que você tiver testes, você pode atualizar a sua versão do **FastAPI** para uma mais recente e se certificar de que todo o seu código está funcionando corretamente executando seus testes. -Se tudo estiver funcionando, ou após você realizar as alterações necessárias e todos os testes estiverem passando, então você pode fixar sua versão de `FastAPI` para essa mais nova. +Se tudo estiver funcionando, ou após você realizar as alterações necessárias e todos os testes estiverem passando, então você pode fixar sua versão de `fastapi` para essa versão mais recente. -## Sobre Starlette +## Sobre Starlette { #about-starlette } Não é recomendado fixar a versão de `starlette`. Versões diferentes de **FastAPI** utilizarão uma versão específica e mais recente de Starlette. -Então, você pode deixar **FastAPI** escolher a versão compatível e correta de Starlette. +Então, você pode deixar **FastAPI** usar a versão correta do Starlette. -## Sobre Pydantic +## Sobre Pydantic { #about-pydantic } -Pydantic incluí os testes para **FastAPI** em seus próprios testes, então as novas versões de Pydantic (acima da `1.0.0`) sempre serão compatíveis com FastAPI. +Pydantic inclui os testes para **FastAPI** em seus próprios testes, então novas versões do Pydantic (acima de `1.0.0`) são sempre compatíveis com FastAPI. -Você pode fixar qualquer versão de Pydantic que desejar, desde que seja acima da `1.0.0` e abaixo da `2.0.0`. +Você pode fixar o Pydantic em qualquer versão acima de `1.0.0` que funcione para você. Por exemplo: ```txt -pydantic>=1.2.0,<2.0.0 +pydantic>=2.7.0,<3.0.0 ``` diff --git a/docs/pt/docs/environment-variables.md b/docs/pt/docs/environment-variables.md index 432f78af04..342361b913 100644 --- a/docs/pt/docs/environment-variables.md +++ b/docs/pt/docs/environment-variables.md @@ -1,4 +1,4 @@ -# Variáveis de Ambiente +# Variáveis de Ambiente { #environment-variables } /// tip | Dica @@ -10,7 +10,7 @@ Uma variável de ambiente (também conhecida como "**env var**") é uma variáve Variáveis de ambiente podem ser úteis para lidar com **configurações** do aplicativo, como parte da **instalação** do Python, etc. -## Criar e Usar Variáveis de Ambiente +## Criar e Usar Variáveis de Ambiente { #create-and-use-env-vars } Você pode **criar** e usar variáveis de ambiente no **shell (terminal)**, sem precisar do Python: @@ -50,7 +50,7 @@ Hello Wade Wilson //// -## Ler Variáveis de Ambiente no Python +## Ler Variáveis de Ambiente no Python { #read-env-vars-in-python } Você também pode criar variáveis de ambiente **fora** do Python, no terminal (ou com qualquer outro método) e depois **lê-las no Python**. @@ -157,7 +157,7 @@ Você pode ler mais sobre isso em ```console -$ fastapi dev main.py -INFO Using path main.py -INFO Resolved absolute path /home/user/code/awesomeapp/main.py -INFO Searching for package file structure from directories with __init__.py files -INFO Importing from /home/user/code/awesomeapp +$ fastapi dev main.py - ╭─ Python module file ─╮ - │ │ - │ 🐍 main.py │ - │ │ - ╰──────────────────────╯ + FastAPI Starting development server 🚀 -INFO Importing module main -INFO Found importable FastAPI app + Searching for package file structure from directories with + __init__.py files + Importing from /home/user/code/awesomeapp - ╭─ Importable FastAPI app ─╮ - │ │ - │ from main import app │ - │ │ - ╰──────────────────────────╯ + module 🐍 main.py -INFO Using import string main:app + code Importing the FastAPI app object from the module with the + following code: - ╭────────── FastAPI CLI - Development mode ───────────╮ - │ │ - │ Serving at: http://127.0.0.1:8000 │ - │ │ - │ API docs: http://127.0.0.1:8000/docs │ - │ │ - │ Running in development mode, for production use: │ - │ │ - fastapi run - │ │ - ╰─────────────────────────────────────────────────────╯ + from main import app -INFO: Will watch for changes in these directories: ['/home/user/code/awesomeapp'] -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -INFO: Started reloader process [2265862] using WatchFiles -INFO: Started server process [2265873] -INFO: Waiting for application startup. -INFO: Application startup complete. + app Using import string: main:app + + server Server started at http://127.0.0.1:8000 + server Documentation at http://127.0.0.1:8000/docs + + tip Running in development mode, for production use: + fastapi run + + Logs: + + INFO Will watch for changes in these directories: + ['/home/user/code/awesomeapp'] + INFO Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to + quit) + INFO Started reloader process [383138] using WatchFiles + INFO Started server process [383153] + INFO Waiting for application startup. + INFO Application startup complete. ```
-Aquele commando por linha de programa chamado `fastapi` é o **FastAPI CLI**. +O programa de linha de comando chamado `fastapi` é o **FastAPI CLI**. -O FastAPI CLI recebe o caminho do seu programa Python, detecta automaticamente a variável com o FastAPI (comumente nomeada `app`) e como importá-la, e então a serve. +O FastAPI CLI recebe o caminho para o seu programa Python (por exemplo, `main.py`), detecta automaticamente a instância de `FastAPI` (comumente nomeada `app`), determina a forma correta de importação e então a serve. -Para produção você usaria `fastapi run` no lugar. 🚀 +Para produção, você usaria `fastapi run`. 🚀 -Internamente, **FastAPI CLI** usa Uvicorn, um servidor ASGI de alta performance e pronto para produção. 😎 +Internamente, o **FastAPI CLI** usa o Uvicorn, um servidor ASGI de alta performance e pronto para produção. 😎 -## `fastapi dev` +## `fastapi dev` { #fastapi-dev } -Quando você roda `fastapi dev`, isso vai executar em modo de desenvolvimento. +Executar `fastapi dev` inicia o modo de desenvolvimento. -Por padrão, teremos o **recarregamento automático** ativo, então o programa irá recarregar o servidor automaticamente toda vez que você fizer mudanças no seu código. Isso usa muitos recursos e pode ser menos estável. Você deve apenas usá-lo em modo de desenvolvimento. +Por padrão, o recarregamento automático está ativado, recarregando o servidor automaticamente quando você faz mudanças no seu código. Isso consome muitos recursos e pode ser menos estável do que quando está desativado. Você deve usá-lo apenas no desenvolvimento. Ele também escuta no endereço IP `127.0.0.1`, que é o IP para a sua máquina se comunicar apenas consigo mesma (`localhost`). -O servidor de desenvolvimento escutará no endereço de IP `127.0.0.1` por padrão, este é o IP que sua máquina usa para se comunicar com ela mesma (`localhost`). +## `fastapi run` { #fastapi-run } -## `fastapi run` +Executar `fastapi run` inicia o FastAPI em modo de produção por padrão. -Quando você rodar `fastapi run`, isso executará em modo de produção por padrão. +Por padrão, o recarregamento automático está desativado. Ele também escuta no endereço IP `0.0.0.0`, o que significa todos os endereços IP disponíveis; dessa forma, ficará acessível publicamente para qualquer pessoa que consiga se comunicar com a máquina. É assim que você normalmente o executaria em produção, por exemplo, em um contêiner. -Este modo terá **recarregamento automático desativado** por padrão. +Na maioria dos casos, você teria (e deveria ter) um "proxy de terminação" tratando o HTTPS por cima; isso dependerá de como você faz o deploy da sua aplicação, seu provedor pode fazer isso por você ou talvez seja necessário que você configure isso por conta própria. -Isso irá escutar no endereço de IP `0.0.0.0`, o que significa todos os endereços IP disponíveis, dessa forma o programa estará acessível publicamente para qualquer um que consiga se comunicar com a máquina. Isso é como você normalmente roda em produção em um contêiner, por exemplo. +/// tip | Dica -Em muitos casos você pode ter (e deveria ter) um "proxy de saída" tratando HTTPS no topo, isso dependerá de como você fará o deploy da sua aplicação, seu provedor pode fazer isso pra você ou talvez seja necessário fazer você mesmo. - -/// tip - -Você pode aprender mais sobre em [documentação de deployment](deployment/index.md){.internal-link target=_blank}. +Você pode aprender mais sobre isso na [documentação de deployment](deployment/index.md){.internal-link target=_blank}. /// diff --git a/docs/pt/docs/features.md b/docs/pt/docs/features.md index ccc3300d64..2753077751 100644 --- a/docs/pt/docs/features.md +++ b/docs/pt/docs/features.md @@ -1,19 +1,19 @@ -# Recursos +# Recursos { #features } -## Recursos do FastAPI +## Recursos do FastAPI { #fastapi-features } **FastAPI** te oferece o seguinte: -### Baseado em padrões abertos +### Baseado em padrões abertos { #based-on-open-standards } -* OpenAPI para criação de APIs, incluindo declarações de operações de caminho, parâmetros, requisições de corpo, segurança etc. +* OpenAPI para criação de APIs, incluindo declarações de caminho operações, parâmetros, requisições de corpo, segurança etc. * Modelo de documentação automática com JSON Schema (já que o OpenAPI em si é baseado no JSON Schema). * Projetado em cima desses padrões após um estudo meticuloso, em vez de uma reflexão breve. * Isso também permite o uso de **geração de código do cliente** automaticamente em muitas linguagens. -### Documentação automática +### Documentação automática { #automatic-docs } -Documentação interativa da API e navegação _web_ da interface de usuário. Como o _framework_ é baseado no OpenAPI, há várias opções, 2 incluídas por padrão. +Documentação interativa da API e navegação web da interface de usuário. Como o framework é baseado no OpenAPI, há várias opções, 2 incluídas por padrão. * Swagger UI, com navegação interativa, chame e teste sua API diretamente do navegador. @@ -23,9 +23,9 @@ Documentação interativa da API e navegação _web_ da interface de usuário. C ![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) -### Apenas Python moderno +### Apenas Python moderno { #just-modern-python } -Tudo é baseado no padrão das declarações de **tipos do Python 3.8** (graças ao Pydantic). Nenhuma sintaxe nova para aprender. Apenas o padrão moderno do Python. +Tudo é baseado no padrão das declarações de **tipos do Python** (graças ao Pydantic). Nenhuma sintaxe nova para aprender. Apenas o padrão moderno do Python. Se você precisa refrescar a memória rapidamente sobre como usar tipos do Python (mesmo que você não use o FastAPI), confira esse rápido tutorial: [Tipos do Python](python-types.md){.internal-link target=_blank}. @@ -63,7 +63,7 @@ second_user_data = { my_second_user: User = User(**second_user_data) ``` -/// info +/// info | Informação `**second_user_data` quer dizer: @@ -71,13 +71,13 @@ Passe as chaves e valores do dicionário `second_user_data` diretamente como arg /// -### Suporte de editores +### Suporte de editores { #editor-support } -Todo o _framework_ foi projetado para ser fácil e intuitivo de usar, todas as decisões foram testadas em vários editores antes do início do desenvolvimento, para garantir a melhor experiência de desenvolvimento. +Todo o framework foi projetado para ser fácil e intuitivo de usar, todas as decisões foram testadas em vários editores antes do início do desenvolvimento, para garantir a melhor experiência de desenvolvimento. -Na última pesquisa do desenvolvedor Python ficou claro que o recurso mais utilizado é o "auto completar". +Na pesquisa de desenvolvedores Python, ficou claro que um dos recursos mais utilizados é o "preenchimento automático". -Todo o _framework_ **FastAPI** é feito para satisfazer isso. Auto completação funciona em todos os lugares. +Todo o framework **FastAPI** é feito para satisfazer isso. O preenchimento automático funciona em todos os lugares. Você raramente precisará voltar à documentação. @@ -91,17 +91,17 @@ Aqui está como o editor poderá te ajudar: ![editor support](https://fastapi.tiangolo.com/img/pycharm-completion.png) -Você terá completação do seu código que você poderia considerar impossível antes. Como por exemplo, a chave `price` dentro do corpo JSON (que poderia ter sido aninhado) que vem de uma requisição. +Você terá preenchimento automático no seu código que você poderia considerar impossível antes. Como por exemplo, a chave `price` dentro do corpo JSON (que poderia ter sido aninhado) que vem de uma requisição. -Sem a necessidade de digitar nomes de chaves erroneamente, ir e voltar entre documentações, ou rolar pela página para descobrir se você utilizou `username` or `user_name`. +Sem a necessidade de digitar nomes de chaves erroneamente, ir e voltar entre documentações, ou rolar pela página para descobrir se você utilizou `username` ou `user_name`. -### Breve +### Breve { #short } Há **padrões** sensíveis para tudo, com configurações adicionais em todos os lugares. Todos os parâmetros podem ser regulados para fazer o que você precisa e para definir a API que você necessita. Por padrão, tudo **"simplesmente funciona"**. -### Validação +### Validação { #validation } * Validação para a maioria dos (ou todos?) **tipos de dados** do Python, incluindo: * objetos JSON (`dict`). @@ -117,7 +117,7 @@ Por padrão, tudo **"simplesmente funciona"**. Toda a validação é controlada pelo robusto e bem estabelecido **Pydantic**. -### Segurança e autenticação +### Segurança e autenticação { #security-and-authentication } Segurança e autenticação integradas. Sem nenhum compromisso com bancos de dados ou modelos de dados. @@ -130,34 +130,34 @@ Todos os esquemas de seguranças definidos no OpenAPI, incluindo: * parâmetros da Query. * Cookies etc. -Além disso, todos os recursos de seguranças do Starlette (incluindo **cookies de sessão**). +Além disso, todos os recursos de segurança do Starlette (incluindo **cookies de sessão**). Tudo construído como ferramentas e componentes reutilizáveis que são fáceis de integrar com seus sistemas, armazenamento de dados, banco de dados relacionais e não-relacionais etc. -### Injeção de dependência +### Injeção de dependência { #dependency-injection } FastAPI inclui um sistema de injeção de dependência extremamente fácil de usar, mas extremamente poderoso. * Mesmo dependências podem ter dependências, criando uma hierarquia ou **"grafo" de dependências**. -* Tudo **automaticamente controlado** pelo _framework_. +* Tudo **automaticamente controlado** pelo framework. * Todas as dependências podem pedir dados das requisições e **ampliar** as restrições e documentação automática da **operação de caminho**. * **Validação automática** mesmo para parâmetros da *operação de caminho* definidos em dependências. * Suporte para sistemas de autenticação complexos, **conexões com banco de dados** etc. -* **Sem comprometer** os bancos de dados, _frontends_ etc. Mas fácil integração com todos eles. +* **Sem comprometer** os bancos de dados, frontends etc. Mas fácil integração com todos eles. -### "Plug-ins" ilimitados +### "Plug-ins" ilimitados { #unlimited-plug-ins } Ou, de outra forma, sem a necessidade deles, importe e use o código que precisar. Qualquer integração é projetada para ser tão simples de usar (com dependências) que você pode criar um "plug-in" para suas aplicações com 2 linhas de código usando a mesma estrutura e sintaxe para as suas *operações de caminho*. -### Testado +### Testado { #tested } * 100% de cobertura de testes. -* 100% do código utiliza type annotations. +* 100% do código com anotações de tipo. * Usado para aplicações em produção. -## Recursos do Starlette +## Recursos do Starlette { #starlette-features } **FastAPI** é totalmente compatível com (e baseado no) Starlette. Então, qualquer código adicional Starlette que você tiver, também funcionará. @@ -165,18 +165,17 @@ Qualquer integração é projetada para ser tão simples de usar (com dependênc Com **FastAPI**, você terá todos os recursos do **Starlette** (já que FastAPI é apenas um Starlette com esteróides): -* Desempenho realmente impressionante. É um dos _frameworks_ Python disponíveis mais rápidos, a par com o **NodeJS** e **Go**. +* Desempenho realmente impressionante. É um dos frameworks Python disponíveis mais rápidos, a par com o **NodeJS** e **Go**. * Suporte a **WebSocket**. -* Suporte a **GraphQL**. -* Tarefas em processo _background_. +* Tarefas em processo background. * Eventos na inicialização e encerramento. * Cliente de testes construído sobre HTTPX. * Respostas em **CORS**, GZip, Static Files, Streaming. * Suporte a **Session e Cookie**. * 100% de cobertura de testes. -* 100% do código utilizando _type annotations_. +* 100% do código utilizando anotações de tipo. -## Recursos do Pydantic +## Recursos do Pydantic { #pydantic-features } **FastAPI** é totalmente compatível com (e baseado no) Pydantic. Então, qualquer código Pydantic adicional que você tiver, também funcionará. @@ -192,7 +191,7 @@ Com **FastAPI** você terá todos os recursos do **Pydantic** (já que FastAPI u * Sem novas definições de esquema de micro-linguagem para aprender. * Se você conhece os tipos do Python, você sabe como usar o Pydantic. * Vai bem com o/a seu/sua **IDE/linter/cérebro**: - * Como as estruturas de dados do Pydantic são apenas instâncias de classes que você define, a auto completação, _linting_, _mypy_ e a sua intuição devem funcionar corretamente com seus dados validados. + * Como as estruturas de dados do Pydantic são apenas instâncias de classes que você define, o preenchimento automático, linting, mypy e a sua intuição devem funcionar corretamente com seus dados validados. * Valida **estruturas complexas**: * Use modelos hierárquicos do Pydantic, `List` e `Dict` do `typing` do Python, etc. * Validadores permitem que esquemas de dados complexos sejam limpos e facilmente definidos, conferidos e documentados como JSON Schema. diff --git a/docs/pt/docs/help-fastapi.md b/docs/pt/docs/help-fastapi.md index 0de1ed6484..4f58c091f7 100644 --- a/docs/pt/docs/help-fastapi.md +++ b/docs/pt/docs/help-fastapi.md @@ -1,149 +1,256 @@ -# Ajuda FastAPI - Obter Ajuda +# Ajude o FastAPI - Obtenha ajuda { #help-fastapi-get-help } Você gosta do **FastAPI**? -Você gostaria de ajudar o FastAPI, outros usários, e o autor? +Você gostaria de ajudar o FastAPI, outras pessoas usuárias e o autor? -Ou você gostaria de obter ajuda relacionada ao **FastAPI**?? +Ou você gostaria de obter ajuda com o **FastAPI**? -Existem métodos muito simples de ajudar (A maioria das ajudas podem ser feitas com um ou dois cliques). +Há maneiras muito simples de ajudar (várias envolvem apenas um ou dois cliques). -E também existem vários modos de se conseguir ajuda. +E também há várias formas de obter ajuda. -## Inscreva-se na newsletter +## Assine a newsletter { #subscribe-to-the-newsletter } -Você pode se inscrever (pouco frequente) [**FastAPI e amigos** newsletter](newsletter.md){.internal-link target=_blank} para receber atualizações: +Você pode assinar a (infrequente) [newsletter do **FastAPI and friends**](newsletter.md){.internal-link target=_blank} para ficar por dentro de: * Notícias sobre FastAPI e amigos 🚀 * Tutoriais 📝 -* Recursos ✨ -* Mudanças de última hora 🚨 -* Truques e dicas ✅ +* Funcionalidades ✨ +* Mudanças incompatíveis 🚨 +* Dicas e truques ✅ -## Siga o FastAPI no X (Twitter) +## Siga o FastAPI no X (Twitter) { #follow-fastapi-on-x-twitter } Siga @fastapi no **X (Twitter)** para receber as últimas notícias sobre o **FastAPI**. 🐦 -## Favorite o **FastAPI** no GitHub +## Dê uma estrela ao **FastAPI** no GitHub { #star-fastapi-in-github } -Você pode "favoritar" o FastAPI no GitHub (clicando na estrela no canto superior direito): https://github.com/fastapi/fastapi. ⭐️ +Você pode “marcar com estrela” o FastAPI no GitHub (clicando no botão de estrela no canto superior direito): https://github.com/fastapi/fastapi. ⭐️ -Favoritando, outros usuários poderão encontrar mais facilmente e verão que já foi útil para muita gente. +Ao adicionar uma estrela, outras pessoas conseguirão encontrá-lo com mais facilidade e verão que já foi útil para muita gente. -## Acompanhe novos updates no repositorio do GitHub +## Acompanhe o repositório no GitHub para lançamentos { #watch-the-github-repository-for-releases } -Você pode "acompanhar" (watch) o FastAPI no GitHub (clicando no botão com um "olho" no canto superior direito): https://github.com/fastapi/fastapi. 👀 +Você pode “acompanhar” (watch) o FastAPI no GitHub (clicando no botão “watch” no canto superior direito): https://github.com/fastapi/fastapi. 👀 -Podendo selecionar apenas "Novos Updates". +Lá você pode selecionar “Apenas lançamentos” (Releases only). -Fazendo isto, serão enviadas notificações (em seu email) sempre que tiver novos updates (uma nova versão) com correções de bugs e novos recursos no **FastAPI** +Fazendo isso, você receberá notificações (no seu email) sempre que houver um novo lançamento (uma nova versão) do **FastAPI** com correções de bugs e novas funcionalidades. -## Conect-se com o autor +## Conecte-se com o autor { #connect-with-the-author } Você pode se conectar comigo (Sebastián Ramírez / `tiangolo`), o autor. Você pode: -* Me siga no **GitHub**. - * Ver também outros projetos Open Source criados por mim que podem te ajudar. - * Me seguir para saber quando um novo projeto Open Source for criado. -* Me siga no **X (Twitter)**. - * Me dizer o motivo pelo o qual você está usando o FastAPI(Adoro ouvir esse tipo de comentário). - * Saber quando eu soltar novos anúncios ou novas ferramentas. - * Também é possivel seguir o @fastapi no X (Twitter) (uma conta aparte). -* Conect-se comigo no **Linkedin**. - * Saber quando eu fizer novos anúncios ou novas ferramentas (apesar de que uso o X (Twitter) com mais frequência 🤷‍♂). -* Ler meus artigos (ou me seguir) no **Dev.to** ou no **Medium**. - * Ficar por dentro de novas ideias, artigos, e ferramentas criadas por mim. - * Me siga para saber quando eu publicar algo novo. +* Me seguir no **GitHub**. + * Ver outros projetos Open Source que criei e que podem ajudar você. + * Me seguir para saber quando eu criar um novo projeto Open Source. +* Me seguir no **X (Twitter)** ou no Mastodon. + * Me contar como você usa o FastAPI (adoro saber disso). + * Ficar sabendo quando eu fizer anúncios ou lançar novas ferramentas. + * Você também pode seguir @fastapi no X (Twitter) (uma conta separada). +* Me seguir no **LinkedIn**. + * Ver quando eu fizer anúncios ou lançar novas ferramentas (embora eu use mais o X (Twitter) 🤷‍♂). +* Ler o que escrevo (ou me seguir) no **Dev.to** ou no **Medium**. + * Ler outras ideias, artigos e conhecer ferramentas que criei. + * Me seguir para ver quando eu publicar algo novo. -## Tweete sobre **FastAPI** +## Tweet sobre o **FastAPI** { #tweet-about-fastapi } -Tweete sobre o **FastAPI** e compartilhe comigo e com os outros o porque de gostar do FastAPI. 🎉 +Tweet sobre o **FastAPI** e conte para mim e para outras pessoas por que você gosta dele. 🎉 -Adoro ouvir sobre como o **FastAPI** é usado, o que você gosta nele, em qual projeto/empresa está sendo usado, etc. +Eu adoro saber como o **FastAPI** está sendo usado, o que você tem curtido nele, em qual projeto/empresa você o utiliza, etc. -## Vote no FastAPI +## Vote no FastAPI { #vote-for-fastapi } * Vote no **FastAPI** no Slant. -* Vote no **FastAPI** no AlternativeTo. +* Vote no **FastAPI** no AlternativeTo. +* Diga que você usa o **FastAPI** no StackShare. -## Responda perguntas no GitHub +## Ajude outras pessoas com perguntas no GitHub { #help-others-with-questions-in-github } -Você pode acompanhar as perguntas existentes e tentar ajudar outros, . 🤓 +Você pode tentar ajudar outras pessoas com suas perguntas em: -Ajudando a responder as questões de varias pessoas, você pode se tornar um [Expert em FastAPI](fastapi-people.md#especialistas){.internal-link target=_blank} oficial. 🎉 +* GitHub Discussions +* GitHub Issues -## Acompanhe o repositório do GitHub +Em muitos casos você já pode saber a resposta para aquelas perguntas. 🤓 -Você pode "acompanhar" (watch) o FastAPI no GitHub (clicando no "olho" no canto superior direito): https://github.com/fastapi/fastapi. 👀 +Se você estiver ajudando muitas pessoas com suas perguntas, você se tornará um(a) [Especialista em FastAPI](fastapi-people.md#fastapi-experts){.internal-link target=_blank} oficial. 🎉 -Se você selecionar "Acompanhando" (Watching) em vez de "Apenas Lançamentos" (Releases only) você receberá notificações quando alguém tiver uma nova pergunta. +Apenas lembre-se, o ponto mais importante é: tente ser gentil. As pessoas chegam com frustrações e, em muitos casos, não perguntam da melhor forma, mas tente ao máximo ser gentil. 🤗 -Assim podendo tentar ajudar a resolver essas questões. - -## Faça perguntas - -É possível criar uma nova pergunta no repositório do GitHub, por exemplo: - -* Faça uma **pergunta** ou pergunte sobre um **problema**. -* Sugira novos **recursos**. - -**Nota**: Se você fizer uma pergunta, então eu gostaria de pedir que você também ajude os outros com suas respectivas perguntas. 😉 - -## Crie um Pull Request - -É possível [contribuir](contributing.md){.internal-link target=_blank} no código fonte fazendo Pull Requests, por exemplo: - -* Para corrigir um erro de digitação que você encontrou na documentação. -* Para compartilhar um artigo, video, ou podcast criados por você sobre o FastAPI editando este arquivo. - * Não se esqueça de adicionar o link no começo da seção correspondente. -* Para ajudar [traduzir a documentação](contributing.md#traducoes){.internal-link target=_blank} para sua lingua. - * Também é possivel revisar as traduções já existentes. -* Para propor novas seções na documentação. -* Para corrigir um bug/questão. -* Para adicionar um novo recurso. - -## Entre no chat - -Entre no 👥 server de conversa do Discord 👥 e conheça novas pessoas da comunidade -do FastAPI. - -/// tip | Dica - -Para perguntas, pergunte nas questões do GitHub, lá tem um chance maior de você ser ajudado sobre o FastAPI [FastAPI Experts](fastapi-people.md#especialistas){.internal-link target=_blank}. - -Use o chat apenas para outro tipo de assunto. - -/// - -### Não faça perguntas no chat - -Tenha em mente que os chats permitem uma "conversa mais livre", dessa forma é muito fácil fazer perguntas que são muito genéricas e dificeís de responder, assim você pode acabar não sendo respondido. - -Nas questões do GitHub o template irá te guiar para que você faça a sua pergunta de um jeito mais correto, fazendo com que você receba respostas mais completas, e até mesmo que você mesmo resolva o problema antes de perguntar. E no GitHub eu garanto que sempre irei responder todas as perguntas, mesmo que leve um tempo. Eu pessoalmente não consigo fazer isso via chat. 😅 - -Conversas no chat não são tão fáceis de serem encontrados quanto no GitHub, então questões e respostas podem se perder dentro da conversa. E apenas as que estão nas questões do GitHub contam para você se tornar um [Expert em FastAPI](fastapi-people.md#especialistas){.internal-link target=_blank}, então você receberá mais atenção nas questões do GitHub. - -Por outro lado, existem milhares de usuários no chat, então tem uma grande chance de você encontrar alguém para trocar uma idéia por lá em qualquer horário. 😄 - -## Patrocine o autor - -Você também pode ajudar o autor financeiramente (eu) através do GitHub sponsors. - -Lá você pode me pagar um cafézinho ☕️ como agradecimento. 😄 - -E você também pode se tornar um patrocinador Prata ou Ouro do FastAPI. 🏅🎉 - -## Patrocine as ferramente que potencializam o FastAPI - -Como você viu na documentação, o FastAPI se apoia em nos gigantes, Starlette e Pydantic. - -Patrocine também: - -* Samuel Colvin (Pydantic) -* Encode (Starlette, Uvicorn) +A ideia é que a comunidade do **FastAPI** seja gentil e acolhedora. Ao mesmo tempo, não aceite bullying ou comportamentos desrespeitosos com outras pessoas. Temos que cuidar uns dos outros. --- -Muito Obrigado! 🚀 +Veja como ajudar outras pessoas com perguntas (em discussions ou issues): + +### Entenda a pergunta { #understand-the-question } + +* Verifique se você consegue entender qual é o **objetivo** e o caso de uso de quem está perguntando. + +* Depois verifique se a pergunta (a grande maioria são perguntas) está **clara**. + +* Em muitos casos a pergunta feita é sobre uma solução imaginada pela pessoa usuária, mas pode haver uma solução **melhor**. Se você entender melhor o problema e o caso de uso, pode sugerir uma **solução alternativa** melhor. + +* Se você não entender a pergunta, peça mais **detalhes**. + +### Reproduza o problema { #reproduce-the-problem } + +Na maioria dos casos e na maioria das perguntas há algo relacionado ao **código original** da pessoa. + +Em muitos casos ela só copia um fragmento do código, mas isso não é suficiente para **reproduzir o problema**. + +* Você pode pedir que forneçam um exemplo mínimo, reproduzível, que você possa **copiar e colar** e executar localmente para ver o mesmo erro ou comportamento que elas estão vendo, ou para entender melhor o caso de uso. + +* Se você estiver muito generoso(a), pode tentar **criar um exemplo** assim você mesmo(a), apenas com base na descrição do problema. Só tenha em mente que isso pode levar bastante tempo e pode ser melhor pedir primeiro que esclareçam o problema. + +### Sugira soluções { #suggest-solutions } + +* Depois de conseguir entender a pergunta, você pode dar uma possível **resposta**. + +* Em muitos casos, é melhor entender o **problema subjacente ou caso de uso**, pois pode haver uma forma melhor de resolver do que aquilo que estão tentando fazer. + +### Peça para encerrar { #ask-to-close } + +Se a pessoa responder, há uma grande chance de você ter resolvido o problema, parabéns, **você é um(a) herói(na)**! 🦸 + +* Agora, se isso resolveu o problema, você pode pedir para: + + * No GitHub Discussions: marcar o comentário como a **resposta**. + * No GitHub Issues: **encerrar** a issue. + +## Acompanhe o repositório do GitHub { #watch-the-github-repository } + +Você pode “acompanhar” (watch) o FastAPI no GitHub (clicando no botão “watch” no canto superior direito): https://github.com/fastapi/fastapi. 👀 + +Se você selecionar “Acompanhando” (Watching) em vez de “Apenas lançamentos” (Releases only), receberá notificações quando alguém criar uma nova issue ou pergunta. Você também pode especificar que quer ser notificado(a) apenas sobre novas issues, ou discussions, ou PRs, etc. + +Assim você pode tentar ajudar a resolver essas questões. + +## Faça perguntas { #ask-questions } + +Você pode criar uma nova pergunta no repositório do GitHub, por exemplo para: + +* Fazer uma **pergunta** ou perguntar sobre um **problema**. +* Sugerir uma nova **funcionalidade**. + +**Nota**: se você fizer isso, então vou pedir que você também ajude outras pessoas. 😉 + +## Revise Pull Requests { #review-pull-requests } + +Você pode me ajudar revisando pull requests de outras pessoas. + +Novamente, por favor tente ao máximo ser gentil. 🤗 + +--- + +Veja o que ter em mente e como revisar um pull request: + +### Entenda o problema { #understand-the-problem } + +* Primeiro, garanta que você **entendeu o problema** que o pull request tenta resolver. Pode haver uma discussão mais longa em uma Discussion ou issue do GitHub. + +* Também há uma boa chance de o pull request não ser realmente necessário porque o problema pode ser resolvido de uma **forma diferente**. Aí você pode sugerir ou perguntar sobre isso. + +### Não se preocupe com estilo { #dont-worry-about-style } + +* Não se preocupe muito com coisas como estilos de mensagens de commit, eu vou fazer squash e merge personalizando o commit manualmente. + +* Também não se preocupe com regras de estilo, já há ferramentas automatizadas verificando isso. + +E se houver qualquer outra necessidade de estilo ou consistência, vou pedir diretamente, ou vou adicionar commits por cima com as mudanças necessárias. + +### Verifique o código { #check-the-code } + +* Verifique e leia o código, veja se faz sentido, **execute localmente** e veja se realmente resolve o problema. + +* Depois **comente** dizendo que você fez isso, é assim que saberei que você realmente verificou. + +/// info | Informação + +Infelizmente, eu não posso simplesmente confiar em PRs que têm várias aprovações. + +Já aconteceu várias vezes de haver PRs com 3, 5 ou mais aprovações, provavelmente porque a descrição é atraente, mas quando eu verifico os PRs, eles estão quebrados, têm um bug, ou não resolvem o problema que afirmam resolver. 😅 + +Por isso, é realmente importante que você leia e execute o código, e me avise nos comentários que você fez isso. 🤓 + +/// + +* Se o PR puder ser simplificado de alguma forma, você pode pedir isso, mas não há necessidade de ser exigente demais, pode haver muitos pontos de vista subjetivos (e eu terei o meu também 🙈), então é melhor focar nas coisas fundamentais. + +### Testes { #tests } + +* Me ajude a verificar se o PR tem **testes**. + +* Verifique se os testes **falham** antes do PR. 🚨 + +* Depois verifique se os testes **passam** após o PR. ✅ + +* Muitos PRs não têm testes, você pode **lembrar** a pessoa de adicionar testes, ou até **sugerir** alguns testes você mesmo(a). Essa é uma das coisas que consomem mais tempo e você pode ajudar muito com isso. + +* Depois também comente o que você testou, assim vou saber que você verificou. 🤓 + +## Crie um Pull Request { #create-a-pull-request } + +Você pode [contribuir](contributing.md){.internal-link target=_blank} com o código-fonte fazendo Pull Requests, por exemplo: + +* Para corrigir um erro de digitação que você encontrou na documentação. +* Para compartilhar um artigo, vídeo ou podcast que você criou ou encontrou sobre o FastAPI, editando este arquivo. + * Garanta que você adicione seu link no início da seção correspondente. +* Para ajudar a [traduzir a documentação](contributing.md#translations){.internal-link target=_blank} para seu idioma. + * Você também pode ajudar a revisar as traduções criadas por outras pessoas. +* Para propor novas seções de documentação. +* Para corrigir uma issue/bug existente. + * Garanta que você adicione testes. +* Para adicionar uma nova funcionalidade. + * Garanta que você adicione testes. + * Garanta que você adicione documentação se for relevante. + +## Ajude a manter o FastAPI { #help-maintain-fastapi } + +Ajude-me a manter o **FastAPI**! 🤓 + +Há muito trabalho a fazer e, para a maior parte dele, **VOCÊ** pode ajudar. + +As principais tarefas que você pode fazer agora são: + +* [Ajudar outras pessoas com perguntas no GitHub](#help-others-with-questions-in-github){.internal-link target=_blank} (veja a seção acima). +* [Revisar Pull Requests](#review-pull-requests){.internal-link target=_blank} (veja a seção acima). + +Essas duas tarefas são as que **mais consomem tempo**. Esse é o principal trabalho de manter o FastAPI. + +Se você puder me ajudar com isso, **você está me ajudando a manter o FastAPI** e garantindo que ele continue **avançando mais rápido e melhor**. 🚀 + +## Entre no chat { #join-the-chat } + +Entre no 👥 servidor de chat do Discord 👥 e converse com outras pessoas da comunidade FastAPI. + +/// tip | Dica + +Para perguntas, faça-as no GitHub Discussions, há uma chance muito maior de você receber ajuda pelos [Especialistas em FastAPI](fastapi-people.md#fastapi-experts){.internal-link target=_blank}. + +Use o chat apenas para outras conversas gerais. + +/// + +### Não use o chat para perguntas { #dont-use-the-chat-for-questions } + +Tenha em mente que, como os chats permitem uma “conversa mais livre”, é fácil fazer perguntas muito gerais e mais difíceis de responder, então você pode acabar não recebendo respostas. + +No GitHub, o template vai orientar você a escrever a pergunta certa para que você consiga obter uma boa resposta com mais facilidade, ou até resolver o problema sozinho(a) antes de perguntar. E no GitHub eu consigo garantir que sempre vou responder tudo, mesmo que leve algum tempo. Eu pessoalmente não consigo fazer isso com os sistemas de chat. 😅 + +As conversas nos sistemas de chat também não são tão fáceis de pesquisar quanto no GitHub, então perguntas e respostas podem se perder na conversa. E somente as que estão no GitHub contam para você se tornar um(a) [Especialista em FastAPI](fastapi-people.md#fastapi-experts){.internal-link target=_blank}, então é bem provável que você receba mais atenção no GitHub. + +Por outro lado, há milhares de usuários nos sistemas de chat, então há uma grande chance de você encontrar alguém para conversar por lá quase o tempo todo. 😄 + +## Patrocine o autor { #sponsor-the-author } + +Se o seu **produto/empresa** depende de ou está relacionado ao **FastAPI** e você quer alcançar suas pessoas usuárias, você pode patrocinar o autor (eu) através do GitHub sponsors. Dependendo do nível, você pode obter benefícios extras, como um selo na documentação. 🎁 + +--- + +Obrigado! 🚀 diff --git a/docs/pt/docs/history-design-future.md b/docs/pt/docs/history-design-future.md index 1d0768c629..6995287391 100644 --- a/docs/pt/docs/history-design-future.md +++ b/docs/pt/docs/history-design-future.md @@ -1,4 +1,4 @@ -# História, Design e Futuro +# História, Design e Futuro { #history-design-and-future } Há algum tempo, um usuário **FastAPI** perguntou: @@ -6,7 +6,7 @@ Há algum tempo, **Pydantic** por suas vantagens. -Então eu contribuí com ele, para deixá-lo completamente de acordo com o JSON Schema, para dar suporte a diferentes maneiras de definir declarações de restrições, e melhorar o suporte a editores (conferências de tipos, auto completações) baseado nos testes em vários editores. +Então eu contribuí com ele, para deixá-lo completamente de acordo com o JSON Schema, para dar suporte a diferentes maneiras de definir declarações de restrições, e melhorar o suporte a editores (conferências de tipos, preenchimento automático) baseado nos testes em vários editores. Durante o desenvolvimento, eu também contribuí com o **Starlette**, outro requisito chave. -## Desenvolvimento +## Desenvolvimento { #development } Quando comecei a criar o **FastAPI** de fato, a maior parte das peças já estavam encaixadas, o design estava definido, os requisitos e ferramentas já estavam prontos, e o conhecimento sobre os padrões e especificações estavam claros e frescos. -## Futuro +## Futuro { #future } Nesse ponto, já está claro que o **FastAPI** com suas ideias está sendo útil para muitas pessoas. diff --git a/docs/pt/docs/how-to/authentication-error-status-code.md b/docs/pt/docs/how-to/authentication-error-status-code.md new file mode 100644 index 0000000000..878a1ca1ac --- /dev/null +++ b/docs/pt/docs/how-to/authentication-error-status-code.md @@ -0,0 +1,17 @@ +# Usar antigos códigos de status de erro de autenticação 403 { #use-old-403-authentication-error-status-codes } + +Antes da versão `0.122.0` do FastAPI, quando os utilitários de segurança integrados retornavam um erro ao cliente após uma falha na autenticação, eles usavam o código de status HTTP `403 Forbidden`. + +A partir da versão `0.122.0` do FastAPI, eles usam o código de status HTTP `401 Unauthorized`, mais apropriado, e retornam um cabeçalho `WWW-Authenticate` adequado na response, seguindo as especificações HTTP, RFC 7235, RFC 9110. + +Mas, se por algum motivo seus clientes dependem do comportamento antigo, você pode voltar a ele sobrescrevendo o método `make_not_authenticated_error` nas suas classes de segurança. + +Por exemplo, você pode criar uma subclasse de `HTTPBearer` que retorne um erro `403 Forbidden` em vez do erro padrão `401 Unauthorized`: + +{* ../../docs_src/authentication_error_status_code/tutorial001_an_py39.py hl[9:13] *} + +/// tip | Dica + +Perceba que a função retorna a instância da exceção, ela não a lança. O lançamento é feito no restante do código interno. + +/// diff --git a/docs/pt/docs/how-to/conditional-openapi.md b/docs/pt/docs/how-to/conditional-openapi.md index 6b44e9c813..b475dae6d6 100644 --- a/docs/pt/docs/how-to/conditional-openapi.md +++ b/docs/pt/docs/how-to/conditional-openapi.md @@ -1,12 +1,12 @@ -# OpenAPI condicional +# OpenAPI condicional { #conditional-openapi } -Se necessário, você pode usar configurações e variáveis ​​de ambiente para configurar o OpenAPI condicionalmente, dependendo do ambiente, e até mesmo desativá-lo completamente. +Se necessário, você pode usar configurações e variáveis de ambiente para configurar o OpenAPI condicionalmente dependendo do ambiente e até mesmo desativá-lo completamente. -## Sobre segurança, APIs e documentos +## Sobre segurança, APIs e documentação { #about-security-apis-and-docs } -Ocultar suas interfaces de usuário de documentação na produção *não deveria* ser a maneira de proteger sua API. +Ocultar suas interfaces de usuário de documentação na produção não *deveria* ser a maneira de proteger sua API. -Isso não adiciona nenhuma segurança extra à sua API; as *operações de rotas* ainda estarão disponíveis onde estão. +Isso não adiciona nenhuma segurança extra à sua API; as *operações de rota* ainda estarão disponíveis onde estão. Se houver uma falha de segurança no seu código, ela ainda existirá. @@ -17,25 +17,25 @@ Se você quiser proteger sua API, há várias coisas melhores que você pode faz * Certifique-se de ter modelos Pydantic bem definidos para seus corpos de solicitação e respostas. * Configure quaisquer permissões e funções necessárias usando dependências. * Nunca armazene senhas em texto simples, apenas hashes de senha. -* Implemente e use ferramentas criptográficas bem conhecidas, como tokens JWT e Passlib, etc. +* Implemente e use ferramentas criptográficas bem conhecidas, como pwdlib e tokens JWT, etc. * Adicione controles de permissão mais granulares com escopos OAuth2 quando necessário. * ...etc. -No entanto, você pode ter um caso de uso muito específico em que realmente precisa desabilitar a documentação da API para algum ambiente (por exemplo, para produção) ou dependendo de configurações de variáveis ​​de ambiente. +No entanto, você pode ter um caso de uso muito específico em que realmente precisa desabilitar a documentação da API para algum ambiente (por exemplo, para produção) ou dependendo de configurações de variáveis de ambiente. -## OpenAPI condicional com configurações e variáveis ​​de ambiente +## OpenAPI condicional com configurações e variáveis de ambiente { #conditional-openapi-from-settings-and-env-vars } -Você pode usar facilmente as mesmas configurações do Pydantic para configurar sua OpenAPI gerada e as interfaces de usuário de documentos. +Você pode usar facilmente as mesmas configurações do Pydantic para configurar sua OpenAPI gerada e as interfaces de usuário da documentação. Por exemplo: -{* ../../docs_src/conditional_openapi/tutorial001.py hl[6,11] *} +{* ../../docs_src/conditional_openapi/tutorial001_py39.py hl[6,11] *} Aqui declaramos a configuração `openapi_url` com o mesmo padrão de `"/openapi.json"`. -E então o usamos ao criar o aplicativo `FastAPI`. +E então a usamos ao criar a aplicação `FastAPI`. -Então você pode desabilitar o OpenAPI (incluindo os documentos da interface do usuário) definindo a variável de ambiente `OPENAPI_URL` como uma string vazia, como: +Então você pode desabilitar o OpenAPI (incluindo a documentação da interface do usuário) definindo a variável de ambiente `OPENAPI_URL` como uma string vazia, como:
@@ -47,7 +47,7 @@ $ OPENAPI_URL= uvicorn main:app
-Então, se você acessar as URLs em `/openapi.json`, `/docs` ou `/redoc`, você receberá apenas um erro `404 Não Encontrado` como: +Então, se você acessar as URLs em `/openapi.json`, `/docs` ou `/redoc`, você receberá apenas um erro `404 Not Found` como: ```JSON { diff --git a/docs/pt/docs/how-to/configure-swagger-ui.md b/docs/pt/docs/how-to/configure-swagger-ui.md index 915b2b5c57..2d1863c64c 100644 --- a/docs/pt/docs/how-to/configure-swagger-ui.md +++ b/docs/pt/docs/how-to/configure-swagger-ui.md @@ -1,14 +1,14 @@ -# Configurar Swagger UI +# Configure a UI do Swagger { #configure-swagger-ui } Você pode configurar alguns parâmetros extras da UI do Swagger. -Para configurá-los, passe o argumento `swagger_ui_parameters` ao criar o objeto de aplicativo `FastAPI()` ou para a função `get_swagger_ui_html()`. +Para configurá-los, passe o argumento `swagger_ui_parameters` ao criar o objeto da aplicação `FastAPI()` ou para a função `get_swagger_ui_html()`. `swagger_ui_parameters` recebe um dicionário com as configurações passadas diretamente para o Swagger UI. O FastAPI converte as configurações para **JSON** para torná-las compatíveis com JavaScript, pois é disso que o Swagger UI precisa. -## Desabilitar realce de sintaxe +## Desabilitar destaque de sintaxe { #disable-syntax-highlighting } Por exemplo, você pode desabilitar o destaque de sintaxe na UI do Swagger. @@ -18,45 +18,45 @@ Sem alterar as configurações, o destaque de sintaxe é habilitado por padrão: Mas você pode desabilitá-lo definindo `syntaxHighlight` como `False`: -{* ../../docs_src/configure_swagger_ui/tutorial001.py hl[3] *} +{* ../../docs_src/configure_swagger_ui/tutorial001_py39.py hl[3] *} ...e então o Swagger UI não mostrará mais o destaque de sintaxe: -## Alterar o tema +## Alterar o tema { #change-the-theme } Da mesma forma que você pode definir o tema de destaque de sintaxe com a chave `"syntaxHighlight.theme"` (observe que há um ponto no meio): -{* ../../docs_src/configure_swagger_ui/tutorial002.py hl[3] *} +{* ../../docs_src/configure_swagger_ui/tutorial002_py39.py hl[3] *} Essa configuração alteraria o tema de cores de destaque de sintaxe: -## Alterar parâmetros de UI padrão do Swagger +## Alterar parâmetros de UI padrão do Swagger { #change-default-swagger-ui-parameters } O FastAPI inclui alguns parâmetros de configuração padrão apropriados para a maioria dos casos de uso. Inclui estas configurações padrão: -{* ../../fastapi/openapi/docs.py ln[7:23] *} +{* ../../fastapi/openapi/docs.py ln[9:24] hl[18:24] *} Você pode substituir qualquer um deles definindo um valor diferente no argumento `swagger_ui_parameters`. Por exemplo, para desabilitar `deepLinking` você pode passar essas configurações para `swagger_ui_parameters`: -{* ../../docs_src/configure_swagger_ui/tutorial003.py hl[3] *} +{* ../../docs_src/configure_swagger_ui/tutorial003_py39.py hl[3] *} -## Outros parâmetros da UI do Swagger +## Outros parâmetros da UI do Swagger { #other-swagger-ui-parameters } Para ver todas as outras configurações possíveis que você pode usar, leia a documentação oficial dos parâmetros da UI do Swagger. -## Configurações somente JavaScript +## Configurações somente JavaScript { #javascript-only-settings } -A interface do usuário do Swagger também permite que outras configurações sejam objetos **somente JavaScript** (por exemplo, funções JavaScript). +A UI do Swagger também permite que outras configurações sejam objetos **somente JavaScript** (por exemplo, funções JavaScript). -O FastAPI também inclui estas configurações de `predefinições` somente para JavaScript: +O FastAPI também inclui estas configurações `presets` somente para JavaScript: ```JavaScript presets: [ @@ -67,4 +67,4 @@ presets: [ Esses são objetos **JavaScript**, não strings, então você não pode passá-los diretamente do código Python. -Se você precisar usar configurações somente JavaScript como essas, você pode usar um dos métodos acima. Sobrescreva todas as *operações de rotas* do Swagger UI e escreva manualmente qualquer JavaScript que você precisar. +Se você precisar usar configurações somente JavaScript como essas, você pode usar um dos métodos acima. Substitua toda a *operação de rota* do Swagger UI e escreva manualmente qualquer JavaScript que você precisar. diff --git a/docs/pt/docs/how-to/custom-docs-ui-assets.md b/docs/pt/docs/how-to/custom-docs-ui-assets.md index b7de6c8bdc..adda9eca50 100644 --- a/docs/pt/docs/how-to/custom-docs-ui-assets.md +++ b/docs/pt/docs/how-to/custom-docs-ui-assets.md @@ -1,26 +1,26 @@ -# Recursos Estáticos Personalizados para a UI de Documentação (Hospedagem Própria) +# Recursos Estáticos Personalizados para a UI de Documentação (Hospedagem Própria) { #custom-docs-ui-static-assets-self-hosting } A documentação da API usa **Swagger UI** e **ReDoc**, e cada um deles precisa de alguns arquivos JavaScript e CSS. -Por padrão, esses arquivos são fornecidos por um CDN. +Por padrão, esses arquivos são fornecidos por um CDN. Mas é possível personalizá-los, você pode definir um CDN específico ou providenciar os arquivos você mesmo. -## CDN Personalizado para JavaScript e CSS +## CDN Personalizado para JavaScript e CSS { #custom-cdn-for-javascript-and-css } -Vamos supor que você deseja usar um CDN diferente, por exemplo, você deseja usar `https://unpkg.com/`. +Vamos supor que você deseja usar um CDN diferente, por exemplo, você deseja usar `https://unpkg.com/`. Isso pode ser útil se, por exemplo, você mora em um país que restringe algumas URLs. -### Desativar a documentação automática +### Desativar a documentação automática { #disable-the-automatic-docs } O primeiro passo é desativar a documentação automática, pois por padrão, ela usa o CDN padrão. -Para desativá-los, defina suas URLs como `None` ao criar seu aplicativo `FastAPI`: +Para desativá-los, defina suas URLs como `None` ao criar sua aplicação FastAPI: -{* ../../docs_src/custom_docs_ui/tutorial001.py hl[8] *} +{* ../../docs_src/custom_docs_ui/tutorial001_py39.py hl[8] *} -### Incluir a documentação personalizada +### Incluir a documentação personalizada { #include-the-custom-docs } Agora você pode criar as *operações de rota* para a documentação personalizada. @@ -34,7 +34,7 @@ Você pode reutilizar as funções internas do FastAPI para criar as páginas HT E de forma semelhante para o ReDoc... -{* ../../docs_src/custom_docs_ui/tutorial001.py hl[2:6,11:19,22:24,27:33] *} +{* ../../docs_src/custom_docs_ui/tutorial001_py39.py hl[2:6,11:19,22:24,27:33] *} /// tip | Dica @@ -46,23 +46,23 @@ Swagger UI lidará com isso nos bastidores para você, mas ele precisa desse aux /// -### Criar uma *operação de rota* para testar +### Criar uma *operação de rota* para testar { #create-a-path-operation-to-test-it } Agora, para poder testar se tudo funciona, crie uma *operação de rota*: -{* ../../docs_src/custom_docs_ui/tutorial001.py hl[36:38] *} +{* ../../docs_src/custom_docs_ui/tutorial001_py39.py hl[36:38] *} -### Teste +### Teste { #test-it } Agora, você deve ser capaz de ir para a documentação em http://127.0.0.1:8000/docs, e recarregar a página, ela carregará esses recursos do novo CDN. -## Hospedagem Própria de JavaScript e CSS para a documentação +## Hospedagem Própria de JavaScript e CSS para a documentação { #self-hosting-javascript-and-css-for-docs } Hospedar o JavaScript e o CSS pode ser útil se, por exemplo, você precisa que seu aplicativo continue funcionando mesmo offline, sem acesso aberto à Internet, ou em uma rede local. -Aqui você verá como providenciar esses arquivos você mesmo, no mesmo aplicativo FastAPI, e configurar a documentação para usá-los. +Aqui você verá como providenciar esses arquivos você mesmo, na mesma aplicação FastAPI, e configurar a documentação para usá-los. -### Estrutura de Arquivos do Projeto +### Estrutura de Arquivos do Projeto { #project-file-structure } Vamos supor que a estrutura de arquivos do seu projeto se pareça com isso: @@ -85,7 +85,7 @@ Sua nova estrutura de arquivos poderia se parecer com isso: └── static/ ``` -### Baixe os arquivos +### Baixe os arquivos { #download-the-files } Baixe os arquivos estáticos necessários para a documentação e coloque-os no diretório `static/`. @@ -96,7 +96,7 @@ Você provavelmente pode clicar com o botão direito em cada link e selecionar u * `swagger-ui-bundle.js` * `swagger-ui.css` -E o **ReDoc** usa os arquivos: +E o **ReDoc** usa o arquivo: * `redoc.standalone.js` @@ -113,14 +113,14 @@ Depois disso, sua estrutura de arquivos deve se parecer com: └── swagger-ui.css ``` -### Prover os arquivos estáticos +### Prover os arquivos estáticos { #serve-the-static-files } * Importe `StaticFiles`. * "Monte" a instância `StaticFiles()` em um caminho específico. -{* ../../docs_src/custom_docs_ui/tutorial002.py hl[7,11] *} +{* ../../docs_src/custom_docs_ui/tutorial002_py39.py hl[7,11] *} -### Teste os arquivos estáticos +### Teste os arquivos estáticos { #test-the-static-files } Inicialize seu aplicativo e vá para http://127.0.0.1:8000/static/redoc.standalone.js. @@ -138,15 +138,15 @@ Isso confirma que você está conseguindo fornecer arquivos estáticos do seu ap Agora, podemos configurar o aplicativo para usar esses arquivos estáticos para a documentação. -### Desativar a documentação automática para arquivos estáticos +### Desativar a documentação automática para arquivos estáticos { #disable-the-automatic-docs-for-static-files } Da mesma forma que ao usar um CDN personalizado, o primeiro passo é desativar a documentação automática, pois ela usa o CDN padrão. -Para desativá-los, defina suas URLs como `None` ao criar seu aplicativo `FastAPI`: +Para desativá-los, defina suas URLs como `None` ao criar sua aplicação FastAPI: -{* ../../docs_src/custom_docs_ui/tutorial002.py hl[9] *} +{* ../../docs_src/custom_docs_ui/tutorial002_py39.py hl[9] *} -### Incluir a documentação personalizada para arquivos estáticos +### Incluir a documentação personalizada para arquivos estáticos { #include-the-custom-docs-for-static-files } E da mesma forma que com um CDN personalizado, agora você pode criar as *operações de rota* para a documentação personalizada. @@ -155,12 +155,12 @@ Novamente, você pode reutilizar as funções internas do FastAPI para criar as * `openapi_url`: a URL onde a página HTML para a documentação pode obter o esquema OpenAPI para a sua API. Você pode usar aqui o atributo `app.openapi_url`. * `title`: o título da sua API. * `oauth2_redirect_url`: Você pode usar `app.swagger_ui_oauth2_redirect_url` aqui para usar o padrão. -* `swagger_js_url`: a URL onde a página HTML para a sua documentação do Swagger UI pode obter o arquivo **JavaScript**. Este é o URL do CDN personalizado. **Este é o URL que seu aplicativo está fornecendo**. +* `swagger_js_url`: a URL onde a página HTML para a sua documentação do Swagger UI pode obter o arquivo **JavaScript**. **Este é o URL que seu aplicativo está fornecendo**. * `swagger_css_url`: a URL onde a página HTML para a sua documentação do Swagger UI pode obter o arquivo **CSS**. **Esse é o que seu aplicativo está fornecendo**. E de forma semelhante para o ReDoc... -{* ../../docs_src/custom_docs_ui/tutorial002.py hl[2:6,14:22,25:27,30:36] *} +{* ../../docs_src/custom_docs_ui/tutorial002_py39.py hl[2:6,14:22,25:27,30:36] *} /// tip | Dica @@ -172,13 +172,13 @@ Swagger UI lidará com isso nos bastidores para você, mas ele precisa desse aux /// -### Criar uma *operação de rota* para testar arquivos estáticos +### Criar uma *operação de rota* para testar arquivos estáticos { #create-a-path-operation-to-test-static-files } Agora, para poder testar se tudo funciona, crie uma *operação de rota*: -{* ../../docs_src/custom_docs_ui/tutorial002.py hl[39:41] *} +{* ../../docs_src/custom_docs_ui/tutorial002_py39.py hl[39:41] *} -### Teste a UI de Arquivos Estáticos +### Teste a UI de Arquivos Estáticos { #test-static-files-ui } Agora, você deve ser capaz de desconectar o WiFi, ir para a documentação em http://127.0.0.1:8000/docs, e recarregar a página. diff --git a/docs/pt/docs/how-to/custom-request-and-route.md b/docs/pt/docs/how-to/custom-request-and-route.md index 151a0f5d44..b4ea1c282b 100644 --- a/docs/pt/docs/how-to/custom-request-and-route.md +++ b/docs/pt/docs/how-to/custom-request-and-route.md @@ -1,12 +1,12 @@ -# Requisições Personalizadas e Classes da APIRoute +# Request e classe APIRoute personalizadas { #custom-request-and-apiroute-class } -Em algum casos, você pode querer sobreescrever a lógica usada pelas classes `Request`e `APIRoute`. +Em alguns casos, você pode querer sobrescrever a lógica usada pelas classes `Request` e `APIRoute`. -Em particular, isso pode ser uma boa alternativa para uma lógica em um middleware +Em particular, isso pode ser uma boa alternativa para uma lógica em um middleware. Por exemplo, se você quiser ler ou manipular o corpo da requisição antes que ele seja processado pela sua aplicação. -/// danger | Perigo +/// danger | Cuidado Isso é um recurso "avançado". @@ -14,7 +14,7 @@ Se você for um iniciante em **FastAPI** você deve considerar pular essa seçã /// -## Casos de Uso +## Casos de Uso { #use-cases } Alguns casos de uso incluem: @@ -22,13 +22,13 @@ Alguns casos de uso incluem: * Descomprimir corpos de requisição comprimidos com gzip. * Registrar automaticamente todos os corpos de requisição. -## Manipulando codificações de corpo de requisição personalizadas +## Manipulando codificações de corpo de requisição personalizadas { #handling-custom-request-body-encodings } Vamos ver como usar uma subclasse personalizada de `Request` para descomprimir requisições gzip. E uma subclasse de `APIRoute` para usar essa classe de requisição personalizada. -### Criar uma classe `GzipRequest` personalizada +### Criar uma classe `GzipRequest` personalizada { #create-a-custom-gziprequest-class } /// tip | Dica @@ -42,9 +42,9 @@ Se não houver `gzip` no cabeçalho, ele não tentará descomprimir o corpo. Dessa forma, a mesma classe de rota pode lidar com requisições comprimidas ou não comprimidas. -{* ../../docs_src/custom_request_and_route/tutorial001.py hl[8:15] *} +{* ../../docs_src/custom_request_and_route/tutorial001_an_py310.py hl[9:16] *} -### Criar uma classe `GzipRoute` personalizada +### Criar uma classe `GzipRoute` personalizada { #create-a-custom-gziproute-class } Em seguida, criamos uma subclasse personalizada de `fastapi.routing.APIRoute` que fará uso do `GzipRequest`. @@ -54,11 +54,11 @@ Esse método retorna uma função. E essa função é o que irá receber uma req Aqui nós usamos para criar um `GzipRequest` a partir da requisição original. -{* ../../docs_src/custom_request_and_route/tutorial001.py hl[18:26] *} +{* ../../docs_src/custom_request_and_route/tutorial001_an_py310.py hl[19:27] *} /// note | Detalhes Técnicos -Um `Request` também tem um `request.receive`, que é uma função para "receber" o corpo da requisição. +Um `Request` tem um atributo `request.scope`, que é apenas um `dict` do Python contendo os metadados relacionados à requisição. Um `Request` também tem um `request.receive`, que é uma função para "receber" o corpo da requisição. @@ -78,7 +78,7 @@ Depois disso, toda a lógica de processamento é a mesma. Mas por causa das nossas mudanças em `GzipRequest.body`, o corpo da requisição será automaticamente descomprimido quando for carregado pelo **FastAPI** quando necessário. -## Acessando o corpo da requisição em um manipulador de exceção +## Acessando o corpo da requisição em um manipulador de exceção { #accessing-the-request-body-in-an-exception-handler } /// tip | Dica @@ -92,18 +92,18 @@ Também podemos usar essa mesma abordagem para acessar o corpo da requisição e Tudo que precisamos fazer é manipular a requisição dentro de um bloco `try`/`except`: -{* ../../docs_src/custom_request_and_route/tutorial002.py hl[13,15] *} +{* ../../docs_src/custom_request_and_route/tutorial002_an_py310.py hl[14,16] *} Se uma exceção ocorrer, a instância `Request` ainda estará em escopo, então podemos ler e fazer uso do corpo da requisição ao lidar com o erro: -{* ../../docs_src/custom_request_and_route/tutorial002.py hl[16:18] *} +{* ../../docs_src/custom_request_and_route/tutorial002_an_py310.py hl[17:19] *} -## Classe `APIRoute` personalizada em um router +## Classe `APIRoute` personalizada em um router { #custom-apiroute-class-in-a-router } -você também pode definir o parametro `route_class` de uma `APIRouter`; +Você também pode definir o parâmetro `route_class` de uma `APIRouter`: -{* ../../docs_src/custom_request_and_route/tutorial003.py hl[26] *} +{* ../../docs_src/custom_request_and_route/tutorial003_py310.py hl[26] *} Nesse exemplo, as *operações de rota* sob o `router` irão usar a classe `TimedRoute` personalizada, e terão um cabeçalho extra `X-Response-Time` na resposta com o tempo que levou para gerar a resposta: -{* ../../docs_src/custom_request_and_route/tutorial003.py hl[13:20] *} +{* ../../docs_src/custom_request_and_route/tutorial003_py310.py hl[13:20] *} diff --git a/docs/pt/docs/how-to/extending-openapi.md b/docs/pt/docs/how-to/extending-openapi.md index b4785edc15..b1b50ce654 100644 --- a/docs/pt/docs/how-to/extending-openapi.md +++ b/docs/pt/docs/how-to/extending-openapi.md @@ -1,10 +1,10 @@ -# Extendendo o OpenAPI +# Extendendo o OpenAPI { #extending-openapi } Existem alguns casos em que pode ser necessário modificar o esquema OpenAPI gerado. Nesta seção, você verá como fazer isso. -## O processo normal +## O processo normal { #the-normal-process } O processo normal (padrão) é o seguinte: @@ -33,31 +33,31 @@ O parâmetro `summary` está disponível no OpenAPI 3.1.0 e superior, suportado /// -## Sobrescrevendo os padrões +## Sobrescrevendo os padrões { #overriding-the-defaults } Com as informações acima, você pode usar a mesma função utilitária para gerar o esquema OpenAPI e sobrescrever cada parte que precisar. Por exemplo, vamos adicionar Extensão OpenAPI do ReDoc para incluir um logo personalizado. -### **FastAPI** Normal +### **FastAPI** Normal { #normal-fastapi } Primeiro, escreva toda a sua aplicação **FastAPI** normalmente: -{* ../../docs_src/extending_openapi/tutorial001.py hl[1,4,7:9] *} +{* ../../docs_src/extending_openapi/tutorial001_py39.py hl[1,4,7:9] *} -### Gerar o esquema OpenAPI +### Gerar o esquema OpenAPI { #generate-the-openapi-schema } Em seguida, use a mesma função utilitária para gerar o esquema OpenAPI, dentro de uma função `custom_openapi()`: -{* ../../docs_src/extending_openapi/tutorial001.py hl[2,15:21] *} +{* ../../docs_src/extending_openapi/tutorial001_py39.py hl[2,15:21] *} -### Modificar o esquema OpenAPI +### Modificar o esquema OpenAPI { #modify-the-openapi-schema } Agora, você pode adicionar a extensão do ReDoc, incluindo um `x-logo` personalizado ao "objeto" `info` no esquema OpenAPI: -{* ../../docs_src/extending_openapi/tutorial001.py hl[22:24] *} +{* ../../docs_src/extending_openapi/tutorial001_py39.py hl[22:24] *} -### Armazenar em cache o esquema OpenAPI +### Armazenar em cache o esquema OpenAPI { #cache-the-openapi-schema } Você pode usar a propriedade `.openapi_schema` como um "cache" para armazenar o esquema gerado. @@ -65,16 +65,16 @@ Dessa forma, sua aplicação não precisará gerar o esquema toda vez que um usu Ele será gerado apenas uma vez, e o mesmo esquema armazenado em cache será utilizado nas próximas requisições. -{* ../../docs_src/extending_openapi/tutorial001.py hl[13:14,25:26] *} +{* ../../docs_src/extending_openapi/tutorial001_py39.py hl[13:14,25:26] *} -### Sobrescrever o método +### Sobrescrever o método { #override-the-method } Agora, você pode substituir o método `.openapi()` pela sua nova função. -{* ../../docs_src/extending_openapi/tutorial001.py hl[29] *} +{* ../../docs_src/extending_openapi/tutorial001_py39.py hl[29] *} -### Verificar +### Verificar { #check-it } Uma vez que você acessar http://127.0.0.1:8000/redoc, verá que está usando seu logo personalizado (neste exemplo, o logo do **FastAPI**): - + diff --git a/docs/pt/docs/how-to/general.md b/docs/pt/docs/how-to/general.md index 4f21463b28..7f91468625 100644 --- a/docs/pt/docs/how-to/general.md +++ b/docs/pt/docs/how-to/general.md @@ -1,39 +1,38 @@ -# Geral - Como Fazer - Receitas +# Geral - Como Fazer - Receitas { #general-how-to-recipes } Aqui estão vários links para outros locais na documentação, para perguntas gerais ou frequentes -## Filtro de dados- Segurança +## Filtro de dados- Segurança { #filter-data-security } -Para assegurar que você não vai retornar mais dados do que deveria, leia a seção [Tutorial - Response Model - Return Type](../tutorial/response-model.md){.internal-link target=_blank}. +Para assegurar que você não vai retornar mais dados do que deveria, leia a seção [Tutorial - Modelo de Resposta - Tipo de Retorno](../tutorial/response-model.md){.internal-link target=_blank}. -## Tags de Documentação - OpenAPI -Para adicionar tags às suas *rotas* e agrupá-las na UI da documentação, leia a seção [Tutorial - Path Operation Configurations - Tags](../tutorial/path-operation-configuration.md#tags){.internal-link target=_blank}. +## Tags de Documentação - OpenAPI { #documentation-tags-openapi } +Para adicionar tags às suas *operações de rota* e agrupá-las na UI da documentação, leia a seção [Tutorial - Configurações da Operação de Rota - Tags](../tutorial/path-operation-configuration.md#tags){.internal-link target=_blank}. -## Resumo e Descrição da documentação - OpenAPI +## Resumo e Descrição da documentação - OpenAPI { #documentation-summary-and-description-openapi } -Para adicionar um resumo e uma descrição às suas *rotas* e exibi-los na UI da documentação, leia a seção [Tutorial - Path Operation Configurations - Summary and Description](../tutorial/path-operation-configuration.md#summary-and-description){.internal-link target=_blank}. +Para adicionar um resumo e uma descrição às suas *operações de rota* e exibi-los na UI da documentação, leia a seção [Tutorial - Configurações da Operação de Rota - Resumo e Descrição](../tutorial/path-operation-configuration.md#summary-and-description){.internal-link target=_blank}. -## Documentação das Descrições de Resposta - OpenAPI +## Documentação das Descrições de Resposta - OpenAPI { #documentation-response-description-openapi } -Para definir a descrição de uma resposta exibida na interface da documentação, leia a seção [Tutorial - Path Operation Configurations - Response description](../tutorial/path-operation-configuration.md#response-description){.internal-link target=_blank}. +Para definir a descrição de uma resposta exibida na interface da documentação, leia a seção [Tutorial - Configurações da Operação de Rota - Descrição da Resposta](../tutorial/path-operation-configuration.md#response-description){.internal-link target=_blank}. -## Documentação para Depreciar uma *Operação de Rota* - OpenAPI +## Documentação para Depreciar uma *Operação de Rota* - OpenAPI { #documentation-deprecate-a-path-operation-openapi } -Para depreciar uma *operação de rota* e exibi-la na interface da documentação, leia a seção [Tutorial - Path Operation Configurations - Deprecation](../tutorial/path-operation-configuration.md#deprecate-a-path-operation){.internal-link target=_blank}. +Para depreciar uma *operação de rota* e exibi-la na interface da documentação, leia a seção [Tutorial - Configurações da Operação de Rota - Depreciação](../tutorial/path-operation-configuration.md#deprecate-a-path-operation){.internal-link target=_blank}. -## Converter qualquer dado para JSON +## Converter qualquer dado para compatível com JSON { #convert-any-data-to-json-compatible } +Para converter qualquer dado para um formato compatível com JSON, leia a seção [Tutorial - Codificador Compatível com JSON](../tutorial/encoder.md){.internal-link target=_blank}. -Para converter qualquer dado para um formato compatível com JSON, leia a seção [Tutorial - JSON Compatible Encoder](../tutorial/encoder.md){.internal-link target=_blank}. +## OpenAPI Metadata - Docs { #openapi-metadata-docs } -## OpenAPI Metadata - Docs +Para adicionar metadados ao seu esquema OpenAPI, incluindo licensa, versão, contato, etc, leia a seção [Tutorial - Metadados e URLs da Documentação](../tutorial/metadata.md){.internal-link target=_blank}. -Para adicionar metadados ao seu esquema OpenAPI, incluindo licensa, versão, contato, etc, leia a seção [Tutorial - Metadata and Docs URLs](../tutorial/metadata.md){.internal-link target=_blank}. +## OpenAPI com URL customizada { #openapi-custom-url } -## OpenAPI com URL customizada +Para customizar a URL do OpenAPI (ou removê-la), leia a seção [Tutorial - Metadados e URLs da Documentação](../tutorial/metadata.md#openapi-url){.internal-link target=_blank}. -Para customizar a URL do OpenAPI (ou removê-la), leia a seção [Tutorial - Metadata and Docs URLs](../tutorial/metadata.md#openapi-url){.internal-link target=_blank}. +## URLs de documentação do OpenAPI { #openapi-docs-urls } -## URLs de documentação do OpenAPI - -Para alterar as URLs usadas ​​para as interfaces de usuário da documentação gerada automaticamente, leia a seção [Tutorial - Metadata and Docs URLs](../tutorial/metadata.md#docs-urls){.internal-link target=_blank}. +Para alterar as URLs usadas ​​para as interfaces de usuário da documentação gerada automaticamente, leia a seção [Tutorial - Metadados e URLs da Documentação](../tutorial/metadata.md#docs-urls){.internal-link target=_blank}. diff --git a/docs/pt/docs/how-to/graphql.md b/docs/pt/docs/how-to/graphql.md index ef0bad7f67..98266cc288 100644 --- a/docs/pt/docs/how-to/graphql.md +++ b/docs/pt/docs/how-to/graphql.md @@ -1,4 +1,4 @@ -# GraphQL +# GraphQL { #graphql } Como o **FastAPI** é baseado no padrão **ASGI**, é muito fácil integrar qualquer biblioteca **GraphQL** também compatível com ASGI. @@ -14,7 +14,7 @@ Certifique-se de avaliar se os **benefícios** para o seu caso de uso compensam /// -## Bibliotecas GraphQL +## Bibliotecas GraphQL { #graphql-libraries } Aqui estão algumas das bibliotecas **GraphQL** que têm suporte **ASGI**. Você pode usá-las com **FastAPI**: @@ -27,21 +27,21 @@ Aqui estão algumas das bibliotecas **GraphQL** que têm suporte **ASGI**. Você * Graphene * Com starlette-graphene3 -## GraphQL com Strawberry +## GraphQL com Strawberry { #graphql-with-strawberry } -Se você precisar ou quiser trabalhar com **GraphQL**, **Strawberry** é a biblioteca **recomendada** pois tem o design mais próximo ao design do **FastAPI**, ela é toda baseada em **type annotations**. +Se você precisar ou quiser trabalhar com **GraphQL**, **Strawberry** é a biblioteca **recomendada** pois tem o design mais próximo ao design do **FastAPI**, ela é toda baseada em **anotações de tipo**. Dependendo do seu caso de uso, você pode preferir usar uma biblioteca diferente, mas se você me perguntasse, eu provavelmente sugeriria que você experimentasse o **Strawberry**. Aqui está uma pequena prévia de como você poderia integrar Strawberry com FastAPI: -{* ../../docs_src/graphql/tutorial001.py hl[3,22,25:26] *} +{* ../../docs_src/graphql_/tutorial001_py39.py hl[3,22,25] *} Você pode aprender mais sobre Strawberry na documentação do Strawberry. E também na documentação sobre Strawberry com FastAPI. -## Antigo `GraphQLApp` do Starlette +## Antigo `GraphQLApp` do Starlette { #older-graphqlapp-from-starlette } Versões anteriores do Starlette incluiam uma classe `GraphQLApp` para integrar com Graphene. @@ -49,11 +49,11 @@ Ela foi descontinuada do Starlette, mas se você tem código que a utilizava, vo /// tip | Dica -Se você precisa de GraphQL, eu ainda recomendaria que você desse uma olhada no Strawberry, pois ele é baseado em type annotations em vez de classes e tipos personalizados. +Se você precisa de GraphQL, eu ainda recomendaria que você desse uma olhada no Strawberry, pois ele é baseado em anotações de tipo em vez de classes e tipos personalizados. /// -## Saiba Mais +## Saiba Mais { #learn-more } Você pode aprender mais sobre **GraphQL** na documentação oficial do GraphQL. diff --git a/docs/pt/docs/how-to/index.md b/docs/pt/docs/how-to/index.md index 6747b01c7c..a3d16380fe 100644 --- a/docs/pt/docs/how-to/index.md +++ b/docs/pt/docs/how-to/index.md @@ -1,12 +1,12 @@ -# Como Fazer - Exemplos Práticos +# Como Fazer - Receitas { #how-to-recipes } -Aqui você encontrará diferentes exemplos práticos ou tutoriais de "como fazer" para vários tópicos. +Aqui você encontrará diferentes exemplos práticos ou tutoriais de "como fazer" para **vários tópicos**. A maioria dessas ideias será mais ou menos **independente**, e na maioria dos casos você só precisará estudá-las se elas se aplicarem diretamente ao **seu projeto**. Se algo parecer interessante e útil para o seu projeto, vá em frente e dê uma olhada. Caso contrário, você pode simplesmente ignorá-lo. -/// tip +/// tip | Dica Se você deseja **aprender FastAPI** de forma estruturada (recomendado), leia capítulo por capítulo [Tutorial - Guia de Usuário](../tutorial/index.md){.internal-link target=_blank} em vez disso. diff --git a/docs/pt/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md b/docs/pt/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md new file mode 100644 index 0000000000..2a2659a03d --- /dev/null +++ b/docs/pt/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md @@ -0,0 +1,133 @@ +# Migrar do Pydantic v1 para o Pydantic v2 { #migrate-from-pydantic-v1-to-pydantic-v2 } + +Se você tem uma aplicação FastAPI antiga, pode estar usando o Pydantic versão 1. + +O FastAPI tem suporte ao Pydantic v1 ou v2 desde a versão 0.100.0. + +Se você tiver o Pydantic v2 instalado, ele será utilizado. Se, em vez disso, tiver o Pydantic v1, será ele que será utilizado. + +O Pydantic v1 está agora descontinuado e o suporte a ele será removido nas próximas versões do FastAPI, você deveria migrar para o Pydantic v2. Assim, você terá as funcionalidades, melhorias e correções mais recentes. + +/// warning | Atenção + +Além disso, a equipe do Pydantic interrompeu o suporte ao Pydantic v1 para as versões mais recentes do Python, a partir do **Python 3.14**. + +Se quiser usar as funcionalidades mais recentes do Python, você precisará garantir que usa o Pydantic v2. + +/// + +Se você tem uma aplicação FastAPI antiga com Pydantic v1, aqui vou mostrar como migrá-la para o Pydantic v2 e as **novas funcionalidades no FastAPI 0.119.0** para ajudar em uma migração gradual. + +## Guia oficial { #official-guide } + +O Pydantic tem um Guia de Migração oficial do v1 para o v2. + +Ele também inclui o que mudou, como as validações agora são mais corretas e rigorosas, possíveis ressalvas, etc. + +Você pode lê-lo para entender melhor o que mudou. + +## Testes { #tests } + +Garanta que você tenha [testes](../tutorial/testing.md){.internal-link target=_blank} para sua aplicação e que os execute na integração contínua (CI). + +Assim, você pode fazer a atualização e garantir que tudo continua funcionando como esperado. + +## `bump-pydantic` { #bump-pydantic } + +Em muitos casos, quando você usa modelos Pydantic regulares sem personalizações, será possível automatizar a maior parte do processo de migração do Pydantic v1 para o Pydantic v2. + +Você pode usar o `bump-pydantic` da própria equipe do Pydantic. + +Essa ferramenta ajuda a alterar automaticamente a maior parte do código que precisa ser modificado. + +Depois disso, você pode rodar os testes e verificar se tudo funciona. Se funcionar, está concluído. 😎 + +## Pydantic v1 no v2 { #pydantic-v1-in-v2 } + +O Pydantic v2 inclui tudo do Pydantic v1 como um submódulo `pydantic.v1`. + +Isso significa que você pode instalar a versão mais recente do Pydantic v2 e importar e usar os componentes antigos do Pydantic v1 a partir desse submódulo, como se tivesse o Pydantic v1 antigo instalado. + +{* ../../docs_src/pydantic_v1_in_v2/tutorial001_an_py310.py hl[1,4] *} + +### Suporte do FastAPI ao Pydantic v1 no v2 { #fastapi-support-for-pydantic-v1-in-v2 } + +Desde o FastAPI 0.119.0, há também suporte parcial ao Pydantic v1 a partir de dentro do Pydantic v2, para facilitar a migração para o v2. + +Assim, você pode atualizar o Pydantic para a versão 2 mais recente e alterar os imports para usar o submódulo `pydantic.v1`, e em muitos casos tudo simplesmente funcionará. + +{* ../../docs_src/pydantic_v1_in_v2/tutorial002_an_py310.py hl[2,5,15] *} + +/// warning | Atenção + +Tenha em mente que, como a equipe do Pydantic não oferece mais suporte ao Pydantic v1 nas versões recentes do Python, a partir do Python 3.14, o uso de `pydantic.v1` também não é suportado no Python 3.14 e superiores. + +/// + +### Pydantic v1 e v2 na mesma aplicação { #pydantic-v1-and-v2-on-the-same-app } + +Não é suportado pelo Pydantic ter um modelo do Pydantic v2 com campos próprios definidos como modelos do Pydantic v1, ou vice-versa. + +```mermaid +graph TB + subgraph "❌ Not Supported" + direction TB + subgraph V2["Pydantic v2 Model"] + V1Field["Pydantic v1 Model"] + end + subgraph V1["Pydantic v1 Model"] + V2Field["Pydantic v2 Model"] + end + end + + style V2 fill:#f9fff3 + style V1 fill:#fff6f0 + style V1Field fill:#fff6f0 + style V2Field fill:#f9fff3 +``` + +...but, you can have separated models using Pydantic v1 and v2 in the same app. + +```mermaid +graph TB + subgraph "✅ Supported" + direction TB + subgraph V2["Pydantic v2 Model"] + V2Field["Pydantic v2 Model"] + end + subgraph V1["Pydantic v1 Model"] + V1Field["Pydantic v1 Model"] + end + end + + style V2 fill:#f9fff3 + style V1 fill:#fff6f0 + style V1Field fill:#fff6f0 + style V2Field fill:#f9fff3 +``` + +Em alguns casos, é até possível ter modelos Pydantic v1 e v2 na mesma operação de rota na sua aplicação FastAPI: + +{* ../../docs_src/pydantic_v1_in_v2/tutorial003_an_py310.py hl[2:3,6,12,21:22] *} + +No exemplo acima, o modelo de entrada é um modelo Pydantic v1, e o modelo de saída (definido em `response_model=ItemV2`) é um modelo Pydantic v2. + +### Parâmetros do Pydantic v1 { #pydantic-v1-parameters } + +Se você precisar usar algumas das ferramentas específicas do FastAPI para parâmetros como `Body`, `Query`, `Form` etc. com modelos do Pydantic v1, pode importá-las de `fastapi.temp_pydantic_v1_params` enquanto conclui a migração para o Pydantic v2: + +{* ../../docs_src/pydantic_v1_in_v2/tutorial004_an_py310.py hl[4,18] *} + +### Migre em etapas { #migrate-in-steps } + +/// tip | Dica + +Primeiro tente com o `bump-pydantic`; se seus testes passarem e isso funcionar, então você concluiu tudo com um único comando. ✨ + +/// + +Se o `bump-pydantic` não funcionar para o seu caso, você pode usar o suporte a modelos Pydantic v1 e v2 na mesma aplicação para fazer a migração para o Pydantic v2 gradualmente. + +Você poderia primeiro atualizar o Pydantic para usar a versão 2 mais recente e alterar os imports para usar `pydantic.v1` para todos os seus modelos. + +Depois, você pode começar a migrar seus modelos do Pydantic v1 para o v2 em grupos, em etapas graduais. 🚶 diff --git a/docs/pt/docs/how-to/separate-openapi-schemas.md b/docs/pt/docs/how-to/separate-openapi-schemas.md index 291b0e1639..8855934fd9 100644 --- a/docs/pt/docs/how-to/separate-openapi-schemas.md +++ b/docs/pt/docs/how-to/separate-openapi-schemas.md @@ -1,4 +1,4 @@ -# Esquemas OpenAPI Separados para Entrada e Saída ou Não +# Esquemas OpenAPI Separados para Entrada e Saída ou Não { #separate-openapi-schemas-for-input-and-output-or-not } Ao usar **Pydantic v2**, o OpenAPI gerado é um pouco mais exato e **correto** do que antes. 😎 @@ -6,21 +6,21 @@ Inclusive, em alguns casos, ele terá até **dois JSON Schemas** no OpenAPI para Vamos ver como isso funciona e como alterar se for necessário. -## Modelos Pydantic para Entrada e Saída +## Modelos Pydantic para Entrada e Saída { #pydantic-models-for-input-and-output } Digamos que você tenha um modelo Pydantic com valores padrão, como este: {* ../../docs_src/separate_openapi_schemas/tutorial001_py310.py ln[1:7] hl[7] *} -### Modelo para Entrada +### Modelo para Entrada { #model-for-input } Se você usar esse modelo como entrada, como aqui: {* ../../docs_src/separate_openapi_schemas/tutorial001_py310.py ln[1:15] hl[14] *} -... então o campo `description` não será obrigatório. Porque ele tem um valor padrão de `None`. +... então o campo `description` **não será obrigatório**. Porque ele tem um valor padrão de `None`. -### Modelo de Entrada na Documentação +### Modelo de Entrada na Documentação { #input-model-in-docs } Você pode confirmar que na documentação, o campo `description` não tem um **asterisco vermelho**, não é marcado como obrigatório: @@ -28,7 +28,7 @@ Você pode confirmar que na documentação, o campo `description` não tem um **
-### Modelo para Saída +### Modelo para Saída { #model-for-output } Mas se você usar o mesmo modelo como saída, como aqui: @@ -36,7 +36,7 @@ Mas se você usar o mesmo modelo como saída, como aqui: ... então, como `description` tem um valor padrão, se você **não retornar nada** para esse campo, ele ainda terá o **valor padrão**. -### Modelo para Dados de Resposta de Saída +### Modelo para Dados de Resposta de Saída { #model-for-output-response-data } Se você interagir com a documentação e verificar a resposta, mesmo que o código não tenha adicionado nada em um dos campos `description`, a resposta JSON contém o valor padrão (`null`): @@ -55,15 +55,15 @@ Por causa disso, o JSON Schema para um modelo pode ser diferente dependendo se e * para **entrada**, o `description` **não será obrigatório** * para **saída**, ele será **obrigatório** (e possivelmente `None`, ou em termos de JSON, `null`) -### Modelo para Saída na Documentação +### Modelo para Saída na Documentação { #model-for-output-in-docs } -Você pode verificar o modelo de saída na documentação também, ambos `name` e `description` são marcados como **obrigatórios** com um **asterisco vermelho**: +Você pode verificar o modelo de saída na documentação também, **ambos** `name` e `description` são marcados como **obrigatórios** com um **asterisco vermelho**:
-### Modelo para Entrada e Saída na Documentação +### Modelo para Entrada e Saída na Documentação { #model-for-input-and-output-in-docs } E se você verificar todos os Schemas disponíveis (JSON Schemas) no OpenAPI, verá que há dois, um `Item-Input` e um `Item-Output`. @@ -77,7 +77,7 @@ Mas para `Item-Output`, `description` **é obrigatório**, tem um asterisco verm Com esse recurso do **Pydantic v2**, sua documentação da API fica mais **precisa**, e se você tiver clientes e SDKs gerados automaticamente, eles serão mais precisos também, proporcionando uma melhor **experiência para desenvolvedores** e consistência. 🎉 -## Não Separe Schemas +## Não Separe Schemas { #do-not-separate-schemas } Agora, há alguns casos em que você pode querer ter o **mesmo esquema para entrada e saída**. @@ -93,7 +93,7 @@ O suporte para `separate_input_output_schemas` foi adicionado no FastAPI `0.102. {* ../../docs_src/separate_openapi_schemas/tutorial002_py310.py hl[10] *} -### Mesmo Esquema para Modelos de Entrada e Saída na Documentação +### Mesmo Esquema para Modelos de Entrada e Saída na Documentação { #same-schema-for-input-and-output-models-in-docs } E agora haverá um único esquema para entrada e saída para o modelo, apenas `Item`, e `description` **não será obrigatório**: diff --git a/docs/pt/docs/how-to/testing-database.md b/docs/pt/docs/how-to/testing-database.md index 02f909f249..4258d1e24c 100644 --- a/docs/pt/docs/how-to/testing-database.md +++ b/docs/pt/docs/how-to/testing-database.md @@ -1,7 +1,7 @@ -# Testando a Base de Dados +# Testando a Base de Dados { #testing-a-database } Você pode estudar sobre bases de dados, SQL e SQLModel na documentação de SQLModel. 🤓 Aqui tem um mini tutorial de como usar SQLModel com FastAPI. ✨ -Esse tutorial inclui uma sessão sobre testar bases de dados SQL. 😎 +Esse tutorial inclui uma seção sobre testar bases de dados SQL. 😎 diff --git a/docs/pt/docs/index.md b/docs/pt/docs/index.md index a361913c3f..0428c3a798 100644 --- a/docs/pt/docs/index.md +++ b/docs/pt/docs/index.md @@ -1,11 +1,11 @@ -# FastAPI +# FastAPI { #fastapi }

- FastAPI + FastAPI

Framework FastAPI, alta performance, fácil de aprender, fácil de codar, pronto para produção @@ -27,7 +27,7 @@ --- -**Documentação**: https://fastapi.tiangolo.com +**Documentação**: https://fastapi.tiangolo.com **Código fonte**: https://github.com/fastapi/fastapi @@ -40,32 +40,38 @@ Os recursos chave são: * **Rápido**: alta performance, equivalente a **NodeJS** e **Go** (graças ao Starlette e Pydantic). [Um dos frameworks mais rápidos disponíveis](#performance). * **Rápido para codar**: Aumenta a velocidade para desenvolver recursos entre 200% a 300%. * * **Poucos bugs**: Reduz cerca de 40% de erros induzidos por humanos (desenvolvedores). * -* **Intuitivo**: Grande suporte a _IDEs_. _Auto-Complete_ em todos os lugares. Menos tempo debugando. +* **Intuitivo**: Grande suporte a _IDEs_. Preenchimento automático em todos os lugares. Menos tempo debugando. * **Fácil**: Projetado para ser fácil de aprender e usar. Menos tempo lendo documentação. -* **Enxuto**: Minimize duplicação de código. Múltiplos recursos para cada declaração de parâmetro. Menos bugs. +* **Enxuto**: Minimize duplicação de código. Múltiplas funcionalidades para cada declaração de parâmetro. Menos bugs. * **Robusto**: Tenha código pronto para produção. E com documentação interativa automática. -* **Baseado em padrões**: Baseado em (e totalmente compatível com) os padrões abertos para APIs: OpenAPI (anteriormente conhecido como Swagger) e _JSON Schema_. +* **Baseado em padrões**: Baseado em (e totalmente compatível com) os padrões abertos para APIs: OpenAPI (anteriormente conhecido como Swagger) e JSON Schema. * estimativas baseadas em testes realizados com equipe interna de desenvolvimento, construindo aplicações em produção. -## Patrocinadores Ouro +## Patrocinadores { #sponsors } -{% if sponsors %} +### Patrocinador Keystone { #keystone-sponsor } + +{% for sponsor in sponsors.keystone -%} + +{% endfor -%} + +### Patrocinadores Ouro e Prata { #gold-and-silver-sponsors } + {% for sponsor in sponsors.gold -%} {% endfor -%} {%- for sponsor in sponsors.silver -%} {% endfor %} -{% endif %} -Outros patrocinadores +Outros patrocinadores -## Opiniões +## Opiniões { #opinions } "*[...] Estou usando **FastAPI** muito esses dias. [...] Estou na verdade planejando utilizar ele em todos os times de **serviços _Machine Learning_ na Microsoft**. Alguns deles estão sendo integrados no _core_ do produto **Windows** e alguns produtos **Office**.*" @@ -111,24 +117,24 @@ Os recursos chave são: --- -## **Typer**, o FastAPI das interfaces de linhas de comando +## **Typer**, o FastAPI das interfaces de linhas de comando { #typer-the-fastapi-of-clis } -Se você estiver construindo uma aplicação _CLI_ para ser utilizada em um terminal ao invés de uma aplicação web, dê uma olhada no **Typer**. +Se você estiver construindo uma aplicação CLI para ser utilizada em um terminal ao invés de uma aplicação web, dê uma olhada no **Typer**. **Typer** é o irmão menor do FastAPI. E seu propósito é ser o **FastAPI das _CLIs_**. ⌨️ 🚀 -## Requisitos +## Requisitos { #requirements } FastAPI está nos ombros de gigantes: * Starlette para as partes web. * Pydantic para a parte de dados. -## Instalação +## Instalação { #installation } -Crie e ative um ambiente virtual, e então instale o FastAPI: +Crie e ative um ambiente virtual e então instale o FastAPI:

@@ -142,11 +148,11 @@ $ pip install "fastapi[standard]" **Nota**: Certifique-se de que você colocou `"fastapi[standard]"` com aspas, para garantir que funcione em todos os terminais. -## Exemplo +## Exemplo { #example } -### Crie +### Crie { #create-it } -* Crie um arquivo `main.py` com: +Crie um arquivo `main.py` com: ```Python from typing import Union @@ -191,11 +197,11 @@ async def read_item(item_id: int, q: Union[str, None] = None): **Nota**: -Se você não sabe, verifique a seção _"Com pressa?"_ sobre `async` e `await` nas docs. +Se você não sabe, verifique a seção _"Com pressa?"_ sobre `async` e `await` nas docs. -### Rode +### Rode { #run-it } Rode o servidor com: @@ -237,7 +243,7 @@ Você pode ler mais sobre isso na http://127.0.0.1:8000/items/5?q=somequery. @@ -254,7 +260,7 @@ Você acabou de criar uma API que: * A _rota_ `/items/{item_id}` tem um _parâmetro de rota_ `item_id` que deve ser um `int`. * A _rota_ `/items/{item_id}` tem um _parâmetro query_ `q` `str` opcional. -### Documentação Interativa da API +### Documentação Interativa da API { #interactive-api-docs } Agora vá para http://127.0.0.1:8000/docs. @@ -262,7 +268,7 @@ Você verá a documentação automática interativa da API (fornecida por http://127.0.0.1:8000/redoc. @@ -270,7 +276,7 @@ Você verá a documentação automática alternativa (fornecida por http://127.0.0.1:8000/docs. @@ -324,7 +330,7 @@ Agora vá para http://127.0.0.1:8000/redoc. @@ -332,7 +338,7 @@ E agora, vá para Conversão de dados de entrada: vindo da rede para dados e tipos Python. Consegue ler: +* Conversão de dados de entrada: vindo da rede para dados e tipos Python. Consegue ler: * JSON. * Parâmetros de rota. * Parâmetros de _query_ . @@ -370,7 +376,7 @@ item: Item * Cabeçalhos. * Formulários. * Arquivos. -* Conversão de dados de saída de tipos e dados Python para dados de rede (como JSON): +* Conversão de dados de saída de tipos e dados Python para dados de rede (como JSON): * Converte tipos Python (`str`, `int`, `float`, `bool`, `list` etc). * Objetos `datetime`. * Objetos `UUID`. @@ -390,7 +396,7 @@ Voltando ao código do exemplo anterior, **FastAPI** irá: * Verificar se existe um parâmetro de _query_ opcional nomeado como `q` (como em `http://127.0.0.1:8000/items/foo?q=somequery`) para requisições `GET`. * Como o parâmetro `q` é declarado com `= None`, ele é opcional. * Sem o `None` ele poderia ser obrigatório (como o corpo no caso de `PUT`). -* Para requisições `PUT` para `/items/{item_id}`, lerá o corpo como JSON e: +* Para requisições `PUT` para `/items/{item_id}`, lerá o corpo como JSON: * Verifica que tem um atributo obrigatório `name` que deve ser `str`. * Verifica que tem um atributo obrigatório `price` que deve ser `float`. * Verifica que tem an atributo opcional `is_offer`, que deve ser `bool`, se presente. @@ -444,18 +450,69 @@ Para um exemplo mais completo incluindo mais recursos, veja FastAPI Cloud, inscreva-se na lista de espera se ainda não o fez. 🚀 + +Se você já tem uma conta na **FastAPI Cloud** (nós convidamos você da lista de espera 😉), pode implantar sua aplicação com um único comando. + +Antes de implantar, certifique-se de que está autenticado: + +
+ +```console +$ fastapi login + +You are logged in to FastAPI Cloud 🚀 +``` + +
+ +Depois, implemente sua aplicação: + +
+ +```console +$ fastapi deploy + +Deploying to FastAPI Cloud... + +✅ Deployment successful! + +🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev +``` + +
+ +É isso! Agora você pode acessar sua aplicação nesse URL. ✨ + +#### Sobre a FastAPI Cloud { #about-fastapi-cloud } + +**FastAPI Cloud** é construída pelo mesmo autor e equipe por trás do **FastAPI**. + +Ela simplifica o processo de **construir**, **implantar** e **acessar** uma API com esforço mínimo. + +Traz a mesma **experiência do desenvolvedor** de construir aplicações com FastAPI para **implantá-las** na nuvem. 🎉 + +A FastAPI Cloud é a principal patrocinadora e financiadora dos projetos open source do ecossistema *FastAPI and friends*. ✨ + +#### Implante em outros provedores de nuvem { #deploy-to-other-cloud-providers } + +FastAPI é open source e baseado em padrões. Você pode implantar aplicações FastAPI em qualquer provedor de nuvem que escolher. + +Siga os tutoriais do seu provedor de nuvem para implantar aplicações FastAPI com eles. 🤓 + +## Performance { #performance } Testes de performance da _Independent TechEmpower_ mostram aplicações **FastAPI** rodando sob Uvicorn como um dos _frameworks_ Python mais rápidos disponíveis, somente atrás de Starlette e Uvicorn (utilizados internamente pelo FastAPI). (*) Para entender mais sobre performance, veja a seção Comparações. -## Dependências +## Dependências { #dependencies } O FastAPI depende do Pydantic e do Starlette. - -### Dependências `standard` +### Dependências `standard` { #standard-dependencies } Quando você instala o FastAPI com `pip install "fastapi[standard]"`, ele vêm com o grupo `standard` (padrão) de dependências opcionais: @@ -467,18 +524,23 @@ Utilizado pelo Starlette: * httpx - Obrigatório caso você queira utilizar o `TestClient`. * jinja2 - Obrigatório se você quer utilizar a configuração padrão de templates. -* python-multipart - Obrigatório se você deseja suporte a "parsing" de formulário, com `request.form()`. +* python-multipart - Obrigatório se você deseja suporte a "parsing" de formulário, com `request.form()`. -Utilizado pelo FastAPI / Starlette: +Utilizado pelo FastAPI: * uvicorn - para o servidor que carrega e serve a sua aplicação. Isto inclui `uvicorn[standard]`, que inclui algumas dependências (e.g. `uvloop`) necessárias para servir em alta performance. -* `fastapi-cli` - que disponibiliza o comando `fastapi`. +* `fastapi-cli[standard]` - que disponibiliza o comando `fastapi`. + * Isso inclui `fastapi-cloud-cli`, que permite implantar sua aplicação FastAPI na FastAPI Cloud. -### Sem as dependências `standard` +### Sem as dependências `standard` { #without-standard-dependencies } Se você não deseja incluir as dependências opcionais `standard`, você pode instalar utilizando `pip install fastapi` ao invés de `pip install "fastapi[standard]"`. -### Dpendências opcionais adicionais +### Sem o `fastapi-cloud-cli` { #without-fastapi-cloud-cli } + +Se você quiser instalar o FastAPI com as dependências padrão, mas sem o `fastapi-cloud-cli`, você pode instalar com `pip install "fastapi[standard-no-fastapi-cloud-cli]"`. + +### Dependências opcionais adicionais { #additional-optional-dependencies } Existem algumas dependências adicionais que você pode querer instalar. @@ -492,6 +554,6 @@ Dependências opcionais adicionais do FastAPI: * orjson - Obrigatório se você deseja utilizar o `ORJSONResponse`. * ujson - Obrigatório se você deseja utilizar o `UJSONResponse`. -## Licença +## Licença { #license } Esse projeto é licenciado sob os termos da licença MIT. diff --git a/docs/pt/docs/learn/index.md b/docs/pt/docs/learn/index.md index b9a7f5972f..1f04929f7d 100644 --- a/docs/pt/docs/learn/index.md +++ b/docs/pt/docs/learn/index.md @@ -1,5 +1,5 @@ -# Aprender +# Aprender { #learn } -Nesta parte da documentação encontramos as seções introdutórias e os tutoriais para aprendermos como usar o **FastAPI**. +Aqui estão as seções introdutórias e os tutoriais para aprender o **FastAPI**. -Nós poderíamos considerar isto um **livro**, **curso**, a maneira **oficial** e recomendada de aprender o FastAPI. 😎 +Pode considerar isto um **livro**, um **curso**, a forma **oficial** e recomendada de aprender o FastAPI. 😎 diff --git a/docs/pt/docs/project-generation.md b/docs/pt/docs/project-generation.md index e337ad762e..419a39879f 100644 --- a/docs/pt/docs/project-generation.md +++ b/docs/pt/docs/project-generation.md @@ -1,4 +1,4 @@ -# Full Stack FastAPI Template +# Full Stack FastAPI Template { #full-stack-fastapi-template } _Templates_, embora tipicamente venham com alguma configuração específica, são desenhados para serem flexíveis e customizáveis. Isso permite que você os modifique e adapte para as especificações do seu projeto, fazendo-os um excelente ponto de partida. 🏁 @@ -6,15 +6,15 @@ Você pode usar esse _template_ para começar, já que ele inclui várias config Repositório GitHub: Full Stack FastAPI Template -## Full Stack FastAPI Template - Pilha de Tecnologias e Recursos +## Full Stack FastAPI Template - Pilha de Tecnologias e Recursos { #full-stack-fastapi-template-technology-stack-and-features } -- ⚡ [**FastAPI**](https://fastapi.tiangolo.com) para a API do backend em Python. +- ⚡ [**FastAPI**](https://fastapi.tiangolo.com/pt) para a API do backend em Python. - 🧰 [SQLModel](https://sqlmodel.tiangolo.com) para as interações do Python com bancos de dados SQL (ORM). - 🔍 [Pydantic](https://docs.pydantic.dev), usado pelo FastAPI, para validação de dados e gerenciamento de configurações. - 💾 [PostgreSQL](https://www.postgresql.org) como banco de dados SQL. - 🚀 [React](https://react.dev) para o frontend. - - 💃 Usando TypeScript, hooks, [Vite](https://vitejs.dev), e outras partes de uma _stack_ frontend moderna. - - 🎨 [Chakra UI](https://chakra-ui.com) para os componentes de frontend. + - 💃 Usando TypeScript, hooks, Vite, e outras partes de uma _stack_ frontend moderna. + - 🎨 [Tailwind CSS](https://tailwindcss.com) e [shadcn/ui](https://ui.shadcn.com) para os componentes de frontend. - 🤖 Um cliente frontend automaticamente gerado. - 🧪 [Playwright](https://playwright.dev) para testes Ponta-a-Ponta. - 🦇 Suporte para modo escuro. diff --git a/docs/pt/docs/python-types.md b/docs/pt/docs/python-types.md index 90a361f403..fc983d1df1 100644 --- a/docs/pt/docs/python-types.md +++ b/docs/pt/docs/python-types.md @@ -1,4 +1,4 @@ -# Introdução aos tipos Python +# Introdução aos tipos Python { #python-types-intro } O Python possui suporte para "dicas de tipo" ou "type hints" (também chamado de "anotações de tipo" ou "type annotations") @@ -18,11 +18,11 @@ Se você é um especialista em Python e já sabe tudo sobre type hints, pule par /// -## Motivação +## Motivação { #motivation } Vamos começar com um exemplo simples: -{* ../../docs_src/python_types/tutorial001.py *} +{* ../../docs_src/python_types/tutorial001_py39.py *} A chamada deste programa gera: @@ -36,9 +36,9 @@ A função faz o seguinte: * Converte a primeira letra de cada uma em maiúsculas com `title()`. * Concatena com um espaço no meio. -{* ../../docs_src/python_types/tutorial001.py hl[2] *} +{* ../../docs_src/python_types/tutorial001_py39.py hl[2] *} -### Edite-o +### Edite-o { #edit-it } É um programa muito simples. @@ -58,7 +58,7 @@ Mas, infelizmente, você não obtém nada útil: -### Adicionar tipos +### Adicionar tipos { #add-types } Vamos modificar uma única linha da versão anterior. @@ -78,7 +78,7 @@ para: Esses são os "type hints": -{* ../../docs_src/python_types/tutorial002.py hl[1] *} +{* ../../docs_src/python_types/tutorial002_py39.py hl[1] *} Isso não é o mesmo que declarar valores padrão como seria com: @@ -102,11 +102,11 @@ Com isso, você pode rolar, vendo as opções, até encontrar o que "soa familia -## Mais motivação +## Mais motivação { #more-motivation } Verifique esta função, ela já possui type hints: -{* ../../docs_src/python_types/tutorial003.py hl[1] *} +{* ../../docs_src/python_types/tutorial003_py39.py hl[1] *} Como o editor conhece os tipos de variáveis, você não obtém apenas o preenchimento automático, mas também as verificações de erro: @@ -114,15 +114,15 @@ Como o editor conhece os tipos de variáveis, você não obtém apenas o preench Agora você sabe que precisa corrigí-lo, converta `age` em uma string com `str(age)`: -{* ../../docs_src/python_types/tutorial004.py hl[2] *} +{* ../../docs_src/python_types/tutorial004_py39.py hl[2] *} -## Declarando Tipos +## Declarando Tipos { #declaring-types } Você acabou de ver o local principal para declarar type hints. Como parâmetros de função. Este também é o principal local em que você os usaria com o **FastAPI**. -### Tipos simples +### Tipos simples { #simple-types } Você pode declarar todos os tipos padrão de Python, não apenas `str`. @@ -133,9 +133,9 @@ Você pode usar, por exemplo: * `bool` * `bytes` -{* ../../docs_src/python_types/tutorial005.py hl[1] *} +{* ../../docs_src/python_types/tutorial005_py39.py hl[1] *} -### Tipos genéricos com parâmetros de tipo +### Tipos genéricos com parâmetros de tipo { #generic-types-with-type-parameters } Existem algumas estruturas de dados que podem conter outros valores, como `dict`, `list`, `set` e `tuple`. E os valores internos também podem ter seu próprio tipo. @@ -143,7 +143,7 @@ Estes tipos que possuem tipos internos são chamados de tipos "**genéricos**". Para declarar esses tipos e os tipos internos, você pode usar o módulo Python padrão `typing`. Ele existe especificamente para suportar esses type hints. -#### Versões mais recentes do Python +#### Versões mais recentes do Python { #newer-versions-of-python } A sintaxe utilizando `typing` é **compatível** com todas as versões, desde o Python 3.6 até as últimas, incluindo o Python 3.9, 3.10, etc. @@ -157,60 +157,28 @@ Por exemplo, "**Python 3.6+**" significa que é compatível com o Python 3.6 ou Se você pode utilizar a **versão mais recente do Python**, utilize os exemplos para as últimas versões. Eles terão as **melhores e mais simples sintaxes**, como por exemplo, "**Python 3.10+**". -#### List +#### List { #list } Por exemplo, vamos definir uma variável para ser uma `list` de `str`. -//// tab | Python 3.9+ +Declare a variável, com a mesma sintaxe com dois pontos (`:`). -Declare uma variável com a mesma sintaxe com dois pontos (`:`) +Como o tipo, coloque `list`. -Como tipo, coloque `list`. +Como a lista é um tipo que contém tipos internos, você os coloca entre colchetes: -Como a lista é o tipo que contém algum tipo interno, você coloca o tipo dentro de colchetes: - -```Python hl_lines="1" -{!> ../../docs_src/python_types/tutorial006_py39.py!} -``` - -//// - -//// tab | Python 3.8+ - -De `typing`, importe `List` (com o `L` maiúsculo): - -```Python hl_lines="1" -{!> ../../docs_src/python_types/tutorial006.py!} -``` - -Declare uma variável com a mesma sintaxe com dois pontos (`:`) - -Como tipo, coloque o `List` que você importou de `typing`. - -Como a lista é o tipo que contém algum tipo interno, você coloca o tipo dentro de colchetes: - -```Python hl_lines="4" -{!> ../../docs_src/python_types/tutorial006.py!} -``` - -//// +{* ../../docs_src/python_types/tutorial006_py39.py hl[1] *} /// info | Informação Estes tipos internos dentro dos colchetes são chamados "parâmetros de tipo" (type parameters). -Neste caso, `str` é o parâmetro de tipo passado para `List` (ou `list` no Python 3.9 ou superior). +Neste caso, `str` é o parâmetro de tipo passado para `list`. /// Isso significa: "a variável `items` é uma `list`, e cada um dos itens desta lista é uma `str`". -/// tip | Dica - -Se você usa o Python 3.9 ou superior, você não precisa importar `List` de `typing`. Você pode utilizar o mesmo tipo `list` no lugar. - -/// - Ao fazer isso, seu editor pode fornecer suporte mesmo durante o processamento de itens da lista: @@ -221,32 +189,18 @@ Observe que a variável `item` é um dos elementos da lista `items`. E, ainda assim, o editor sabe que é um `str` e fornece suporte para isso. -#### Tuple e Set +#### Tuple e Set { #tuple-and-set } Você faria o mesmo para declarar `tuple`s e `set`s: -//// tab | Python 3.9+ - -```Python hl_lines="1" -{!> ../../docs_src/python_types/tutorial007_py39.py!} -``` - -//// - -//// tab | Python 3.8+ - -```Python hl_lines="1 4" -{!> ../../docs_src/python_types/tutorial007.py!} -``` - -//// +{* ../../docs_src/python_types/tutorial007_py39.py hl[1] *} Isso significa que: * A variável `items_t` é uma `tuple` com 3 itens, um `int`, outro `int` e uma `str`. * A variável `items_s` é um `set`, e cada um de seus itens é do tipo `bytes`. -#### Dict +#### Dict { #dict } Para definir um `dict`, você passa 2 parâmetros de tipo, separados por vírgulas. @@ -254,35 +208,21 @@ O primeiro parâmetro de tipo é para as chaves do `dict`. O segundo parâmetro de tipo é para os valores do `dict`: -//// tab | Python 3.9+ - -```Python hl_lines="1" -{!> ../../docs_src/python_types/tutorial008_py39.py!} -``` - -//// - -//// tab | Python 3.8+ - -```Python hl_lines="1 4" -{!> ../../docs_src/python_types/tutorial008.py!} -``` - -//// +{* ../../docs_src/python_types/tutorial008_py39.py hl[1] *} Isso significa que: -* A variável `prices` é um dict`: +* A variável `prices` é um `dict`: * As chaves deste `dict` são do tipo `str` (digamos, o nome de cada item). * Os valores deste `dict` são do tipo `float` (digamos, o preço de cada item). -#### Union +#### Union { #union } Você pode declarar que uma variável pode ser de qualquer um dentre **diversos tipos**. Por exemplo, um `int` ou um `str`. No Python 3.6 e superior (incluindo o Python 3.10), você pode utilizar o tipo `Union` de `typing`, e colocar dentro dos colchetes os possíveis tipos aceitáveis. -No Python 3.10 também existe uma **nova sintaxe** onde você pode colocar os possívels tipos separados por uma barra vertical (`|`). +No Python 3.10 também existe uma **nova sintaxe** onde você pode colocar os possíveis tipos separados por uma barra vertical (`|`). //// tab | Python 3.10+ @@ -292,25 +232,24 @@ No Python 3.10 também existe uma **nova sintaxe** onde você pode colocar os po //// -//// tab | Python 3.8+ +//// tab | Python 3.9+ ```Python hl_lines="1 4" -{!> ../../docs_src/python_types/tutorial008b.py!} +{!> ../../docs_src/python_types/tutorial008b_py39.py!} ``` //// Em ambos os casos, isso significa que `item` poderia ser um `int` ou um `str`. - -#### Possívelmente `None` +#### Possivelmente `None` { #possibly-none } Você pode declarar que um valor pode ter um tipo, como `str`, mas que ele também pode ser `None`. No Python 3.6 e superior (incluindo o Python 3.10) você pode declará-lo importando e utilizando `Optional` do módulo `typing`. ```Python hl_lines="1 4" -{!../../docs_src/python_types/tutorial009.py!} +{!../../docs_src/python_types/tutorial009_py39.py!} ``` O uso de `Optional[str]` em vez de apenas `str` permitirá que o editor o ajude a detectar erros, onde você pode estar assumindo que um valor é sempre um `str`, quando na verdade também pode ser `None`. @@ -327,23 +266,23 @@ Isso também significa que no Python 3.10, você pode utilizar `Something | None //// -//// tab | Python 3.8+ +//// tab | Python 3.9+ ```Python hl_lines="1 4" -{!> ../../docs_src/python_types/tutorial009.py!} +{!> ../../docs_src/python_types/tutorial009_py39.py!} ``` //// -//// tab | Python 3.8+ alternative +//// tab | Python 3.9+ alternativa ```Python hl_lines="1 4" -{!> ../../docs_src/python_types/tutorial009b.py!} +{!> ../../docs_src/python_types/tutorial009b_py39.py!} ``` //// -#### Utilizando `Union` ou `Optional` +#### Utilizando `Union` ou `Optional` { #using-union-or-optional } Se você está utilizando uma versão do Python abaixo da 3.10, aqui vai uma dica do meu ponto de vista bem **subjetivo**: @@ -358,15 +297,15 @@ Isso é apenas sobre palavras e nomes. Mas estas palavras podem afetar como os s Por exemplo, vamos pegar esta função: -{* ../../docs_src/python_types/tutorial009c.py hl[1,4] *} +{* ../../docs_src/python_types/tutorial009c_py39.py hl[1,4] *} -O paâmetro `name` é definido como `Optional[str]`, mas ele **não é opcional**, você não pode chamar a função sem o parâmetro: +O parâmetro `name` é definido como `Optional[str]`, mas ele **não é opcional**, você não pode chamar a função sem o parâmetro: ```Python say_hi() # Oh, no, this throws an error! 😱 ``` -O parâmetro `name` **ainda é obrigatório** (não *opicional*) porque ele não possui um valor padrão. Mesmo assim, `name` aceita `None` como valor: +O parâmetro `name` **ainda é obrigatório** (não *opcional*) porque ele não possui um valor padrão. Mesmo assim, `name` aceita `None` como valor: ```Python say_hi(name=None) # This works, None is valid 🎉 @@ -378,7 +317,7 @@ A boa notícia é, quando você estiver no Python 3.10 você não precisará se E então você não precisará mais se preocupar com nomes como `Optional` e `Union`. 😎 -#### Tipos genéricos +#### Tipos genéricos { #generic-types } Esses tipos que usam parâmetros de tipo entre colchetes são chamados **tipos genéricos** ou **genéricos**. Por exemplo: @@ -391,11 +330,11 @@ Você pode utilizar os mesmos tipos internos como genéricos (com colchetes e ti * `set` * `dict` -E o mesmo como no Python 3.8, do módulo `typing`: +E o mesmo que com versões anteriores do Python, do módulo `typing`: * `Union` -* `Optional` (o mesmo que com o 3.8) -* ...entro outros. +* `Optional` +* ...entre outros. No Python 3.10, como uma alternativa para a utilização dos genéricos `Union` e `Optional`, você pode usar a barra vertical (`|`) para declarar uniões de tipos. Isso é muito melhor e mais simples. @@ -410,39 +349,27 @@ Você pode utilizar os mesmos tipos internos como genéricos (com colchetes e ti * `set` * `dict` -E o mesmo como no Python 3.8, do módulo `typing`: +E genéricos do módulo `typing`: * `Union` * `Optional` -* ...entro outros. +* ...entre outros. //// -//// tab | Python 3.8+ - -* `List` -* `Tuple` -* `Set` -* `Dict` -* `Union` -* `Optional` -* ...entro outros. - -//// - -### Classes como tipos +### Classes como tipos { #classes-as-types } Você também pode declarar uma classe como o tipo de uma variável. Digamos que você tenha uma classe `Person`, com um nome: -{* ../../docs_src/python_types/tutorial010.py hl[1:3] *} +{* ../../docs_src/python_types/tutorial010_py39.py hl[1:3] *} Então você pode declarar que uma variável é do tipo `Person`: -{* ../../docs_src/python_types/tutorial010.py hl[6] *} +{* ../../docs_src/python_types/tutorial010_py39.py hl[6] *} -E então, novamente, você recebe todo o suporte do editor: +E então, novamente, você recebe todo o apoio do editor: @@ -450,7 +377,7 @@ Perceba que isso significa que "`one_person` é uma **instância** da classe `Pe Isso não significa que "`one_person` é a **classe** chamada `Person`". -## Modelos Pydantic +## Modelos Pydantic { #pydantic-models } O Pydantic é uma biblioteca Python para executar a validação de dados. @@ -462,31 +389,9 @@ Em seguida, você cria uma instância dessa classe com alguns valores e ela os v E você recebe todo o suporte do editor com esse objeto resultante. -Retirado dos documentos oficiais dos Pydantic: +Um exemplo da documentação oficial do Pydantic: -//// tab | Python 3.10+ - -```Python -{!> ../../docs_src/python_types/tutorial011_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!> ../../docs_src/python_types/tutorial011_py39.py!} -``` - -//// - -//// tab | Python 3.8+ - -```Python -{!> ../../docs_src/python_types/tutorial011.py!} -``` - -//// +{* ../../docs_src/python_types/tutorial011_py310.py *} /// info | Informação @@ -504,32 +409,13 @@ O Pydantic tem um comportamento especial quando você usa `Optional` ou `Union[S /// - -## Type Hints com Metadados de Anotações +## Type Hints com Metadados de Anotações { #type-hints-with-metadata-annotations } O Python possui uma funcionalidade que nos permite incluir **metadados adicionais** nos type hints utilizando `Annotated`. -//// tab | Python 3.9+ +Desde o Python 3.9, `Annotated` faz parte da biblioteca padrão, então você pode importá-lo de `typing`. -No Python 3.9, `Annotated` é parte da biblioteca padrão, então você pode importá-lo de `typing`. - -```Python hl_lines="1 4" -{!> ../../docs_src/python_types/tutorial013_py39.py!} -``` - -//// - -//// tab | Python 3.8+ - -Em versões abaixo do Python 3.9, você importa `Annotated` de `typing_extensions`. - -Ele já estará instalado com o **FastAPI**. - -```Python hl_lines="1 4" -{!> ../../docs_src/python_types/tutorial013.py!} -``` - -//// +{* ../../docs_src/python_types/tutorial013_py39.py hl[1,4] *} O Python em si não faz nada com este `Annotated`. E para editores e outras ferramentas, o tipo ainda é `str`. @@ -549,8 +435,7 @@ E também que o seu código será muito compatível com diversas outras ferramen /// - -## Type hints no **FastAPI** +## Type hints no **FastAPI** { #type-hints-in-fastapi } O **FastAPI** aproveita esses type hints para fazer várias coisas. @@ -574,6 +459,6 @@ O importante é que, usando tipos padrão de Python, em um único local (em vez /// info | Informação -Se você já passou por todo o tutorial e voltou para ver mais sobre os tipos, um bom recurso é a "cheat sheet" do `mypy` . +Se você já passou por todo o tutorial e voltou para ver mais sobre os tipos, um bom recurso é a "cheat sheet" do `mypy` . /// diff --git a/docs/pt/docs/resources/index.md b/docs/pt/docs/resources/index.md index 6eff8f9e78..24cea95641 100644 --- a/docs/pt/docs/resources/index.md +++ b/docs/pt/docs/resources/index.md @@ -1,3 +1,3 @@ -# Recursos +# Recursos { #resources } -Material complementar, links externos, artigos e muito mais. ✈️ +Material complementar, links externos e mais. ✈️ diff --git a/docs/pt/docs/tutorial/background-tasks.md b/docs/pt/docs/tutorial/background-tasks.md index b8ab58cda3..34805364b4 100644 --- a/docs/pt/docs/tutorial/background-tasks.md +++ b/docs/pt/docs/tutorial/background-tasks.md @@ -1,84 +1,85 @@ -# Tarefas em segundo plano +# Tarefas em segundo plano { #background-tasks } -Você pode definir tarefas em segundo plano a serem executadas _ após _ retornar uma resposta. +Você pode definir tarefas em segundo plano para serem executadas *após* retornar uma resposta. -Isso é útil para operações que precisam acontecer após uma solicitação, mas que o cliente realmente não precisa esperar a operação ser concluída para receber a resposta. +Isso é útil para operações que precisam acontecer após uma request, mas que o cliente não precisa realmente esperar a operação terminar antes de receber a resposta. Isso inclui, por exemplo: -- Envio de notificações por email após a realização de uma ação: - - Como conectar-se a um servidor de e-mail e enviar um e-mail tende a ser "lento" (vários segundos), você pode retornar a resposta imediatamente e enviar a notificação por e-mail em segundo plano. -- Processando dados: - - Por exemplo, digamos que você receba um arquivo que deve passar por um processo lento, você pode retornar uma resposta de "Aceito" (HTTP 202) e processá-lo em segundo plano. +* Notificações por e-mail enviadas após realizar uma ação: + * Como conectar-se a um servidor de e-mail e enviar um e-mail tende a ser “lento” (vários segundos), você pode retornar a resposta imediatamente e enviar a notificação por e-mail em segundo plano. +* Processamento de dados: + * Por exemplo, digamos que você receba um arquivo que precisa passar por um processo lento; você pode retornar uma resposta “Accepted” (HTTP 202) e processar o arquivo em segundo plano. -## Usando `BackgroundTasks` +## Usando `BackgroundTasks` { #using-backgroundtasks } -Primeiro, importe `BackgroundTasks` e defina um parâmetro em sua _função de operação de caminho_ com uma declaração de tipo de `BackgroundTasks`: +Primeiro, importe `BackgroundTasks` e defina um parâmetro na sua *função de operação de rota* com uma declaração de tipo `BackgroundTasks`: -{* ../../docs_src/background_tasks/tutorial001.py hl[1,13] *} +{* ../../docs_src/background_tasks/tutorial001_py39.py hl[1,13] *} O **FastAPI** criará o objeto do tipo `BackgroundTasks` para você e o passará como esse parâmetro. -## Criar uma função de tarefa +## Crie uma função de tarefa { #create-a-task-function } -Crie uma função a ser executada como tarefa em segundo plano. +Crie uma função para ser executada como a tarefa em segundo plano. É apenas uma função padrão que pode receber parâmetros. -Pode ser uma função `async def` ou `def` normal, o **FastAPI** saberá como lidar com isso corretamente. +Pode ser uma função `async def` ou um `def` normal, o **FastAPI** saberá como lidar com isso corretamente. -Nesse caso, a função de tarefa gravará em um arquivo (simulando o envio de um e-mail). +Neste caso, a função da tarefa escreverá em um arquivo (simulando o envio de um e-mail). -E como a operação de gravação não usa `async` e `await`, definimos a função com `def` normal: +E como a operação de escrita não usa `async` e `await`, definimos a função com um `def` normal: -{* ../../docs_src/background_tasks/tutorial001.py hl[6:9] *} +{* ../../docs_src/background_tasks/tutorial001_py39.py hl[6:9] *} -## Adicionar a tarefa em segundo plano +## Adicione a tarefa em segundo plano { #add-the-background-task } -Dentro de sua _função de operação de caminho_, passe sua função de tarefa para o objeto _tarefas em segundo plano_ com o método `.add_task()`: +Dentro da sua *função de operação de rota*, passe sua função de tarefa para o objeto de *tarefas em segundo plano* com o método `.add_task()`: -{* ../../docs_src/background_tasks/tutorial001.py hl[14] *} +{* ../../docs_src/background_tasks/tutorial001_py39.py hl[14] *} -`.add_task()` recebe como argumentos: +O `.add_task()` recebe como argumentos: -- Uma função de tarefa a ser executada em segundo plano (`write_notification`). -- Qualquer sequência de argumentos que deve ser passada para a função de tarefa na ordem (`email`). -- Quaisquer argumentos nomeados que devem ser passados ​​para a função de tarefa (`mensagem = "alguma notificação"`). +* Uma função de tarefa a ser executada em segundo plano (`write_notification`). +* Qualquer sequência de argumentos que deve ser passada para a função de tarefa na ordem (`email`). +* Quaisquer argumentos nomeados que devem ser passados para a função de tarefa (`message="some notification"`). -## Injeção de dependência +## Injeção de dependências { #dependency-injection } -Usar `BackgroundTasks` também funciona com o sistema de injeção de dependência, você pode declarar um parâmetro do tipo `BackgroundTasks` em vários níveis: em uma _função de operação de caminho_, em uma dependência (confiável), em uma subdependência, etc. +Usar `BackgroundTasks` também funciona com o sistema de injeção de dependências; você pode declarar um parâmetro do tipo `BackgroundTasks` em vários níveis: em uma *função de operação de rota*, em uma dependência (dependable), em uma subdependência, etc. -O **FastAPI** sabe o que fazer em cada caso e como reutilizar o mesmo objeto, de forma que todas as tarefas em segundo plano sejam mescladas e executadas em segundo plano posteriormente: +O **FastAPI** sabe o que fazer em cada caso e como reutilizar o mesmo objeto, de forma que todas as tarefas em segundo plano sejam combinadas e executadas em segundo plano depois: -{* ../../docs_src/background_tasks/tutorial002.py hl[13,15,22,25] *} -Neste exemplo, as mensagens serão gravadas no arquivo `log.txt` _após_ o envio da resposta. +{* ../../docs_src/background_tasks/tutorial002_an_py310.py hl[13,15,22,25] *} -Se houver uma consulta na solicitação, ela será gravada no log em uma tarefa em segundo plano. +Neste exemplo, as mensagens serão escritas no arquivo `log.txt` *após* o envio da resposta. -E então outra tarefa em segundo plano gerada na _função de operação de caminho_ escreverá uma mensagem usando o parâmetro de caminho `email`. +Se houver uma query na request, ela será registrada em uma tarefa em segundo plano. -## Detalhes técnicos +E então outra tarefa em segundo plano gerada na *função de operação de rota* escreverá uma mensagem usando o parâmetro de path `email`. + +## Detalhes técnicos { #technical-details } A classe `BackgroundTasks` vem diretamente de `starlette.background`. -Ela é importada/incluída diretamente no FastAPI para que você possa importá-la do `fastapi` e evitar a importação acidental da alternativa `BackgroundTask` (sem o `s` no final) de `starlette.background`. +Ela é importada/incluída diretamente no FastAPI para que você possa importá-la de `fastapi` e evitar importar acidentalmente a alternativa `BackgroundTask` (sem o `s` no final) de `starlette.background`. -Usando apenas `BackgroundTasks` (e não `BackgroundTask`), é então possível usá-la como um parâmetro de _função de operação de caminho_ e deixar o **FastAPI** cuidar do resto para você, assim como ao usar o objeto `Request` diretamente. +Usando apenas `BackgroundTasks` (e não `BackgroundTask`), é possível usá-la como um parâmetro de *função de operação de rota* e deixar o **FastAPI** cuidar do resto para você, assim como ao usar o objeto `Request` diretamente. -Ainda é possível usar `BackgroundTask` sozinho no FastAPI, mas você deve criar o objeto em seu código e retornar uma Starlette `Response` incluindo-o. +Ainda é possível usar `BackgroundTask` sozinho no FastAPI, mas você precisa criar o objeto no seu código e retornar uma `Response` da Starlette incluindo-o. -Você pode ver mais detalhes na documentação oficiais da Starlette para tarefas em segundo plano . +Você pode ver mais detalhes na documentação oficial da Starlette para tarefas em segundo plano. -## Ressalva +## Ressalva { #caveat } -Se você precisa realizar cálculos pesados ​​em segundo plano e não necessariamente precisa que seja executado pelo mesmo processo (por exemplo, você não precisa compartilhar memória, variáveis, etc), você pode se beneficiar do uso de outras ferramentas maiores, como Celery . +Se você precisar realizar computação pesada em segundo plano e não necessariamente precisar que seja executada pelo mesmo processo (por exemplo, você não precisa compartilhar memória, variáveis, etc.), pode se beneficiar do uso de outras ferramentas maiores, como o Celery. -Eles tendem a exigir configurações mais complexas, um gerenciador de fila de mensagens/tarefas, como RabbitMQ ou Redis, mas permitem que você execute tarefas em segundo plano em vários processos e, especialmente, em vários servidores. +Elas tendem a exigir configurações mais complexas, um gerenciador de fila de mensagens/tarefas, como RabbitMQ ou Redis, mas permitem executar tarefas em segundo plano em vários processos e, especialmente, em vários servidores. -Mas se você precisa acessar variáveis ​​e objetos do mesmo aplicativo **FastAPI**, ou precisa realizar pequenas tarefas em segundo plano (como enviar uma notificação por e-mail), você pode simplesmente usar `BackgroundTasks`. +Mas se você precisa acessar variáveis e objetos da mesma aplicação **FastAPI**, ou precisa realizar pequenas tarefas em segundo plano (como enviar uma notificação por e-mail), você pode simplesmente usar `BackgroundTasks`. -## Recapitulando +## Recapitulando { #recap } -Importe e use `BackgroundTasks` com parâmetros em _funções de operação de caminho_ e dependências para adicionar tarefas em segundo plano. +Importe e use `BackgroundTasks` com parâmetros em *funções de operação de rota* e dependências para adicionar tarefas em segundo plano. diff --git a/docs/pt/docs/tutorial/bigger-applications.md b/docs/pt/docs/tutorial/bigger-applications.md index b621f3c726..9dec7b1968 100644 --- a/docs/pt/docs/tutorial/bigger-applications.md +++ b/docs/pt/docs/tutorial/bigger-applications.md @@ -1,4 +1,4 @@ -# Aplicações Maiores - Múltiplos Arquivos +# Aplicações Maiores - Múltiplos Arquivos { #bigger-applications-multiple-files } Se você está construindo uma aplicação ou uma API web, é raro que você possa colocar tudo em um único arquivo. @@ -10,7 +10,7 @@ Se você vem do Flask, isso seria o equivalente aos Blueprints do Flask. /// -## Um exemplo de estrutura de arquivos +## Um exemplo de estrutura de arquivos { #an-example-file-structure } Digamos que você tenha uma estrutura de arquivos como esta: @@ -71,7 +71,7 @@ A mesma estrutura de arquivos com comentários: │   └── admin.py # "admin" submódulo, e.g. import app.internal.admin ``` -## `APIRouter` +## `APIRouter` { #apirouter } Vamos supor que o arquivo dedicado a lidar apenas com usuários seja o submódulo em `/app/routers/users.py`. @@ -81,23 +81,19 @@ Mas ele ainda faz parte da mesma aplicação/web API **FastAPI** (faz parte do m Você pode criar as *operações de rotas* para esse módulo usando o `APIRouter`. -### Importar `APIRouter` +### Importe `APIRouter` { #import-apirouter } você o importa e cria uma "instância" da mesma maneira que faria com a classe `FastAPI`: -```Python hl_lines="1 3" title="app/routers/users.py" -{!../../docs_src/bigger_applications/app/routers/users.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/routers/users.py hl[1,3] title["app/routers/users.py"] *} -### *Operações de Rota* com `APIRouter` +### *Operações de Rota* com `APIRouter` { #path-operations-with-apirouter } E então você o utiliza para declarar suas *operações de rota*. Utilize-o da mesma maneira que utilizaria a classe `FastAPI`: -```Python hl_lines="6 11 16" title="app/routers/users.py" -{!../../docs_src/bigger_applications/app/routers/users.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/routers/users.py hl[6,11,16] title["app/routers/users.py"] *} Você pode pensar em `APIRouter` como uma classe "mini `FastAPI`". @@ -113,7 +109,7 @@ Neste exemplo, a variável é chamada de `router`, mas você pode nomeá-la como Vamos incluir este `APIRouter` na aplicação principal `FastAPI`, mas primeiro, vamos verificar as dependências e outro `APIRouter`. -## Dependências +## Dependências { #dependencies } Vemos que precisaremos de algumas dependências usadas em vários lugares da aplicação. @@ -121,35 +117,7 @@ Então, as colocamos em seu próprio módulo de `dependencies` (`app/dependencie Agora usaremos uma dependência simples para ler um cabeçalho `X-Token` personalizado: -//// tab | Python 3.9+ - -```Python hl_lines="3 6-8" title="app/dependencies.py" -{!> ../../docs_src/bigger_applications/app_an_py39/dependencies.py!} -``` - -//// - -//// tab | Python 3.8+ - -```Python hl_lines="1 5-7" title="app/dependencies.py" -{!> ../../docs_src/bigger_applications/app_an/dependencies.py!} -``` - -//// - -//// tab | Python 3.8+ non-Annotated - -/// tip | Dica - -Prefira usar a versão `Annotated` se possível. - -/// - -```Python hl_lines="1 4-6" title="app/dependencies.py" -{!> ../../docs_src/bigger_applications/app/dependencies.py!} -``` - -//// +{* ../../docs_src/bigger_applications/app_an_py39/dependencies.py hl[3,6:8] title["app/dependencies.py"] *} /// tip | Dica @@ -159,7 +127,7 @@ Mas em casos reais, você obterá melhores resultados usando os [Utilitários de /// -## Outro módulo com `APIRouter` +## Outro módulo com `APIRouter` { #another-module-with-apirouter } Digamos que você também tenha os endpoints dedicados a manipular "itens" do seu aplicativo no módulo em `app/routers/items.py`. @@ -177,13 +145,11 @@ Sabemos que todas as *operações de rota* neste módulo têm o mesmo: * Path `prefix`: `/items`. * `tags`: (apenas uma tag: `items`). * Extra `responses`. -* `dependências`: todas elas precisam da dependência `X-Token` que criamos. +* `dependencies`: todas elas precisam da dependência `X-Token` que criamos. Então, em vez de adicionar tudo isso a cada *operação de rota*, podemos adicioná-lo ao `APIRouter`. -```Python hl_lines="5-10 16 21" title="app/routers/items.py" -{!../../docs_src/bigger_applications/app/routers/items.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/routers/items.py hl[5:10,16,21] title["app/routers/items.py"] *} Como o caminho de cada *operação de rota* deve começar com `/`, como em: @@ -199,7 +165,7 @@ Então, o prefixo neste caso é `/items`. Também podemos adicionar uma lista de `tags` e `responses` extras que serão aplicadas a todas as *operações de rota* incluídas neste roteador. -E podemos adicionar uma lista de `dependencies` que serão adicionadas a todas as *operações de rota* no roteador e serão executadas/resolvidas para cada solicitação feita a elas. +E podemos adicionar uma lista de `dependencies` que serão adicionadas a todas as *operações de rota* no roteador e serão executadas/resolvidas para cada request feita a elas. /// tip | Dica @@ -224,17 +190,17 @@ O resultado final é que os caminhos dos itens agora são: /// tip | Dica -Ter `dependências` no `APIRouter` pode ser usado, por exemplo, para exigir autenticação para um grupo inteiro de *operações de rota*. Mesmo que as dependências não sejam adicionadas individualmente a cada uma delas. +Ter `dependencies` no `APIRouter` pode ser usado, por exemplo, para exigir autenticação para um grupo inteiro de *operações de rota*. Mesmo que as dependências não sejam adicionadas individualmente a cada uma delas. /// -/// check +/// check | Verifique Os parâmetros `prefix`, `tags`, `responses` e `dependencies` são (como em muitos outros casos) apenas um recurso do **FastAPI** para ajudar a evitar duplicação de código. /// -### Importar as dependências +### Importe as dependências { #import-the-dependencies } Este código reside no módulo `app.routers.items`, o arquivo `app/routers/items.py`. @@ -242,11 +208,9 @@ E precisamos obter a função de dependência do módulo `app.dependencies`, o a Então usamos uma importação relativa com `..` para as dependências: -```Python hl_lines="3" title="app/routers/items.py" -{!../../docs_src/bigger_applications/app/routers/items.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/routers/items.py hl[3] title["app/routers/items.py"] *} -#### Como funcionam as importações relativas +#### Como funcionam as importações relativas { #how-relative-imports-work } /// tip | Dica @@ -309,15 +273,13 @@ Isso se referiria a algum pacote acima de `app/`, com seu próprio arquivo `__in Mas agora você sabe como funciona, então você pode usar importações relativas em seus próprios aplicativos, não importa o quão complexos eles sejam. 🤓 -### Adicione algumas `tags`, `respostas` e `dependências` personalizadas +### Adicione algumas `tags`, `responses` e `dependencies` personalizadas { #add-some-custom-tags-responses-and-dependencies } Não estamos adicionando o prefixo `/items` nem `tags=["items"]` a cada *operação de rota* porque os adicionamos ao `APIRouter`. -Mas ainda podemos adicionar _mais_ `tags` que serão aplicadas a uma *operação de rota* específica, e também algumas `respostas` extras específicas para essa *operação de rota*: +Mas ainda podemos adicionar _mais_ `tags` que serão aplicadas a uma *operação de rota* específica, e também algumas `responses` extras específicas para essa *operação de rota*: -```Python hl_lines="30-31" title="app/routers/items.py" -{!../../docs_src/bigger_applications/app/routers/items.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/routers/items.py hl[30:31] title["app/routers/items.py"] *} /// tip | Dica @@ -327,7 +289,7 @@ E também terá ambas as respostas na documentação, uma para `404` e uma para /// -## O principal `FastAPI` +## O principal `FastAPI` { #the-main-fastapi } Agora, vamos ver o módulo em `app/main.py`. @@ -337,27 +299,23 @@ Este será o arquivo principal em seu aplicativo que une tudo. E como a maior parte de sua lógica agora viverá em seu próprio módulo específico, o arquivo principal será bem simples. -### Importar `FastAPI` +### Importe o `FastAPI` { #import-fastapi } Você importa e cria uma classe `FastAPI` normalmente. E podemos até declarar [dependências globais](dependencies/global-dependencies.md){.internal-link target=_blank} que serão combinadas com as dependências para cada `APIRouter`: -```Python hl_lines="1 3 7" title="app/main.py" -{!../../docs_src/bigger_applications/app/main.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[1,3,7] title["app/main.py"] *} -### Importe o `APIRouter` +### Importe o `APIRouter` { #import-the-apirouter } Agora importamos os outros submódulos que possuem `APIRouter`s: -```Python hl_lines="4-5" title="app/main.py" -{!../../docs_src/bigger_applications/app/main.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[4:5] title["app/main.py"] *} Como os arquivos `app/routers/users.py` e `app/routers/items.py` são submódulos que fazem parte do mesmo pacote Python `app`, podemos usar um único ponto `.` para importá-los usando "importações relativas". -### Como funciona a importação +### Como funciona a importação { #how-the-importing-works } A seção: @@ -399,7 +357,7 @@ Para saber mais sobre pacotes e módulos Python, leia ```console -$ uvicorn app.main:app --reload +$ fastapi dev app/main.py INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) ``` @@ -537,7 +485,7 @@ Você verá a documentação automática da API, incluindo os caminhos de todos -## Incluir o mesmo roteador várias vezes com `prefixos` diferentes +## Inclua o mesmo roteador várias vezes com `prefix` diferentes { #include-the-same-router-multiple-times-with-different-prefix } Você também pode usar `.include_router()` várias vezes com o *mesmo* roteador usando prefixos diferentes. @@ -545,7 +493,7 @@ Isso pode ser útil, por exemplo, para expor a mesma API sob prefixos diferentes Esse é um uso avançado que você pode não precisar, mas está lá caso precise. -## Incluir um `APIRouter` em outro +## Inclua um `APIRouter` em outro { #include-an-apirouter-in-another } Da mesma forma que você pode incluir um `APIRouter` em um aplicativo `FastAPI`, você pode incluir um `APIRouter` em outro `APIRouter` usando: diff --git a/docs/pt/docs/tutorial/body-fields.md b/docs/pt/docs/tutorial/body-fields.md index e7dfb07f2a..25e11189e7 100644 --- a/docs/pt/docs/tutorial/body-fields.md +++ b/docs/pt/docs/tutorial/body-fields.md @@ -1,28 +1,28 @@ -# Corpo - Campos +# Corpo - Campos { #body-fields } -Da mesma forma que você pode declarar validações adicionais e metadados nos parâmetros de *funções de operações de rota* com `Query`, `Path` e `Body`, você pode declarar validações e metadados dentro de modelos do Pydantic usando `Field` do Pydantic. +Da mesma forma que você pode declarar validações adicionais e metadados nos parâmetros de uma *função de operação de rota* com `Query`, `Path` e `Body`, você pode declarar validações e metadados dentro de modelos do Pydantic usando `Field` do Pydantic. -## Importe `Field` +## Importe `Field` { #import-field } Primeiro, você tem que importá-lo: -{* ../../docs_src/body_fields/tutorial001.py hl[4] *} +{* ../../docs_src/body_fields/tutorial001_an_py310.py hl[4] *} -/// warning | Aviso +/// warning | Atenção Note que `Field` é importado diretamente do `pydantic`, não do `fastapi` como todo o resto (`Query`, `Path`, `Body`, etc). /// -## Declare atributos do modelo +## Declare atributos do modelo { #declare-model-attributes } Você pode então utilizar `Field` com atributos do modelo: -{* ../../docs_src/body_fields/tutorial001.py hl[11:14] *} +{* ../../docs_src/body_fields/tutorial001_an_py310.py hl[11:14] *} `Field` funciona da mesma forma que `Query`, `Path` e `Body`, ele possui todos os mesmos parâmetros, etc. -/// note | Detalhes técnicos +/// note | Detalhes Técnicos Na realidade, `Query`, `Path` e outros que você verá em seguida, criam objetos de subclasses de uma classe `Param` comum, que é ela mesma uma subclasse da classe `FieldInfo` do Pydantic. @@ -40,13 +40,20 @@ Note como cada atributo do modelo com um tipo, valor padrão e `Field` possuem a /// -## Adicione informações extras +## Adicione informações extras { #add-extra-information } Você pode declarar informação extra em `Field`, `Query`, `Body`, etc. E isso será incluído no JSON Schema gerado. Você irá aprender mais sobre adicionar informações extras posteriormente nessa documentação, quando estiver aprendendo a declarar exemplos. -## Recapitulando +/// warning | Atenção + +Chaves extras passadas para `Field` também estarão presentes no schema OpenAPI resultante da sua aplicação. +Como essas chaves podem não fazer necessariamente parte da especificação OpenAPI, algumas ferramentas de OpenAPI, por exemplo [o validador do OpenAPI](https://validator.swagger.io/), podem não funcionar com o schema gerado. + +/// + +## Recapitulando { #recap } Você pode usar `Field` do Pydantic para declarar validações extras e metadados para atributos do modelo. diff --git a/docs/pt/docs/tutorial/body-multiple-params.md b/docs/pt/docs/tutorial/body-multiple-params.md index eda9b4dfff..3cba04912c 100644 --- a/docs/pt/docs/tutorial/body-multiple-params.md +++ b/docs/pt/docs/tutorial/body-multiple-params.md @@ -1,14 +1,14 @@ -# Corpo - Múltiplos parâmetros +# Corpo - Múltiplos parâmetros { #body-multiple-parameters } Agora que nós vimos como usar `Path` e `Query`, veremos usos mais avançados de declarações no corpo da requisição. -## Misture `Path`, `Query` e parâmetros de corpo +## Misture `Path`, `Query` e parâmetros de corpo { #mix-path-query-and-body-parameters } Primeiro, é claro, você pode misturar `Path`, `Query` e declarações de parâmetro no corpo da requisição livremente e o **FastAPI** saberá o que fazer. E você também pode declarar parâmetros de corpo como opcionais, definindo o valor padrão com `None`: -{* ../../docs_src/body_multiple_params/tutorial001_py310.py hl[17:19] *} +{* ../../docs_src/body_multiple_params/tutorial001_an_py310.py hl[18:20] *} /// note | Nota @@ -16,7 +16,7 @@ Repare que, neste caso, o `item` que seria capturado a partir do corpo é opcion /// -## Múltiplos parâmetros de corpo +## Múltiplos parâmetros de corpo { #multiple-body-parameters } No exemplo anterior, as *operações de rota* esperariam um JSON no corpo contendo os atributos de um `Item`, exemplo: @@ -62,7 +62,7 @@ O **FastAPI** fará a conversão automática a partir da requisição, assim ess Ele executará a validação dos dados compostos e irá documentá-los de maneira compatível com o esquema OpenAPI e documentação automática. -## Valores singulares no corpo +## Valores singulares no corpo { #singular-values-in-body } Assim como existem uma `Query` e uma `Path` para definir dados adicionais para parâmetros de consulta e de rota, o **FastAPI** provê o equivalente para `Body`. @@ -72,7 +72,7 @@ Se você declará-lo como é, porque é um valor singular, o **FastAPI** assumir Mas você pode instruir o **FastAPI** para tratá-lo como outra chave do corpo usando `Body`: -{* ../../docs_src/body_multiple_params/tutorial003.py hl[22] *} +{* ../../docs_src/body_multiple_params/tutorial003_an_py310.py hl[23] *} Neste caso, o **FastAPI** esperará um corpo como: @@ -94,7 +94,7 @@ Neste caso, o **FastAPI** esperará um corpo como: Mais uma vez, ele converterá os tipos de dados, validar, documentar, etc. -## Múltiplos parâmetros de corpo e consulta +## Múltiplos parâmetros de corpo e consulta { #multiple-body-params-and-query } Obviamente, você também pode declarar parâmetros de consulta assim que você precisar, de modo adicional a quaisquer parâmetros de corpo. @@ -112,7 +112,7 @@ q: str | None = None Por exemplo: -{* ../../docs_src/body_multiple_params/tutorial004_py310.py hl[26] *} +{* ../../docs_src/body_multiple_params/tutorial004_an_py310.py hl[28] *} /// info | Informação @@ -120,7 +120,7 @@ Por exemplo: /// -## Declare um único parâmetro de corpo indicando sua chave +## Declare um único parâmetro de corpo indicando sua chave { #embed-a-single-body-parameter } Suponha que você tem um único parâmetro de corpo `item`, a partir de um modelo Pydantic `Item`. @@ -134,7 +134,7 @@ item: Item = Body(embed=True) como em: -{* ../../docs_src/body_multiple_params/tutorial005_py310.py hl[15] *} +{* ../../docs_src/body_multiple_params/tutorial005_an_py310.py hl[17] *} Neste caso o **FastAPI** esperará um corpo como: @@ -160,7 +160,7 @@ ao invés de: } ``` -## Recapitulando +## Recapitulando { #recap } Você pode adicionar múltiplos parâmetros de corpo para sua *função de operação de rota*, mesmo que a requisição possa ter somente um único corpo. diff --git a/docs/pt/docs/tutorial/body-nested-models.md b/docs/pt/docs/tutorial/body-nested-models.md index 2954ae3db3..f2bec19a2f 100644 --- a/docs/pt/docs/tutorial/body-nested-models.md +++ b/docs/pt/docs/tutorial/body-nested-models.md @@ -1,36 +1,26 @@ -# Corpo - Modelos aninhados +# Corpo - Modelos aninhados { #body-nested-models } -Com o **FastAPI**, você pode definir, validar, documentar e usar modelos profundamente aninhados de forma arbitrária (graças ao Pydantic). +Com o **FastAPI**, você pode definir, validar, documentar e usar modelos arbitrariamente e profundamente aninhados (graças ao Pydantic). -## Campos do tipo Lista +## Campos do tipo Lista { #list-fields } Você pode definir um atributo como um subtipo. Por exemplo, uma `list` do Python: -{* ../../docs_src/body_nested_models/tutorial001.py hl[14] *} +{* ../../docs_src/body_nested_models/tutorial001_py310.py hl[12] *} Isso fará com que tags seja uma lista de itens mesmo sem declarar o tipo dos elementos desta lista. -## Campos do tipo Lista com um parâmetro de tipo +## Campos do tipo Lista com um parâmetro de tipo { #list-fields-with-type-parameter } Mas o Python tem uma maneira específica de declarar listas com tipos internos ou "parâmetros de tipo": -### Importe `List` do typing +### Declare uma `list` com um parâmetro de tipo { #declare-a-list-with-a-type-parameter } -Primeiramente, importe `List` do módulo `typing` que já vem por padrão no Python: - -{* ../../docs_src/body_nested_models/tutorial002.py hl[1] *} - -### Declare a `List` com um parâmetro de tipo - -Para declarar tipos que têm parâmetros de tipo(tipos internos), como `list`, `dict`, `tuple`: - -* Importe os do modulo `typing` -* Passe o(s) tipo(s) interno(s) como "parâmetros de tipo" usando colchetes: `[` e `]` +Para declarar tipos que têm parâmetros de tipo (tipos internos), como `list`, `dict`, `tuple`, +passe o(s) tipo(s) interno(s) como "parâmetros de tipo" usando colchetes: `[` e `]` ```Python -from typing import List - -my_list: List[str] +my_list: list[str] ``` Essa é a sintaxe padrão do Python para declarações de tipo. @@ -39,20 +29,17 @@ Use a mesma sintaxe padrão para atributos de modelo com tipos internos. Portanto, em nosso exemplo, podemos fazer com que `tags` sejam especificamente uma "lista de strings": +{* ../../docs_src/body_nested_models/tutorial002_py310.py hl[12] *} -{* ../../docs_src/body_nested_models/tutorial002.py hl[14] *} - -## Tipo "set" - +## Tipos "set" { #set-types } Mas então, quando nós pensamos mais, percebemos que as tags não devem se repetir, elas provavelmente devem ser strings únicas. E que o Python tem um tipo de dados especial para conjuntos de itens únicos, o `set`. -Então podemos importar `Set` e declarar `tags` como um `set` de `str`s: +Então podemos declarar `tags` como um conjunto de strings: - -{* ../../docs_src/body_nested_models/tutorial003.py hl[1,14] *} +{* ../../docs_src/body_nested_models/tutorial003_py310.py hl[12] *} Com isso, mesmo que você receba uma requisição contendo dados duplicados, ela será convertida em um conjunto de itens exclusivos. @@ -60,7 +47,7 @@ E sempre que você enviar esses dados como resposta, mesmo se a fonte tiver dupl E também teremos anotações/documentação em conformidade. -## Modelos aninhados +## Modelos aninhados { #nested-models } Cada atributo de um modelo Pydantic tem um tipo. @@ -70,17 +57,17 @@ Portanto, você pode declarar "objects" JSON profundamente aninhados com nomes, Tudo isso, aninhado arbitrariamente. -### Defina um sub-modelo +### Defina um sub-modelo { #define-a-submodel } Por exemplo, nós podemos definir um modelo `Image`: -{* ../../docs_src/body_nested_models/tutorial004.py hl[9:11] *} +{* ../../docs_src/body_nested_models/tutorial004_py310.py hl[7:9] *} -### Use o sub-modelo como um tipo +### Use o sub-modelo como um tipo { #use-the-submodel-as-a-type } E então podemos usa-lo como o tipo de um atributo: -{* ../../docs_src/body_nested_models/tutorial004.py hl[20] *} +{* ../../docs_src/body_nested_models/tutorial004_py310.py hl[18] *} Isso significa que o **FastAPI** vai esperar um corpo similar à: @@ -100,28 +87,28 @@ Isso significa que o **FastAPI** vai esperar um corpo similar à: Novamente, apenas fazendo essa declaração, com o **FastAPI**, você ganha: -* Suporte do editor de texto (compleção, etc), inclusive para modelos aninhados +* Suporte do editor (preenchimento automático, etc.), inclusive para modelos aninhados * Conversão de dados * Validação de dados * Documentação automatica -## Tipos especiais e validação +## Tipos especiais e validação { #special-types-and-validation } Além dos tipos singulares normais como `str`, `int`, `float`, etc. Você também pode usar tipos singulares mais complexos que herdam de `str`. -Para ver todas as opções possíveis, cheque a documentação para ostipos exoticos do Pydantic. Você verá alguns exemplos no próximo capitulo. +Para ver todas as opções possíveis, consulte a Visão geral dos tipos do Pydantic. Você verá alguns exemplos no próximo capítulo. Por exemplo, no modelo `Image` nós temos um campo `url`, nós podemos declara-lo como um `HttpUrl` do Pydantic invés de como uma `str`: -{* ../../docs_src/body_nested_models/tutorial005.py hl[4,10] *} +{* ../../docs_src/body_nested_models/tutorial005_py310.py hl[2,8] *} -A string será verificada para se tornar uma URL válida e documentada no esquema JSON/1OpenAPI como tal. +A string será verificada para se tornar uma URL válida e documentada no JSON Schema / OpenAPI como tal. -## Atributos como listas de submodelos +## Atributos como listas de submodelos { #attributes-with-lists-of-submodels } Você também pode usar modelos Pydantic como subtipos de `list`, `set`, etc: -{* ../../docs_src/body_nested_models/tutorial006.py hl[20] *} +{* ../../docs_src/body_nested_models/tutorial006_py310.py hl[18] *} Isso vai esperar(converter, validar, documentar, etc) um corpo JSON tal qual: @@ -149,38 +136,37 @@ Isso vai esperar(converter, validar, documentar, etc) um corpo JSON tal qual: } ``` -/// info | informação +/// info | Informação -Note como o campo `images` agora tem uma lista de objetos de image. +Observe como a chave `images` agora tem uma lista de objetos de imagem. /// -## Modelos profundamente aninhados +## Modelos profundamente aninhados { #deeply-nested-models } Você pode definir modelos profundamente aninhados de forma arbitrária: -{* ../../docs_src/body_nested_models/tutorial007.py hl[9,14,20,23,27] *} +{* ../../docs_src/body_nested_models/tutorial007_py310.py hl[7,12,18,21,25] *} -/// info | informação +/// info | Informação -Note como `Offer` tem uma lista de `Item`s, que por sua vez possui opcionalmente uma lista `Image`s +Observe como `Offer` tem uma lista de `Item`s, que por sua vez têm uma lista opcional de `Image`s /// -## Corpos de listas puras +## Corpos de listas puras { #bodies-of-pure-lists } Se o valor de primeiro nível do corpo JSON que você espera for um `array` do JSON (uma` lista` do Python), você pode declarar o tipo no parâmetro da função, da mesma forma que nos modelos do Pydantic: - ```Python -images: List[Image] +images: list[Image] ``` como em: -{* ../../docs_src/body_nested_models/tutorial008.py hl[15] *} +{* ../../docs_src/body_nested_models/tutorial008_py39.py hl[13] *} -## Suporte de editor em todo canto +## Suporte de editor em todo canto { #editor-support-everywhere } E você obtém suporte do editor em todos os lugares. @@ -192,7 +178,7 @@ Você não conseguiria este tipo de suporte de editor se estivesse trabalhando d Mas você também não precisa se preocupar com eles, os dicts de entrada são convertidos automaticamente e sua saída é convertida automaticamente para JSON também. -## Corpos de `dict`s arbitrários +## Corpos de `dict`s arbitrários { #bodies-of-arbitrary-dicts } Você também pode declarar um corpo como um `dict` com chaves de algum tipo e valores de outro tipo. @@ -208,7 +194,7 @@ Outro caso útil é quando você deseja ter chaves de outro tipo, por exemplo, ` Neste caso, você aceitaria qualquer `dict`, desde que tenha chaves` int` com valores `float`: -{* ../../docs_src/body_nested_models/tutorial009.py hl[9] *} +{* ../../docs_src/body_nested_models/tutorial009_py39.py hl[7] *} /// tip | Dica @@ -222,14 +208,14 @@ E o `dict` que você recebe como `weights` terá, na verdade, chaves `int` e val /// -## Recapitulação +## Recapitulação { #recap } Com **FastAPI** você tem a flexibilidade máxima fornecida pelos modelos Pydantic, enquanto seu código é mantido simples, curto e elegante. Mas com todos os benefícios: -* Suporte do editor (compleção em todo canto!) -* Conversão de dados (leia-se parsing/serialização) +* Suporte do editor (preenchimento automático em todo canto!) +* Conversão de dados (parsing/serialização) * Validação de dados * Documentação dos esquemas * Documentação automática diff --git a/docs/pt/docs/tutorial/body-updates.md b/docs/pt/docs/tutorial/body-updates.md index bf38aeb9e9..67bf684925 100644 --- a/docs/pt/docs/tutorial/body-updates.md +++ b/docs/pt/docs/tutorial/body-updates.md @@ -1,6 +1,6 @@ -# Corpo - Atualizações +# Corpo - Atualizações { #body-updates } -## Atualização de dados existentes com `PUT` +## Atualização de dados existentes com `PUT` { #update-replacing-with-put } Para atualizar um item, você pode usar a operação HTTP `PUT`. @@ -10,7 +10,7 @@ Você pode usar `jsonable_encoder` para converter os dados de entrada em dados q `PUT` é usado para receber dados que devem substituir os dados existentes. -### Aviso sobre a substituição +### Aviso sobre a substituição { #warning-about-replacing } Isso significa que, se você quiser atualizar o item `bar` usando `PUT` com um corpo contendo: @@ -26,9 +26,9 @@ Como ele não inclui o atributo já armazenado `"tax": 20.2`, o modelo de entrad E os dados seriam salvos com esse "novo" `tax` de `10.5`. -## Atualizações parciais com `PATCH` +## Atualizações parciais com `PATCH` { #partial-updates-with-patch } -Você também pode usar a operação HTTP `PATCH` para *atualizar* parcialmente os dados. +Você também pode usar a operação HTTP `PATCH` para atualizar parcialmente os dados. Isso significa que você pode enviar apenas os dados que deseja atualizar, deixando o restante intacto. @@ -44,7 +44,7 @@ Mas este guia te dá uma ideia de como eles são destinados a serem usados. /// -### Usando o parâmetro `exclude_unset` do Pydantic +### Usando o parâmetro `exclude_unset` do Pydantic { #using-pydantics-exclude-unset-parameter } Se você quiser receber atualizações parciais, é muito útil usar o parâmetro `exclude_unset` no método `.model_dump()` do modelo do Pydantic. @@ -52,7 +52,7 @@ Como `item.model_dump(exclude_unset=True)`. /// info | Informação -No Pydantic v1, o método que era chamado `.dict()` e foi depreciado (mas ainda suportado) no Pydantic v2. Agora, deve-se usar o método `.model_dump()`. +No Pydantic v1, o método que era chamado `.dict()` e foi descontinuado (mas ainda suportado) no Pydantic v2. Agora, deve-se usar o método `.model_dump()`. Os exemplos aqui usam `.dict()` para compatibilidade com o Pydantic v1, mas você deve usar `.model_dump()` a partir do Pydantic v2. @@ -64,13 +64,13 @@ Então, você pode usar isso para gerar um `dict` com apenas os dados definidos {* ../../docs_src/body_updates/tutorial002_py310.py hl[32] *} -### Usando o parâmetro `update` do Pydantic +### Usando o parâmetro `update` do Pydantic { #using-pydantics-update-parameter } Agora, você pode criar uma cópia do modelo existente usando `.model_copy()`, e passar o parâmetro `update` com um `dict` contendo os dados para atualizar. /// info | Informação -No Pydantic v1, o método era chamado `.copy()`, ele foi depreciado (mas ainda suportado) no Pydantic v2, e renomeado para `.model_copy()`. +No Pydantic v1, o método era chamado `.copy()`, ele foi descontinuado (mas ainda suportado) no Pydantic v2, e renomeado para `.model_copy()`. Os exemplos aqui usam `.copy()` para compatibilidade com o Pydantic v1, mas você deve usar `.model_copy()` com o Pydantic v2. @@ -80,7 +80,7 @@ Como `stored_item_model.model_copy(update=update_data)`: {* ../../docs_src/body_updates/tutorial002_py310.py hl[33] *} -### Recapitulando as atualizações parciais +### Recapitulando as atualizações parciais { #partial-updates-recap } Resumindo, para aplicar atualizações parciais você pode: diff --git a/docs/pt/docs/tutorial/body.md b/docs/pt/docs/tutorial/body.md index 2508d7981b..1330f4458f 100644 --- a/docs/pt/docs/tutorial/body.md +++ b/docs/pt/docs/tutorial/body.md @@ -1,16 +1,16 @@ -# Corpo da Requisição +# Corpo da requisição { #request-body } -Quando você precisa enviar dados de um cliente (como de um navegador web) para sua API, você o envia como um **corpo da requisição**. +Quando você precisa enviar dados de um cliente (como de um navegador) para sua API, você os envia como um **corpo da requisição**. O corpo da **requisição** é a informação enviada pelo cliente para sua API. O corpo da **resposta** é a informação que sua API envia para o cliente. -Sua API quase sempre irá enviar um corpo na **resposta**. Mas os clientes não necessariamente precisam enviar um corpo em toda **requisição**. +Sua API quase sempre precisa enviar um corpo na **resposta**. Mas os clientes não necessariamente precisam enviar **corpos de requisição** o tempo todo, às vezes eles apenas requisitam um path, talvez com alguns parâmetros de consulta, mas não enviam um corpo. Para declarar um corpo da **requisição**, você utiliza os modelos do Pydantic com todos os seus poderes e benefícios. /// info | Informação -Para enviar dados, você deve usar utilizar um dos métodos: `POST` (Mais comum), `PUT`, `DELETE` ou `PATCH`. +Para enviar dados, você deve usar um dos: `POST` (o mais comum), `PUT`, `DELETE` ou `PATCH`. Enviar um corpo em uma requisição `GET` não tem um comportamento definido nas especificações, porém é suportado pelo FastAPI, apenas para casos de uso bem complexos/extremos. @@ -18,19 +18,19 @@ Como é desencorajado, a documentação interativa com Swagger UI não irá most /// -## Importe o `BaseModel` do Pydantic +## Importe o `BaseModel` do Pydantic { #import-pydantics-basemodel } Primeiro, você precisa importar `BaseModel` do `pydantic`: -{* ../../docs_src/body/tutorial001.py hl[4] *} +{* ../../docs_src/body/tutorial001_py310.py hl[2] *} -## Crie seu modelo de dados +## Crie seu modelo de dados { #create-your-data-model } Então você declara seu modelo de dados como uma classe que herda `BaseModel`. Utilize os tipos Python padrão para todos os atributos: -{* ../../docs_src/body/tutorial001.py hl[7:11] *} +{* ../../docs_src/body/tutorial001_py310.py hl[5:9] *} Assim como quando declaramos parâmetros de consulta, quando um atributo do modelo possui um valor padrão, ele se torna opcional. Caso contrário, se torna obrigatório. Use `None` para torná-lo opcional. @@ -39,13 +39,13 @@ Por exemplo, o modelo acima declara um JSON "`object`" (ou `dict` no Python) com ```JSON { "name": "Foo", - "description": "Uma descrição opcional", + "description": "An optional description", "price": 45.2, "tax": 3.5 } ``` -...como `description` e `tax` são opcionais (Com um valor padrão de `None`), esse JSON "`object`" também é válido: +...como `description` e `tax` são opcionais (com um valor padrão de `None`), esse JSON "`object`" também é válido: ```JSON { @@ -54,40 +54,40 @@ Por exemplo, o modelo acima declara um JSON "`object`" (ou `dict` no Python) com } ``` -## Declare como um parâmetro +## Declare como um parâmetro { #declare-it-as-a-parameter } -Para adicionar o corpo na *função de operação de rota*, declare-o da mesma maneira que você declarou parâmetros de rota e consulta: +Para adicioná-lo à sua *operação de rota*, declare-o da mesma maneira que você declarou parâmetros de rota e de consulta: -{* ../../docs_src/body/tutorial001.py hl[18] *} +{* ../../docs_src/body/tutorial001_py310.py hl[16] *} -...E declare o tipo como o modelo que você criou, `Item`. +...e declare o seu tipo como o modelo que você criou, `Item`. -## Resultados +## Resultados { #results } -Apenas com esse declaração de tipos do Python, o **FastAPI** irá: +Apenas com essa declaração de tipos do Python, o **FastAPI** irá: * Ler o corpo da requisição como um JSON. * Converter os tipos correspondentes (se necessário). * Validar os dados. - * Se algum dados for inválido, irá retornar um erro bem claro, indicando exatamente onde e o que está incorreto. + * Se algum dado for inválido, irá retornar um erro bem claro, indicando exatamente onde e o que estava incorreto. * Entregar a você a informação recebida no parâmetro `item`. * Como você o declarou na função como do tipo `Item`, você também terá o suporte do editor (completação, etc) para todos os atributos e seus tipos. -* Gerar um Esquema JSON com as definições do seu modelo, você também pode utilizá-lo em qualquer lugar que quiser, se fizer sentido para seu projeto. -* Esses esquemas farão parte do esquema OpenAPI, e utilizados nas UIs de documentação automática. +* Gerar definições de JSON Schema para o seu modelo; você também pode usá-las em qualquer outro lugar se fizer sentido para o seu projeto. +* Esses schemas farão parte do esquema OpenAPI gerado, e serão usados pelas UIs de documentação automática. -## Documentação automática +## Documentação automática { #automatic-docs } -Os esquemas JSON dos seus modelos farão parte do esquema OpenAPI gerado para sua aplicação, e aparecerão na documentação interativa da API: +Os JSON Schemas dos seus modelos farão parte do esquema OpenAPI gerado para sua aplicação, e aparecerão na documentação interativa da API: -E também serão utilizados em cada *função de operação de rota* que utilizá-los: +E também serão utilizados na documentação da API dentro de cada *operação de rota* que precisar deles: -## Suporte do editor de texto: +## Suporte do editor { #editor-support } -No seu editor de texto, dentro da função você receberá dicas de tipos e completação em todo lugar (isso não aconteceria se você recebesse um `dict` em vez de um modelo Pydantic): +No seu editor, dentro da função você receberá dicas de tipos e completação em todo lugar (isso não aconteceria se você recebesse um `dict` em vez de um modelo Pydantic): @@ -111,9 +111,9 @@ Mas você terá o mesmo suporte do editor no PyCharm como editor, você pode utilizar o Plugin do Pydantic para o PyCharm . -Melhora o suporte do editor para seus modelos Pydantic com:: +Melhora o suporte do editor para seus modelos Pydantic com: -* completação automática +* preenchimento automático * verificação de tipos * refatoração * buscas @@ -121,42 +121,52 @@ Melhora o suporte do editor para seus modelos Pydantic com:: /// -## Use o modelo +## Use o modelo { #use-the-model } Dentro da função, você pode acessar todos os atributos do objeto do modelo diretamente: -{* ../../docs_src/body/tutorial002.py hl[21] *} +{* ../../docs_src/body/tutorial002_py310.py *} -## Corpo da requisição + parâmetros de rota +/// info | Informação + +No Pydantic v1 o método se chamava `.dict()`, ele foi descontinuado (mas ainda é suportado) no Pydantic v2, e renomeado para `.model_dump()`. + +Os exemplos aqui usam `.dict()` para compatibilidade com o Pydantic v1, mas você deve usar `.model_dump()` se puder usar o Pydantic v2. + +/// + +## Corpo da requisição + parâmetros de rota { #request-body-path-parameters } Você pode declarar parâmetros de rota e corpo da requisição ao mesmo tempo. -O **FastAPI** irá reconhecer que os parâmetros da função que combinam com parâmetros de rota devem ser **retirados da rota**, e parâmetros da função que são declarados como modelos Pydantic sejam **retirados do corpo da requisição**. +O **FastAPI** irá reconhecer que os parâmetros da função que combinam com parâmetros de rota devem ser **retirados da rota**, e que parâmetros da função que são declarados como modelos Pydantic sejam **retirados do corpo da requisição**. -{* ../../docs_src/body/tutorial003.py hl[17:18] *} +{* ../../docs_src/body/tutorial003_py310.py hl[15:16] *} -## Corpo da requisição + parâmetros de rota + parâmetros de consulta +## Corpo da requisição + parâmetros de rota + parâmetros de consulta { #request-body-path-query-parameters } Você também pode declarar parâmetros de **corpo**, **rota** e **consulta**, ao mesmo tempo. O **FastAPI** irá reconhecer cada um deles e retirar a informação do local correto. -{* ../../docs_src/body/tutorial004.py hl[18] *} +{* ../../docs_src/body/tutorial004_py310.py hl[16] *} Os parâmetros da função serão reconhecidos conforme abaixo: -* Se o parâmetro também é declarado na **rota**, será utilizado como um parâmetro de rota. +* Se o parâmetro também é declarado no **path**, será utilizado como um parâmetro de rota. * Se o parâmetro é de um **tipo único** (como `int`, `float`, `str`, `bool`, etc) será interpretado como um parâmetro de **consulta**. * Se o parâmetro é declarado como um **modelo Pydantic**, será interpretado como o **corpo** da requisição. -/// note | Observação +/// note | Nota O FastAPI saberá que o valor de `q` não é obrigatório por causa do valor padrão `= None`. -O `Union` em `Union[str, None]` não é utilizado pelo FastAPI, mas permite ao seu editor de texto lhe dar um suporte melhor e detectar erros. +O `str | None` (Python 3.10+) ou o `Union` em `Union[str, None]` (Python 3.9+) não é utilizado pelo FastAPI para determinar que o valor não é obrigatório, ele saberá que não é obrigatório porque tem um valor padrão `= None`. + +Mas adicionar as anotações de tipo permitirá ao seu editor oferecer um suporte melhor e detectar erros. /// -## Sem o Pydantic +## Sem o Pydantic { #without-pydantic } -Se você não quer utilizar os modelos Pydantic, você também pode utilizar o parâmetro **Body**. Veja a documentação para [Body - Parâmetros múltiplos: Valores singulares no body](body-multiple-params.md#valores-singulares-no-corpo){.internal-link target=_blank}. +Se você não quer utilizar os modelos Pydantic, você também pode utilizar o parâmetro **Body**. Veja a documentação para [Body - Parâmetros múltiplos: Valores singulares no body](body-multiple-params.md#singular-values-in-body){.internal-link target=_blank}. diff --git a/docs/pt/docs/tutorial/cookie-param-models.md b/docs/pt/docs/tutorial/cookie-param-models.md index 3d46ba44cb..73c94050fb 100644 --- a/docs/pt/docs/tutorial/cookie-param-models.md +++ b/docs/pt/docs/tutorial/cookie-param-models.md @@ -1,4 +1,4 @@ -# Modelos de Parâmetros de Cookie +# Modelos de Parâmetros de Cookie { #cookie-parameter-models } Se você possui um grupo de **cookies** que estão relacionados, você pode criar um **modelo Pydantic** para declará-los. 🍪 @@ -16,7 +16,7 @@ Essa mesma técnica se aplica para `Query`, `Cookie`, e `Header`. 😎 /// -## Cookies com Modelos Pydantic +## Cookies com Modelos Pydantic { #cookies-with-a-pydantic-model } Declare o parâmetro de **cookie** que você precisa em um **modelo Pydantic**, e depois declare o parâmetro como um `Cookie`: @@ -24,9 +24,9 @@ Declare o parâmetro de **cookie** que você precisa em um **modelo Pydantic**, O **FastAPI** irá **extrair** os dados para **cada campo** dos **cookies** recebidos na requisição e lhe fornecer o modelo Pydantic que você definiu. -## Verifique os Documentos +## Verifique a Documentação { #check-the-docs } -Você pode ver os cookies definidos na IU dos documentos em `/docs`: +Você pode ver os cookies definidos na IU da documentação em `/docs`:
@@ -36,29 +36,27 @@ Você pode ver os cookies definidos na IU dos documentos em `/docs`: Tenha em mente que, como os **navegadores lidam com cookies** de maneira especial e por baixo dos panos, eles **não** permitem facilmente que o **JavaScript** lidem com eles. -Se você for na **IU de documentos da API** em `/docs` você poderá ver a **documentação** para cookies das suas *operações de rotas*. +Se você for na **IU da documentação da API** em `/docs` você poderá ver a **documentação** para cookies das suas *operações de rotas*. -Mas mesmo que você **adicionar os dados** e clicar em "Executar", pelo motivo da IU dos documentos trabalharem com **JavaScript**, os cookies não serão enviados, e você verá uma mensagem de **erro** como se você não tivesse escrito nenhum dado. +Mas mesmo que você **adicionar os dados** e clicar em "Executar", pelo motivo da IU da documentação trabalhar com **JavaScript**, os cookies não serão enviados, e você verá uma mensagem de **erro** como se você não tivesse escrito nenhum dado. /// -## Proibir Cookies Adicionais +## Proibir Cookies Adicionais { #forbid-extra-cookies } Em alguns casos especiais (provavelmente não muito comuns), você pode querer **restringir** os cookies que você deseja receber. -Agora a sua API possui o poder de contrar o seu próprio consentimento de cookie. 🤪🍪 +Agora a sua API possui o poder de controlar o seu próprio consentimento de cookie. 🤪🍪 +Você pode utilizar a configuração do modelo Pydantic para `proibir` qualquer campo `extra`: - Você pode utilizar a configuração do modelo Pydantic para `proibir` qualquer campo `extra`. - - -{* ../../docs_src/cookie_param_models/tutorial002_an_py39.py hl[10] *} +{* ../../docs_src/cookie_param_models/tutorial002_an_py310.py hl[10] *} Se o cliente tentar enviar alguns **cookies extras**, eles receberão um retorno de **erro**. Coitados dos banners de cookies com todo o seu esforço para obter o seu consentimento para a API rejeitá-lo. 🍪 -Por exemplo, se o cliente tentar enviar um cookie `santa_tracker` com o valor de `good-list-please`, o cliente receberá uma resposta de **erro** informando que o cookie `santa_tracker` is not allowed: +Por exemplo, se o cliente tentar enviar um cookie `santa_tracker` com o valor de `good-list-please`, o cliente receberá uma resposta de **erro** informando que o `santa_tracker` cookie não é permitido: ```json { @@ -73,6 +71,6 @@ Por exemplo, se o cliente tentar enviar um cookie `santa_tracker` com o valor de } ``` -## Resumo +## Resumo { #summary } Você consegue utilizar **modelos Pydantic** para declarar **cookies** no **FastAPI**. 😎 diff --git a/docs/pt/docs/tutorial/cookie-params.md b/docs/pt/docs/tutorial/cookie-params.md index da85d796ea..5540a67d20 100644 --- a/docs/pt/docs/tutorial/cookie-params.md +++ b/docs/pt/docs/tutorial/cookie-params.md @@ -1,20 +1,19 @@ -# Parâmetros de Cookie +# Parâmetros de Cookie { #cookie-parameters } -Você pode definir parâmetros de Cookie da mesma maneira que define paramêtros com `Query` e `Path`. +Você pode definir parâmetros de Cookie da mesma maneira que define parâmetros com `Query` e `Path`. -## Importe `Cookie` +## Importe `Cookie` { #import-cookie } Primeiro importe `Cookie`: {* ../../docs_src/cookie_params/tutorial001_an_py310.py hl[3] *} -## Declare parâmetros de `Cookie` +## Declare parâmetros de `Cookie` { #declare-cookie-parameters } -Então declare os paramêtros de cookie usando a mesma estrutura que em `Path` e `Query`. +Então declare os parâmetros de cookie usando a mesma estrutura que em `Path` e `Query`. Você pode definir o valor padrão, assim como todas as validações extras ou parâmetros de anotação: - {* ../../docs_src/cookie_params/tutorial001_an_py310.py hl[9] *} /// note | Detalhes Técnicos @@ -31,6 +30,16 @@ Para declarar cookies, você precisa usar `Cookie`, pois caso contrário, os par /// -## Recapitulando +/// info | Informação + +Tenha em mente que, como os **navegadores lidam com cookies** de maneiras especiais e nos bastidores, eles **não** permitem facilmente que o **JavaScript** os acesse. + +Se você for à **interface de documentação da API** em `/docs`, poderá ver a **documentação** de cookies para suas *operações de rota*. + +Mas mesmo que você **preencha os dados** e clique em "Execute", como a interface de documentação funciona com **JavaScript**, os cookies não serão enviados e você verá uma mensagem de **erro** como se você não tivesse escrito nenhum valor. + +/// + +## Recapitulando { #recap } Declare cookies com `Cookie`, usando o mesmo padrão comum que utiliza-se em `Query` e `Path`. diff --git a/docs/pt/docs/tutorial/cors.md b/docs/pt/docs/tutorial/cors.md index 0ab07a3c21..0f99db888e 100644 --- a/docs/pt/docs/tutorial/cors.md +++ b/docs/pt/docs/tutorial/cors.md @@ -1,8 +1,8 @@ -# CORS (Cross-Origin Resource Sharing) +# CORS (Cross-Origin Resource Sharing) { #cors-cross-origin-resource-sharing } CORS ou "Cross-Origin Resource Sharing" refere-se às situações em que um frontend rodando em um navegador possui um código JavaScript que se comunica com um backend, e o backend está em uma "origem" diferente do frontend. -## Origem +## Origem { #origin } Uma origem é a combinação de protocolo (`http`, `https`), domínio (`myapp.com`, `localhost`, `localhost.tiangolo.com`), e porta (`80`, `443`, `8080`). @@ -12,27 +12,27 @@ Então, todos estes são origens diferentes: * `https://localhost` * `http://localhost:8080` -Mesmo se todos estiverem em `localhost`, eles usam diferentes protocolos e portas, portanto, são "origens" diferentes. +Mesmo se todos estiverem em `localhost`, eles usam diferentes protocolos ou portas, portanto, são "origens" diferentes. -## Passos +## Passos { #steps } -Então, digamos que você tenha um frontend rodando no seu navegador em `http://localhost:8080`, e seu JavaScript esteja tentando se comunicar com um backend rodando em http://localhost (como não especificamos uma porta, o navegador assumirá a porta padrão `80`). +Então, digamos que você tenha um frontend rodando no seu navegador em `http://localhost:8080`, e seu JavaScript esteja tentando se comunicar com um backend rodando em `http://localhost` (como não especificamos uma porta, o navegador assumirá a porta padrão `80`). -Portanto, o navegador irá enviar uma requisição HTTP `OPTIONS` ao backend, e se o backend enviar os cabeçalhos apropriados autorizando a comunicação a partir de uma origem diferente (`http://localhost:8080`) então o navegador deixará o JavaScript no frontend enviar sua requisição para o backend. +Portanto, o navegador enviará uma requisição HTTP `OPTIONS` ao backend `:80`, e se o backend enviar os cabeçalhos apropriados autorizando a comunicação a partir dessa origem diferente (`http://localhost:8080`), então o navegador `:8080` permitirá que o JavaScript no frontend envie sua requisição para o backend `:80`. -Para conseguir isso, o backend deve ter uma lista de "origens permitidas". +Para conseguir isso, o backend `:80` deve ter uma lista de "origens permitidas". -Neste caso, ele terá que incluir `http://localhost:8080` para o frontend funcionar corretamente. +Neste caso, a lista terá que incluir `http://localhost:8080` para o frontend `:8080` funcionar corretamente. -## Curingas +## Curingas { #wildcards } -É possível declarar uma lista com `"*"` (um "curinga") para dizer que tudo está permitido. +É possível declarar a lista como `"*"` (um "curinga") para dizer que tudo está permitido. Mas isso só permitirá certos tipos de comunicação, excluindo tudo que envolva credenciais: cookies, cabeçalhos de autorização como aqueles usados ​​com Bearer Tokens, etc. Então, para que tudo funcione corretamente, é melhor especificar explicitamente as origens permitidas. -## Usar `CORSMiddleware` +## Usar `CORSMiddleware` { #use-corsmiddleware } Você pode configurá-lo em sua aplicação **FastAPI** usando o `CORSMiddleware`. @@ -46,9 +46,9 @@ Você também pode especificar se o seu backend permite: * Métodos HTTP específicos (`POST`, `PUT`) ou todos eles com o curinga `"*"`. * Cabeçalhos HTTP específicos ou todos eles com o curinga `"*"`. -{* ../../docs_src/cors/tutorial001.py hl[2,6:11,13:19] *} +{* ../../docs_src/cors/tutorial001_py39.py hl[2,6:11,13:19] *} -Os parâmetros padrão usados ​​pela implementação `CORSMiddleware` são restritivos por padrão, então você precisará habilitar explicitamente as origens, métodos ou cabeçalhos específicos para que os navegadores tenham permissão para usá-los em um contexto de domínios diferentes. +Os parâmetros padrão usados ​​pela implementação `CORSMiddleware` são restritivos por padrão, então você precisará habilitar explicitamente as origens, métodos ou cabeçalhos específicos para que os navegadores tenham permissão para usá-los em um contexto cross domain. Os seguintes argumentos são suportados: @@ -56,27 +56,30 @@ Os seguintes argumentos são suportados: * `allow_origin_regex` - Uma string regex para corresponder às origens que devem ter permissão para fazer requisições de origem cruzada. Por exemplo, `'https://.*\.example\.org'`. * `allow_methods` - Uma lista de métodos HTTP que devem ser permitidos para requisições de origem cruzada. O padrão é `['GET']`. Você pode usar `['*']` para permitir todos os métodos padrão. * `allow_headers` - Uma lista de cabeçalhos de solicitação HTTP que devem ter suporte para requisições de origem cruzada. O padrão é `[]`. Você pode usar `['*']` para permitir todos os cabeçalhos. Os cabeçalhos `Accept`, `Accept-Language`, `Content-Language` e `Content-Type` são sempre permitidos para requisições CORS simples. -* `allow_credentials` - Indica que os cookies devem ser suportados para requisições de origem cruzada. O padrão é `False`. Além disso, `allow_origins` não pode ser definido como `['*']` para que as credenciais sejam permitidas, as origens devem ser especificadas. +* `allow_credentials` - Indica que os cookies devem ser suportados para requisições de origem cruzada. O padrão é `False`. + + Nenhum de `allow_origins`, `allow_methods` e `allow_headers` pode ser definido como `['*']` se `allow_credentials` estiver definido como `True`. Todos eles devem ser especificados explicitamente. + * `expose_headers` - Indica quaisquer cabeçalhos de resposta que devem ser disponibilizados ao navegador. O padrão é `[]`. * `max_age` - Define um tempo máximo em segundos para os navegadores armazenarem em cache as respostas CORS. O padrão é `600`. O middleware responde a dois tipos específicos de solicitação HTTP... -### Requisições CORS pré-voo (preflight) +### Requisições CORS pré-voo (preflight) { #cors-preflight-requests } Estas são quaisquer solicitações `OPTIONS` com cabeçalhos `Origin` e `Access-Control-Request-Method`. Nesse caso, o middleware interceptará a solicitação recebida e responderá com cabeçalhos CORS apropriados e uma resposta `200` ou `400` para fins informativos. -### Requisições Simples +### Requisições Simples { #simple-requests } Qualquer solicitação com um cabeçalho `Origin`. Neste caso, o middleware passará a solicitação normalmente, mas incluirá cabeçalhos CORS apropriados na resposta. -## Mais informações +## Mais informações { #more-info } -Para mais informações CORS, acesse Mozilla CORS documentation. +Para mais informações sobre CORS, consulte a documentação do CORS da Mozilla. -/// note | Detalhes técnicos +/// note | Detalhes Técnicos Você também pode usar `from starlette.middleware.cors import CORSMiddleware`. diff --git a/docs/pt/docs/tutorial/debugging.md b/docs/pt/docs/tutorial/debugging.md index 67b7644577..e39c7d1280 100644 --- a/docs/pt/docs/tutorial/debugging.md +++ b/docs/pt/docs/tutorial/debugging.md @@ -1,14 +1,14 @@ -# Depuração +# Depuração { #debugging } Você pode conectar o depurador no seu editor, por exemplo, com o Visual Studio Code ou PyCharm. -## Chamar `uvicorn` +## Chamar `uvicorn` { #call-uvicorn } -Em seu aplicativo FastAPI, importe e execute `uvicorn` diretamente: +Em sua aplicação FastAPI, importe e execute `uvicorn` diretamente: -{* ../../docs_src/debugging/tutorial001.py hl[1,15] *} +{* ../../docs_src/debugging/tutorial001_py39.py hl[1,15] *} -### Sobre `__name__ == "__main__"` +### Sobre `__name__ == "__main__"` { #about-name-main } O objetivo principal de `__name__ == "__main__"` é ter algum código que seja executado quando seu arquivo for chamado com: @@ -26,7 +26,7 @@ mas não é chamado quando outro arquivo o importa, como em: from myapp import app ``` -#### Mais detalhes +#### Mais detalhes { #more-details } Digamos que seu arquivo se chama `myapp.py`. @@ -78,9 +78,9 @@ Para mais informações, consulte -## Pegando um Atalho +## Pegando um Atalho { #shortcut } Mas você pode ver que temos uma repetição do código neste exemplo, escrevendo `CommonQueryParams` duas vezes: -//// tab | Python 3.8+ +//// tab | Python 3.9+ ```Python commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] @@ -205,7 +205,7 @@ commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] //// -//// tab | Python 3.8+ non-Annotated +//// tab | Python 3.9+ non-Annotated /// tip | Dica @@ -225,7 +225,7 @@ Para esses casos específicos, você pode fazer o seguinte: Em vez de escrever: -//// tab | Python 3.8+ +//// tab | Python 3.9+ ```Python commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] @@ -233,7 +233,7 @@ commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] //// -//// tab | Python 3.8+ non-Annotated +//// tab | Python 3.9+ non-Annotated /// tip | Dica @@ -249,7 +249,7 @@ commons: CommonQueryParams = Depends(CommonQueryParams) ...escreva: -//// tab | Python 3.8+ +//// tab | Python 3.9+ ```Python commons: Annotated[CommonQueryParams, Depends()] @@ -257,7 +257,7 @@ commons: Annotated[CommonQueryParams, Depends()] //// -//// tab | Python 3.8 non-Annotated +//// tab | Python 3.9+ non-Annotated /// tip | Dica diff --git a/docs/pt/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md b/docs/pt/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md index d7d31bb45e..ee8a58dc24 100644 --- a/docs/pt/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md +++ b/docs/pt/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md @@ -1,4 +1,4 @@ -# Dependências em decoradores de operações de rota +# Dependências em decoradores de operações de rota { #dependencies-in-path-operation-decorators } Em alguns casos você não precisa necessariamente retornar o valor de uma dependência dentro de uma *função de operação de rota*. @@ -8,7 +8,7 @@ Mas você ainda precisa que ela seja executada/resolvida. Para esses casos, em vez de declarar um parâmetro em uma *função de operação de rota* com `Depends`, você pode adicionar um argumento `dependencies` do tipo `list` ao decorador da operação de rota. -## Adicionando `dependencies` ao decorador da operação de rota +## Adicione `dependencies` ao decorador da operação de rota { #add-dependencies-to-the-path-operation-decorator } O *decorador da operação de rota* recebe um argumento opcional `dependencies`. @@ -22,7 +22,7 @@ Essas dependências serão executadas/resolvidas da mesma forma que dependência Alguns editores de texto checam parâmetros de funções não utilizados, e os mostram como erros. -Utilizando `dependencies` no *decorador da operação de rota* você pode garantir que elas serão executadas enquanto evita errors de editores/ferramentas. +Utilizando `dependencies` no *decorador da operação de rota* você pode garantir que elas serão executadas enquanto evita erros de editores/ferramentas. Isso também pode ser útil para evitar confundir novos desenvolvedores que ao ver um parâmetro não usado no seu código podem pensar que ele é desnecessário. @@ -30,29 +30,29 @@ Isso também pode ser útil para evitar confundir novos desenvolvedores que ao v /// info | Informação -Neste exemplo utilizamos cabeçalhos personalizados inventados `X-Keys` e `X-Token`. +Neste exemplo utilizamos cabeçalhos personalizados inventados `X-Key` e `X-Token`. Mas em situações reais, como implementações de segurança, você pode obter mais vantagens em usar as [Ferramentas de segurança integradas (o próximo capítulo)](../security/index.md){.internal-link target=_blank}. /// -## Erros das dependências e valores de retorno +## Erros das dependências e valores de retorno { #dependencies-errors-and-return-values } Você pode utilizar as mesmas *funções* de dependências que você usaria normalmente. -### Requisitos de Dependências +### Requisitos de Dependências { #dependency-requirements } Dependências podem declarar requisitos de requisições (como cabeçalhos) ou outras subdependências: {* ../../docs_src/dependencies/tutorial006_an_py39.py hl[8,13] *} -### Levantando exceções +### Levantar exceções { #raise-exceptions } -Essas dependências podem levantar exceções, da mesma forma que dependências comuns: +Essas dependências podem `raise` exceções, da mesma forma que dependências comuns: {* ../../docs_src/dependencies/tutorial006_an_py39.py hl[10,15] *} -### Valores de retorno +### Valores de retorno { #return-values } E elas também podem ou não retornar valores, eles não serão utilizados. @@ -60,10 +60,10 @@ Então, você pode reutilizar uma dependência comum (que retorna um valor) que {* ../../docs_src/dependencies/tutorial006_an_py39.py hl[11,16] *} -## Dependências para um grupo de *operações de rota* +## Dependências para um grupo de *operações de rota* { #dependencies-for-a-group-of-path-operations } -Mais a frente, quando você ler sobre como estruturar aplicações maiores ([Bigger Applications - Multiple Files](../../tutorial/bigger-applications.md){.internal-link target=_blank}), possivelmente com múltiplos arquivos, você aprenderá a declarar um único parâmetro `dependencies` para um grupo de *operações de rota*. +Mais a frente, quando você ler sobre como estruturar aplicações maiores ([Aplicações maiores - Múltiplos arquivos](../../tutorial/bigger-applications.md){.internal-link target=_blank}), possivelmente com múltiplos arquivos, você aprenderá a declarar um único parâmetro `dependencies` para um grupo de *operações de rota*. -## Dependências globais +## Dependências globais { #global-dependencies } -No próximo passo veremos como adicionar dependências para uma aplicação `FastAPI` inteira, para que ela seja aplicada em toda *operação de rota*. +No próximo passo veremos como adicionar dependências para uma aplicação `FastAPI` inteira, para que elas sejam aplicadas em toda *operação de rota*. diff --git a/docs/pt/docs/tutorial/dependencies/dependencies-with-yield.md b/docs/pt/docs/tutorial/dependencies/dependencies-with-yield.md index eaf711197f..3678730139 100644 --- a/docs/pt/docs/tutorial/dependencies/dependencies-with-yield.md +++ b/docs/pt/docs/tutorial/dependencies/dependencies-with-yield.md @@ -1,12 +1,12 @@ -# Dependências com yield +# Dependências com yield { #dependencies-with-yield } -O FastAPI possui suporte para dependências que realizam alguns passos extras ao finalizar. +O **FastAPI** possui suporte para dependências que realizam alguns passos extras ao finalizar. Para fazer isso, utilize `yield` em vez de `return`, e escreva os passos extras (código) depois. /// tip | Dica -Garanta que `yield` é utilizado apenas uma vez. +Garanta utilizar `yield` apenas uma vez por dependência. /// @@ -23,21 +23,21 @@ Na realidade, o FastAPI utiliza esses dois decoradores internamente. /// -## Uma dependência de banco de dados com `yield` +## Uma dependência de banco de dados com `yield` { #a-database-dependency-with-yield } -Por exemplo, você poderia utilizar isso para criar uma sessão do banco de dados, e fechá-la após terminar sua operação. +Por exemplo, você poderia utilizar isso para criar uma sessão do banco de dados, e fechá-la após terminar. -Apenas o código anterior a declaração com `yield` e o código contendo essa declaração são executados antes de criar uma resposta. +Apenas o código anterior à declaração com `yield` e o código contendo essa declaração são executados antes de criar uma resposta: -{* ../../docs_src/dependencies/tutorial007.py hl[2:4] *} +{* ../../docs_src/dependencies/tutorial007_py39.py hl[2:4] *} -O valor gerado (yielded) é o que é injetado nas *operações de rota* e outras dependências. +O valor gerado (yielded) é o que é injetado nas *operações de rota* e outras dependências: -{* ../../docs_src/dependencies/tutorial007.py hl[4] *} +{* ../../docs_src/dependencies/tutorial007_py39.py hl[4] *} -O código após o `yield` é executado após a resposta ser entregue: +O código após o `yield` é executado após a resposta: -{* ../../docs_src/dependencies/tutorial007.py hl[5:6] *} +{* ../../docs_src/dependencies/tutorial007_py39.py hl[5:6] *} /// tip | Dica @@ -47,21 +47,19 @@ O **FastAPI** saberá o que fazer com cada uma, da mesma forma que as dependênc /// -## Uma dependência com `yield` e `try` +## Uma dependência com `yield` e `try` { #a-dependency-with-yield-and-try } Se você utilizar um bloco `try` em uma dependência com `yield`, você irá capturar qualquer exceção que for lançada enquanto a dependência é utilizada. -Por exemplo, se algum código em um certo momento no meio da operação, em outra dependência ou em uma *operação de rota*, fizer um "rollback" de uma transação de banco de dados ou causar qualquer outro erro, você irá capturar a exceção em sua dependência. +Por exemplo, se algum código em um certo momento no meio, em outra dependência ou em uma *operação de rota*, fizer um "rollback" de uma transação de banco de dados ou causar qualquer outra exceção, você irá capturar a exceção em sua dependência. Então, você pode procurar por essa exceção específica dentro da dependência com `except AlgumaExcecao`. Da mesma forma, você pode utilizar `finally` para garantir que os passos de saída são executados, com ou sem exceções. -```python hl_lines="3 5" -{!../../docs_src/dependencies/tutorial007.py!} -``` +{* ../../docs_src/dependencies/tutorial007_py39.py hl[3,5] *} -## Subdependências com `yield` +## Subdependências com `yield` { #sub-dependencies-with-yield } Você pode ter subdependências e "árvores" de subdependências de qualquer tamanho e forma, e qualquer uma ou todas elas podem utilizar `yield`. @@ -69,73 +67,17 @@ O **FastAPI** garantirá que o "código de saída" em cada dependência com `yie Por exemplo, `dependency_c` pode depender de `dependency_b`, e `dependency_b` depender de `dependency_a`: -//// tab | python 3.9+ - -```python hl_lines="6 14 22" -{!> ../../docs_src/dependencies/tutorial008_an_py39.py!} -``` - -//// - -//// tab | python 3.8+ - -```python hl_lines="5 13 21" -{!> ../../docs_src/dependencies/tutorial008_an.py!} -``` - -//// - -//// tab | python 3.8+ non-annotated - -/// tip | Dica - -Utilize a versão com `Annotated` se possível. - -/// - -```python hl_lines="4 12 20" -{!> ../../docs_src/dependencies/tutorial008.py!} -``` - -//// +{* ../../docs_src/dependencies/tutorial008_an_py39.py hl[6,14,22] *} E todas elas podem utilizar `yield`. -Neste caso, `dependency_c` precisa que o valor de `dependency_b` (nomeada de `dep_b` aqui) continue disponível para executar seu código de saída. +Neste caso, `dependency_c`, para executar seu código de saída, precisa que o valor de `dependency_b` (nomeado de `dep_b` aqui) continue disponível. -E, por outro lado, `dependency_b` precisa que o valor de `dependency_a` (nomeada de `dep_a`) continue disponível para executar seu código de saída. +E, por outro lado, `dependency_b` precisa que o valor de `dependency_a` (nomeado de `dep_a`) esteja disponível para executar seu código de saída. -//// tab | python 3.9+ +{* ../../docs_src/dependencies/tutorial008_an_py39.py hl[18:19,26:27] *} -```python hl_lines="18-19 26-27" -{!> ../../docs_src/dependencies/tutorial008_an_py39.py!} -``` - -//// - -//// tab | python 3.8+ - -```python hl_lines="17-18 25-26" -{!> ../../docs_src/dependencies/tutorial008_an.py!} -``` - -//// - -//// tab | python 3.8+ non-annotated - -/// tip | Dica - -Utilize a versão com `Annotated` se possível. - -/// - -```python hl_lines="16-17 24-25" -{!> ../../docs_src/dependencies/tutorial008.py!} -``` - -//// - -Da mesma forma, você pode ter algumas dependências com `yield` e outras com `return` e ter uma relação de dependência entre algumas dos dois tipos. +Da mesma forma, você pode ter algumas dependências com `yield` e outras com `return` e ter uma relação de dependência entre algumas das duas. E você poderia ter uma única dependência que precisa de diversas outras dependências com `yield`, etc. @@ -151,83 +93,45 @@ O **FastAPI** utiliza eles internamente para alcançar isso. /// -## Dependências com `yield` e `httpexception` +## Dependências com `yield` e `HTTPException` { #dependencies-with-yield-and-httpexception } -Você viu que dependências podem ser utilizadas com `yield` e podem incluir blocos `try` para capturar exceções. +Você viu que pode usar dependências com `yield` e ter blocos `try` que tentam executar algum código e depois executar algum código de saída com `finally`. -Da mesma forma, você pode lançar uma `httpexception` ou algo parecido no código de saída, após o `yield` +Você também pode usar `except` para capturar a exceção que foi levantada e fazer algo com ela. + +Por exemplo, você pode levantar uma exceção diferente, como `HTTPException`. /// tip | Dica -Essa é uma técnica relativamente avançada, e na maioria dos casos você não precisa dela totalmente, já que você pode lançar exceções (incluindo `httpexception`) dentro do resto do código da sua aplicação, por exemplo, em uma *função de operação de rota*. +Essa é uma técnica relativamente avançada, e na maioria dos casos você não vai precisar dela, já que você pode levantar exceções (incluindo `HTTPException`) dentro do resto do código da sua aplicação, por exemplo, na *função de operação de rota*. Mas ela existe para ser utilizada caso você precise. 🤓 /// -//// tab | python 3.9+ +{* ../../docs_src/dependencies/tutorial008b_an_py39.py hl[18:22,31] *} -```python hl_lines="18-22 31" -{!> ../../docs_src/dependencies/tutorial008b_an_py39.py!} -``` +Se você quiser capturar exceções e criar uma resposta personalizada com base nisso, crie um [Manipulador de Exceções Customizado](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank}. -//// +## Dependências com `yield` e `except` { #dependencies-with-yield-and-except } -//// tab | python 3.8+ - -```python hl_lines="17-21 30" -{!> ../../docs_src/dependencies/tutorial008b_an.py!} -``` - -//// - -//// tab | python 3.8+ non-annotated - -/// tip | Dica - -Utilize a versão com `Annotated` se possível. - -/// - -```python hl_lines="16-20 29" -{!> ../../docs_src/dependencies/tutorial008b.py!} -``` - -//// - -Uma alternativa que você pode utilizar para capturar exceções (e possivelmente lançar outra HTTPException) é criar um [Manipulador de Exceções Customizado](../handling-errors.md#instalando-manipuladores-de-excecoes-customizados){.internal-link target=_blank}. - -## Dependências com `yield` e `except` - -Se você capturar uma exceção com `except` em uma dependência que utilize `yield` e ela não for levantada novamente (ou uma nova exceção for levantada), o FastAPI não será capaz de identifcar que houve uma exceção, da mesma forma que aconteceria com Python puro: +Se você capturar uma exceção com `except` em uma dependência que utilize `yield` e ela não for levantada novamente (ou uma nova exceção for levantada), o FastAPI não será capaz de identificar que houve uma exceção, da mesma forma que aconteceria com Python puro: {* ../../docs_src/dependencies/tutorial008c_an_py39.py hl[15:16] *} Neste caso, o cliente irá ver uma resposta *HTTP 500 Internal Server Error* como deveria acontecer, já que não estamos levantando nenhuma `HTTPException` ou coisa parecida, mas o servidor **não terá nenhum log** ou qualquer outra indicação de qual foi o erro. 😱 -### Sempre levante (`raise`) exceções em Dependências com `yield` e `except` +### Sempre levante (`raise`) em Dependências com `yield` e `except` { #always-raise-in-dependencies-with-yield-and-except } -Se você capturar uma exceção em uma dependência com `yield`, a menos que você esteja levantando outra `HTTPException` ou coisa parecida, você deveria relançar a exceção original. +Se você capturar uma exceção em uma dependência com `yield`, a menos que você esteja levantando outra `HTTPException` ou coisa parecida, **você deve relançar a exceção original**. Você pode relançar a mesma exceção utilizando `raise`: {* ../../docs_src/dependencies/tutorial008d_an_py39.py hl[17] *} -//// tab | python 3.8+ non-annotated - -/// tip | Dica - -Utilize a versão com `Annotated` se possível. - -/// - -{* ../../docs_src/dependencies/tutorial008d.py hl[15] *} - -//// - Agora o cliente irá receber a mesma resposta *HTTP 500 Internal Server Error*, mas o servidor terá nosso `InternalError` personalizado nos logs. 😎 -## Execução de dependências com `yield` +## Execução de dependências com `yield` { #execution-of-dependencies-with-yield } A sequência de execução é mais ou menos como esse diagrama. O tempo passa do topo para baixo. E cada coluna é uma das partes interagindo ou executando código. @@ -270,57 +174,69 @@ participant tasks as Tarefas de Background Apenas **uma resposta** será enviada para o cliente. Ela pode ser uma das respostas de erro, ou então a resposta da *operação de rota*. -Após uma dessas respostas ser enviada, nenhuma outra resposta pode ser enviada +Após uma dessas respostas ser enviada, nenhuma outra resposta pode ser enviada. /// /// tip | Dica -Esse diagrama mostra `HttpException`, mas você pode levantar qualquer outra exceção que você capture em uma dependência com `yield` ou um [Manipulador de exceções personalizado](../handling-errors.md#instalando-manipuladores-de-excecoes-customizados){.internal-link target=_blank}. - -Se você lançar qualquer exceção, ela será passada para as dependências com yield, inlcuindo a `HTTPException`. Na maioria dos casos você vai querer relançar essa mesma exceção ou uma nova a partir da dependência com `yield` para garantir que ela seja tratada adequadamente. +Se você levantar qualquer exceção no código da *função de operação de rota*, ela será passada para as dependências com `yield`, incluindo `HTTPException`. Na maioria dos casos, você vai querer relançar essa mesma exceção ou uma nova a partir da dependência com `yield` para garantir que ela seja tratada adequadamente. /// -## Dependências com `yield`, `HTTPException`, `except` e Tarefas de Background +## Saída antecipada e `scope` { #early-exit-and-scope } -/// warning | Aviso +Normalmente, o código de saída das dependências com `yield` é executado **após a resposta** ser enviada ao cliente. -Você provavelmente não precisa desses detalhes técnicos, você pode pular essa seção e continuar na próxima seção abaixo. +Mas se você sabe que não precisará usar a dependência depois de retornar da *função de operação de rota*, você pode usar `Depends(scope="function")` para dizer ao FastAPI que deve fechar a dependência depois que a *função de operação de rota* retornar, mas **antes** de a **resposta ser enviada**. -Esses detalhes são úteis principalmente se você estiver usando uma versão do FastAPI anterior à 0.106.0 e utilizando recursos de dependências com `yield` em tarefas de background. +{* ../../docs_src/dependencies/tutorial008e_an_py39.py hl[12,16] *} -/// +`Depends()` recebe um parâmetro `scope` que pode ser: -### Dependências com `yield` e `except`, Detalhes Técnicos +* `"function"`: iniciar a dependência antes da *função de operação de rota* que trata a requisição, encerrar a dependência depois que a *função de operação de rota* termina, mas **antes** de a resposta ser enviada de volta ao cliente. Assim, a função da dependência será executada **em torno** da *função de operação de rota*. +* `"request"`: iniciar a dependência antes da *função de operação de rota* que trata a requisição (semelhante a quando se usa `"function"`), mas encerrar **depois** que a resposta é enviada de volta ao cliente. Assim, a função da dependência será executada **em torno** do ciclo de **requisição** e resposta. -Antes do FastAPI 0.110.0, se você utilizasse uma dependência com `yield`, e então capturasse uma dependência com `except` nessa dependência, caso a exceção não fosse relançada, ela era automaticamente lançada para qualquer manipulador de exceções ou o manipulador de erros interno do servidor. +Se não for especificado e a dependência tiver `yield`, ela terá `scope` igual a `"request"` por padrão. -Isso foi modificado na versão 0.110.0 para consertar o consumo de memória não controlado das exceções relançadas automaticamente sem um manipulador (erros internos do servidor), e para manter o comportamento consistente com o código Python tradicional. +### `scope` para subdependências { #scope-for-sub-dependencies } -### Tarefas de Background e Dependências com `yield`, Detalhes Técnicos +Quando você declara uma dependência com `scope="request"` (o padrão), qualquer subdependência também precisa ter `scope` igual a `"request"`. -Antes do FastAPI 0.106.0, levantar exceções após um `yield` não era possível, o código de saída nas dependências com `yield` era executado *após* a resposta ser enviada, então os [Manipuladores de Exceções](../handling-errors.md#instalando-manipuladores-de-excecoes-customizados){.internal-link target=_blank} já teriam executado. +Mas uma dependência com `scope` igual a `"function"` pode ter dependências com `scope` igual a `"function"` e com `scope` igual a `"request"`. -Isso foi implementado dessa forma principalmente para permitir que os mesmos objetos fornecidos ("yielded") pelas dependências dentro de tarefas de background fossem reutilizados, por que o código de saída era executado antes das tarefas de background serem finalizadas. +Isso porque qualquer dependência precisa conseguir executar seu código de saída antes das subdependências, pois pode ainda precisar usá-las durante seu código de saída. -Ainda assim, como isso exigiria esperar que a resposta navegasse pela rede enquanto mantia ativo um recurso desnecessário na dependência com yield (por exemplo, uma conexão com banco de dados), isso mudou na versão 0.106.0 do FastAPI. +```mermaid +sequenceDiagram -/// tip | Dica +participant client as Cliente +participant dep_req as Dep scope="request" +participant dep_func as Dep scope="function" +participant operation as Operação de Rota -Adicionalmente, uma tarefa de background é, normalmente, um conjunto de lógicas independentes que devem ser manipuladas separadamente, com seus próprios recursos (e.g. sua própria conexão com banco de dados). + client ->> dep_req: Iniciar requisição + Note over dep_req: Executar código até o yield + dep_req ->> dep_func: Passar dependência + Note over dep_func: Executar código até o yield + dep_func ->> operation: Executar operação de rota com dependência + operation ->> dep_func: Retornar da operação de rota + Note over dep_func: Executar código após o yield + Note over dep_func: ✅ Dependência fechada + dep_func ->> client: Enviar resposta ao cliente + Note over client: Resposta enviada + Note over dep_req: Executar código após o yield + Note over dep_req: ✅ Dependência fechada +``` -Então, dessa forma você provavelmente terá um código mais limpo. +## Dependências com `yield`, `HTTPException`, `except` e Tarefas de Background { #dependencies-with-yield-httpexception-except-and-background-tasks } -/// +Dependências com `yield` evoluíram ao longo do tempo para cobrir diferentes casos de uso e corrigir alguns problemas. -Se você costumava depender desse comportamento, agora você precisa criar os recursos para uma tarefa de background dentro dela mesma, e usar internamente apenas dados que não dependam de recursos de dependências com `yield`. +Se você quiser ver o que mudou em diferentes versões do FastAPI, você pode ler mais sobre isso no guia avançado, em [Dependências Avançadas - Dependências com `yield`, `HTTPException`, `except` e Tarefas de Background](../../advanced/advanced-dependencies.md#dependencies-with-yield-httpexception-except-and-background-tasks){.internal-link target=_blank}. +## Gerenciadores de contexto { #context-managers } -Por exemplo, em vez de utilizar a mesma sessão do banco de dados, você criaria uma nova sessão dentro da tarefa de background, e você obteria os objetos do banco de dados utilizando essa nova sessão. E então, em vez de passar o objeto obtido do banco de dados como um parâmetro para a função da tarefa de background, você passaria o ID desse objeto e buscaria ele novamente dentro da função da tarefa de background. - -## Gerenciadores de contexto - -### O que são gerenciadores de contexto +### O que são "Gerenciadores de Contexto" { #what-are-context-managers } "Gerenciadores de Contexto" são qualquer um dos objetos Python que podem ser utilizados com a declaração `with`. @@ -338,9 +254,9 @@ Quando o bloco `with` finaliza, ele se certifica de fechar o arquivo, mesmo que Quando você cria uma dependência com `yield`, o **FastAPI** irá criar um gerenciador de contexto internamente para ela, e combiná-lo com algumas outras ferramentas relacionadas. -### Utilizando gerenciadores de contexto em dependências com `yield` +### Utilizando gerenciadores de contexto em dependências com `yield` { #using-context-managers-in-dependencies-with-yield } -/// warning | Aviso +/// warning | Atenção Isso é uma ideia mais ou menos "avançada". @@ -348,18 +264,18 @@ Se você está apenas iniciando com o **FastAPI** você pode querer pular isso p /// -Em python, você pode criar Gerenciadores de Contexto ao criar uma classe com dois métodos: `__enter__()` e `__exit__()`. +Em Python, você pode criar Gerenciadores de Contexto ao criar uma classe com dois métodos: `__enter__()` e `__exit__()`. -Você também pode usá-los dentro de dependências com `yield` do **FastAPI** ao utilizar `with` ou `async with` dentro da função da dependência: +Você também pode usá-los dentro de dependências com `yield` do **FastAPI** ao utilizar +`with` ou `async with` dentro da função da dependência: -{* ../../docs_src/dependencies/tutorial010.py hl[1:9,13] *} +{* ../../docs_src/dependencies/tutorial010_py39.py hl[1:9,13] *} /// tip | Dica Outra forma de criar um gerenciador de contexto é utilizando: * `@contextlib.contextmanager` ou - * `@contextlib.asynccontextmanager` Para decorar uma função com um único `yield`. diff --git a/docs/pt/docs/tutorial/dependencies/global-dependencies.md b/docs/pt/docs/tutorial/dependencies/global-dependencies.md index a9a7e3b899..4d6e8a69a0 100644 --- a/docs/pt/docs/tutorial/dependencies/global-dependencies.md +++ b/docs/pt/docs/tutorial/dependencies/global-dependencies.md @@ -1,15 +1,16 @@ -# Dependências Globais +# Dependências Globais { #global-dependencies } -Para alguns tipos de aplicação específicos você pode querer adicionar dependências para toda a aplicação. +Para alguns tipos de aplicação você pode querer adicionar dependências para toda a aplicação. -De forma semelhante a [adicionar dependências (`dependencies`) em *decoradores de operação de rota*](dependencies-in-path-operation-decorators.md){.internal-link target=_blank}, você pode adicioná-las à aplicação `FastAPI`. +De forma semelhante a [adicionar `dependencies` aos *decoradores de operação de rota*](dependencies-in-path-operation-decorators.md){.internal-link target=_blank}, você pode adicioná-las à aplicação `FastAPI`. Nesse caso, elas serão aplicadas a todas as *operações de rota* da aplicação: -{* ../../docs_src/dependencies/tutorial012_an_py39.py hl[16] *} +{* ../../docs_src/dependencies/tutorial012_an_py39.py hl[17] *} -E todos os conceitos apresentados na sessão sobre [adicionar dependências em *decoradores de operação de rota*](dependencies-in-path-operation-decorators.md){.internal-link target=_blank} ainda se aplicam, mas nesse caso, para todas as *operações de rota* da aplicação. -## Dependências para conjuntos de *operações de rota* +E todos os conceitos apresentados na seção sobre [adicionar `dependencies` aos *decoradores de operação de rota*](dependencies-in-path-operation-decorators.md){.internal-link target=_blank} ainda se aplicam, mas nesse caso, para todas as *operações de rota* da aplicação. -Mais para a frente, quando você ler sobre como estruturar aplicações maiores ([Bigger Applications - Multiple Files](../../tutorial/bigger-applications.md){.internal-link target=_blank}), possivelmente com múltiplos arquivos, você irá aprender a declarar um único parâmetro `dependencies` para um conjunto de *operações de rota*. +## Dependências para conjuntos de *operações de rota* { #dependencies-for-groups-of-path-operations } + +Mais para a frente, quando você ler sobre como estruturar aplicações maiores ([Aplicações Maiores - Múltiplos Arquivos](../../tutorial/bigger-applications.md){.internal-link target=_blank}), possivelmente com múltiplos arquivos, você irá aprender a declarar um único parâmetro `dependencies` para um conjunto de *operações de rota*. diff --git a/docs/pt/docs/tutorial/dependencies/index.md b/docs/pt/docs/tutorial/dependencies/index.md index 1500b715a7..bdfe1ac39e 100644 --- a/docs/pt/docs/tutorial/dependencies/index.md +++ b/docs/pt/docs/tutorial/dependencies/index.md @@ -1,10 +1,10 @@ -# Dependências +# Dependências { #dependencies } -O **FastAPI** possui um poderoso, mas intuitivo sistema de **Injeção de Dependência**. +O **FastAPI** possui um poderoso, mas intuitivo sistema de **Injeção de Dependência**. Esse sistema foi pensado para ser fácil de usar, e permitir que qualquer desenvolvedor possa integrar facilmente outros componentes ao **FastAPI**. -## O que é "Injeção de Dependência" +## O que é "Injeção de Dependência" { #what-is-dependency-injection } **"Injeção de Dependência"** no mundo da programação significa, que existe uma maneira de declarar no seu código (nesse caso, suas *funções de operação de rota*) para declarar as coisas que ele precisa para funcionar e que serão utilizadas: "dependências". @@ -19,13 +19,13 @@ Isso é bastante útil quando você precisa: Tudo isso, enquanto minimizamos a repetição de código. -## Primeiros passos +## Primeiros passos { #first-steps } Vamos ver um exemplo simples. Tão simples que não será muito útil, por enquanto. Mas dessa forma podemos focar em como o sistema de **Injeção de Dependência** funciona. -### Criando uma dependência, ou "injetável" +### Criando uma dependência, ou "injetável" { #create-a-dependency-or-dependable } Primeiro vamos focar na dependência. @@ -57,15 +57,15 @@ FastAPI passou a suportar a notação `Annotated` (e começou a recomendá-la) n Se você utiliza uma versão anterior, ocorrerão erros ao tentar utilizar `Annotated`. -Certifique-se de [Atualizar a versão do FastAPI](../../deployment/versions.md#atualizando-as-versoes-do-fastapi){.internal-link target=_blank} para pelo menos 0.95.1 antes de usar `Annotated`. +Certifique-se de [Atualizar a versão do FastAPI](../../deployment/versions.md#upgrading-the-fastapi-versions){.internal-link target=_blank} para pelo menos 0.95.1 antes de usar `Annotated`. /// -### Importando `Depends` +### Importando `Depends` { #import-depends } {* ../../docs_src/dependencies/tutorial001_an_py310.py hl[3] *} -### Declarando a dependência, no "dependente" +### Declarando a dependência, no "dependente" { #declare-the-dependency-in-the-dependant } Da mesma forma que você utiliza `Body`, `Query`, etc. Como parâmetros de sua *função de operação de rota*, utilize `Depends` com um novo parâmetro: @@ -106,7 +106,7 @@ common_parameters --> read_users Assim, você escreve um código compartilhado apenas uma vez e o **FastAPI** se encarrega de chamá-lo em suas *operações de rota*. -/// check | Checando +/// check | Verifique Perceba que você não precisa criar uma classe especial e enviar a dependência para algum outro lugar em que o **FastAPI** a "registre" ou realize qualquer operação similar. @@ -114,7 +114,7 @@ Você apenas envia para `Depends` e o **FastAPI** sabe como fazer o resto. /// -## Compartilhando dependências `Annotated` +## Compartilhando dependências `Annotated` { #share-annotated-dependencies } Nos exemplos acima, você pode ver que existe uma pequena **duplicação de código**. @@ -140,7 +140,7 @@ As dependências continuarão funcionando como esperado, e a **melhor parte** é Isso é especialmente útil para uma **base de código grande** onde **as mesmas dependências** são utilizadas repetidamente em **muitas *operações de rota***. -## `Async` ou não, eis a questão +## `Async` ou não, eis a questão { #to-async-or-not-to-async } Como as dependências também serão chamadas pelo **FastAPI** (da mesma forma que *funções de operação de rota*), as mesmas regras se aplicam ao definir suas funções. @@ -152,11 +152,11 @@ Não faz diferença. O **FastAPI** sabe o que fazer. /// note | Nota -Caso você não conheça, veja em [Async: *"Com Pressa?"*](../../async.md#com-pressa){.internal-link target=_blank} a sessão acerca de `async` e `await` na documentação. +Caso você não conheça, veja em [Async: *"Com Pressa?"*](../../async.md#in-a-hurry){.internal-link target=_blank} a sessão acerca de `async` e `await` na documentação. /// -## Integrando com OpenAPI +## Integrando com OpenAPI { #integrated-with-openapi } Todas as declarações de requisições, validações e requisitos para suas dependências (e sub-dependências) serão integradas em um mesmo esquema OpenAPI. @@ -164,7 +164,7 @@ Então, a documentação interativa também terá toda a informação sobre essa -## Caso de Uso Simples +## Caso de Uso Simples { #simple-usage } Se você parar para ver, *funções de operação de rota* são declaradas para serem usadas sempre que uma *rota* e uma *operação* se encaixam, e então o **FastAPI** se encarrega de chamar a função correspondente com os argumentos corretos, extraindo os dados da requisição. @@ -182,7 +182,7 @@ Outros termos comuns para essa mesma ideia de "injeção de dependência" são: * injetáveis * componentes -## Plug-ins em **FastAPI** +## Plug-ins em **FastAPI** { #fastapi-plug-ins } Integrações e "plug-ins" podem ser construídos com o sistema de **Injeção de Dependência**. Mas na verdade, **não há necessidade de criar "plug-ins"**, já que utilizando dependências é possível declarar um número infinito de integrações e interações que se tornam disponíveis para as suas *funções de operação de rota*. @@ -190,7 +190,7 @@ E as dependências pode ser criadas de uma forma bastante simples e intuitiva qu Você verá exemplos disso nos próximos capítulos, acerca de bancos de dados relacionais e NoSQL, segurança, etc. -## Compatibilidade do **FastAPI** +## Compatibilidade do **FastAPI** { #fastapi-compatibility } A simplicidade do sistema de injeção de dependência do **FastAPI** faz ele compatível com: @@ -203,7 +203,7 @@ A simplicidade do sistema de injeção de dependência do **FastAPI** faz ele co * sistemas de injeção de dados de resposta * etc. -## Simples e Poderoso +## Simples e Poderoso { #simple-and-powerful } Mesmo que o sistema hierárquico de injeção de dependência seja simples de definir e utilizar, ele ainda é bastante poderoso. @@ -243,7 +243,7 @@ admin_user --> activate_user paying_user --> pro_items ``` -## Integração com **OpenAPI** +## Integração com **OpenAPI** { #integrated-with-openapi_1 } Todas essas dependências, ao declarar os requisitos para suas *operações de rota*, também adicionam parâmetros, validações, etc. diff --git a/docs/pt/docs/tutorial/dependencies/sub-dependencies.md b/docs/pt/docs/tutorial/dependencies/sub-dependencies.md index 3975ce182e..e2dc9bbf9a 100644 --- a/docs/pt/docs/tutorial/dependencies/sub-dependencies.md +++ b/docs/pt/docs/tutorial/dependencies/sub-dependencies.md @@ -1,4 +1,4 @@ -# Subdependências +# Subdependências { #sub-dependencies } Você pode criar dependências que possuem **subdependências**. @@ -6,9 +6,9 @@ Elas podem ter o nível de **profundidade** que você achar necessário. O **FastAPI** se encarrega de resolver essas dependências. -## Primeira dependência "injetável" +## Primeira dependência "dependable" { #first-dependency-dependable } -Você pode criar uma primeira dependência (injetável) dessa forma: +Você pode criar uma primeira dependência ("dependable") dessa forma: {* ../../docs_src/dependencies/tutorial005_an_py310.py hl[8:9] *} @@ -16,20 +16,20 @@ Esse código declara um parâmetro de consulta opcional, `q`, com o tipo `str`, Isso é bastante simples (e não muito útil), mas irá nos ajudar a focar em como as subdependências funcionam. -## Segunda dependência, "injetável" e "dependente" +## Segunda dependência, "dependable" e "dependente" { #second-dependency-dependable-and-dependant } -Então, você pode criar uma outra função para uma dependência (um "injetável") que ao mesmo tempo declara sua própria dependência (o que faz dela um "dependente" também): +Então, você pode criar uma outra função para uma dependência (um "dependable") que ao mesmo tempo declara sua própria dependência (o que faz dela um "dependente" também): {* ../../docs_src/dependencies/tutorial005_an_py310.py hl[13] *} Vamos focar nos parâmetros declarados: -* Mesmo que essa função seja uma dependência ("injetável") por si mesma, ela também declara uma outra dependência (ela "depende" de outra coisa). +* Mesmo que essa função seja uma dependência ("dependable") por si mesma, ela também declara uma outra dependência (ela "depende" de outra coisa). * Ela depende do `query_extractor`, e atribui o valor retornado pela função ao parâmetro `q`. * Ela também declara um cookie opcional `last_query`, do tipo `str`. * Se o usuário não passou nenhuma consulta `q`, a última consulta é utilizada, que foi salva em um cookie anteriormente. -## Utilizando a dependência +## Utilizando a dependência { #use-the-dependency } Então podemos utilizar a dependência com: @@ -54,7 +54,7 @@ read_query["/items/"] query_extractor --> query_or_cookie_extractor --> read_query ``` -## Utilizando a mesma dependência múltiplas vezes +## Utilizando a mesma dependência múltiplas vezes { #using-the-same-dependency-multiple-times } Se uma de suas dependências é declarada várias vezes para a mesma *operação de rota*, por exemplo, múltiplas dependências com uma mesma subdependência, o **FastAPI** irá chamar essa subdependência uma única vez para cada requisição. @@ -62,7 +62,7 @@ E o valor retornado é salvo em um ISO. +Então, um objeto `datetime` teria que ser convertido em um `str` contendo os dados no formato ISO. Da mesma forma, este banco de dados não receberia um modelo Pydantic (um objeto com atributos), apenas um `dict`. diff --git a/docs/pt/docs/tutorial/extra-data-types.md b/docs/pt/docs/tutorial/extra-data-types.md index 09c838be0a..97e4cc4757 100644 --- a/docs/pt/docs/tutorial/extra-data-types.md +++ b/docs/pt/docs/tutorial/extra-data-types.md @@ -1,4 +1,4 @@ -# Tipos de dados extras +# Tipos de dados extras { #extra-data-types } Até agora, você tem usado tipos de dados comuns, tais como: @@ -17,7 +17,7 @@ E você ainda terá os mesmos recursos que viu até agora: * Validação de dados. * Anotação e documentação automáticas. -## Outros tipos de dados +## Outros tipos de dados { #other-data-types } Aqui estão alguns dos tipos de dados adicionais que você pode usar: @@ -36,7 +36,7 @@ Aqui estão alguns dos tipos de dados adicionais que você pode usar: * `datetime.timedelta`: * O `datetime.timedelta` do Python. * Em requisições e respostas será representado como um `float` de segundos totais. - * O Pydantic também permite representá-lo como uma "codificação ISO 8601 diferença de tempo", cheque a documentação para mais informações. + * O Pydantic também permite representá-lo como uma "codificação ISO 8601 diferença de tempo", cheque a documentação para mais informações. * `frozenset`: * Em requisições e respostas, será tratado da mesma forma que um `set`: * Nas requisições, uma lista será lida, eliminando duplicadas e convertendo-a em um `set`. @@ -49,14 +49,14 @@ Aqui estão alguns dos tipos de dados adicionais que você pode usar: * `Decimal`: * O `Decimal` padrão do Python. * Em requisições e respostas será representado como um `float`. -* Você pode checar todos os tipos de dados válidos do Pydantic aqui: Tipos de dados do Pydantic. +* Você pode checar todos os tipos de dados válidos do Pydantic aqui: Tipos de dados do Pydantic. -## Exemplo +## Exemplo { #example } Aqui está um exemplo de *operação de rota* com parâmetros utilizando-se de alguns dos tipos acima. -{* ../../docs_src/extra_data_types/tutorial001.py hl[1,3,12:16] *} +{* ../../docs_src/extra_data_types/tutorial001_an_py310.py hl[1,3,12:16] *} Note que os parâmetros dentro da função tem seu tipo de dados natural, e você pode, por exemplo, realizar manipulações normais de data, como: -{* ../../docs_src/extra_data_types/tutorial001.py hl[18:19] *} +{* ../../docs_src/extra_data_types/tutorial001_an_py310.py hl[18:19] *} diff --git a/docs/pt/docs/tutorial/extra-models.md b/docs/pt/docs/tutorial/extra-models.md index cccef16e37..c0d22df573 100644 --- a/docs/pt/docs/tutorial/extra-models.md +++ b/docs/pt/docs/tutorial/extra-models.md @@ -1,4 +1,4 @@ -# Modelos Adicionais +# Modelos Adicionais { #extra-models } Continuando com o exemplo anterior, será comum ter mais de um modelo relacionado. @@ -6,9 +6,9 @@ Isso é especialmente o caso para modelos de usuários, porque: * O **modelo de entrada** precisa ser capaz de ter uma senha. * O **modelo de saída** não deve ter uma senha. -* O **modelo de banco de dados** provavelmente precisaria ter uma senha criptografada. +* O **modelo de banco de dados** provavelmente precisaria ter uma senha com hash. -/// danger +/// danger | Cuidado Nunca armazene senhas em texto simples dos usuários. Sempre armazene uma "hash segura" que você pode verificar depois. @@ -16,15 +16,23 @@ Se não souber, você aprenderá o que é uma "senha hash" nos [capítulos de se /// -## Múltiplos modelos +## Múltiplos modelos { #multiple-models } Aqui está uma ideia geral de como os modelos poderiam parecer com seus campos de senha e os lugares onde são usados: -{* ../../docs_src/extra_models/tutorial001.py hl[9,11,16,22,24,29:30,33:35,40:41] *} +{* ../../docs_src/extra_models/tutorial001_py310.py hl[7,9,14,20,22,27:28,31:33,38:39] *} -### Sobre `**user_in.dict()` +/// info | Informação -#### O `.dict()` do Pydantic +No Pydantic v1 o método se chamava `.dict()`, ele foi descontinuado (mas ainda é suportado) no Pydantic v2 e renomeado para `.model_dump()`. + +Os exemplos aqui usam `.dict()` por compatibilidade com o Pydantic v1, mas você deve usar `.model_dump()` se puder usar o Pydantic v2. + +/// + +### Sobre `**user_in.dict()` { #about-user-in-dict } + +#### O `.dict()` do Pydantic { #pydantics-dict } `user_in` é um modelo Pydantic da classe `UserIn`. @@ -61,7 +69,7 @@ teríamos um `dict` Python com: } ``` -#### Desembrulhando um `dict` +#### Desembrulhando um `dict` { #unpacking-a-dict } Se tomarmos um `dict` como `user_dict` e passarmos para uma função (ou classe) com `**user_dict`, o Python irá "desembrulhá-lo". Ele passará as chaves e valores do `user_dict` diretamente como argumentos chave-valor. @@ -93,7 +101,7 @@ UserInDB( ) ``` -#### Um modelo Pydantic a partir do conteúdo de outro +#### Um modelo Pydantic a partir do conteúdo de outro { #a-pydantic-model-from-the-contents-of-another } Como no exemplo acima, obtivemos o `user_dict` a partir do `user_in.dict()`, este código: @@ -108,11 +116,11 @@ seria equivalente a: UserInDB(**user_in.dict()) ``` -...porque `user_in.dict()` é um `dict`, e depois fazemos o Python "desembrulhá-lo" passando-o para UserInDB precedido por `**`. +...porque `user_in.dict()` é um `dict`, e depois fazemos o Python "desembrulhá-lo" passando-o para `UserInDB` precedido por `**`. Então, obtemos um modelo Pydantic a partir dos dados em outro modelo Pydantic. -#### Desembrulhando um `dict` e palavras-chave extras +#### Desembrulhando um `dict` e palavras-chave extras { #unpacking-a-dict-and-extra-keywords } E, então, adicionando o argumento de palavra-chave extra `hashed_password=hashed_password`, como em: @@ -132,13 +140,13 @@ UserInDB( ) ``` -/// warning +/// warning | Atenção -As funções adicionais de suporte são apenas para demonstração de um fluxo possível dos dados, mas é claro que elas não fornecem segurança real. +As funções adicionais de suporte `fake_password_hasher` e `fake_save_user` servem apenas para demonstrar um fluxo possível dos dados, mas é claro que elas não fornecem segurança real. /// -## Reduzir duplicação +## Reduzir duplicação { #reduce-duplication } Reduzir a duplicação de código é uma das ideias principais no **FastAPI**. @@ -154,25 +162,25 @@ Toda conversão de dados, validação, documentação, etc. ainda funcionará no Dessa forma, podemos declarar apenas as diferenças entre os modelos (com `password` em texto claro, com `hashed_password` e sem senha): -{* ../../docs_src/extra_models/tutorial002.py hl[9,15:16,19:20,23:24] *} +{* ../../docs_src/extra_models/tutorial002_py310.py hl[7,13:14,17:18,21:22] *} -## `Union` ou `anyOf` +## `Union` ou `anyOf` { #union-or-anyof } -Você pode declarar uma resposta como o `Union` de dois tipos, o que significa que a resposta seria qualquer um dos dois. +Você pode declarar uma resposta como o `Union` de dois ou mais tipos, o que significa que a resposta seria qualquer um deles. Isso será definido no OpenAPI com `anyOf`. -Para fazer isso, use a dica de tipo padrão do Python `typing.Union`: +Para fazer isso, use a anotação de tipo padrão do Python `typing.Union`: -/// note +/// note | Nota Ao definir um `Union`, inclua o tipo mais específico primeiro, seguido pelo tipo menos específico. No exemplo abaixo, o tipo mais específico `PlaneItem` vem antes de `CarItem` em `Union[PlaneItem, CarItem]`. /// -{* ../../docs_src/extra_models/tutorial003.py hl[1,14:15,18:20,33] *} +{* ../../docs_src/extra_models/tutorial003_py310.py hl[1,14:15,18:20,33] *} -### `Union` no Python 3.10 +### `Union` no Python 3.10 { #union-in-python-3-10 } Neste exemplo, passamos `Union[PlaneItem, CarItem]` como o valor do argumento `response_model`. @@ -184,27 +192,27 @@ Se estivesse em uma anotação de tipo, poderíamos ter usado a barra vertical, some_variable: PlaneItem | CarItem ``` -Mas se colocarmos isso em `response_model=PlaneItem | CarItem` teríamos um erro, pois o Python tentaria executar uma **operação inválida** entre `PlaneItem` e `CarItem` em vez de interpretar isso como uma anotação de tipo. +Mas se colocarmos isso na atribuição `response_model=PlaneItem | CarItem`, teríamos um erro, pois o Python tentaria executar uma **operação inválida** entre `PlaneItem` e `CarItem` em vez de interpretar isso como uma anotação de tipo. -## Lista de modelos +## Lista de modelos { #list-of-models } Da mesma forma, você pode declarar respostas de listas de objetos. Para isso, use o padrão Python `typing.List` (ou simplesmente `list` no Python 3.9 e superior): -{* ../../docs_src/extra_models/tutorial004.py hl[1,20] *} +{* ../../docs_src/extra_models/tutorial004_py39.py hl[18] *} -## Resposta com `dict` arbitrário +## Resposta com `dict` arbitrário { #response-with-arbitrary-dict } Você também pode declarar uma resposta usando um simples `dict` arbitrário, declarando apenas o tipo das chaves e valores, sem usar um modelo Pydantic. Isso é útil se você não souber os nomes de campo / atributo válidos (que seriam necessários para um modelo Pydantic) antecipadamente. -Neste caso, você pode usar `typing.Dict` (ou simplesmente dict no Python 3.9 e superior): +Neste caso, você pode usar `typing.Dict` (ou simplesmente `dict` no Python 3.9 e superior): -{* ../../docs_src/extra_models/tutorial005.py hl[1,8] *} +{* ../../docs_src/extra_models/tutorial005_py39.py hl[6] *} -## Em resumo +## Recapitulação { #recap } Use vários modelos Pydantic e herde livremente para cada caso. diff --git a/docs/pt/docs/tutorial/first-steps.md b/docs/pt/docs/tutorial/first-steps.md index e696bbbb7d..86cddde5da 100644 --- a/docs/pt/docs/tutorial/first-steps.md +++ b/docs/pt/docs/tutorial/first-steps.md @@ -1,8 +1,8 @@ -# Primeiros Passos +# Primeiros Passos { #first-steps } O arquivo FastAPI mais simples pode se parecer com: -{* ../../docs_src/first_steps/tutorial001.py *} +{* ../../docs_src/first_steps/tutorial001_py39.py *} Copie o conteúdo para um arquivo `main.py`. @@ -48,15 +48,15 @@ $ fastapi dev http://127.0.0.1:8000. @@ -66,7 +66,7 @@ Você verá essa resposta em JSON: {"message": "Hello World"} ``` -### Documentação Interativa de APIs +### Documentação Interativa de APIs { #interactive-api-docs } Agora vá para http://127.0.0.1:8000/docs. @@ -74,7 +74,7 @@ Você verá a documentação interativa automática da API (fornecida por http://127.0.0.1:8000/redoc. @@ -82,31 +82,31 @@ Você verá a documentação alternativa automática (fornecida por OpenAPI é uma especificação que determina como definir um *schema* da sua API. -Esta definição de *schema* inclui as rotas da sua API, os parâmetros possíveis que elas usam, etc. +Esta definição de *schema* inclui os paths da sua API, os parâmetros possíveis que eles usam, etc. -#### "*Schema*" de dados +#### "*Schema*" de dados { #data-schema } O termo "*schema*" também pode se referir à forma de alguns dados, como um conteúdo JSON. Nesse caso, significaria os atributos JSON e os tipos de dados que eles possuem, etc. -#### OpenAPI e JSON *Schema* +#### OpenAPI e JSON Schema { #openapi-and-json-schema } -OpenAPI define um *schema* de API para sua API. E esse *schema* inclui definições (ou "*schemas*") dos dados enviados e recebidos por sua API usando **JSON *Schema***, o padrão para *schemas* de dados JSON. +OpenAPI define um *schema* de API para sua API. E esse *schema* inclui definições (ou "*schemas*") dos dados enviados e recebidos por sua API usando **JSON Schema**, o padrão para *schemas* de dados JSON. -#### Verifique o `openapi.json` +#### Verifique o `openapi.json` { #check-the-openapi-json } Se você está curioso(a) sobre a aparência do *schema* bruto OpenAPI, o FastAPI gera automaticamente um JSON (*schema*) com as descrições de toda a sua API. @@ -116,7 +116,7 @@ Ele mostrará um JSON começando com algo como: ```JSON { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": { "title": "FastAPI", "version": "0.1.0" @@ -135,7 +135,7 @@ Ele mostrará um JSON começando com algo como: ... ``` -#### Para que serve o OpenAPI +#### Para que serve o OpenAPI { #what-is-openapi-for } O *schema* OpenAPI é o que possibilita os dois sistemas de documentação interativos mostrados. @@ -143,15 +143,51 @@ E existem dezenas de alternativas, todas baseadas em OpenAPI. Você pode facilme Você também pode usá-lo para gerar código automaticamente para clientes que se comunicam com sua API. Por exemplo, aplicativos front-end, móveis ou IoT. -## Recapitulando, passo a passo +### Faça o deploy da sua aplicação (opcional) { #deploy-your-app-optional } -### Passo 1: importe `FastAPI` +Você pode, opcionalmente, fazer o deploy da sua aplicação FastAPI na FastAPI Cloud; acesse e entre na lista de espera, se ainda não entrou. 🚀 -{* ../../docs_src/first_steps/tutorial001.py hl[1] *} +Se você já tem uma conta na **FastAPI Cloud** (nós convidamos você da lista de espera 😉), pode fazer o deploy da sua aplicação com um único comando. + +Antes do deploy, certifique-se de que está autenticado: + +
+ +```console +$ fastapi login + +You are logged in to FastAPI Cloud 🚀 +``` + +
+ +Em seguida, faça o deploy da sua aplicação: + +
+ +```console +$ fastapi deploy + +Deploying to FastAPI Cloud... + +✅ Deployment successful! + +🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev +``` + +
+ +É isso! Agora você pode acessar sua aplicação nessa URL. ✨ + +## Recapitulando, passo a passo { #recap-step-by-step } + +### Passo 1: importe `FastAPI` { #step-1-import-fastapi } + +{* ../../docs_src/first_steps/tutorial001_py39.py hl[1] *} `FastAPI` é uma classe Python que fornece todas as funcionalidades para sua API. -/// note | Detalhes técnicos +/// note | Detalhes Técnicos `FastAPI` é uma classe que herda diretamente de `Starlette`. @@ -159,19 +195,19 @@ Você pode usar todas as funcionalidades do operador get
+* o path `/` +* usando uma operação get -/// info | `@decorador` +/// info | Informações sobre `@decorator` Essa sintaxe `@alguma_coisa` em Python é chamada de "decorador". @@ -245,9 +281,9 @@ Você o coloca em cima de uma função. Como um chapéu decorativo (acho que é Um "decorador" pega a função abaixo e faz algo com ela. -Em nosso caso, este decorador informa ao **FastAPI** que a função abaixo corresponde a **rota** `/` com uma **operação** `get`. +Em nosso caso, este decorador informa ao **FastAPI** que a função abaixo corresponde ao **path** `/` com uma **operação** `get`. -É o "**decorador de rota**". +É o "**decorador de operação de rota**". /// @@ -276,48 +312,69 @@ Por exemplo, ao usar GraphQL, você normalmente executa todas as ações usando /// -### Passo 4: defina uma **função de rota** +### Passo 4: defina a função de operação de rota { #step-4-define-the-path-operation-function } -Esta é a nossa "**função de rota**": +Esta é a nossa "**função de operação de rota**": -* **rota**: é `/`. +* **path**: é `/`. * **operação**: é `get`. * **função**: é a função abaixo do "decorador" (abaixo do `@app.get("/")`). -{* ../../docs_src/first_steps/tutorial001.py hl[7] *} +{* ../../docs_src/first_steps/tutorial001_py39.py hl[7] *} Esta é uma função Python. -Ela será chamada pelo **FastAPI** sempre que receber uma requisição para a URL "`/ `" usando uma operação `GET`. +Ela será chamada pelo **FastAPI** sempre que receber uma requisição para a URL "`/`" usando uma operação `GET`. -Neste caso, é uma função `assíncrona`. +Neste caso, é uma função `async`. --- Você também pode defini-la como uma função normal em vez de `async def`: -{* ../../docs_src/first_steps/tutorial003.py hl[7] *} +{* ../../docs_src/first_steps/tutorial003_py39.py hl[7] *} /// note | Nota -Se você não sabe a diferença, verifique o [Async: *"Com pressa?"*](../async.md#com-pressa){.internal-link target=_blank}. +Se você não sabe a diferença, verifique o [Async: *"Com pressa?"*](../async.md#in-a-hurry){.internal-link target=_blank}. /// -### Passo 5: retorne o conteúdo +### Passo 5: retorne o conteúdo { #step-5-return-the-content } -{* ../../docs_src/first_steps/tutorial001.py hl[8] *} +{* ../../docs_src/first_steps/tutorial001_py39.py hl[8] *} Você pode retornar um `dict`, `list` e valores singulares como `str`, `int`, etc. -Você também pode devolver modelos Pydantic (você verá mais sobre isso mais tarde). +Você também pode devolver modelos Pydantic ( você verá mais sobre isso mais tarde). Existem muitos outros objetos e modelos que serão convertidos automaticamente para JSON (incluindo ORMs, etc). Tente usar seus favoritos, é altamente provável que já sejam compatíveis. -## Recapitulando +### Passo 6: Faça o deploy { #step-6-deploy-it } + +Faça o deploy da sua aplicação para a **FastAPI Cloud** com um comando: `fastapi deploy`. 🎉 + +#### Sobre o FastAPI Cloud { #about-fastapi-cloud } + +A **FastAPI Cloud** é construída pelo mesmo autor e equipe por trás do **FastAPI**. + +Ela simplifica o processo de **construir**, **fazer deploy** e **acessar** uma API com o mínimo de esforço. + +Traz a mesma **experiência do desenvolvedor** de criar aplicações com FastAPI para **fazer o deploy** delas na nuvem. 🎉 + +A FastAPI Cloud é a principal patrocinadora e financiadora dos projetos open source do ecossistema *FastAPI and friends*. ✨ + +#### Faça o deploy em outros provedores de nuvem { #deploy-to-other-cloud-providers } + +FastAPI é open source e baseado em padrões. Você pode fazer deploy de aplicações FastAPI em qualquer provedor de nuvem que preferir. + +Siga os tutoriais do seu provedor de nuvem para fazer deploy de aplicações FastAPI com eles. 🤓 + +## Recapitulando { #recap } * Importe `FastAPI`. * Crie uma instância do `app`. -* Coloque o **decorador que define a operação** (como `@app.get("/")`). -* Escreva uma **função para a operação da rota** (como `def root(): ...`) abaixo. -* Execute o servidor de desenvolvimento (como `uvicorn main:app --reload`). +* Escreva um **decorador de operação de rota** usando decoradores como `@app.get("/")`. +* Defina uma **função de operação de rota**; por exemplo, `def root(): ...`. +* Execute o servidor de desenvolvimento usando o comando `fastapi dev`. +* Opcionalmente, faça o deploy da sua aplicação com `fastapi deploy`. diff --git a/docs/pt/docs/tutorial/handling-errors.md b/docs/pt/docs/tutorial/handling-errors.md index 5cb92c744f..1c162c205f 100644 --- a/docs/pt/docs/tutorial/handling-errors.md +++ b/docs/pt/docs/tutorial/handling-errors.md @@ -1,4 +1,4 @@ -# Manipulação de erros +# Manipulação de erros { #handling-errors } Há diversas situações em que você precisa notificar um erro a um cliente que está utilizando a sua API. @@ -11,7 +11,6 @@ Pode ser que você precise comunicar ao cliente que: * O item que o cliente está tentando acessar não existe. * etc. - Nesses casos, você normalmente retornaria um **HTTP status code** próximo ao status code na faixa do status code **400** (do 400 ao 499). Isso é bastante similar ao caso do HTTP status code 200 (do 200 ao 299). Esses "200" status codes significam que, de algum modo, houve sucesso na requisição. @@ -20,15 +19,15 @@ Os status codes na faixa dos 400 significam que houve um erro por parte do clien Você se lembra de todos aqueles erros (e piadas) a respeito do "**404 Not Found**"? -## Use o `HTTPException` +## Use o `HTTPException` { #use-httpexception } Para retornar ao cliente *responses* HTTP com erros, use o `HTTPException`. -### Import `HTTPException` +### Import `HTTPException` { #import-httpexception } -{* ../../docs_src/handling_errors/tutorial001.py hl[1] *} +{* ../../docs_src/handling_errors/tutorial001_py39.py hl[1] *} -### Lance o `HTTPException` no seu código. +### Lance o `HTTPException` no seu código { #raise-an-httpexception-in-your-code } `HTTPException`, ao fundo, nada mais é do que a conjunção entre uma exceção comum do Python e informações adicionais relevantes para APIs. @@ -40,15 +39,14 @@ O benefício de lançar uma exceção em vez de retornar um valor ficará mais e Neste exemplo, quando o cliente pede, na requisição, por um item cujo ID não existe, a exceção com o status code `404` é lançada: -{* ../../docs_src/handling_errors/tutorial001.py hl[11] *} - -### A response resultante +{* ../../docs_src/handling_errors/tutorial001_py39.py hl[11] *} +### A response resultante { #the-resulting-response } Se o cliente faz uma requisição para `http://example.com/items/foo` (um `item_id` `"foo"`), esse cliente receberá um HTTP status code 200, e uma resposta JSON: -``` +```JSON { "item": "The Foo Wrestlers" } @@ -71,7 +69,7 @@ Esses tipos de dados são manipulados automaticamente pelo **FastAPI** e convert /// -## Adicione headers customizados +## Adicione headers customizados { #add-custom-headers } Há certas situações em que é bastante útil poder adicionar headers customizados no HTTP error. Exemplo disso seria adicionar headers customizados para tipos de segurança. @@ -79,17 +77,17 @@ Você provavelmente não precisará utilizar esses headers diretamente no seu c Mas caso você precise, para um cenário mais complexo, você pode adicionar headers customizados: -{* ../../docs_src/handling_errors/tutorial002.py hl[14] *} +{* ../../docs_src/handling_errors/tutorial002_py39.py hl[14] *} -## Instalando manipuladores de exceções customizados +## Instale manipuladores de exceções customizados { #install-custom-exception-handlers } -Você pode adicionar manipuladores de exceção customizados com a mesma seção de utilidade de exceções presentes no Starlette +Você pode adicionar manipuladores de exceção customizados com a mesma seção de utilidade de exceções presentes no Starlette. Digamos que você tenha uma exceção customizada `UnicornException` que você (ou uma biblioteca que você use) precise lançar (`raise`). Nesse cenário, se você precisa manipular essa exceção de modo global com o FastAPI, você pode adicionar um manipulador de exceção customizada com `@app.exception_handler()`. -{* ../../docs_src/handling_errors/tutorial003.py hl[5:7,13:18,24] *} +{* ../../docs_src/handling_errors/tutorial003_py39.py hl[5:7,13:18,24] *} Nesse cenário, se você fizer uma requisição para `/unicorns/yolo`, a *operação de caminho* vai lançar (`raise`) o `UnicornException`. @@ -103,27 +101,31 @@ Dessa forma você receberá um erro "limpo", com o HTTP status code `418` e um J /// note | Detalhes Técnicos -Você também pode usar `from starlette.requests import Request` and `from starlette.responses import JSONResponse`. +Você também pode usar `from starlette.requests import Request` e `from starlette.responses import JSONResponse`. **FastAPI** disponibiliza o mesmo `starlette.responses` através do `fastapi.responses` por conveniência ao desenvolvedor. Contudo, a maior parte das respostas disponíveis vem diretamente do Starlette. O mesmo acontece com o `Request`. /// -## Sobrescreva o manipulador padrão de exceções +## Sobrescreva os manipuladores de exceções padrão { #override-the-default-exception-handlers } **FastAPI** tem alguns manipuladores padrão de exceções. -Esses manipuladores são os responsáveis por retornar o JSON padrão de respostas quando você lança (`raise`) o `HTTPException` e quando a requisição tem dados invalidos. +Esses manipuladores são os responsáveis por retornar o JSON padrão de respostas quando você lança (`raise`) o `HTTPException` e quando a requisição tem dados inválidos. Você pode sobrescrever esses manipuladores de exceção com os seus próprios manipuladores. -## Sobrescreva exceções de validação da requisição +### Sobrescreva exceções de validação da requisição { #override-request-validation-exceptions } Quando a requisição contém dados inválidos, **FastAPI** internamente lança para o `RequestValidationError`. +E também inclui um manipulador de exceções padrão para ele. + Para sobrescrevê-lo, importe o `RequestValidationError` e use-o com o `@app.exception_handler(RequestValidationError)` para decorar o manipulador de exceções. -{* ../../docs_src/handling_errors/tutorial004.py hl[2,14:16] *} +O manipulador de exceções receberá um `Request` e a exceção. + +{* ../../docs_src/handling_errors/tutorial004_py39.py hl[2,14:19] *} Se você for ao `/items/foo`, em vez de receber o JSON padrão com o erro: @@ -145,34 +147,17 @@ Se você for ao `/items/foo`, em vez de receber o JSON padrão com o erro: você receberá a versão em texto: ``` -1 validation error -path -> item_id - value is not a valid integer (type=type_error.integer) +Validation errors: +Field: ('path', 'item_id'), Error: Input should be a valid integer, unable to parse string as an integer ``` -### `RequestValidationError` vs `ValidationError` +### Sobrescreva o manipulador de erro `HTTPException` { #override-the-httpexception-error-handler } -/// warning | Aviso - -Você pode pular estes detalhes técnicos caso eles não sejam importantes para você neste momento. - -/// - -`RequestValidationError` é uma subclasse do `ValidationError` existente no Pydantic. - -**FastAPI** faz uso dele para que você veja o erro no seu log, caso você utilize um modelo de Pydantic em `response_model`, e seus dados tenham erro. - -Contudo, o cliente ou usuário não terão acesso a ele. Ao contrário, o cliente receberá um "Internal Server Error" com o HTTP status code `500`. - -E assim deve ser porque seria um bug no seu código ter o `ValidationError` do Pydantic na sua *response*, ou em qualquer outro lugar do seu código (que não na requisição do cliente). - -E enquanto você conserta o bug, os clientes / usuários não deveriam ter acesso às informações internas do erro, porque, desse modo, haveria exposição de uma vulnerabilidade de segurança. - -Do mesmo modo, você pode sobreescrever o `HTTPException`. +Do mesmo modo, você pode sobrescrever o `HTTPException`. Por exemplo, você pode querer retornar uma *response* em *plain text* ao invés de um JSON para os seguintes erros: -{* ../../docs_src/handling_errors/tutorial004.py hl[3:4,9:11,22] *} +{* ../../docs_src/handling_errors/tutorial004_py39.py hl[3:4,9:11,25] *} /// note | Detalhes Técnicos @@ -182,11 +167,21 @@ Você pode usar `from starlette.responses import PlainTextResponse`. /// -### Use o body do `RequestValidationError`. +/// warning | Atenção + +Tenha em mente que o `RequestValidationError` contém as informações do nome do arquivo e da linha onde o erro de validação acontece, para que você possa mostrá-las nos seus logs com as informações relevantes, se quiser. + +Mas isso significa que, se você simplesmente convertê-lo para uma string e retornar essa informação diretamente, você pode acabar vazando um pouco de informação sobre o seu sistema; por isso, aqui o código extrai e mostra cada erro de forma independente. + +/// + +### Use o body do `RequestValidationError` { #use-the-requestvalidationerror-body } O `RequestValidationError` contém o `body` que ele recebeu de dados inválidos. -Você pode utilizá-lo enquanto desenvolve seu app para conectar o *body* e debugá-lo, e assim retorná-lo ao usuário, etc. +Você pode utilizá-lo enquanto desenvolve seu app para registrar o *body* e debugá-lo, e assim retorná-lo ao usuário, etc. + +{* ../../docs_src/handling_errors/tutorial005_py39.py hl[14] *} Tente enviar um item inválido como este: @@ -197,7 +192,7 @@ Tente enviar um item inválido como este: } ``` -Você receberá uma *response* informando-o de que a data é inválida, e contendo o *body* recebido: +Você receberá uma *response* informando-o de que os dados são inválidos, e contendo o *body* recebido: ```JSON hl_lines="12-15" { @@ -218,30 +213,30 @@ Você receberá uma *response* informando-o de que a data é inválida, e conten } ``` -#### O `HTTPException` do FastAPI vs o `HTTPException` do Starlette. +#### O `HTTPException` do FastAPI vs o `HTTPException` do Starlette { #fastapis-httpexception-vs-starlettes-httpexception } O **FastAPI** tem o seu próprio `HTTPException`. E a classe de erro `HTTPException` do **FastAPI** herda da classe de erro do `HTTPException` do Starlette. -A diferença entre os dois é a de que o `HTTPException` do **FastAPI** permite que você adicione *headers* que serão incluídos nas *responses*. - -Esses *headers* são necessários/utilizados internamente pelo OAuth 2.0 e também por outras utilidades de segurança. +A única diferença é que o `HTTPException` do **FastAPI** aceita qualquer dado que possa ser convertido em JSON para o campo `detail`, enquanto o `HTTPException` do Starlette aceita apenas strings para esse campo. Portanto, você pode continuar lançando o `HTTPException` do **FastAPI** normalmente no seu código. Porém, quando você registrar um manipulador de exceção, você deve registrá-lo através do `HTTPException` do Starlette. -Dessa forma, se qualquer parte do código interno, extensão ou plug-in do Starlette lançar o `HTTPException`, o seu manipulador de exceção poderá capturar esse lançamento e tratá-lo. +Dessa forma, se qualquer parte do código interno, extensão ou plug-in do Starlette lançar um `HTTPException` do Starlette, o seu manipulador poderá capturar e tratá-lo. + +Neste exemplo, para poder ter ambos os `HTTPException` no mesmo código, a exceção do Starlette é renomeada para `StarletteHTTPException`: ```Python from starlette.exceptions import HTTPException as StarletteHTTPException ``` -### Re-use os manipulares de exceção do **FastAPI** +### Reutilize os manipuladores de exceção do **FastAPI** { #reuse-fastapis-exception-handlers } Se você quer usar a exceção em conjunto com o mesmo manipulador de exceção *default* do **FastAPI**, você pode importar e re-usar esses manipuladores de exceção do `fastapi.exception_handlers`: -{* ../../docs_src/handling_errors/tutorial006.py hl[2:5,15,21] *} +{* ../../docs_src/handling_errors/tutorial006_py39.py hl[2:5,15,21] *} Nesse exemplo você apenas imprime (`print`) o erro com uma mensagem expressiva. Mesmo assim, dá para pegar a ideia. Você pode usar a exceção e então apenas re-usar o manipulador de exceção *default*. diff --git a/docs/pt/docs/tutorial/header-param-models.md b/docs/pt/docs/tutorial/header-param-models.md index 9a88dbfec4..046c99c291 100644 --- a/docs/pt/docs/tutorial/header-param-models.md +++ b/docs/pt/docs/tutorial/header-param-models.md @@ -1,8 +1,8 @@ -# Modelos de Parâmetros do Cabeçalho +# Modelos de Parâmetros do Cabeçalho { #header-parameter-models } Se você possui um grupo de **parâmetros de cabeçalho** relacionados, você pode criar um **modelo do Pydantic** para declará-los. -Isso vai lhe permitir **reusar o modelo** em **múltiplos lugares** e também declarar validações e metadadados para todos os parâmetros de uma vez. 😎 +Isso vai lhe permitir **reusar o modelo** em **múltiplos lugares** e também declarar validações e metadados para todos os parâmetros de uma vez. 😎 /// note | Nota @@ -10,7 +10,7 @@ Isso é possível desde a versão `0.115.0` do FastAPI. 🤓 /// -## Parâmetros do Cabeçalho com um Modelo Pydantic +## Parâmetros do Cabeçalho com um Modelo Pydantic { #header-parameters-with-a-pydantic-model } Declare os **parâmetros de cabeçalho** que você precisa em um **modelo do Pydantic**, e então declare o parâmetro como `Header`: @@ -18,7 +18,7 @@ Declare os **parâmetros de cabeçalho** que você precisa em um **modelo do Pyd O **FastAPI** irá **extrair** os dados de **cada campo** a partir dos **cabeçalhos** da requisição e te retornará o modelo do Pydantic que você definiu. -### Checando a documentação +## Checando a documentação { #check-the-docs } Você pode ver os headers necessários na interface gráfica da documentação em `/docs`: @@ -26,7 +26,7 @@ Você pode ver os headers necessários na interface gráfica da documentação e
-### Proibindo Cabeçalhos adicionais +## Proibindo Cabeçalhos adicionais { #forbid-extra-headers } Em alguns casos de uso especiais (provavelmente não muito comuns), você pode querer **restringir** os cabeçalhos que você quer receber. @@ -51,6 +51,22 @@ Por exemplo, se o cliente tentar enviar um cabeçalho `tool` com o valor `plumbu } ``` -## Resumo +## Desativar conversão de underscores { #disable-convert-underscores } + +Da mesma forma que com parâmetros de cabeçalho normais, quando você tem caracteres de sublinhado nos nomes dos parâmetros, eles são **automaticamente convertidos em hifens**. + +Por exemplo, se você tem um parâmetro de cabeçalho `save_data` no código, o cabeçalho HTTP esperado será `save-data`, e ele aparecerá assim na documentação. + +Se por algum motivo você precisar desativar essa conversão automática, também poderá fazê-lo para modelos do Pydantic para parâmetros de cabeçalho. + +{* ../../docs_src/header_param_models/tutorial003_an_py310.py hl[19] *} + +/// warning | Atenção + +Antes de definir `convert_underscores` como `False`, tenha em mente que alguns proxies e servidores HTTP não permitem o uso de cabeçalhos com sublinhados. + +/// + +## Resumo { #summary } Você pode utilizar **modelos do Pydantic** para declarar **cabeçalhos** no **FastAPI**. 😎 diff --git a/docs/pt/docs/tutorial/header-params.md b/docs/pt/docs/tutorial/header-params.md index d071bcc358..a0deb98bee 100644 --- a/docs/pt/docs/tutorial/header-params.md +++ b/docs/pt/docs/tutorial/header-params.md @@ -1,20 +1,20 @@ -# Parâmetros de Cabeçalho +# Parâmetros de Cabeçalho { #header-parameters } Você pode definir parâmetros de Cabeçalho da mesma maneira que define paramêtros com `Query`, `Path` e `Cookie`. -## importe `Header` +## Importe `Header` { #import-header } Primeiro importe `Header`: -{* ../../docs_src/header_params/tutorial001_py310.py hl[1] *} +{* ../../docs_src/header_params/tutorial001_an_py310.py hl[3] *} -## Declare parâmetros de `Header` +## Declare parâmetros de `Header` { #declare-header-parameters } Então declare os paramêtros de cabeçalho usando a mesma estrutura que em `Path`, `Query` e `Cookie`. O primeiro valor é o valor padrão, você pode passar todas as validações adicionais ou parâmetros de anotação: -{* ../../docs_src/header_params/tutorial001_py310.py hl[7] *} +{* ../../docs_src/header_params/tutorial001_an_py310.py hl[9] *} /// note | Detalhes Técnicos @@ -24,13 +24,13 @@ Mas lembre-se que quando você importa `Query`, `Path`, `Header`, e outras de `f /// -/// info +/// info | Informação Para declarar headers, você precisa usar `Header`, caso contrário, os parâmetros seriam interpretados como parâmetros de consulta. /// -## Conversão automática +## Conversão automática { #automatic-conversion } `Header` tem algumas funcionalidades a mais em relação a `Path`, `Query` e `Cookie`. @@ -46,15 +46,15 @@ Portanto, você pode usar `user_agent` como faria normalmente no código Python, Se por algum motivo você precisar desabilitar a conversão automática de sublinhados para hífens, defina o parâmetro `convert_underscores` de `Header` para `False`: -{* ../../docs_src/header_params/tutorial002_py310.py hl[8] *} +{* ../../docs_src/header_params/tutorial002_an_py310.py hl[10] *} -/// warning | Aviso +/// warning | Atenção Antes de definir `convert_underscores` como `False`, lembre-se de que alguns proxies e servidores HTTP não permitem o uso de cabeçalhos com sublinhados. /// -## Cabeçalhos duplicados +## Cabeçalhos duplicados { #duplicate-headers } É possível receber cabeçalhos duplicados. Isso significa, o mesmo cabeçalho com vários valores. @@ -64,9 +64,9 @@ Você receberá todos os valores do cabeçalho duplicado como uma `list` Python. Por exemplo, para declarar um cabeçalho de `X-Token` que pode aparecer mais de uma vez, você pode escrever: -{* ../../docs_src/header_params/tutorial003_py310.py hl[7] *} +{* ../../docs_src/header_params/tutorial003_an_py310.py hl[9] *} -Se você se comunicar com essa *operação de caminho* enviando dois cabeçalhos HTTP como: +Se você se comunicar com essa *operação de rota* enviando dois cabeçalhos HTTP como: ``` X-Token: foo @@ -84,8 +84,8 @@ A resposta seria como: } ``` -## Recapitulando +## Recapitulando { #recap } Declare cabeçalhos com `Header`, usando o mesmo padrão comum que utiliza-se em `Query`, `Path` e `Cookie`. -E não se preocupe com sublinhados em suas variáveis, FastAPI cuidará da conversão deles. +E não se preocupe com sublinhados em suas variáveis, **FastAPI** cuidará da conversão deles. diff --git a/docs/pt/docs/tutorial/index.md b/docs/pt/docs/tutorial/index.md index 7c04b17f2e..cd7dd88fe9 100644 --- a/docs/pt/docs/tutorial/index.md +++ b/docs/pt/docs/tutorial/index.md @@ -1,4 +1,4 @@ -# Tutorial - Guia de Usuário +# Tutorial - Guia de Usuário { #tutorial-user-guide } Esse tutorial mostra como usar o **FastAPI** com a maior parte de seus recursos, passo a passo. @@ -6,11 +6,11 @@ Cada seção constrói, gradualmente, sobre as anteriores, mas sua estrutura sã Ele também foi construído para servir como uma referência futura, então você pode voltar e ver exatamente o que você precisa. -## Rode o código +## Rode o código { #run-the-code } Todos os blocos de código podem ser copiados e utilizados diretamente (eles são, na verdade, arquivos Python testados). -Para rodar qualquer um dos exemplos, copie o codigo para um arquivo `main.py`, e inicie o `uvivorn` com: +Para rodar qualquer um dos exemplos, copie o código para um arquivo `main.py`, e inicie o `fastapi dev` com:
@@ -54,15 +54,15 @@ $ fastapi dev @@ -76,17 +76,19 @@ $ pip install "fastapi[standard]" /// note | Nota -Quando você instala com pip install "fastapi[standard]", ele vem com algumas dependências opcionais padrão. +Quando você instala com `pip install "fastapi[standard]"`, ele vem com algumas dependências opcionais padrão, incluindo `fastapi-cloud-cli`, que permite fazer deploy na FastAPI Cloud. -Se você não quiser ter essas dependências opcionais, pode instalar pip install fastapi em vez disso. +Se você não quiser ter essas dependências opcionais, pode instalar `pip install fastapi` em vez disso. + +Se você quiser instalar as dependências padrão, mas sem o `fastapi-cloud-cli`, você pode instalar com `pip install "fastapi[standard-no-fastapi-cloud-cli]"`. /// -## Guia Avançado de Usuário +## Guia Avançado de Usuário { #advanced-user-guide } Há também um **Guia Avançado de Usuário** que você pode ler após esse **Tutorial - Guia de Usuário**. -O **Guia Avançado de Usuário** constrói sobre esse, usa os mesmos conceitos e te ensina alguns recursos extras. +O **Guia Avançado de Usuário** constrói sobre esse, usa os mesmos conceitos e te ensina algumas funcionalidades extras. Mas você deveria ler primeiro o **Tutorial - Guia de Usuário** (que você está lendo agora). diff --git a/docs/pt/docs/tutorial/metadata.md b/docs/pt/docs/tutorial/metadata.md index 57effb3ff3..df77e56484 100644 --- a/docs/pt/docs/tutorial/metadata.md +++ b/docs/pt/docs/tutorial/metadata.md @@ -1,8 +1,8 @@ -# Metadados e Urls de Documentos +# Metadados e Urls de Documentos { #metadata-and-docs-urls } Você pode personalizar várias configurações de metadados na sua aplicação **FastAPI**. -## Metadados para API +## Metadados para API { #metadata-for-api } Você pode definir os seguintes campos que são usados na especificação OpenAPI e nas interfaces automáticas de documentação da API: @@ -18,7 +18,7 @@ Você pode definir os seguintes campos que são usados na especificação OpenAP Você pode defini-los da seguinte maneira: -{* ../../docs_src/metadata/tutorial001.py hl[3:16,19:32] *} +{* ../../docs_src/metadata/tutorial001_py39.py hl[3:16, 19:32] *} /// tip | Dica @@ -30,15 +30,15 @@ Com essa configuração, a documentação automática da API se pareceria com: -## Identificador de Licença +## Identificador de Licença { #license-identifier } Desde o OpenAPI 3.1.0 e FastAPI 0.99.0, você também pode definir o license_info com um identifier em vez de uma url. Por exemplo: -{* ../../docs_src/metadata/tutorial001_1.py hl[31] *} +{* ../../docs_src/metadata/tutorial001_1_py39.py hl[31] *} -## Metadados para tags +## Metadados para tags { #metadata-for-tags } Você também pode adicionar metadados adicionais para as diferentes tags usadas para agrupar suas operações de rota com o parâmetro `openapi_tags`. @@ -52,13 +52,13 @@ Cada dicionário pode conter: * `description`: uma `str` com uma breve descrição da documentação externa. * `url` (**obrigatório**): uma `str` com a URL da documentação externa. -### Criar Metadados para tags +### Criar Metadados para tags { #create-metadata-for-tags } Vamos tentar isso em um exemplo com tags para `users` e `items`. Crie metadados para suas tags e passe-os para o parâmetro `openapi_tags`: -{* ../../docs_src/metadata/tutorial004.py hl[3:16,18] *} +{* ../../docs_src/metadata/tutorial004_py39.py hl[3:16,18] *} Observe que você pode usar Markdown dentro das descrições. Por exemplo, "login" será exibido em negrito (**login**) e "fancy" será exibido em itálico (_fancy_). @@ -68,31 +68,31 @@ Você não precisa adicionar metadados para todas as tags que você usa. /// -### Use suas tags +### Use suas tags { #use-your-tags } Use o parâmetro `tags` com suas *operações de rota* (e `APIRouter`s) para atribuí-los a diferentes tags: -{* ../../docs_src/metadata/tutorial004.py hl[21,26] *} +{* ../../docs_src/metadata/tutorial004_py39.py hl[21,26] *} -/// info | Informação +/// info | Informação -Leia mais sobre tags em [Configuração de Operação de Caminho](path-operation-configuration.md#tags){.internal-link target=_blank}. +Leia mais sobre tags em [Configuração de operação de rota](path-operation-configuration.md#tags){.internal-link target=_blank}. /// -### Cheque os documentos +### Cheque os documentos { #check-the-docs } Agora, se você verificar a documentação, ela exibirá todos os metadados adicionais: -### Ordem das tags +### Ordem das tags { #order-of-tags } A ordem de cada dicionário de metadados de tag também define a ordem exibida na interface de documentação. Por exemplo, embora `users` apareça após `items` em ordem alfabética, ele é exibido antes deles, porque adicionamos seus metadados como o primeiro dicionário na lista. -## URL da OpenAPI +## URL da OpenAPI { #openapi-url } Por padrão, o esquema OpenAPI é servido em `/openapi.json`. @@ -100,11 +100,11 @@ Mas você pode configurá-lo com o parâmetro `openapi_url`. Por exemplo, para defini-lo para ser servido em `/api/v1/openapi.json`: -{* ../../docs_src/metadata/tutorial002.py hl[3] *} +{* ../../docs_src/metadata/tutorial002_py39.py hl[3] *} Se você quiser desativar completamente o esquema OpenAPI, pode definir `openapi_url=None`, o que também desativará as interfaces de documentação que o utilizam. -## URLs da Documentação +## URLs da Documentação { #docs-urls } Você pode configurar as duas interfaces de documentação incluídas: @@ -117,4 +117,4 @@ Você pode configurar as duas interfaces de documentação incluídas: Por exemplo, para definir o Swagger UI para ser servido em `/documentation` e desativar o ReDoc: -{* ../../docs_src/metadata/tutorial003.py hl[3] *} +{* ../../docs_src/metadata/tutorial003_py39.py hl[3] *} diff --git a/docs/pt/docs/tutorial/middleware.md b/docs/pt/docs/tutorial/middleware.md index 0f5009b6db..b49c1eaa11 100644 --- a/docs/pt/docs/tutorial/middleware.md +++ b/docs/pt/docs/tutorial/middleware.md @@ -1,4 +1,4 @@ -# Middleware +# Middleware { #middleware } Você pode adicionar middleware à suas aplicações **FastAPI**. @@ -11,15 +11,15 @@ Um "middleware" é uma função que manipula cada **requisição** antes de ser * Ele pode fazer algo com essa **resposta** ou executar qualquer código necessário. * Então ele retorna a **resposta**. -/// note | Detalhes técnicos +/// note | Detalhes Técnicos Se você tiver dependências com `yield`, o código de saída será executado *depois* do middleware. -Se houver alguma tarefa em segundo plano (documentada posteriormente), ela será executada *depois* de todo o middleware. +Se houver alguma tarefa em segundo plano (abordada na seção [Tarefas em segundo plano](background-tasks.md){.internal-link target=_blank}, que você verá mais adiante), ela será executada *depois* de todo o middleware. /// -## Criar um middleware +## Criar um middleware { #create-a-middleware } Para criar um middleware, use o decorador `@app.middleware("http")` logo acima de uma função. @@ -31,17 +31,17 @@ A função middleware recebe: * Então ela retorna a `response` gerada pela *operação de rota* correspondente. * Você pode então modificar ainda mais o `response` antes de retorná-lo. -{* ../../docs_src/middleware/tutorial001.py hl[8:9,11,14] *} +{* ../../docs_src/middleware/tutorial001_py39.py hl[8:9,11,14] *} /// tip | Dica -Tenha em mente que cabeçalhos proprietários personalizados podem ser adicionados usando o prefixo 'X-'. +Tenha em mente que cabeçalhos proprietários personalizados podem ser adicionados usando o prefixo `X-`. Mas se você tiver cabeçalhos personalizados desejando que um cliente em um navegador esteja apto a ver, você precisa adicioná-los às suas configurações CORS ([CORS (Cross-Origin Resource Sharing)](cors.md){.internal-link target=_blank}) usando o parâmetro `expose_headers` documentado em Documentos CORS da Starlette. /// -/// note | Detalhes técnicos +/// note | Detalhes Técnicos Você também pode usar `from starlette.requests import Request`. @@ -49,7 +49,7 @@ Você também pode usar `from starlette.requests import Request`. /// -### Antes e depois da `response` +### Antes e depois da `response` { #before-and-after-the-response } Você pode adicionar código para ser executado com a `request`, antes que qualquer *operação de rota* o receba. @@ -57,9 +57,38 @@ E também depois que a `response` é gerada, antes de retorná-la. Por exemplo, você pode adicionar um cabeçalho personalizado `X-Process-Time` contendo o tempo em segundos que levou para processar a solicitação e gerar uma resposta: -{* ../../docs_src/middleware/tutorial001.py hl[10,12:13] *} +{* ../../docs_src/middleware/tutorial001_py39.py hl[10,12:13] *} -## Outros middlewares +/// tip | Dica + +Aqui usamos `time.perf_counter()` em vez de `time.time()` porque ele pode ser mais preciso para esses casos de uso. 🤓 + +/// + +## Ordem de execução de múltiplos middlewares { #multiple-middleware-execution-order } + +Quando você adiciona múltiplos middlewares usando o decorador `@app.middleware()` ou o método `app.add_middleware()`, cada novo middleware envolve a aplicação, formando uma pilha. O último middleware adicionado é o mais externo, e o primeiro é o mais interno. + +No caminho da requisição, o middleware mais externo roda primeiro. + +No caminho da resposta, ele roda por último. + +Por exemplo: + +```Python +app.add_middleware(MiddlewareA) +app.add_middleware(MiddlewareB) +``` + +Isso resulta na seguinte ordem de execução: + +* **Requisição**: MiddlewareB → MiddlewareA → rota + +* **Resposta**: rota → MiddlewareA → MiddlewareB + +Esse comportamento de empilhamento garante que os middlewares sejam executados em uma ordem previsível e controlável. + +## Outros middlewares { #other-middlewares } Mais tarde, você pode ler mais sobre outros middlewares no [Guia do usuário avançado: Middleware avançado](../advanced/middleware.md){.internal-link target=_blank}. diff --git a/docs/pt/docs/tutorial/path-operation-configuration.md b/docs/pt/docs/tutorial/path-operation-configuration.md index f183c9d23b..84eebc1285 100644 --- a/docs/pt/docs/tutorial/path-operation-configuration.md +++ b/docs/pt/docs/tutorial/path-operation-configuration.md @@ -1,14 +1,14 @@ -# Configuração da Operação de Rota +# Configuração da Operação de Rota { #path-operation-configuration } Existem vários parâmetros que você pode passar para o seu *decorador de operação de rota* para configurá-lo. -/// warning | Aviso +/// warning | Atenção Observe que esses parâmetros são passados diretamente para o *decorador de operação de rota*, não para a sua *função de operação de rota*. /// -## Código de Status da Resposta +## Código de Status da Resposta { #response-status-code } Você pode definir o `status_code` (HTTP) para ser usado na resposta da sua *operação de rota*. @@ -16,7 +16,7 @@ Você pode passar diretamente o código `int`, como `404`. Mas se você não se lembrar o que cada código numérico significa, pode usar as constantes de atalho em `status`: -{* ../../docs_src/path_operation_configuration/tutorial001.py hl[3,17] *} +{* ../../docs_src/path_operation_configuration/tutorial001_py310.py hl[1,15] *} Esse código de status será usado na resposta e será adicionado ao esquema OpenAPI. @@ -28,17 +28,17 @@ Você também poderia usar `from starlette import status`. /// -## Tags +## Tags { #tags } Você pode adicionar tags para sua *operação de rota*, passe o parâmetro `tags` com uma `list` de `str` (comumente apenas um `str`): -{* ../../docs_src/path_operation_configuration/tutorial002.py hl[17,22,27] *} +{* ../../docs_src/path_operation_configuration/tutorial002_py310.py hl[15,20,25] *} Eles serão adicionados ao esquema OpenAPI e usados pelas interfaces de documentação automática: -### Tags com Enums +### Tags com Enums { #tags-with-enums } Se você tem uma grande aplicação, você pode acabar acumulando **várias tags**, e você gostaria de ter certeza de que você sempre usa a **mesma tag** para *operações de rota* relacionadas. @@ -46,32 +46,31 @@ Nestes casos, pode fazer sentido armazenar as tags em um `Enum`. **FastAPI** suporta isso da mesma maneira que com strings simples: -{* ../../docs_src/path_operation_configuration/tutorial002b.py hl[1,8:10,13,18] *} +{* ../../docs_src/path_operation_configuration/tutorial002b_py39.py hl[1,8:10,13,18] *} -## Resumo e descrição +## Resumo e descrição { #summary-and-description } Você pode adicionar um `summary` e uma `description`: -{* ../../docs_src/path_operation_configuration/tutorial003.py hl[20:21] *} +{* ../../docs_src/path_operation_configuration/tutorial003_py310.py hl[18:19] *} -## Descrição do docstring +## Descrição do docstring { #description-from-docstring } Como as descrições tendem a ser longas e cobrir várias linhas, você pode declarar a descrição da *operação de rota* na docstring da função e o **FastAPI** irá lê-la de lá. Você pode escrever Markdown na docstring, ele será interpretado e exibido corretamente (levando em conta a indentação da docstring). -{* ../../docs_src/path_operation_configuration/tutorial004.py hl[19:27] *} +{* ../../docs_src/path_operation_configuration/tutorial004_py310.py hl[17:25] *} Ela será usada nas documentações interativas: - -## Descrição da resposta +## Descrição da resposta { #response-description } Você pode especificar a descrição da resposta com o parâmetro `response_description`: -{* ../../docs_src/path_operation_configuration/tutorial005.py hl[21] *} +{* ../../docs_src/path_operation_configuration/tutorial005_py310.py hl[19] *} /// info | Informação @@ -79,7 +78,7 @@ Note que `response_description` se refere especificamente à resposta, a `descri /// -/// check +/// check | Verifique OpenAPI especifica que cada *operação de rota* requer uma descrição de resposta. @@ -89,11 +88,11 @@ Então, se você não fornecer uma, o **FastAPI** irá gerar automaticamente uma -## Depreciar uma *operação de rota* +## Descontinuar uma *operação de rota* { #deprecate-a-path-operation } Se você precisar marcar uma *operação de rota* como descontinuada, mas sem removê-la, passe o parâmetro `deprecated`: -{* ../../docs_src/path_operation_configuration/tutorial006.py hl[16] *} +{* ../../docs_src/path_operation_configuration/tutorial006_py39.py hl[16] *} Ela será claramente marcada como descontinuada nas documentações interativas: @@ -103,6 +102,6 @@ Verifique como *operações de rota* descontinuadas e não descontinuadas se par -## Resumindo +## Resumindo { #recap } Você pode configurar e adicionar metadados para suas *operações de rota* facilmente passando parâmetros para os *decoradores de operação de rota*. diff --git a/docs/pt/docs/tutorial/path-params-numeric-validations.md b/docs/pt/docs/tutorial/path-params-numeric-validations.md index 3aea1188dd..9f12ba38fe 100644 --- a/docs/pt/docs/tutorial/path-params-numeric-validations.md +++ b/docs/pt/docs/tutorial/path-params-numeric-validations.md @@ -1,91 +1,128 @@ -# Parâmetros da Rota e Validações Numéricas +# Parâmetros de path e validações numéricas { #path-parameters-and-numeric-validations } -Do mesmo modo que você pode declarar mais validações e metadados para parâmetros de consulta com `Query`, você pode declarar os mesmos tipos de validações e metadados para parâmetros de rota com `Path`. +Da mesma forma que você pode declarar mais validações e metadados para parâmetros de consulta com `Query`, você pode declarar o mesmo tipo de validações e metadados para parâmetros de path com `Path`. -## Importe `Path` +## Importe `Path` { #import-path } -Primeiro, importe `Path` de `fastapi`: +Primeiro, importe `Path` de `fastapi`, e importe `Annotated`: -{* ../../docs_src/path_params_numeric_validations/tutorial001_py310.py hl[1] *} +{* ../../docs_src/path_params_numeric_validations/tutorial001_an_py310.py hl[1,3] *} -## Declare metadados +/// info | Informação -Você pode declarar todos os parâmetros da mesma maneira que na `Query`. +O FastAPI adicionou suporte a `Annotated` (e passou a recomendá-lo) na versão 0.95.0. -Por exemplo para declarar um valor de metadado `title` para o parâmetro de rota `item_id` você pode digitar: +Se você tiver uma versão mais antiga, verá erros ao tentar usar `Annotated`. -{* ../../docs_src/path_params_numeric_validations/tutorial001_py310.py hl[8] *} - -/// note | Nota - -Um parâmetro de rota é sempre obrigatório, como se fizesse parte da rota. - -Então, você deve declará-lo com `...` para marcá-lo como obrigatório. - -Mesmo que você declare-o como `None` ou defina um valor padrão, isso não teria efeito algum, o parâmetro ainda seria obrigatório. +Certifique-se de [Atualizar a versão do FastAPI](../deployment/versions.md#upgrading-the-fastapi-versions){.internal-link target=_blank} para pelo menos 0.95.1 antes de usar `Annotated`. /// -## Ordene os parâmetros de acordo com sua necessidade +## Declare metadados { #declare-metadata } -Suponha que você queira declarar o parâmetro de consulta `q` como uma `str` obrigatória. +Você pode declarar todos os mesmos parâmetros que em `Query`. -E você não precisa declarar mais nada em relação a este parâmetro, então você não precisa necessariamente usar `Query`. +Por exemplo, para declarar um valor de metadado `title` para o parâmetro de path `item_id` você pode digitar: -Mas você ainda precisa usar `Path` para o parâmetro de rota `item_id`. +{* ../../docs_src/path_params_numeric_validations/tutorial001_an_py310.py hl[10] *} -O Python irá acusar se você colocar um elemento com um valor padrão definido antes de outro que não tenha um valor padrão. +/// note | Nota -Mas você pode reordená-los, colocando primeiro o elemento sem o valor padrão (o parâmetro de consulta `q`). +Um parâmetro de path é sempre obrigatório, pois precisa fazer parte do path. Mesmo que você o declare como `None` ou defina um valor padrão, isso não afetaria nada, ele ainda seria sempre obrigatório. -Isso não faz diferença para o **FastAPI**. Ele vai detectar os parâmetros pelos seus nomes, tipos e definições padrão (`Query`, `Path`, etc), sem se importar com a ordem. +/// + +## Ordene os parâmetros de acordo com sua necessidade { #order-the-parameters-as-you-need } + +/// tip | Dica + +Isso provavelmente não é tão importante ou necessário se você usar `Annotated`. + +/// + +Vamos supor que você queira declarar o parâmetro de consulta `q` como uma `str` obrigatória. + +E você não precisa declarar mais nada para esse parâmetro, então você realmente não precisa usar `Query`. + +Mas você ainda precisa usar `Path` para o parâmetro de path `item_id`. E você não quer usar `Annotated` por algum motivo. + +O Python vai reclamar se você colocar um valor com “padrão” antes de um valor que não tem “padrão”. + +Mas você pode reordená-los e colocar primeiro o valor sem padrão (o parâmetro de consulta `q`). + +Isso não faz diferença para o **FastAPI**. Ele vai detectar os parâmetros pelos seus nomes, tipos e declarações de padrão (`Query`, `Path`, etc.), sem se importar com a ordem. Então, você pode declarar sua função assim: -{* ../../docs_src/path_params_numeric_validations/tutorial002.py hl[7] *} +{* ../../docs_src/path_params_numeric_validations/tutorial002_py39.py hl[7] *} -## Ordene os parâmetros de a acordo com sua necessidade, truques +Mas tenha em mente que, se você usar `Annotated`, você não terá esse problema, não fará diferença, pois você não está usando valores padrão de parâmetros de função para `Query()` ou `Path()`. -Se você quiser declarar o parâmetro de consulta `q` sem um `Query` nem um valor padrão, e o parâmetro de rota `item_id` usando `Path`, e definí-los em uma ordem diferente, Python tem um pequeno truque na sintaxe para isso. +{* ../../docs_src/path_params_numeric_validations/tutorial002_an_py39.py *} + +## Ordene os parâmetros de acordo com sua necessidade, truques { #order-the-parameters-as-you-need-tricks } + +/// tip | Dica + +Isso provavelmente não é tão importante ou necessário se você usar `Annotated`. + +/// + +Aqui vai um pequeno truque que pode ser útil, mas você não vai precisar dele com frequência. + +Se você quiser: + +* declarar o parâmetro de consulta `q` sem um `Query` nem qualquer valor padrão +* declarar o parâmetro de path `item_id` usando `Path` +* tê-los em uma ordem diferente +* não usar `Annotated` + +...o Python tem uma pequena sintaxe especial para isso. Passe `*`, como o primeiro parâmetro da função. -O Python não vai fazer nada com esse `*`, mas ele vai saber que a partir dali os parâmetros seguintes deverão ser chamados argumentos nomeados (pares chave-valor), também conhecidos como kwargs. Mesmo que eles não possuam um valor padrão. +O Python não fará nada com esse `*`, mas saberá que todos os parâmetros seguintes devem ser chamados como argumentos nomeados (pares chave-valor), também conhecidos como kwargs. Mesmo que eles não tenham um valor padrão. -{* ../../docs_src/path_params_numeric_validations/tutorial003.py hl[7] *} +{* ../../docs_src/path_params_numeric_validations/tutorial003_py39.py hl[7] *} -## Validações numéricas: maior que ou igual +### Melhor com `Annotated` { #better-with-annotated } -Com `Query` e `Path` (e outras que você verá mais tarde) você pode declarar restrições numéricas. +Tenha em mente que, se você usar `Annotated`, como você não está usando valores padrão de parâmetros de função, você não terá esse problema e provavelmente não precisará usar `*`. -Aqui, com `ge=1`, `item_id` precisará ser um número inteiro maior que ("`g`reater than") ou igual ("`e`qual") a 1. +{* ../../docs_src/path_params_numeric_validations/tutorial003_an_py39.py hl[10] *} -{* ../../docs_src/path_params_numeric_validations/tutorial004.py hl[8] *} +## Validações numéricas: maior que ou igual { #number-validations-greater-than-or-equal } -## Validações numéricas: maior que e menor que ou igual +Com `Query` e `Path` (e outras que você verá depois) você pode declarar restrições numéricas. -O mesmo se aplica para: +Aqui, com `ge=1`, `item_id` precisará ser um número inteiro “`g`reater than or `e`qual” a `1`. + +{* ../../docs_src/path_params_numeric_validations/tutorial004_an_py39.py hl[10] *} + +## Validações numéricas: maior que e menor que ou igual { #number-validations-greater-than-and-less-than-or-equal } + +O mesmo se aplica a: * `gt`: maior que (`g`reater `t`han) * `le`: menor que ou igual (`l`ess than or `e`qual) -{* ../../docs_src/path_params_numeric_validations/tutorial005.py hl[9] *} +{* ../../docs_src/path_params_numeric_validations/tutorial005_an_py39.py hl[10] *} -## Validações numéricas: valores do tipo float, maior que e menor que +## Validações numéricas: floats, maior que e menor que { #number-validations-floats-greater-than-and-less-than } -Validações numéricas também funcionam para valores do tipo `float`. +Validações numéricas também funcionam para valores `float`. -Aqui é onde se torna importante a possibilidade de declarar gt e não apenas ge. Com isso você pode especificar, por exemplo, que um valor deve ser maior que `0`, ainda que seja menor que `1`. +Aqui é onde se torna importante poder declarar gt e não apenas ge. Com isso você pode exigir, por exemplo, que um valor seja maior que `0`, mesmo que seja menor que `1`. -Assim, `0.5` seria um valor válido. Mas `0.0` ou `0` não seria. +Assim, `0.5` seria um valor válido. Mas `0.0` ou `0` não seriam. -E o mesmo para lt. +E o mesmo para lt. -{* ../../docs_src/path_params_numeric_validations/tutorial006.py hl[11] *} +{* ../../docs_src/path_params_numeric_validations/tutorial006_an_py39.py hl[13] *} -## Recapitulando +## Recapitulando { #recap } -Com `Query`, `Path` (e outras que você ainda não viu) você pode declarar metadados e validações de texto do mesmo modo que com [Parâmetros de consulta e validações de texto](query-params-str-validations.md){.internal-link target=_blank}. +Com `Query`, `Path` (e outras que você ainda não viu) você pode declarar metadados e validações de string do mesmo modo que em [Parâmetros de consulta e validações de string](query-params-str-validations.md){.internal-link target=_blank}. E você também pode declarar validações numéricas: @@ -96,7 +133,7 @@ E você também pode declarar validações numéricas: /// info | Informação -`Query`, `Path` e outras classes que você verá a frente são subclasses de uma classe comum `Param`. +`Query`, `Path` e outras classes que você verá depois são subclasses de uma classe comum `Param`. Todas elas compartilham os mesmos parâmetros para validação adicional e metadados que você viu. @@ -106,12 +143,12 @@ Todas elas compartilham os mesmos parâmetros para validação adicional e metad Quando você importa `Query`, `Path` e outras de `fastapi`, elas são na verdade funções. -Que quando chamadas, retornam instâncias de classes de mesmo nome. +Que, quando chamadas, retornam instâncias de classes de mesmo nome. Então, você importa `Query`, que é uma função. E quando você a chama, ela retorna uma instância de uma classe também chamada `Query`. -Estas funções são assim (ao invés de apenas usar as classes diretamente) para que seu editor não acuse erros sobre seus tipos. +Essas funções existem (em vez de usar diretamente as classes) para que seu editor não marque erros sobre seus tipos. -Dessa maneira você pode user seu editor e ferramentas de desenvolvimento sem precisar adicionar configurações customizadas para ignorar estes erros. +Dessa forma, você pode usar seu editor e ferramentas de codificação normais sem precisar adicionar configurações personalizadas para desconsiderar esses erros. /// diff --git a/docs/pt/docs/tutorial/path-params.md b/docs/pt/docs/tutorial/path-params.md index ecf77d676d..1f47ca6e59 100644 --- a/docs/pt/docs/tutorial/path-params.md +++ b/docs/pt/docs/tutorial/path-params.md @@ -1,207 +1,184 @@ -# Parâmetros da rota da URL +# Parâmetros de path { #path-parameters } -Você pode declarar os "parâmetros" ou "variáveis" com a mesma sintaxe utilizada pelo formato de strings do Python: +Você pode declarar "parâmetros" ou "variáveis" de path com a mesma sintaxe usada por strings de formatação do Python: -{* ../../docs_src/path_params/tutorial001.py hl[6:7] *} +{* ../../docs_src/path_params/tutorial001_py39.py hl[6:7] *} -O valor do parâmetro que foi passado à `item_id` será passado para a sua função como o argumento `item_id`. +O valor do parâmetro de path `item_id` será passado para a sua função como o argumento `item_id`. -Então, se você rodar este exemplo e for até http://127.0.0.1:8000/items/foo, você verá a seguinte resposta: +Então, se você executar este exemplo e acessar http://127.0.0.1:8000/items/foo, você verá uma resposta: ```JSON {"item_id":"foo"} ``` -## Parâmetros da rota com tipos +## Parâmetros de path com tipos { #path-parameters-with-types } -Você pode declarar o tipo de um parâmetro na função usando as anotações padrões do Python: +Você pode declarar o tipo de um parâmetro de path na função, usando as anotações de tipo padrão do Python: -{* ../../docs_src/path_params/tutorial002.py hl[7] *} +{* ../../docs_src/path_params/tutorial002_py39.py hl[7] *} -Nesse caso, `item_id` está sendo declarado como um `int`. +Neste caso, `item_id` é declarado como um `int`. /// check | Verifique - - - +Isso fornecerá suporte do editor dentro da sua função, com verificações de erros, preenchimento automático, etc. /// - Isso vai dar à você suporte do seu editor dentro das funções, com verificações de erros, autocompletar, etc. +## Dados conversão { #data-conversion } -## Conversão de dados - -Se você rodar esse exemplo e abrir o seu navegador em http://127.0.0.1:8000/items/3, você verá a seguinte resposta: +Se você executar este exemplo e abrir seu navegador em http://127.0.0.1:8000/items/3, você verá uma resposta: ```JSON {"item_id":3} ``` /// check | Verifique +Perceba que o valor que sua função recebeu (e retornou) é `3`, como um `int` do Python, não uma string `"3"`. - - +Então, com essa declaração de tipo, o **FastAPI** fornece "parsing" automático do request. /// - Observe que o valor recebido pela função (e também retornado por ela) é `3`, como um Python `int`, não como uma string `"3"`. +## Validação de dados { #data-validation } - Então, com essa declaração de tipo, o **FastAPI** dá pra você um "parsing" automático no request . - -## Validação de dados - -Mas se você abrir o seu navegador em http://127.0.0.1:8000/items/foo, você verá um belo erro HTTP: +Mas se você for no navegador para http://127.0.0.1:8000/items/foo, verá um bom erro HTTP: ```JSON { - "detail": [ - { - "loc": [ - "path", - "item_id" - ], - "msg": "value is not a valid integer", - "type": "type_error.integer" - } - ] + "detail": [ + { + "type": "int_parsing", + "loc": [ + "path", + "item_id" + ], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "foo" + } + ] } ``` -devido ao parâmetro da rota `item_id` ter um valor `"foo"`, que não é um `int`. +porque o parâmetro de path `item_id` tinha o valor `"foo"`, que não é um `int`. -O mesmo erro apareceria se você tivesse fornecido um `float` ao invés de um `int`, como em: http://127.0.0.1:8000/items/4.2 +O mesmo erro apareceria se você fornecesse um `float` em vez de um `int`, como em: http://127.0.0.1:8000/items/4.2 /// check | Verifique +Então, com a mesma declaração de tipo do Python, o **FastAPI** fornece validação de dados. +Observe que o erro também declara claramente exatamente o ponto onde a validação não passou. - +Isso é incrivelmente útil ao desenvolver e depurar código que interage com sua API. /// - Então, com a mesma declaração de tipo do Python, o **FastAPI** dá pra você validação de dados. +## Documentação { #documentation } - Observe que o erro também mostra claramente o ponto exato onde a validação não passou. - - Isso é incrivelmente útil enquanto se desenvolve e debuga o código que interage com a sua API. - -## Documentação - -Quando você abrir o seu navegador em http://127.0.0.1:8000/docs, você verá de forma automática e interativa a documentação da API como: +E quando você abrir seu navegador em http://127.0.0.1:8000/docs, você verá documentação automática, interativa, da API como: /// check | Verifique +Novamente, apenas com a mesma declaração de tipo do Python, o **FastAPI** fornece documentação automática e interativa (integrando o Swagger UI). - - +Observe que o parâmetro de path está declarado como um inteiro. /// - Novamente, apenas com a mesma declaração de tipo do Python, o **FastAPI** te dá de forma automática e interativa a documentação (integrada com o Swagger UI). +## Benefícios baseados em padrões, documentação alternativa { #standards-based-benefits-alternative-documentation } - Veja que o parâmetro de rota está declarado como sendo um inteiro (int). +E como o schema gerado é do padrão OpenAPI, existem muitas ferramentas compatíveis. -## Beneficios baseados em padrões, documentação alternativa - -Devido ao schema gerado ser o padrão do OpenAPI, existem muitas ferramentas compatíveis. - -Por esse motivo, o próprio **FastAPI** fornece uma API alternativa para documentação (utilizando ReDoc), que você pode acessar em http://127.0.0.1:8000/redoc: +Por causa disso, o próprio **FastAPI** fornece uma documentação alternativa da API (usando ReDoc), que você pode acessar em http://127.0.0.1:8000/redoc: Da mesma forma, existem muitas ferramentas compatíveis. Incluindo ferramentas de geração de código para muitas linguagens. -## Pydantic +## Pydantic { #pydantic } -Toda a validação de dados é feita por baixo dos panos pelo Pydantic, então você tem todos os benefícios disso. E assim você sabe que está em boas mãos. +Toda a validação de dados é realizada nos bastidores pelo Pydantic, então você recebe todos os benefícios disso. E você sabe que está em boas mãos. -Você pode usar as mesmas declarações de tipo com `str`, `float`, `bool` e muitos outros tipos complexos de dados. +Você pode usar as mesmas declarações de tipo com `str`, `float`, `bool` e muitos outros tipos de dados complexos. -Vamos explorar muitos destes tipos nos próximos capítulos do tutorial. +Vários deles são explorados nos próximos capítulos do tutorial. -## A ordem importa +## A ordem importa { #order-matters } -Quando você cria operações de rota, você pode se deparar com situações onde você pode ter uma rota fixa. +Ao criar *operações de rota*, você pode encontrar situações em que tem um path fixo. -Algo como `/users/me` por exemplo, digamos que essa rota seja utilizada para pegar dados sobre o usuário atual. +Como `/users/me`, digamos que seja para obter dados sobre o usuário atual. -E então você pode ter também uma rota `/users/{user_id}` para pegar dados sobre um usuário específico associado a um ID de usuário. +E então você também pode ter um path `/users/{user_id}` para obter dados sobre um usuário específico por algum ID de usuário. -Porque as operações de rota são avaliadas em ordem, você precisa ter certeza que a rota para `/users/me` está sendo declarado antes da rota `/users/{user_id}`: +Como as *operações de rota* são avaliadas em ordem, você precisa garantir que o path para `/users/me` seja declarado antes do de `/users/{user_id}`: -{* ../../docs_src/path_params/tutorial003.py hl[6,11] *} +{* ../../docs_src/path_params/tutorial003_py39.py hl[6,11] *} -Caso contrário, a rota para `/users/{user_id}` coincidiria também para `/users/me`, "pensando" que estaria recebendo o parâmetro `user_id` com o valor de `"me"`. +Caso contrário, o path para `/users/{user_id}` também corresponderia a `/users/me`, "achando" que está recebendo um parâmetro `user_id` com o valor `"me"`. -## Valores predefinidos +Da mesma forma, você não pode redefinir uma operação de rota: -Se você tem uma operação de rota que recebe um parâmetro da rota, mas que você queira que esses valores possíveis do parâmetro da rota sejam predefinidos, você pode usar `Enum` padrão do Python. +{* ../../docs_src/path_params/tutorial003b_py39.py hl[6,11] *} -### Criando uma classe `Enum` +A primeira sempre será usada, já que o path corresponde primeiro. -Importe `Enum` e crie uma sub-classe que herde de `str` e de `Enum`. +## Valores predefinidos { #predefined-values } -Por herdar de `str` a documentação da API vai ser capaz de saber que os valores devem ser do tipo `string` e assim ser capaz de mostrar eles corretamente. +Se você tem uma *operação de rota* que recebe um *parâmetro de path*, mas quer que os valores válidos possíveis do *parâmetro de path* sejam predefinidos, você pode usar um `Enum` padrão do Python. -Assim, crie atributos de classe com valores fixos, que serão os valores válidos disponíveis. +### Crie uma classe `Enum` { #create-an-enum-class } -{* ../../docs_src/path_params/tutorial005.py hl[1,6:9] *} +Importe `Enum` e crie uma subclasse que herde de `str` e de `Enum`. -/// info | informação +Ao herdar de `str`, a documentação da API saberá que os valores devem ser do tipo `string` e poderá renderizá-los corretamente. -Enumerations (ou enums) estão disponíveis no Python desde a versão 3.4. +Em seguida, crie atributos de classe com valores fixos, que serão os valores válidos disponíveis: -/// +{* ../../docs_src/path_params/tutorial005_py39.py hl[1,6:9] *} /// tip | Dica - - - +Se você está se perguntando, "AlexNet", "ResNet" e "LeNet" são apenas nomes de modelos de Aprendizado de Máquina. /// - Se você está se perguntando, "AlexNet", "ResNet", e "LeNet" são apenas nomes de modelos de Machine Learning (aprendizado de máquina). +### Declare um parâmetro de path { #declare-a-path-parameter } -### Declare um *parâmetro de rota* +Em seguida, crie um *parâmetro de path* com anotação de tipo usando a classe enum que você criou (`ModelName`): -Logo, crie um *parâmetro de rota* com anotações de tipo usando a classe enum que você criou (`ModelName`): +{* ../../docs_src/path_params/tutorial005_py39.py hl[16] *} -{* ../../docs_src/path_params/tutorial005.py hl[16] *} +### Verifique a documentação { #check-the-docs } -### Revise a documentação - -Visto que os valores disponíveis para o parâmetro da rota estão predefinidos, a documentação interativa pode mostrar esses valores de uma forma bem legal: +Como os valores disponíveis para o *parâmetro de path* são predefinidos, a documentação interativa pode mostrá-los de forma agradável: -### Trabalhando com os *enumeration* do Python +### Trabalhando com *enumerações* do Python { #working-with-python-enumerations } -O valor do *parâmetro da rota* será um *membro de enumeration*. +O valor do *parâmetro de path* será um *membro de enumeração*. -#### Compare *membros de enumeration* +#### Compare membros de enumeração { #compare-enumeration-members } -Você pode comparar eles com o *membro de enumeration* no enum `ModelName` que você criou: +Você pode compará-lo com o *membro de enumeração* no seu enum `ModelName` criado: -{* ../../docs_src/path_params/tutorial005.py hl[17] *} +{* ../../docs_src/path_params/tutorial005_py39.py hl[17] *} -#### Obtenha o *valor de enumerate* +#### Obtenha o valor da enumeração { #get-the-enumeration-value } -Você pode ter o valor exato de enumerate (um `str` nesse caso) usando `model_name.value`, ou em geral, `your_enum_member.value`: +Você pode obter o valor real (um `str` neste caso) usando `model_name.value`, ou, em geral, `your_enum_member.value`: -{* ../../docs_src/path_params/tutorial005.py hl[20] *} +{* ../../docs_src/path_params/tutorial005_py39.py hl[20] *} /// tip | Dica - - - +Você também pode acessar o valor `"lenet"` com `ModelName.lenet.value`. /// - Você também poderia acessar o valor `"lenet"` com `ModelName.lenet.value` +#### Retorne membros de enumeração { #return-enumeration-members } -#### Retorne *membros de enumeration* +Você pode retornar *membros de enum* da sua *operação de rota*, até mesmo aninhados em um corpo JSON (por exemplo, um `dict`). -Você pode retornar *membros de enum* da sua *rota de operação*, em um corpo JSON aninhado (por exemplo um `dict`). +Eles serão convertidos para seus valores correspondentes (strings neste caso) antes de serem retornados ao cliente: -Eles serão convertidos para o seus valores correspondentes (strings nesse caso) antes de serem retornados ao cliente: +{* ../../docs_src/path_params/tutorial005_py39.py hl[18,21,23] *} -{* ../../docs_src/path_params/tutorial005.py hl[18,21,23] *} - -No seu cliente você vai obter uma resposta JSON como: +No seu cliente, você receberá uma resposta JSON como: ```JSON { @@ -210,56 +187,51 @@ No seu cliente você vai obter uma resposta JSON como: } ``` -## Parâmetros de rota que contém caminhos +## Parâmetros de path que contêm paths { #path-parameters-containing-paths } -Digamos que você tenha uma *operação de rota* com uma rota `/files/{file_path}`. +Digamos que você tenha uma *operação de rota* com um path `/files/{file_path}`. -Mas você precisa que o próprio `file_path` contenha uma *rota*, como `home/johndoe/myfile.txt`. +Mas você precisa que o próprio `file_path` contenha um *path*, como `home/johndoe/myfile.txt`. -Então, a URL para este arquivo deveria ser algo como: `/files/home/johndoe/myfile.txt`. +Então, a URL para esse arquivo seria algo como: `/files/home/johndoe/myfile.txt`. -### Suporte do OpenAPI +### Suporte do OpenAPI { #openapi-support } -O OpenAPI não suporta uma maneira de declarar um *parâmetro de rota* que contenha uma *rota* dentro, dado que isso poderia levar a cenários que são difíceis de testar e definir. +O OpenAPI não oferece suporte a uma maneira de declarar um *parâmetro de path* que contenha um *path* dentro, pois isso poderia levar a cenários difíceis de testar e definir. -No entanto, você pode fazer isso no **FastAPI**, usando uma das ferramentas internas do Starlette. +Ainda assim, você pode fazer isso no **FastAPI**, usando uma das ferramentas internas do Starlette. -A documentação continuaria funcionando, ainda que não adicionaria nenhuma informação dizendo que o parâmetro deveria conter uma rota. +E a documentação continuará funcionando, embora não adicione nenhuma informação dizendo que o parâmetro deve conter um path. -### Conversor de rota +### Conversor de path { #path-convertor } -Usando uma opção direta do Starlette você pode declarar um *parâmetro de rota* contendo uma *rota* usando uma URL como: +Usando uma opção diretamente do Starlette você pode declarar um *parâmetro de path* contendo um *path* usando uma URL como: ``` /files/{file_path:path} ``` -Nesse caso, o nome do parâmetro é `file_path`, e a última parte, `:path`, diz que o parâmetro deveria coincidir com qualquer *rota*. +Nesse caso, o nome do parâmetro é `file_path`, e a última parte, `:path`, diz que o parâmetro deve corresponder a qualquer *path*. -Então, você poderia usar ele com: +Então, você pode usá-lo com: -{* ../../docs_src/path_params/tutorial004.py hl[6] *} +{* ../../docs_src/path_params/tutorial004_py39.py hl[6] *} /// tip | Dica +Você pode precisar que o parâmetro contenha `/home/johndoe/myfile.txt`, com uma barra inicial (`/`). - - +Nesse caso, a URL seria: `/files//home/johndoe/myfile.txt`, com uma barra dupla (`//`) entre `files` e `home`. /// - Você poderia precisar que o parâmetro contivesse `/home/johndoe/myfile.txt`, com uma barra no inicio (`/`). +## Recapitulação { #recap } - Neste caso, a URL deveria ser: `/files//home/johndoe/myfile.txt`, com barra dupla (`//`) entre `files` e `home`. +Com o **FastAPI**, ao usar declarações de tipo do Python curtas, intuitivas e padrão, você obtém: +- Suporte no editor: verificações de erro, autocompletar, etc. +- "Parsing" de dados +- Validação de dados +- Anotação da API e documentação automática -## Recapitulando +E você só precisa declará-los uma vez. -Com o **FastAPI**, usando as declarações de tipo do Python, você obtém: - -* Suporte no editor: verificação de erros, e opção de autocompletar, etc. -* "Parsing" de dados -* Validação de dados -* Anotação da API e documentação automática - -Você apenas tem que declará-los uma vez. - -Essa é provavelmente a vantagem mais visível do **FastAPI** se comparado com frameworks alternativos (além do desempenho puro). +Essa é provavelmente a principal vantagem visível do **FastAPI** em comparação com frameworks alternativos (além do desempenho bruto). diff --git a/docs/pt/docs/tutorial/query-param-models.md b/docs/pt/docs/tutorial/query-param-models.md index 01a6e462f1..42d2604cd4 100644 --- a/docs/pt/docs/tutorial/query-param-models.md +++ b/docs/pt/docs/tutorial/query-param-models.md @@ -1,4 +1,4 @@ -# Modelos de Parâmetros de Consulta +# Modelos de Parâmetros de Consulta { #query-parameter-models } Se você possui um grupo de **parâmetros de consultas** que são relacionados, você pode criar um **modelo Pydantic** para declará-los. @@ -10,7 +10,7 @@ Isso é suportado desde o FastAPI versão `0.115.0`. 🤓 /// -## Parâmetros de Consulta com um Modelo Pydantic +## Parâmetros de Consulta com um Modelo Pydantic { #query-parameters-with-a-pydantic-model } Declare os **parâmetros de consulta** que você precisa em um **modelo Pydantic**, e então declare o parâmetro como `Query`: @@ -19,7 +19,7 @@ Declare os **parâmetros de consulta** que você precisa em um **modelo Pydantic O **FastAPI** **extrairá** os dados para **cada campo** dos **parâmetros de consulta** presentes na requisição, e fornecerá o modelo Pydantic que você definiu. -## Verifique os Documentos +## Verifique os Documentos { #check-the-docs } Você pode ver os parâmetros de consulta nos documentos de IU em `/docs`: @@ -27,7 +27,7 @@ Você pode ver os parâmetros de consulta nos documentos de IU em `/docs`:
-## Restrinja Parâmetros de Consulta Extras +## Restrinja Parâmetros de Consulta Extras { #forbid-extra-query-parameters } Em alguns casos especiais (provavelmente não muito comuns), você queira **restrinjir** os parâmetros de consulta que deseja receber. @@ -58,7 +58,7 @@ Eles receberão um retorno de **erro** informando-os que o parâmentro de consul } ``` -## Resumo +## Resumo { #summary } Você pode utilizar **modelos Pydantic** para declarar **parâmetros de consulta** no **FastAPI**. 😎 diff --git a/docs/pt/docs/tutorial/query-params-str-validations.md b/docs/pt/docs/tutorial/query-params-str-validations.md index 8c4f2e655e..5ec1b1b55e 100644 --- a/docs/pt/docs/tutorial/query-params-str-validations.md +++ b/docs/pt/docs/tutorial/query-params-str-validations.md @@ -1,164 +1,286 @@ -# Parâmetros de consulta e validações de texto +# Parâmetros de consulta e validações de string { #query-parameters-and-string-validations } -O **FastAPI** permite que você declare informações adicionais e validações aos seus parâmetros. +O **FastAPI** permite declarar informações adicionais e validações para os seus parâmetros. -Vamos utilizar essa aplicação como exemplo: +Vamos usar esta aplicação como exemplo: -{* ../../docs_src/query_params_str_validations/tutorial001.py hl[9] *} +{* ../../docs_src/query_params_str_validations/tutorial001_py310.py hl[7] *} -O parâmetro de consulta `q` é do tipo `Union[str, None]`, o que significa que é do tipo `str` mas que também pode ser `None`, e de fato, o valor padrão é `None`, então o FastAPI saberá que não é obrigatório. +O parâmetro de consulta `q` é do tipo `str | None`, isso significa que é do tipo `str`, mas também pode ser `None`, e de fato, o valor padrão é `None`, então o FastAPI saberá que não é obrigatório. -/// note | Observação +/// note | Nota O FastAPI saberá que o valor de `q` não é obrigatório por causa do valor padrão `= None`. -O `Union` em `Union[str, None]` não é usado pelo FastAPI, mas permitirá que seu editor lhe dê um melhor suporte e detecte erros. +Ter `str | None` permitirá que seu editor lhe ofereça melhor suporte e detecte erros. /// -## Validação adicional +## Validação adicional { #additional-validation } -Nós iremos forçar que mesmo o parâmetro `q` seja opcional, sempre que informado, **seu tamanho não exceda 50 caracteres**. +Vamos impor que, embora `q` seja opcional, sempre que for fornecido, **seu comprimento não exceda 50 caracteres**. -### Importe `Query` +### Importe `Query` e `Annotated` { #import-query-and-annotated } -Para isso, primeiro importe `Query` de `fastapi`: +Para isso, primeiro importe: -{* ../../docs_src/query_params_str_validations/tutorial002.py hl[3] *} +* `Query` de `fastapi` +* `Annotated` de `typing` -## Use `Query` como o valor padrão +{* ../../docs_src/query_params_str_validations/tutorial002_an_py310.py hl[1,3] *} -Agora utilize-o como valor padrão do seu parâmetro, definindo o parâmetro `max_length` para 50: +/// info | Informação -{* ../../docs_src/query_params_str_validations/tutorial002.py hl[9] *} +O FastAPI adicionou suporte a `Annotated` (e passou a recomendá-lo) na versão 0.95.0. -Note que substituímos o valor padrão de `None` para `Query(default=None)`, o primeiro parâmetro de `Query` serve para o mesmo propósito: definir o valor padrão do parâmetro. +Se você tiver uma versão mais antiga, terá erros ao tentar usar `Annotated`. -Então: +Certifique-se de [Atualizar a versão do FastAPI](../deployment/versions.md#upgrading-the-fastapi-versions){.internal-link target=_blank} para pelo menos 0.95.1 antes de usar `Annotated`. + +/// + +## Use `Annotated` no tipo do parâmetro `q` { #use-annotated-in-the-type-for-the-q-parameter } + +Lembra que eu disse antes que `Annotated` pode ser usado para adicionar metadados aos seus parâmetros na [Introdução aos tipos do Python](../python-types.md#type-hints-with-metadata-annotations){.internal-link target=_blank}? + +Agora é a hora de usá-lo com FastAPI. 🚀 + +Tínhamos esta anotação de tipo: + +//// tab | Python 3.10+ ```Python -q: Union[str, None] = Query(default=None) +q: str | None = None ``` -...Torna o parâmetro opcional, da mesma maneira que: +//// + +//// tab | Python 3.9+ ```Python q: Union[str, None] = None ``` -Mas o declara explicitamente como um parâmetro de consulta. +//// -/// info | Informação +O que faremos é envolver isso com `Annotated`, para que fique assim: -Tenha em mente que o FastAPI se preocupa com a parte: +//// tab | Python 3.10+ ```Python -= None +q: Annotated[str | None] = None ``` -Ou com: +//// + +//// tab | Python 3.9+ ```Python -= Query(default=None) +q: Annotated[Union[str, None]] = None ``` -E irá utilizar o `None` para detectar que o parâmetro de consulta não é obrigatório. +//// -O `Union` é apenas para permitir que seu editor de texto lhe dê um melhor suporte. +Ambas as versões significam a mesma coisa, `q` é um parâmetro que pode ser `str` ou `None`, e por padrão é `None`. + +Agora vamos pular para a parte divertida. 🎉 + +## Adicione `Query` ao `Annotated` no parâmetro `q` { #add-query-to-annotated-in-the-q-parameter } + +Agora que temos esse `Annotated` onde podemos colocar mais informações (neste caso, uma validação adicional), adicione `Query` dentro de `Annotated` e defina o parâmetro `max_length` como `50`: + +{* ../../docs_src/query_params_str_validations/tutorial002_an_py310.py hl[9] *} + +Perceba que o valor padrão continua sendo `None`, então o parâmetro ainda é opcional. + +Mas agora, com `Query(max_length=50)` dentro de `Annotated`, estamos dizendo ao FastAPI que queremos **validação adicional** para este valor, queremos que tenha no máximo 50 caracteres. 😎 + +/// tip | Dica + +Aqui estamos usando `Query()` porque este é um **parâmetro de consulta**. Mais adiante veremos outros como `Path()`, `Body()`, `Header()` e `Cookie()`, que também aceitam os mesmos argumentos que `Query()`. /// -Então, podemos passar mais parâmetros para `Query`. Neste caso, o parâmetro `max_length` que se aplica a textos: +Agora o FastAPI vai: -```Python -q: str = Query(default=None, max_length=50) -``` +* **Validar** os dados garantindo que o comprimento máximo seja de 50 caracteres +* Mostrar um **erro claro** para o cliente quando os dados não forem válidos +* **Documentar** o parâmetro na *operação de rota* do esquema OpenAPI (então ele aparecerá na **UI de docs automática**) -Isso irá validar os dados, mostrar um erro claro quando os dados forem inválidos, e documentar o parâmetro na *operação de rota* do esquema OpenAPI.. +## Alternativa (antiga): `Query` como valor padrão { #alternative-old-query-as-the-default-value } -## Adicionando mais validações +Versões anteriores do FastAPI (antes de 0.95.0) exigiam que você usasse `Query` como valor padrão do seu parâmetro, em vez de colocá-lo em `Annotated`. É muito provável que você veja código assim por aí, então vou te explicar. -Você também pode incluir um parâmetro `min_length`: +/// tip | Dica -{* ../../docs_src/query_params_str_validations/tutorial003.py hl[10] *} - -## Adicionando expressões regulares - -Você pode definir uma expressão regular que combine com um padrão esperado pelo parâmetro: - -{* ../../docs_src/query_params_str_validations/tutorial004.py hl[11] *} - -Essa expressão regular específica verifica se o valor recebido no parâmetro: - -* `^`: Inicia com os seguintes caracteres, ou seja, não contém caracteres anteriores. -* `fixedquery`: contém o valor exato `fixedquery`. -* `$`: termina aqui, não contém nenhum caractere após `fixedquery`. - -Se você se sente perdido com todo esse assunto de **"expressão regular"**, não se preocupe. Esse é um assunto complicado para a maioria das pessoas. Você ainda pode fazer muitas coisas sem utilizar expressões regulares. - -Mas assim que você precisar e já tiver aprendido sobre, saiba que você poderá usá-las diretamente no **FastAPI**. - -## Valores padrão - -Da mesma maneira que você utiliza `None` como o primeiro argumento para ser utilizado como um valor padrão, você pode usar outros valores. - -Vamos dizer que você queira que o parâmetro de consulta `q` tenha um `min_length` de `3`, e um valor padrão de `"fixedquery"`, então declararíamos assim: - -{* ../../docs_src/query_params_str_validations/tutorial005.py hl[7] *} - -/// note | Observação - -O parâmetro torna-se opcional quando possui um valor padrão. +Para código novo e sempre que possível, use `Annotated` como explicado acima. Há múltiplas vantagens (explicadas abaixo) e nenhuma desvantagem. 🍰 /// -## Torne-o obrigatório +É assim que você usaria `Query()` como valor padrão do parâmetro da sua função, definindo o parâmetro `max_length` como 50: -Quando você não necessita de validações ou de metadados adicionais, podemos fazer com que o parâmetro de consulta `q` seja obrigatório por não declarar um valor padrão, dessa forma: +{* ../../docs_src/query_params_str_validations/tutorial002_py310.py hl[7] *} + +Como neste caso (sem usar `Annotated`) temos que substituir o valor padrão `None` na função por `Query()`, agora precisamos definir o valor padrão com o parâmetro `Query(default=None)`, ele serve ao mesmo propósito de definir esse valor padrão (pelo menos para o FastAPI). + +Então: + +```Python +q: str | None = Query(default=None) +``` + +...torna o parâmetro opcional, com um valor padrão de `None`, o mesmo que: + + +```Python +q: str | None = None +``` + +Mas a versão com `Query` o declara explicitamente como sendo um parâmetro de consulta. + +Então, podemos passar mais parâmetros para `Query`. Neste caso, o parâmetro `max_length` que se aplica a strings: + +```Python +q: str | None = Query(default=None, max_length=50) +``` + +Isso validará os dados, mostrará um erro claro quando os dados não forem válidos e documentará o parâmetro na *operação de rota* do esquema OpenAPI. + +### `Query` como valor padrão ou em `Annotated` { #query-as-the-default-value-or-in-annotated } + +Tenha em mente que, ao usar `Query` dentro de `Annotated`, você não pode usar o parâmetro `default` de `Query`. + +Em vez disso, use o valor padrão real do parâmetro da função. Caso contrário, haveria inconsistência. + +Por exemplo, isto não é permitido: + +```Python +q: Annotated[str, Query(default="rick")] = "morty" +``` + +...porque não está claro se o valor padrão deveria ser `"rick"` ou `"morty"`. + +Então, você usaria (preferencialmente): + +```Python +q: Annotated[str, Query()] = "rick" +``` + +...ou em bases de código mais antigas você encontrará: + +```Python +q: str = Query(default="rick") +``` + +### Vantagens de `Annotated` { #advantages-of-annotated } + +**Usar `Annotated` é recomendado** em vez do valor padrão nos parâmetros da função, é **melhor** por vários motivos. 🤓 + +O valor **padrão** do **parâmetro da função** é o **valor padrão real**, isso é mais intuitivo com Python em geral. 😌 + +Você poderia **chamar** essa mesma função em **outros lugares** sem FastAPI, e ela **funcionaria como esperado**. Se houver um parâmetro **obrigatório** (sem valor padrão), seu **editor** vai avisar com um erro, e o **Python** também reclamará se você executá-la sem passar o parâmetro obrigatório. + +Quando você não usa `Annotated` e em vez disso usa o estilo de **valor padrão (antigo)**, se você chamar essa função sem FastAPI em **outros lugares**, terá que **lembrar** de passar os argumentos para a função para que funcione corretamente, caso contrário os valores serão diferentes do esperado (por exemplo, `QueryInfo` ou algo parecido em vez de `str`). E seu editor não vai avisar, e o Python também não vai reclamar ao executar a função, apenas quando as operações internas falharem. + +Como `Annotated` pode ter mais de uma anotação de metadados, você agora pode até usar a mesma função com outras ferramentas, como o Typer. 🚀 + +## Adicione mais validações { #add-more-validations } + +Você também pode adicionar um parâmetro `min_length`: + +{* ../../docs_src/query_params_str_validations/tutorial003_an_py310.py hl[10] *} + +## Adicione expressões regulares { #add-regular-expressions } + +Você pode definir um `pattern` de expressão regular que o parâmetro deve corresponder: + +{* ../../docs_src/query_params_str_validations/tutorial004_an_py310.py hl[11] *} + +Esse padrão específico de expressão regular verifica se o valor recebido no parâmetro: + +* `^`: começa com os caracteres seguintes, não tem caracteres antes. +* `fixedquery`: tem exatamente o valor `fixedquery`. +* `$`: termina ali, não tem mais caracteres depois de `fixedquery`. + +Se você se sentir perdido com essas ideias de **"expressão regular"**, não se preocupe. Esse é um assunto difícil para muitas pessoas. Você ainda pode fazer muitas coisas sem precisar de expressões regulares por enquanto. + +Agora você sabe que, sempre que precisar delas, pode usá-las no **FastAPI**. + +### Pydantic v1 `regex` em vez de `pattern` { #pydantic-v1-regex-instead-of-pattern } + +Antes da versão 2 do Pydantic e antes do FastAPI 0.100.0, o parâmetro se chamava `regex` em vez de `pattern`, mas agora está descontinuado. + +Você ainda pode ver algum código usando isso: + +//// tab | Pydantic v1 + +{* ../../docs_src/query_params_str_validations/tutorial004_regex_an_py310.py hl[11] *} + +//// + +Mas saiba que isso está descontinuado e deve ser atualizado para usar o novo parâmetro `pattern`. 🤓 + +## Valores padrão { #default-values } + +Você pode, claro, usar valores padrão diferentes de `None`. + +Digamos que você queira declarar o parâmetro de consulta `q` com `min_length` de `3` e ter um valor padrão de `"fixedquery"`: + +{* ../../docs_src/query_params_str_validations/tutorial005_an_py39.py hl[9] *} + +/// note | Nota + +Ter um valor padrão de qualquer tipo, incluindo `None`, torna o parâmetro opcional (não obrigatório). + +/// + +## Parâmetros obrigatórios { #required-parameters } + +Quando não precisamos declarar mais validações ou metadados, podemos tornar o parâmetro de consulta `q` obrigatório simplesmente não declarando um valor padrão, assim: ```Python q: str ``` -em vez desta: +em vez de: ```Python -q: Union[str, None] = None +q: str | None = None ``` -Mas agora nós o estamos declarando como `Query`, conforme abaixo: +Mas agora estamos declarando com `Query`, por exemplo assim: ```Python -q: Union[str, None] = Query(default=None, min_length=3) +q: Annotated[str | None, Query(min_length=3)] = None ``` -Então, quando você precisa declarar um parâmetro obrigatório utilizando o `Query`, você pode utilizar `...` como o primeiro argumento: +Então, quando você precisa declarar um valor como obrigatório usando `Query`, você pode simplesmente não declarar um valor padrão: -{* ../../docs_src/query_params_str_validations/tutorial006.py hl[7] *} +{* ../../docs_src/query_params_str_validations/tutorial006_an_py39.py hl[9] *} -/// info | Informação +### Obrigatório, pode ser `None` { #required-can-be-none } -Se você nunca viu os `...` antes: é um valor único especial, faz parte do Python e é chamado "Ellipsis". +Você pode declarar que um parâmetro pode aceitar `None`, mas que ainda assim é obrigatório. Isso forçaria os clientes a enviarem um valor, mesmo que o valor seja `None`. -/// +Para isso, você pode declarar que `None` é um tipo válido, mas simplesmente não declarar um valor padrão: -Dessa forma o **FastAPI** saberá que o parâmetro é obrigatório. +{* ../../docs_src/query_params_str_validations/tutorial006c_an_py310.py hl[9] *} -## Lista de parâmetros de consulta / múltiplos valores +## Lista de parâmetros de consulta / múltiplos valores { #query-parameter-list-multiple-values } -Quando você declara explicitamente um parâmetro com `Query` você pode declará-lo para receber uma lista de valores, ou podemos dizer, que irá receber mais de um valor. +Quando você define explicitamente um parâmetro de consulta com `Query`, você também pode declará-lo para receber uma lista de valores, ou seja, receber múltiplos valores. -Por exemplo, para declarar que o parâmetro `q` pode aparecer diversas vezes na URL, você escreveria: +Por exemplo, para declarar um parâmetro de consulta `q` que pode aparecer várias vezes na URL, você pode escrever: -{* ../../docs_src/query_params_str_validations/tutorial011.py hl[9] *} +{* ../../docs_src/query_params_str_validations/tutorial011_an_py310.py hl[9] *} -Então, com uma URL assim: +Então, com uma URL como: ``` http://localhost:8000/items/?q=foo&q=bar ``` -você receberá os múltiplos *parâmetros de consulta* `q` com os valores (`foo` e `bar`) em uma lista (`list`) Python dentro da *função de operação de rota*, no *parâmetro da função* `q`. +você receberá os múltiplos valores do *parâmetro de consulta* `q` (`foo` e `bar`) em uma `list` Python dentro da sua *função de operação de rota*, no *parâmetro da função* `q`. Assim, a resposta para essa URL seria: @@ -173,19 +295,19 @@ Assim, a resposta para essa URL seria: /// tip | Dica -Para declarar um parâmetro de consulta com o tipo `list`, como no exemplo acima, você precisa usar explicitamente o `Query`, caso contrário será interpretado como um corpo da requisição. +Para declarar um parâmetro de consulta com tipo `list`, como no exemplo acima, você precisa usar explicitamente `Query`, caso contrário seria interpretado como um corpo da requisição. /// -A documentação interativa da API irá atualizar de acordo, permitindo múltiplos valores: +A documentação interativa da API será atualizada de acordo, permitindo múltiplos valores: -### Lista de parâmetros de consulta / múltiplos valores por padrão +### Lista de parâmetros de consulta / múltiplos valores com valores padrão { #query-parameter-list-multiple-values-with-defaults } -E você também pode definir uma lista (`list`) de valores padrão caso nenhum seja informado: +Você também pode definir uma `list` de valores padrão caso nenhum seja fornecido: -{* ../../docs_src/query_params_str_validations/tutorial012.py hl[9] *} +{* ../../docs_src/query_params_str_validations/tutorial012_an_py39.py hl[9] *} Se você for até: @@ -193,7 +315,7 @@ Se você for até: http://localhost:8000/items/ ``` -O valor padrão de `q` será: `["foo", "bar"]` e sua resposta será: +o valor padrão de `q` será: `["foo", "bar"]` e sua resposta será: ```JSON { @@ -204,93 +326,163 @@ O valor padrão de `q` será: `["foo", "bar"]` e sua resposta será: } ``` -#### Usando `list` +#### Usando apenas `list` { #using-just-list } -Você também pode utilizar o tipo `list` diretamente em vez de `List[str]`: +Você também pode usar `list` diretamente em vez de `list[str]`: -{* ../../docs_src/query_params_str_validations/tutorial013.py hl[7] *} +{* ../../docs_src/query_params_str_validations/tutorial013_an_py39.py hl[9] *} -/// note | Observação +/// note | Nota -Tenha em mente que neste caso, o FastAPI não irá validar os conteúdos da lista. +Tenha em mente que, neste caso, o FastAPI não verificará o conteúdo da lista. -Por exemplo, um `List[int]` iria validar (e documentar) que os contéudos da lista são números inteiros. Mas apenas `list` não. +Por exemplo, `list[int]` verificaria (e documentaria) que os conteúdos da lista são inteiros. Mas `list` sozinho não. /// -## Declarando mais metadados +## Declare mais metadados { #declare-more-metadata } Você pode adicionar mais informações sobre o parâmetro. -Essa informações serão inclusas no esquema do OpenAPI e utilizado pela documentação interativa e ferramentas externas. +Essas informações serão incluídas no OpenAPI gerado e usadas pelas interfaces de documentação e por ferramentas externas. -/// note | Observação +/// note | Nota -Tenha em mente que cada ferramenta oferece diferentes níveis de suporte ao OpenAPI. +Tenha em mente que ferramentas diferentes podem ter níveis diferentes de suporte ao OpenAPI. -Algumas delas não exibem todas as informações extras que declaramos, ainda que na maioria dos casos, esses recursos estão planejados para desenvolvimento. +Algumas delas podem ainda não mostrar todas as informações extras declaradas, embora na maioria dos casos o recurso ausente já esteja planejado para desenvolvimento. /// Você pode adicionar um `title`: -{* ../../docs_src/query_params_str_validations/tutorial007.py hl[10] *} +{* ../../docs_src/query_params_str_validations/tutorial007_an_py310.py hl[10] *} E uma `description`: -{* ../../docs_src/query_params_str_validations/tutorial008.py hl[13] *} +{* ../../docs_src/query_params_str_validations/tutorial008_an_py310.py hl[14] *} -## Apelidos (alias) de parâmetros +## Parâmetros com alias { #alias-parameters } -Imagine que você queira que um parâmetro tenha o nome `item-query`. +Imagine que você queira que o parâmetro seja `item-query`. -Desta maneira: +Assim: ``` http://127.0.0.1:8000/items/?item-query=foobaritems ``` -Mas o nome `item-query` não é um nome de váriavel válido no Python. +Mas `item-query` não é um nome de variável Python válido. -O que mais se aproxima é `item_query`. +O mais próximo seria `item_query`. -Mas ainda você precisa que o nome seja exatamente `item-query`... +Mas você ainda precisa que seja exatamente `item-query`... -Então você pode declarar um `alias`, e esse apelido (alias) que será utilizado para encontrar o valor do parâmetro: +Então você pode declarar um `alias`, e esse alias será usado para encontrar o valor do parâmetro: -{* ../../docs_src/query_params_str_validations/tutorial009.py hl[9] *} +{* ../../docs_src/query_params_str_validations/tutorial009_an_py310.py hl[9] *} -## Parâmetros descontinuados +## Descontinuando parâmetros { #deprecating-parameters } -Agora vamos dizer que você não queria mais utilizar um parâmetro. +Agora digamos que você não gosta mais desse parâmetro. -Você tem que deixá-lo ativo por um tempo, já que existem clientes o utilizando. Mas você quer que a documentação deixe claro que este parâmetro será descontinuado. +Você tem que deixá-lo por um tempo, pois há clientes usando-o, mas quer que a documentação mostre claramente que ele está descontinuado. -Então você passa o parâmetro `deprecated=True` para `Query`: +Então passe o parâmetro `deprecated=True` para `Query`: -{* ../../docs_src/query_params_str_validations/tutorial010.py hl[18] *} +{* ../../docs_src/query_params_str_validations/tutorial010_an_py310.py hl[19] *} -Na documentação aparecerá assim: +A documentação vai mostrar assim: -## Recapitulando +## Excluir parâmetros do OpenAPI { #exclude-parameters-from-openapi } -Você pode adicionar validações e metadados adicionais aos seus parâmetros. +Para excluir um parâmetro de consulta do OpenAPI gerado (e portanto, dos sistemas de documentação automáticos), defina o parâmetro `include_in_schema` de `Query` como `False`: -Validações genéricas e metadados: +{* ../../docs_src/query_params_str_validations/tutorial014_an_py310.py hl[10] *} + +## Validação personalizada { #custom-validation } + +Podem existir casos em que você precise fazer alguma **validação personalizada** que não pode ser feita com os parâmetros mostrados acima. + +Nesses casos, você pode usar uma **função validadora personalizada** que é aplicada após a validação normal (por exemplo, depois de validar que o valor é uma `str`). + +Você pode fazer isso usando o `AfterValidator` do Pydantic dentro de `Annotated`. + +/// tip | Dica + +O Pydantic também tem `BeforeValidator` e outros. 🤓 + +/// + +Por exemplo, este validador personalizado verifica se o ID do item começa com `isbn-` para um número de livro ISBN ou com `imdb-` para um ID de URL de filme IMDB: + +{* ../../docs_src/query_params_str_validations/tutorial015_an_py310.py hl[5,16:19,24] *} + +/// info | Informação + +Isso está disponível com a versão 2 do Pydantic ou superior. 😎 + +/// + +/// tip | Dica + +Se você precisar fazer qualquer tipo de validação que exija comunicação com algum **componente externo**, como um banco de dados ou outra API, você deve usar **Dependências do FastAPI** em vez disso; você aprenderá sobre elas mais adiante. + +Esses validadores personalizados são para coisas que podem ser verificadas **apenas** com os **mesmos dados** fornecidos na requisição. + +/// + +### Entenda esse código { #understand-that-code } + +O ponto importante é apenas usar **`AfterValidator` com uma função dentro de `Annotated`**. Sinta-se à vontade para pular esta parte. 🤸 + +--- + +Mas se você está curioso sobre este exemplo específico e ainda entretido, aqui vão alguns detalhes extras. + +#### String com `value.startswith()` { #string-with-value-startswith } + +Percebeu? Uma string usando `value.startswith()` pode receber uma tupla, e verificará cada valor na tupla: + +{* ../../docs_src/query_params_str_validations/tutorial015_an_py310.py ln[16:19] hl[17] *} + +#### Um item aleatório { #a-random-item } + +Com `data.items()` obtemos um objeto iterável com tuplas contendo a chave e o valor de cada item do dicionário. + +Convertimos esse objeto iterável em uma `list` adequada com `list(data.items())`. + +Em seguida, com `random.choice()` podemos obter um **valor aleatório** da lista, então obtemos uma tupla com `(id, name)`. Será algo como `("imdb-tt0371724", "The Hitchhiker's Guide to the Galaxy")`. + +Depois **atribuímos esses dois valores** da tupla às variáveis `id` e `name`. + +Assim, se o usuário não fornecer um ID de item, ele ainda receberá uma sugestão aleatória. + +...fazemos tudo isso em **uma única linha simples**. 🤯 Você não ama Python? 🐍 + +{* ../../docs_src/query_params_str_validations/tutorial015_an_py310.py ln[22:30] hl[29] *} + +## Recapitulando { #recap } + +Você pode declarar validações adicionais e metadados para seus parâmetros. + +Validações e metadados genéricos: * `alias` * `title` * `description` * `deprecated` -Validações específicas para textos: +Validações específicas para strings: * `min_length` * `max_length` -* `regex` +* `pattern` -Nesses exemplos você viu como declarar validações em valores do tipo `str`. +Validações personalizadas usando `AfterValidator`. -Leia os próximos capítulos para ver como declarar validação de outros tipos, como números. +Nestes exemplos você viu como declarar validações para valores `str`. + +Veja os próximos capítulos para aprender a declarar validações para outros tipos, como números. diff --git a/docs/pt/docs/tutorial/query-params.md b/docs/pt/docs/tutorial/query-params.md index 8199de5af9..8826602a25 100644 --- a/docs/pt/docs/tutorial/query-params.md +++ b/docs/pt/docs/tutorial/query-params.md @@ -1,8 +1,8 @@ -# Parâmetros de Consulta +# Parâmetros de Consulta { #query-parameters } Quando você declara outros parâmetros na função que não fazem parte dos parâmetros da rota, esses parâmetros são automaticamente interpretados como parâmetros de "consulta". -{* ../../docs_src/query_params/tutorial001.py hl[9] *} +{* ../../docs_src/query_params/tutorial001_py39.py hl[9] *} A consulta é o conjunto de pares chave-valor que vai depois de `?` na URL, separado pelo caractere `&`. @@ -28,7 +28,7 @@ Todo o processo que era aplicado para parâmetros de rota também é aplicado pa * Validação de dados * Documentação automática -## Valores padrão +## Valores padrão { #defaults } Como os parâmetros de consulta não são uma parte fixa da rota, eles podem ser opcionais e podem ter valores padrão. @@ -57,7 +57,7 @@ Os valores dos parâmetros na sua função serão: * `skip=20`: Por que você definiu isso na URL * `limit=10`: Por que esse era o valor padrão -## Parâmetros opcionais +## Parâmetros opcionais { #optional-parameters } Da mesma forma, você pode declarar parâmetros de consulta opcionais, definindo o valor padrão para `None`: @@ -65,13 +65,13 @@ Da mesma forma, você pode declarar parâmetros de consulta opcionais, definindo Nesse caso, o parâmetro da função `q` será opcional, e `None` será o padrão. -/// check | Verificar +/// check | Verifique Você também pode notar que o **FastAPI** é esperto o suficiente para perceber que o parâmetro da rota `item_id` é um parâmetro da rota, e `q` não é, portanto, `q` é o parâmetro de consulta. /// -## Conversão dos tipos de parâmetros de consulta +## Conversão dos tipos de parâmetros de consulta { #query-parameter-type-conversion } Você também pode declarar tipos `bool`, e eles serão convertidos: @@ -109,7 +109,7 @@ http://127.0.0.1:8000/items/foo?short=yes ou qualquer outra variação (tudo em maiúscula, primeira letra em maiúscula, etc), a sua função vai ver o parâmetro `short` com um valor `bool` de `True`. Caso contrário `False`. -## Múltiplos parâmetros de rota e consulta +## Múltiplos parâmetros de rota e consulta { #multiple-path-and-query-parameters } Você pode declarar múltiplos parâmetros de rota e parâmetros de consulta ao mesmo tempo, o **FastAPI** vai saber o quê é o quê. @@ -119,7 +119,7 @@ Eles serão detectados pelo nome: {* ../../docs_src/query_params/tutorial004_py310.py hl[6,8] *} -## Parâmetros de consulta obrigatórios +## Parâmetros de consulta obrigatórios { #required-query-parameters } Quando você declara um valor padrão para parâmetros que não são de rota (até agora, nós vimos apenas parâmetros de consulta), então eles não são obrigatórios. @@ -127,7 +127,7 @@ Caso você não queira adicionar um valor específico mas queira apenas torná-l Porém, quando você quiser fazer com que o parâmetro de consulta seja obrigatório, você pode simplesmente não declarar nenhum valor como padrão. -{* ../../docs_src/query_params/tutorial005.py hl[6:7] *} +{* ../../docs_src/query_params/tutorial005_py39.py hl[6:7] *} Aqui o parâmetro de consulta `needy` é um valor obrigatório, do tipo `str`. @@ -141,16 +141,17 @@ http://127.0.0.1:8000/items/foo-item ```JSON { - "detail": [ - { - "loc": [ - "query", - "needy" - ], - "msg": "field required", - "type": "value_error.missing" - } - ] + "detail": [ + { + "type": "missing", + "loc": [ + "query", + "needy" + ], + "msg": "Field required", + "input": null + } + ] } ``` @@ -181,6 +182,6 @@ Nesse caso, existem 3 parâmetros de consulta: /// tip | Dica -Você também poderia usar `Enum` da mesma forma que com [Path Parameters](path-params.md#valores-predefinidos){.internal-link target=_blank}. +Você também poderia usar `Enum` da mesma forma que com [Path Parameters](path-params.md#predefined-values){.internal-link target=_blank}. /// diff --git a/docs/pt/docs/tutorial/request-files.md b/docs/pt/docs/tutorial/request-files.md index c22c1c5133..5d0891163a 100644 --- a/docs/pt/docs/tutorial/request-files.md +++ b/docs/pt/docs/tutorial/request-files.md @@ -1,4 +1,4 @@ -# Arquivos de Requisição +# Arquivos de Requisição { #request-files } Você pode definir arquivos para serem enviados pelo cliente usando `File`. @@ -16,13 +16,13 @@ Isso é necessário, visto que os arquivos enviados são enviados como "dados de /// -## Importe `File` +## Importe `File` { #import-file } Importe `File` e `UploadFile` de `fastapi`: {* ../../docs_src/request_files/tutorial001_an_py39.py hl[3] *} -## Definir Parâmetros `File` +## Definir Parâmetros `File` { #define-file-parameters } Crie parâmetros de arquivo da mesma forma que você faria para `Body` ou `Form`: @@ -50,7 +50,7 @@ Mantenha em mente que isso significa que todo o conteúdo será armazenado na me Mas há muitos casos em que você pode se beneficiar do uso de `UploadFile`. -## Parâmetros de Arquivo com `UploadFile` +## Parâmetros de Arquivo com `UploadFile` { #file-parameters-with-uploadfile } Defina um parâmetro de arquivo com um tipo de `UploadFile`: @@ -66,12 +66,12 @@ Utilizar `UploadFile` tem várias vantagens sobre `bytes`: * Ele tem uma file-like interface `assíncrona`. * Ele expõe um objeto python `SpooledTemporaryFile` que você pode passar diretamente para outras bibliotecas que esperam um objeto semelhante a um arquivo("file-like"). -### `UploadFile` +### `UploadFile` { #uploadfile } `UploadFile` tem os seguintes atributos: * `filename`: Uma `str` com o nome do arquivo original que foi enviado (por exemplo, `myimage.jpg`). -* `content_type`: Uma `str` com o tipo de conteúdo (tipo MIME / tipo de mídia) (por exemplo, `image/jpeg`). +* `content_type`: Uma `str` com o tipo de conteúdo (MIME type / media type) (por exemplo, `image/jpeg`). * `file`: Um `SpooledTemporaryFile` (um file-like objeto). Este é o objeto de arquivo Python que você pode passar diretamente para outras funções ou bibliotecas que esperam um objeto semelhante a um arquivo("file-like"). `UploadFile` tem os seguintes métodos `assíncronos`. Todos eles chamam os métodos de arquivo correspondentes por baixo dos panos (usando o `SpooledTemporaryFile` interno). @@ -105,11 +105,11 @@ Quando você usa os métodos `async`, o **FastAPI** executa os métodos de arqui /// note | Detalhes Técnicos do Starlette -O `UploadFile` do ***FastAPI** herda diretamente do `UploadFile` do **Starlette** , mas adiciona algumas partes necessárias para torná-lo compatível com o **Pydantic** e as outras partes do FastAPI. +O `UploadFile` do **FastAPI** herda diretamente do `UploadFile` do **Starlette**, mas adiciona algumas partes necessárias para torná-lo compatível com o **Pydantic** e as outras partes do FastAPI. /// -## O que é "Form Data" +## O que é "Form Data" { #what-is-form-data } O jeito que os formulários HTML (`
`) enviam os dados para o servidor normalmente usa uma codificação "especial" para esses dados, a qual é diferente do JSON. @@ -117,15 +117,15 @@ O jeito que os formulários HTML (`
`) enviam os dados para o servid /// note | Detalhes Técnicos -Dados de formulários normalmente são codificados usando o "media type" (tipo de mídia) `application/x-www-form-urlencoded` quando não incluem arquivos. +Dados de formulários normalmente são codificados usando o "media type" `application/x-www-form-urlencoded` quando não incluem arquivos. Mas quando o formulário inclui arquivos, ele é codificado como `multipart/form-data`. Se você usar `File`, o **FastAPI** saberá que tem que pegar os arquivos da parte correta do corpo da requisição. -Se você quiser ler mais sobre essas codificações e campos de formulário, vá para a MDN web docs para POST. +Se você quiser ler mais sobre essas codificações e campos de formulário, vá para a MDN web docs para POST. /// -/// warning | Aviso +/// warning | Atenção Você pode declarar múltiplos parâmetros `File` e `Form` em uma *operação de rota*, mas você não pode declarar campos `Body` que você espera receber como JSON, pois a requisição terá o corpo codificado usando `multipart/form-data` ao invés de `application/json`. @@ -133,19 +133,19 @@ Isso não é uma limitação do **FastAPI**, é parte do protocolo HTTP. /// -## Upload de Arquivo Opcional +## Upload de Arquivo Opcional { #optional-file-upload } Você pode tornar um arquivo opcional usando anotações de tipo padrão e definindo um valor padrão de `None`: {* ../../docs_src/request_files/tutorial001_02_an_py310.py hl[9,17] *} -## `UploadFile` com Metadados Adicionais +## `UploadFile` com Metadados Adicionais { #uploadfile-with-additional-metadata } Você também pode usar `File()` com `UploadFile`, por exemplo, para definir metadados adicionais: {* ../../docs_src/request_files/tutorial001_03_an_py39.py hl[9,15] *} -## Uploads de Múltiplos Arquivos +## Uploads de Múltiplos Arquivos { #multiple-file-uploads } É possível realizar o upload de vários arquivos ao mesmo tempo. @@ -165,12 +165,12 @@ Você pode também pode usar `from starlette.responses import HTMLResponse`. /// -### Uploads de Múltiplos Arquivos com Metadados Adicionais +### Uploads de Múltiplos Arquivos com Metadados Adicionais { #multiple-file-uploads-with-additional-metadata } Da mesma forma de antes, você pode usar `File()` para definir parâmetros adicionais, mesmo para `UploadFile`: {* ../../docs_src/request_files/tutorial003_an_py39.py hl[11,18:20] *} -## Recapitulando +## Recapitulando { #recap } Utilize `File`, `bytes` e `UploadFile` para declarar arquivos a serem enviados na requisição, enviados como dados de formulário. diff --git a/docs/pt/docs/tutorial/request-form-models.md b/docs/pt/docs/tutorial/request-form-models.md index ea0e63d381..8eeffac2ad 100644 --- a/docs/pt/docs/tutorial/request-form-models.md +++ b/docs/pt/docs/tutorial/request-form-models.md @@ -1,4 +1,4 @@ -# Modelos de Formulários +# Modelos de Formulários { #form-models } Você pode utilizar **Modelos Pydantic** para declarar **campos de formulários** no FastAPI. @@ -20,7 +20,7 @@ Isto é suportado desde a versão `0.113.0` do FastAPI. 🤓 /// -## Modelos Pydantic para Formulários +## Modelos Pydantic para Formulários { #pydantic-models-for-forms } Você precisa apenas declarar um **modelo Pydantic** com os campos que deseja receber como **campos de formulários**, e então declarar o parâmetro como um `Form`: @@ -28,7 +28,7 @@ Você precisa apenas declarar um **modelo Pydantic** com os campos que deseja re O **FastAPI** irá **extrair** as informações para **cada campo** dos **dados do formulário** na requisição e dar para você o modelo Pydantic que você definiu. -## Confira os Documentos +## Confira os Documentos { #check-the-docs } Você pode verificar na UI de documentação em `/docs`: @@ -36,7 +36,7 @@ Você pode verificar na UI de documentação em `/docs`:
-## Proibir Campos Extras de Formulários +## Proibir Campos Extras de Formulários { #forbid-extra-form-fields } Em alguns casos de uso especiais (provavelmente não muito comum), você pode desejar **restringir** os campos do formulário para aceitar apenas os declarados no modelo Pydantic. E **proibir** qualquer campo **extra**. @@ -73,6 +73,6 @@ Ele receberá um retorno de erro informando-o que o campo `extra` não é permit } ``` -## Resumo +## Resumo { #summary } Você pode utilizar modelos Pydantic para declarar campos de formulários no FastAPI. 😎 diff --git a/docs/pt/docs/tutorial/request-forms-and-files.md b/docs/pt/docs/tutorial/request-forms-and-files.md index b08d870132..277fc2f600 100644 --- a/docs/pt/docs/tutorial/request-forms-and-files.md +++ b/docs/pt/docs/tutorial/request-forms-and-files.md @@ -1,4 +1,4 @@ -# Formulários e Arquivos da Requisição +# Formulários e Arquivos da Requisição { #request-forms-and-files } Você pode definir arquivos e campos de formulário ao mesmo tempo usando `File` e `Form`. @@ -6,32 +6,36 @@ Você pode definir arquivos e campos de formulário ao mesmo tempo usando `File` Para receber arquivos carregados e/ou dados de formulário, primeiro instale `python-multipart`. -Por exemplo: `pip install python-multipart`. +Certifique-se de criar um [ambiente virtual](../virtual-environments.md){.internal-link target=_blank}, ativá-lo e então instalar, por exemplo: + +```console +$ pip install python-multipart +``` /// -## Importe `File` e `Form` +## Importe `File` e `Form` { #import-file-and-form } -{* ../../docs_src/request_forms_and_files/tutorial001.py hl[1] *} +{* ../../docs_src/request_forms_and_files/tutorial001_an_py39.py hl[3] *} -## Defina parâmetros de `File` e `Form` +## Defina parâmetros de `File` e `Form` { #define-file-and-form-parameters } Crie parâmetros de arquivo e formulário da mesma forma que você faria para `Body` ou `Query`: -{* ../../docs_src/request_forms_and_files/tutorial001.py hl[8] *} +{* ../../docs_src/request_forms_and_files/tutorial001_an_py39.py hl[10:12] *} Os arquivos e campos de formulário serão carregados como dados de formulário e você receberá os arquivos e campos de formulário. E você pode declarar alguns dos arquivos como `bytes` e alguns como `UploadFile`. -/// warning | Aviso +/// warning | Atenção Você pode declarar vários parâmetros `File` e `Form` em uma *operação de caminho*, mas não é possível declarar campos `Body` para receber como JSON, pois a requisição terá o corpo codificado usando `multipart/form-data` ao invés de `application/json`. -Isso não é uma limitação do **FastAPI** , é parte do protocolo HTTP. +Isso não é uma limitação do **FastAPI**, é parte do protocolo HTTP. /// -## Recapitulando +## Recapitulando { #recap } Usar `File` e `Form` juntos quando precisar receber dados e arquivos na mesma requisição. diff --git a/docs/pt/docs/tutorial/request-forms.md b/docs/pt/docs/tutorial/request-forms.md index 572ddf003b..faa50bcbfe 100644 --- a/docs/pt/docs/tutorial/request-forms.md +++ b/docs/pt/docs/tutorial/request-forms.md @@ -1,12 +1,12 @@ -# Dados do formulário +# Dados do formulário { #form-data } -Quando você precisar receber campos de formulário ao invés de JSON, você pode usar `Form`. +Quando você precisar receber campos de formulário em vez de JSON, você pode usar `Form`. /// info | Informação Para usar formulários, primeiro instale `python-multipart`. -Lembre-se de criar um [ambiente virtual](../virtual-environments.md){.internal-link target=_blank}, ativá-lo e então instalar a dependência, por exemplo: +Certifique-se de criar um [ambiente virtual](../virtual-environments.md){.internal-link target=_blank}, ativá-lo e então instalá-lo, por exemplo: ```console $ pip install python-multipart @@ -14,23 +14,23 @@ $ pip install python-multipart /// -## Importe `Form` +## Importe `Form` { #import-form } Importe `Form` de `fastapi`: -{* ../../docs_src/request_forms/tutorial001.py hl[1] *} +{* ../../docs_src/request_forms/tutorial001_an_py39.py hl[3] *} -## Declare parâmetros de `Form` +## Defina parâmetros de `Form` { #define-form-parameters } Crie parâmetros de formulário da mesma forma que você faria para `Body` ou `Query`: -{* ../../docs_src/request_forms/tutorial001.py hl[7] *} +{* ../../docs_src/request_forms/tutorial001_an_py39.py hl[9] *} Por exemplo, em uma das maneiras que a especificação OAuth2 pode ser usada (chamada "fluxo de senha"), é necessário enviar um `username` e uma `password` como campos do formulário. -A spec exige que os campos sejam exatamente nomeados como `username` e `password` e sejam enviados como campos de formulário, não JSON. +A spec exige que os campos sejam exatamente nomeados como `username` e `password` e sejam enviados como campos de formulário, não JSON. -Com `Form` você pode declarar os mesmos metadados e validação que com `Body` (e `Query`, `Path`, `Cookie`). +Com `Form` você pode declarar as mesmas configurações que com `Body` (e `Query`, `Path`, `Cookie`), incluindo validação, exemplos, um alias (por exemplo, `user-name` em vez de `username`), etc. /// info | Informação @@ -44,30 +44,30 @@ Para declarar corpos de formulário, você precisa usar `Form` explicitamente, p /// -## Sobre "Campos de formulário" +## Sobre "Campos de formulário" { #about-form-fields } A forma como os formulários HTML (`
`) enviam os dados para o servidor normalmente usa uma codificação "especial" para esses dados, é diferente do JSON. O **FastAPI** fará a leitura desses dados no lugar certo em vez de JSON. -/// note | Detalhes técnicos +/// note | Detalhes Técnicos -Os dados dos formulários são normalmente codificados usando o "tipo de mídia" `application/x-www-form-urlencoded`. +Os dados dos formulários são normalmente codificados usando o "media type" `application/x-www-form-urlencoded`. - Mas quando o formulário inclui arquivos, ele é codificado como `multipart/form-data`. Você lerá sobre como lidar com arquivos no próximo capítulo. +Mas quando o formulário inclui arquivos, ele é codificado como `multipart/form-data`. Você lerá sobre como lidar com arquivos no próximo capítulo. -Se você quiser ler mais sobre essas codificações e campos de formulário, vá para o MDN web docs para POST. +Se você quiser ler mais sobre essas codificações e campos de formulário, vá para o MDN web docs para POST. /// -/// warning | Aviso +/// warning | Atenção -Você pode declarar vários parâmetros `Form` em uma *operação de caminho*, mas não pode declarar campos `Body` que espera receber como JSON, pois a solicitação terá o corpo codificado usando `application/x-www- form-urlencoded` em vez de `application/json`. +Você pode declarar vários parâmetros `Form` em uma *operação de rota*, mas não pode declarar campos `Body` que espera receber como JSON, pois a requisição terá o corpo codificado usando `application/x-www-form-urlencoded` em vez de `application/json`. -Esta não é uma limitação do **FastAPI**, é parte do protocolo HTTP. +Isso não é uma limitação do **FastAPI**, é parte do protocolo HTTP. /// -## Recapitulando +## Recapitulando { #recap } Use `Form` para declarar os parâmetros de entrada de dados de formulário. diff --git a/docs/pt/docs/tutorial/request_files.md b/docs/pt/docs/tutorial/request_files.md deleted file mode 100644 index 15c1ad825a..0000000000 --- a/docs/pt/docs/tutorial/request_files.md +++ /dev/null @@ -1,172 +0,0 @@ -# Arquivos de Requisição - -Você pode definir arquivos para serem enviados para o cliente utilizando `File`. - -/// info - -Para receber arquivos compartilhados, primeiro instale `python-multipart`. - -E.g. `pip install python-multipart`. - -Isso se deve por que arquivos enviados são enviados como "dados de formulário". - -/// - -## Importe `File` - -Importe `File` e `UploadFile` do `fastapi`: - -{* ../../docs_src/request_files/tutorial001_an_py39.py hl[3] *} - -## Defina os parâmetros de `File` - -Cria os parâmetros do arquivo da mesma forma que você faria para `Body` ou `Form`: - -{* ../../docs_src/request_files/tutorial001_an_py39.py hl[9] *} - -/// info | Informação - -`File` é uma classe que herda diretamente de `Form`. - -Mas lembre-se que quando você importa `Query`,`Path`, `File`, entre outros, do `fastapi`, essas são na verdade funções que retornam classes especiais. - -/// - -/// tip | Dica - -Para declarar o corpo de arquivos, você precisa utilizar `File`, do contrário os parâmetros seriam interpretados como parâmetros de consulta ou corpo (JSON) da requisição. - -/// - -Os arquivos serão enviados como "form data". - -Se você declarar o tipo do seu parâmetro na sua *função de operação de rota* como `bytes`, o **FastAPI** irá ler o arquivo para você e você receberá o conteúdo como `bytes`. - -Lembre-se que isso significa que o conteúdo inteiro será armazenado em memória. Isso funciona bem para arquivos pequenos. - -Mas existem vários casos em que você pode se beneficiar ao usar `UploadFile`. - -## Parâmetros de arquivo com `UploadFile` - -Defina um parâmetro de arquivo com o tipo `UploadFile` - -{* ../../docs_src/request_files/tutorial001_an_py39.py hl[14] *} - -Utilizando `UploadFile` tem várias vantagens sobre `bytes`: - -* Você não precisa utilizar `File()` como o valor padrão do parâmetro. -* A classe utiliza um arquivo em "spool": - * Um arquivo guardado em memória até um tamanho máximo, depois desse limite ele é guardado em disco. -* Isso significa que a classe funciona bem com arquivos grandes como imagens, vídeos, binários extensos, etc. Sem consumir toda a memória. -* Você pode obter metadados do arquivo enviado. -* Ela possui uma interface semelhante a arquivos `async`. -* Ela expõe um objeto python `SpooledTemporaryFile` que você pode repassar para bibliotecas que esperam um objeto com comportamento de arquivo. - -### `UploadFile` - -`UploadFile` tem os seguintes atributos: - -* `filename`: Uma string (`str`) com o nome original do arquivo enviado (e.g. `myimage.jpg`). -* `content-type`: Uma `str` com o tipo do conteúdo (tipo MIME / media) (e.g. `image/jpeg`). -* `file`: Um objeto do tipo `SpooledTemporaryFile` (um objeto file-like). O arquivo propriamente dito que você pode passar diretamente para outras funções ou bibliotecas que esperam um objeto "file-like". - -`UploadFile` tem os seguintes métodos `async`. Todos eles chamam os métodos de arquivos por baixo dos panos (usando o objeto `SpooledTemporaryFile` interno). - -* `write(data)`: escreve dados (`data`) em `str` ou `bytes` no arquivo. -* `read(size)`: Lê um número de bytes/caracteres de acordo com a quantidade `size` (`int`). -* `seek(offset)`: Navega para o byte na posição `offset` (`int`) do arquivo. - * E.g., `await myfile.seek(0)` navegaria para o ínicio do arquivo. - * Isso é especialmente útil se você executar `await myfile.read()` uma vez e depois precisar ler os conteúdos do arquivo de novo. -* `close()`: Fecha o arquivo. - -Como todos esses métodos são assíncronos (`async`) você precisa esperar ("await") por eles. - -Por exemplo, dentro de uma *função de operação de rota* assíncrona você pode obter os conteúdos com: - -```Python -contents = await myfile.read() -``` - -Se você estiver dentro de uma *função de operação de rota* definida normalmente com `def`, você pode acessar `UploadFile.file` diretamente, por exemplo: - -```Python -contents = myfile.file.read() -``` - -/// note | Detalhes técnicos do `async` - -Quando você utiliza métodos assíncronos, o **FastAPI** executa os métodos do arquivo em uma threadpool e espera por eles. - -/// - -/// note | Detalhes técnicos do Starlette - -O `UploadFile` do **FastAPI** herda diretamente do `UploadFile` do **Starlette**, mas adiciona algumas funcionalidades necessárias para ser compatível com o **Pydantic** - -/// - -## O que é "Form Data" - -A forma como formulários HTML(`
`) enviam dados para o servidor normalmente utilizam uma codificação "especial" para esses dados, que é diferente do JSON. - -O **FastAPI** garante que os dados serão lidos da forma correta, em vez do JSON. - -/// note | Detalhes Técnicos - -Dados vindos de formulários geralmente tem a codificação com o "media type" `application/x-www-form-urlencoded` quando estes não incluem arquivos. - -Mas quando os dados incluem arquivos, eles são codificados como `multipart/form-data`. Se você utilizar `File`, **FastAPI** saberá que deve receber os arquivos da parte correta do corpo da requisição. - -Se você quer ler mais sobre essas codificações e campos de formulário, veja a documentação online da MDN sobre POST . - -/// - -/// warning | Aviso - -Você pode declarar múltiplos parâmetros `File` e `Form` em uma *operação de rota*, mas você não pode declarar campos `Body`que seriam recebidos como JSON junto desses parâmetros, por que a codificação do corpo da requisição será `multipart/form-data` em vez de `application/json`. - -Isso não é uma limitação do **FastAPI**, é uma parte do protocolo HTTP. - -/// - -## Arquivo de upload opcional - -Você pode definir um arquivo como opcional utilizando as anotações de tipo padrão e definindo o valor padrão como `None`: - -{* ../../docs_src/request_files/tutorial001_02_an_py310.py hl[9,17] *} - -## `UploadFile` com Metadados Adicionais - -Você também pode utilizar `File()` com `UploadFile`, por exemplo, para definir metadados adicionais: - -{* ../../docs_src/request_files/tutorial001_03_an_py39.py hl[9,15] *} - -## Envio de Múltiplos Arquivos - -É possível enviar múltiplos arquivos ao mesmo tmepo. - -Ele ficam associados ao mesmo "campo do formulário" enviado com "form data". - -Para usar isso, declare uma lista de `bytes` ou `UploadFile`: - -{* ../../docs_src/request_files/tutorial002_an_py39.py hl[10,15] *} - -Você irá receber, como delcarado uma lista (`list`) de `bytes` ou `UploadFile`s, - -/// note | Detalhes Técnicos - -Você também poderia utilizar `from starlette.responses import HTMLResponse`. - -O **FastAPI** fornece as mesmas `starlette.responses` como `fastapi.responses` apenas como um facilitador para você, desenvolvedor. Mas a maior parte das respostas vem diretamente do Starlette. - -/// - -### Enviando Múltiplos Arquivos com Metadados Adicionais - -E da mesma forma que antes, você pode utilizar `File()` para definir parâmetros adicionais, até mesmo para `UploadFile`: - -{* ../../docs_src/request_files/tutorial003_an_py39.py hl[11,18:20] *} - -## Recapitulando - -Use `File`, `bytes` e `UploadFile` para declarar arquivos que serão enviados na requisição, enviados como dados do formulário. diff --git a/docs/pt/docs/tutorial/response-model.md b/docs/pt/docs/tutorial/response-model.md index 6726a20a72..dc66bb46c4 100644 --- a/docs/pt/docs/tutorial/response-model.md +++ b/docs/pt/docs/tutorial/response-model.md @@ -1,6 +1,6 @@ -# Modelo de resposta - Tipo de retorno +# Modelo de resposta - Tipo de retorno { #response-model-return-type } -Você pode declarar o tipo usado para a resposta anotando o **tipo de retorno** *da função de operação de rota*. +Você pode declarar o tipo usado para a resposta anotando o **tipo de retorno** da *função de operação de rota*. Você pode usar **anotações de tipo** da mesma forma que usaria para dados de entrada em **parâmetros** de função, você pode usar modelos Pydantic, listas, dicionários, valores escalares como inteiros, booleanos, etc. @@ -10,7 +10,7 @@ O FastAPI usará este tipo de retorno para: * **Validar** os dados retornados. * Se os dados forem inválidos (por exemplo, se estiver faltando um campo), significa que o código do *seu* aplicativo está quebrado, não retornando o que deveria, e retornará um erro de servidor em vez de retornar dados incorretos. Dessa forma, você e seus clientes podem ter certeza de que receberão os dados e o formato de dados esperados. -* Adicionar um **Esquema JSON** para a resposta, na *operação de rota* do OpenAPI. +* Adicionar um **JSON Schema** para a resposta, na *operação de rota* do OpenAPI. * Isso será usado pela **documentação automática**. * Também será usado por ferramentas de geração automática de código do cliente. @@ -19,7 +19,7 @@ Mas o mais importante: * Ele **limitará e filtrará** os dados de saída para o que está definido no tipo de retorno. * Isso é particularmente importante para a **segurança**, veremos mais sobre isso abaixo. -## Parâmetro `response_model` +## Parâmetro `response_model` { #response-model-parameter } Existem alguns casos em que você precisa ou deseja retornar alguns dados que não são exatamente o que o tipo declara. @@ -27,7 +27,7 @@ Por exemplo, você pode querer **retornar um dicionário** ou um objeto de banco Se você adicionasse a anotação do tipo de retorno, ferramentas e editores reclamariam com um erro (correto) informando que sua função está retornando um tipo (por exemplo, um dict) diferente do que você declarou (por exemplo, um modelo Pydantic). -Nesses casos, você pode usar o parâmetro `response_model` do *decorador de operação de rota* em vez do tipo de retorno. +Nesses casos, você pode usar o parâmetro `response_model` do *decorador de operação de rota* em vez do tipo de retorno. Você pode usar o parâmetro `response_model` em qualquer uma das *operações de rota*: @@ -45,7 +45,7 @@ Observe que `response_model` é um parâmetro do método "decorator" (`get`, `po /// -`response_model` recebe o mesmo tipo que você declararia para um campo de modelo Pydantic, então, pode ser um modelo Pydantic, mas também pode ser, por exemplo, uma `lista` de modelos Pydantic, como `List[Item]`. +`response_model` recebe o mesmo tipo que você declararia para um campo de modelo Pydantic, então, pode ser um modelo Pydantic, mas também pode ser, por exemplo, uma `list` de modelos Pydantic, como `List[Item]`. O FastAPI usará este `response_model` para fazer toda a documentação de dados, validação, etc. e também para **converter e filtrar os dados de saída** para sua declaração de tipo. @@ -57,7 +57,7 @@ Dessa forma, você diz ao editor que está retornando qualquer coisa intencional /// -### Prioridade `response_model` +### Prioridade `response_model` { #response-model-priority } Se você declarar tanto um tipo de retorno quanto um `response_model`, o `response_model` terá prioridade e será usado pelo FastAPI. @@ -65,7 +65,7 @@ Dessa forma, você pode adicionar anotações de tipo corretas às suas funçõe Você também pode usar `response_model=None` para desabilitar a criação de um modelo de resposta para essa *operação de rota*, você pode precisar fazer isso se estiver adicionando anotações de tipo para coisas que não são campos Pydantic válidos, você verá um exemplo disso em uma das seções abaixo. -## Retorna os mesmos dados de entrada +## Retorne os mesmos dados de entrada { #return-the-same-input-data } Aqui estamos declarando um modelo `UserIn`, ele conterá uma senha em texto simples: @@ -99,13 +99,13 @@ Neste caso, pode não ser um problema, porque é o mesmo usuário enviando a sen Mas se usarmos o mesmo modelo para outra *operação de rota*, poderíamos estar enviando as senhas dos nossos usuários para todos os clientes. -/// danger | Perigo +/// danger | Cuidado Nunca armazene a senha simples de um usuário ou envie-a em uma resposta como esta, a menos que você saiba todas as ressalvas e saiba o que está fazendo. /// -## Adicionar um modelo de saída +## Adicione um modelo de saída { #add-an-output-model } Podemos, em vez disso, criar um modelo de entrada com a senha em texto simples e um modelo de saída sem ela: @@ -121,7 +121,7 @@ Aqui, embora nossa *função de operação de rota* esteja retornando o mesmo us Então, **FastAPI** cuidará de filtrar todos os dados que não são declarados no modelo de saída (usando Pydantic). -### `response_model` ou Tipo de Retorno +### `response_model` ou Tipo de Retorno { #response-model-or-return-type } Neste caso, como os dois modelos são diferentes, se anotássemos o tipo de retorno da função como `UserOut`, o editor e as ferramentas reclamariam que estamos retornando um tipo inválido, pois são classes diferentes. @@ -129,7 +129,7 @@ Neste caso, como os dois modelos são diferentes, se anotássemos o tipo de reto ...mas continue lendo abaixo para ver como superar isso. -## Tipo de Retorno e Filtragem de Dados +## Tipo de Retorno e Filtragem de Dados { #return-type-and-data-filtering } Vamos continuar do exemplo anterior. Queríamos **anotar a função com um tipo**, mas queríamos poder retornar da função algo que realmente incluísse **mais dados**. @@ -147,7 +147,7 @@ Com isso, temos suporte de ferramentas, de editores e mypy, pois este código es Como isso funciona? Vamos verificar. 🤓 -### Anotações de tipo e ferramentas +### Anotações de tipo e ferramentas { #type-annotations-and-tooling } Primeiro, vamos ver como editores, mypy e outras ferramentas veriam isso. @@ -157,7 +157,7 @@ Anotamos o tipo de retorno da função como `BaseUser`, mas na verdade estamos r O editor, mypy e outras ferramentas não reclamarão disso porque, em termos de digitação, `UserIn` é uma subclasse de `BaseUser`, o que significa que é um tipo *válido* quando o que é esperado é qualquer coisa que seja um `BaseUser`. -### Filtragem de dados FastAPI +### Filtragem de dados FastAPI { #fastapi-data-filtering } Agora, para FastAPI, ele verá o tipo de retorno e garantirá que o que você retornar inclua **apenas** os campos que são declarados no tipo. @@ -165,7 +165,7 @@ O FastAPI faz várias coisas internamente com o Pydantic para garantir que essas Dessa forma, você pode obter o melhor dos dois mundos: anotações de tipo com **suporte a ferramentas** e **filtragem de dados**. -## Veja na documentação +## Veja na documentação { #see-it-in-the-docs } Quando você vê a documentação automática, pode verificar se o modelo de entrada e o modelo de saída terão seus próprios esquemas JSON: @@ -175,29 +175,29 @@ E ambos os modelos serão usados ​​para a documentação interativa da API: -## Outras anotações de tipo de retorno +## Outras anotações de tipo de retorno { #other-return-type-annotations } Pode haver casos em que você retorna algo que não é um campo Pydantic válido e anota na função, apenas para obter o suporte fornecido pelas ferramentas (o editor, mypy, etc). -### Retornar uma resposta diretamente +### Retorne uma Response diretamente { #return-a-response-directly } -O caso mais comum seria [retornar uma resposta diretamente, conforme explicado posteriormente na documentação avançada](../advanced/response-directly.md){.internal-link target=_blank}. +O caso mais comum seria [retornar uma Response diretamente, conforme explicado posteriormente na documentação avançada](../advanced/response-directly.md){.internal-link target=_blank}. -{* ../../docs_src/response_model/tutorial003_02.py hl[8,10:11] *} +{* ../../docs_src/response_model/tutorial003_02_py39.py hl[8,10:11] *} Este caso simples é tratado automaticamente pelo FastAPI porque a anotação do tipo de retorno é a classe (ou uma subclasse de) `Response`. E as ferramentas também ficarão felizes porque `RedirectResponse` e ​​`JSONResponse` são subclasses de `Response`, então a anotação de tipo está correta. -### Anotar uma subclasse de resposta +### Anote uma subclasse de Response { #annotate-a-response-subclass } Você também pode usar uma subclasse de `Response` na anotação de tipo: -{* ../../docs_src/response_model/tutorial003_03.py hl[8:9] *} +{* ../../docs_src/response_model/tutorial003_03_py39.py hl[8:9] *} Isso também funcionará porque `RedirectResponse` é uma subclasse de `Response`, e o FastAPI tratará automaticamente este caso simples. -### Anotações de Tipo de Retorno Inválido +### Anotações de Tipo de Retorno Inválido { #invalid-return-type-annotations } Mas quando você retorna algum outro objeto arbitrário que não é um tipo Pydantic válido (por exemplo, um objeto de banco de dados) e você o anota dessa forma na função, o FastAPI tentará criar um modelo de resposta Pydantic a partir dessa anotação de tipo e falhará. @@ -205,9 +205,9 @@ O mesmo aconteceria se você tivesse algo como uma /// note | Nota - +Não importa o que você digite no formulário, ainda não vai funcionar. Mas nós vamos chegar lá. /// - Não importa o que você digita no formulário, não vai funcionar ainda. Mas nós vamos chegar lá. +Claro que este não é o frontend para os usuários finais, mas é uma ótima ferramenta automática para documentar interativamente toda a sua API. -Claro que este não é o frontend para os usuários finais, mas é uma ótima ferramenta automática para documentar interativamente toda sua API. +Pode ser usada pelo time de frontend (que pode ser você mesmo). -Pode ser usado pelo time de frontend (que pode ser você no caso). +Pode ser usada por aplicações e sistemas de terceiros. -Pode ser usado por aplicações e sistemas third party (de terceiros). +E também pode ser usada por você mesmo, para depurar, verificar e testar a mesma aplicação. -E também pode ser usada por você mesmo, para debugar, checar e testar a mesma aplicação. - -## O Fluxo da `senha` +## O fluxo de `password` { #the-password-flow } Agora vamos voltar um pouco e entender o que é isso tudo. -O "fluxo" da `senha` é um dos caminhos ("fluxos") definidos no OAuth2, para lidar com a segurança e autenticação. +O "fluxo" `password` é uma das formas ("fluxos") definidas no OAuth2 para lidar com segurança e autenticação. -OAuth2 foi projetado para que o backend ou a API pudesse ser independente do servidor que autentica o usuário. +O OAuth2 foi projetado para que o backend ou a API pudesse ser independente do servidor que autentica o usuário. -Mas nesse caso, a mesma aplicação **FastAPI** irá lidar com a API e a autenticação. +Mas, neste caso, a mesma aplicação **FastAPI** irá lidar com a API e com a autenticação. Então, vamos rever de um ponto de vista simplificado: -* O usuário digita o `username` e a `senha` no frontend e aperta `Enter`. -* O frontend (rodando no browser do usuário) manda o `username` e a `senha` para uma URL específica na sua API (declarada com `tokenUrl="token"`). -* A API checa aquele `username` e `senha`, e responde com um "token" (nós não implementamos nada disso ainda). - * Um "token" é apenas uma string com algum conteúdo que nós podemos utilizar mais tarde para verificar o usuário. - * Normalmente, um token é definido para expirar depois de um tempo. - * Então, o usuário terá que se logar de novo depois de um tempo. - * E se o token for roubado, o risco é menor. Não é como se fosse uma chave permanente que vai funcionar para sempre (na maioria dos casos). - * O frontend armazena aquele token temporariamente em algum lugar. - * O usuário clica no frontend para ir à outra seção daquele frontend do aplicativo web. - * O frontend precisa buscar mais dados daquela API. - * Mas precisa de autenticação para aquele endpoint em específico. - * Então, para autenticar com nossa API, ele manda um header de `Autorização` com o valor `Bearer` mais o token. - * Se o token contém `foobar`, o conteúdo do header de `Autorização` será: `Bearer foobar`. +* O usuário digita o `username` e o `password` no frontend e pressiona `Enter`. +* O frontend (rodando no navegador do usuário) envia esse `username` e `password` para uma URL específica na nossa API (declarada com `tokenUrl="token"`). +* A API verifica esse `username` e `password`, e responde com um "token" (ainda não implementamos nada disso). + * Um "token" é apenas uma string com algum conteúdo que podemos usar depois para verificar esse usuário. + * Normalmente, um token é definido para expirar depois de algum tempo. + * Então, o usuário terá que fazer login novamente em algum momento. + * E se o token for roubado, o risco é menor. Não é como uma chave permanente que funcionará para sempre (na maioria dos casos). +* O frontend armazena esse token temporariamente em algum lugar. +* O usuário clica no frontend para ir para outra seção do aplicativo web. +* O frontend precisa buscar mais dados da API. + * Mas precisa de autenticação para aquele endpoint específico. + * Então, para autenticar com nossa API, ele envia um header `Authorization` com o valor `Bearer ` mais o token. + * Se o token contém `foobar`, o conteúdo do header `Authorization` seria: `Bearer foobar`. -## **FastAPI**'s `OAuth2PasswordBearer` +## O `OAuth2PasswordBearer` do **FastAPI** { #fastapis-oauth2passwordbearer } -**FastAPI** fornece várias ferramentas, em diferentes níveis de abstração, para implementar esses recursos de segurança. +O **FastAPI** fornece várias ferramentas, em diferentes níveis de abstração, para implementar essas funcionalidades de segurança. -Neste exemplo, nós vamos usar o **OAuth2** com o fluxo de **Senha**, usando um token **Bearer**. Fazemos isso usando a classe `OAuth2PasswordBearer`. +Neste exemplo, vamos usar **OAuth2**, com o fluxo **Password**, usando um token **Bearer**. Fazemos isso usando a classe `OAuth2PasswordBearer`. -/// info | informação +/// info | Informação +Um token "bearer" não é a única opção. +Mas é a melhor para o nosso caso de uso. + +E pode ser a melhor para a maioria dos casos de uso, a menos que você seja um especialista em OAuth2 e saiba exatamente por que existe outra opção que se adapta melhor às suas necessidades. + +Nesse caso, o **FastAPI** também fornece as ferramentas para construí-la. /// - Um token "bearer" não é a única opção. +Quando criamos uma instância da classe `OAuth2PasswordBearer`, passamos o parâmetro `tokenUrl`. Esse parâmetro contém a URL que o client (o frontend rodando no navegador do usuário) usará para enviar o `username` e o `password` para obter um token. - Mas é a melhor no nosso caso. - - E talvez seja a melhor para a maioria dos casos, a não ser que você seja um especialista em OAuth2 e saiba exatamente o porquê de existir outras opções que se adequam melhor às suas necessidades. - - Nesse caso, **FastAPI** também fornece as ferramentas para construir. - -Quando nós criamos uma instância da classe `OAuth2PasswordBearer`, nós passamos pelo parâmetro `tokenUrl` Esse parâmetro contém a URL que o client (o frontend rodando no browser do usuário) vai usar para mandar o `username` e `senha` para obter um token. - -{* ../../docs_src/security/tutorial001.py hl[6] *} +{* ../../docs_src/security/tutorial001_an_py39.py hl[8] *} /// tip | Dica +Aqui `tokenUrl="token"` refere-se a uma URL relativa `token` que ainda não criamos. Como é uma URL relativa, é equivalente a `./token`. +Como estamos usando uma URL relativa, se sua API estivesse localizada em `https://example.com/`, então se referiria a `https://example.com/token`. Mas se sua API estivesse localizada em `https://example.com/api/v1/`, então se referiria a `https://example.com/api/v1/token`. + +Usar uma URL relativa é importante para garantir que sua aplicação continue funcionando mesmo em um caso de uso avançado como [Atrás de um Proxy](../../advanced/behind-a-proxy.md){.internal-link target=_blank}. /// - Esse `tokenUrl="token"` se refere a uma URL relativa que nós não criamos ainda. Como é uma URL relativa, é equivalente a `./token`. +Esse parâmetro não cria aquele endpoint/operação de rota, mas declara que a URL `/token` será aquela que o client deve usar para obter o token. Essa informação é usada no OpenAPI e depois nos sistemas de documentação interativa da API. - Porque estamos usando uma URL relativa, se sua API estava localizada em `https://example.com/`, então irá referir-se à `https://example.com/token`. Mas se sua API estava localizada em `https://example.com/api/v1/`, então irá referir-se à `https://example.com/api/v1/token`. +Em breve também criaremos a operação de rota real. - Usar uma URL relativa é importante para garantir que sua aplicação continue funcionando, mesmo em um uso avançado tipo [Atrás de um Proxy](../../advanced/behind-a-proxy.md){.internal-link target=_blank}. - -Esse parâmetro não cria um endpoint / *path operation*, mas declara que a URL `/token` vai ser aquela que o client deve usar para obter o token. Essa informação é usada no OpenAPI, e depois na API Interativa de documentação de sistemas. - -Em breve também criaremos o atual path operation. - -/// info | informação +/// info | Informação +Se você é um "Pythonista" muito rigoroso, pode não gostar do estilo do nome do parâmetro `tokenUrl` em vez de `token_url`. +Isso ocorre porque ele usa o mesmo nome da especificação do OpenAPI. Assim, se você precisar investigar mais sobre qualquer um desses esquemas de segurança, pode simplesmente copiar e colar para encontrar mais informações sobre isso. /// - Se você é um "Pythonista" muito rigoroso, você pode não gostar do estilo do nome do parâmetro `tokenUrl` em vez de `token_url`. +A variável `oauth2_scheme` é uma instância de `OAuth2PasswordBearer`, mas também é "chamável" (callable). - Isso ocorre porque está utilizando o mesmo nome que está nas especificações do OpenAPI. Então, se você precisa investigar mais sobre qualquer um desses esquemas de segurança, você pode simplesmente copiar e colar para encontrar mais informações sobre isso. - -A variável `oauth2_scheme` é um instância de `OAuth2PasswordBearer`, mas também é um "callable". - -Pode ser chamada de: +Ela pode ser chamada como: ```Python oauth2_scheme(some, parameters) ``` -Então, pode ser usado com `Depends`. +Então, pode ser usada com `Depends`. -## Use-o +### Use-o { #use-it } -Agora você pode passar aquele `oauth2_scheme` em uma dependência com `Depends`. +Agora você pode passar esse `oauth2_scheme` em uma dependência com `Depends`. -{* ../../docs_src/security/tutorial001.py hl[10] *} +{* ../../docs_src/security/tutorial001_an_py39.py hl[12] *} -Esse dependência vai fornecer uma `str` que é atribuído ao parâmetro `token da *função do path operation* +Essa dependência fornecerá uma `str` que é atribuída ao parâmetro `token` da função de operação de rota. -A **FastAPI** saberá que pode usar essa dependência para definir um "esquema de segurança" no esquema da OpenAPI (e na documentação da API automática). +O **FastAPI** saberá que pode usar essa dependência para definir um "esquema de segurança" no esquema OpenAPI (e na documentação automática da API). -/// info | Detalhes técnicos +/// info | Detalhes Técnicos +O **FastAPI** saberá que pode usar a classe `OAuth2PasswordBearer` (declarada em uma dependência) para definir o esquema de segurança no OpenAPI porque ela herda de `fastapi.security.oauth2.OAuth2`, que por sua vez herda de `fastapi.security.base.SecurityBase`. +Todos os utilitários de segurança que se integram com o OpenAPI (e com a documentação automática da API) herdam de `SecurityBase`, é assim que o **FastAPI** sabe como integrá-los ao OpenAPI. /// - **FastAPI** saberá que pode usar a classe `OAuth2PasswordBearer` (declarada na dependência) para definir o esquema de segurança na OpenAPI porque herda de `fastapi.security.oauth2.OAuth2`, que por sua vez herda de `fastapi.security.base.Securitybase`. +## O que ele faz { #what-it-does } - Todos os utilitários de segurança que se integram com OpenAPI (e na documentação da API automática) herdam de `SecurityBase`, é assim que **FastAPI** pode saber como integrá-los no OpenAPI. +Ele irá procurar na requisição pelo header `Authorization`, verificar se o valor é `Bearer ` mais algum token e retornará o token como uma `str`. -## O que ele faz +Se não houver um header `Authorization`, ou se o valor não tiver um token `Bearer `, ele responderá diretamente com um erro de status 401 (`UNAUTHORIZED`). -Ele irá e olhará na requisição para aquele header de `Autorização`, verificará se o valor é `Bearer` mais algum token, e vai retornar o token como uma `str` - -Se ele não ver o header de `Autorização` ou o valor não tem um token `Bearer`, vai responder com um código de erro 401 (`UNAUTHORIZED`) diretamente. - -Você nem precisa verificar se o token existe para retornar um erro. Você pode ter certeza de que se a sua função for executada, ela terá um `str` nesse token. +Você nem precisa verificar se o token existe para retornar um erro. Você pode ter certeza de que, se sua função for executada, ela terá uma `str` nesse token. Você já pode experimentar na documentação interativa: -Não estamos verificando a validade do token ainda, mas isso já é um começo +Ainda não estamos verificando a validade do token, mas isso já é um começo. -## Recapitulando +## Recapitulando { #recap } -Então, em apenas 3 ou 4 linhas extras, você já tem alguma forma primitiva de segurança. +Então, com apenas 3 ou 4 linhas extras, você já tem alguma forma primitiva de segurança. diff --git a/docs/pt/docs/tutorial/security/get-current-user.md b/docs/pt/docs/tutorial/security/get-current-user.md index 1a2badb83b..2135ae236d 100644 --- a/docs/pt/docs/tutorial/security/get-current-user.md +++ b/docs/pt/docs/tutorial/security/get-current-user.md @@ -1,105 +1,105 @@ -# Obter Usuário Atual - -No capítulo anterior, o sistema de segurança (que é baseado no sistema de injeção de dependências) estava fornecendo à *função de operação de rota* um `token` como uma `str`: - -{* ../../docs_src/security/tutorial001_an_py39.py hl[12] *} - -Mas isso ainda não é tão útil. - -Vamos fazer com que ele nos forneça o usuário atual. - -## Criar um modelo de usuário - -Primeiro, vamos criar um modelo de usuário com Pydantic. - -Da mesma forma que usamos o Pydantic para declarar corpos, podemos usá-lo em qualquer outro lugar: - -{* ../../docs_src/security/tutorial002_an_py310.py hl[5,12:6] *} - -## Criar uma dependência `get_current_user` - -Vamos criar uma dependência chamada `get_current_user`. - -Lembra que as dependências podem ter subdependências? - -`get_current_user` terá uma dependência com o mesmo `oauth2_scheme` que criamos antes. - -Da mesma forma que estávamos fazendo antes diretamente na *operação de rota*, a nossa nova dependência `get_current_user` receberá um `token` como uma `str` da subdependência `oauth2_scheme`: - -{* ../../docs_src/security/tutorial002_an_py310.py hl[25] *} - -## Obter o usuário - -`get_current_user` usará uma função utilitária (falsa) que criamos, que recebe um token como uma `str` e retorna nosso modelo Pydantic `User`: - -{* ../../docs_src/security/tutorial002_an_py310.py hl[19:22,26:27] *} - -## Injetar o usuário atual - -Então agora nós podemos usar o mesmo `Depends` com nosso `get_current_user` na *operação de rota*: - -{* ../../docs_src/security/tutorial002_an_py310.py hl[31] *} - -Observe que nós declaramos o tipo de `current_user` como o modelo Pydantic `User`. - -Isso nos ajudará dentro da função com todo o preenchimento automático e verificações de tipo. - -/// tip | Dica - -Você pode se lembrar que corpos de requisição também são declarados com modelos Pydantic. - -Aqui, o **FastAPI** não ficará confuso porque você está usando `Depends`. - -/// - -/// check | Verifique - -A forma como esse sistema de dependências foi projetado nos permite ter diferentes dependências (diferentes "dependables") que retornam um modelo `User`. - -Não estamos restritos a ter apenas uma dependência que possa retornar esse tipo de dado. - -/// - -## Outros modelos - -Agora você pode obter o usuário atual diretamente nas *funções de operação de rota* e lidar com os mecanismos de segurança no nível da **Injeção de Dependências**, usando `Depends`. - -E você pode usar qualquer modelo ou dado para os requisitos de segurança (neste caso, um modelo Pydantic `User`). - -Mas você não está restrito a usar um modelo de dados, classe ou tipo específico. - -Você quer ter apenas um `id` e `email`, sem incluir nenhum `username` no modelo? Claro. Você pode usar essas mesmas ferramentas. - -Você quer ter apenas uma `str`? Ou apenas um `dict`? Ou uma instância de modelo de classe de banco de dados diretamente? Tudo funciona da mesma forma. - -Na verdade, você não tem usuários que fazem login no seu aplicativo, mas sim robôs, bots ou outros sistemas, que possuem apenas um token de acesso? Novamente, tudo funciona da mesma forma. - -Apenas use qualquer tipo de modelo, qualquer tipo de classe, qualquer tipo de banco de dados que você precise para a sua aplicação. O **FastAPI** cobre tudo com o sistema de injeção de dependências. - -## Tamanho do código - -Este exemplo pode parecer verboso. Lembre-se de que estamos misturando segurança, modelos de dados, funções utilitárias e *operações de rota* no mesmo arquivo. - -Mas aqui está o ponto principal. - -O código relacionado à segurança e à injeção de dependências é escrito apenas uma vez. - -E você pode torná-lo tão complexo quanto quiser. E ainda assim, tê-lo escrito apenas uma vez, em um único lugar. Com toda a flexibilidade. - -Mas você pode ter milhares de endpoints (*operações de rota*) usando o mesmo sistema de segurança. - -E todos eles (ou qualquer parte deles que você desejar) podem aproveitar o reuso dessas dependências ou de quaisquer outras dependências que você criar. - -E todos esses milhares de *operações de rota* podem ter apenas 3 linhas: - -{* ../../docs_src/security/tutorial002_an_py310.py hl[30:32] *} - -## Recapitulação - -Agora você pode obter o usuário atual diretamente na sua *função de operação de rota*. - -Já estamos na metade do caminho. - -Só precisamos adicionar uma *operação de rota* para que o usuário/cliente realmente envie o `username` e `password`. - +# Obter Usuário Atual { #get-current-user } + +No capítulo anterior, o sistema de segurança (que é baseado no sistema de injeção de dependências) estava fornecendo à *função de operação de rota* um `token` como uma `str`: + +{* ../../docs_src/security/tutorial001_an_py39.py hl[12] *} + +Mas isso ainda não é tão útil. + +Vamos fazer com que ele nos forneça o usuário atual. + +## Criar um modelo de usuário { #create-a-user-model } + +Primeiro, vamos criar um modelo de usuário com Pydantic. + +Da mesma forma que usamos o Pydantic para declarar corpos, podemos usá-lo em qualquer outro lugar: + +{* ../../docs_src/security/tutorial002_an_py310.py hl[5,12:6] *} + +## Criar uma dependência `get_current_user` { #create-a-get-current-user-dependency } + +Vamos criar uma dependência chamada `get_current_user`. + +Lembra que as dependências podem ter subdependências? + +`get_current_user` terá uma dependência com o mesmo `oauth2_scheme` que criamos antes. + +Da mesma forma que estávamos fazendo antes diretamente na *operação de rota*, a nossa nova dependência `get_current_user` receberá um `token` como uma `str` da subdependência `oauth2_scheme`: + +{* ../../docs_src/security/tutorial002_an_py310.py hl[25] *} + +## Obter o usuário { #get-the-user } + +`get_current_user` usará uma função utilitária (falsa) que criamos, que recebe um token como uma `str` e retorna nosso modelo Pydantic `User`: + +{* ../../docs_src/security/tutorial002_an_py310.py hl[19:22,26:27] *} + +## Injetar o usuário atual { #inject-the-current-user } + +Então agora nós podemos usar o mesmo `Depends` com nosso `get_current_user` na *operação de rota*: + +{* ../../docs_src/security/tutorial002_an_py310.py hl[31] *} + +Observe que nós declaramos o tipo de `current_user` como o modelo Pydantic `User`. + +Isso nos ajudará dentro da função com todo o preenchimento automático e verificações de tipo. + +/// tip | Dica + +Você pode se lembrar que corpos de requisição também são declarados com modelos Pydantic. + +Aqui, o **FastAPI** não ficará confuso porque você está usando `Depends`. + +/// + +/// check | Verifique + +A forma como esse sistema de dependências foi projetado nos permite ter diferentes dependências (diferentes "dependables") que retornam um modelo `User`. + +Não estamos restritos a ter apenas uma dependência que possa retornar esse tipo de dado. + +/// + +## Outros modelos { #other-models } + +Agora você pode obter o usuário atual diretamente nas *funções de operação de rota* e lidar com os mecanismos de segurança no nível da **Injeção de Dependências**, usando `Depends`. + +E você pode usar qualquer modelo ou dado para os requisitos de segurança (neste caso, um modelo Pydantic `User`). + +Mas você não está restrito a usar um modelo de dados, classe ou tipo específico. + +Você quer ter apenas um `id` e `email`, sem incluir nenhum `username` no modelo? Claro. Você pode usar essas mesmas ferramentas. + +Você quer ter apenas uma `str`? Ou apenas um `dict`? Ou uma instância de modelo de classe de banco de dados diretamente? Tudo funciona da mesma forma. + +Na verdade, você não tem usuários que fazem login no seu aplicativo, mas sim robôs, bots ou outros sistemas, que possuem apenas um token de acesso? Novamente, tudo funciona da mesma forma. + +Apenas use qualquer tipo de modelo, qualquer tipo de classe, qualquer tipo de banco de dados que você precise para a sua aplicação. O **FastAPI** cobre tudo com o sistema de injeção de dependências. + +## Tamanho do código { #code-size } + +Este exemplo pode parecer verboso. Lembre-se de que estamos misturando segurança, modelos de dados, funções utilitárias e *operações de rota* no mesmo arquivo. + +Mas aqui está o ponto principal. + +O código relacionado à segurança e à injeção de dependências é escrito apenas uma vez. + +E você pode torná-lo tão complexo quanto quiser. E ainda assim, tê-lo escrito apenas uma vez, em um único lugar. Com toda a flexibilidade. + +Mas você pode ter milhares de endpoints (*operações de rota*) usando o mesmo sistema de segurança. + +E todos eles (ou qualquer parte deles que você desejar) podem aproveitar o reuso dessas dependências ou de quaisquer outras dependências que você criar. + +E todos esses milhares de *operações de rota* podem ter apenas 3 linhas: + +{* ../../docs_src/security/tutorial002_an_py310.py hl[30:32] *} + +## Recapitulação { #recap } + +Agora você pode obter o usuário atual diretamente na sua *função de operação de rota*. + +Já estamos na metade do caminho. + +Só precisamos adicionar uma *operação de rota* para que o usuário/cliente realmente envie o `username` e `password`. + Isso vem a seguir. diff --git a/docs/pt/docs/tutorial/security/index.md b/docs/pt/docs/tutorial/security/index.md index 2ebb87fcd8..d3de3e0509 100644 --- a/docs/pt/docs/tutorial/security/index.md +++ b/docs/pt/docs/tutorial/security/index.md @@ -1,4 +1,4 @@ -# Segurança +# Segurança { #security } Há várias formas de lidar segurança, autenticação e autorização. @@ -10,11 +10,11 @@ Em muitos frameworks e sistemas, apenas lidar com segurança e autenticação ex Mas primeiro, vamos verificar alguns pequenos conceitos. -## Está com pressa? +## Está com pressa? { #in-a-hurry } Se você não se importa com qualquer um desses termos e só precisa adicionar segurança com autenticação baseada em usuário e senha _agora_, pule para os próximos capítulos. -## OAuth2 +## OAuth2 { #oauth2 } OAuth2 é uma especificação que define várias formas para lidar com autenticação e autorização. @@ -24,7 +24,7 @@ Ela inclui uma forma para autenticação usando “third party”/aplicações d Isso é o que todos os sistemas com “Login with Facebook, Google, X (Twitter), GitHub” usam por baixo. -### OAuth 1 +### OAuth 1 { #oauth-1 } Havia um OAuth 1, que é bem diferente do OAuth2, e mais complexo, isso incluía diretamente as especificações de como criptografar a comunicação. @@ -38,7 +38,7 @@ Na seção sobre **deployment** você irá ver como configurar HTTPS de modo gra /// -## OpenID Connect +## OpenID Connect { #openid-connect } OpenID Connect é outra especificação, baseada em **OAuth2**. @@ -48,7 +48,7 @@ Por exemplo, o login do Google usa OpenID Connect (que por baixo dos panos usa O Mas o login do Facebook não tem suporte para OpenID Connect. Ele tem a própria implementação do OAuth2. -### OpenID (não "OpenID Connect") +### OpenID (não "OpenID Connect") { #openid-not-openid-connect } Houve também uma especificação “OpenID”. Ela tentou resolver a mesma coisa que a **OpenID Connect**, mas não baseada em OAuth2. @@ -56,7 +56,7 @@ Então, ela foi um sistema adicional completo. Ela não é muito popular ou usada nos dias de hoje. -## OpenAPI +## OpenAPI { #openapi } OpenAPI (anteriormente conhecido como Swagger) é a especificação aberta para a criação de APIs (agora parte da Linux Foundation). @@ -97,7 +97,7 @@ O problema mais complexo é criar um provedor de autenticação/autorização co /// -## **FastAPI** utilitários +## **FastAPI** utilitários { #fastapi-utilities } **FastAPI** fornece várias ferramentas para cada um desses esquemas de segurança no módulo `fastapi.security` que simplesmente usa esses mecanismos de segurança. diff --git a/docs/pt/docs/tutorial/security/oauth2-jwt.md b/docs/pt/docs/tutorial/security/oauth2-jwt.md index 7d80d12fab..f68b8c39ea 100644 --- a/docs/pt/docs/tutorial/security/oauth2-jwt.md +++ b/docs/pt/docs/tutorial/security/oauth2-jwt.md @@ -1,4 +1,4 @@ -# OAuth2 com Senha (e hashing), Bearer com tokens JWT +# OAuth2 com Senha (e hashing), Bearer com tokens JWT { #oauth2-with-password-and-hashing-bearer-with-jwt-tokens } Agora que temos todo o fluxo de segurança, vamos tornar a aplicação realmente segura, usando tokens JWT e hashing de senhas seguras. @@ -6,7 +6,7 @@ Este código é algo que você pode realmente usar na sua aplicação, salvar os Vamos começar de onde paramos no capítulo anterior e incrementá-lo. -## Sobre o JWT +## Sobre o JWT { #about-jwt } JWT significa "JSON Web Tokens". @@ -26,7 +26,7 @@ Depois de uma semana, o token expirará e o usuário não estará autorizado, pr Se você quiser brincar com tokens JWT e ver como eles funcionam, visite https://jwt.io. -## Instalar `PyJWT` +## Instalar `PyJWT` { #install-pyjwt } Nós precisamos instalar o `PyJWT` para criar e verificar os tokens JWT em Python. @@ -50,7 +50,7 @@ Você pode ler mais sobre isso na ```console -$ pip install "passlib[bcrypt]" +$ pip install "pwdlib[argon2]" ---> 100% ``` @@ -86,7 +86,7 @@ $ pip install "passlib[bcrypt]" /// tip | Dica -Com o `passlib`, você poderia até configurá-lo para ser capaz de ler senhas criadas pelo **Django**, um plug-in de segurança do **Flask** ou muitos outros. +Com o `pwdlib`, você poderia até configurá-lo para ser capaz de ler senhas criadas pelo **Django**, um plug-in de segurança do **Flask** ou muitos outros. Assim, você poderia, por exemplo, compartilhar os mesmos dados de um aplicativo Django em um banco de dados com um aplicativo FastAPI. Ou migrar gradualmente uma aplicação Django usando o mesmo banco de dados. @@ -94,17 +94,17 @@ E seus usuários poderiam fazer login tanto pela sua aplicação Django quanto p /// -## Criar o hash e verificar as senhas +## Criar o hash e verificar as senhas { #hash-and-verify-the-passwords } -Importe as ferramentas que nós precisamos de `passlib`. +Importe as ferramentas que nós precisamos de `pwdlib`. -Crie um "contexto" do PassLib. Este será usado para criar o hash e verificar as senhas. +Crie uma instância de PasswordHash com as configurações recomendadas – ela será usada para criar o hash e verificar as senhas. /// tip | Dica -O contexto do PassLib também possui funcionalidades para usar diferentes algoritmos de hashing, incluindo algoritmos antigos que estão obsoletos, apenas para permitir verificá-los, etc. +pwdlib também oferece suporte ao algoritmo de hashing bcrypt, mas não inclui algoritmos legados – para trabalhar com hashes antigos, é recomendado usar a biblioteca passlib. -Por exemplo, você poderia usá-lo para ler e verificar senhas geradas por outro sistema (como Django), mas criar o hash de novas senhas com um algoritmo diferente, como o Bcrypt. +Por exemplo, você poderia usá-lo para ler e verificar senhas geradas por outro sistema (como Django), mas criar o hash de novas senhas com um algoritmo diferente, como o Argon2 ou o Bcrypt. E ser compatível com todos eles ao mesmo tempo. @@ -120,11 +120,11 @@ E outra para autenticar e retornar um usuário. /// note | Nota -Se você verificar o novo banco de dados (falso) `fake_users_db`, você verá como o hash da senha se parece agora: `"$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW"`. +Se você verificar o novo banco de dados (falso) `fake_users_db`, você verá como o hash da senha se parece agora: `"$argon2id$v=19$m=65536,t=3,p=4$wagCPXjifgvUFBzq4hqe3w$CYaIb8sB+wtD+Vu/P4uod1+Qof8h+1g7bbDlBID48Rc"`. /// -## Manipular tokens JWT +## Manipular tokens JWT { #handle-jwt-tokens } Importe os módulos instalados. @@ -154,7 +154,7 @@ Crie uma função utilitária para gerar um novo token de acesso. {* ../../docs_src/security/tutorial004_an_py310.py hl[4,7,13:15,29:31,79:87] *} -## Atualize as dependências +## Atualize as dependências { #update-the-dependencies } Atualize `get_current_user` para receber o mesmo token de antes, mas desta vez, usando tokens JWT. @@ -164,7 +164,7 @@ Se o token for inválido, retorne um erro HTTP imediatamente. {* ../../docs_src/security/tutorial004_an_py310.py hl[90:107] *} -## Atualize a *operação de rota* `/token` +## Atualize a *operação de rota* `/token` { #update-the-token-path-operation } Crie um `timedelta` com o tempo de expiração do token. @@ -172,7 +172,7 @@ Crie um token de acesso JWT real e o retorne. {* ../../docs_src/security/tutorial004_an_py310.py hl[118:133] *} -### Detalhes técnicos sobre o "sujeito" `sub` do JWT +### Detalhes técnicos sobre o "sujeito" `sub` do JWT { #technical-details-about-the-jwt-subject-sub } A especificação JWT diz que existe uma chave `sub`, com o sujeito do token. @@ -194,7 +194,7 @@ Então, para evitar colisões de ID, ao criar o token JWT para o usuário, você O importante a se lembrar é que a chave `sub` deve ter um identificador único em toda a aplicação e deve ser uma string. -## Testando +## Verifique { #check-it } Execute o servidor e vá para a documentação: http://127.0.0.1:8000/docs. @@ -240,7 +240,7 @@ Perceba que o cabeçalho `Authorization`, com o valor que começa com `Bearer `. /// -## Uso avançado com `scopes` +## Uso avançado com `scopes` { #advanced-usage-with-scopes } O OAuth2 tem a noção de "scopes" (escopos). @@ -250,8 +250,7 @@ Então, você pode dar este token diretamente a um usuário ou a uma terceira pa Você pode aprender como usá-los e como eles são integrados ao **FastAPI** mais adiante no **Guia Avançado do Usuário**. - -## Recapitulação +## Recapitulação { #recap } Com o que você viu até agora, você pode configurar uma aplicação **FastAPI** segura usando padrões como OAuth2 e JWT. @@ -265,7 +264,7 @@ O **FastAPI** não faz nenhuma concessão com nenhum banco de dados, modelo de d Ele oferece toda a flexibilidade para você escolher as opções que melhor se ajustam ao seu projeto. -E você pode usar diretamente muitos pacotes bem mantidos e amplamente utilizados, como `passlib` e `PyJWT`, porque o **FastAPI** não exige mecanismos complexos para integrar pacotes externos. +E você pode usar diretamente muitos pacotes bem mantidos e amplamente utilizados, como `pwdlib` e `PyJWT`, porque o **FastAPI** não exige mecanismos complexos para integrar pacotes externos. Mas ele fornece as ferramentas para simplificar o processo o máximo possível, sem comprometer a flexibilidade, robustez ou segurança. diff --git a/docs/pt/docs/tutorial/security/simple-oauth2.md b/docs/pt/docs/tutorial/security/simple-oauth2.md index 1cf05785ee..902ae2d225 100644 --- a/docs/pt/docs/tutorial/security/simple-oauth2.md +++ b/docs/pt/docs/tutorial/security/simple-oauth2.md @@ -1,8 +1,8 @@ -# Simples OAuth2 com senha e Bearer +# Simples OAuth2 com senha e Bearer { #simple-oauth2-with-password-and-bearer } Agora vamos construir a partir do capítulo anterior e adicionar as partes que faltam para ter um fluxo de segurança completo. -## Pegue o `username` (nome de usuário) e `password` (senha) +## Obtenha o `username` e a `password` { #get-the-username-and-password } É utilizado o utils de segurança da **FastAPI** para obter o `username` e a `password`. @@ -18,9 +18,9 @@ Mas para a *operação de rota* de login, precisamos usar esses nomes para serem A especificação também afirma que o `username` e a `password` devem ser enviados como dados de formulário (portanto, não há JSON aqui). -### `scope` +### `scope` { #scope } -A especificação também diz que o cliente pode enviar outro campo de formulário "`scope`" (Escopo). +A especificação também diz que o cliente pode enviar outro campo de formulário "`scope`". O nome do campo do formulário é `scope` (no singular), mas na verdade é uma longa string com "escopos" separados por espaços. @@ -44,11 +44,11 @@ Para OAuth2 são apenas strings. /// -## Código para conseguir o `username` e a `password` +## Código para conseguir o `username` e a `password` { #code-to-get-the-username-and-password } Agora vamos usar os utilitários fornecidos pelo **FastAPI** para lidar com isso. -### `OAuth2PasswordRequestForm` +### `OAuth2PasswordRequestForm` { #oauth2passwordrequestform } Primeiro, importe `OAuth2PasswordRequestForm` e use-o como uma dependência com `Depends` na *operação de rota* para `/token`: @@ -59,7 +59,7 @@ Primeiro, importe `OAuth2PasswordRequestForm` e use-o como uma dependência com * O `username`. * A `password`. * Um campo `scope` opcional como uma string grande, composta de strings separadas por espaços. -* Um `grant_type` (tipo de concessão) opcional. +* Um `grant_type` opcional. /// tip | Dica @@ -84,7 +84,7 @@ Mas como é um caso de uso comum, ele é fornecido diretamente pelo **FastAPI**, /// -### Use os dados do formulário +### Use os dados do formulário { #use-the-form-data } /// tip | Dica @@ -96,13 +96,13 @@ Não estamos usando `scopes` neste exemplo, mas a funcionalidade está disponív Agora, obtenha os dados do usuário do banco de dados (falso), usando o `username` do campo do formulário. -Se não existir tal usuário, retornaremos um erro dizendo "Incorrect username or password" (Nome de usuário ou senha incorretos). +Se não existir tal usuário, retornaremos um erro dizendo "Incorrect username or password". Para o erro, usamos a exceção `HTTPException`: {* ../../docs_src/security/tutorial003_an_py310.py hl[3,79:81] *} -### Confira a password (senha) +### Confira a senha { #check-the-password } Neste ponto temos os dados do usuário do nosso banco de dados, mas não verificamos a senha. @@ -112,7 +112,7 @@ Você nunca deve salvar senhas em texto simples, portanto, usaremos o sistema de Se as senhas não corresponderem, retornaremos o mesmo erro. -#### Hashing de senha +#### Hashing de senha { #password-hashing } "Hashing" significa: converter algum conteúdo (uma senha neste caso) em uma sequência de bytes (apenas uma string) que parece algo sem sentido. @@ -120,7 +120,7 @@ Sempre que você passa exatamente o mesmo conteúdo (exatamente a mesma senha), Mas você não pode converter a sequência aleatória de caracteres de volta para a senha. -##### Porque usar hashing de senha +##### Porque usar hashing de senha { #why-use-password-hashing } Se o seu banco de dados for roubado, o ladrão não terá as senhas em texto simples dos seus usuários, apenas os hashes. @@ -128,11 +128,11 @@ Assim, o ladrão não poderá tentar usar essas mesmas senhas em outro sistema ( {* ../../docs_src/security/tutorial003_an_py310.py hl[82:85] *} -#### Sobre `**user_dict` +#### Sobre `**user_dict` { #about-user-dict } `UserInDB(**user_dict)` significa: -*Passe as keys (chaves) e values (valores) de `user_dict` diretamente como argumentos de valor-chave, equivalente a:* +*Passe as chaves e valores de `user_dict` diretamente como argumentos de valor-chave, equivalente a:* ```Python UserInDB( @@ -146,11 +146,11 @@ UserInDB( /// info | Informação -Para uma explicação mais completa de `**user_dict`, verifique [a documentação para **Extra Models**](../extra-models.md#about-user_indict){.internal-link target=_blank}. +Para uma explicação mais completa de `**user_dict`, verifique [a documentação para **Extra Models**](../extra-models.md#about-user-in-dict){.internal-link target=_blank}. /// -## Retorne o token +## Retorne o token { #return-the-token } A resposta do endpoint `token` deve ser um objeto JSON. @@ -182,11 +182,11 @@ De resto, **FastAPI** cuida disso para você. /// -## Atualize as dependências +## Atualize as dependências { #update-the-dependencies } Agora vamos atualizar nossas dependências. -Queremos obter o `user_user` *somente* se este usuário estiver ativo. +Queremos obter o `current_user` *somente* se este usuário estiver ativo. Portanto, criamos uma dependência adicional `get_current_active_user` que por sua vez usa `get_current_user` como dependência. @@ -214,11 +214,11 @@ Esse é o benefício dos padrões... /// -## Veja em ação +## Veja em ação { #see-it-in-action } Abra o docs interativo: http://127.0.0.1:8000/docs. -### Autenticação +### Autentique-se { #authenticate } Clique no botão "Authorize". @@ -234,7 +234,7 @@ Após autenticar no sistema, você verá assim: -### Obtenha seus próprios dados de usuário +### Obtenha seus próprios dados de usuário { #get-your-own-user-data } Agora use a operação `GET` com o caminho `/users/me`. @@ -260,7 +260,7 @@ Se você clicar no ícone de cadeado, sair e tentar a mesma operação novamente } ``` -### Usuário inativo +### Usuário inativo { #inactive-user } Agora tente com um usuário inativo, autentique-se com: @@ -278,7 +278,7 @@ Você receberá um erro "Usuário inativo", como: } ``` -## Recaptulando +## Recapitulando { #recap } Agora você tem as ferramentas para implementar um sistema de segurança completo baseado em `username` e `password` para sua API. diff --git a/docs/pt/docs/tutorial/sql-databases.md b/docs/pt/docs/tutorial/sql-databases.md index 3d76a532cc..543e164e9f 100644 --- a/docs/pt/docs/tutorial/sql-databases.md +++ b/docs/pt/docs/tutorial/sql-databases.md @@ -1,4 +1,4 @@ -# Bancos de Dados SQL (Relacionais) +# Bancos de Dados SQL (Relacionais) { #sql-relational-databases } **FastAPI** não exige que você use um banco de dados SQL (relacional). Mas você pode usar **qualquer banco de dados** que quiser. @@ -8,7 +8,7 @@ Aqui veremos um exemplo usando "ORMs"), o FastAPI não obriga você a usar nada. 😎 +Você pode usar qualquer outra biblioteca de banco de dados SQL ou NoSQL que quiser (em alguns casos chamadas de "ORMs"), o FastAPI não obriga você a usar nada. 😎 /// @@ -32,7 +32,7 @@ Existe um gerador de projetos oficial com **FastAPI** e **PostgreSQL** incluindo Este é um tutorial muito simples e curto, se você quiser aprender sobre bancos de dados em geral, sobre SQL ou recursos mais avançados, acesse a documentação do SQLModel. -## Instalar o `SQLModel` +## Instalar o `SQLModel` { #install-sqlmodel } Primeiro, certifique-se de criar seu [ambiente virtual](../virtual-environments.md){.internal-link target=_blank}, ativá-lo e, em seguida, instalar o `sqlmodel`: @@ -45,13 +45,13 @@ $ pip install sqlmodel
-## Criar o App com um Único Modelo +## Criar o App com um Único Modelo { #create-the-app-with-a-single-model } Vamos criar a primeira versão mais simples do app com um único modelo **SQLModel**. Depois, vamos melhorá-lo aumentando a segurança e versatilidade com **múltiplos modelos** abaixo. 🤓 -### Criar Modelos +### Criar Modelos { #create-models } Importe o `SQLModel` e crie um modelo de banco de dados: @@ -65,13 +65,13 @@ Existem algumas diferenças: * `Field(primary_key=True)` informa ao SQLModel que o `id` é a **chave primária** no banco de dados SQL (você pode aprender mais sobre chaves primárias SQL na documentação do SQLModel). - Ao ter o tipo como `int | None`, o SQLModel saberá que essa coluna deve ser um `INTEGER` no banco de dados SQL e que ela deve ser `NULLABLE`. + **Nota:** Usamos `int | None` para o campo de chave primária para que, no código Python, possamos *criar um objeto sem um `id`* (`id=None`), assumindo que o banco de dados irá *gerá-lo ao salvar*. O SQLModel entende que o banco de dados fornecerá o `id` e *define a coluna como um `INTEGER` não nulo* no esquema do banco de dados. Veja a documentação do SQLModel sobre chaves primárias para detalhes. * `Field(index=True)` informa ao SQLModel que ele deve criar um **índice SQL** para essa coluna, o que permitirá buscas mais rápidas no banco de dados ao ler dados filtrados por essa coluna. O SQLModel saberá que algo declarado como `str` será uma coluna SQL do tipo `TEXT` (ou `VARCHAR`, dependendo do banco de dados). -### Criar um Engine +### Criar um Engine { #create-an-engine } Um `engine` SQLModel (por baixo dos panos, ele é na verdade um `engine` do SQLAlchemy) é o que **mantém as conexões** com o banco de dados. Você teria **um único objeto `engine`** para todo o seu código se conectar ao mesmo banco de dados. @@ -82,13 +82,13 @@ Usar `check_same_thread=False` permite que o FastAPI use o mesmo banco de dados Não se preocupe, com a forma como o código está estruturado, garantiremos que usamos **uma única *sessão* SQLModel por requisição** mais tarde, isso é realmente o que o `check_same_thread` está tentando conseguir. -### Criar as Tabelas +### Criar as Tabelas { #create-the-tables } Em seguida, adicionamos uma função que usa `SQLModel.metadata.create_all(engine)` para **criar as tabelas** para todos os *modelos de tabela*. {* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[21:22] hl[21:22] *} -### Criar uma Dependência de Sessão +### Criar uma Dependência de Sessão { #create-a-session-dependency } Uma **`Session`** é o que armazena os **objetos na memória** e acompanha as alterações necessárias nos dados, para então **usar o `engine`** para se comunicar com o banco de dados. @@ -98,7 +98,7 @@ Então, criamos uma dependência `Annotated` chamada `SessionDep` para simplific {* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[25:30] hl[25:27,30] *} -### Criar Tabelas de Banco de Dados na Inicialização +### Criar Tabelas de Banco de Dados na Inicialização { #create-database-tables-on-startup } Vamos criar as tabelas do banco de dados quando o aplicativo for iniciado. @@ -114,7 +114,7 @@ O SQLModel terá utilitários de migração envolvendo o Alembic, mas por enquan /// -### Criar um Hero +### Criar um Hero { #create-a-hero } Como cada modelo SQLModel também é um modelo Pydantic, você pode usá-lo nas mesmas **anotações de tipo** que usaria para modelos Pydantic. @@ -124,29 +124,27 @@ Da mesma forma, você pode declará-lo como o **tipo de retorno** da função, e {* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[40:45] hl[40:45] *} - - Aqui, usamos a dependência `SessionDep` (uma `Session`) para adicionar o novo `Hero` à instância `Session`, fazer commit das alterações no banco de dados, atualizar os dados no `hero` e então retorná-lo. -### Ler Heroes +### Ler Heroes { #read-heroes } Podemos **ler** `Hero`s do banco de dados usando um `select()`. Podemos incluir um `limit` e `offset` para paginar os resultados. {* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[48:55] hl[51:52,54] *} -### Ler um Único Hero +### Ler um Único Hero { #read-one-hero } Podemos **ler** um único `Hero`. {* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[58:63] hl[60] *} -### Deletar um Hero +### Deletar um Hero { #delete-a-hero } Também podemos **deletar** um `Hero`. {* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[66:73] hl[71] *} -### Executar o App +### Executar o App { #run-the-app } Você pode executar o app: @@ -166,7 +164,7 @@ Então, vá para a interface `/docs`, você verá que o **FastAPI** está usando
-## Atualizar o App com Múltiplos Modelos +## Atualizar o App com Múltiplos Modelos { #update-the-app-with-multiple-models } Agora vamos **refatorar** este app um pouco para aumentar a **segurança** e **versatilidade**. @@ -178,7 +176,7 @@ Além disso, criamos um `secret_name` para o hero, mas até agora estamos retorn Vamos corrigir essas coisas adicionando alguns **modelos extras**. Aqui é onde o SQLModel vai brilhar. ✨ -### Criar Múltiplos Modelos +### Criar Múltiplos Modelos { #create-multiple-models } No **SQLModel**, qualquer classe de modelo que tenha `table=True` é um **modelo de tabela**. @@ -186,7 +184,7 @@ E qualquer classe de modelo que não tenha `table=True` é um **modelo de dados* Com o SQLModel, podemos usar a **herança** para **evitar duplicação** de todos os campos em todos os casos. -#### `HeroBase` - a classe base +#### `HeroBase` - a classe base { #herobase-the-base-class } Vamos começar com um modelo `HeroBase` que tem todos os **campos compartilhados** por todos os modelos: @@ -195,7 +193,7 @@ Vamos começar com um modelo `HeroBase` que tem todos os **campos compartilhados {* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:9] hl[7:9] *} -#### `Hero` - o *modelo de tabela* +#### `Hero` - o *modelo de tabela* { #hero-the-table-model } Em seguida, vamos criar `Hero`, o verdadeiro *modelo de tabela*, com os **campos extras** que nem sempre estão nos outros modelos: @@ -211,7 +209,7 @@ Como `Hero` herda de `HeroBase`, ele **também** tem os **campos** declarados em {* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:14] hl[12:14] *} -#### `HeroPublic` - o *modelo de dados* público +#### `HeroPublic` - o *modelo de dados* público { #heropublic-the-public-data-model } Em seguida, criamos um modelo `HeroPublic`, que será **retornado** para os clientes da API. @@ -234,11 +232,10 @@ Todos os campos em `HeroPublic` são os mesmos que em `HeroBase`, com `id` decla * `id` * `name` * `age` -* `secret_name` {* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:18] hl[17:18] *} -#### `HeroCreate` - o *modelo de dados* para criar um hero +#### `HeroCreate` - o *modelo de dados* para criar um hero { #herocreate-the-data-model-to-create-a-hero } Agora criamos um modelo `HeroCreate`, este é o que **validará** os dados dos clientes. @@ -262,7 +259,7 @@ Os campos de `HeroCreate` são: {* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:22] hl[21:22] *} -#### `HeroUpdate` - o *modelo de dados* para atualizar um hero +#### `HeroUpdate` - o *modelo de dados* para atualizar um hero { #heroupdate-the-data-model-to-update-a-hero } Não tínhamos uma maneira de **atualizar um hero** na versão anterior do app, mas agora com **múltiplos modelos**, podemos fazer isso. 🎉 @@ -280,7 +277,7 @@ Os campos de `HeroUpdate` são: {* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:28] hl[25:28] *} -### Criar com `HeroCreate` e retornar um `HeroPublic` +### Criar com `HeroCreate` e retornar um `HeroPublic` { #create-with-herocreate-and-return-a-heropublic } Agora que temos **múltiplos modelos**, podemos atualizar as partes do app que os utilizam. @@ -302,19 +299,19 @@ Ao declará-lo no `response_model`, estamos dizendo ao **FastAPI** para fazer o /// -### Ler Heroes com `HeroPublic` +### Ler Heroes com `HeroPublic` { #read-heroes-with-heropublic } Podemos fazer o mesmo que antes para **ler** `Hero`s, novamente, usamos `response_model=list[HeroPublic]` para garantir que os dados sejam validados e serializados corretamente. {* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[65:72] hl[65] *} -### Ler Um Hero com `HeroPublic` +### Ler Um Hero com `HeroPublic` { #read-one-hero-with-heropublic } Podemos **ler** um único herói: {* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[75:80] hl[77] *} -### Atualizar um Hero com `HeroUpdate` +### Atualizar um Hero com `HeroUpdate` { #update-a-hero-with-heroupdate } Podemos **atualizar um hero**. Para isso, usamos uma operação HTTP `PATCH`. @@ -324,7 +321,7 @@ Em seguida, usamos `hero_db.sqlmodel_update(hero_data)` para atualizar o `hero_d {* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[83:93] hl[83:84,88:89] *} -### Deletar um Hero Novamente +### Deletar um Hero Novamente { #delete-a-hero-again } **Deletar** um hero permanece praticamente o mesmo. @@ -332,7 +329,7 @@ Não vamos satisfazer o desejo de refatorar tudo neste aqui. 😅 {* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[96:103] hl[101] *} -### Executar o App Novamente +### Executar o App Novamente { #run-the-app-again } Você pode executar o app novamente: @@ -346,13 +343,13 @@ $ fastapi dev main.py -If you go to the `/docs` API UI, you will see that it is now updated, and it won't expect to receive the `id` from the client when creating a hero, etc. +Se você for para a interface `/docs` da API, verá que agora ela está atualizada e não esperará receber o `id` do cliente ao criar um hero, etc.
-## Recapitulando +## Recapitulando { #recap } Você pode usar **SQLModel** para interagir com um banco de dados SQL e simplificar o código com *modelos de dados* e *modelos de tabela*. diff --git a/docs/pt/docs/tutorial/static-files.md b/docs/pt/docs/tutorial/static-files.md index 30e1af8e64..04a02c7f96 100644 --- a/docs/pt/docs/tutorial/static-files.md +++ b/docs/pt/docs/tutorial/static-files.md @@ -1,15 +1,15 @@ -# Arquivos Estáticos +# Arquivos Estáticos { #static-files } -Você pode servir arquivos estáticos automaticamente de um diretório usando `StaticFiles`. +Você pode servir arquivos estáticos automaticamente a partir de um diretório usando `StaticFiles`. -## Use `StaticFiles` +## Use `StaticFiles` { #use-staticfiles } * Importe `StaticFiles`. -* "Monte" uma instância de `StaticFiles()` em um caminho específico. +* "Monte" uma instância de `StaticFiles()` em um path específico. -{* ../../docs_src/static_files/tutorial001.py hl[2,6] *} +{* ../../docs_src/static_files/tutorial001_py39.py hl[2,6] *} -/// note | Detalhes técnicos +/// note | Detalhes Técnicos Você também pode usar `from starlette.staticfiles import StaticFiles`. @@ -17,24 +17,24 @@ O **FastAPI** fornece o mesmo que `starlette.staticfiles` como `fastapi.staticfi /// -### O que é "Montagem" +### O que é "Montagem" { #what-is-mounting } -"Montagem" significa adicionar um aplicativo completamente "independente" em uma rota específica, que então cuida de todas as subrotas. +"Montagem" significa adicionar uma aplicação completamente "independente" em um path específico, que então cuida de lidar com todos os sub-paths. -Isso é diferente de usar um `APIRouter`, pois um aplicativo montado é completamente independente. A OpenAPI e a documentação do seu aplicativo principal não incluirão nada do aplicativo montado, etc. +Isso é diferente de usar um `APIRouter`, pois uma aplicação montada é completamente independente. A OpenAPI e a documentação da sua aplicação principal não incluirão nada da aplicação montada, etc. -Você pode ler mais sobre isso no **Guia Avançado do Usuário**. +Você pode ler mais sobre isso no [Guia Avançado do Usuário](../advanced/index.md){.internal-link target=_blank}. -## Detalhes +## Detalhes { #details } -O primeiro `"/static"` refere-se à subrota em que este "subaplicativo" será "montado". Portanto, qualquer caminho que comece com `"/static"` será tratado por ele. +O primeiro `"/static"` refere-se ao sub-path no qual este "subaplicativo" será "montado". Assim, qualquer path que comece com `"/static"` será tratado por ele. O `directory="static"` refere-se ao nome do diretório que contém seus arquivos estáticos. -O `name="static"` dá a ela um nome que pode ser usado internamente pelo FastAPI. +O `name="static"` dá a ele um nome que pode ser usado internamente pelo **FastAPI**. -Todos esses parâmetros podem ser diferentes de "`static`", ajuste-os de acordo com as necessidades e detalhes específicos de sua própria aplicação. +Todos esses parâmetros podem ser diferentes de "`static`", ajuste-os de acordo com as necessidades e detalhes específicos da sua própria aplicação. -## Mais informações +## Mais informações { #more-info } -Para mais detalhes e opções, verifique Starlette's docs about Static Files. +Para mais detalhes e opções, consulte a documentação da Starlette sobre Arquivos Estáticos. diff --git a/docs/pt/docs/tutorial/testing.md b/docs/pt/docs/tutorial/testing.md index dc505105ac..e56edcb8c2 100644 --- a/docs/pt/docs/tutorial/testing.md +++ b/docs/pt/docs/tutorial/testing.md @@ -1,12 +1,12 @@ -# Testando +# Testando { #testing } -Graças ao Starlette, testar aplicativos **FastAPI** é fácil e agradável. +Graças ao Starlette, testar aplicações **FastAPI** é fácil e agradável. Ele é baseado no HTTPX, que por sua vez é projetado com base em Requests, por isso é muito familiar e intuitivo. Com ele, você pode usar o pytest diretamente com **FastAPI**. -## Usando `TestClient` +## Usando `TestClient` { #using-testclient } /// info | Informação @@ -22,7 +22,7 @@ $ pip install httpx Importe `TestClient`. -Crie um `TestClient` passando seu aplicativo **FastAPI** para ele. +Crie um `TestClient` passando sua aplicação **FastAPI** para ele. Crie funções com um nome que comece com `test_` (essa é a convenção padrão do `pytest`). @@ -30,7 +30,7 @@ Use o objeto `TestClient` da mesma forma que você faz com `httpx`. Escreva instruções `assert` simples com as expressões Python padrão que você precisa verificar (novamente, `pytest` padrão). -{* ../../docs_src/app_testing/tutorial001.py hl[2,12,15:18] *} +{* ../../docs_src/app_testing/tutorial001_py39.py hl[2,12,15:18] *} /// tip | Dica @@ -42,7 +42,7 @@ Isso permite que você use `pytest` diretamente sem complicações. /// -/// note | Detalhes técnicos +/// note | Detalhes Técnicos Você também pode usar `from starlette.testclient import TestClient`. @@ -52,19 +52,19 @@ Você também pode usar `from starlette.testclient import TestClient`. /// tip | Dica -Se você quiser chamar funções `async` em seus testes além de enviar solicitações ao seu aplicativo FastAPI (por exemplo, funções de banco de dados assíncronas), dê uma olhada em [Testes assíncronos](../advanced/async-tests.md){.internal-link target=_blank} no tutorial avançado. +Se você quiser chamar funções `async` em seus testes além de enviar solicitações à sua aplicação FastAPI (por exemplo, funções de banco de dados assíncronas), dê uma olhada em [Testes assíncronos](../advanced/async-tests.md){.internal-link target=_blank} no tutorial avançado. /// -## Separando testes +## Separando testes { #separating-tests } Em uma aplicação real, você provavelmente teria seus testes em um arquivo diferente. -E seu aplicativo **FastAPI** também pode ser composto de vários arquivos/módulos, etc. +E sua aplicação **FastAPI** também pode ser composta de vários arquivos/módulos, etc. -### Arquivo do aplicativo **FastAPI** +### Arquivo da aplicação **FastAPI** { #fastapi-app-file } -Digamos que você tenha uma estrutura de arquivo conforme descrito em [Aplicativos maiores](bigger-applications.md){.internal-link target=_blank}: +Digamos que você tenha uma estrutura de arquivo conforme descrito em [Aplicações maiores](bigger-applications.md){.internal-link target=_blank}: ``` . @@ -73,12 +73,12 @@ Digamos que você tenha uma estrutura de arquivo conforme descrito em [Aplicativ │   └── main.py ``` -No arquivo `main.py` você tem seu aplicativo **FastAPI**: +No arquivo `main.py` você tem sua aplicação **FastAPI**: -{* ../../docs_src/app_testing/main.py *} +{* ../../docs_src/app_testing/app_a_py39/main.py *} -### Arquivo de teste +### Arquivo de teste { #testing-file } Então você poderia ter um arquivo `test_main.py` com seus testes. Ele poderia estar no mesmo pacote Python (o mesmo diretório com um arquivo `__init__.py`): @@ -92,15 +92,15 @@ Então você poderia ter um arquivo `test_main.py` com seus testes. Ele poderia Como esse arquivo está no mesmo pacote, você pode usar importações relativas para importar o objeto `app` do módulo `main` (`main.py`): -{* ../../docs_src/app_testing/test_main.py hl[3] *} +{* ../../docs_src/app_testing/app_a_py39/test_main.py hl[3] *} ...e ter o código para os testes como antes. -## Testando: exemplo estendido +## Testando: exemplo estendido { #testing-extended-example } Agora vamos estender este exemplo e adicionar mais detalhes para ver como testar diferentes partes. -### Arquivo de aplicativo **FastAPI** estendido +### Arquivo de aplicação **FastAPI** estendido { #extended-fastapi-app-file } Vamos continuar com a mesma estrutura de arquivo de antes: @@ -112,7 +112,7 @@ Vamos continuar com a mesma estrutura de arquivo de antes: │   └── test_main.py ``` -Digamos que agora o arquivo `main.py` com seu aplicativo **FastAPI** tenha algumas outras **operações de rotas**. +Digamos que agora o arquivo `main.py` com sua aplicação **FastAPI** tenha algumas outras **operações de rotas**. Ele tem uma operação `GET` que pode retornar um erro. @@ -120,63 +120,13 @@ Ele tem uma operação `POST` que pode retornar vários erros. Ambas as *operações de rotas* requerem um cabeçalho `X-Token`. -//// tab | Python 3.10+ +{* ../../docs_src/app_testing/app_b_an_py310/main.py *} -```Python -{!> ../../docs_src/app_testing/app_b_an_py310/main.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!> ../../docs_src/app_testing/app_b_an_py39/main.py!} -``` - -//// - -//// tab | Python 3.8+ - -```Python -{!> ../../docs_src/app_testing/app_b_an/main.py!} -``` - -//// - -//// tab | Python 3.10+ non-Annotated - -/// tip | Dica - -Prefira usar a versão `Annotated` se possível. - -/// - -```Python -{!> ../../docs_src/app_testing/app_b_py310/main.py!} -``` - -//// - -//// tab | Python 3.8+ non-Annotated - -/// tip | Dica - -Prefira usar a versão `Annotated` se possível. - -/// - -```Python -{!> ../../docs_src/app_testing/app_b/main.py!} -``` - -//// - -### Arquivo de teste estendido +### Arquivo de teste estendido { #extended-testing-file } Você pode então atualizar `test_main.py` com os testes estendidos: -{* ../../docs_src/app_testing/app_b/test_main.py *} +{* ../../docs_src/app_testing/app_b_an_py310/test_main.py *} Sempre que você precisar que o cliente passe informações na requisição e não souber como, você pode pesquisar (no Google) como fazer isso no `httpx`, ou até mesmo como fazer isso com `requests`, já que o design do HTTPX é baseado no design do Requests. @@ -200,7 +150,7 @@ Se você tiver um modelo Pydantic em seu teste e quiser enviar seus dados para o /// -## Execute-o +## Execute-o { #run-it } Depois disso, você só precisa instalar o `pytest`. diff --git a/docs/pt/docs/virtual-environments.md b/docs/pt/docs/virtual-environments.md index 5fc1a88666..5736f71095 100644 --- a/docs/pt/docs/virtual-environments.md +++ b/docs/pt/docs/virtual-environments.md @@ -1,4 +1,4 @@ -# Ambientes Virtuais +# Ambientes Virtuais { #virtual-environments } Ao trabalhar em projetos Python, você provavelmente deve usar um **ambiente virtual** (ou um mecanismo similar) para isolar os pacotes que você instala para cada projeto. @@ -26,7 +26,7 @@ Se você estiver pronto para adotar uma **ferramenta que gerencia tudo** para vo /// -## Criar um Projeto +## Criar um Projeto { #create-a-project } Primeiro, crie um diretório para seu projeto. @@ -51,7 +51,7 @@ $ cd awesome-project -## Crie um ambiente virtual +## Crie um ambiente virtual { #create-a-virtual-environment } Ao começar a trabalhar em um projeto Python **pela primeira vez**, crie um ambiente virtual **dentro do seu projeto**. @@ -114,7 +114,7 @@ Você pode criar o ambiente virtual em um diretório diferente, mas há uma conv /// -## Ative o ambiente virtual +## Ative o ambiente virtual { #activate-the-virtual-environment } Ative o novo ambiente virtual para que qualquer comando Python que você executar ou pacote que você instalar o utilize. @@ -166,17 +166,17 @@ $ source .venv/Scripts/activate Toda vez que você instalar um **novo pacote** naquele ambiente, **ative** o ambiente novamente. -Isso garante que, se você usar um **programa de terminal (CLI)** instalado por esse pacote, você usará aquele do seu ambiente virtual e não qualquer outro que possa ser instalado globalmente, provavelmente com uma versão diferente do que você precisa. +Isso garante que, se você usar um **programa de terminal (CLI)** instalado por esse pacote, você usará aquele do seu ambiente virtual e não qualquer outro que possa ser instalado globalmente, provavelmente com uma versão diferente do que você precisa. /// -## Verifique se o ambiente virtual está ativo +## Verifique se o ambiente virtual está ativo { #check-the-virtual-environment-is-active } Verifique se o ambiente virtual está ativo (o comando anterior funcionou). /// tip | Dica -Isso é **opcional**, mas é uma boa maneira de **verificar** se tudo está funcionando conforme o esperado e se você está usando o ambiente virtual pretendido. +Isso é **opcional**, mas é uma boa maneira de **verificar** se tudo está funcionando conforme o esperado e se você está usando o ambiente virtual intendido. /// @@ -212,7 +212,7 @@ Se ele mostrar o binário `python` em `.venv\Scripts\python`, dentro do seu proj //// -## Atualizar `pip` +## Atualizar `pip` { #upgrade-pip } /// tip | Dica @@ -242,7 +242,27 @@ $ python -m pip install --upgrade pip -## Adicionar `.gitignore` +/// tip | Dica + +Às vezes, você pode receber um erro **`No module named pip`** ao tentar atualizar o pip. + +Se isso acontecer, instale e atualize o pip usando o comando abaixo: + +
+ +```console +$ python -m ensurepip --upgrade + +---> 100% +``` + +
+ +Esse comando instalará o pip caso ele ainda não esteja instalado e também garante que a versão instalada do pip seja pelo menos tão recente quanto a disponível em `ensurepip`. + +/// + +## Adicionar `.gitignore` { #add-gitignore } Se você estiver usando **Git** (você deveria), adicione um arquivo `.gitignore` para excluir tudo em seu `.venv` do Git. @@ -282,7 +302,7 @@ Esse comando criará um arquivo `.gitignore` com o conteúdo: /// -## Instalar Pacotes +## Instalar Pacotes { #install-packages } Após ativar o ambiente, você pode instalar pacotes nele. @@ -294,7 +314,7 @@ Se precisar atualizar uma versão ou adicionar um novo pacote, você **fará iss /// -### Instalar pacotes diretamente +### Instalar pacotes diretamente { #install-packages-directly } Se estiver com pressa e não quiser usar um arquivo para declarar os requisitos de pacote do seu projeto, você pode instalá-los diretamente. @@ -333,7 +353,7 @@ $ uv pip install "fastapi[standard]" //// -### Instalar a partir de `requirements.txt` +### Instalar a partir de `requirements.txt` { #install-from-requirements-txt } Se você tiver um `requirements.txt`, agora poderá usá-lo para instalar seus pacotes. @@ -376,7 +396,7 @@ pydantic==2.8.0 /// -## Execute seu programa +## Execute seu programa { #run-your-program } Depois de ativar o ambiente virtual, você pode executar seu programa, e ele usará o Python dentro do seu ambiente virtual com os pacotes que você instalou lá. @@ -390,9 +410,9 @@ Hello World -## Configure seu editor +## Configure seu editor { #configure-your-editor } -Você provavelmente usaria um editor. Certifique-se de configurá-lo para usar o mesmo ambiente virtual que você criou (ele provavelmente o detectará automaticamente) para que você possa obter erros de preenchimento automático e em linha. +Você provavelmente usaria um editor. Certifique-se de configurá-lo para usar o mesmo ambiente virtual que você criou (ele provavelmente o detectará automaticamente) para que você possa obter preenchimento automático e erros em linha. Por exemplo: @@ -405,7 +425,7 @@ Normalmente, você só precisa fazer isso **uma vez**, ao criar o ambiente virtu /// -## Desativar o ambiente virtual +## Desativar o ambiente virtual { #deactivate-the-virtual-environment } Quando terminar de trabalhar no seu projeto, você pode **desativar** o ambiente virtual. @@ -419,7 +439,7 @@ $ deactivate Dessa forma, quando você executar `python`, ele não tentará executá-lo naquele ambiente virtual com os pacotes instalados nele. -## Pronto para trabalhar +## Pronto para trabalhar { #ready-to-work } Agora você está pronto para começar a trabalhar no seu projeto. @@ -433,7 +453,7 @@ Continue lendo. 👇🤓 /// -## Por que ambientes virtuais +## Por que ambientes virtuais { #why-virtual-environments } Para trabalhar com o FastAPI, você precisa instalar o Python. @@ -443,7 +463,7 @@ Para instalar pacotes, você normalmente usaria o comando `pip` que vem com o Py No entanto, se você usar `pip` diretamente, os pacotes serão instalados no seu **ambiente Python global** (a instalação global do Python). -### O Problema +### O Problema { #the-problem } Então, qual é o problema em instalar pacotes no ambiente global do Python? @@ -526,7 +546,7 @@ Agora, imagine isso com **muitos** outros **pacotes** dos quais todos os seus ** Além disso, dependendo do seu sistema operacional (por exemplo, Linux, Windows, macOS), ele pode ter vindo com o Python já instalado. E, nesse caso, provavelmente tinha alguns pacotes pré-instalados com algumas versões específicas **necessárias para o seu sistema**. Se você instalar pacotes no ambiente global do Python, poderá acabar **quebrando** alguns dos programas que vieram com seu sistema operacional. -## Onde os pacotes são instalados +## Onde os pacotes são instalados { #where-are-packages-installed } Quando você instala o Python, ele cria alguns diretórios com alguns arquivos no seu computador. @@ -552,7 +572,7 @@ Em seguida, ele **extrairá** todos esses arquivos e os colocará em um diretór Por padrão, ele colocará os arquivos baixados e extraídos no diretório que vem com a instalação do Python, que é o **ambiente global**. -## O que são ambientes virtuais +## O que são ambientes virtuais { #what-are-virtual-environments } A solução para os problemas de ter todos os pacotes no ambiente global é usar um **ambiente virtual para cada projeto** em que você trabalha. @@ -577,7 +597,7 @@ flowchart TB stone-project ~~~ azkaban-project ``` -## O que significa ativar um ambiente virtual +## O que significa ativar um ambiente virtual { #what-does-activating-a-virtual-environment-mean } Quando você ativa um ambiente virtual, por exemplo com: @@ -714,7 +734,7 @@ Um detalhe importante é que ele colocará o caminho do ambiente virtual no **in Ativar um ambiente virtual também muda algumas outras coisas, mas esta é uma das mais importantes. -## Verificando um ambiente virtual +## Verificando um ambiente virtual { #checking-a-virtual-environment } Ao verificar se um ambiente virtual está ativo, por exemplo com: @@ -752,7 +772,7 @@ você usa `which` no Linux e macOS e `Get-Command` no Windows PowerShell. A maneira como esse comando funciona é que ele vai e verifica na variável de ambiente `PATH`, passando por **cada caminho em ordem**, procurando pelo programa chamado `python`. Uma vez que ele o encontre, ele **mostrará o caminho** para esse programa. -A parte mais importante é que quando você chama ``python`, esse é exatamente o "`python`" que será executado. +A parte mais importante é que quando você chama `python`, esse é exatamente o "`python`" que será executado. Assim, você pode confirmar se está no ambiente virtual correto. @@ -766,7 +786,7 @@ E o segundo projeto **não funcionaria** porque você está usando o **Python in /// -## Por que desativar um ambiente virtual +## Por que desativar um ambiente virtual { #why-deactivate-a-virtual-environment } Por exemplo, você pode estar trabalhando em um projeto `philosophers-stone`, **ativar esse ambiente virtual**, instalar pacotes e trabalhar com esse ambiente. @@ -820,7 +840,7 @@ Eu juro solenemente 🐺 -## Alternativas +## Alternativas { #alternatives } Este é um guia simples para você começar e lhe ensinar como tudo funciona **por baixo**. @@ -834,10 +854,10 @@ Quando estiver pronto e quiser usar uma ferramenta para **gerenciar todo o proje * Gerenciar o **ambiente virtual** para seus projetos * Instalar **pacotes** * Gerenciar **dependências e versões** de pacotes para seu projeto -* Certifique-se de ter um conjunto **exato** de pacotes e versões para instalar, incluindo suas dependências, para que você possa ter certeza de que pode executar seu projeto em produção exatamente da mesma forma que em seu computador durante o desenvolvimento, isso é chamado de **bloqueio** +* Certificar-se de que você tenha um conjunto **exato** de pacotes e versões para instalar, incluindo suas dependências, para que você possa ter certeza de que pode executar seu projeto em produção exatamente da mesma forma que em seu computador durante o desenvolvimento, isso é chamado de **bloqueio** * E muitas outras coisas -## Conclusão +## Conclusão { #conclusion } Se você leu e entendeu tudo isso, agora **você sabe muito mais** sobre ambientes virtuais do que muitos desenvolvedores por aí. 🤓 diff --git a/docs/pt/llm-prompt.md b/docs/pt/llm-prompt.md new file mode 100644 index 0000000000..3f5208e910 --- /dev/null +++ b/docs/pt/llm-prompt.md @@ -0,0 +1,66 @@ +### Target language + +Translate to Portuguese (Português). + +Language code: pt. + +For instructions or titles in imperative, keep them in imperative, for example "Import FastAPI" to "Importe o FastAPI". + +Keep existing translations as they are if the term is already translated. + +When translating documentation into Portuguese, use neutral and widely understandable language. Although Portuguese originated in Portugal and has its largest number of speakers in Brazil, it is also an official language in several countries and regions, such as Equatorial Guinea, Mozambique, Angola, Cape Verde, and São Tomé and Príncipe. Avoid words or expressions that are specific to a single country or region. + +--- + +For the next terms, use the following translations: + +* /// check: /// check | Verifique +* /// danger: /// danger | Cuidado +* /// info: /// info | Informação +* /// note | Technical Details: /// note | Detalhes Técnicos +* /// info | Very Technical Details: /// note | Detalhes Técnicos Avançados +* /// note: /// note | Nota +* /// tip: /// tip | Dica +* /// warning: /// warning | Atenção +* (you should): (você deveria) +* async context manager: gerenciador de contexto assíncrono +* autocomplete: autocompletar +* autocompletion: preenchimento automático +* bug: bug +* context manager: gerenciador de contexto +* cross domain: cross domain (do not translate to "domínio cruzado") +* cross origin: cross origin (do not translate to "origem cruzada") +* Cross-Origin Resource Sharing: Cross-Origin Resource Sharing (do not translate to "Compartilhamento de Recursos de Origem Cruzada") +* Deep Learning: Deep Learning (do not translate to "Aprendizado Profundo") +* dependable: dependable +* dependencies: dependências +* deprecated: descontinuado +* docs: documentação +* FastAPI app: aplicação FastAPI +* framework: framework (do not translate) +* feature: funcionalidade +* guides: tutoriais +* I/O (as in "input and output"): I/O (do not translate to "E/S") +* JSON Schema: JSON Schema +* library: biblioteca +* lifespan: lifespan (do not translate to "vida útil") +* list (as in Python list): list +* Machine Learning: Aprendizado de Máquina +* media type: media type (do not translate to "tipo de mídia") +* non-Annotated: non-Annotated (do not translate non-Annotated when it comes after a Python version.e.g., “Python 3.10+ non-Annotated”) +* operation IDs: IDs de operação +* path (as in URL path): path +* path operation: operação de rota +* path operation function: função de operação de rota +* prefix: prefixo +* request (as in HTTP request): request (do not change if it's already translated to requisição) +* router (as in FastAPI's router): router (do not change if it's already translated to "roteador" or "roteadores") +* response (as in HTTP response): response (do not change if it's already translated to resposta) +* shutdown (of the app): encerramento +* shutdown event (of the app): evento de encerramento +* startup (as in the event of the app): inicialização +* startup event (as in the event of the app): evento de inicialização +* Stream: Stream +* string: string +* type hints: anotações de tipo +* wildcards: curingas diff --git a/docs/ru/docs/_llm-test.md b/docs/ru/docs/_llm-test.md index 476cc19244..9a15f6bb21 100644 --- a/docs/ru/docs/_llm-test.md +++ b/docs/ru/docs/_llm-test.md @@ -15,7 +15,7 @@ Тесты: -## Фрагменты кода { #code-snippets} +## Фрагменты кода { #code-snippets } //// tab | Тест @@ -53,7 +53,7 @@ LLM, вероятно, переведёт это неправильно. Инт //// -## Кавычки во фрагментах кода { #quotes-in-code-snippets} +## Кавычки во фрагментах кода { #quotes-in-code-snippets } //// tab | Тест diff --git a/docs/ru/docs/advanced/additional-responses.md b/docs/ru/docs/advanced/additional-responses.md index c63c0c08b0..fca4f072da 100644 --- a/docs/ru/docs/advanced/additional-responses.md +++ b/docs/ru/docs/advanced/additional-responses.md @@ -26,7 +26,7 @@ Например, чтобы объявить ещё один ответ со статус-кодом `404` и Pydantic-моделью `Message`, можно написать: -{* ../../docs_src/additional_responses/tutorial001.py hl[18,22] *} +{* ../../docs_src/additional_responses/tutorial001_py39.py hl[18,22] *} /// note | Примечание @@ -175,7 +175,7 @@ Например, вы можете добавить дополнительный тип содержимого `image/png`, объявив, что ваша операция пути может возвращать JSON‑объект (с типом содержимого `application/json`) или PNG‑изображение: -{* ../../docs_src/additional_responses/tutorial002.py hl[19:24,28] *} +{* ../../docs_src/additional_responses/tutorial002_py310.py hl[17:22,26] *} /// note | Примечание @@ -203,7 +203,7 @@ А также ответ со статус-кодом `200`, который использует ваш `response_model`, но включает пользовательский `example`: -{* ../../docs_src/additional_responses/tutorial003.py hl[20:31] *} +{* ../../docs_src/additional_responses/tutorial003_py39.py hl[20:31] *} Всё это будет объединено и включено в ваш OpenAPI и отображено в документации API: @@ -237,7 +237,7 @@ new_dict = {**old_dict, "new key": "new value"} Например: -{* ../../docs_src/additional_responses/tutorial004.py hl[13:17,26] *} +{* ../../docs_src/additional_responses/tutorial004_py310.py hl[11:15,24] *} ## Дополнительная информация об ответах OpenAPI { #more-information-about-openapi-responses } diff --git a/docs/ru/docs/advanced/advanced-dependencies.md b/docs/ru/docs/advanced/advanced-dependencies.md index 75a6f0d1fa..cc6691b300 100644 --- a/docs/ru/docs/advanced/advanced-dependencies.md +++ b/docs/ru/docs/advanced/advanced-dependencies.md @@ -70,12 +70,22 @@ checker(q="somequery") Скорее всего, вам не понадобятся эти технические детали. -Они полезны главным образом, если у вас было приложение FastAPI версии ниже 0.118.0 и вы столкнулись с проблемами зависимостей с `yield`. +Они полезны главным образом, если у вас было приложение FastAPI версии ниже 0.121.0 и вы столкнулись с проблемами зависимостей с `yield`. /// Зависимости с `yield` со временем изменялись, чтобы учитывать разные случаи применения и исправлять проблемы. Ниже — краткое резюме изменений. +### Зависимости с `yield` и `scope` { #dependencies-with-yield-and-scope } + +В версии 0.121.0 FastAPI добавил поддержку `Depends(scope="function")` для зависимостей с `yield`. + +При использовании `Depends(scope="function")` код после `yield` выполняется сразу после завершения *функции-обработчика пути*, до отправки ответа клиенту. + +А при использовании `Depends(scope="request")` (значение по умолчанию) код после `yield` выполняется после отправки ответа. + +Подробнее читайте в документации: [Зависимости с `yield` — раннее завершение и `scope`](../tutorial/dependencies/dependencies-with-yield.md#early-exit-and-scope). + ### Зависимости с `yield` и `StreamingResponse`, технические детали { #dependencies-with-yield-and-streamingresponse-technical-details } До FastAPI 0.118.0, если вы использовали зависимость с `yield`, код после `yield` выполнялся после возврата из *функции-обработчика пути*, но прямо перед отправкой ответа. @@ -134,7 +144,7 @@ checker(q="somequery") ### Фоновые задачи и зависимости с `yield`, технические детали { #background-tasks-and-dependencies-with-yield-technical-details } -До FastAPI 0.106.0 вызывать исключения после `yield` было невозможно: код после `yield` в зависимостях выполнялся уже после отправки ответа, поэтому [Обработчики исключений](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank} к тому моменту уже отработали. +До FastAPI 0.106.0 вызывать исключения после `yield` было невозможно: код после `yield` в зависимостях выполнялся уже после отправки ответа, поэтому [Обработчики исключений](../tutorial/handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank} к тому моменту уже отработали. Так было сделано в основном для того, чтобы можно было использовать те же объекты, «отданные» зависимостями через `yield`, внутри фоновых задач, потому что код после `yield` выполнялся после завершения фоновых задач. diff --git a/docs/ru/docs/advanced/async-tests.md b/docs/ru/docs/advanced/async-tests.md index 5062bc52e7..e689704066 100644 --- a/docs/ru/docs/advanced/async-tests.md +++ b/docs/ru/docs/advanced/async-tests.md @@ -32,11 +32,11 @@ Файл `main.py`: -{* ../../docs_src/async_tests/main.py *} +{* ../../docs_src/async_tests/app_a_py39/main.py *} Файл `test_main.py` содержит тесты для `main.py`, теперь он может выглядеть так: -{* ../../docs_src/async_tests/test_main.py *} +{* ../../docs_src/async_tests/app_a_py39/test_main.py *} ## Запуск тестов { #run-it } @@ -56,7 +56,7 @@ $ pytest Маркер `@pytest.mark.anyio` говорит pytest, что тестовая функция должна быть вызвана асинхронно: -{* ../../docs_src/async_tests/test_main.py hl[7] *} +{* ../../docs_src/async_tests/app_a_py39/test_main.py hl[7] *} /// tip | Подсказка @@ -66,7 +66,7 @@ $ pytest Затем мы можем создать `AsyncClient` со ссылкой на приложение и посылать асинхронные запросы, используя `await`. -{* ../../docs_src/async_tests/test_main.py hl[9:12] *} +{* ../../docs_src/async_tests/app_a_py39/test_main.py hl[9:12] *} Это эквивалентно следующему: diff --git a/docs/ru/docs/advanced/behind-a-proxy.md b/docs/ru/docs/advanced/behind-a-proxy.md index 281cb7f735..f78da01a09 100644 --- a/docs/ru/docs/advanced/behind-a-proxy.md +++ b/docs/ru/docs/advanced/behind-a-proxy.md @@ -44,7 +44,7 @@ $ fastapi run --forwarded-allow-ips="*" Например, вы объявили операцию пути `/items/`: -{* ../../docs_src/behind_a_proxy/tutorial001_01.py hl[6] *} +{* ../../docs_src/behind_a_proxy/tutorial001_01_py39.py hl[6] *} Если клиент обратится к `/items`, по умолчанию произойдёт редирект на `/items/`. @@ -64,7 +64,7 @@ https://mysuperapp.com/items/ /// -### Как работают пересылаемые заголовки прокси +### Как работают пересылаемые заголовки прокси { #how-proxy-forwarded-headers-work } Ниже показано, как прокси добавляет пересылаемые заголовки между клиентом и сервером приложения: @@ -115,7 +115,7 @@ sequenceDiagram Хотя весь ваш код написан с расчётом, что путь один — `/app`. -{* ../../docs_src/behind_a_proxy/tutorial001.py hl[6] *} +{* ../../docs_src/behind_a_proxy/tutorial001_py39.py hl[6] *} Прокси будет «обрезать» префикс пути на лету перед передачей запроса на сервер приложения (скорее всего Uvicorn, запущенный через FastAPI CLI), поддерживая у вашего приложения иллюзию, что его обслуживают по `/app`, чтобы вам не пришлось менять весь код и добавлять префикс `/api/v1`. @@ -193,7 +193,7 @@ $ fastapi run main.py --forwarded-allow-ips="*" --root-path /api/v1 Здесь мы добавляем его в сообщение лишь для демонстрации. -{* ../../docs_src/behind_a_proxy/tutorial001.py hl[8] *} +{* ../../docs_src/behind_a_proxy/tutorial001_py39.py hl[8] *} Затем, если вы запустите Uvicorn так: @@ -220,7 +220,7 @@ $ fastapi run main.py --forwarded-allow-ips="*" --root-path /api/v1 Если нет возможности передать опцию командной строки `--root-path` (или аналог), вы можете указать параметр `root_path` при создании приложения FastAPI: -{* ../../docs_src/behind_a_proxy/tutorial002.py hl[3] *} +{* ../../docs_src/behind_a_proxy/tutorial002_py39.py hl[3] *} Передача `root_path` в `FastAPI` эквивалентна опции командной строки `--root-path` для Uvicorn или Hypercorn. @@ -400,7 +400,7 @@ $ fastapi run main.py --forwarded-allow-ips="*" --root-path /api/v1 Например: -{* ../../docs_src/behind_a_proxy/tutorial003.py hl[4:7] *} +{* ../../docs_src/behind_a_proxy/tutorial003_py39.py hl[4:7] *} Будет сгенерирована схема OpenAPI примерно такая: @@ -443,11 +443,19 @@ $ fastapi run main.py --forwarded-allow-ips="*" --root-path /api/v1 /// +/// note | Технические детали + +Свойство `servers` в спецификации OpenAPI является необязательным. + +Если вы не укажете параметр `servers`, а `root_path` равен `/`, то свойство `servers` в сгенерированной схеме OpenAPI по умолчанию будет опущено. Это эквивалентно серверу со значением `url` равным `/`. + +/// + ### Отключить автоматическое добавление сервера из `root_path` { #disable-automatic-server-from-root-path } Если вы не хотите, чтобы FastAPI добавлял автоматический сервер, используя `root_path`, укажите параметр `root_path_in_servers=False`: -{* ../../docs_src/behind_a_proxy/tutorial004.py hl[9] *} +{* ../../docs_src/behind_a_proxy/tutorial004_py39.py hl[9] *} и тогда этот сервер не будет добавлен в схему OpenAPI. diff --git a/docs/ru/docs/advanced/custom-response.md b/docs/ru/docs/advanced/custom-response.md index 2c238bd95b..49550b49ff 100644 --- a/docs/ru/docs/advanced/custom-response.md +++ b/docs/ru/docs/advanced/custom-response.md @@ -30,7 +30,7 @@ Но если вы уверены, что содержимое, которое вы возвращаете, **сериализуемо в JSON**, вы можете передать его напрямую в класс ответа и избежать дополнительных накладных расходов, которые FastAPI понёс бы, пропуская возвращаемое содержимое через `jsonable_encoder` перед передачей в класс ответа. -{* ../../docs_src/custom_response/tutorial001b.py hl[2,7] *} +{* ../../docs_src/custom_response/tutorial001b_py39.py hl[2,7] *} /// info | Информация @@ -55,7 +55,7 @@ - Импортируйте `HTMLResponse`. - Передайте `HTMLResponse` в параметр `response_class` вашего декоратора операции пути. -{* ../../docs_src/custom_response/tutorial002.py hl[2,7] *} +{* ../../docs_src/custom_response/tutorial002_py39.py hl[2,7] *} /// info | Информация @@ -73,7 +73,7 @@ Тот же пример сверху, возвращающий `HTMLResponse`, может выглядеть так: -{* ../../docs_src/custom_response/tutorial003.py hl[2,7,19] *} +{* ../../docs_src/custom_response/tutorial003_py39.py hl[2,7,19] *} /// warning | Предупреждение @@ -97,7 +97,7 @@ Например, это может быть что-то вроде: -{* ../../docs_src/custom_response/tutorial004.py hl[7,21,23] *} +{* ../../docs_src/custom_response/tutorial004_py39.py hl[7,21,23] *} В этом примере функция `generate_html_response()` уже генерирует и возвращает `Response` вместо возврата HTML в `str`. @@ -136,7 +136,7 @@ FastAPI (фактически Starlette) автоматически добавит заголовок Content-Length. Также будет добавлен заголовок Content-Type, основанный на `media_type` и с добавлением charset для текстовых типов. -{* ../../docs_src/response_directly/tutorial002.py hl[1,18] *} +{* ../../docs_src/response_directly/tutorial002_py39.py hl[1,18] *} ### `HTMLResponse` { #htmlresponse } @@ -146,7 +146,7 @@ FastAPI (фактически Starlette) автоматически добави Принимает текст или байты и возвращает ответ в виде простого текста. -{* ../../docs_src/custom_response/tutorial005.py hl[2,7,9] *} +{* ../../docs_src/custom_response/tutorial005_py39.py hl[2,7,9] *} ### `JSONResponse` { #jsonresponse } @@ -180,7 +180,7 @@ FastAPI (фактически Starlette) автоматически добави /// -{* ../../docs_src/custom_response/tutorial001.py hl[2,7] *} +{* ../../docs_src/custom_response/tutorial001_py39.py hl[2,7] *} /// tip | Совет @@ -194,13 +194,13 @@ FastAPI (фактически Starlette) автоматически добави Вы можете вернуть `RedirectResponse` напрямую: -{* ../../docs_src/custom_response/tutorial006.py hl[2,9] *} +{* ../../docs_src/custom_response/tutorial006_py39.py hl[2,9] *} --- Или можно использовать его в параметре `response_class`: -{* ../../docs_src/custom_response/tutorial006b.py hl[2,7,9] *} +{* ../../docs_src/custom_response/tutorial006b_py39.py hl[2,7,9] *} Если вы сделаете так, то сможете возвращать URL напрямую из своей функции-обработчика пути. @@ -210,13 +210,13 @@ FastAPI (фактически Starlette) автоматически добави Также вы можете использовать параметр `status_code` в сочетании с параметром `response_class`: -{* ../../docs_src/custom_response/tutorial006c.py hl[2,7,9] *} +{* ../../docs_src/custom_response/tutorial006c_py39.py hl[2,7,9] *} ### `StreamingResponse` { #streamingresponse } Принимает асинхронный генератор или обычный генератор/итератор и отправляет тело ответа потоково. -{* ../../docs_src/custom_response/tutorial007.py hl[2,14] *} +{* ../../docs_src/custom_response/tutorial007_py39.py hl[2,14] *} #### Использование `StreamingResponse` с файлоподобными объектами { #using-streamingresponse-with-file-like-objects } @@ -226,7 +226,7 @@ FastAPI (фактически Starlette) автоматически добави Это включает многие библиотеки для работы с облачным хранилищем, обработки видео и т.д. -{* ../../docs_src/custom_response/tutorial008.py hl[2,10:12,14] *} +{* ../../docs_src/custom_response/tutorial008_py39.py hl[2,10:12,14] *} 1. Это функция-генератор. Она является «функцией-генератором», потому что содержит оператор(ы) `yield` внутри. 2. Используя блок `with`, мы гарантируем, что файлоподобный объект будет закрыт после завершения работы функции-генератора. То есть после того, как она закончит отправку ответа. @@ -255,11 +255,11 @@ FastAPI (фактически Starlette) автоматически добави Файловые ответы будут содержать соответствующие заголовки `Content-Length`, `Last-Modified` и `ETag`. -{* ../../docs_src/custom_response/tutorial009.py hl[2,10] *} +{* ../../docs_src/custom_response/tutorial009_py39.py hl[2,10] *} Вы также можете использовать параметр `response_class`: -{* ../../docs_src/custom_response/tutorial009b.py hl[2,8,10] *} +{* ../../docs_src/custom_response/tutorial009b_py39.py hl[2,8,10] *} В этом случае вы можете возвращать путь к файлу напрямую из своей функции-обработчика пути. @@ -273,7 +273,7 @@ FastAPI (фактически Starlette) автоматически добави Вы могли бы создать `CustomORJSONResponse`. Главное, что вам нужно сделать — реализовать метод `Response.render(content)`, который возвращает содержимое как `bytes`: -{* ../../docs_src/custom_response/tutorial009c.py hl[9:14,17] *} +{* ../../docs_src/custom_response/tutorial009c_py39.py hl[9:14,17] *} Теперь вместо того, чтобы возвращать: @@ -299,7 +299,7 @@ FastAPI (фактически Starlette) автоматически добави В примере ниже **FastAPI** будет использовать `ORJSONResponse` по умолчанию во всех операциях пути вместо `JSONResponse`. -{* ../../docs_src/custom_response/tutorial010.py hl[2,4] *} +{* ../../docs_src/custom_response/tutorial010_py39.py hl[2,4] *} /// tip | Совет diff --git a/docs/ru/docs/advanced/dataclasses.md b/docs/ru/docs/advanced/dataclasses.md index 816f744048..b3ced37c1e 100644 --- a/docs/ru/docs/advanced/dataclasses.md +++ b/docs/ru/docs/advanced/dataclasses.md @@ -4,7 +4,7 @@ FastAPI построен поверх **Pydantic**, и я показывал в Но FastAPI также поддерживает использование `dataclasses` тем же способом: -{* ../../docs_src/dataclasses/tutorial001.py hl[1,7:12,19:20] *} +{* ../../docs_src/dataclasses_/tutorial001_py310.py hl[1,6:11,18:19] *} Это по-прежнему поддерживается благодаря **Pydantic**, так как в нём есть встроенная поддержка `dataclasses`. @@ -32,7 +32,7 @@ FastAPI построен поверх **Pydantic**, и я показывал в Вы также можете использовать `dataclasses` в параметре `response_model`: -{* ../../docs_src/dataclasses/tutorial002.py hl[1,7:13,19] *} +{* ../../docs_src/dataclasses_/tutorial002_py310.py hl[1,6:12,18] *} Этот dataclass будет автоматически преобразован в Pydantic dataclass. @@ -48,7 +48,7 @@ FastAPI построен поверх **Pydantic**, и я показывал в В таком случае вы можете просто заменить стандартные `dataclasses` на `pydantic.dataclasses`, которая является полностью совместимой заменой (drop-in replacement): -{* ../../docs_src/dataclasses/tutorial003.py hl[1,5,8:11,14:17,23:25,28] *} +{* ../../docs_src/dataclasses_/tutorial003_py310.py hl[1,4,7:10,13:16,22:24,27] *} 1. Мы по-прежнему импортируем `field` из стандартных `dataclasses`. diff --git a/docs/ru/docs/advanced/events.md b/docs/ru/docs/advanced/events.md index 20d1df98a9..db73d9094e 100644 --- a/docs/ru/docs/advanced/events.md +++ b/docs/ru/docs/advanced/events.md @@ -30,7 +30,7 @@ Мы создаём асинхронную функцию `lifespan()` с `yield` примерно так: -{* ../../docs_src/events/tutorial003.py hl[16,19] *} +{* ../../docs_src/events/tutorial003_py39.py hl[16,19] *} Здесь мы симулируем дорогую операцию startup по загрузке модели, помещая (фиктивную) функцию модели в словарь с моделями Машинного обучения до `yield`. Этот код будет выполнен до того, как приложение начнет принимать запросы, во время startup. @@ -48,7 +48,7 @@ Первое, на что стоит обратить внимание, — мы определяем асинхронную функцию с `yield`. Это очень похоже на Зависимости с `yield`. -{* ../../docs_src/events/tutorial003.py hl[14:19] *} +{* ../../docs_src/events/tutorial003_py39.py hl[14:19] *} Первая часть функции, до `yield`, будет выполнена до запуска приложения. @@ -60,7 +60,7 @@ Это превращает функцию в «асинхронный менеджер контекста». -{* ../../docs_src/events/tutorial003.py hl[1,13] *} +{* ../../docs_src/events/tutorial003_py39.py hl[1,13] *} Менеджер контекста в Python — это то, что можно использовать в операторе `with`. Например, `open()` можно использовать как менеджер контекста: @@ -82,7 +82,7 @@ async with lifespan(app): Параметр `lifespan` приложения `FastAPI` принимает асинхронный менеджер контекста, поэтому мы можем передать ему наш новый асинхронный менеджер контекста `lifespan`. -{* ../../docs_src/events/tutorial003.py hl[22] *} +{* ../../docs_src/events/tutorial003_py39.py hl[22] *} ## Альтернативные события (устаревшие) { #alternative-events-deprecated } @@ -104,7 +104,7 @@ async with lifespan(app): Чтобы добавить функцию, которую нужно запустить до старта приложения, объявите её как обработчик события `"startup"`: -{* ../../docs_src/events/tutorial001.py hl[8] *} +{* ../../docs_src/events/tutorial001_py39.py hl[8] *} В этом случае функция-обработчик события `startup` инициализирует «базу данных» items (это просто `dict`) некоторыми значениями. @@ -116,7 +116,7 @@ async with lifespan(app): Чтобы добавить функцию, которую нужно запустить при завершении работы приложения, объявите её как обработчик события `"shutdown"`: -{* ../../docs_src/events/tutorial002.py hl[6] *} +{* ../../docs_src/events/tutorial002_py39.py hl[6] *} Здесь функция-обработчик события `shutdown` запишет строку текста `"Application shutdown"` в файл `log.txt`. diff --git a/docs/ru/docs/advanced/generate-clients.md b/docs/ru/docs/advanced/generate-clients.md index ee52412c6d..00bdd31fe8 100644 --- a/docs/ru/docs/advanced/generate-clients.md +++ b/docs/ru/docs/advanced/generate-clients.md @@ -167,7 +167,7 @@ FastAPI использует **уникальный ID** для каждой *о Мы можем скачать OpenAPI JSON в файл `openapi.json`, а затем **убрать этот префикс‑тег** таким скриптом: -{* ../../docs_src/generate_clients/tutorial004.py *} +{* ../../docs_src/generate_clients/tutorial004_py39.py *} //// tab | Node.js diff --git a/docs/ru/docs/advanced/middleware.md b/docs/ru/docs/advanced/middleware.md index 82c86b2317..5ebe010782 100644 --- a/docs/ru/docs/advanced/middleware.md +++ b/docs/ru/docs/advanced/middleware.md @@ -57,13 +57,13 @@ app.add_middleware(UnicornMiddleware, some_config="rainbow") Любой входящий запрос по `http` или `ws` будет перенаправлен на безопасную схему. -{* ../../docs_src/advanced_middleware/tutorial001.py hl[2,6] *} +{* ../../docs_src/advanced_middleware/tutorial001_py39.py hl[2,6] *} ## `TrustedHostMiddleware` { #trustedhostmiddleware } Гарантирует, что во всех входящих запросах корректно установлен `Host`‑заголовок, чтобы защититься от атак на HTTP‑заголовок Host. -{* ../../docs_src/advanced_middleware/tutorial002.py hl[2,6:8] *} +{* ../../docs_src/advanced_middleware/tutorial002_py39.py hl[2,6:8] *} Поддерживаются следующие аргументы: @@ -78,7 +78,7 @@ app.add_middleware(UnicornMiddleware, some_config="rainbow") Это middleware обрабатывает как обычные, так и потоковые ответы. -{* ../../docs_src/advanced_middleware/tutorial003.py hl[2,6] *} +{* ../../docs_src/advanced_middleware/tutorial003_py39.py hl[2,6] *} Поддерживаются следующие аргументы: diff --git a/docs/ru/docs/advanced/openapi-callbacks.md b/docs/ru/docs/advanced/openapi-callbacks.md index faf58370be..de7e283017 100644 --- a/docs/ru/docs/advanced/openapi-callbacks.md +++ b/docs/ru/docs/advanced/openapi-callbacks.md @@ -31,7 +31,7 @@ Эта часть вполне обычна, большая часть кода вам уже знакома: -{* ../../docs_src/openapi_callbacks/tutorial001.py hl[9:13,36:53] *} +{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[7:11,34:51] *} /// tip | Совет @@ -90,7 +90,7 @@ httpx.post(callback_url, json={"description": "Invoice paid", "paid": True}) Сначала создайте новый `APIRouter`, который будет содержать один или несколько обратных вызовов. -{* ../../docs_src/openapi_callbacks/tutorial001.py hl[3,25] *} +{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[1,23] *} ### Создайте *операцию пути* для обратного вызова { #create-the-callback-path-operation } @@ -101,7 +101,7 @@ httpx.post(callback_url, json={"description": "Invoice paid", "paid": True}) * Вероятно, в ней должно быть объявление тела запроса, например `body: InvoiceEvent`. * А также может быть объявление модели ответа, например `response_model=InvoiceEventReceived`. -{* ../../docs_src/openapi_callbacks/tutorial001.py hl[16:18,21:22,28:32] *} +{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[14:16,19:20,26:30] *} Есть 2 основных отличия от обычной *операции пути*: @@ -169,7 +169,7 @@ https://www.external.org/events/invoices/2expen51ve Теперь используйте параметр `callbacks` в *декораторе операции пути вашего API*, чтобы передать атрибут `.routes` (это, по сути, просто `list` маршрутов/*операций пути*) из этого маршрутизатора обратных вызовов: -{* ../../docs_src/openapi_callbacks/tutorial001.py hl[35] *} +{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[33] *} /// tip | Совет diff --git a/docs/ru/docs/advanced/openapi-webhooks.md b/docs/ru/docs/advanced/openapi-webhooks.md index d38cf315f7..3a2b9fff74 100644 --- a/docs/ru/docs/advanced/openapi-webhooks.md +++ b/docs/ru/docs/advanced/openapi-webhooks.md @@ -32,7 +32,7 @@ При создании приложения на **FastAPI** есть атрибут `webhooks`, с помощью которого можно объявлять вебхуки так же, как вы объявляете операции пути (обработчики пути), например с `@app.webhooks.post()`. -{* ../../docs_src/openapi_webhooks/tutorial001.py hl[9:13,36:53] *} +{* ../../docs_src/openapi_webhooks/tutorial001_py39.py hl[9:13,36:53] *} Определенные вами вебхуки попадут в схему **OpenAPI** и в автоматический **интерфейс документации**. diff --git a/docs/ru/docs/advanced/path-operation-advanced-configuration.md b/docs/ru/docs/advanced/path-operation-advanced-configuration.md index fcb3cd47f2..eaf9ad0528 100644 --- a/docs/ru/docs/advanced/path-operation-advanced-configuration.md +++ b/docs/ru/docs/advanced/path-operation-advanced-configuration.md @@ -12,7 +12,7 @@ Нужно убедиться, что он уникален для каждой операции. -{* ../../docs_src/path_operation_advanced_configuration/tutorial001.py hl[6] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial001_py39.py hl[6] *} ### Использование имени функции-обработчика пути как operationId { #using-the-path-operation-function-name-as-the-operationid } @@ -20,7 +20,7 @@ Делать это следует после добавления всех *операций пути*. -{* ../../docs_src/path_operation_advanced_configuration/tutorial002.py hl[2, 12:21, 24] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial002_py39.py hl[2, 12:21, 24] *} /// tip | Совет @@ -40,7 +40,7 @@ Чтобы исключить *операцию пути* из генерируемой схемы OpenAPI (а значит, и из автоматической документации), используйте параметр `include_in_schema` и установите его в `False`: -{* ../../docs_src/path_operation_advanced_configuration/tutorial003.py hl[6] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial003_py39.py hl[6] *} ## Расширенное описание из docstring { #advanced-description-from-docstring } @@ -50,7 +50,7 @@ Эта часть не попадёт в документацию, но другие инструменты (например, Sphinx) смогут использовать остальное. -{* ../../docs_src/path_operation_advanced_configuration/tutorial004.py hl[19:29] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial004_py310.py hl[17:27] *} ## Дополнительные ответы { #additional-responses } @@ -92,7 +92,7 @@ `openapi_extra` может пригодиться, например, чтобы объявить [Расширения OpenAPI](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#specificationExtensions): -{* ../../docs_src/path_operation_advanced_configuration/tutorial005.py hl[6] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial005_py39.py hl[6] *} Если вы откроете автоматическую документацию API, ваше расширение появится внизу страницы конкретной *операции пути*. @@ -139,7 +139,7 @@ Это можно сделать с помощью `openapi_extra`: -{* ../../docs_src/path_operation_advanced_configuration/tutorial006.py hl[19:36, 39:40] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial006_py39.py hl[19:36, 39:40] *} В этом примере мы не объявляли никакую Pydantic-модель. Фактически тело запроса даже не распарсено как JSON, оно читается напрямую как `bytes`, а функция `magic_data_reader()` будет отвечать за его парсинг каким-то способом. @@ -155,13 +155,13 @@ //// tab | Pydantic v2 -{* ../../docs_src/path_operation_advanced_configuration/tutorial007.py hl[17:22, 24] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial007_py39.py hl[15:20, 22] *} //// //// tab | Pydantic v1 -{* ../../docs_src/path_operation_advanced_configuration/tutorial007_pv1.py hl[17:22, 24] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial007_pv1_py39.py hl[15:20, 22] *} //// @@ -179,13 +179,13 @@ //// tab | Pydantic v2 -{* ../../docs_src/path_operation_advanced_configuration/tutorial007.py hl[26:33] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial007_py39.py hl[24:31] *} //// //// tab | Pydantic v1 -{* ../../docs_src/path_operation_advanced_configuration/tutorial007_pv1.py hl[26:33] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial007_pv1_py39.py hl[24:31] *} //// diff --git a/docs/ru/docs/advanced/response-change-status-code.md b/docs/ru/docs/advanced/response-change-status-code.md index e9e1c9470f..85d9050ffc 100644 --- a/docs/ru/docs/advanced/response-change-status-code.md +++ b/docs/ru/docs/advanced/response-change-status-code.md @@ -20,7 +20,7 @@ И затем вы можете установить `status_code` в этом *временном* объекте ответа. -{* ../../docs_src/response_change_status_code/tutorial001.py hl[1,9,12] *} +{* ../../docs_src/response_change_status_code/tutorial001_py39.py hl[1,9,12] *} После этого вы можете вернуть любой объект, который вам нужен, как обычно (`dict`, модель базы данных и т.д.). diff --git a/docs/ru/docs/advanced/response-cookies.md b/docs/ru/docs/advanced/response-cookies.md index 9319aba6e2..2872d6c0ad 100644 --- a/docs/ru/docs/advanced/response-cookies.md +++ b/docs/ru/docs/advanced/response-cookies.md @@ -6,7 +6,7 @@ Затем установить cookies в этом временном объекте ответа. -{* ../../docs_src/response_cookies/tutorial002.py hl[1, 8:9] *} +{* ../../docs_src/response_cookies/tutorial002_py39.py hl[1, 8:9] *} После этого можно вернуть любой объект, как и раньше (например, `dict`, объект модели базы данных и так далее). @@ -24,7 +24,7 @@ Затем установите cookies и верните этот объект: -{* ../../docs_src/response_cookies/tutorial001.py hl[10:12] *} +{* ../../docs_src/response_cookies/tutorial001_py39.py hl[10:12] *} /// tip | Совет diff --git a/docs/ru/docs/advanced/response-directly.md b/docs/ru/docs/advanced/response-directly.md index febd40ed4a..b452810714 100644 --- a/docs/ru/docs/advanced/response-directly.md +++ b/docs/ru/docs/advanced/response-directly.md @@ -34,7 +34,7 @@ В таких случаях вы можете использовать `jsonable_encoder` для преобразования данных перед передачей их в ответ: -{* ../../docs_src/response_directly/tutorial001.py hl[6:7,21:22] *} +{* ../../docs_src/response_directly/tutorial001_py310.py hl[5:6,20:21] *} /// note | Технические детали @@ -54,7 +54,7 @@ Вы можете поместить ваш XML-контент в строку, поместить её в `Response` и вернуть: -{* ../../docs_src/response_directly/tutorial002.py hl[1,18] *} +{* ../../docs_src/response_directly/tutorial002_py39.py hl[1,18] *} ## Примечания { #notes } diff --git a/docs/ru/docs/advanced/response-headers.md b/docs/ru/docs/advanced/response-headers.md index 1c9360b31d..8f24f05b07 100644 --- a/docs/ru/docs/advanced/response-headers.md +++ b/docs/ru/docs/advanced/response-headers.md @@ -6,7 +6,7 @@ А затем вы можете устанавливать HTTP-заголовки в этом *временном* объекте ответа. -{* ../../docs_src/response_headers/tutorial002.py hl[1, 7:8] *} +{* ../../docs_src/response_headers/tutorial002_py39.py hl[1, 7:8] *} После этого вы можете вернуть любой нужный объект, как обычно (например, `dict`, модель из базы данных и т.д.). @@ -22,7 +22,7 @@ Создайте ответ, как описано в [Вернуть Response напрямую](response-directly.md){.internal-link target=_blank}, и передайте заголовки как дополнительный параметр: -{* ../../docs_src/response_headers/tutorial001.py hl[10:12] *} +{* ../../docs_src/response_headers/tutorial001_py39.py hl[10:12] *} /// note | Технические детали diff --git a/docs/ru/docs/advanced/settings.md b/docs/ru/docs/advanced/settings.md index a335548c3c..b96ee44a3a 100644 --- a/docs/ru/docs/advanced/settings.md +++ b/docs/ru/docs/advanced/settings.md @@ -62,7 +62,7 @@ $ pip install "fastapi[all]" //// tab | Pydantic v2 -{* ../../docs_src/settings/tutorial001.py hl[2,5:8,11] *} +{* ../../docs_src/settings/tutorial001_py39.py hl[2,5:8,11] *} //// @@ -74,7 +74,7 @@ $ pip install "fastapi[all]" /// -{* ../../docs_src/settings/tutorial001_pv1.py hl[2,5:8,11] *} +{* ../../docs_src/settings/tutorial001_pv1_py39.py hl[2,5:8,11] *} //// @@ -92,7 +92,7 @@ $ pip install "fastapi[all]" Затем вы можете использовать новый объект `settings` в вашем приложении: -{* ../../docs_src/settings/tutorial001.py hl[18:20] *} +{* ../../docs_src/settings/tutorial001_py39.py hl[18:20] *} ### Запуск сервера { #run-the-server } @@ -126,11 +126,11 @@ $ ADMIN_EMAIL="deadpool@example.com" APP_NAME="ChimichangApp" fastapi run main.p Например, у вас может быть файл `config.py` со следующим содержимым: -{* ../../docs_src/settings/app01/config.py *} +{* ../../docs_src/settings/app01_py39/config.py *} А затем использовать его в файле `main.py`: -{* ../../docs_src/settings/app01/main.py hl[3,11:13] *} +{* ../../docs_src/settings/app01_py39/main.py hl[3,11:13] *} /// tip | Совет @@ -148,7 +148,7 @@ $ ADMIN_EMAIL="deadpool@example.com" APP_NAME="ChimichangApp" fastapi run main.p Продолжая предыдущий пример, ваш файл `config.py` может выглядеть так: -{* ../../docs_src/settings/app02/config.py hl[10] *} +{* ../../docs_src/settings/app02_an_py39/config.py hl[10] *} Обратите внимание, что теперь мы не создаем экземпляр по умолчанию `settings = Settings()`. @@ -174,7 +174,7 @@ $ ADMIN_EMAIL="deadpool@example.com" APP_NAME="ChimichangApp" fastapi run main.p Далее будет очень просто предоставить другой объект настроек во время тестирования, создав переопределение зависимости для `get_settings`: -{* ../../docs_src/settings/app02/test_main.py hl[9:10,13,21] *} +{* ../../docs_src/settings/app02_an_py39/test_main.py hl[9:10,13,21] *} В переопределении зависимости мы задаем новое значение `admin_email` при создании нового объекта `Settings`, а затем возвращаем этот новый объект. @@ -217,7 +217,7 @@ APP_NAME="ChimichangApp" //// tab | Pydantic v2 -{* ../../docs_src/settings/app03_an/config.py hl[9] *} +{* ../../docs_src/settings/app03_an_py39/config.py hl[9] *} /// tip | Совет @@ -229,7 +229,7 @@ APP_NAME="ChimichangApp" //// tab | Pydantic v1 -{* ../../docs_src/settings/app03_an/config_pv1.py hl[9:10] *} +{* ../../docs_src/settings/app03_an_py39/config_pv1.py hl[9:10] *} /// tip | Совет diff --git a/docs/ru/docs/advanced/sub-applications.md b/docs/ru/docs/advanced/sub-applications.md index 3464f17040..fa5a683f45 100644 --- a/docs/ru/docs/advanced/sub-applications.md +++ b/docs/ru/docs/advanced/sub-applications.md @@ -10,7 +10,7 @@ Сначала создайте основное, верхнего уровня, приложение **FastAPI** и его *операции пути*: -{* ../../docs_src/sub_applications/tutorial001.py hl[3, 6:8] *} +{* ../../docs_src/sub_applications/tutorial001_py39.py hl[3, 6:8] *} ### Подприложение { #sub-application } @@ -18,7 +18,7 @@ Это подприложение — обычное стандартное приложение FastAPI, но именно оно будет «смонтировано»: -{* ../../docs_src/sub_applications/tutorial001.py hl[11, 14:16] *} +{* ../../docs_src/sub_applications/tutorial001_py39.py hl[11, 14:16] *} ### Смонтируйте подприложение { #mount-the-sub-application } @@ -26,7 +26,7 @@ В этом случае оно будет смонтировано по пути `/subapi`: -{* ../../docs_src/sub_applications/tutorial001.py hl[11, 19] *} +{* ../../docs_src/sub_applications/tutorial001_py39.py hl[11, 19] *} ### Проверьте автоматическую документацию API { #check-the-automatic-api-docs } diff --git a/docs/ru/docs/advanced/templates.md b/docs/ru/docs/advanced/templates.md index 204e887605..460e2e4660 100644 --- a/docs/ru/docs/advanced/templates.md +++ b/docs/ru/docs/advanced/templates.md @@ -27,7 +27,7 @@ $ pip install jinja2 - Объявите параметр `Request` в *операции пути*, которая будет возвращать шаблон. - Используйте созданный `templates`, чтобы отрендерить и вернуть `TemplateResponse`; передайте имя шаблона, объект `request` и словарь «context» с парами ключ-значение для использования внутри шаблона Jinja2. -{* ../../docs_src/templates/tutorial001.py hl[4,11,15:18] *} +{* ../../docs_src/templates/tutorial001_py39.py hl[4,11,15:18] *} /// note | Примечание diff --git a/docs/ru/docs/advanced/testing-events.md b/docs/ru/docs/advanced/testing-events.md index e0ec774399..82caea845b 100644 --- a/docs/ru/docs/advanced/testing-events.md +++ b/docs/ru/docs/advanced/testing-events.md @@ -2,11 +2,11 @@ Если вам нужно, чтобы `lifespan` выполнялся в ваших тестах, вы можете использовать `TestClient` вместе с оператором `with`: -{* ../../docs_src/app_testing/tutorial004.py hl[9:15,18,27:28,30:32,41:43] *} +{* ../../docs_src/app_testing/tutorial004_py39.py hl[9:15,18,27:28,30:32,41:43] *} Вы можете узнать больше подробностей в статье [Запуск lifespan в тестах на официальном сайте документации Starlette.](https://www.starlette.dev/lifespan/#running-lifespan-in-tests) Для устаревших событий `startup` и `shutdown` вы можете использовать `TestClient` следующим образом: -{* ../../docs_src/app_testing/tutorial003.py hl[9:12,20:24] *} +{* ../../docs_src/app_testing/tutorial003_py39.py hl[9:12,20:24] *} diff --git a/docs/ru/docs/advanced/testing-websockets.md b/docs/ru/docs/advanced/testing-websockets.md index e840a03f28..b6626679e4 100644 --- a/docs/ru/docs/advanced/testing-websockets.md +++ b/docs/ru/docs/advanced/testing-websockets.md @@ -4,7 +4,7 @@ Для этого используйте `TestClient` с менеджером контекста `with`, подключаясь к WebSocket: -{* ../../docs_src/app_testing/tutorial002.py hl[27:31] *} +{* ../../docs_src/app_testing/tutorial002_py39.py hl[27:31] *} /// note | Примечание diff --git a/docs/ru/docs/advanced/using-request-directly.md b/docs/ru/docs/advanced/using-request-directly.md index b922216105..cdf500c0e2 100644 --- a/docs/ru/docs/advanced/using-request-directly.md +++ b/docs/ru/docs/advanced/using-request-directly.md @@ -29,7 +29,7 @@ Для этого нужно обратиться к запросу напрямую. -{* ../../docs_src/using_request_directly/tutorial001.py hl[1,7:8] *} +{* ../../docs_src/using_request_directly/tutorial001_py39.py hl[1,7:8] *} Если объявить параметр *функции-обработчика пути* с типом `Request`, **FastAPI** поймёт, что нужно передать объект `Request` в этот параметр. diff --git a/docs/ru/docs/advanced/websockets.md b/docs/ru/docs/advanced/websockets.md index f26185bea5..fa5e4738eb 100644 --- a/docs/ru/docs/advanced/websockets.md +++ b/docs/ru/docs/advanced/websockets.md @@ -38,13 +38,13 @@ $ pip install websockets Для примера нам нужен наиболее простой способ, который позволит сосредоточиться на серверной части веб‑сокетов и получить рабочий код: -{* ../../docs_src/websockets/tutorial001.py hl[2,6:38,41:43] *} +{* ../../docs_src/websockets/tutorial001_py39.py hl[2,6:38,41:43] *} ## Создание `websocket` { #create-a-websocket } Создайте `websocket` в своем **FastAPI** приложении: -{* ../../docs_src/websockets/tutorial001.py hl[1,46:47] *} +{* ../../docs_src/websockets/tutorial001_py39.py hl[1,46:47] *} /// note | Технические детали @@ -58,7 +58,7 @@ $ pip install websockets Через эндпоинт веб-сокета вы можете получать и отправлять сообщения. -{* ../../docs_src/websockets/tutorial001.py hl[48:52] *} +{* ../../docs_src/websockets/tutorial001_py39.py hl[48:52] *} Вы можете получать и отправлять двоичные, текстовые и JSON данные. diff --git a/docs/ru/docs/advanced/wsgi.md b/docs/ru/docs/advanced/wsgi.md index 1c5bf0a626..64d7c7a289 100644 --- a/docs/ru/docs/advanced/wsgi.md +++ b/docs/ru/docs/advanced/wsgi.md @@ -12,7 +12,7 @@ После этого смонтируйте его на путь. -{* ../../docs_src/wsgi/tutorial001.py hl[2:3,3] *} +{* ../../docs_src/wsgi/tutorial001_py39.py hl[2:3,3] *} ## Проверьте { #check-it } diff --git a/docs/ru/docs/deployment/cloud.md b/docs/ru/docs/deployment/cloud.md index a400d18434..955db2a157 100644 --- a/docs/ru/docs/deployment/cloud.md +++ b/docs/ru/docs/deployment/cloud.md @@ -4,11 +4,19 @@ В большинстве случаев у основных облачных провайдеров есть руководства по развертыванию FastAPI на их платформе. +## FastAPI Cloud { #fastapi-cloud } + +**FastAPI Cloud** создан тем же автором и командой, стоящими за **FastAPI**. + +Он упрощает процесс **создания образа**, **развертывания** и **доступа** к API с минимальными усилиями. + +Он переносит тот же **опыт разработчика** создания приложений с FastAPI на их **развертывание** в облаке. 🎉 + +FastAPI Cloud — основной спонсор и источник финансирования для open source проектов *FastAPI and friends*. ✨ + ## Облачные провайдеры — спонсоры { #cloud-providers-sponsors } -Некоторые облачные провайдеры ✨ [**спонсируют FastAPI**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨ — это обеспечивает непрерывное и здоровое развитие FastAPI и его экосистемы. - -И это показывает их искреннюю приверженность FastAPI и его сообществу (вам): они не только хотят предоставить вам хороший сервис, но и стремятся гарантировать, что у вас будет хороший и стабильный фреймворк — FastAPI. 🙇 +Некоторые другие облачные провайдеры ✨ [**спонсируют FastAPI**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨ тоже. 🙇 Возможно, вы захотите попробовать их сервисы и воспользоваться их руководствами: diff --git a/docs/ru/docs/deployment/fastapicloud.md b/docs/ru/docs/deployment/fastapicloud.md new file mode 100644 index 0000000000..9e7430ecb0 --- /dev/null +++ b/docs/ru/docs/deployment/fastapicloud.md @@ -0,0 +1,65 @@ +# FastAPI Cloud { #fastapi-cloud } + +Вы можете развернуть своё приложение FastAPI в FastAPI Cloud одной командой, присоединяйтесь к списку ожидания, если ещё не сделали этого. 🚀 + +## Вход { #login } + +Убедитесь, что у вас уже есть аккаунт **FastAPI Cloud** (мы пригласили вас из списка ожидания 😉). + +Затем выполните вход: + +
+ +```console +$ fastapi login + +You are logged in to FastAPI Cloud 🚀 +``` + +
+ +## Деплой { #deploy } + +Теперь разверните приложение одной командой: + +
+ +```console +$ fastapi deploy + +Deploying to FastAPI Cloud... + +✅ Deployment successful! + +🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev +``` + +
+ +Вот и всё! Теперь вы можете открыть своё приложение по этому URL. ✨ + +## О FastAPI Cloud { #about-fastapi-cloud } + +**FastAPI Cloud** создан тем же автором и командой, что и **FastAPI**. + +Он упрощает процесс **создания образа**, **развертывания** и **доступа** к API с минимальными усилиями. + +Он переносит тот же **опыт разработчика**, что вы получаете при создании приложений на FastAPI, на их **развертывание** в облаке. 🎉 + +Он также возьмёт на себя большинство вещей, которые требуются при развертывании приложения, например: + +* HTTPS +* Репликация с автоматическим масштабированием на основе запросов +* и т.д. + +FastAPI Cloud — основной спонсор и источник финансирования open source‑проектов «FastAPI и друзья». ✨ + +## Развертывание у других облачных провайдеров { #deploy-to-other-cloud-providers } + +FastAPI — проект с открытым исходным кодом и основан на стандартах. Вы можете развернуть приложения FastAPI у любого облачного провайдера на ваш выбор. + +Следуйте руководствам вашего облачного провайдера, чтобы развернуть приложения FastAPI у них. 🤓 + +## Развертывание на собственном сервере { #deploy-your-own-server } + +Позже в этом руководстве по **развертыванию** я также расскажу все детали — чтобы вы понимали, что происходит, что нужно сделать и как развернуть приложения FastAPI самостоятельно, в том числе на собственных серверах. 🤓 diff --git a/docs/ru/docs/deployment/index.md b/docs/ru/docs/deployment/index.md index c85fa0d529..ffb77641dd 100644 --- a/docs/ru/docs/deployment/index.md +++ b/docs/ru/docs/deployment/index.md @@ -12,10 +12,12 @@ ## Стратегии развёртывания { #deployment-strategies } -В зависимости от вашего конкретного случая, есть несколько способов сделать это. +Есть несколько способов сделать это, в зависимости от вашего конкретного случая и используемых вами инструментов. Вы можете **развернуть сервер** самостоятельно, используя различные инструменты. Например, можно использовать **облачный сервис**, который выполнит часть работы за вас. Также возможны и другие варианты. +Например, мы, команда, стоящая за FastAPI, создали **FastAPI Cloud**, чтобы сделать развёртывание приложений FastAPI в облаке как можно более простым и прямолинейным, с тем же удобством для разработчика, что и при работе с FastAPI. + В этом блоке я покажу вам некоторые из основных концепций, которые вы, вероятно, должны иметь в виду при развертывании приложения **FastAPI** (хотя большинство из них применимо к любому другому типу веб-приложений). В последующих разделах вы узнаете больше деталей и методов, необходимых для этого. ✨ diff --git a/docs/ru/docs/how-to/authentication-error-status-code.md b/docs/ru/docs/how-to/authentication-error-status-code.md new file mode 100644 index 0000000000..5675cecc52 --- /dev/null +++ b/docs/ru/docs/how-to/authentication-error-status-code.md @@ -0,0 +1,17 @@ +# Использование старых статус-кодов ошибок аутентификации 403 { #use-old-403-authentication-error-status-codes } + +До версии FastAPI `0.122.0`, когда встроенные утилиты безопасности возвращали ошибку клиенту после неудачной аутентификации, они использовали HTTP статус-код `403 Forbidden`. + +Начиная с версии FastAPI `0.122.0`, используется более подходящий HTTP статус-код `401 Unauthorized`, и в ответе возвращается имеющий смысл HTTP-заголовок `WWW-Authenticate` в соответствии со спецификациями HTTP, RFC 7235, RFC 9110. + +Но если по какой-то причине ваши клиенты зависят от старого поведения, вы можете вернуть его, переопределив метод `make_not_authenticated_error` в ваших Security-классах. + +Например, вы можете создать подкласс `HTTPBearer`, который будет возвращать ошибку `403 Forbidden` вместо стандартной `401 Unauthorized`: + +{* ../../docs_src/authentication_error_status_code/tutorial001_an_py39.py hl[9:13] *} + +/// tip | Совет + +Обратите внимание, что функция возвращает экземпляр исключения, не вызывает его. Выброс выполняется остальным внутренним кодом. + +/// diff --git a/docs/ru/docs/how-to/conditional-openapi.md b/docs/ru/docs/how-to/conditional-openapi.md index dc987ae26b..d0845b91e2 100644 --- a/docs/ru/docs/how-to/conditional-openapi.md +++ b/docs/ru/docs/how-to/conditional-openapi.md @@ -29,7 +29,7 @@ Например: -{* ../../docs_src/conditional_openapi/tutorial001.py hl[6,11] *} +{* ../../docs_src/conditional_openapi/tutorial001_py39.py hl[6,11] *} Здесь мы объявляем настройку `openapi_url` с тем же значением по умолчанию — `"/openapi.json"`. diff --git a/docs/ru/docs/how-to/configure-swagger-ui.md b/docs/ru/docs/how-to/configure-swagger-ui.md index 4793cc9db3..b3b1c1ba62 100644 --- a/docs/ru/docs/how-to/configure-swagger-ui.md +++ b/docs/ru/docs/how-to/configure-swagger-ui.md @@ -18,7 +18,7 @@ FastAPI преобразует эти настройки в **JSON**, чтобы Но вы можете отключить её, установив `syntaxHighlight` в `False`: -{* ../../docs_src/configure_swagger_ui/tutorial001.py hl[3] *} +{* ../../docs_src/configure_swagger_ui/tutorial001_py39.py hl[3] *} …и после этого Swagger UI больше не будет показывать подсветку синтаксиса: @@ -28,7 +28,7 @@ FastAPI преобразует эти настройки в **JSON**, чтобы Аналогично вы можете задать тему подсветки синтаксиса с ключом "syntaxHighlight.theme" (обратите внимание, что посередине стоит точка): -{* ../../docs_src/configure_swagger_ui/tutorial002.py hl[3] *} +{* ../../docs_src/configure_swagger_ui/tutorial002_py39.py hl[3] *} Эта настройка изменит цветовую тему подсветки синтаксиса: @@ -40,13 +40,13 @@ FastAPI включает некоторые параметры конфигур Это включает следующие настройки по умолчанию: -{* ../../fastapi/openapi/docs.py ln[8:23] hl[17:23] *} +{* ../../fastapi/openapi/docs.py ln[9:24] hl[18:24] *} Вы можете переопределить любую из них, указав другое значение в аргументе `swagger_ui_parameters`. Например, чтобы отключить `deepLinking`, можно передать такие настройки в `swagger_ui_parameters`: -{* ../../docs_src/configure_swagger_ui/tutorial003.py hl[3] *} +{* ../../docs_src/configure_swagger_ui/tutorial003_py39.py hl[3] *} ## Другие параметры Swagger UI { #other-swagger-ui-parameters } diff --git a/docs/ru/docs/how-to/custom-docs-ui-assets.md b/docs/ru/docs/how-to/custom-docs-ui-assets.md index c07a9695b9..f524911e65 100644 --- a/docs/ru/docs/how-to/custom-docs-ui-assets.md +++ b/docs/ru/docs/how-to/custom-docs-ui-assets.md @@ -18,7 +18,7 @@ Чтобы отключить её, установите их URL в значение `None` при создании вашего приложения `FastAPI`: -{* ../../docs_src/custom_docs_ui/tutorial001.py hl[8] *} +{* ../../docs_src/custom_docs_ui/tutorial001_py39.py hl[8] *} ### Подключить пользовательскую документацию { #include-the-custom-docs } @@ -34,7 +34,7 @@ Аналогично и для ReDoc... -{* ../../docs_src/custom_docs_ui/tutorial001.py hl[2:6,11:19,22:24,27:33] *} +{* ../../docs_src/custom_docs_ui/tutorial001_py39.py hl[2:6,11:19,22:24,27:33] *} /// tip | Совет @@ -50,7 +50,7 @@ Swagger UI сделает это за вас «за кулисами», но д Чтобы убедиться, что всё работает, создайте *операцию пути*: -{* ../../docs_src/custom_docs_ui/tutorial001.py hl[36:38] *} +{* ../../docs_src/custom_docs_ui/tutorial001_py39.py hl[36:38] *} ### Тестирование { #test-it } @@ -118,7 +118,7 @@ Swagger UI сделает это за вас «за кулисами», но д * Импортируйте `StaticFiles`. * Смонтируйте экземпляр `StaticFiles()` в определённый путь. -{* ../../docs_src/custom_docs_ui/tutorial002.py hl[7,11] *} +{* ../../docs_src/custom_docs_ui/tutorial002_py39.py hl[7,11] *} ### Протестируйте статические файлы { #test-the-static-files } @@ -144,7 +144,7 @@ Swagger UI сделает это за вас «за кулисами», но д Чтобы отключить её, установите их URL в значение `None` при создании вашего приложения `FastAPI`: -{* ../../docs_src/custom_docs_ui/tutorial002.py hl[9] *} +{* ../../docs_src/custom_docs_ui/tutorial002_py39.py hl[9] *} ### Подключить пользовательскую документацию со статическими файлами { #include-the-custom-docs-for-static-files } @@ -160,7 +160,7 @@ Swagger UI сделает это за вас «за кулисами», но д Аналогично и для ReDoc... -{* ../../docs_src/custom_docs_ui/tutorial002.py hl[2:6,14:22,25:27,30:36] *} +{* ../../docs_src/custom_docs_ui/tutorial002_py39.py hl[2:6,14:22,25:27,30:36] *} /// tip | Совет @@ -176,7 +176,7 @@ Swagger UI сделает это за вас «за кулисами», но д Чтобы убедиться, что всё работает, создайте *операцию пути*: -{* ../../docs_src/custom_docs_ui/tutorial002.py hl[39:41] *} +{* ../../docs_src/custom_docs_ui/tutorial002_py39.py hl[39:41] *} ### Тестирование UI со статическими файлами { #test-static-files-ui } diff --git a/docs/ru/docs/how-to/custom-request-and-route.md b/docs/ru/docs/how-to/custom-request-and-route.md index 1b8d7f7ed3..feef9670ad 100644 --- a/docs/ru/docs/how-to/custom-request-and-route.md +++ b/docs/ru/docs/how-to/custom-request-and-route.md @@ -42,7 +42,7 @@ Таким образом, один и тот же класс маршрута сможет обрабатывать как gzip-сжатые, так и несжатые запросы. -{* ../../docs_src/custom_request_and_route/tutorial001.py hl[8:15] *} +{* ../../docs_src/custom_request_and_route/tutorial001_an_py310.py hl[9:16] *} ### Создать пользовательский класс `GzipRoute` { #create-a-custom-gziproute-class } @@ -54,7 +54,7 @@ Здесь мы используем её, чтобы создать `GzipRequest` из исходного HTTP-запроса. -{* ../../docs_src/custom_request_and_route/tutorial001.py hl[18:26] *} +{* ../../docs_src/custom_request_and_route/tutorial001_an_py310.py hl[19:27] *} /// note | Технические детали @@ -92,18 +92,18 @@ Нужно лишь обработать запрос внутри блока `try`/`except`: -{* ../../docs_src/custom_request_and_route/tutorial002.py hl[13,15] *} +{* ../../docs_src/custom_request_and_route/tutorial002_an_py310.py hl[14,16] *} Если произойдёт исключение, экземпляр `Request` всё ещё будет в области видимости, поэтому мы сможем прочитать тело запроса и использовать его при обработке ошибки: -{* ../../docs_src/custom_request_and_route/tutorial002.py hl[16:18] *} +{* ../../docs_src/custom_request_and_route/tutorial002_an_py310.py hl[17:19] *} ## Пользовательский класс `APIRoute` в роутере { #custom-apiroute-class-in-a-router } Вы также можете задать параметр `route_class` у `APIRouter`: -{* ../../docs_src/custom_request_and_route/tutorial003.py hl[26] *} +{* ../../docs_src/custom_request_and_route/tutorial003_py310.py hl[26] *} В этом примере *операции пути*, объявленные в `router`, будут использовать пользовательский класс `TimedRoute` и получат дополнительный HTTP-заголовок `X-Response-Time` в ответе с временем, затраченным на формирование ответа: -{* ../../docs_src/custom_request_and_route/tutorial003.py hl[13:20] *} +{* ../../docs_src/custom_request_and_route/tutorial003_py310.py hl[13:20] *} diff --git a/docs/ru/docs/how-to/extending-openapi.md b/docs/ru/docs/how-to/extending-openapi.md index 2897fb89ba..1d69cbdb3a 100644 --- a/docs/ru/docs/how-to/extending-openapi.md +++ b/docs/ru/docs/how-to/extending-openapi.md @@ -43,19 +43,19 @@ Сначала напишите приложение **FastAPI** как обычно: -{* ../../docs_src/extending_openapi/tutorial001.py hl[1,4,7:9] *} +{* ../../docs_src/extending_openapi/tutorial001_py39.py hl[1,4,7:9] *} ### Сгенерируйте схему OpenAPI { #generate-the-openapi-schema } Затем используйте ту же вспомогательную функцию для генерации схемы OpenAPI внутри функции `custom_openapi()`: -{* ../../docs_src/extending_openapi/tutorial001.py hl[2,15:21] *} +{* ../../docs_src/extending_openapi/tutorial001_py39.py hl[2,15:21] *} ### Измените схему OpenAPI { #modify-the-openapi-schema } Теперь можно добавить расширение ReDoc, добавив кастомный `x-logo` в «объект» `info` в схеме OpenAPI: -{* ../../docs_src/extending_openapi/tutorial001.py hl[22:24] *} +{* ../../docs_src/extending_openapi/tutorial001_py39.py hl[22:24] *} ### Кэшируйте схему OpenAPI { #cache-the-openapi-schema } @@ -65,13 +65,13 @@ Она будет создана один раз, а затем тот же кэшированный вариант будет использоваться для последующих запросов. -{* ../../docs_src/extending_openapi/tutorial001.py hl[13:14,25:26] *} +{* ../../docs_src/extending_openapi/tutorial001_py39.py hl[13:14,25:26] *} ### Переопределите метод { #override-the-method } Теперь вы можете заменить метод `.openapi()` на вашу новую функцию. -{* ../../docs_src/extending_openapi/tutorial001.py hl[29] *} +{* ../../docs_src/extending_openapi/tutorial001_py39.py hl[29] *} ### Проверьте { #check-it } diff --git a/docs/ru/docs/how-to/graphql.md b/docs/ru/docs/how-to/graphql.md index 9ed6d95ca1..50c321e7dd 100644 --- a/docs/ru/docs/how-to/graphql.md +++ b/docs/ru/docs/how-to/graphql.md @@ -35,7 +35,7 @@ Вот небольшой пример того, как можно интегрировать Strawberry с FastAPI: -{* ../../docs_src/graphql/tutorial001.py hl[3,22,25] *} +{* ../../docs_src/graphql_/tutorial001_py39.py hl[3,22,25] *} Подробнее о Strawberry можно узнать в документации Strawberry. diff --git a/docs/ru/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md b/docs/ru/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md new file mode 100644 index 0000000000..95481bc668 --- /dev/null +++ b/docs/ru/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md @@ -0,0 +1,133 @@ +# Миграция с Pydantic v1 на Pydantic v2 { #migrate-from-pydantic-v1-to-pydantic-v2 } + +Если у вас старое приложение FastAPI, возможно, вы используете Pydantic версии 1. + +FastAPI поддерживает и Pydantic v1, и v2 начиная с версии 0.100.0. + +Если у вас был установлен Pydantic v2, использовался он. Если вместо этого был установлен Pydantic v1 — использовался он. + +Сейчас Pydantic v1 объявлен устаревшим, и поддержка его будет удалена в следующих версиях FastAPI, поэтому вам следует **перейти на Pydantic v2**. Так вы получите последние возможности, улучшения и исправления. + +/// warning | Предупреждение + +Кроме того, команда Pydantic прекратила поддержку Pydantic v1 для последних версий Python, начиная с **Python 3.14**. + +Если вы хотите использовать последние возможности Python, вам нужно убедиться, что вы используете Pydantic v2. + +/// + +Если у вас старое приложение FastAPI с Pydantic v1, здесь я покажу, как мигрировать на Pydantic v2, и **новые возможности в FastAPI 0.119.0**, которые помогут выполнить постепенную миграцию. + +## Официальное руководство { #official-guide } + +У Pydantic есть официальное руководство по миграции с v1 на v2. + +Там также описано, что изменилось, как валидации стали более корректными и строгими, возможные нюансы и т.д. + +Прочитайте его, чтобы лучше понять, что изменилось. + +## Тесты { #tests } + +Убедитесь, что у вас есть [тесты](../tutorial/testing.md){.internal-link target=_blank} для вашего приложения и что вы запускаете их в системе непрерывной интеграции (CI). + +Так вы сможете выполнить обновление и убедиться, что всё работает как ожидается. + +## `bump-pydantic` { #bump-pydantic } + +Во многих случаях, когда вы используете обычные Pydantic‑модели без пользовательских настроек, вы сможете автоматизировать большую часть процесса миграции с Pydantic v1 на Pydantic v2. + +Вы можете использовать `bump-pydantic` от той же команды Pydantic. + +Этот инструмент поможет автоматически внести большую часть необходимых изменений в код. + +После этого запустите тесты и проверьте, что всё работает. Если да — на этом всё. 😎 + +## Pydantic v1 в v2 { #pydantic-v1-in-v2 } + +Pydantic v2 включает всё из Pydantic v1 как подмодуль `pydantic.v1`. + +Это означает, что вы можете установить последнюю версию Pydantic v2 и импортировать и использовать старые компоненты Pydantic v1 из этого подмодуля так, как если бы у вас был установлен старый Pydantic v1. + +{* ../../docs_src/pydantic_v1_in_v2/tutorial001_an_py310.py hl[1,4] *} + +### Поддержка FastAPI для Pydantic v1 внутри v2 { #fastapi-support-for-pydantic-v1-in-v2 } + +Начиная с FastAPI 0.119.0, есть также частичная поддержка Pydantic v1 в составе Pydantic v2, чтобы упростить миграцию на v2. + +Таким образом, вы можете обновить Pydantic до последней версии 2 и сменить импорты на подмодуль `pydantic.v1` — во многих случаях всё просто заработает. + +{* ../../docs_src/pydantic_v1_in_v2/tutorial002_an_py310.py hl[2,5,15] *} + +/// warning | Предупреждение + +Имейте в виду, что так как команда Pydantic больше не поддерживает Pydantic v1 в последних версиях Python, начиная с Python 3.14, использование `pydantic.v1` также не поддерживается в Python 3.14 и выше. + +/// + +### Pydantic v1 и v2 в одном приложении { #pydantic-v1-and-v2-on-the-same-app } + +В Pydantic **не поддерживается** ситуация, когда в одной модели Pydantic v2 используются поля, определённые как модели Pydantic v1, и наоборот. + +```mermaid +graph TB + subgraph "❌ Not Supported" + direction TB + subgraph V2["Pydantic v2 Model"] + V1Field["Pydantic v1 Model"] + end + subgraph V1["Pydantic v1 Model"] + V2Field["Pydantic v2 Model"] + end + end + + style V2 fill:#f9fff3 + style V1 fill:#fff6f0 + style V1Field fill:#fff6f0 + style V2Field fill:#f9fff3 +``` + +…но в одном и том же приложении вы можете иметь отдельные модели на Pydantic v1 и v2. + +```mermaid +graph TB + subgraph "✅ Supported" + direction TB + subgraph V2["Pydantic v2 Model"] + V2Field["Pydantic v2 Model"] + end + subgraph V1["Pydantic v1 Model"] + V1Field["Pydantic v1 Model"] + end + end + + style V2 fill:#f9fff3 + style V1 fill:#fff6f0 + style V1Field fill:#fff6f0 + style V2Field fill:#f9fff3 +``` + +В некоторых случаях можно использовать и модели Pydantic v1, и v2 в одной и той же операции пути (обработчике пути) вашего приложения FastAPI: + +{* ../../docs_src/pydantic_v1_in_v2/tutorial003_an_py310.py hl[2:3,6,12,21:22] *} + +В примере выше модель входных данных — это модель Pydantic v1, а модель выходных данных (указанная в `response_model=ItemV2`) — это модель Pydantic v2. + +### Параметры Pydantic v1 { #pydantic-v1-parameters } + +Если вам нужно использовать некоторые специфичные для FastAPI инструменты для параметров, такие как `Body`, `Query`, `Form` и т.п., с моделями Pydantic v1, вы можете импортировать их из `fastapi.temp_pydantic_v1_params`, пока завершаете миграцию на Pydantic v2: + +{* ../../docs_src/pydantic_v1_in_v2/tutorial004_an_py310.py hl[4,18] *} + +### Мигрируйте по шагам { #migrate-in-steps } + +/// tip | Совет + +Сначала попробуйте `bump-pydantic`. Если тесты проходят и всё работает, вы справились одной командой. ✨ + +/// + +Если `bump-pydantic` не подходит для вашего случая, вы можете использовать поддержку одновременной работы моделей Pydantic v1 и v2 в одном приложении, чтобы мигрировать на Pydantic v2 постепенно. + +Сначала обновите Pydantic до последней 2-й версии и измените импорты так, чтобы все ваши модели использовали `pydantic.v1`. + +Затем начните мигрировать ваши модели с Pydantic v1 на v2 группами, поэтапно. 🚶 diff --git a/docs/ru/docs/index.md b/docs/ru/docs/index.md index 75cd63223d..b562cbe5bc 100644 --- a/docs/ru/docs/index.md +++ b/docs/ru/docs/index.md @@ -52,14 +52,20 @@ FastAPI — это современный, быстрый (высокопрои -{% if sponsors %} +### Ключевой-спонсор { #keystone-sponsor } + +{% for sponsor in sponsors.keystone -%} + +{% endfor -%} + +### Золотые и серебряные спонсоры { #gold-and-silver-sponsors } + {% for sponsor in sponsors.gold -%} {% endfor -%} {%- for sponsor in sponsors.silver -%} {% endfor %} -{% endif %} @@ -444,6 +450,58 @@ item: Item * **сессии с использованием cookie** * ...и многое другое. +### Разверните приложение (опционально) { #deploy-your-app-optional } + +При желании вы можете развернуть своё приложение FastAPI в FastAPI Cloud, присоединяйтесь к списку ожидания, если ещё не сделали этого. 🚀 + +Если у вас уже есть аккаунт **FastAPI Cloud** (мы пригласили вас из списка ожидания 😉), вы можете развернуть ваше приложение одной командой. + +Перед развертыванием убедитесь, что вы вошли в систему: + +
+ +```console +$ fastapi login + +You are logged in to FastAPI Cloud 🚀 +``` + +
+ +Затем разверните приложение: + +
+ +```console +$ fastapi deploy + +Deploying to FastAPI Cloud... + +✅ Deployment successful! + +🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev +``` + +
+ +Вот и всё! Теперь вы можете открыть ваше приложение по этой ссылке. ✨ + +#### О FastAPI Cloud { #about-fastapi-cloud } + +**FastAPI Cloud** создан тем же автором и командой, что и **FastAPI**. + +Он упрощает процесс **создания образа**, **развертывания** и **доступа** к API при минимальных усилиях. + +Он переносит тот же **опыт разработчика**, что и при создании приложений на FastAPI, на их **развертывание** в облаке. 🎉 + +FastAPI Cloud — основной спонсор и источник финансирования для проектов с открытым исходным кодом из экосистемы *FastAPI and friends*. ✨ + +#### Развертывание у других облачных провайдеров { #deploy-to-other-cloud-providers } + +FastAPI — это open source и стандартизированный фреймворк. Вы можете развернуть приложения FastAPI у любого облачного провайдера на ваш выбор. + +Следуйте руководствам вашего облачного провайдера по развертыванию приложений FastAPI. 🤓 + ## Производительность { #performance } Независимые бенчмарки TechEmpower показывают приложения **FastAPI**, работающие под управлением Uvicorn, как один из самых быстрых доступных фреймворков Python, уступающий только самим Starlette и Uvicorn (используются внутри FastAPI). (*) diff --git a/docs/ru/docs/project-generation.md b/docs/ru/docs/project-generation.md index 8c5681115b..dbedf76fe3 100644 --- a/docs/ru/docs/project-generation.md +++ b/docs/ru/docs/project-generation.md @@ -13,8 +13,8 @@ - 🔍 [Pydantic](https://docs.pydantic.dev), используется FastAPI, для валидации данных и управления настройками. - 💾 [PostgreSQL](https://www.postgresql.org) в качестве SQL‑базы данных. - 🚀 [React](https://react.dev) для фронтенда. - - 💃 Используются TypeScript, хуки, [Vite](https://vitejs.dev) и другие части современного фронтенд‑стека. - - 🎨 [Chakra UI](https://chakra-ui.com) для компонентов фронтенда. + - 💃 Используются TypeScript, хуки, Vite и другие части современного фронтенд‑стека. + - 🎨 [Tailwind CSS](https://tailwindcss.com) и [shadcn/ui](https://ui.shadcn.com) для компонентов фронтенда. - 🤖 Автоматически сгенерированный фронтенд‑клиент. - 🧪 [Playwright](https://playwright.dev) для End‑to‑End тестирования. - 🦇 Поддержка тёмной темы. diff --git a/docs/ru/docs/python-types.md b/docs/ru/docs/python-types.md index 84a901f547..ae4a1e2b79 100644 --- a/docs/ru/docs/python-types.md +++ b/docs/ru/docs/python-types.md @@ -22,7 +22,7 @@ Python поддерживает необязательные «подсказк Давайте начнем с простого примера: -{* ../../docs_src/python_types/tutorial001.py *} +{* ../../docs_src/python_types/tutorial001_py39.py *} Вызов этой программы выводит: @@ -36,7 +36,7 @@ John Doe * Преобразует первую букву каждого значения в верхний регистр с помощью `title()`. * Соединяет их пробелом посередине. -{* ../../docs_src/python_types/tutorial001.py hl[2] *} +{* ../../docs_src/python_types/tutorial001_py39.py hl[2] *} ### Отредактируем пример { #edit-it } @@ -78,7 +78,7 @@ John Doe Это и есть «подсказки типов»: -{* ../../docs_src/python_types/tutorial002.py hl[1] *} +{* ../../docs_src/python_types/tutorial002_py39.py hl[1] *} Это не то же самое, что объявление значений по умолчанию, как, например: @@ -106,7 +106,7 @@ John Doe Посмотрите на эту функцию — у неё уже есть подсказки типов: -{* ../../docs_src/python_types/tutorial003.py hl[1] *} +{* ../../docs_src/python_types/tutorial003_py39.py hl[1] *} Так как редактор кода знает типы переменных, вы получаете не только автозавершение, но и проверки ошибок: @@ -114,7 +114,7 @@ John Doe Теперь вы знаете, что нужно исправить — преобразовать `age` в строку с помощью `str(age)`: -{* ../../docs_src/python_types/tutorial004.py hl[2] *} +{* ../../docs_src/python_types/tutorial004_py39.py hl[2] *} ## Объявление типов { #declaring-types } @@ -133,7 +133,7 @@ John Doe * `bool` * `bytes` -{* ../../docs_src/python_types/tutorial005.py hl[1] *} +{* ../../docs_src/python_types/tutorial005_py39.py hl[1] *} ### Generic-типы с параметрами типов { #generic-types-with-type-parameters } @@ -161,56 +161,24 @@ John Doe Например, давайте определим переменную как `list` из `str`. -//// tab | Python 3.9+ - Объявите переменную с тем же синтаксисом двоеточия (`:`). В качестве типа укажите `list`. Так как список — это тип, содержащий внутренние типы, укажите их в квадратных скобках: -```Python hl_lines="1" -{!> ../../docs_src/python_types/tutorial006_py39.py!} -``` - -//// - -//// tab | Python 3.8+ - -Из `typing` импортируйте `List` (с заглавной `L`): - -```Python hl_lines="1" -{!> ../../docs_src/python_types/tutorial006.py!} -``` - -Объявите переменную с тем же синтаксисом двоеточия (`:`). - -В качестве типа используйте `List`, который вы импортировали из `typing`. - -Так как список — это тип, содержащий внутренние типы, укажите их в квадратных скобках: - -```Python hl_lines="4" -{!> ../../docs_src/python_types/tutorial006.py!} -``` - -//// +{* ../../docs_src/python_types/tutorial006_py39.py hl[1] *} /// info | Информация Эти внутренние типы в квадратных скобках называются «параметрами типов». -В данном случае `str` — это параметр типа, передаваемый в `List` (или `list` в Python 3.9 и выше). +В данном случае `str` — это параметр типа, передаваемый в `list`. /// Это означает: «переменная `items` — это `list`, и каждый элемент этого списка — `str`». -/// tip | Совет - -Если вы используете Python 3.9 или выше, вам не нужно импортировать `List` из `typing`, можно использовать обычный встроенный тип `list`. - -/// - Таким образом, ваш редактор кода сможет помогать даже при обработке элементов списка: @@ -225,21 +193,7 @@ John Doe Аналогично вы бы объявили `tuple` и `set`: -//// tab | Python 3.9+ - -```Python hl_lines="1" -{!> ../../docs_src/python_types/tutorial007_py39.py!} -``` - -//// - -//// tab | Python 3.8+ - -```Python hl_lines="1 4" -{!> ../../docs_src/python_types/tutorial007.py!} -``` - -//// +{* ../../docs_src/python_types/tutorial007_py39.py hl[1] *} Это означает: @@ -254,21 +208,7 @@ John Doe Второй параметр типа — для значений `dict`: -//// tab | Python 3.9+ - -```Python hl_lines="1" -{!> ../../docs_src/python_types/tutorial008_py39.py!} -``` - -//// - -//// tab | Python 3.8+ - -```Python hl_lines="1 4" -{!> ../../docs_src/python_types/tutorial008.py!} -``` - -//// +{* ../../docs_src/python_types/tutorial008_py39.py hl[1] *} Это означает: @@ -292,10 +232,10 @@ John Doe //// -//// tab | Python 3.8+ +//// tab | Python 3.9+ ```Python hl_lines="1 4" -{!> ../../docs_src/python_types/tutorial008b.py!} +{!> ../../docs_src/python_types/tutorial008b_py39.py!} ``` //// @@ -309,7 +249,7 @@ John Doe В Python 3.6 и выше (включая Python 3.10) это можно объявить, импортировав и используя `Optional` из модуля `typing`. ```Python hl_lines="1 4" -{!../../docs_src/python_types/tutorial009.py!} +{!../../docs_src/python_types/tutorial009_py39.py!} ``` Использование `Optional[str]` вместо просто `str` позволит редактору кода помочь вам обнаружить ошибки, когда вы предполагаете, что значение всегда `str`, хотя на самом деле оно может быть и `None`. @@ -326,18 +266,18 @@ John Doe //// -//// tab | Python 3.8+ +//// tab | Python 3.9+ ```Python hl_lines="1 4" -{!> ../../docs_src/python_types/tutorial009.py!} +{!> ../../docs_src/python_types/tutorial009_py39.py!} ``` //// -//// tab | Python 3.8+ альтернативный вариант +//// tab | Python 3.9+ альтернативный вариант ```Python hl_lines="1 4" -{!> ../../docs_src/python_types/tutorial009b.py!} +{!> ../../docs_src/python_types/tutorial009b_py39.py!} ``` //// @@ -357,7 +297,7 @@ John Doe В качестве примера возьмём эту функцию: -{* ../../docs_src/python_types/tutorial009c.py hl[1,4] *} +{* ../../docs_src/python_types/tutorial009c_py39.py hl[1,4] *} Параметр `name` определён как `Optional[str]`, но он **не необязательный** — вы не можете вызвать функцию без этого параметра: @@ -390,10 +330,10 @@ say_hi(name=None) # Это работает, None допустим 🎉 * `set` * `dict` -И, как и в Python 3.8, из модуля `typing`: +И, как и в предыдущих версиях Python, из модуля `typing`: * `Union` -* `Optional` (так же, как в Python 3.8) +* `Optional` * ...и другие. В Python 3.10, как альтернативу generics `Union` и `Optional`, можно использовать вертикальную черту (`|`) для объявления объединений типов — это гораздо лучше и проще. @@ -409,7 +349,7 @@ say_hi(name=None) # Это работает, None допустим 🎉 * `set` * `dict` -И, как и в Python 3.8, из модуля `typing`: +И generics из модуля `typing`: * `Union` * `Optional` @@ -417,29 +357,17 @@ say_hi(name=None) # Это работает, None допустим 🎉 //// -//// tab | Python 3.8+ - -* `List` -* `Tuple` -* `Set` -* `Dict` -* `Union` -* `Optional` -* ...и другие. - -//// - ### Классы как типы { #classes-as-types } Вы также можете объявлять класс как тип переменной. Допустим, у вас есть класс `Person` с именем: -{* ../../docs_src/python_types/tutorial010.py hl[1:3] *} +{* ../../docs_src/python_types/tutorial010_py39.py hl[1:3] *} Тогда вы можете объявить переменную типа `Person`: -{* ../../docs_src/python_types/tutorial010.py hl[6] *} +{* ../../docs_src/python_types/tutorial010_py39.py hl[6] *} И снова вы получите полную поддержку редактора кода: @@ -463,29 +391,7 @@ say_hi(name=None) # Это работает, None допустим 🎉 Пример из официальной документации Pydantic: -//// tab | Python 3.10+ - -```Python -{!> ../../docs_src/python_types/tutorial011_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!> ../../docs_src/python_types/tutorial011_py39.py!} -``` - -//// - -//// tab | Python 3.8+ - -```Python -{!> ../../docs_src/python_types/tutorial011.py!} -``` - -//// +{* ../../docs_src/python_types/tutorial011_py310.py *} /// info | Информация @@ -507,27 +413,9 @@ say_hi(name=None) # Это работает, None допустим 🎉 В Python также есть возможность добавлять **дополнительные метаданные** к подсказкам типов с помощью `Annotated`. -//// tab | Python 3.9+ +Начиная с Python 3.9, `Annotated` входит в стандартную библиотеку, поэтому вы можете импортировать его из `typing`. -В Python 3.9 `Annotated` входит в стандартную библиотеку, поэтому вы можете импортировать его из `typing`. - -```Python hl_lines="1 4" -{!> ../../docs_src/python_types/tutorial013_py39.py!} -``` - -//// - -//// tab | Python 3.8+ - -В версиях ниже Python 3.9 импортируйте `Annotated` из `typing_extensions`. - -Он уже будет установлен вместе с **FastAPI**. - -```Python hl_lines="1 4" -{!> ../../docs_src/python_types/tutorial013.py!} -``` - -//// +{* ../../docs_src/python_types/tutorial013_py39.py hl[1,4] *} Сам Python ничего не делает с `Annotated`. А для редакторов кода и других инструментов тип по-прежнему `str`. @@ -558,7 +446,7 @@ say_hi(name=None) # Это работает, None допустим 🎉 ...и **FastAPI** использует эти же объявления для: -* **Определения требований**: из path-параметров, query-параметров, HTTP-заголовков, тел запросов, зависимостей и т.д. +* **Определения требований**: из path-параметров пути запроса, query-параметров, HTTP-заголовков, тел запросов, зависимостей и т.д. * **Преобразования данных**: из HTTP-запроса к требуемому типу. * **Валидации данных**: приходящих с каждого HTTP-запроса: * Генерации **автоматических ошибок**, возвращаемых клиенту, когда данные некорректны. diff --git a/docs/ru/docs/resources/index.md b/docs/ru/docs/resources/index.md index 54be4e5fd3..faf80f7f49 100644 --- a/docs/ru/docs/resources/index.md +++ b/docs/ru/docs/resources/index.md @@ -1,3 +1,3 @@ # Ресурсы { #resources } -Дополнительные ресурсы, внешние ссылки, статьи и многое другое. ✈️ +Дополнительные ресурсы, внешние ссылки и многое другое. ✈️ diff --git a/docs/ru/docs/tutorial/background-tasks.md b/docs/ru/docs/tutorial/background-tasks.md index 1ed8522d69..8d7b7442f9 100644 --- a/docs/ru/docs/tutorial/background-tasks.md +++ b/docs/ru/docs/tutorial/background-tasks.md @@ -15,7 +15,7 @@ Сначала импортируйте `BackgroundTasks` и объявите параметр в вашей функции‑обработчике пути с типом `BackgroundTasks`: -{* ../../docs_src/background_tasks/tutorial001.py hl[1,13] *} +{* ../../docs_src/background_tasks/tutorial001_py39.py hl[1,13] *} **FastAPI** создаст объект типа `BackgroundTasks` для вас и передаст его через этот параметр. @@ -31,13 +31,13 @@ Так как операция записи не использует `async` и `await`, мы определим функцию как обычную `def`: -{* ../../docs_src/background_tasks/tutorial001.py hl[6:9] *} +{* ../../docs_src/background_tasks/tutorial001_py39.py hl[6:9] *} ## Добавление фоновой задачи { #add-the-background-task } Внутри вашей функции‑обработчика пути передайте функцию задачи объекту фоновых задач методом `.add_task()`: -{* ../../docs_src/background_tasks/tutorial001.py hl[14] *} +{* ../../docs_src/background_tasks/tutorial001_py39.py hl[14] *} `.add_task()` принимает следующие аргументы: diff --git a/docs/ru/docs/tutorial/bigger-applications.md b/docs/ru/docs/tutorial/bigger-applications.md index b832383cc9..5e5d6ada94 100644 --- a/docs/ru/docs/tutorial/bigger-applications.md +++ b/docs/ru/docs/tutorial/bigger-applications.md @@ -85,17 +85,13 @@ from app.routers import items Точно также, как и в случае с классом `FastAPI`, вам нужно импортировать и создать объект класса `APIRouter`. -```Python hl_lines="1 3" title="app/routers/users.py" -{!../../docs_src/bigger_applications/app/routers/users.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/routers/users.py hl[1,3] title["app/routers/users.py"] *} ### Создание *эндпоинтов* с помощью `APIRouter` { #path-operations-with-apirouter } В дальнейшем используйте `APIRouter` для объявления *эндпоинтов*, точно также, как вы используете класс `FastAPI`: -```Python hl_lines="6 11 16" title="app/routers/users.py" -{!../../docs_src/bigger_applications/app/routers/users.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/routers/users.py hl[6,11,16] title["app/routers/users.py"] *} Вы можете думать об `APIRouter` как об "уменьшенной версии" класса FastAPI`. @@ -119,35 +115,7 @@ from app.routers import items Теперь мы воспользуемся простой зависимостью, чтобы прочитать кастомизированный `X-Token` из заголовка: -//// tab | Python 3.9+ - -```Python hl_lines="3 6-8" title="app/dependencies.py" -{!> ../../docs_src/bigger_applications/app_an_py39/dependencies.py!} -``` - -//// - -//// tab | Python 3.8+ - -```Python hl_lines="1 5-7" title="app/dependencies.py" -{!> ../../docs_src/bigger_applications/app_an/dependencies.py!} -``` - -//// - -//// tab | Python 3.8+ non-Annotated - -/// tip | Подсказка - -Мы рекомендуем использовать версию `Annotated`, когда это возможно. - -/// - -```Python hl_lines="1 4-6" title="app/dependencies.py" -{!> ../../docs_src/bigger_applications/app/dependencies.py!} -``` - -//// +{* ../../docs_src/bigger_applications/app_an_py39/dependencies.py hl[3,6:8] title["app/dependencies.py"] *} /// tip | Подсказка @@ -180,9 +148,7 @@ from app.routers import items Таким образом, вместо того чтобы добавлять все эти свойства в функцию каждого отдельного *эндпоинта*, мы добавим их в `APIRouter`. -```Python hl_lines="5-10 16 21" title="app/routers/items.py" -{!../../docs_src/bigger_applications/app/routers/items.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/routers/items.py hl[5:10,16,21] title["app/routers/items.py"] *} Так как каждый *эндпоинт* начинается с символа `/`: @@ -241,9 +207,7 @@ async def read_item(item_id: str): Мы используем операцию относительного импорта `..` для импорта зависимости: -```Python hl_lines="3" title="app/routers/items.py" -{!../../docs_src/bigger_applications/app/routers/items.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/routers/items.py hl[3] title["app/routers/items.py"] *} #### Как работает относительный импорт? { #how-relative-imports-work } @@ -313,9 +277,7 @@ from ...dependencies import get_token_header Но помимо этого мы можем добавить новые теги для каждого отдельного *эндпоинта*, а также некоторые дополнительные ответы (`responses`), характерные для данного *эндпоинта*: -```Python hl_lines="30-31" title="app/routers/items.py" -{!../../docs_src/bigger_applications/app/routers/items.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/routers/items.py hl[30:31] title["app/routers/items.py"] *} /// tip | Подсказка @@ -341,17 +303,13 @@ from ...dependencies import get_token_header Мы даже можем объявить [глобальные зависимости](dependencies/global-dependencies.md){.internal-link target=_blank}, которые будут объединены с зависимостями для каждого отдельного маршрутизатора: -```Python hl_lines="1 3 7" title="app/main.py" -{!../../docs_src/bigger_applications/app/main.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[1,3,7] title["app/main.py"] *} ### Импорт `APIRouter` { #import-the-apirouter } Теперь мы импортируем другие суб-модули, содержащие `APIRouter`: -```Python hl_lines="4-5" title="app/main.py" -{!../../docs_src/bigger_applications/app/main.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[4:5] title["app/main.py"] *} Так как файлы `app/routers/users.py` и `app/routers/items.py` являются суб-модулями одного и того же Python-пакета `app`, то мы сможем их импортировать, воспользовавшись операцией относительного импорта `.`. @@ -414,17 +372,13 @@ from .routers.users import router Поэтому, для того чтобы использовать обе эти переменные в одном файле, мы импортировали соответствующие суб-модули: -```Python hl_lines="5" title="app/main.py" -{!../../docs_src/bigger_applications/app/main.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[5] title["app/main.py"] *} ### Подключение маршрутизаторов (`APIRouter`) для `users` и для `items` { #include-the-apirouters-for-users-and-items } Давайте подключим маршрутизаторы (`router`) из суб-модулей `users` и `items`: -```Python hl_lines="10-11" title="app/main.py" -{!../../docs_src/bigger_applications/app/main.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[10:11] title["app/main.py"] *} /// info | Примечание @@ -465,17 +419,13 @@ from .routers.users import router В данном примере это сделать очень просто. Но давайте предположим, что поскольку файл используется для нескольких проектов, то мы не можем модифицировать его, добавляя префиксы (`prefix`), зависимости (`dependencies`), теги (`tags`), и т.д. непосредственно в `APIRouter`: -```Python hl_lines="3" title="app/internal/admin.py" -{!../../docs_src/bigger_applications/app/internal/admin.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/internal/admin.py hl[3] title["app/internal/admin.py"] *} Но, несмотря на это, мы хотим использовать кастомный префикс (`prefix`) для подключенного маршрутизатора (`APIRouter`), в результате чего, каждая *операция пути* будет начинаться с `/admin`. Также мы хотим защитить наш маршрутизатор с помощью зависимостей, созданных для нашего проекта. И ещё мы хотим включить теги (`tags`) и ответы (`responses`). Мы можем применить все вышеперечисленные настройки, не изменяя начальный `APIRouter`. Нам всего лишь нужно передать нужные параметры в `app.include_router()`. -```Python hl_lines="14-17" title="app/main.py" -{!../../docs_src/bigger_applications/app/main.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[14:17] title["app/main.py"] *} Таким образом, оригинальный `APIRouter` не будет модифицирован, и мы сможем использовать файл `app/internal/admin.py` сразу в нескольких проектах организации. @@ -496,9 +446,7 @@ from .routers.users import router Здесь мы это делаем ... просто, чтобы показать, что это возможно 🤷: -```Python hl_lines="21-23" title="app/main.py" -{!../../docs_src/bigger_applications/app/main.py!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[21:23] title["app/main.py"] *} и это будет работать корректно вместе с другими *эндпоинтами*, добавленными с помощью `app.include_router()`. diff --git a/docs/ru/docs/tutorial/body-nested-models.md b/docs/ru/docs/tutorial/body-nested-models.md index 5bb5abbe63..4c914b97f0 100644 --- a/docs/ru/docs/tutorial/body-nested-models.md +++ b/docs/ru/docs/tutorial/body-nested-models.md @@ -14,35 +14,14 @@ В Python есть специальный способ объявлять списки с внутренними типами, или «параметрами типа»: -### Импортируйте `List` из модуля typing { #import-typings-list } - -В Python 3.9 и выше вы можете использовать стандартный тип `list` для объявления аннотаций типов, как мы увидим ниже. 💡 - -Но в версиях Python до 3.9 (начиная с 3.6) сначала вам необходимо импортировать `List` из стандартного модуля `typing`: - -{* ../../docs_src/body_nested_models/tutorial002.py hl[1] *} - ### Объявите `list` с параметром типа { #declare-a-list-with-a-type-parameter } -Для объявления типов, у которых есть параметры типа (внутренние типы), таких как `list`, `dict`, `tuple`: - -* Если у вас Python версии ниже 3.9, импортируйте их аналоги из модуля `typing` -* Передайте внутренний(ие) тип(ы) как «параметры типа», используя квадратные скобки: `[` и `]` - -В Python 3.9 это будет: +Для объявления типов, у которых есть параметры типа (внутренние типы), таких как `list`, `dict`, `tuple`, передайте внутренний(ие) тип(ы) как «параметры типа», используя квадратные скобки: `[` и `]` ```Python my_list: list[str] ``` -В версиях Python до 3.9 это будет: - -```Python -from typing import List - -my_list: List[str] -``` - Это всё стандартный синтаксис Python для объявления типов. Используйте этот же стандартный синтаксис для атрибутов модели с внутренними типами. @@ -107,7 +86,7 @@ my_list: List[str] Ещё раз: сделав такое объявление, с помощью **FastAPI** вы получите: -* Поддержку редактора кода (автозавершение и т. д.), даже для вложенных моделей +* Поддержку редактора кода (автозавершение и т.д.), даже для вложенных моделей * Преобразование данных * Валидацию данных * Автоматическую документацию @@ -178,12 +157,6 @@ my_list: List[str] Если верхний уровень значения тела JSON-объекта представляет собой JSON `array` (в Python — `list`), вы можете объявить тип в параметре функции, так же как в моделях Pydantic: -```Python -images: List[Image] -``` - -или в Python 3.9 и выше: - ```Python images: list[Image] ``` diff --git a/docs/ru/docs/tutorial/body.md b/docs/ru/docs/tutorial/body.md index 16ff6466c1..b61f3e7a09 100644 --- a/docs/ru/docs/tutorial/body.md +++ b/docs/ru/docs/tutorial/body.md @@ -161,7 +161,7 @@ JSON Schema ваших моделей будет частью сгенериро FastAPI понимает, что значение `q` не является обязательным из-за значения по умолчанию `= None`. -Аннотации типов `str | None` (Python 3.10+) или `Union[str, None]` (Python 3.8+) не используются FastAPI для определения обязательности; он узнает, что параметр не обязателен, потому что у него есть значение по умолчанию `= None`. +Аннотации типов `str | None` (Python 3.10+) или `Union[str, None]` (Python 3.9+) не используются FastAPI для определения обязательности; он узнает, что параметр не обязателен, потому что у него есть значение по умолчанию `= None`. Но добавление аннотаций типов позволит вашему редактору кода лучше вас поддерживать и обнаруживать ошибки. diff --git a/docs/ru/docs/tutorial/cookie-param-models.md b/docs/ru/docs/tutorial/cookie-param-models.md index daac764e3f..182813afdf 100644 --- a/docs/ru/docs/tutorial/cookie-param-models.md +++ b/docs/ru/docs/tutorial/cookie-param-models.md @@ -50,7 +50,7 @@ Вы можете сконфигурировать Pydantic-модель так, чтобы запретить (`forbid`) любые дополнительные (`extra`) поля: -{* ../../docs_src/cookie_param_models/tutorial002_an_py39.py hl[10] *} +{* ../../docs_src/cookie_param_models/tutorial002_an_py310.py hl[10] *} Если клиент попробует отправить **дополнительные cookies**, то в ответ он получит **ошибку**. diff --git a/docs/ru/docs/tutorial/cors.md b/docs/ru/docs/tutorial/cors.md index b0704351a9..d09a31e2c3 100644 --- a/docs/ru/docs/tutorial/cors.md +++ b/docs/ru/docs/tutorial/cors.md @@ -46,7 +46,7 @@ * Отдельных HTTP-методов (`POST`, `PUT`) или всех вместе, используя `"*"`. * Отдельных HTTP-заголовков или всех вместе, используя `"*"`. -{* ../../docs_src/cors/tutorial001.py hl[2,6:11,13:19] *} +{* ../../docs_src/cors/tutorial001_py39.py hl[2,6:11,13:19] *} `CORSMiddleware` использует "запрещающие" значения по умолчанию, поэтому вам нужно явным образом разрешить использование отдельных источников, методов или заголовков, чтобы браузеры могли использовать их в кросс-доменном контексте. diff --git a/docs/ru/docs/tutorial/debugging.md b/docs/ru/docs/tutorial/debugging.md index a5340af089..51955835e6 100644 --- a/docs/ru/docs/tutorial/debugging.md +++ b/docs/ru/docs/tutorial/debugging.md @@ -6,7 +6,7 @@ В вашем FastAPI приложении, импортируйте и вызовите `uvicorn` напрямую: -{* ../../docs_src/debugging/tutorial001.py hl[1,15] *} +{* ../../docs_src/debugging/tutorial001_py39.py hl[1,15] *} ### Описание `__name__ == "__main__"` { #about-name-main } diff --git a/docs/ru/docs/tutorial/dependencies/classes-as-dependencies.md b/docs/ru/docs/tutorial/dependencies/classes-as-dependencies.md index ec7770d960..a38e885d43 100644 --- a/docs/ru/docs/tutorial/dependencies/classes-as-dependencies.md +++ b/docs/ru/docs/tutorial/dependencies/classes-as-dependencies.md @@ -101,7 +101,7 @@ fluffy = Cat(name="Mr Fluffy") Обратите внимание, что в приведенном выше коде мы два раза пишем `CommonQueryParams`: -//// tab | Python 3.8+ +//// tab | Python 3.9+ ```Python commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] @@ -109,7 +109,7 @@ commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] //// -//// tab | Python 3.8+ non-Annotated +//// tab | Python 3.9+ non-Annotated /// tip | Подсказка @@ -137,7 +137,7 @@ commons: CommonQueryParams = Depends(CommonQueryParams) В этом случае первый `CommonQueryParams`, в: -//// tab | Python 3.8+ +//// tab | Python 3.9+ ```Python commons: Annotated[CommonQueryParams, ... @@ -145,7 +145,7 @@ commons: Annotated[CommonQueryParams, ... //// -//// tab | Python 3.8+ non-Annotated +//// tab | Python 3.9+ non-Annotated /// tip | Подсказка @@ -163,7 +163,7 @@ commons: CommonQueryParams ... На самом деле можно написать просто: -//// tab | Python 3.8+ +//// tab | Python 3.9+ ```Python commons: Annotated[Any, Depends(CommonQueryParams)] @@ -171,7 +171,7 @@ commons: Annotated[Any, Depends(CommonQueryParams)] //// -//// tab | Python 3.8+ non-Annotated +//// tab | Python 3.9+ non-Annotated /// tip | Подсказка @@ -197,7 +197,7 @@ commons = Depends(CommonQueryParams) Но вы видите, что здесь мы имеем некоторое повторение кода, дважды написав `CommonQueryParams`: -//// tab | Python 3.8+ +//// tab | Python 3.9+ ```Python commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] @@ -205,7 +205,7 @@ commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] //// -//// tab | Python 3.8+ non-Annotated +//// tab | Python 3.9+ non-Annotated /// tip | Подсказка @@ -225,7 +225,7 @@ commons: CommonQueryParams = Depends(CommonQueryParams) Вместо того чтобы писать: -//// tab | Python 3.8+ +//// tab | Python 3.9+ ```Python commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] @@ -233,7 +233,7 @@ commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] //// -//// tab | Python 3.8+ non-Annotated +//// tab | Python 3.9+ non-Annotated /// tip | Подсказка @@ -249,7 +249,7 @@ commons: CommonQueryParams = Depends(CommonQueryParams) ...следует написать: -//// tab | Python 3.8+ +//// tab | Python 3.9+ ```Python commons: Annotated[CommonQueryParams, Depends()] @@ -257,7 +257,7 @@ commons: Annotated[CommonQueryParams, Depends()] //// -//// tab | Python 3.8 non-Annotated +//// tab | Python 3.9+ non-Annotated /// tip | Подсказка diff --git a/docs/ru/docs/tutorial/dependencies/dependencies-with-yield.md b/docs/ru/docs/tutorial/dependencies/dependencies-with-yield.md index 267faa4068..dc202db616 100644 --- a/docs/ru/docs/tutorial/dependencies/dependencies-with-yield.md +++ b/docs/ru/docs/tutorial/dependencies/dependencies-with-yield.md @@ -29,15 +29,15 @@ FastAPI поддерживает зависимости, которые выпо Перед созданием ответа будет выполнен только код до и включая оператор `yield`: -{* ../../docs_src/dependencies/tutorial007.py hl[2:4] *} +{* ../../docs_src/dependencies/tutorial007_py39.py hl[2:4] *} Значение, полученное из `yield`, внедряется в *операции пути* и другие зависимости: -{* ../../docs_src/dependencies/tutorial007.py hl[4] *} +{* ../../docs_src/dependencies/tutorial007_py39.py hl[4] *} Код, следующий за оператором `yield`, выполняется после ответа: -{* ../../docs_src/dependencies/tutorial007.py hl[5:6] *} +{* ../../docs_src/dependencies/tutorial007_py39.py hl[5:6] *} /// tip | Подсказка @@ -57,7 +57,7 @@ FastAPI поддерживает зависимости, которые выпо Точно так же можно использовать `finally`, чтобы убедиться, что обязательные шаги при выходе выполнены независимо от того, было ли исключение или нет. -{* ../../docs_src/dependencies/tutorial007.py hl[3,5] *} +{* ../../docs_src/dependencies/tutorial007_py39.py hl[3,5] *} ## Подзависимости с `yield` { #sub-dependencies-with-yield } @@ -184,6 +184,51 @@ participant tasks as Background tasks /// +## Ранний выход и `scope` { #early-exit-and-scope } + +Обычно «код выхода» зависимостей с `yield` выполняется **после того, как ответ** отправлен клиенту. + +Но если вы знаете, что не будете использовать зависимость после возврата из *функции-обработчика пути*, вы можете использовать `Depends(scope="function")`, чтобы сообщить FastAPI, что он должен закрыть зависимость после возврата из *функции-обработчика пути*, но **до того**, как **ответ будет отправлен**. + +{* ../../docs_src/dependencies/tutorial008e_an_py39.py hl[12,16] *} + +`Depends()` принимает параметр `scope`, который может быть: + +* `"function"`: начать зависимость до *функции-обработчика пути*, которая обрабатывает запрос, завершить зависимость после окончания *функции-обработчика пути*, но **до того**, как ответ будет отправлен обратно клиенту. То есть функция зависимости будет выполнена **вокруг** *функции-обработчика пути*. +* `"request"`: начать зависимость до *функции-обработчика пути*, которая обрабатывает запрос (как и при использовании `"function"`), но завершить **после** того, как ответ будет отправлен обратно клиенту. То есть функция зависимости будет выполнена **вокруг** цикла запроса (**request**) и ответа. + +Если не указано и в зависимости есть `yield`, по умолчанию будет `scope` со значением `"request"`. + +### `scope` для подзависимостей { #scope-for-sub-dependencies } + +Когда вы объявляете зависимость с `scope="request"` (значение по умолчанию), любая подзависимость также должна иметь `scope` равный `"request"`. + +Но зависимость со `scope` равным `"function"` может иметь зависимости со `scope` `"function"` и со `scope` `"request"`. + +Это потому, что любая зависимость должна иметь возможность выполнить свой код выхода раньше подзависимостей, так как ей может понадобиться использовать их во время своего кода выхода. + +```mermaid +sequenceDiagram + +participant client as Client +participant dep_req as Зависимость scope="request" +participant dep_func as Зависимость scope="function" +participant operation as Функция-обработчик пути + + client ->> dep_req: Запрос + Note over dep_req: Выполнить код до yield + dep_req ->> dep_func: Передать значение + Note over dep_func: Выполнить код до yield + dep_func ->> operation: Выполнить функцию-обработчик пути + operation ->> dep_func: Выход из функции-обработчика пути + Note over dep_func: Выполнить код после yield + Note over dep_func: ✅ Зависимость закрыта + dep_func ->> client: Отправить ответ клиенту + Note over client: Ответ отправлен + Note over dep_req: Выполнить код после yield + Note over dep_req: ✅ Зависимость закрыта +``` + ## Зависимости с `yield`, `HTTPException`, `except` и фоновыми задачами { #dependencies-with-yield-httpexception-except-and-background-tasks } Зависимости с `yield` со временем эволюционировали, чтобы покрыть разные сценарии и исправить некоторые проблемы. @@ -224,7 +269,7 @@ with open("./somefile.txt") as f: Их также можно использовать внутри зависимостей **FastAPI** с `yield`, применяя операторы `with` или `async with` внутри функции зависимости: -{* ../../docs_src/dependencies/tutorial010.py hl[1:9,13] *} +{* ../../docs_src/dependencies/tutorial010_py39.py hl[1:9,13] *} /// tip | Подсказка diff --git a/docs/ru/docs/tutorial/dependencies/global-dependencies.md b/docs/ru/docs/tutorial/dependencies/global-dependencies.md index 075d6b0baa..2347c6dd83 100644 --- a/docs/ru/docs/tutorial/dependencies/global-dependencies.md +++ b/docs/ru/docs/tutorial/dependencies/global-dependencies.md @@ -6,7 +6,7 @@ В этом случае они будут применяться ко всем *операциям пути* в приложении: -{* ../../docs_src/dependencies/tutorial012_an_py39.py hl[16] *} +{* ../../docs_src/dependencies/tutorial012_an_py39.py hl[17] *} Все способы [добавления `dependencies` (зависимостей) в *декораторах операций пути*](dependencies-in-path-operation-decorators.md){.internal-link target=_blank} по-прежнему применимы, но в данном случае зависимости применяются ко всем *операциям пути* приложения. diff --git a/docs/ru/docs/tutorial/dependencies/sub-dependencies.md b/docs/ru/docs/tutorial/dependencies/sub-dependencies.md index efe8d98c31..da31a6682f 100644 --- a/docs/ru/docs/tutorial/dependencies/sub-dependencies.md +++ b/docs/ru/docs/tutorial/dependencies/sub-dependencies.md @@ -62,7 +62,7 @@ query_extractor --> query_or_cookie_extractor --> read_query В расширенном сценарии, когда вы знаете, что вам нужно, чтобы зависимость вызывалась на каждом шаге (возможно, несколько раз) в одном и том же запросе, вместо использования "кэшированного" значения, вы можете установить параметр `use_cache=False` при использовании `Depends`: -//// tab | Python 3.8+ +//// tab | Python 3.9+ ```Python hl_lines="1" async def needy_dependency(fresh_value: Annotated[str, Depends(get_value, use_cache=False)]): @@ -71,7 +71,7 @@ async def needy_dependency(fresh_value: Annotated[str, Depends(get_value, use_ca //// -//// tab | Python 3.8+ без Annotated +//// tab | Python 3.9+ без Annotated /// tip | Подсказка diff --git a/docs/ru/docs/tutorial/first-steps.md b/docs/ru/docs/tutorial/first-steps.md index c82118cbe5..798c03d517 100644 --- a/docs/ru/docs/tutorial/first-steps.md +++ b/docs/ru/docs/tutorial/first-steps.md @@ -2,7 +2,7 @@ Самый простой файл FastAPI может выглядеть так: -{* ../../docs_src/first_steps/tutorial001.py *} +{* ../../docs_src/first_steps/tutorial001_py39.py *} Скопируйте это в файл `main.py`. @@ -143,11 +143,47 @@ OpenAPI определяет схему API для вашего API. И эта Вы также можете использовать её для автоматической генерации кода для клиентов, которые взаимодействуют с вашим API. Например, для фронтенд-, мобильных или IoT-приложений. +### Разверните приложение (необязательно) { #deploy-your-app-optional } + +При желании вы можете развернуть своё приложение FastAPI в FastAPI Cloud, перейдите и присоединитесь к списку ожидания, если ещё не сделали этого. 🚀 + +Если у вас уже есть аккаунт **FastAPI Cloud** (мы пригласили вас из списка ожидания 😉), вы можете развернуть приложение одной командой. + +Перед развертыванием убедитесь, что вы вошли в систему: + +
+ +```console +$ fastapi login + +You are logged in to FastAPI Cloud 🚀 +``` + +
+ +Затем разверните приложение: + +
+ +```console +$ fastapi deploy + +Deploying to FastAPI Cloud... + +✅ Deployment successful! + +🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev +``` + +
+ +Готово! Теперь вы можете открыть своё приложение по этому URL. ✨ + ## Рассмотрим поэтапно { #recap-step-by-step } ### Шаг 1: импортируйте `FastAPI` { #step-1-import-fastapi } -{* ../../docs_src/first_steps/tutorial001.py hl[1] *} +{* ../../docs_src/first_steps/tutorial001_py39.py hl[1] *} `FastAPI` — это класс на Python, который предоставляет всю функциональность для вашего API. @@ -161,7 +197,7 @@ OpenAPI определяет схему API для вашего API. И эта ### Шаг 2: создайте экземпляр `FastAPI` { #step-2-create-a-fastapi-instance } -{* ../../docs_src/first_steps/tutorial001.py hl[3] *} +{* ../../docs_src/first_steps/tutorial001_py39.py hl[3] *} Здесь переменная `app` будет экземпляром класса `FastAPI`. @@ -230,7 +266,7 @@ https://example.com/items/foo #### Определите *декоратор операции пути (path operation decorator)* { #define-a-path-operation-decorator } -{* ../../docs_src/first_steps/tutorial001.py hl[6] *} +{* ../../docs_src/first_steps/tutorial001_py39.py hl[6] *} `@app.get("/")` сообщает **FastAPI**, что функция прямо под ним отвечает за обработку запросов, поступающих: @@ -284,7 +320,7 @@ https://example.com/items/foo * **операция**: `get`. * **функция**: функция ниже «декоратора» (ниже `@app.get("/")`). -{* ../../docs_src/first_steps/tutorial001.py hl[7] *} +{* ../../docs_src/first_steps/tutorial001_py39.py hl[7] *} Это функция на Python. @@ -296,7 +332,7 @@ https://example.com/items/foo Вы также можете определить её как обычную функцию вместо `async def`: -{* ../../docs_src/first_steps/tutorial003.py hl[7] *} +{* ../../docs_src/first_steps/tutorial003_py39.py hl[7] *} /// note | Примечание @@ -306,7 +342,7 @@ https://example.com/items/foo ### Шаг 5: верните содержимое { #step-5-return-the-content } -{* ../../docs_src/first_steps/tutorial001.py hl[8] *} +{* ../../docs_src/first_steps/tutorial001_py39.py hl[8] *} Вы можете вернуть `dict`, `list`, отдельные значения `str`, `int` и т.д. @@ -314,6 +350,26 @@ https://example.com/items/foo Многие другие объекты и модели будут автоматически преобразованы в JSON (включая ORM и т. п.). Попробуйте использовать те, что вам привычнее, с высокой вероятностью они уже поддерживаются. +### Шаг 6: разверните приложение { #step-6-deploy-it } + +Разверните приложение в **FastAPI Cloud** одной командой: `fastapi deploy`. 🎉 + +#### О FastAPI Cloud { #about-fastapi-cloud } + +**FastAPI Cloud** создан тем же автором и командой, что и **FastAPI**. + +Он упрощает процесс **создания образа**, **развертывания** и **доступа** к API с минимальными усилиями. + +Он переносит тот же **опыт разработчика** при создании приложений с FastAPI на их **развертывание** в облаке. 🎉 + +FastAPI Cloud — основной спонсор и источник финансирования для open-source проектов «FastAPI и друзья». ✨ + +#### Развертывание у других облачных провайдеров { #deploy-to-other-cloud-providers } + +FastAPI — open-source и основан на стандартах. Вы можете развернуть приложения FastAPI у любого облачного провайдера по вашему выбору. + +Следуйте руководствам вашего облачного провайдера, чтобы развернуть приложения FastAPI у них. 🤓 + ## Резюме { #recap } * Импортируйте `FastAPI`. @@ -321,3 +377,4 @@ https://example.com/items/foo * Напишите **декоратор операции пути**, например `@app.get("/")`. * Определите **функцию операции пути**; например, `def root(): ...`. * Запустите сервер разработки командой `fastapi dev`. +* При желании разверните приложение командой `fastapi deploy`. diff --git a/docs/ru/docs/tutorial/handling-errors.md b/docs/ru/docs/tutorial/handling-errors.md index 2378c8b04c..2e00d70759 100644 --- a/docs/ru/docs/tutorial/handling-errors.md +++ b/docs/ru/docs/tutorial/handling-errors.md @@ -25,7 +25,7 @@ ### Импортируйте `HTTPException` { #import-httpexception } -{* ../../docs_src/handling_errors/tutorial001.py hl[1] *} +{* ../../docs_src/handling_errors/tutorial001_py39.py hl[1] *} ### Вызовите `HTTPException` в своем коде { #raise-an-httpexception-in-your-code } @@ -39,7 +39,7 @@ В данном примере, когда клиент запрашивает элемент по несуществующему ID, возникает исключение со статус-кодом `404`: -{* ../../docs_src/handling_errors/tutorial001.py hl[11] *} +{* ../../docs_src/handling_errors/tutorial001_py39.py hl[11] *} ### Возвращаемый ответ { #the-resulting-response } @@ -77,11 +77,11 @@ Но в случае, если это необходимо для продвинутого сценария, можно добавить пользовательские заголовки: -{* ../../docs_src/handling_errors/tutorial002.py hl[14] *} +{* ../../docs_src/handling_errors/tutorial002_py39.py hl[14] *} ## Установка пользовательских обработчиков исключений { #install-custom-exception-handlers } -Вы можете добавить пользовательские обработчики исключений с помощью то же самое исключение - утилиты от Starlette. +Вы можете добавить пользовательские обработчики исключений с помощью тех же утилит обработки исключений из Starlette. Допустим, у вас есть пользовательское исключение `UnicornException`, которое вы (или используемая вами библиотека) можете `вызвать`. @@ -89,7 +89,7 @@ Можно добавить собственный обработчик исключений с помощью `@app.exception_handler()`: -{* ../../docs_src/handling_errors/tutorial003.py hl[5:7,13:18,24] *} +{* ../../docs_src/handling_errors/tutorial003_py39.py hl[5:7,13:18,24] *} Здесь, если запросить `/unicorns/yolo`, то *операция пути* вызовет `UnicornException`. @@ -117,7 +117,7 @@ Вы можете переопределить эти обработчики исключений на свои собственные. -### Переопределение исключений проверки запроса { #override-request-validation-exceptions } +### Переопределение обработчика исключений проверки запроса { #override-request-validation-exceptions } Когда запрос содержит недопустимые данные, **FastAPI** внутренне вызывает ошибку `RequestValidationError`. @@ -127,7 +127,7 @@ Обработчик исключения получит объект `Request` и исключение. -{* ../../docs_src/handling_errors/tutorial004.py hl[2,14:16] *} +{* ../../docs_src/handling_errors/tutorial004_py39.py hl[2,14:19] *} Теперь, если перейти к `/items/foo`, то вместо стандартной JSON-ошибки с: @@ -149,36 +149,17 @@ вы получите текстовую версию: ``` -1 validation error -path -> item_id - value is not a valid integer (type=type_error.integer) +Validation errors: +Field: ('path', 'item_id'), Error: Input should be a valid integer, unable to parse string as an integer ``` -#### `RequestValidationError` или `ValidationError` { #requestvalidationerror-vs-validationerror } - -/// warning | Внимание - -Это технические детали, которые можно пропустить, если они не важны для вас сейчас. - -/// - -`RequestValidationError` является подклассом Pydantic `ValidationError`. - -**FastAPI** использует его для того, чтобы, если вы используете Pydantic-модель в `response_model`, и ваши данные содержат ошибку, вы увидели ошибку в журнале. - -Но клиент/пользователь этого не увидит. Вместо этого клиент получит сообщение "Internal Server Error" с кодом состояния HTTP `500`. - -Так и должно быть, потому что если в вашем *ответе* или где-либо в вашем коде (не в *запросе* клиента) возникает Pydantic `ValidationError`, то это действительно ошибка в вашем коде. - -И пока вы не устраните ошибку, ваши клиенты/пользователи не должны иметь доступа к внутренней информации о ней, так как это может привести к уязвимости в системе безопасности. - ### Переопределите обработчик ошибок `HTTPException` { #override-the-httpexception-error-handler } Аналогичным образом можно переопределить обработчик `HTTPException`. Например, для этих ошибок можно вернуть обычный текстовый ответ вместо JSON: -{* ../../docs_src/handling_errors/tutorial004.py hl[3:4,9:11,22] *} +{* ../../docs_src/handling_errors/tutorial004_py39.py hl[3:4,9:11,25] *} /// note | Технические детали @@ -188,13 +169,21 @@ path -> item_id /// +/// warning | Внимание + +Имейте в виду, что `RequestValidationError` содержит информацию об имени файла и строке, где произошла ошибка валидации, чтобы вы могли при желании отобразить её в логах с релевантными данными. + +Но это означает, что если вы просто преобразуете её в строку и вернёте эту информацию напрямую, вы можете допустить небольшую утечку информации о своей системе, поэтому здесь код извлекает и показывает каждую ошибку отдельно. + +/// + ### Используйте тело `RequestValidationError` { #use-the-requestvalidationerror-body } Ошибка `RequestValidationError` содержит полученное `тело` с недопустимыми данными. Вы можете использовать его при разработке приложения для регистрации тела и его отладки, возврата пользователю и т.д. -{* ../../docs_src/handling_errors/tutorial005.py hl[14] *} +{* ../../docs_src/handling_errors/tutorial005_py39.py hl[14] *} Теперь попробуйте отправить недействительный элемент, например: @@ -250,6 +239,6 @@ from starlette.exceptions import HTTPException as StarletteHTTPException Если вы хотите использовать исключение вместе с теми же обработчиками исключений по умолчанию из **FastAPI**, вы можете импортировать и повторно использовать обработчики исключений по умолчанию из `fastapi.exception_handlers`: -{* ../../docs_src/handling_errors/tutorial006.py hl[2:5,15,21] *} +{* ../../docs_src/handling_errors/tutorial006_py39.py hl[2:5,15,21] *} В этом примере вы просто `выводите в терминал` ошибку с очень выразительным сообщением, но идея вам понятна. Вы можете использовать исключение, а затем просто повторно использовать стандартные обработчики исключений. diff --git a/docs/ru/docs/tutorial/metadata.md b/docs/ru/docs/tutorial/metadata.md index 2e359752e1..e4fe5fb544 100644 --- a/docs/ru/docs/tutorial/metadata.md +++ b/docs/ru/docs/tutorial/metadata.md @@ -18,7 +18,7 @@ Вы можете задать их следующим образом: -{* ../../docs_src/metadata/tutorial001.py hl[3:16, 19:32] *} +{* ../../docs_src/metadata/tutorial001_py39.py hl[3:16, 19:32] *} /// tip | Подсказка @@ -36,7 +36,7 @@ К примеру: -{* ../../docs_src/metadata/tutorial001_1.py hl[31] *} +{* ../../docs_src/metadata/tutorial001_1_py39.py hl[31] *} ## Метаданные для тегов { #metadata-for-tags } @@ -58,7 +58,7 @@ Создайте метаданные для ваших тегов и передайте их в параметре `openapi_tags`: -{* ../../docs_src/metadata/tutorial004.py hl[3:16,18] *} +{* ../../docs_src/metadata/tutorial004_py39.py hl[3:16,18] *} Помните, что вы можете использовать Markdown внутри описания, к примеру "login" будет отображен жирным шрифтом (**login**) и "fancy" будет отображаться курсивом (_fancy_). @@ -72,7 +72,7 @@ Используйте параметр `tags` с вашими *операциями пути* (и `APIRouter`ами), чтобы присвоить им различные теги: -{* ../../docs_src/metadata/tutorial004.py hl[21,26] *} +{* ../../docs_src/metadata/tutorial004_py39.py hl[21,26] *} /// info | Дополнительная информация @@ -100,7 +100,7 @@ К примеру, чтобы задать её отображение по адресу `/api/v1/openapi.json`: -{* ../../docs_src/metadata/tutorial002.py hl[3] *} +{* ../../docs_src/metadata/tutorial002_py39.py hl[3] *} Если вы хотите отключить схему OpenAPI полностью, вы можете задать `openapi_url=None`, это также отключит пользовательские интерфейсы документации, которые её используют. @@ -117,4 +117,4 @@ К примеру, чтобы задать отображение Swagger UI по адресу `/documentation` и отключить ReDoc: -{* ../../docs_src/metadata/tutorial003.py hl[3] *} +{* ../../docs_src/metadata/tutorial003_py39.py hl[3] *} diff --git a/docs/ru/docs/tutorial/middleware.md b/docs/ru/docs/tutorial/middleware.md index 5803b398b9..a83d3c0111 100644 --- a/docs/ru/docs/tutorial/middleware.md +++ b/docs/ru/docs/tutorial/middleware.md @@ -33,7 +33,7 @@ * Затем она возвращает ответ `response`, сгенерированный *операцией пути*. * Также имеется возможность видоизменить `response`, перед тем как его вернуть. -{* ../../docs_src/middleware/tutorial001.py hl[8:9,11,14] *} +{* ../../docs_src/middleware/tutorial001_py39.py hl[8:9,11,14] *} /// tip | Примечание @@ -59,7 +59,7 @@ Например, вы можете добавить собственный заголовок `X-Process-Time`, содержащий время в секундах, необходимое для обработки запроса и генерации ответа: -{* ../../docs_src/middleware/tutorial001.py hl[10,12:13] *} +{* ../../docs_src/middleware/tutorial001_py39.py hl[10,12:13] *} /// tip | Примечание diff --git a/docs/ru/docs/tutorial/path-operation-configuration.md b/docs/ru/docs/tutorial/path-operation-configuration.md index 63b48a3941..96a54ffea0 100644 --- a/docs/ru/docs/tutorial/path-operation-configuration.md +++ b/docs/ru/docs/tutorial/path-operation-configuration.md @@ -46,7 +46,7 @@ **FastAPI** поддерживает это так же, как и в случае с обычными строками: -{* ../../docs_src/path_operation_configuration/tutorial002b.py hl[1,8:10,13,18] *} +{* ../../docs_src/path_operation_configuration/tutorial002b_py39.py hl[1,8:10,13,18] *} ## Краткое и развёрнутое содержание { #summary-and-description } @@ -92,7 +92,7 @@ OpenAPI указывает, что каждой *операции пути* не Если вам необходимо пометить *операцию пути* как устаревшую, при этом не удаляя её, передайте параметр `deprecated`: -{* ../../docs_src/path_operation_configuration/tutorial006.py hl[16] *} +{* ../../docs_src/path_operation_configuration/tutorial006_py39.py hl[16] *} Он будет четко помечен как устаревший в интерактивной документации: diff --git a/docs/ru/docs/tutorial/path-params-numeric-validations.md b/docs/ru/docs/tutorial/path-params-numeric-validations.md index ccea1945e4..f0fe788051 100644 --- a/docs/ru/docs/tutorial/path-params-numeric-validations.md +++ b/docs/ru/docs/tutorial/path-params-numeric-validations.md @@ -54,7 +54,7 @@ Path-параметр всегда является обязательным, п Поэтому вы можете определить функцию так: -{* ../../docs_src/path_params_numeric_validations/tutorial002.py hl[7] *} +{* ../../docs_src/path_params_numeric_validations/tutorial002_py39.py hl[7] *} Но имейте в виду, что если вы используете `Annotated`, вы не столкнётесь с этой проблемой, так как вы не используете значения по умолчанию параметров функции для `Query()` или `Path()`. @@ -83,7 +83,7 @@ Path-параметр всегда является обязательным, п Python не будет ничего делать с `*`, но он будет знать, что все следующие параметры являются именованными аргументами (парами ключ-значение), также известными как kwargs, даже если у них нет значений по умолчанию. -{* ../../docs_src/path_params_numeric_validations/tutorial003.py hl[7] *} +{* ../../docs_src/path_params_numeric_validations/tutorial003_py39.py hl[7] *} ### Лучше с `Annotated` { #better-with-annotated } diff --git a/docs/ru/docs/tutorial/path-params.md b/docs/ru/docs/tutorial/path-params.md index f7d138afbe..83a7ed3ffe 100644 --- a/docs/ru/docs/tutorial/path-params.md +++ b/docs/ru/docs/tutorial/path-params.md @@ -2,7 +2,7 @@ Вы можете определить "параметры" или "переменные" пути, используя синтаксис форматированных строк Python: -{* ../../docs_src/path_params/tutorial001.py hl[6:7] *} +{* ../../docs_src/path_params/tutorial001_py39.py hl[6:7] *} Значение параметра пути `item_id` будет передано в функцию в качестве аргумента `item_id`. @@ -16,7 +16,7 @@ Вы можете объявить тип параметра пути в функции, используя стандартные аннотации типов Python: -{* ../../docs_src/path_params/tutorial002.py hl[7] *} +{* ../../docs_src/path_params/tutorial002_py39.py hl[7] *} Здесь, `item_id` объявлен типом `int`. @@ -118,13 +118,13 @@ Поскольку *операции пути* выполняются в порядке их объявления, необходимо, чтобы путь для `/users/me` был объявлен раньше, чем путь для `/users/{user_id}`: -{* ../../docs_src/path_params/tutorial003.py hl[6,11] *} +{* ../../docs_src/path_params/tutorial003_py39.py hl[6,11] *} Иначе путь для `/users/{user_id}` также будет соответствовать `/users/me`, "подразумевая", что он получает параметр `user_id` со значением `"me"`. Аналогично, вы не можете переопределить операцию с путем: -{* ../../docs_src/path_params/tutorial003b.py hl[6,11] *} +{* ../../docs_src/path_params/tutorial003b_py39.py hl[6,11] *} Первый будет выполняться всегда, так как путь совпадает первым. @@ -140,13 +140,7 @@ Затем создайте атрибуты класса с фиксированными допустимыми значениями: -{* ../../docs_src/path_params/tutorial005.py hl[1,6:9] *} - -/// info | Дополнительная информация - -Перечисления (enum) доступны в Python начиная с версии 3.4. - -/// +{* ../../docs_src/path_params/tutorial005_py39.py hl[1,6:9] *} /// tip | Подсказка @@ -158,7 +152,7 @@ Определите *параметр пути*, используя в аннотации типа класс перечисления (`ModelName`), созданный ранее: -{* ../../docs_src/path_params/tutorial005.py hl[16] *} +{* ../../docs_src/path_params/tutorial005_py39.py hl[16] *} ### Проверьте документацию { #check-the-docs } @@ -174,13 +168,13 @@ Вы можете сравнить это значение с *элементом перечисления* класса `ModelName`: -{* ../../docs_src/path_params/tutorial005.py hl[17] *} +{* ../../docs_src/path_params/tutorial005_py39.py hl[17] *} #### Получение *значения перечисления* { #get-the-enumeration-value } Можно получить фактическое значение (в данном случае - `str`) с помощью `model_name.value` или в общем случае `your_enum_member.value`: -{* ../../docs_src/path_params/tutorial005.py hl[20] *} +{* ../../docs_src/path_params/tutorial005_py39.py hl[20] *} /// tip | Подсказка @@ -194,7 +188,7 @@ Они будут преобразованы в соответствующие значения (в данном случае - строки) перед их возвратом клиенту: -{* ../../docs_src/path_params/tutorial005.py hl[18,21,23] *} +{* ../../docs_src/path_params/tutorial005_py39.py hl[18,21,23] *} Вы отправите клиенту такой JSON-ответ: ```JSON @@ -232,7 +226,7 @@ OpenAPI не поддерживает способов объявления *п Можете использовать так: -{* ../../docs_src/path_params/tutorial004.py hl[6] *} +{* ../../docs_src/path_params/tutorial004_py39.py hl[6] *} /// tip | Подсказка diff --git a/docs/ru/docs/tutorial/query-params-str-validations.md b/docs/ru/docs/tutorial/query-params-str-validations.md index 302901d4ec..3a4ecc37dc 100644 --- a/docs/ru/docs/tutorial/query-params-str-validations.md +++ b/docs/ru/docs/tutorial/query-params-str-validations.md @@ -55,7 +55,7 @@ q: str | None = None //// -//// tab | Python 3.8+ +//// tab | Python 3.9+ ```Python q: Union[str, None] = None @@ -73,7 +73,7 @@ q: Annotated[str | None] = None //// -//// tab | Python 3.8+ +//// tab | Python 3.9+ ```Python q: Annotated[Union[str, None]] = None diff --git a/docs/ru/docs/tutorial/query-params.md b/docs/ru/docs/tutorial/query-params.md index 5a84f9768c..be1c0e46e1 100644 --- a/docs/ru/docs/tutorial/query-params.md +++ b/docs/ru/docs/tutorial/query-params.md @@ -2,7 +2,7 @@ Когда вы объявляете параметры функции, которые не являются параметрами пути, они автоматически интерпретируются как "query"-параметры. -{* ../../docs_src/query_params/tutorial001.py hl[9] *} +{* ../../docs_src/query_params/tutorial001_py39.py hl[9] *} Query-параметры представляют из себя набор пар ключ-значение, которые идут после знака `?` в URL-адресе, разделенные символами `&`. @@ -127,7 +127,7 @@ http://127.0.0.1:8000/items/foo?short=yes Но если вы хотите сделать query-параметр обязательным, вы можете просто не указывать значение по умолчанию: -{* ../../docs_src/query_params/tutorial005.py hl[6:7] *} +{* ../../docs_src/query_params/tutorial005_py39.py hl[6:7] *} Здесь параметр запроса `needy` является обязательным параметром с типом данных `str`. diff --git a/docs/ru/docs/tutorial/response-model.md b/docs/ru/docs/tutorial/response-model.md index c045f9ed78..07308c1db2 100644 --- a/docs/ru/docs/tutorial/response-model.md +++ b/docs/ru/docs/tutorial/response-model.md @@ -183,7 +183,7 @@ FastAPI делает несколько вещей внутри вместе с Самый распространённый случай — [возвращать Response напрямую, как описано далее в разделах для продвинутых](../advanced/response-directly.md){.internal-link target=_blank}. -{* ../../docs_src/response_model/tutorial003_02.py hl[8,10:11] *} +{* ../../docs_src/response_model/tutorial003_02_py39.py hl[8,10:11] *} Этот простой случай обрабатывается FastAPI автоматически, потому что аннотация возвращаемого типа — это класс (или подкласс) `Response`. @@ -193,7 +193,7 @@ FastAPI делает несколько вещей внутри вместе с Вы также можете использовать подкласс `Response` в аннотации типа: -{* ../../docs_src/response_model/tutorial003_03.py hl[8:9] *} +{* ../../docs_src/response_model/tutorial003_03_py39.py hl[8:9] *} Это тоже сработает, так как `RedirectResponse` — подкласс `Response`, и FastAPI автоматически обработает этот случай. diff --git a/docs/ru/docs/tutorial/response-status-code.md b/docs/ru/docs/tutorial/response-status-code.md index f5b1ff6ad7..30f642b643 100644 --- a/docs/ru/docs/tutorial/response-status-code.md +++ b/docs/ru/docs/tutorial/response-status-code.md @@ -8,7 +8,7 @@ * `@app.delete()` * и других. -{* ../../docs_src/response_status_code/tutorial001.py hl[6] *} +{* ../../docs_src/response_status_code/tutorial001_py39.py hl[6] *} /// note | Примечание @@ -74,7 +74,7 @@ FastAPI знает об этом и создаст документацию Open Рассмотрим предыдущий пример еще раз: -{* ../../docs_src/response_status_code/tutorial001.py hl[6] *} +{* ../../docs_src/response_status_code/tutorial001_py39.py hl[6] *} `201` – это код статуса "Создано". @@ -82,7 +82,7 @@ FastAPI знает об этом и создаст документацию Open Для удобства вы можете использовать переменные из `fastapi.status`. -{* ../../docs_src/response_status_code/tutorial002.py hl[1,6] *} +{* ../../docs_src/response_status_code/tutorial002_py39.py hl[1,6] *} Они содержат те же числовые значения, но позволяют использовать автозавершение редактора кода для выбора кода статуса: diff --git a/docs/ru/docs/tutorial/security/index.md b/docs/ru/docs/tutorial/security/index.md index 8fb4bf24f2..ebac013b6c 100644 --- a/docs/ru/docs/tutorial/security/index.md +++ b/docs/ru/docs/tutorial/security/index.md @@ -1,4 +1,4 @@ -# Настройка авторизации +# Настройка авторизации { #security } Существует множество способов обеспечения безопасности, аутентификации и авторизации. @@ -10,11 +10,11 @@ Но сначала давайте рассмотрим некоторые небольшие концепции. -## Куда-то торопишься? +## Куда-то торопишься? { #in-a-hurry } Если вам не нужна информация о каких-либо из следующих терминов и вам просто нужно добавить защиту с аутентификацией на основе логина и пароля *прямо сейчас*, переходите к следующим главам. -## OAuth2 +## OAuth2 { #oauth2 } OAuth2 - это протокол, который определяет несколько способов обработки аутентификации и авторизации. @@ -24,7 +24,7 @@ OAuth2 включает в себя способы аутентификации Это то, что используют под собой все кнопки "вход с помощью Facebook, Google, X (Twitter), GitHub" на страницах авторизации. -### OAuth 1 +### OAuth 1 { #oauth-1 } Ранее использовался протокол OAuth 1, который сильно отличается от OAuth2 и является более сложным, поскольку он включал прямые описания того, как шифровать сообщение. @@ -34,11 +34,11 @@ OAuth2 не указывает, как шифровать сообщение, о /// tip | Подсказка -В разделе **Развертывание** вы увидите [как настроить протокол HTTPS бесплатно, используя Traefik и Let's Encrypt.](https://fastapi.tiangolo.com/ru/deployment/https/) +В разделе **Развертывание** вы увидите как настроить протокол HTTPS бесплатно, используя Traefik и Let's Encrypt. /// -## OpenID Connect +## OpenID Connect { #openid-connect } OpenID Connect - это еще один протокол, основанный на **OAuth2**. @@ -48,7 +48,7 @@ OpenID Connect - это еще один протокол, основанный Но вход в Facebook не поддерживает OpenID Connect. У него есть собственная вариация OAuth2. -### OpenID (не "OpenID Connect") +### OpenID (не "OpenID Connect") { #openid-not-openid-connect } Также ранее использовался стандарт "OpenID", который пытался решить ту же проблему, что и **OpenID Connect**, но не был основан на OAuth2. @@ -56,7 +56,7 @@ OpenID Connect - это еще один протокол, основанный В настоящее время не очень популярен и не используется. -## OpenAPI +## OpenAPI { #openapi } OpenAPI (ранее известный как Swagger) - это открытая спецификация для создания API (в настоящее время является частью Linux Foundation). @@ -97,7 +97,7 @@ OpenAPI может использовать следующие схемы авт /// -## Преимущества **FastAPI** +## Преимущества **FastAPI** { #fastapi-utilities } Fast API предоставляет несколько инструментов для каждой из этих схем безопасности в модуле `fastapi.security`, которые упрощают использование этих механизмов безопасности. diff --git a/docs/ru/docs/tutorial/sql-databases.md b/docs/ru/docs/tutorial/sql-databases.md index c44f37b9ae..1d03465337 100644 --- a/docs/ru/docs/tutorial/sql-databases.md +++ b/docs/ru/docs/tutorial/sql-databases.md @@ -63,9 +63,9 @@ $ pip install sqlmodel * `table=True` сообщает SQLModel, что это *модель-таблица*, она должна представлять **таблицу** в SQL базе данных, это не просто *модель данных* (как обычный класс Pydantic). -* `Field(primary_key=True)` сообщает SQLModel, что `id` — это **первичный ключ** в SQL базе данных (подробнее о первичных ключах можно узнать в документации SQLModel). +* `Field(primary_key=True)` сообщает SQLModel, что `id` — это **первичный ключ** в SQL базе данных (подробнее о первичных ключах SQL можно узнать в документации SQLModel). - Благодаря типу `int | None`, SQLModel будет знать, что этот столбец должен быть `INTEGER` в SQL базе данных и должен допускать значение `NULL`. + **Примечание:** Мы используем `int | None` для поля первичного ключа, чтобы в Python-коде можно было *создать объект без `id`* (`id=None`), предполагая, что база данных *сгенерирует его при сохранении*. SQLModel понимает, что база данных предоставит `id`, и *определяет столбец как `INTEGER` (не `NULL`)* в схеме базы данных. См. документацию SQLModel о первичных ключах для подробностей. * `Field(index=True)` сообщает SQLModel, что нужно создать **SQL индекс** для этого столбца, что позволит быстрее выполнять выборки при чтении данных, отфильтрованных по этому столбцу. @@ -107,7 +107,7 @@ $ pip install sqlmodel Здесь мы создаём таблицы в обработчике события запуска приложения. -Для продакшна вы, вероятно, будете использовать скрипт миграций, который выполняется до запуска приложения. 🤓 +Для продакшн вы, вероятно, будете использовать скрипт миграций, который выполняется до запуска приложения. 🤓 /// tip | Подсказка diff --git a/docs/ru/docs/tutorial/static-files.md b/docs/ru/docs/tutorial/static-files.md index 8455aea0a2..f40cfe9b04 100644 --- a/docs/ru/docs/tutorial/static-files.md +++ b/docs/ru/docs/tutorial/static-files.md @@ -7,7 +7,7 @@ * Импортируйте `StaticFiles`. * "Примонтируйте" экземпляр `StaticFiles()` к определённому пути. -{* ../../docs_src/static_files/tutorial001.py hl[2,6] *} +{* ../../docs_src/static_files/tutorial001_py39.py hl[2,6] *} /// note | Технические детали diff --git a/docs/ru/docs/tutorial/testing.md b/docs/ru/docs/tutorial/testing.md index 0224798b1a..ab58429c51 100644 --- a/docs/ru/docs/tutorial/testing.md +++ b/docs/ru/docs/tutorial/testing.md @@ -30,7 +30,7 @@ $ pip install httpx Напишите простое утверждение с `assert` дабы проверить истинность Python-выражения (это тоже стандарт `pytest`). -{* ../../docs_src/app_testing/tutorial001.py hl[2,12,15:18] *} +{* ../../docs_src/app_testing/tutorial001_py39.py hl[2,12,15:18] *} /// tip | Подсказка @@ -76,7 +76,7 @@ $ pip install httpx В файле `main.py` находится Ваше приложение **FastAPI**: -{* ../../docs_src/app_testing/main.py *} +{* ../../docs_src/app_testing/app_a_py39/main.py *} ### Файл тестов { #testing-file } @@ -92,7 +92,7 @@ $ pip install httpx Так как оба файла находятся в одной директории, для импорта объекта приложения из файла `main` в файл `test_main` Вы можете использовать относительный импорт: -{* ../../docs_src/app_testing/test_main.py hl[3] *} +{* ../../docs_src/app_testing/app_a_py39/test_main.py hl[3] *} ...и писать дальше тесты, как и раньше. @@ -121,63 +121,13 @@ $ pip install httpx Обе *операции пути* требуют наличия в запросе заголовка `X-Token`. -//// tab | Python 3.10+ - -```Python -{!> ../../docs_src/app_testing/app_b_an_py310/main.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!> ../../docs_src/app_testing/app_b_an_py39/main.py!} -``` - -//// - -//// tab | Python 3.8+ - -```Python -{!> ../../docs_src/app_testing/app_b_an/main.py!} -``` - -//// - -//// tab | Python 3.10+ без Annotated - -/// tip | Подсказка - -По возможности используйте версию с `Annotated`. - -/// - -```Python -{!> ../../docs_src/app_testing/app_b_py310/main.py!} -``` - -//// - -//// tab | Python 3.8+ без Annotated - -/// tip | Подсказка - -По возможности используйте версию с `Annotated`. - -/// - -```Python -{!> ../../docs_src/app_testing/app_b/main.py!} -``` - -//// +{* ../../docs_src/app_testing/app_b_an_py310/main.py *} ### Расширенный файл тестов { #extended-testing-file } Теперь обновим файл `test_main.py`, добавив в него тестов: -{* ../../docs_src/app_testing/app_b/test_main.py *} +{* ../../docs_src/app_testing/app_b_an_py310/test_main.py *} Если Вы не знаете, как передать информацию в запросе, можете воспользоваться поисковиком (погуглить) и задать вопрос: "Как передать информацию в запросе с помощью `httpx`", можно даже спросить: "Как передать информацию в запросе с помощью `requests`", поскольку дизайн HTTPX основан на дизайне Requests. diff --git a/docs/ru/docs/virtual-environments.md b/docs/ru/docs/virtual-environments.md index 5153cd4864..43136298a3 100644 --- a/docs/ru/docs/virtual-environments.md +++ b/docs/ru/docs/virtual-environments.md @@ -242,6 +242,26 @@ $ python -m pip install --upgrade pip +/// tip | Подсказка + +Иногда при попытке обновить pip вы можете получить ошибку **`No module named pip`**. + +Если это произошло, установите и обновите pip с помощью команды ниже: + +
+ +```console +$ python -m ensurepip --upgrade + +---> 100% +``` + +
+ +Эта команда установит pip, если он ещё не установлен, а также гарантирует, что установленная версия pip будет не старее, чем версия, доступная в `ensurepip`. + +/// + ## Добавление `.gitignore` { #add-gitignore } Если вы используете **Git** (а вам стоит его использовать), добавьте файл `.gitignore`, чтобы исключить из Git всё, что находится в вашей `.venv`. @@ -834,7 +854,7 @@ I solemnly swear 🐺 * Управлять **виртуальным окружением** ваших проектов * Устанавливать **пакеты** * Управлять **зависимостями и версиями** пакетов вашего проекта -* Обеспечивать наличие **точного** набора пакетов и версий к установке, включая их зависимости, чтобы вы были уверены, что сможете запускать проект в продакшне точно так же, как и на компьютере при разработке — это называется **locking** +* Обеспечивать наличие **точного** набора пакетов и версий к установке, включая их зависимости, чтобы вы были уверены, что сможете запускать проект в продакшн точно так же, как и на компьютере при разработке — это называется **locking** * И многое другое ## Заключение { #conclusion } diff --git a/docs/tr/llm-prompt.md b/docs/tr/llm-prompt.md new file mode 100644 index 0000000000..297b0a0e6c --- /dev/null +++ b/docs/tr/llm-prompt.md @@ -0,0 +1,52 @@ +### Target language + +Translate to Turkish (Türkçe). + +Language code: tr. + +### Grammar and tone + +- Use instructional Turkish, consistent with existing Turkish docs. +- Use imperative/guide language when appropriate (e.g. “açalım”, “gidin”, “kopyalayalım”). + +### Headings + +- Follow existing Turkish heading style (Title Case where used; no trailing period). + +### Quotes + +- Alıntı stili mevcut Türkçe dokümanlarla tutarlı tutun (genellikle metin içinde ASCII tırnak işaretleri kullanılır). +- Satır içi kod, kod blokları, URL'ler veya dosya yolları içindeki tırnak işaretlerini asla değiştirmeyin. + +### Ellipsis + +- Üç nokta (...) stili mevcut Türkçe dokümanlarla tutarlı tutun. +- Kod, URL veya CLI örneklerindeki `...` ifadesini asla değiştirmeyin. + +### Preferred translations / glossary + +Do not translate technical terms like path, route, request, response, query, body, cookie, and header, keep them as is. + +- Suffixing is very important, when adding Turkish suffixes to the English words, do that based on the pronunciation of the word and with an apostrophe. + +- Suffixes also changes based on what word comes next in Turkish too, here is an example: + +"Server'a gelen request'leri intercept... " or this could have been "request'e", "request'i" etc. + +- Some words are tricky like "path'e" can't be used like "path'a" but it could have been "path'i" "path'leri" etc. + +- You can use a more instructional style, that is consistent with the document, you can add the Turkish version of the term in parenthesis if it is not something very obvious, or an advanced concept, but do not over do it, do it only the first time it is mentioned, but keep the English term as the primary word. + +### `///` admonitions + +- Keep the admonition keyword in English (do not translate `note`, `tip`, etc.). +- If a title is present, prefer these canonical titles: + +- `/// note | Not` +- `/// note | Teknik Detaylar` +- `/// tip | İpucu` +- `/// warning | Uyarı` +- `/// info | Bilgi` +- `/// check | Ek bilgi` + +Prefer `İpucu` over `Ipucu`. diff --git a/docs/uk/docs/index.md b/docs/uk/docs/index.md index 0811a4c7bb..526409c5cf 100644 --- a/docs/uk/docs/index.md +++ b/docs/uk/docs/index.md @@ -1,8 +1,14 @@ +# FastAPI { #fastapi } + + +

- FastAPI + FastAPI

- Готовий до продакшину, високопродуктивний, простий у вивченні та швидкий для написання коду фреймворк + Фреймворк FastAPI: висока продуктивність, легко вивчати, швидко писати код, готовий до продакшину

@@ -21,46 +27,51 @@ --- -**Документація**: https://fastapi.tiangolo.com +**Документація**: https://fastapi.tiangolo.com -**Програмний код**: https://github.com/fastapi/fastapi +**Вихідний код**: https://github.com/fastapi/fastapi --- -FastAPI - це сучасний, швидкий (високопродуктивний), вебфреймворк для створення API за допомогою Python,в основі якого лежить стандартна анотація типів Python. +FastAPI — це сучасний, швидкий (високопродуктивний) вебфреймворк для створення API за допомогою Python, що базується на стандартних підказках типів Python. Ключові особливості: -* **Швидкий**: Дуже висока продуктивність, на рівні з **NodeJS** та **Go** (завдяки Starlette та Pydantic). [Один із найшвидших фреймворків](#performance). +* **Швидкий**: дуже висока продуктивність, на рівні з **NodeJS** та **Go** (завдяки Starlette та Pydantic). [Один із найшвидших Python-фреймворків](#performance). +* **Швидке написання коду**: пришвидшує розробку функціоналу приблизно на 200%–300%. * +* **Менше помилок**: зменшує приблизно на 40% кількість помилок, спричинених людиною (розробником). * +* **Інтуїтивний**: чудова підтримка редакторами коду. Автодоповнення всюди. Менше часу на налагодження. +* **Простий**: спроєктований так, щоб бути простим у використанні та вивченні. Менше часу на читання документації. +* **Короткий**: мінімізує дублювання коду. Кілька можливостей з кожного оголошення параметра. Менше помилок. +* **Надійний**: ви отримуєте код, готовий до продакшину. З автоматичною інтерактивною документацією. +* **Заснований на стандартах**: базується на (і повністю сумісний з) відкритими стандартами для API: OpenAPI (раніше відомий як Swagger) та JSON Schema. -* **Швидке написання коду**: Пришвидшує розробку функціоналу приблизно на 200%-300%. * -* **Менше помилок**: Зменшить кількість помилок спричинених людиною (розробником) на 40%. * -* **Інтуїтивний**: Чудова підтримка редакторами коду. Доповнення всюди. Зменште час на налагодження. -* **Простий**: Спроектований, для легкого використання та навчання. Знадобиться менше часу на читання документації. -* **Короткий**: Зведе до мінімуму дублювання коду. Кожен оголошений параметр може виконувати кілька функцій. -* **Надійний**: Ви матимете стабільний код готовий до продакшину з автоматичною інтерактивною документацією. -* **Стандартизований**: Оснований та повністю сумісний з відкритими стандартами для API: OpenAPI (попередньо відомий як Swagger) та JSON Schema. +* оцінка на основі тестів, проведених внутрішньою командою розробників, що створює продакшн-застосунки. -* оцінка на основі тестів внутрішньої команди розробників, створення продуктових застосунків. - -## Спонсори +## Спонсори { #sponsors } -{% if sponsors %} +### Ключовий спонсор { #keystone-sponsor } + +{% for sponsor in sponsors.keystone -%} + +{% endfor -%} + +### Золоті та срібні спонсори { #gold-and-silver-sponsors } + {% for sponsor in sponsors.gold -%} {% endfor -%} {%- for sponsor in sponsors.silver -%} {% endfor %} -{% endif %} -Other sponsors +Інші спонсори -## Враження +## Враження { #opinions } "_[...] I'm using **FastAPI** a ton these days. [...] I'm actually planning to use it for all of my team's **ML services at Microsoft**. Some of them are getting integrated into the core **Windows** product and some **Office** products._" @@ -88,7 +99,7 @@ FastAPI - це сучасний, швидкий (високопродуктив "_Honestly, what you've built looks super solid and polished. In many ways, it's what I wanted **Hug** to be - it's really inspiring to see someone build that._" -

Timothy Crosley - Hug creator (ref)
+
Timothy Crosley - Hug creator (ref)
--- @@ -100,50 +111,54 @@ FastAPI - це сучасний, швидкий (високопродуктив --- -## **Typer**, FastAPI CLI +"_If anyone is looking to build a production Python API, I would highly recommend **FastAPI**. It is **beautifully designed**, **simple to use** and **highly scalable**, it has become a **key component** in our API first development strategy and is driving many automations and services such as our Virtual TAC Engineer._" + +
Deon Pillsbury - Cisco (ref)
+ +--- + +## Міні-документальний фільм про FastAPI { #fastapi-mini-documentary } + +Наприкінці 2025 року вийшов міні-документальний фільм про FastAPI, ви можете переглянути його онлайн: + +FastAPI Mini Documentary + +## **Typer**, FastAPI для CLI { #typer-the-fastapi-of-clis } -Створюючи CLI застосунок для використання в терміналі, замість веб-API зверніть увагу на **Typer**. +Якщо ви створюєте застосунок CLI для використання в терміналі замість веб-API, зверніть увагу на **Typer**. -**Typer** є молодшим братом FastAPI. І це **FastAPI для CLI**. ⌨️ 🚀 +**Typer** — молодший брат FastAPI. І його задумано як **FastAPI для CLI**. ⌨️ 🚀 -## Вимоги +## Вимоги { #requirements } FastAPI стоїть на плечах гігантів: -* Starlette для web частини. +* Starlette для вебчастини. * Pydantic для частини даних. -## Вставновлення +## Встановлення { #installation } + +Створіть і активуйте віртуальне середовище, а потім встановіть FastAPI:
```console -$ pip install fastapi +$ pip install "fastapi[standard]" ---> 100% ```
-Вам також знадобиться сервер ASGI для продакшину, наприклад Uvicorn або Hypercorn. +**Примітка**: переконайтеся, що ви взяли `"fastapi[standard]"` у лапки, щоб це працювало в усіх терміналах. -
+## Приклад { #example } -```console -$ pip install uvicorn[standard] +### Створіть { #create-it } ----> 100% -``` - -
- -## Приклад - -### Створіть - -* Створіть файл `main.py` з: +Створіть файл `main.py` з: ```Python from typing import Union @@ -188,22 +203,35 @@ async def read_item(item_id: int, q: Union[str, None] = None): **Примітка**: -Стикнувшись з проблемами, не зайвим буде ознайомитися з розділом _"In a hurry?"_ про `async` та `await` у документації. +Якщо ви не знаєте, перегляньте розділ _"In a hurry?"_ про `async` та `await` у документації. -### Запустіть +### Запустіть { #run-it } -Запустіть server з: +Запустіть сервер за допомогою:
```console -$ uvicorn main:app --reload +$ fastapi dev main.py + ╭────────── FastAPI CLI - Development mode ───────────╮ + │ │ + │ Serving at: http://127.0.0.1:8000 │ + │ │ + │ API docs: http://127.0.0.1:8000/docs │ + │ │ + │ Running in development mode, for production use: │ + │ │ + │ fastapi run │ + │ │ + ╰─────────────────────────────────────────────────────╯ + +INFO: Will watch for changes in these directories: ['/home/user/code/awesomeapp'] INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -INFO: Started reloader process [28720] -INFO: Started server process [28722] +INFO: Started reloader process [2248755] using WatchFiles +INFO: Started server process [2248757] INFO: Waiting for application startup. INFO: Application startup complete. ``` @@ -211,21 +239,21 @@ INFO: Application startup complete.
-Про команди uvicorn main:app --reload... +Про команду fastapi dev main.py... -Команда `uvicorn main:app` посилається на: +Команда `fastapi dev` читає ваш файл `main.py`, знаходить у ньому застосунок **FastAPI** і запускає сервер за допомогою Uvicorn. -* `main`: файл `main.py` ("Модуль" Python). -* `app`: об’єкт створений усередині `main.py` рядком `app = FastAPI()`. -* `--reload`: перезапускає сервер після зміни коду. Використовуйте виключно для розробки. +За замовчуванням `fastapi dev` запускається з авто-перезавантаженням для локальної розробки. + +Докладніше читайте в документації FastAPI CLI.
-### Перевірте +### Перевірте { #check-it } -Відкрийте браузер та введіть адресу http://127.0.0.1:8000/items/5?q=somequery. +Відкрийте браузер і перейдіть на http://127.0.0.1:8000/items/5?q=somequery. -Ви побачите у відповідь подібний JSON: +Ви побачите JSON-відповідь: ```JSON {"item_id": 5, "q": "somequery"} @@ -233,32 +261,32 @@ INFO: Application startup complete. Ви вже створили API, який: -* Отримує HTTP запити за _шляхами_ `/` та `/items/{item_id}`. +* Отримує HTTP-запити за _шляхами_ `/` та `/items/{item_id}`. * Обидва _шляхи_ приймають `GET` операції (також відомі як HTTP _методи_). -* _Шлях_ `/items/{item_id}` містить _параметр шляху_ `item_id` який має бути типу `int`. +* _Шлях_ `/items/{item_id}` містить _параметр шляху_ `item_id`, який має бути типу `int`. * _Шлях_ `/items/{item_id}` містить необовʼязковий `str` _параметр запиту_ `q`. -### Інтерактивні документації API +### Інтерактивна документація API { #interactive-api-docs } -Перейдемо сюди http://127.0.0.1:8000/docs. +Тепер перейдіть на http://127.0.0.1:8000/docs. -Ви побачите автоматичну інтерактивну API документацію (створену завдяки Swagger UI): +Ви побачите автоматичну інтерактивну документацію API (надану Swagger UI): ![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) -### Альтернативні документації API +### Альтернативна документація API { #alternative-api-docs } -Тепер перейдемо сюди http://127.0.0.1:8000/redoc. +А тепер перейдіть на http://127.0.0.1:8000/redoc. -Ви побачите альтернативну автоматичну документацію (створену завдяки ReDoc): +Ви побачите альтернативну автоматичну документацію (надану ReDoc): ![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) -## Приклад оновлення +## Приклад оновлення { #example-upgrade } -Тепер модифікуйте файл `main.py`, щоб отримати вміст запиту `PUT`. +Тепер змініть файл `main.py`, щоб отримувати тіло `PUT`-запиту. -Оголошуйте вміст запиту за допомогою стандартних типів Python завдяки Pydantic. +Оголосіть тіло, використовуючи стандартні типи Python, завдяки Pydantic. ```Python hl_lines="4 9-12 25-27" from typing import Union @@ -290,41 +318,41 @@ def update_item(item_id: int, item: Item): return {"item_name": item.name, "item_id": item_id} ``` -Сервер повинен автоматично перезавантажуватися (тому що Ви додали `--reload` до `uvicorn` команди вище). +Сервер `fastapi dev` має автоматично перезавантажитися. -### Оновлення інтерактивної API документації +### Оновлення інтерактивної документації API { #interactive-api-docs-upgrade } -Тепер перейдемо сюди http://127.0.0.1:8000/docs. +Тепер перейдіть на http://127.0.0.1:8000/docs. -* Інтерактивна документація API буде автоматично оновлена, включаючи новий вміст: +* Інтерактивна документація API буде автоматично оновлена, включно з новим тілом: ![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) -* Натисніть кнопку "Try it out", це дозволить вам заповнити параметри та безпосередньо взаємодіяти з API: +* Натисніть кнопку "Try it out", вона дозволяє заповнити параметри та безпосередньо взаємодіяти з API: ![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) -* Потім натисніть кнопку "Execute", інтерфейс користувача зв'яжеться з вашим API, надішле параметри, у відповідь отримає результати та покаже їх на екрані: +* Потім натисніть кнопку "Execute", інтерфейс користувача зв'яжеться з вашим API, надішле параметри, отримає результати та покаже їх на екрані: ![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) -### Оновлення альтернативної API документації +### Оновлення альтернативної документації API { #alternative-api-docs-upgrade } -Зараз перейдемо http://127.0.0.1:8000/redoc. +А тепер перейдіть на http://127.0.0.1:8000/redoc. -* Альтернативна документація також показуватиме новий параметр і вміст запиту: +* Альтернативна документація також відобразить новий параметр запиту та тіло: ![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) -### Підсумки +### Підсумки { #recap } -Таким чином, Ви **один раз** оголошуєте типи параметрів, тіла тощо, як параметри функції. +Отже, ви оголошуєте **один раз** типи параметрів, тіла тощо як параметри функції. Ви робите це за допомогою стандартних сучасних типів Python. Вам не потрібно вивчати новий синтаксис, методи чи класи конкретної бібліотеки тощо. -Використовуючи стандартний **Python**. +Лише стандартний **Python**. Наприклад, для `int`: @@ -332,35 +360,35 @@ def update_item(item_id: int, item: Item): item_id: int ``` -або для більш складної моделі `Item`: +або для складнішої моделі `Item`: ```Python item: Item ``` -...і з цим єдиним оголошенням Ви отримуєте: +...і з цим єдиним оголошенням ви отримуєте: -* Підтримку редактора, включаючи: - * Варіанти заповнення. - * Перевірку типів. -* Перевірку даних: - * Автоматичні та зрозумілі помилки, у разі некоректних даних. - * Перевірка навіть для JSON з високим рівнем вкладеності. -* Перетворення вхідних даних: з мережі до даних і типів Python. Читання з: +* Підтримку редактора, включно з: + * Автодоповненням. + * Перевіркою типів. +* Валідацію даних: + * Автоматичні та зрозумілі помилки, коли дані некоректні. + * Валідацію навіть для глибоко вкладених JSON-обʼєктів. +* Перетворення вхідних даних: з мережі до даних і типів Python. Читання з: * JSON. * Параметрів шляху. * Параметрів запиту. * Cookies. * Headers. * Forms. - * Файлів. -* Перетворення вихідних даних: з типів і даних Python до мережевих даних (як JSON): - * Конвертація Python типів (`str`, `int`, `float`, `bool`, `list`, тощо). - * `datetime` об'єкти. - * `UUID` об'єкти. - * Моделі бази даних. + * Files. +* Перетворення вихідних даних: перетворення з даних і типів Python у мережеві дані (як JSON): + * Перетворення типів Python (`str`, `int`, `float`, `bool`, `list`, тощо). + * Обʼєктів `datetime`. + * Обʼєктів `UUID`. + * Моделей бази даних. * ...та багато іншого. -* Автоматичну інтерактивну документацію API, включаючи 2 альтернативні інтерфейси користувача: +* Автоматичну інтерактивну документацію API, включно з 2 альтернативними інтерфейсами користувача: * Swagger UI. * ReDoc. @@ -368,26 +396,26 @@ item: Item Повертаючись до попереднього прикладу коду, **FastAPI**: -* Підтвердить наявність `item_id` у шляху для запитів `GET` та `PUT`. -* Підтвердить, що `item_id` має тип `int` для запитів `GET` and `PUT`. +* Перевірить, що `item_id` є у шляху для `GET` та `PUT`-запитів. +* Перевірить, що `item_id` має тип `int` для `GET` та `PUT`-запитів. * Якщо це не так, клієнт побачить корисну, зрозумілу помилку. -* Перевірить, чи є необов'язковий параметр запиту з назвою `q` (а саме `http://127.0.0.1:8000/items/foo?q=somequery`) для запитів `GET`. +* Перевірить, чи є необов'язковий параметр запиту з назвою `q` (як у `http://127.0.0.1:8000/items/foo?q=somequery`) для `GET`-запитів. * Оскільки параметр `q` оголошено як `= None`, він необов'язковий. - * За відсутності `None` він був би обов'язковим (як і вміст у випадку з `PUT`). -* Для запитів `PUT` із `/items/{item_id}`, читає вміст як JSON: - * Перевірить, чи має обов'язковий атрибут `name` тип `str`. - * Перевірить, чи має обов'язковий атрибут `price` тип `float`. - * Перевірить, чи існує необов'язковий атрибут `is_offer` та чи має він тип `bool`. - * Усе це також працюватиме для глибоко вкладених об'єктів JSON. -* Автоматично конвертує із та в JSON. -* Документує все за допомогою OpenAPI, який може бути використано в: + * Без `None` він був би обов'язковим (як і тіло у випадку з `PUT`). +* Для `PUT`-запитів до `/items/{item_id}` прочитає тіло як JSON: + * Перевірить, що є обовʼязковий атрибут `name`, який має бути типу `str`. + * Перевірить, що є обовʼязковий атрибут `price`, який має бути типу `float`. + * Перевірить, що є необовʼязковий атрибут `is_offer`, який має бути типу `bool`, якщо він присутній. + * Усе це також працюватиме для глибоко вкладених JSON-обʼєктів. +* Автоматично перетворюватиме з та в JSON. +* Документуватиме все за допомогою OpenAPI, який може бути використано в: * Інтерактивних системах документації. * Системах автоматичної генерації клієнтського коду для багатьох мов. -* Надає безпосередньо 2 вебінтерфейси інтерактивної документації. +* Надаватиме безпосередньо 2 вебінтерфейси інтерактивної документації. --- -Ми лише трішки доторкнулися до коду, але Ви вже маєте уявлення про те, як все працює. +Ми лише трішки доторкнулися до поверхні, але ви вже маєте уявлення про те, як усе працює. Спробуйте змінити рядок: @@ -407,57 +435,131 @@ item: Item ... "item_price": item.price ... ``` -...і побачите, як ваш редактор автоматично заповнюватиме атрибути та знатиме їхні типи: +...і побачите, як ваш редактор автоматично доповнюватиме атрибути та знатиме їхні типи: ![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) -Для більш повного ознайомлення з додатковими функціями, перегляньте Туторіал - Посібник Користувача. +Для більш повного прикладу, що включає більше можливостей, перегляньте Туторіал — Посібник користувача. -**Spoiler alert**: туторіал - посібник користувача містить: +**Spoiler alert**: туторіал — посібник користувача містить: -* Оголошення **параметрів** з інших місць як: **headers**, **cookies**, **form fields** та **files**. -* Як встановити **перевірку обмежень** як `maximum_length` або `regex`. -* Дуже потужна і проста у використанні система **Ін'єкція Залежностей**. -* Безпека та автентифікація, включаючи підтримку **OAuth2** з **JWT tokens** та **HTTP Basic** автентифікацію. +* Оголошення **параметрів** з інших різних місць, як-от: **headers**, **cookies**, **form fields** та **files**. +* Як встановлювати **обмеження валідації** як `maximum_length` або `regex`. +* Дуже потужну і просту у використанні систему **Dependency Injection**. +* Безпеку та автентифікацію, включно з підтримкою **OAuth2** з **JWT tokens** та **HTTP Basic** auth. * Досконаліші (але однаково прості) техніки для оголошення **глибоко вкладених моделей JSON** (завдяки Pydantic). -* Багато додаткових функцій (завдяки Starlette) як-от: +* Інтеграцію **GraphQL** з Strawberry та іншими бібліотеками. +* Багато додаткових можливостей (завдяки Starlette) як-от: * **WebSockets** * надзвичайно прості тести на основі HTTPX та `pytest` * **CORS** * **Cookie Sessions** * ...та більше. -## Продуктивність +### Розгортання застосунку (необовʼязково) { #deploy-your-app-optional } -Незалежні тести TechEmpower показують що застосунки **FastAPI**, які працюють під керуванням Uvicorn є одними з найшвидших серед доступних фреймворків в Python, поступаючись лише Starlette та Uvicorn (які внутрішньо використовуються в FastAPI). (*) +За бажання ви можете розгорнути ваш застосунок FastAPI у FastAPI Cloud, перейдіть і приєднайтеся до списку очікування, якщо ви ще цього не зробили. 🚀 -Щоб дізнатися більше про це, перегляньте розділ Benchmarks. +Якщо у вас вже є обліковий запис **FastAPI Cloud** (ми запросили вас зі списку очікування 😉), ви можете розгорнути ваш застосунок однією командою. -## Необов'язкові залежності +Перед розгортанням переконайтеся, що ви ввійшли в систему: -Pydantic використовує: +
+ +```console +$ fastapi login + +You are logged in to FastAPI Cloud 🚀 +``` + +
+ +Потім розгорніть ваш застосунок: + +
+ +```console +$ fastapi deploy + +Deploying to FastAPI Cloud... + +✅ Deployment successful! + +🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev +``` + +
+ +Ось і все! Тепер ви можете отримати доступ до вашого застосунку за цією URL-адресою. ✨ + +#### Про FastAPI Cloud { #about-fastapi-cloud } + +**FastAPI Cloud** створено тим самим автором і командою, що стоять за **FastAPI**. + +Він спрощує процес **створення**, **розгортання** та **доступу** до API з мінімальними зусиллями. + +Він забезпечує той самий **developer experience** створення застосунків на FastAPI під час їх **розгортання** у хмарі. 🎉 + +FastAPI Cloud — основний спонсор і джерело фінансування open source проєктів *FastAPI and friends*. ✨ + +#### Розгортання в інших хмарних провайдерів { #deploy-to-other-cloud-providers } + +FastAPI — open source і базується на стандартах. Ви можете розгортати застосунки FastAPI в будь-якому хмарному провайдері, який ви оберете. + +Дотримуйтеся інструкцій вашого хмарного провайдера, щоб розгорнути застосунки FastAPI у нього. 🤓 + +## Продуктивність { #performance } + +Незалежні тести TechEmpower показують застосунки **FastAPI**, які працюють під керуванням Uvicorn, як одні з найшвидших доступних Python-фреймворків, поступаючись лише Starlette та Uvicorn (які внутрішньо використовуються в FastAPI). (*) + +Щоб дізнатися більше, перегляньте розділ Benchmarks. + +## Залежності { #dependencies } + +FastAPI залежить від Pydantic і Starlette. + +### Залежності `standard` { #standard-dependencies } + +Коли ви встановлюєте FastAPI за допомогою `pip install "fastapi[standard]"`, ви отримуєте групу необовʼязкових залежностей `standard`: + +Використовується Pydantic: * email-validator - для валідації електронної пошти. -* pydantic-settings - для управління налаштуваннями. + +Використовується Starlette: + +* httpx - потрібно, якщо ви хочете використовувати `TestClient`. +* jinja2 - потрібно, якщо ви хочете використовувати конфігурацію шаблонів за замовчуванням. +* python-multipart - потрібно, якщо ви хочете підтримувати «parsing» форм за допомогою `request.form()`. + +Використовується FastAPI: + +* uvicorn - для сервера, який завантажує та обслуговує ваш застосунок. Це включає `uvicorn[standard]`, до якого входять деякі залежності (наприклад, `uvloop`), потрібні для високопродуктивної роботи сервера. +* `fastapi-cli[standard]` - щоб надати команду `fastapi`. + * Це включає `fastapi-cloud-cli`, який дозволяє розгортати ваш застосунок FastAPI у FastAPI Cloud. + +### Без залежностей `standard` { #without-standard-dependencies } + +Якщо ви не хочете включати необовʼязкові залежності `standard`, ви можете встановити через `pip install fastapi` замість `pip install "fastapi[standard]"`. + +### Без `fastapi-cloud-cli` { #without-fastapi-cloud-cli } + +Якщо ви хочете встановити FastAPI зі стандартними залежностями, але без `fastapi-cloud-cli`, ви можете встановити через `pip install "fastapi[standard-no-fastapi-cloud-cli]"`. + +### Додаткові необовʼязкові залежності { #additional-optional-dependencies } + +Є ще деякі додаткові залежності, які ви можете захотіти встановити. + +Додаткові необовʼязкові залежності Pydantic: + +* pydantic-settings - для керування налаштуваннями. * pydantic-extra-types - для додаткових типів, що можуть бути використані з Pydantic. +Додаткові необовʼязкові залежності FastAPI: -Starlette використовує: +* orjson - потрібно, якщо ви хочете використовувати `ORJSONResponse`. +* ujson - потрібно, якщо ви хочете використовувати `UJSONResponse`. -* httpx - Необхідно, якщо Ви хочете використовувати `TestClient`. -* jinja2 - Необхідно, якщо Ви хочете використовувати шаблони як конфігурацію за замовчуванням. -* python-multipart - Необхідно, якщо Ви хочете підтримувати "розбір" форми за допомогою `request.form()`. -* itsdangerous - Необхідно для підтримки `SessionMiddleware`. -* pyyaml - Необхідно для підтримки Starlette `SchemaGenerator` (ймовірно, вам це не потрібно з FastAPI). - -FastAPI / Starlette використовують: - -* uvicorn - для сервера, який завантажує та обслуговує вашу програму. -* orjson - Необхідно, якщо Ви хочете використовувати `ORJSONResponse`. -* ujson - Необхідно, якщо Ви хочете використовувати `UJSONResponse`. - -Ви можете встановити все це за допомогою `pip install fastapi[all]`. - -## Ліцензія +## Ліцензія { #license } Цей проєкт ліцензовано згідно з умовами ліцензії MIT. diff --git a/docs/uk/docs/learn/index.md b/docs/uk/docs/learn/index.md index 7f9f21e572..6e28d414ad 100644 --- a/docs/uk/docs/learn/index.md +++ b/docs/uk/docs/learn/index.md @@ -1,5 +1,5 @@ -# Навчання +# Навчання { #learn } -У цьому розділі надані вступні та навчальні матеріали для вивчення FastAPI. +У цьому розділі надані вступні розділи та навчальні матеріали для вивчення **FastAPI**. Це можна розглядати як **книгу**, **курс**, або **офіційний** та рекомендований спосіб освоїти FastAPI. 😎 diff --git a/docs/uk/docs/python-types.md b/docs/uk/docs/python-types.md index 676bafb15c..a82d13a285 100644 --- a/docs/uk/docs/python-types.md +++ b/docs/uk/docs/python-types.md @@ -1,29 +1,28 @@ -# Вступ до типів Python +# Вступ до типів Python { #python-types-intro } -Python підтримує додаткові "підказки типу" ("type hints") (також звані "анотаціями типу" ("type annotations")). +Python підтримує додаткові «підказки типів» (також звані «анотаціями типів»). -Ці **"type hints"** є спеціальним синтаксисом, що дозволяє оголошувати тип змінної. +Ці **«підказки типів»** або анотації — це спеціальний синтаксис, що дозволяє оголошувати тип змінної. -За допомогою оголошення типів для ваших змінних, редактори та інструменти можуть надати вам кращу підтримку. +За допомогою оголошення типів для ваших змінних редактори та інструменти можуть надати вам кращу підтримку. -Це просто **швидкий посібник / нагадування** про анотації типів у Python. Він покриває лише мінімум, необхідний щоб використовувати їх з **FastAPI**... що насправді дуже мало. +Це лише **швидкий туторіал / нагадування** про підказки типів у Python. Він покриває лише мінімум, необхідний щоб використовувати їх з **FastAPI**... що насправді дуже мало. -**FastAPI** повністю базується на цих анотаціях типів, вони дають йому багато переваг. +**FastAPI** повністю базується на цих підказках типів, вони дають йому багато переваг і користі. Але навіть якщо ви ніколи не використаєте **FastAPI**, вам буде корисно дізнатись трохи про них. -/// note +/// note | Примітка -Якщо ви експерт у Python і ви вже знаєте усе про анотації типів - перейдіть до наступного розділу. +Якщо ви експерт у Python і ви вже знаєте все про підказки типів, перейдіть до наступного розділу. /// -## Мотивація +## Мотивація { #motivation } Давайте почнемо з простого прикладу: -{* ../../docs_src/python_types/tutorial001.py *} - +{* ../../docs_src/python_types/tutorial001_py39.py *} Виклик цієї програми виводить: @@ -34,13 +33,12 @@ John Doe Функція виконує наступне: * Бере `first_name` та `last_name`. -* Конвертує кожну літеру кожного слова у верхній регістр за допомогою `title()`. -* Конкатенує їх разом із пробілом по середині. +* Перетворює першу літеру кожного з них у верхній регістр за допомогою `title()`. +* Конкатенує їх разом із пробілом по середині. -{* ../../docs_src/python_types/tutorial001.py hl[2] *} +{* ../../docs_src/python_types/tutorial001_py39.py hl[2] *} - -### Редагуйте це +### Редагуйте це { #edit-it } Це дуже проста програма. @@ -48,11 +46,11 @@ John Doe У певний момент ви розпочали б визначення функції, у вас були б готові параметри... -Але тоді вам потрібно викликати "той метод, який переводить першу літеру у верхній регістр". +Але тоді вам потрібно викликати «той метод, який перетворює першу літеру у верхній регістр». Це буде `upper`? Чи `uppercase`? `first_uppercase`? `capitalize`? -Тоді ви спробуєте давнього друга програміста - автозаповнення редактора коду. +Тоді ви спробуєте давнього друга програміста — автозаповнення редактора коду. Ви надрукуєте перший параметр функції, `first_name`, тоді крапку (`.`), а тоді натиснете `Ctrl+Space`, щоб запустити автозаповнення. @@ -60,7 +58,7 @@ John Doe -### Додайте типи +### Додайте типи { #add-types } Давайте змінимо один рядок з попередньої версії. @@ -78,10 +76,9 @@ John Doe Ось і все. -Це "type hints": - -{* ../../docs_src/python_types/tutorial002.py hl[1] *} +Це «підказки типів»: +{* ../../docs_src/python_types/tutorial002_py39.py hl[1] *} Це не те саме, що оголошення значень за замовчуванням, як це було б з: @@ -91,43 +88,41 @@ John Doe Це зовсім інше. -Ми використовуємо двокрапку (`:`), не дорівнює (`=`). +Ми використовуємо двокрапку (`:`), не знак дорівнює (`=`). -І додавання анотації типу зазвичай не змінює того, що сталось би без них. +І додавання підказок типів зазвичай не змінює того, що відбувається, порівняно з тим, що відбувалося б без них. -Але тепер, уявіть що ви посеред процесу створення функції, але з анотаціями типів. +Але тепер уявіть, що ви знову посеред процесу створення функції, але з підказками типів. -В цей же момент, ви спробуєте викликати автозаповнення з допомогою `Ctrl+Space` і побачите: +У той самий момент ви спробуєте викликати автозаповнення за допомогою `Ctrl+Space` і побачите: -Разом з цим, ви можете прокручувати, переглядати опції, допоки ви не знайдете одну, що звучить схоже: +Разом з цим ви можете прокручувати, переглядаючи опції, допоки не знайдете ту, що «щось вам підказує»: -## Більше мотивації +## Більше мотивації { #more-motivation } -Перевірте цю функцію, вона вже має анотацію типу: - -{* ../../docs_src/python_types/tutorial003.py hl[1] *} +Перевірте цю функцію, вона вже має підказки типів: +{* ../../docs_src/python_types/tutorial003_py39.py hl[1] *} Оскільки редактор знає типи змінних, ви не тільки отримаєте автозаповнення, ви також отримаєте перевірку помилок: -Тепер ви знаєте, щоб виправити це, вам потрібно перетворити `age` у строку з допомогою `str(age)`: +Тепер ви знаєте, щоб виправити це, вам потрібно перетворити `age` у рядок за допомогою `str(age)`: -{* ../../docs_src/python_types/tutorial004.py hl[2] *} +{* ../../docs_src/python_types/tutorial004_py39.py hl[2] *} +## Оголошення типів { #declaring-types } -## Оголошення типів - -Щойно ви побачили основне місце для оголошення анотацій типу. Як параметри функції. +Щойно ви побачили основне місце для оголошення підказок типів. Як параметри функції. Це також основне місце, де ви б їх використовували у **FastAPI**. -### Прості типи +### Прості типи { #simple-types } Ви можете оголошувати усі стандартні типи у Python, не тільки `str`. @@ -138,78 +133,51 @@ John Doe * `bool` * `bytes` -{* ../../docs_src/python_types/tutorial005.py hl[1] *} +{* ../../docs_src/python_types/tutorial005_py39.py hl[1] *} - -### Generic-типи з параметрами типів +### Generic-типи з параметрами типів { #generic-types-with-type-parameters } Існують деякі структури даних, які можуть містити інші значення, наприклад `dict`, `list`, `set` та `tuple`. І внутрішні значення також можуть мати свій тип. -Ці типи, які мають внутрішні типи, називаються "**generic**" типами. І оголосити їх можна навіть із внутрішніми типами. +Ці типи, які мають внутрішні типи, називаються «**generic**» типами. І оголосити їх можна навіть із внутрішніми типами. -Щоб оголосити ці типи та внутрішні типи, ви можете використовувати стандартний модуль Python `typing`. Він існує спеціально для підтримки анотацій типів. +Щоб оголосити ці типи та внутрішні типи, ви можете використовувати стандартний модуль Python `typing`. Він існує спеціально для підтримки цих підказок типів. -#### Новіші версії Python +#### Новіші версії Python { #newer-versions-of-python } Синтаксис із використанням `typing` **сумісний** з усіма версіями, від Python 3.6 до останніх, включаючи Python 3.9, Python 3.10 тощо. -У міру розвитку Python **новіші версії** мають покращену підтримку анотацій типів і в багатьох випадках вам навіть не потрібно буде імпортувати та використовувати модуль `typing` для оголошення анотацій типу. +У міру розвитку Python **новіші версії** мають покращену підтримку цих анотацій типів і в багатьох випадках вам навіть не потрібно буде імпортувати та використовувати модуль `typing` для оголошення анотацій типів. -Якщо ви можете вибрати новішу версію Python для свого проекту, ви зможете скористатися цією додатковою простотою. Дивіться кілька прикладів нижче. +Якщо ви можете вибрати новішу версію Python для свого проекту, ви зможете скористатися цією додатковою простотою. -#### List (список) +У всій документації є приклади, сумісні з кожною версією Python (коли є різниця). + +Наприклад, «**Python 3.6+**» означає, що це сумісно з Python 3.6 або вище (включно з 3.7, 3.8, 3.9, 3.10 тощо). А «**Python 3.9+**» означає, що це сумісно з Python 3.9 або вище (включаючи 3.10 тощо). + +Якщо ви можете використовувати **останні версії Python**, використовуйте приклади для останньої версії — вони матимуть **найкращий і найпростіший синтаксис**, наприклад, «**Python 3.10+**». + +#### List { #list } Наприклад, давайте визначимо змінну, яка буде `list` із `str`. -//// tab | Python 3.8 і вище +Оголосіть змінну з тим самим синтаксисом двокрапки (`:`). -З модуля `typing`, імпортуємо `List` (з великої літери `L`): - -```Python hl_lines="1" -{!> ../../docs_src/python_types/tutorial006.py!} -``` - -Оголосимо змінну з тим самим синтаксисом двокрапки (`:`). - -Як тип вкажемо `List`, який ви імпортували з `typing`. +Як тип вкажіть `list`. Оскільки список є типом, який містить деякі внутрішні типи, ви поміщаєте їх у квадратні дужки: -```Python hl_lines="4" -{!> ../../docs_src/python_types/tutorial006.py!} -``` +{* ../../docs_src/python_types/tutorial006_py39.py hl[1] *} -//// +/// info | Інформація -//// tab | Python 3.9 і вище +Ці внутрішні типи в квадратних дужках називаються «параметрами типу». -Оголосимо змінну з тим самим синтаксисом двокрапки (`:`). - -Як тип вкажемо `list`. - -Оскільки список є типом, який містить деякі внутрішні типи, ви поміщаєте їх у квадратні дужки: - -```Python hl_lines="1" -{!> ../../docs_src/python_types/tutorial006_py39.py!} -``` - -//// - -/// info - -Ці внутрішні типи в квадратних дужках називаються "параметрами типу". - -У цьому випадку, `str` це параметр типу переданий у `List` (або `list` у Python 3.9 і вище). +У цьому випадку `str` — це параметр типу, переданий у `list`. /// -Це означає: "змінна `items` це `list`, і кожен з елементів у цьому списку - `str`". - -/// tip - -Якщо ви використовуєте Python 3.9 і вище, вам не потрібно імпортувати `List` з `typing`, ви можете використовувати натомість тип `list`. - -/// +Це означає: «змінна `items` — це `list`, і кожен з елементів у цьому списку — `str`». Зробивши це, ваш редактор може надати підтримку навіть під час обробки елементів зі списку: @@ -221,78 +189,42 @@ John Doe І все ж редактор знає, що це `str`, і надає підтримку для цього. -#### Tuple and Set (кортеж та набір) +#### Tuple and Set { #tuple-and-set } Ви повинні зробити те ж саме, щоб оголосити `tuple` і `set`: -//// tab | Python 3.8 і вище - -```Python hl_lines="1 4" -{!> ../../docs_src/python_types/tutorial007.py!} -``` - -//// - -//// tab | Python 3.9 і вище - -```Python hl_lines="1" -{!> ../../docs_src/python_types/tutorial007_py39.py!} -``` - -//// +{* ../../docs_src/python_types/tutorial007_py39.py hl[1] *} Це означає: -* Змінна `items_t` це `tuple` з 3 елементами, `int`, ще `int`, та `str`. -* Змінна `items_s` це `set`, і кожен його елемент типу `bytes`. +* Змінна `items_t` — це `tuple` з 3 елементами: `int`, ще `int`, та `str`. +* Змінна `items_s` — це `set`, і кожен його елемент має тип `bytes`. -#### Dict (словник) +#### Dict { #dict } Щоб оголосити `dict`, вам потрібно передати 2 параметри типу, розділені комами. -Перший параметр типу для ключа у `dict`. +Перший параметр типу для ключів у `dict`. -Другий параметр типу для значення у `dict`: +Другий параметр типу для значень у `dict`: -//// tab | Python 3.8 і вище - -```Python hl_lines="1 4" -{!> ../../docs_src/python_types/tutorial008.py!} -``` - -//// - -//// tab | Python 3.9 і вище - -```Python hl_lines="1" -{!> ../../docs_src/python_types/tutorial008_py39.py!} -``` - -//// +{* ../../docs_src/python_types/tutorial008_py39.py hl[1] *} Це означає: -* Змінна `prices` це `dict`: - * Ключі цього `dict` типу `str` (наприклад, назва кожного елементу). - * Значення цього `dict` типу `float` (наприклад, ціна кожного елементу). +* Змінна `prices` — це `dict`: + * Ключі цього `dict` мають тип `str` (скажімо, назва кожного елементу). + * Значення цього `dict` мають тип `float` (скажімо, ціна кожного елементу). -#### Union (об'єднання) +#### Union { #union } -Ви можете оголосити, що змінна може бути будь-яким із **кількох типів**, наприклад, `int` або `str`. +Ви можете оголосити, що змінна може бути будь-яким із **кількох типів**, наприклад `int` або `str`. У Python 3.6 і вище (включаючи Python 3.10) ви можете використовувати тип `Union` з `typing` і вставляти в квадратні дужки можливі типи, які можна прийняти. -У Python 3.10 також є **альтернативний синтаксис**, у якому ви можете розділити можливі типи за допомогою вертикальної смуги (`|`). +У Python 3.10 також є **новий синтаксис**, у якому ви можете розділити можливі типи за допомогою вертикальної смуги (`|`). -//// tab | Python 3.8 і вище - -```Python hl_lines="1 4" -{!> ../../docs_src/python_types/tutorial008b.py!} -``` - -//// - -//// tab | Python 3.10 і вище +//// tab | Python 3.10+ ```Python hl_lines="1" {!> ../../docs_src/python_types/tutorial008b_py310.py!} @@ -300,16 +232,24 @@ John Doe //// +//// tab | Python 3.9+ + +```Python hl_lines="1 4" +{!> ../../docs_src/python_types/tutorial008b_py39.py!} +``` + +//// + В обох випадках це означає, що `item` може бути `int` або `str`. -#### Possibly `None` (Optional) +#### Можливо `None` { #possibly-none } Ви можете оголосити, що значення може мати тип, наприклад `str`, але також може бути `None`. У Python 3.6 і вище (включаючи Python 3.10) ви можете оголосити його, імпортувавши та використовуючи `Optional` з модуля `typing`. ```Python hl_lines="1 4" -{!../../docs_src/python_types/tutorial009.py!} +{!../../docs_src/python_types/tutorial009_py39.py!} ``` Використання `Optional[str]` замість просто `str` дозволить редактору допомогти вам виявити помилки, коли ви могли б вважати, що значенням завжди є `str`, хоча насправді воно також може бути `None`. @@ -318,23 +258,7 @@ John Doe Це також означає, що в Python 3.10 ви можете використовувати `Something | None`: -//// tab | Python 3.8 і вище - -```Python hl_lines="1 4" -{!> ../../docs_src/python_types/tutorial009.py!} -``` - -//// - -//// tab | Python 3.8 і вище - альтернатива - -```Python hl_lines="1 4" -{!> ../../docs_src/python_types/tutorial009b.py!} -``` - -//// - -//// tab | Python 3.10 і вище +//// tab | Python 3.10+ ```Python hl_lines="1" {!> ../../docs_src/python_types/tutorial009_py310.py!} @@ -342,32 +266,90 @@ John Doe //// -#### Generic типи +//// tab | Python 3.9+ + +```Python hl_lines="1 4" +{!> ../../docs_src/python_types/tutorial009_py39.py!} +``` + +//// + +//// tab | Python 3.9+ alternative + +```Python hl_lines="1 4" +{!> ../../docs_src/python_types/tutorial009b_py39.py!} +``` + +//// + +#### Використання `Union` або `Optional` { #using-union-or-optional } + +Якщо ви використовуєте версію Python нижче 3.10, ось порада з моєї дуже **суб’єктивної** точки зору: + +* 🚨 Уникайте використання `Optional[SomeType]` +* Натомість ✨ **використовуйте `Union[SomeType, None]`** ✨. + +Обидва варіанти еквівалентні й «під капотом» це одне й те саме, але я рекомендую `Union` замість `Optional`, тому що слово «**optional**» може створювати враження, ніби значення є необов’язковим, хоча насправді це означає «воно може бути `None`», навіть якщо воно не є необов’язковим і все одно є обов’язковим. + +Я вважаю, що `Union[SomeType, None]` більш явно показує, що саме мається на увазі. + +Це лише про слова й назви. Але ці слова можуть впливати на те, як ви та ваша команда думаєте про код. + +Як приклад, розгляньмо цю функцію: + +{* ../../docs_src/python_types/tutorial009c_py39.py hl[1,4] *} + +Параметр `name` визначено як `Optional[str]`, але він **не є необов’язковим**, ви не можете викликати функцію без параметра: + +```Python +say_hi() # Ой, ні, це викликає помилку! 😱 +``` + +Параметр `name` **все ще є обов’язковим** (не *optional*), тому що він не має значення за замовчуванням. Водночас `name` приймає `None` як значення: + +```Python +say_hi(name=None) # Це працює, None є валідним 🎉 +``` + +Добра новина: щойно ви перейдете на Python 3.10, вам не доведеться про це хвилюватися, адже ви зможете просто використовувати `|` для визначення об’єднань типів: + +{* ../../docs_src/python_types/tutorial009c_py310.py hl[1,4] *} + +І тоді вам не доведеться хвилюватися про назви на кшталт `Optional` і `Union`. 😎 + +#### Generic типи { #generic-types } Ці типи, які приймають параметри типу у квадратних дужках, називаються **Generic types** or **Generics**, наприклад: -//// tab | Python 3.8 і вище +//// tab | Python 3.10+ -* `List` -* `Tuple` -* `Set` -* `Dict` -* `Union` -* `Optional` -* ...та інші. - -//// - -//// tab | Python 3.9 і вище - -Ви можете використовувати ті самі вбудовані типи, як generic (з квадратними дужками та типами всередині): +Ви можете використовувати ті самі вбудовані типи як generic (з квадратними дужками та типами всередині): * `list` * `tuple` * `set` * `dict` -І те саме, що й у Python 3.8, із модуля `typing`: +І так само, як і в попередніх версіях Python, з модуля `typing`: + +* `Union` +* `Optional` +* ...та інші. + +У Python 3.10 як альтернативу використанню generic `Union` та `Optional` ви можете використовувати вертикальну смугу (`|`) для оголошення об’єднань типів — це значно краще й простіше. + +//// + +//// tab | Python 3.9+ + +Ви можете використовувати ті самі вбудовані типи як generic (з квадратними дужками та типами всередині): + +* `list` +* `tuple` +* `set` +* `dict` + +І generic з модуля `typing`: * `Union` * `Optional` @@ -375,46 +357,29 @@ John Doe //// -//// tab | Python 3.10 і вище - -Ви можете використовувати ті самі вбудовані типи, як generic (з квадратними дужками та типами всередині): - -* `list` -* `tuple` -* `set` -* `dict` - -І те саме, що й у Python 3.8, із модуля `typing`: - -* `Union` -* `Optional` (так само як у Python 3.8) -* ...та інші. - -У Python 3.10, як альтернатива використанню `Union` та `Optional`, ви можете використовувати вертикальну смугу (`|`) щоб оголосити об'єднання типів. - -//// - -### Класи як типи +### Класи як типи { #classes-as-types } Ви також можете оголосити клас як тип змінної. Скажімо, у вас є клас `Person` з імʼям: -{* ../../docs_src/python_types/tutorial010.py hl[1:3] *} - +{* ../../docs_src/python_types/tutorial010_py39.py hl[1:3] *} Потім ви можете оголосити змінну типу `Person`: -{* ../../docs_src/python_types/tutorial010.py hl[6] *} - +{* ../../docs_src/python_types/tutorial010_py39.py hl[6] *} І знову ж таки, ви отримуєте всю підтримку редактора: -## Pydantic моделі +Зверніть увагу, що це означає: «`one_person` — це **екземпляр** класу `Person`». -Pydantic це бібліотека Python для валідації даних. +Це не означає: «`one_person` — це **клас** з назвою `Person`». + +## Pydantic моделі { #pydantic-models } + +Pydantic — це бібліотека Python для валідації даних. Ви оголошуєте «форму» даних як класи з атрибутами. @@ -424,33 +389,11 @@ John Doe І ви отримуєте всю підтримку редактора з цим отриманим об’єктом. -Приклад з документації Pydantic: +Приклад з офіційної документації Pydantic: -//// tab | Python 3.8 і вище +{* ../../docs_src/python_types/tutorial011_py310.py *} -```Python -{!> ../../docs_src/python_types/tutorial011.py!} -``` - -//// - -//// tab | Python 3.9 і вище - -```Python -{!> ../../docs_src/python_types/tutorial011_py39.py!} -``` - -//// - -//// tab | Python 3.10 і вище - -```Python -{!> ../../docs_src/python_types/tutorial011_py310.py!} -``` - -//// - -/// info +/// info | Інформація Щоб дізнатись більше про Pydantic, перегляньте його документацію. @@ -460,11 +403,43 @@ John Doe Ви побачите набагато більше цього всього на практиці в [Tutorial - User Guide](tutorial/index.md){.internal-link target=_blank}. -## Анотації типів у **FastAPI** +/// tip | Порада -**FastAPI** використовує ці підказки для виконання кількох речей. +Pydantic має спеціальну поведінку, коли ви використовуєте `Optional` або `Union[Something, None]` без значення за замовчуванням; детальніше про це можна прочитати в документації Pydantic про Required Optional fields. -З **FastAPI** ви оголошуєте параметри з підказками типу, і отримуєте: +/// + +## Підказки типів з анотаціями метаданих { #type-hints-with-metadata-annotations } + +У Python також є можливість додавати **додаткові метадані** до цих підказок типів за допомогою `Annotated`. + +Починаючи з Python 3.9, `Annotated` є частиною стандартної бібліотеки, тож ви можете імпортувати його з `typing`. + +{* ../../docs_src/python_types/tutorial013_py39.py hl[1,4] *} + +Сам Python нічого не робить із цим `Annotated`. А для редакторів та інших інструментів тип усе ще є `str`. + +Але ви можете використати це місце в `Annotated`, щоб надати **FastAPI** додаткові метадані про те, як ви хочете, щоб ваш застосунок поводився. + +Важливо пам’ятати, що **перший *параметр типу***, який ви передаєте в `Annotated`, — це **фактичний тип**. Решта — це лише метадані для інших інструментів. + +Наразі вам просто потрібно знати, що `Annotated` існує і що це стандартний Python. 😎 + +Пізніше ви побачите, наскільки **потужним** це може бути. + +/// tip | Порада + +Той факт, що це **стандартний Python**, означає, що ви й надалі отримуватимете **найкращий можливий досвід розробки** у вашому редакторі, з інструментами, які ви використовуєте для аналізу та рефакторингу коду тощо. ✨ + +А також те, що ваш код буде дуже сумісним із багатьма іншими інструментами та бібліотеками Python. 🚀 + +/// + +## Анотації типів у **FastAPI** { #type-hints-in-fastapi } + +**FastAPI** використовує ці підказки типів для виконання кількох речей. + +З **FastAPI** ви оголошуєте параметри з підказками типів, і отримуєте: * **Підтримку редактора**. * **Перевірку типів**. @@ -473,17 +448,17 @@ John Doe * **Визначення вимог**: з параметрів шляху запиту, параметрів запиту, заголовків, тіл, залежностей тощо. * **Перетворення даних**: із запиту в необхідний тип. -* **Перевірка даних**: що надходять від кожного запиту: +* **Перевірки даних**: що надходять від кожного запиту: * Генерування **автоматичних помилок**, що повертаються клієнту, коли дані недійсні. * **Документування** API за допомогою OpenAPI: * який потім використовується для автоматичної інтерактивної документації користувальницьких інтерфейсів. -Все це може здатися абстрактним. Не хвилюйтеся. Ви побачите все це в дії в [Туторіал - Посібник користувача](tutorial/index.md){.internal-link target=_blank}. +Все це може здатися абстрактним. Не хвилюйтеся. Ви побачите все це в дії в [Tutorial - User Guide](tutorial/index.md){.internal-link target=_blank}. Важливо те, що за допомогою стандартних типів Python в одному місці (замість того, щоб додавати більше класів, декораторів тощо), **FastAPI** зробить багато роботи за вас. -/// info +/// info | Інформація -Якщо ви вже пройшли весь навчальний посібник і повернулися, щоб дізнатися більше про типи, ось хороший ресурс "шпаргалка" від `mypy`. +Якщо ви вже пройшли весь туторіал і повернулися, щоб дізнатися більше про типи, ось хороший ресурс: «шпаргалка» від `mypy`. /// diff --git a/docs/uk/docs/tutorial/background-tasks.md b/docs/uk/docs/tutorial/background-tasks.md index 0a9349650d..6d7804195e 100644 --- a/docs/uk/docs/tutorial/background-tasks.md +++ b/docs/uk/docs/tutorial/background-tasks.md @@ -1,28 +1,27 @@ -# Фонові задачі +# Фонові задачі { #background-tasks } Ви можете створювати фонові задачі, які будуть виконуватися *після* повернення відповіді. Це корисно для операцій, які потрібно виконати після обробки запиту, але клієнту не обов’язково чекати завершення цієї операції перед отриманням відповіді. -Приклади використання: +Це включає, наприклад: * Надсилання email-сповіщень після виконання певної дії: - * Підключення до поштового сервера та надсилання листа може займати кілька секунд. Ви можете відразу повернути відповідь, а email відправити у фоні. + * Підключення до поштового сервера та надсилання листа може займати кілька секунд. Ви можете відразу повернути відповідь, а email-сповіщення надіслати у фоні. * Обробка даних: - * Наприклад, якщо отримано файл, який потрібно обробити довготривалим процесом, можна повернути відповідь "Accepted" ("Прийнято", HTTP 202) і виконати обробку файлу у фоні. + * Наприклад, якщо ви отримали файл, який потрібно обробити довготривалим процесом, можна повернути відповідь «Accepted» (HTTP 202) і виконати обробку файлу у фоні. -## Використання `BackgroundTasks` +## Використання `BackgroundTasks` { #using-backgroundtasks } -Спочатку імпортуйте `BackgroundTasks` і додайте його як параметр у Вашу *функцію операції шляху* (path operation function) до `BackgroundTasks`: +Спочатку імпортуйте `BackgroundTasks` і оголосіть параметр у вашій *функції операції шляху* з анотацією типу `BackgroundTasks`: -{* ../../docs_src/background_tasks/tutorial001.py hl[1,13] *} +{* ../../docs_src/background_tasks/tutorial001_py39.py hl[1,13] *} -**FastAPI** автоматично створить об'єкт `BackgroundTasks` і передасть його у цей параметр. +**FastAPI** створить для вас об’єкт типу `BackgroundTasks` і передасть його як цей параметр. +## Створення функції задачі { #create-a-task-function } -## Створення функції задачі - -Створіть функцію, яка буде виконувати фонову задачу. +Створіть функцію, яка буде виконуватися як фонова задача. Це звичайна функція, яка може отримувати параметри. @@ -32,54 +31,54 @@ І оскільки операція запису не використовує `async` та `await`, ми визначаємо функцію як звичайну `def`: -{* ../../docs_src/background_tasks/tutorial001.py hl[6:9] *} +{* ../../docs_src/background_tasks/tutorial001_py39.py hl[6:9] *} -## Додавання фонової задачі +## Додавання фонової задачі { #add-the-background-task } -Усередині Вашої *функції обробки шляху*, передайте функцію задачі в об'єкт *background tasks*, використовуючи метод `.add_task()`: +Усередині вашої *функції операції шляху*, передайте функцію задачі в об'єкт *background tasks*, використовуючи метод `.add_task()`: -{* ../../docs_src/background_tasks/tutorial001.py hl[14] *} +{* ../../docs_src/background_tasks/tutorial001_py39.py hl[14] *} `.add_task()` приймає аргументи: -* Функція задача, яка буде виконуватися у фоновому режимі (`write_notification`). Зверніть увагу, що передається обʼєкт без дужок. -* Будь-яка послідовність аргументів, які потрібно передати у функцію завдання у відповідному порядку (`email`). -* Будь-які іменовані аргументи, які потрібно передати у функцію задачу (`message="some notification"`). +* Функцію задачі, яка буде виконуватися у фоновому режимі (`write_notification`). +* Будь-яку послідовність аргументів, які потрібно передати у функцію задачі у відповідному порядку (`email`). +* Будь-які іменовані аргументи, які потрібно передати у функцію задачі (`message="some notification"`). -## Впровадження залежностей +## Впровадження залежностей { #dependency-injection } -Використання `BackgroundTasks` також працює з системою впровадження залежностей. Ви можете оголосити параметр типу `BackgroundTasks` на різних рівнях: у *функції операції шляху*, у залежності (dependable), у під залежності тощо. +Використання `BackgroundTasks` також працює з системою впровадження залежностей. Ви можете оголосити параметр типу `BackgroundTasks` на різних рівнях: у *функції операції шляху*, у залежності (dependable), у підзалежності тощо. -**FastAPI** знає, як діяти в кожному випадку і як повторно використовувати один і той самий об'єкт, щоб усі фонові задачі були об’єднані та виконувалися у фоновому режимі після завершення основного запиту. +**FastAPI** знає, як діяти в кожному випадку і як повторно використовувати один і той самий об'єкт, щоб усі фонові задачі були об’єднані та виконувалися у фоновому режимі після завершення основного запиту: {* ../../docs_src/background_tasks/tutorial002_an_py310.py hl[13,15,22,25] *} У цьому прикладі повідомлення будуть записані у файл `log.txt` *після* того, як відповідь буде надіслана. -Якщо у запиті був переданий query-параметр, він буде записаний у лог у фоновій задачі. +Якщо у запиті був переданий query, він буде записаний у лог у фоновій задачі. -А потім інша фонова задача, яка створюється у *функції операції шляху*, запише повідомлення з використанням path параметра `email`. +А потім інша фонова задача, згенерована у *функції операції шляху*, запише повідомлення з використанням path параметра `email`. -## Технічні деталі +## Технічні деталі { #technical-details } Клас `BackgroundTasks` походить безпосередньо з `starlette.background`. -Він імпортується безпосередньо у FastAPI, щоб Ви могли використовувати його з `fastapi` і випадково не імпортували `BackgroundTask` (без s в кінці) з `starlette.background`. +Він імпортується/включається безпосередньо у FastAPI, щоб ви могли імпортувати його з `fastapi` і випадково не імпортували альтернативний `BackgroundTask` (без `s` в кінці) з `starlette.background`. Якщо використовувати лише `BackgroundTasks` (а не `BackgroundTask`), то його можна передавати як параметр у *функції операції шляху*, і **FastAPI** подбає про все інше, так само як і про використання об'єкта `Request`. -Також можна використовувати `BackgroundTask` окремо в FastAPI, але для цього Вам доведеться створити об'єкт у коді та повернути Starlette `Response`, включаючи його. +Також можна використовувати `BackgroundTask` окремо в FastAPI, але для цього вам доведеться створити об'єкт у коді та повернути Starlette `Response`, включаючи його. -Детальніше можна почитати в офіційній документації Starlette про фонові задачі . +Детальніше можна почитати в офіційній документації Starlette про Background Tasks. -## Застереження +## Застереження { #caveat } -Якщо Вам потрібно виконувати складні фонові обчислення, і при цьому нема потреби запускати їх у тому ж процесі (наприклад, не потрібно спільного доступу до пам’яті чи змінних), можливо, варто скористатися більш потужними інструментами, такими як Celery. +Якщо вам потрібно виконувати складні фонові обчислення, і при цьому нема потреби запускати їх у тому ж процесі (наприклад, не потрібно спільного доступу до пам’яті чи змінних), можливо, варто скористатися більш потужними інструментами, такими як Celery. -Такі інструменти зазвичай потребують складнішої конфігурації та менеджера черги повідомлень/завдань, наприклад, RabbitMQ або Redis. Однак вони дозволяють виконувати фонові задачі в кількох процесах і навіть на кількох серверах. +Такі інструменти зазвичай потребують складнішої конфігурації та менеджера черги повідомлень/завдань, наприклад, RabbitMQ або Redis. Однак вони дозволяють виконувати фонові задачі в кількох процесах і особливо — на кількох серверах. -Якщо ж Вам потрібно отримати доступ до змінних і об’єктів із тієї ж **FastAPI** - програми або виконувати невеликі фонові завдання (наприклад, надсилати сповіщення електронною поштою), достатньо просто використовувати `BackgroundTasks`. +Якщо ж вам потрібно отримати доступ до змінних і об’єктів із тієї ж **FastAPI**-програми або виконувати невеликі фонові завдання (наприклад, надсилати email-сповіщення), достатньо просто використовувати `BackgroundTasks`. -## Підсумок +## Підсумок { #recap } -Імпортуйте та використовуйте `BackgroundTasks` як параметр у *функціях операції шляху* та залежностях, щоб додавати фонові задачі. +Імпортуйте та використовуйте `BackgroundTasks` як параметри у *функціях операції шляху* та залежностях, щоб додавати фонові задачі. diff --git a/docs/uk/docs/tutorial/body-fields.md b/docs/uk/docs/tutorial/body-fields.md index 7ddd9d104b..70d94f3d6e 100644 --- a/docs/uk/docs/tutorial/body-fields.md +++ b/docs/uk/docs/tutorial/body-fields.md @@ -1,60 +1,61 @@ -# Тіло - Поля +# Тіло — Поля { #body-fields } -Так само як ви можете визначати додаткову валідацію та метадані у параметрах *функції обробки шляху* за допомогою `Query`, `Path` та `Body`, ви можете визначати валідацію та метадані всередині моделей Pydantic за допомогою `Field` від Pydantic. +Так само як ви можете оголошувати додаткову валідацію та метадані в параметрах *функції операції шляху* за допомогою `Query`, `Path` та `Body`, ви можете оголошувати валідацію та метадані всередині моделей Pydantic, використовуючи `Field` від Pydantic. -## Імпорт `Field` +## Імпорт `Field` { #import-field } Спочатку вам потрібно імпортувати це: {* ../../docs_src/body_fields/tutorial001_an_py310.py hl[4] *} -/// warning -Зверніть увагу, що `Field` імпортується прямо з `pydantic`, а не з `fastapi`, як всі інші (`Query`, `Path`, `Body` тощо). +/// warning | Попередження + +Зверніть увагу, що `Field` імпортується безпосередньо з `pydantic`, а не з `fastapi`, як усе інше (`Query`, `Path`, `Body` тощо). /// -## Оголошення атрибутів моделі +## Оголошення атрибутів моделі { #declare-model-attributes } -Ви можете використовувати `Field` з атрибутами моделі: +Потім ви можете використовувати `Field` з атрибутами моделі: {* ../../docs_src/body_fields/tutorial001_an_py310.py hl[11:14] *} -`Field` працює так само, як `Query`, `Path` і `Body`, у нього такі самі параметри тощо. +`Field` працює так само, як `Query`, `Path` і `Body`, має ті самі параметри тощо. /// note | Технічні деталі -Насправді, `Query`, `Path` та інші, що ви побачите далі, створюють об'єкти підкласів загального класу `Param`, котрий сам є підкласом класу `FieldInfo` з Pydantic. +Насправді `Query`, `Path` та інші, які ви побачите далі, створюють об'єкти підкласів спільного класу `Param`, який сам є підкласом класу `FieldInfo` з Pydantic. І `Field` від Pydantic також повертає екземпляр `FieldInfo`. -`Body` також безпосередньо повертає об'єкти підкласу `FieldInfo`. І є інші підкласи, які ви побачите пізніше, що є підкласами класу Body. +`Body` також безпосередньо повертає об'єкти підкласу `FieldInfo`. І є інші, які ви побачите пізніше, що є підкласами класу `Body`. -Пам'ятайте, що коли ви імпортуєте 'Query', 'Path' та інше з 'fastapi', вони фактично є функціями, які повертають спеціальні класи. +Пам'ятайте, що коли ви імпортуєте `Query`, `Path` та інші з `fastapi`, це фактично функції, які повертають спеціальні класи. /// -/// tip +/// tip | Порада -Зверніть увагу, що кожен атрибут моделі із типом, значенням за замовчуванням та `Field` має ту саму структуру, що й параметр *функції обробки шляху*, з `Field` замість `Path`, `Query` і `Body`. +Зверніть увагу, що кожен атрибут моделі з типом, значенням за замовчуванням і `Field` має ту саму структуру, що й параметр *функції операції шляху*, з `Field` замість `Path`, `Query` і `Body`. /// -## Додавання додаткової інформації +## Додавання додаткової інформації { #add-extra-information } -Ви можете визначити додаткову інформацію у `Field`, `Query`, `Body` тощо. І вона буде включена у згенеровану JSON схему. +Ви можете оголошувати додаткову інформацію в `Field`, `Query`, `Body` тощо. І вона буде включена до згенерованої JSON Schema. -Ви дізнаєтеся більше про додавання додаткової інформації пізніше у документації, коли вивчатимете визначення прикладів. +Ви дізнаєтеся більше про додавання додаткової інформації пізніше в документації, коли вивчатимете, як оголошувати приклади. -/// warning +/// warning | Попередження -Додаткові ключі, передані в `Field`, також будуть присутні у згенерованій схемі OpenAPI для вашого додатка. -Оскільки ці ключі не обов'язково можуть бути частиною специфікації OpenAPI, деякі інструменти OpenAPI, наприклад, [OpenAPI валідатор](https://validator.swagger.io/), можуть не працювати з вашою згенерованою схемою. +Додаткові ключі, передані в `Field`, також будуть присутні в отриманій схемі OpenAPI для вашого застосунку. +Оскільки ці ключі не обов'язково є частиною специфікації OpenAPI, деякі інструменти OpenAPI, наприклад [валідатор OpenAPI](https://validator.swagger.io/), можуть не працювати з вашою згенерованою схемою. /// -## Підсумок +## Підсумок { #recap } -Ви можете використовувати `Field` з Pydantic для визначення додаткових перевірок та метаданих для атрибутів моделі. +Ви можете використовувати `Field` від Pydantic, щоб оголошувати додаткову валідацію та метадані для атрибутів моделі. -Ви також можете використовувати додаткові іменовані аргументи для передачі додаткових метаданих JSON схеми. +Ви також можете використовувати додаткові keyword arguments, щоб передавати додаткові метадані JSON Schema. diff --git a/docs/uk/docs/tutorial/body-multiple-params.md b/docs/uk/docs/tutorial/body-multiple-params.md index e2acf8a703..dc9a768c35 100644 --- a/docs/uk/docs/tutorial/body-multiple-params.md +++ b/docs/uk/docs/tutorial/body-multiple-params.md @@ -1,24 +1,24 @@ -# Тіло запиту - Декілька параметрів +# Тіло - Декілька параметрів { #body-multiple-parameters } -Тепер, коли ми розглянули використання `Path` та `Query`, розгляньмо більш просунуті способи оголошення тіла запиту в **FastAPI**. +Тепер, коли ми побачили, як використовувати `Path` і `Query`, розгляньмо більш просунуті варіанти оголошення тіла запиту. -## Змішування `Path`, `Query` та параметрів тіла запиту +## Змішування `Path`, `Query` та параметрів тіла { #mix-path-query-and-body-parameters } -По-перше, звісно, Ви можете вільно змішувати оголошення параметрів `Path`, `Query` та тіла запиту, і **FastAPI** правильно їх обробить. +По-перше, звісно, ви можете вільно змішувати оголошення параметрів `Path`, `Query` та тіла запиту, і **FastAPI** знатиме, що робити. -Також Ви можете оголосити параметри тіла як необов’язкові, встановивши для них значення за замовчуванням `None`: +Також ви можете оголошувати параметри тіла як необов’язкові, встановивши для них значення за замовчуванням `None`: {* ../../docs_src/body_multiple_params/tutorial001_an_py310.py hl[18:20] *} /// note | Примітка -Зверніть увагу, що в цьому випадку параметр `item`, який береться з тіла запиту, є необов'язковим, оскільки має значення за замовчуванням `None`. +Зверніть увагу, що в цьому випадку параметр `item`, який береться з тіла, є необов'язковим. Оскільки має значення за замовчуванням `None`. /// -## Декілька параметрів тіла запиту +## Декілька параметрів тіла { #multiple-body-parameters } -У попередньому прикладі *операція шляху* очікувала JSON з атрибутами `Item`, наприклад: +У попередньому прикладі *операції шляху* очікували б JSON-тіло з атрибутами `Item`, наприклад: ```JSON { @@ -28,13 +28,15 @@ "tax": 3.2 } ``` -Але Ви також можете оголосити декілька параметрів тіла, наприклад `item` та `user`: + +Але ви також можете оголосити декілька параметрів тіла, наприклад `item` та `user`: {* ../../docs_src/body_multiple_params/tutorial002_py310.py hl[20] *} -У цьому випадку **FastAPI** розпізнає, що є кілька параметрів тіла (два параметри є моделями Pydantic). -Тому він використає назви параметрів як ключі (назви полів) у тілі запиту, очікуючи: +У цьому випадку **FastAPI** помітить, що у функції є більше ніж один параметр тіла (є два параметри, які є моделями Pydantic). + +Тож він використає назви параметрів як ключі (назви полів) у тілі та очікуватиме тіло такого вигляду: ```JSON { @@ -53,27 +55,28 @@ /// note | Примітка -Зверніть увагу, що хоча `item` оголошено, так само як і раніше, тепер він очікується в тілі під ключем `item`. +Зверніть увагу, що хоча `item` оголошено так само, як і раніше, тепер він очікується всередині тіла з ключем `item`. /// -**FastAPI** автоматично конвертує дані із запиту таким чином, щоб параметр `item` отримав свій вміст, і те ж саме стосується `user`. +**FastAPI** виконає автоматичне перетворення із запиту, щоб параметр `item` отримав свій конкретний вміст, і те ж саме для `user`. -Він виконає валідацію складених даних і задокументує їх відповідним чином у схемі OpenAPI та в автоматичній документації. +Він виконає валідацію складених даних і задокументує це таким чином у схемі OpenAPI та в автоматичній документації. -## Одиничні значення в тілі запиту +## Одиничні значення в тілі { #singular-values-in-body } Так само як є `Query` і `Path` для визначення додаткових даних для параметрів запиту та шляху, **FastAPI** надає еквівалентний `Body`. -Наприклад, розширюючи попередню модель, Ви можете вирішити додати ще один ключ `importance` в те ж саме тіло запиту разом із `item` і `user`. +Наприклад, розширивши попередню модель, ви можете вирішити додати ще один ключ `importance` у те саме тіло, окрім `item` і `user`. -Якщо Ви оголосите його як є, то, оскільки це одиничне значення, **FastAPI** припускатиме, що це параметр запиту (query parameter). +Якщо оголосити його як є, оскільки це одиничне значення, **FastAPI** припустить, що це параметр запиту. -Але Ви можете вказати **FastAPI** обробляти його як інший ключ тіла (body key), використовуючи `Body`: +Але ви можете вказати **FastAPI** обробляти його як інший ключ тіла, використовуючи `Body`: {* ../../docs_src/body_multiple_params/tutorial003_an_py310.py hl[23] *} -У цьому випадку **FastAPI** очікуватиме тіло запиту у такому вигляді: + +У цьому випадку **FastAPI** очікуватиме тіло такого вигляду: ```JSON { @@ -90,19 +93,20 @@ "importance": 5 } ``` -Знову ж таки, **FastAPI** конвертуватиме типи даних, перевірятиме їх, створюватиме документацію тощо. -## Декілька body та query параметрів +Знову ж таки, він перетворюватиме типи даних, перевірятиме, документуватиме тощо. -Звісно, Ви можете оголошувати додаткові query параметри запиту, коли це необхідно, на додаток до будь-яких параметрів тіла запиту. +## Декілька параметрів тіла та query { #multiple-body-params-and-query } -Оскільки за замовчуванням окремі значення інтерпретуються як параметри запиту, Вам не потрібно явно додавати `Query`, можна просто використати: +Звісно, ви також можете оголошувати додаткові query параметри щоразу, коли це потрібно, додатково до будь-яких параметрів тіла. + +Оскільки за замовчуванням одиничні значення інтерпретуються як параметри запиту, вам не потрібно явно додавати `Query`, ви можете просто зробити: ```Python q: Union[str, None] = None ``` -Або в Python 3.10 та вище: +Або в Python 3.10 і вище: ```Python q: str | None = None @@ -115,17 +119,17 @@ q: str | None = None /// info | Інформація -`Body` також має ті самі додаткові параметри валідації та метаданих, що й `Query`, `Path` та інші, які Ви побачите пізніше. +`Body` також має всі ті самі додаткові параметри валідації та метаданих, що й `Query`, `Path` та інші, які ви побачите пізніше. /// -## Вкладений поодинокий параметр тіла запиту +## Вбудувати один параметр тіла { #embed-a-single-body-parameter } -Припустимо, у вас є лише один параметр тіла запиту `item` з моделі Pydantic `Item`. +Скажімо, у вас є лише один параметр тіла `item` з моделі Pydantic `Item`. -За замовчуванням **FastAPI** очікуватиме, що тіло запиту міститиме вміст безпосередньо. +За замовчуванням **FastAPI** очікуватиме його тіло безпосередньо. -Але якщо Ви хочете, щоб він очікував JSON з ключем `item`, а всередині — вміст моделі (так, як це відбувається при оголошенні додаткових параметрів тіла), Ви можете використати спеціальний параметр `Body` — `embed`: +Але якщо ви хочете, щоб він очікував JSON з ключем `item`, а всередині нього — вміст моделі, як це відбувається, коли ви оголошуєте додаткові параметри тіла, ви можете використати спеціальний параметр `Body` — `embed`: ```Python item: Item = Body(embed=True) @@ -135,7 +139,8 @@ item: Item = Body(embed=True) {* ../../docs_src/body_multiple_params/tutorial005_an_py310.py hl[17] *} -У цьому випадку **FastAPI** очікуватиме тіло запиту такого вигляду: + +У цьому випадку **FastAPI** очікуватиме тіло такого вигляду: ```JSON hl_lines="2" { @@ -159,12 +164,12 @@ item: Item = Body(embed=True) } ``` -## Підсумок +## Підсумок { #recap } -Ви можете додавати кілька параметрів тіла до Вашої *функції операції шляху* (*path operation function*), навіть якщо запит може мати лише одне тіло. +Ви можете додавати кілька параметрів тіла до вашої *функції операції шляху*, навіть якщо запит може мати лише одне тіло. -Але **FastAPI** обробить це, надасть Вам потрібні дані у функції, перевірить їх та задокументує коректну схему в *операції шляху*. +Але **FastAPI** обробить це, надасть вам правильні дані у функції та перевірить і задокументує правильну схему в *операції шляху*. -Також Ви можете оголошувати окремі значення, які будуть отримані як частина тіла запиту. +Також ви можете оголошувати одиничні значення, щоб отримувати їх як частину тіла. -Крім того, Ви можете вказати **FastAPI** вбудовувати тіло в ключ, навіть якщо оголошено лише один параметр. +І ви можете вказати **FastAPI** вбудовувати тіло в ключ, навіть коли оголошено лише один параметр. diff --git a/docs/uk/docs/tutorial/body-nested-models.md b/docs/uk/docs/tutorial/body-nested-models.md index abc33f2eb3..6d0669358a 100644 --- a/docs/uk/docs/tutorial/body-nested-models.md +++ b/docs/uk/docs/tutorial/body-nested-models.md @@ -1,8 +1,8 @@ -# Тіло запиту - Вкладені моделі +# Тіло - Вкладені моделі { #body-nested-models } -З **FastAPI** Ви можете визначати, перевіряти, документувати та використовувати моделі, які можуть бути вкладені на будь-яку глибину (завдяки Pydantic). +З **FastAPI** ви можете визначати, перевіряти, документувати та використовувати моделі, які можуть бути вкладені на будь-яку глибину (завдяки Pydantic). -## Поля списку +## Поля списку { #list-fields } Ви можете визначити атрибут як підтип. Наприклад, Python-список (`list`): @@ -10,47 +10,28 @@ Це зробить `tags` списком, хоча не визначається тип елементів списку. -## Поля списку з параметром типу +## Поля списку з параметром типу { #list-fields-with-type-parameter } -Але Python має специфічний спосіб оголошення списків з внутрішніми типами або "параметрами типу": -### Імпортуємо `List` з модуля typing +Але Python має специфічний спосіб оголошення списків з внутрішніми типами або «параметрами типу»: -У Python 3.9 і вище можна використовувати стандартний `list` для оголошення таких типів, як ми побачимо нижче. 💡 +### Оголошення `list` з параметром типу { #declare-a-list-with-a-type-parameter } -Але в Python версії до 3.9 (від 3.6 і вище) спочатку потрібно імпортувати `List` з модуля стандартної бібліотеки Python `typing`: - -{* ../../docs_src/body_nested_models/tutorial002.py hl[1] *} - -### Оголошення `list` з параметром типу - -Щоб оголосити типи з параметрами типу (внутрішніми типами), такими як `list`, `dict`, `tuple`: - -* Якщо Ви використовуєте версію Python до 3.9, імпортуйте їх відповідну версію з модуля `typing`. -* Передайте внутрішні типи як "параметри типу", використовуючи квадратні дужки: `[` and `]`. - -У Python 3.9 це буде виглядати так: +Щоб оголосити типи з параметрами типу (внутрішніми типами), такими як `list`, `dict`, `tuple`, +передайте внутрішні тип(и) як «параметри типу», використовуючи квадратні дужки: `[` and `]` ```Python my_list: list[str] ``` -У версіях Python до 3.9 це виглядає так: - -```Python -from typing import List - -my_list: List[str] -``` - Це стандартний синтаксис Python для оголошення типів. Використовуйте той самий стандартний синтаксис для атрибутів моделей з внутрішніми типами. -Отже, у нашому прикладі, ми можемо зробити `tags` саме "списком рядків": +Отже, у нашому прикладі, ми можемо зробити `tags` саме «списком рядків»: {* ../../docs_src/body_nested_models/tutorial002_py310.py hl[12] *} -## Типи множин +## Типи множин { #set-types } Але потім ми подумали, що теги не повинні повторюватися, вони, ймовірно, повинні бути унікальними рядками. @@ -60,29 +41,29 @@ my_list: List[str] {* ../../docs_src/body_nested_models/tutorial003_py310.py hl[12] *} -Навіть якщо Ви отримаєте запит з дубльованими даними, він буде перетворений у множину унікальних елементів. +Навіть якщо ви отримаєте запит з дубльованими даними, він буде перетворений у множину унікальних елементів. -І коли Ви будете виводити ці дані, навіть якщо джерело містить дублікати, вони будуть виведені як множина унікальних елементів. +І коли ви будете виводити ці дані, навіть якщо джерело містить дублікати, вони будуть виведені як множина унікальних елементів. І це буде анотовано/документовано відповідно. -## Вкладені моделі +## Вкладені моделі { #nested-models } Кожен атрибут моделі Pydantic має тип. Але цей тип сам може бути іншою моделлю Pydantic. -Отже, Ви можете оголосити глибоко вкладені JSON "об'єкти" з конкретними іменами атрибутів, типами та перевірками. +Отже, ви можете оголосити глибоко вкладені JSON «об'єкти» з конкретними іменами атрибутів, типами та перевірками. Усе це, вкладене без обмежень. -### Визначення підмоделі +### Визначення підмоделі { #define-a-submodel } Наприклад, ми можемо визначити модель `Image`: {* ../../docs_src/body_nested_models/tutorial004_py310.py hl[7:9] *} -### Використання підмоделі як типу +### Використання підмоделі як типу { #use-the-submodel-as-a-type } А потім ми можемо використовувати її як тип атрибута: @@ -104,16 +85,16 @@ my_list: List[str] } ``` -Завдяки такій декларації у **FastAPI** Ви отримуєте: +Завдяки такій декларації у **FastAPI** ви отримуєте: * Підтримку в редакторі (автозавершення тощо), навіть для вкладених моделей * Конвертацію даних * Валідацію даних * Автоматичну документацію -## Спеціальні типи та валідація +## Спеціальні типи та валідація { #special-types-and-validation } -Окрім звичайних типів, таких як `str`, `int`, `float`, та ін. Ви можете використовувати складніші типи, які наслідують `str`. +Окрім звичайних типів, таких як `str`, `int`, `float`, та ін. ви можете використовувати складніші типи, які наслідують `str`. Щоб побачити всі доступні варіанти, ознайомтеся з оглядом типів у Pydantic. Деякі приклади будуть у наступних розділах. @@ -123,9 +104,9 @@ my_list: List[str] Рядок буде перевірено як дійсну URL-адресу і задокументовано в JSON Schema / OpenAPI як URL. -## Атрибути зі списками підмоделей +## Атрибути зі списками підмоделей { #attributes-with-lists-of-submodels } -У Pydantic Ви можете використовувати моделі як підтипи для `list`, `set` тощо: +У Pydantic ви можете використовувати моделі як підтипи для `list`, `set` тощо: {* ../../docs_src/body_nested_models/tutorial006_py310.py hl[18] *} @@ -161,7 +142,7 @@ my_list: List[str] /// -## Глибоко вкладені моделі +## Глибоко вкладені моделі { #deeply-nested-models } Ви можете визначати вкладені моделі довільної глибини: @@ -173,14 +154,9 @@ my_list: List[str] /// -## Тіла запитів, що складаються зі списків +## Тіла запитів, що складаються зі списків { #bodies-of-pure-lists } -Якщо верхній рівень JSON тіла, яке Ви очікуєте, є JSON `масивом` (у Python — `list`), Ви можете оголосити тип у параметрі функції, як і в моделях Pydantic: - -```Python -images: List[Image] -``` -або в Python 3.9 і вище: +Якщо верхній рівень JSON тіла, яке ви очікуєте, є JSON `масивом` (у Python — `list`), ви можете оголосити тип у параметрі функції, як і в моделях Pydantic: ```Python images: list[Image] @@ -190,7 +166,7 @@ images: list[Image] {* ../../docs_src/body_nested_models/tutorial008_py39.py hl[13] *} -## Підтримка в редакторі всюди +## Підтримка в редакторі всюди { #editor-support-everywhere } Ви отримаєте підтримку в редакторі всюди. @@ -200,23 +176,23 @@ images: list[Image] Ви не змогли б отримати таку підтримку в редакторі, якби працювали напряму зі `dict`, а не з моделями Pydantic. -Але Вам не потрібно турбуватися про це: вхідні dict'и автоматично конвертуються, а вихідні дані автоматично перетворюються в JSON. +Але вам не потрібно турбуватися про це: вхідні dict'и автоматично конвертуються, а вихідні дані автоматично перетворюються в JSON. -## Тіла з довільними `dict` +## Тіла з довільними `dict` { #bodies-of-arbitrary-dicts } Ви також можете оголосити тіло як `dict` з ключами одного типу та значеннями іншого типу. -Це корисно, якщо Ви не знаєте наперед, які імена полів будуть дійсними (як у випадку з моделями Pydantic). +Це корисно, якщо ви не знаєте наперед, які імена полів будуть дійсними (як у випадку з моделями Pydantic). -Це буде корисно, якщо Ви хочете приймати ключі, які заздалегідь невідомі. +Це буде корисно, якщо ви хочете приймати ключі, які заздалегідь невідомі. --- -Це також зручно, якщо Ви хочете мати ключі іншого типу (наприклад, `int`). +Це також зручно, якщо ви хочете мати ключі іншого типу (наприклад, `int`). Ось що ми розглянемо далі. -У цьому випадку Ви можете приймати будь-який `dict`, якщо його ключі — це `int`, а значення — `float`: +У цьому випадку ви можете приймати будь-який `dict`, якщо його ключі — це `int`, а значення — `float`: {* ../../docs_src/body_nested_models/tutorial009_py39.py hl[7] *} @@ -228,18 +204,18 @@ images: list[Image] Це означає, що навіть якщо клієнти вашого API надсилатимуть ключі у вигляді рядків, якщо вони містять цілі числа, Pydantic конвертує їх і проведе валідацію. -Тобто `dict`, який Ви отримаєте як `weights`, матиме ключі типу `int` та значення типу `float`. +Тобто `dict`, який ви отримаєте як `weights`, матиме ключі типу `int` та значення типу `float`. /// -## Підсумок +## Підсумок { #recap } -З **FastAPI** Ви маєте максимальну гнучкість завдяки моделям Pydantic, зберігаючи при цьому код простим, коротким та елегантним. +З **FastAPI** ви маєте максимальну гнучкість завдяки моделям Pydantic, зберігаючи при цьому код простим, коротким та елегантним. А також отримуєте всі переваги: * Підтримка в редакторі (автодоповнення всюди!) * Конвертація даних (парсинг/сериалізація) -* Валідація даних +* Валідацію даних * Документація схем * Автоматичне створення документації diff --git a/docs/uk/docs/tutorial/body-updates.md b/docs/uk/docs/tutorial/body-updates.md index e78b5a5bf3..2ae68291ca 100644 --- a/docs/uk/docs/tutorial/body-updates.md +++ b/docs/uk/docs/tutorial/body-updates.md @@ -1,8 +1,8 @@ -# Тіло – Оновлення +# Тіло — Оновлення { #body-updates } -## Оновлення з використанням `PUT` +## Оновлення із заміною за допомогою `PUT` { #update-replacing-with-put } -Щоб оновити елемент, Ви можете використати HTTP `PUT` операцію. +Щоб оновити елемент, ви можете використати HTTP `PUT` операцію. Ви можете використати `jsonable_encoder`, щоб перетворити вхідні дані на такі, які можна зберігати як JSON (наприклад, у NoSQL базі даних). Наприклад, перетворюючи `datetime` у `str`. @@ -10,7 +10,7 @@ `PUT` використовується для отримання даних, які мають замінити чинні дані. -### Попередження про заміну +### Попередження про заміну { #warning-about-replacing } Це означає, що якщо Ви хочете оновити елемент `bar`, використовуючи `PUT` з тілом: @@ -26,7 +26,7 @@ І дані будуть збережені з цим "новим" значенням `tax` = `10.5`. -## Часткові оновлення з `PATCH` +## Часткові оновлення з `PATCH` { #partial-updates-with-patch } Ви також можете використовувати операцію HTTP `PATCH` для *часткового* оновлення даних. @@ -34,53 +34,37 @@ /// note | Примітка -`PATCH` менш відомий і рідше використовується, ніж `PUT`. +`PATCH` менш поширений і менш відомий, ніж `PUT`. І багато команд використовують лише `PUT`, навіть для часткових оновлень. -Ви **вільні** використовувати їх так, як хочете, **FastAPI** не накладає обмежень. +Ви **вільні** використовувати їх так, як хочете, **FastAPI** не накладає жодних обмежень. -Але цей посібник показує Вам більш-менш як їх задумано використовувати. +Але цей посібник показує вам, більш-менш, як їх задумано використовувати. /// -### Використання параметра `exclude_unset` у Pydantic +### Використання параметра `exclude_unset` у Pydantic { #using-pydantics-exclude-unset-parameter } -Якщо Ви хочете отримати часткові оновлення, дуже зручно використовувати параметр `exclude_unset` у методі `.model_dump()` моделі Pydantic. +Якщо Ви хочете отримувати часткові оновлення, дуже корисно використовувати параметр `exclude_unset` у `.model_dump()` моделі Pydantic. Наприклад: `item.model_dump(exclude_unset=True)`. -/// info | Інформація +Це згенерує `dict` лише з тими даними, які були встановлені під час створення моделі `item`, виключаючи значення за замовчуванням. -У Pydantic v1 цей метод називався `.dict()`, він був застарілий (але все ще підтримується) у Pydantic v2, і був перейменований у `.model_dump()`. - -Приклади тут використовують `.dict()` для сумісності з Pydantic v1, але Вам слід використовувати `.model_dump()`, якщо можете використовувати Pydantic v2. - -/// - -Це створить `dict` лише з тими даними, які були явно встановлені під час створення моделі `item`, виключаючи значення за замовчуванням. - -Тоді Ви можете використовувати це, щоб створити `dict` лише з даними, які були встановлені (надіслані у запиті), пропускаючи значення за замовчуванням: +Тоді Ви можете використовувати це, щоб згенерувати `dict` лише з даними, які були встановлені (надіслані у запиті), пропускаючи значення за замовчуванням: {* ../../docs_src/body_updates/tutorial002_py310.py hl[32] *} -### Використання параметра `update` у Pydantic +### Використання параметра `update` у Pydantic { #using-pydantics-update-parameter } -Тепер Ви можете створити копію наявної моделі за допомогою `.model_copy()`, і передати параметр `update` з `dict` , який містить дані для оновлення. - -/// info | Інформація - -У Pydantic v1 метод називався `.copy()`, він був застарілий (але все ще підтримується) у Pydantic v2, і був перейменований у `.model_copy()`. - -Приклади тут використовують `.copy()` для сумісності з Pydantic v1, але якщо Ви можете використовувати Pydantic v2 — Вам слід використовувати `.model_copy()` замість цього. - -/// +Тепер Ви можете створити копію наявної моделі за допомогою `.model_copy()`, і передати параметр `update` з `dict`, який містить дані для оновлення. Наприклад: `stored_item_model.model_copy(update=update_data)`: {* ../../docs_src/body_updates/tutorial002_py310.py hl[33] *} -### Підсумок часткових оновлень +### Підсумок часткових оновлень { #partial-updates-recap } У підсумку, щоб застосувати часткові оновлення, Ви: @@ -101,7 +85,7 @@ Насправді Ви можете використовувати цю саму техніку і з операцією HTTP `PUT`. -Але приклад тут використовує `PATCH`, тому що він був створений саме для таких випадків. +Але приклад тут використовує `PATCH`, тому що він був створений для таких випадків. /// @@ -109,7 +93,7 @@ Зверніть увагу, що модель запиту все ще проходить валідацію. -Тож, якщо Ви хочете отримувати часткові оновлення, які можуть не містити жодного атрибута, Вам потрібно мати модель, де всі атрибути позначені як необов’язкові (зі значеннями за замовчуванням або `None`). +Тож, якщо Ви хочете отримувати часткові оновлення, які можуть пропускати всі атрибути, Вам потрібно мати модель, де всі атрибути позначені як необов’язкові (зі значеннями за замовчуванням або `None`). Щоб розрізняти моделі з усіма необов’язковими значеннями для **оновлення** і моделі з обов’язковими значеннями для **створення**, Ви можете скористатись ідеями, описаними у [Додаткові моделі](extra-models.md){.internal-link target=_blank}. diff --git a/docs/uk/docs/tutorial/body.md b/docs/uk/docs/tutorial/body.md index 38fed7bb8d..ca1f308ab5 100644 --- a/docs/uk/docs/tutorial/body.md +++ b/docs/uk/docs/tutorial/body.md @@ -1,14 +1,14 @@ -# Тіло запиту +# Тіло запиту { #request-body } Коли вам потрібно надіслати дані з клієнта (скажімо, браузера) до вашого API, ви надсилаєте їх як **тіло запиту**. Тіло **запиту** — це дані, надіслані клієнтом до вашого API. Тіло **відповіді** — це дані, які ваш API надсилає клієнту. -Ваш API майже завжди має надсилати тіло **відповіді**. Але клієнтам не обов’язково потрібно постійно надсилати тіла **запитів**. +Ваш API майже завжди має надсилати тіло **відповіді**. Але клієнтам не обов’язково потрібно постійно надсилати тіла **запитів** — інколи вони лише запитують шлях, можливо з деякими параметрами запиту, але не надсилають тіло. Щоб оголосити тіло **запиту**, ви використовуєте Pydantic моделі з усією їх потужністю та перевагами. -/// info +/// info | Інформація Щоб надіслати дані, ви повинні використовувати один із: `POST` (більш поширений), `PUT`, `DELETE` або `PATCH`. @@ -18,21 +18,22 @@ /// -## Імпортуйте `BaseModel` від Pydantic +## Імпортуйте `BaseModel` від Pydantic { #import-pydantics-basemodel } Спочатку вам потрібно імпортувати `BaseModel` з `pydantic`: -{* ../../docs_src/body/tutorial001.py hl[4] *} +{* ../../docs_src/body/tutorial001_py310.py hl[2] *} -## Створіть свою модель даних +## Створіть свою модель даних { #create-your-data-model } Потім ви оголошуєте свою модель даних як клас, який успадковується від `BaseModel`. Використовуйте стандартні типи Python для всіх атрибутів: -{* ../../docs_src/body/tutorial001.py hl[7:11] *} +{* ../../docs_src/body/tutorial001_py310.py hl[5:9] *} -Так само, як і при оголошенні параметрів запиту, коли атрибут моделі має значення за замовчуванням, він не є обов’язковим. В іншому випадку це потрібно. Використовуйте `None`, щоб зробити його необов'язковим. + +Так само, як і при оголошенні параметрів запиту, коли атрибут моделі має значення за замовчуванням, він не є обов’язковим. В іншому випадку це потрібно. Використовуйте `None`, щоб зробити його просто необов'язковим. Наприклад, ця модель вище оголошує JSON "`об'єкт`" (або Python `dict`), як: @@ -54,15 +55,15 @@ } ``` -## Оголоси її як параметр +## Оголосіть її як параметр { #declare-it-as-a-parameter } Щоб додати модель даних до вашої *операції шляху*, оголосіть її так само, як ви оголосили параметри шляху та запиту: -{* ../../docs_src/body/tutorial001.py hl[18] *} +{* ../../docs_src/body/tutorial001_py310.py hl[16] *} ...і вкажіть її тип як модель, яку ви створили, `Item`. -## Результати +## Результати { #results } Лише з цим оголошенням типу Python **FastAPI** буде: @@ -73,9 +74,9 @@ * Надавати отримані дані у параметрі `item`. * Оскільки ви оголосили його у функції як тип `Item`, ви також матимете всю підтримку редактора (автозаповнення, тощо) для всіх атрибутів та їх типів. * Генерувати JSON Schema визначення для вашої моделі, ви також можете використовувати їх де завгодно, якщо це має сенс для вашого проекту. -* Ці схеми будуть частиною згенерованої схеми OpenAPI і використовуватимуться автоматичною документацією інтерфейсу користувача. +* Ці схеми будуть частиною згенерованої схеми OpenAPI і використовуватимуться автоматичною документацією UIs. -## Автоматична документація +## Автоматична документація { #automatic-docs } Схеми JSON ваших моделей будуть частиною вашої схеми, згенерованої OpenAPI, і будуть показані в інтерактивній API документації: @@ -85,7 +86,7 @@ -## Підтримка редактора +## Підтримка редактора { #editor-support } У вашому редакторі, всередині вашої функції, ви будете отримувати підказки типу та завершення скрізь (це б не сталося, якби ви отримали `dict` замість моделі Pydantic): @@ -107,7 +108,7 @@ -/// tip +/// tip | Порада Якщо ви використовуєте PyCharm як ваш редактор, ви можете використати Pydantic PyCharm Plugin. @@ -121,42 +122,45 @@ /// -## Використовуйте модель +## Використовуйте модель { #use-the-model } Усередині функції ви можете отримати прямий доступ до всіх атрибутів об’єкта моделі: -{* ../../docs_src/body/tutorial002.py hl[21] *} +{* ../../docs_src/body/tutorial002_py310.py *} -## Тіло запиту + параметри шляху +## Тіло запиту + параметри шляху { #request-body-path-parameters } Ви можете одночасно оголошувати параметри шляху та тіло запиту. **FastAPI** розпізнає, що параметри функції, які відповідають параметрам шляху, мають бути **взяті з шляху**, а параметри функції, які оголошуються як моделі Pydantic, **взяті з тіла запиту**. -{* ../../docs_src/body/tutorial003.py hl[17:18] *} +{* ../../docs_src/body/tutorial003_py310.py hl[15:16] *} -## Тіло запиту + шлях + параметри запиту + +## Тіло запиту + шлях + параметри запиту { #request-body-path-query-parameters } Ви також можете оголосити параметри **тіло**, **шлях** і **запит** одночасно. **FastAPI** розпізнає кожен з них і візьме дані з потрібного місця. -{* ../../docs_src/body/tutorial004.py hl[18] *} +{* ../../docs_src/body/tutorial004_py310.py hl[16] *} Параметри функції будуть розпізнаватися наступним чином: * Якщо параметр також оголошено в **шляху**, він використовуватиметься як параметр шляху. * Якщо параметр має **сингулярний тип** (наприклад, `int`, `float`, `str`, `bool` тощо), він буде інтерпретуватися як параметр **запиту**. -* Якщо параметр оголошується як тип **Pydantic моделі**, він інтерпретується як **тіло** запиту. +* Якщо параметр оголошується як тип **Pydantic моделі**, він інтерпретується як **тіло** **запиту**. -/// note +/// note | Примітка -FastAPI буде знати, що значення "q" не є обов'язковим через значення за замовчуванням "= None". +FastAPI буде знати, що значення `q` не є обов'язковим через значення за замовчуванням `= None`. -`Optional` у `Optional[str]` не використовується FastAPI, але дозволить вашому редактору надати вам кращу підтримку та виявляти помилки. +`str | None` (Python 3.10+) або `Union` у `Union[str, None]` (Python 3.9+) FastAPI не використовує, щоб визначити, що значення не є обов’язковим — він знатиме, що воно не є обов’язковим, тому що має значення за замовчуванням `= None`. + +Але додавання анотацій типів дозволить вашому редактору надати вам кращу підтримку та виявляти помилки. /// -## Без Pydantic +## Без Pydantic { #without-pydantic } -Якщо ви не хочете використовувати моделі Pydantic, ви також можете використовувати параметри **Body**. Перегляньте документацію для [Тіло – Кілька параметрів: сингулярні значення в тілі](body-multiple-params.md#singular-values-in-body){.internal-link target=_blank}. +Якщо ви не хочете використовувати моделі Pydantic, ви також можете використовувати параметри **Body**. Перегляньте документацію для [Body - Multiple Parameters: Singular values in body](body-multiple-params.md#singular-values-in-body){.internal-link target=_blank}. diff --git a/docs/uk/docs/tutorial/cookie-param-models.md b/docs/uk/docs/tutorial/cookie-param-models.md index f070b6ac83..3c6407716e 100644 --- a/docs/uk/docs/tutorial/cookie-param-models.md +++ b/docs/uk/docs/tutorial/cookie-param-models.md @@ -1,32 +1,32 @@ -# Моделі для Cookie-параметрів +# Моделі параметрів Cookie { #cookie-parameter-models } -Якщо у Вас є група **cookies** параметрів, які пов'язані між собою, Ви можете створити **Pydantic-модель**, щоб оголосити їх. 🍪 +Якщо у Вас є група **cookies**, які пов'язані між собою, Ви можете створити **Pydantic-модель**, щоб оголосити їх. 🍪 Це дозволить Вам повторно **використовувати модель** у **різних місцях**, а також оголосити валідацію та метадані для всіх параметрів одночасно. 😎 -/// note | Нотатки +/// note | Примітка -Це підтримується з версії FastAPI `0.115.0`. 🤓 +Це підтримується з версії FastAPI `0.115.0`. 🤓 /// /// tip | Порада -Ця ж техніка застосовується до `Query`, `Cookie`, та `Header`. 😎 +Ця ж техніка застосовується до `Query`, `Cookie` та `Header`. 😎 /// -## Cookie з Pydantic-моделлю +## Cookie з Pydantic-моделлю { #cookies-with-a-pydantic-model } -Оголосіть **cookie-параметри**, які Вам потрібні, у **Pydantic-моделі**, а потім оголосіть параметр як `Cookie`: +Оголосіть **cookie**-параметри, які Вам потрібні, у **Pydantic-моделі**, а потім оголосіть параметр як `Cookie`: {* ../../docs_src/cookie_param_models/tutorial001_an_py310.py hl[9:12,16] *} -**FastAPI** буде **витягувати** дані для **кожного поля** з **cookie** параметрів, отриманих у запиті, і передавати Вам Pydantic-модель, яку Ви визначили. +**FastAPI** буде **витягувати** дані для **кожного поля** з **cookies**, отриманих у запиті, і передавати Вам Pydantic-модель, яку Ви визначили. -## Перевірка у документації +## Перевірка у документації { #check-the-docs } -Ви можете побачити визначені cookie в інтерфейсі документації за адресою `/docs`: +Ви можете побачити визначені cookies в інтерфейсі документації за адресою `/docs`:
@@ -34,29 +34,29 @@ /// info | Інформація -Майте на увазі, що оскільки **браузери обробляють cookie** особливим чином і "за лаштунками", вони **не** дозволяють **JavaScript** легко з ними працювати. +Майте на увазі, що оскільки **браузери обробляють cookies** особливим чином і «за лаштунками», вони **не** дозволяють **JavaScript** легко з ними працювати. -Якщо Ви зайдете до **інтерфейсу документації API** за адресою `/docs`, Ви зможете побачити **документацію** для cookie у Ваших **операціях шляху**. +Якщо Ви зайдете до **інтерфейсу документації API** за адресою `/docs`, Ви зможете побачити **документацію** для cookies у Ваших *операціях шляху*. -Але навіть якщо Ви заповните дані й натиснете "Execute", оскільки інтерфейс документації працює з **JavaScript**, cookie не будуть відправлені, і Ви побачите **помилку**, ніби Ви не ввели жодних значень. +Але навіть якщо Ви заповните дані й натиснете "Execute", оскільки інтерфейс документації працює з **JavaScript**, cookies не будуть відправлені, і Ви побачите **помилку**, ніби Ви не ввели жодних значень. /// -## Заборона додаткових cookie +## Заборона додаткових cookie { #forbid-extra-cookies } -У деяких спеціальних випадках (ймовірно, не дуже поширених) Ви можете захотіти **обмежити** список cookie, які хочете отримувати. +У деяких спеціальних випадках (ймовірно, не дуже поширених) Ви можете захотіти **обмежити** cookies, які хочете отримувати. -Ваша API тепер має можливість контролювати власну згоду на cookie. 🤪🍪 +Ваша API тепер має можливість контролювати власну згоду на cookie. 🤪🍪 -Ви можете використовувати налаштування моделі Pydantic, щоб `заборонити` будь-які `додаткові` поля: +Ви можете використовувати налаштування моделі Pydantic, щоб `forbid` будь-які `extra` поля: -{* ../../docs_src/cookie_param_models/tutorial002_an_py39.py hl[10] *} +{* ../../docs_src/cookie_param_models/tutorial002_an_py310.py hl[10] *} -Якщо клієнт спробує надіслати якісь **додаткові cookie**, він отримає відповідь з **помилкою**. +Якщо клієнт спробує надіслати якісь **додаткові cookies**, він отримає відповідь з **помилкою**. -Бідні банери cookie, які так старанно намагаються отримати Вашу згоду, щоб API її відхилила. 🍪 +Бідні банери cookie, які так старанно намагаються отримати Вашу згоду, щоб API її відхилила. 🍪 -Наприклад, якщо клієнт спробує надіслати cookie `santa_tracker` зі значенням `good-list-please`, він отримає відповідь з помилкою, яка повідомить, що cookie `santa_tracker` не дозволено: +Наприклад, якщо клієнт спробує надіслати cookie `santa_tracker` зі значенням `good-list-please`, він отримає відповідь з помилкою, яка повідомить, що `santa_tracker` cookie не дозволено: ```json { @@ -71,6 +71,6 @@ } ``` -## Підсумок +## Підсумок { #summary } -Ви можете використовувати **Pydantic-моделі** для оголошення cookie у FastAPI. 😎 +Ви можете використовувати **Pydantic-моделі** для оголошення **cookies** у **FastAPI**. 😎 diff --git a/docs/uk/docs/tutorial/cookie-params.md b/docs/uk/docs/tutorial/cookie-params.md index b320645cb3..8a5b44e8aa 100644 --- a/docs/uk/docs/tutorial/cookie-params.md +++ b/docs/uk/docs/tutorial/cookie-params.md @@ -1,24 +1,25 @@ -# Параметри Cookie +# Параметри Cookie { #cookie-parameters } -Ви можете визначити параметри Cookie таким же чином, як визначаються параметри `Query` і `Path`. +Ви можете визначати параметри Cookie таким же чином, як визначаються параметри `Query` і `Path`. -## Імпорт `Cookie` +## Імпорт `Cookie` { #import-cookie } Спочатку імпортуйте `Cookie`: {* ../../docs_src/cookie_params/tutorial001_an_py310.py hl[3] *} -## Визначення параметрів `Cookie` +## Визначення параметрів `Cookie` { #declare-cookie-parameters } Потім визначте параметри cookie, використовуючи таку ж конструкцію як для `Path` і `Query`. -Перше значення це значення за замовчуванням, ви можете також передати всі додаткові параметри валідації чи анотації: +Ви можете визначити значення за замовчуванням, а також усі додаткові параметри валідації чи анотації: {* ../../docs_src/cookie_params/tutorial001_an_py310.py hl[9] *} -/// note | Технічні Деталі +/// note | Технічні деталі + +`Cookie` це "сестра" класів `Path` і `Query`. Вони також наслідуються від одного спільного класу `Param`. -`Cookie` це "сестра" класів `Path` і `Query`. Вони наслідуються від одного батьківського класу `Param`. Але пам'ятайте, що коли ви імпортуєте `Query`, `Path`, `Cookie` та інше з `fastapi`, це фактично функції, що повертають спеціальні класи. /// @@ -29,6 +30,16 @@ /// -## Підсумки +/// info + +Майте на увазі, що оскільки **браузери обробляють cookies** спеціальним чином і за лаштунками, вони **не** дозволяють **JavaScript** легко взаємодіяти з ними. + +Якщо ви перейдете до **інтерфейсу документації API** за адресою `/docs`, ви зможете побачити **документацію** для cookies для ваших *операцій шляху*. + +Але навіть якщо ви **заповните дані** і натиснете "Execute", оскільки інтерфейс документації працює з **JavaScript**, cookies не буде надіслано, і ви побачите повідомлення про **помилку**, ніби ви не ввели жодних значень. + +/// + +## Підсумки { #recap } Визначайте cookies за допомогою `Cookie`, використовуючи той же спільний шаблон, що і `Query` та `Path`. diff --git a/docs/uk/docs/tutorial/cors.md b/docs/uk/docs/tutorial/cors.md index 95b204d0f7..f3ed8a7d94 100644 --- a/docs/uk/docs/tutorial/cors.md +++ b/docs/uk/docs/tutorial/cors.md @@ -1,8 +1,8 @@ -# CORS (Обмін ресурсами між різними джерелами) +# CORS (Обмін ресурсами між різними джерелами) { #cors-cross-origin-resource-sharing } -CORS або "Обмін ресурсами між різними джерелами" є ситуація, коли фронтенд, що працює в браузері, містить JavaScript-код, який взаємодіє з бекендом, розташованим в іншому "джерелі" (origin). +CORS або "Cross-Origin Resource Sharing" є ситуація, коли фронтенд, що працює в браузері, містить JavaScript-код, який взаємодіє з бекендом, розташованим в іншому "джерелі" (origin). -## Джерело (Origin) +## Джерело (Origin) { #origin } Джерело визначається комбінацією протоколу (`http`, `https`), домену (`myapp.com`, `localhost`, `localhost.tiangolo.com`), порту (`80`, `443`, `8080`). @@ -15,7 +15,7 @@ Навіть якщо вони всі містять `localhost`, вони мають різні протоколи або порти, що робить їх окремими "джерелами". -## Кроки +## Кроки { #steps } Припустимо, що Ваш фронтенд працює в браузері на `http://localhost:8080`, а його JavaScript намагається відправити запит до бекенду, який працює на `http://localhost` (Оскільки ми не вказуємо порт, браузер за замовчуванням припускає порт `80`). @@ -25,15 +25,15 @@ У цьому випадку список має містити `http://localhost:8080`, щоб фронтенд на порту `:8080` працював коректно. -## Символьне підставляння +## Символьне підставляння { #wildcards } Можна також оголосити список як `"*"` ("символьне підставляння"), що означає дозвіл для всіх джерел. -Однак це дозволить лише певні типи комунікації, виключаючи все, що пов'язане з обліковими даними: Cookies, заголовки авторизації, такі як ті, що використовуються з Bearer токенами тощо. +Однак це дозволить лише певні типи комунікації, виключаючи все, що пов'язане з обліковими даними: Cookies, заголовки авторизації, такі як ті, що використовуються з Bearer Tokens, тощо. Тому для коректної роботи краще явно вказувати дозволені джерела. -## Використання `CORSMiddleware` +## Використання `CORSMiddleware` { #use-corsmiddleware } Ви можете налаштувати це у Вашому додатку **FastAPI** за допомогою `CORSMiddleware`. @@ -44,41 +44,44 @@ Також можна вказати, чи дозволяє Ваш бекенд: -* Облікові дані (заголовки авторизації, сookies, тощо). +* Облікові дані (заголовки авторизації, Cookies, тощо). * Конкретні HTTP-методи (`POST`, `PUT`) або всі за допомогою `"*"` * Конкретні HTTP-заголовки або всі за допомогою `"*"`. -{* ../../docs_src/cors/tutorial001.py hl[2,6:11,13:19] *} +{* ../../docs_src/cors/tutorial001_py39.py hl[2,6:11,13:19] *} -Параметри за замовчуванням у `CORSMiddleware` є досить обмеженими, тому Вам потрібно явно вказати конкретні джерела, методи або заголовки, щоб браузери могли використовувати їх у контексті запитів між різними доменами. +Параметри за замовчуванням у реалізації `CORSMiddleware` є досить обмеженими, тому Вам потрібно явно увімкнути конкретні джерела, методи або заголовки, щоб браузерам було дозволено використовувати їх у міждоменному контексті. Підтримуються такі аргументи: -* `allow_origins` - Список джерел, яким дозволено здійснювати міждоменні запити. Наприклад `['https://example.org', 'https://www.example.org']`. Ви можете використовувати ['*'], щоб дозволити всі джерела. +* `allow_origins` - Список джерел, яким дозволено здійснювати міждоменні запити. Наприклад `['https://example.org', 'https://www.example.org']`. Ви можете використовувати `['*']`, щоб дозволити будь-яке джерело. * `allow_origin_regex` - Рядок регулярного виразу для відповідності джерелам, яким дозволено здійснювати міждоменні запити. Наприклад, `'https://.*\.example\.org'`. * `allow_methods` - Список HTTP-методів, дозволених для міждоменних запитів. За замовчуванням `['GET']`. Ви можете використовувати `['*']`, щоб дозволити всі стандартні методи. -* `allow_headers` - Список HTTP-заголовків, які підтримуються для міждоменних запитів. За замовчуванням `[]`. Ви можете використовувати `['*']`, щоб дозволити всі заголовки. Заголовки `Accept`, `Accept-Language`, `Content-Language` і `Content-Type` завжди дозволені для простих CORS-запитів. -* `allow_credentials` - Визначає, чи підтримуються файли cookie для міждоменних запитів. За замовчуванням `False`. Також, якщо потрібно дозволити обмін обліковими даними (`allow_credentials = True`), параметр `allow_origins` не може бути встановлений як `['*']`, необхідно вказати конкретні джерела. +* `allow_headers` - Список HTTP-заголовків запиту, які підтримуються для міждоменних запитів. За замовчуванням `[]`. Ви можете використовувати `['*']`, щоб дозволити всі заголовки. Заголовки `Accept`, `Accept-Language`, `Content-Language` і `Content-Type` завжди дозволені для простих CORS-запитів. +* `allow_credentials` - Визначає, чи повинні підтримуватися cookies для міждоменних запитів. За замовчуванням `False`. + + Жоден із параметрів `allow_origins`, `allow_methods` і `allow_headers` не можна встановлювати як `['*']`, якщо `allow_credentials` встановлено як `True`. Усі вони мають бути явно вказані. + * `expose_headers` - Вказує, які заголовки відповіді повинні бути доступні для браузера. За замовчуванням `[]`. * `max_age` - Встановлює максимальний час (у секундах) для кешування CORS-відповідей у браузерах. За замовчуванням `600`. Цей middleware обробляє два типи HTTP-запитів... -### Попередні CORS-запити (preflight requests) +### Попередні CORS-запити { #cors-preflight-requests } Це будь-які `OPTIONS` - запити, що містять заголовки `Origin` та `Access-Control-Request-Method`. У такому випадку middleware перехопить вхідний запит і відповість відповідними CORS-заголовками, повертаючи або `200`, або `400` для інформаційних цілей. -### Прості запити +### Прості запити { #simple-requests } Будь-які запити із заголовком `Origin`. У цьому випадку middleware пропустить запит як звичайний, але додасть відповідні CORS-заголовки у відповідь. -## Додаткова інформація +## Додаткова інформація { #more-info } -Більше про CORS можна дізнатися в документації Mozilla. +Більше про CORS можна дізнатися в документації Mozilla про CORS. /// note | Технічні деталі diff --git a/docs/uk/docs/tutorial/debugging.md b/docs/uk/docs/tutorial/debugging.md index b0e5344f81..0db418dcc9 100644 --- a/docs/uk/docs/tutorial/debugging.md +++ b/docs/uk/docs/tutorial/debugging.md @@ -1,16 +1,16 @@ -# Налагодження (Debugging) +# Налагодження { #debugging } -Ви можете під'єднати дебагер у Вашому редакторі коду, наприклад, у Visual Studio Code або PyCharm. +Ви можете під'єднати дебагер у вашому редакторі коду, наприклад, у Visual Studio Code або PyCharm. -## Виклик `uvicorn` +## Виклик `uvicorn` { #call-uvicorn } -У Вашому FastAPI-додатку імпортуйте та запустіть `uvicorn` безпосередньо: +У вашому FastAPI-додатку імпортуйте та запустіть `uvicorn` безпосередньо: -{* ../../docs_src/debugging/tutorial001.py hl[1,15] *} +{* ../../docs_src/debugging/tutorial001_py39.py hl[1,15] *} -### Про `__name__ == "__main__"` +### Про `__name__ == "__main__"` { #about-name-main } -Головна мета використання `__name__ == "__main__"` — це забезпечення виконання певного коду тільки тоді, коли файл запускається безпосередньо: +Головна мета використання `__name__ == "__main__"` — це забезпечення виконання певного коду лише тоді, коли ваш файл запускається так:
@@ -20,17 +20,17 @@ $ python myapp.py
-але не виконується при його імпорті в інший файл, наприклад: +але не виконується, коли інший файл імпортує його, наприклад: ```Python from myapp import app ``` -#### Детальніше +#### Детальніше { #more-details } -Припустимо, Ваш файл називається `myapp.py`. +Припустимо, ваш файл називається `myapp.py`. -Якщо Ви запустите його так: +Якщо ви запустите його так:
@@ -40,7 +40,7 @@ $ python myapp.py
-тоді внутрішня змінна `__name__`, яка створюється автоматично Python, матиме значення `"__main__"`. +тоді внутрішня змінна `__name__` у вашому файлі, яка створюється автоматично Python, матиме значення рядка `"__main__"`. Отже, цей блок коду: @@ -52,17 +52,17 @@ $ python myapp.py --- -Це не станеться, якщо Ви імпортуєте цей модуль (файл). +Це не станеться, якщо ви імпортуєте цей модуль (файл). -Якщо у Вас є інший файл, наприклад `importer.py`, з наступним кодом: +Отже, якщо у вас є інший файл `importer.py` з: ```Python from myapp import app -# Додатковий код +# Some more code ``` -У цьому випадку автоматично створена змінна у файлі `myapp.py` не матиме значення змінної `__name__` як `"__main__"`. +у цьому випадку автоматично створена змінна `__name__` всередині `myapp.py` не матиме значення `"__main__"`. Отже, рядок: @@ -74,38 +74,39 @@ from myapp import app /// info | Інформація -Більш детальну інформацію можна знайти в офіційній документації Python. +Для отримання додаткової інформації дивіться офіційну документацію Python. /// -## Запуск коду з вашим дебагером +## Запуск коду з вашим дебагером { #run-your-code-with-your-debugger } -Оскільки Ви запускаєте сервер Uvicorn безпосередньо з Вашого коду, Ви можете запустити вашу Python програму (ваш FastAPI додаток) безпосередньо з дебагера. +Оскільки ви запускаєте сервер Uvicorn безпосередньо з вашого коду, ви можете запустити вашу Python програму (ваш FastAPI-додаток) безпосередньо з дебагера. --- -Наприклад, у Visual Studio Code Ви можете: +Наприклад, у Visual Studio Code ви можете: -* Перейдіть на вкладку "Debug". -* Натисніть "Add configuration...". -* Виберіть "Python" +* Перейдіть на панель «Debug». +* «Add configuration...». +* Виберіть «Python» * Запустіть дебагер з опцією "`Python: Current File (Integrated Terminal)`". -Це запустить сервер з Вашим **FastAPI** кодом, зупиниться на точках зупину тощо. +Після цього він запустить сервер з вашим кодом **FastAPI**, зупиниться на точках зупину тощо. Ось як це може виглядати: --- -Якщо Ви використовуєте PyCharm, ви можете: -* Відкрити меню "Run". -* Вибрати опцію "Debug...". +Якщо ви використовуєте PyCharm, ви можете: + +* Відкрити меню «Run». +* Вибрати опцію «Debug...». * Потім з'явиться контекстне меню. * Вибрати файл для налагодження (у цьому випадку, `main.py`). -Це запустить сервер з Вашим **FastAPI** кодом, зупиниться на точках зупину тощо. +Після цього він запустить сервер з вашим кодом **FastAPI**, зупиниться на точках зупину тощо. Ось як це може виглядати: diff --git a/docs/uk/docs/tutorial/encoder.md b/docs/uk/docs/tutorial/encoder.md index f202c7989e..1b403d5bba 100644 --- a/docs/uk/docs/tutorial/encoder.md +++ b/docs/uk/docs/tutorial/encoder.md @@ -1,32 +1,32 @@ -# JSON Compatible Encoder +# JSON-сумісний кодувальник { #json-compatible-encoder } -Існують випадки, коли вам може знадобитися перетворити тип даних (наприклад, модель Pydantic) в щось сумісне з JSON (наприклад, `dict`, `list`, і т. д.). +Існують випадки, коли вам може знадобитися перетворити тип даних (наприклад, модель Pydantic) на щось сумісне з JSON (наприклад, `dict`, `list` тощо). Наприклад, якщо вам потрібно зберегти це в базі даних. -Для цього, **FastAPI** надає `jsonable_encoder()` функцію. +Для цього **FastAPI** надає функцію `jsonable_encoder()`. -## Використання `jsonable_encoder` +## Використання `jsonable_encoder` { #using-the-jsonable-encoder } Давайте уявимо, що у вас є база даних `fake_db`, яка приймає лише дані, сумісні з JSON. Наприклад, вона не приймає об'єкти типу `datetime`, оскільки вони не сумісні з JSON. -Отже, об'єкт типу `datetime` потрібно перетворити в рядок `str`, який містить дані в ISO форматі. +Отже, об'єкт типу `datetime` потрібно перетворити на `str`, який містить дані в форматі ISO. -Тим самим способом ця база даних не прийматиме об'єкт типу Pydantic model (об'єкт з атрибутами), а лише `dict`. +Так само ця база даних не прийматиме модель Pydantic (об'єкт з атрибутами), а лише `dict`. Ви можете використовувати `jsonable_encoder` для цього. -Вона приймає об'єкт, такий як Pydantic model, і повертає його версію, сумісну з JSON: +Вона приймає об'єкт, такий як модель Pydantic, і повертає його версію, сумісну з JSON: {* ../../docs_src/encoder/tutorial001_py310.py hl[4,21] *} -У цьому прикладі вона конвертує Pydantic model у `dict`, а `datetime` у `str`. +У цьому прикладі вона конвертує модель Pydantic у `dict`, а `datetime` у `str`. -Результат виклику цієї функції - це щось, що можна кодувати з використанням стандарту Python `json.dumps()`. +Результат виклику цієї функції — це щось, що можна кодувати з використанням стандарту Python `json.dumps()`. -Вона не повертає велику строку `str`, яка містить дані у форматі JSON (як строка). Вона повертає стандартну структуру даних Python (наприклад `dict`) із значеннями та підзначеннями, які є сумісними з JSON. +Вона не повертає великий `str`, який містить дані у форматі JSON (як рядок). Вона повертає стандартну структуру даних Python (наприклад, `dict`) зі значеннями та підзначеннями, які є сумісними з JSON. /// note | Примітка diff --git a/docs/uk/docs/tutorial/extra-data-types.md b/docs/uk/docs/tutorial/extra-data-types.md index 5da942b6e0..a3545e0746 100644 --- a/docs/uk/docs/tutorial/extra-data-types.md +++ b/docs/uk/docs/tutorial/extra-data-types.md @@ -1,13 +1,13 @@ -# Додаткові типи даних +# Додаткові типи даних { #extra-data-types } -До цього часу, ви використовували загальнопоширені типи даних, такі як: +До цього часу ви використовували загальнопоширені типи даних, такі як: * `int` * `float` * `str` * `bool` -Але можна також використовувати більш складні типи даних. +Але ви також можете використовувати більш складні типи даних. І ви все ще матимете ті ж можливості, які були показані до цього: @@ -17,30 +17,30 @@ * Валідація даних. * Автоматична анотація та документація. -## Інші типи даних +## Інші типи даних { #other-data-types } Ось додаткові типи даних для використання: * `UUID`: - * Стандартний "Універсальний Унікальний Ідентифікатор", який часто використовується як ідентифікатор у багатьох базах даних та системах. + * Стандартний "Універсальний унікальний ідентифікатор", який часто використовується як ID у багатьох базах даних та системах. * У запитах та відповідях буде представлений як `str`. * `datetime.datetime`: * Пайтонівський `datetime.datetime`. - * У запитах та відповідях буде представлений як `str` в форматі ISO 8601, як: `2008-09-15T15:53:00+05:00`. + * У запитах та відповідях буде представлений як `str` у форматі ISO 8601, як: `2008-09-15T15:53:00+05:00`. * `datetime.date`: * Пайтонівський `datetime.date`. - * У запитах та відповідях буде представлений як `str` в форматі ISO 8601, як: `2008-09-15`. + * У запитах та відповідях буде представлений як `str` у форматі ISO 8601, як: `2008-09-15`. * `datetime.time`: * Пайтонівський `datetime.time`. - * У запитах та відповідях буде представлений як `str` в форматі ISO 8601, як: `14:23:55.003`. + * У запитах та відповідях буде представлений як `str` у форматі ISO 8601, як: `14:23:55.003`. * `datetime.timedelta`: * Пайтонівський `datetime.timedelta`. * У запитах та відповідях буде представлений як `float` загальної кількості секунд. - * Pydantic також дозволяє представляти це як "ISO 8601 time diff encoding", більше інформації дивись у документації. + * Pydantic також дозволяє представляти це як "ISO 8601 time diff encoding", дивіться документацію для отримання додаткової інформації. * `frozenset`: * У запитах і відповідях це буде оброблено так само, як і `set`: - * У запитах список буде зчитано, дублікати будуть видалені та він буде перетворений на `set`. - * У відповідях, `set` буде перетворений на `list`. + * У запитах список буде зчитано, дублікати буде видалено, і його буде перетворено на `set`. + * У відповідях `set` буде перетворено на `list`. * Згенерована схема буде вказувати, що значення `set` є унікальними (з використанням JSON Schema's `uniqueItems`). * `bytes`: * Стандартний Пайтонівський `bytes`. @@ -49,11 +49,11 @@ * `Decimal`: * Стандартний Пайтонівський `Decimal`. * У запитах і відповідях це буде оброблено так само, як і `float`. -* Ви можете перевірити всі дійсні типи даних Pydantic тут: типи даних Pydantic. +* Ви можете перевірити всі дійсні типи даних Pydantic тут: типи даних Pydantic. -## Приклад +## Приклад { #example } -Ось приклад *path operation* з параметрами, використовуючи деякі з вищезазначених типів. +Ось приклад *операції шляху* з параметрами, використовуючи деякі з вищезазначених типів. {* ../../docs_src/extra_data_types/tutorial001_an_py310.py hl[1,3,12:16] *} diff --git a/docs/uk/docs/tutorial/first-steps.md b/docs/uk/docs/tutorial/first-steps.md index 3f861cb484..5f3750010c 100644 --- a/docs/uk/docs/tutorial/first-steps.md +++ b/docs/uk/docs/tutorial/first-steps.md @@ -1,126 +1,118 @@ -# Перші кроки +# Перші кроки { #first-steps } Найпростіший файл FastAPI може виглядати так: -{* ../../docs_src/first_steps/tutorial001.py *} +{* ../../docs_src/first_steps/tutorial001_py39.py *} Скопіюйте це до файлу `main.py`. -Запустіть сервер: +Запустіть live-сервер:
```console -$ fastapi dev main.py -INFO Using path main.py -INFO Resolved absolute path /home/user/code/awesomeapp/main.py -INFO Searching for package file structure from directories with __init__.py files -INFO Importing from /home/user/code/awesomeapp +$ fastapi dev main.py - ╭─ Python module file ─╮ - │ │ - │ 🐍 main.py │ - │ │ - ╰──────────────────────╯ + FastAPI Starting development server 🚀 -INFO Importing module main -INFO Found importable FastAPI app + Searching for package file structure from directories + with __init__.py files + Importing from /home/user/code/awesomeapp - ╭─ Importable FastAPI app ─╮ - │ │ - │ from main import app │ - │ │ - ╰──────────────────────────╯ + module 🐍 main.py -INFO Using import string main:app + code Importing the FastAPI app object from the module with + the following code: - ╭────────── FastAPI CLI - Development mode ───────────╮ - │ │ - │ Serving at: http://127.0.0.1:8000 │ - │ │ - │ API docs: http://127.0.0.1:8000/docs │ - │ │ - │ Running in development mode, for production use: │ - │ │ - fastapi run - │ │ - ╰─────────────────────────────────────────────────────╯ + from main import app -INFO: Will watch for changes in these directories: ['/home/user/code/awesomeapp'] -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -INFO: Started reloader process [2265862] using WatchFiles -INFO: Started server process [2265873] -INFO: Waiting for application startup. -INFO: Application startup complete. + app Using import string: main:app + + server Server started at http://127.0.0.1:8000 + server Documentation at http://127.0.0.1:8000/docs + + tip Running in development mode, for production use: + fastapi run + + Logs: + + INFO Will watch for changes in these directories: + ['/home/user/code/awesomeapp'] + INFO Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C + to quit) + INFO Started reloader process [383138] using WatchFiles + INFO Started server process [383153] + INFO Waiting for application startup. + INFO Application startup complete. ```
-У консолі буде рядок приблизно такого змісту: +У виводі є рядок приблизно такого змісту: ```hl_lines="4" INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) ``` -Цей рядок показує URL, за яким додаток запускається на вашій локальній машині. +Цей рядок показує URL, за яким ваш застосунок обслуговується на вашій локальній машині. -### Перевірте +### Перевірте { #check-it } Відкрийте браузер та введіть адресу http://127.0.0.1:8000. -Ви побачите у відповідь таке повідомлення у форматі JSON: +Ви побачите JSON-відповідь: ```JSON {"message": "Hello World"} ``` -### Інтерактивна API документація +### Інтерактивна API документація { #interactive-api-docs } -Перейдемо сюди http://127.0.0.1:8000/docs. +Тепер перейдіть сюди http://127.0.0.1:8000/docs. -Ви побачите автоматичну інтерактивну API документацію (створену завдяки Swagger UI): +Ви побачите автоматичну інтерактивну API документацію (надається Swagger UI): ![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) -### Альтернативна API документація +### Альтернативна API документація { #alternative-api-docs } -Тепер перейдемо сюди http://127.0.0.1:8000/redoc. +А тепер перейдіть сюди http://127.0.0.1:8000/redoc. -Ви побачите альтернативну автоматичну документацію (створену завдяки ReDoc): +Ви побачите альтернативну автоматичну документацію (надається ReDoc): ![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) -### OpenAPI +### OpenAPI { #openapi } -**FastAPI** генерує "схему" з усім вашим API, використовуючи стандарт **OpenAPI** для визначення API. +**FastAPI** генерує «схему» з усім вашим API, використовуючи стандарт **OpenAPI** для визначення API. -#### "Схема" +#### «Схема» { #schema } -"Схема" - це визначення або опис чогось. Це не код, який його реалізує, а просто абстрактний опис. +«Схема» — це визначення або опис чогось. Це не код, який його реалізує, а просто абстрактний опис. -#### API "схема" +#### API «схема» { #api-schema } У цьому випадку, OpenAPI є специфікацією, яка визначає, як описати схему вашого API. -Це визначення схеми включає шляхи (paths) вашого API, можливі параметри, які вони приймають тощо. +Це визначення схеми включає шляхи (paths) вашого API, можливі параметри, які вони приймають, тощо. -#### "Схема" даних +#### «Схема» даних { #data-schema } -Термін "схема" також може відноситися до структури даних, наприклад, JSON. +Термін «схема» також може відноситися до форми деяких даних, наприклад, вмісту JSON. -У цьому випадку це означає - атрибути JSON і типи даних, які вони мають тощо. +У цьому випадку це означає атрибути JSON і типи даних, які вони мають, тощо. -#### OpenAPI і JSON Schema +#### OpenAPI і JSON Schema { #openapi-and-json-schema } -OpenAPI описує схему для вашого API. І ця схема включає визначення (або "схеми") даних, що надсилаються та отримуються вашим API за допомогою **JSON Schema**, стандарту для схем даних JSON. +OpenAPI описує схему API для вашого API. І ця схема включає визначення (або «схеми») даних, що надсилаються та отримуються вашим API, за допомогою **JSON Schema**, стандарту для схем даних JSON. -#### Розглянемо `openapi.json` +#### Перевірте `openapi.json` { #check-the-openapi-json } -Якщо вас цікавить, як виглядає вихідна схема OpenAPI, то FastAPI автоматично генерує JSON-схему з усіма описами API. +Якщо вас цікавить, як виглядає «сирий» OpenAPI schema, FastAPI автоматично генерує JSON (schema) з описами всього вашого API. -Ознайомитися можна за посиланням: http://127.0.0.1:8000/openapi.json. +Ви можете побачити це напряму тут: http://127.0.0.1:8000/openapi.json. -Ви побачите приблизно такий JSON: +Ви побачите JSON, що починається приблизно так: ```JSON { @@ -143,42 +135,79 @@ OpenAPI описує схему для вашого API. І ця схема вк ... ``` -#### Для чого потрібний OpenAPI +#### Для чого потрібний OpenAPI { #what-is-openapi-for } -Схема OpenAPI є основою для обох систем інтерактивної документації. +OpenAPI schema — це те, на чому працюють дві включені системи інтерактивної документації. -Існують десятки альтернативних інструментів, заснованих на OpenAPI. Ви можете легко додати будь-який з них до **FastAPI** додатку. +Також існують десятки альтернатив, і всі вони засновані на OpenAPI. Ви можете легко додати будь-яку з цих альтернатив до вашого застосунку, створеного з **FastAPI**. -Ви також можете використовувати OpenAPI для автоматичної генерації коду для клієнтів, які взаємодіють з API. Наприклад, для фронтенд-, мобільних або IoT-додатків +Ви також можете використовувати його для автоматичної генерації коду для клієнтів, які взаємодіють з вашим API. Наприклад, для фронтенд-, мобільних або IoT-застосунків. -## А тепер крок за кроком +### Розгорніть ваш застосунок (необовʼязково) { #deploy-your-app-optional } -### Крок 1: імпортуємо `FastAPI` +За бажанням ви можете розгорнути ваш FastAPI-застосунок у FastAPI Cloud, перейдіть і приєднайтеся до списку очікування, якщо ви цього ще не зробили. 🚀 -{* ../../docs_src/first_steps/tutorial001.py hl[1] *} +Якщо у вас вже є обліковий запис **FastAPI Cloud** (ми запросили вас зі списку очікування 😉), ви можете розгорнути ваш застосунок однією командою. -`FastAPI` це клас у Python, який надає всю функціональність для API. +Перед розгортанням переконайтеся, що ви увійшли: + +
+ +```console +$ fastapi login + +You are logged in to FastAPI Cloud 🚀 +``` + +
+ +Потім розгорніть ваш застосунок: + +
+ +```console +$ fastapi deploy + +Deploying to FastAPI Cloud... + +✅ Deployment successful! + +🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev +``` + +
+ +Ось і все! Тепер ви можете отримати доступ до вашого застосунку за цим URL. ✨ + +## Підібʼємо підсумки, крок за кроком { #recap-step-by-step } + +### Крок 1: імпортуємо `FastAPI` { #step-1-import-fastapi } + +{* ../../docs_src/first_steps/tutorial001_py39.py hl[1] *} + +`FastAPI` — це клас у Python, який надає всю функціональність для вашого API. /// note | Технічні деталі -`FastAPI` це клас, який успадковується безпосередньо від `Starlette`. +`FastAPI` — це клас, який успадковується безпосередньо від `Starlette`. Ви також можете використовувати всю функціональність Starlette у `FastAPI`. /// -### Крок 2: створюємо екземпляр `FastAPI` +### Крок 2: створюємо «екземпляр» `FastAPI` { #step-2-create-a-fastapi-instance } -{* ../../docs_src/first_steps/tutorial001.py hl[3] *} -Змінна `app` є екземпляром класу `FastAPI`. +{* ../../docs_src/first_steps/tutorial001_py39.py hl[3] *} -Це буде головна точка для створення і взаємодії з API. +Тут змінна `app` буде «екземпляром» класу `FastAPI`. -### Крок 3: визначте операцію шляху (path operation) +Це буде головна точка взаємодії для створення всього вашого API. -#### Шлях (path) +### Крок 3: створіть *операцію шляху* { #step-3-create-a-path-operation } -"Шлях" це частина URL, яка йде одразу після символу `/`. +#### Шлях { #path } + +«Шлях» тут означає останню частину URL, починаючи з першого `/`. Отже, у такому URL, як: @@ -192,16 +221,17 @@ https://example.com/items/foo /items/foo ``` -/// info | Додаткова інформація +/// info | Інформація -"Шлях" (path) також зазвичай називають "ендпоінтом" (endpoint) або "маршрутом" (route). +«Шлях» також зазвичай називають «ендпоінтом» або «маршрутом». /// -При створенні API, "шлях" є основним способом розділення "завдань" і "ресурсів". -#### Operation +Під час створення API «шлях» є основним способом розділити «завдання» і «ресурси». -"Операція" (operation) тут означає один з "методів" HTTP. +#### Операція { #operation } + +«Операція» тут означає один з HTTP «методів». Один з: @@ -217,46 +247,47 @@ https://example.com/items/foo * `PATCH` * `TRACE` -У HTTP-протоколі можна спілкуватися з кожним шляхом, використовуючи один (або кілька) з цих "методів". +У протоколі HTTP ви можете спілкуватися з кожним шляхом, використовуючи один (або кілька) з цих «методів». --- -При створенні API зазвичай використовуються конкретні методи HTTP для виконання певних дій. +Під час створення API зазвичай використовують ці конкретні HTTP методи, щоб виконати певну дію. -Як правило, використовують: +Зазвичай використовують: -* `POST`: для створення даних. -* `GET`: для читання даних. -* `PUT`: для оновлення даних. -* `DELETE`: для видалення даних. +* `POST`: щоб створити дані. +* `GET`: щоб читати дані. +* `PUT`: щоб оновити дані. +* `DELETE`: щоб видалити дані. -В OpenAPI кожен HTTP метод називається "операція". +Отже, в OpenAPI кожен з HTTP методів називається «операцією». -Ми також будемо дотримуватися цього терміна. +Ми також будемо називати їх «**операціями**». -#### Визначте декоратор операції шляху (path operation decorator) +#### Визначте *декоратор операції шляху* { #define-a-path-operation-decorator } -{* ../../docs_src/first_steps/tutorial001.py hl[6] *} -Декоратор `@app.get("/")` вказує **FastAPI**, що функція нижче, відповідає за обробку запитів, які надходять до неї: +{* ../../docs_src/first_steps/tutorial001_py39.py hl[6] *} -* шлях `/` +Декоратор `@app.get("/")` повідомляє **FastAPI**, що функція одразу нижче відповідає за обробку запитів, які надходять до: + +* шляху `/` * використовуючи get операцію -/// info | `@decorator` Додаткова інформація +/// info | `@decorator` Інформація -Синтаксис `@something` у Python називається "декоратором". +Синтаксис `@something` у Python називається «декоратором». Ви розташовуєте його над функцією. Як гарний декоративний капелюх (мабуть, звідти походить термін). -"Декоратор" приймає функцію нижче і виконує з нею якусь дію. +«Декоратор» бере функцію нижче і виконує з нею якусь дію. У нашому випадку, цей декоратор повідомляє **FastAPI**, що функція нижче відповідає **шляху** `/` і **операції** `get`. -Це і є "декоратор операції шляху (path operation decorator)". +Це і є «**декоратор операції шляху**». /// -Можна також використовувати операції: +Можна також використовувати інші операції: * `@app.post()` * `@app.put()` @@ -271,58 +302,79 @@ https://example.com/items/foo /// tip | Порада -Ви можете використовувати кожну операцію (HTTP-метод) на свій розсуд. +Ви можете використовувати кожну операцію (HTTP-метод) як забажаєте. -**FastAPI** не нав'язує жодного певного значення для кожного методу. +**FastAPI** не навʼязує жодного конкретного значення. -Наведена тут інформація є рекомендацією, а не обов'язковою вимогою. +Наведена тут інформація подана як настанова, а не вимога. -Наприклад, під час використання GraphQL зазвичай усі дії виконуються тільки за допомогою `POST` операцій. +Наприклад, під час використання GraphQL ви зазвичай виконуєте всі дії, використовуючи лише `POST` операції. /// -### Крок 4: визначте **функцію операції шляху (path operation function)** +### Крок 4: визначте **функцію операції шляху** { #step-4-define-the-path-operation-function } -Ось "**функція операції шляху**": +Ось наша «**функція операції шляху**»: * **шлях**: це `/`. * **операція**: це `get`. -* **функція**: це функція, яка знаходиться нижче "декоратора" (нижче `@app.get("/")`). +* **функція**: це функція нижче «декоратора» (нижче `@app.get("/")`). -{* ../../docs_src/first_steps/tutorial001.py hl[7] *} +{* ../../docs_src/first_steps/tutorial001_py39.py hl[7] *} -Це звичайна функція Python. +Це функція Python. -FastAPI викликатиме її щоразу, коли отримає запит до URL із шляхом "/", використовуючи операцію `GET`. +**FastAPI** викликатиме її щоразу, коли отримає запит до URL «`/`», використовуючи операцію `GET`. -У даному випадку це асинхронна функція. +У цьому випадку це `async` функція. --- Ви також можете визначити її як звичайну функцію замість `async def`: -{* ../../docs_src/first_steps/tutorial003.py hl[7] *} +{* ../../docs_src/first_steps/tutorial003_py39.py hl[7] *} /// note | Примітка -Якщо не знаєте в чому різниця, подивіться [Конкурентність: *"Поспішаєш?"*](../async.md#in-a-hurry){.internal-link target=_blank}. +Якщо ви не знаєте різницю, подивіться [Асинхронність: *«Поспішаєте?»*](../async.md#in-a-hurry){.internal-link target=_blank}. /// -### Крок 5: поверніть результат +### Крок 5: поверніть вміст { #step-5-return-the-content } -{* ../../docs_src/first_steps/tutorial001.py hl[8] *} +{* ../../docs_src/first_steps/tutorial001_py39.py hl[8] *} -Ви можете повернути `dict`, `list`, а також окремі значення `str`, `int`, ітд. +Ви можете повернути `dict`, `list`, а також окремі значення `str`, `int` тощо. Також можна повернути моделі Pydantic (про це ви дізнаєтесь пізніше). -Існує багато інших об'єктів і моделей, які будуть автоматично конвертовані в JSON (зокрема ORM тощо). Спробуйте використати свої улюблені, велика ймовірність, що вони вже підтримуються. +Існує багато інших обʼєктів і моделей, які будуть автоматично конвертовані в JSON (зокрема ORM тощо). Спробуйте використати свої улюблені — велика ймовірність, що вони вже підтримуються. -## Підіб'ємо підсумки +### Крок 6: розгорніть його { #step-6-deploy-it } -* Імпортуємо `FastAPI`. -* Створюємо екземпляр `app`. -* Пишемо **декоратор операції шляху** як `@app.get("/")`. -* Пишемо **функцію операції шляху**; наприклад, `def root(): ...`. -* Запускаємо сервер у режимі розробки `fastapi dev`. +Розгорніть ваш застосунок у **FastAPI Cloud** однією командою: `fastapi deploy`. 🎉 + +#### Про FastAPI Cloud { #about-fastapi-cloud } + +**FastAPI Cloud** створено тим самим автором і командою, які стоять за **FastAPI**. + +Він спрощує процес **створення**, **розгортання** та **доступу** до API з мінімальними зусиллями. + +Він переносить той самий **досвід розробника** зі створення застосунків на FastAPI на **розгортання** їх у хмарі. 🎉 + +FastAPI Cloud — основний спонсор і джерело фінансування для open source проєктів *FastAPI and friends*. ✨ + +#### Розгортання в інших хмарних провайдерах { #deploy-to-other-cloud-providers } + +FastAPI — це open source і базується на стандартах. Ви можете розгортати FastAPI-застосунки у будь-якого хмарного провайдера на ваш вибір. + +Дотримуйтеся інструкцій вашого хмарного провайдера, щоб розгорнути FastAPI-застосунки з їхньою допомогою. 🤓 + +## Підібʼємо підсумки { #recap } + +* Імпортуйте `FastAPI`. +* Створіть екземпляр `app`. +* Напишіть **декоратор операції шляху**, використовуючи декоратори на кшталт `@app.get("/")`. +* Визначте **функцію операції шляху**; наприклад, `def root(): ...`. +* Запустіть сервер розробки командою `fastapi dev`. +* За бажанням розгорніть ваш застосунок за допомогою `fastapi deploy`. diff --git a/docs/uk/docs/tutorial/handling-errors.md b/docs/uk/docs/tutorial/handling-errors.md index 32de73b2a0..53b8b12f61 100644 --- a/docs/uk/docs/tutorial/handling-errors.md +++ b/docs/uk/docs/tutorial/handling-errors.md @@ -1,10 +1,10 @@ -# Обробка Помилок +# Обробка помилок { #handling-errors } -Є багато ситуацій, коли потрібно повідомити клієнта, який використовує Ваш API, про помилку. +Є багато ситуацій, коли вам потрібно повідомити про помилку клієнта, який використовує ваш API. Цим клієнтом може бути браузер із фронтендом, код іншого розробника, IoT-пристрій тощо. -Можливо, Вам потрібно повідомити клієнта, що: +Можливо, вам потрібно повідомити клієнта, що: * У нього недостатньо прав для виконання цієї операції. * Він не має доступу до цього ресурсу. @@ -13,37 +13,37 @@ У таких випадках зазвичай повертається **HTTP статус-код** в діапазоні **400** (від 400 до 499). -Це схоже на HTTP статус-коди 200 (від 200 до 299). Ці "200" статус-коди означають, що запит пройшов успішно. +Це схоже на HTTP статус-коди 200 (від 200 до 299). Ці «200» статус-коди означають, що якимось чином запит був «успішним». Статус-коди в діапазоні 400 означають, що сталася помилка з боку клієнта. -Пам'ятаєте всі ці помилки **404 Not Found** (і жарти про них)? +Пам'ятаєте всі ці помилки **«404 Not Found»** (і жарти про них)? -## Використання `HTTPException` +## Використання `HTTPException` { #use-httpexception } Щоб повернути HTTP-відповіді з помилками клієнту, використовуйте `HTTPException`. -### Імпорт `HTTPException` +### Імпорт `HTTPException` { #import-httpexception } -{* ../../docs_src/handling_errors/tutorial001.py hl[1] *} +{* ../../docs_src/handling_errors/tutorial001_py39.py hl[1] *} -### Використання `HTTPException` у коді +### Згенеруйте `HTTPException` у своєму коді { #raise-an-httpexception-in-your-code } `HTTPException` — це звичайна помилка Python із додатковими даними, які стосуються API. -Оскільки це помилка Python, Ви не `повертаєте` його, а `генеруєте` (генеруєте помилку). +Оскільки це помилка Python, ви не `return` її, а `raise` її. -Це також означає, що якщо Ви перебуваєте всередині допоміжної функції, яку викликаєте всередині своєї *функції операції шляху*, і там генеруєте `HTTPException`, всередині цієї допоміжної функції, то решта коду в *функції операції шляху* не буде виконана. Запит одразу завершиться, і HTTP-помилка з `HTTPException` буде надіслана клієнту. +Це також означає, що якщо ви перебуваєте всередині допоміжної функції, яку викликаєте всередині своєї *функції операції шляху*, і там згенеруєте `HTTPException` всередині цієї допоміжної функції, то решта коду в *функції операції шляху* не буде виконана. Запит одразу завершиться, і HTTP-помилка з `HTTPException` буде надіслана клієнту. -Перевага використання `генерації` (raise) помилки замість `повернення` значення (return) стане більш очевидним в розділі про Залежності та Безпеку. +Перевага генерації виключення замість повернення значення стане більш очевидною в розділі про залежності та безпеку. -У цьому прикладі, якщо клієнт запитує елемент за ID, якого не існує, буде згенеровано помилку зі статус-кодом `404`: +У цьому прикладі, коли клієнт запитує елемент за ID, якого не існує, згенеруйте виключення зі статус-кодом `404`: -{* ../../docs_src/handling_errors/tutorial001.py hl[11] *} +{* ../../docs_src/handling_errors/tutorial001_py39.py hl[11] *} -### Отримана відповідь +### Отримана відповідь { #the-resulting-response } -Якщо клієнт робить запит за шляхом `http://example.com/items/foo` (де `item_id` `"foo"`), він отримає статус-код 200 і JSON відповідь: +Якщо клієнт робить запит за шляхом `http://example.com/items/foo` (де `item_id` `"foo"`), він отримає HTTP статус-код 200 і JSON відповідь: ```JSON { @@ -51,7 +51,7 @@ } ``` -Але якщо клієнт робить запит на `http://example.com/items/bar` (де `item_id` має не існуюче значення `"bar"`), то отримає статус-код 404 (помилка "не знайдено") та відповідь: +Але якщо клієнт робить запит на `http://example.com/items/bar` (де `item_id` має не існуюче значення `"bar"`), то отримає HTTP статус-код 404 (помилка «не знайдено») та JSON відповідь: ```JSON { @@ -61,7 +61,7 @@ /// tip | Порада -Під час виклику `HTTPException` Ви можете передати будь-яке значення, яке може бути перетворене в JSON, як параметр `detail`, а не лише рядок (`str`). +Під час генерації `HTTPException` ви можете передати будь-яке значення, яке може бути перетворене в JSON, як параметр `detail`, а не лише `str`. Ви можете передати `dict`, `list` тощо. @@ -69,33 +69,33 @@ /// -## Додавання власних заголовків +## Додавання власних заголовків { #add-custom-headers } -Іноді потрібно додати власні заголовки до HTTP-помилки, наприклад, для певних типів безпеки. +Є деякі ситуації, коли корисно мати можливість додавати власні заголовки до HTTP-помилки. Наприклад, для деяких типів безпеки. -Ймовірно, Вам не доведеться використовувати це безпосередньо у своєму коді. +Ймовірно, вам не доведеться використовувати це безпосередньо у своєму коді. -Але якщо Вам знадобиться це для складного сценарію, Ви можете додати власні заголовки: +Але якщо вам знадобиться це для складного сценарію, ви можете додати власні заголовки: -{* ../../docs_src/handling_errors/tutorial002.py hl[14] *} +{* ../../docs_src/handling_errors/tutorial002_py39.py hl[14] *} -## Встановлення власних обробників помилок +## Встановлення власних обробників виключень { #install-custom-exception-handlers } -Ви можете додати власні обробники помилок за допомогою тих самих утиліт обробки помилок зі Starlette. +Ви можете додати власні обробники виключень за допомогою тих самих утиліт для виключень зі Starlette. -Припустимо, у Вас є власний обʼєкт помилки `UnicornException`, яке Ви (або бібліотека, яку Ви використовуєте) може `згенерувати` (`raise`). +Припустімо, у вас є власне виключення `UnicornException`, яке ви (або бібліотека, яку ви використовуєте) можете `raise`. -І Ви хочете обробляти це виключення глобально за допомогою FastAPI. +І ви хочете обробляти це виключення глобально за допомогою FastAPI. Ви можете додати власний обробник виключень за допомогою `@app.exception_handler()`: -{* ../../docs_src/handling_errors/tutorial003.py hl[5:7,13:18,24] *} +{* ../../docs_src/handling_errors/tutorial003_py39.py hl[5:7,13:18,24] *} -Тут, якщо Ви звернетеся до `/unicorns/yolo`, то згенерується помилка `UnicornException`. +Тут, якщо ви звернетеся до `/unicorns/yolo`, *операція шляху* згенерує (`raise`) `UnicornException`. Але вона буде оброблена функцією-обробником `unicorn_exception_handler`. -Отже, Ви отримаєте зрозумілу помилку зі HTTP-статусом `418` і JSON-відповіддю: +Отже, ви отримаєте зрозумілу помилку зі HTTP-статусом `418` і JSON-вмістом: ```JSON {"message": "Oops! yolo did something. There goes a rainbow..."} @@ -105,31 +105,31 @@ Ви також можете використовувати `from starlette.requests import Request` і `from starlette.responses import JSONResponse`. -**FastAPI** надає ті самі `starlette.responses`, що й `fastapi.responses`, просто для зручності розробника. Але більшість доступних відповідей надходять безпосередньо зі Starlette. Те ж саме стосується і `Request`. +**FastAPI** надає ті самі `starlette.responses`, що й `fastapi.responses`, просто для зручності для вас, розробника. Але більшість доступних відповідей надходять безпосередньо зі Starlette. Те ж саме з `Request`. /// -## Перевизначення обробників помилок за замовчуванням +## Перевизначення обробників виключень за замовчуванням { #override-the-default-exception-handlers } -**FastAPI** має кілька обробників помилок за замовчуванням. +**FastAPI** має кілька обробників виключень за замовчуванням. -Ці обробники відповідають за повернення стандартних JSON-відповідей, коли Ви `генеруєте` (`raise`) `HTTPException`, а також коли запит містить некоректні дані. +Ці обробники відповідають за повернення стандартних JSON-відповідей, коли ви `raise` `HTTPException`, а також коли запит містить некоректні дані. -Ви можете перевизначити ці обробники, створивши власні. +Ви можете перевизначити ці обробники виключень власними. -### Перевизначення помилок валідації запиту +### Перевизначення виключень валідації запиту { #override-request-validation-exceptions } -Коли запит містить некоректні дані, **FastAPI** генерує `RequestValidationError`. +Коли запит містить некоректні дані, **FastAPI** внутрішньо генерує `RequestValidationError`. -І також включає обробник помилок за замовчуванням для нього. +І також включає обробник виключень за замовчуванням для нього. -Щоб перевизначити його, імпортуйте `RequestValidationError` і використовуйте його з `@app.exception_handler(RequestValidationError)` для декорування обробника помилок. +Щоб перевизначити його, імпортуйте `RequestValidationError` і використовуйте його з `@app.exception_handler(RequestValidationError)` для декорування обробника виключень. -Обробник помилок отримує `Request` і саму помилку. +Обробник виключень отримає `Request` і саме виключення. -{* ../../docs_src/handling_errors/tutorial004.py hl[2,14:16] *} +{* ../../docs_src/handling_errors/tutorial004_py39.py hl[2,14:19] *} -Тепер, якщо Ви перейдете за посиланням `/items/foo`, замість того, щоб отримати стандартну JSON-помилку: +Тепер, якщо ви перейдете за посиланням `/items/foo`, замість того, щоб отримати стандартну JSON-помилку: ```JSON { @@ -146,55 +146,44 @@ } ``` -Ви отримаєте текстову версію: +ви отримаєте текстову версію: ``` -1 validation error -path -> item_id - value is not a valid integer (type=type_error.integer) +Validation errors: +Field: ('path', 'item_id'), Error: Input should be a valid integer, unable to parse string as an integer ``` -#### `RequestValidationError` проти `ValidationError` +### Перевизначення обробника помилок `HTTPException` { #override-the-httpexception-error-handler } -/// warning | Увага +Аналогічно, ви можете перевизначити обробник `HTTPException`. -Це технічні деталі, які Ви можете пропустити, якщо вони зараз не важливі для Вас. +Наприклад, ви можете захотіти повернути відповідь у вигляді простого тексту замість JSON для цих помилок: -/// - -`RequestValidationError` є підкласом Pydantic `ValidationError`. - -**FastAPI** використовує його для того, якщо Ви використовуєте модель Pydantic у `response_model` і у ваших даних є помилка, Ви побачили помилку у своєму журналі. - -Але клієнт/користувач не побачить її. Натомість клієнт отримає "Internal Server Error" зі статусом HTTP `500`. - -Так має бути, якщо у Вас виникла `ValidationError` Pydantic у *відповіді* або деінде у вашому коді (не у *запиті* клієнта), це насправді є помилкою у Вашому коді. - -І поки Ви її виправляєте, клієнти/користувачі не повинні мати доступу до внутрішньої інформації про помилку, оскільки це може призвести до вразливості безпеки. - -### Перевизначення обробника помилок `HTTPException` - -Аналогічно, Ви можете перевизначити обробник `HTTPException`. - -Наприклад, Ви можете захотіти повернути текстову відповідь замість JSON для цих помилок: - -{* ../../docs_src/handling_errors/tutorial004.py hl[3:4,9:11,22] *} +{* ../../docs_src/handling_errors/tutorial004_py39.py hl[3:4,9:11,25] *} /// note | Технічні деталі Ви також можете використовувати `from starlette.responses import PlainTextResponse`. -**FastAPI** надає ті самі `starlette.responses`, що й `fastapi.responses`, просто для зручності розробника. Але більшість доступних відповідей надходять безпосередньо зі Starlette. +**FastAPI** надає ті самі `starlette.responses`, що й `fastapi.responses`, просто для зручності для вас, розробника. Але більшість доступних відповідей надходять безпосередньо зі Starlette. /// -### Використання тіла `RequestValidationError` +/// warning | Попередження + +Пам’ятайте, що `RequestValidationError` містить інформацію про назву файлу та рядок, де сталася помилка валідації, щоб за потреби ви могли показати це у своїх логах із відповідною інформацією. + +Але це означає, що якщо ви просто перетворите це на рядок і повернете цю інформацію напряму, ви можете розкрити трохи інформації про вашу систему, тому тут код витягає та показує кожну помилку незалежно. + +/// + +### Використання тіла `RequestValidationError` { #use-the-requestvalidationerror-body } `RequestValidationError` містить `body`, який він отримав із некоректними даними. Ви можете використовувати це під час розробки свого додатка, щоб логувати тіло запиту та налагоджувати його, повертати користувачеві тощо. -{* ../../docs_src/handling_errors/tutorial005.py hl[14] *} +{* ../../docs_src/handling_errors/tutorial005_py39.py hl[14] *} Тепер спробуйте надіслати некоректний елемент, наприклад: @@ -204,8 +193,8 @@ path -> item_id "size": "XL" } ``` -Ви отримаєте відповідь, яка повідомить Вам, які саме дані є некоректні у вашому тілі запиту: +Ви отримаєте відповідь, яка повідомить вам, що дані є некоректними, і міститиме отримане тіло запиту: ```JSON hl_lines="12-15" { @@ -226,30 +215,30 @@ path -> item_id } ``` -#### `HTTPException` FastAPI проти `HTTPException` Starlette +#### `HTTPException` FastAPI проти `HTTPException` Starlette { #fastapis-httpexception-vs-starlettes-httpexception } **FastAPI** має власний `HTTPException`. -І клас помилки `HTTPException` в **FastAPI** успадковується від класу помилки `HTTPException` в Starlette. +І клас помилки `HTTPException` в **FastAPI** успадковується від класу помилки `HTTPException` у Starlette. Єдина різниця полягає в тому, що `HTTPException` в **FastAPI** приймає будь-які дані, які можна перетворити на JSON, для поля `detail`, тоді як `HTTPException` у Starlette приймає тільки рядки. -Отже, Ви можете продовжувати використовувати `HTTPException` в **FastAPI** як зазвичай у своєму коді. +Отже, ви можете продовжувати генерувати `HTTPException` **FastAPI** як зазвичай у своєму коді. -Але коли Ви реєструєте обробник виключень, слід реєструвати його для `HTTPException` зі Starlette. +Але коли ви реєструєте обробник виключень, слід реєструвати його для `HTTPException` зі Starlette. -Таким чином, якщо будь-яка частина внутрішнього коду Starlette або розширення чи плагін Starlette згенерує (raise) `HTTPException`, Ваш обробник зможе перехопити та обробити її. +Таким чином, якщо будь-яка частина внутрішнього коду Starlette або розширення чи плагін Starlette згенерує Starlette `HTTPException`, ваш обробник зможе перехопити та обробити її. -У цьому прикладі, щоб мати можливість використовувати обидва `HTTPException` в одному коді, помилка Starlette перейменовується на `StarletteHTTPException`: +У цьому прикладі, щоб мати можливість використовувати обидва `HTTPException` в одному коді, виключення Starlette перейменовується на `StarletteHTTPException`: ```Python from starlette.exceptions import HTTPException as StarletteHTTPException ``` -### Повторне використання обробників помилок **FastAPI** +### Повторне використання обробників виключень **FastAPI** { #reuse-fastapis-exception-handlers } -Якщо Ви хочете використовувати помилки разом із такими ж обробниками помилок за замовчуванням, як у **FastAPI**, Ви можете імпортувати та повторно використовувати їх із `fastapi.exception_handlers`: +Якщо ви хочете використовувати виключення разом із такими ж обробниками виключень за замовчуванням, як у **FastAPI**, ви можете імпортувати та повторно використати обробники виключень за замовчуванням із `fastapi.exception_handlers`: -{* ../../docs_src/handling_errors/tutorial006.py hl[2:5,15,21] *} +{* ../../docs_src/handling_errors/tutorial006_py39.py hl[2:5,15,21] *} -У цьому прикладі Ви просто використовуєте `print` для виведення дуже інформативного повідомлення, але Ви зрозуміли основну ідею. Ви можете обробити помилку та повторно використовувати обробники помилок за замовчуванням. +У цьому прикладі ви просто друкуєте помилку з дуже виразним повідомленням, але ви зрозуміли основну ідею. Ви можете використовувати виключення, а потім просто повторно використати обробники виключень за замовчуванням. diff --git a/docs/uk/docs/tutorial/header-param-models.md b/docs/uk/docs/tutorial/header-param-models.md index 6f7b0bdae7..c080c19f06 100644 --- a/docs/uk/docs/tutorial/header-param-models.md +++ b/docs/uk/docs/tutorial/header-param-models.md @@ -1,26 +1,24 @@ -# Моделі Параметрів Заголовків +# Моделі параметрів заголовків { #header-parameter-models } -Якщо у Вас є група пов’язаних параметрів заголовків, Ви можете створити **Pydantic модель** для їх оголошення. +Якщо у Вас є група пов’язаних **параметрів заголовків**, Ви можете створити **Pydantic модель** для їх оголошення. Це дозволить Вам повторно **використовувати модель** в **різних місцях**, а також оголосити валідації та метадані для всіх параметрів одночасно. 😎 -/// note | Нотатки +/// note | Примітка Ця можливість підтримується починаючи з версії FastAPI `0.115.0`. 🤓 /// -## Параметри Заголовків з Використанням Pydantic Model +## Параметри заголовків з використанням Pydantic моделі { #header-parameters-with-a-pydantic-model } Оголосіть потрібні **параметри заголовків** у **Pydantic моделі**, а потім оголосіть параметр як `Header`: {* ../../docs_src/header_param_models/tutorial001_an_py310.py hl[9:14,18] *} -FastAPI буде витягувати дані для кожного поля з заголовків у запиті та передавати їх у створену Вами Pydantic модель. - **FastAPI** буде **витягувати** дані для **кожного поля** з **заголовків** у запиті та передавати їх у створену Вами Pydantic модель. -## Перевірка в Документації +## Перевірка в документації { #check-the-docs } Ви можете побачити необхідні заголовки в інтерфейсі документації за адресою `/docs`: @@ -28,7 +26,7 @@ FastAPI буде витягувати дані для кожного поля з
-## Заборона Додаткових Заголовків +## Заборонити додаткові заголовки { #forbid-extra-headers } У деяких особливих випадках (ймовірно, не дуже поширених) Ви можете захотіти **обмежити** заголовки, які хочете отримати. @@ -38,7 +36,7 @@ FastAPI буде витягувати дані для кожного поля з Якщо клієнт спробує надіслати **додаткові заголовки**, він отримає **помилку** у відповіді. -Наприклад, якщо клієнт спробує надіслати заголовок `tool` зі значенням `plumbus`, він отримає **помилку** з повідомленням про те, що параметр заголовка `tool` не дозволений: +Наприклад, якщо клієнт спробує надіслати заголовок `tool` зі значенням `plumbus`, він отримає **помилку** у відповіді з повідомленням про те, що параметр заголовка `tool` не дозволений: ```json { @@ -53,6 +51,22 @@ FastAPI буде витягувати дані для кожного поля з } ``` -## Підсумок +## Вимкнути перетворення підкреслень { #disable-convert-underscores } + +Так само, як і зі звичайними параметрами заголовків, коли у назвах параметрів є символи підкреслення, вони **автоматично перетворюються на дефіси**. + +Наприклад, якщо у коді у Вас є параметр заголовка `save_data`, очікуваний HTTP-заголовок буде `save-data`, і він так само відображатиметься в документації. + +Якщо з якоїсь причини Вам потрібно вимкнути це автоматичне перетворення, Ви також можете зробити це для Pydantic моделей для параметрів заголовків. + +{* ../../docs_src/header_param_models/tutorial003_an_py310.py hl[19] *} + +/// warning | Попередження + +Перш ніж встановлювати `convert_underscores` у значення `False`, пам’ятайте, що деякі HTTP проксі та сервери забороняють використання заголовків із підкресленнями. + +/// + +## Підсумок { #summary } Ви можете використовувати **Pydantic моделі** для оголошення **заголовків** у **FastAPI**. 😎 diff --git a/docs/uk/docs/tutorial/header-params.md b/docs/uk/docs/tutorial/header-params.md index 09c70a4f61..f5a4ea18d1 100644 --- a/docs/uk/docs/tutorial/header-params.md +++ b/docs/uk/docs/tutorial/header-params.md @@ -1,14 +1,14 @@ -# Header-параметри +# Параметри заголовків { #header-parameters } -Ви можете визначати параметри заголовків, так само як визначаєте `Query`, `Path` і `Cookie` параметри. +Ви можете визначати параметри заголовків так само, як визначаєте параметри `Query`, `Path` і `Cookie`. -## Імпорт `Header` +## Імпорт `Header` { #import-header } Спочатку імпортуйте `Header`: {* ../../docs_src/header_params/tutorial001_an_py310.py hl[3] *} -## Оголошення параметрів `Header` +## Оголошення параметрів `Header` { #declare-header-parameters } Потім оголосіть параметри заголовків, використовуючи ту ж структуру, що й для `Path`, `Query` та `Cookie`. @@ -18,9 +18,9 @@ /// note | Технічні деталі -`Header`є "сестринським" класом для `Path`, `Query` і `Cookie`. Він також успадковується від загального класу `Param`. +`Header` є «сестринським» класом для `Path`, `Query` і `Cookie`. Він також успадковується від того ж спільного класу `Param`. -Але пам’ятайте, що при імпорті `Query`, `Path`, `Header` та інших із `fastapi`, то насправді вони є функціями, які повертають спеціальні класи. +Але пам’ятайте, що коли ви імпортуєте `Query`, `Path`, `Header` та інші з `fastapi`, то насправді це функції, які повертають спеціальні класи. /// @@ -30,43 +30,43 @@ /// -## Автоматичне перетворення +## Автоматичне перетворення { #automatic-conversion } -`Header` має додатковий функціонал порівняно з `Path`, `Query` та `Cookie`. +`Header` має трохи додаткової функціональності порівняно з тим, що надають `Path`, `Query` та `Cookie`. -Більшість стандартних заголовків розділяються символом «дефіс», також відомим як «мінус» (`-`). +Більшість стандартних заголовків розділяються символом «дефіс», також відомим як «символ мінуса» (`-`). Але змінна, така як `user-agent`, є недійсною в Python. -Тому, за замовчуванням, `Header` автоматично перетворює символи підкреслення (`_`) на дефіси (`-`) для отримання та документування заголовків. +Тому, за замовчуванням, `Header` перетворюватиме символи підкреслення (`_`) у назвах параметрів на дефіси (`-`), щоб отримувати та документувати заголовки. -Оскільки заголовки HTTP не чутливі до регістру, Ви можете використовувати стандартний стиль Python ("snake_case"). +Також заголовки HTTP не чутливі до регістру, тож ви можете оголошувати їх у стандартному стилі Python (також відомому як «snake_case»). -Тому Ви можете використовувати `user_agent`, як зазвичай у коді Python, замість того щоб писати з великої літери, як `User_Agent` або щось подібне. +Тому ви можете використовувати `user_agent`, як зазвичай у коді Python, замість того щоб потрібно було писати з великої літери перші літери, як `User_Agent` або щось подібне. -Якщо Вам потрібно вимкнути автоматичне перетворення підкреслень у дефіси, встановіть `convert_underscores` в `Header` значення `False`: +Якщо з якоїсь причини вам потрібно вимкнути автоматичне перетворення підкреслень на дефіси, встановіть параметр `convert_underscores` у `Header` в `False`: {* ../../docs_src/header_params/tutorial002_an_py310.py hl[10] *} -/// warning | Увага +/// warning | Попередження -Перед тим як встановити значення `False` для `convert_underscores` пам’ятайте, що деякі HTTP-проксі та сервери не підтримують заголовки з підкресленнями. +Перед тим як встановити `convert_underscores` в `False`, пам’ятайте, що деякі HTTP-проксі та сервери забороняють використання заголовків із підкресленнями. /// -## Дубльовані заголовки +## Дубльовані заголовки { #duplicate-headers } -Можливо отримати дубльовані заголовки, тобто той самий заголовок із кількома значеннями. +Можливо отримати дубльовані заголовки. Тобто один і той самий заголовок із кількома значеннями. -Це можна визначити, використовуючи список у типізації параметра. +Ви можете визначити такі випадки, використовуючи список у типізації. -Ви отримаєте всі значення дубльованого заголовка у вигляді `list` у Python. +Ви отримаєте всі значення дубльованого заголовка у вигляді Python-`list`. -Наприклад, щоб оголосити заголовок `X-Token`, який може з’являтися більше ніж один раз: +Наприклад, щоб оголосити заголовок `X-Token`, який може з’являтися більше ніж один раз, ви можете написати: {* ../../docs_src/header_params/tutorial003_an_py310.py hl[9] *} -Якщо Ви взаємодієте з цією операцією шляху, надсилаючи два HTTP-заголовки, наприклад: +Якщо ви взаємодієте з цією *операцією шляху*, надсилаючи два HTTP-заголовки, наприклад: ``` X-Token: foo @@ -84,8 +84,8 @@ X-Token: bar } ``` -## Підсумок +## Підсумок { #recap } -Оголошуйте заголовки за допомогою `Header`, використовуючи той самий підхід, що й для `Query`, `Path` та `Cookie`. +Оголошуйте заголовки за допомогою `Header`, використовуючи той самий загальний підхід, що й для `Query`, `Path` та `Cookie`. -Не хвилюйтеся про підкреслення у змінних — **FastAPI** автоматично конвертує їх. +І не хвилюйтеся про підкреслення у ваших змінних — **FastAPI** подбає про їх перетворення. diff --git a/docs/uk/docs/tutorial/index.md b/docs/uk/docs/tutorial/index.md index 92c3e77a36..6848090ecc 100644 --- a/docs/uk/docs/tutorial/index.md +++ b/docs/uk/docs/tutorial/index.md @@ -1,29 +1,53 @@ -# Туторіал - Посібник користувача +# Туторіал - Посібник користувача { #tutorial-user-guide } У цьому посібнику показано, як користуватися **FastAPI** з більшістю його функцій, крок за кроком. Кожен розділ поступово надбудовується на попередні, але він структурований на окремі теми, щоб ви могли перейти безпосередньо до будь-якої конкретної, щоб вирішити ваші конкретні потреби API. -Він також створений як довідник для роботи у майбутньому. +Також він створений як довідник для роботи у майбутньому, тож ви можете повернутися і побачити саме те, що вам потрібно. -Тож ви можете повернутися і побачити саме те, що вам потрібно. - -## Запустіть код +## Запустіть код { #run-the-code } Усі блоки коду можна скопіювати та використовувати безпосередньо (це фактично перевірені файли Python). -Щоб запустити будь-який із прикладів, скопіюйте код у файл `main.py` і запустіть `uvicorn` за допомогою: +Щоб запустити будь-який із прикладів, скопіюйте код у файл `main.py` і запустіть `fastapi dev` за допомогою:
```console -$ uvicorn main:app --reload +$ fastapi dev main.py -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -INFO: Started reloader process [28720] -INFO: Started server process [28722] -INFO: Waiting for application startup. -INFO: Application startup complete. + FastAPI Starting development server 🚀 + + Searching for package file structure from directories + with __init__.py files + Importing from /home/user/code/awesomeapp + + module 🐍 main.py + + code Importing the FastAPI app object from the module with + the following code: + + from main import app + + app Using import string: main:app + + server Server started at http://127.0.0.1:8000 + server Documentation at http://127.0.0.1:8000/docs + + tip Running in development mode, for production use: + fastapi run + + Logs: + + INFO Will watch for changes in these directories: + ['/home/user/code/awesomeapp'] + INFO Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C + to quit) + INFO Started reloader process [383138] using WatchFiles + INFO Started server process [383153] + INFO Waiting for application startup. + INFO Application startup complete. ```
@@ -34,45 +58,33 @@ $ uvicorn main:app --reload --- -## Встановлення FastAPI +## Встановлення FastAPI { #install-fastapi } Першим кроком є встановлення FastAPI. -Для туторіалу ви можете встановити його з усіма необов’язковими залежностями та функціями: +Переконайтеся, що ви створили [віртуальне середовище](../virtual-environments.md){.internal-link target=_blank}, активували його, а потім **встановіть FastAPI**:
```console -$ pip install "fastapi[all]" +$ pip install "fastapi[standard]" ---> 100% ```
-...який також включає `uvicorn`, який ви можете використовувати як сервер, який запускає ваш код. +/// note | Примітка -/// note +Коли ви встановлюєте через `pip install "fastapi[standard]"`, він постачається з деякими типовими необов’язковими стандартними залежностями, включно з `fastapi-cloud-cli`, який дозволяє розгортати в FastAPI Cloud. -Ви також можете встановити його частина за частиною. +Якщо ви не хочете мати ці необов’язкові залежності, натомість можете встановити `pip install fastapi`. -Це те, що ви, ймовірно, зробили б, коли захочете розгорнути свою програму у виробничому середовищі: - -``` -pip install fastapi -``` - -Також встановіть `uvicorn`, щоб він працював як сервер: - -``` -pip install "uvicorn[standard]" -``` - -І те саме для кожної з опціональних залежностей, які ви хочете використовувати. +Якщо ви хочете встановити стандартні залежності, але без `fastapi-cloud-cli`, ви можете встановити через `pip install "fastapi[standard-no-fastapi-cloud-cli]"`. /// -## Розширений посібник користувача +## Розширений посібник користувача { #advanced-user-guide } Існує також **Розширений посібник користувача**, який ви зможете прочитати пізніше після цього **Туторіал - Посібник користувача**. @@ -80,4 +92,4 @@ pip install "uvicorn[standard]" Але вам слід спочатку прочитати **Туторіал - Посібник користувача** (те, що ви зараз читаєте). -Він розроблений таким чином, що ви можете створити повну програму лише за допомогою **Туторіал - Посібник користувача**, а потім розширити її різними способами, залежно від ваших потреб, використовуючи деякі з додаткових ідей з **Розширеного посібника користувача** . +Він розроблений таким чином, що ви можете створити повну програму лише за допомогою **Туторіал - Посібник користувача**, а потім розширити її різними способами, залежно від ваших потреб, використовуючи деякі з додаткових ідей з **Розширеного посібника користувача**. diff --git a/docs/uk/docs/tutorial/metadata.md b/docs/uk/docs/tutorial/metadata.md index 64e667ec69..cf17045628 100644 --- a/docs/uk/docs/tutorial/metadata.md +++ b/docs/uk/docs/tutorial/metadata.md @@ -1,26 +1,26 @@ -# Метадані та URL-адреси документації +# Метадані та URL-адреси документації { #metadata-and-docs-urls } Ви можете налаштувати кілька конфігурацій метаданих у Вашому додатку **FastAPI**. -## Метадані для API +## Метадані для API { #metadata-for-api } Ви можете встановити такі поля, які використовуються в специфікації OpenAPI та в автоматично згенерованих інтерфейсах документації API: | Параметр | Тип | Опис | |------------|------|-------------| | `title` | `str` | Назва API. | -| `summary` | `str` | Короткий опис API. Доступно з OpenAPI 3.1.0, FastAPI 0.99.0. | -| `description` | `str` | Більш детальний опис API. Може використовувати Markdown. | +| `summary` | `str` | Короткий підсумок API. Доступно з OpenAPI 3.1.0, FastAPI 0.99.0. | +| `description` | `str` | Короткий опис API. Може використовувати Markdown. | | `version` | `string` | Версія API. Це версія Вашого додатка, а не OpenAPI. Наприклад, `2.5.0`. | -| `terms_of_service` | `str` | URL до умов використання API. Якщо вказано, має бути у форматі URL. | -| `contact` | `dict` | Інформація для контакту з API. Може містити кілька полів.
contact поля
ПараметрТипОпис
namestrІм'я контактної особи або організації.
urlstrURL з інформацією для контакту. Повинен бути у форматі URL.
emailstrEmail контактної особи або організації. Повинен бути у форматі електронної пошти.
| -| `license_info` | `dict` | Інформація про ліцензію для API. Може містити кілька полів.
license_info поля
ПараметрТипОпис
namestrОБОВ'ЯЗКОВО (якщо встановлено license_info). Назва ліцензії для API.
identifierstrЛіцензійний вираз за SPDX для API. Поле identifier взаємовиключне з полем url. Доступно з OpenAPI 3.1.0, FastAPI 0.99.0.
urlstrURL до ліцензії, яка використовується для API. Повинен бути у форматі URL.
| +| `terms_of_service` | `str` | URL до умов використання API. Якщо вказано, має бути у форматі URL. | +| `contact` | `dict` | Інформація для контакту з опублікованим API. Може містити кілька полів.
contact поля
ПараметрТипОпис
namestrІдентифікаційне ім'я контактної особи або організації.
urlstrURL, що вказує на контактну інформацію. МАЄ бути у форматі URL.
emailstrАдреса електронної пошти контактної особи або організації. МАЄ бути у форматі адреси електронної пошти.
| +| `license_info` | `dict` | Інформація про ліцензію для опублікованого API. Може містити кілька полів.
license_info поля
ПараметрТипОпис
namestrОБОВ'ЯЗКОВО (якщо встановлено license_info). Назва ліцензії для API.
identifierstrЛіцензійний вираз за SPDX для API. Поле identifier взаємовиключне з полем url. Доступно з OpenAPI 3.1.0, FastAPI 0.99.0.
urlstrURL до ліцензії, яка використовується для API. МАЄ бути у форматі URL.
| Ви можете налаштувати їх наступним чином: -{* ../../docs_src/metadata/tutorial001.py hl[3:16, 19:32] *} +{* ../../docs_src/metadata/tutorial001_py39.py hl[3:16, 19:32] *} -/// tip | Підказка +/// tip | Порада У полі `description` можна використовувати Markdown, і він буде відображатися у результаті. @@ -30,15 +30,15 @@ -## Ідентифікатор ліцензії +## Ідентифікатор ліцензії { #license-identifier } З початку використання OpenAPI 3.1.0 та FastAPI 0.99.0 Ви також можете налаштувати `license_info` за допомогою `identifier` замість `url`. Наприклад: -{* ../../docs_src/metadata/tutorial001_1.py hl[31] *} +{* ../../docs_src/metadata/tutorial001_1_py39.py hl[31] *} -## Метадані для тегів +## Метадані для тегів { #metadata-for-tags } Ви також можете додати додаткові метадані для різних тегів, які використовуються для групування операцій шляхів, за допомогою параметра `openapi_tags`. @@ -46,53 +46,53 @@ Кожен словник може містити: -* `name` (**обов'язково**): `str` з тією ж назвою тегу, яку Ви використовуєте у параметрі `tags` у Ваших *операціях шляху* та `APIRouter`s. -* `description`: `str` з коротким описом тегу. Може містити Markdown і буде відображено в інтерфейсі документації. -* `externalDocs`: `dict` який описує зовнішню документацію з такими полями: +* `name` (**обов'язково**): `str` з тією ж назвою тегу, яку Ви використовуєте у параметрі `tags` у Ваших *операціях шляху* та `APIRouter`s. +* `description`: `str` з коротким описом тегу. Може містити Markdown і буде показано в інтерфейсі документації. +* `externalDocs`: `dict`, який описує зовнішню документацію з такими полями: * `description`: `str` з коротким описом зовнішньої документації. - * `url` (**обов'язково**): `str`з URL-адресою зовнішньої документації. + * `url` (**обов'язково**): `str` з URL-адресою зовнішньої документації. -### Створення метаданих для тегів +### Створення метаданих для тегів { #create-metadata-for-tags } Спробуймо це на прикладі з тегами для `users` та `items`. -Створіть метадані для своїх тегів і передайте їх у параметр `openapi_tags`: +Створіть метадані для своїх тегів і передайте їх у параметр `openapi_tags`: -{* ../../docs_src/metadata/tutorial004.py hl[3:16,18] *} +{* ../../docs_src/metadata/tutorial004_py39.py hl[3:16,18] *} Зверніть увагу, що в описах можна використовувати Markdown, наприклад, "login" буде показано жирним шрифтом (**login**), а "fancy" буде показано курсивом (_fancy_). /// tip | Порада -Не обов'язково додавати метадані для всіх тегів, які Ви використовуєте. +Вам не потрібно додавати метадані для всіх тегів, які Ви використовуєте. /// -### Використання тегів +### Використовуйте свої теги { #use-your-tags } -Використовуйте параметр `tags` зі своїми *операціями шляху* (і `APIRouter`) для призначення їх до різних тегів: +Використовуйте параметр `tags` зі своїми *операціями шляху* (і `APIRouter`s), щоб призначити їх до різних тегів: -{* ../../docs_src/metadata/tutorial004.py hl[21,26] *} +{* ../../docs_src/metadata/tutorial004_py39.py hl[21,26] *} /// info | Інформація -Детальніше про теги читайте в розділі [Конфігурація шляхів операцій](path-operation-configuration.md#tags){.internal-link target=_blank}. +Детальніше про теги читайте в розділі [Конфігурація операції шляху](path-operation-configuration.md#tags){.internal-link target=_blank}. /// -### Перевірка документації +### Перевірте документацію { #check-the-docs } -Якщо Ви зараз перевірите документацію, вона покаже всі додаткові метадані: +Тепер, якщо Ви перевірите документацію, вона покаже всі додаткові метадані: -### Порядок тегів +### Порядок тегів { #order-of-tags } Порядок кожного словника метаданих тегу також визначає порядок відображення в інтерфейсі документації. -Наприклад, хоча `users` мав би йти після `items` в алфавітному порядку, він відображається перед ними, оскільки ми додали його метадані як перший словник у списку. +Наприклад, хоча `users` мав би йти після `items` в алфавітному порядку, він відображається перед ними, оскільки ми додали їхні метадані як перший словник у списку. -## URL для OpenAPI +## URL для OpenAPI { #openapi-url } За замовчуванням схема OpenAPI надається за адресою `/openapi.json`. @@ -100,11 +100,11 @@ Наприклад, щоб налаштувати його на `/api/v1/openapi.json`: -{* ../../docs_src/metadata/tutorial002.py hl[3] *} +{* ../../docs_src/metadata/tutorial002_py39.py hl[3] *} Якщо Ви хочете повністю вимкнути схему OpenAPI, Ви можете встановити `openapi_url=None`, це також вимкне інтерфейси документації, які її використовують. -## URL-адреси документації +## URL-адреси документації { #docs-urls } Ви можете налаштувати два інтерфейси користувача для документації, які включені: @@ -117,4 +117,4 @@ Наприклад, щоб налаштувати Swagger UI на `/documentation` і вимкнути ReDoc: -{* ../../docs_src/metadata/tutorial003.py hl[3] *} +{* ../../docs_src/metadata/tutorial003_py39.py hl[3] *} diff --git a/docs/uk/docs/tutorial/middleware.md b/docs/uk/docs/tutorial/middleware.md index 13ce8573de..2d1580e49e 100644 --- a/docs/uk/docs/tutorial/middleware.md +++ b/docs/uk/docs/tutorial/middleware.md @@ -1,45 +1,43 @@ -# Middleware (Проміжний шар) +# Middleware { #middleware } -У **FastAPI** можна додавати middleware (проміжний шар). +У **FastAPI** можна додавати middleware. -"Middleware" — це функція, яка працює з кожним **запитом** перед його обробкою будь-якою конкретною *операцією шляху* (*path operation*), а також з кожною **відповіддю** перед її поверненням. +«Middleware» — це функція, яка працює з кожним **запитом** перед його обробкою будь-якою конкретною *операцією шляху*. А також з кожною **відповіддю** перед її поверненням. -* Middleware отримує кожен **запит**, що надходить до Вашого застосунку. -* Може виконати певні дії із цим **запитом** або запустити необхідний код. -* Далі передає **запит** для обробки основним застосунком (*операцією шляху*). -* Отримує **відповідь**, сформовану застосунком (*операцією шляху*). -* Може змінити цю **відповідь** або виконати додатковий код. -* Повертає **відповідь** клієнту. +* Вона отримує кожен **запит**, що надходить до вашого застосунку. +* Потім вона може виконати певні дії із цим **запитом** або запустити необхідний код. +* Далі вона передає **запит** для обробки рештою застосунку (якоюсь *операцією шляху*). +* Потім вона отримує **відповідь**, сформовану застосунком (якоюсь *операцією шляху*). +* Вона може виконати певні дії із цією **відповіддю** або запустити необхідний код. +* Потім вона повертає **відповідь**. /// note | Технічні деталі -Якщо у Вас є залежності з `yield`, код виходу виконається *після* middleware. +Якщо у вас є залежності з `yield`, код виходу виконається *після* middleware. -Якщо були заплановані фонові задачі (background tasks - розглянуто далі), вони виконаються *після* всіх middleware. +Якщо були заплановані фонові задачі (розглянуто в розділі [Background Tasks](background-tasks.md){.internal-link target=_blank}, ви побачите це пізніше), вони виконаються *після* всіх middleware. /// -## Створення middleware +## Створення middleware { #create-a-middleware } -Щоб створити middleware, Ви використовуєте декоратор `@app.middleware("http")` на функції. +Щоб створити middleware, ви використовуєте декоратор `@app.middleware("http")` над функцією. Функція middleware отримує: -* `Запит`. -* Функцію `call_next`, яка приймає `запит` як параметр. - * Ця функція передає `запит` відповідній *операції шляху*. - * Потім вона повертає `відповідь`, згенеровану цією *операцією шляху*. +* `request`. +* Функцію `call_next`, яка отримає `request` як параметр. + * Ця функція передасть `request` відповідній *операції шляху*. + * Потім вона поверне `response`, згенеровану відповідною *операцією шляху*. +* Потім ви можете додатково змінити `response` перед тим, як повернути її. -* Ви можете ще змінити `відповідь` перед тим, як повернути її. - - -{* ../../docs_src/middleware/tutorial001.py hl[8:9,11,14] *} +{* ../../docs_src/middleware/tutorial001_py39.py hl[8:9,11,14] *} /// tip | Порада -Не забувайте, що власні заголовки можна додавати, використовуючи префікс 'X-'. +Пам’ятайте, що власні пропрієтарні заголовки можна додавати використовуючи префікс `X-`. -Але якщо у Вас є власні заголовки, які Ви хочете, щоб браузерний клієнт міг побачити, потрібно додати їх до Вашої конфігурації CORS (див. [CORS (Обмін ресурсами між різними джерелами)](cors.md){.internal-link target=_blank} за допомогою параметра `expose_headers`, описаного в документації Starlette по CORS. +Але якщо у вас є власні заголовки, які ви хочете, щоб клієнт у браузері міг побачити, потрібно додати їх до ваших конфігурацій CORS ([CORS (Cross-Origin Resource Sharing)](cors.md){.internal-link target=_blank}) за допомогою параметра `expose_headers`, описаного в документації Starlette по CORS. /// @@ -47,28 +45,50 @@ Ви також можете використати `from starlette.requests import Request`. -**FastAPI** надає це для Вашої зручності як розробника. Але він походить безпосередньо зі Starlette. +**FastAPI** надає це для вашої зручності як розробника. Але воно походить безпосередньо зі Starlette. /// -### До і після `response`(`відповіді`) +### До і після `response` { #before-and-after-the-response } -Ви можете додати код, який буде виконуватися з `запитом` (`request`), до того, як його обробить будь-яка *операція шляху* (*path operation*). +Ви можете додати код, який буде виконуватися з `request`, до того, як його отримає будь-яка *операція шляху*. -Також Ви можете додати код, який буде виконуватися після того, як `відповідь` (`response`) буде згенеровано, перед тим як його повернути. +Також ви можете додати код, який буде виконуватися після того, як `response` буде згенеровано, перед тим як її повернути. -Наприклад, Ви можете додати власний заголовок `X-Process-Time`, який міститиме час у секундах, який витратився на обробку запиту та генерацію відповіді: +Наприклад, ви можете додати власний заголовок `X-Process-Time`, який міститиме час у секундах, який витратився на обробку запиту та генерацію відповіді: -{* ../../docs_src/middleware/tutorial001.py hl[10,12:13] *} +{* ../../docs_src/middleware/tutorial001_py39.py hl[10,12:13] *} - -/// tip | Підказка +/// tip | Порада Тут ми використовуємо `time.perf_counter()` замість `time.time()` оскільки він може бути більш точним для таких випадків. 🤓 /// -## Інші middlewares +## Порядок виконання кількох middleware { #multiple-middleware-execution-order } + +Коли ви додаєте кілька middleware, використовуючи або декоратор `@app.middleware()` або метод `app.add_middleware()`, кожен новий middleware обгортає застосунок, утворюючи стек. Останній доданий middleware є *зовнішнім*, а перший — *внутрішнім*. + +На шляху запиту першим виконується *зовнішній* middleware. + +На шляху відповіді він виконується останнім. + +Наприклад: + +```Python +app.add_middleware(MiddlewareA) +app.add_middleware(MiddlewareB) +``` + +Це призводить до такого порядку виконання: + +* **Запит**: MiddlewareB → MiddlewareA → route + +* **Відповідь**: route → MiddlewareA → MiddlewareB + +Така поведінка стеку гарантує, що middleware виконуються у передбачуваному та керованому порядку. + +## Інші middlewares { #other-middlewares } Ви можете пізніше прочитати більше про інші middlewares в [Advanced User Guide: Advanced Middleware](../advanced/middleware.md){.internal-link target=_blank}. diff --git a/docs/uk/docs/tutorial/path-params-numeric-validations.md b/docs/uk/docs/tutorial/path-params-numeric-validations.md index 8ee4f480fe..f6aa920193 100644 --- a/docs/uk/docs/tutorial/path-params-numeric-validations.md +++ b/docs/uk/docs/tutorial/path-params-numeric-validations.md @@ -1,8 +1,8 @@ -# Path Параметри та валідація числових даних +# Параметри шляху та валідація числових даних { #path-parameters-and-numeric-validations } -Так само як Ви можете оголошувати додаткові перевірки та метадані для query параметрів за допомогою `Query`, Ви можете оголошувати той самий тип перевірок і метаданих для параметрів шляху за допомогою `Path`. +Так само як ви можете оголошувати додаткові перевірки та метадані для query параметрів за допомогою `Query`, ви можете оголошувати той самий тип перевірок і метаданих для параметрів шляху за допомогою `Path`. -## Імпорт Path +## Імпорт `Path` { #import-path } Спочатку імпортуйте `Path` з `fastapi` і імпортуйте `Annotated`: @@ -10,70 +10,69 @@ /// info | Інформація -FastAPI додав підтримку `Annotated` (і почав рекомендувати його використання) у версії 0.95.0. +FastAPI додав підтримку `Annotated` (і почав рекомендувати його використання) у версії 0.95.0. -Якщо у Вас стара версія, при спробі використати `Annotated` можуть виникати помилки. +Якщо у вас стара версія, при спробі використати `Annotated` можуть виникати помилки. -Переконайтеся, що Ви [оновили версію FastAPI](../deployment/versions.md#upgrading-the-fastapi-versions){.internal-link target=_blank} принаймні до версії 0.95.1 перед використанням `Annotated`. +Переконайтеся, що ви [оновили версію FastAPI](../deployment/versions.md#upgrading-the-fastapi-versions){.internal-link target=_blank} принаймні до версії 0.95.1 перед використанням `Annotated`. /// -## Оголошення метаданих +## Оголошення метаданих { #declare-metadata } Ви можете оголошувати всі ті ж параметри, що і для `Query`. -Наприклад, щоб оголосити значення метаданих `title` для параметра шляху `item_id`, Ви можете написати: +Наприклад, щоб оголосити значення метаданих `title` для параметра шляху `item_id`, ви можете написати: {* ../../docs_src/path_params_numeric_validations/tutorial001_an_py310.py hl[10] *} /// note | Примітка -Параметр шляху завжди є обов’язковим, оскільки він має бути частиною шляху. Навіть якщо Ви оголосите його зі значенням `None` або встановите значення за замовчуванням — він все одно залишатиметься обов’язковим. +Параметр шляху завжди є обов’язковим, оскільки він має бути частиною шляху. Навіть якщо ви оголосите його зі значенням `None` або встановите значення за замовчуванням — це ні на що не вплине, він все одно завжди буде обов’язковим. /// -## Упорядковуйте параметри, як Вам потрібно +## Упорядковуйте параметри, як вам потрібно { #order-the-parameters-as-you-need } -/// tip | Підказка +/// tip | Порада -Це, мабуть, не настільки важливо або необхідно, якщо Ви використовуєте `Annotated`. +Це, мабуть, не настільки важливо або необхідно, якщо ви використовуєте `Annotated`. /// -Припустимо, Ви хочете оголосити параметр запиту `q` як обов’язковий `str`. +Припустимо, ви хочете оголосити параметр запиту `q` як обов’язковий `str`. -І Вам не потрібно оголошувати нічого іншого для цього параметра, тому немає потреби використовувати `Query`. +І вам не потрібно оголошувати нічого іншого для цього параметра, тому вам насправді не потрібно використовувати `Query`. -Але Вам все одно потрібно використовувати `Path` для параметра шляху `item_id`. І з певних причин Ви не хочете використовувати `Annotated`. +Але вам все одно потрібно використовувати `Path` для параметра шляху `item_id`. І з певних причин ви не хочете використовувати `Annotated`. Python видасть помилку, якщо розмістити значення з "default" перед значенням, яке не має "default". -Але Ви можете змінити порядок і розмістити значення без значення за замовчуванням (параметр запиту `q`) першим. +Але ви можете змінити порядок і розмістити значення без значення за замовчуванням (параметр запиту `q`) першим. +Для **FastAPI** порядок не має значення. Він визначає параметри за їхніми іменами, типами та оголошеннями за замовчуванням (`Query`, `Path` тощо) і не звертає уваги на порядок. -Для **FastAPI** порядок не має значення. Він визначає параметри за їх іменами, типами та значеннями за замовчуванням (`Query`, `Path` тощо) і не звертає уваги на порядок. +Тому ви можете оголосити вашу функцію так: -Тому Ви можете оголосити Вашу функцію так: +{* ../../docs_src/path_params_numeric_validations/tutorial002_py39.py hl[7] *} -{* ../../docs_src/path_params_numeric_validations/tutorial002.py hl[7] *} - -Але майте на увазі, що якщо Ви використовуєте `Annotated`, ця проблема не виникне, оскільки Ви не використовуєте значення за замовчуванням для параметрів `Query()` або `Path()`. +Але майте на увазі, що якщо ви використовуєте `Annotated`, цієї проблеми не буде, це не матиме значення, оскільки ви не використовуєте значення за замовчуванням параметрів функції для `Query()` або `Path()`. {* ../../docs_src/path_params_numeric_validations/tutorial002_an_py39.py *} -## Упорядковуйте параметри за потребою, хитрощі +## Упорядковуйте параметри, як вам потрібно: хитрощі { #order-the-parameters-as-you-need-tricks } -/// tip | Підказка +/// tip | Порада -Це, мабуть, не настільки важливо або необхідно, якщо Ви використовуєте `Annotated`. +Це, мабуть, не настільки важливо або необхідно, якщо ви використовуєте `Annotated`. /// Ось **невелика хитрість**, яка може стати в пригоді, хоча вона рідко знадобиться. -Якщо Ви хочете: +Якщо ви хочете: -* оголосити параметр запиту `q` без використання `Query` або значення за замовчуванням +* оголосити параметр запиту `q` без `Query` і без жодного значення за замовчуванням * оголосити параметр шляху `item_id`, використовуючи `Path` * розмістити їх у різному порядку * не використовувати `Annotated` @@ -84,72 +83,72 @@ Python видасть помилку, якщо розмістити значен Python нічого не зробить із цією `*`, але розпізнає, що всі наступні параметри слід викликати як аргументи за ключовим словом (пари ключ-значення), також відомі як kwargs. Навіть якщо вони не мають значення за замовчуванням. -{* ../../docs_src/path_params_numeric_validations/tutorial003.py hl[7] *} +{* ../../docs_src/path_params_numeric_validations/tutorial003_py39.py hl[7] *} -### Краще з `Annotated` +### Краще з `Annotated` { #better-with-annotated } -Майте на увазі, якщо Ви використовуєте `Annotated`, оскільки Ви не використовуєте значення за замовчуванням для параметрів функції, цієї проблеми не виникне, і, швидше за все, Вам не потрібно буде використовувати `*`. +Майте на увазі, що якщо ви використовуєте `Annotated`, оскільки ви не використовуєте значення за замовчуванням параметрів функції, цієї проблеми не буде, і, ймовірно, вам не потрібно буде використовувати `*`. {* ../../docs_src/path_params_numeric_validations/tutorial003_an_py39.py hl[10] *} -## Валідація числових даних: більше або дорівнює +## Валідація числових даних: більше або дорівнює { #number-validations-greater-than-or-equal } -За допомогою `Query` і `Path` (та інших, які Ви побачите пізніше) можна оголошувати числові обмеження. +За допомогою `Query` і `Path` (та інших, які ви побачите пізніше) можна оголошувати числові обмеження. Тут, завдяки `ge=1`, `item_id` має бути цілим числом, яке "`g`reater than or `e`qual" (більше або дорівнює) `1`. {* ../../docs_src/path_params_numeric_validations/tutorial004_an_py39.py hl[10] *} -## Валідація числових даних: більше ніж і менше або дорівнює +## Валідація числових даних: більше ніж і менше або дорівнює { #number-validations-greater-than-and-less-than-or-equal } Те саме застосовується до: -* `gt`: `g`reater `t`han (більше ніж) -* `le`: `l`ess than or `e`qual (менше або дорівнює) +* `gt`: `g`reater `t`han +* `le`: `l`ess than or `e`qual {* ../../docs_src/path_params_numeric_validations/tutorial005_an_py39.py hl[10] *} -## Валідація числових даних: float, більше ніж і менше ніж +## Валідація числових даних: float, більше ніж і менше ніж { #number-validations-floats-greater-than-and-less-than } Валідація чисел також працює для значень типу `float`. -Ось де стає важливо мати можливість оголошувати gt, а не тільки ge. Це дозволяє, наприклад, вимагати, щоб значення було більше `0`, навіть якщо воно менше `1`. +Ось де стає важливо мати можливість оголошувати gt, а не тільки ge. Це дозволяє, наприклад, вимагати, щоб значення було більше `0`, навіть якщо воно менше `1`. Таким чином, значення `0.5` буде допустимим. Але `0.0` або `0` — ні. -Те саме стосується lt. +Те саме стосується lt. {* ../../docs_src/path_params_numeric_validations/tutorial006_an_py39.py hl[13] *} -## Підсумок +## Підсумок { #recap } -За допомогою `Query`, `Path` (і інших параметрів, які Ви ще не бачили) можна оголошувати метадані та перевірки рядків, так само як у [Query параметри та валідація рядків](query-params-str-validations.md){.internal-link target=_blank}. +За допомогою `Query`, `Path` (і інших параметрів, які ви ще не бачили) можна оголошувати метадані та перевірки рядків так само як у [Query параметри та валідація рядків](query-params-str-validations.md){.internal-link target=_blank}. Також можна оголошувати числові перевірки: -* `gt`: `g`reater `t`han (більше ніж) -* `ge`: `g`reater than or `e`qual (більше або дорівнює) -* `lt`: `l`ess `t`han (менше ніж) -* `le`: `l`ess than or `e`qual (менше або дорівнює) +* `gt`: `g`reater `t`han +* `ge`: `g`reater than or `e`qual +* `lt`: `l`ess `t`han +* `le`: `l`ess than or `e`qual /// info | Інформація -`Query`, `Path` та інші класи, які Ви побачите пізніше, є підкласами спільного класу `Param`. +`Query`, `Path` та інші класи, які ви побачите пізніше, є підкласами спільного класу `Param`. -Всі вони мають однакові параметри для додаткових перевірок і метаданих, які Ви вже бачили. +Всі вони мають однакові параметри для додаткових перевірок і метаданих, які ви вже бачили. /// /// note | Технічні деталі -Коли Ви імпортуєте `Query`, `Path` та інші з `fastapi`, насправді це функції. +Коли ви імпортуєте `Query`, `Path` та інші з `fastapi`, насправді це функції. При виклику вони повертають екземпляри класів з такими ж іменами. -Тобто Ви імпортуєте `Query`, яка є функцією. А коли Ви її викликаєте, вона повертає екземпляр класу, який теж називається `Query`. +Тобто ви імпортуєте `Query`, яка є функцією. А коли ви її викликаєте, вона повертає екземпляр класу, який теж називається `Query`. -Ці функції створені таким чином (замість використання класів напряму), щоб Ваш редактор не відзначав їхні типи як помилки. +Ці функції створені таким чином (замість використання класів напряму), щоб ваш редактор не відзначав їхні типи як помилки. -Таким чином, Ви можете користуватися своїм звичайним редактором і інструментами для програмування без додаткових налаштувань для ігнорування таких помилок. +Таким чином, ви можете користуватися своїм звичайним редактором і інструментами для програмування без додаткових налаштувань для ігнорування таких помилок. /// diff --git a/docs/uk/docs/tutorial/path-params.md b/docs/uk/docs/tutorial/path-params.md index da4ff2f9ea..0598905495 100644 --- a/docs/uk/docs/tutorial/path-params.md +++ b/docs/uk/docs/tutorial/path-params.md @@ -1,34 +1,34 @@ -# Path Параметри +# Параметри шляху { #path-parameters } -Ви можете визначити "параметри" або "змінні" шляху, використовуючи синтаксис форматованих рядків: +Ви можете оголосити «параметри» або «змінні» шляху, використовуючи той самий синтаксис, що й у форматованих рядках Python: -{* ../../docs_src/path_params/tutorial001.py hl[6:7] *} +{* ../../docs_src/path_params/tutorial001_py39.py hl[6:7] *} -Значення параметра шляху `item_id` передається у функцію як аргумент `item_id`. +Значення параметра шляху `item_id` буде передано у вашу функцію як аргумент `item_id`. -Якщо запустити цей приклад та перейти за посиланням http://127.0.0.1:8000/items/foo, то отримаємо таку відповідь: +Отже, якщо ви запустите цей приклад і перейдете за посиланням http://127.0.0.1:8000/items/foo, то побачите відповідь: ```JSON {"item_id":"foo"} ``` -## Path параметри з типами +## Параметри шляху з типами { #path-parameters-with-types } -Ви можете визначити тип параметра шляху у функції, використовуючи стандартні анотації типів Python: +Ви можете оголосити тип параметра шляху у функції, використовуючи стандартні анотації типів Python: -{* ../../docs_src/path_params/tutorial002.py hl[7] *} +{* ../../docs_src/path_params/tutorial002_py39.py hl[7] *} -У такому випадку `item_id` визначається як `int`. +У цьому випадку `item_id` оголошено як `int`. /// check | Примітка -Це дасть можливість підтримки редактора всередині функції з перевірками помилок, автодоповнення тощо. +Це дасть вам підтримку редактора всередині функції з перевірками помилок, автодоповненням тощо. /// -## Перетворення даних +## Перетворення даних { #data-conversion } -Якщо запустити цей приклад і перейти за посиланням http://127.0.0.1:8000/items/3, то отримаєте таку відповідь: +Якщо ви запустите цей приклад і відкриєте у браузері http://127.0.0.1:8000/items/3, то побачите відповідь: ```JSON {"item_id":3} @@ -36,15 +36,15 @@ /// check | Примітка -Зверніть увагу, що значення, яке отримала (і повернула) ваша функція, — це `3`. Це Python `int`, а не рядок `"3"`. +Зверніть увагу, що значення, яке отримала (і повернула) ваша функція, — це `3`, як Python `int`, а не рядок `"3"`. -Отже, з таким оголошенням типу **FastAPI** автоматично виконує "парсинг" запитів. +Отже, з таким оголошенням типу **FastAPI** надає вам автоматичний «parsing» запиту. /// -## Перевірка даних +## Валідація даних { #data-validation } -Якщо ж відкрити у браузері посилання http://127.0.0.1:8000/items/foo, то побачимо цікаву HTTP-помилку: +Але якщо ви перейдете у браузері за посиланням http://127.0.0.1:8000/items/foo, ви побачите гарну HTTP-помилку: ```JSON { @@ -61,144 +61,136 @@ ] } ``` -тому що параметр шляху має значення `"foo"`, яке не є типом `int`. -Таку саму помилку отримаємо, якщо передати `float` замість `int`, як бачимо, у цьому прикладі: http://127.0.0.1:8000/items/4.2 +тому що параметр шляху `item_id` мав значення `"foo"`, яке не є `int`. + +Та сама помилка з’явиться, якщо ви передасте `float` замість `int`, як у: http://127.0.0.1:8000/items/4.2 /// check | Примітка -Отже, **FastAPI** надає перевірку типів з таким самим оголошенням типу в Python. +Отже, з тим самим оголошенням типу в Python **FastAPI** надає вам валідацію даних. -Зверніть увагу, що помилка також чітко вказує саме на те місце, де валідація не пройшла. +Зверніть увагу, що помилка також чітко вказує саме місце, де валідація не пройшла. -Це неймовірно корисно під час розробки та дебагінгу коду, що взаємодіє з вашим API. +Це неймовірно корисно під час розробки та налагодження коду, що взаємодіє з вашим API. /// -## Документація +## Документація { #documentation } -Тепер коли відкриєте свій браузер за посиланням http://127.0.0.1:8000/docs, то побачите автоматично згенеровану, інтерактивну API-документацію: +А коли ви відкриєте у браузері http://127.0.0.1:8000/docs, ви побачите автоматичну, інтерактивну, API-документацію на кшталт: /// check | Примітка -Знову ж таки, лише з цим самим оголошенням типу в Python, FastAPI надає вам автоматичну, інтерактивну документацію (з інтеграцією Swagger UI). - -Зверніть увагу, що параметр шляху оголошений як ціле число. +Знову ж таки, лише з тим самим оголошенням типу в Python **FastAPI** надає вам автоматичну, інтерактивну документацію (з інтеграцією Swagger UI). +Зверніть увагу, що параметр шляху оголошено як ціле число. /// -## Переваги стандартизації, альтернативна документація +## Переваги стандартів, альтернативна документація { #standards-based-benefits-alternative-documentation } І оскільки згенерована схема відповідає стандарту OpenAPI, існує багато сумісних інструментів. -З цієї причини FastAPI також надає альтернативну документацію API (використовуючи ReDoc), до якої можна отримати доступ за посиланням http://127.0.0.1:8000/redoc: +Через це **FastAPI** також надає альтернативну API-документацію (використовуючи ReDoc), до якої ви можете отримати доступ за посиланням http://127.0.0.1:8000/redoc: -Таким чином, існує багато сумісних інструментів, включаючи інструменти для генерації коду для багатьох мов. +Так само, існує багато сумісних інструментів. Зокрема інструменти генерації коду для багатьох мов. +## Pydantic { #pydantic } -## Pydantic +Уся валідація даних виконується за лаштунками за допомогою Pydantic, тож ви отримуєте всі переваги від його використання. І ви знаєте, що ви в надійних руках. -Вся валідація даних виконується за лаштунками за допомогою Pydantic, тому Ви отримуєте всі переваги від його використання. І можете бути впевнені, що все в надійних руках. +Ви можете використовувати ті самі оголошення типів з `str`, `float`, `bool` та багатьма іншими складними типами даних. -Ви можете використовувати ті самі оголошення типів з `str`, `float`, `bool` та багатьма іншими складними типами даних. +Декілька з них розглядаються в наступних розділах посібника. -Декілька з них будуть розглянуті в наступних розділах посібника. +## Порядок має значення { #order-matters } -## Порядок має значення +Під час створення *операцій шляху* можуть виникати ситуації, коли у вас є фіксований шлях. -При створенні *операцій шляху* можуть виникати ситуації, коли шлях фіксований. +Наприклад, `/users/me` — припустімо, це для отримання даних про поточного користувача. -Наприклад, `/users/me`. Припустимо, що це шлях для отримання даних про поточного користувача. +І тоді у вас також може бути шлях `/users/{user_id}` для отримання даних про конкретного користувача за його ID. -А також у вас може бути шлях `/users/{user_id}`, щоб отримати дані про конкретного користувача за його ID. +Оскільки *операції шляху* оцінюються по черзі, вам потрібно переконатися, що шлях для `/users/me` оголошено перед шляхом для `/users/{user_id}`: -Оскільки *операції шляху* оцінюються по черзі, Ви повинні переконатися, що шлях для `/users/me` оголошений перед шляхом для `/users/{user_id}`: +{* ../../docs_src/path_params/tutorial003_py39.py hl[6,11] *} -{* ../../docs_src/path_params/tutorial003.py hl[6,11] *} +Інакше шлях для `/users/{user_id}` також відповідатиме `/users/me`, «вважаючи», що отримує параметр `user_id` зі значенням `"me"`. -Інакше шлях для `/users/{user_id}` також буде відповідати для `/users/me`, "вважаючи", що він отримує параметр `user_id` зі значенням `"me"`. +Так само ви не можете перевизначити операцію шляху: -Аналогічно, Ви не можете оголосити операцію шляху: +{* ../../docs_src/path_params/tutorial003b_py39.py hl[6,11] *} -{* ../../docs_src/path_params/tutorial003b.py hl[6,11] *} +Завжди використовуватиметься перша, оскільки шлях збігається першим. -Перша операція буде завжди використовуватися, оскільки шлях збігається першим. -## Попередньо визначені значення +## Попередньо визначені значення { #predefined-values } -Якщо у вас є *операція шляху*, яка приймає *параметр шляху*, але Ви хочете, щоб можливі допустимі значення *параметра шляху* були попередньо визначені, Ви можете використати стандартний Python Enum. +Якщо у вас є *операція шляху*, яка отримує *параметр шляху*, але ви хочете, щоб можливі коректні значення *параметра шляху* були попередньо визначені, ви можете використати стандартний Python `Enum`. -### Створення класу `Enum` +### Створіть клас `Enum` { #create-an-enum-class } Імпортуйте `Enum` і створіть підклас, що наслідується від `str` та `Enum`. -Наслідуючи від `str`, документація API зможе визначити, що значення повинні бути типу `string`, і правильно їх відобразить. +Завдяки наслідуванню від `str` документація API зможе визначити, що значення повинні бути типу `string`, і зможе коректно їх відобразити. -Після цього створіть атрибути класу з фіксованими значеннями, які будуть доступними допустимими значеннями: +Після цього створіть атрибути класу з фіксованими значеннями, які будуть доступними коректними значеннями: -{* ../../docs_src/path_params/tutorial005.py hl[1,6:9] *} - -/// info | Додаткова інформація - -Перелічення (або enums) доступні в Python починаючи з версії 3.4. - -/// +{* ../../docs_src/path_params/tutorial005_py39.py hl[1,6:9] *} /// tip | Порада -Якщо вам цікаво, "AlexNet", "ResNet" та "LeNet" — це просто назви ML моделей Machine Learning. +Якщо вам цікаво, «AlexNet», «ResNet» та «LeNet» — це просто назви Machine Learning models. /// - -### Оголосіть *параметр шляху* +### Оголосіть *параметр шляху* { #declare-a-path-parameter } Потім створіть *параметр шляху* з анотацією типу, використовуючи створений вами клас enum (`ModelName`): -{* ../../docs_src/path_params/tutorial005.py hl[16] *} +{* ../../docs_src/path_params/tutorial005_py39.py hl[16] *} -### Перевірка документації +### Перевірте документацію { #check-the-docs } -Оскільки доступні значення для *параметра шляху* визначені заздалегідь, інтерактивна документація зможе красиво їх відобразити: +Оскільки доступні значення для *параметра шляху* визначені заздалегідь, інтерактивна документація може красиво їх показати: -### Робота з *перелічуваннями* у Python +### Робота з Python *переліченнями* { #working-with-python-enumerations } -Значення *параметра шляху* буде елементом *перелічування*. +Значення *параметра шляху* буде *елементом перелічування*. -#### Порівняння *елементів перелічування* +#### Порівняйте *елементи перелічування* { #compare-enumeration-members } -Ви можете порівнювати його з *елементами перелічування* у створеному вами enum `ModelName`: +Ви можете порівнювати його з *елементом перелічування* у створеному вами enum `ModelName`: -{* ../../docs_src/path_params/tutorial005.py hl[17] *} +{* ../../docs_src/path_params/tutorial005_py39.py hl[17] *} -#### Отримання *значення перелічування* +#### Отримайте *значення перелічування* { #get-the-enumeration-value } Ви можете отримати фактичне значення (у цьому випадку це `str`), використовуючи `model_name.value`, або загалом `your_enum_member.value`: -{* ../../docs_src/path_params/tutorial005.py hl[20] *} +{* ../../docs_src/path_params/tutorial005_py39.py hl[20] *} /// tip | Порада -Ви також можете отримати доступ до значення `"lenet"`, використовуючи `ModelName.lenet.value`. +Ви також можете отримати доступ до значення `"lenet"` через `ModelName.lenet.value`. /// +#### Поверніть *елементи перелічування* { #return-enumeration-members } -#### Повернення *елементів перелічування* - -Ви можете повертати *елементи перелічування* з вашої *операції шляху*, навіть вкладені у JSON-тіло (наприклад, `dict`). +Ви можете повертати *елементи enum* з вашої *операції шляху*, навіть вкладені у JSON-тіло (наприклад, `dict`). Вони будуть перетворені на відповідні значення (у цьому випадку рядки) перед поверненням клієнту: -{* ../../docs_src/path_params/tutorial005.py hl[18,21,23] *} +{* ../../docs_src/path_params/tutorial005_py39.py hl[18,21,23] *} -На стороні клієнта Ви отримаєте відповідь у форматі JSON, наприклад: +На стороні клієнта ви отримаєте відповідь у форматі JSON, наприклад: ```JSON { @@ -207,36 +199,35 @@ } ``` -## Path-параметри, що містять шляхи +## Параметри шляху, що містять шляхи { #path-parameters-containing-paths } -Припустимо, у вас є *операція шляху* з маршрутом `/files/{file_path}`. +Припустімо, у вас є *операція шляху* зі шляхом `/files/{file_path}`. -Але вам потрібно, щоб `file_path` містив *шлях*, наприклад `home/johndoe/myfile.txt`. +Але вам потрібно, щоб `file_path` сам містив *шлях*, наприклад `home/johndoe/myfile.txt`. -Отже, URL для цього файлу виглядатиме так: `/files/home/johndoe/myfile.txt`. +Отже, URL для цього файлу виглядатиме приблизно так: `/files/home/johndoe/myfile.txt`. +### Підтримка OpenAPI { #openapi-support } +OpenAPI не підтримує спосіб оголошення *параметра шляху*, який має містити всередині *шлях*, оскільки це може призвести до сценаріїв, які складно тестувати та визначати. -### Підтримка OpenAPI +Проте ви все одно можете зробити це в **FastAPI**, використовуючи один із внутрішніх інструментів Starlette. -OpenAPI не підтримує спосіб оголошення *параметра шляху*, що містить *шлях* всередині, оскільки це може призвести до сценаріїв, які складно тестувати та визначати. +І документація все ще працюватиме, хоча й не додаватиме жодної документації, яка б казала, що параметр має містити шлях. -Однак (одначе), Ви все одно можете зробити це в **FastAPI**, використовуючи один із внутрішніх інструментів Starlette. +### Конвертер шляху { #path-convertor } -Документація все ще працюватиме, хоча й не додаватиме опису про те, що параметр повинен містити шлях. - -### Конвертер шляху - -Використовуючи опцію безпосередньо зі Starlette, Ви можете оголосити *параметр шляху*, що містить *шлях*, використовуючи URL на кшталт: +Використовуючи опцію безпосередньо зі Starlette, ви можете оголосити *параметр шляху*, що містить *шлях*, використовуючи URL на кшталт: ``` /files/{file_path:path} ``` -У цьому випадку ім'я параметра — `file_path`, а остання частина `:path` вказує на те, що параметр повинен відповідати будь-якому *шляху*. -Отже, Ви можете використати його так: +У цьому випадку ім’я параметра — `file_path`, а остання частина `:path` вказує, що параметр має відповідати будь-якому *шляху*. -{* ../../docs_src/path_params/tutorial004.py hl[6] *} +Отже, ви можете використати його так: + +{* ../../docs_src/path_params/tutorial004_py39.py hl[6] *} /// tip | Порада @@ -246,15 +237,15 @@ OpenAPI не підтримує спосіб оголошення *параме /// -## Підсумок +## Підсумок { #recap } -З **FastAPI**, використовуючи короткі, інтуїтивно зрозумілі та стандартні оголошення типів Python, Ви отримуєте: +З **FastAPI**, використовуючи короткі, інтуїтивно зрозумілі та стандартні оголошення типів Python, ви отримуєте: -* Підтримку в редакторі: перевірка помилок, автодоповнення тощо. -* "Парсинг" даних +* Підтримку редактора: перевірка помилок, автодоповнення тощо. +* Перетворення даних «parsing» * Валідацію даних * Анотацію API та автоматичну документацію І вам потрібно оголосити їх лише один раз. -Це, ймовірно, основна видима перевага **FastAPI** порівняно з альтернативними фреймворками (окрім високої продуктивності). +Це, ймовірно, основна видима перевага **FastAPI** порівняно з альтернативними фреймворками (окрім сирої продуктивності). diff --git a/docs/uk/docs/tutorial/query-param-models.md b/docs/uk/docs/tutorial/query-param-models.md index 97eb82fa1b..a28bf6c27c 100644 --- a/docs/uk/docs/tutorial/query-param-models.md +++ b/docs/uk/docs/tutorial/query-param-models.md @@ -1,4 +1,4 @@ -# Моделі Query параметрів +# Моделі параметрів запиту { #query-parameter-models } Якщо у Вас є група **query параметрів**, які пов’язані між собою, Ви можете створити **Pydantic-модель** для їх оголошення. @@ -10,7 +10,7 @@ /// -## Query параметри з Pydantic-моделлю +## Query параметри з Pydantic-моделлю { #query-parameters-with-a-pydantic-model } Оголосіть **query параметри**, які Вам потрібні, у **Pydantic-моделі**, а потім оголосіть цей параметр як `Query`: @@ -18,7 +18,7 @@ **FastAPI** буде **витягувати** дані для **кожного поля** з **query параметрів** у запиті та передавати їх у визначену вами Pydantic-модель. -## Перевірте документацію +## Перевірте документацію { #check-the-docs } Ви можете побачити параметри запиту в UI документації за `/docs`: @@ -26,7 +26,7 @@ -## Заборона зайвих Query параметрів +## Заборона зайвих Query параметрів { #forbid-extra-query-parameters } У деяких особливих випадках (ймовірно, не дуже поширених) Ви можете захотіти **обмежити** query параметри, які дозволено отримувати. @@ -34,7 +34,7 @@ {* ../../docs_src/query_param_models/tutorial002_an_py310.py hl[10] *} -Якщо клієнт спробує надіслати **зайві** дані у **query параметрах**, він отримає **помилку**. +Якщо клієнт спробує надіслати **зайві** дані у **query параметрах**, він отримає **помилку** відповідь. Наприклад, якщо клієнт спробує надіслати query параметр `tool` зі значенням `plumbus`, як у цьому запиті: @@ -57,11 +57,11 @@ https://example.com/items/?limit=10&tool=plumbus } ``` -## Підсумок +## Підсумок { #summary } Ви можете використовувати **Pydantic-моделі** для оголошення **query параметрів** у **FastAPI**. 😎 -/// tip | Підказка +/// tip | Порада Спойлер: Ви також можете використовувати Pydantic-моделі для оголошення cookie та заголовків, але про це Ви дізнаєтеся пізніше в цьому посібнику. 🤫 diff --git a/docs/uk/docs/tutorial/query-params-str-validations.md b/docs/uk/docs/tutorial/query-params-str-validations.md index cd3f4ad935..414987880b 100644 --- a/docs/uk/docs/tutorial/query-params-str-validations.md +++ b/docs/uk/docs/tutorial/query-params-str-validations.md @@ -1,26 +1,26 @@ -# Query параметри та валідація рядків +# Query параметри та валідація рядків { #query-parameters-and-string-validations } -**FastAPI** дозволяє оголошувати додаткову інформацію та виконувати валідацію для Ваших параметрів. +**FastAPI** дозволяє оголошувати додаткову інформацію та виконувати валідацію для ваших параметрів. Розглянемо цей додаток як приклад: {* ../../docs_src/query_params_str_validations/tutorial001_py310.py hl[7] *} -Query параметр `q` має тип `str | None`, що означає, що він може бути як `str`, так і `None`. За замовчуванням він має значення `None`, тому FastAPI розуміє, що цей параметр не є обов'язковим. +Query параметр `q` має тип `str | None`, що означає, що він має тип `str`, але також може бути `None`, і справді, значення за замовчуванням — `None`, тож FastAPI знатиме, що він не є обов'язковим. /// note | Примітка -FastAPI знає, що `q` не є обов’язковим, завдяки значенню за замовчуванням `= None`. +FastAPI знатиме, що значення `q` не є обов’язковим, завдяки значенню за замовчуванням `= None`. -Використання `str | None` дозволить Вашому редактору коду надавати кращу підтримку та виявляти помилки. +Використання `str | None` дозволить вашому редактору коду надавати кращу підтримку та виявляти помилки. /// -## Додаткова валідація +## Додаткова валідація { #additional-validation } -Ми хочемо, щоб навіть якщо `q` є необов’язковим, **його довжина не перевищувала 50 символів**, якщо він все ж буде переданий. +Ми хочемо, щоб навіть якщо `q` є необов’язковим, коли його передають, **його довжина не перевищувала 50 символів**. -### Імпорт `Query` та `Annotated` +### Імпорт `Query` та `Annotated` { #import-query-and-annotated } Щоб це зробити, спочатку імпортуємо: @@ -33,13 +33,13 @@ FastAPI знає, що `q` не є обов’язковим, завдяки з FastAPI додав підтримку `Annotated` (і почав рекомендувати його) у версії 0.95.0. -Якщо у Вас старіша версія, під час використання `Annotated` можуть виникати помилки. +Якщо у вас старіша версія, під час використання `Annotated` можуть виникати помилки. -Переконайтеся, що Ви [оновили версію FastAPI](../deployment/versions.md#upgrading-the-fastapi-versions){.internal-link target=_blank} до принаймні 0.95.1, перш ніж використовувати `Annotated`. +Переконайтеся, що ви [оновили версію FastAPI](../deployment/versions.md#upgrading-the-fastapi-versions){.internal-link target=_blank} до принаймні 0.95.1, перш ніж використовувати `Annotated`. /// -## Використання `Annotated` у типі параметра `q` +## Використання `Annotated` у типі параметра `q` { #use-annotated-in-the-type-for-the-q-parameter } Пам’ятаєте, як я раніше розповідав, що `Annotated` можна використовувати для додавання метаданих до параметрів у [Вступі до типів Python](../python-types.md#type-hints-with-metadata-annotations){.internal-link target=_blank}? @@ -55,7 +55,7 @@ q: str | None = None //// -//// tab | Python 3.8+ +//// tab | Python 3.9+ ```Python q: Union[str, None] = None @@ -73,7 +73,7 @@ q: Annotated[str | None] = None //// -//// tab | Python 3.8+ +//// tab | Python 3.9+ ```Python q: Annotated[Union[str, None]] = None @@ -85,33 +85,33 @@ q: Annotated[Union[str, None]] = None А тепер переходимо до цікавого! 🎉 -## Додавання `Query` до `Annotated` у параметр `q` +## Додавання `Query` до `Annotated` у параметр `q` { #add-query-to-annotated-in-the-q-parameter } -Тепер, коли у нас є `Annotated`, де ми можемо додавати додаткову інформацію (зокрема валідацію), додамо `Query` всередину `Annotated` і встановимо параметр `max_length` у `50`: +Тепер, коли у нас є `Annotated`, де ми можемо додавати додаткову інформацію (у цьому випадку — додаткову валідацію), додамо `Query` всередину `Annotated` і встановимо параметр `max_length` у `50`: {* ../../docs_src/query_params_str_validations/tutorial002_an_py310.py hl[9] *} Зверніть увагу, що значення за замовчуванням усе ще `None`, тому параметр залишається необов'язковим. -Але тепер, додавши `Query(max_length=50)` всередину `Annotated`, ми повідомляємо FastAPI, що хочемо **додаткову валідацію** для цього значення — воно має містити максимум 50 символів. 😎 +Але тепер, додавши `Query(max_length=50)` всередину `Annotated`, ми повідомляємо FastAPI, що хочемо **додаткову валідацію** для цього значення: ми хочемо, щоб воно мало максимум 50 символів. 😎 -/// tip | Підказка +/// tip | Порада -Ми використовуємо `Query()`, оскільки це **query параметр**. Далі ми розглянемо інші варіанти, як-от `Path()`, `Body()`, `Header()` та `Cookie()`, які приймають ті самі аргументи, що й `Query()`. +Тут ми використовуємо `Query()`, оскільки це **query параметр**. Далі ми розглянемо інші варіанти, як-от `Path()`, `Body()`, `Header()` та `Cookie()`, які приймають ті самі аргументи, що й `Query()`. /// Тепер FastAPI: -* **Перевірить** дані, щоб переконатися, що їхня довжина не перевищує 50 символів +* **Перевірить** дані, щоб переконатися, що їхня максимальна довжина — 50 символів * Покажe **чітку помилку** клієнту, якщо дані недійсні * **Задокументує** параметр в OpenAPI-схемі *операції шляху* (що відобразиться в **автоматично згенерованій документації**) -## Альтернативний (застарілий) метод: Query як значення за замовчуванням +## Альтернативний (застарілий) метод: `Query` як значення за замовчуванням { #alternative-old-query-as-the-default-value } -У попередніх версіях FastAPI (до 0.95.0) `Query` використовувався як значення за замовчуванням для параметра, а не всередині `Annotated`. Ви, ймовірно, побачите код, який використовує цей підхід, тому варто розглянути його. +У попередніх версіях FastAPI (до 0.95.0) потрібно було використовувати `Query` як значення за замовчуванням параметра, замість того, щоб додавати його в `Annotated`. Є висока ймовірність, що ви зустрінете код із таким підходом, тож я поясню його. -/// tip | Підказка +/// tip | Порада Для нового коду та коли це можливо, використовуйте `Annotated`, як показано вище. Це має багато переваг (пояснених нижче) і не має недоліків. 🍰 @@ -121,7 +121,7 @@ q: Annotated[Union[str, None]] = None {* ../../docs_src/query_params_str_validations/tutorial002_py310.py hl[7] *} -Оскільки в цьому випадку (без `Annotated`) нам потрібно замінити `None` у функції на `Query()`, тепер ми повинні явно встановити значення за замовчуванням через параметр `Query(default=None)`. Це виконує ту саму роль визначення значення за замовчуванням (принаймні для FastAPI). +Оскільки в цьому випадку (без `Annotated`) нам потрібно замінити значення за замовчуванням `None` у функції на `Query()`, тепер ми повинні встановити значення за замовчуванням через параметр `Query(default=None)`. Це виконує ту саму роль визначення значення за замовчуванням (принаймні для FastAPI). Таким чином: @@ -135,9 +135,10 @@ q: str | None = Query(default=None) ```Python q: str | None = None ``` -Але у версії з `Query` ми явно вказуємо, що це query параметр. -Далі ми можемо передавати `Query` додаткові параметри, зокрема `max_length`, який застосовується до рядків: +Але у версії з `Query` ми явно вказуємо, що це query параметр. + +Далі ми можемо передавати `Query` додаткові параметри. У цьому випадку — параметр `max_length`, який застосовується до рядків: ```Python q: str | None = Query(default=None, max_length=50) @@ -145,11 +146,11 @@ q: str | None = Query(default=None, max_length=50) Це забезпечить валідацію даних, виведе зрозумілу помилку у разі недійсних даних і задокументує параметр у схемі OpenAPI *операції шляху*. -### `Query` як значення за замовчуванням або всередині `Annotated` +### `Query` як значення за замовчуванням або всередині `Annotated` { #query-as-the-default-value-or-in-annotated } Важливо пам’ятати, якщо використовувати `Query` всередині `Annotated`, не можна задавати параметр `default` у `Query`. -Замість цього використовуйте значення за замовчуванням у самій функції. Інакше це буде нелогічно. +Замість цього використовуйте фактичне значення за замовчуванням параметра функції. Інакше це буде непослідовно. Наприклад, цей варіант є некоректним: @@ -159,39 +160,39 @@ q: Annotated[str, Query(default="rick")] = "morty" ...тому, що не зрозуміло, яке значення має бути значенням за замовчуванням: `"rick"` чи `"morty"`. -Коректні варіанти: +Тож ви будете використовувати (бажано): ```Python q: Annotated[str, Query()] = "rick" ``` -...або у старих кодових базах Ви знайдете: +...або у старих кодових базах ви знайдете: ```Python q: str = Query(default="rick") ``` -### Переваги використання `Annotated` +### Переваги використання `Annotated` { #advantages-of-annotated } **Використання `Annotated` є рекомендованим** замість задання значення за замовчуванням у параметрах функції, оскільки воно **краще** з кількох причин. 🤓 -Значення **за замовчуванням** параметра **функції** є його **фактичним значенням за замовчуванням**, що є більш інтуїтивним у Python загалом. 😌 +Значення **за замовчуванням** параметра **функції** є **фактичним значенням за замовчуванням**, що є більш інтуїтивним у Python загалом. 😌 -Ви можете **викликати** ту саму функцію **в інших місцях** без FastAPI, і вона **працюватиме очікувано**. Якщо параметр є **обов’язковим** (без значення за замовчуванням), Ваш **редактор** повідомить про помилку, а **Python** також видасть помилку, якщо Ви виконаєте функцію без передавання цього параметра. +Ви можете **викликати** ту саму функцію **в інших місцях** без FastAPI, і вона **працюватиме очікувано**. Якщо параметр є **обов’язковим** (без значення за замовчуванням), ваш **редактор** повідомить про помилку, а **Python** також видасть помилку, якщо ви виконаєте функцію без передавання цього параметра. -Якщо Ви не використовуєте `Annotated`, а використовуєте **(старий) стиль значень за замовчуванням**, то при виклику цієї функції без FastAPI **в інших місцях**, потрібно **не забути** передати їй аргументи, інакше значення будуть відрізнятися від очікуваних (наприклад, Ви отримаєте `QueryInfo` або подібне замість `str`). Ваш редактор не повідомить про помилку, і Python також не видасть помилку при запуску функції, поки не виникне помилка під час виконання операцій усередині. +Якщо ви не використовуєте `Annotated`, а використовуєте **(старий) стиль значень за замовчуванням**, то при виклику цієї функції без FastAPI **в інших місцях**, потрібно **пам’ятати** передати їй аргументи, щоб вона працювала коректно, інакше значення будуть відрізнятися від очікуваних (наприклад, ви отримаєте `QueryInfo` або щось подібне замість `str`). І ваш редактор не повідомить про помилку, і Python не скаржитиметься під час запуску цієї функції — лише коли операції всередині завершаться помилкою. -Оскільки `Annotated` може містити кілька анотацій метаданих, Ви навіть можете використовувати ту саму функцію з іншими інструментами, такими як Typer. 🚀 +Оскільки `Annotated` може містити кілька анотацій метаданих, тепер ви навіть можете використовувати ту саму функцію з іншими інструментами, такими як Typer. 🚀 -## Додавання додаткових валідацій +## Додавання додаткових валідацій { #add-more-validations } Ви також можете додати параметр `min_length`: {* ../../docs_src/query_params_str_validations/tutorial003_an_py310.py hl[10] *} -## Додавання регулярних виразів +## Додавання регулярних виразів { #add-regular-expressions } -Ви можете визначити регулярний вираз pattern, якому має відповідати параметр: +Ви можете визначити regular expression `pattern`, якому має відповідати параметр: {* ../../docs_src/query_params_str_validations/tutorial004_an_py310.py hl[11] *} @@ -201,41 +202,27 @@ q: str = Query(default="rick") * `fixedquery`: точно відповідає значенню `fixedquery`. * `$`: закінчується тут, після `fixedquery` немає жодних символів. -Якщо Ви почуваєтеся розгублено щодо **"регулярних виразів"**, не хвилюйтеся. Вони є складною темою для багатьох людей. Ви все одно можете зробити багато речей без їх використання. +Якщо ви почуваєтеся розгублено щодо **«regular expression»**, не хвилюйтеся. Це складна тема для багатьох людей. Ви все одно можете робити багато речей без використання регулярних виразів. -Але тепер Ви знаєте, що коли вони знадобляться, їх можна застосовувати у **FastAPI**. +Тепер ви знаєте, що коли вони знадобляться, їх можна застосовувати у **FastAPI**. -### Pydantic v1 `regex` замість `pattern` +## Значення за замовчуванням { #default-values } -До версії Pydantic 2 і FastAPI 0.100.0 параметр називався `regex` замість `pattern`, але тепер він застарів. +Ви можете, звісно, використовувати значення за замовчуванням, відмінні від `None`. -Ви все ще можете зустріти код, який використовує його: - -//// tab | Pydantic v1 - -{* ../../docs_src/query_params_str_validations/tutorial004_regex_an_py310.py hl[11] *} - -//// - -Але майте на увазі, що він є застарілим і його слід оновити до нового параметра `pattern`. 🤓 - -## Значення за замовчуванням - -Ви можете використовувати значення за замовчуванням, відмінні від `None`. - -Наприклад, якщо Ви хочете оголосити параметр запиту `q` з `min_length` `3` і значенням за замовчуванням `"fixedquery"`: +Припустімо, що ви хочете оголосити query параметр `q` з `min_length` `3` і значенням за замовчуванням `"fixedquery"`: {* ../../docs_src/query_params_str_validations/tutorial005_an_py39.py hl[9] *} -/// note | Технічні деталі +/// note | Примітка Наявність значення за замовчуванням будь-якого типу, включаючи `None`, робить параметр необов’язковим (not required). /// -## Обов’язкові параметри +## Обов’язкові параметри { #required-parameters } -Якщо нам не потрібно вказувати додаткові перевірки або метадані, ми можемо зробити параметр `q` обов’язковим, просто не оголошуючи значення за замовчуванням, наприклад: +Якщо нам не потрібно оголошувати додаткові валідації або метадані, ми можемо зробити query параметр `q` обов’язковим, просто не вказуючи значення за замовчуванням, наприклад: ```Python q: str @@ -247,43 +234,39 @@ q: str q: str | None = None ``` -Але тепер ми оголошуємо його з `Query`, наприклад: - -//// tab | Annotated +Але тепер ми оголошуємо його з `Query`, наприклад так: ```Python q: Annotated[str | None, Query(min_length=3)] = None ``` -//// - -Тому, якщо Вам потрібно зробити значення обов’язковим, використовуючи `Query`, просто не вказуйте значення за замовчуванням: +Тому, якщо вам потрібно оголосити значення як обов’язкове під час використання `Query`, просто не вказуйте значення за замовчуванням: {* ../../docs_src/query_params_str_validations/tutorial006_an_py39.py hl[9] *} -### Обов’язкове значення, яке може бути `None` +### Обов’язковий, може бути `None` { #required-can-be-none } -Ви можете вказати, що параметр може приймати `None`, але при цьому залишається обов’язковим. Це змусить клієнтів надіслати значення, навіть якщо воно дорівнює `None`. +Ви можете вказати, що параметр може приймати `None`, але при цьому залишається обов’язковим. Це змусить клієнтів надіслати значення, навіть якщо значення дорівнює `None`. -Щоб зробити це, оголосіть, що `None` є допустимим типом, але не вказуйте значення за замовчуванням: +Щоб зробити це, оголосіть, що `None` є допустимим типом, але просто не вказуйте значення за замовчуванням: {* ../../docs_src/query_params_str_validations/tutorial006c_an_py310.py hl[9] *} -## Список параметрів запиту / кілька значень +## Список query параметрів / кілька значень { #query-parameter-list-multiple-values } -Якщо Ви визначаєте параметр запиту за допомогою `Query`, Ви також можете дозволити отримання списку значень, тобто дозволити отримання кількох значень. +Коли ви явно визначаєте query параметр за допомогою `Query`, ви також можете оголосити, що він має приймати список значень, або, іншими словами, кілька значень. -Наприклад, щоб дозволити параметру запиту `q` з'являтися кілька разів в URL, можна написати: +Наприклад, щоб оголосити query параметр `q`, який може з’являтися в URL кілька разів, можна написати: {* ../../docs_src/query_params_str_validations/tutorial011_an_py310.py hl[9] *} -Тоді, у випадку запиту за URL: +Тоді, у випадку URL: ``` http://localhost:8000/items/?q=foo&q=bar ``` -Ви отримаєте кілька значень *query параметра* `q` (`foo` і `bar`) у вигляді списку `list` в Python у Вашій *функції обробки шляху*, у *параметрі функції* `q`. +ви отримаєте кілька значень `q` *query параметрів* (`foo` і `bar`) у вигляді Python `list` у вашій *функції операції шляху*, у *параметрі функції* `q`. Отже, відповідь на цей URL буде: @@ -296,9 +279,9 @@ http://localhost:8000/items/?q=foo&q=bar } ``` -/// tip | Підказка +/// tip | Порада -Щоб оголосити параметр запиту з типом `list`, як у наведеному вище прикладі, потрібно явно використовувати `Query`, інакше він буде інтерпретований як тіло запиту. +Щоб оголосити query параметр з типом `list`, як у наведеному вище прикладі, потрібно явно використовувати `Query`, інакше він буде інтерпретований як тіло запиту. /// @@ -306,19 +289,19 @@ http://localhost:8000/items/?q=foo&q=bar -### Список параметрів запиту / кілька значень за замовчуванням +### Список query параметрів / кілька значень за замовчуванням { #query-parameter-list-multiple-values-with-defaults } -Ви також можете визначити значення за замовчуванням для `list`, якщо жодне значення не було передане: +Ви також можете визначити значення за замовчуванням `list`, якщо жодне значення не було передане: {* ../../docs_src/query_params_str_validations/tutorial012_an_py39.py hl[9] *} -Якщо Ви перейдете за посиланням: +Якщо ви перейдете за посиланням: ``` http://localhost:8000/items/ ``` -то значення `q` за замовчуванням буде: `["foo", "bar"]`, і Ваша відповідь виглядатиме так: +то значення `q` за замовчуванням буде: `["foo", "bar"]`, і ваша відповідь виглядатиме так: ```JSON { @@ -329,35 +312,35 @@ http://localhost:8000/items/ } ``` -#### Використання тільки `list` +#### Використання тільки `list` { #using-just-list } -Ви також можете використовувати `list` без уточнення типу, замість `list[str]`: +Ви також можете використовувати `list` напряму замість `list[str]`: {* ../../docs_src/query_params_str_validations/tutorial013_an_py39.py hl[9] *} -/// note | Технічні деталі +/// note | Примітка Майте на увазі, що в цьому випадку FastAPI не перевірятиме вміст списку. -Наприклад, `list[int]` перевірятиме (і документуватиме), що всі елементи списку є цілими числами. Але `list` без уточнення цього не робитиме. +Наприклад, `list[int]` перевірятиме (і документуватиме), що вміст списку — цілі числа. Але `list` без уточнення цього не робитиме. /// -## Додавання додаткових метаданих +## Оголосити більше метаданих { #declare-more-metadata } Ви можете додати більше інформації про параметр. -Ця інформація буде включена у згенерований OpenAPI та використана в інтерфейсах документації та зовнішніх інструментах. +Ця інформація буде включена у згенерований OpenAPI та використана інтерфейсами документації та зовнішніми інструментами. -/// note | Технічні деталі +/// note | Примітка Майте на увазі, що різні інструменти можуть мати різний рівень підтримки OpenAPI. -Деякі з них можуть ще не відображати всю додаткову інформацію, хоча в більшості випадків ця функція вже запланована для розробки. +Деякі з них можуть ще не відображати всю додаткову інформацію, хоча в більшості випадків відсутню функцію вже заплановано до реалізації. /// -Ви можете додати `title` : +Ви можете додати `title`: {* ../../docs_src/query_params_str_validations/tutorial007_an_py310.py hl[10] *} @@ -365,9 +348,9 @@ http://localhost:8000/items/ {* ../../docs_src/query_params_str_validations/tutorial008_an_py310.py hl[14] *} -## Аліаси параметрів +## Аліаси параметрів { #alias-parameters } -Уявіть, що Ви хочете, щоб параметр називався `item-query`. +Уявіть, що ви хочете, щоб параметр називався `item-query`. Наприклад: @@ -379,19 +362,19 @@ http://127.0.0.1:8000/items/?item-query=foobaritems Найближчий допустимий варіант — `item_query`. -Проте Вам потрібно, щоб параметр залишався саме `item-query`... +Проте вам потрібно, щоб параметр залишався саме `item-query`... У такому випадку можна оголосити `alias`, і саме він буде використовуватися для отримання значення параметра: {* ../../docs_src/query_params_str_validations/tutorial009_an_py310.py hl[9] *} -## Виведення параметрів як застарілих +## Позначення параметрів як застарілих { #deprecating-parameters } -Припустимо, що Ви більше не хочете використовувати цей параметр. +Припустімо, що вам більше не подобається цей параметр. -Вам потрібно залишити його на деякий час, оскільки ним користуються клієнти, але Ви хочете, щоб документація чітко показувала, що він є застарілим. +Вам потрібно залишити його на деякий час, оскільки ним користуються клієнти, але ви хочете, щоб документація чітко показувала, що він є deprecated. -Тоді Ви можете передати параметр `deprecated=True` до `Query`: +Тоді передайте параметр `deprecated=True` до `Query`: {* ../../docs_src/query_params_str_validations/tutorial010_an_py310.py hl[19] *} @@ -399,27 +382,27 @@ http://127.0.0.1:8000/items/?item-query=foobaritems -## Виняток параметрів з OpenAPI +## Виняток параметрів з OpenAPI { #exclude-parameters-from-openapi } -Щоб виключити параметр запиту зі згенерованої схеми OpenAPI (і, таким чином, з автоматичних систем документації), встановіть параметр `include_in_schema` для `Query` в `False`: +Щоб виключити query параметр зі згенерованої схеми OpenAPI (і, таким чином, з автоматичних систем документації), встановіть параметр `include_in_schema` для `Query` в `False`: {* ../../docs_src/query_params_str_validations/tutorial014_an_py310.py hl[10] *} -## Кастомна валідація +## Кастомна валідація { #custom-validation } -Можуть бути випадки, коли Вам потрібно провести **кастомну валідацію**, яку не можна реалізувати за допомогою параметрів, показаних вище. +Можуть бути випадки, коли вам потрібно провести **кастомну валідацію**, яку не можна реалізувати за допомогою параметрів, показаних вище. -У таких випадках ви можете використати **кастомну функцію валідації**, яка буде застосована після звичайної валідації (наприклад, після перевірки, що значення є типом `str`). +У таких випадках ви можете використати **кастомну функцію-валідатор**, яка буде застосована після звичайної валідації (наприклад, після перевірки, що значення є типом `str`). Це можна досягти за допомогою Pydantic's `AfterValidator` в середині `Annotated`. -/// tip | Підказка +/// tip | Порада Pydantic також має `BeforeValidator` та інші. 🤓 /// -Наприклад, цей кастомний валідатор перевіряє, чи починається ID елемента з `isbn-` для номера книги ISBN або з `imdb-` для ID URL фільму на IMDB: +Наприклад, цей кастомний валідатор перевіряє, чи починається ID елемента з `isbn-` для номера книги ISBN або з `imdb-` для ID URL фільму на IMDB: {* ../../docs_src/query_params_str_validations/tutorial015_an_py310.py hl[5,16:19,24] *} @@ -429,49 +412,49 @@ Pydantic також має ітерабельний об'єкт
із кортежами, що містять ключ і значення для кожного елемента словника. +За допомогою `data.items()` ми отримуємо iterable object із кортежами, що містять ключ і значення для кожного елемента словника. -Ми перетворюємо цей ітерабельний об'єкт у звичайний `list` за допомогою `list(data.items())`. +Ми перетворюємо цей iterable object у звичайний `list` за допомогою `list(data.items())`. -Потім, використовуючи `random.choice()`, ми можемо отримати випадкове значення зі списку, тобто отримуємо кортеж із `(id, name)`. Це може бути щось на зразок `("imdb-tt0371724", "The Hitchhiker's Guide to the Galaxy")`. +Потім, використовуючи `random.choice()`, ми можемо отримати **випадкове значення** зі списку, тобто отримуємо кортеж із `(id, name)`. Це може бути щось на зразок `("imdb-tt0371724", "The Hitchhiker's Guide to the Galaxy")`. Далі ми **присвоюємо ці два значення** кортежу змінним `id` і `name`. Тож, якщо користувач не вказав ID елемента, він все одно отримає випадкову рекомендацію. -...і все це реалізовано в **одному рядку коду**. 🤯 Хіба не прекрасний Python? 🐍 +...ми робимо все це в **одному простому рядку**. 🤯 Хіба ви не любите Python? 🐍 {* ../../docs_src/query_params_str_validations/tutorial015_an_py310.py ln[22:30] hl[29] *} -## Підсумок +## Підсумок { #recap } -Ви можете оголошувати додаткові валідації та метаінформацію для своїх параметрів. +Ви можете оголошувати додаткові валідації та метадані для ваших параметрів. -Загальні валідації та метаінформація: +Загальні валідації та метадані: * `alias` * `title` @@ -486,6 +469,6 @@ Pydantic також має "Парсинг"
даних +* Підтримка в редакторі (очевидно) +* «parsing» даних * Валідація даних * Автоматична документація +## Значення за замовчуванням { #defaults } -## Значення за замовчуванням - -Оскільки query параметри не є фіксованою частиною шляху, вони можуть бути необов’язковими та мати значення за замовчуванням. +Оскільки параметри query не є фіксованою частиною шляху, вони можуть бути необов’язковими та мати значення за замовчуванням. У наведеному вище прикладі вони мають значення за замовчуванням: `skip=0` і `limit=10`. -Отже, результат переходу за URL: +Отже, перехід за URL: ``` http://127.0.0.1:8000/items/ ``` + буде таким самим, як і перехід за посиланням: ``` http://127.0.0.1:8000/items/?skip=0&limit=10 ``` -Але якщо Ви перейдете, наприклад, за посиланням: +Але якщо ви перейдете, наприклад, за посиланням: ``` http://127.0.0.1:8000/items/?skip=20 @@ -54,12 +54,12 @@ http://127.0.0.1:8000/items/?skip=20 Значення параметрів у вашій функції будуть такими: -* `skip=20`: оскільки Ви вказали його в URL +* `skip=20`: оскільки ви вказали його в URL * `limit=10`: оскільки це значення за замовчуванням -## Необов'язкові параметри +## Необов'язкові параметри { #optional-parameters } -Аналогічно, Ви можете оголосити необов’язкові query параметри, встановивши для них значення за замовчуванням `None`: +Так само ви можете оголосити необов’язкові параметри query, встановивши для них значення за замовчуванням `None`: {* ../../docs_src/query_params/tutorial002_py310.py hl[7] *} @@ -67,18 +67,17 @@ http://127.0.0.1:8000/items/?skip=20 /// check | Примітка -Також зверніть увагу, що **FastAPI** достатньо розумний, щоб визначити, що параметр шляху `item_id` є параметром шляху, а `q` — ні, отже, це query параметр. +Також зверніть увагу, що **FastAPI** достатньо розумний, щоб визначити, що параметр шляху `item_id` є параметром шляху, а `q` — ні, отже, це параметр query. /// -## Перетворення типу Query параметра +## Перетворення типу параметра query { #query-parameter-type-conversion } Ви також можете оголошувати параметри типу `bool`, і вони будуть автоматично конвертовані: {* ../../docs_src/query_params/tutorial003_py310.py hl[7] *} -У цьому випадку, якщо Ви звернетесь до: - +У цьому випадку, якщо ви перейдете за: ``` http://127.0.0.1:8000/items/foo?short=1 @@ -108,38 +107,38 @@ http://127.0.0.1:8000/items/foo?short=on http://127.0.0.1:8000/items/foo?short=yes ``` -або будь-який інший варіант написання (великі літери, перша літера велика тощо), ваша функція побачить параметр `short` зі значенням `True` з типом даних `bool`. В іншому випадку – `False`. - -## Кілька path і query параметрів - -Ви можете одночасно оголошувати кілька path і query параметрів, і **FastAPI** автоматично визначить, який з них до чого належить. +або будь-який інший варіант написання (великі літери, перша літера велика тощо), ваша функція побачить параметр `short` зі значенням `True` типу `bool`. В іншому випадку — `False`. -Не потрібно дотримуватись певного порядку їх оголошення. +## Кілька path і query параметрів { #multiple-path-and-query-parameters } + +Ви можете одночасно оголошувати кілька параметрів шляху та параметрів query, **FastAPI** знає, який з них який. + +І вам не потрібно оголошувати їх у якомусь конкретному порядку. Вони визначаються за назвою: {* ../../docs_src/query_params/tutorial004_py310.py hl[6,8] *} -## Обов’язкові Query параметри +## Обов’язкові параметри query { #required-query-parameters } -Якщо Ви оголошуєте значення за замовчуванням для параметрів, які не є path-параметрами (у цьому розділі ми бачили поки що лише path параметри), тоді вони стають необов’язковими. +Коли ви оголошуєте значення за замовчуванням для не-path-параметрів (поки що ми бачили лише параметри query), тоді вони не є обов’язковими. -Якщо Ви не хочете вказувати конкретні значення, але хочете зробити параметр опціональним, задайте `None` як значення за замовчуванням. +Якщо ви не хочете задавати конкретне значення, а просто зробити параметр необов’язковим, задайте `None` як значення за замовчуванням. -Але якщо Ви хочете зробити query параметр обов’язковим, просто не вказуйте для нього значення за замовчуванням: +Але якщо ви хочете зробити параметр query обов’язковим, просто не вказуйте для нього значення за замовчуванням: -{* ../../docs_src/query_params/tutorial005.py hl[6:7] *} +{* ../../docs_src/query_params/tutorial005_py39.py hl[6:7] *} -Тут `needy` – обов’язковий query параметр типу `str`. +Тут параметр query `needy` — обов’язковий параметр query типу `str`. -Якщо Ви відкриєте у браузері URL-адресу: +Якщо ви відкриєте у браузері URL-адресу: ``` http://127.0.0.1:8000/items/foo-item ``` -...без додавання обов’язкового параметра `needy`, Ви побачите помилку: +...без додавання обов’язкового параметра `needy`, ви побачите помилку на кшталт: ```JSON { @@ -163,7 +162,7 @@ http://127.0.0.1:8000/items/foo-item http://127.0.0.1:8000/items/foo-item?needy=sooooneedy ``` -...цей запит поверне: +...це спрацює: ```JSON { @@ -172,20 +171,18 @@ http://127.0.0.1:8000/items/foo-item?needy=sooooneedy } ``` - -Звичайно, Ви можете визначити деякі параметри як обов’язкові, інші зі значенням за замовчуванням, а ще деякі — повністю опціональні: +І звісно, ви можете визначити деякі параметри як обов’язкові, деякі — зі значенням за замовчуванням, а деякі — повністю необов’язкові: {* ../../docs_src/query_params/tutorial006_py310.py hl[8] *} -У цьому випадку є 3 query параметри: +У цьому випадку є 3 параметри query: * `needy`, обов’язковий `str`. * `skip`, `int` зі значенням за замовчуванням `0`. -* `limit`, опціональний `int`. +* `limit`, необов’язковий `int`. +/// tip | Порада -/// tip | Підказка - -Ви також можете використовувати `Enum`-и, так само як і з [Path Parameters](path-params.md#predefined-values){.internal-link target=_blank}. +Ви також можете використовувати `Enum` так само, як і з [Path Parameters](path-params.md#predefined-values){.internal-link target=_blank}. /// diff --git a/docs/uk/docs/tutorial/request-files.md b/docs/uk/docs/tutorial/request-files.md index 18b7cc01c9..a6ff70dc09 100644 --- a/docs/uk/docs/tutorial/request-files.md +++ b/docs/uk/docs/tutorial/request-files.md @@ -1,56 +1,56 @@ -# Запит файлів +# Запит файлів { #request-files } Ви можете визначити файли, які будуть завантажуватися клієнтом, використовуючи `File`. /// info | Інформація -Щоб отримувати завантажені файли, спочатку встановіть python-multipart. +Щоб отримувати завантажені файли, спочатку встановіть `python-multipart`. -Переконайтеся, що Ви створили [віртуальне середовище](../virtual-environments.md){.internal-link target=_blank}, активували його та встановили пакет, наприклад: +Переконайтеся, що ви створили [віртуальне середовище](../virtual-environments.md){.internal-link target=_blank}, активували його, а потім встановили пакет, наприклад: ```console $ pip install python-multipart ``` -Це необхідно, оскільки завантажені файли передаються у вигляді "форматованих даних форми". +Це необхідно, оскільки завантажені файли передаються у вигляді «form data». /// -## Імпорт `File` +## Імпорт `File` { #import-file } Імпортуйте `File` та `UploadFile` з `fastapi`: {* ../../docs_src/request_files/tutorial001_an_py39.py hl[3] *} -## Визначення параметрів `File` +## Визначення параметрів `File` { #define-file-parameters } -Створіть параметри файлів так само як Ви б створювали `Body` або `Form`: +Створіть параметри файлів так само як ви б створювали `Body` або `Form`: {* ../../docs_src/request_files/tutorial001_an_py39.py hl[9] *} /// info | Інформація -`File` — це клас, який безпосередньо успадковує `Form`. +`File` — це клас, який безпосередньо успадковує `Form`. -Але пам’ятайте, що коли Ви імпортуєте `Query`, `Path`, `File` та інші з `fastapi`, це насправді функції, які повертають спеціальні класи. +Але пам’ятайте, що коли ви імпортуєте `Query`, `Path`, `File` та інші з `fastapi`, це насправді функції, які повертають спеціальні класи. /// -/// tip | Підказка +/// tip | Порада -Щоб оголосити тіла файлів, Вам потрібно використовувати `File`, тому що інакше параметри будуть інтерпретовані як параметри запиту або параметри тіла (JSON). +Щоб оголосити тіла файлів, вам потрібно використовувати `File`, тому що інакше параметри будуть інтерпретовані як параметри запиту або параметри тіла (JSON). /// -Файли будуть завантажені у вигляді "форматованих даних форми". +Файли будуть завантажені у вигляді «form data». -Якщо Ви оголосите тип параметра функції обробника маршруту як `bytes`, **FastAPI** прочитає файл за Вас, і Ви отримаєте його вміст у вигляді `bytes`. +Якщо ви оголосите тип параметра *функції операції шляху* як `bytes`, **FastAPI** прочитає файл за вас, і ви отримаєте його вміст у вигляді `bytes`. -Однак майте на увазі, що весь вміст буде збережено в пам'яті. Це працюватиме добре для малих файлів. +Майте на увазі, що це означає, що весь вміст буде збережено в пам'яті. Це працюватиме добре для малих файлів. -Але в деяких випадках Вам може знадобитися `UploadFile`. +Але є кілька випадків, у яких вам може бути корисно використовувати `UploadFile`. -## Параметри файлу з `UploadFile` +## Параметри файлу з `UploadFile` { #file-parameters-with-uploadfile } Визначте параметр файлу з типом `UploadFile`: @@ -59,38 +59,39 @@ $ pip install python-multipart Використання `UploadFile` має кілька переваг перед `bytes`: * Вам не потрібно використовувати `File()` у значенні за замовчуванням параметра. -* Використовується "буферизований" файл: - * Файл зберігається в пам'яті до досягнення певного обмеження, після чого він записується на диск. -* Це означає, що він добре працює для великих файлів, таких як зображення, відео, великі двійкові файли тощо, не споживаючи всю пам'ять. -Ви можете отримати метадані про завантажений файл. -* Він має file-like `асинхронний файловий інтерфейс` interface. -* Він надає фактичний об'єкт Python `SpooledTemporaryFile`, який можна передавати безпосередньо іншим бібліотекам. +* Використовується «spooled» файл: + * Файл зберігається в пам'яті до досягнення максимального обмеження розміру, після чого він буде збережений на диску. +* Це означає, що він добре працюватиме для великих файлів, таких як зображення, відео, великі двійкові файли тощо, не споживаючи всю пам'ять. +* Ви можете отримати метадані про завантажений файл. +* Він має file-like `async` інтерфейс. +* Він надає фактичний об'єкт Python `SpooledTemporaryFile`, який можна передавати безпосередньо іншим бібліотекам, що очікують file-like об'єкт. -### `UploadFile` +### `UploadFile` { #uploadfile } `UploadFile` має такі атрибути: * `filename`: Рядок `str` з оригінальною назвою файлу, який був завантажений (наприклад, `myimage.jpg`). -* `content_type`: Рядок `str` з MIME-типом (наприклад, `image/jpeg`). -* `file`: Об'єкт SpooledTemporaryFile (файлоподібний об'єкт). Це фактичний файловий об'єкт Python, який можна безпосередньо передавати іншим функціям або бібліотекам, що очікують "файлоподібний" об'єкт. +* `content_type`: Рядок `str` з типом вмісту (MIME type / media type) (наприклад, `image/jpeg`). +* `file`: `SpooledTemporaryFile` (file-like об'єкт). Це фактичний файловий об'єкт Python, який ви можете передавати безпосередньо іншим функціям або бібліотекам, що очікують «file-like» об'єкт. -`UploadFile` має такі асинхронні `async` методи. Вони викликають відповідні методи файлу під капотом (використовуючи внутрішній `SpooledTemporaryFile`). +`UploadFile` має такі асинхронні `async` методи. Вони всі викликають відповідні методи файлу під капотом (використовуючи внутрішній `SpooledTemporaryFile`). * `write(data)`: Записує `data` (`str` або `bytes`) у файл. * `read(size)`: Читає `size` (`int`) байтів/символів з файлу. -* `seek(offset)`: Переміщується до позиції `offset` (`int`) у файлі. - * Наприклад, `await myfile.seek(0)` поверне курсор на початок файлу. - * This is especially useful if you run `await myfile.read()` once and then need to read the contents again. Це особливо корисно, якщо Ви виконуєте await `await myfile.read()` один раз, а потім потрібно знову прочитати вміст. +* `seek(offset)`: Переходить до байтової позиції `offset` (`int`) у файлі. + * Наприклад, `await myfile.seek(0)` перейде на початок файлу. + * Це особливо корисно, якщо ви виконаєте `await myfile.read()` один раз, а потім потрібно знову прочитати вміст. * `close()`: Закриває файл. -Оскільки всі ці методи є асинхронними `async`, Вам потрібно використовувати "await": +Оскільки всі ці методи є асинхронними `async` методами, вам потрібно їх «await»-ити. -Наприклад, всередині `async` *функції обробки шляху* Ви можете отримати вміст за допомогою: +Наприклад, всередині `async` *функції операції шляху* ви можете отримати вміст за допомогою: ```Python contents = await myfile.read() ``` -Якщо Ви знаходитесь у звичайній `def` *функції обробки шляху*, Ви можете отримати доступ до `UploadFile.file` безпосередньо, наприклад: + +Якщо ви знаходитесь у звичайній `def` *функції операції шляху*, ви можете отримати доступ до `UploadFile.file` безпосередньо, наприклад: ```Python contents = myfile.file.read() @@ -98,57 +99,57 @@ contents = myfile.file.read() /// note | Технічні деталі `async` -Коли Ви використовуєте `async` методи, **FastAPI** виконує файлові операції у пулі потоків та очікує їх завершення. +Коли ви використовуєте `async` методи, **FastAPI** виконує файлові методи у пулі потоків і очікує на них. /// /// note | Технічні деталі Starlette -`UploadFile` у **FastAPI** успадковується безпосередньо від `UploadFile` у **Starlette**, але додає деякі необхідні частини, щоб зробити його сумісним із **Pydantic** та іншими компонентами FastAPI. +`UploadFile` у **FastAPI** успадковується безпосередньо від `UploadFile` у **Starlette**, але додає деякі необхідні частини, щоб зробити його сумісним із **Pydantic** та іншими частинами FastAPI. /// -## Що таке "Form Data" +## Що таке «Form Data» { #what-is-form-data } -Спосіб, у який HTML-форми (`
`) надсилають дані на сервер, зазвичай використовує "спеціальне" кодування, відмінне від JSON. +Спосіб, у який HTML-форми (`
`) надсилають дані на сервер, зазвичай використовує «спеціальне» кодування для цих даних, відмінне від JSON. -**FastAPI** забезпечує правильне зчитування цих даних з відповідної частини запиту, а не з JSON. +**FastAPI** забезпечить зчитування цих даних з правильного місця, а не з JSON. /// note | Технічні деталі -Дані з форм зазвичай кодуються за допомогою "media type" `application/x-www-form-urlencoded`, якщо вони не містять файлів. +Дані з форм зазвичай кодуються за допомогою «media type» `application/x-www-form-urlencoded`, якщо вони не містять файлів. -Але якщо форма містить файли, вона кодується у форматі `multipart/form-data`. Якщо Ви використовуєте `File`, **FastAPI** визначить, що потрібно отримати файли з відповідної частини тіла запиту. +Але якщо форма містить файли, вона кодується як `multipart/form-data`. Якщо ви використовуєте `File`, **FastAPI** знатиме, що потрібно отримати файли з правильної частини тіла. -Щоб дізнатися більше про ці типи кодування та формові поля, ознайомтеся з документацією MDN щодо POST. +Якщо ви хочете дізнатися більше про ці типи кодування та формові поля, ознайомтеся з MDN web docs для POST. /// -/// warning | Увага +/// warning | Попередження -Ви можете оголосити кілька параметрів `File` і `Form` в *операції шляху*, але Ви не можете одночасно оголошувати поля `Body`, які мають надходити у форматі JSON, оскільки тіло запиту буде закодоване у форматі `multipart/form-data`, а не `application/json`. +Ви можете оголосити кілька параметрів `File` і `Form` в *операції шляху*, але ви не можете одночасно оголошувати поля `Body`, які ви очікуєте отримати як JSON, оскільки запит матиме тіло, закодоване як `multipart/form-data`, а не `application/json`. -Це не обмеження **FastAPI**, а особливість протоколу HTTP. +Це не обмеження **FastAPI**, а частина протоколу HTTP. /// -## Опціональне Завантаження Файлів +## Необов’язкове завантаження файлу { #optional-file-upload } -Файл можна зробити необов’язковим, використовуючи стандартні анотації типів і встановлюючи значення за замовчуванням `None`: +Ви можете зробити файл необов’язковим, використовуючи стандартні анотації типів і встановивши значення за замовчуванням `None`: {* ../../docs_src/request_files/tutorial001_02_an_py310.py hl[9,17] *} -## `UploadFile` із Додатковими Мета Даними +## `UploadFile` із додатковими метаданими { #uploadfile-with-additional-metadata } -Ви також можете використовувати `File()` разом із `UploadFile`, наприклад, для встановлення додаткових метаданих: +Ви також можете використовувати `File()` разом із `UploadFile`, наприклад, щоб встановити додаткові метадані: {* ../../docs_src/request_files/tutorial001_03_an_py39.py hl[9,15] *} -## Завантаження Кількох Файлів +## Завантаження кількох файлів { #multiple-file-uploads } Можна завантажувати кілька файлів одночасно. -Вони будуть пов’язані з одним і тим самим "form field", який передається у вигляді "form data". +Вони будуть пов’язані з одним і тим самим «form field», який передається у вигляді «form data». Щоб це реалізувати, потрібно оголосити список `bytes` або `UploadFile`: @@ -160,16 +161,16 @@ contents = myfile.file.read() Ви також можете використати `from starlette.responses import HTMLResponse`. -**FastAPI** надає ті ж самі `starlette.responses`, що й `fastapi.responses`, для зручності розробників. Однак більшість доступних відповідей надходять безпосередньо від Starlette. +**FastAPI** надає ті ж самі `starlette.responses`, що й `fastapi.responses`, просто для зручності для вас, розробника. Але більшість доступних відповідей надходять безпосередньо від Starlette. /// -### Завантаження декількох файлів із додатковими метаданими +### Завантаження кількох файлів із додатковими метаданими { #multiple-file-uploads-with-additional-metadata } -Так само як і раніше, Ви можете використовувати `File()`, щоб встановити додаткові параметри навіть для `UploadFile`: +Так само як і раніше, ви можете використовувати `File()`, щоб встановити додаткові параметри навіть для `UploadFile`: {* ../../docs_src/request_files/tutorial003_an_py39.py hl[11,18:20] *} -## Підсумок +## Підсумок { #recap } -Використовуйте `File`, `bytes`та `UploadFile`, щоб оголошувати файли для завантаження у запитах, які надсилаються у вигляді form data. +Використовуйте `File`, `bytes` та `UploadFile`, щоб оголошувати файли для завантаження в запиті, надіслані у вигляді form data. diff --git a/docs/uk/docs/tutorial/request-form-models.md b/docs/uk/docs/tutorial/request-form-models.md index 7f5759e79a..1bfd368d60 100644 --- a/docs/uk/docs/tutorial/request-form-models.md +++ b/docs/uk/docs/tutorial/request-form-models.md @@ -1,12 +1,12 @@ -# Моделі форм (Form Models) +# Моделі форм { #form-models } -У FastAPI Ви можете використовувати **Pydantic-моделі** для оголошення **полів форми**. +У FastAPI ви можете використовувати **Pydantic-моделі** для оголошення **полів форми**. /// info | Інформація -Щоб використовувати форми, спочатку встановіть python-multipart. +Щоб використовувати форми, спочатку встановіть `python-multipart`. -Переконайтеся, що Ви створили [віртуальне середовище](../virtual-environments.md){.internal-link target=_blank}, активували його, а потім встановили бібліотеку, наприклад: +Переконайтеся, що ви створили [віртуальне середовище](../virtual-environments.md){.internal-link target=_blank}, активували його, а потім встановили його, наприклад: ```console $ pip install python-multipart @@ -14,21 +14,21 @@ $ pip install python-multipart /// -/// note | Підказка +/// note | Примітка -Ця функція підтримується, починаючи з FastAPI версії `0.113.0`. 🤓 +Це підтримується, починаючи з FastAPI версії `0.113.0`. 🤓 /// -## Використання Pydantic-моделей для форм +## Pydantic-моделі для форм { #pydantic-models-for-forms } -Вам просто потрібно оголосити **Pydantic-модель** з полями, які Ви хочете отримати як **поля форми**, а потім оголосити параметр як `Form`: +Вам просто потрібно оголосити **Pydantic-модель** з полями, які ви хочете отримати як **поля форми**, а потім оголосити параметр як `Form`: {* ../../docs_src/request_form_models/tutorial001_an_py39.py hl[9:11,15] *} -**FastAPI** **витягне** дані для **кожного поля** з **формових даних** у запиті та надасть вам Pydantic-модель, яку Ви визначили. +**FastAPI** **витягне** дані для **кожного поля** з **формових даних** у запиті та надасть вам Pydantic-модель, яку ви визначили. -## Перевірка документації +## Перевірте документацію { #check-the-docs } Ви можете перевірити це в UI документації за `/docs`: @@ -36,13 +36,13 @@ $ pip install python-multipart -## Заборона додаткових полів форми +## Забороніть додаткові поля форми { #forbid-extra-form-fields } -У деяких особливих випадках (ймовірно, рідко) Ви можете **обмежити** форму лише тими полями, які були оголошені в Pydantic-моделі, і **заборонити** будь-які **додаткові** поля. +У деяких особливих випадках (ймовірно, не дуже поширених) ви можете **обмежити** поля форми лише тими, які були оголошені в Pydantic-моделі. І **заборонити** будь-які **додаткові** поля. -/// note | Підказка +/// note | Примітка -Ця функція підтримується, починаючи з FastAPI версії `0.114.0`. 🤓 +Це підтримується, починаючи з FastAPI версії `0.114.0`. 🤓 /// @@ -52,7 +52,7 @@ $ pip install python-multipart Якщо клієнт спробує надіслати додаткові дані, він отримає **відповідь з помилкою**. -Наприклад, якщо клієнт спробує надіслати наступні поля форми: +Наприклад, якщо клієнт спробує надіслати поля форми: * `username`: `Rick` * `password`: `Portal Gun` @@ -73,6 +73,6 @@ $ pip install python-multipart } ``` -## Підсумок +## Підсумок { #summary } -Ви можете використовувати Pydantic-моделі для оголошення полів форми у FastAPI. 😎 +У FastAPI ви можете використовувати Pydantic-моделі для оголошення полів форми. 😎 diff --git a/docs/uk/docs/tutorial/request-forms-and-files.md b/docs/uk/docs/tutorial/request-forms-and-files.md index a089ef945a..e809bee225 100644 --- a/docs/uk/docs/tutorial/request-forms-and-files.md +++ b/docs/uk/docs/tutorial/request-forms-and-files.md @@ -1,10 +1,10 @@ -# Запити з формами та файлами +# Запити з формами та файлами { #request-forms-and-files } -У FastAPI Ви можете одночасно отримувати файли та поля форми, використовуючи `File` і `Form`. +Ви можете одночасно визначати файли та поля форми, використовуючи `File` і `Form`. /// info | Інформація -Щоб отримувати завантажені файли та/або дані форми, спочатку встановіть python-multipart. +Щоб отримувати завантажені файли та/або дані форми, спочатку встановіть `python-multipart`. Переконайтеся, що Ви створили [віртуальне середовище](../virtual-environments.md){.internal-link target=_blank}, активували його, а потім встановили бібліотеку, наприклад: @@ -14,21 +14,21 @@ $ pip install python-multipart /// -## Імпорт `File` та `Form` +## Імпорт `File` та `Form` { #import-file-and-form } {* ../../docs_src/request_forms_and_files/tutorial001_an_py39.py hl[3] *} -## Оголошення параметрів `File` та `Form` +## Оголошення параметрів `File` та `Form` { #define-file-and-form-parameters } Створіть параметри файлів та форми так само як і для `Body` або `Query`: {* ../../docs_src/request_forms_and_files/tutorial001_an_py39.py hl[10:12] *} -Файли та поля форми будуть завантажені як формові дані, і Ви отримаєте як файли, так і введені користувачем поля. +Файли та поля форми будуть завантажені як формові дані, і Ви отримаєте файли та поля форми. Ви також можете оголосити деякі файли як `bytes`, а деякі як `UploadFile`. -/// warning | Увага +/// warning | Попередження Ви можете оголосити кілька параметрів `File` і `Form` в операції *шляху*, але не можете одночасно оголошувати `Body`-поля, які очікуєте отримати у форматі JSON, оскільки запит матиме тіло, закодоване за допомогою `multipart/form-data`, а не `application/json`. @@ -36,6 +36,6 @@ $ pip install python-multipart /// -## Підсумок +## Підсумок { #recap } -Використовуйте `File` та `Form` разом, коли вам потрібно отримувати дані форми та файли в одному запиті. +Використовуйте `File` та `Form` разом, коли вам потрібно отримувати дані та файли в одному запиті. diff --git a/docs/uk/docs/tutorial/request-forms.md b/docs/uk/docs/tutorial/request-forms.md index 10c58a73e4..2a22ad922a 100644 --- a/docs/uk/docs/tutorial/request-forms.md +++ b/docs/uk/docs/tutorial/request-forms.md @@ -1,12 +1,12 @@ -# Дані форми +# Дані форми { #form-data } -Якщо Вам потрібно отримувати поля форми замість JSON, Ви можете використовувати `Form`. +Якщо вам потрібно отримувати поля форми замість JSON, ви можете використовувати `Form`. /// info | Інформація Щоб використовувати форми, спочатку встановіть `python-multipart`. -Переконайтеся, що Ви створили [віртуальне середовище](../virtual-environments.md){.internal-link target=_blank}, активували його, і потім встановили бібліотеку, наприклад: +Переконайтеся, що ви створили [віртуальне середовище](../virtual-environments.md){.internal-link target=_blank}, активували його, і потім встановили бібліотеку, наприклад: ```console $ pip install python-multipart @@ -14,23 +14,23 @@ $ pip install python-multipart /// -## Імпорт `Form` +## Імпорт `Form` { #import-form } Імпортуйте `Form` з `fastapi`: {* ../../docs_src/request_forms/tutorial001_an_py39.py hl[3] *} -## Оголошення параметрів `Form` +## Оголошення параметрів `Form` { #define-form-parameters } -Створюйте параметри форми так само як Ви б створювали `Body` або `Query`: +Створюйте параметри форми так само як ви б створювали `Body` або `Query`: {* ../../docs_src/request_forms/tutorial001_an_py39.py hl[9] *} Наприклад, один зі способів використання специфікації OAuth2 (так званий "password flow") вимагає надсилати `username` та `password` як поля форми. -spec вимагає, щоб ці поля мали точні назви `username` і `password` та надсилалися у вигляді полів форми, а не JSON. +spec вимагає, щоб ці поля мали точні назви `username` і `password` та надсилалися у вигляді полів форми, а не JSON. -З `Form` Ви можете оголошувати ті ж конфігурації, що і з `Body` (та `Query`, `Path`, `Cookie`), включаючи валідацію, приклади, псевдоніми (наприклад, `user-name` замість `username`) тощо. +З `Form` ви можете оголошувати ті ж конфігурації, що і з `Body` (та `Query`, `Path`, `Cookie`), включаючи валідацію, приклади, псевдоніми (наприклад, `user-name` замість `username`) тощо. /// info | Інформація @@ -44,7 +44,7 @@ $ pip install python-multipart /// -## Про "поля форми" +## Про "поля форми" { #about-form-fields } HTML-форми (`
`) надсилають дані на сервер у "спеціальному" кодуванні, яке відрізняється від JSON. @@ -56,18 +56,18 @@ HTML-форми (`
`) надсилають дані на серве Але якщо форма містить файли, вона кодується як `multipart/form-data`. Ви дізнаєтеся про обробку файлів у наступному розділі. -Якщо Ви хочете дізнатися більше про ці кодування та поля форм, зверніться до MDN вебдокументації для POST. +Якщо ви хочете дізнатися більше про ці кодування та поля форм, зверніться до MDN вебдокументації для POST. /// /// warning | Попередження -Ви можете оголосити кілька параметрів `Form` в *операції шляху*, але не можете одночасно оголосити поля `Body`, які Ви очікуєте отримати у форматі JSON, оскільки тіло запиту буде закодовано у форматі `application/x-www-form-urlencoded`, а не `application/json`. +Ви можете оголосити кілька параметрів `Form` в *операції шляху*, але не можете одночасно оголосити поля `Body`, які ви очікуєте отримати у форматі JSON, оскільки запит матиме тіло, закодоване як `application/x-www-form-urlencoded`, а не `application/json`. Це не обмеження **FastAPI**, а частина HTTP-протоколу. /// -## Підсумок +## Підсумок { #recap } Використовуйте `Form` для оголошення вхідних параметрів у вигляді даних форми. diff --git a/docs/uk/docs/tutorial/response-model.md b/docs/uk/docs/tutorial/response-model.md index def1f8a2d5..2fcad94387 100644 --- a/docs/uk/docs/tutorial/response-model.md +++ b/docs/uk/docs/tutorial/response-model.md @@ -1,36 +1,35 @@ -# Модель відповіді — Тип, що повертається +# Модель відповіді — Тип, що повертається { #response-model-return-type } -Ви можете оголосити тип, який використовуватиметься у відповіді, за допомогою *анотації типу, що повертається* *функцією операцією шляху* (path operation) +Ви можете оголосити тип, який використовуватиметься у відповіді, анотувавши **тип повернення** *функції операції шляху*. -**Анотацію типу** можна вказати так само як і для вхідних **параметрів** функції: це можуть бути моделі Pydantic, списки (lists), словники (dictionaries), скалярні значення, як-от цілі числа (integers), булеві значення (booleans) тощо. +**Анотації типів** можна використовувати так само, як і для вхідних даних у **параметрах** функції: можна використовувати моделі Pydantic, списки, словники, скалярні значення, як-от цілі числа, булеві значення тощо. {* ../../docs_src/response_model/tutorial001_01_py310.py hl[16,21] *} -FastAPI використовуватиме цей тип, щоб: +FastAPI використовуватиме цей тип повернення, щоб: * **Перевірити правильність** повернених даних. - * Якщо дані не валідні (наприклад, відсутнє поле), це означає, що Ваш код додатку працює некоректно і не повертає те, що повинен. У такому випадку FastAPI поверне помилку сервера, замість того щоб віддати недопустимі дані. Так Ви та Ваші клієнти будете впевнені, що отримуєте очікувані дані у правильному форматі. - -* Додати **JSON Schema** відповіді до специфікації OpenAPI в *операціях шляху*. + * Якщо дані не валідні (наприклад, відсутнє поле), це означає, що *ваш* код застосунку зламаний, не повертає те, що повинен, і буде повернуто помилку сервера замість некоректних даних. Так ви та ваші клієнти можете бути впевнені, що отримаєте дані й очікувану структуру даних. +* Додати **JSON Schema** для відповіді в OpenAPI *операції шляху*. * Це буде використано в **автоматичній документації**. - * А також інструментами, які автоматично генерують клієнтський код. + * Це також буде використано інструментами, які автоматично генерують клієнтський код. Але найголовніше: -* FastAPI **обмежить та відфільтрує** вихідні дані відповідно до типу, вказаного у відповіді. - * Це особливо важливо для **безпеки**. Деталі нижче. +* Це **обмежить та відфільтрує** вихідні дані до того, що визначено в типі повернення. + * Це особливо важливо для **безпеки**, нижче ми побачимо про це більше. -## Параметр `response_model` +## Параметр `response_model` { #response-model-parameter } -Іноді Вам потрібно або зручно повертати інші типи даних, ніж ті, що зазначені як тип відповіді. +Є випадки, коли вам потрібно або ви хочете повертати дані, які не зовсім відповідають тому, що оголошено типом. -Наприклад, Ви можете **повертати словник** або об’єкт бази даних, але **оголосити модель Pydantic** як модель відповіді. Тоді модель Pydantic автоматично оброблятиме валідацію, документацію тощо. +Наприклад, ви можете захотіти **повертати словник** або об’єкт бази даних, але **оголосити його як модель Pydantic**. Таким чином модель Pydantic виконуватиме всю документацію даних, валідацію тощо для об’єкта, який ви повернули (наприклад, словника або об’єкта бази даних). -Якщо Ви додасте анотацію типу для повернення, редактор коду або mypy можуть поскаржитися, що функція повертає інший тип (наприклад, dict замість Item). +Якщо ви додали анотацію типу повернення, інструменти та редактори скаржитимуться (коректною) помилкою, повідомляючи, що ваша функція повертає тип (наприклад, dict), який відрізняється від того, що ви оголосили (наприклад, модель Pydantic). -У таких випадках можна скористатися параметром `response_model` в декораторі маршруту (наприклад, @app.get()). +У таких випадках можна скористатися параметром *декоратора операції шляху* `response_model` замість типу повернення. -Параметр `response_model` працює з будь-яким *оператором шляху*: +Ви можете використовувати параметр `response_model` у будь-якій з *операцій шляху*: * `@app.get()` * `@app.post()` @@ -42,33 +41,33 @@ FastAPI використовуватиме цей тип, щоб: /// note | Примітка -Зверніть увагу, що `response_model` є параметром методу-декоратора (`get`, `post`, тощо), а не *функцією операцією шляху* (path operation function), як це робиться з параметрами або тілом запиту. +Зверніть увагу, що `response_model` є параметром методу «декоратора» (`get`, `post` тощо). А не вашої *функції операції шляху*, як усі параметри та тіло. /// -`response_model` приймає такий самий тип, який Ви б вказали для поля моделі Pydantic. Тобто це може бути як Pydantic-модель, так і, наприклад, `list` із моделей Pydantic — `List[Item]`. +`response_model` приймає такий самий тип, який ви б вказали для поля моделі Pydantic, тобто це може бути модель Pydantic, але також це може бути, наприклад, `list` моделей Pydantic, як-от `List[Item]`. -FastAPI використовуватиме `response_model` для створення документації, валідації даних та — найважливіше — **перетворення та фільтрації вихідних даних** згідно з оголошеним типом. +FastAPI використовуватиме цей `response_model` для виконання всієї документації даних, валідації тощо, а також для **перетворення та фільтрації вихідних даних** до оголошеного типу. /// tip | Порада -Якщо у Вас увімкнено сувору перевірку типів у редакторі, mypy тощо, Ви можете оголосити тип повернення функції як `Any`. +Якщо у вас увімкнено сувору перевірку типів у редакторі, mypy тощо, ви можете оголосити тип повернення функції як `Any`. -Таким чином, Ви повідомляєте редактору, що свідомо повертаєте будь-що. Але FastAPI усе одно виконуватиме створення документації, валідацію, фільтрацію тощо за допомогою параметра `response_model`. +Таким чином, ви повідомляєте редактору, що свідомо повертаєте будь-що. Але FastAPI усе одно виконуватиме документацію даних, валідацію, фільтрацію тощо за допомогою `response_model`. /// -### Пріоритет `response_model` +### Пріоритет `response_model` { #response-model-priority } -Якщо Ви вказуєте і тип повернення, і `response_model`, то FastAPI використовуватиме `response_model` з пріоритетом. +Якщо ви оголошуєте і тип повернення, і `response_model`, то `response_model` матиме пріоритет і буде використаний FastAPI. -Таким чином, Ви можете додати правильні анотації типів до ваших функцій, навіть якщо вони повертають тип, відмінний від `response_model`. Це буде корисно для редакторів коду та інструментів, таких як mypy. І при цьому FastAPI продовжить виконувати валідацію даних, генерувати документацію тощо на основі `response_model`. +Таким чином ви можете додати правильні анотації типів до ваших функцій, навіть коли повертаєте тип, відмінний від моделі відповіді, щоб це використовували редактор і інструменти на кшталт mypy. І при цьому FastAPI все одно виконуватиме валідацію даних, документацію тощо, використовуючи `response_model`. -Ви також можете використати `response_model=None`, щоб вимкнути створення моделі відповіді для цієї *операції шляху*. Це може знадобитися, якщо Ви додаєте анотації типів до об'єктів, які не є допустимими полями Pydantic — приклад цього Ви побачите в одному з наступних розділів. +Ви також можете використати `response_model=None`, щоб вимкнути створення моделі відповіді для цієї *операції шляху*; це може знадобитися, якщо ви додаєте анотації типів для речей, які не є валідними полями Pydantic, приклад цього ви побачите в одному з розділів нижче. -## Повернути ті самі вхідні дані +## Повернути ті самі вхідні дані { #return-the-same-input-data } -Тут ми оголошуємо модель `UserIn`, яка містить звичайний текстовий пароль: +Тут ми оголошуємо модель `UserIn`, вона міститиме пароль у відкритому вигляді: {* ../../docs_src/response_model/tutorial002_py310.py hl[7,9] *} @@ -76,7 +75,7 @@ FastAPI використовуватиме `response_model` для створе Щоб використовувати `EmailStr`, спочатку встановіть `email-validator`. -Переконайтесь, що Ви створили [віртуальне середовище](../virtual-environments.md){.internal-link target=_blank}, активували його, а потім встановили пакет, наприклад: +Переконайтеся, що ви створили [віртуальне середовище](../virtual-environments.md){.internal-link target=_blank}, активували його, а потім встановили пакет, наприклад: ```console $ pip install email-validator @@ -90,29 +89,29 @@ $ pip install "pydantic[email]" /// -І ми використовуємо цю модель, щоб оголосити і вхідні, і вихідні дані: +І ми використовуємо цю модель, щоб оголосити наші вхідні дані, і цю ж модель, щоб оголосити наші вихідні дані: {* ../../docs_src/response_model/tutorial002_py310.py hl[16] *} -Тепер, коли браузер створює користувача з паролем, API поверне той самий пароль у відповіді. +Тепер, щоразу коли браузер створює користувача з паролем, API поверне той самий пароль у відповіді. -У цьому випадку це може не бути проблемою, адже саме користувач надіслав пароль. +У цьому випадку це може не бути проблемою, адже це той самий користувач надсилає пароль. -Але якщо ми використаємо цю ж модель для іншої операції шляху, ми можемо випадково надіслати паролі наших користувачів кожному клієнту. +Але якщо ми використаємо цю ж модель для іншої *операції шляху*, ми можемо надсилати паролі наших користувачів кожному клієнту. /// danger | Обережно -Ніколи не зберігайте пароль користувача у відкритому вигляді та не надсилайте його у відповіді, якщо тільки Ви не знаєте всі ризики і точно розумієте, що робите. +Ніколи не зберігайте пароль користувача у відкритому вигляді та не надсилайте його у відповіді таким чином, якщо тільки ви не знаєте всіх застережень і точно розумієте, що робите. /// -## Додайте окрему вихідну модель +## Додати вихідну модель { #add-an-output-model } -Замість цього ми можемо створити вхідну модель з відкритим паролем і вихідну модель без нього: +Замість цього ми можемо створити вхідну модель з паролем у відкритому вигляді і вихідну модель без нього: {* ../../docs_src/response_model/tutorial003_py310.py hl[9,11,16] *} -Тут, навіть якщо *функція операції шляху* повертає об'єкт користувача, який містить пароль: +Тут, хоча наша *функція операції шляху* повертає того самого вхідного користувача, який містить пароль: {* ../../docs_src/response_model/tutorial003_py310.py hl[24] *} @@ -120,107 +119,107 @@ $ pip install "pydantic[email]" {* ../../docs_src/response_model/tutorial003_py310.py hl[22] *} -Таким чином, **FastAPI** автоматично відфільтрує всі дані, які не вказані у вихідній моделі (за допомогою Pydantic). +Таким чином, **FastAPI** подбає про фільтрацію всіх даних, які не оголошені у вихідній моделі (використовуючи Pydantic). -### `response_model` або тип повернення +### `response_model` або тип повернення { #response-model-or-return-type } -У цьому випадку, оскільки дві моделі різні, якщо ми анотуємо тип повернення функції як `UserOut`, редактор і такі інструменти, як mypy, видадуть помилку, бо фактично ми повертаємо інший тип. +У цьому випадку, оскільки дві моделі різні, якщо ми анотуємо тип повернення функції як `UserOut`, редактор і інструменти скаржитимуться, що ми повертаємо невалідний тип, адже це різні класи. -Тому в цьому прикладі ми використовуємо параметр `response_model`, а не анотацію типу повернення. +Саме тому в цьому прикладі нам треба оголосити це через параметр `response_model`. -...але читайте далі, щоб дізнатися, як обійти це обмеження. +...але читайте далі нижче, щоб побачити, як це обійти. -## Тип повернення і фільтрація даних +## Тип повернення і фільтрація даних { #return-type-and-data-filtering } -Продовжимо з попереднього прикладу. Ми хотіли **анотувати функцію одним типом**, але при цьому повертати з неї більше даних. +Продовжимо з попереднього прикладу. Ми хотіли **анотувати функцію одним типом**, але хотіли мати змогу повертати з функції те, що насправді містить **більше даних**. -Ми хочемо, щоб FastAPI продовжував **фільтрувати** ці дані за допомогою response_model. Тобто навіть якщо функція повертає більше інформації, у відповіді будуть лише ті поля, які вказані у response_model. +Ми хочемо, щоб FastAPI продовжував **фільтрувати** дані, використовуючи модель відповіді. Тобто навіть якщо функція повертає більше даних, відповідь міститиме лише поля, оголошені в моделі відповіді. -У попередньому прикладі, оскільки класи були різні, нам довелося використовувати параметр `response_model`. Але це означає, що ми не отримуємо підтримки з боку редактора коду та інструментів перевірки типів щодо типу, який повертає функція. +У попередньому прикладі, оскільки класи були різні, нам довелося використовувати параметр `response_model`. Але це також означає, що ми не отримуємо підтримки від редактора та інструментів, які перевіряють тип повернення функції. -Проте в більшості випадків, коли нам потрібно зробити щось подібне, ми просто хочемо, щоб модель **відфільтрувала або прибрала** частину даних, як у цьому прикладі. +Проте в більшості випадків, коли нам потрібно зробити щось подібне, ми просто хочемо, щоб модель **відфільтрувала/прибрала** частину даних, як у цьому прикладі. -У таких випадках ми можемо використати класи та спадкування, щоб скористатися **анотаціями типів** функцій — це дає кращу підтримку з боку редактора та інструментів типу mypy, і при цьому FastAPI продовжує виконувати **фільтрацію даних** у відповіді. +І в таких випадках ми можемо використати класи та спадкування, щоб скористатися **анотаціями типів** функцій і отримати кращу підтримку в редакторі та інструментах, і при цьому зберегти **фільтрацію даних** у FastAPI. {* ../../docs_src/response_model/tutorial003_01_py310.py hl[7:10,13:14,18] *} -Завдяки цьому ми отримуємо підтримку інструментів — від редакторів і mypy, оскільки цей код є коректним з точки зору типів, — але ми також отримуємо фільтрацію даних від FastAPI. +Завдяки цьому ми отримуємо підтримку інструментів — від редакторів і mypy, адже цей код коректний з точки зору типів, — але ми також отримуємо фільтрацію даних від FastAPI. Як це працює? Давайте розберемося. 🤓 -### Типи та підтримка інструментів +### Анотації типів і підтримка інструментів { #type-annotations-and-tooling } -Спершу подивимось, як це бачать редактори, mypy та інші інструменти. +Спершу подивімося, як це бачать редактори, mypy та інші інструменти. -`BaseUser` має базові поля. Потім `UserIn` успадковує `BaseUser` і додає поле `password`, отже, він матиме всі поля з обох моделей. +`BaseUser` має базові поля. Потім `UserIn` успадковує `BaseUser` і додає поле `password`, отже він включатиме всі поля з обох моделей. -Ми зазначаємо тип повернення функції як `BaseUser`, але фактично повертаємо екземпляр `UserIn`. +Ми анотуємо тип повернення функції як `BaseUser`, але фактично повертаємо екземпляр `UserIn`. -Редактор, mypy та інші інструменти не скаржитимуться на це, тому що з точки зору типізації `UserIn` є підкласом `BaseUser`, а це означає, що він є `валідним` типом, коли очікується будь-що, що є `BaseUser`. +Редактор, mypy та інші інструменти не скаржитимуться на це, тому що з точки зору типізації `UserIn` є підкласом `BaseUser`, а це означає, що він є *валідним* типом, коли очікується будь-що, що є `BaseUser`. -### Фільтрація даних у FastAPI +### Фільтрація даних у FastAPI { #fastapi-data-filtering } -Тепер для FastAPI він бачить тип повернення і переконується, що те, що Ви повертаєте, містить **тільки** поля, які оголошені у цьому типі. +Тепер для FastAPI він побачить тип повернення і переконається, що те, що ви повертаєте, містить **лише** поля, які оголошені у цьому типі. -FastAPI виконує кілька внутрішніх операцій з Pydantic, щоб гарантувати, що правила наслідування класів не застосовуються для фільтрації повернених даних, інакше Ви могли б повернути значно більше даних, ніж очікували. +FastAPI виконує кілька внутрішніх операцій з Pydantic, щоб гарантувати, що ті самі правила наслідування класів не застосовуються для фільтрації повернених даних, інакше ви могли б зрештою повертати значно більше даних, ніж очікували. -Таким чином, Ви отримуєте найкраще з двох світів: анотації типів **з підтримкою інструментів** і **фільтрацію даних**. +Таким чином ви можете отримати найкраще з двох світів: анотації типів із **підтримкою інструментів** і **фільтрацію даних**. -## Подивитись у документації +## Подивитися в документації { #see-it-in-the-docs } -Коли Ви дивитесь автоматичну документацію, Ви можете побачити, що вхідна модель і вихідна модель мають власну JSON-схему: +Коли ви дивитеся автоматичну документацію, ви можете перевірити, що вхідна модель і вихідна модель матимуть власну JSON Schema: -І обидві моделі використовуються для інтерактивної API-документації: +І обидві моделі будуть використані для інтерактивної документації API: -## Інші анотації типів повернення +## Інші анотації типів повернення { #other-return-type-annotations } -Існують випадки, коли Ви повертаєте щось, що не є допустимим полем Pydantic, але анотуєте це у функції лише для того, щоб отримати підтримку від інструментів (редактора, mypy тощо). +Можуть бути випадки, коли ви повертаєте щось, що не є валідним полем Pydantic, і анотуєте це у функції лише для того, щоб отримати підтримку від інструментів (редактора, mypy тощо). -### Повернення Response напряму +### Повернути Response напряму { #return-a-response-directly } Найпоширенішим випадком буде [повернення Response напряму, як пояснюється пізніше у розширеній документації](../advanced/response-directly.md){.internal-link target=_blank}. -{* ../../docs_src/response_model/tutorial003_02.py hl[8,10:11] *} +{* ../../docs_src/response_model/tutorial003_02_py39.py hl[8,10:11] *} Цей простий випадок автоматично обробляється FastAPI, тому що анотація типу повернення — це клас (або підклас) `Response`. І інструменти також будуть задоволені, бо і `RedirectResponse`, і `JSONResponse` є підкласами `Response`, отже анотація типу коректна. -### Анотація підкласу Response +### Анотувати підклас Response { #annotate-a-response-subclass } -Також можна використовувати підклас `Response` у анотації типу: +Ви також можете використати підклас `Response` в анотації типу: -{* ../../docs_src/response_model/tutorial003_03.py hl[8:9] *} +{* ../../docs_src/response_model/tutorial003_03_py39.py hl[8:9] *} Це теж працюватиме, бо `RedirectResponse` — підклас `Response`, і FastAPI автоматично обробить цей простий випадок. -### Некоректні анотації типу повернення +### Некоректні анотації типу повернення { #invalid-return-type-annotations } -Але коли Ви повертаєте якийсь інший довільний об’єкт, що не є валідним типом Pydantic (наприклад, об’єкт бази даних), і анотуєте його так у функції, FastAPI спробує створити Pydantic модель відповіді на основі цієї анотації типу, і це завершиться помилкою. +Але коли ви повертаєте якийсь інший довільний об’єкт, що не є валідним типом Pydantic (наприклад, об’єкт бази даних), і анотуєте його так у функції, FastAPI спробує створити модель відповіді Pydantic на основі цієї анотації типу і це завершиться помилкою. -Те саме станеться, якщо Ви використовуєте union між різними типами, де один або більше не є валідними типами Pydantic, наприклад, це спричинить помилку 💥: +Те саме станеться, якщо ви використаєте union між різними типами, де один або більше не є валідними типами Pydantic, наприклад, це завершиться помилкою 💥: {* ../../docs_src/response_model/tutorial003_04_py310.py hl[8] *} -...це не працює, тому що тип анотації не є типом Pydantic і не є просто класом `Response` або його підкласом, а є об’єднанням (union) — або `Response`, або `dict`. +...це не працює, тому що анотація типу не є типом Pydantic і не є просто одним класом `Response` або його підкласом, це union (будь-який із двох) між `Response` і `dict`. -### Відключення Моделі Відповіді +### Вимкнути модель відповіді { #disable-response-model } -Продовжуючи приклад вище, можливо, Ви не хочете використовувати стандартну валідацію даних, автоматичну документацію, фільтрацію тощо, які FastAPI виконує за замовчуванням. +Продовжуючи приклад вище, можливо, ви не хочете мати стандартну валідацію даних, документацію, фільтрацію тощо, які виконує FastAPI. -Але ви все одно можете залишити анотацію типу у функції, щоб зберегти підтримку з боку інструментів, таких як редактори коду або статичні перевірки типів (наприклад, mypy). +Але ви можете все одно хотіти залишити анотацію типу повернення у функції, щоб отримати підтримку від інструментів, як-от редактори та перевірки типів (наприклад, mypy). У такому випадку ви можете вимкнути генерацію моделі відповіді, встановивши `response_model=None`: {* ../../docs_src/response_model/tutorial003_05_py310.py hl[7] *} -Це змусить FastAPI пропустити генерацію моделі відповіді, і таким чином Ви зможете використовувати будь-які анотації типів повернення без впливу на вашу FastAPI аплікацію. 🤓 +Це змусить FastAPI пропустити генерацію моделі відповіді, і таким чином ви зможете використовувати будь-які потрібні анотації типів повернення без впливу на ваш FastAPI застосунок. 🤓 -## Параметри кодування моделі відповіді +## Параметри кодування моделі відповіді { #response-model-encoding-parameters } Ваша модель відповіді може мати значення за замовчуванням, наприклад: @@ -230,19 +229,19 @@ FastAPI виконує кілька внутрішніх операцій з Pyd * `tax: float = 10.5` має значення за замовчуванням `10.5`. * `tags: List[str] = []` має значення за замовчуванням порожній список: `[]`. -Але Ви можете захотіти не включати їх у результат, якщо вони фактично не були збережені. +але ви можете захотіти не включати їх у результат, якщо вони фактично не були збережені. -Наприклад, якщо у Вас є моделі з багатьма необов’язковими атрибутами у NoSQL базі даних, але Ви не хочете відправляти дуже довгі JSON-відповіді, повні значень за замовчуванням. +Наприклад, якщо у вас є моделі з багатьма необов’язковими атрибутами у NoSQL базі даних, але ви не хочете надсилати дуже довгі JSON-відповіді, повні значень за замовчуванням. -### Використовуйте параметр `response_model_exclude_unset` +### Використовуйте параметр `response_model_exclude_unset` { #use-the-response-model-exclude-unset-parameter } -Ви можете встановити параметр декоратора шляху `response_model_exclude_unset=True`: +Ви можете встановити параметр *декоратора операції шляху* `response_model_exclude_unset=True`: {* ../../docs_src/response_model/tutorial004_py310.py hl[22] *} -і ці значення за замовчуванням не будуть включені у відповідь, тільки фактично встановлені значення. +і ці значення за замовчуванням не будуть включені у відповідь, лише значення, які фактично встановлені. -Отже, якщо Ви надішлете запит до цього оператора шляху для елемента з item_id `foo`, відповідь (без включення значень за замовчуванням) буде: +Отже, якщо ви надішлете запит до цієї *операції шляху* для елемента з ID `foo`, відповідь (без включення значень за замовчуванням) буде: ```JSON { @@ -253,32 +252,18 @@ FastAPI виконує кілька внутрішніх операцій з Pyd /// info | Інформація -У Pydantic версії 1 метод називався `.dict()`, він був застарілий (але ще підтримується) у Pydantic версії 2 і перейменований у `.model_dump()`. - -Приклади тут використовують `.dict()` для сумісності з Pydantic v1, але Вам слід використовувати `.model_dump()`, якщо Ви можете використовувати Pydantic v2. - -/// - -/// info | Інформація - -FastAPI використовує `.dict()` моделі Pydantic з параметром `exclude_unset`, щоб досягти цього. - -/// - -/// info | Інформація - Ви також можете використовувати: * `response_model_exclude_defaults=True` * `response_model_exclude_none=True` -як описано в документації Pydantic for `exclude_defaults` та `exclude_none`. +як описано в документації Pydantic для `exclude_defaults` та `exclude_none`. /// -#### Дані зі значеннями для полів із типовими значеннями +#### Дані зі значеннями для полів із типовими значеннями { #data-with-values-for-fields-with-defaults } -Але якщо Ваші дані мають значення для полів моделі з типовими значеннями, як у елемента з item_id `bar`: +Але якщо ваші дані мають значення для полів моделі з типовими значеннями, як у елемента з ID `bar`: ```Python hl_lines="3 5" { @@ -288,11 +273,12 @@ FastAPI використовує `.dict()` моделі Pydantic з `http.HTTPStatus`. + +`status_code` також може, як альтернативу, приймати `IntEnum`, наприклад, Python `http.HTTPStatus`. /// Він буде: -* Повертати вказаний код статусу у відповіді. -* Документувати його як такий у схемі OpenAPI (і, таким чином, в інтерфейсі користувача): +* Повертати цей код статусу у відповіді. +* Документувати його як такий у схемі OpenAPI (і, таким чином, в інтерфейсах користувача): -/// note | Нотатка +/// note | Примітка Деякі коди відповіді (див. наступний розділ) вказують, що відповідь не має тіла. -FastAPI знає про це і створить OpenAPI документацію, яка вказує, що тіла відповіді немає. +FastAPI знає про це і створить документацію OpenAPI, яка вказує, що тіла відповіді немає. /// -## Про HTTP статус коди +## Про HTTP коди статусу { #about-http-status-codes } -/// note | Нотатка +/// note | Примітка -Якщо Ви вже знаєте, що таке HTTP коди статусу, переходьте до наступного розділу. +Якщо ви вже знаєте, що таке HTTP коди статусу, перейдіть до наступного розділу. /// -В HTTP Ви надсилаєте числовий код статусу з 3 цифр як частину відповіді. +В HTTP ви надсилаєте числовий код статусу з 3 цифр як частину відповіді. -Ці коди статусу мають пов’язану назву для їх розпізнавання, але найважливішою частиною є саме число. +Ці коди статусу мають пов’язану назву для їх розпізнавання, але важливою частиною є число. Коротко: -* **`100 - 199`** "Інформаційні" відповіді. Ви рідко використовуєте їх напряму. Відповіді з такими кодами не можуть мати тіла. -* **`200 - 299`** "Успішні" відповіді. Це ті, які Ви використовуватимете найчастіше. - * `200` - код за замовчуванням, який означає, що все пройшло "OK". - * Інший приклад – `201`, "Created" (створено). Його зазвичай використовують після створення нового запису в базі даних. - * Особливий випадок – `204`, "No Content" (немає вмісту). Ця відповідь використовується, коли немає даних для повернення клієнту, тому відповідь не повинна мати тіла. -* **`300 - 399`** "Перенаправлення". Відповіді з цими кодами можуть мати або не мати тіла, за винятком `304`, "Not Modified" (не змінено), яка не повинна мати тіла. -* **`400 - 499`** "Помилка клієнта". Це другий тип, який Ви, ймовірно, будете використовувати найчастіше. - * Приклад `404`, "Not Found" (не знайдено). - * Для загальних помилок клієнта можна використовувати `400`. -* `500 - 599` "Помилки сервера". Ви майже ніколи не використовуєте їх напряму. Якщо в коді Вашого застосунку або на сервері щось пішло не так, автоматично буде повернено один із цих кодів статусу. +* `100 - 199` — для «Information». Ви рідко використовуєте їх напряму. Відповіді з такими кодами статусу не можуть мати тіла. +* **`200 - 299`** — для «Successful» відповідей. Це ті, які ви використовуватимете найчастіше. + * `200` — код статусу за замовчуванням, який означає, що все було «OK». + * Інший приклад — `201`, «Created». Його зазвичай використовують після створення нового запису в базі даних. + * Особливий випадок — `204`, «No Content». Цю відповідь використовують, коли немає вмісту для повернення клієнту, і тому відповідь не повинна мати тіла. +* **`300 - 399`** — для «Redirection». Відповіді з цими кодами статусу можуть мати або не мати тіла, за винятком `304`, «Not Modified», яка не повинна мати тіла. +* **`400 - 499`** — для відповідей «Client error». Це другий тип, який ви, ймовірно, будете використовувати найчастіше. + * Приклад — `404`, для відповіді «Not Found». + * Для загальних помилок з боку клієнта ви можете просто використовувати `400`. +* `500 - 599` — для помилок сервера. Ви майже ніколи не використовуєте їх напряму. Коли щось піде не так у якійсь частині коду вашого застосунку або на сервері, автоматично буде повернено один із цих кодів статусу. /// tip | Порада -Щоб дізнатися більше про кожен код статусу і призначення кожного з них, перегляньте документацію MDN про HTTP коди статусу. +Щоб дізнатися більше про кожен код статусу і для чого призначений кожен із них, перегляньте документацію MDN про HTTP коди статусу. /// -## Легкий спосіб запам'ятати назви +## Скорочення, щоб запам’ятати назви { #shortcut-to-remember-the-names } -Розглянемо ще раз попередній приклад: +Розглянемо попередній приклад ще раз: -{* ../../docs_src/response_status_code/tutorial001.py hl[6] *} +{* ../../docs_src/response_status_code/tutorial001_py39.py hl[6] *} -`201` - це код статусу для "Created" (створено). +`201` — це код статусу для «Created». -Але Вам не потрібно запам'ятовувати, що означає кожен із цих кодів. +Але вам не потрібно запам'ятовувати, що означає кожен із цих кодів. -Ви можете використовувати зручні змінні з `fastapi.status` +Ви можете використовувати зручні змінні з `fastapi.status`. -{* ../../docs_src/response_status_code/tutorial002.py hl[1,6] *} +{* ../../docs_src/response_status_code/tutorial002_py39.py hl[1,6] *} -Ці змінні просто для зручності. Вони містять ті ж самі числа, але Ви можете скористатися автозаповненням в редакторі: +Вони — просто для зручності, містять те саме число, але так ви можете скористатися автозаповненням редактора, щоб знайти їх: @@ -91,10 +92,10 @@ FastAPI знає про це і створить OpenAPI документаці Ви також можете використати `from starlette import status`. -**FastAPI** надає ті ж самі змінні `starlette.status` як `fastapi.status`, просто для зручності розробника. Однак вони походять безпосередньо зі Starlette. +**FastAPI** надає той самий `starlette.status` як `fastapi.status` просто як зручність для вас, розробника. Але він походить безпосередньо зі Starlette. /// -## Зміна значення за замовчуванням +## Зміна значення за замовчуванням { #changing-the-default } -Далі, у Посібнику для досвідчених користувачів{.internal-link target=_blank}, Ви дізнаєтесь, як повернути інший код статусу, ніж той, який Ви оголосили тут. +Пізніше, у [Посібнику для досвідчених користувачів](../advanced/response-change-status-code.md){.internal-link target=_blank}, ви побачите, як повертати інший код статусу, ніж значення за замовчуванням, яке ви оголошуєте тут. diff --git a/docs/uk/docs/tutorial/schema-extra-example.md b/docs/uk/docs/tutorial/schema-extra-example.md index 853fd5e659..54608c2ab1 100644 --- a/docs/uk/docs/tutorial/schema-extra-example.md +++ b/docs/uk/docs/tutorial/schema-extra-example.md @@ -1,44 +1,22 @@ -# Декларування прикладів вхідних даних +# Декларування прикладів вхідних даних { #declare-request-example-data } Ви можете задати приклади даних, які Ваш застосунок може отримувати. Ось кілька способів, як це зробити. -## Додаткові дані JSON-схеми в моделях Pydantic +## Додаткові дані JSON-схеми в моделях Pydantic { #extra-json-schema-data-in-pydantic-models } Ви можете задати `examples` для моделі Pydantic, які буде додано до згенерованої JSON-схеми. -//// tab | Pydantic v2 - {* ../../docs_src/schema_extra_example/tutorial001_py310.py hl[13:24] *} -//// +Ця додаткова інформація буде додана як є до **JSON-схеми** для цієї моделі, і вона буде використана в документації до API. -//// tab | Pydantic v1 - -{* ../../docs_src/schema_extra_example/tutorial001_pv1_py310.py hl[13:23] *} - -//// - -Ця додаткова інформація буде додана як є до **JSON-схеми**, і вона буде використовуватися в документації до API. - -//// tab | Pydantic v2 - -У версії Pydantic 2 використовується атрибут `model_config`, який приймає `dict`, як описано в документації Pydantic: Конфігурація. +Ви можете використати атрибут `model_config`, який приймає `dict`, як описано в документації Pydantic: Configuration. Ви можете встановити `"json_schema_extra"` як `dict`, що містить будь-які додаткові дані, які Ви хочете відобразити у згенерованій JSON-схемі, включаючи `examples`. -//// - -//// tab | Pydantic v1 - -У версії Pydantic 1 використовується внутрішній клас `Config` і параметр `schema_extra`, як описано в документації Pydantic: Налаштування схеми. - -Ви можете задати `schema_extra` як `dict`, що містить будь-які додаткові дані, які Ви хочете бачити у згенерованій JSON-схемі, включаючи `examples`. - -//// - -/// tip | Підказка +/// tip | Порада Ви можете використати ту ж техніку, щоб розширити JSON-схему і додати власну додаткову інформацію. @@ -50,19 +28,19 @@ OpenAPI 3.1.0 (який використовується починаючи з FastAPI 0.99.0) додав підтримку `examples`, що є частиною стандарту **JSON-схеми**. -До цього підтримувався лише ключ `example` з одним прикладом. Він все ще підтримується в OpenAPI 3.1.0, але є застарілим і не входить до стандарту JSON Schema. Тому рекомендується перейти з `example` на `examples`. 🤓 +До цього підтримувався лише ключ `example` з одним прикладом. Він все ще підтримується в OpenAPI 3.1.0, але є застарілим і не входить до стандарту JSON Schema. Тому рекомендується перейти з `example` на `examples`. 🤓 Більше про це можна прочитати в кінці цієї сторінки. /// -## Додаткові аргументи `Field` +## Додаткові аргументи `Field` { #field-additional-arguments } Коли ви використовуєте `Field()` у моделях Pydantic, Ви також можете вказати додаткові `examples`: {* ../../docs_src/schema_extra_example/tutorial002_py310.py hl[2,8:11] *} -## `examples` у JSON-схемі — OpenAPI +## `examples` у JSON-схемі — OpenAPI { #examples-in-json-schema-openapi } При використанні будь-кого з наступного: @@ -76,41 +54,41 @@ OpenAPI 3.1.0 (який використовується починаючи з F Ви також можете задати набір `examples` з додатковою інформацією, яка буде додана до їхніх **JSON-схем** у **OpenAPI**. -### `Body` з `examples` +### `Body` з `examples` { #body-with-examples } Тут ми передаємо `examples`, які містять один приклад очікуваних даних у `Body()`: {* ../../docs_src/schema_extra_example/tutorial003_an_py310.py hl[22:29] *} -### Приклад у UI документації +### Приклад у UI документації { #example-in-the-docs-ui } -За допомогою будь-якого з наведених вище методів це виглядатиме так у документації за `/docs`: +За допомогою будь-якого з наведених вище методів це виглядатиме так у `/docs`: -### `Body` з кількома `examples` +### `Body` з кількома `examples` { #body-with-multiple-examples } Звичайно, Ви також можете передати кілька `examples`: {* ../../docs_src/schema_extra_example/tutorial004_an_py310.py hl[23:38] *} -Коли Ви це робите, приклади будуть частиною внутрішньої **JSON-схеми** для цих даних. +Коли Ви це робите, приклади будуть частиною внутрішньої **JSON-схеми** для цих даних тіла. -Втім, на момент написання цього (26 серпня 2023), Swagger UI — інструмент, який відповідає за відображення UI документації — не підтримує показ кількох прикладів у **JSON-схеми**. Але нижче можна прочитати про обхідний шлях. +Втім, на момент написання цього (час написання цього), Swagger UI — інструмент, який відповідає за відображення UI документації — не підтримує показ кількох прикладів для даних у **JSON-схемі**. Але нижче можна прочитати про обхідний шлях. -### Специфічні для OpenAPI `examples` +### Специфічні для OpenAPI `examples` { #openapi-specific-examples } -Ще до того, як **JSON-схема** почала підтримувати `examples`, OpenAPI вже мала підтримку поля з такою ж назвою — `examples`. +Ще до того, як **JSON-схема** почала підтримувати `examples`, OpenAPI вже мала підтримку іншого поля, яке також називається `examples`. -Це **специфічне для OpenAPI** поле `examples` розміщується в іншій частині специфікації OpenAPI — у **деталях кожної *операції шляху***, а не всередині самої JSON-схеми. +Це **специфічне для OpenAPI** поле `examples` розміщується в іншому розділі специфікації OpenAPI. Воно розміщується в **деталях кожної *операції шляху***, а не всередині кожної JSON-схеми. -Swagger UI вже давно підтримує це поле `examples`. Тому Ви можете використовувати його, щоб **відображати** кілька **прикладів у документації**. +І Swagger UI вже давно підтримує це поле `examples`. Тому Ви можете використовувати його, щоб **відображати** різні **приклади в UI документації**. -Це поле `examples` у специфікації OpenAPI — це `dict` (словник) з **кількома прикладами** (а не список `list`), кожен із яких може містити додаткову інформацію, що буде додана до **OpenAPI**. +Форма цього специфічного для OpenAPI поля `examples` — це `dict` з **кількома прикладами** (а не `list`), кожен із яких має додаткову інформацію, яка також буде додана до **OpenAPI**. -Воно не включається до JSON Schema кожного параметра, а розміщується зовні, безпосередньо в *операції шляху*. +Воно не включається всередину кожної JSON-схеми, що міститься в OpenAPI, воно розміщується зовні, безпосередньо в *операції шляху*. -### Використання параметра `openapi_examples` +### Використання параметра `openapi_examples` { #using-the-openapi-examples-parameter } Ви можете оголосити специфічні для OpenAPI `examples` у FastAPI за допомогою параметра `openapi_examples` для: @@ -122,30 +100,32 @@ Swagger UI вже давно підтримує це поле `examples`. Том * `Form()` * `File()` -Ключі словника (`dict`) ідентифікують кожен приклад, а кожне значення `dict` — кожен специфічний словник `dict` в `examples` може містити: +Ключі `dict` ідентифікують кожен приклад, а кожне значення — це інший `dict`. + +Кожен специфічний `dict` прикладу в `examples` може містити: * `summary`: короткий опис прикладу. -* `description`: розгорнутий опис (може містити Markdown). -* `value`: сам приклад, наприклад, словник (`dict`). -* `externalValue`: альтернатива `value`, URL-адреса, що вказує на приклад. Проте ця опція може не підтримуватися більшістю інструментів, на відміну від `value`. +* `description`: розгорнутий опис, який може містити Markdown. +* `value`: це сам приклад, який буде показано, наприклад `dict`. +* `externalValue`: альтернатива `value`, URL-адреса, що вказує на приклад. Проте це може не підтримуватися такою кількістю інструментів, як `value`. Використання виглядає так: {* ../../docs_src/schema_extra_example/tutorial005_an_py310.py hl[23:49] *} -### Приклади OpenAPI у UI документації +### Приклади OpenAPI в UI документації { #openapi-examples-in-the-docs-ui } -З параметром `openapi_examples`, доданим до `Body()`, документація `/docs` виглядатиме так: +З `openapi_examples`, доданим до `Body()`, `/docs` виглядатиме так: -## Технічні деталі +## Технічні деталі { #technical-details } -/// tip | Підказка +/// tip | Порада -Якщо Ви вже використовуєте **FastAPI** версії **0.99.0 або вище**, Ви можете **пропустити** цей розділ. +Якщо Ви вже використовуєте **FastAPI** версії **0.99.0 або вище**, Ви, ймовірно, можете **пропустити** ці технічні деталі. -Він більш актуальний для старих версій, до появи OpenAPI 3.1.0. +Вони більш актуальні для старих версій, до появи OpenAPI 3.1.0. Можна вважати це коротким **історичним екскурсом** у OpenAPI та JSON Schema. 🤓 @@ -155,68 +135,68 @@ Swagger UI вже давно підтримує це поле `examples`. Том Це дуже технічна інформація про стандарти **JSON Schema** і **OpenAPI**. -Якщо вищезгадані ідеї вже працюють у Вас — можете не заглиблюватися в ці деталі. +Якщо вищезгадані ідеї вже працюють у Вас, цього може бути достатньо, і Вам, ймовірно, не потрібні ці деталі — можете пропустити. /// -До OpenAPI 3.1.0 специфікація використовувала стару та модифіковану версію **JSON Schema**. +До OpenAPI 3.1.0 OpenAPI використовував стару та модифіковану версію **JSON Schema**. -Оскільки JSON Schema раніше не підтримувала `examples`, OpenAPI додала власне поле `examples`. +JSON Schema не мала `examples`, тож OpenAPI додала власне поле `example` до своєї модифікованої версії. -OpenAPI також додала `example` і `examples` до інших частин специфікації: +OpenAPI також додала поля `example` і `examples` до інших частин специфікації: -* `Parameter Object` (в специфікації) використовується FastAPI для: +* `Parameter Object` (в специфікації), який використовувався утилітами FastAPI: * `Path()` * `Query()` * `Header()` * `Cookie()` -* `Request Body Object`, в полі `content`, в `Media Type Object` (в специфікації) використовується FastAPI для: +* `Request Body Object`, у полі `content`, у `Media Type Object` (в специфікації), який використовувався утилітами FastAPI: * `Body()` * `File()` * `Form()` /// info | Інформація -Цей старий параметр `examples`, специфічний для OpenAPI, тепер називається `openapi_examples`, починаючи з FastAPI версії `0.103.0`. +Цей старий специфічний для OpenAPI параметр `examples` тепер називається `openapi_examples`, починаючи з FastAPI `0.103.0`. /// -### Поле `examples` у JSON Schema +### Поле `examples` у JSON Schema { #json-schemas-examples-field } Пізніше JSON Schema додала поле `examples` у нову версію специфікації. -І вже OpenAPI 3.1.0 базується на цій новій версії (JSON Schema 2020-12), яка включає поле `examples`. +А потім новий OpenAPI 3.1.0 базувався на найновішій версії (JSON Schema 2020-12), яка включала це нове поле `examples`. -Тепер це поле `examples` є пріоритетним і замінює старе (і кастомне) поле `example`, яке стало застарілим. +І тепер це нове поле `examples` має вищий пріоритет за старе одиночне (і кастомне) поле `example`, яке тепер є застарілим. -Нове поле `examples` у JSON Schema — це **просто список (`list`)** прикладів, без додаткових метаданих (на відміну від OpenAPI). +Це нове поле `examples` у JSON Schema — це **просто `list`** прикладів, а не `dict` з додатковими метаданими, як в інших місцях OpenAPI (описаних вище). /// info | Інформація -Навіть після того, як з'явився OpenAPI 3.1.0, який підтримував examples у JSON Schema, інструмент Swagger UI ще деякий час не підтримував цю версію (підтримка з’явилась з версії 5.0.0 🎉). +Навіть після релізу OpenAPI 3.1.0 з цією новою простішою інтеграцією з JSON Schema, протягом певного часу Swagger UI, інструмент, який надає автоматичну документацію, не підтримував OpenAPI 3.1.0 (тепер підтримує, починаючи з версії 5.0.0 🎉). Через це версії FastAPI до 0.99.0 все ще використовували версії OpenAPI нижчі за 3.1.0. /// -### `Examples` в Pydantic і FastAPI +### `examples` у Pydantic і FastAPI { #pydantic-and-fastapi-examples } -Коли Ви додаєте `examples` у модель Pydantic через `schema_extra` або `Field(examples=["something"])`, ці приклади додаються до **JSON Schema** цієї моделі. +Коли Ви додаєте `examples` у модель Pydantic через `schema_extra` або `Field(examples=["something"])`, цей приклад додається до **JSON Schema** для цієї моделі Pydantic. -І ця **JSON Schema** Pydantic-моделі включається до **OpenAPI** Вашого API, а потім використовується в UI документації (docs UI). +І ця **JSON Schema** Pydantic-моделі включається до **OpenAPI** Вашого API, а потім використовується в UI документації. -У версіях FastAPI до 0.99.0 (починаючи з 0.99.0 використовується новіший OpenAPI 3.1.0), коли Ви використовували `example` або `examples` з іншими утилітами (`Query()`, `Body()` тощо), ці приклади не додавалися до JSON Schema, який описує ці дані (навіть не до власної версії JSON Schema у OpenAPI). Натомість вони додавалися безпосередньо до опису *обробника шляху* *(path operation)* в OpenAPI (тобто поза межами частин, які використовують JSON Schema). +У версіях FastAPI до 0.99.0 (0.99.0 і вище використовують новіший OpenAPI 3.1.0), коли Ви використовували `example` або `examples` з будь-якими іншими утилітами (`Query()`, `Body()` тощо), ці приклади не додавалися до JSON Schema, що описує ці дані (навіть не до власної версії JSON Schema в OpenAPI), натомість вони додавалися безпосередньо до декларації *операції шляху* в OpenAPI (поза межами частин OpenAPI, які використовують JSON Schema). -Але тепер, коли FastAPI 0.99.0 і вище використовують OpenAPI 3.1.0, а той — JSON Schema 2020-12, разом із Swagger UI 5.0.0 і вище — все стало більш узгодженим, і examples тепер включаються до JSON Schema. +Але тепер, коли FastAPI 0.99.0 і вище використовує OpenAPI 3.1.0, який використовує JSON Schema 2020-12, і Swagger UI 5.0.0 і вище, все стало більш узгодженим, і приклади включаються до JSON Schema. -### Swagger UI та специфічні для OpenAPI `examples` +### Swagger UI та специфічні для OpenAPI `examples` { #swagger-ui-and-openapi-specific-examples } -Раніше (станом на 26 серпня 2023 року) Swagger UI не підтримував кілька прикладів у JSON Schema, тому користувачі не мали можливості показати декілька прикладів у документації. +Оскільки Swagger UI не підтримував кілька прикладів JSON Schema (станом на 2023-08-26), користувачі не мали можливості показати кілька прикладів у документації. -Щоб вирішити це, FastAPI починаючи з версії 0.103.0 **додав підтримку** старого **OpenAPI-специфічного** поля `examples` через новий параметр `openapi_examples`. 🤓 +Щоб вирішити це, FastAPI `0.103.0` **додав підтримку** оголошення того самого старого **OpenAPI-специфічного** поля `examples` через новий параметр `openapi_examples`. 🤓 -### Підсумок +### Підсумок { #summary } -Раніше я казав, що не люблю історію... а тепер ось я — розповідаю "технічні історичні" лекції. 😅 +Раніше я казав, що не дуже люблю історію... а тепер подивіться на мене — читаю «технічні історичні» лекції. 😅 Коротко: **оновіться до FastAPI 0.99.0 або вище** — і все стане значно **простішим, узгодженим та інтуїтивно зрозумілим**, і Вам не доведеться знати всі ці історичні деталі. 😎 diff --git a/docs/uk/docs/tutorial/security/index.md b/docs/uk/docs/tutorial/security/index.md index c3d94be8d9..f1fb25178d 100644 --- a/docs/uk/docs/tutorial/security/index.md +++ b/docs/uk/docs/tutorial/security/index.md @@ -1,70 +1,72 @@ -# Безпека +# Безпека { #security } Існує багато способів реалізувати безпеку, автентифікацію та авторизацію. -Це зазвичай складна і "непроста" тема. +І зазвичай це складна і «непроста» тема. -У багатьох фреймворках і системах забезпечення безпеки та автентифікації займає величезну частину зусиль і коду (іноді — понад 50% всього написаного коду). +У багатьох фреймворках і системах лише обробка безпеки та автентифікації потребує великих зусиль і коду (у багатьох випадках це може бути 50% або більше від усього написаного коду). -**FastAPI** надає кілька інструментів, які допоможуть Вам впоратися з **безпекою** легко, швидко, стандартним способом, без необхідності вивчати всі специфікації безпеки. +**FastAPI** надає кілька інструментів, які допоможуть вам працювати з **безпекою** легко, швидко, стандартним способом, без необхідності вивчати всі специфікації безпеки. -Але спочатку — кілька коротких понять. +Але спочатку розгляньмо кілька невеликих понять. -## Поспішаєте? +## Поспішаєте? { #in-a-hurry } -Якщо Вам не цікаві всі ці терміни й просто потрібно *швидко* додати автентифікацію за логіном і паролем — переходьте до наступних розділів. +Якщо вам не цікаві всі ці терміни й вам просто потрібно додати безпеку з автентифікацією на основі імені користувача та пароля *прямо зараз*, переходьте до наступних розділів. -## OAuth2 +## OAuth2 { #oauth2 } -OAuth2 — це специфікація, що описує кілька способів обробки автентифікації та авторизації. +OAuth2 — це специфікація, що визначає кілька способів обробки автентифікації та авторизації. -Це досить об'ємна специфікація, яка охоплює складні випадки використання. +Це досить об'ємна специфікація, яка охоплює кілька складних випадків використання. -Вона включає способи автентифікації через "третю сторону". +Вона включає способи автентифікації через «третю сторону». -Саме це лежить в основі "входу через Google, Facebook, X (Twitter), GitHub" тощо. +Саме це лежить в основі всіх систем із «увійти через Facebook, Google, X (Twitter), GitHub». -### OAuth 1 +### OAuth 1 { #oauth-1 } -Раніше існував OAuth 1, який значно відрізняється від OAuth2 і є складнішим, оскільки містив специфікації для шифрування комунікацій. +Раніше існував OAuth 1, який значно відрізняється від OAuth2 і є складнішим, оскільки містив прямі специфікації щодо того, як шифрувати комунікацію. -Зараз майже не використовується. +Зараз він не дуже популярний або використовується. -OAuth2 не вказує, як саме шифрувати з'єднання — воно очікує, що ваш застосунок працює через HTTPS. +OAuth2 не вказує, як саме шифрувати комунікацію — він очікує, що ваш застосунок доступний через HTTPS. /// tip | Порада -У розділі про **деплой** Ви побачите, як налаштувати HTTPS безкоштовно з Traefik та Let's Encrypt. +У розділі про **деплой** ви побачите, як налаштувати HTTPS безкоштовно з Traefik та Let's Encrypt. /// -## OpenID Connect +## OpenID Connect { #openid-connect } OpenID Connect — ще одна специфікація, побудована на основі **OAuth2**. -Вона розширює OAuth2, уточнюючи деякі неоднозначності для досягнення кращої сумісності. +Вона просто розширює OAuth2, уточнюючи деякі відносно неоднозначні речі в OAuth2, щоб зробити його більш сумісним. -Наприклад, вхід через Google використовує OpenID Connect (який базується на OAuth2). +Наприклад, вхід через Google використовує OpenID Connect (який під капотом використовує OAuth2). -Але вхід через Facebook — ні. Він має власну реалізацію на базі OAuth2. +Але вхід через Facebook не підтримує OpenID Connect. Він має власний різновид OAuth2. -### OpenID (не "OpenID Connect") +### OpenID (не «OpenID Connect») { #openid-not-openid-connect } -Існувала також специфікація "OpenID", яка намагалася розвʼязати ті самі задачі, що й **OpenID Connect**, але не базувалась на OAuth2. +Існувала також специфікація «OpenID». Вона намагалася розвʼязати те саме, що й **OpenID Connect**, але не базувалась на OAuth2. -Це була зовсім інша система, і сьогодні вона майже не використовується. +Тож це була повністю додаткова система. -## OpenAPI +Зараз вона не дуже популярна або використовується. -OpenAPI (раніше Swagger) — це специфікація для побудови API (тепер під егідою Linux Foundation). +## OpenAPI { #openapi } + +OpenAPI (раніше відомий як Swagger) — це відкрита специфікація для побудови API (тепер частина Linux Foundation). **FastAPI** базується на **OpenAPI**. -Завдяки цьому Ви отримуєте автоматичну інтерактивну документацію, генерацію коду та багато іншого. +Саме це робить можливими кілька автоматичних інтерактивних інтерфейсів документації, генерацію коду тощо. -OpenAPI дозволяє описувати різні "схеми" безпеки. +OpenAPI має спосіб визначати різні «схеми» безпеки. -Використовуючи їх, Ви можете скористатися всіма цими інструментами, що базуються на стандартах, зокрема інтерактивними системами документації. +Використовуючи їх, ви можете скористатися всіма цими інструментами, що базуються на стандартах, зокрема цими інтерактивними системами документації. OpenAPI визначає такі схеми безпеки: @@ -72,33 +74,33 @@ OpenAPI визначає такі схеми безпеки: * Параметр запиту. * Заголовок. * Cookie. -* `http`: стандартні методи HTTP-автентифікації, включаючи: - * `bearer`: заголовок `Authorization` зі значенням `Bearer` та токеном. Це успадковано з OAuth2. - * HTTP Basic автентифікація - * HTTP Digest, тощо. +* `http`: стандартні системи HTTP-автентифікації, включаючи: + * `bearer`: заголовок `Authorization` зі значенням `Bearer ` та токеном. Це успадковано з OAuth2. + * HTTP Basic автентифікацію. + * HTTP Digest тощо. * `oauth2`: усі способи обробки безпеки за допомогою OAuth2 (так звані «потоки»). - * Деякі з цих потоків підходять для створення власного провайдера автентифікації OAuth 2.0 (наприклад, Google, Facebook, X (Twitter), GitHub тощо): - * `implicit`— неявний - * `clientCredentials`— облікові дані клієнта - * `authorizationCode` — код авторизації - * Але є один окремий «потік», який ідеально підходить для реалізації автентифікації всередині одного додатку: - * `password`: у наступних розділах буде приклад використання цього потоку. -* `openIdConnect`: дозволяє автоматично виявляти параметри автентифікації OAuth2. - * Це автоматичне виявлення визначається у специфікації OpenID Connect. + * Декілька з цих потоків підходять для створення провайдера автентифікації OAuth 2.0 (наприклад, Google, Facebook, X (Twitter), GitHub тощо): + * `implicit` + * `clientCredentials` + * `authorizationCode` + * Але є один окремий «потік», який можна ідеально використати для обробки автентифікації напряму в цьому ж застосунку: + * `password`: у кількох наступних розділах будуть приклади цього. +* `openIdConnect`: має спосіб визначити, як автоматично виявляти дані автентифікації OAuth2. + * Саме це автоматичне виявлення визначено у специфікації OpenID Connect. /// tip | Порада -Інтеграція інших провайдерів автентифікації/авторизації, таких як Google, Facebook, X (Twitter), GitHub тощо — також можлива і відносно проста. +Інтеграція інших провайдерів автентифікації/авторизації, таких як Google, Facebook, X (Twitter), GitHub тощо, також можлива і відносно проста. -Найскладніше — це створити власного провайдера автентифікації/авторизації, як Google чи Facebook. Але **FastAPI** надає Вам інструменти, щоб зробити це легко, беручи на себе важку частину роботи. +Найскладніше — це створити провайдера автентифікації/авторизації на кшталт таких, але **FastAPI** надає вам інструменти, щоб зробити це легко, виконуючи важку частину роботи за вас. /// -## Інструменти **FastAPI** +## Утиліти **FastAPI** { #fastapi-utilities } -FastAPI надає кілька інструментів для кожної з описаних схем безпеки в модулі `fastapi.security`, які спрощують використання цих механізмів захисту. +FastAPI надає кілька інструментів для кожної з описаних схем безпеки в модулі `fastapi.security`, які спрощують використання цих механізмів безпеки. -У наступних розділах Ви побачите, як додати безпеку до свого API за допомогою цих інструментів **FastAPI**. +У наступних розділах ви побачите, як додати безпеку до свого API за допомогою цих інструментів, які надає **FastAPI**. -А також побачите, як вона автоматично інтегрується в інтерактивну документацію вашого API. +А також побачите, як це автоматично інтегрується в інтерактивну систему документації. diff --git a/docs/uk/docs/tutorial/static-files.md b/docs/uk/docs/tutorial/static-files.md index 3427f23765..32ca1311d2 100644 --- a/docs/uk/docs/tutorial/static-files.md +++ b/docs/uk/docs/tutorial/static-files.md @@ -1,13 +1,13 @@ -# Статичні файли +# Статичні файли { #static-files } Ви можете автоматично надавати статичні файли з каталогу, використовуючи `StaticFiles`. -## Використання `StaticFiles` +## Використання `StaticFiles` { #use-staticfiles } * Імпортуйте `StaticFiles`. -* "Під'єднати" екземпляр `StaticFiles()` з вказанням необхідного шляху. +* «Під'єднати» екземпляр `StaticFiles()` з вказанням необхідного шляху. -{* ../../docs_src/static_files/tutorial001.py hl[2,6] *} +{* ../../docs_src/static_files/tutorial001_py39.py hl[2,6] *} /// note | Технічні деталі @@ -17,24 +17,24 @@ /// -### Що таке "Під'єднання" +### Що таке «Під'єднання» { #what-is-mounting } -"Під'єднання" означає додавання повноцінного "незалежного" застосунку за певним шляхом, який потім обробляє всі під шляхи. +«Під'єднання» означає додавання повноцінного «незалежного» застосунку за певним шляхом, який потім обробляє всі під шляхи. -Це відрізняється від використання `APIRouter`, оскільки під'єднаний застосунок є повністю незалежним. OpenAPI та документація вашого основного застосунку не будуть знати нічого про ваш під'єднаний застосунок. +Це відрізняється від використання `APIRouter`, оскільки під'єднаний застосунок є повністю незалежним. OpenAPI та документація вашого основного застосунку не будуть знати нічого про ваш під'єднаний застосунок тощо. Ви можете дізнатися більше про це в [Посібнику для просунутих користувачів](../advanced/index.md){.internal-link target=_blank}. -## Деталі +## Деталі { #details } -Перше `"/static"` вказує на під шлях, за яким буде "під'єднано" цей новий "застосунок". Тому будь-який шлях, який починається з `"/static"`, буде оброблятися ним. +Перше `"/static"` вказує на під шлях, за яким буде «під'єднано» цей новий «підзастосунок». Тому будь-який шлях, який починається з `"/static"`, буде оброблятися ним. -`directory="static"` визначає каталог, що містить ваші статичні файли. +`directory="static"` визначає назву каталогу, що містить ваші статичні файли. `name="static"` це ім'я, яке можна використовувати всередині **FastAPI**. -Усі ці параметри можуть бути змінені відповідно до потреб і особливостей вашого застосунку. +Усі ці параметри можуть бути іншими за "`static`", налаштуйте їх відповідно до потреб і особливостей вашого застосунку. -## Додаткова інформація +## Додаткова інформація { #more-info } Детальніше про налаштування та можливості можна дізнатися в документації Starlette про статичні файли. diff --git a/docs/uk/docs/tutorial/testing.md b/docs/uk/docs/tutorial/testing.md index 1105c6b0a8..462592829f 100644 --- a/docs/uk/docs/tutorial/testing.md +++ b/docs/uk/docs/tutorial/testing.md @@ -1,17 +1,18 @@ -# Тестування +# Тестування { #testing } -Тестування **FastAPI** додатків є простим та ефективним завдяки бібліотеці Starlette, яка базується на HTTPX. -Оскільки HTTPX розроблений на основі Requests, його API є інтуїтивно зрозумілим для тих, хто вже знайомий з Requests. +Завдяки Starlette тестувати застосунки **FastAPI** просто й приємно. -З його допомогою Ви можете використовувати pytest безпосередньо з **FastAPI**. +Воно базується на HTTPX, який, своєю чергою, спроєктований на основі Requests, тож він дуже знайомий та інтуїтивно зрозумілий. -## Використання `TestClient` +З його допомогою ви можете використовувати pytest безпосередньо з **FastAPI**. + +## Використання `TestClient` { #using-testclient } /// info | Інформація Щоб використовувати `TestClient`, спочатку встановіть `httpx`. -Переконайтеся, що Ви створили [віртуальне середовище](../virtual-environments.md){.internal-link target=_blank}, активували його, а потім встановили саму бібліотеку, наприклад: +Переконайтеся, що ви створили [віртуальне середовище](../virtual-environments.md){.internal-link target=_blank}, активували його, а потім встановили `httpx`, наприклад: ```console $ pip install httpx @@ -21,7 +22,7 @@ $ pip install httpx Імпортуйте `TestClient`. -Створіть `TestClient`, передавши йому Ваш застосунок **FastAPI**. +Створіть `TestClient`, передавши йому ваш застосунок **FastAPI**. Створюйте функції з іменами, що починаються з `test_` (це стандартна угода для `pytest`). @@ -29,8 +30,7 @@ $ pip install httpx Записуйте прості `assert`-вирази зі стандартними виразами Python, які потрібно перевірити (це також стандарт для `pytest`). -{* ../../docs_src/app_testing/tutorial001.py hl[2,12,15:18] *} - +{* ../../docs_src/app_testing/tutorial001_py39.py hl[2,12,15:18] *} /// tip | Порада @@ -46,25 +46,25 @@ $ pip install httpx Ви також можете використовувати `from starlette.testclient import TestClient`. -**FastAPI** надає той самий `starlette.testclient` під назвою `fastapi.testclient` для зручності розробників, але він безпосередньо походить із Starlette. +**FastAPI** надає той самий `starlette.testclient` під назвою `fastapi.testclient` просто для зручності для вас, розробника. Але він безпосередньо походить із Starlette. /// /// tip | Порада -Якщо Вам потрібно викликати `async`-функції у ваших тестах, окрім відправлення запитів до FastAPI-застосунку (наприклад, асинхронні функції роботи з базою даних), перегляньте [Асинхронні тести](../advanced/async-tests.md){.internal-link target=_blank} у розширеному керівництві. +Якщо ви хочете викликати `async`-функції у ваших тестах, окрім відправлення запитів до вашого застосунку FastAPI (наприклад, асинхронні функції роботи з базою даних), перегляньте [Async Tests](../advanced/async-tests.md){.internal-link target=_blank} у розширеному керівництві. /// -## Розділення тестів +## Розділення тестів { #separating-tests } -У реальному застосунку Ваші тести, ймовірно, будуть в окремому файлі. +У реальному застосунку ваші тести, ймовірно, будуть в окремому файлі. -Також Ваш **FastAPI**-застосунок може складатися з кількох файлів або модулів тощо. +Також ваш застосунок **FastAPI** може складатися з кількох файлів/модулів тощо. -### Файл застосунку **FastAPI** +### Файл застосунку **FastAPI** { #fastapi-app-file } -Припустимо, у Вас є структура файлів, описана в розділі [Більші застосунки](bigger-applications.md){.internal-link target=_blank}: +Припустимо, у вас є структура файлів, описана в розділі [Bigger Applications](bigger-applications.md){.internal-link target=_blank}: ``` . @@ -72,14 +72,15 @@ $ pip install httpx │   ├── __init__.py │   └── main.py ``` -У файлі `main.py` знаходиться Ваш застосунок **FastAPI** : -{* ../../docs_src/app_testing/main.py *} +У файлі `main.py` знаходиться ваш застосунок **FastAPI**: -### Файл тестування -Ви можете створити файл `test_main.py` з Вашими тестами. Він може знаходитися в тому ж пакеті Python (у тій самій директорії з файлом `__init__.py`): +{* ../../docs_src/app_testing/app_a_py39/main.py *} +### Файл тестування { #testing-file } + +Ви можете створити файл `test_main.py` з вашими тестами. Він може знаходитися в тому ж пакеті Python (у тій самій директорії з файлом `__init__.py`): ``` hl_lines="5" . @@ -89,18 +90,18 @@ $ pip install httpx │   └── test_main.py ``` -Оскільки цей файл знаходиться в тому ж пакеті, Ви можете використовувати відносний імпорт, щоб імпортувати об'єкт `app` із модуля `main` (`main.py`): +Оскільки цей файл знаходиться в тому ж пакеті, ви можете використовувати відносний імпорт, щоб імпортувати об'єкт `app` із модуля `main` (`main.py`): -{* ../../docs_src/app_testing/test_main.py hl[3] *} +{* ../../docs_src/app_testing/app_a_py39/test_main.py hl[3] *} ...і написати код для тестів так само як і раніше. -## Тестування: розширений приклад +## Тестування: розширений приклад { #testing-extended-example } Тепер розширимо цей приклад і додамо більше деталей, щоб побачити, як тестувати різні частини. -### Розширений файл застосунку **FastAPI** +### Розширений файл застосунку **FastAPI** { #extended-fastapi-app-file } Залишимо ту саму структуру файлів: @@ -112,75 +113,26 @@ $ pip install httpx │   └── test_main.py ``` -Припустимо, що тепер файл `main.py` із Вашим **FastAPI**-застосунком містить додаткові операції шляху (**path operations**). +Припустимо, що тепер файл `main.py` із вашим застосунком **FastAPI** містить інші **операції шляху**. Він має `GET`-операцію, яка може повертати помилку. Він має `POST`-операцію, яка може повертати кілька помилок. -Обидві операції шляху вимагають заголовок `X-Token`. +Обидві *операції шляху* вимагають заголовок `X-Token`. -//// tab | Python 3.10+ +{* ../../docs_src/app_testing/app_b_an_py310/main.py *} -```Python -{!> ../../docs_src/app_testing/app_b_an_py310/main.py!} -``` +### Розширений тестовий файл { #extended-testing-file } -//// +Потім ви можете оновити `test_main.py`, додавши розширені тести: -//// tab | Python 3.9+ +{* ../../docs_src/app_testing/app_b_an_py310/test_main.py *} -```Python -{!> ../../docs_src/app_testing/app_b_an_py39/main.py!} -``` -//// +Коли вам потрібно передати клієнту інформацію в запиті, але ви не знаєте, як це зробити, ви можете пошукати (Google), як це зробити в `httpx`, або навіть як це зробити з `requests`, оскільки дизайн HTTPX базується на дизайні Requests. -//// tab | Python 3.8+ - -```Python -{!> ../../docs_src/app_testing/app_b_an/main.py!} -``` - -//// - -//// tab | Python 3.10+ non-Annotated - -/// tip | Порада - -Бажано використовувати версію з `Annotated`, якщо це можливо - -/// - -```Python -{!> ../../docs_src/app_testing/app_b_py310/main.py!} -``` - -//// - -//// tab | Python 3.8+ non-Annotated - -/// tip | Порада - -Бажано використовувати версію з `Annotated`, якщо це можливо - -/// - -```Python -{!> ../../docs_src/app_testing/app_b/main.py!} -``` - -//// - -### Розширений тестовий файл - -Потім Ви можете оновити `test_main.py`, додавши розширені тести: - -{* ../../docs_src/app_testing/app_b/test_main.py *} - -Коли Вам потрібно передати клієнту інформацію в запиті, але Ви не знаєте, як це зробити, Ви можете пошукати (наприклад, у Google) спосіб реалізації в `httpx`, або навіть у `requests`, оскільки HTTPX розроблений на основі дизайну Requests. - -Далі Ви просто повторюєте ці ж дії у ваших тестах. +Далі ви просто повторюєте ці ж дії у ваших тестах. Наприклад: @@ -195,15 +147,16 @@ $ pip install httpx /// info | Інформація Зверніть увагу, що `TestClient` отримує дані, які можна конвертувати в JSON, а не Pydantic-моделі. -Якщо у Вас є Pydantic-модель у тесті, і Ви хочете передати її дані в додаток під час тестування, Ви можете використати `jsonable_encoder`, описаний у розділі [JSON Compatible Encoder](encoder.md){.internal-link target=_blank}. + +Якщо у вас є Pydantic-модель у тесті, і ви хочете передати її дані в застосунок під час тестування, ви можете використати `jsonable_encoder`, описаний у розділі [JSON Compatible Encoder](encoder.md){.internal-link target=_blank}. /// -## Запуск тестів +## Запуск { #run-it } Після цього вам потрібно встановити `pytest`. -Переконайтеся, що Ви створили [віртуальне середовище]{.internal-link target=_blank}, активували його і встановили необхідні пакети, наприклад: +Переконайтеся, що ви створили [віртуальне середовище](../virtual-environments.md){.internal-link target=_blank}, активували його і встановили необхідні пакети, наприклад:
@@ -215,7 +168,7 @@ $ pip install pytest
-`pytest` автоматично знайде файли з тестами, виконає їх і надасть вам результати. +Він автоматично знайде файли та тести, виконає їх і повідомить вам результати. Запустіть тести за допомогою: diff --git a/docs/uk/llm-prompt.md b/docs/uk/llm-prompt.md new file mode 100644 index 0000000000..f1c5377a48 --- /dev/null +++ b/docs/uk/llm-prompt.md @@ -0,0 +1,46 @@ +### Target language + +Translate to Ukrainian (українська). + +Language code: uk. + +### Grammar and tone + +- Use polite/formal address consistent with existing Ukrainian docs (use “ви/ваш”). +- Keep the tone concise and technical. + +### Headings + +- Follow existing Ukrainian heading style; keep headings short and instructional. +- Do not add trailing punctuation to headings. + +### Quotes + +- Prefer Ukrainian guillemets «…» for quoted terms in prose, matching existing Ukrainian docs. +- Never change quotes inside inline code, code blocks, URLs, or file paths. + +### Ellipsis + +- Keep ellipsis style consistent with existing Ukrainian docs. +- Never change `...` in code, URLs, or CLI examples. + +### Preferred translations / glossary + +Use the following preferred translations when they apply in documentation prose: + +- request (HTTP): запит +- response (HTTP): відповідь +- path operation: операція шляху +- path operation function: функція операції шляху + +### `///` admonitions + +- Keep the admonition keyword in English (do not translate `note`, `tip`, etc.). +- If a title is present, prefer these canonical titles (choose one canonical form where variants exist): + +- `/// note | Примітка` +- `/// note | Технічні деталі` +- `/// tip | Порада` +- `/// warning | Попередження` +- `/// info | Інформація` +- `/// danger | Обережно` diff --git a/docs/vi/docs/deployment/cloud.md b/docs/vi/docs/deployment/cloud.md deleted file mode 100644 index 1d76522c3e..0000000000 --- a/docs/vi/docs/deployment/cloud.md +++ /dev/null @@ -1,14 +0,0 @@ -# Triển khai FastAPI trên các Dịch vụ Cloud - -Bạn có thể sử dụng **bất kỳ nhà cung cấp dịch vụ cloud** nào để triển khai ứng dụng FastAPI của mình. - -Trong hầu hết các trường hợp, các nhà cung cấp dịch vụ cloud lớn đều có hướng dẫn triển khai FastAPI với họ. - -## Nhà cung cấp dịch vụ Cloud - Nhà tài trợ -Một vài nhà cung cấp dịch vụ cloud ✨ [**tài trợ cho FastAPI**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨, điều này giúp đảm bảo sự phát triển liên tục và khỏe mạnh của FastAPI và hệ sinh thái của nó. - -Thêm nữa, điều này cũng thể hiện cam kết thực sự của họ đối với FastAPI và **cộng đồng người dùng** (bạn), vì họ không chỉ muốn cung cấp cho bạn một **dịch vụ tốt** mà còn muốn đảm bảo rằng bạn có một **framework tốt và bền vững**, đó chính là FastAPI. 🙇 - -Bạn có thể thử các dịch vụ của họ và làm theo hướng dẫn của họ: - -* Render diff --git a/docs/vi/docs/deployment/index.md b/docs/vi/docs/deployment/index.md deleted file mode 100644 index 24ffdc71b0..0000000000 --- a/docs/vi/docs/deployment/index.md +++ /dev/null @@ -1,21 +0,0 @@ -# Triển khai - -Triển khai một ứng dụng **FastAPI** khá dễ dàng. - -## Triển khai là gì - -Triển khai một ứng dụng có nghĩa là thực hiện các bước cần thiết để làm cho nó **sẵn sàng phục vụ người dùng**. - -Đối với một **API web**, điều này có nghĩa là đặt nó trong một **máy chủ từ xa**, với một **chương trình máy chủ** cung cấp hiệu suất tốt, ổn định, v.v., để người dùng của bạn có thể truy cập ứng dụng của bạn một cách hiệu quả và không bị gián đoạn hoặc gặp vấn đề. - -Điều này trái ngược với các **giai đoạn phát triển**, trong đó bạn liên tục thay đổi mã, phá vỡ nó và sửa nó, ngừng và khởi động lại máy chủ phát triển, v.v. - -## Các Chiến lược Triển khai - -Có nhiều cách để triển khai ứng dụng của bạn tùy thuộc vào trường hợp sử dụng của bạn và các công cụ mà bạn sử dụng. - -Bạn có thể **triển khai một máy chủ** của riêng bạn bằng cách sử dụng một sự kết hợp các công cụ, hoặc bạn có thể sử dụng một **dịch vụ cloud** để làm một số công việc cho bạn, hoặc các tùy chọn khác. - -Tôi sẽ chỉ ra một số khái niệm chính cần thiết khi triển khai một ứng dụng **FastAPI** (mặc dù hầu hết nó áp dụng cho bất kỳ loại ứng dụng web nào). - -Bạn sẽ thấy nhiều chi tiết cần thiết và một số kỹ thuật để triển khai trong các phần tiếp theo. ✨ diff --git a/docs/vi/docs/deployment/versions.md b/docs/vi/docs/deployment/versions.md deleted file mode 100644 index 04de393e72..0000000000 --- a/docs/vi/docs/deployment/versions.md +++ /dev/null @@ -1,93 +0,0 @@ -# Về các phiên bản của FastAPI - -**FastAPI** đã được sử dụng ở quy mô thực tế (production) trong nhiều ứng dụng và hệ thống. Và phạm vi kiểm thử được giữ ở mức 100%. Nhưng việc phát triển của nó vẫn đang diễn ra nhanh chóng. - -Các tính năng mới được bổ sung thường xuyên, lỗi được sửa định kỳ, và mã nguồn vẫn đang được cải thiện liên tục - -Đó là lí do các phiên bản hiện tại vẫn còn là 0.x.x, điều này phản ánh rằng mỗi phiên bản có thể có các thay đổi gây mất tương thích. Điều này tuân theo các quy ước về Semantic Versioning. - -Bạn có thể tạo ra sản phẩm thực tế với **FastAPI** ngay bây giờ (và bạn có thể đã làm điều này trong một thời gian dài), bạn chỉ cần đảm bảo rằng bạn sử dụng một phiên bản hoạt động đúng với các đoạn mã còn lại của bạn. - -## Cố định phiên bản của `fastapi` - -Điều đầu tiên bạn nên làm là "cố định" phiên bản của **FastAPI** bạn đang sử dụng để phiên bản mới nhất mà bạn biết hoạt động đúng với ứng dụng của bạn. - -Ví dụ, giả sử bạn đang sử dụng phiên bản `0.112.0` trong ứng dụng của bạn. - -Nếu bạn sử dụng một tệp `requirements.txt` bạn có thể chỉ định phiên bản với: - -```txt -fastapi[standard]==0.112.0 -``` - -Như vậy, bạn sẽ sử dụng chính xác phiên bản `0.112.0`. - -Hoặc bạn cũng có thể cố định nó với: - -```txt -fastapi[standard]>=0.112.0,<0.113.0 -``` - -Như vậy, bạn sẽ sử dụng các phiên bản `0.112.0` trở lên, nhưng nhỏ hơn `0.113.0`, ví dụ, một phiên bản `0.112.2` vẫn được chấp nhận. - -Nếu bạn sử dụng bất kỳ công cụ nào để quản lý cài đặt của bạn, như `uv`, Poetry, Pipenv, hoặc bất kỳ công cụ nào khác, chúng đều có một cách để bạn có thể định nghĩa các phiên bản cụ thể cho các gói của bạn. - -## Các phiên bản có sẵn - -Bạn có thể xem các phiên bản có sẵn (ví dụ để kiểm tra phiên bản mới nhất) trong [Release Notes](../release-notes.md){.internal-link target=_blank}. - -## Về các phiên bản - -Theo quy ước về Semantic Versioning, bất kỳ phiên bản nào bên dưới `1.0.0` có thể thêm các thay đổi gây mất tương thích. - -**FastAPI** cũng theo quy ước rằng bất kỳ thay đổi phiên bản "PATCH" nào là cho các lỗi và các thay đổi không gây mất tương thích. - -/// tip - -"PATCH" là số cuối cùng, ví dụ, trong `0.2.3`, phiên bản PATCH là `3`. - -/// - -Vì vậy, bạn có thể cố định đến một phiên bản như: - -```txt -fastapi>=0.45.0,<0.46.0 -``` - -Các thay đổi gây mất tương thích và các tính năng mới được thêm vào trong các phiên bản "MINOR". - -/// tip - -"MINOR" là số ở giữa, ví dụ, trong `0.2.3`, phiên bản MINOR là `2`. - -/// - -## Nâng cấp các phiên bản của FastAPI - -Bạn nên thêm các bài kiểm tra (tests) cho ứng dụng của bạn. - -Với **FastAPI** điều này rất dễ dàng (nhờ vào Starlette), kiểm tra tài liệu: [Testing](../tutorial/testing.md){.internal-link target=_blank} - -Sau khi bạn có các bài kiểm tra, bạn có thể nâng cấp phiên bản **FastAPI** lên một phiên bản mới hơn, và đảm bảo rằng tất cả mã của bạn hoạt động đúng bằng cách chạy các bài kiểm tra của bạn. - -Nếu mọi thứ đang hoạt động, hoặc sau khi bạn thực hiện các thay đổi cần thiết, và tất cả các bài kiểm tra của bạn đều đi qua, thì bạn có thể cố định phiên bản của `fastapi` đến phiên bản mới hơn. - -## Về Starlette - -Bạn không nên cố định phiên bản của `starlette`. - -Các phiên bản khác nhau của **FastAPI** sẽ sử dụng một phiên bản Starlette mới hơn. - -Vì vậy, bạn có thể để **FastAPI** sử dụng phiên bản Starlette phù hợp. - -## Về Pydantic - -Pydantic bao gồm các bài kiểm tra của riêng nó cho **FastAPI**, vì vậy các phiên bản mới hơn của Pydantic (trên `1.0.0`) luôn tương thích với **FastAPI**. - -Bạn có thể cố định Pydantic đến bất kỳ phiên bản nào trên `1.0.0` mà bạn muốn. - -Ví dụ: - -```txt -pydantic>=2.7.0,<3.0.0 -``` diff --git a/docs/vi/docs/environment-variables.md b/docs/vi/docs/environment-variables.md deleted file mode 100644 index dd06f8959a..0000000000 --- a/docs/vi/docs/environment-variables.md +++ /dev/null @@ -1,300 +0,0 @@ -# Biến môi trường (Environment Variables) - -/// tip - -Nếu bạn đã biết về "biến môi trường" và cách sử dụng chúng, bạn có thể bỏ qua phần này. - -/// - -Một biến môi trường (còn được gọi là "**env var**") là một biến mà tồn tại **bên ngoài** đoạn mã Python, ở trong **hệ điều hành**, và có thể được đọc bởi đoạn mã Python của bạn (hoặc bởi các chương trình khác). - -Các biến môi trường có thể được sử dụng để xử lí **các thiết lập** của ứng dụng, như một phần của **các quá trình cài đặt** Python, v.v. - -## Tạo và Sử dụng các Biến Môi Trường - -Bạn có thể **tạo** và sử dụng các biến môi trường trong **shell (terminal)**, mà không cần sử dụng Python: - -//// tab | Linux, macOS, Windows Bash - -
- -```console -// Bạn có thể tạo một biến môi trường MY_NAME với -$ export MY_NAME="Wade Wilson" - -// Sau đó bạn có thể sử dụng nó với các chương trình khác, như -$ echo "Hello $MY_NAME" - -Hello Wade Wilson -``` - -
- -//// - -//// tab | Windows PowerShell - -
- -```console -// Tạo một biến môi trường MY_NAME -$ $Env:MY_NAME = "Wade Wilson" - -// Sử dụng nó với các chương trình khác, như là -$ echo "Hello $Env:MY_NAME" - -Hello Wade Wilson -``` - -
- -//// - -## Đọc các Biến Môi Trường trong Python - -Bạn cũng có thể tạo các biến môi trường **bên ngoài** đoạn mã Python, trong terminal (hoặc bằng bất kỳ phương pháp nào khác), và sau đó **đọc chúng trong Python**. - -Ví dụ, bạn có một file `main.py` với: - -```Python hl_lines="3" -import os - -name = os.getenv("MY_NAME", "World") -print(f"Hello {name} from Python") -``` - -/// tip - -Tham số thứ hai cho `os.getenv()` là giá trị mặc định để trả về. - -Nếu không được cung cấp, nó mặc định là `None`, ở đây chúng ta cung cấp `"World"` là giá trị mặc định để sử dụng. - -/// - -Sau đó bạn có thể gọi chương trình Python: - -//// tab | Linux, macOS, Windows Bash - -
- -```console -// Ở đây chúng ta chưa cài đặt biến môi trường -$ python main.py - -// Vì chúng ta chưa cài đặt biến môi trường, chúng ta nhận được giá trị mặc định - -Hello World from Python - -// Nhưng nếu chúng ta tạo một biến môi trường trước đó -$ export MY_NAME="Wade Wilson" - -// Và sau đó gọi chương trình lại -$ python main.py - -// Bây giờ nó có thể đọc biến môi trường - -Hello Wade Wilson from Python -``` - -
- -//// - -//// tab | Windows PowerShell - -
- -```console -// Ở đây chúng ta chưa cài đặt biến môi trường -$ python main.py - -// Vì chúng ta chưa cài đặt biến môi trường, chúng ta nhận được giá trị mặc định - -Hello World from Python - -// Nhưng nếu chúng ta tạo một biến môi trường trước đó -$ $Env:MY_NAME = "Wade Wilson" - -// Và sau đó gọi chương trình lại -$ python main.py - -// Bây giờ nó có thể đọc biến môi trường - -Hello Wade Wilson from Python -``` - -
- -//// - -Vì các biến môi trường có thể được tạo bên ngoài đoạn mã Python, nhưng có thể được đọc bởi đoạn mã Python, và không cần được lưu trữ (commit vào `git`) cùng với các file khác, nên chúng thường được sử dụng để lưu các thiết lập hoặc **cấu hình**. - -Bạn cũng có thể tạo ra một biến môi trường dành riêng cho một **lần gọi chương trình**, chỉ có thể được sử dụng bởi chương trình đó, và chỉ trong thời gian chạy của chương trình. - -Để làm điều này, tạo nó ngay trước chương trình đó, trên cùng một dòng: - -
- -```console -// Tạo một biến môi trường MY_NAME cho lần gọi chương trình này -$ MY_NAME="Wade Wilson" python main.py - -// Bây giờ nó có thể đọc biến môi trường - -Hello Wade Wilson from Python - -// Biến môi trường không còn tồn tại sau đó -$ python main.py - -Hello World from Python -``` - -
- -/// tip - -Bạn có thể đọc thêm về điều này tại The Twelve-Factor App: Config. - -/// - -## Các Kiểu (Types) và Kiểm tra (Validation) - -Các biến môi trường có thể chỉ xử lí **chuỗi ký tự**, vì chúng nằm bên ngoài đoạn mã Python và phải tương thích với các chương trình khác và phần còn lại của hệ thống (và thậm chí với các hệ điều hành khác, như Linux, Windows, macOS). - -Điều này có nghĩa là **bất kỳ giá trị nào** được đọc trong Python từ một biến môi trường **sẽ là một `str`**, và bất kỳ hành động chuyển đổi sang kiểu dữ liệu khác hoặc hành động kiểm tra nào cũng phải được thực hiện trong đoạn mã. - -Bạn sẽ học thêm về việc sử dụng biến môi trường để xử lí **các thiết lập ứng dụng** trong [Hướng dẫn nâng cao - Các thiết lập và biến môi trường](./advanced/settings.md){.internal-link target=_blank}. - -## Biến môi trường `PATH` - -Có một biến môi trường **đặc biệt** được gọi là **`PATH`** được sử dụng bởi các hệ điều hành (Linux, macOS, Windows) nhằm tìm các chương trình để thực thi. - -Giá trị của biến môi trường `PATH` là một chuỗi dài được tạo bởi các thư mục được phân tách bởi dấu hai chấm `:` trên Linux và macOS, và bởi dấu chấm phẩy `;` trên Windows. - -Ví dụ, biến môi trường `PATH` có thể có dạng như sau: - -//// tab | Linux, macOS - -```plaintext -/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin -``` - -Điều này có nghĩa là hệ thống sẽ tìm kiếm các chương trình trong các thư mục: - -* `/usr/local/bin` -* `/usr/bin` -* `/bin` -* `/usr/sbin` -* `/sbin` - -//// - -//// tab | Windows - -```plaintext -C:\Program Files\Python312\Scripts;C:\Program Files\Python312;C:\Windows\System32 -``` - -Điều này có nghĩa là hệ thống sẽ tìm kiếm các chương trình trong các thư mục: - -* `C:\Program Files\Python312\Scripts` -* `C:\Program Files\Python312` -* `C:\Windows\System32` - -//// - -Khi bạn gõ một **lệnh** trong terminal, hệ điều hành **tìm kiếm** chương trình trong **mỗi thư mục** được liệt kê trong biến môi trường `PATH`. - -Ví dụ, khi bạn gõ `python` trong terminal, hệ điều hành tìm kiếm một chương trình được gọi `python` trong **thư mục đầu tiên** trong danh sách đó. - -Nếu tìm thấy, nó sẽ **sử dụng** nó. Nếu không tìm thấy, nó sẽ tiếp tục tìm kiếm trong **các thư mục khác**. - -### Cài đặt Python và cập nhật biến môi trường `PATH` - -Khi bạn cài đặt Python, bạn có thể được hỏi nếu bạn muốn cập nhật biến môi trường `PATH`. - -//// tab | Linux, macOS - -Giả sử bạn cài đặt Python vào thư mục `/opt/custompython/bin`. - -Nếu bạn chọn cập nhật biến môi trường `PATH`, thì cài đặt sẽ thêm `/opt/custompython/bin` vào biến môi trường `PATH`. - -Nó có thể có dạng như sau: - -```plaintext -/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/custompython/bin -``` - -Như vậy, khi bạn gõ `python` trong terminal, hệ thống sẽ tìm thấy chương trình Python trong `/opt/custompython/bin` (thư mục cuối) và sử dụng nó. - -//// - -//// tab | Windows - -Giả sử bạn cài đặt Python vào thư mục `C:\opt\custompython\bin`. - -Nếu bạn chọn cập nhật biến môi trường `PATH`, thì cài đặt sẽ thêm `C:\opt\custompython\bin` vào biến môi trường `PATH`. - -Nó có thể có dạng như sau: - -```plaintext -C:\Program Files\Python312\Scripts;C:\Program Files\Python312;C:\Windows\System32;C:\opt\custompython\bin -``` - -Như vậy, khi bạn gõ `python` trong terminal, hệ thống sẽ tìm thấy chương trình Python trong `C:\opt\custompython\bin` (thư mục cuối) và sử dụng nó. - -//// - -Vậy, nếu bạn gõ: - -
- -```console -$ python -``` - -
- -//// tab | Linux, macOS - -Hệ thống sẽ **tìm kiếm** chương trình `python` trong `/opt/custompython/bin` và thực thi nó. - -Nó tương đương với việc bạn gõ: - -
- -```console -$ /opt/custompython/bin/python -``` - -
- -//// - -//// tab | Windows - -Hệ thống sẽ **tìm kiếm** chương trình `python` trong `C:\opt\custompython\bin\python` và thực thi nó. - -Nó tương đương với việc bạn gõ: - -
- -```console -$ C:\opt\custompython\bin\python -``` - -
- -//// - -Thông tin này sẽ hữu ích khi bạn học về [Môi trường ảo](virtual-environments.md){.internal-link target=_blank}. - -## Kết luận - -Với những thông tin này, bạn có thể hiểu được **các biến môi trường là gì** và **cách sử dụng chúng trong Python**. - -Bạn có thể đọc thêm về chúng tại Wikipedia cho Biến môi trường. - -Trong nhiều trường hợp, cách các biến môi trường trở nên hữu ích và có thể áp dụng không thực sự rõ ràng ngay từ đầu, nhưng chúng sẽ liên tục xuất hiện trong rất nhiều tình huống khi bạn phát triển ứng dụng, vì vậy việc hiểu biết về chúng là hữu ích. - -Chẳng hạn, bạn sẽ cần những thông tin này khi bạn học về [Môi trường ảo](virtual-environments.md). diff --git a/docs/vi/docs/fastapi-cli.md b/docs/vi/docs/fastapi-cli.md deleted file mode 100644 index e758f4d3a7..0000000000 --- a/docs/vi/docs/fastapi-cli.md +++ /dev/null @@ -1,75 +0,0 @@ -# FastAPI CLI - -**FastAPI CLI** là một chương trình dòng lệnh có thể được sử dụng để phục vụ ứng dụng FastAPI của bạn, quản lý dự án FastAPI của bạn và nhiều hoạt động khác. - -Khi bạn cài đặt FastAPI (vd với `pip install "fastapi[standard]"`), nó sẽ bao gồm một gói được gọi là `fastapi-cli`, gói này cung cấp lệnh `fastapi` trong terminal. - -Để chạy ứng dụng FastAPI của bạn cho quá trình phát triển (development), bạn có thể sử dụng lệnh `fastapi dev`: - -
- -```console -$ fastapi dev main.py - - FastAPI Starting development server 🚀 - - Searching for package file structure from directories with - __init__.py files - Importing from /home/user/code/awesomeapp - - module 🐍 main.py - - code Importing the FastAPI app object from the module with the - following code: - - from main import app - - app Using import string: main:app - - server Server started at http://127.0.0.1:8000 - server Documentation at http://127.0.0.1:8000/docs - - tip Running in development mode, for production use: - fastapi run - - Logs: - - INFO Will watch for changes in these directories: - ['/home/user/code/awesomeapp'] - INFO Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to - quit) - INFO Started reloader process [383138] using WatchFiles - INFO Started server process [383153] - INFO Waiting for application startup. - INFO Application startup complete. -``` - -
- -Chương trình dòng lệnh `fastapi` là **FastAPI CLI**. - -FastAPI CLI nhận đường dẫn đến chương trình Python của bạn (vd `main.py`) và tự động phát hiện đối tượng `FastAPI` (thường được gọi là `app`), xác định quá trình nhập đúng, và sau đó chạy nó (serve). - -Đối với vận hành thực tế (production), bạn sẽ sử dụng `fastapi run` thay thế. 🚀 - -Ở bên trong, **FastAPI CLI** sử dụng Uvicorn, một server ASGI có hiệu suất cao, sẵn sàng cho vận hành thực tế (production). 😎 - -## `fastapi dev` - -Chạy `fastapi dev` sẽ khởi động quá trình phát triển. - -Mặc định, **auto-reload** được bật, tự động tải lại server khi bạn thay đổi code của bạn. Điều này tốn nhiều tài nguyên và có thể kém ổn định hơn khi nó bị tắt. Bạn nên sử dụng nó cho quá trình phát triển. Nó cũng lắng nghe địa chỉ IP `127.0.0.1`, đó là địa chỉ IP của máy tính để tự giao tiếp với chính nó (`localhost`). - -## `fastapi run` - -Chạy `fastapi run` mặc định sẽ khởi động FastAPI cho quá trình vận hành thực tế. - -Mặc định, **auto-reload** bị tắt. Nó cũng lắng nghe địa chỉ IP `0.0.0.0`, đó là tất cả các địa chỉ IP có sẵn, như vậy nó sẽ được truy cập công khai bởi bất kỳ ai có thể giao tiếp với máy tính. Đây là cách bạn thường chạy nó trong sản phẩm hoàn thiện, ví dụ trong một container. - -Trong hầu hết các trường hợp, bạn sẽ (và nên) có một "proxy điểm cuối (termination proxy)" xử lý HTTPS cho bạn, điều này sẽ phụ thuộc vào cách bạn triển khai ứng dụng của bạn, nhà cung cấp có thể làm điều này cho bạn, hoặc bạn có thể cần thiết lập nó. - -/// tip - -Bạn có thể tìm hiểu thêm về FastAPI CLI trong [tài liệu triển khai](deployment/index.md){.internal-link target=_blank}. - -/// diff --git a/docs/vi/docs/features.md b/docs/vi/docs/features.md deleted file mode 100644 index 2220d9fa5a..0000000000 --- a/docs/vi/docs/features.md +++ /dev/null @@ -1,200 +0,0 @@ -# Tính năng - -## Tính năng của FastAPI - -**FastAPI** cho bạn những tính năng sau: - -### Dựa trên những tiêu chuẩn mở - -* OpenAPI cho việc tạo API, bao gồm những khai báo về đường dẫn các toán tử, tham số, body requests, cơ chế bảo mật, etc. -* Tự động tài liệu hóa data model theo JSON Schema (OpenAPI bản thân nó được dựa trên JSON Schema). -* Được thiết kế xung quanh các tiêu chuẩn này sau khi nghiên cứu tỉ mỉ thay vì chỉ suy nghĩ đơn giản và sơ xài. -* Điều này cho phép tự động hóa **trình sinh code client** cho nhiều ngôn ngữ lập trình khác nhau. - -### Tự động hóa tài liệu - - -Tài liệu tương tác API và web giao diện người dùng. Là một framework được dựa trên OpenAPI do đó có nhiều tùy chọn giao diện cho tài liệu API, 2 giao diện bên dưới là mặc định. - -* Swagger UI, với giao diện khám phá, gọi và kiểm thử API trực tiếp từ trình duyệt. - -![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) - -* Thay thế với tài liệu API với ReDoc. - -![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) - -### Chỉ cần phiên bản Python hiện đại - -Tất cả được dựa trên khai báo kiểu dữ liệu chuẩn của **Python 3.8** (cảm ơn Pydantic). Bạn không cần học cú pháp mới, chỉ cần biết chuẩn Python hiện đại. - -Nếu bạn cần 2 phút để làm mới lại cách sử dụng các kiểu dữ liệu mới của Python (thậm chí nếu bạn không sử dụng FastAPI), xem hướng dẫn ngắn: [Kiểu dữ liệu Python](python-types.md){.internal-link target=_blank}. - -Bạn viết chuẩn Python với kiểu dữ liệu như sau: - -```Python -from datetime import date - -from pydantic import BaseModel - -# Declare a variable as a str -# and get editor support inside the function -def main(user_id: str): - return user_id - - -# A Pydantic model -class User(BaseModel): - id: int - name: str - joined: date -``` - -Sau đó có thể được sử dụng: - -```Python -my_user: User = User(id=3, name="John Doe", joined="2018-07-19") - -second_user_data = { - "id": 4, - "name": "Mary", - "joined": "2018-11-30", -} - -my_second_user: User = User(**second_user_data) -``` - -/// info - -`**second_user_data` nghĩa là: - -Truyền các khóa và giá trị của dict `second_user_data` trực tiếp như các tham số kiểu key-value, tương đương với: `User(id=4, name="Mary", joined="2018-11-30")` - -/// - -### Được hỗ trợ từ các trình soạn thảo - - -Toàn bộ framework được thiết kế để sử dụng dễ dàng và trực quan, toàn bộ quyết định đã được kiểm thử trên nhiều trình soạn thảo thậm chí trước khi bắt đầu quá trình phát triển, để chắc chắn trải nghiệm phát triển là tốt nhất. - -Trong lần khảo sát cuối cùng dành cho các lập trình viên Python, đã rõ ràng rằng đa số các lập trình viên sử dụng tính năng "autocompletion". - -Toàn bộ framework "FastAPI" phải đảm bảo rằng: autocompletion hoạt động ở mọi nơi. Bạn sẽ hiếm khi cần quay lại để đọc tài liệu. - -Đây là các trình soạn thảo có thể giúp bạn: - -* trong Visual Studio Code: - -![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) - -* trong PyCharm: - -![editor support](https://fastapi.tiangolo.com/img/pycharm-completion.png) - -Bạn sẽ có được auto-completion trong code, thậm chí trước đó là không thể. Như trong ví dụ, khóa `price` bên trong một JSON (đó có thể được lồng nhau) đến từ một request. - -Không còn nhập sai tên khóa, quay đi quay lại giữa các tài liệu hoặc cuộn lên cuộn xuống để tìm xem cuối cùng bạn đã sử dụng `username` hay `user_name`. - -### Ngắn gọn - -FastAPI có các giá trị mặc định hợp lý cho mọi thứ, với các cấu hình tùy chọn ở mọi nơi. Tất cả các tham số có thể được tinh chỉnh để thực hiện những gì bạn cần và để định nghĩa API bạn cần. - -Nhưng mặc định, tất cả **đều hoạt động**. - -### Validation - -* Validation cho đa số (hoặc tất cả?) **các kiểu dữ liệu** Python, bao gồm: - * JSON objects (`dict`). - * Mảng JSON (`list`) định nghĩa kiểu dữ liệu từng phần tử. - * Xâu (`str`), định nghĩa độ dài lớn nhất, nhỏ nhất. - * Số (`int`, `float`) với các giá trị lớn nhất, nhỏ nhất, etc. - -* Validation cho nhiều kiểu dữ liệu bên ngoài như: - * URL. - * Email. - * UUID. - * ...và nhiều cái khác. - -Tất cả validation được xử lí bằng những thiết lập tốt và mạnh mẽ của **Pydantic**. - -### Bảo mật và xác thực - -Bảo mật và xác thực đã tích hợp mà không làm tổn hại tới cơ sở dữ liệu hoặc data models. - -Tất cả cơ chế bảo mật định nghĩa trong OpenAPI, bao gồm: - -* HTTP Basic. -* **OAuth2** (với **JWT tokens**). Xem hướng dẫn [OAuth2 with JWT](tutorial/security/oauth2-jwt.md){.internal-link target=_blank}. -* API keys in: - * Headers. - * Các tham số trong query string. - * Cookies, etc. - -Cộng với tất cả các tính năng bảo mật từ Starlette (bao gồm **session cookies**). - -Tất cả được xây dựng dưới dạng các công cụ và thành phần có thể tái sử dụng, dễ dàng tích hợp với hệ thống, kho lưu trữ dữ liệu, cơ sở dữ liệu quan hệ và NoSQL của bạn,... - -### Dependency Injection - -FastAPI bao gồm một hệ thống Dependency Injection vô cùng dễ sử dụng nhưng vô cùng mạnh mẽ. - -* Thậm chí, các dependency có thể có các dependency khác, tạo thành một phân cấp hoặc **"một đồ thị" của các dependency**. -* Tất cả **được xử lí tự động** bởi framework. -* Tất cả các dependency có thể yêu cầu dữ liệu từ request và **tăng cường các ràng buộc từ đường dẫn** và tự động tài liệu hóa. -* **Tự động hóa validation**, thậm chí với các tham số *đường dẫn* định nghĩa trong các dependency. -* Hỗ trợ hệ thống xác thực người dùng phức tạp, **các kết nối cơ sở dữ liệu**,... -* **Không làm tổn hại** cơ sở dữ liệu, frontends,... Nhưng dễ dàng tích hợp với tất cả chúng. - -### Không giới hạn "plug-ins" - -Hoặc theo một cách nào khác, không cần chúng, import và sử dụng code bạn cần. - -Bất kì tích hợp nào được thiết kế để sử dụng đơn giản (với các dependency), đến nỗi bạn có thể tạo một "plug-in" cho ứng dụng của mình trong 2 dòng code bằng cách sử dụng cùng một cấu trúc và cú pháp được sử dụng cho *path operations* của bạn. - -### Đã được kiểm thử - -* 100% test coverage. -* 100% type annotated code base. -* Được sử dụng cho các ứng dụng sản phẩm. - -## Tính năng của Starlette - -`FastAPI` is thực sự là một sub-class của `Starlette`. Do đó, nếu bạn đã biết hoặc đã sử dụng Starlette, đa số các chức năng sẽ làm việc giống như vậy. - -Với **FastAPI**, bạn có được tất cả những tính năng của **Starlette**: - -* Hiệu năng thực sự ấn tượng. Nó là một trong nhưng framework Python nhanh nhất, khi so sánh với **NodeJS** và **Go**. -* Hỗ trợ **WebSocket**. -* In-process background tasks. -* Startup and shutdown events. -* Client cho kiểm thử xây dựng trên HTTPX. -* **CORS**, GZip, Static Files, Streaming responses. -* Hỗ trợ **Session and Cookie**. -* 100% test coverage. -* 100% type annotated codebase. - -## Tính năng của Pydantic - -**FastAPI** tương thích đầy đủ với (và dựa trên) Pydantic. Do đó, bất kì code Pydantic nào bạn thêm vào cũng sẽ hoạt động. - -Bao gồm các thư viện bên ngoài cũng dựa trên Pydantic, như ORMs, ODMs cho cơ sở dữ liệu. - -Nó cũng có nghĩa là trong nhiều trường hợp, bạn có thể truyền cùng object bạn có từ một request **trực tiếp cho cơ sở dữ liệu**, vì mọi thứ được validate tự động. - -Điều tương tự áp dụng cho các cách khác nhau, trong nhiều trường hợp, bạn có thể chỉ truyền object từ cơ sở dữ liêu **trực tiếp tới client**. - -Với **FastAPI**, bạn có tất cả những tính năng của **Pydantic** (FastAPI dựa trên Pydantic cho tất cả những xử lí về dữ liệu): - -* **Không gây rối não**: - * Không cần học ngôn ngữ mô tả cấu trúc mới. - * Nếu bạn biết kiểu dữ liệu Python, bạn biết cách sử dụng Pydantic. -* Sử dụng tốt với **IDE/linter/não của bạn**: - - * Bởi vì các cấu trúc dữ liệu của Pydantic chỉ là các instances của class bạn định nghĩa; auto-completion, linting, mypy và trực giác của bạn nên làm việc riêng biệt với những dữ liệu mà bạn đã validate. -* Validate **các cấu trúc phức tạp**: - * Sử dụng các models Pydantic phân tầng, `List` và `Dict` của Python `typing`,... - * Và các validators cho phép các cấu trúc dữ liệu phức tạp trở nên rõ ràng và dễ dàng để định nghĩa, kiểm tra và tài liệu hóa thành JSON Schema. - * Bạn có thể có các object **JSON lồng nhau** và tất cả chúng đã validate và annotated. -* **Có khả năng mở rộng**: - * Pydantic cho phép bạn tùy chỉnh kiểu dữ liệu bằng việc định nghĩa hoặc bạn có thể mở rộng validation với các decorator trong model. -* 100% test coverage. diff --git a/docs/vi/docs/index.md b/docs/vi/docs/index.md deleted file mode 100644 index a5ac1bfb76..0000000000 --- a/docs/vi/docs/index.md +++ /dev/null @@ -1,475 +0,0 @@ -# FastAPI - - - -

- FastAPI -

-

- FastAPI framework, hiệu năng cao, dễ học, dễ code, sẵn sàng để tạo ra sản phẩm -

-

- - Test - - - Coverage - - - Package version - - - Supported Python versions - -

- ---- - -**Tài liệu**: https://fastapi.tiangolo.com - -**Mã nguồn**: https://github.com/fastapi/fastapi - ---- - -FastAPI là một web framework hiện đại, hiệu năng cao để xây dựng web APIs với Python dựa trên tiêu chuẩn Python type hints. - -Những tính năng như: - -* **Nhanh**: Hiệu năng rất cao khi so sánh với **NodeJS** và **Go** (cảm ơn Starlette và Pydantic). [Một trong những Python framework nhanh nhất](#hieu-nang). -* **Code nhanh**: Tăng tốc độ phát triển tính năng từ 200% tới 300%. * -* **Ít lỗi hơn**: Giảm khoảng 40% những lỗi phát sinh bởi con người (nhà phát triển). * -* **Trực giác tốt hơn**: Được các trình soạn thảo hỗ tuyệt vời. Completion mọi nơi. Ít thời gian gỡ lỗi. -* **Dễ dàng**: Được thiết kế để dễ dàng học và sử dụng. Ít thời gian đọc tài liệu. -* **Ngắn**: Tối thiểu code bị trùng lặp. Nhiều tính năng được tích hợp khi định nghĩa tham số. Ít lỗi hơn. -* **Tăng tốc**: Có được sản phẩm cùng với tài liệu (được tự động tạo) có thể tương tác. -* **Được dựa trên các tiêu chuẩn**: Dựa trên (và hoàn toàn tương thích với) các tiêu chuẩn mở cho APIs : OpenAPI (trước đó được biết đến là Swagger) và JSON Schema. - -* ước tính được dựa trên những kiểm chứng trong nhóm phát triển nội bộ, xây dựng các ứng dụng sản phẩm. - -## Nhà tài trợ - - - -{% if sponsors %} -{% for sponsor in sponsors.gold -%} - -{% endfor -%} -{%- for sponsor in sponsors.silver -%} - -{% endfor %} -{% endif %} - - - -Những nhà tài trợ khác - -## Ý kiến đánh giá - -"_[...] Tôi đang sử dụng **FastAPI** vô cùng nhiều vào những ngày này. [...] Tôi thực sự đang lên kế hoạch sử dụng nó cho tất cả các nhóm **dịch vụ ML tại Microsoft**. Một vài trong số đó đang tích hợp vào sản phẩm lõi của **Window** và một vài sản phẩm cho **Office**._" - -
Kabir Khan - Microsoft (ref)
- ---- - -"_Chúng tôi tích hợp thư viện **FastAPI** để sinh ra một **REST** server, nó có thể được truy vấn để thu được những **dự đoán**._ [bởi Ludwid] " - -
Piero Molino, Yaroslav Dudin, và Sai Sumanth Miryala - Uber (ref)
- ---- - -"_**Netflix** vui mừng thông báo việc phát hành framework mã nguồn mở của chúng tôi cho *quản lí khủng hoảng* tập trung: **Dispatch**! [xây dựng với **FastAPI**]_" - -
Kevin Glisson, Marc Vilanova, Forest Monsen - Netflix (ref)
- ---- - -"_Tôi vô cùng hào hứng về **FastAPI**. Nó rất thú vị_" - -
Brian Okken - Python Bytes podcast host (ref)
- ---- - -"_Thành thật, những gì bạn đã xây dựng nhìn siêu chắc chắn và bóng bẩy. Theo nhiều cách, nó là những gì tôi đã muốn Hug trở thành - thật sự truyền cảm hứng để thấy ai đó xây dựng nó._" - -
Timothy Crosley - người tạo ra Hug (ref)
- ---- - -"_Nếu bạn đang tìm kiếm một **framework hiện đại** để xây dựng một REST APIs, thử xem xét **FastAPI** [...] Nó nhanh, dễ dùng và dễ học [...]_" - -"_Chúng tôi đã chuyển qua **FastAPI cho **APIs** của chúng tôi [...] Tôi nghĩ bạn sẽ thích nó [...]_" - -
Ines Montani - Matthew Honnibal - Explosion AI founders - spaCy creators (ref) - (ref)
-
Ines Montani - Matthew Honnibal - nhà sáng lập Explosion AI - người tạo ra spaCy (ref) - (ref)
- ---- - -"_Nếu ai đó đang tìm cách xây dựng sản phẩm API bằng Python, tôi sẽ đề xuất **FastAPI**. Nó **được thiết kế đẹp đẽ**, **sử dụng đơn giản** và **có khả năng mở rộng cao**, nó đã trở thành một **thành phần quan trọng** trong chiến lược phát triển API của chúng tôi và đang thúc đẩy nhiều dịch vụ và mảng tự động hóa như Kỹ sư TAC ảo của chúng tôi._" - -
Deon Pillsbury - Cisco (ref)
- ---- - -## **Typer**, giao diện dòng lệnh của FastAPI - - - -Nếu bạn đang xây dựng một CLI - ứng dụng được sử dụng trong giao diện dòng lệnh, xem về **Typer**. - -**Typer** là một người anh em của FastAPI. Và nó được dự định trở thành **giao diện dòng lệnh cho FastAPI**. ⌨️ 🚀 - -## Yêu cầu - -FastAPI đứng trên vai những người khổng lồ: - -* Starlette cho phần web. -* Pydantic cho phần data. - -## Cài đặt - -
- -```console -$ pip install fastapi - ----> 100% -``` - -
- -Bạn cũng sẽ cần một ASGI server cho production như Uvicorn hoặc Hypercorn. - -
- -```console -$ pip install "uvicorn[standard]" - ----> 100% -``` - -
- -## Ví dụ - -### Khởi tạo - -* Tạo một tệp tin `main.py` như sau: - -```Python -from typing import Union - -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} -``` - -
-Hoặc sử dụng async def... - -Nếu code của bạn sử dụng `async` / `await`, hãy sử dụng `async def`: - -```Python hl_lines="9 14" -from typing import Union - -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -async def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -async def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} -``` - -**Lưu ý**: - -Nếu bạn không biết, xem phần _"In a hurry?"_ về `async` và `await` trong tài liệu này. - -
- -### Chạy ứng dụng - -Chạy server như sau: - -
- -```console -$ uvicorn main:app --reload - -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -INFO: Started reloader process [28720] -INFO: Started server process [28722] -INFO: Waiting for application startup. -INFO: Application startup complete. -``` - -
- -
-Về lệnh uvicorn main:app --reload... - -Lệnh `uvicorn main:app` tham chiếu tới những thành phần sau: - -* `main`: tệp tin `main.py` (một Python "module"). -* `app`: object được tạo trong tệp tin `main.py` tại dòng `app = FastAPI()`. -* `--reload`: chạy lại server sau khi code thay đổi. Chỉ sử dụng trong quá trình phát triển. - -
- -### Kiểm tra - -Mở trình duyệt của bạn tại http://127.0.0.1:8000/items/5?q=somequery. - -Bạn sẽ thấy một JSON response: - -```JSON -{"item_id": 5, "q": "somequery"} -``` - -Bạn đã sẵn sàng để tạo một API như sau: - -* Nhận HTTP request với _đường dẫn_ `/` và `/items/{item_id}`. -* Cả hai _đường dẫn_ sử dụng toán tử `GET` (cũng đươc biết đến là _phương thức_ HTTP). -* _Đường dẫn_ `/items/{item_id}` có một _tham số đường dẫn_ `item_id`, nó là một tham số kiểu `int`. -* _Đường dẫn_ `/items/{item_id}` có một _tham số query string_ `q`, nó là một tham số tùy chọn kiểu `str`. - -### Tài liệu tương tác API - -Truy cập http://127.0.0.1:8000/docs. - -Bạn sẽ thấy tài liệu tương tác API được tạo tự động (cung cấp bởi Swagger UI): - -![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) - -### Tài liệu API thay thế - -Và bây giờ, hãy truy cập tới http://127.0.0.1:8000/redoc. - -Bạn sẽ thấy tài liệu được thay thế (cung cấp bởi ReDoc): - -![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) - -## Nâng cấp ví dụ - -Bây giờ sửa tệp tin `main.py` để nhận body từ một request `PUT`. - -Định nghĩa của body sử dụng kiểu dữ liệu chuẩn của Python, cảm ơn Pydantic. - -```Python hl_lines="4 9-12 25-27" -from typing import Union - -from fastapi import FastAPI -from pydantic import BaseModel - -app = FastAPI() - - -class Item(BaseModel): - name: str - price: float - is_offer: Union[bool, None] = None - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} - - -@app.put("/items/{item_id}") -def update_item(item_id: int, item: Item): - return {"item_name": item.name, "item_id": item_id} -``` - -Server nên tự động chạy lại (bởi vì bạn đã thêm `--reload` trong lệnh `uvicorn` ở trên). - -### Nâng cấp tài liệu API - -Bây giờ truy cập tới http://127.0.0.1:8000/docs. - -* Tài liệu API sẽ được tự động cập nhật, bao gồm body mới: - -![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) - -* Click vào nút "Try it out", nó cho phép bạn điền những tham số và tương tác trực tiếp với API: - -![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) - -* Sau khi click vào nút "Execute", giao diện người dùng sẽ giao tiếp với API của bạn bao gồm: gửi các tham số, lấy kết quả và hiển thị chúng trên màn hình: - -![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) - -### Nâng cấp tài liệu API thay thế - -Và bây giờ truy cập tới http://127.0.0.1:8000/redoc. - -* Tài liệu thay thế cũng sẽ phản ánh tham số và body mới: - -![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) - -### Tóm lại - -Bạn khai báo **một lần** kiểu dữ liệu của các tham số, body, etc là các tham số của hàm số. - -Bạn định nghĩa bằng cách sử dụng các kiểu dữ liệu chuẩn của Python. - -Bạn không phải học một cú pháp mới, các phương thức và class của một thư viện cụ thể nào. - -Chỉ cần sử dụng các chuẩn của **Python**. - -Ví dụ, với một tham số kiểu `int`: - -```Python -item_id: int -``` - -hoặc với một model `Item` phức tạp hơn: - -```Python -item: Item -``` - -...và với định nghĩa đơn giản đó, bạn có được: - -* Sự hỗ trợ từ các trình soạn thảo, bao gồm: - * Completion. - * Kiểm tra kiểu dữ liệu. -* Kiểm tra kiểu dữ liệu: - * Tự động sinh lỗi rõ ràng khi dữ liệu không hợp lệ . - * Kiểm tra JSON lồng nhau . -* Chuyển đổi dữ liệu đầu vào: tới từ network sang dữ liệu kiểu Python. Đọc từ: - * JSON. - * Các tham số trong đường dẫn. - * Các tham số trong query string. - * Cookies. - * Headers. - * Forms. - * Files. -* Chuyển đổi dữ liệu đầu ra: chuyển đổi từ kiểu dữ liệu Python sang dữ liệu network (như JSON): - * Chuyển đổi kiểu dữ liệu Python (`str`, `int`, `float`, `bool`, `list`,...). - * `datetime` objects. - * `UUID` objects. - * Database models. - * ...và nhiều hơn thế. -* Tự động tạo tài liệu tương tác API, bao gồm 2 giao diện người dùng: - * Swagger UI. - * ReDoc. - ---- - -Quay trở lại ví dụ trước, **FastAPI** sẽ thực hiện: - -* Kiểm tra xem có một `item_id` trong đường dẫn với các request `GET` và `PUT` không? -* Kiểm tra xem `item_id` có phải là kiểu `int` trong các request `GET` và `PUT` không? - * Nếu không, client sẽ thấy một lỗi rõ ràng và hữu ích. -* Kiểm tra xem nếu có một tham số `q` trong query string (ví dụ như `http://127.0.0.1:8000/items/foo?q=somequery`) cho request `GET`. - * Tham số `q` được định nghĩa `= None`, nó là tùy chọn. - * Nếu không phải `None`, nó là bắt buộc (như body trong trường hợp của `PUT`). -* Với request `PUT` tới `/items/{item_id}`, đọc body như JSON: - * Kiểm tra xem nó có một thuộc tính bắt buộc kiểu `str` là `name` không? - * Kiểm tra xem nó có một thuộc tính bắt buộc kiểu `float` là `price` không? - * Kiểm tra xem nó có một thuộc tính tùy chọn là `is_offer` không? Nếu có, nó phải có kiểu `bool`. - * Tất cả những kiểm tra này cũng được áp dụng với các JSON lồng nhau. -* Chuyển đổi tự động các JSON object đến và JSON object đi. -* Tài liệu hóa mọi thứ với OpenAPI, tài liệu đó có thể được sử dụng bởi: - - * Các hệ thống tài liệu có thể tương tác. - * Hệ thống sinh code tự động, cho nhiều ngôn ngữ lập trình. -* Cung cấp trực tiếp 2 giao diện web cho tài liệu tương tác - ---- - -Chúng tôi chỉ trình bày những thứ cơ bản bên ngoài, nhưng bạn đã hiểu cách thức hoạt động của nó. - -Thử thay đổi dòng này: - -```Python - return {"item_name": item.name, "item_id": item_id} -``` - -...từ: - -```Python - ... "item_name": item.name ... -``` - -...sang: - -```Python - ... "item_price": item.price ... -``` - -...và thấy trình soạn thảo của bạn nhận biết kiểu dữ liệu và gợi ý hoàn thiện các thuộc tính. - -![trình soạn thảo hỗ trợ](https://fastapi.tiangolo.com/img/vscode-completion.png) - -Ví dụ hoàn chỉnh bao gồm nhiều tính năng hơn, xem Tutorial - User Guide. - - -**Cảnh báo tiết lỗ**: Tutorial - User Guide: - -* Định nghĩa **tham số** từ các nguồn khác nhau như: **headers**, **cookies**, **form fields** và **files**. -* Cách thiết lập **các ràng buộc cho validation** như `maximum_length` hoặc `regex`. -* Một hệ thống **Dependency Injection vô cùng mạnh mẽ và dễ dàng sử dụng. -* Bảo mật và xác thực, hỗ trợ **OAuth2**(với **JWT tokens**) và **HTTP Basic**. -* Những kĩ thuật nâng cao hơn (nhưng tương đối dễ) để định nghĩa **JSON models lồng nhau** (cảm ơn Pydantic). -* Tích hợp **GraphQL** với Strawberry và các thư viện khác. -* Nhiều tính năng mở rộng (cảm ơn Starlette) như: - * **WebSockets** - * kiểm thử vô cùng dễ dàng dựa trên HTTPX và `pytest` - * **CORS** - * **Cookie Sessions** - * ...và nhiều hơn thế. - -## Hiệu năng - -Independent TechEmpower benchmarks cho thấy các ứng dụng **FastAPI** chạy dưới Uvicorn là một trong những Python framework nhanh nhất, chỉ đứng sau Starlette và Uvicorn (được sử dụng bên trong FastAPI). (*) - -Để hiểu rõ hơn, xem phần Benchmarks. - -## Các dependency tùy chọn - -Sử dụng bởi Pydantic: - -* email-validator - cho email validation. - -Sử dụng Starlette: - -* httpx - Bắt buộc nếu bạn muốn sử dụng `TestClient`. -* jinja2 - Bắt buộc nếu bạn muốn sử dụng cấu hình template engine mặc định. -* python-multipart - Bắt buộc nếu bạn muốn hỗ trợ "parsing", form với `request.form()`. -* itsdangerous - Bắt buộc để hỗ trợ `SessionMiddleware`. -* pyyaml - Bắt buộc để hỗ trợ `SchemaGenerator` cho Starlette (bạn có thể không cần nó trong FastAPI). - -Sử dụng bởi FastAPI / Starlette: - -* uvicorn - Server để chạy ứng dụng của bạn. -* orjson - Bắt buộc nếu bạn muốn sử dụng `ORJSONResponse`. -* ujson - Bắt buộc nếu bạn muốn sử dụng `UJSONResponse`. - -Bạn có thể cài đặt tất cả những dependency trên với `pip install "fastapi[all]"`. - -## Giấy phép - -Dự án này được cấp phép dưới những điều lệ của giấy phép MIT. diff --git a/docs/vi/docs/python-types.md b/docs/vi/docs/python-types.md deleted file mode 100644 index 403e899300..0000000000 --- a/docs/vi/docs/python-types.md +++ /dev/null @@ -1,593 +0,0 @@ -# Giới thiệu kiểu dữ liệu Python - -Python hỗ trợ tùy chọn "type hints" (còn được gọi là "type annotations"). - -Những **"type hints"** hay chú thích là một cú pháp đặc biệt cho phép khai báo kiểu dữ liệu của một biến. - -Bằng việc khai báo kiểu dữ liệu cho các biến của bạn, các trình soạn thảo và các công cụ có thể hỗ trợ bạn tốt hơn. - -Đây chỉ là một **hướng dẫn nhanh** về gợi ý kiểu dữ liệu trong Python. Nó chỉ bao gồm những điều cần thiết tối thiểu để sử dụng chúng với **FastAPI**... đó thực sự là rất ít. - -**FastAPI** hoàn toàn được dựa trên những gợi ý kiểu dữ liệu, chúng mang đến nhiều ưu điểm và lợi ích. - -Nhưng thậm chí nếu bạn không bao giờ sử dụng **FastAPI**, bạn sẽ được lợi từ việc học một ít về chúng. - -/// note - -Nếu bạn là một chuyên gia về Python, và bạn đã biết mọi thứ về gợi ý kiểu dữ liệu, bỏ qua và đi tới chương tiếp theo. - -/// - -## Động lực - -Hãy bắt đầu với một ví dụ đơn giản: - -{* ../../docs_src/python_types/tutorial001.py *} - - -Kết quả khi gọi chương trình này: - -``` -John Doe -``` - -Hàm thực hiện như sau: - -* Lấy một `first_name` và `last_name`. -* Chuyển đổi kí tự đầu tiên của mỗi biến sang kiểu chữ hoa với `title()`. -* Nối chúng lại với nhau bằng một kí tự trắng ở giữa. - -{* ../../docs_src/python_types/tutorial001.py hl[2] *} - - -### Sửa đổi - -Nó là một chương trình rất đơn giản. - -Nhưng bây giờ hình dung rằng bạn đang viết nó từ đầu. - -Tại một vài thời điểm, bạn sẽ bắt đầu định nghĩa hàm, bạn có các tham số... - -Nhưng sau đó bạn phải gọi "phương thức chuyển đổi kí tự đầu tiên sang kiểu chữ hoa". - -Có phải là `upper`? Có phải là `uppercase`? `first_uppercase`? `capitalize`? - -Sau đó, bạn thử hỏi người bạn cũ của mình, autocompletion của trình soạn thảo. - -Bạn gõ tham số đầu tiên của hàm, `first_name`, sau đó một dấu chấm (`.`) và sau đó ấn `Ctrl+Space` để kích hoạt bộ hoàn thành. - -Nhưng đáng buồn, bạn không nhận được điều gì hữu ích cả: - - - -### Thêm kiểu dữ liệu - -Hãy sửa một dòng từ phiên bản trước. - -Chúng ta sẽ thay đổi chính xác đoạn này, tham số của hàm, từ: - -```Python - first_name, last_name -``` - -sang: - -```Python - first_name: str, last_name: str -``` - -Chính là nó. - -Những thứ đó là "type hints": - -{* ../../docs_src/python_types/tutorial002.py hl[1] *} - - -Đó không giống như khai báo những giá trị mặc định giống như: - -```Python - first_name="john", last_name="doe" -``` - -Nó là một thứ khác. - -Chúng ta sử dụng dấu hai chấm (`:`), không phải dấu bằng (`=`). - -Và việc thêm gợi ý kiểu dữ liệu không làm thay đổi những gì xảy ra so với khi chưa thêm chúng. - -But now, imagine you are again in the middle of creating that function, but with type hints. - -Tại cùng một điểm, bạn thử kích hoạt autocomplete với `Ctrl+Space` và bạn thấy: - - - -Với cái đó, bạn có thể cuộn, nhìn thấy các lựa chọn, cho đến khi bạn tìm thấy một "tiếng chuông": - - - -## Động lực nhiều hơn - -Kiểm tra hàm này, nó đã có gợi ý kiểu dữ liệu: - -{* ../../docs_src/python_types/tutorial003.py hl[1] *} - - -Bởi vì trình soạn thảo biết kiểu dữ liệu của các biến, bạn không chỉ có được completion, bạn cũng được kiểm tra lỗi: - - - -Bây giờ bạn biết rằng bạn phải sửa nó, chuyển `age` sang một xâu với `str(age)`: - -{* ../../docs_src/python_types/tutorial004.py hl[2] *} - - -## Khai báo các kiểu dữ liệu - -Bạn mới chỉ nhìn thấy những nơi chủ yếu để đặt khai báo kiểu dữ liệu. Như là các tham số của hàm. - -Đây cũng là nơi chủ yếu để bạn sử dụng chúng với **FastAPI**. - -### Kiểu dữ liệu đơn giản - -Bạn có thể khai báo tất cả các kiểu dữ liệu chuẩn của Python, không chỉ là `str`. - -Bạn có thể sử dụng, ví dụ: - -* `int` -* `float` -* `bool` -* `bytes` - -{* ../../docs_src/python_types/tutorial005.py hl[1] *} - - -### Các kiểu dữ liệu tổng quát với tham số kiểu dữ liệu - -Có một vài cấu trúc dữ liệu có thể chứa các giá trị khác nhau như `dict`, `list`, `set` và `tuple`. Và những giá trị nội tại cũng có thể có kiểu dữ liệu của chúng. - -Những kiểu dữ liệu nội bộ này được gọi là những kiểu dữ liệu "**tổng quát**". Và có khả năng khai báo chúng, thậm chí với các kiểu dữ liệu nội bộ của chúng. - -Để khai báo những kiểu dữ liệu và những kiểu dữ liệu nội bộ đó, bạn có thể sử dụng mô đun chuẩn của Python là `typing`. Nó có hỗ trợ những gợi ý kiểu dữ liệu này. - -#### Những phiên bản mới hơn của Python - -Cú pháp sử dụng `typing` **tương thích** với tất cả các phiên bản, từ Python 3.6 tới những phiên bản cuối cùng, bao gồm Python 3.9, Python 3.10,... - -As Python advances, **những phiên bản mới** mang tới sự hỗ trợ được cải tiến cho những chú thích kiểu dữ liệu và trong nhiều trường hợp bạn thậm chí sẽ không cần import và sử dụng mô đun `typing` để khai báo chú thích kiểu dữ liệu. - -Nếu bạn có thể chọn một phiên bản Python gần đây hơn cho dự án của bạn, ban sẽ có được những ưu điểm của những cải tiến đơn giản đó. - -Trong tất cả các tài liệu tồn tại những ví dụ tương thích với mỗi phiên bản Python (khi có một sự khác nhau). - -Cho ví dụ "**Python 3.6+**" có nghĩa là nó tương thích với Python 3.7 hoặc lớn hơn (bao gồm 3.7, 3.8, 3.9, 3.10,...). và "**Python 3.9+**" nghĩa là nó tương thích với Python 3.9 trở lên (bao gồm 3.10,...). - -Nếu bạn có thể sử dụng **phiên bản cuối cùng của Python**, sử dụng những ví dụ cho phiên bản cuối, những cái đó sẽ có **cú pháp đơn giản và tốt nhât**, ví dụ, "**Python 3.10+**". - -#### List - -Ví dụ, hãy định nghĩa một biến là `list` các `str`. - -//// tab | Python 3.9+ - -Khai báo biến với cùng dấu hai chấm (`:`). - -Tương tự kiểu dữ liệu `list`. - -Như danh sách là một kiểu dữ liệu chứa một vài kiểu dữ liệu có sẵn, bạn đặt chúng trong các dấu ngoặc vuông: - -```Python hl_lines="1" -{!> ../../docs_src/python_types/tutorial006_py39.py!} -``` - -//// - -//// tab | Python 3.8+ - -Từ `typing`, import `List` (với chữ cái `L` viết hoa): - -```Python hl_lines="1" -{!> ../../docs_src/python_types/tutorial006.py!} -``` - -Khai báo biến với cùng dấu hai chấm (`:`). - -Tương tự như kiểu dữ liệu, `List` bạn import từ `typing`. - -Như danh sách là một kiểu dữ liệu chứa các kiểu dữ liệu có sẵn, bạn đặt chúng bên trong dấu ngoặc vuông: - -```Python hl_lines="4" -{!> ../../docs_src/python_types/tutorial006.py!} -``` - -//// - -/// info - -Các kiểu dữ liệu có sẵn bên trong dấu ngoặc vuông được gọi là "tham số kiểu dữ liệu". - -Trong trường hợp này, `str` là tham số kiểu dữ liệu được truyền tới `List` (hoặc `list` trong Python 3.9 trở lên). - -/// - -Có nghĩa là: "biến `items` là một `list`, và mỗi phần tử trong danh sách này là một `str`". - -/// tip - -Nếu bạn sử dụng Python 3.9 hoặc lớn hơn, bạn không phải import `List` từ `typing`, bạn có thể sử dụng `list` để thay thế. - -/// - -Bằng cách này, trình soạn thảo của bạn có thể hỗ trợ trong khi xử lí các phần tử trong danh sách: - - - -Đa phần đều không thể đạt được nếu không có các kiểu dữ liệu. - -Chú ý rằng, biến `item` là một trong các phần tử trong danh sách `items`. - -Và do vậy, trình soạn thảo biết nó là một `str`, và cung cấp sự hỗ trợ cho nó. - -#### Tuple and Set - -Bạn sẽ làm điều tương tự để khai báo các `tuple` và các `set`: - -//// tab | Python 3.9+ - -```Python hl_lines="1" -{!> ../../docs_src/python_types/tutorial007_py39.py!} -``` - -//// - -//// tab | Python 3.8+ - -```Python hl_lines="1 4" -{!> ../../docs_src/python_types/tutorial007.py!} -``` - -//// - -Điều này có nghĩa là: - -* Biến `items_t` là một `tuple` với 3 phần tử, một `int`, một `int` nữa, và một `str`. -* Biến `items_s` là một `set`, và mỗi phần tử của nó có kiểu `bytes`. - -#### Dict - -Để định nghĩa một `dict`, bạn truyền 2 tham số kiểu dữ liệu, phân cách bởi dấu phẩy. - -Tham số kiểu dữ liệu đầu tiên dành cho khóa của `dict`. - -Tham số kiểu dữ liệu thứ hai dành cho giá trị của `dict`. - -//// tab | Python 3.9+ - -```Python hl_lines="1" -{!> ../../docs_src/python_types/tutorial008_py39.py!} -``` - -//// - -//// tab | Python 3.8+ - -```Python hl_lines="1 4" -{!> ../../docs_src/python_types/tutorial008.py!} -``` - -//// - -Điều này có nghĩa là: - -* Biến `prices` là một `dict`: - * Khóa của `dict` này là kiểu `str` (đó là tên của mỗi vật phẩm). - * Giá trị của `dict` này là kiểu `float` (đó là giá của mỗi vật phẩm). - -#### Union - -Bạn có thể khai báo rằng một biến có thể là **một vài kiểu dữ liệu" bất kì, ví dụ, một `int` hoặc một `str`. - -Trong Python 3.6 hoặc lớn hơn (bao gồm Python 3.10) bạn có thể sử dụng kiểu `Union` từ `typing` và đặt trong dấu ngoặc vuông những giá trị được chấp nhận. - -In Python 3.10 there's also a **new syntax** where you can put the possible types separated by a vertical bar (`|`). - -Trong Python 3.10 cũng có một **cú pháp mới** mà bạn có thể đặt những kiểu giá trị khả thi phân cách bởi một dấu sổ dọc (`|`). - - -//// tab | Python 3.10+ - -```Python hl_lines="1" -{!> ../../docs_src/python_types/tutorial008b_py310.py!} -``` - -//// - -//// tab | Python 3.8+ - -```Python hl_lines="1 4" -{!> ../../docs_src/python_types/tutorial008b.py!} -``` - -//// - -Trong cả hai trường hợp có nghĩa là `item` có thể là một `int` hoặc `str`. - -#### Khả năng `None` - -Bạn có thể khai báo một giá trị có thể có một kiểu dữ liệu, giống như `str`, nhưng nó cũng có thể là `None`. - -Trong Python 3.6 hoặc lớn hơn (bao gồm Python 3.10) bạn có thể khai báo nó bằng các import và sử dụng `Optional` từ mô đun `typing`. - -```Python hl_lines="1 4" -{!../../docs_src/python_types/tutorial009.py!} -``` - -Sử dụng `Optional[str]` thay cho `str` sẽ cho phép trình soạn thảo giúp bạn phát hiện các lỗi mà bạn có thể gặp như một giá trị luôn là một `str`, trong khi thực tế nó rất có thể là `None`. - -`Optional[Something]` là một cách viết ngắn gọn của `Union[Something, None]`, chúng là tương đương nhau. - -Điều này cũng có nghĩa là trong Python 3.10, bạn có thể sử dụng `Something | None`: - -//// tab | Python 3.10+ - -```Python hl_lines="1" -{!> ../../docs_src/python_types/tutorial009_py310.py!} -``` - -//// - -//// tab | Python 3.8+ - -```Python hl_lines="1 4" -{!> ../../docs_src/python_types/tutorial009.py!} -``` - -//// - -//// tab | Python 3.8+ alternative - -```Python hl_lines="1 4" -{!> ../../docs_src/python_types/tutorial009b.py!} -``` - -//// - -#### Sử dụng `Union` hay `Optional` - -If you are using a Python version below 3.10, here's a tip from my very **subjective** point of view: - -Nếu bạn đang sử dụng phiên bản Python dưới 3.10, đây là một mẹo từ ý kiến rất "chủ quan" của tôi: - -* 🚨 Tránh sử dụng `Optional[SomeType]` -* Thay vào đó ✨ **sử dụng `Union[SomeType, None]`** ✨. - -Cả hai là tương đương và bên dưới chúng giống nhau, nhưng tôi sẽ đễ xuất `Union` thay cho `Optional` vì từ "**tùy chọn**" có vẻ ngầm định giá trị là tùy chọn, và nó thực sự có nghĩa rằng "nó có thể là `None`", do đó nó không phải là tùy chọn và nó vẫn được yêu cầu. - -Tôi nghĩ `Union[SomeType, None]` là rõ ràng hơn về ý nghĩa của nó. - -Nó chỉ là về các từ và tên. Nhưng những từ đó có thể ảnh hưởng cách bạn và những đồng đội của bạn suy nghĩ về code. - -Cho một ví dụ, hãy để ý hàm này: - -{* ../../docs_src/python_types/tutorial009c.py hl[1,4] *} - - -Tham số `name` được định nghĩa là `Optional[str]`, nhưng nó **không phải là tùy chọn**, bạn không thể gọi hàm mà không có tham số: - -```Python -say_hi() # Oh, no, this throws an error! 😱 -``` - -Tham số `name` **vẫn được yêu cầu** (không phải là *tùy chọn*) vì nó không có giá trị mặc định. Trong khi đó, `name` chấp nhận `None` như là giá trị: - -```Python -say_hi(name=None) # This works, None is valid 🎉 -``` - -Tin tốt là, khi bạn sử dụng Python 3.10, bạn sẽ không phải lo lắng về điều đó, bạn sẽ có thể sử dụng `|` để định nghĩa hợp của các kiểu dữ liệu một cách đơn giản: - -{* ../../docs_src/python_types/tutorial009c_py310.py hl[1,4] *} - - -Và sau đó, bạn sẽ không phải lo rằng những cái tên như `Optional` và `Union`. 😎 - - -#### Những kiểu dữ liệu tổng quát - -Những kiểu dữ liệu này lấy tham số kiểu dữ liệu trong dấu ngoặc vuông được gọi là **Kiểu dữ liệu tổng quát**, cho ví dụ: - -//// tab | Python 3.10+ - -Bạn có thể sử dụng các kiểu dữ liệu có sẵn như là kiểu dữ liệu tổng quát (với ngoặc vuông và kiểu dữ liệu bên trong): - -* `list` -* `tuple` -* `set` -* `dict` - -Và tương tự với Python 3.6, từ mô đun `typing`: - -* `Union` -* `Optional` (tương tự như Python 3.6) -* ...và các kiểu dữ liệu khác. - -Trong Python 3.10, thay vì sử dụng `Union` và `Optional`, bạn có thể sử dụng sổ dọc ('|') để khai báo hợp của các kiểu dữ liệu, điều đó tốt hơn và đơn giản hơn nhiều. - -//// - -//// tab | Python 3.9+ - -Bạn có thể sử dụng các kiểu dữ liệu có sẵn tương tự như (với ngoặc vuông và kiểu dữ liệu bên trong): - -* `list` -* `tuple` -* `set` -* `dict` - -Và tương tự với Python 3.6, từ mô đun `typing`: - -* `Union` -* `Optional` -* ...and others. - -//// - -//// tab | Python 3.8+ - -* `List` -* `Tuple` -* `Set` -* `Dict` -* `Union` -* `Optional` -* ...và các kiểu khác. - -//// - -### Lớp như kiểu dữ liệu - -Bạn cũng có thể khai báo một lớp như là kiểu dữ liệu của một biến. - -Hãy nói rằng bạn muốn có một lớp `Person` với một tên: - -{* ../../docs_src/python_types/tutorial010.py hl[1:3] *} - - -Sau đó bạn có thể khai báo một biến có kiểu là `Person`: - -{* ../../docs_src/python_types/tutorial010.py hl[6] *} - - -Và lại một lần nữa, bạn có được tất cả sự hỗ trợ từ trình soạn thảo: - - - -Lưu ý rằng, điều này có nghĩa rằng "`one_person`" là một **thực thể** của lớp `Person`. - -Nó không có nghĩa "`one_person`" là một **lớp** gọi là `Person`. - -## Pydantic models - -Pydantic là một thư viện Python để validate dữ liệu hiệu năng cao. - -Bạn có thể khai báo "hình dạng" của dữa liệu như là các lớp với các thuộc tính. - -Và mỗi thuộc tính có một kiểu dữ liệu. - -Sau đó bạn tạo một thực thể của lớp đó với một vài giá trị và nó sẽ validate các giá trị, chuyển đổi chúng sang kiểu dữ liệu phù hợp (nếu đó là trường hợp) và cho bạn một object với toàn bộ dữ liệu. - -Và bạn nhận được tất cả sự hỗ trợ của trình soạn thảo với object kết quả đó. - -Một ví dụ từ tài liệu chính thức của Pydantic: - -//// tab | Python 3.10+ - -```Python -{!> ../../docs_src/python_types/tutorial011_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!> ../../docs_src/python_types/tutorial011_py39.py!} -``` - -//// - -//// tab | Python 3.8+ - -```Python -{!> ../../docs_src/python_types/tutorial011.py!} -``` - -//// - -/// info - -Để học nhiều hơn về Pydantic, tham khảo tài liệu của nó. - -/// - -**FastAPI** được dựa hoàn toàn trên Pydantic. - -Bạn sẽ thấy nhiều ví dụ thực tế hơn trong [Hướng dẫn sử dụng](tutorial/index.md){.internal-link target=_blank}. - -/// tip - -Pydantic có một hành vi đặc biệt khi bạn sử dụng `Optional` hoặc `Union[Something, None]` mà không có giá trị mặc dịnh, bạn có thể đọc nhiều hơn về nó trong tài liệu của Pydantic về Required Optional fields. - -/// - -## Type Hints với Metadata Annotations - -Python cũng có một tính năng cho phép đặt **metadata bổ sung** trong những gợi ý kiểu dữ liệu này bằng cách sử dụng `Annotated`. - -//// tab | Python 3.9+ - -Trong Python 3.9, `Annotated` là một phần của thư viện chuẩn, do đó bạn có thể import nó từ `typing`. - -```Python hl_lines="1 4" -{!> ../../docs_src/python_types/tutorial013_py39.py!} -``` - -//// - -//// tab | Python 3.8+ - -Ở phiên bản dưới Python 3.9, bạn import `Annotated` từ `typing_extensions`. - -Nó đã được cài đặt sẵng cùng với **FastAPI**. - -```Python hl_lines="1 4" -{!> ../../docs_src/python_types/tutorial013.py!} -``` - -//// - -Python bản thân nó không làm bất kì điều gì với `Annotated`. Với các trình soạn thảo và các công cụ khác, kiểu dữ liệu vẫn là `str`. - -Nhưng bạn có thể sử dụng `Annotated` để cung cấp cho **FastAPI** metadata bổ sung về cách mà bạn muốn ứng dụng của bạn xử lí. - -Điều quan trọng cần nhớ là ***tham số kiểu dữ liệu* đầu tiên** bạn truyền tới `Annotated` là **kiểu giá trị thực sự**. Phần còn lại chỉ là metadata cho các công cụ khác. - -Bây giờ, bạn chỉ cần biết rằng `Annotated` tồn tại, và nó là tiêu chuẩn của Python. 😎 - - -Sau đó, bạn sẽ thấy sự **mạnh mẽ** mà nó có thể làm. - -/// tip - -Thực tế, cái này là **tiêu chuẩn của Python**, nghĩa là bạn vẫn sẽ có được **trải nghiệm phát triển tốt nhất có thể** với trình soạn thảo của bạn, với các công cụ bạn sử dụng để phân tích và tái cấu trúc code của bạn, etc. ✨ - -Và code của bạn sẽ tương thích với nhiều công cụ và thư viện khác của Python. 🚀 - -/// - -## Các gợi ý kiểu dữ liệu trong **FastAPI** - -**FastAPI** lấy các ưu điểm của các gợi ý kiểu dữ liệu để thực hiện một số thứ. - -Với **FastAPI**, bạn khai báo các tham số với gợi ý kiểu và bạn có được: - -* **Sự hỗ trợ từ các trình soạn thảo**. -* **Kiểm tra kiểu dữ liệu (type checking)**. - -...và **FastAPI** sử dụng các khia báo để: - -* **Định nghĩa các yêu cầu**: từ tham số đường dẫn của request, tham số query, headers, bodies, các phụ thuộc (dependencies),... -* **Chuyển dổi dữ liệu*: từ request sang kiểu dữ liệu được yêu cầu. -* **Kiểm tra tính đúng đắn của dữ liệu**: tới từ mỗi request: - * Sinh **lỗi tự động** để trả về máy khác khi dữ liệu không hợp lệ. -* **Tài liệu hóa** API sử dụng OpenAPI: - * cái mà sau được được sử dụng bởi tài liệu tương tác người dùng. - -Điều này có thể nghe trừu tượng. Đừng lo lắng. Bạn sẽ thấy tất cả chúng trong [Hướng dẫn sử dụng](tutorial/index.md){.internal-link target=_blank}. - -Điều quan trọng là bằng việc sử dụng các kiểu dữ liệu chuẩn của Python (thay vì thêm các lớp, decorators,...), **FastAPI** sẽ thực hiện nhiều công việc cho bạn. - -/// info - -Nếu bạn đã đi qua toàn bộ các hướng dẫn và quay trở lại để tìm hiểu nhiều hơn về các kiểu dữ liệu, một tài nguyên tốt như "cheat sheet" từ `mypy`. - -/// diff --git a/docs/vi/docs/tutorial/first-steps.md b/docs/vi/docs/tutorial/first-steps.md deleted file mode 100644 index d1650539c1..0000000000 --- a/docs/vi/docs/tutorial/first-steps.md +++ /dev/null @@ -1,335 +0,0 @@ -# Những bước đầu tiên - -Tệp tin FastAPI đơn giản nhất có thể trông như này: - -{* ../../docs_src/first_steps/tutorial001.py *} - -Sao chép sang một tệp tin `main.py`. - -Chạy live server: - -
- -```console -$ uvicorn main:app --reload - -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -INFO: Started reloader process [28720] -INFO: Started server process [28722] -INFO: Waiting for application startup. -INFO: Application startup complete. -``` - -
- -/// note - -Câu lệnh `uvicorn main:app` được giải thích như sau: - -* `main`: tệp tin `main.py` (một Python "mô đun"). -* `app`: một object được tạo ra bên trong `main.py` với dòng `app = FastAPI()`. -* `--reload`: làm server khởi động lại sau mỗi lần thay đổi. Chỉ sử dụng trong môi trường phát triển. - -/// - -Trong output, có một dòng giống như: - -```hl_lines="4" -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -``` - -Dòng đó cho thấy URL, nơi mà app của bạn đang được chạy, trong máy local của bạn. - -### Kiểm tra - -Mở trình duyệt của bạn tại http://127.0.0.1:8000. - -Bạn sẽ thấy một JSON response như: - -```JSON -{"message": "Hello World"} -``` - -### Tài liệu tương tác API - -Bây giờ tới http://127.0.0.1:8000/docs. - -Bạn sẽ thấy một tài liệu tương tác API (cung cấp bởi Swagger UI): - -![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) - -### Phiên bản thay thế của tài liệu API - -Và bây giờ tới http://127.0.0.1:8000/redoc. - -Bạn sẽ thấy một bản thay thế của tài liệu (cung cấp bởi ReDoc): - -![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) - -### OpenAPI - -**FastAPI** sinh một "schema" với tất cả API của bạn sử dụng tiêu chuẩn **OpenAPI** cho định nghĩa các API. - -#### "Schema" - -Một "schema" là một định nghĩa hoặc mô tả thứ gì đó. Không phải code triển khai của nó, nhưng chỉ là một bản mô tả trừu tượng. - -#### API "schema" - -Trong trường hợp này, OpenAPI là một bản mô tả bắt buộc cơ chế định nghĩa API của bạn. - -Định nghĩa cấu trúc này bao gồm những đường dẫn API của bạn, các tham số có thể có,... - -#### "Cấu trúc" dữ liệu - -Thuật ngữ "cấu trúc" (schema) cũng có thể được coi như là hình dạng của dữ liệu, tương tự như một JSON content. - -Trong trường hợp đó, nó có nghĩa là các thuộc tính JSON và các kiểu dữ liệu họ có,... - -#### OpenAPI và JSON Schema - -OpenAPI định nghĩa một cấu trúc API cho API của bạn. Và cấu trúc đó bao gồm các dịnh nghĩa (or "schema") về dữ liệu được gửi đi và nhận về bởi API của bạn, sử dụng **JSON Schema**, một tiêu chuẩn cho cấu trúc dữ liệu JSON. - -#### Kiểm tra `openapi.json` - -Nếu bạn tò mò về việc cấu trúc OpenAPI nhìn như thế nào thì FastAPI tự động sinh một JSON (schema) với các mô tả cho tất cả API của bạn. - -Bạn có thể thấy nó trực tiếp tại: http://127.0.0.1:8000/openapi.json. - -Nó sẽ cho thấy một JSON bắt đầu giống như: - -```JSON -{ - "openapi": "3.1.0", - "info": { - "title": "FastAPI", - "version": "0.1.0" - }, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - - - -... -``` - -#### OpenAPI dùng để làm gì? - -Cấu trúc OpenAPI là sức mạnh của tài liệu tương tác. - -Và có hàng tá các bản thay thế, tất cả đều dựa trên OpenAPI. Bạn có thể dễ dàng thêm bất kì bản thay thế bào cho ứng dụng của bạn được xây dựng với **FastAPI**. - -Bạn cũng có thể sử dụng nó để sinh code tự động, với các client giao viết qua API của bạn. Ví dụ, frontend, mobile hoặc các ứng dụng IoT. - -## Tóm lại, từng bước một - -### Bước 1: import `FastAPI` - -{* ../../docs_src/first_steps/tutorial001.py hl[1] *} - -`FastAPI` là một Python class cung cấp tất cả chức năng cho API của bạn. - -/// note | Chi tiết kĩ thuật - -`FastAPI` là một class kế thừa trực tiếp `Starlette`. - -Bạn cũng có thể sử dụng tất cả Starlette chức năng với `FastAPI`. - -/// - -### Bước 2: Tạo một `FastAPI` "instance" - -{* ../../docs_src/first_steps/tutorial001.py hl[3] *} - -Biến `app` này là một "instance" của class `FastAPI`. - -Đây sẽ là điểm cốt lõi để tạo ra tất cả API của bạn. - -`app` này chính là điều được nhắc tới bởi `uvicorn` trong câu lệnh: - -
- -```console -$ uvicorn main:app --reload - -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -``` - -
- -Nếu bạn tạo ứng dụng của bạn giống như: - -{* ../../docs_src/first_steps/tutorial002.py hl[3] *} - -Và đặt nó trong một tệp tin `main.py`, sau đó bạn sẽ gọi `uvicorn` giống như: - -
- -```console -$ uvicorn main:my_awesome_api --reload - -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -``` - -
- -### Bước 3: tạo một *đường dẫn toán tử* - -#### Đường dẫn - -"Đường dẫn" ở đây được nhắc tới là phần cuối cùng của URL bắt đầu từ `/`. - -Do đó, trong một URL nhìn giống như: - -``` -https://example.com/items/foo -``` - -...đường dẫn sẽ là: - -``` -/items/foo -``` - -/// info - -Một đường dẫn cũng là một cách gọi chung cho một "endpoint" hoặc một "route". - -/// - -Trong khi xây dựng một API, "đường dẫn" là các chính để phân tách "mối quan hệ" và "tài nguyên". - -#### Toán tử (Operation) - -"Toán tử" ở đây được nhắc tới là một trong các "phương thức" HTTP. - -Một trong những: - -* `POST` -* `GET` -* `PUT` -* `DELETE` - -...và một trong những cái còn lại: - -* `OPTIONS` -* `HEAD` -* `PATCH` -* `TRACE` - -Trong giao thức HTTP, bạn có thể giao tiếp trong mỗi đường dẫn sử dụng một (hoặc nhiều) trong các "phương thức này". - ---- - -Khi xây dựng các API, bạn thường sử dụng cụ thể các phương thức HTTP này để thực hiện một hành động cụ thể. - -Thông thường, bạn sử dụng - -* `POST`: để tạo dữ liệu. -* `GET`: để đọc dữ liệu. -* `PUT`: để cập nhật dữ liệu. -* `DELETE`: để xóa dữ liệu. - -Do đó, trong OpenAPI, mỗi phương thức HTTP được gọi là một "toán tử (operation)". - -Chúng ta cũng sẽ gọi chúng là "**các toán tử**". - -#### Định nghĩa moojt *decorator cho đường dẫn toán tử* - -{* ../../docs_src/first_steps/tutorial001.py hl[6] *} - -`@app.get("/")` nói **FastAPI** rằng hàm bên dưới có trách nhiệm xử lí request tới: - -* đường dẫn `/` -* sử dụng một toán tửget - -/// info | Thông tin về "`@decorator`" - -Cú pháp `@something` trong Python được gọi là một "decorator". - -Bạn đặt nó trên một hàm. Giống như một chiếc mũ xinh xắn (Tôi ddonas đó là lí do mà thuật ngữ này ra đời). - -Một "decorator" lấy một hàm bên dưới và thực hiện một vài thứ với nó. - -Trong trường hợp của chúng ta, decorator này nói **FastAPI** rằng hàm bên dưới ứng với **đường dẫn** `/` và một **toán tử** `get`. - -Nó là một "**decorator đường dẫn toán tử**". - -/// - -Bạn cũng có thể sử dụng với các toán tử khác: - -* `@app.post()` -* `@app.put()` -* `@app.delete()` - -Và nhiều hơn với các toán tử còn lại: - -* `@app.options()` -* `@app.head()` -* `@app.patch()` -* `@app.trace()` - -/// tip - -Bạn thoải mái sử dụng mỗi toán tử (phương thức HTTP) như bạn mơ ước. - -**FastAPI** không bắt buộc bất kì ý nghĩa cụ thể nào. - -Thông tin ở đây được biểu thị như là một chỉ dẫn, không phải là một yêu cầu bắt buộc. - -Ví dụ, khi sử dụng GraphQL bạn thông thường thực hiện tất cả các hành động chỉ bằng việc sử dụng các toán tử `POST`. - -/// - -### Step 4: Định nghĩa **hàm cho đường dẫn toán tử** - -Đây là "**hàm cho đường dẫn toán tử**": - -* **đường dẫn**: là `/`. -* **toán tử**: là `get`. -* **hàm**: là hàm bên dưới "decorator" (bên dưới `@app.get("/")`). - -{* ../../docs_src/first_steps/tutorial001.py hl[7] *} - -Đây là một hàm Python. - -Nó sẽ được gọi bởi **FastAPI** bất cứ khi nào nó nhận một request tới URL "`/`" sử dụng một toán tử `GET`. - -Trong trường hợp này, nó là một hàm `async`. - ---- - -Bạn cũng có thể định nghĩa nó như là một hàm thông thường thay cho `async def`: - -{* ../../docs_src/first_steps/tutorial003.py hl[7] *} - -/// note - -Nếu bạn không biết sự khác nhau, kiểm tra [Async: *"Trong khi vội vàng?"*](../async.md#in-a-hurry){.internal-link target=_blank}. - -/// - -### Bước 5: Nội dung trả về - -{* ../../docs_src/first_steps/tutorial001.py hl[8] *} - -Bạn có thể trả về một `dict`, `list`, một trong những giá trị đơn như `str`, `int`,... - -Bạn cũng có thể trả về Pydantic model (bạn sẽ thấy nhiều hơn về nó sau). - -Có nhiều object và model khác nhau sẽ được tự động chuyển đổi sang JSON (bao gồm cả ORM,...). Thử sử dụng loại ưa thích của bạn, nó có khả năng cao đã được hỗ trợ. - -## Tóm lại - -* Import `FastAPI`. -* Tạo một `app` instance. -* Viết một **decorator cho đường dẫn toán tử** (giống như `@app.get("/")`). -* Viết một **hàm cho đường dẫn toán tử** (giống như `def root(): ...` ở trên). -* Chạy server trong môi trường phát triển (giống như `uvicorn main:app --reload`). diff --git a/docs/vi/docs/tutorial/index.md b/docs/vi/docs/tutorial/index.md deleted file mode 100644 index dfeeed8c56..0000000000 --- a/docs/vi/docs/tutorial/index.md +++ /dev/null @@ -1,83 +0,0 @@ -# Hướng dẫn sử dụng - -Hướng dẫn này cho bạn thấy từng bước cách sử dụng **FastAPI** đa số các tính năng của nó. - -Mỗi phần được xây dựng từ những phần trước đó, nhưng nó được cấu trúc thành các chủ đề riêng biệt, do đó bạn có thể xem trực tiếp từng phần cụ thể bất kì để giải quyết những API cụ thể mà bạn cần. - -Nó cũng được xây dựng để làm việc như một tham chiếu trong tương lai. - -Do đó bạn có thể quay lại và tìm chính xác những gì bạn cần. - -## Chạy mã - -Tất cả các code block có thể được sao chép và sử dụng trực tiếp (chúng thực chất là các tệp tin Python đã được kiểm thử). - -Để chạy bất kì ví dụ nào, sao chép code tới tệp tin `main.py`, và bắt đầu `uvicorn` với: - -
- -```console -$ uvicorn main:app --reload - -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -INFO: Started reloader process [28720] -INFO: Started server process [28722] -INFO: Waiting for application startup. -INFO: Application startup complete. -``` - -
- -**Khuyến khích** bạn viết hoặc sao chép code, sửa và chạy nó ở local. - -Sử dụng nó trong trình soạn thảo của bạn thực sự cho bạn thấy những lợi ích của FastAPI, thấy được cách bạn viết code ít hơn, tất cả đều được type check, autocompletion,... - ---- - -## Cài đặt FastAPI - -Bước đầu tiên là cài đặt FastAPI. - -Với hướng dẫn này, bạn có thể muốn cài đặt nó với tất cả các phụ thuộc và tính năng tùy chọn: - -
- -```console -$ pip install "fastapi[all]" - ----> 100% -``` - -
- -...dó cũng bao gồm `uvicorn`, bạn có thể sử dụng như một server để chạy code của bạn. - -/// note - -Bạn cũng có thể cài đặt nó từng phần. - -Đây là những gì bạn có thể sẽ làm một lần duy nhất bạn muốn triển khai ứng dụng của bạn lên production: - -``` -pip install fastapi -``` - -Cũng cài đặt `uvicorn` để làm việc như một server: - -``` -pip install "uvicorn[standard]" -``` - -Và tương tự với từng phụ thuộc tùy chọn mà bạn muốn sử dụng. - -/// - -## Hướng dẫn nâng cao - -Cũng có một **Hướng dẫn nâng cao** mà bạn có thể đọc nó sau **Hướng dẫn sử dụng**. - -**Hướng dẫn sử dụng nâng cao**, xây dựng dựa trên cái này, sử dụng các khái niệm tương tự, và dạy bạn những tính năng mở rộng. - -Nhưng bạn nên đọc **Hướng dẫn sử dụng** đầu tiên (những gì bạn đang đọc). - -Nó được thiết kế do đó bạn có thể xây dựng một ứng dụng hoàn chỉnh chỉ với **Hướng dẫn sử dụng**, và sau đó mở rộng nó theo các cách khác nhau, phụ thuộc vào những gì bạn cần, sử dụng một vài ý tưởng bổ sung từ **Hướng dẫn sử dụng nâng cao**. diff --git a/docs/vi/docs/tutorial/static-files.md b/docs/vi/docs/tutorial/static-files.md deleted file mode 100644 index 1bbec29e7c..0000000000 --- a/docs/vi/docs/tutorial/static-files.md +++ /dev/null @@ -1,40 +0,0 @@ -# Tệp tĩnh (Static Files) - -Bạn có thể triển khai tệp tĩnh tự động từ một thư mục bằng cách sử dụng StaticFiles. - -## Sử dụng `Tệp tĩnh` - -- Nhập `StaticFiles`. -- "Mount" a `StaticFiles()` instance in a specific path. - -{* ../../docs_src/static_files/tutorial001.py hl[2,6] *} - -/// note | Chi tiết kỹ thuật - -Bạn cũng có thể sử dụng `from starlette.staticfiles import StaticFiles`. - -**FastAPI** cung cấp cùng `starlette.staticfiles` như `fastapi.staticfiles` giúp đơn giản hóa việc sử dụng, nhưng nó thực sự đến từ Starlette. - -/// - -### "Mounting" là gì - -"Mounting" có nghĩa là thêm một ứng dụng "độc lập" hoàn chỉnh vào một đường dẫn cụ thể, sau đó ứng dụng đó sẽ chịu trách nhiệm xử lý tất cả các đường dẫn con. - -Điều này khác với việc sử dụng `APIRouter` vì một ứng dụng được gắn kết là hoàn toàn độc lập. OpenAPI và tài liệu từ ứng dụng chính của bạn sẽ không bao gồm bất kỳ thứ gì từ ứng dụng được gắn kết, v.v. - -Bạn có thể đọc thêm về điều này trong [Hướng dẫn Người dùng Nâng cao](../advanced/index.md){.internal-link target=\_blank}. - -## Chi tiết - -Đường dẫn đầu tiên `"/static"` là đường dẫn con mà "ứng dụng con" này sẽ được "gắn" vào. Vì vậy, bất kỳ đường dẫn nào bắt đầu bằng `"/static"` sẽ được xử lý bởi nó. - -Đường dẫn `directory="static"` là tên của thư mục chứa tệp tĩnh của bạn. - -Tham số `name="static"` đặt tên cho nó để có thể được sử dụng bên trong **FastAPI**. - -Tất cả các tham số này có thể khác với `static`, điều chỉnh chúng với phù hợp với ứng dụng của bạn. - -## Thông tin thêm - -Để biết thêm chi tiết và tùy chọn, hãy xem Starlette's docs about Static Files. diff --git a/docs/vi/docs/virtual-environments.md b/docs/vi/docs/virtual-environments.md deleted file mode 100644 index 22d8e153e5..0000000000 --- a/docs/vi/docs/virtual-environments.md +++ /dev/null @@ -1,842 +0,0 @@ -# Môi trường ảo (Virtual Environments) - -Khi bạn làm việc trong các dự án Python, bạn có thể sử dụng một **môi trường ảo** (hoặc một cơ chế tương tự) để cách ly các gói bạn cài đặt cho mỗi dự án. - -/// info -Nếu bạn đã biết về các môi trường ảo, cách tạo chúng và sử dụng chúng, bạn có thể bỏ qua phần này. 🤓 - -/// - -/// tip - -Một **môi trường ảo** khác với một **biến môi trường (environment variable)**. - -Một **biến môi trường** là một biến trong hệ thống có thể được sử dụng bởi các chương trình. - -Một **môi trường ảo** là một thư mục với một số tệp trong đó. - -/// - -/// info - -Trang này sẽ hướng dẫn bạn cách sử dụng các **môi trường ảo** và cách chúng hoạt động. - -Nếu bạn đã sẵn sàng sử dụng một **công cụ có thể quản lý tất cả mọi thứ** cho bạn (bao gồm cả việc cài đặt Python), hãy thử uv. - -/// - -## Tạo một Dự án - -Đầu tiên, tạo một thư mục cho dự án của bạn. - -Cách tôi thường làm là tạo một thư mục có tên `code` trong thư mục `home/user`. - -Và trong thư mục đó, tôi tạo một thư mục cho mỗi dự án. - -
- -```console -// Đi đến thư mục home -$ cd -// Tạo một thư mục cho tất cả các dự án của bạn -$ mkdir code -// Vào thư mục code -$ cd code -// Tạo một thư mục cho dự án này -$ mkdir awesome-project -// Vào thư mục dự án -$ cd awesome-project -``` - -
- -## Tạo một Môi trường ảo - -Khi bạn bắt đầu làm việc với một dự án Python **trong lần đầu**, hãy tạo một môi trường ảo **trong thư mục dự án của bạn**. - -/// tip - -Bạn cần làm điều này **một lần cho mỗi dự án**, không phải mỗi khi bạn làm việc. -/// - -//// tab | `venv` - -Để tạo một môi trường ảo, bạn có thể sử dụng module `venv` có sẵn của Python. - -
- -```console -$ python -m venv .venv -``` - -
- -/// details | Cách các lệnh hoạt động - -* `python`: sử dụng chương trình `python` -* `-m`: gọi một module như một script, chúng ta sẽ nói về module đó sau -* `venv`: sử dụng module `venv` được cài đặt sẵn của Python -* `.venv`: tạo môi trường ảo trong thư mục mới `.venv` - -/// - -//// - -//// tab | `uv` - -Nếu bạn có `uv` được cài đặt, bạn có thể sử dụng nó để tạo một môi trường ảo. - -
- -```console -$ uv venv -``` - -
- -/// tip - -Mặc định, `uv` sẽ tạo một môi trường ảo trong một thư mục có tên `.venv`. - -Nhưng bạn có thể tùy chỉnh nó bằng cách thêm một đối số với tên thư mục. - -/// - -//// - -Lệnh này tạo một môi trường ảo mới trong một thư mục có tên `.venv`. - -/// details | `.venv` hoặc tên khác - -Bạn có thể tạo môi trường ảo trong một thư mục khác, nhưng thường người ta quy ước đặt nó là `.venv`. - -/// - -## Kích hoạt Môi trường ảo - -Kích hoạt môi trường ảo mới để bất kỳ lệnh Python nào bạn chạy hoặc gói nào bạn cài đặt sẽ sử dụng nó. - -/// tip - -Làm điều này **mỗi khi** bạn bắt đầu một **phiên terminal mới** để làm việc trên dự án. - -/// - -//// tab | Linux, macOS - -
- -```console -$ source .venv/bin/activate -``` - -
- -//// - -//// tab | Windows PowerShell - -
- -```console -$ .venv\Scripts\Activate.ps1 -``` - -
- -//// - -//// tab | Windows Bash - -Nếu bạn sử dụng Bash cho Windows (ví dụ: Git Bash): - -
- -```console -$ source .venv/Scripts/activate -``` - -
- -//// - -/// tip - -Mỗi khi bạn cài đặt thêm một **package mới** trong môi trường đó, hãy **kích hoạt** môi trường đó lại. - -Điều này đảm bảo rằng khi bạn sử dụng một **chương trình dòng lệnh (CLI)** được cài đặt từ gói đó, bạn sẽ dùng bản cài đặt từ môi trường ảo của mình thay vì bản được cài đặt toàn cục khác có thể có phiên bản khác với phiên bản bạn cần. - -/// - -## Kiểm tra xem Môi trường ảo đã được Kích hoạt chưa - -Kiểm tra xem môi trường ảo đã được kích hoạt chưa (lệnh trước đó đã hoạt động). - -/// tip - -Điều này là **không bắt buộc**, nhưng nó là một cách tốt để **kiểm tra** rằng mọi thứ đang hoạt động như mong đợi và bạn đang sử dụng đúng môi trường ảo mà bạn đã định. - -/// - -//// tab | Linux, macOS, Windows Bash - -
- -```console -$ which python - -/home/user/code/awesome-project/.venv/bin/python -``` - -
- -Nếu nó hiển thị `python` binary tại `.venv/bin/python`, trong dự án của bạn (trong trường hợp `awesome-project`), thì tức là nó hoạt động. 🎉 - -//// - -//// tab | Windows PowerShell - -
- -```console -$ Get-Command python - -C:\Users\user\code\awesome-project\.venv\Scripts\python -``` - -
- -Nếu nó hiển thị `python` binary tại `.venv\Scripts\python`, trong dự án của bạn (trong trường hợp `awesome-project`), thì tức là nó hoạt động. 🎉 - -//// - -## Nâng cấp `pip` - -/// tip - -Nếu bạn sử dụng `uv` bạn sử dụng nó để cài đặt thay vì `pip`, thì bạn không cần cập nhật `pip`. 😎 - -/// - -Nếu bạn sử dụng `pip` để cài đặt gói (nó được cài đặt mặc định với Python), bạn nên **nâng cấp** nó lên phiên bản mới nhất. - -Nhiều lỗi khác nhau trong khi cài đặt gói được giải quyết chỉ bằng cách nâng cấp `pip` trước. - -/// tip - -Bạn thường làm điều này **một lần**, ngay sau khi bạn tạo môi trường ảo. - -/// - -Đảm bảo rằng môi trường ảo đã được kích hoạt (với lệnh trên) và sau đó chạy: - -
- -```console -$ python -m pip install --upgrade pip - ----> 100% -``` - -
- -## Thêm `.gitignore` - -Nếu bạn sử dụng **Git** (nên làm), hãy thêm một file `.gitignore` để Git bỏ qua mọi thứ trong `.venv`. - -/// tip - -Nếu bạn sử dụng `uv` để tạo môi trường ảo, nó đã tự động làm điều này cho bạn, bạn có thể bỏ qua bước này. 😎 - -/// - -/// tip - -Làm điều này **một lần**, ngay sau khi bạn tạo môi trường ảo. - -/// - -
- -```console -$ echo "*" > .venv/.gitignore -``` - -
- -/// details | Cách lệnh hoạt động - -* `echo "*"`: sẽ "in" văn bản `*` trong terminal (phần tiếp theo sẽ thay đổi điều đó một chút) -* `>`: bất kỳ văn bản nào được in ra terminal bởi lệnh trước `>` không được in ra mà thay vào đó được viết vào file ở phía bên phải của `>` -* `.gitignore`: tên của file mà văn bản sẽ được viết vào - -Và `*` với Git có nghĩa là "mọi thứ". Vì vậy, nó sẽ bỏ qua mọi thứ trong thư mục `.venv`. - -Lệnh này sẽ tạo một file `.gitignore` với nội dung: - -```gitignore -* -``` - -/// - -## Cài đặt gói (packages) - -Sau khi kích hoạt môi trường, bạn có thể cài đặt các gói trong đó. - -/// tip - -Thực hiện điều này **một lần** khi cài đặt hoặc cập nhật gói cần thiết cho dự án của bạn. - -Nếu bạn cần cập nhật phiên bản hoặc thêm một gói mới, bạn sẽ **thực hiện điều này lại**. - -/// - -### Cài đặt gói trực tiếp - -Nếu bạn cần cập nhật phiên bản hoặc thêm một gói mới, bạn sẽ **thực hiện điều này lại**. - -/// tip -Để quản lý dự án tốt hơn, hãy liệt kê tất cả các gói và phiên bản cần thiết trong một file (ví dụ `requirements.txt` hoặc `pyproject.toml`). - -/// - -//// tab | `pip` - -
- -```console -$ pip install "fastapi[standard]" - ----> 100% -``` - -
- -//// - -//// tab | `uv` - -Nếu bạn có `uv`: - -
- -```console -$ uv pip install "fastapi[standard]" ----> 100% -``` - -
- -//// - -### Cài đặt từ `requirements.txt` - -Nếu bạn có một tệp `requirements.txt`, bạn có thể sử dụng nó để cài đặt các gói. - -//// tab | `pip` - -
- -```console -$ pip install -r requirements.txt ----> 100% -``` - -
- -//// - -//// tab | `uv` - -Nếu bạn có `uv`: - -
- -```console -$ uv pip install -r requirements.txt ----> 100% -``` - -
- -//// - -/// details | `requirements.txt` - -Một tệp `requirements.txt` với một số gói sẽ trông như thế này: - -```requirements.txt -fastapi[standard]==0.113.0 -pydantic==2.8.0 -``` - -/// - -## Chạy Chương trình của bạn - -Sau khi kích hoạt môi trường ảo, bạn có thể chạy chương trình của mình, nó sẽ sử dụng Python trong môi trường ảo của bạn với các gói bạn đã cài đặt. - -
- -```console -$ python main.py - -Hello World -``` - -
- -## Cấu hình Trình soạn thảo của bạn - -Nếu bạn sử dụng một trình soạn thảo, hãy đảm bảo bạn cấu hình nó để sử dụng cùng môi trường ảo mà bạn đã tạo (trình soạn thảo sẽ tự động phát hiện môi trường ảo) để bạn có thể nhận được tính năng tự động hoàn thành câu lệnh (autocomplete) và in lỗi trực tiếp trong trình soạn thảo (inline errors). - -Ví dụ: - -* VS Code -* PyCharm - -/// tip - -Bạn thường chỉ cần làm điều này **một lần**, khi bạn tạo môi trường ảo. - -/// - -## Huỷ kích hoạt Môi trường ảo - -Khi bạn hoàn tất việc làm trên dự án của bạn, bạn có thể **huỷ kích hoạt** môi trường ảo. - -
- -```console -$ deactivate -``` - -
- -Như vậy, khi bạn chạy `python`, nó sẽ không chạy từ môi trường ảo đó với các gói đã cài đặt. - -## Sẵn sàng để Làm việc - -Bây giờ bạn đã sẵn sàng để làm việc trên dự án của mình rồi đấy. - -/// tip - -Bạn muốn hiểu tất cả những gì ở trên? - -Tiếp tục đọc. 👇🤓 - -/// - -## Tại sao cần Môi trường ảo - -Để làm việc với FastAPI, bạn cần cài đặt Python. - -Sau đó, bạn sẽ cần **cài đặt** FastAPI và bất kỳ **gói** nào mà bạn muốn sử dụng. - -Để cài đặt gói, bạn thường sử dụng lệnh `pip` có sẵn với Python (hoặc các phiên bản tương tự). - -Tuy nhiên, nếu bạn sử dụng `pip` trực tiếp, các gói sẽ được cài đặt trong **môi trường Python toàn cục** của bạn (phần cài đặt toàn cục của Python). - -### Vấn đề - -Vậy, vấn đề gì khi cài đặt gói trong môi trường Python toàn cục? - -Trong một vài thời điểm, bạn sẽ phải viết nhiều chương trình khác nhau phụ thuộc vào **các gói khác nhau**. Và một số dự án bạn thực hiện lại phụ thuộc vào **các phiên bản khác nhau** của cùng một gói. 😱 - -Ví dụ, bạn có thể tạo một dự án được gọi là `philosophers-stone`, chương trình này phụ thuộc vào một gói khác được gọi là **`harry`, sử dụng phiên bản `1`**. Vì vậy, bạn cần cài đặt `harry`. - -```mermaid -flowchart LR - stone(philosophers-stone) -->|phụ thuộc| harry-1[harry v1] -``` - -Sau đó, vào một vài thời điểm sau, bạn tạo một dự án khác được gọi là `prisoner-of-azkaban`, và dự án này cũng phụ thuộc vào `harry`, nhưng dự án này cần **`harry` phiên bản `3`**. - -```mermaid -flowchart LR - azkaban(prisoner-of-azkaban) --> |phụ thuộc| harry-3[harry v3] -``` - -Bây giờ, vấn đề là, nếu bạn cài đặt các gói toàn cục (trong môi trường toàn cục) thay vì trong một **môi trường ảo cục bộ**, bạn sẽ phải chọn phiên bản `harry` nào để cài đặt. - -Nếu bạn muốn chạy `philosophers-stone` bạn sẽ cần phải cài đặt `harry` phiên bản `1`, ví dụ với: - -
- -```console -$ pip install "harry==1" -``` - -
- -Và sau đó bạn sẽ có `harry` phiên bản `1` được cài đặt trong môi trường Python toàn cục của bạn. - -```mermaid -flowchart LR - subgraph global[môi trường toàn cục] - harry-1[harry v1] - end - subgraph stone-project[dự án philosophers-stone ] - stone(philosophers-stone) -->|phụ thuộc| harry-1 - end -``` - -Nhưng sau đó, nếu bạn muốn chạy `prisoner-of-azkaban`, bạn sẽ cần phải gỡ bỏ `harry` phiên bản `1` và cài đặt `harry` phiên bản `3` (hoặc chỉ cần cài đặt phiên bản `3` sẽ tự động gỡ bỏ phiên bản `1`). - -
- -```console -$ pip install "harry==3" -``` - -
- -Và sau đó bạn sẽ có `harry` phiên bản `3` được cài đặt trong môi trường Python toàn cục của bạn. - -Và nếu bạn cố gắng chạy `philosophers-stone` lại, có khả năng nó sẽ **không hoạt động** vì nó cần `harry` phiên bản `1`. - -```mermaid -flowchart LR - subgraph global[môi trường toàn cục] - harry-1[harry v1] - style harry-1 fill:#ccc,stroke-dasharray: 5 5 - harry-3[harry v3] - end - subgraph stone-project[dự án philosophers-stone ] - stone(philosophers-stone) -.-x|⛔️| harry-1 - end - subgraph azkaban-project[dự án prisoner-of-azkaban ] - azkaban(prisoner-of-azkaban) --> |phụ thuộc| harry-3 - end -``` - -/// tip - -Mặc dù các gói Python thường cố gắng **tránh các thay đổi làm hỏng code** trong **phiên bản mới**, nhưng để đảm bảo an toàn, bạn nên chủ động cài đặt phiên bản mới và chạy kiểm thử để xác nhận mọi thứ vẫn hoạt động đúng. - -/// - -Bây giờ, hãy hình dung về **nhiều** gói khác nhau mà tất cả các dự án của bạn phụ thuộc vào. Rõ ràng rất khó để quản lý. Điều này dẫn tới việc là bạn sẽ có nhiều dự án với **các phiên bản không tương thích** của các gói, và bạn có thể không biết tại sao một số thứ không hoạt động. - -Hơn nữa, tuỳ vào hệ điều hành của bạn (vd Linux, Windows, macOS), có thể đã có Python được cài đặt sẵn. Trong trường hợp ấy, một vài gói nhiều khả năng đã được cài đặt trước với các phiên bản **cần thiết cho hệ thống của bạn**. Nếu bạn cài đặt các gói trong môi trường Python toàn cục, bạn có thể sẽ **phá vỡ** một số chương trình đã được cài đặt sẵn cùng hệ thống. - -## Nơi các Gói được Cài đặt - -Khi bạn cài đặt Python, nó sẽ tạo ra một vài thư mục và tệp trong máy tính của bạn. - -Một vài thư mục này là những thư mục chịu trách nhiệm có tất cả các gói bạn cài đặt. - -Khi bạn chạy: - -
- -```console -// Đừng chạy lệnh này ngay, đây chỉ là một ví dụ 🤓 -$ pip install "fastapi[standard]" ----> 100% -``` - -
- -Lệnh này sẽ tải xuống một tệp nén với mã nguồn FastAPI, thường là từ PyPI. - -Nó cũng sẽ **tải xuống** các tệp cho các gói khác mà FastAPI phụ thuộc vào. - -Sau đó, nó sẽ **giải nén** tất cả các tệp đó và đưa chúng vào một thư mục trong máy tính của bạn. - -Mặc định, nó sẽ đưa các tệp đã tải xuống và giải nén vào thư mục được cài đặt cùng Python của bạn, đó là **môi trường toàn cục**. - -## Những Môi trường ảo là gì? - -Cách giải quyết cho vấn đề có tất cả các gói trong môi trường toàn cục là sử dụng một **môi trường ảo cho mỗi dự án** bạn làm việc. - -Một môi trường ảo là một **thư mục**, rất giống với môi trường toàn cục, trong đó bạn có thể cài đặt các gói cho một dự án. - -Vì vậy, mỗi dự án sẽ có một môi trường ảo riêng của nó (thư mục `.venv`) với các gói riêng của nó. - -```mermaid -flowchart TB - subgraph stone-project[dự án philosophers-stone ] - stone(philosophers-stone) --->|phụ thuộc| harry-1 - subgraph venv1[.venv] - harry-1[harry v1] - end - end - subgraph azkaban-project[dự án prisoner-of-azkaban ] - azkaban(prisoner-of-azkaban) --->|phụ thuộc| harry-3 - subgraph venv2[.venv] - harry-3[harry v3] - end - end - stone-project ~~~ azkaban-project -``` - -## Kích hoạt Môi trường ảo nghĩa là gì - -Khi bạn kích hoạt một môi trường ảo, ví dụ với: - -//// tab | Linux, macOS - -
- -```console -$ source .venv/bin/activate -``` - -
- -//// - -//// tab | Windows PowerShell - -
- -```console -$ .venv\Scripts\Activate.ps1 -``` - -
- -//// - -//// tab | Windows Bash - -Nếu bạn sử dụng Bash cho Windows (ví dụ Git Bash): - -
- -```console -$ source .venv/Scripts/activate -``` - -
- -//// - -Lệnh này sẽ tạo hoặc sửa đổi một số [biến môi trường](environment-variables.md){.internal-link target=_blank} mà sẽ được sử dụng cho các lệnh tiếp theo. - -Một trong số đó là biến `PATH`. - -/// tip - -Bạn có thể tìm hiểu thêm về biến `PATH` trong [Biến môi trường](environment-variables.md#path-environment-variable){.internal-link target=_blank} section. - -/// - -Kích hoạt môi trường ảo thêm đường dẫn `.venv/bin` (trên Linux và macOS) hoặc `.venv\Scripts` (trên Windows) vào biến `PATH`. - -Giả sử rằng trước khi kích hoạt môi trường, biến `PATH` như sau: - -//// tab | Linux, macOS - -```plaintext -/usr/bin:/bin:/usr/sbin:/sbin -``` - -Nghĩa là hệ thống sẽ tìm kiếm chương trình trong: - -* `/usr/bin` -* `/bin` -* `/usr/sbin` -* `/sbin` - -//// - -//// tab | Windows - -```plaintext -C:\Windows\System32 -``` - -Nghĩa là hệ thống sẽ tìm kiếm chương trình trong: - -* `C:\Windows\System32` - -//// - -Sau khi kích hoạt môi trường ảo, biến `PATH` sẽ như sau: - -//// tab | Linux, macOS - -```plaintext -/home/user/code/awesome-project/.venv/bin:/usr/bin:/bin:/usr/sbin:/sbin -``` - -Nghĩa là hệ thống sẽ bắt đầu tìm kiếm chương trình trong: - -```plaintext -/home/user/code/awesome-project/.venv/bin -``` - -trước khi tìm kiếm trong các thư mục khác. - -Vì vậy, khi bạn gõ `python` trong terminal, hệ thống sẽ tìm thấy chương trình Python trong: - -```plaintext -/home/user/code/awesome-project/.venv/bin/python -``` - -và sử dụng chương trình đó. - -//// - -//// tab | Windows - -```plaintext -C:\Users\user\code\awesome-project\.venv\Scripts;C:\Windows\System32 -``` - -Nghĩa là hệ thống sẽ bắt đầu tìm kiếm chương trình trong: - -```plaintext -C:\Users\user\code\awesome-project\.venv\Scripts -``` - -trước khi tìm kiếm trong các thư mục khác. - -Vì vậy, khi bạn gõ `python` trong terminal, hệ thống sẽ tìm thấy chương trình Python trong: - -```plaintext -C:\Users\user\code\awesome-project\.venv\Scripts\python -``` - -và sử dụng chương trình đó. - -//// - -Một chi tiết quan trọng là nó sẽ đưa địa chỉ của môi trường ảo vào **đầu** của biến `PATH`. Hệ thống sẽ tìm kiếm nó **trước** khi tìm kiếm bất kỳ Python nào khác có sẵn. Vì vậy, khi bạn chạy `python`, nó sẽ sử dụng Python **từ môi trường ảo** thay vì bất kỳ Python nào khác (ví dụ, Python từ môi trường toàn cục). - -Kích hoạt một môi trường ảo cũng thay đổi một vài thứ khác, nhưng đây là một trong những điều quan trọng nhất mà nó thực hiện. - -## Kiểm tra một Môi trường ảo - -Khi bạn kiểm tra một môi trường ảo đã được kích hoạt chưa, ví dụ với: - -//// tab | Linux, macOS, Windows Bash - -
- -```console -$ which python - -/home/user/code/awesome-project/.venv/bin/python -``` - -
- -//// - -//// tab | Windows PowerShell - -
- -```console -$ Get-Command python - -C:\Users\user\code\awesome-project\.venv\Scripts\python -``` - -
- -//// - - -Điều đó có nghĩa là chương trình `python` sẽ được sử dụng là chương trình **trong môi trường ảo**. - -Bạn sử dụng `which` trên Linux và macOS và `Get-Command` trên Windows PowerShell. - -Cách hoạt động của lệnh này là nó sẽ đi và kiểm tra biến `PATH`, đi qua **mỗi đường dẫn theo thứ tự**, tìm kiếm chương trình được gọi là `python`. Khi nó tìm thấy nó, nó sẽ **hiển thị cho bạn đường dẫn** đến chương trình đó. - -Điều quan trọng nhất là khi bạn gọi `python`, đó chính là chương trình `python` được thực thi. - -Vì vậy, bạn có thể xác nhận nếu bạn đang ở trong môi trường ảo đúng. - -/// tip - -Dễ dàng kích hoạt một môi trường ảo, cài đặt Python, và sau đó **chuyển đến một dự án khác**. - -Và dự án thứ hai **sẽ không hoạt động** vì bạn đang sử dụng **Python không đúng**, từ một môi trường ảo cho một dự án khác. - -Thật tiện lợi khi có thể kiểm tra `python` nào đang được sử dụng 🤓 - -/// - -## Tại sao lại Huỷ kích hoạt một Môi trường ảo - -Ví dụ, bạn có thể làm việc trên một dự án `philosophers-stone`, **kích hoạt môi trường ảo**, cài đặt các gói và làm việc với môi trường ảo đó. - -Sau đó, bạn muốn làm việc trên **dự án khác** `prisoner-of-azkaban`. - -Bạn đi đến dự án đó: - -
- -```console -$ cd ~/code/prisoner-of-azkaban -``` - -
- -Nếu bạn không tắt môi trường ảo cho `philosophers-stone`, khi bạn chạy `python` trong terminal, nó sẽ cố gắng sử dụng Python từ `philosophers-stone`. - -
- -```console -$ cd ~/code/prisoner-of-azkaban - -$ python main.py - -// Lỗi khi import sirius, nó không được cài đặt 😱 -Traceback (most recent call last): - File "main.py", line 1, in - import sirius -``` - -
- -Nếu bạn huỷ kích hoạt môi trường ảo hiện tại và kích hoạt môi trường ảo mới cho `prisoner-of-azkaban`, khi bạn chạy `python`, nó sẽ sử dụng Python từ môi trường ảo trong `prisoner-of-azkaban`. - -
- -```console -$ cd ~/code/prisoner-of-azkaban - -// Bạn không cần phải ở trong thư mục trước để huỷ kích hoạt, bạn có thể làm điều đó ở bất kỳ đâu, ngay cả sau khi đi đến dự án khác 😎 -$ deactivate - -// Kích hoạt môi trường ảo trong prisoner-of-azkaban/.venv 🚀 -$ source .venv/bin/activate - -// Bây giờ khi bạn chạy python, nó sẽ tìm thấy gói sirius được cài đặt trong môi trường ảo này ✨ -$ python main.py - -I solemnly swear 🐺 - -(Tôi long trọng thề 🐺 - câu này được lấy từ Harry Potter, chú thích của người dịch) -``` - -
- -## Các cách làm tương tự - -Đây là một hướng dẫn đơn giản để bạn có thể bắt đầu và hiểu cách mọi thứ hoạt động **bên trong**. - -Có nhiều **cách khác nhau** để quản lí các môi trường ảo, các gói phụ thuộc (requirements), và các dự án. - -Một khi bạn đã sẵn sàng và muốn sử dụng một công cụ để **quản lí cả dự án**, các gói phụ thuộc, các môi trường ảo, v.v. Tôi sẽ khuyên bạn nên thử uv. - -`uv` có thể làm nhiều thứ, chẳng hạn: - -* **Cài đặt Python** cho bạn, bao gồm nhiều phiên bản khác nhau -* Quản lí **các môi trường ảo** cho các dự án của bạn -* Cài đặt **các gói (packages)** -* Quản lí **các thành phần phụ thuộc và phiên bản** của các gói cho dự án của bạn -* Đảm bảo rằng bạn có một **tập hợp chính xác** các gói và phiên bản để cài đặt, bao gồm các thành phần phụ thuộc của chúng, để bạn có thể đảm bảo rằng bạn có thể chạy dự án của bạn trong sản xuất chính xác như trong máy tính của bạn trong khi phát triển, điều này được gọi là **locking** -* Và còn nhiều thứ khác nữa - -## Kết luận - -Nếu bạn đã đọc và hiểu hết những điều này, khá chắc là bây giờ bạn đã **biết nhiều hơn** về môi trường ảo so với kha khá lập trình viên khác đấy. 🤓 - -Những hiểu biết chi tiết này có thể sẽ hữu ích với bạn trong tương lai khi mà bạn cần gỡ lỗi một vài thứ phức tạp, và bạn đã có những hiểu biết về **ngọn ngành gốc rễ cách nó hoạt động**. 😎 diff --git a/docs/vi/mkdocs.yml b/docs/vi/mkdocs.yml deleted file mode 100644 index de18856f44..0000000000 --- a/docs/vi/mkdocs.yml +++ /dev/null @@ -1 +0,0 @@ -INHERIT: ../en/mkdocs.yml diff --git a/docs/zh-hant/llm-prompt.md b/docs/zh-hant/llm-prompt.md new file mode 100644 index 0000000000..d44709015c --- /dev/null +++ b/docs/zh-hant/llm-prompt.md @@ -0,0 +1,60 @@ +### Target language + +Translate to Traditional Chinese (繁體中文). + +Language code: zh-hant. + +### Grammar and tone + +- Use clear, concise technical Traditional Chinese consistent with existing docs. +- Address the reader naturally (commonly using “你/你的”). + +### Headings + +- Follow existing Traditional Chinese heading style (short and descriptive). +- Do not add trailing punctuation to headings. + +### Quotes and punctuation + +- Keep punctuation style consistent with existing Traditional Chinese docs (they often mix English terms like “FastAPI” with Chinese text). +- Never change punctuation inside inline code, code blocks, URLs, or file paths. +- For more details, please follow the [Chinese Copywriting Guidelines](https://github.com/sparanoid/chinese-copywriting-guidelines). + +### Ellipsis + +- Keep ellipsis style consistent within each document, prefer `...` over `……`. +- Never change ellipsis in code, URLs, or CLI examples. + +### Preferred translations / glossary + +- Should avoid using simplified Chinese characters and terms. Always examine if the translation can be easily comprehended by the Traditional Chinese readers. +- For some Python-specific terms like "pickle", "list", "dict" etc, we don't have to translate them. +- Use the following preferred translations when they apply in documentation prose: + +- request (HTTP): 請求 +- response (HTTP): 回應 +- path operation: 路徑操作 +- path operation function: 路徑操作函式 + +The translation can optionally include the original English text only in the first occurrence of each page (e.g. "路徑操作 (path operation)") if the translation is hard to be comprehended by most of the Chinese readers. + +### `///` admonitions + +1) Keep the admonition keyword in English (do not translate `note`, `tip`, etc.). +2) Many Traditional Chinese docs currently omit titles in `///` blocks; that is OK. +3) If a generic title is present, prefer these canonical titles: + +- `/// note | 注意` + +Notes: + +- `details` blocks exist; keep `/// details` as-is and translate only the title after `|`. +- Example canonical titles used in existing docs: + +``` +/// details | 上述指令的含義 +``` + +``` +/// details | 關於 `requirements.txt` +``` diff --git a/docs/zh/docs/advanced/dataclasses.md b/docs/zh/docs/advanced/dataclasses.md index c74ce65c3e..4e8e77d2ac 100644 --- a/docs/zh/docs/advanced/dataclasses.md +++ b/docs/zh/docs/advanced/dataclasses.md @@ -4,7 +4,7 @@ FastAPI 基于 **Pydantic** 构建,前文已经介绍过如何使用 Pydantic 但 FastAPI 还可以使用数据类(`dataclasses`): -{* ../../docs_src/dataclasses/tutorial001.py hl[1,7:12,19:20] *} +{* ../../docs_src/dataclasses_/tutorial001.py hl[1,7:12,19:20] *} 这还是借助于 **Pydantic** 及其内置的 `dataclasses`。 @@ -32,7 +32,7 @@ FastAPI 基于 **Pydantic** 构建,前文已经介绍过如何使用 Pydantic 在 `response_model` 参数中使用 `dataclasses`: -{* ../../docs_src/dataclasses/tutorial002.py hl[1,7:13,19] *} +{* ../../docs_src/dataclasses_/tutorial002.py hl[1,7:13,19] *} 本例把数据类自动转换为 Pydantic 数据类。 @@ -49,7 +49,7 @@ API 文档中也会显示相关概图: 本例把标准的 `dataclasses` 直接替换为 `pydantic.dataclasses`: ```{ .python .annotate hl_lines="1 5 8-11 14-17 23-25 28" } -{!../../docs_src/dataclasses/tutorial003.py!} +{!../../docs_src/dataclasses_/tutorial003.py!} ``` 1. 本例依然要从标准的 `dataclasses` 中导入 `field`; diff --git a/docs/zh/llm-prompt.md b/docs/zh/llm-prompt.md new file mode 100644 index 0000000000..7ce6f96a47 --- /dev/null +++ b/docs/zh/llm-prompt.md @@ -0,0 +1,46 @@ +### Target language + +Translate to Simplified Chinese (简体中文). + +Language code: zh. + +### Grammar and tone + +- Use clear, concise technical Chinese consistent with existing docs. +- Address the reader naturally (commonly using “你/你的”). + +### Headings + +- Follow existing Simplified Chinese heading style (short and descriptive). +- Do not add trailing punctuation to headings. +- If a heading contains only the name of a FastAPI feature, do not translate it. + +### Quotes and punctuation + +- Keep punctuation style consistent with existing Simplified Chinese docs (they often mix English terms like “FastAPI” with Chinese text). +- Never change punctuation inside inline code, code blocks, URLs, or file paths. + +### Ellipsis + +- Keep ellipsis style consistent within each document, prefer `...` over `……`. +- Never change ellipsis in code, URLs, or CLI examples. + +### Preferred translations / glossary + +Use the following preferred translations when they apply in documentation prose: + +- request (HTTP): 请求 +- response (HTTP): 响应 +- path operation: 路径操作 +- path operation function: 路径操作函数 + +### `///` admonitions + +- Keep the admonition keyword in English (do not translate `note`, `tip`, etc.). +- If a title is present, prefer these canonical titles: + +- `/// tip | 提示` +- `/// note | 注意` +- `/// warning | 警告` +- `/// info | 信息` +- `/// danger | 危险` diff --git a/docs_src/app_testing/app_b/__init__.py b/docs_src/additional_responses/__init__.py similarity index 100% rename from docs_src/app_testing/app_b/__init__.py rename to docs_src/additional_responses/__init__.py diff --git a/docs_src/additional_responses/tutorial001.py b/docs_src/additional_responses/tutorial001_py39.py similarity index 100% rename from docs_src/additional_responses/tutorial001.py rename to docs_src/additional_responses/tutorial001_py39.py diff --git a/docs_src/additional_responses/tutorial002_py310.py b/docs_src/additional_responses/tutorial002_py310.py new file mode 100644 index 0000000000..a94b740c91 --- /dev/null +++ b/docs_src/additional_responses/tutorial002_py310.py @@ -0,0 +1,28 @@ +from fastapi import FastAPI +from fastapi.responses import FileResponse +from pydantic import BaseModel + + +class Item(BaseModel): + id: str + value: str + + +app = FastAPI() + + +@app.get( + "/items/{item_id}", + response_model=Item, + responses={ + 200: { + "content": {"image/png": {}}, + "description": "Return the JSON item or an image.", + } + }, +) +async def read_item(item_id: str, img: bool | None = None): + if img: + return FileResponse("image.png", media_type="image/png") + else: + return {"id": "foo", "value": "there goes my hero"} diff --git a/docs_src/additional_responses/tutorial002.py b/docs_src/additional_responses/tutorial002_py39.py similarity index 100% rename from docs_src/additional_responses/tutorial002.py rename to docs_src/additional_responses/tutorial002_py39.py diff --git a/docs_src/additional_responses/tutorial003.py b/docs_src/additional_responses/tutorial003_py39.py similarity index 100% rename from docs_src/additional_responses/tutorial003.py rename to docs_src/additional_responses/tutorial003_py39.py diff --git a/docs_src/additional_responses/tutorial004_py310.py b/docs_src/additional_responses/tutorial004_py310.py new file mode 100644 index 0000000000..65cbef6346 --- /dev/null +++ b/docs_src/additional_responses/tutorial004_py310.py @@ -0,0 +1,30 @@ +from fastapi import FastAPI +from fastapi.responses import FileResponse +from pydantic import BaseModel + + +class Item(BaseModel): + id: str + value: str + + +responses = { + 404: {"description": "Item not found"}, + 302: {"description": "The item was moved"}, + 403: {"description": "Not enough privileges"}, +} + + +app = FastAPI() + + +@app.get( + "/items/{item_id}", + response_model=Item, + responses={**responses, 200: {"content": {"image/png": {}}}}, +) +async def read_item(item_id: str, img: bool | None = None): + if img: + return FileResponse("image.png", media_type="image/png") + else: + return {"id": "foo", "value": "there goes my hero"} diff --git a/docs_src/additional_responses/tutorial004.py b/docs_src/additional_responses/tutorial004_py39.py similarity index 100% rename from docs_src/additional_responses/tutorial004.py rename to docs_src/additional_responses/tutorial004_py39.py diff --git a/docs_src/app_testing/app_b_an/__init__.py b/docs_src/additional_status_codes/__init__.py similarity index 100% rename from docs_src/app_testing/app_b_an/__init__.py rename to docs_src/additional_status_codes/__init__.py diff --git a/docs_src/additional_status_codes/tutorial001_an.py b/docs_src/additional_status_codes/tutorial001_an.py deleted file mode 100644 index b5ad6a16b6..0000000000 --- a/docs_src/additional_status_codes/tutorial001_an.py +++ /dev/null @@ -1,26 +0,0 @@ -from typing import Union - -from fastapi import Body, FastAPI, status -from fastapi.responses import JSONResponse -from typing_extensions import Annotated - -app = FastAPI() - -items = {"foo": {"name": "Fighters", "size": 6}, "bar": {"name": "Tenders", "size": 3}} - - -@app.put("/items/{item_id}") -async def upsert_item( - item_id: str, - name: Annotated[Union[str, None], Body()] = None, - size: Annotated[Union[int, None], Body()] = None, -): - if item_id in items: - item = items[item_id] - item["name"] = name - item["size"] = size - return item - else: - item = {"name": name, "size": size} - items[item_id] = item - return JSONResponse(status_code=status.HTTP_201_CREATED, content=item) diff --git a/docs_src/additional_status_codes/tutorial001.py b/docs_src/additional_status_codes/tutorial001_py39.py similarity index 100% rename from docs_src/additional_status_codes/tutorial001.py rename to docs_src/additional_status_codes/tutorial001_py39.py diff --git a/docs_src/bigger_applications/app/__init__.py b/docs_src/advanced_middleware/__init__.py similarity index 100% rename from docs_src/bigger_applications/app/__init__.py rename to docs_src/advanced_middleware/__init__.py diff --git a/docs_src/advanced_middleware/tutorial001.py b/docs_src/advanced_middleware/tutorial001_py39.py similarity index 100% rename from docs_src/advanced_middleware/tutorial001.py rename to docs_src/advanced_middleware/tutorial001_py39.py diff --git a/docs_src/advanced_middleware/tutorial002.py b/docs_src/advanced_middleware/tutorial002_py39.py similarity index 100% rename from docs_src/advanced_middleware/tutorial002.py rename to docs_src/advanced_middleware/tutorial002_py39.py diff --git a/docs_src/advanced_middleware/tutorial003.py b/docs_src/advanced_middleware/tutorial003_py39.py similarity index 100% rename from docs_src/advanced_middleware/tutorial003.py rename to docs_src/advanced_middleware/tutorial003_py39.py diff --git a/docs_src/bigger_applications/app/internal/__init__.py b/docs_src/app_testing/app_a_py39/__init__.py similarity index 100% rename from docs_src/bigger_applications/app/internal/__init__.py rename to docs_src/app_testing/app_a_py39/__init__.py diff --git a/docs_src/app_testing/main.py b/docs_src/app_testing/app_a_py39/main.py similarity index 100% rename from docs_src/app_testing/main.py rename to docs_src/app_testing/app_a_py39/main.py diff --git a/docs_src/app_testing/test_main.py b/docs_src/app_testing/app_a_py39/test_main.py similarity index 100% rename from docs_src/app_testing/test_main.py rename to docs_src/app_testing/app_a_py39/test_main.py diff --git a/docs_src/app_testing/app_b_an/main.py b/docs_src/app_testing/app_b_an/main.py deleted file mode 100644 index c66278fdd0..0000000000 --- a/docs_src/app_testing/app_b_an/main.py +++ /dev/null @@ -1,39 +0,0 @@ -from typing import Union - -from fastapi import FastAPI, Header, HTTPException -from pydantic import BaseModel -from typing_extensions import Annotated - -fake_secret_token = "coneofsilence" - -fake_db = { - "foo": {"id": "foo", "title": "Foo", "description": "There goes my hero"}, - "bar": {"id": "bar", "title": "Bar", "description": "The bartenders"}, -} - -app = FastAPI() - - -class Item(BaseModel): - id: str - title: str - description: Union[str, None] = None - - -@app.get("/items/{item_id}", response_model=Item) -async def read_main(item_id: str, x_token: Annotated[str, Header()]): - if x_token != fake_secret_token: - raise HTTPException(status_code=400, detail="Invalid X-Token header") - if item_id not in fake_db: - raise HTTPException(status_code=404, detail="Item not found") - return fake_db[item_id] - - -@app.post("/items/", response_model=Item) -async def create_item(item: Item, x_token: Annotated[str, Header()]): - if x_token != fake_secret_token: - raise HTTPException(status_code=400, detail="Invalid X-Token header") - if item.id in fake_db: - raise HTTPException(status_code=409, detail="Item already exists") - fake_db[item.id] = item - return item diff --git a/docs_src/app_testing/app_b_an/test_main.py b/docs_src/app_testing/app_b_an/test_main.py deleted file mode 100644 index 4e1c51ecc8..0000000000 --- a/docs_src/app_testing/app_b_an/test_main.py +++ /dev/null @@ -1,65 +0,0 @@ -from fastapi.testclient import TestClient - -from .main import app - -client = TestClient(app) - - -def test_read_item(): - response = client.get("/items/foo", headers={"X-Token": "coneofsilence"}) - assert response.status_code == 200 - assert response.json() == { - "id": "foo", - "title": "Foo", - "description": "There goes my hero", - } - - -def test_read_item_bad_token(): - response = client.get("/items/foo", headers={"X-Token": "hailhydra"}) - assert response.status_code == 400 - assert response.json() == {"detail": "Invalid X-Token header"} - - -def test_read_nonexistent_item(): - response = client.get("/items/baz", headers={"X-Token": "coneofsilence"}) - assert response.status_code == 404 - assert response.json() == {"detail": "Item not found"} - - -def test_create_item(): - response = client.post( - "/items/", - headers={"X-Token": "coneofsilence"}, - json={"id": "foobar", "title": "Foo Bar", "description": "The Foo Barters"}, - ) - assert response.status_code == 200 - assert response.json() == { - "id": "foobar", - "title": "Foo Bar", - "description": "The Foo Barters", - } - - -def test_create_item_bad_token(): - response = client.post( - "/items/", - headers={"X-Token": "hailhydra"}, - json={"id": "bazz", "title": "Bazz", "description": "Drop the bazz"}, - ) - assert response.status_code == 400 - assert response.json() == {"detail": "Invalid X-Token header"} - - -def test_create_existing_item(): - response = client.post( - "/items/", - headers={"X-Token": "coneofsilence"}, - json={ - "id": "foo", - "title": "The Foo ID Stealers", - "description": "There goes my stealer", - }, - ) - assert response.status_code == 409 - assert response.json() == {"detail": "Item already exists"} diff --git a/docs_src/bigger_applications/app/routers/__init__.py b/docs_src/app_testing/app_b_py39/__init__.py similarity index 100% rename from docs_src/bigger_applications/app/routers/__init__.py rename to docs_src/app_testing/app_b_py39/__init__.py diff --git a/docs_src/app_testing/app_b/main.py b/docs_src/app_testing/app_b_py39/main.py similarity index 100% rename from docs_src/app_testing/app_b/main.py rename to docs_src/app_testing/app_b_py39/main.py diff --git a/docs_src/app_testing/app_b/test_main.py b/docs_src/app_testing/app_b_py39/test_main.py similarity index 100% rename from docs_src/app_testing/app_b/test_main.py rename to docs_src/app_testing/app_b_py39/test_main.py diff --git a/docs_src/app_testing/tutorial001.py b/docs_src/app_testing/tutorial001_py39.py similarity index 100% rename from docs_src/app_testing/tutorial001.py rename to docs_src/app_testing/tutorial001_py39.py diff --git a/docs_src/app_testing/tutorial002.py b/docs_src/app_testing/tutorial002_py39.py similarity index 100% rename from docs_src/app_testing/tutorial002.py rename to docs_src/app_testing/tutorial002_py39.py diff --git a/docs_src/app_testing/tutorial003.py b/docs_src/app_testing/tutorial003_py39.py similarity index 100% rename from docs_src/app_testing/tutorial003.py rename to docs_src/app_testing/tutorial003_py39.py diff --git a/docs_src/app_testing/tutorial004.py b/docs_src/app_testing/tutorial004_py39.py similarity index 100% rename from docs_src/app_testing/tutorial004.py rename to docs_src/app_testing/tutorial004_py39.py diff --git a/docs_src/bigger_applications/app_an/__init__.py b/docs_src/async_tests/app_a_py39/__init__.py similarity index 100% rename from docs_src/bigger_applications/app_an/__init__.py rename to docs_src/async_tests/app_a_py39/__init__.py diff --git a/docs_src/async_tests/main.py b/docs_src/async_tests/app_a_py39/main.py similarity index 100% rename from docs_src/async_tests/main.py rename to docs_src/async_tests/app_a_py39/main.py diff --git a/docs_src/async_tests/test_main.py b/docs_src/async_tests/app_a_py39/test_main.py similarity index 100% rename from docs_src/async_tests/test_main.py rename to docs_src/async_tests/app_a_py39/test_main.py diff --git a/docs_src/bigger_applications/app_an/internal/__init__.py b/docs_src/authentication_error_status_code/__init__.py similarity index 100% rename from docs_src/bigger_applications/app_an/internal/__init__.py rename to docs_src/authentication_error_status_code/__init__.py diff --git a/docs_src/authentication_error_status_code/tutorial001_an_py39.py b/docs_src/authentication_error_status_code/tutorial001_an_py39.py new file mode 100644 index 0000000000..7bbc2f717d --- /dev/null +++ b/docs_src/authentication_error_status_code/tutorial001_an_py39.py @@ -0,0 +1,21 @@ +from typing import Annotated + +from fastapi import Depends, FastAPI, HTTPException, status +from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer + +app = FastAPI() + + +class HTTPBearer403(HTTPBearer): + def make_not_authenticated_error(self) -> HTTPException: + return HTTPException( + status_code=status.HTTP_403_FORBIDDEN, detail="Not authenticated" + ) + + +CredentialsDep = Annotated[HTTPAuthorizationCredentials, Depends(HTTPBearer403())] + + +@app.get("/me") +def read_me(credentials: CredentialsDep): + return {"message": "You are authenticated", "token": credentials.credentials} diff --git a/docs_src/bigger_applications/app_an/routers/__init__.py b/docs_src/background_tasks/__init__.py similarity index 100% rename from docs_src/bigger_applications/app_an/routers/__init__.py rename to docs_src/background_tasks/__init__.py diff --git a/docs_src/background_tasks/tutorial001.py b/docs_src/background_tasks/tutorial001_py39.py similarity index 100% rename from docs_src/background_tasks/tutorial001.py rename to docs_src/background_tasks/tutorial001_py39.py diff --git a/docs_src/background_tasks/tutorial002_an.py b/docs_src/background_tasks/tutorial002_an.py deleted file mode 100644 index f63502b09a..0000000000 --- a/docs_src/background_tasks/tutorial002_an.py +++ /dev/null @@ -1,27 +0,0 @@ -from typing import Union - -from fastapi import BackgroundTasks, Depends, FastAPI -from typing_extensions import Annotated - -app = FastAPI() - - -def write_log(message: str): - with open("log.txt", mode="a") as log: - log.write(message) - - -def get_query(background_tasks: BackgroundTasks, q: Union[str, None] = None): - if q: - message = f"found query: {q}\n" - background_tasks.add_task(write_log, message) - return q - - -@app.post("/send-notification/{email}") -async def send_notification( - email: str, background_tasks: BackgroundTasks, q: Annotated[str, Depends(get_query)] -): - message = f"message to {email}\n" - background_tasks.add_task(write_log, message) - return {"message": "Message sent"} diff --git a/docs_src/background_tasks/tutorial002.py b/docs_src/background_tasks/tutorial002_py39.py similarity index 100% rename from docs_src/background_tasks/tutorial002.py rename to docs_src/background_tasks/tutorial002_py39.py diff --git a/docs_src/settings/app01/__init__.py b/docs_src/behind_a_proxy/__init__.py similarity index 100% rename from docs_src/settings/app01/__init__.py rename to docs_src/behind_a_proxy/__init__.py diff --git a/docs_src/behind_a_proxy/tutorial001_01.py b/docs_src/behind_a_proxy/tutorial001_01_py39.py similarity index 100% rename from docs_src/behind_a_proxy/tutorial001_01.py rename to docs_src/behind_a_proxy/tutorial001_01_py39.py diff --git a/docs_src/behind_a_proxy/tutorial001.py b/docs_src/behind_a_proxy/tutorial001_py39.py similarity index 100% rename from docs_src/behind_a_proxy/tutorial001.py rename to docs_src/behind_a_proxy/tutorial001_py39.py diff --git a/docs_src/behind_a_proxy/tutorial002.py b/docs_src/behind_a_proxy/tutorial002_py39.py similarity index 100% rename from docs_src/behind_a_proxy/tutorial002.py rename to docs_src/behind_a_proxy/tutorial002_py39.py diff --git a/docs_src/behind_a_proxy/tutorial003.py b/docs_src/behind_a_proxy/tutorial003_py39.py similarity index 100% rename from docs_src/behind_a_proxy/tutorial003.py rename to docs_src/behind_a_proxy/tutorial003_py39.py diff --git a/docs_src/behind_a_proxy/tutorial004.py b/docs_src/behind_a_proxy/tutorial004_py39.py similarity index 100% rename from docs_src/behind_a_proxy/tutorial004.py rename to docs_src/behind_a_proxy/tutorial004_py39.py diff --git a/docs_src/bigger_applications/app_an/dependencies.py b/docs_src/bigger_applications/app_an/dependencies.py deleted file mode 100644 index 1374c54b31..0000000000 --- a/docs_src/bigger_applications/app_an/dependencies.py +++ /dev/null @@ -1,12 +0,0 @@ -from fastapi import Header, HTTPException -from typing_extensions import Annotated - - -async def get_token_header(x_token: Annotated[str, Header()]): - if x_token != "fake-super-secret-token": - raise HTTPException(status_code=400, detail="X-Token header invalid") - - -async def get_query_token(token: str): - if token != "jessica": - raise HTTPException(status_code=400, detail="No Jessica token provided") diff --git a/docs_src/bigger_applications/app_an/internal/admin.py b/docs_src/bigger_applications/app_an/internal/admin.py deleted file mode 100644 index 99d3da86b9..0000000000 --- a/docs_src/bigger_applications/app_an/internal/admin.py +++ /dev/null @@ -1,8 +0,0 @@ -from fastapi import APIRouter - -router = APIRouter() - - -@router.post("/") -async def update_admin(): - return {"message": "Admin getting schwifty"} diff --git a/docs_src/bigger_applications/app_an/main.py b/docs_src/bigger_applications/app_an/main.py deleted file mode 100644 index ae544a3aac..0000000000 --- a/docs_src/bigger_applications/app_an/main.py +++ /dev/null @@ -1,23 +0,0 @@ -from fastapi import Depends, FastAPI - -from .dependencies import get_query_token, get_token_header -from .internal import admin -from .routers import items, users - -app = FastAPI(dependencies=[Depends(get_query_token)]) - - -app.include_router(users.router) -app.include_router(items.router) -app.include_router( - admin.router, - prefix="/admin", - tags=["admin"], - dependencies=[Depends(get_token_header)], - responses={418: {"description": "I'm a teapot"}}, -) - - -@app.get("/") -async def root(): - return {"message": "Hello Bigger Applications!"} diff --git a/docs_src/bigger_applications/app_an/routers/items.py b/docs_src/bigger_applications/app_an/routers/items.py deleted file mode 100644 index bde9ff4d55..0000000000 --- a/docs_src/bigger_applications/app_an/routers/items.py +++ /dev/null @@ -1,38 +0,0 @@ -from fastapi import APIRouter, Depends, HTTPException - -from ..dependencies import get_token_header - -router = APIRouter( - prefix="/items", - tags=["items"], - dependencies=[Depends(get_token_header)], - responses={404: {"description": "Not found"}}, -) - - -fake_items_db = {"plumbus": {"name": "Plumbus"}, "gun": {"name": "Portal Gun"}} - - -@router.get("/") -async def read_items(): - return fake_items_db - - -@router.get("/{item_id}") -async def read_item(item_id: str): - if item_id not in fake_items_db: - raise HTTPException(status_code=404, detail="Item not found") - return {"name": fake_items_db[item_id]["name"], "item_id": item_id} - - -@router.put( - "/{item_id}", - tags=["custom"], - responses={403: {"description": "Operation forbidden"}}, -) -async def update_item(item_id: str): - if item_id != "plumbus": - raise HTTPException( - status_code=403, detail="You can only update the item: plumbus" - ) - return {"item_id": item_id, "name": "The great Plumbus"} diff --git a/docs_src/bigger_applications/app_an/routers/users.py b/docs_src/bigger_applications/app_an/routers/users.py deleted file mode 100644 index 39b3d7e7cf..0000000000 --- a/docs_src/bigger_applications/app_an/routers/users.py +++ /dev/null @@ -1,18 +0,0 @@ -from fastapi import APIRouter - -router = APIRouter() - - -@router.get("/users/", tags=["users"]) -async def read_users(): - return [{"username": "Rick"}, {"username": "Morty"}] - - -@router.get("/users/me", tags=["users"]) -async def read_user_me(): - return {"username": "fakecurrentuser"} - - -@router.get("/users/{username}", tags=["users"]) -async def read_user(username: str): - return {"username": username} diff --git a/docs_src/settings/app02/__init__.py b/docs_src/bigger_applications/app_py39/__init__.py similarity index 100% rename from docs_src/settings/app02/__init__.py rename to docs_src/bigger_applications/app_py39/__init__.py diff --git a/docs_src/bigger_applications/app/dependencies.py b/docs_src/bigger_applications/app_py39/dependencies.py similarity index 100% rename from docs_src/bigger_applications/app/dependencies.py rename to docs_src/bigger_applications/app_py39/dependencies.py diff --git a/docs_src/settings/app02_an/__init__.py b/docs_src/bigger_applications/app_py39/internal/__init__.py similarity index 100% rename from docs_src/settings/app02_an/__init__.py rename to docs_src/bigger_applications/app_py39/internal/__init__.py diff --git a/docs_src/bigger_applications/app/internal/admin.py b/docs_src/bigger_applications/app_py39/internal/admin.py similarity index 100% rename from docs_src/bigger_applications/app/internal/admin.py rename to docs_src/bigger_applications/app_py39/internal/admin.py diff --git a/docs_src/bigger_applications/app/main.py b/docs_src/bigger_applications/app_py39/main.py similarity index 100% rename from docs_src/bigger_applications/app/main.py rename to docs_src/bigger_applications/app_py39/main.py diff --git a/docs_src/settings/app03/__init__.py b/docs_src/bigger_applications/app_py39/routers/__init__.py similarity index 100% rename from docs_src/settings/app03/__init__.py rename to docs_src/bigger_applications/app_py39/routers/__init__.py diff --git a/docs_src/bigger_applications/app/routers/items.py b/docs_src/bigger_applications/app_py39/routers/items.py similarity index 100% rename from docs_src/bigger_applications/app/routers/items.py rename to docs_src/bigger_applications/app_py39/routers/items.py diff --git a/docs_src/bigger_applications/app/routers/users.py b/docs_src/bigger_applications/app_py39/routers/users.py similarity index 100% rename from docs_src/bigger_applications/app/routers/users.py rename to docs_src/bigger_applications/app_py39/routers/users.py diff --git a/docs_src/settings/app03_an/__init__.py b/docs_src/body/__init__.py similarity index 100% rename from docs_src/settings/app03_an/__init__.py rename to docs_src/body/__init__.py diff --git a/docs_src/body/tutorial001.py b/docs_src/body/tutorial001_py39.py similarity index 100% rename from docs_src/body/tutorial001.py rename to docs_src/body/tutorial001_py39.py diff --git a/docs_src/body/tutorial002_py310.py b/docs_src/body/tutorial002_py310.py index 454c45c886..a829a4dc9d 100644 --- a/docs_src/body/tutorial002_py310.py +++ b/docs_src/body/tutorial002_py310.py @@ -14,7 +14,7 @@ app = FastAPI() @app.post("/items/") async def create_item(item: Item): - item_dict = item.dict() + item_dict = item.model_dump() if item.tax is not None: price_with_tax = item.price + item.tax item_dict.update({"price_with_tax": price_with_tax}) diff --git a/docs_src/body/tutorial002.py b/docs_src/body/tutorial002_py39.py similarity index 92% rename from docs_src/body/tutorial002.py rename to docs_src/body/tutorial002_py39.py index 5cd86216b3..fb212e8e79 100644 --- a/docs_src/body/tutorial002.py +++ b/docs_src/body/tutorial002_py39.py @@ -16,7 +16,7 @@ app = FastAPI() @app.post("/items/") async def create_item(item: Item): - item_dict = item.dict() + item_dict = item.model_dump() if item.tax is not None: price_with_tax = item.price + item.tax item_dict.update({"price_with_tax": price_with_tax}) diff --git a/docs_src/body/tutorial003_py310.py b/docs_src/body/tutorial003_py310.py index 440b210e6b..51ac8aafac 100644 --- a/docs_src/body/tutorial003_py310.py +++ b/docs_src/body/tutorial003_py310.py @@ -14,4 +14,4 @@ app = FastAPI() @app.put("/items/{item_id}") async def update_item(item_id: int, item: Item): - return {"item_id": item_id, **item.dict()} + return {"item_id": item_id, **item.model_dump()} diff --git a/docs_src/body/tutorial003.py b/docs_src/body/tutorial003_py39.py similarity index 85% rename from docs_src/body/tutorial003.py rename to docs_src/body/tutorial003_py39.py index 2f33cc0389..636ba2275a 100644 --- a/docs_src/body/tutorial003.py +++ b/docs_src/body/tutorial003_py39.py @@ -16,4 +16,4 @@ app = FastAPI() @app.put("/items/{item_id}") async def update_item(item_id: int, item: Item): - return {"item_id": item_id, **item.dict()} + return {"item_id": item_id, **item.model_dump()} diff --git a/docs_src/body/tutorial004_py310.py b/docs_src/body/tutorial004_py310.py index b352b70ab6..53b10d97b6 100644 --- a/docs_src/body/tutorial004_py310.py +++ b/docs_src/body/tutorial004_py310.py @@ -14,7 +14,7 @@ app = FastAPI() @app.put("/items/{item_id}") async def update_item(item_id: int, item: Item, q: str | None = None): - result = {"item_id": item_id, **item.dict()} + result = {"item_id": item_id, **item.model_dump()} if q: result.update({"q": q}) return result diff --git a/docs_src/body/tutorial004.py b/docs_src/body/tutorial004_py39.py similarity index 87% rename from docs_src/body/tutorial004.py rename to docs_src/body/tutorial004_py39.py index 0671e0a278..2c157abfa1 100644 --- a/docs_src/body/tutorial004.py +++ b/docs_src/body/tutorial004_py39.py @@ -16,7 +16,7 @@ app = FastAPI() @app.put("/items/{item_id}") async def update_item(item_id: int, item: Item, q: Union[str, None] = None): - result = {"item_id": item_id, **item.dict()} + result = {"item_id": item_id, **item.model_dump()} if q: result.update({"q": q}) return result diff --git a/tests/test_filter_pydantic_sub_model/__init__.py b/docs_src/body_fields/__init__.py similarity index 100% rename from tests/test_filter_pydantic_sub_model/__init__.py rename to docs_src/body_fields/__init__.py diff --git a/docs_src/body_fields/tutorial001_an.py b/docs_src/body_fields/tutorial001_an.py deleted file mode 100644 index 15ea1b53dc..0000000000 --- a/docs_src/body_fields/tutorial001_an.py +++ /dev/null @@ -1,22 +0,0 @@ -from typing import Union - -from fastapi import Body, FastAPI -from pydantic import BaseModel, Field -from typing_extensions import Annotated - -app = FastAPI() - - -class Item(BaseModel): - name: str - description: Union[str, None] = Field( - default=None, title="The description of the item", max_length=300 - ) - price: float = Field(gt=0, description="The price must be greater than zero") - tax: Union[float, None] = None - - -@app.put("/items/{item_id}") -async def update_item(item_id: int, item: Annotated[Item, Body(embed=True)]): - results = {"item_id": item_id, "item": item} - return results diff --git a/docs_src/body_fields/tutorial001.py b/docs_src/body_fields/tutorial001_py39.py similarity index 100% rename from docs_src/body_fields/tutorial001.py rename to docs_src/body_fields/tutorial001_py39.py diff --git a/tests/test_pydantic_v1_v2_multifile/__init__.py b/docs_src/body_multiple_params/__init__.py similarity index 100% rename from tests/test_pydantic_v1_v2_multifile/__init__.py rename to docs_src/body_multiple_params/__init__.py diff --git a/docs_src/body_multiple_params/tutorial001_an.py b/docs_src/body_multiple_params/tutorial001_an.py deleted file mode 100644 index 308eee8544..0000000000 --- a/docs_src/body_multiple_params/tutorial001_an.py +++ /dev/null @@ -1,28 +0,0 @@ -from typing import Union - -from fastapi import FastAPI, Path -from pydantic import BaseModel -from typing_extensions import Annotated - -app = FastAPI() - - -class Item(BaseModel): - name: str - description: Union[str, None] = None - price: float - tax: Union[float, None] = None - - -@app.put("/items/{item_id}") -async def update_item( - item_id: Annotated[int, Path(title="The ID of the item to get", ge=0, le=1000)], - q: Union[str, None] = None, - item: Union[Item, None] = None, -): - results = {"item_id": item_id} - if q: - results.update({"q": q}) - if item: - results.update({"item": item}) - return results diff --git a/docs_src/body_multiple_params/tutorial001.py b/docs_src/body_multiple_params/tutorial001_py39.py similarity index 100% rename from docs_src/body_multiple_params/tutorial001.py rename to docs_src/body_multiple_params/tutorial001_py39.py diff --git a/docs_src/body_multiple_params/tutorial002.py b/docs_src/body_multiple_params/tutorial002_py39.py similarity index 100% rename from docs_src/body_multiple_params/tutorial002.py rename to docs_src/body_multiple_params/tutorial002_py39.py diff --git a/docs_src/body_multiple_params/tutorial003_an.py b/docs_src/body_multiple_params/tutorial003_an.py deleted file mode 100644 index 39ef7340a5..0000000000 --- a/docs_src/body_multiple_params/tutorial003_an.py +++ /dev/null @@ -1,27 +0,0 @@ -from typing import Union - -from fastapi import Body, FastAPI -from pydantic import BaseModel -from typing_extensions import Annotated - -app = FastAPI() - - -class Item(BaseModel): - name: str - description: Union[str, None] = None - price: float - tax: Union[float, None] = None - - -class User(BaseModel): - username: str - full_name: Union[str, None] = None - - -@app.put("/items/{item_id}") -async def update_item( - item_id: int, item: Item, user: User, importance: Annotated[int, Body()] -): - results = {"item_id": item_id, "item": item, "user": user, "importance": importance} - return results diff --git a/docs_src/body_multiple_params/tutorial003.py b/docs_src/body_multiple_params/tutorial003_py39.py similarity index 100% rename from docs_src/body_multiple_params/tutorial003.py rename to docs_src/body_multiple_params/tutorial003_py39.py diff --git a/docs_src/body_multiple_params/tutorial004_an.py b/docs_src/body_multiple_params/tutorial004_an.py deleted file mode 100644 index f6830f3920..0000000000 --- a/docs_src/body_multiple_params/tutorial004_an.py +++ /dev/null @@ -1,34 +0,0 @@ -from typing import Union - -from fastapi import Body, FastAPI -from pydantic import BaseModel -from typing_extensions import Annotated - -app = FastAPI() - - -class Item(BaseModel): - name: str - description: Union[str, None] = None - price: float - tax: Union[float, None] = None - - -class User(BaseModel): - username: str - full_name: Union[str, None] = None - - -@app.put("/items/{item_id}") -async def update_item( - *, - item_id: int, - item: Item, - user: User, - importance: Annotated[int, Body(gt=0)], - q: Union[str, None] = None, -): - results = {"item_id": item_id, "item": item, "user": user, "importance": importance} - if q: - results.update({"q": q}) - return results diff --git a/docs_src/body_multiple_params/tutorial004.py b/docs_src/body_multiple_params/tutorial004_py39.py similarity index 100% rename from docs_src/body_multiple_params/tutorial004.py rename to docs_src/body_multiple_params/tutorial004_py39.py diff --git a/docs_src/body_multiple_params/tutorial005_an.py b/docs_src/body_multiple_params/tutorial005_an.py deleted file mode 100644 index dadde80b55..0000000000 --- a/docs_src/body_multiple_params/tutorial005_an.py +++ /dev/null @@ -1,20 +0,0 @@ -from typing import Union - -from fastapi import Body, FastAPI -from pydantic import BaseModel -from typing_extensions import Annotated - -app = FastAPI() - - -class Item(BaseModel): - name: str - description: Union[str, None] = None - price: float - tax: Union[float, None] = None - - -@app.put("/items/{item_id}") -async def update_item(item_id: int, item: Annotated[Item, Body(embed=True)]): - results = {"item_id": item_id, "item": item} - return results diff --git a/docs_src/body_multiple_params/tutorial005.py b/docs_src/body_multiple_params/tutorial005_py39.py similarity index 100% rename from docs_src/body_multiple_params/tutorial005.py rename to docs_src/body_multiple_params/tutorial005_py39.py diff --git a/tests/test_tutorial/test_pydantic_v1_in_v2/__init__.py b/docs_src/body_nested_models/__init__.py similarity index 100% rename from tests/test_tutorial/test_pydantic_v1_in_v2/__init__.py rename to docs_src/body_nested_models/__init__.py diff --git a/docs_src/body_nested_models/tutorial001.py b/docs_src/body_nested_models/tutorial001_py39.py similarity index 100% rename from docs_src/body_nested_models/tutorial001.py rename to docs_src/body_nested_models/tutorial001_py39.py diff --git a/docs_src/body_nested_models/tutorial002.py b/docs_src/body_nested_models/tutorial002.py deleted file mode 100644 index 155cff7885..0000000000 --- a/docs_src/body_nested_models/tutorial002.py +++ /dev/null @@ -1,20 +0,0 @@ -from typing import List, Union - -from fastapi import FastAPI -from pydantic import BaseModel - -app = FastAPI() - - -class Item(BaseModel): - name: str - description: Union[str, None] = None - price: float - tax: Union[float, None] = None - tags: List[str] = [] - - -@app.put("/items/{item_id}") -async def update_item(item_id: int, item: Item): - results = {"item_id": item_id, "item": item} - return results diff --git a/docs_src/body_nested_models/tutorial003.py b/docs_src/body_nested_models/tutorial003.py deleted file mode 100644 index 84ed18bf48..0000000000 --- a/docs_src/body_nested_models/tutorial003.py +++ /dev/null @@ -1,20 +0,0 @@ -from typing import Set, Union - -from fastapi import FastAPI -from pydantic import BaseModel - -app = FastAPI() - - -class Item(BaseModel): - name: str - description: Union[str, None] = None - price: float - tax: Union[float, None] = None - tags: Set[str] = set() - - -@app.put("/items/{item_id}") -async def update_item(item_id: int, item: Item): - results = {"item_id": item_id, "item": item} - return results diff --git a/docs_src/body_nested_models/tutorial004.py b/docs_src/body_nested_models/tutorial004.py deleted file mode 100644 index a07bfacac1..0000000000 --- a/docs_src/body_nested_models/tutorial004.py +++ /dev/null @@ -1,26 +0,0 @@ -from typing import Set, Union - -from fastapi import FastAPI -from pydantic import BaseModel - -app = FastAPI() - - -class Image(BaseModel): - url: str - name: str - - -class Item(BaseModel): - name: str - description: Union[str, None] = None - price: float - tax: Union[float, None] = None - tags: Set[str] = set() - image: Union[Image, None] = None - - -@app.put("/items/{item_id}") -async def update_item(item_id: int, item: Item): - results = {"item_id": item_id, "item": item} - return results diff --git a/docs_src/body_nested_models/tutorial005.py b/docs_src/body_nested_models/tutorial005.py deleted file mode 100644 index 5a01264eda..0000000000 --- a/docs_src/body_nested_models/tutorial005.py +++ /dev/null @@ -1,26 +0,0 @@ -from typing import Set, Union - -from fastapi import FastAPI -from pydantic import BaseModel, HttpUrl - -app = FastAPI() - - -class Image(BaseModel): - url: HttpUrl - name: str - - -class Item(BaseModel): - name: str - description: Union[str, None] = None - price: float - tax: Union[float, None] = None - tags: Set[str] = set() - image: Union[Image, None] = None - - -@app.put("/items/{item_id}") -async def update_item(item_id: int, item: Item): - results = {"item_id": item_id, "item": item} - return results diff --git a/docs_src/body_nested_models/tutorial006.py b/docs_src/body_nested_models/tutorial006.py deleted file mode 100644 index 75f1f30e33..0000000000 --- a/docs_src/body_nested_models/tutorial006.py +++ /dev/null @@ -1,26 +0,0 @@ -from typing import List, Set, Union - -from fastapi import FastAPI -from pydantic import BaseModel, HttpUrl - -app = FastAPI() - - -class Image(BaseModel): - url: HttpUrl - name: str - - -class Item(BaseModel): - name: str - description: Union[str, None] = None - price: float - tax: Union[float, None] = None - tags: Set[str] = set() - images: Union[List[Image], None] = None - - -@app.put("/items/{item_id}") -async def update_item(item_id: int, item: Item): - results = {"item_id": item_id, "item": item} - return results diff --git a/docs_src/body_nested_models/tutorial007.py b/docs_src/body_nested_models/tutorial007.py deleted file mode 100644 index 641f09dced..0000000000 --- a/docs_src/body_nested_models/tutorial007.py +++ /dev/null @@ -1,32 +0,0 @@ -from typing import List, Set, Union - -from fastapi import FastAPI -from pydantic import BaseModel, HttpUrl - -app = FastAPI() - - -class Image(BaseModel): - url: HttpUrl - name: str - - -class Item(BaseModel): - name: str - description: Union[str, None] = None - price: float - tax: Union[float, None] = None - tags: Set[str] = set() - images: Union[List[Image], None] = None - - -class Offer(BaseModel): - name: str - description: Union[str, None] = None - price: float - items: List[Item] - - -@app.post("/offers/") -async def create_offer(offer: Offer): - return offer diff --git a/docs_src/body_nested_models/tutorial008.py b/docs_src/body_nested_models/tutorial008.py deleted file mode 100644 index 3431cc6365..0000000000 --- a/docs_src/body_nested_models/tutorial008.py +++ /dev/null @@ -1,16 +0,0 @@ -from typing import List - -from fastapi import FastAPI -from pydantic import BaseModel, HttpUrl - -app = FastAPI() - - -class Image(BaseModel): - url: HttpUrl - name: str - - -@app.post("/images/multiple/") -async def create_multiple_images(images: List[Image]): - return images diff --git a/docs_src/body_nested_models/tutorial009.py b/docs_src/body_nested_models/tutorial009.py deleted file mode 100644 index 41dce946ec..0000000000 --- a/docs_src/body_nested_models/tutorial009.py +++ /dev/null @@ -1,10 +0,0 @@ -from typing import Dict - -from fastapi import FastAPI - -app = FastAPI() - - -@app.post("/index-weights/") -async def create_index_weights(weights: Dict[int, float]): - return weights diff --git a/docs/en/mkdocs.no-insiders.yml b/docs_src/body_updates/__init__.py similarity index 100% rename from docs/en/mkdocs.no-insiders.yml rename to docs_src/body_updates/__init__.py diff --git a/docs_src/body_updates/tutorial001.py b/docs_src/body_updates/tutorial001.py deleted file mode 100644 index 4e65d77e26..0000000000 --- a/docs_src/body_updates/tutorial001.py +++ /dev/null @@ -1,34 +0,0 @@ -from typing import List, Union - -from fastapi import FastAPI -from fastapi.encoders import jsonable_encoder -from pydantic import BaseModel - -app = FastAPI() - - -class Item(BaseModel): - name: Union[str, None] = None - description: Union[str, None] = None - price: Union[float, None] = None - tax: float = 10.5 - tags: List[str] = [] - - -items = { - "foo": {"name": "Foo", "price": 50.2}, - "bar": {"name": "Bar", "description": "The bartenders", "price": 62, "tax": 20.2}, - "baz": {"name": "Baz", "description": None, "price": 50.2, "tax": 10.5, "tags": []}, -} - - -@app.get("/items/{item_id}", response_model=Item) -async def read_item(item_id: str): - return items[item_id] - - -@app.put("/items/{item_id}", response_model=Item) -async def update_item(item_id: str, item: Item): - update_item_encoded = jsonable_encoder(item) - items[item_id] = update_item_encoded - return update_item_encoded diff --git a/docs_src/body_updates/tutorial002.py b/docs_src/body_updates/tutorial002.py deleted file mode 100644 index c3a0fe79ea..0000000000 --- a/docs_src/body_updates/tutorial002.py +++ /dev/null @@ -1,37 +0,0 @@ -from typing import List, Union - -from fastapi import FastAPI -from fastapi.encoders import jsonable_encoder -from pydantic import BaseModel - -app = FastAPI() - - -class Item(BaseModel): - name: Union[str, None] = None - description: Union[str, None] = None - price: Union[float, None] = None - tax: float = 10.5 - tags: List[str] = [] - - -items = { - "foo": {"name": "Foo", "price": 50.2}, - "bar": {"name": "Bar", "description": "The bartenders", "price": 62, "tax": 20.2}, - "baz": {"name": "Baz", "description": None, "price": 50.2, "tax": 10.5, "tags": []}, -} - - -@app.get("/items/{item_id}", response_model=Item) -async def read_item(item_id: str): - return items[item_id] - - -@app.patch("/items/{item_id}", response_model=Item) -async def update_item(item_id: str, item: Item): - stored_item_data = items[item_id] - stored_item_model = Item(**stored_item_data) - update_data = item.dict(exclude_unset=True) - updated_item = stored_item_model.copy(update=update_data) - items[item_id] = jsonable_encoder(updated_item) - return updated_item diff --git a/docs_src/body_updates/tutorial002_py310.py b/docs_src/body_updates/tutorial002_py310.py index 3498414966..e5db711108 100644 --- a/docs_src/body_updates/tutorial002_py310.py +++ b/docs_src/body_updates/tutorial002_py310.py @@ -29,7 +29,7 @@ async def read_item(item_id: str): async def update_item(item_id: str, item: Item): stored_item_data = items[item_id] stored_item_model = Item(**stored_item_data) - update_data = item.dict(exclude_unset=True) - updated_item = stored_item_model.copy(update=update_data) + update_data = item.model_dump(exclude_unset=True) + updated_item = stored_item_model.model_copy(update=update_data) items[item_id] = jsonable_encoder(updated_item) return updated_item diff --git a/docs_src/body_updates/tutorial002_py39.py b/docs_src/body_updates/tutorial002_py39.py index eb35b35215..eddd7af716 100644 --- a/docs_src/body_updates/tutorial002_py39.py +++ b/docs_src/body_updates/tutorial002_py39.py @@ -31,7 +31,7 @@ async def read_item(item_id: str): async def update_item(item_id: str, item: Item): stored_item_data = items[item_id] stored_item_model = Item(**stored_item_data) - update_data = item.dict(exclude_unset=True) - updated_item = stored_item_model.copy(update=update_data) + update_data = item.model_dump(exclude_unset=True) + updated_item = stored_item_model.model_copy(update=update_data) items[item_id] = jsonable_encoder(updated_item) return updated_item diff --git a/docs_src/conditional_openapi/__init__.py b/docs_src/conditional_openapi/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/conditional_openapi/tutorial001.py b/docs_src/conditional_openapi/tutorial001_py39.py similarity index 100% rename from docs_src/conditional_openapi/tutorial001.py rename to docs_src/conditional_openapi/tutorial001_py39.py diff --git a/docs_src/configure_swagger_ui/__init__.py b/docs_src/configure_swagger_ui/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/configure_swagger_ui/tutorial001.py b/docs_src/configure_swagger_ui/tutorial001_py39.py similarity index 100% rename from docs_src/configure_swagger_ui/tutorial001.py rename to docs_src/configure_swagger_ui/tutorial001_py39.py diff --git a/docs_src/configure_swagger_ui/tutorial002.py b/docs_src/configure_swagger_ui/tutorial002_py39.py similarity index 100% rename from docs_src/configure_swagger_ui/tutorial002.py rename to docs_src/configure_swagger_ui/tutorial002_py39.py diff --git a/docs_src/configure_swagger_ui/tutorial003.py b/docs_src/configure_swagger_ui/tutorial003_py39.py similarity index 100% rename from docs_src/configure_swagger_ui/tutorial003.py rename to docs_src/configure_swagger_ui/tutorial003_py39.py diff --git a/docs_src/cookie_param_models/__init__.py b/docs_src/cookie_param_models/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/cookie_param_models/tutorial001_an.py b/docs_src/cookie_param_models/tutorial001_an.py deleted file mode 100644 index e5839ffd54..0000000000 --- a/docs_src/cookie_param_models/tutorial001_an.py +++ /dev/null @@ -1,18 +0,0 @@ -from typing import Union - -from fastapi import Cookie, FastAPI -from pydantic import BaseModel -from typing_extensions import Annotated - -app = FastAPI() - - -class Cookies(BaseModel): - session_id: str - fatebook_tracker: Union[str, None] = None - googall_tracker: Union[str, None] = None - - -@app.get("/items/") -async def read_items(cookies: Annotated[Cookies, Cookie()]): - return cookies diff --git a/docs_src/cookie_param_models/tutorial001.py b/docs_src/cookie_param_models/tutorial001_py39.py similarity index 100% rename from docs_src/cookie_param_models/tutorial001.py rename to docs_src/cookie_param_models/tutorial001_py39.py diff --git a/docs_src/cookie_param_models/tutorial002_an.py b/docs_src/cookie_param_models/tutorial002_an.py deleted file mode 100644 index ce5644b7bd..0000000000 --- a/docs_src/cookie_param_models/tutorial002_an.py +++ /dev/null @@ -1,20 +0,0 @@ -from typing import Union - -from fastapi import Cookie, FastAPI -from pydantic import BaseModel -from typing_extensions import Annotated - -app = FastAPI() - - -class Cookies(BaseModel): - model_config = {"extra": "forbid"} - - session_id: str - fatebook_tracker: Union[str, None] = None - googall_tracker: Union[str, None] = None - - -@app.get("/items/") -async def read_items(cookies: Annotated[Cookies, Cookie()]): - return cookies diff --git a/docs_src/cookie_param_models/tutorial002_pv1.py b/docs_src/cookie_param_models/tutorial002_pv1.py deleted file mode 100644 index 13f78b850e..0000000000 --- a/docs_src/cookie_param_models/tutorial002_pv1.py +++ /dev/null @@ -1,20 +0,0 @@ -from typing import Union - -from fastapi import Cookie, FastAPI -from pydantic import BaseModel - -app = FastAPI() - - -class Cookies(BaseModel): - class Config: - extra = "forbid" - - session_id: str - fatebook_tracker: Union[str, None] = None - googall_tracker: Union[str, None] = None - - -@app.get("/items/") -async def read_items(cookies: Cookies = Cookie()): - return cookies diff --git a/docs_src/cookie_param_models/tutorial002_pv1_an.py b/docs_src/cookie_param_models/tutorial002_pv1_an.py deleted file mode 100644 index ddfda9b6f5..0000000000 --- a/docs_src/cookie_param_models/tutorial002_pv1_an.py +++ /dev/null @@ -1,21 +0,0 @@ -from typing import Union - -from fastapi import Cookie, FastAPI -from pydantic import BaseModel -from typing_extensions import Annotated - -app = FastAPI() - - -class Cookies(BaseModel): - class Config: - extra = "forbid" - - session_id: str - fatebook_tracker: Union[str, None] = None - googall_tracker: Union[str, None] = None - - -@app.get("/items/") -async def read_items(cookies: Annotated[Cookies, Cookie()]): - return cookies diff --git a/docs_src/cookie_param_models/tutorial002_pv1_an_py310.py b/docs_src/cookie_param_models/tutorial002_pv1_an_py310.py deleted file mode 100644 index ac00360b60..0000000000 --- a/docs_src/cookie_param_models/tutorial002_pv1_an_py310.py +++ /dev/null @@ -1,20 +0,0 @@ -from typing import Annotated - -from fastapi import Cookie, FastAPI -from pydantic import BaseModel - -app = FastAPI() - - -class Cookies(BaseModel): - class Config: - extra = "forbid" - - session_id: str - fatebook_tracker: str | None = None - googall_tracker: str | None = None - - -@app.get("/items/") -async def read_items(cookies: Annotated[Cookies, Cookie()]): - return cookies diff --git a/docs_src/cookie_param_models/tutorial002_pv1_an_py39.py b/docs_src/cookie_param_models/tutorial002_pv1_an_py39.py deleted file mode 100644 index 573caea4b1..0000000000 --- a/docs_src/cookie_param_models/tutorial002_pv1_an_py39.py +++ /dev/null @@ -1,20 +0,0 @@ -from typing import Annotated, Union - -from fastapi import Cookie, FastAPI -from pydantic import BaseModel - -app = FastAPI() - - -class Cookies(BaseModel): - class Config: - extra = "forbid" - - session_id: str - fatebook_tracker: Union[str, None] = None - googall_tracker: Union[str, None] = None - - -@app.get("/items/") -async def read_items(cookies: Annotated[Cookies, Cookie()]): - return cookies diff --git a/docs_src/cookie_param_models/tutorial002_pv1_py310.py b/docs_src/cookie_param_models/tutorial002_pv1_py310.py deleted file mode 100644 index 2c59aad123..0000000000 --- a/docs_src/cookie_param_models/tutorial002_pv1_py310.py +++ /dev/null @@ -1,18 +0,0 @@ -from fastapi import Cookie, FastAPI -from pydantic import BaseModel - -app = FastAPI() - - -class Cookies(BaseModel): - class Config: - extra = "forbid" - - session_id: str - fatebook_tracker: str | None = None - googall_tracker: str | None = None - - -@app.get("/items/") -async def read_items(cookies: Cookies = Cookie()): - return cookies diff --git a/docs_src/cookie_param_models/tutorial002.py b/docs_src/cookie_param_models/tutorial002_py39.py similarity index 100% rename from docs_src/cookie_param_models/tutorial002.py rename to docs_src/cookie_param_models/tutorial002_py39.py diff --git a/docs_src/cookie_params/__init__.py b/docs_src/cookie_params/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/cookie_params/tutorial001_an.py b/docs_src/cookie_params/tutorial001_an.py deleted file mode 100644 index 6d5931229c..0000000000 --- a/docs_src/cookie_params/tutorial001_an.py +++ /dev/null @@ -1,11 +0,0 @@ -from typing import Union - -from fastapi import Cookie, FastAPI -from typing_extensions import Annotated - -app = FastAPI() - - -@app.get("/items/") -async def read_items(ads_id: Annotated[Union[str, None], Cookie()] = None): - return {"ads_id": ads_id} diff --git a/docs_src/cookie_params/tutorial001.py b/docs_src/cookie_params/tutorial001_py39.py similarity index 100% rename from docs_src/cookie_params/tutorial001.py rename to docs_src/cookie_params/tutorial001_py39.py diff --git a/docs_src/cors/__init__.py b/docs_src/cors/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/cors/tutorial001.py b/docs_src/cors/tutorial001_py39.py similarity index 100% rename from docs_src/cors/tutorial001.py rename to docs_src/cors/tutorial001_py39.py diff --git a/docs_src/custom_docs_ui/__init__.py b/docs_src/custom_docs_ui/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/custom_docs_ui/tutorial001.py b/docs_src/custom_docs_ui/tutorial001_py39.py similarity index 100% rename from docs_src/custom_docs_ui/tutorial001.py rename to docs_src/custom_docs_ui/tutorial001_py39.py diff --git a/docs_src/custom_docs_ui/tutorial002.py b/docs_src/custom_docs_ui/tutorial002_py39.py similarity index 100% rename from docs_src/custom_docs_ui/tutorial002.py rename to docs_src/custom_docs_ui/tutorial002_py39.py diff --git a/docs_src/custom_request_and_route/__init__.py b/docs_src/custom_request_and_route/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/custom_request_and_route/tutorial001_an_py310.py b/docs_src/custom_request_and_route/tutorial001_an_py310.py new file mode 100644 index 0000000000..381bab6d84 --- /dev/null +++ b/docs_src/custom_request_and_route/tutorial001_an_py310.py @@ -0,0 +1,36 @@ +import gzip +from collections.abc import Callable +from typing import Annotated + +from fastapi import Body, FastAPI, Request, Response +from fastapi.routing import APIRoute + + +class GzipRequest(Request): + async def body(self) -> bytes: + if not hasattr(self, "_body"): + body = await super().body() + if "gzip" in self.headers.getlist("Content-Encoding"): + body = gzip.decompress(body) + self._body = body + return self._body + + +class GzipRoute(APIRoute): + def get_route_handler(self) -> Callable: + original_route_handler = super().get_route_handler() + + async def custom_route_handler(request: Request) -> Response: + request = GzipRequest(request.scope, request.receive) + return await original_route_handler(request) + + return custom_route_handler + + +app = FastAPI() +app.router.route_class = GzipRoute + + +@app.post("/sum") +async def sum_numbers(numbers: Annotated[list[int], Body()]): + return {"sum": sum(numbers)} diff --git a/docs_src/custom_request_and_route/tutorial001_an_py39.py b/docs_src/custom_request_and_route/tutorial001_an_py39.py new file mode 100644 index 0000000000..076727e643 --- /dev/null +++ b/docs_src/custom_request_and_route/tutorial001_an_py39.py @@ -0,0 +1,35 @@ +import gzip +from typing import Annotated, Callable + +from fastapi import Body, FastAPI, Request, Response +from fastapi.routing import APIRoute + + +class GzipRequest(Request): + async def body(self) -> bytes: + if not hasattr(self, "_body"): + body = await super().body() + if "gzip" in self.headers.getlist("Content-Encoding"): + body = gzip.decompress(body) + self._body = body + return self._body + + +class GzipRoute(APIRoute): + def get_route_handler(self) -> Callable: + original_route_handler = super().get_route_handler() + + async def custom_route_handler(request: Request) -> Response: + request = GzipRequest(request.scope, request.receive) + return await original_route_handler(request) + + return custom_route_handler + + +app = FastAPI() +app.router.route_class = GzipRoute + + +@app.post("/sum") +async def sum_numbers(numbers: Annotated[list[int], Body()]): + return {"sum": sum(numbers)} diff --git a/docs_src/custom_request_and_route/tutorial001_py310.py b/docs_src/custom_request_and_route/tutorial001_py310.py new file mode 100644 index 0000000000..c678088ce7 --- /dev/null +++ b/docs_src/custom_request_and_route/tutorial001_py310.py @@ -0,0 +1,35 @@ +import gzip +from collections.abc import Callable + +from fastapi import Body, FastAPI, Request, Response +from fastapi.routing import APIRoute + + +class GzipRequest(Request): + async def body(self) -> bytes: + if not hasattr(self, "_body"): + body = await super().body() + if "gzip" in self.headers.getlist("Content-Encoding"): + body = gzip.decompress(body) + self._body = body + return self._body + + +class GzipRoute(APIRoute): + def get_route_handler(self) -> Callable: + original_route_handler = super().get_route_handler() + + async def custom_route_handler(request: Request) -> Response: + request = GzipRequest(request.scope, request.receive) + return await original_route_handler(request) + + return custom_route_handler + + +app = FastAPI() +app.router.route_class = GzipRoute + + +@app.post("/sum") +async def sum_numbers(numbers: list[int] = Body()): + return {"sum": sum(numbers)} diff --git a/docs_src/custom_request_and_route/tutorial001.py b/docs_src/custom_request_and_route/tutorial001_py39.py similarity index 91% rename from docs_src/custom_request_and_route/tutorial001.py rename to docs_src/custom_request_and_route/tutorial001_py39.py index 268ce9019e..54b20b9425 100644 --- a/docs_src/custom_request_and_route/tutorial001.py +++ b/docs_src/custom_request_and_route/tutorial001_py39.py @@ -1,5 +1,5 @@ import gzip -from typing import Callable, List +from typing import Callable from fastapi import Body, FastAPI, Request, Response from fastapi.routing import APIRoute @@ -31,5 +31,5 @@ app.router.route_class = GzipRoute @app.post("/sum") -async def sum_numbers(numbers: List[int] = Body()): +async def sum_numbers(numbers: list[int] = Body()): return {"sum": sum(numbers)} diff --git a/docs_src/custom_request_and_route/tutorial002_an_py310.py b/docs_src/custom_request_and_route/tutorial002_an_py310.py new file mode 100644 index 0000000000..69b7de4859 --- /dev/null +++ b/docs_src/custom_request_and_route/tutorial002_an_py310.py @@ -0,0 +1,30 @@ +from collections.abc import Callable +from typing import Annotated + +from fastapi import Body, FastAPI, HTTPException, Request, Response +from fastapi.exceptions import RequestValidationError +from fastapi.routing import APIRoute + + +class ValidationErrorLoggingRoute(APIRoute): + def get_route_handler(self) -> Callable: + original_route_handler = super().get_route_handler() + + async def custom_route_handler(request: Request) -> Response: + try: + return await original_route_handler(request) + except RequestValidationError as exc: + body = await request.body() + detail = {"errors": exc.errors(), "body": body.decode()} + raise HTTPException(status_code=422, detail=detail) + + return custom_route_handler + + +app = FastAPI() +app.router.route_class = ValidationErrorLoggingRoute + + +@app.post("/") +async def sum_numbers(numbers: Annotated[list[int], Body()]): + return sum(numbers) diff --git a/docs_src/custom_request_and_route/tutorial002_an_py39.py b/docs_src/custom_request_and_route/tutorial002_an_py39.py new file mode 100644 index 0000000000..e7de09de44 --- /dev/null +++ b/docs_src/custom_request_and_route/tutorial002_an_py39.py @@ -0,0 +1,29 @@ +from typing import Annotated, Callable + +from fastapi import Body, FastAPI, HTTPException, Request, Response +from fastapi.exceptions import RequestValidationError +from fastapi.routing import APIRoute + + +class ValidationErrorLoggingRoute(APIRoute): + def get_route_handler(self) -> Callable: + original_route_handler = super().get_route_handler() + + async def custom_route_handler(request: Request) -> Response: + try: + return await original_route_handler(request) + except RequestValidationError as exc: + body = await request.body() + detail = {"errors": exc.errors(), "body": body.decode()} + raise HTTPException(status_code=422, detail=detail) + + return custom_route_handler + + +app = FastAPI() +app.router.route_class = ValidationErrorLoggingRoute + + +@app.post("/") +async def sum_numbers(numbers: Annotated[list[int], Body()]): + return sum(numbers) diff --git a/docs_src/custom_request_and_route/tutorial002_py310.py b/docs_src/custom_request_and_route/tutorial002_py310.py new file mode 100644 index 0000000000..13a5ca5426 --- /dev/null +++ b/docs_src/custom_request_and_route/tutorial002_py310.py @@ -0,0 +1,29 @@ +from collections.abc import Callable + +from fastapi import Body, FastAPI, HTTPException, Request, Response +from fastapi.exceptions import RequestValidationError +from fastapi.routing import APIRoute + + +class ValidationErrorLoggingRoute(APIRoute): + def get_route_handler(self) -> Callable: + original_route_handler = super().get_route_handler() + + async def custom_route_handler(request: Request) -> Response: + try: + return await original_route_handler(request) + except RequestValidationError as exc: + body = await request.body() + detail = {"errors": exc.errors(), "body": body.decode()} + raise HTTPException(status_code=422, detail=detail) + + return custom_route_handler + + +app = FastAPI() +app.router.route_class = ValidationErrorLoggingRoute + + +@app.post("/") +async def sum_numbers(numbers: list[int] = Body()): + return sum(numbers) diff --git a/docs_src/custom_request_and_route/tutorial002.py b/docs_src/custom_request_and_route/tutorial002_py39.py similarity index 90% rename from docs_src/custom_request_and_route/tutorial002.py rename to docs_src/custom_request_and_route/tutorial002_py39.py index cee4a95f08..c4e4748281 100644 --- a/docs_src/custom_request_and_route/tutorial002.py +++ b/docs_src/custom_request_and_route/tutorial002_py39.py @@ -1,4 +1,4 @@ -from typing import Callable, List +from typing import Callable from fastapi import Body, FastAPI, HTTPException, Request, Response from fastapi.exceptions import RequestValidationError @@ -25,5 +25,5 @@ app.router.route_class = ValidationErrorLoggingRoute @app.post("/") -async def sum_numbers(numbers: List[int] = Body()): +async def sum_numbers(numbers: list[int] = Body()): return sum(numbers) diff --git a/docs_src/custom_request_and_route/tutorial003_py310.py b/docs_src/custom_request_and_route/tutorial003_py310.py new file mode 100644 index 0000000000..f4e60be61b --- /dev/null +++ b/docs_src/custom_request_and_route/tutorial003_py310.py @@ -0,0 +1,39 @@ +import time +from collections.abc import Callable + +from fastapi import APIRouter, FastAPI, Request, Response +from fastapi.routing import APIRoute + + +class TimedRoute(APIRoute): + def get_route_handler(self) -> Callable: + original_route_handler = super().get_route_handler() + + async def custom_route_handler(request: Request) -> Response: + before = time.time() + response: Response = await original_route_handler(request) + duration = time.time() - before + response.headers["X-Response-Time"] = str(duration) + print(f"route duration: {duration}") + print(f"route response: {response}") + print(f"route response headers: {response.headers}") + return response + + return custom_route_handler + + +app = FastAPI() +router = APIRouter(route_class=TimedRoute) + + +@app.get("/") +async def not_timed(): + return {"message": "Not timed"} + + +@router.get("/timed") +async def timed(): + return {"message": "It's the time of my life"} + + +app.include_router(router) diff --git a/docs_src/custom_request_and_route/tutorial003.py b/docs_src/custom_request_and_route/tutorial003_py39.py similarity index 100% rename from docs_src/custom_request_and_route/tutorial003.py rename to docs_src/custom_request_and_route/tutorial003_py39.py diff --git a/docs_src/custom_response/__init__.py b/docs_src/custom_response/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/custom_response/tutorial001.py b/docs_src/custom_response/tutorial001_py39.py similarity index 100% rename from docs_src/custom_response/tutorial001.py rename to docs_src/custom_response/tutorial001_py39.py diff --git a/docs_src/custom_response/tutorial001b.py b/docs_src/custom_response/tutorial001b_py39.py similarity index 100% rename from docs_src/custom_response/tutorial001b.py rename to docs_src/custom_response/tutorial001b_py39.py diff --git a/docs_src/custom_response/tutorial002.py b/docs_src/custom_response/tutorial002_py39.py similarity index 100% rename from docs_src/custom_response/tutorial002.py rename to docs_src/custom_response/tutorial002_py39.py diff --git a/docs_src/custom_response/tutorial003.py b/docs_src/custom_response/tutorial003_py39.py similarity index 100% rename from docs_src/custom_response/tutorial003.py rename to docs_src/custom_response/tutorial003_py39.py diff --git a/docs_src/custom_response/tutorial004.py b/docs_src/custom_response/tutorial004_py39.py similarity index 100% rename from docs_src/custom_response/tutorial004.py rename to docs_src/custom_response/tutorial004_py39.py diff --git a/docs_src/custom_response/tutorial005.py b/docs_src/custom_response/tutorial005_py39.py similarity index 100% rename from docs_src/custom_response/tutorial005.py rename to docs_src/custom_response/tutorial005_py39.py diff --git a/docs_src/custom_response/tutorial006.py b/docs_src/custom_response/tutorial006_py39.py similarity index 100% rename from docs_src/custom_response/tutorial006.py rename to docs_src/custom_response/tutorial006_py39.py diff --git a/docs_src/custom_response/tutorial006b.py b/docs_src/custom_response/tutorial006b_py39.py similarity index 100% rename from docs_src/custom_response/tutorial006b.py rename to docs_src/custom_response/tutorial006b_py39.py diff --git a/docs_src/custom_response/tutorial006c.py b/docs_src/custom_response/tutorial006c_py39.py similarity index 100% rename from docs_src/custom_response/tutorial006c.py rename to docs_src/custom_response/tutorial006c_py39.py diff --git a/docs_src/custom_response/tutorial007.py b/docs_src/custom_response/tutorial007_py39.py similarity index 100% rename from docs_src/custom_response/tutorial007.py rename to docs_src/custom_response/tutorial007_py39.py diff --git a/docs_src/custom_response/tutorial008.py b/docs_src/custom_response/tutorial008_py39.py similarity index 100% rename from docs_src/custom_response/tutorial008.py rename to docs_src/custom_response/tutorial008_py39.py diff --git a/docs_src/custom_response/tutorial009.py b/docs_src/custom_response/tutorial009_py39.py similarity index 100% rename from docs_src/custom_response/tutorial009.py rename to docs_src/custom_response/tutorial009_py39.py diff --git a/docs_src/custom_response/tutorial009b.py b/docs_src/custom_response/tutorial009b_py39.py similarity index 100% rename from docs_src/custom_response/tutorial009b.py rename to docs_src/custom_response/tutorial009b_py39.py diff --git a/docs_src/custom_response/tutorial009c.py b/docs_src/custom_response/tutorial009c_py39.py similarity index 100% rename from docs_src/custom_response/tutorial009c.py rename to docs_src/custom_response/tutorial009c_py39.py diff --git a/docs_src/custom_response/tutorial010.py b/docs_src/custom_response/tutorial010_py39.py similarity index 100% rename from docs_src/custom_response/tutorial010.py rename to docs_src/custom_response/tutorial010_py39.py diff --git a/docs_src/dataclasses_/__init__.py b/docs_src/dataclasses_/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/dataclasses_/tutorial001_py310.py b/docs_src/dataclasses_/tutorial001_py310.py new file mode 100644 index 0000000000..ab709a7c85 --- /dev/null +++ b/docs_src/dataclasses_/tutorial001_py310.py @@ -0,0 +1,19 @@ +from dataclasses import dataclass + +from fastapi import FastAPI + + +@dataclass +class Item: + name: str + price: float + description: str | None = None + tax: float | None = None + + +app = FastAPI() + + +@app.post("/items/") +async def create_item(item: Item): + return item diff --git a/docs_src/dataclasses/tutorial001.py b/docs_src/dataclasses_/tutorial001_py39.py similarity index 100% rename from docs_src/dataclasses/tutorial001.py rename to docs_src/dataclasses_/tutorial001_py39.py diff --git a/docs_src/dataclasses_/tutorial002_py310.py b/docs_src/dataclasses_/tutorial002_py310.py new file mode 100644 index 0000000000..e16249f1e1 --- /dev/null +++ b/docs_src/dataclasses_/tutorial002_py310.py @@ -0,0 +1,25 @@ +from dataclasses import dataclass, field + +from fastapi import FastAPI + + +@dataclass +class Item: + name: str + price: float + tags: list[str] = field(default_factory=list) + description: str | None = None + tax: float | None = None + + +app = FastAPI() + + +@app.get("/items/next", response_model=Item) +async def read_next_item(): + return { + "name": "Island In The Moon", + "price": 12.99, + "description": "A place to be playin' and havin' fun", + "tags": ["breater"], + } diff --git a/docs_src/dataclasses/tutorial002.py b/docs_src/dataclasses_/tutorial002_py39.py similarity index 85% rename from docs_src/dataclasses/tutorial002.py rename to docs_src/dataclasses_/tutorial002_py39.py index ece2f150cc..0c23765d84 100644 --- a/docs_src/dataclasses/tutorial002.py +++ b/docs_src/dataclasses_/tutorial002_py39.py @@ -1,5 +1,5 @@ from dataclasses import dataclass, field -from typing import List, Union +from typing import Union from fastapi import FastAPI @@ -8,7 +8,7 @@ from fastapi import FastAPI class Item: name: str price: float - tags: List[str] = field(default_factory=list) + tags: list[str] = field(default_factory=list) description: Union[str, None] = None tax: Union[float, None] = None diff --git a/docs_src/dataclasses_/tutorial003_py310.py b/docs_src/dataclasses_/tutorial003_py310.py new file mode 100644 index 0000000000..9b9a3fd635 --- /dev/null +++ b/docs_src/dataclasses_/tutorial003_py310.py @@ -0,0 +1,54 @@ +from dataclasses import field # (1) + +from fastapi import FastAPI +from pydantic.dataclasses import dataclass # (2) + + +@dataclass +class Item: + name: str + description: str | None = None + + +@dataclass +class Author: + name: str + items: list[Item] = field(default_factory=list) # (3) + + +app = FastAPI() + + +@app.post("/authors/{author_id}/items/", response_model=Author) # (4) +async def create_author_items(author_id: str, items: list[Item]): # (5) + return {"name": author_id, "items": items} # (6) + + +@app.get("/authors/", response_model=list[Author]) # (7) +def get_authors(): # (8) + return [ # (9) + { + "name": "Breaters", + "items": [ + { + "name": "Island In The Moon", + "description": "A place to be playin' and havin' fun", + }, + {"name": "Holy Buddies"}, + ], + }, + { + "name": "System of an Up", + "items": [ + { + "name": "Salt", + "description": "The kombucha mushroom people's favorite", + }, + {"name": "Pad Thai"}, + { + "name": "Lonely Night", + "description": "The mostests lonliest nightiest of allest", + }, + ], + }, + ] diff --git a/docs_src/dataclasses/tutorial003.py b/docs_src/dataclasses_/tutorial003_py39.py similarity index 84% rename from docs_src/dataclasses/tutorial003.py rename to docs_src/dataclasses_/tutorial003_py39.py index c613155133..991708c009 100644 --- a/docs_src/dataclasses/tutorial003.py +++ b/docs_src/dataclasses_/tutorial003_py39.py @@ -1,5 +1,5 @@ from dataclasses import field # (1) -from typing import List, Union +from typing import Union from fastapi import FastAPI from pydantic.dataclasses import dataclass # (2) @@ -14,18 +14,18 @@ class Item: @dataclass class Author: name: str - items: List[Item] = field(default_factory=list) # (3) + items: list[Item] = field(default_factory=list) # (3) app = FastAPI() @app.post("/authors/{author_id}/items/", response_model=Author) # (4) -async def create_author_items(author_id: str, items: List[Item]): # (5) +async def create_author_items(author_id: str, items: list[Item]): # (5) return {"name": author_id, "items": items} # (6) -@app.get("/authors/", response_model=List[Author]) # (7) +@app.get("/authors/", response_model=list[Author]) # (7) def get_authors(): # (8) return [ # (9) { diff --git a/docs_src/debugging/__init__.py b/docs_src/debugging/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/debugging/tutorial001.py b/docs_src/debugging/tutorial001_py39.py similarity index 100% rename from docs_src/debugging/tutorial001.py rename to docs_src/debugging/tutorial001_py39.py diff --git a/docs_src/dependencies/__init__.py b/docs_src/dependencies/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/dependencies/tutorial001_02_an.py b/docs_src/dependencies/tutorial001_02_an.py deleted file mode 100644 index 455d60c822..0000000000 --- a/docs_src/dependencies/tutorial001_02_an.py +++ /dev/null @@ -1,25 +0,0 @@ -from typing import Union - -from fastapi import Depends, FastAPI -from typing_extensions import Annotated - -app = FastAPI() - - -async def common_parameters( - q: Union[str, None] = None, skip: int = 0, limit: int = 100 -): - return {"q": q, "skip": skip, "limit": limit} - - -CommonsDep = Annotated[dict, Depends(common_parameters)] - - -@app.get("/items/") -async def read_items(commons: CommonsDep): - return commons - - -@app.get("/users/") -async def read_users(commons: CommonsDep): - return commons diff --git a/docs_src/dependencies/tutorial001_an.py b/docs_src/dependencies/tutorial001_an.py deleted file mode 100644 index 81e24fe86c..0000000000 --- a/docs_src/dependencies/tutorial001_an.py +++ /dev/null @@ -1,22 +0,0 @@ -from typing import Union - -from fastapi import Depends, FastAPI -from typing_extensions import Annotated - -app = FastAPI() - - -async def common_parameters( - q: Union[str, None] = None, skip: int = 0, limit: int = 100 -): - return {"q": q, "skip": skip, "limit": limit} - - -@app.get("/items/") -async def read_items(commons: Annotated[dict, Depends(common_parameters)]): - return commons - - -@app.get("/users/") -async def read_users(commons: Annotated[dict, Depends(common_parameters)]): - return commons diff --git a/docs_src/dependencies/tutorial001.py b/docs_src/dependencies/tutorial001_py39.py similarity index 100% rename from docs_src/dependencies/tutorial001.py rename to docs_src/dependencies/tutorial001_py39.py diff --git a/docs_src/dependencies/tutorial002_an.py b/docs_src/dependencies/tutorial002_an.py deleted file mode 100644 index 964ccf66ca..0000000000 --- a/docs_src/dependencies/tutorial002_an.py +++ /dev/null @@ -1,26 +0,0 @@ -from typing import Union - -from fastapi import Depends, FastAPI -from typing_extensions import Annotated - -app = FastAPI() - - -fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}] - - -class CommonQueryParams: - def __init__(self, q: Union[str, None] = None, skip: int = 0, limit: int = 100): - self.q = q - self.skip = skip - self.limit = limit - - -@app.get("/items/") -async def read_items(commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)]): - response = {} - if commons.q: - response.update({"q": commons.q}) - items = fake_items_db[commons.skip : commons.skip + commons.limit] - response.update({"items": items}) - return response diff --git a/docs_src/dependencies/tutorial002.py b/docs_src/dependencies/tutorial002_py39.py similarity index 100% rename from docs_src/dependencies/tutorial002.py rename to docs_src/dependencies/tutorial002_py39.py diff --git a/docs_src/dependencies/tutorial003_an.py b/docs_src/dependencies/tutorial003_an.py deleted file mode 100644 index ba8e9f7174..0000000000 --- a/docs_src/dependencies/tutorial003_an.py +++ /dev/null @@ -1,26 +0,0 @@ -from typing import Any, Union - -from fastapi import Depends, FastAPI -from typing_extensions import Annotated - -app = FastAPI() - - -fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}] - - -class CommonQueryParams: - def __init__(self, q: Union[str, None] = None, skip: int = 0, limit: int = 100): - self.q = q - self.skip = skip - self.limit = limit - - -@app.get("/items/") -async def read_items(commons: Annotated[Any, Depends(CommonQueryParams)]): - response = {} - if commons.q: - response.update({"q": commons.q}) - items = fake_items_db[commons.skip : commons.skip + commons.limit] - response.update({"items": items}) - return response diff --git a/docs_src/dependencies/tutorial003.py b/docs_src/dependencies/tutorial003_py39.py similarity index 100% rename from docs_src/dependencies/tutorial003.py rename to docs_src/dependencies/tutorial003_py39.py diff --git a/docs_src/dependencies/tutorial004_an.py b/docs_src/dependencies/tutorial004_an.py deleted file mode 100644 index 78881a354c..0000000000 --- a/docs_src/dependencies/tutorial004_an.py +++ /dev/null @@ -1,26 +0,0 @@ -from typing import Union - -from fastapi import Depends, FastAPI -from typing_extensions import Annotated - -app = FastAPI() - - -fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}] - - -class CommonQueryParams: - def __init__(self, q: Union[str, None] = None, skip: int = 0, limit: int = 100): - self.q = q - self.skip = skip - self.limit = limit - - -@app.get("/items/") -async def read_items(commons: Annotated[CommonQueryParams, Depends()]): - response = {} - if commons.q: - response.update({"q": commons.q}) - items = fake_items_db[commons.skip : commons.skip + commons.limit] - response.update({"items": items}) - return response diff --git a/docs_src/dependencies/tutorial004.py b/docs_src/dependencies/tutorial004_py39.py similarity index 100% rename from docs_src/dependencies/tutorial004.py rename to docs_src/dependencies/tutorial004_py39.py diff --git a/docs_src/dependencies/tutorial005_an.py b/docs_src/dependencies/tutorial005_an.py deleted file mode 100644 index 1d78c17a2a..0000000000 --- a/docs_src/dependencies/tutorial005_an.py +++ /dev/null @@ -1,26 +0,0 @@ -from typing import Union - -from fastapi import Cookie, Depends, FastAPI -from typing_extensions import Annotated - -app = FastAPI() - - -def query_extractor(q: Union[str, None] = None): - return q - - -def query_or_cookie_extractor( - q: Annotated[str, Depends(query_extractor)], - last_query: Annotated[Union[str, None], Cookie()] = None, -): - if not q: - return last_query - return q - - -@app.get("/items/") -async def read_query( - query_or_default: Annotated[str, Depends(query_or_cookie_extractor)], -): - return {"q_or_cookie": query_or_default} diff --git a/docs_src/dependencies/tutorial005.py b/docs_src/dependencies/tutorial005_py39.py similarity index 100% rename from docs_src/dependencies/tutorial005.py rename to docs_src/dependencies/tutorial005_py39.py diff --git a/docs_src/dependencies/tutorial006_an.py b/docs_src/dependencies/tutorial006_an.py deleted file mode 100644 index 5aaea04d16..0000000000 --- a/docs_src/dependencies/tutorial006_an.py +++ /dev/null @@ -1,20 +0,0 @@ -from fastapi import Depends, FastAPI, Header, HTTPException -from typing_extensions import Annotated - -app = FastAPI() - - -async def verify_token(x_token: Annotated[str, Header()]): - if x_token != "fake-super-secret-token": - raise HTTPException(status_code=400, detail="X-Token header invalid") - - -async def verify_key(x_key: Annotated[str, Header()]): - if x_key != "fake-super-secret-key": - raise HTTPException(status_code=400, detail="X-Key header invalid") - return x_key - - -@app.get("/items/", dependencies=[Depends(verify_token), Depends(verify_key)]) -async def read_items(): - return [{"item": "Foo"}, {"item": "Bar"}] diff --git a/docs_src/dependencies/tutorial006.py b/docs_src/dependencies/tutorial006_py39.py similarity index 100% rename from docs_src/dependencies/tutorial006.py rename to docs_src/dependencies/tutorial006_py39.py diff --git a/docs_src/dependencies/tutorial007.py b/docs_src/dependencies/tutorial007_py39.py similarity index 100% rename from docs_src/dependencies/tutorial007.py rename to docs_src/dependencies/tutorial007_py39.py diff --git a/docs_src/dependencies/tutorial008_an.py b/docs_src/dependencies/tutorial008_an.py deleted file mode 100644 index 2de86f042d..0000000000 --- a/docs_src/dependencies/tutorial008_an.py +++ /dev/null @@ -1,26 +0,0 @@ -from fastapi import Depends -from typing_extensions import Annotated - - -async def dependency_a(): - dep_a = generate_dep_a() - try: - yield dep_a - finally: - dep_a.close() - - -async def dependency_b(dep_a: Annotated[DepA, Depends(dependency_a)]): - dep_b = generate_dep_b() - try: - yield dep_b - finally: - dep_b.close(dep_a) - - -async def dependency_c(dep_b: Annotated[DepB, Depends(dependency_b)]): - dep_c = generate_dep_c() - try: - yield dep_c - finally: - dep_c.close(dep_b) diff --git a/docs_src/dependencies/tutorial008.py b/docs_src/dependencies/tutorial008_py39.py similarity index 100% rename from docs_src/dependencies/tutorial008.py rename to docs_src/dependencies/tutorial008_py39.py diff --git a/docs_src/dependencies/tutorial008b_an.py b/docs_src/dependencies/tutorial008b_an.py deleted file mode 100644 index 84d8f12c14..0000000000 --- a/docs_src/dependencies/tutorial008b_an.py +++ /dev/null @@ -1,31 +0,0 @@ -from fastapi import Depends, FastAPI, HTTPException -from typing_extensions import Annotated - -app = FastAPI() - - -data = { - "plumbus": {"description": "Freshly pickled plumbus", "owner": "Morty"}, - "portal-gun": {"description": "Gun to create portals", "owner": "Rick"}, -} - - -class OwnerError(Exception): - pass - - -def get_username(): - try: - yield "Rick" - except OwnerError as e: - raise HTTPException(status_code=400, detail=f"Owner error: {e}") - - -@app.get("/items/{item_id}") -def get_item(item_id: str, username: Annotated[str, Depends(get_username)]): - if item_id not in data: - raise HTTPException(status_code=404, detail="Item not found") - item = data[item_id] - if item["owner"] != username: - raise OwnerError(username) - return item diff --git a/docs_src/dependencies/tutorial008b.py b/docs_src/dependencies/tutorial008b_py39.py similarity index 100% rename from docs_src/dependencies/tutorial008b.py rename to docs_src/dependencies/tutorial008b_py39.py diff --git a/docs_src/dependencies/tutorial008c_an.py b/docs_src/dependencies/tutorial008c_an.py deleted file mode 100644 index 94f59f9aa6..0000000000 --- a/docs_src/dependencies/tutorial008c_an.py +++ /dev/null @@ -1,28 +0,0 @@ -from fastapi import Depends, FastAPI, HTTPException -from typing_extensions import Annotated - -app = FastAPI() - - -class InternalError(Exception): - pass - - -def get_username(): - try: - yield "Rick" - except InternalError: - print("Oops, we didn't raise again, Britney 😱") - - -@app.get("/items/{item_id}") -def get_item(item_id: str, username: Annotated[str, Depends(get_username)]): - if item_id == "portal-gun": - raise InternalError( - f"The portal gun is too dangerous to be owned by {username}" - ) - if item_id != "plumbus": - raise HTTPException( - status_code=404, detail="Item not found, there's only a plumbus here" - ) - return item_id diff --git a/docs_src/dependencies/tutorial008c.py b/docs_src/dependencies/tutorial008c_py39.py similarity index 100% rename from docs_src/dependencies/tutorial008c.py rename to docs_src/dependencies/tutorial008c_py39.py diff --git a/docs_src/dependencies/tutorial008d_an.py b/docs_src/dependencies/tutorial008d_an.py deleted file mode 100644 index c354245744..0000000000 --- a/docs_src/dependencies/tutorial008d_an.py +++ /dev/null @@ -1,29 +0,0 @@ -from fastapi import Depends, FastAPI, HTTPException -from typing_extensions import Annotated - -app = FastAPI() - - -class InternalError(Exception): - pass - - -def get_username(): - try: - yield "Rick" - except InternalError: - print("We don't swallow the internal error here, we raise again 😎") - raise - - -@app.get("/items/{item_id}") -def get_item(item_id: str, username: Annotated[str, Depends(get_username)]): - if item_id == "portal-gun": - raise InternalError( - f"The portal gun is too dangerous to be owned by {username}" - ) - if item_id != "plumbus": - raise HTTPException( - status_code=404, detail="Item not found, there's only a plumbus here" - ) - return item_id diff --git a/docs_src/dependencies/tutorial008d.py b/docs_src/dependencies/tutorial008d_py39.py similarity index 100% rename from docs_src/dependencies/tutorial008d.py rename to docs_src/dependencies/tutorial008d_py39.py diff --git a/docs_src/dependencies/tutorial008e_an.py b/docs_src/dependencies/tutorial008e_an.py deleted file mode 100644 index c8a0af2b3b..0000000000 --- a/docs_src/dependencies/tutorial008e_an.py +++ /dev/null @@ -1,16 +0,0 @@ -from fastapi import Depends, FastAPI -from typing_extensions import Annotated - -app = FastAPI() - - -def get_username(): - try: - yield "Rick" - finally: - print("Cleanup up before response is sent") - - -@app.get("/users/me") -def get_user_me(username: Annotated[str, Depends(get_username, scope="function")]): - return username diff --git a/docs_src/dependencies/tutorial008e.py b/docs_src/dependencies/tutorial008e_py39.py similarity index 100% rename from docs_src/dependencies/tutorial008e.py rename to docs_src/dependencies/tutorial008e_py39.py diff --git a/docs_src/dependencies/tutorial009.py b/docs_src/dependencies/tutorial009.py deleted file mode 100644 index 8472f642de..0000000000 --- a/docs_src/dependencies/tutorial009.py +++ /dev/null @@ -1,25 +0,0 @@ -from fastapi import Depends - - -async def dependency_a(): - dep_a = generate_dep_a() - try: - yield dep_a - finally: - dep_a.close() - - -async def dependency_b(dep_a=Depends(dependency_a)): - dep_b = generate_dep_b() - try: - yield dep_b - finally: - dep_b.close(dep_a) - - -async def dependency_c(dep_b=Depends(dependency_b)): - dep_c = generate_dep_c() - try: - yield dep_c - finally: - dep_c.close(dep_b) diff --git a/docs_src/dependencies/tutorial010.py b/docs_src/dependencies/tutorial010_py39.py similarity index 100% rename from docs_src/dependencies/tutorial010.py rename to docs_src/dependencies/tutorial010_py39.py diff --git a/docs_src/dependencies/tutorial011_an.py b/docs_src/dependencies/tutorial011_an.py deleted file mode 100644 index 6c13d9033f..0000000000 --- a/docs_src/dependencies/tutorial011_an.py +++ /dev/null @@ -1,22 +0,0 @@ -from fastapi import Depends, FastAPI -from typing_extensions import Annotated - -app = FastAPI() - - -class FixedContentQueryChecker: - def __init__(self, fixed_content: str): - self.fixed_content = fixed_content - - def __call__(self, q: str = ""): - if q: - return self.fixed_content in q - return False - - -checker = FixedContentQueryChecker("bar") - - -@app.get("/query-checker/") -async def read_query_check(fixed_content_included: Annotated[bool, Depends(checker)]): - return {"fixed_content_in_query": fixed_content_included} diff --git a/docs_src/dependencies/tutorial011.py b/docs_src/dependencies/tutorial011_py39.py similarity index 100% rename from docs_src/dependencies/tutorial011.py rename to docs_src/dependencies/tutorial011_py39.py diff --git a/docs_src/dependencies/tutorial012_an.py b/docs_src/dependencies/tutorial012_an.py deleted file mode 100644 index 7541e6bf41..0000000000 --- a/docs_src/dependencies/tutorial012_an.py +++ /dev/null @@ -1,26 +0,0 @@ -from fastapi import Depends, FastAPI, Header, HTTPException -from typing_extensions import Annotated - - -async def verify_token(x_token: Annotated[str, Header()]): - if x_token != "fake-super-secret-token": - raise HTTPException(status_code=400, detail="X-Token header invalid") - - -async def verify_key(x_key: Annotated[str, Header()]): - if x_key != "fake-super-secret-key": - raise HTTPException(status_code=400, detail="X-Key header invalid") - return x_key - - -app = FastAPI(dependencies=[Depends(verify_token), Depends(verify_key)]) - - -@app.get("/items/") -async def read_items(): - return [{"item": "Portal Gun"}, {"item": "Plumbus"}] - - -@app.get("/users/") -async def read_users(): - return [{"username": "Rick"}, {"username": "Morty"}] diff --git a/docs_src/dependencies/tutorial012_an_py39.py b/docs_src/dependencies/tutorial012_an_py39.py index 7541e6bf41..6503591fc3 100644 --- a/docs_src/dependencies/tutorial012_an_py39.py +++ b/docs_src/dependencies/tutorial012_an_py39.py @@ -1,5 +1,6 @@ +from typing import Annotated + from fastapi import Depends, FastAPI, Header, HTTPException -from typing_extensions import Annotated async def verify_token(x_token: Annotated[str, Header()]): diff --git a/docs_src/dependencies/tutorial012.py b/docs_src/dependencies/tutorial012_py39.py similarity index 100% rename from docs_src/dependencies/tutorial012.py rename to docs_src/dependencies/tutorial012_py39.py diff --git a/docs_src/dependency_testing/__init__.py b/docs_src/dependency_testing/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/dependency_testing/tutorial001_an.py b/docs_src/dependency_testing/tutorial001_an.py deleted file mode 100644 index 4c76a87ff5..0000000000 --- a/docs_src/dependency_testing/tutorial001_an.py +++ /dev/null @@ -1,60 +0,0 @@ -from typing import Union - -from fastapi import Depends, FastAPI -from fastapi.testclient import TestClient -from typing_extensions import Annotated - -app = FastAPI() - - -async def common_parameters( - q: Union[str, None] = None, skip: int = 0, limit: int = 100 -): - return {"q": q, "skip": skip, "limit": limit} - - -@app.get("/items/") -async def read_items(commons: Annotated[dict, Depends(common_parameters)]): - return {"message": "Hello Items!", "params": commons} - - -@app.get("/users/") -async def read_users(commons: Annotated[dict, Depends(common_parameters)]): - return {"message": "Hello Users!", "params": commons} - - -client = TestClient(app) - - -async def override_dependency(q: Union[str, None] = None): - return {"q": q, "skip": 5, "limit": 10} - - -app.dependency_overrides[common_parameters] = override_dependency - - -def test_override_in_items(): - response = client.get("/items/") - assert response.status_code == 200 - assert response.json() == { - "message": "Hello Items!", - "params": {"q": None, "skip": 5, "limit": 10}, - } - - -def test_override_in_items_with_q(): - response = client.get("/items/?q=foo") - assert response.status_code == 200 - assert response.json() == { - "message": "Hello Items!", - "params": {"q": "foo", "skip": 5, "limit": 10}, - } - - -def test_override_in_items_with_params(): - response = client.get("/items/?q=foo&skip=100&limit=200") - assert response.status_code == 200 - assert response.json() == { - "message": "Hello Items!", - "params": {"q": "foo", "skip": 5, "limit": 10}, - } diff --git a/docs_src/dependency_testing/tutorial001.py b/docs_src/dependency_testing/tutorial001_py39.py similarity index 100% rename from docs_src/dependency_testing/tutorial001.py rename to docs_src/dependency_testing/tutorial001_py39.py diff --git a/docs_src/encoder/__init__.py b/docs_src/encoder/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/encoder/tutorial001.py b/docs_src/encoder/tutorial001_py39.py similarity index 100% rename from docs_src/encoder/tutorial001.py rename to docs_src/encoder/tutorial001_py39.py diff --git a/docs_src/events/__init__.py b/docs_src/events/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/events/tutorial001.py b/docs_src/events/tutorial001_py39.py similarity index 100% rename from docs_src/events/tutorial001.py rename to docs_src/events/tutorial001_py39.py diff --git a/docs_src/events/tutorial002.py b/docs_src/events/tutorial002_py39.py similarity index 100% rename from docs_src/events/tutorial002.py rename to docs_src/events/tutorial002_py39.py diff --git a/docs_src/events/tutorial003.py b/docs_src/events/tutorial003_py39.py similarity index 100% rename from docs_src/events/tutorial003.py rename to docs_src/events/tutorial003_py39.py diff --git a/docs_src/extending_openapi/__init__.py b/docs_src/extending_openapi/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/extending_openapi/tutorial001.py b/docs_src/extending_openapi/tutorial001_py39.py similarity index 100% rename from docs_src/extending_openapi/tutorial001.py rename to docs_src/extending_openapi/tutorial001_py39.py diff --git a/docs_src/extra_data_types/__init__.py b/docs_src/extra_data_types/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/extra_data_types/tutorial001_an.py b/docs_src/extra_data_types/tutorial001_an.py deleted file mode 100644 index 257d0c7c86..0000000000 --- a/docs_src/extra_data_types/tutorial001_an.py +++ /dev/null @@ -1,29 +0,0 @@ -from datetime import datetime, time, timedelta -from typing import Union -from uuid import UUID - -from fastapi import Body, FastAPI -from typing_extensions import Annotated - -app = FastAPI() - - -@app.put("/items/{item_id}") -async def read_items( - item_id: UUID, - start_datetime: Annotated[datetime, Body()], - end_datetime: Annotated[datetime, Body()], - process_after: Annotated[timedelta, Body()], - repeat_at: Annotated[Union[time, None], Body()] = None, -): - start_process = start_datetime + process_after - duration = end_datetime - start_process - return { - "item_id": item_id, - "start_datetime": start_datetime, - "end_datetime": end_datetime, - "process_after": process_after, - "repeat_at": repeat_at, - "start_process": start_process, - "duration": duration, - } diff --git a/docs_src/extra_data_types/tutorial001.py b/docs_src/extra_data_types/tutorial001_py39.py similarity index 100% rename from docs_src/extra_data_types/tutorial001.py rename to docs_src/extra_data_types/tutorial001_py39.py diff --git a/docs_src/extra_models/__init__.py b/docs_src/extra_models/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/extra_models/tutorial001_py310.py b/docs_src/extra_models/tutorial001_py310.py index 669386ae6e..cf39142e4a 100644 --- a/docs_src/extra_models/tutorial001_py310.py +++ b/docs_src/extra_models/tutorial001_py310.py @@ -30,7 +30,7 @@ def fake_password_hasher(raw_password: str): def fake_save_user(user_in: UserIn): hashed_password = fake_password_hasher(user_in.password) - user_in_db = UserInDB(**user_in.dict(), hashed_password=hashed_password) + user_in_db = UserInDB(**user_in.model_dump(), hashed_password=hashed_password) print("User saved! ..not really") return user_in_db diff --git a/docs_src/extra_models/tutorial001.py b/docs_src/extra_models/tutorial001_py39.py similarity index 91% rename from docs_src/extra_models/tutorial001.py rename to docs_src/extra_models/tutorial001_py39.py index 4be56cd2a7..327ffcdf09 100644 --- a/docs_src/extra_models/tutorial001.py +++ b/docs_src/extra_models/tutorial001_py39.py @@ -32,7 +32,7 @@ def fake_password_hasher(raw_password: str): def fake_save_user(user_in: UserIn): hashed_password = fake_password_hasher(user_in.password) - user_in_db = UserInDB(**user_in.dict(), hashed_password=hashed_password) + user_in_db = UserInDB(**user_in.model_dump(), hashed_password=hashed_password) print("User saved! ..not really") return user_in_db diff --git a/docs_src/extra_models/tutorial002_py310.py b/docs_src/extra_models/tutorial002_py310.py index 5b8ed7de3d..e8a4f5f29b 100644 --- a/docs_src/extra_models/tutorial002_py310.py +++ b/docs_src/extra_models/tutorial002_py310.py @@ -28,7 +28,7 @@ def fake_password_hasher(raw_password: str): def fake_save_user(user_in: UserIn): hashed_password = fake_password_hasher(user_in.password) - user_in_db = UserInDB(**user_in.dict(), hashed_password=hashed_password) + user_in_db = UserInDB(**user_in.model_dump(), hashed_password=hashed_password) print("User saved! ..not really") return user_in_db diff --git a/docs_src/extra_models/tutorial002.py b/docs_src/extra_models/tutorial002_py39.py similarity index 90% rename from docs_src/extra_models/tutorial002.py rename to docs_src/extra_models/tutorial002_py39.py index 70fa16441d..6543796015 100644 --- a/docs_src/extra_models/tutorial002.py +++ b/docs_src/extra_models/tutorial002_py39.py @@ -30,7 +30,7 @@ def fake_password_hasher(raw_password: str): def fake_save_user(user_in: UserIn): hashed_password = fake_password_hasher(user_in.password) - user_in_db = UserInDB(**user_in.dict(), hashed_password=hashed_password) + user_in_db = UserInDB(**user_in.model_dump(), hashed_password=hashed_password) print("User saved! ..not really") return user_in_db diff --git a/docs_src/extra_models/tutorial003.py b/docs_src/extra_models/tutorial003_py39.py similarity index 100% rename from docs_src/extra_models/tutorial003.py rename to docs_src/extra_models/tutorial003_py39.py diff --git a/docs_src/extra_models/tutorial004.py b/docs_src/extra_models/tutorial004.py deleted file mode 100644 index a8e0f7af5a..0000000000 --- a/docs_src/extra_models/tutorial004.py +++ /dev/null @@ -1,22 +0,0 @@ -from typing import List - -from fastapi import FastAPI -from pydantic import BaseModel - -app = FastAPI() - - -class Item(BaseModel): - name: str - description: str - - -items = [ - {"name": "Foo", "description": "There comes my hero"}, - {"name": "Red", "description": "It's my aeroplane"}, -] - - -@app.get("/items/", response_model=List[Item]) -async def read_items(): - return items diff --git a/docs_src/extra_models/tutorial005.py b/docs_src/extra_models/tutorial005.py deleted file mode 100644 index a81cbc2c58..0000000000 --- a/docs_src/extra_models/tutorial005.py +++ /dev/null @@ -1,10 +0,0 @@ -from typing import Dict - -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/keyword-weights/", response_model=Dict[str, float]) -async def read_keyword_weights(): - return {"foo": 2.3, "bar": 3.4} diff --git a/docs_src/first_steps/__init__.py b/docs_src/first_steps/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/first_steps/tutorial001.py b/docs_src/first_steps/tutorial001_py39.py similarity index 100% rename from docs_src/first_steps/tutorial001.py rename to docs_src/first_steps/tutorial001_py39.py diff --git a/docs_src/first_steps/tutorial002.py b/docs_src/first_steps/tutorial002.py deleted file mode 100644 index ca7d48cff9..0000000000 --- a/docs_src/first_steps/tutorial002.py +++ /dev/null @@ -1,8 +0,0 @@ -from fastapi import FastAPI - -my_awesome_api = FastAPI() - - -@my_awesome_api.get("/") -async def root(): - return {"message": "Hello World"} diff --git a/docs_src/first_steps/tutorial003.py b/docs_src/first_steps/tutorial003_py39.py similarity index 100% rename from docs_src/first_steps/tutorial003.py rename to docs_src/first_steps/tutorial003_py39.py diff --git a/docs_src/generate_clients/__init__.py b/docs_src/generate_clients/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/generate_clients/tutorial001.py b/docs_src/generate_clients/tutorial001.py deleted file mode 100644 index 2d1f91bc6c..0000000000 --- a/docs_src/generate_clients/tutorial001.py +++ /dev/null @@ -1,28 +0,0 @@ -from typing import List - -from fastapi import FastAPI -from pydantic import BaseModel - -app = FastAPI() - - -class Item(BaseModel): - name: str - price: float - - -class ResponseMessage(BaseModel): - message: str - - -@app.post("/items/", response_model=ResponseMessage) -async def create_item(item: Item): - return {"message": "item received"} - - -@app.get("/items/", response_model=List[Item]) -async def get_items(): - return [ - {"name": "Plumbus", "price": 3}, - {"name": "Portal Gun", "price": 9001}, - ] diff --git a/docs_src/generate_clients/tutorial002.py b/docs_src/generate_clients/tutorial002.py deleted file mode 100644 index bd80449af9..0000000000 --- a/docs_src/generate_clients/tutorial002.py +++ /dev/null @@ -1,38 +0,0 @@ -from typing import List - -from fastapi import FastAPI -from pydantic import BaseModel - -app = FastAPI() - - -class Item(BaseModel): - name: str - price: float - - -class ResponseMessage(BaseModel): - message: str - - -class User(BaseModel): - username: str - email: str - - -@app.post("/items/", response_model=ResponseMessage, tags=["items"]) -async def create_item(item: Item): - return {"message": "Item received"} - - -@app.get("/items/", response_model=List[Item], tags=["items"]) -async def get_items(): - return [ - {"name": "Plumbus", "price": 3}, - {"name": "Portal Gun", "price": 9001}, - ] - - -@app.post("/users/", response_model=ResponseMessage, tags=["users"]) -async def create_user(user: User): - return {"message": "User received"} diff --git a/docs_src/generate_clients/tutorial003.py b/docs_src/generate_clients/tutorial003.py deleted file mode 100644 index 49eab73a1b..0000000000 --- a/docs_src/generate_clients/tutorial003.py +++ /dev/null @@ -1,44 +0,0 @@ -from typing import List - -from fastapi import FastAPI -from fastapi.routing import APIRoute -from pydantic import BaseModel - - -def custom_generate_unique_id(route: APIRoute): - return f"{route.tags[0]}-{route.name}" - - -app = FastAPI(generate_unique_id_function=custom_generate_unique_id) - - -class Item(BaseModel): - name: str - price: float - - -class ResponseMessage(BaseModel): - message: str - - -class User(BaseModel): - username: str - email: str - - -@app.post("/items/", response_model=ResponseMessage, tags=["items"]) -async def create_item(item: Item): - return {"message": "Item received"} - - -@app.get("/items/", response_model=List[Item], tags=["items"]) -async def get_items(): - return [ - {"name": "Plumbus", "price": 3}, - {"name": "Portal Gun", "price": 9001}, - ] - - -@app.post("/users/", response_model=ResponseMessage, tags=["users"]) -async def create_user(user: User): - return {"message": "User received"} diff --git a/docs_src/generate_clients/tutorial004.py b/docs_src/generate_clients/tutorial004_py39.py similarity index 100% rename from docs_src/generate_clients/tutorial004.py rename to docs_src/generate_clients/tutorial004_py39.py diff --git a/docs_src/graphql_/__init__.py b/docs_src/graphql_/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/graphql/tutorial001.py b/docs_src/graphql_/tutorial001_py39.py similarity index 100% rename from docs_src/graphql/tutorial001.py rename to docs_src/graphql_/tutorial001_py39.py diff --git a/docs_src/handling_errors/__init__.py b/docs_src/handling_errors/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/handling_errors/tutorial001.py b/docs_src/handling_errors/tutorial001_py39.py similarity index 100% rename from docs_src/handling_errors/tutorial001.py rename to docs_src/handling_errors/tutorial001_py39.py diff --git a/docs_src/handling_errors/tutorial002.py b/docs_src/handling_errors/tutorial002_py39.py similarity index 100% rename from docs_src/handling_errors/tutorial002.py rename to docs_src/handling_errors/tutorial002_py39.py diff --git a/docs_src/handling_errors/tutorial003.py b/docs_src/handling_errors/tutorial003_py39.py similarity index 100% rename from docs_src/handling_errors/tutorial003.py rename to docs_src/handling_errors/tutorial003_py39.py diff --git a/docs_src/handling_errors/tutorial004.py b/docs_src/handling_errors/tutorial004_py39.py similarity index 70% rename from docs_src/handling_errors/tutorial004.py rename to docs_src/handling_errors/tutorial004_py39.py index 300a3834ff..ae50807e97 100644 --- a/docs_src/handling_errors/tutorial004.py +++ b/docs_src/handling_errors/tutorial004_py39.py @@ -12,8 +12,11 @@ async def http_exception_handler(request, exc): @app.exception_handler(RequestValidationError) -async def validation_exception_handler(request, exc): - return PlainTextResponse(str(exc), status_code=400) +async def validation_exception_handler(request, exc: RequestValidationError): + message = "Validation errors:" + for error in exc.errors(): + message += f"\nField: {error['loc']}, Error: {error['msg']}" + return PlainTextResponse(message, status_code=400) @app.get("/items/{item_id}") diff --git a/docs_src/handling_errors/tutorial005.py b/docs_src/handling_errors/tutorial005_py39.py similarity index 100% rename from docs_src/handling_errors/tutorial005.py rename to docs_src/handling_errors/tutorial005_py39.py diff --git a/docs_src/handling_errors/tutorial006.py b/docs_src/handling_errors/tutorial006_py39.py similarity index 100% rename from docs_src/handling_errors/tutorial006.py rename to docs_src/handling_errors/tutorial006_py39.py diff --git a/docs_src/header_param_models/__init__.py b/docs_src/header_param_models/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/header_param_models/tutorial001.py b/docs_src/header_param_models/tutorial001.py deleted file mode 100644 index 4caaba87b9..0000000000 --- a/docs_src/header_param_models/tutorial001.py +++ /dev/null @@ -1,19 +0,0 @@ -from typing import List, Union - -from fastapi import FastAPI, Header -from pydantic import BaseModel - -app = FastAPI() - - -class CommonHeaders(BaseModel): - host: str - save_data: bool - if_modified_since: Union[str, None] = None - traceparent: Union[str, None] = None - x_tag: List[str] = [] - - -@app.get("/items/") -async def read_items(headers: CommonHeaders = Header()): - return headers diff --git a/docs_src/header_param_models/tutorial001_an.py b/docs_src/header_param_models/tutorial001_an.py deleted file mode 100644 index b55c6b56b1..0000000000 --- a/docs_src/header_param_models/tutorial001_an.py +++ /dev/null @@ -1,20 +0,0 @@ -from typing import List, Union - -from fastapi import FastAPI, Header -from pydantic import BaseModel -from typing_extensions import Annotated - -app = FastAPI() - - -class CommonHeaders(BaseModel): - host: str - save_data: bool - if_modified_since: Union[str, None] = None - traceparent: Union[str, None] = None - x_tag: List[str] = [] - - -@app.get("/items/") -async def read_items(headers: Annotated[CommonHeaders, Header()]): - return headers diff --git a/docs_src/header_param_models/tutorial002.py b/docs_src/header_param_models/tutorial002.py deleted file mode 100644 index 3f9aac58d2..0000000000 --- a/docs_src/header_param_models/tutorial002.py +++ /dev/null @@ -1,21 +0,0 @@ -from typing import List, Union - -from fastapi import FastAPI, Header -from pydantic import BaseModel - -app = FastAPI() - - -class CommonHeaders(BaseModel): - model_config = {"extra": "forbid"} - - host: str - save_data: bool - if_modified_since: Union[str, None] = None - traceparent: Union[str, None] = None - x_tag: List[str] = [] - - -@app.get("/items/") -async def read_items(headers: CommonHeaders = Header()): - return headers diff --git a/docs_src/header_param_models/tutorial002_an.py b/docs_src/header_param_models/tutorial002_an.py deleted file mode 100644 index 771135d770..0000000000 --- a/docs_src/header_param_models/tutorial002_an.py +++ /dev/null @@ -1,22 +0,0 @@ -from typing import List, Union - -from fastapi import FastAPI, Header -from pydantic import BaseModel -from typing_extensions import Annotated - -app = FastAPI() - - -class CommonHeaders(BaseModel): - model_config = {"extra": "forbid"} - - host: str - save_data: bool - if_modified_since: Union[str, None] = None - traceparent: Union[str, None] = None - x_tag: List[str] = [] - - -@app.get("/items/") -async def read_items(headers: Annotated[CommonHeaders, Header()]): - return headers diff --git a/docs_src/header_param_models/tutorial002_pv1.py b/docs_src/header_param_models/tutorial002_pv1.py deleted file mode 100644 index 7e56cd993b..0000000000 --- a/docs_src/header_param_models/tutorial002_pv1.py +++ /dev/null @@ -1,22 +0,0 @@ -from typing import List, Union - -from fastapi import FastAPI, Header -from pydantic import BaseModel - -app = FastAPI() - - -class CommonHeaders(BaseModel): - class Config: - extra = "forbid" - - host: str - save_data: bool - if_modified_since: Union[str, None] = None - traceparent: Union[str, None] = None - x_tag: List[str] = [] - - -@app.get("/items/") -async def read_items(headers: CommonHeaders = Header()): - return headers diff --git a/docs_src/header_param_models/tutorial002_pv1_an.py b/docs_src/header_param_models/tutorial002_pv1_an.py deleted file mode 100644 index 236778231a..0000000000 --- a/docs_src/header_param_models/tutorial002_pv1_an.py +++ /dev/null @@ -1,23 +0,0 @@ -from typing import List, Union - -from fastapi import FastAPI, Header -from pydantic import BaseModel -from typing_extensions import Annotated - -app = FastAPI() - - -class CommonHeaders(BaseModel): - class Config: - extra = "forbid" - - host: str - save_data: bool - if_modified_since: Union[str, None] = None - traceparent: Union[str, None] = None - x_tag: List[str] = [] - - -@app.get("/items/") -async def read_items(headers: Annotated[CommonHeaders, Header()]): - return headers diff --git a/docs_src/header_param_models/tutorial002_pv1_an_py310.py b/docs_src/header_param_models/tutorial002_pv1_an_py310.py deleted file mode 100644 index e99e24ea55..0000000000 --- a/docs_src/header_param_models/tutorial002_pv1_an_py310.py +++ /dev/null @@ -1,22 +0,0 @@ -from typing import Annotated - -from fastapi import FastAPI, Header -from pydantic import BaseModel - -app = FastAPI() - - -class CommonHeaders(BaseModel): - class Config: - extra = "forbid" - - host: str - save_data: bool - if_modified_since: str | None = None - traceparent: str | None = None - x_tag: list[str] = [] - - -@app.get("/items/") -async def read_items(headers: Annotated[CommonHeaders, Header()]): - return headers diff --git a/docs_src/header_param_models/tutorial002_pv1_an_py39.py b/docs_src/header_param_models/tutorial002_pv1_an_py39.py deleted file mode 100644 index 18398b726c..0000000000 --- a/docs_src/header_param_models/tutorial002_pv1_an_py39.py +++ /dev/null @@ -1,22 +0,0 @@ -from typing import Annotated, Union - -from fastapi import FastAPI, Header -from pydantic import BaseModel - -app = FastAPI() - - -class CommonHeaders(BaseModel): - class Config: - extra = "forbid" - - host: str - save_data: bool - if_modified_since: Union[str, None] = None - traceparent: Union[str, None] = None - x_tag: list[str] = [] - - -@app.get("/items/") -async def read_items(headers: Annotated[CommonHeaders, Header()]): - return headers diff --git a/docs_src/header_param_models/tutorial002_pv1_py310.py b/docs_src/header_param_models/tutorial002_pv1_py310.py deleted file mode 100644 index 3dbff9d7bf..0000000000 --- a/docs_src/header_param_models/tutorial002_pv1_py310.py +++ /dev/null @@ -1,20 +0,0 @@ -from fastapi import FastAPI, Header -from pydantic import BaseModel - -app = FastAPI() - - -class CommonHeaders(BaseModel): - class Config: - extra = "forbid" - - host: str - save_data: bool - if_modified_since: str | None = None - traceparent: str | None = None - x_tag: list[str] = [] - - -@app.get("/items/") -async def read_items(headers: CommonHeaders = Header()): - return headers diff --git a/docs_src/header_param_models/tutorial002_pv1_py39.py b/docs_src/header_param_models/tutorial002_pv1_py39.py deleted file mode 100644 index 86e19be0d1..0000000000 --- a/docs_src/header_param_models/tutorial002_pv1_py39.py +++ /dev/null @@ -1,22 +0,0 @@ -from typing import Union - -from fastapi import FastAPI, Header -from pydantic import BaseModel - -app = FastAPI() - - -class CommonHeaders(BaseModel): - class Config: - extra = "forbid" - - host: str - save_data: bool - if_modified_since: Union[str, None] = None - traceparent: Union[str, None] = None - x_tag: list[str] = [] - - -@app.get("/items/") -async def read_items(headers: CommonHeaders = Header()): - return headers diff --git a/docs_src/header_param_models/tutorial003.py b/docs_src/header_param_models/tutorial003.py deleted file mode 100644 index dc2eb74bd1..0000000000 --- a/docs_src/header_param_models/tutorial003.py +++ /dev/null @@ -1,19 +0,0 @@ -from typing import List, Union - -from fastapi import FastAPI, Header -from pydantic import BaseModel - -app = FastAPI() - - -class CommonHeaders(BaseModel): - host: str - save_data: bool - if_modified_since: Union[str, None] = None - traceparent: Union[str, None] = None - x_tag: List[str] = [] - - -@app.get("/items/") -async def read_items(headers: CommonHeaders = Header(convert_underscores=False)): - return headers diff --git a/docs_src/header_param_models/tutorial003_an.py b/docs_src/header_param_models/tutorial003_an.py deleted file mode 100644 index e3edb11891..0000000000 --- a/docs_src/header_param_models/tutorial003_an.py +++ /dev/null @@ -1,22 +0,0 @@ -from typing import List, Union - -from fastapi import FastAPI, Header -from pydantic import BaseModel -from typing_extensions import Annotated - -app = FastAPI() - - -class CommonHeaders(BaseModel): - host: str - save_data: bool - if_modified_since: Union[str, None] = None - traceparent: Union[str, None] = None - x_tag: List[str] = [] - - -@app.get("/items/") -async def read_items( - headers: Annotated[CommonHeaders, Header(convert_underscores=False)], -): - return headers diff --git a/docs_src/header_params/__init__.py b/docs_src/header_params/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/header_params/tutorial001_an.py b/docs_src/header_params/tutorial001_an.py deleted file mode 100644 index 816c000862..0000000000 --- a/docs_src/header_params/tutorial001_an.py +++ /dev/null @@ -1,11 +0,0 @@ -from typing import Union - -from fastapi import FastAPI, Header -from typing_extensions import Annotated - -app = FastAPI() - - -@app.get("/items/") -async def read_items(user_agent: Annotated[Union[str, None], Header()] = None): - return {"User-Agent": user_agent} diff --git a/docs_src/header_params/tutorial001.py b/docs_src/header_params/tutorial001_py39.py similarity index 100% rename from docs_src/header_params/tutorial001.py rename to docs_src/header_params/tutorial001_py39.py diff --git a/docs_src/header_params/tutorial002_an.py b/docs_src/header_params/tutorial002_an.py deleted file mode 100644 index 82fe49ba2b..0000000000 --- a/docs_src/header_params/tutorial002_an.py +++ /dev/null @@ -1,15 +0,0 @@ -from typing import Union - -from fastapi import FastAPI, Header -from typing_extensions import Annotated - -app = FastAPI() - - -@app.get("/items/") -async def read_items( - strange_header: Annotated[ - Union[str, None], Header(convert_underscores=False) - ] = None, -): - return {"strange_header": strange_header} diff --git a/docs_src/header_params/tutorial002.py b/docs_src/header_params/tutorial002_py39.py similarity index 100% rename from docs_src/header_params/tutorial002.py rename to docs_src/header_params/tutorial002_py39.py diff --git a/docs_src/header_params/tutorial003.py b/docs_src/header_params/tutorial003.py deleted file mode 100644 index a61314aedb..0000000000 --- a/docs_src/header_params/tutorial003.py +++ /dev/null @@ -1,10 +0,0 @@ -from typing import List, Union - -from fastapi import FastAPI, Header - -app = FastAPI() - - -@app.get("/items/") -async def read_items(x_token: Union[List[str], None] = Header(default=None)): - return {"X-Token values": x_token} diff --git a/docs_src/header_params/tutorial003_an.py b/docs_src/header_params/tutorial003_an.py deleted file mode 100644 index 5406fd1f88..0000000000 --- a/docs_src/header_params/tutorial003_an.py +++ /dev/null @@ -1,11 +0,0 @@ -from typing import List, Union - -from fastapi import FastAPI, Header -from typing_extensions import Annotated - -app = FastAPI() - - -@app.get("/items/") -async def read_items(x_token: Annotated[Union[List[str], None], Header()] = None): - return {"X-Token values": x_token} diff --git a/docs_src/header_params/tutorial003_an_py39.py b/docs_src/header_params/tutorial003_an_py39.py index c1dd499611..5aad89407e 100644 --- a/docs_src/header_params/tutorial003_an_py39.py +++ b/docs_src/header_params/tutorial003_an_py39.py @@ -1,4 +1,4 @@ -from typing import Annotated, List, Union +from typing import Annotated, Union from fastapi import FastAPI, Header @@ -6,5 +6,5 @@ app = FastAPI() @app.get("/items/") -async def read_items(x_token: Annotated[Union[List[str], None], Header()] = None): +async def read_items(x_token: Annotated[Union[list[str], None], Header()] = None): return {"X-Token values": x_token} diff --git a/docs_src/metadata/__init__.py b/docs_src/metadata/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/metadata/tutorial001_1.py b/docs_src/metadata/tutorial001_1_py39.py similarity index 100% rename from docs_src/metadata/tutorial001_1.py rename to docs_src/metadata/tutorial001_1_py39.py diff --git a/docs_src/metadata/tutorial001.py b/docs_src/metadata/tutorial001_py39.py similarity index 100% rename from docs_src/metadata/tutorial001.py rename to docs_src/metadata/tutorial001_py39.py diff --git a/docs_src/metadata/tutorial002.py b/docs_src/metadata/tutorial002_py39.py similarity index 100% rename from docs_src/metadata/tutorial002.py rename to docs_src/metadata/tutorial002_py39.py diff --git a/docs_src/metadata/tutorial003.py b/docs_src/metadata/tutorial003_py39.py similarity index 100% rename from docs_src/metadata/tutorial003.py rename to docs_src/metadata/tutorial003_py39.py diff --git a/docs_src/metadata/tutorial004.py b/docs_src/metadata/tutorial004_py39.py similarity index 100% rename from docs_src/metadata/tutorial004.py rename to docs_src/metadata/tutorial004_py39.py diff --git a/docs_src/middleware/__init__.py b/docs_src/middleware/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/middleware/tutorial001.py b/docs_src/middleware/tutorial001_py39.py similarity index 100% rename from docs_src/middleware/tutorial001.py rename to docs_src/middleware/tutorial001_py39.py diff --git a/docs_src/openapi_callbacks/__init__.py b/docs_src/openapi_callbacks/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/openapi_callbacks/tutorial001_py310.py b/docs_src/openapi_callbacks/tutorial001_py310.py new file mode 100644 index 0000000000..3efe0ee25f --- /dev/null +++ b/docs_src/openapi_callbacks/tutorial001_py310.py @@ -0,0 +1,51 @@ +from fastapi import APIRouter, FastAPI +from pydantic import BaseModel, HttpUrl + +app = FastAPI() + + +class Invoice(BaseModel): + id: str + title: str | None = None + customer: str + total: float + + +class InvoiceEvent(BaseModel): + description: str + paid: bool + + +class InvoiceEventReceived(BaseModel): + ok: bool + + +invoices_callback_router = APIRouter() + + +@invoices_callback_router.post( + "{$callback_url}/invoices/{$request.body.id}", response_model=InvoiceEventReceived +) +def invoice_notification(body: InvoiceEvent): + pass + + +@app.post("/invoices/", callbacks=invoices_callback_router.routes) +def create_invoice(invoice: Invoice, callback_url: HttpUrl | None = None): + """ + Create an invoice. + + This will (let's imagine) let the API user (some external developer) create an + invoice. + + And this path operation will: + + * Send the invoice to the client. + * Collect the money from the client. + * Send a notification back to the API user (the external developer), as a callback. + * At this point is that the API will somehow send a POST request to the + external API with the notification of the invoice event + (e.g. "payment successful"). + """ + # Send the invoice, collect the money, send the notification (the callback) + return {"msg": "Invoice received"} diff --git a/docs_src/openapi_callbacks/tutorial001.py b/docs_src/openapi_callbacks/tutorial001_py39.py similarity index 100% rename from docs_src/openapi_callbacks/tutorial001.py rename to docs_src/openapi_callbacks/tutorial001_py39.py diff --git a/docs_src/openapi_webhooks/__init__.py b/docs_src/openapi_webhooks/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/openapi_webhooks/tutorial001.py b/docs_src/openapi_webhooks/tutorial001_py39.py similarity index 100% rename from docs_src/openapi_webhooks/tutorial001.py rename to docs_src/openapi_webhooks/tutorial001_py39.py diff --git a/docs_src/path_operation_advanced_configuration/__init__.py b/docs_src/path_operation_advanced_configuration/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/path_operation_advanced_configuration/tutorial001.py b/docs_src/path_operation_advanced_configuration/tutorial001_py39.py similarity index 100% rename from docs_src/path_operation_advanced_configuration/tutorial001.py rename to docs_src/path_operation_advanced_configuration/tutorial001_py39.py diff --git a/docs_src/path_operation_advanced_configuration/tutorial002.py b/docs_src/path_operation_advanced_configuration/tutorial002_py39.py similarity index 100% rename from docs_src/path_operation_advanced_configuration/tutorial002.py rename to docs_src/path_operation_advanced_configuration/tutorial002_py39.py diff --git a/docs_src/path_operation_advanced_configuration/tutorial003.py b/docs_src/path_operation_advanced_configuration/tutorial003_py39.py similarity index 100% rename from docs_src/path_operation_advanced_configuration/tutorial003.py rename to docs_src/path_operation_advanced_configuration/tutorial003_py39.py diff --git a/docs_src/path_operation_configuration/tutorial004.py b/docs_src/path_operation_advanced_configuration/tutorial004_py310.py similarity index 80% rename from docs_src/path_operation_configuration/tutorial004.py rename to docs_src/path_operation_advanced_configuration/tutorial004_py310.py index 8f865c58a6..a815a564b7 100644 --- a/docs_src/path_operation_configuration/tutorial004.py +++ b/docs_src/path_operation_advanced_configuration/tutorial004_py310.py @@ -1,5 +1,3 @@ -from typing import Set, Union - from fastapi import FastAPI from pydantic import BaseModel @@ -8,10 +6,10 @@ app = FastAPI() class Item(BaseModel): name: str - description: Union[str, None] = None + description: str | None = None price: float - tax: Union[float, None] = None - tags: Set[str] = set() + tax: float | None = None + tags: set[str] = set() @app.post("/items/", response_model=Item, summary="Create an item") @@ -24,5 +22,7 @@ async def create_item(item: Item): - **price**: required - **tax**: if the item doesn't have tax, you can omit this - **tags**: a set of unique tag strings for this item + \f + :param item: User input. """ return item diff --git a/docs_src/path_operation_advanced_configuration/tutorial004.py b/docs_src/path_operation_advanced_configuration/tutorial004_py39.py similarity index 92% rename from docs_src/path_operation_advanced_configuration/tutorial004.py rename to docs_src/path_operation_advanced_configuration/tutorial004_py39.py index a3aad4ac40..d5fe6705ca 100644 --- a/docs_src/path_operation_advanced_configuration/tutorial004.py +++ b/docs_src/path_operation_advanced_configuration/tutorial004_py39.py @@ -1,4 +1,4 @@ -from typing import Set, Union +from typing import Union from fastapi import FastAPI from pydantic import BaseModel @@ -11,7 +11,7 @@ class Item(BaseModel): description: Union[str, None] = None price: float tax: Union[float, None] = None - tags: Set[str] = set() + tags: set[str] = set() @app.post("/items/", response_model=Item, summary="Create an item") diff --git a/docs_src/path_operation_advanced_configuration/tutorial005.py b/docs_src/path_operation_advanced_configuration/tutorial005_py39.py similarity index 100% rename from docs_src/path_operation_advanced_configuration/tutorial005.py rename to docs_src/path_operation_advanced_configuration/tutorial005_py39.py diff --git a/docs_src/path_operation_advanced_configuration/tutorial006.py b/docs_src/path_operation_advanced_configuration/tutorial006_py39.py similarity index 100% rename from docs_src/path_operation_advanced_configuration/tutorial006.py rename to docs_src/path_operation_advanced_configuration/tutorial006_py39.py diff --git a/docs_src/path_operation_advanced_configuration/tutorial007_pv1.py b/docs_src/path_operation_advanced_configuration/tutorial007_pv1_py39.py similarity index 88% rename from docs_src/path_operation_advanced_configuration/tutorial007_pv1.py rename to docs_src/path_operation_advanced_configuration/tutorial007_pv1_py39.py index d51752bb87..849f648e12 100644 --- a/docs_src/path_operation_advanced_configuration/tutorial007_pv1.py +++ b/docs_src/path_operation_advanced_configuration/tutorial007_pv1_py39.py @@ -1,15 +1,13 @@ -from typing import List - import yaml from fastapi import FastAPI, HTTPException, Request -from pydantic import BaseModel, ValidationError +from pydantic.v1 import BaseModel, ValidationError app = FastAPI() class Item(BaseModel): name: str - tags: List[str] + tags: list[str] @app.post( diff --git a/docs_src/path_operation_advanced_configuration/tutorial007.py b/docs_src/path_operation_advanced_configuration/tutorial007_py39.py similarity index 94% rename from docs_src/path_operation_advanced_configuration/tutorial007.py rename to docs_src/path_operation_advanced_configuration/tutorial007_py39.py index 54e2e9399e..ff64ef7923 100644 --- a/docs_src/path_operation_advanced_configuration/tutorial007.py +++ b/docs_src/path_operation_advanced_configuration/tutorial007_py39.py @@ -1,5 +1,3 @@ -from typing import List - import yaml from fastapi import FastAPI, HTTPException, Request from pydantic import BaseModel, ValidationError @@ -9,7 +7,7 @@ app = FastAPI() class Item(BaseModel): name: str - tags: List[str] + tags: list[str] @app.post( diff --git a/docs_src/path_operation_configuration/__init__.py b/docs_src/path_operation_configuration/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/path_operation_configuration/tutorial001.py b/docs_src/path_operation_configuration/tutorial001.py deleted file mode 100644 index 83fd8377ab..0000000000 --- a/docs_src/path_operation_configuration/tutorial001.py +++ /dev/null @@ -1,19 +0,0 @@ -from typing import Set, Union - -from fastapi import FastAPI, status -from pydantic import BaseModel - -app = FastAPI() - - -class Item(BaseModel): - name: str - description: Union[str, None] = None - price: float - tax: Union[float, None] = None - tags: Set[str] = set() - - -@app.post("/items/", response_model=Item, status_code=status.HTTP_201_CREATED) -async def create_item(item: Item): - return item diff --git a/docs_src/path_operation_configuration/tutorial002.py b/docs_src/path_operation_configuration/tutorial002.py deleted file mode 100644 index 798b0c2311..0000000000 --- a/docs_src/path_operation_configuration/tutorial002.py +++ /dev/null @@ -1,29 +0,0 @@ -from typing import Set, Union - -from fastapi import FastAPI -from pydantic import BaseModel - -app = FastAPI() - - -class Item(BaseModel): - name: str - description: Union[str, None] = None - price: float - tax: Union[float, None] = None - tags: Set[str] = set() - - -@app.post("/items/", response_model=Item, tags=["items"]) -async def create_item(item: Item): - return item - - -@app.get("/items/", tags=["items"]) -async def read_items(): - return [{"name": "Foo", "price": 42}] - - -@app.get("/users/", tags=["users"]) -async def read_users(): - return [{"username": "johndoe"}] diff --git a/docs_src/path_operation_configuration/tutorial002b.py b/docs_src/path_operation_configuration/tutorial002b_py39.py similarity index 100% rename from docs_src/path_operation_configuration/tutorial002b.py rename to docs_src/path_operation_configuration/tutorial002b_py39.py diff --git a/docs_src/path_operation_configuration/tutorial003.py b/docs_src/path_operation_configuration/tutorial003.py deleted file mode 100644 index 26bf7dabae..0000000000 --- a/docs_src/path_operation_configuration/tutorial003.py +++ /dev/null @@ -1,24 +0,0 @@ -from typing import Set, Union - -from fastapi import FastAPI -from pydantic import BaseModel - -app = FastAPI() - - -class Item(BaseModel): - name: str - description: Union[str, None] = None - price: float - tax: Union[float, None] = None - tags: Set[str] = set() - - -@app.post( - "/items/", - response_model=Item, - summary="Create an item", - description="Create an item with all the information, name, description, price, tax and a set of unique tags", -) -async def create_item(item: Item): - return item diff --git a/docs_src/path_operation_configuration/tutorial005.py b/docs_src/path_operation_configuration/tutorial005.py deleted file mode 100644 index 2c1be4a34b..0000000000 --- a/docs_src/path_operation_configuration/tutorial005.py +++ /dev/null @@ -1,33 +0,0 @@ -from typing import Set, Union - -from fastapi import FastAPI -from pydantic import BaseModel - -app = FastAPI() - - -class Item(BaseModel): - name: str - description: Union[str, None] = None - price: float - tax: Union[float, None] = None - tags: Set[str] = set() - - -@app.post( - "/items/", - response_model=Item, - summary="Create an item", - response_description="The created item", -) -async def create_item(item: Item): - """ - Create an item with all the information: - - - **name**: each item must have a name - - **description**: a long description - - **price**: required - - **tax**: if the item doesn't have tax, you can omit this - - **tags**: a set of unique tag strings for this item - """ - return item diff --git a/docs_src/path_operation_configuration/tutorial006.py b/docs_src/path_operation_configuration/tutorial006_py39.py similarity index 100% rename from docs_src/path_operation_configuration/tutorial006.py rename to docs_src/path_operation_configuration/tutorial006_py39.py diff --git a/docs_src/path_params/__init__.py b/docs_src/path_params/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/path_params/tutorial001.py b/docs_src/path_params/tutorial001_py39.py similarity index 100% rename from docs_src/path_params/tutorial001.py rename to docs_src/path_params/tutorial001_py39.py diff --git a/docs_src/path_params/tutorial002.py b/docs_src/path_params/tutorial002_py39.py similarity index 100% rename from docs_src/path_params/tutorial002.py rename to docs_src/path_params/tutorial002_py39.py diff --git a/docs_src/path_params/tutorial003.py b/docs_src/path_params/tutorial003_py39.py similarity index 100% rename from docs_src/path_params/tutorial003.py rename to docs_src/path_params/tutorial003_py39.py diff --git a/docs_src/path_params/tutorial003b.py b/docs_src/path_params/tutorial003b_py39.py similarity index 100% rename from docs_src/path_params/tutorial003b.py rename to docs_src/path_params/tutorial003b_py39.py diff --git a/docs_src/path_params/tutorial004.py b/docs_src/path_params/tutorial004_py39.py similarity index 100% rename from docs_src/path_params/tutorial004.py rename to docs_src/path_params/tutorial004_py39.py diff --git a/docs_src/path_params/tutorial005.py b/docs_src/path_params/tutorial005_py39.py similarity index 100% rename from docs_src/path_params/tutorial005.py rename to docs_src/path_params/tutorial005_py39.py diff --git a/docs_src/path_params_numeric_validations/__init__.py b/docs_src/path_params_numeric_validations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/path_params_numeric_validations/tutorial001_an.py b/docs_src/path_params_numeric_validations/tutorial001_an.py deleted file mode 100644 index 621be7b045..0000000000 --- a/docs_src/path_params_numeric_validations/tutorial001_an.py +++ /dev/null @@ -1,17 +0,0 @@ -from typing import Union - -from fastapi import FastAPI, Path, Query -from typing_extensions import Annotated - -app = FastAPI() - - -@app.get("/items/{item_id}") -async def read_items( - item_id: Annotated[int, Path(title="The ID of the item to get")], - q: Annotated[Union[str, None], Query(alias="item-query")] = None, -): - results = {"item_id": item_id} - if q: - results.update({"q": q}) - return results diff --git a/docs_src/path_params_numeric_validations/tutorial001.py b/docs_src/path_params_numeric_validations/tutorial001_py39.py similarity index 100% rename from docs_src/path_params_numeric_validations/tutorial001.py rename to docs_src/path_params_numeric_validations/tutorial001_py39.py diff --git a/docs_src/path_params_numeric_validations/tutorial002_an.py b/docs_src/path_params_numeric_validations/tutorial002_an.py deleted file mode 100644 index 322f8cf0bb..0000000000 --- a/docs_src/path_params_numeric_validations/tutorial002_an.py +++ /dev/null @@ -1,14 +0,0 @@ -from fastapi import FastAPI, Path -from typing_extensions import Annotated - -app = FastAPI() - - -@app.get("/items/{item_id}") -async def read_items( - q: str, item_id: Annotated[int, Path(title="The ID of the item to get")] -): - results = {"item_id": item_id} - if q: - results.update({"q": q}) - return results diff --git a/docs_src/path_params_numeric_validations/tutorial002.py b/docs_src/path_params_numeric_validations/tutorial002_py39.py similarity index 100% rename from docs_src/path_params_numeric_validations/tutorial002.py rename to docs_src/path_params_numeric_validations/tutorial002_py39.py diff --git a/docs_src/path_params_numeric_validations/tutorial003_an.py b/docs_src/path_params_numeric_validations/tutorial003_an.py deleted file mode 100644 index d0fa8b3db9..0000000000 --- a/docs_src/path_params_numeric_validations/tutorial003_an.py +++ /dev/null @@ -1,14 +0,0 @@ -from fastapi import FastAPI, Path -from typing_extensions import Annotated - -app = FastAPI() - - -@app.get("/items/{item_id}") -async def read_items( - item_id: Annotated[int, Path(title="The ID of the item to get")], q: str -): - results = {"item_id": item_id} - if q: - results.update({"q": q}) - return results diff --git a/docs_src/path_params_numeric_validations/tutorial003.py b/docs_src/path_params_numeric_validations/tutorial003_py39.py similarity index 100% rename from docs_src/path_params_numeric_validations/tutorial003.py rename to docs_src/path_params_numeric_validations/tutorial003_py39.py diff --git a/docs_src/path_params_numeric_validations/tutorial004_an.py b/docs_src/path_params_numeric_validations/tutorial004_an.py deleted file mode 100644 index ffc50f6c5b..0000000000 --- a/docs_src/path_params_numeric_validations/tutorial004_an.py +++ /dev/null @@ -1,14 +0,0 @@ -from fastapi import FastAPI, Path -from typing_extensions import Annotated - -app = FastAPI() - - -@app.get("/items/{item_id}") -async def read_items( - item_id: Annotated[int, Path(title="The ID of the item to get", ge=1)], q: str -): - results = {"item_id": item_id} - if q: - results.update({"q": q}) - return results diff --git a/docs_src/path_params_numeric_validations/tutorial004.py b/docs_src/path_params_numeric_validations/tutorial004_py39.py similarity index 100% rename from docs_src/path_params_numeric_validations/tutorial004.py rename to docs_src/path_params_numeric_validations/tutorial004_py39.py diff --git a/docs_src/path_params_numeric_validations/tutorial005_an.py b/docs_src/path_params_numeric_validations/tutorial005_an.py deleted file mode 100644 index 433c69129f..0000000000 --- a/docs_src/path_params_numeric_validations/tutorial005_an.py +++ /dev/null @@ -1,15 +0,0 @@ -from fastapi import FastAPI, Path -from typing_extensions import Annotated - -app = FastAPI() - - -@app.get("/items/{item_id}") -async def read_items( - item_id: Annotated[int, Path(title="The ID of the item to get", gt=0, le=1000)], - q: str, -): - results = {"item_id": item_id} - if q: - results.update({"q": q}) - return results diff --git a/docs_src/path_params_numeric_validations/tutorial005.py b/docs_src/path_params_numeric_validations/tutorial005_py39.py similarity index 100% rename from docs_src/path_params_numeric_validations/tutorial005.py rename to docs_src/path_params_numeric_validations/tutorial005_py39.py diff --git a/docs_src/path_params_numeric_validations/tutorial006_an.py b/docs_src/path_params_numeric_validations/tutorial006_an.py deleted file mode 100644 index ac47325732..0000000000 --- a/docs_src/path_params_numeric_validations/tutorial006_an.py +++ /dev/null @@ -1,19 +0,0 @@ -from fastapi import FastAPI, Path, Query -from typing_extensions import Annotated - -app = FastAPI() - - -@app.get("/items/{item_id}") -async def read_items( - *, - item_id: Annotated[int, Path(title="The ID of the item to get", ge=0, le=1000)], - q: str, - size: Annotated[float, Query(gt=0, lt=10.5)], -): - results = {"item_id": item_id} - if q: - results.update({"q": q}) - if size: - results.update({"size": size}) - return results diff --git a/docs_src/path_params_numeric_validations/tutorial006.py b/docs_src/path_params_numeric_validations/tutorial006_py39.py similarity index 100% rename from docs_src/path_params_numeric_validations/tutorial006.py rename to docs_src/path_params_numeric_validations/tutorial006_py39.py diff --git a/docs_src/pydantic_v1_in_v2/__init__.py b/docs_src/pydantic_v1_in_v2/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/pydantic_v1_in_v2/tutorial001_an.py b/docs_src/pydantic_v1_in_v2/tutorial001_an_py39.py similarity index 100% rename from docs_src/pydantic_v1_in_v2/tutorial001_an.py rename to docs_src/pydantic_v1_in_v2/tutorial001_an_py39.py diff --git a/docs_src/pydantic_v1_in_v2/tutorial002_an.py b/docs_src/pydantic_v1_in_v2/tutorial002_an_py39.py similarity index 100% rename from docs_src/pydantic_v1_in_v2/tutorial002_an.py rename to docs_src/pydantic_v1_in_v2/tutorial002_an_py39.py diff --git a/docs_src/pydantic_v1_in_v2/tutorial003_an.py b/docs_src/pydantic_v1_in_v2/tutorial003_an_py39.py similarity index 100% rename from docs_src/pydantic_v1_in_v2/tutorial003_an.py rename to docs_src/pydantic_v1_in_v2/tutorial003_an_py39.py diff --git a/docs_src/pydantic_v1_in_v2/tutorial004_an.py b/docs_src/pydantic_v1_in_v2/tutorial004_an.py deleted file mode 100644 index cca8a9ea80..0000000000 --- a/docs_src/pydantic_v1_in_v2/tutorial004_an.py +++ /dev/null @@ -1,20 +0,0 @@ -from typing import Union - -from fastapi import FastAPI -from fastapi.temp_pydantic_v1_params import Body -from pydantic.v1 import BaseModel -from typing_extensions import Annotated - - -class Item(BaseModel): - name: str - description: Union[str, None] = None - size: float - - -app = FastAPI() - - -@app.post("/items/") -async def create_item(item: Annotated[Item, Body(embed=True)]) -> Item: - return item diff --git a/docs_src/python_types/__init__.py b/docs_src/python_types/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/python_types/tutorial001.py b/docs_src/python_types/tutorial001_py39.py similarity index 100% rename from docs_src/python_types/tutorial001.py rename to docs_src/python_types/tutorial001_py39.py diff --git a/docs_src/python_types/tutorial002.py b/docs_src/python_types/tutorial002_py39.py similarity index 100% rename from docs_src/python_types/tutorial002.py rename to docs_src/python_types/tutorial002_py39.py diff --git a/docs_src/python_types/tutorial003.py b/docs_src/python_types/tutorial003_py39.py similarity index 100% rename from docs_src/python_types/tutorial003.py rename to docs_src/python_types/tutorial003_py39.py diff --git a/docs_src/python_types/tutorial004.py b/docs_src/python_types/tutorial004_py39.py similarity index 100% rename from docs_src/python_types/tutorial004.py rename to docs_src/python_types/tutorial004_py39.py diff --git a/docs_src/python_types/tutorial005.py b/docs_src/python_types/tutorial005_py39.py similarity index 59% rename from docs_src/python_types/tutorial005.py rename to docs_src/python_types/tutorial005_py39.py index 08ab44a941..6c8edb0ec4 100644 --- a/docs_src/python_types/tutorial005.py +++ b/docs_src/python_types/tutorial005_py39.py @@ -1,2 +1,2 @@ def get_items(item_a: str, item_b: int, item_c: float, item_d: bool, item_e: bytes): - return item_a, item_b, item_c, item_d, item_d, item_e + return item_a, item_b, item_c, item_d, item_e diff --git a/docs_src/python_types/tutorial006.py b/docs_src/python_types/tutorial006.py deleted file mode 100644 index 87394ecb0c..0000000000 --- a/docs_src/python_types/tutorial006.py +++ /dev/null @@ -1,6 +0,0 @@ -from typing import List - - -def process_items(items: List[str]): - for item in items: - print(item) diff --git a/docs_src/python_types/tutorial007.py b/docs_src/python_types/tutorial007.py deleted file mode 100644 index 5b13f15494..0000000000 --- a/docs_src/python_types/tutorial007.py +++ /dev/null @@ -1,5 +0,0 @@ -from typing import Set, Tuple - - -def process_items(items_t: Tuple[int, int, str], items_s: Set[bytes]): - return items_t, items_s diff --git a/docs_src/python_types/tutorial008.py b/docs_src/python_types/tutorial008.py deleted file mode 100644 index 9fb1043bb8..0000000000 --- a/docs_src/python_types/tutorial008.py +++ /dev/null @@ -1,7 +0,0 @@ -from typing import Dict - - -def process_items(prices: Dict[str, float]): - for item_name, item_price in prices.items(): - print(item_name) - print(item_price) diff --git a/docs_src/python_types/tutorial008b.py b/docs_src/python_types/tutorial008b_py39.py similarity index 100% rename from docs_src/python_types/tutorial008b.py rename to docs_src/python_types/tutorial008b_py39.py diff --git a/docs_src/python_types/tutorial009.py b/docs_src/python_types/tutorial009_py39.py similarity index 100% rename from docs_src/python_types/tutorial009.py rename to docs_src/python_types/tutorial009_py39.py diff --git a/docs_src/python_types/tutorial009b.py b/docs_src/python_types/tutorial009b_py39.py similarity index 100% rename from docs_src/python_types/tutorial009b.py rename to docs_src/python_types/tutorial009b_py39.py diff --git a/docs_src/python_types/tutorial009c.py b/docs_src/python_types/tutorial009c_py39.py similarity index 100% rename from docs_src/python_types/tutorial009c.py rename to docs_src/python_types/tutorial009c_py39.py diff --git a/docs_src/python_types/tutorial010.py b/docs_src/python_types/tutorial010_py39.py similarity index 100% rename from docs_src/python_types/tutorial010.py rename to docs_src/python_types/tutorial010_py39.py diff --git a/docs_src/python_types/tutorial011.py b/docs_src/python_types/tutorial011.py deleted file mode 100644 index 297a84db68..0000000000 --- a/docs_src/python_types/tutorial011.py +++ /dev/null @@ -1,23 +0,0 @@ -from datetime import datetime -from typing import List, Union - -from pydantic import BaseModel - - -class User(BaseModel): - id: int - name: str = "John Doe" - signup_ts: Union[datetime, None] = None - friends: List[int] = [] - - -external_data = { - "id": "123", - "signup_ts": "2017-06-01 12:22", - "friends": [1, "2", b"3"], -} -user = User(**external_data) -print(user) -# > User id=123 name='John Doe' signup_ts=datetime.datetime(2017, 6, 1, 12, 22) friends=[1, 2, 3] -print(user.id) -# > 123 diff --git a/docs_src/python_types/tutorial012.py b/docs_src/python_types/tutorial012_py39.py similarity index 100% rename from docs_src/python_types/tutorial012.py rename to docs_src/python_types/tutorial012_py39.py diff --git a/docs_src/python_types/tutorial013.py b/docs_src/python_types/tutorial013.py deleted file mode 100644 index 0ec7735199..0000000000 --- a/docs_src/python_types/tutorial013.py +++ /dev/null @@ -1,5 +0,0 @@ -from typing_extensions import Annotated - - -def say_hello(name: Annotated[str, "this is just metadata"]) -> str: - return f"Hello {name}" diff --git a/docs_src/query_param_models/__init__.py b/docs_src/query_param_models/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/query_param_models/tutorial001.py b/docs_src/query_param_models/tutorial001.py deleted file mode 100644 index 0c0ab315e8..0000000000 --- a/docs_src/query_param_models/tutorial001.py +++ /dev/null @@ -1,19 +0,0 @@ -from typing import List - -from fastapi import FastAPI, Query -from pydantic import BaseModel, Field -from typing_extensions import Literal - -app = FastAPI() - - -class FilterParams(BaseModel): - limit: int = Field(100, gt=0, le=100) - offset: int = Field(0, ge=0) - order_by: Literal["created_at", "updated_at"] = "created_at" - tags: List[str] = [] - - -@app.get("/items/") -async def read_items(filter_query: FilterParams = Query()): - return filter_query diff --git a/docs_src/query_param_models/tutorial001_an.py b/docs_src/query_param_models/tutorial001_an.py deleted file mode 100644 index 28375057c1..0000000000 --- a/docs_src/query_param_models/tutorial001_an.py +++ /dev/null @@ -1,19 +0,0 @@ -from typing import List - -from fastapi import FastAPI, Query -from pydantic import BaseModel, Field -from typing_extensions import Annotated, Literal - -app = FastAPI() - - -class FilterParams(BaseModel): - limit: int = Field(100, gt=0, le=100) - offset: int = Field(0, ge=0) - order_by: Literal["created_at", "updated_at"] = "created_at" - tags: List[str] = [] - - -@app.get("/items/") -async def read_items(filter_query: Annotated[FilterParams, Query()]): - return filter_query diff --git a/docs_src/query_param_models/tutorial001_an_py39.py b/docs_src/query_param_models/tutorial001_an_py39.py index ba690d3e3f..71427acae1 100644 --- a/docs_src/query_param_models/tutorial001_an_py39.py +++ b/docs_src/query_param_models/tutorial001_an_py39.py @@ -1,6 +1,7 @@ +from typing import Annotated, Literal + from fastapi import FastAPI, Query from pydantic import BaseModel, Field -from typing_extensions import Annotated, Literal app = FastAPI() diff --git a/docs_src/query_param_models/tutorial001_py39.py b/docs_src/query_param_models/tutorial001_py39.py index 54b52a054c..3ebf9f4d70 100644 --- a/docs_src/query_param_models/tutorial001_py39.py +++ b/docs_src/query_param_models/tutorial001_py39.py @@ -1,6 +1,7 @@ +from typing import Literal + from fastapi import FastAPI, Query from pydantic import BaseModel, Field -from typing_extensions import Literal app = FastAPI() diff --git a/docs_src/query_param_models/tutorial002.py b/docs_src/query_param_models/tutorial002.py deleted file mode 100644 index 1633bc4644..0000000000 --- a/docs_src/query_param_models/tutorial002.py +++ /dev/null @@ -1,21 +0,0 @@ -from typing import List - -from fastapi import FastAPI, Query -from pydantic import BaseModel, Field -from typing_extensions import Literal - -app = FastAPI() - - -class FilterParams(BaseModel): - model_config = {"extra": "forbid"} - - limit: int = Field(100, gt=0, le=100) - offset: int = Field(0, ge=0) - order_by: Literal["created_at", "updated_at"] = "created_at" - tags: List[str] = [] - - -@app.get("/items/") -async def read_items(filter_query: FilterParams = Query()): - return filter_query diff --git a/docs_src/query_param_models/tutorial002_an.py b/docs_src/query_param_models/tutorial002_an.py deleted file mode 100644 index 69705d4b4b..0000000000 --- a/docs_src/query_param_models/tutorial002_an.py +++ /dev/null @@ -1,21 +0,0 @@ -from typing import List - -from fastapi import FastAPI, Query -from pydantic import BaseModel, Field -from typing_extensions import Annotated, Literal - -app = FastAPI() - - -class FilterParams(BaseModel): - model_config = {"extra": "forbid"} - - limit: int = Field(100, gt=0, le=100) - offset: int = Field(0, ge=0) - order_by: Literal["created_at", "updated_at"] = "created_at" - tags: List[str] = [] - - -@app.get("/items/") -async def read_items(filter_query: Annotated[FilterParams, Query()]): - return filter_query diff --git a/docs_src/query_param_models/tutorial002_an_py39.py b/docs_src/query_param_models/tutorial002_an_py39.py index 2d4c1a62b5..9759565023 100644 --- a/docs_src/query_param_models/tutorial002_an_py39.py +++ b/docs_src/query_param_models/tutorial002_an_py39.py @@ -1,6 +1,7 @@ +from typing import Annotated, Literal + from fastapi import FastAPI, Query from pydantic import BaseModel, Field -from typing_extensions import Annotated, Literal app = FastAPI() diff --git a/docs_src/query_param_models/tutorial002_pv1.py b/docs_src/query_param_models/tutorial002_pv1.py deleted file mode 100644 index 71ccd961d3..0000000000 --- a/docs_src/query_param_models/tutorial002_pv1.py +++ /dev/null @@ -1,22 +0,0 @@ -from typing import List - -from fastapi import FastAPI, Query -from pydantic import BaseModel, Field -from typing_extensions import Literal - -app = FastAPI() - - -class FilterParams(BaseModel): - class Config: - extra = "forbid" - - limit: int = Field(100, gt=0, le=100) - offset: int = Field(0, ge=0) - order_by: Literal["created_at", "updated_at"] = "created_at" - tags: List[str] = [] - - -@app.get("/items/") -async def read_items(filter_query: FilterParams = Query()): - return filter_query diff --git a/docs_src/query_param_models/tutorial002_pv1_an.py b/docs_src/query_param_models/tutorial002_pv1_an.py deleted file mode 100644 index 1dd29157a4..0000000000 --- a/docs_src/query_param_models/tutorial002_pv1_an.py +++ /dev/null @@ -1,22 +0,0 @@ -from typing import List - -from fastapi import FastAPI, Query -from pydantic import BaseModel, Field -from typing_extensions import Annotated, Literal - -app = FastAPI() - - -class FilterParams(BaseModel): - class Config: - extra = "forbid" - - limit: int = Field(100, gt=0, le=100) - offset: int = Field(0, ge=0) - order_by: Literal["created_at", "updated_at"] = "created_at" - tags: List[str] = [] - - -@app.get("/items/") -async def read_items(filter_query: Annotated[FilterParams, Query()]): - return filter_query diff --git a/docs_src/query_param_models/tutorial002_pv1_an_py310.py b/docs_src/query_param_models/tutorial002_pv1_an_py310.py deleted file mode 100644 index d635aae88f..0000000000 --- a/docs_src/query_param_models/tutorial002_pv1_an_py310.py +++ /dev/null @@ -1,21 +0,0 @@ -from typing import Annotated, Literal - -from fastapi import FastAPI, Query -from pydantic import BaseModel, Field - -app = FastAPI() - - -class FilterParams(BaseModel): - class Config: - extra = "forbid" - - limit: int = Field(100, gt=0, le=100) - offset: int = Field(0, ge=0) - order_by: Literal["created_at", "updated_at"] = "created_at" - tags: list[str] = [] - - -@app.get("/items/") -async def read_items(filter_query: Annotated[FilterParams, Query()]): - return filter_query diff --git a/docs_src/query_param_models/tutorial002_pv1_an_py39.py b/docs_src/query_param_models/tutorial002_pv1_an_py39.py deleted file mode 100644 index 494fef11fc..0000000000 --- a/docs_src/query_param_models/tutorial002_pv1_an_py39.py +++ /dev/null @@ -1,20 +0,0 @@ -from fastapi import FastAPI, Query -from pydantic import BaseModel, Field -from typing_extensions import Annotated, Literal - -app = FastAPI() - - -class FilterParams(BaseModel): - class Config: - extra = "forbid" - - limit: int = Field(100, gt=0, le=100) - offset: int = Field(0, ge=0) - order_by: Literal["created_at", "updated_at"] = "created_at" - tags: list[str] = [] - - -@app.get("/items/") -async def read_items(filter_query: Annotated[FilterParams, Query()]): - return filter_query diff --git a/docs_src/query_param_models/tutorial002_pv1_py310.py b/docs_src/query_param_models/tutorial002_pv1_py310.py deleted file mode 100644 index 9ffdeefc06..0000000000 --- a/docs_src/query_param_models/tutorial002_pv1_py310.py +++ /dev/null @@ -1,21 +0,0 @@ -from typing import Literal - -from fastapi import FastAPI, Query -from pydantic import BaseModel, Field - -app = FastAPI() - - -class FilterParams(BaseModel): - class Config: - extra = "forbid" - - limit: int = Field(100, gt=0, le=100) - offset: int = Field(0, ge=0) - order_by: Literal["created_at", "updated_at"] = "created_at" - tags: list[str] = [] - - -@app.get("/items/") -async def read_items(filter_query: FilterParams = Query()): - return filter_query diff --git a/docs_src/query_param_models/tutorial002_pv1_py39.py b/docs_src/query_param_models/tutorial002_pv1_py39.py deleted file mode 100644 index 7fa456a791..0000000000 --- a/docs_src/query_param_models/tutorial002_pv1_py39.py +++ /dev/null @@ -1,20 +0,0 @@ -from fastapi import FastAPI, Query -from pydantic import BaseModel, Field -from typing_extensions import Literal - -app = FastAPI() - - -class FilterParams(BaseModel): - class Config: - extra = "forbid" - - limit: int = Field(100, gt=0, le=100) - offset: int = Field(0, ge=0) - order_by: Literal["created_at", "updated_at"] = "created_at" - tags: list[str] = [] - - -@app.get("/items/") -async def read_items(filter_query: FilterParams = Query()): - return filter_query diff --git a/docs_src/query_param_models/tutorial002_py39.py b/docs_src/query_param_models/tutorial002_py39.py index f9bba028c2..6ec4184991 100644 --- a/docs_src/query_param_models/tutorial002_py39.py +++ b/docs_src/query_param_models/tutorial002_py39.py @@ -1,6 +1,7 @@ +from typing import Literal + from fastapi import FastAPI, Query from pydantic import BaseModel, Field -from typing_extensions import Literal app = FastAPI() diff --git a/docs_src/query_params/__init__.py b/docs_src/query_params/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/query_params/tutorial001.py b/docs_src/query_params/tutorial001_py39.py similarity index 100% rename from docs_src/query_params/tutorial001.py rename to docs_src/query_params/tutorial001_py39.py diff --git a/docs_src/query_params/tutorial002.py b/docs_src/query_params/tutorial002_py39.py similarity index 100% rename from docs_src/query_params/tutorial002.py rename to docs_src/query_params/tutorial002_py39.py diff --git a/docs_src/query_params/tutorial003.py b/docs_src/query_params/tutorial003_py39.py similarity index 100% rename from docs_src/query_params/tutorial003.py rename to docs_src/query_params/tutorial003_py39.py diff --git a/docs_src/query_params/tutorial004.py b/docs_src/query_params/tutorial004_py39.py similarity index 100% rename from docs_src/query_params/tutorial004.py rename to docs_src/query_params/tutorial004_py39.py diff --git a/docs_src/query_params/tutorial005.py b/docs_src/query_params/tutorial005_py39.py similarity index 100% rename from docs_src/query_params/tutorial005.py rename to docs_src/query_params/tutorial005_py39.py diff --git a/docs_src/query_params/tutorial006.py b/docs_src/query_params/tutorial006_py39.py similarity index 100% rename from docs_src/query_params/tutorial006.py rename to docs_src/query_params/tutorial006_py39.py diff --git a/docs_src/query_params/tutorial006b.py b/docs_src/query_params/tutorial006b.py deleted file mode 100644 index f0dbfe08fe..0000000000 --- a/docs_src/query_params/tutorial006b.py +++ /dev/null @@ -1,13 +0,0 @@ -from typing import Union - -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/items/{item_id}") -async def read_user_item( - item_id: str, needy: str, skip: int = 0, limit: Union[int, None] = None -): - item = {"item_id": item_id, "needy": needy, "skip": skip, "limit": limit} - return item diff --git a/docs_src/query_params_str_validations/__init__.py b/docs_src/query_params_str_validations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/query_params_str_validations/tutorial001.py b/docs_src/query_params_str_validations/tutorial001_py39.py similarity index 100% rename from docs_src/query_params_str_validations/tutorial001.py rename to docs_src/query_params_str_validations/tutorial001_py39.py diff --git a/docs_src/query_params_str_validations/tutorial002_an.py b/docs_src/query_params_str_validations/tutorial002_an_py39.py similarity index 81% rename from docs_src/query_params_str_validations/tutorial002_an.py rename to docs_src/query_params_str_validations/tutorial002_an_py39.py index cb1b38940c..2d8fc97985 100644 --- a/docs_src/query_params_str_validations/tutorial002_an.py +++ b/docs_src/query_params_str_validations/tutorial002_an_py39.py @@ -1,7 +1,6 @@ -from typing import Union +from typing import Annotated, Union from fastapi import FastAPI, Query -from typing_extensions import Annotated app = FastAPI() diff --git a/docs_src/query_params_str_validations/tutorial002.py b/docs_src/query_params_str_validations/tutorial002_py39.py similarity index 100% rename from docs_src/query_params_str_validations/tutorial002.py rename to docs_src/query_params_str_validations/tutorial002_py39.py diff --git a/docs_src/query_params_str_validations/tutorial003_an.py b/docs_src/query_params_str_validations/tutorial003_an.py deleted file mode 100644 index 0dd14086cf..0000000000 --- a/docs_src/query_params_str_validations/tutorial003_an.py +++ /dev/null @@ -1,16 +0,0 @@ -from typing import Union - -from fastapi import FastAPI, Query -from typing_extensions import Annotated - -app = FastAPI() - - -@app.get("/items/") -async def read_items( - q: Annotated[Union[str, None], Query(min_length=3, max_length=50)] = None, -): - results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} - if q: - results.update({"q": q}) - return results diff --git a/docs_src/query_params_str_validations/tutorial003.py b/docs_src/query_params_str_validations/tutorial003_py39.py similarity index 100% rename from docs_src/query_params_str_validations/tutorial003.py rename to docs_src/query_params_str_validations/tutorial003_py39.py diff --git a/docs_src/query_params_str_validations/tutorial004_an.py b/docs_src/query_params_str_validations/tutorial004_an.py deleted file mode 100644 index c75d45d63e..0000000000 --- a/docs_src/query_params_str_validations/tutorial004_an.py +++ /dev/null @@ -1,18 +0,0 @@ -from typing import Union - -from fastapi import FastAPI, Query -from typing_extensions import Annotated - -app = FastAPI() - - -@app.get("/items/") -async def read_items( - q: Annotated[ - Union[str, None], Query(min_length=3, max_length=50, pattern="^fixedquery$") - ] = None, -): - results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} - if q: - results.update({"q": q}) - return results diff --git a/docs_src/query_params_str_validations/tutorial004.py b/docs_src/query_params_str_validations/tutorial004_py39.py similarity index 100% rename from docs_src/query_params_str_validations/tutorial004.py rename to docs_src/query_params_str_validations/tutorial004_py39.py diff --git a/docs_src/query_params_str_validations/tutorial005_an.py b/docs_src/query_params_str_validations/tutorial005_an.py deleted file mode 100644 index 452d4d38d7..0000000000 --- a/docs_src/query_params_str_validations/tutorial005_an.py +++ /dev/null @@ -1,12 +0,0 @@ -from fastapi import FastAPI, Query -from typing_extensions import Annotated - -app = FastAPI() - - -@app.get("/items/") -async def read_items(q: Annotated[str, Query(min_length=3)] = "fixedquery"): - results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} - if q: - results.update({"q": q}) - return results diff --git a/docs_src/query_params_str_validations/tutorial005.py b/docs_src/query_params_str_validations/tutorial005_py39.py similarity index 100% rename from docs_src/query_params_str_validations/tutorial005.py rename to docs_src/query_params_str_validations/tutorial005_py39.py diff --git a/docs_src/query_params_str_validations/tutorial006_an.py b/docs_src/query_params_str_validations/tutorial006_an.py deleted file mode 100644 index 559480d2bf..0000000000 --- a/docs_src/query_params_str_validations/tutorial006_an.py +++ /dev/null @@ -1,12 +0,0 @@ -from fastapi import FastAPI, Query -from typing_extensions import Annotated - -app = FastAPI() - - -@app.get("/items/") -async def read_items(q: Annotated[str, Query(min_length=3)]): - results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} - if q: - results.update({"q": q}) - return results diff --git a/docs_src/query_params_str_validations/tutorial006.py b/docs_src/query_params_str_validations/tutorial006_py39.py similarity index 100% rename from docs_src/query_params_str_validations/tutorial006.py rename to docs_src/query_params_str_validations/tutorial006_py39.py diff --git a/docs_src/query_params_str_validations/tutorial006c_an.py b/docs_src/query_params_str_validations/tutorial006c_an.py deleted file mode 100644 index 55c4f4adca..0000000000 --- a/docs_src/query_params_str_validations/tutorial006c_an.py +++ /dev/null @@ -1,14 +0,0 @@ -from typing import Union - -from fastapi import FastAPI, Query -from typing_extensions import Annotated - -app = FastAPI() - - -@app.get("/items/") -async def read_items(q: Annotated[Union[str, None], Query(min_length=3)]): - results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} - if q: - results.update({"q": q}) - return results diff --git a/docs_src/query_params_str_validations/tutorial006c.py b/docs_src/query_params_str_validations/tutorial006c_py39.py similarity index 100% rename from docs_src/query_params_str_validations/tutorial006c.py rename to docs_src/query_params_str_validations/tutorial006c_py39.py diff --git a/docs_src/query_params_str_validations/tutorial007_an.py b/docs_src/query_params_str_validations/tutorial007_an.py deleted file mode 100644 index 4b3c8de4b9..0000000000 --- a/docs_src/query_params_str_validations/tutorial007_an.py +++ /dev/null @@ -1,16 +0,0 @@ -from typing import Union - -from fastapi import FastAPI, Query -from typing_extensions import Annotated - -app = FastAPI() - - -@app.get("/items/") -async def read_items( - q: Annotated[Union[str, None], Query(title="Query string", min_length=3)] = None, -): - results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} - if q: - results.update({"q": q}) - return results diff --git a/docs_src/query_params_str_validations/tutorial007.py b/docs_src/query_params_str_validations/tutorial007_py39.py similarity index 100% rename from docs_src/query_params_str_validations/tutorial007.py rename to docs_src/query_params_str_validations/tutorial007_py39.py diff --git a/docs_src/query_params_str_validations/tutorial008_an.py b/docs_src/query_params_str_validations/tutorial008_an.py deleted file mode 100644 index 01606a9203..0000000000 --- a/docs_src/query_params_str_validations/tutorial008_an.py +++ /dev/null @@ -1,23 +0,0 @@ -from typing import Union - -from fastapi import FastAPI, Query -from typing_extensions import Annotated - -app = FastAPI() - - -@app.get("/items/") -async def read_items( - q: Annotated[ - Union[str, None], - Query( - title="Query string", - description="Query string for the items to search in the database that have a good match", - min_length=3, - ), - ] = None, -): - results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} - if q: - results.update({"q": q}) - return results diff --git a/docs_src/query_params_str_validations/tutorial008.py b/docs_src/query_params_str_validations/tutorial008_py39.py similarity index 100% rename from docs_src/query_params_str_validations/tutorial008.py rename to docs_src/query_params_str_validations/tutorial008_py39.py diff --git a/docs_src/query_params_str_validations/tutorial009_an.py b/docs_src/query_params_str_validations/tutorial009_an.py deleted file mode 100644 index 2894e2d51a..0000000000 --- a/docs_src/query_params_str_validations/tutorial009_an.py +++ /dev/null @@ -1,14 +0,0 @@ -from typing import Union - -from fastapi import FastAPI, Query -from typing_extensions import Annotated - -app = FastAPI() - - -@app.get("/items/") -async def read_items(q: Annotated[Union[str, None], Query(alias="item-query")] = None): - results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} - if q: - results.update({"q": q}) - return results diff --git a/docs_src/query_params_str_validations/tutorial009.py b/docs_src/query_params_str_validations/tutorial009_py39.py similarity index 100% rename from docs_src/query_params_str_validations/tutorial009.py rename to docs_src/query_params_str_validations/tutorial009_py39.py diff --git a/docs_src/query_params_str_validations/tutorial010_an.py b/docs_src/query_params_str_validations/tutorial010_an.py deleted file mode 100644 index ed343230f4..0000000000 --- a/docs_src/query_params_str_validations/tutorial010_an.py +++ /dev/null @@ -1,27 +0,0 @@ -from typing import Union - -from fastapi import FastAPI, Query -from typing_extensions import Annotated - -app = FastAPI() - - -@app.get("/items/") -async def read_items( - q: Annotated[ - Union[str, None], - Query( - alias="item-query", - title="Query string", - description="Query string for the items to search in the database that have a good match", - min_length=3, - max_length=50, - pattern="^fixedquery$", - deprecated=True, - ), - ] = None, -): - results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} - if q: - results.update({"q": q}) - return results diff --git a/docs_src/query_params_str_validations/tutorial010.py b/docs_src/query_params_str_validations/tutorial010_py39.py similarity index 100% rename from docs_src/query_params_str_validations/tutorial010.py rename to docs_src/query_params_str_validations/tutorial010_py39.py diff --git a/docs_src/query_params_str_validations/tutorial011.py b/docs_src/query_params_str_validations/tutorial011.py deleted file mode 100644 index 65bbce781a..0000000000 --- a/docs_src/query_params_str_validations/tutorial011.py +++ /dev/null @@ -1,11 +0,0 @@ -from typing import List, Union - -from fastapi import FastAPI, Query - -app = FastAPI() - - -@app.get("/items/") -async def read_items(q: Union[List[str], None] = Query(default=None)): - query_items = {"q": q} - return query_items diff --git a/docs_src/query_params_str_validations/tutorial011_an.py b/docs_src/query_params_str_validations/tutorial011_an.py deleted file mode 100644 index 8ed699337d..0000000000 --- a/docs_src/query_params_str_validations/tutorial011_an.py +++ /dev/null @@ -1,12 +0,0 @@ -from typing import List, Union - -from fastapi import FastAPI, Query -from typing_extensions import Annotated - -app = FastAPI() - - -@app.get("/items/") -async def read_items(q: Annotated[Union[List[str], None], Query()] = None): - query_items = {"q": q} - return query_items diff --git a/docs_src/query_params_str_validations/tutorial012.py b/docs_src/query_params_str_validations/tutorial012.py deleted file mode 100644 index e77d56974d..0000000000 --- a/docs_src/query_params_str_validations/tutorial012.py +++ /dev/null @@ -1,11 +0,0 @@ -from typing import List - -from fastapi import FastAPI, Query - -app = FastAPI() - - -@app.get("/items/") -async def read_items(q: List[str] = Query(default=["foo", "bar"])): - query_items = {"q": q} - return query_items diff --git a/docs_src/query_params_str_validations/tutorial012_an.py b/docs_src/query_params_str_validations/tutorial012_an.py deleted file mode 100644 index 261af250a1..0000000000 --- a/docs_src/query_params_str_validations/tutorial012_an.py +++ /dev/null @@ -1,12 +0,0 @@ -from typing import List - -from fastapi import FastAPI, Query -from typing_extensions import Annotated - -app = FastAPI() - - -@app.get("/items/") -async def read_items(q: Annotated[List[str], Query()] = ["foo", "bar"]): - query_items = {"q": q} - return query_items diff --git a/docs_src/query_params_str_validations/tutorial013_an.py b/docs_src/query_params_str_validations/tutorial013_an.py deleted file mode 100644 index f12a250559..0000000000 --- a/docs_src/query_params_str_validations/tutorial013_an.py +++ /dev/null @@ -1,10 +0,0 @@ -from fastapi import FastAPI, Query -from typing_extensions import Annotated - -app = FastAPI() - - -@app.get("/items/") -async def read_items(q: Annotated[list, Query()] = []): - query_items = {"q": q} - return query_items diff --git a/docs_src/query_params_str_validations/tutorial013.py b/docs_src/query_params_str_validations/tutorial013_py39.py similarity index 100% rename from docs_src/query_params_str_validations/tutorial013.py rename to docs_src/query_params_str_validations/tutorial013_py39.py diff --git a/docs_src/query_params_str_validations/tutorial014_an.py b/docs_src/query_params_str_validations/tutorial014_an.py deleted file mode 100644 index 2eaa585407..0000000000 --- a/docs_src/query_params_str_validations/tutorial014_an.py +++ /dev/null @@ -1,16 +0,0 @@ -from typing import Union - -from fastapi import FastAPI, Query -from typing_extensions import Annotated - -app = FastAPI() - - -@app.get("/items/") -async def read_items( - hidden_query: Annotated[Union[str, None], Query(include_in_schema=False)] = None, -): - if hidden_query: - return {"hidden_query": hidden_query} - else: - return {"hidden_query": "Not found"} diff --git a/docs_src/query_params_str_validations/tutorial014.py b/docs_src/query_params_str_validations/tutorial014_py39.py similarity index 100% rename from docs_src/query_params_str_validations/tutorial014.py rename to docs_src/query_params_str_validations/tutorial014_py39.py diff --git a/docs_src/query_params_str_validations/tutorial015_an.py b/docs_src/query_params_str_validations/tutorial015_an.py deleted file mode 100644 index f2ec6db123..0000000000 --- a/docs_src/query_params_str_validations/tutorial015_an.py +++ /dev/null @@ -1,31 +0,0 @@ -import random -from typing import Union - -from fastapi import FastAPI -from pydantic import AfterValidator -from typing_extensions import Annotated - -app = FastAPI() - -data = { - "isbn-9781529046137": "The Hitchhiker's Guide to the Galaxy", - "imdb-tt0371724": "The Hitchhiker's Guide to the Galaxy", - "isbn-9781439512982": "Isaac Asimov: The Complete Stories, Vol. 2", -} - - -def check_valid_id(id: str): - if not id.startswith(("isbn-", "imdb-")): - raise ValueError('Invalid ID format, it must start with "isbn-" or "imdb-"') - return id - - -@app.get("/items/") -async def read_items( - id: Annotated[Union[str, None], AfterValidator(check_valid_id)] = None, -): - if id: - item = data.get(id) - else: - id, item = random.choice(list(data.items())) - return {"id": id, "name": item} diff --git a/docs_src/request_files/__init__.py b/docs_src/request_files/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/request_files/tutorial001_02_an.py b/docs_src/request_files/tutorial001_02_an.py deleted file mode 100644 index 5007fef159..0000000000 --- a/docs_src/request_files/tutorial001_02_an.py +++ /dev/null @@ -1,22 +0,0 @@ -from typing import Union - -from fastapi import FastAPI, File, UploadFile -from typing_extensions import Annotated - -app = FastAPI() - - -@app.post("/files/") -async def create_file(file: Annotated[Union[bytes, None], File()] = None): - if not file: - return {"message": "No file sent"} - else: - return {"file_size": len(file)} - - -@app.post("/uploadfile/") -async def create_upload_file(file: Union[UploadFile, None] = None): - if not file: - return {"message": "No upload file sent"} - else: - return {"filename": file.filename} diff --git a/docs_src/request_files/tutorial001_02.py b/docs_src/request_files/tutorial001_02_py39.py similarity index 100% rename from docs_src/request_files/tutorial001_02.py rename to docs_src/request_files/tutorial001_02_py39.py diff --git a/docs_src/request_files/tutorial001_03_an.py b/docs_src/request_files/tutorial001_03_an.py deleted file mode 100644 index 8a6b0a2455..0000000000 --- a/docs_src/request_files/tutorial001_03_an.py +++ /dev/null @@ -1,16 +0,0 @@ -from fastapi import FastAPI, File, UploadFile -from typing_extensions import Annotated - -app = FastAPI() - - -@app.post("/files/") -async def create_file(file: Annotated[bytes, File(description="A file read as bytes")]): - return {"file_size": len(file)} - - -@app.post("/uploadfile/") -async def create_upload_file( - file: Annotated[UploadFile, File(description="A file read as UploadFile")], -): - return {"filename": file.filename} diff --git a/docs_src/request_files/tutorial001_03.py b/docs_src/request_files/tutorial001_03_py39.py similarity index 100% rename from docs_src/request_files/tutorial001_03.py rename to docs_src/request_files/tutorial001_03_py39.py diff --git a/docs_src/request_files/tutorial001_an.py b/docs_src/request_files/tutorial001_an.py deleted file mode 100644 index ca2f76d5c6..0000000000 --- a/docs_src/request_files/tutorial001_an.py +++ /dev/null @@ -1,14 +0,0 @@ -from fastapi import FastAPI, File, UploadFile -from typing_extensions import Annotated - -app = FastAPI() - - -@app.post("/files/") -async def create_file(file: Annotated[bytes, File()]): - return {"file_size": len(file)} - - -@app.post("/uploadfile/") -async def create_upload_file(file: UploadFile): - return {"filename": file.filename} diff --git a/docs_src/request_files/tutorial001.py b/docs_src/request_files/tutorial001_py39.py similarity index 100% rename from docs_src/request_files/tutorial001.py rename to docs_src/request_files/tutorial001_py39.py diff --git a/docs_src/request_files/tutorial002.py b/docs_src/request_files/tutorial002.py deleted file mode 100644 index b4d0acc68f..0000000000 --- a/docs_src/request_files/tutorial002.py +++ /dev/null @@ -1,33 +0,0 @@ -from typing import List - -from fastapi import FastAPI, File, UploadFile -from fastapi.responses import HTMLResponse - -app = FastAPI() - - -@app.post("/files/") -async def create_files(files: List[bytes] = File()): - return {"file_sizes": [len(file) for file in files]} - - -@app.post("/uploadfiles/") -async def create_upload_files(files: List[UploadFile]): - return {"filenames": [file.filename for file in files]} - - -@app.get("/") -async def main(): - content = """ - -
- - -
-
- - -
- - """ - return HTMLResponse(content=content) diff --git a/docs_src/request_files/tutorial002_an.py b/docs_src/request_files/tutorial002_an.py deleted file mode 100644 index eaa90da2b6..0000000000 --- a/docs_src/request_files/tutorial002_an.py +++ /dev/null @@ -1,34 +0,0 @@ -from typing import List - -from fastapi import FastAPI, File, UploadFile -from fastapi.responses import HTMLResponse -from typing_extensions import Annotated - -app = FastAPI() - - -@app.post("/files/") -async def create_files(files: Annotated[List[bytes], File()]): - return {"file_sizes": [len(file) for file in files]} - - -@app.post("/uploadfiles/") -async def create_upload_files(files: List[UploadFile]): - return {"filenames": [file.filename for file in files]} - - -@app.get("/") -async def main(): - content = """ - -
- - -
-
- - -
- - """ - return HTMLResponse(content=content) diff --git a/docs_src/request_files/tutorial003.py b/docs_src/request_files/tutorial003.py deleted file mode 100644 index e3f805f605..0000000000 --- a/docs_src/request_files/tutorial003.py +++ /dev/null @@ -1,37 +0,0 @@ -from typing import List - -from fastapi import FastAPI, File, UploadFile -from fastapi.responses import HTMLResponse - -app = FastAPI() - - -@app.post("/files/") -async def create_files( - files: List[bytes] = File(description="Multiple files as bytes"), -): - return {"file_sizes": [len(file) for file in files]} - - -@app.post("/uploadfiles/") -async def create_upload_files( - files: List[UploadFile] = File(description="Multiple files as UploadFile"), -): - return {"filenames": [file.filename for file in files]} - - -@app.get("/") -async def main(): - content = """ - -
- - -
-
- - -
- - """ - return HTMLResponse(content=content) diff --git a/docs_src/request_files/tutorial003_an.py b/docs_src/request_files/tutorial003_an.py deleted file mode 100644 index 2238e3c94b..0000000000 --- a/docs_src/request_files/tutorial003_an.py +++ /dev/null @@ -1,40 +0,0 @@ -from typing import List - -from fastapi import FastAPI, File, UploadFile -from fastapi.responses import HTMLResponse -from typing_extensions import Annotated - -app = FastAPI() - - -@app.post("/files/") -async def create_files( - files: Annotated[List[bytes], File(description="Multiple files as bytes")], -): - return {"file_sizes": [len(file) for file in files]} - - -@app.post("/uploadfiles/") -async def create_upload_files( - files: Annotated[ - List[UploadFile], File(description="Multiple files as UploadFile") - ], -): - return {"filenames": [file.filename for file in files]} - - -@app.get("/") -async def main(): - content = """ - -
- - -
-
- - -
- - """ - return HTMLResponse(content=content) diff --git a/docs_src/request_form_models/__init__.py b/docs_src/request_form_models/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/request_form_models/tutorial001_an.py b/docs_src/request_form_models/tutorial001_an.py deleted file mode 100644 index 30483d4455..0000000000 --- a/docs_src/request_form_models/tutorial001_an.py +++ /dev/null @@ -1,15 +0,0 @@ -from fastapi import FastAPI, Form -from pydantic import BaseModel -from typing_extensions import Annotated - -app = FastAPI() - - -class FormData(BaseModel): - username: str - password: str - - -@app.post("/login/") -async def login(data: Annotated[FormData, Form()]): - return data diff --git a/docs_src/request_form_models/tutorial001.py b/docs_src/request_form_models/tutorial001_py39.py similarity index 100% rename from docs_src/request_form_models/tutorial001.py rename to docs_src/request_form_models/tutorial001_py39.py diff --git a/docs_src/request_form_models/tutorial002_an.py b/docs_src/request_form_models/tutorial002_an.py deleted file mode 100644 index bcb0227959..0000000000 --- a/docs_src/request_form_models/tutorial002_an.py +++ /dev/null @@ -1,16 +0,0 @@ -from fastapi import FastAPI, Form -from pydantic import BaseModel -from typing_extensions import Annotated - -app = FastAPI() - - -class FormData(BaseModel): - username: str - password: str - model_config = {"extra": "forbid"} - - -@app.post("/login/") -async def login(data: Annotated[FormData, Form()]): - return data diff --git a/docs_src/request_form_models/tutorial002_pv1.py b/docs_src/request_form_models/tutorial002_pv1.py deleted file mode 100644 index d5f7db2a67..0000000000 --- a/docs_src/request_form_models/tutorial002_pv1.py +++ /dev/null @@ -1,17 +0,0 @@ -from fastapi import FastAPI, Form -from pydantic import BaseModel - -app = FastAPI() - - -class FormData(BaseModel): - username: str - password: str - - class Config: - extra = "forbid" - - -@app.post("/login/") -async def login(data: FormData = Form()): - return data diff --git a/docs_src/request_form_models/tutorial002_pv1_an.py b/docs_src/request_form_models/tutorial002_pv1_an.py deleted file mode 100644 index fe9dbc344b..0000000000 --- a/docs_src/request_form_models/tutorial002_pv1_an.py +++ /dev/null @@ -1,18 +0,0 @@ -from fastapi import FastAPI, Form -from pydantic import BaseModel -from typing_extensions import Annotated - -app = FastAPI() - - -class FormData(BaseModel): - username: str - password: str - - class Config: - extra = "forbid" - - -@app.post("/login/") -async def login(data: Annotated[FormData, Form()]): - return data diff --git a/docs_src/request_form_models/tutorial002_pv1_an_py39.py b/docs_src/request_form_models/tutorial002_pv1_an_py39.py deleted file mode 100644 index 942d5d4118..0000000000 --- a/docs_src/request_form_models/tutorial002_pv1_an_py39.py +++ /dev/null @@ -1,19 +0,0 @@ -from typing import Annotated - -from fastapi import FastAPI, Form -from pydantic import BaseModel - -app = FastAPI() - - -class FormData(BaseModel): - username: str - password: str - - class Config: - extra = "forbid" - - -@app.post("/login/") -async def login(data: Annotated[FormData, Form()]): - return data diff --git a/docs_src/request_form_models/tutorial002.py b/docs_src/request_form_models/tutorial002_py39.py similarity index 100% rename from docs_src/request_form_models/tutorial002.py rename to docs_src/request_form_models/tutorial002_py39.py diff --git a/docs_src/request_forms/__init__.py b/docs_src/request_forms/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/request_forms/tutorial001_an.py b/docs_src/request_forms/tutorial001_an.py deleted file mode 100644 index 677fbf2db8..0000000000 --- a/docs_src/request_forms/tutorial001_an.py +++ /dev/null @@ -1,9 +0,0 @@ -from fastapi import FastAPI, Form -from typing_extensions import Annotated - -app = FastAPI() - - -@app.post("/login/") -async def login(username: Annotated[str, Form()], password: Annotated[str, Form()]): - return {"username": username} diff --git a/docs_src/request_forms/tutorial001.py b/docs_src/request_forms/tutorial001_py39.py similarity index 100% rename from docs_src/request_forms/tutorial001.py rename to docs_src/request_forms/tutorial001_py39.py diff --git a/docs_src/request_forms_and_files/__init__.py b/docs_src/request_forms_and_files/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/request_forms_and_files/tutorial001_an.py b/docs_src/request_forms_and_files/tutorial001_an.py deleted file mode 100644 index 0ea285ac87..0000000000 --- a/docs_src/request_forms_and_files/tutorial001_an.py +++ /dev/null @@ -1,17 +0,0 @@ -from fastapi import FastAPI, File, Form, UploadFile -from typing_extensions import Annotated - -app = FastAPI() - - -@app.post("/files/") -async def create_file( - file: Annotated[bytes, File()], - fileb: Annotated[UploadFile, File()], - token: Annotated[str, Form()], -): - return { - "file_size": len(file), - "token": token, - "fileb_content_type": fileb.content_type, - } diff --git a/docs_src/request_forms_and_files/tutorial001.py b/docs_src/request_forms_and_files/tutorial001_py39.py similarity index 100% rename from docs_src/request_forms_and_files/tutorial001.py rename to docs_src/request_forms_and_files/tutorial001_py39.py diff --git a/docs_src/response_change_status_code/__init__.py b/docs_src/response_change_status_code/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/response_change_status_code/tutorial001.py b/docs_src/response_change_status_code/tutorial001_py39.py similarity index 100% rename from docs_src/response_change_status_code/tutorial001.py rename to docs_src/response_change_status_code/tutorial001_py39.py diff --git a/docs_src/response_cookies/__init__.py b/docs_src/response_cookies/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/response_cookies/tutorial001.py b/docs_src/response_cookies/tutorial001_py39.py similarity index 100% rename from docs_src/response_cookies/tutorial001.py rename to docs_src/response_cookies/tutorial001_py39.py diff --git a/docs_src/response_cookies/tutorial002.py b/docs_src/response_cookies/tutorial002_py39.py similarity index 100% rename from docs_src/response_cookies/tutorial002.py rename to docs_src/response_cookies/tutorial002_py39.py diff --git a/docs_src/response_directly/__init__.py b/docs_src/response_directly/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/response_directly/tutorial001_py310.py b/docs_src/response_directly/tutorial001_py310.py new file mode 100644 index 0000000000..81e094dc69 --- /dev/null +++ b/docs_src/response_directly/tutorial001_py310.py @@ -0,0 +1,21 @@ +from datetime import datetime + +from fastapi import FastAPI +from fastapi.encoders import jsonable_encoder +from fastapi.responses import JSONResponse +from pydantic import BaseModel + + +class Item(BaseModel): + title: str + timestamp: datetime + description: str | None = None + + +app = FastAPI() + + +@app.put("/items/{id}") +def update_item(id: str, item: Item): + json_compatible_item_data = jsonable_encoder(item) + return JSONResponse(content=json_compatible_item_data) diff --git a/docs_src/response_directly/tutorial001.py b/docs_src/response_directly/tutorial001_py39.py similarity index 100% rename from docs_src/response_directly/tutorial001.py rename to docs_src/response_directly/tutorial001_py39.py diff --git a/docs_src/response_directly/tutorial002.py b/docs_src/response_directly/tutorial002_py39.py similarity index 100% rename from docs_src/response_directly/tutorial002.py rename to docs_src/response_directly/tutorial002_py39.py diff --git a/docs_src/response_headers/__init__.py b/docs_src/response_headers/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/response_headers/tutorial001.py b/docs_src/response_headers/tutorial001_py39.py similarity index 100% rename from docs_src/response_headers/tutorial001.py rename to docs_src/response_headers/tutorial001_py39.py diff --git a/docs_src/response_headers/tutorial002.py b/docs_src/response_headers/tutorial002_py39.py similarity index 100% rename from docs_src/response_headers/tutorial002.py rename to docs_src/response_headers/tutorial002_py39.py diff --git a/docs_src/response_model/__init__.py b/docs_src/response_model/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/response_model/tutorial001.py b/docs_src/response_model/tutorial001.py deleted file mode 100644 index fd1c902a52..0000000000 --- a/docs_src/response_model/tutorial001.py +++ /dev/null @@ -1,27 +0,0 @@ -from typing import Any, List, Union - -from fastapi import FastAPI -from pydantic import BaseModel - -app = FastAPI() - - -class Item(BaseModel): - name: str - description: Union[str, None] = None - price: float - tax: Union[float, None] = None - tags: List[str] = [] - - -@app.post("/items/", response_model=Item) -async def create_item(item: Item) -> Any: - return item - - -@app.get("/items/", response_model=List[Item]) -async def read_items() -> Any: - return [ - {"name": "Portal Gun", "price": 42.0}, - {"name": "Plumbus", "price": 32.0}, - ] diff --git a/docs_src/response_model/tutorial001_01.py b/docs_src/response_model/tutorial001_01.py deleted file mode 100644 index 98d30d540f..0000000000 --- a/docs_src/response_model/tutorial001_01.py +++ /dev/null @@ -1,27 +0,0 @@ -from typing import List, Union - -from fastapi import FastAPI -from pydantic import BaseModel - -app = FastAPI() - - -class Item(BaseModel): - name: str - description: Union[str, None] = None - price: float - tax: Union[float, None] = None - tags: List[str] = [] - - -@app.post("/items/") -async def create_item(item: Item) -> Item: - return item - - -@app.get("/items/") -async def read_items() -> List[Item]: - return [ - Item(name="Portal Gun", price=42.0), - Item(name="Plumbus", price=32.0), - ] diff --git a/docs_src/response_model/tutorial002.py b/docs_src/response_model/tutorial002_py39.py similarity index 100% rename from docs_src/response_model/tutorial002.py rename to docs_src/response_model/tutorial002_py39.py diff --git a/docs_src/response_model/tutorial003_01.py b/docs_src/response_model/tutorial003_01_py39.py similarity index 100% rename from docs_src/response_model/tutorial003_01.py rename to docs_src/response_model/tutorial003_01_py39.py diff --git a/docs_src/response_model/tutorial003_02.py b/docs_src/response_model/tutorial003_02_py39.py similarity index 100% rename from docs_src/response_model/tutorial003_02.py rename to docs_src/response_model/tutorial003_02_py39.py diff --git a/docs_src/response_model/tutorial003_03.py b/docs_src/response_model/tutorial003_03_py39.py similarity index 100% rename from docs_src/response_model/tutorial003_03.py rename to docs_src/response_model/tutorial003_03_py39.py diff --git a/docs_src/response_model/tutorial003_04.py b/docs_src/response_model/tutorial003_04_py39.py similarity index 100% rename from docs_src/response_model/tutorial003_04.py rename to docs_src/response_model/tutorial003_04_py39.py diff --git a/docs_src/response_model/tutorial003_05.py b/docs_src/response_model/tutorial003_05_py39.py similarity index 100% rename from docs_src/response_model/tutorial003_05.py rename to docs_src/response_model/tutorial003_05_py39.py diff --git a/docs_src/response_model/tutorial003.py b/docs_src/response_model/tutorial003_py39.py similarity index 100% rename from docs_src/response_model/tutorial003.py rename to docs_src/response_model/tutorial003_py39.py diff --git a/docs_src/response_model/tutorial004.py b/docs_src/response_model/tutorial004.py deleted file mode 100644 index 10b48039ae..0000000000 --- a/docs_src/response_model/tutorial004.py +++ /dev/null @@ -1,26 +0,0 @@ -from typing import List, Union - -from fastapi import FastAPI -from pydantic import BaseModel - -app = FastAPI() - - -class Item(BaseModel): - name: str - description: Union[str, None] = None - price: float - tax: float = 10.5 - tags: List[str] = [] - - -items = { - "foo": {"name": "Foo", "price": 50.2}, - "bar": {"name": "Bar", "description": "The bartenders", "price": 62, "tax": 20.2}, - "baz": {"name": "Baz", "description": None, "price": 50.2, "tax": 10.5, "tags": []}, -} - - -@app.get("/items/{item_id}", response_model=Item, response_model_exclude_unset=True) -async def read_item(item_id: str): - return items[item_id] diff --git a/docs_src/response_model/tutorial005.py b/docs_src/response_model/tutorial005_py39.py similarity index 100% rename from docs_src/response_model/tutorial005.py rename to docs_src/response_model/tutorial005_py39.py diff --git a/docs_src/response_model/tutorial006.py b/docs_src/response_model/tutorial006_py39.py similarity index 100% rename from docs_src/response_model/tutorial006.py rename to docs_src/response_model/tutorial006_py39.py diff --git a/docs_src/response_status_code/__init__.py b/docs_src/response_status_code/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/response_status_code/tutorial001.py b/docs_src/response_status_code/tutorial001_py39.py similarity index 100% rename from docs_src/response_status_code/tutorial001.py rename to docs_src/response_status_code/tutorial001_py39.py diff --git a/docs_src/response_status_code/tutorial002.py b/docs_src/response_status_code/tutorial002_py39.py similarity index 100% rename from docs_src/response_status_code/tutorial002.py rename to docs_src/response_status_code/tutorial002_py39.py diff --git a/docs_src/schema_extra_example/__init__.py b/docs_src/schema_extra_example/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/schema_extra_example/tutorial001_pv1_py310.py b/docs_src/schema_extra_example/tutorial001_pv1_py310.py index ec83f1112f..b13b8a8c2c 100644 --- a/docs_src/schema_extra_example/tutorial001_pv1_py310.py +++ b/docs_src/schema_extra_example/tutorial001_pv1_py310.py @@ -1,5 +1,5 @@ from fastapi import FastAPI -from pydantic import BaseModel +from pydantic.v1 import BaseModel app = FastAPI() diff --git a/docs_src/schema_extra_example/tutorial001_pv1.py b/docs_src/schema_extra_example/tutorial001_pv1_py39.py similarity index 94% rename from docs_src/schema_extra_example/tutorial001_pv1.py rename to docs_src/schema_extra_example/tutorial001_pv1_py39.py index 6ab96ff859..3240b35d6d 100644 --- a/docs_src/schema_extra_example/tutorial001_pv1.py +++ b/docs_src/schema_extra_example/tutorial001_pv1_py39.py @@ -1,7 +1,7 @@ from typing import Union from fastapi import FastAPI -from pydantic import BaseModel +from pydantic.v1 import BaseModel app = FastAPI() diff --git a/docs_src/schema_extra_example/tutorial001.py b/docs_src/schema_extra_example/tutorial001_py39.py similarity index 100% rename from docs_src/schema_extra_example/tutorial001.py rename to docs_src/schema_extra_example/tutorial001_py39.py diff --git a/docs_src/schema_extra_example/tutorial002.py b/docs_src/schema_extra_example/tutorial002_py39.py similarity index 100% rename from docs_src/schema_extra_example/tutorial002.py rename to docs_src/schema_extra_example/tutorial002_py39.py diff --git a/docs_src/schema_extra_example/tutorial003_an.py b/docs_src/schema_extra_example/tutorial003_an.py deleted file mode 100644 index 23675aba14..0000000000 --- a/docs_src/schema_extra_example/tutorial003_an.py +++ /dev/null @@ -1,35 +0,0 @@ -from typing import Union - -from fastapi import Body, FastAPI -from pydantic import BaseModel -from typing_extensions import Annotated - -app = FastAPI() - - -class Item(BaseModel): - name: str - description: Union[str, None] = None - price: float - tax: Union[float, None] = None - - -@app.put("/items/{item_id}") -async def update_item( - item_id: int, - item: Annotated[ - Item, - Body( - examples=[ - { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - } - ], - ), - ], -): - results = {"item_id": item_id, "item": item} - return results diff --git a/docs_src/schema_extra_example/tutorial003.py b/docs_src/schema_extra_example/tutorial003_py39.py similarity index 100% rename from docs_src/schema_extra_example/tutorial003.py rename to docs_src/schema_extra_example/tutorial003_py39.py diff --git a/docs_src/schema_extra_example/tutorial004_an.py b/docs_src/schema_extra_example/tutorial004_an.py deleted file mode 100644 index e817302a22..0000000000 --- a/docs_src/schema_extra_example/tutorial004_an.py +++ /dev/null @@ -1,44 +0,0 @@ -from typing import Union - -from fastapi import Body, FastAPI -from pydantic import BaseModel -from typing_extensions import Annotated - -app = FastAPI() - - -class Item(BaseModel): - name: str - description: Union[str, None] = None - price: float - tax: Union[float, None] = None - - -@app.put("/items/{item_id}") -async def update_item( - *, - item_id: int, - item: Annotated[ - Item, - Body( - examples=[ - { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, - { - "name": "Bar", - "price": "35.4", - }, - { - "name": "Baz", - "price": "thirty five point four", - }, - ], - ), - ], -): - results = {"item_id": item_id, "item": item} - return results diff --git a/docs_src/schema_extra_example/tutorial004.py b/docs_src/schema_extra_example/tutorial004_py39.py similarity index 100% rename from docs_src/schema_extra_example/tutorial004.py rename to docs_src/schema_extra_example/tutorial004_py39.py diff --git a/docs_src/schema_extra_example/tutorial005_an.py b/docs_src/schema_extra_example/tutorial005_an.py deleted file mode 100644 index 4b2d9c662b..0000000000 --- a/docs_src/schema_extra_example/tutorial005_an.py +++ /dev/null @@ -1,55 +0,0 @@ -from typing import Union - -from fastapi import Body, FastAPI -from pydantic import BaseModel -from typing_extensions import Annotated - -app = FastAPI() - - -class Item(BaseModel): - name: str - description: Union[str, None] = None - price: float - tax: Union[float, None] = None - - -@app.put("/items/{item_id}") -async def update_item( - *, - item_id: int, - item: Annotated[ - Item, - Body( - openapi_examples={ - "normal": { - "summary": "A normal example", - "description": "A **normal** item works correctly.", - "value": { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, - }, - "converted": { - "summary": "An example with converted data", - "description": "FastAPI can convert price `strings` to actual `numbers` automatically", - "value": { - "name": "Bar", - "price": "35.4", - }, - }, - "invalid": { - "summary": "Invalid data is rejected with an error", - "value": { - "name": "Baz", - "price": "thirty five point four", - }, - }, - }, - ), - ], -): - results = {"item_id": item_id, "item": item} - return results diff --git a/docs_src/schema_extra_example/tutorial005.py b/docs_src/schema_extra_example/tutorial005_py39.py similarity index 100% rename from docs_src/schema_extra_example/tutorial005.py rename to docs_src/schema_extra_example/tutorial005_py39.py diff --git a/docs_src/security/__init__.py b/docs_src/security/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/security/tutorial001_an.py b/docs_src/security/tutorial001_an.py deleted file mode 100644 index dac915b7ca..0000000000 --- a/docs_src/security/tutorial001_an.py +++ /dev/null @@ -1,12 +0,0 @@ -from fastapi import Depends, FastAPI -from fastapi.security import OAuth2PasswordBearer -from typing_extensions import Annotated - -app = FastAPI() - -oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") - - -@app.get("/items/") -async def read_items(token: Annotated[str, Depends(oauth2_scheme)]): - return {"token": token} diff --git a/docs_src/security/tutorial001.py b/docs_src/security/tutorial001_py39.py similarity index 100% rename from docs_src/security/tutorial001.py rename to docs_src/security/tutorial001_py39.py diff --git a/docs_src/security/tutorial002_an.py b/docs_src/security/tutorial002_an.py deleted file mode 100644 index 291b3bf530..0000000000 --- a/docs_src/security/tutorial002_an.py +++ /dev/null @@ -1,33 +0,0 @@ -from typing import Union - -from fastapi import Depends, FastAPI -from fastapi.security import OAuth2PasswordBearer -from pydantic import BaseModel -from typing_extensions import Annotated - -app = FastAPI() - -oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") - - -class User(BaseModel): - username: str - email: Union[str, None] = None - full_name: Union[str, None] = None - disabled: Union[bool, None] = None - - -def fake_decode_token(token): - return User( - username=token + "fakedecoded", email="john@example.com", full_name="John Doe" - ) - - -async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]): - user = fake_decode_token(token) - return user - - -@app.get("/users/me") -async def read_users_me(current_user: Annotated[User, Depends(get_current_user)]): - return current_user diff --git a/docs_src/security/tutorial002.py b/docs_src/security/tutorial002_py39.py similarity index 100% rename from docs_src/security/tutorial002.py rename to docs_src/security/tutorial002_py39.py diff --git a/docs_src/security/tutorial003_an.py b/docs_src/security/tutorial003_an.py deleted file mode 100644 index 8fb40dd4aa..0000000000 --- a/docs_src/security/tutorial003_an.py +++ /dev/null @@ -1,95 +0,0 @@ -from typing import Union - -from fastapi import Depends, FastAPI, HTTPException, status -from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm -from pydantic import BaseModel -from typing_extensions import Annotated - -fake_users_db = { - "johndoe": { - "username": "johndoe", - "full_name": "John Doe", - "email": "johndoe@example.com", - "hashed_password": "fakehashedsecret", - "disabled": False, - }, - "alice": { - "username": "alice", - "full_name": "Alice Wonderson", - "email": "alice@example.com", - "hashed_password": "fakehashedsecret2", - "disabled": True, - }, -} - -app = FastAPI() - - -def fake_hash_password(password: str): - return "fakehashed" + password - - -oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") - - -class User(BaseModel): - username: str - email: Union[str, None] = None - full_name: Union[str, None] = None - disabled: Union[bool, None] = None - - -class UserInDB(User): - hashed_password: str - - -def get_user(db, username: str): - if username in db: - user_dict = db[username] - return UserInDB(**user_dict) - - -def fake_decode_token(token): - # This doesn't provide any security at all - # Check the next version - user = get_user(fake_users_db, token) - return user - - -async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]): - user = fake_decode_token(token) - if not user: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Invalid authentication credentials", - headers={"WWW-Authenticate": "Bearer"}, - ) - return user - - -async def get_current_active_user( - current_user: Annotated[User, Depends(get_current_user)], -): - if current_user.disabled: - raise HTTPException(status_code=400, detail="Inactive user") - return current_user - - -@app.post("/token") -async def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]): - user_dict = fake_users_db.get(form_data.username) - if not user_dict: - raise HTTPException(status_code=400, detail="Incorrect username or password") - user = UserInDB(**user_dict) - hashed_password = fake_hash_password(form_data.password) - if not hashed_password == user.hashed_password: - raise HTTPException(status_code=400, detail="Incorrect username or password") - - return {"access_token": user.username, "token_type": "bearer"} - - -@app.get("/users/me") -async def read_users_me( - current_user: Annotated[User, Depends(get_current_active_user)], -): - return current_user diff --git a/docs_src/security/tutorial003_an_py310.py b/docs_src/security/tutorial003_an_py310.py index ced4a2fbc1..4a2743f6f8 100644 --- a/docs_src/security/tutorial003_an_py310.py +++ b/docs_src/security/tutorial003_an_py310.py @@ -60,7 +60,7 @@ async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]): if not user: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, - detail="Invalid authentication credentials", + detail="Not authenticated", headers={"WWW-Authenticate": "Bearer"}, ) return user diff --git a/docs_src/security/tutorial003_an_py39.py b/docs_src/security/tutorial003_an_py39.py index 068a3933e1..b396210c82 100644 --- a/docs_src/security/tutorial003_an_py39.py +++ b/docs_src/security/tutorial003_an_py39.py @@ -60,7 +60,7 @@ async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]): if not user: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, - detail="Invalid authentication credentials", + detail="Not authenticated", headers={"WWW-Authenticate": "Bearer"}, ) return user diff --git a/docs_src/security/tutorial003_py310.py b/docs_src/security/tutorial003_py310.py index af935e997d..081259b317 100644 --- a/docs_src/security/tutorial003_py310.py +++ b/docs_src/security/tutorial003_py310.py @@ -58,7 +58,7 @@ async def get_current_user(token: str = Depends(oauth2_scheme)): if not user: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, - detail="Invalid authentication credentials", + detail="Not authenticated", headers={"WWW-Authenticate": "Bearer"}, ) return user diff --git a/docs_src/security/tutorial003.py b/docs_src/security/tutorial003_py39.py similarity index 97% rename from docs_src/security/tutorial003.py rename to docs_src/security/tutorial003_py39.py index 4b324866f6..ce7a71b68b 100644 --- a/docs_src/security/tutorial003.py +++ b/docs_src/security/tutorial003_py39.py @@ -60,7 +60,7 @@ async def get_current_user(token: str = Depends(oauth2_scheme)): if not user: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, - detail="Invalid authentication credentials", + detail="Not authenticated", headers={"WWW-Authenticate": "Bearer"}, ) return user diff --git a/docs_src/security/tutorial004_an.py b/docs_src/security/tutorial004_an.py deleted file mode 100644 index 018234e300..0000000000 --- a/docs_src/security/tutorial004_an.py +++ /dev/null @@ -1,148 +0,0 @@ -from datetime import datetime, timedelta, timezone -from typing import Union - -import jwt -from fastapi import Depends, FastAPI, HTTPException, status -from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm -from jwt.exceptions import InvalidTokenError -from pwdlib import PasswordHash -from pydantic import BaseModel -from typing_extensions import Annotated - -# to get a string like this run: -# openssl rand -hex 32 -SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7" -ALGORITHM = "HS256" -ACCESS_TOKEN_EXPIRE_MINUTES = 30 - - -fake_users_db = { - "johndoe": { - "username": "johndoe", - "full_name": "John Doe", - "email": "johndoe@example.com", - "hashed_password": "$argon2id$v=19$m=65536,t=3,p=4$wagCPXjifgvUFBzq4hqe3w$CYaIb8sB+wtD+Vu/P4uod1+Qof8h+1g7bbDlBID48Rc", - "disabled": False, - } -} - - -class Token(BaseModel): - access_token: str - token_type: str - - -class TokenData(BaseModel): - username: Union[str, None] = None - - -class User(BaseModel): - username: str - email: Union[str, None] = None - full_name: Union[str, None] = None - disabled: Union[bool, None] = None - - -class UserInDB(User): - hashed_password: str - - -password_hash = PasswordHash.recommended() - -oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") - -app = FastAPI() - - -def verify_password(plain_password, hashed_password): - return password_hash.verify(plain_password, hashed_password) - - -def get_password_hash(password): - return password_hash.hash(password) - - -def get_user(db, username: str): - if username in db: - user_dict = db[username] - return UserInDB(**user_dict) - - -def authenticate_user(fake_db, username: str, password: str): - user = get_user(fake_db, username) - if not user: - return False - if not verify_password(password, user.hashed_password): - return False - return user - - -def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None): - to_encode = data.copy() - if expires_delta: - expire = datetime.now(timezone.utc) + expires_delta - else: - expire = datetime.now(timezone.utc) + timedelta(minutes=15) - to_encode.update({"exp": expire}) - encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) - return encoded_jwt - - -async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]): - credentials_exception = HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Could not validate credentials", - headers={"WWW-Authenticate": "Bearer"}, - ) - try: - payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) - username = payload.get("sub") - if username is None: - raise credentials_exception - token_data = TokenData(username=username) - except InvalidTokenError: - raise credentials_exception - user = get_user(fake_users_db, username=token_data.username) - if user is None: - raise credentials_exception - return user - - -async def get_current_active_user( - current_user: Annotated[User, Depends(get_current_user)], -): - if current_user.disabled: - raise HTTPException(status_code=400, detail="Inactive user") - return current_user - - -@app.post("/token") -async def login_for_access_token( - form_data: Annotated[OAuth2PasswordRequestForm, Depends()], -) -> Token: - user = authenticate_user(fake_users_db, form_data.username, form_data.password) - if not user: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Incorrect username or password", - headers={"WWW-Authenticate": "Bearer"}, - ) - access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) - access_token = create_access_token( - data={"sub": user.username}, expires_delta=access_token_expires - ) - return Token(access_token=access_token, token_type="bearer") - - -@app.get("/users/me/", response_model=User) -async def read_users_me( - current_user: Annotated[User, Depends(get_current_active_user)], -): - return current_user - - -@app.get("/users/me/items/") -async def read_own_items( - current_user: Annotated[User, Depends(get_current_active_user)], -): - return [{"item_id": "Foo", "owner": current_user.username}] diff --git a/docs_src/security/tutorial004.py b/docs_src/security/tutorial004_py39.py similarity index 100% rename from docs_src/security/tutorial004.py rename to docs_src/security/tutorial004_py39.py diff --git a/docs_src/security/tutorial005.py b/docs_src/security/tutorial005.py deleted file mode 100644 index fdd73bcd8a..0000000000 --- a/docs_src/security/tutorial005.py +++ /dev/null @@ -1,177 +0,0 @@ -from datetime import datetime, timedelta, timezone -from typing import List, Union - -import jwt -from fastapi import Depends, FastAPI, HTTPException, Security, status -from fastapi.security import ( - OAuth2PasswordBearer, - OAuth2PasswordRequestForm, - SecurityScopes, -) -from jwt.exceptions import InvalidTokenError -from pwdlib import PasswordHash -from pydantic import BaseModel, ValidationError - -# to get a string like this run: -# openssl rand -hex 32 -SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7" -ALGORITHM = "HS256" -ACCESS_TOKEN_EXPIRE_MINUTES = 30 - - -fake_users_db = { - "johndoe": { - "username": "johndoe", - "full_name": "John Doe", - "email": "johndoe@example.com", - "hashed_password": "$argon2id$v=19$m=65536,t=3,p=4$wagCPXjifgvUFBzq4hqe3w$CYaIb8sB+wtD+Vu/P4uod1+Qof8h+1g7bbDlBID48Rc", - "disabled": False, - }, - "alice": { - "username": "alice", - "full_name": "Alice Chains", - "email": "alicechains@example.com", - "hashed_password": "$argon2id$v=19$m=65536,t=3,p=4$g2/AV1zwopqUntPKJavBFw$BwpRGDCyUHLvHICnwijyX8ROGoiUPwNKZ7915MeYfCE", - "disabled": True, - }, -} - - -class Token(BaseModel): - access_token: str - token_type: str - - -class TokenData(BaseModel): - username: Union[str, None] = None - scopes: List[str] = [] - - -class User(BaseModel): - username: str - email: Union[str, None] = None - full_name: Union[str, None] = None - disabled: Union[bool, None] = None - - -class UserInDB(User): - hashed_password: str - - -password_hash = PasswordHash.recommended() - -oauth2_scheme = OAuth2PasswordBearer( - tokenUrl="token", - scopes={"me": "Read information about the current user.", "items": "Read items."}, -) - -app = FastAPI() - - -def verify_password(plain_password, hashed_password): - return password_hash.verify(plain_password, hashed_password) - - -def get_password_hash(password): - return password_hash.hash(password) - - -def get_user(db, username: str): - if username in db: - user_dict = db[username] - return UserInDB(**user_dict) - - -def authenticate_user(fake_db, username: str, password: str): - user = get_user(fake_db, username) - if not user: - return False - if not verify_password(password, user.hashed_password): - return False - return user - - -def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None): - to_encode = data.copy() - if expires_delta: - expire = datetime.now(timezone.utc) + expires_delta - else: - expire = datetime.now(timezone.utc) + timedelta(minutes=15) - to_encode.update({"exp": expire}) - encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) - return encoded_jwt - - -async def get_current_user( - security_scopes: SecurityScopes, token: str = Depends(oauth2_scheme) -): - if security_scopes.scopes: - authenticate_value = f'Bearer scope="{security_scopes.scope_str}"' - else: - authenticate_value = "Bearer" - credentials_exception = HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Could not validate credentials", - headers={"WWW-Authenticate": authenticate_value}, - ) - try: - payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) - username: str = payload.get("sub") - if username is None: - raise credentials_exception - scope: str = payload.get("scope", "") - token_scopes = scope.split(" ") - token_data = TokenData(scopes=token_scopes, username=username) - except (InvalidTokenError, ValidationError): - raise credentials_exception - user = get_user(fake_users_db, username=token_data.username) - if user is None: - raise credentials_exception - for scope in security_scopes.scopes: - if scope not in token_data.scopes: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Not enough permissions", - headers={"WWW-Authenticate": authenticate_value}, - ) - return user - - -async def get_current_active_user( - current_user: User = Security(get_current_user, scopes=["me"]), -): - if current_user.disabled: - raise HTTPException(status_code=400, detail="Inactive user") - return current_user - - -@app.post("/token") -async def login_for_access_token( - form_data: OAuth2PasswordRequestForm = Depends(), -) -> Token: - user = authenticate_user(fake_users_db, form_data.username, form_data.password) - if not user: - raise HTTPException(status_code=400, detail="Incorrect username or password") - access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) - access_token = create_access_token( - data={"sub": user.username, "scope": " ".join(form_data.scopes)}, - expires_delta=access_token_expires, - ) - return Token(access_token=access_token, token_type="bearer") - - -@app.get("/users/me/", response_model=User) -async def read_users_me(current_user: User = Depends(get_current_active_user)): - return current_user - - -@app.get("/users/me/items/") -async def read_own_items( - current_user: User = Security(get_current_active_user, scopes=["items"]), -): - return [{"item_id": "Foo", "owner": current_user.username}] - - -@app.get("/status/") -async def read_system_status(current_user: User = Depends(get_current_user)): - return {"status": "ok"} diff --git a/docs_src/security/tutorial005_an.py b/docs_src/security/tutorial005_an.py deleted file mode 100644 index e1d7b4f62a..0000000000 --- a/docs_src/security/tutorial005_an.py +++ /dev/null @@ -1,180 +0,0 @@ -from datetime import datetime, timedelta, timezone -from typing import List, Union - -import jwt -from fastapi import Depends, FastAPI, HTTPException, Security, status -from fastapi.security import ( - OAuth2PasswordBearer, - OAuth2PasswordRequestForm, - SecurityScopes, -) -from jwt.exceptions import InvalidTokenError -from pwdlib import PasswordHash -from pydantic import BaseModel, ValidationError -from typing_extensions import Annotated - -# to get a string like this run: -# openssl rand -hex 32 -SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7" -ALGORITHM = "HS256" -ACCESS_TOKEN_EXPIRE_MINUTES = 30 - - -fake_users_db = { - "johndoe": { - "username": "johndoe", - "full_name": "John Doe", - "email": "johndoe@example.com", - "hashed_password": "$argon2id$v=19$m=65536,t=3,p=4$wagCPXjifgvUFBzq4hqe3w$CYaIb8sB+wtD+Vu/P4uod1+Qof8h+1g7bbDlBID48Rc", - "disabled": False, - }, - "alice": { - "username": "alice", - "full_name": "Alice Chains", - "email": "alicechains@example.com", - "hashed_password": "$argon2id$v=19$m=65536,t=3,p=4$g2/AV1zwopqUntPKJavBFw$BwpRGDCyUHLvHICnwijyX8ROGoiUPwNKZ7915MeYfCE", - "disabled": True, - }, -} - - -class Token(BaseModel): - access_token: str - token_type: str - - -class TokenData(BaseModel): - username: Union[str, None] = None - scopes: List[str] = [] - - -class User(BaseModel): - username: str - email: Union[str, None] = None - full_name: Union[str, None] = None - disabled: Union[bool, None] = None - - -class UserInDB(User): - hashed_password: str - - -password_hash = PasswordHash.recommended() - -oauth2_scheme = OAuth2PasswordBearer( - tokenUrl="token", - scopes={"me": "Read information about the current user.", "items": "Read items."}, -) - -app = FastAPI() - - -def verify_password(plain_password, hashed_password): - return password_hash.verify(plain_password, hashed_password) - - -def get_password_hash(password): - return password_hash.hash(password) - - -def get_user(db, username: str): - if username in db: - user_dict = db[username] - return UserInDB(**user_dict) - - -def authenticate_user(fake_db, username: str, password: str): - user = get_user(fake_db, username) - if not user: - return False - if not verify_password(password, user.hashed_password): - return False - return user - - -def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None): - to_encode = data.copy() - if expires_delta: - expire = datetime.now(timezone.utc) + expires_delta - else: - expire = datetime.now(timezone.utc) + timedelta(minutes=15) - to_encode.update({"exp": expire}) - encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) - return encoded_jwt - - -async def get_current_user( - security_scopes: SecurityScopes, token: Annotated[str, Depends(oauth2_scheme)] -): - if security_scopes.scopes: - authenticate_value = f'Bearer scope="{security_scopes.scope_str}"' - else: - authenticate_value = "Bearer" - credentials_exception = HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Could not validate credentials", - headers={"WWW-Authenticate": authenticate_value}, - ) - try: - payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) - username = payload.get("sub") - if username is None: - raise credentials_exception - scope: str = payload.get("scope", "") - token_scopes = scope.split(" ") - token_data = TokenData(scopes=token_scopes, username=username) - except (InvalidTokenError, ValidationError): - raise credentials_exception - user = get_user(fake_users_db, username=token_data.username) - if user is None: - raise credentials_exception - for scope in security_scopes.scopes: - if scope not in token_data.scopes: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Not enough permissions", - headers={"WWW-Authenticate": authenticate_value}, - ) - return user - - -async def get_current_active_user( - current_user: Annotated[User, Security(get_current_user, scopes=["me"])], -): - if current_user.disabled: - raise HTTPException(status_code=400, detail="Inactive user") - return current_user - - -@app.post("/token") -async def login_for_access_token( - form_data: Annotated[OAuth2PasswordRequestForm, Depends()], -) -> Token: - user = authenticate_user(fake_users_db, form_data.username, form_data.password) - if not user: - raise HTTPException(status_code=400, detail="Incorrect username or password") - access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) - access_token = create_access_token( - data={"sub": user.username, "scope": " ".join(form_data.scopes)}, - expires_delta=access_token_expires, - ) - return Token(access_token=access_token, token_type="bearer") - - -@app.get("/users/me/", response_model=User) -async def read_users_me( - current_user: Annotated[User, Depends(get_current_active_user)], -): - return current_user - - -@app.get("/users/me/items/") -async def read_own_items( - current_user: Annotated[User, Security(get_current_active_user, scopes=["items"])], -): - return [{"item_id": "Foo", "owner": current_user.username}] - - -@app.get("/status/") -async def read_system_status(current_user: Annotated[User, Depends(get_current_user)]): - return {"status": "ok"} diff --git a/docs_src/security/tutorial006_an.py b/docs_src/security/tutorial006_an.py deleted file mode 100644 index 985e4b2ad2..0000000000 --- a/docs_src/security/tutorial006_an.py +++ /dev/null @@ -1,12 +0,0 @@ -from fastapi import Depends, FastAPI -from fastapi.security import HTTPBasic, HTTPBasicCredentials -from typing_extensions import Annotated - -app = FastAPI() - -security = HTTPBasic() - - -@app.get("/users/me") -def read_current_user(credentials: Annotated[HTTPBasicCredentials, Depends(security)]): - return {"username": credentials.username, "password": credentials.password} diff --git a/docs_src/security/tutorial006.py b/docs_src/security/tutorial006_py39.py similarity index 100% rename from docs_src/security/tutorial006.py rename to docs_src/security/tutorial006_py39.py diff --git a/docs_src/security/tutorial007_an.py b/docs_src/security/tutorial007_an.py deleted file mode 100644 index 0d211dfde5..0000000000 --- a/docs_src/security/tutorial007_an.py +++ /dev/null @@ -1,36 +0,0 @@ -import secrets - -from fastapi import Depends, FastAPI, HTTPException, status -from fastapi.security import HTTPBasic, HTTPBasicCredentials -from typing_extensions import Annotated - -app = FastAPI() - -security = HTTPBasic() - - -def get_current_username( - credentials: Annotated[HTTPBasicCredentials, Depends(security)], -): - current_username_bytes = credentials.username.encode("utf8") - correct_username_bytes = b"stanleyjobson" - is_correct_username = secrets.compare_digest( - current_username_bytes, correct_username_bytes - ) - current_password_bytes = credentials.password.encode("utf8") - correct_password_bytes = b"swordfish" - is_correct_password = secrets.compare_digest( - current_password_bytes, correct_password_bytes - ) - if not (is_correct_username and is_correct_password): - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Incorrect username or password", - headers={"WWW-Authenticate": "Basic"}, - ) - return credentials.username - - -@app.get("/users/me") -def read_current_user(username: Annotated[str, Depends(get_current_username)]): - return {"username": username} diff --git a/docs_src/security/tutorial007.py b/docs_src/security/tutorial007_py39.py similarity index 100% rename from docs_src/security/tutorial007.py rename to docs_src/security/tutorial007_py39.py diff --git a/docs_src/separate_openapi_schemas/__init__.py b/docs_src/separate_openapi_schemas/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/separate_openapi_schemas/tutorial001.py b/docs_src/separate_openapi_schemas/tutorial001.py deleted file mode 100644 index 415eef8e28..0000000000 --- a/docs_src/separate_openapi_schemas/tutorial001.py +++ /dev/null @@ -1,28 +0,0 @@ -from typing import List, Union - -from fastapi import FastAPI -from pydantic import BaseModel - - -class Item(BaseModel): - name: str - description: Union[str, None] = None - - -app = FastAPI() - - -@app.post("/items/") -def create_item(item: Item): - return item - - -@app.get("/items/") -def read_items() -> List[Item]: - return [ - Item( - name="Portal Gun", - description="Device to travel through the multi-rick-verse", - ), - Item(name="Plumbus"), - ] diff --git a/docs_src/separate_openapi_schemas/tutorial002.py b/docs_src/separate_openapi_schemas/tutorial002.py deleted file mode 100644 index 7df93783b9..0000000000 --- a/docs_src/separate_openapi_schemas/tutorial002.py +++ /dev/null @@ -1,28 +0,0 @@ -from typing import List, Union - -from fastapi import FastAPI -from pydantic import BaseModel - - -class Item(BaseModel): - name: str - description: Union[str, None] = None - - -app = FastAPI(separate_input_output_schemas=False) - - -@app.post("/items/") -def create_item(item: Item): - return item - - -@app.get("/items/") -def read_items() -> List[Item]: - return [ - Item( - name="Portal Gun", - description="Device to travel through the multi-rick-verse", - ), - Item(name="Plumbus"), - ] diff --git a/docs_src/settings/__init__.py b/docs_src/settings/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/settings/app01_py39/__init__.py b/docs_src/settings/app01_py39/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/settings/app01/config.py b/docs_src/settings/app01_py39/config.py similarity index 100% rename from docs_src/settings/app01/config.py rename to docs_src/settings/app01_py39/config.py diff --git a/docs_src/settings/app01/main.py b/docs_src/settings/app01_py39/main.py similarity index 100% rename from docs_src/settings/app01/main.py rename to docs_src/settings/app01_py39/main.py diff --git a/docs_src/settings/app02_an/config.py b/docs_src/settings/app02_an/config.py deleted file mode 100644 index e17b5035dc..0000000000 --- a/docs_src/settings/app02_an/config.py +++ /dev/null @@ -1,7 +0,0 @@ -from pydantic_settings import BaseSettings - - -class Settings(BaseSettings): - app_name: str = "Awesome API" - admin_email: str - items_per_user: int = 50 diff --git a/docs_src/settings/app02_an/main.py b/docs_src/settings/app02_an/main.py deleted file mode 100644 index 3a578cc338..0000000000 --- a/docs_src/settings/app02_an/main.py +++ /dev/null @@ -1,22 +0,0 @@ -from functools import lru_cache - -from fastapi import Depends, FastAPI -from typing_extensions import Annotated - -from .config import Settings - -app = FastAPI() - - -@lru_cache -def get_settings(): - return Settings() - - -@app.get("/info") -async def info(settings: Annotated[Settings, Depends(get_settings)]): - return { - "app_name": settings.app_name, - "admin_email": settings.admin_email, - "items_per_user": settings.items_per_user, - } diff --git a/docs_src/settings/app02_an/test_main.py b/docs_src/settings/app02_an/test_main.py deleted file mode 100644 index 7a04d7e8ee..0000000000 --- a/docs_src/settings/app02_an/test_main.py +++ /dev/null @@ -1,23 +0,0 @@ -from fastapi.testclient import TestClient - -from .config import Settings -from .main import app, get_settings - -client = TestClient(app) - - -def get_settings_override(): - return Settings(admin_email="testing_admin@example.com") - - -app.dependency_overrides[get_settings] = get_settings_override - - -def test_app(): - response = client.get("/info") - data = response.json() - assert data == { - "app_name": "Awesome API", - "admin_email": "testing_admin@example.com", - "items_per_user": 50, - } diff --git a/docs_src/settings/app02_py39/__init__.py b/docs_src/settings/app02_py39/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/settings/app02/config.py b/docs_src/settings/app02_py39/config.py similarity index 100% rename from docs_src/settings/app02/config.py rename to docs_src/settings/app02_py39/config.py diff --git a/docs_src/settings/app02/main.py b/docs_src/settings/app02_py39/main.py similarity index 100% rename from docs_src/settings/app02/main.py rename to docs_src/settings/app02_py39/main.py diff --git a/docs_src/settings/app02/test_main.py b/docs_src/settings/app02_py39/test_main.py similarity index 100% rename from docs_src/settings/app02/test_main.py rename to docs_src/settings/app02_py39/test_main.py diff --git a/docs_src/settings/app03_an/main.py b/docs_src/settings/app03_an/main.py deleted file mode 100644 index 2f64b9cd17..0000000000 --- a/docs_src/settings/app03_an/main.py +++ /dev/null @@ -1,22 +0,0 @@ -from functools import lru_cache -from typing import Annotated - -from fastapi import Depends, FastAPI - -from . import config - -app = FastAPI() - - -@lru_cache -def get_settings(): - return config.Settings() - - -@app.get("/info") -async def info(settings: Annotated[config.Settings, Depends(get_settings)]): - return { - "app_name": settings.app_name, - "admin_email": settings.admin_email, - "items_per_user": settings.items_per_user, - } diff --git a/docs_src/settings/app03_an_py39/config.py b/docs_src/settings/app03_an_py39/config.py index 942aea3e58..08f8f88c28 100644 --- a/docs_src/settings/app03_an_py39/config.py +++ b/docs_src/settings/app03_an_py39/config.py @@ -1,4 +1,4 @@ -from pydantic_settings import BaseSettings +from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): @@ -6,5 +6,4 @@ class Settings(BaseSettings): admin_email: str items_per_user: int = 50 - class Config: - env_file = ".env" + model_config = SettingsConfigDict(env_file=".env") diff --git a/docs_src/settings/app03_an/config_pv1.py b/docs_src/settings/app03_an_py39/config_pv1.py similarity index 81% rename from docs_src/settings/app03_an/config_pv1.py rename to docs_src/settings/app03_an_py39/config_pv1.py index e1c3ee3006..7ae66ef77c 100644 --- a/docs_src/settings/app03_an/config_pv1.py +++ b/docs_src/settings/app03_an_py39/config_pv1.py @@ -1,4 +1,4 @@ -from pydantic import BaseSettings +from pydantic.v1 import BaseSettings class Settings(BaseSettings): diff --git a/docs_src/settings/app03_an_py39/main.py b/docs_src/settings/app03_an_py39/main.py index 62f3476396..2f64b9cd17 100644 --- a/docs_src/settings/app03_an_py39/main.py +++ b/docs_src/settings/app03_an_py39/main.py @@ -1,7 +1,7 @@ from functools import lru_cache +from typing import Annotated from fastapi import Depends, FastAPI -from typing_extensions import Annotated from . import config diff --git a/docs_src/settings/app03_py39/__init__.py b/docs_src/settings/app03_py39/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/settings/app03_an/config.py b/docs_src/settings/app03_py39/config.py similarity index 100% rename from docs_src/settings/app03_an/config.py rename to docs_src/settings/app03_py39/config.py diff --git a/docs_src/settings/app03/config.py b/docs_src/settings/app03_py39/config_pv1.py similarity index 78% rename from docs_src/settings/app03/config.py rename to docs_src/settings/app03_py39/config_pv1.py index 942aea3e58..7ae66ef77c 100644 --- a/docs_src/settings/app03/config.py +++ b/docs_src/settings/app03_py39/config_pv1.py @@ -1,4 +1,4 @@ -from pydantic_settings import BaseSettings +from pydantic.v1 import BaseSettings class Settings(BaseSettings): diff --git a/docs_src/settings/app03/main.py b/docs_src/settings/app03_py39/main.py similarity index 100% rename from docs_src/settings/app03/main.py rename to docs_src/settings/app03_py39/main.py diff --git a/docs_src/settings/tutorial001_pv1.py b/docs_src/settings/tutorial001_pv1_py39.py similarity index 91% rename from docs_src/settings/tutorial001_pv1.py rename to docs_src/settings/tutorial001_pv1_py39.py index 0cfd1b6632..20ad2bbf62 100644 --- a/docs_src/settings/tutorial001_pv1.py +++ b/docs_src/settings/tutorial001_pv1_py39.py @@ -1,5 +1,5 @@ from fastapi import FastAPI -from pydantic import BaseSettings +from pydantic.v1 import BaseSettings class Settings(BaseSettings): diff --git a/docs_src/settings/tutorial001.py b/docs_src/settings/tutorial001_py39.py similarity index 100% rename from docs_src/settings/tutorial001.py rename to docs_src/settings/tutorial001_py39.py diff --git a/docs_src/sql_databases/tutorial001.py b/docs_src/sql_databases/tutorial001.py deleted file mode 100644 index be86ec0eeb..0000000000 --- a/docs_src/sql_databases/tutorial001.py +++ /dev/null @@ -1,71 +0,0 @@ -from typing import List, Union - -from fastapi import Depends, FastAPI, HTTPException, Query -from sqlmodel import Field, Session, SQLModel, create_engine, select - - -class Hero(SQLModel, table=True): - id: Union[int, None] = Field(default=None, primary_key=True) - name: str = Field(index=True) - age: Union[int, None] = Field(default=None, index=True) - secret_name: str - - -sqlite_file_name = "database.db" -sqlite_url = f"sqlite:///{sqlite_file_name}" - -connect_args = {"check_same_thread": False} -engine = create_engine(sqlite_url, connect_args=connect_args) - - -def create_db_and_tables(): - SQLModel.metadata.create_all(engine) - - -def get_session(): - with Session(engine) as session: - yield session - - -app = FastAPI() - - -@app.on_event("startup") -def on_startup(): - create_db_and_tables() - - -@app.post("/heroes/") -def create_hero(hero: Hero, session: Session = Depends(get_session)) -> Hero: - session.add(hero) - session.commit() - session.refresh(hero) - return hero - - -@app.get("/heroes/") -def read_heroes( - session: Session = Depends(get_session), - offset: int = 0, - limit: int = Query(default=100, le=100), -) -> List[Hero]: - heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() - return heroes - - -@app.get("/heroes/{hero_id}") -def read_hero(hero_id: int, session: Session = Depends(get_session)) -> Hero: - hero = session.get(Hero, hero_id) - if not hero: - raise HTTPException(status_code=404, detail="Hero not found") - return hero - - -@app.delete("/heroes/{hero_id}") -def delete_hero(hero_id: int, session: Session = Depends(get_session)): - hero = session.get(Hero, hero_id) - if not hero: - raise HTTPException(status_code=404, detail="Hero not found") - session.delete(hero) - session.commit() - return {"ok": True} diff --git a/docs_src/sql_databases/tutorial001_an.py b/docs_src/sql_databases/tutorial001_an.py deleted file mode 100644 index 8c000d31c7..0000000000 --- a/docs_src/sql_databases/tutorial001_an.py +++ /dev/null @@ -1,74 +0,0 @@ -from typing import List, Union - -from fastapi import Depends, FastAPI, HTTPException, Query -from sqlmodel import Field, Session, SQLModel, create_engine, select -from typing_extensions import Annotated - - -class Hero(SQLModel, table=True): - id: Union[int, None] = Field(default=None, primary_key=True) - name: str = Field(index=True) - age: Union[int, None] = Field(default=None, index=True) - secret_name: str - - -sqlite_file_name = "database.db" -sqlite_url = f"sqlite:///{sqlite_file_name}" - -connect_args = {"check_same_thread": False} -engine = create_engine(sqlite_url, connect_args=connect_args) - - -def create_db_and_tables(): - SQLModel.metadata.create_all(engine) - - -def get_session(): - with Session(engine) as session: - yield session - - -SessionDep = Annotated[Session, Depends(get_session)] - -app = FastAPI() - - -@app.on_event("startup") -def on_startup(): - create_db_and_tables() - - -@app.post("/heroes/") -def create_hero(hero: Hero, session: SessionDep) -> Hero: - session.add(hero) - session.commit() - session.refresh(hero) - return hero - - -@app.get("/heroes/") -def read_heroes( - session: SessionDep, - offset: int = 0, - limit: Annotated[int, Query(le=100)] = 100, -) -> List[Hero]: - heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() - return heroes - - -@app.get("/heroes/{hero_id}") -def read_hero(hero_id: int, session: SessionDep) -> Hero: - hero = session.get(Hero, hero_id) - if not hero: - raise HTTPException(status_code=404, detail="Hero not found") - return hero - - -@app.delete("/heroes/{hero_id}") -def delete_hero(hero_id: int, session: SessionDep): - hero = session.get(Hero, hero_id) - if not hero: - raise HTTPException(status_code=404, detail="Hero not found") - session.delete(hero) - session.commit() - return {"ok": True} diff --git a/docs_src/sql_databases/tutorial002.py b/docs_src/sql_databases/tutorial002.py deleted file mode 100644 index 4350d19c61..0000000000 --- a/docs_src/sql_databases/tutorial002.py +++ /dev/null @@ -1,104 +0,0 @@ -from typing import List, Union - -from fastapi import Depends, FastAPI, HTTPException, Query -from sqlmodel import Field, Session, SQLModel, create_engine, select - - -class HeroBase(SQLModel): - name: str = Field(index=True) - age: Union[int, None] = Field(default=None, index=True) - - -class Hero(HeroBase, table=True): - id: Union[int, None] = Field(default=None, primary_key=True) - secret_name: str - - -class HeroPublic(HeroBase): - id: int - - -class HeroCreate(HeroBase): - secret_name: str - - -class HeroUpdate(HeroBase): - name: Union[str, None] = None - age: Union[int, None] = None - secret_name: Union[str, None] = None - - -sqlite_file_name = "database.db" -sqlite_url = f"sqlite:///{sqlite_file_name}" - -connect_args = {"check_same_thread": False} -engine = create_engine(sqlite_url, connect_args=connect_args) - - -def create_db_and_tables(): - SQLModel.metadata.create_all(engine) - - -def get_session(): - with Session(engine) as session: - yield session - - -app = FastAPI() - - -@app.on_event("startup") -def on_startup(): - create_db_and_tables() - - -@app.post("/heroes/", response_model=HeroPublic) -def create_hero(hero: HeroCreate, session: Session = Depends(get_session)): - db_hero = Hero.model_validate(hero) - session.add(db_hero) - session.commit() - session.refresh(db_hero) - return db_hero - - -@app.get("/heroes/", response_model=List[HeroPublic]) -def read_heroes( - session: Session = Depends(get_session), - offset: int = 0, - limit: int = Query(default=100, le=100), -): - heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() - return heroes - - -@app.get("/heroes/{hero_id}", response_model=HeroPublic) -def read_hero(hero_id: int, session: Session = Depends(get_session)): - hero = session.get(Hero, hero_id) - if not hero: - raise HTTPException(status_code=404, detail="Hero not found") - return hero - - -@app.patch("/heroes/{hero_id}", response_model=HeroPublic) -def update_hero( - hero_id: int, hero: HeroUpdate, session: Session = Depends(get_session) -): - hero_db = session.get(Hero, hero_id) - if not hero_db: - raise HTTPException(status_code=404, detail="Hero not found") - hero_data = hero.model_dump(exclude_unset=True) - hero_db.sqlmodel_update(hero_data) - session.add(hero_db) - session.commit() - session.refresh(hero_db) - return hero_db - - -@app.delete("/heroes/{hero_id}") -def delete_hero(hero_id: int, session: Session = Depends(get_session)): - hero = session.get(Hero, hero_id) - if not hero: - raise HTTPException(status_code=404, detail="Hero not found") - session.delete(hero) - session.commit() - return {"ok": True} diff --git a/docs_src/sql_databases/tutorial002_an.py b/docs_src/sql_databases/tutorial002_an.py deleted file mode 100644 index 15e3d7c3a5..0000000000 --- a/docs_src/sql_databases/tutorial002_an.py +++ /dev/null @@ -1,104 +0,0 @@ -from typing import List, Union - -from fastapi import Depends, FastAPI, HTTPException, Query -from sqlmodel import Field, Session, SQLModel, create_engine, select -from typing_extensions import Annotated - - -class HeroBase(SQLModel): - name: str = Field(index=True) - age: Union[int, None] = Field(default=None, index=True) - - -class Hero(HeroBase, table=True): - id: Union[int, None] = Field(default=None, primary_key=True) - secret_name: str - - -class HeroPublic(HeroBase): - id: int - - -class HeroCreate(HeroBase): - secret_name: str - - -class HeroUpdate(HeroBase): - name: Union[str, None] = None - age: Union[int, None] = None - secret_name: Union[str, None] = None - - -sqlite_file_name = "database.db" -sqlite_url = f"sqlite:///{sqlite_file_name}" - -connect_args = {"check_same_thread": False} -engine = create_engine(sqlite_url, connect_args=connect_args) - - -def create_db_and_tables(): - SQLModel.metadata.create_all(engine) - - -def get_session(): - with Session(engine) as session: - yield session - - -SessionDep = Annotated[Session, Depends(get_session)] -app = FastAPI() - - -@app.on_event("startup") -def on_startup(): - create_db_and_tables() - - -@app.post("/heroes/", response_model=HeroPublic) -def create_hero(hero: HeroCreate, session: SessionDep): - db_hero = Hero.model_validate(hero) - session.add(db_hero) - session.commit() - session.refresh(db_hero) - return db_hero - - -@app.get("/heroes/", response_model=List[HeroPublic]) -def read_heroes( - session: SessionDep, - offset: int = 0, - limit: Annotated[int, Query(le=100)] = 100, -): - heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() - return heroes - - -@app.get("/heroes/{hero_id}", response_model=HeroPublic) -def read_hero(hero_id: int, session: SessionDep): - hero = session.get(Hero, hero_id) - if not hero: - raise HTTPException(status_code=404, detail="Hero not found") - return hero - - -@app.patch("/heroes/{hero_id}", response_model=HeroPublic) -def update_hero(hero_id: int, hero: HeroUpdate, session: SessionDep): - hero_db = session.get(Hero, hero_id) - if not hero_db: - raise HTTPException(status_code=404, detail="Hero not found") - hero_data = hero.model_dump(exclude_unset=True) - hero_db.sqlmodel_update(hero_data) - session.add(hero_db) - session.commit() - session.refresh(hero_db) - return hero_db - - -@app.delete("/heroes/{hero_id}") -def delete_hero(hero_id: int, session: SessionDep): - hero = session.get(Hero, hero_id) - if not hero: - raise HTTPException(status_code=404, detail="Hero not found") - session.delete(hero) - session.commit() - return {"ok": True} diff --git a/docs_src/static_files/__init__.py b/docs_src/static_files/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/static_files/tutorial001.py b/docs_src/static_files/tutorial001_py39.py similarity index 100% rename from docs_src/static_files/tutorial001.py rename to docs_src/static_files/tutorial001_py39.py diff --git a/docs_src/sub_applications/__init__.py b/docs_src/sub_applications/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/sub_applications/tutorial001.py b/docs_src/sub_applications/tutorial001_py39.py similarity index 100% rename from docs_src/sub_applications/tutorial001.py rename to docs_src/sub_applications/tutorial001_py39.py diff --git a/docs_src/templates/__init__.py b/docs_src/templates/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/templates/static/__init__.py b/docs_src/templates/static/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/templates/templates/__init__.py b/docs_src/templates/templates/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/templates/tutorial001.py b/docs_src/templates/tutorial001_py39.py similarity index 100% rename from docs_src/templates/tutorial001.py rename to docs_src/templates/tutorial001_py39.py diff --git a/docs_src/using_request_directly/__init__.py b/docs_src/using_request_directly/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/using_request_directly/tutorial001.py b/docs_src/using_request_directly/tutorial001_py39.py similarity index 100% rename from docs_src/using_request_directly/tutorial001.py rename to docs_src/using_request_directly/tutorial001_py39.py diff --git a/docs_src/websockets/tutorial001.py b/docs_src/websockets/tutorial001_py39.py similarity index 100% rename from docs_src/websockets/tutorial001.py rename to docs_src/websockets/tutorial001_py39.py diff --git a/docs_src/websockets/tutorial002_an.py b/docs_src/websockets/tutorial002_an.py deleted file mode 100644 index c838fbd306..0000000000 --- a/docs_src/websockets/tutorial002_an.py +++ /dev/null @@ -1,93 +0,0 @@ -from typing import Union - -from fastapi import ( - Cookie, - Depends, - FastAPI, - Query, - WebSocket, - WebSocketException, - status, -) -from fastapi.responses import HTMLResponse -from typing_extensions import Annotated - -app = FastAPI() - -html = """ - - - - Chat - - -

WebSocket Chat

-
- - - -
- - -
-
    -
- - - -""" - - -@app.get("/") -async def get(): - return HTMLResponse(html) - - -async def get_cookie_or_token( - websocket: WebSocket, - session: Annotated[Union[str, None], Cookie()] = None, - token: Annotated[Union[str, None], Query()] = None, -): - if session is None and token is None: - raise WebSocketException(code=status.WS_1008_POLICY_VIOLATION) - return session or token - - -@app.websocket("/items/{item_id}/ws") -async def websocket_endpoint( - *, - websocket: WebSocket, - item_id: str, - q: Union[int, None] = None, - cookie_or_token: Annotated[str, Depends(get_cookie_or_token)], -): - await websocket.accept() - while True: - data = await websocket.receive_text() - await websocket.send_text( - f"Session cookie or query token value is: {cookie_or_token}" - ) - if q is not None: - await websocket.send_text(f"Query parameter q is: {q}") - await websocket.send_text(f"Message text was: {data}, for item ID: {item_id}") diff --git a/docs_src/websockets/tutorial002.py b/docs_src/websockets/tutorial002_py39.py similarity index 100% rename from docs_src/websockets/tutorial002.py rename to docs_src/websockets/tutorial002_py39.py diff --git a/docs_src/websockets/tutorial003.py b/docs_src/websockets/tutorial003.py deleted file mode 100644 index d561633a8d..0000000000 --- a/docs_src/websockets/tutorial003.py +++ /dev/null @@ -1,83 +0,0 @@ -from typing import List - -from fastapi import FastAPI, WebSocket, WebSocketDisconnect -from fastapi.responses import HTMLResponse - -app = FastAPI() - -html = """ - - - - Chat - - -

WebSocket Chat

-

Your ID:

-
- - -
-
    -
- - - -""" - - -class ConnectionManager: - def __init__(self): - self.active_connections: List[WebSocket] = [] - - async def connect(self, websocket: WebSocket): - await websocket.accept() - self.active_connections.append(websocket) - - def disconnect(self, websocket: WebSocket): - self.active_connections.remove(websocket) - - async def send_personal_message(self, message: str, websocket: WebSocket): - await websocket.send_text(message) - - async def broadcast(self, message: str): - for connection in self.active_connections: - await connection.send_text(message) - - -manager = ConnectionManager() - - -@app.get("/") -async def get(): - return HTMLResponse(html) - - -@app.websocket("/ws/{client_id}") -async def websocket_endpoint(websocket: WebSocket, client_id: int): - await manager.connect(websocket) - try: - while True: - data = await websocket.receive_text() - await manager.send_personal_message(f"You wrote: {data}", websocket) - await manager.broadcast(f"Client #{client_id} says: {data}") - except WebSocketDisconnect: - manager.disconnect(websocket) - await manager.broadcast(f"Client #{client_id} left the chat") diff --git a/docs_src/wsgi/__init__.py b/docs_src/wsgi/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/wsgi/tutorial001.py b/docs_src/wsgi/tutorial001_py39.py similarity index 100% rename from docs_src/wsgi/tutorial001.py rename to docs_src/wsgi/tutorial001_py39.py diff --git a/fastapi/__init__.py b/fastapi/__init__.py index f4a952bf5c..6133787b06 100644 --- a/fastapi/__init__.py +++ b/fastapi/__init__.py @@ -1,6 +1,6 @@ """FastAPI framework, high performance, easy to learn, fast to code, ready for production""" -__version__ = "0.121.0" +__version__ = "0.128.0" from starlette import status as status diff --git a/fastapi/_compat/__init__.py b/fastapi/_compat/__init__.py index 0aadd68de2..3dfaf9b712 100644 --- a/fastapi/_compat/__init__.py +++ b/fastapi/_compat/__init__.py @@ -1,44 +1,9 @@ -from .main import BaseConfig as BaseConfig -from .main import PydanticSchemaGenerationError as PydanticSchemaGenerationError -from .main import RequiredParam as RequiredParam -from .main import Undefined as Undefined -from .main import UndefinedType as UndefinedType -from .main import Url as Url -from .main import Validator as Validator -from .main import _get_model_config as _get_model_config -from .main import _is_error_wrapper as _is_error_wrapper -from .main import _is_model_class as _is_model_class -from .main import _is_model_field as _is_model_field -from .main import _is_undefined as _is_undefined -from .main import _model_dump as _model_dump -from .main import _model_rebuild as _model_rebuild -from .main import copy_field_info as copy_field_info -from .main import create_body_model as create_body_model -from .main import evaluate_forwardref as evaluate_forwardref -from .main import get_annotation_from_field_info as get_annotation_from_field_info -from .main import get_cached_model_fields as get_cached_model_fields -from .main import get_compat_model_name_map as get_compat_model_name_map -from .main import get_definitions as get_definitions -from .main import get_missing_field_error as get_missing_field_error -from .main import get_schema_from_model_field as get_schema_from_model_field -from .main import is_bytes_field as is_bytes_field -from .main import is_bytes_sequence_field as is_bytes_sequence_field -from .main import is_scalar_field as is_scalar_field -from .main import is_scalar_sequence_field as is_scalar_sequence_field -from .main import is_sequence_field as is_sequence_field -from .main import serialize_sequence_value as serialize_sequence_value -from .main import ( - with_info_plain_validator_function as with_info_plain_validator_function, -) -from .may_v1 import CoreSchema as CoreSchema -from .may_v1 import GetJsonSchemaHandler as GetJsonSchemaHandler -from .may_v1 import JsonSchemaValue as JsonSchemaValue -from .may_v1 import _normalize_errors as _normalize_errors -from .model_field import ModelField as ModelField from .shared import PYDANTIC_V2 as PYDANTIC_V2 from .shared import PYDANTIC_VERSION_MINOR_TUPLE as PYDANTIC_VERSION_MINOR_TUPLE from .shared import annotation_is_pydantic_v1 as annotation_is_pydantic_v1 from .shared import field_annotation_is_scalar as field_annotation_is_scalar +from .shared import is_pydantic_v1_model_class as is_pydantic_v1_model_class +from .shared import is_pydantic_v1_model_instance as is_pydantic_v1_model_instance from .shared import ( is_uploadfile_or_nonable_uploadfile_annotation as is_uploadfile_or_nonable_uploadfile_annotation, ) @@ -48,3 +13,29 @@ from .shared import ( from .shared import lenient_issubclass as lenient_issubclass from .shared import sequence_types as sequence_types from .shared import value_is_sequence as value_is_sequence +from .v2 import BaseConfig as BaseConfig +from .v2 import ModelField as ModelField +from .v2 import PydanticSchemaGenerationError as PydanticSchemaGenerationError +from .v2 import RequiredParam as RequiredParam +from .v2 import Undefined as Undefined +from .v2 import UndefinedType as UndefinedType +from .v2 import Url as Url +from .v2 import Validator as Validator +from .v2 import _regenerate_error_with_loc as _regenerate_error_with_loc +from .v2 import copy_field_info as copy_field_info +from .v2 import create_body_model as create_body_model +from .v2 import evaluate_forwardref as evaluate_forwardref +from .v2 import get_cached_model_fields as get_cached_model_fields +from .v2 import get_compat_model_name_map as get_compat_model_name_map +from .v2 import get_definitions as get_definitions +from .v2 import get_missing_field_error as get_missing_field_error +from .v2 import get_schema_from_model_field as get_schema_from_model_field +from .v2 import is_bytes_field as is_bytes_field +from .v2 import is_bytes_sequence_field as is_bytes_sequence_field +from .v2 import is_scalar_field as is_scalar_field +from .v2 import is_scalar_sequence_field as is_scalar_sequence_field +from .v2 import is_sequence_field as is_sequence_field +from .v2 import serialize_sequence_value as serialize_sequence_value +from .v2 import ( + with_info_plain_validator_function as with_info_plain_validator_function, +) diff --git a/fastapi/_compat/main.py b/fastapi/_compat/main.py deleted file mode 100644 index e5275950e8..0000000000 --- a/fastapi/_compat/main.py +++ /dev/null @@ -1,362 +0,0 @@ -import sys -from functools import lru_cache -from typing import ( - Any, - Dict, - List, - Sequence, - Tuple, - Type, -) - -from fastapi._compat import may_v1 -from fastapi._compat.shared import PYDANTIC_V2, lenient_issubclass -from fastapi.types import ModelNameMap -from pydantic import BaseModel -from typing_extensions import Literal - -from .model_field import ModelField - -if PYDANTIC_V2: - from .v2 import BaseConfig as BaseConfig - from .v2 import FieldInfo as FieldInfo - from .v2 import PydanticSchemaGenerationError as PydanticSchemaGenerationError - from .v2 import RequiredParam as RequiredParam - from .v2 import Undefined as Undefined - from .v2 import UndefinedType as UndefinedType - from .v2 import Url as Url - from .v2 import Validator as Validator - from .v2 import evaluate_forwardref as evaluate_forwardref - from .v2 import get_missing_field_error as get_missing_field_error - from .v2 import ( - with_info_plain_validator_function as with_info_plain_validator_function, - ) -else: - from .v1 import BaseConfig as BaseConfig # type: ignore[assignment] - from .v1 import FieldInfo as FieldInfo - from .v1 import ( # type: ignore[assignment] - PydanticSchemaGenerationError as PydanticSchemaGenerationError, - ) - from .v1 import RequiredParam as RequiredParam - from .v1 import Undefined as Undefined - from .v1 import UndefinedType as UndefinedType - from .v1 import Url as Url # type: ignore[assignment] - from .v1 import Validator as Validator - from .v1 import evaluate_forwardref as evaluate_forwardref - from .v1 import get_missing_field_error as get_missing_field_error - from .v1 import ( # type: ignore[assignment] - with_info_plain_validator_function as with_info_plain_validator_function, - ) - - -@lru_cache -def get_cached_model_fields(model: Type[BaseModel]) -> List[ModelField]: - if lenient_issubclass(model, may_v1.BaseModel): - from fastapi._compat import v1 - - return v1.get_model_fields(model) - else: - from . import v2 - - return v2.get_model_fields(model) # type: ignore[return-value] - - -def _is_undefined(value: object) -> bool: - if isinstance(value, may_v1.UndefinedType): - return True - elif PYDANTIC_V2: - from . import v2 - - return isinstance(value, v2.UndefinedType) - return False - - -def _get_model_config(model: BaseModel) -> Any: - if isinstance(model, may_v1.BaseModel): - from fastapi._compat import v1 - - return v1._get_model_config(model) - elif PYDANTIC_V2: - from . import v2 - - return v2._get_model_config(model) - - -def _model_dump( - model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any -) -> Any: - if isinstance(model, may_v1.BaseModel): - from fastapi._compat import v1 - - return v1._model_dump(model, mode=mode, **kwargs) - elif PYDANTIC_V2: - from . import v2 - - return v2._model_dump(model, mode=mode, **kwargs) - - -def _is_error_wrapper(exc: Exception) -> bool: - if isinstance(exc, may_v1.ErrorWrapper): - return True - elif PYDANTIC_V2: - from . import v2 - - return isinstance(exc, v2.ErrorWrapper) - return False - - -def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo: - if isinstance(field_info, may_v1.FieldInfo): - from fastapi._compat import v1 - - return v1.copy_field_info(field_info=field_info, annotation=annotation) - else: - assert PYDANTIC_V2 - from . import v2 - - return v2.copy_field_info(field_info=field_info, annotation=annotation) - - -def create_body_model( - *, fields: Sequence[ModelField], model_name: str -) -> Type[BaseModel]: - if fields and isinstance(fields[0], may_v1.ModelField): - from fastapi._compat import v1 - - return v1.create_body_model(fields=fields, model_name=model_name) - else: - assert PYDANTIC_V2 - from . import v2 - - return v2.create_body_model(fields=fields, model_name=model_name) # type: ignore[arg-type] - - -def get_annotation_from_field_info( - annotation: Any, field_info: FieldInfo, field_name: str -) -> Any: - if isinstance(field_info, may_v1.FieldInfo): - from fastapi._compat import v1 - - return v1.get_annotation_from_field_info( - annotation=annotation, field_info=field_info, field_name=field_name - ) - else: - assert PYDANTIC_V2 - from . import v2 - - return v2.get_annotation_from_field_info( - annotation=annotation, field_info=field_info, field_name=field_name - ) - - -def is_bytes_field(field: ModelField) -> bool: - if isinstance(field, may_v1.ModelField): - from fastapi._compat import v1 - - return v1.is_bytes_field(field) - else: - assert PYDANTIC_V2 - from . import v2 - - return v2.is_bytes_field(field) # type: ignore[arg-type] - - -def is_bytes_sequence_field(field: ModelField) -> bool: - if isinstance(field, may_v1.ModelField): - from fastapi._compat import v1 - - return v1.is_bytes_sequence_field(field) - else: - assert PYDANTIC_V2 - from . import v2 - - return v2.is_bytes_sequence_field(field) # type: ignore[arg-type] - - -def is_scalar_field(field: ModelField) -> bool: - if isinstance(field, may_v1.ModelField): - from fastapi._compat import v1 - - return v1.is_scalar_field(field) - else: - assert PYDANTIC_V2 - from . import v2 - - return v2.is_scalar_field(field) # type: ignore[arg-type] - - -def is_scalar_sequence_field(field: ModelField) -> bool: - if isinstance(field, may_v1.ModelField): - from fastapi._compat import v1 - - return v1.is_scalar_sequence_field(field) - else: - assert PYDANTIC_V2 - from . import v2 - - return v2.is_scalar_sequence_field(field) # type: ignore[arg-type] - - -def is_sequence_field(field: ModelField) -> bool: - if isinstance(field, may_v1.ModelField): - from fastapi._compat import v1 - - return v1.is_sequence_field(field) - else: - assert PYDANTIC_V2 - from . import v2 - - return v2.is_sequence_field(field) # type: ignore[arg-type] - - -def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]: - if isinstance(field, may_v1.ModelField): - from fastapi._compat import v1 - - return v1.serialize_sequence_value(field=field, value=value) - else: - assert PYDANTIC_V2 - from . import v2 - - return v2.serialize_sequence_value(field=field, value=value) # type: ignore[arg-type] - - -def _model_rebuild(model: Type[BaseModel]) -> None: - if lenient_issubclass(model, may_v1.BaseModel): - from fastapi._compat import v1 - - v1._model_rebuild(model) - elif PYDANTIC_V2: - from . import v2 - - v2._model_rebuild(model) - - -def get_compat_model_name_map(fields: List[ModelField]) -> ModelNameMap: - v1_model_fields = [ - field for field in fields if isinstance(field, may_v1.ModelField) - ] - if v1_model_fields: - from fastapi._compat import v1 - - v1_flat_models = v1.get_flat_models_from_fields( - v1_model_fields, known_models=set() - ) - all_flat_models = v1_flat_models - else: - all_flat_models = set() - if PYDANTIC_V2: - from . import v2 - - v2_model_fields = [ - field for field in fields if isinstance(field, v2.ModelField) - ] - v2_flat_models = v2.get_flat_models_from_fields( - v2_model_fields, known_models=set() - ) - all_flat_models = all_flat_models.union(v2_flat_models) - - model_name_map = v2.get_model_name_map(all_flat_models) - return model_name_map - from fastapi._compat import v1 - - model_name_map = v1.get_model_name_map(all_flat_models) - return model_name_map - - -def get_definitions( - *, - fields: List[ModelField], - model_name_map: ModelNameMap, - separate_input_output_schemas: bool = True, -) -> Tuple[ - Dict[ - Tuple[ModelField, Literal["validation", "serialization"]], - may_v1.JsonSchemaValue, - ], - Dict[str, Dict[str, Any]], -]: - if sys.version_info < (3, 14): - v1_fields = [field for field in fields if isinstance(field, may_v1.ModelField)] - v1_field_maps, v1_definitions = may_v1.get_definitions( - fields=v1_fields, - model_name_map=model_name_map, - separate_input_output_schemas=separate_input_output_schemas, - ) - if not PYDANTIC_V2: - return v1_field_maps, v1_definitions - else: - from . import v2 - - v2_fields = [field for field in fields if isinstance(field, v2.ModelField)] - v2_field_maps, v2_definitions = v2.get_definitions( - fields=v2_fields, - model_name_map=model_name_map, - separate_input_output_schemas=separate_input_output_schemas, - ) - all_definitions = {**v1_definitions, **v2_definitions} - all_field_maps = {**v1_field_maps, **v2_field_maps} - return all_field_maps, all_definitions - - # Pydantic v1 is not supported since Python 3.14 - else: - from . import v2 - - v2_fields = [field for field in fields if isinstance(field, v2.ModelField)] - v2_field_maps, v2_definitions = v2.get_definitions( - fields=v2_fields, - model_name_map=model_name_map, - separate_input_output_schemas=separate_input_output_schemas, - ) - return v2_field_maps, v2_definitions - - -def get_schema_from_model_field( - *, - field: ModelField, - model_name_map: ModelNameMap, - field_mapping: Dict[ - Tuple[ModelField, Literal["validation", "serialization"]], - may_v1.JsonSchemaValue, - ], - separate_input_output_schemas: bool = True, -) -> Dict[str, Any]: - if isinstance(field, may_v1.ModelField): - from fastapi._compat import v1 - - return v1.get_schema_from_model_field( - field=field, - model_name_map=model_name_map, - field_mapping=field_mapping, - separate_input_output_schemas=separate_input_output_schemas, - ) - else: - assert PYDANTIC_V2 - from . import v2 - - return v2.get_schema_from_model_field( - field=field, # type: ignore[arg-type] - model_name_map=model_name_map, - field_mapping=field_mapping, # type: ignore[arg-type] - separate_input_output_schemas=separate_input_output_schemas, - ) - - -def _is_model_field(value: Any) -> bool: - if isinstance(value, may_v1.ModelField): - return True - elif PYDANTIC_V2: - from . import v2 - - return isinstance(value, v2.ModelField) - return False - - -def _is_model_class(value: Any) -> bool: - if lenient_issubclass(value, may_v1.BaseModel): - return True - elif PYDANTIC_V2: - from . import v2 - - return lenient_issubclass(value, v2.BaseModel) # type: ignore[attr-defined] - return False diff --git a/fastapi/_compat/may_v1.py b/fastapi/_compat/may_v1.py deleted file mode 100644 index beea4d167f..0000000000 --- a/fastapi/_compat/may_v1.py +++ /dev/null @@ -1,123 +0,0 @@ -import sys -from typing import Any, Dict, List, Literal, Sequence, Tuple, Type, Union - -from fastapi.types import ModelNameMap - -if sys.version_info >= (3, 14): - - class AnyUrl: - pass - - class BaseConfig: - pass - - class BaseModel: - pass - - class Color: - pass - - class CoreSchema: - pass - - class ErrorWrapper: - pass - - class FieldInfo: - pass - - class GetJsonSchemaHandler: - pass - - class JsonSchemaValue: - pass - - class ModelField: - pass - - class NameEmail: - pass - - class RequiredParam: - pass - - class SecretBytes: - pass - - class SecretStr: - pass - - class Undefined: - pass - - class UndefinedType: - pass - - class Url: - pass - - from .v2 import ValidationError, create_model - - def get_definitions( - *, - fields: List[ModelField], - model_name_map: ModelNameMap, - separate_input_output_schemas: bool = True, - ) -> Tuple[ - Dict[ - Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue - ], - Dict[str, Dict[str, Any]], - ]: - return {}, {} # pragma: no cover - - -else: - from .v1 import AnyUrl as AnyUrl - from .v1 import BaseConfig as BaseConfig - from .v1 import BaseModel as BaseModel - from .v1 import Color as Color - from .v1 import CoreSchema as CoreSchema - from .v1 import ErrorWrapper as ErrorWrapper - from .v1 import FieldInfo as FieldInfo - from .v1 import GetJsonSchemaHandler as GetJsonSchemaHandler - from .v1 import JsonSchemaValue as JsonSchemaValue - from .v1 import ModelField as ModelField - from .v1 import NameEmail as NameEmail - from .v1 import RequiredParam as RequiredParam - from .v1 import SecretBytes as SecretBytes - from .v1 import SecretStr as SecretStr - from .v1 import Undefined as Undefined - from .v1 import UndefinedType as UndefinedType - from .v1 import Url as Url - from .v1 import ValidationError, create_model - from .v1 import get_definitions as get_definitions - - -RequestErrorModel: Type[BaseModel] = create_model("Request") - - -def _normalize_errors(errors: Sequence[Any]) -> List[Dict[str, Any]]: - use_errors: List[Any] = [] - for error in errors: - if isinstance(error, ErrorWrapper): - new_errors = ValidationError( # type: ignore[call-arg] - errors=[error], model=RequestErrorModel - ).errors() - use_errors.extend(new_errors) - elif isinstance(error, list): - use_errors.extend(_normalize_errors(error)) - else: - use_errors.append(error) - return use_errors - - -def _regenerate_error_with_loc( - *, errors: Sequence[Any], loc_prefix: Tuple[Union[str, int], ...] -) -> List[Dict[str, Any]]: - updated_loc_errors: List[Any] = [ - {**err, "loc": loc_prefix + err.get("loc", ())} - for err in _normalize_errors(errors) - ] - - return updated_loc_errors diff --git a/fastapi/_compat/model_field.py b/fastapi/_compat/model_field.py deleted file mode 100644 index fa2008c5e0..0000000000 --- a/fastapi/_compat/model_field.py +++ /dev/null @@ -1,53 +0,0 @@ -from typing import ( - Any, - Dict, - List, - Tuple, - Union, -) - -from fastapi.types import IncEx -from pydantic.fields import FieldInfo -from typing_extensions import Literal, Protocol - - -class ModelField(Protocol): - field_info: "FieldInfo" - name: str - mode: Literal["validation", "serialization"] = "validation" - _version: Literal["v1", "v2"] = "v1" - - @property - def alias(self) -> str: ... - - @property - def required(self) -> bool: ... - - @property - def default(self) -> Any: ... - - @property - def type_(self) -> Any: ... - - def get_default(self) -> Any: ... - - def validate( - self, - value: Any, - values: Dict[str, Any] = {}, # noqa: B006 - *, - loc: Tuple[Union[int, str], ...] = (), - ) -> Tuple[Any, Union[List[Dict[str, Any]], None]]: ... - - def serialize( - self, - value: Any, - *, - mode: Literal["json", "python"] = "json", - include: Union[IncEx, None] = None, - exclude: Union[IncEx, None] = None, - by_alias: bool = True, - exclude_unset: bool = False, - exclude_defaults: bool = False, - exclude_none: bool = False, - ) -> Any: ... diff --git a/fastapi/_compat/shared.py b/fastapi/_compat/shared.py index cabf482283..419b58f7f2 100644 --- a/fastapi/_compat/shared.py +++ b/fastapi/_compat/shared.py @@ -1,36 +1,24 @@ import sys import types import typing +import warnings from collections import deque +from collections.abc import Mapping, Sequence from dataclasses import is_dataclass from typing import ( + Annotated, Any, - Deque, - FrozenSet, - List, - Mapping, - Sequence, - Set, - Tuple, - Type, Union, ) -from fastapi._compat import may_v1 from fastapi.types import UnionType from pydantic import BaseModel from pydantic.version import VERSION as PYDANTIC_VERSION from starlette.datastructures import UploadFile -from typing_extensions import Annotated, get_args, get_origin +from typing_extensions import get_args, get_origin # Copy from Pydantic v2, compatible with v1 -if sys.version_info < (3, 9): - # Pydantic no longer supports Python 3.8, this might be incorrect, but the code - # this is used for is also never reached in this codebase, as it's a copy of - # Pydantic's lenient_issubclass, just for compatibility with v1 - # TODO: remove when dropping support for Python 3.8 - WithArgsTypes: Tuple[Any, ...] = () -elif sys.version_info < (3, 10): +if sys.version_info < (3, 10): WithArgsTypes: tuple[Any, ...] = (typing._GenericAlias, types.GenericAlias) # type: ignore[attr-defined] else: WithArgsTypes: tuple[Any, ...] = ( @@ -45,26 +33,21 @@ PYDANTIC_V2 = PYDANTIC_VERSION_MINOR_TUPLE[0] == 2 sequence_annotation_to_type = { Sequence: list, - List: list, list: list, - Tuple: tuple, tuple: tuple, - Set: set, set: set, - FrozenSet: frozenset, frozenset: frozenset, - Deque: deque, deque: deque, } sequence_types = tuple(sequence_annotation_to_type.keys()) -Url: Type[Any] +Url: type[Any] # Copy of Pydantic v2, compatible with v1 def lenient_issubclass( - cls: Any, class_or_tuple: Union[Type[Any], Tuple[Type[Any], ...], None] + cls: Any, class_or_tuple: Union[type[Any], tuple[type[Any], ...], None] ) -> bool: try: return isinstance(cls, type) and issubclass(cls, class_or_tuple) # type: ignore[arg-type] @@ -74,13 +57,13 @@ def lenient_issubclass( raise # pragma: no cover -def _annotation_is_sequence(annotation: Union[Type[Any], None]) -> bool: +def _annotation_is_sequence(annotation: Union[type[Any], None]) -> bool: if lenient_issubclass(annotation, (str, bytes)): return False - return lenient_issubclass(annotation, sequence_types) # type: ignore[arg-type] + return lenient_issubclass(annotation, sequence_types) -def field_annotation_is_sequence(annotation: Union[Type[Any], None]) -> bool: +def field_annotation_is_sequence(annotation: Union[type[Any], None]) -> bool: origin = get_origin(annotation) if origin is Union or origin is UnionType: for arg in get_args(annotation): @@ -93,20 +76,18 @@ def field_annotation_is_sequence(annotation: Union[Type[Any], None]) -> bool: def value_is_sequence(value: Any) -> bool: - return isinstance(value, sequence_types) and not isinstance(value, (str, bytes)) # type: ignore[arg-type] + return isinstance(value, sequence_types) and not isinstance(value, (str, bytes)) -def _annotation_is_complex(annotation: Union[Type[Any], None]) -> bool: +def _annotation_is_complex(annotation: Union[type[Any], None]) -> bool: return ( - lenient_issubclass( - annotation, (BaseModel, may_v1.BaseModel, Mapping, UploadFile) - ) + lenient_issubclass(annotation, (BaseModel, Mapping, UploadFile)) or _annotation_is_sequence(annotation) or is_dataclass(annotation) ) -def field_annotation_is_complex(annotation: Union[Type[Any], None]) -> bool: +def field_annotation_is_complex(annotation: Union[type[Any], None]) -> bool: origin = get_origin(annotation) if origin is Union or origin is UnionType: return any(field_annotation_is_complex(arg) for arg in get_args(annotation)) @@ -127,7 +108,7 @@ def field_annotation_is_scalar(annotation: Any) -> bool: return annotation is Ellipsis or not field_annotation_is_complex(annotation) -def field_annotation_is_scalar_sequence(annotation: Union[Type[Any], None]) -> bool: +def field_annotation_is_scalar_sequence(annotation: Union[type[Any], None]) -> bool: origin = get_origin(annotation) if origin is Union or origin is UnionType: at_least_one_scalar_sequence = False @@ -196,13 +177,27 @@ def is_uploadfile_sequence_annotation(annotation: Any) -> bool: ) +def is_pydantic_v1_model_instance(obj: Any) -> bool: + with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + from pydantic import v1 + return isinstance(obj, v1.BaseModel) + + +def is_pydantic_v1_model_class(cls: Any) -> bool: + with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + from pydantic import v1 + return lenient_issubclass(cls, v1.BaseModel) + + def annotation_is_pydantic_v1(annotation: Any) -> bool: - if lenient_issubclass(annotation, may_v1.BaseModel): + if is_pydantic_v1_model_class(annotation): return True origin = get_origin(annotation) if origin is Union or origin is UnionType: for arg in get_args(annotation): - if lenient_issubclass(arg, may_v1.BaseModel): + if is_pydantic_v1_model_class(arg): return True if field_annotation_is_sequence(annotation): for sub_annotation in get_args(annotation): diff --git a/fastapi/_compat/v1.py b/fastapi/_compat/v1.py deleted file mode 100644 index e17ce8beaf..0000000000 --- a/fastapi/_compat/v1.py +++ /dev/null @@ -1,312 +0,0 @@ -from copy import copy -from dataclasses import dataclass, is_dataclass -from enum import Enum -from typing import ( - Any, - Callable, - Dict, - List, - Sequence, - Set, - Tuple, - Type, - Union, -) - -from fastapi._compat import shared -from fastapi.openapi.constants import REF_PREFIX as REF_PREFIX -from fastapi.types import ModelNameMap -from pydantic.version import VERSION as PYDANTIC_VERSION -from typing_extensions import Literal - -PYDANTIC_VERSION_MINOR_TUPLE = tuple(int(x) for x in PYDANTIC_VERSION.split(".")[:2]) -PYDANTIC_V2 = PYDANTIC_VERSION_MINOR_TUPLE[0] == 2 -# Keeping old "Required" functionality from Pydantic V1, without -# shadowing typing.Required. -RequiredParam: Any = Ellipsis - -if not PYDANTIC_V2: - from pydantic import BaseConfig as BaseConfig - from pydantic import BaseModel as BaseModel - from pydantic import ValidationError as ValidationError - from pydantic import create_model as create_model - from pydantic.class_validators import Validator as Validator - from pydantic.color import Color as Color - from pydantic.error_wrappers import ErrorWrapper as ErrorWrapper - from pydantic.errors import MissingError - from pydantic.fields import ( # type: ignore[attr-defined] - SHAPE_FROZENSET, - SHAPE_LIST, - SHAPE_SEQUENCE, - SHAPE_SET, - SHAPE_SINGLETON, - SHAPE_TUPLE, - SHAPE_TUPLE_ELLIPSIS, - ) - from pydantic.fields import FieldInfo as FieldInfo - from pydantic.fields import ModelField as ModelField # type: ignore[attr-defined] - from pydantic.fields import Undefined as Undefined # type: ignore[attr-defined] - from pydantic.fields import ( # type: ignore[attr-defined] - UndefinedType as UndefinedType, - ) - from pydantic.networks import AnyUrl as AnyUrl - from pydantic.networks import NameEmail as NameEmail - from pydantic.schema import TypeModelSet as TypeModelSet - from pydantic.schema import ( - field_schema, - model_process_schema, - ) - from pydantic.schema import ( - get_annotation_from_field_info as get_annotation_from_field_info, - ) - from pydantic.schema import get_flat_models_from_field as get_flat_models_from_field - from pydantic.schema import ( - get_flat_models_from_fields as get_flat_models_from_fields, - ) - from pydantic.schema import get_model_name_map as get_model_name_map - from pydantic.types import SecretBytes as SecretBytes - from pydantic.types import SecretStr as SecretStr - from pydantic.typing import evaluate_forwardref as evaluate_forwardref - from pydantic.utils import lenient_issubclass as lenient_issubclass - - -else: - from pydantic.v1 import BaseConfig as BaseConfig # type: ignore[assignment] - from pydantic.v1 import BaseModel as BaseModel # type: ignore[assignment] - from pydantic.v1 import ( # type: ignore[assignment] - ValidationError as ValidationError, - ) - from pydantic.v1 import create_model as create_model # type: ignore[no-redef] - from pydantic.v1.class_validators import Validator as Validator - from pydantic.v1.color import Color as Color # type: ignore[assignment] - from pydantic.v1.error_wrappers import ErrorWrapper as ErrorWrapper - from pydantic.v1.errors import MissingError - from pydantic.v1.fields import ( - SHAPE_FROZENSET, - SHAPE_LIST, - SHAPE_SEQUENCE, - SHAPE_SET, - SHAPE_SINGLETON, - SHAPE_TUPLE, - SHAPE_TUPLE_ELLIPSIS, - ) - from pydantic.v1.fields import FieldInfo as FieldInfo # type: ignore[assignment] - from pydantic.v1.fields import ModelField as ModelField - from pydantic.v1.fields import Undefined as Undefined - from pydantic.v1.fields import UndefinedType as UndefinedType - from pydantic.v1.networks import AnyUrl as AnyUrl - from pydantic.v1.networks import ( # type: ignore[assignment] - NameEmail as NameEmail, - ) - from pydantic.v1.schema import TypeModelSet as TypeModelSet - from pydantic.v1.schema import ( - field_schema, - model_process_schema, - ) - from pydantic.v1.schema import ( - get_annotation_from_field_info as get_annotation_from_field_info, - ) - from pydantic.v1.schema import ( - get_flat_models_from_field as get_flat_models_from_field, - ) - from pydantic.v1.schema import ( - get_flat_models_from_fields as get_flat_models_from_fields, - ) - from pydantic.v1.schema import get_model_name_map as get_model_name_map - from pydantic.v1.types import ( # type: ignore[assignment] - SecretBytes as SecretBytes, - ) - from pydantic.v1.types import ( # type: ignore[assignment] - SecretStr as SecretStr, - ) - from pydantic.v1.typing import evaluate_forwardref as evaluate_forwardref - from pydantic.v1.utils import lenient_issubclass as lenient_issubclass - - -GetJsonSchemaHandler = Any -JsonSchemaValue = Dict[str, Any] -CoreSchema = Any -Url = AnyUrl - -sequence_shapes = { - SHAPE_LIST, - SHAPE_SET, - SHAPE_FROZENSET, - SHAPE_TUPLE, - SHAPE_SEQUENCE, - SHAPE_TUPLE_ELLIPSIS, -} -sequence_shape_to_type = { - SHAPE_LIST: list, - SHAPE_SET: set, - SHAPE_TUPLE: tuple, - SHAPE_SEQUENCE: list, - SHAPE_TUPLE_ELLIPSIS: list, -} - - -@dataclass -class GenerateJsonSchema: - ref_template: str - - -class PydanticSchemaGenerationError(Exception): - pass - - -RequestErrorModel: Type[BaseModel] = create_model("Request") - - -def with_info_plain_validator_function( - function: Callable[..., Any], - *, - ref: Union[str, None] = None, - metadata: Any = None, - serialization: Any = None, -) -> Any: - return {} - - -def get_model_definitions( - *, - flat_models: Set[Union[Type[BaseModel], Type[Enum]]], - model_name_map: Dict[Union[Type[BaseModel], Type[Enum]], str], -) -> Dict[str, Any]: - definitions: Dict[str, Dict[str, Any]] = {} - for model in flat_models: - m_schema, m_definitions, m_nested_models = model_process_schema( - model, model_name_map=model_name_map, ref_prefix=REF_PREFIX - ) - definitions.update(m_definitions) - model_name = model_name_map[model] - definitions[model_name] = m_schema - for m_schema in definitions.values(): - if "description" in m_schema: - m_schema["description"] = m_schema["description"].split("\f")[0] - return definitions - - -def is_pv1_scalar_field(field: ModelField) -> bool: - from fastapi import params - - field_info = field.field_info - if not ( - field.shape == SHAPE_SINGLETON - and not lenient_issubclass(field.type_, BaseModel) - and not lenient_issubclass(field.type_, dict) - and not shared.field_annotation_is_sequence(field.type_) - and not is_dataclass(field.type_) - and not isinstance(field_info, params.Body) - ): - return False - if field.sub_fields: - if not all(is_pv1_scalar_field(f) for f in field.sub_fields): - return False - return True - - -def is_pv1_scalar_sequence_field(field: ModelField) -> bool: - if (field.shape in sequence_shapes) and not lenient_issubclass( - field.type_, BaseModel - ): - if field.sub_fields is not None: - for sub_field in field.sub_fields: - if not is_pv1_scalar_field(sub_field): - return False - return True - if shared._annotation_is_sequence(field.type_): - return True - return False - - -def _model_rebuild(model: Type[BaseModel]) -> None: - model.update_forward_refs() - - -def _model_dump( - model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any -) -> Any: - return model.dict(**kwargs) - - -def _get_model_config(model: BaseModel) -> Any: - return model.__config__ # type: ignore[attr-defined] - - -def get_schema_from_model_field( - *, - field: ModelField, - model_name_map: ModelNameMap, - field_mapping: Dict[ - Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue - ], - separate_input_output_schemas: bool = True, -) -> Dict[str, Any]: - return field_schema( # type: ignore[no-any-return] - field, model_name_map=model_name_map, ref_prefix=REF_PREFIX - )[0] - - -# def get_compat_model_name_map(fields: List[ModelField]) -> ModelNameMap: -# models = get_flat_models_from_fields(fields, known_models=set()) -# return get_model_name_map(models) # type: ignore[no-any-return] - - -def get_definitions( - *, - fields: List[ModelField], - model_name_map: ModelNameMap, - separate_input_output_schemas: bool = True, -) -> Tuple[ - Dict[Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue], - Dict[str, Dict[str, Any]], -]: - models = get_flat_models_from_fields(fields, known_models=set()) - return {}, get_model_definitions(flat_models=models, model_name_map=model_name_map) - - -def is_scalar_field(field: ModelField) -> bool: - return is_pv1_scalar_field(field) - - -def is_sequence_field(field: ModelField) -> bool: - return field.shape in sequence_shapes or shared._annotation_is_sequence(field.type_) - - -def is_scalar_sequence_field(field: ModelField) -> bool: - return is_pv1_scalar_sequence_field(field) - - -def is_bytes_field(field: ModelField) -> bool: - return lenient_issubclass(field.type_, bytes) # type: ignore[no-any-return] - - -def is_bytes_sequence_field(field: ModelField) -> bool: - return field.shape in sequence_shapes and lenient_issubclass(field.type_, bytes) - - -def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo: - return copy(field_info) - - -def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]: - return sequence_shape_to_type[field.shape](value) # type: ignore[no-any-return] - - -def get_missing_field_error(loc: Tuple[str, ...]) -> Dict[str, Any]: - missing_field_error = ErrorWrapper(MissingError(), loc=loc) - new_error = ValidationError([missing_field_error], RequestErrorModel) - return new_error.errors()[0] # type: ignore[return-value] - - -def create_body_model( - *, fields: Sequence[ModelField], model_name: str -) -> Type[BaseModel]: - BodyModel = create_model(model_name) - for f in fields: - BodyModel.__fields__[f.name] = f # type: ignore[index] - return BodyModel - - -def get_model_fields(model: Type[BaseModel]) -> List[ModelField]: - return list(model.__fields__.values()) # type: ignore[attr-defined] diff --git a/fastapi/_compat/v2.py b/fastapi/_compat/v2.py index 6a87b9ae9d..25b6814536 100644 --- a/fastapi/_compat/v2.py +++ b/fastapi/_compat/v2.py @@ -1,24 +1,21 @@ import re import warnings +from collections.abc import Sequence from copy import copy, deepcopy -from dataclasses import dataclass +from dataclasses import dataclass, is_dataclass from enum import Enum +from functools import lru_cache from typing import ( + Annotated, Any, - Dict, - List, - Sequence, - Set, - Tuple, - Type, Union, cast, ) -from fastapi._compat import may_v1, shared +from fastapi._compat import shared from fastapi.openapi.constants import REF_TEMPLATE -from fastapi.types import IncEx, ModelNameMap -from pydantic import BaseModel, TypeAdapter, create_model +from fastapi.types import IncEx, ModelNameMap, UnionType +from pydantic import BaseModel, ConfigDict, Field, TypeAdapter, create_model from pydantic import PydanticSchemaGenerationError as PydanticSchemaGenerationError from pydantic import PydanticUndefinedAnnotation as PydanticUndefinedAnnotation from pydantic import ValidationError as ValidationError @@ -33,7 +30,7 @@ from pydantic.json_schema import JsonSchemaValue as JsonSchemaValue from pydantic_core import CoreSchema as CoreSchema from pydantic_core import PydanticUndefined, PydanticUndefinedType from pydantic_core import Url as Url -from typing_extensions import Annotated, Literal, get_args, get_origin +from typing_extensions import Literal, get_args, get_origin try: from pydantic_core.core_schema import ( @@ -50,6 +47,45 @@ UndefinedType = PydanticUndefinedType evaluate_forwardref = eval_type_lenient Validator = Any +# TODO: remove when dropping support for Pydantic < v2.12.3 +_Attrs = { + "default": ..., + "default_factory": None, + "alias": None, + "alias_priority": None, + "validation_alias": None, + "serialization_alias": None, + "title": None, + "field_title_generator": None, + "description": None, + "examples": None, + "exclude": None, + "exclude_if": None, + "discriminator": None, + "deprecated": None, + "json_schema_extra": None, + "frozen": None, + "validate_default": None, + "repr": True, + "init": None, + "init_var": None, + "kw_only": None, +} + + +# TODO: remove when dropping support for Pydantic < v2.12.3 +def asdict(field_info: FieldInfo) -> dict[str, Any]: + attributes = {} + for attr in _Attrs: + value = getattr(field_info, attr, Undefined) + if value is not Undefined: + attributes[attr] = value + return { + "annotation": field_info.annotation, + "metadata": field_info.metadata, + "attributes": attributes, + } + class BaseConfig: pass @@ -64,12 +100,25 @@ class ModelField: field_info: FieldInfo name: str mode: Literal["validation", "serialization"] = "validation" + config: Union[ConfigDict, None] = None @property def alias(self) -> str: a = self.field_info.alias return a if a is not None else self.name + @property + def validation_alias(self) -> Union[str, None]: + va = self.field_info.validation_alias + if isinstance(va, str) and va: + return va + return None + + @property + def serialization_alias(self) -> Union[str, None]: + sa = self.field_info.serialization_alias + return sa or None + @property def required(self) -> bool: return self.field_info.is_required() @@ -94,8 +143,19 @@ class ModelField: warnings.simplefilter( "ignore", category=UnsupportedFieldAttributeWarning ) + # TODO: remove after dropping support for Python 3.8 and + # setting the min Pydantic to v2.12.3 that adds asdict() + field_dict = asdict(self.field_info) + annotated_args = ( + field_dict["annotation"], + *field_dict["metadata"], + # this FieldInfo needs to be created again so that it doesn't include + # the old field info metadata and only the rest of the attributes + Field(**field_dict["attributes"]), + ) self._type_adapter: TypeAdapter[Any] = TypeAdapter( - Annotated[self.field_info.annotation, self.field_info] + Annotated[annotated_args], + config=self.config, ) def get_default(self) -> Any: @@ -106,17 +166,17 @@ class ModelField: def validate( self, value: Any, - values: Dict[str, Any] = {}, # noqa: B006 + values: dict[str, Any] = {}, # noqa: B006 *, - loc: Tuple[Union[int, str], ...] = (), - ) -> Tuple[Any, Union[List[Dict[str, Any]], None]]: + loc: tuple[Union[int, str], ...] = (), + ) -> tuple[Any, Union[list[dict[str, Any]], None]]: try: return ( self._type_adapter.validate_python(value, from_attributes=True), None, ) except ValidationError as exc: - return None, may_v1._regenerate_error_with_loc( + return None, _regenerate_error_with_loc( errors=exc.errors(include_url=False), loc_prefix=loc ) @@ -151,44 +211,39 @@ class ModelField: return id(self) -def get_annotation_from_field_info( - annotation: Any, field_info: FieldInfo, field_name: str -) -> Any: - return annotation - - -def _model_rebuild(model: Type[BaseModel]) -> None: - model.model_rebuild() - - -def _model_dump( - model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any -) -> Any: - return model.model_dump(mode=mode, **kwargs) - - -def _get_model_config(model: BaseModel) -> Any: - return model.model_config +def _has_computed_fields(field: ModelField) -> bool: + computed_fields = field._type_adapter.core_schema.get("schema", {}).get( + "computed_fields", [] + ) + return len(computed_fields) > 0 def get_schema_from_model_field( *, field: ModelField, model_name_map: ModelNameMap, - field_mapping: Dict[ - Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue + field_mapping: dict[ + tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue ], separate_input_output_schemas: bool = True, -) -> Dict[str, Any]: +) -> dict[str, Any]: override_mode: Union[Literal["validation"], None] = ( - None if separate_input_output_schemas else "validation" + None + if (separate_input_output_schemas or _has_computed_fields(field)) + else "validation" ) + field_alias = ( + (field.validation_alias or field.alias) + if field.mode == "validation" + else (field.serialization_alias or field.alias) + ) + # This expects that GenerateJsonSchema was already used to generate the definitions json_schema = field_mapping[(field, override_mode or field.mode)] if "$ref" not in json_schema: # TODO remove when deprecating Pydantic v1 # Ref: https://github.com/pydantic/pydantic/blob/d61792cc42c80b13b23e3ffa74bc37ec7c77f7d1/pydantic/schema.py#L207 - json_schema["title"] = field.field_info.title or field.alias.title().replace( + json_schema["title"] = field.field_info.title or field_alias.title().replace( "_", " " ) return json_schema @@ -199,14 +254,11 @@ def get_definitions( fields: Sequence[ModelField], model_name_map: ModelNameMap, separate_input_output_schemas: bool = True, -) -> Tuple[ - Dict[Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue], - Dict[str, Dict[str, Any]], +) -> tuple[ + dict[tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue], + dict[str, dict[str, Any]], ]: schema_generator = GenerateJsonSchema(ref_template=REF_TEMPLATE) - override_mode: Union[Literal["validation"], None] = ( - None if separate_input_output_schemas else "validation" - ) validation_fields = [field for field in fields if field.mode == "validation"] serialization_fields = [field for field in fields if field.mode == "serialization"] flat_validation_models = get_flat_models_from_fields( @@ -236,13 +288,20 @@ def get_definitions( unique_flat_model_fields = { f for f in flat_model_fields if f.type_ not in input_types } - inputs = [ - (field, override_mode or field.mode, field._type_adapter.core_schema) + ( + field, + ( + field.mode + if (separate_input_output_schemas or _has_computed_fields(field)) + else "validation" + ), + field._type_adapter.core_schema, + ) for field in list(fields) + list(unique_flat_model_fields) ] field_mapping, definitions = schema_generator.generate_definitions(inputs=inputs) - for item_def in cast(Dict[str, Dict[str, Any]], definitions).values(): + for item_def in cast(dict[str, dict[str, Any]], definitions).values(): if "description" in item_def: item_description = cast(str, item_def["description"]).split("\f")[0] item_def["description"] = item_description @@ -256,18 +315,18 @@ def get_definitions( def _replace_refs( *, - schema: Dict[str, Any], - old_name_to_new_name_map: Dict[str, str], -) -> Dict[str, Any]: + schema: dict[str, Any], + old_name_to_new_name_map: dict[str, str], +) -> dict[str, Any]: new_schema = deepcopy(schema) for key, value in new_schema.items(): if key == "$ref": - ref_name = schema["$ref"].split("/")[-1] - if ref_name in old_name_to_new_name_map: - new_name = old_name_to_new_name_map[ref_name] - new_schema["$ref"] = REF_TEMPLATE.format(model=new_name) - else: - new_schema["$ref"] = schema["$ref"] + value = schema["$ref"] + if isinstance(value, str): + ref_name = schema["$ref"].split("/")[-1] + if ref_name in old_name_to_new_name_map: + new_name = old_name_to_new_name_map[ref_name] + new_schema["$ref"] = REF_TEMPLATE.format(model=new_name) continue if isinstance(value, dict): new_schema[key] = _replace_refs( @@ -293,18 +352,18 @@ def _replace_refs( def _remap_definitions_and_field_mappings( *, model_name_map: ModelNameMap, - definitions: Dict[str, Any], - field_mapping: Dict[ - Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue + definitions: dict[str, Any], + field_mapping: dict[ + tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue ], -) -> Tuple[ - Dict[Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue], - Dict[str, Any], +) -> tuple[ + dict[tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue], + dict[str, Any], ]: old_name_to_new_name_map = {} for field_key, schema in field_mapping.items(): model = field_key[0].type_ - if model not in model_name_map: + if model not in model_name_map or "$ref" not in schema: continue new_name = model_name_map[model] old_name = schema["$ref"].split("/")[-1] @@ -312,8 +371,8 @@ def _remap_definitions_and_field_mappings( continue old_name_to_new_name_map[old_name] = new_name - new_field_mapping: Dict[ - Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue + new_field_mapping: dict[ + tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue ] = {} for field_key, schema in field_mapping.items(): new_schema = _replace_refs( @@ -371,11 +430,18 @@ def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo: def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]: origin_type = get_origin(field.field_info.annotation) or field.field_info.annotation + if origin_type is Union or origin_type is UnionType: # Handle optional sequences + union_args = get_args(field.field_info.annotation) + for union_arg in union_args: + if union_arg is type(None): + continue + origin_type = get_origin(union_arg) or union_arg + break assert issubclass(origin_type, shared.sequence_types) # type: ignore[arg-type] - return shared.sequence_annotation_to_type[origin_type](value) # type: ignore[no-any-return] + return shared.sequence_annotation_to_type[origin_type](value) # type: ignore[no-any-return,index] -def get_missing_field_error(loc: Tuple[str, ...]) -> Dict[str, Any]: +def get_missing_field_error(loc: tuple[str, ...]) -> dict[str, Any]: error = ValidationError.from_exception_data( "Field required", [{"type": "missing", "loc": loc, "input": {}}] ).errors(include_url=False)[0] @@ -385,50 +451,67 @@ def get_missing_field_error(loc: Tuple[str, ...]) -> Dict[str, Any]: def create_body_model( *, fields: Sequence[ModelField], model_name: str -) -> Type[BaseModel]: +) -> type[BaseModel]: field_params = {f.name: (f.field_info.annotation, f.field_info) for f in fields} - BodyModel: Type[BaseModel] = create_model(model_name, **field_params) # type: ignore[call-overload] + BodyModel: type[BaseModel] = create_model(model_name, **field_params) # type: ignore[call-overload] return BodyModel -def get_model_fields(model: Type[BaseModel]) -> List[ModelField]: - return [ - ModelField(field_info=field_info, name=name) - for name, field_info in model.model_fields.items() - ] +def get_model_fields(model: type[BaseModel]) -> list[ModelField]: + model_fields: list[ModelField] = [] + for name, field_info in model.model_fields.items(): + type_ = field_info.annotation + if lenient_issubclass(type_, (BaseModel, dict)) or is_dataclass(type_): + model_config = None + else: + model_config = model.model_config + model_fields.append( + ModelField( + field_info=field_info, + name=name, + config=model_config, + ) + ) + return model_fields + + +@lru_cache +def get_cached_model_fields(model: type[BaseModel]) -> list[ModelField]: + return get_model_fields(model) # type: ignore[return-value] # Duplicate of several schema functions from Pydantic v1 to make them compatible with # Pydantic v2 and allow mixing the models -TypeModelOrEnum = Union[Type["BaseModel"], Type[Enum]] -TypeModelSet = Set[TypeModelOrEnum] +TypeModelOrEnum = Union[type["BaseModel"], type[Enum]] +TypeModelSet = set[TypeModelOrEnum] def normalize_name(name: str) -> str: return re.sub(r"[^a-zA-Z0-9.\-_]", "_", name) -def get_model_name_map(unique_models: TypeModelSet) -> Dict[TypeModelOrEnum, str]: +def get_model_name_map(unique_models: TypeModelSet) -> dict[TypeModelOrEnum, str]: name_model_map = {} - conflicting_names: Set[str] = set() for model in unique_models: model_name = normalize_name(model.__name__) - if model_name in conflicting_names: - model_name = get_long_model_name(model) - name_model_map[model_name] = model - elif model_name in name_model_map: - conflicting_names.add(model_name) - conflicting_model = name_model_map.pop(model_name) - name_model_map[get_long_model_name(conflicting_model)] = conflicting_model - name_model_map[get_long_model_name(model)] = model - else: - name_model_map[model_name] = model + name_model_map[model_name] = model return {v: k for k, v in name_model_map.items()} +def get_compat_model_name_map(fields: list[ModelField]) -> ModelNameMap: + all_flat_models = set() + + v2_model_fields = [field for field in fields if isinstance(field, ModelField)] + v2_flat_models = get_flat_models_from_fields(v2_model_fields, known_models=set()) + all_flat_models = all_flat_models.union(v2_flat_models) # type: ignore[arg-type] + + model_name_map = get_model_name_map(all_flat_models) # type: ignore[arg-type] + return model_name_map + + def get_flat_models_from_model( - model: Type["BaseModel"], known_models: Union[TypeModelSet, None] = None + model: type["BaseModel"], known_models: Union[TypeModelSet, None] = None ) -> TypeModelSet: known_models = known_models or set() fields = get_model_fields(model) @@ -475,5 +558,11 @@ def get_flat_models_from_fields( return known_models -def get_long_model_name(model: TypeModelOrEnum) -> str: - return f"{model.__module__}__{model.__qualname__}".replace(".", "__") +def _regenerate_error_with_loc( + *, errors: Sequence[Any], loc_prefix: tuple[Union[str, int], ...] +) -> list[dict[str, Any]]: + updated_loc_errors: list[Any] = [ + {**err, "loc": loc_prefix + err.get("loc", ())} for err in errors + ] + + return updated_loc_errors diff --git a/fastapi/applications.py b/fastapi/applications.py index 0a47699aef..54175cb3b0 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -1,14 +1,10 @@ +from collections.abc import Awaitable, Coroutine, Sequence from enum import Enum from typing import ( + Annotated, Any, - Awaitable, Callable, - Coroutine, - Dict, - List, Optional, - Sequence, - Type, TypeVar, Union, ) @@ -44,7 +40,7 @@ from starlette.requests import Request from starlette.responses import HTMLResponse, JSONResponse, Response from starlette.routing import BaseRoute from starlette.types import ASGIApp, ExceptionHandler, Lifespan, Receive, Scope, Send -from typing_extensions import Annotated, deprecated +from typing_extensions import deprecated AppType = TypeVar("AppType", bound="FastAPI") @@ -81,7 +77,7 @@ class FastAPI(Starlette): ), ] = False, routes: Annotated[ - Optional[List[BaseRoute]], + Optional[list[BaseRoute]], Doc( """ **Note**: you probably shouldn't use this parameter, it is inherited @@ -230,7 +226,7 @@ class FastAPI(Starlette): ), ] = "/openapi.json", openapi_tags: Annotated[ - Optional[List[Dict[str, Any]]], + Optional[list[dict[str, Any]]], Doc( """ A list of tags used by OpenAPI, these are the same `tags` you can set @@ -290,7 +286,7 @@ class FastAPI(Starlette): ), ] = None, servers: Annotated[ - Optional[List[Dict[str, Union[str, Any]]]], + Optional[list[dict[str, Union[str, Any]]]], Doc( """ A `list` of `dict`s with connectivity information to a target server. @@ -301,7 +297,12 @@ class FastAPI(Starlette): browser tabs open). Or if you want to leave fixed the possible URLs. If the servers `list` is not provided, or is an empty `list`, the - default value would be a `dict` with a `url` value of `/`. + `servers` property in the generated OpenAPI will be: + + * a `dict` with a `url` value of the application's mounting point + (`root_path`) if it's different from `/`. + * otherwise, the `servers` property will be omitted from the OpenAPI + schema. Each item in the `list` is a `dict` containing: @@ -356,7 +357,7 @@ class FastAPI(Starlette): ), ] = None, default_response_class: Annotated[ - Type[Response], + type[Response], Doc( """ The default response class to be used. @@ -462,7 +463,7 @@ class FastAPI(Starlette): ), ] = "/docs/oauth2-redirect", swagger_ui_init_oauth: Annotated[ - Optional[Dict[str, Any]], + Optional[dict[str, Any]], Doc( """ OAuth2 configuration for the Swagger UI, by default shown at `/docs`. @@ -488,8 +489,8 @@ class FastAPI(Starlette): ] = None, exception_handlers: Annotated[ Optional[ - Dict[ - Union[int, Type[Exception]], + dict[ + Union[int, type[Exception]], Callable[[Request, Any], Coroutine[Any, Any, Response]], ] ], @@ -562,7 +563,7 @@ class FastAPI(Starlette): ), ] = None, contact: Annotated[ - Optional[Dict[str, Union[str, Any]]], + Optional[dict[str, Union[str, Any]]], Doc( """ A dictionary with the contact information for the exposed API. @@ -595,7 +596,7 @@ class FastAPI(Starlette): ), ] = None, license_info: Annotated[ - Optional[Dict[str, Union[str, Any]]], + Optional[dict[str, Union[str, Any]]], Doc( """ A dictionary with the license information for the exposed API. @@ -684,7 +685,7 @@ class FastAPI(Starlette): ), ] = True, responses: Annotated[ - Optional[Dict[Union[int, str], Dict[str, Any]]], + Optional[dict[Union[int, str], dict[str, Any]]], Doc( """ Additional responses to be shown in OpenAPI. @@ -700,7 +701,7 @@ class FastAPI(Starlette): ), ] = None, callbacks: Annotated[ - Optional[List[BaseRoute]], + Optional[list[BaseRoute]], Doc( """ OpenAPI callbacks that should apply to all *path operations*. @@ -757,7 +758,7 @@ class FastAPI(Starlette): ), ] = True, swagger_ui_parameters: Annotated[ - Optional[Dict[str, Any]], + Optional[dict[str, Any]], Doc( """ Parameters to configure Swagger UI, the autogenerated interactive API @@ -815,7 +816,7 @@ class FastAPI(Starlette): ), ] = True, openapi_external_docs: Annotated[ - Optional[Dict[str, Any]], + Optional[dict[str, Any]], Doc( """ This field allows you to provide additional external documentation links. @@ -901,7 +902,7 @@ class FastAPI(Starlette): """ ), ] = "3.1.0" - self.openapi_schema: Optional[Dict[str, Any]] = None + self.openapi_schema: Optional[dict[str, Any]] = None if self.openapi_url: assert self.title, "A title must be provided for OpenAPI, e.g.: 'My API'" assert self.version, "A version must be provided for OpenAPI, e.g.: '2.1.0'" @@ -944,7 +945,7 @@ class FastAPI(Starlette): ), ] = State() self.dependency_overrides: Annotated[ - Dict[Callable[..., Any], Callable[..., Any]], + dict[Callable[..., Any], Callable[..., Any]], Doc( """ A dictionary with overrides for the dependencies. @@ -975,7 +976,7 @@ class FastAPI(Starlette): responses=responses, generate_unique_id_function=generate_unique_id_function, ) - self.exception_handlers: Dict[ + self.exception_handlers: dict[ Any, Callable[[Request, Any], Union[Response, Awaitable[Response]]] ] = {} if exception_handlers is None else dict(exception_handlers) self.exception_handlers.setdefault(HTTPException, http_exception_handler) @@ -988,7 +989,7 @@ class FastAPI(Starlette): websocket_request_validation_exception_handler, # type: ignore ) - self.user_middleware: List[Middleware] = ( + self.user_middleware: list[Middleware] = ( [] if middleware is None else list(middleware) ) self.middleware_stack: Union[ASGIApp, None] = None @@ -1042,7 +1043,7 @@ class FastAPI(Starlette): app = cls(app, *args, **kwargs) return app - def openapi(self) -> Dict[str, Any]: + def openapi(self) -> dict[str, Any]: """ Generate the OpenAPI schema of the application. This is called by FastAPI internally. @@ -1140,14 +1141,14 @@ class FastAPI(Starlette): *, response_model: Any = Default(None), status_code: Optional[int] = None, - tags: Optional[List[Union[str, Enum]]] = None, + tags: Optional[list[Union[str, Enum]]] = None, dependencies: Optional[Sequence[Depends]] = None, summary: Optional[str] = None, description: Optional[str] = None, response_description: str = "Successful Response", - responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, + responses: Optional[dict[Union[int, str], dict[str, Any]]] = None, deprecated: Optional[bool] = None, - methods: Optional[List[str]] = None, + methods: Optional[list[str]] = None, operation_id: Optional[str] = None, response_model_include: Optional[IncEx] = None, response_model_exclude: Optional[IncEx] = None, @@ -1156,11 +1157,11 @@ class FastAPI(Starlette): response_model_exclude_defaults: bool = False, response_model_exclude_none: bool = False, include_in_schema: bool = True, - response_class: Union[Type[Response], DefaultPlaceholder] = Default( + response_class: Union[type[Response], DefaultPlaceholder] = Default( JSONResponse ), name: Optional[str] = None, - openapi_extra: Optional[Dict[str, Any]] = None, + openapi_extra: Optional[dict[str, Any]] = None, generate_unique_id_function: Callable[[routing.APIRoute], str] = Default( generate_unique_id ), @@ -1198,14 +1199,14 @@ class FastAPI(Starlette): *, response_model: Any = Default(None), status_code: Optional[int] = None, - tags: Optional[List[Union[str, Enum]]] = None, + tags: Optional[list[Union[str, Enum]]] = None, dependencies: Optional[Sequence[Depends]] = None, summary: Optional[str] = None, description: Optional[str] = None, response_description: str = "Successful Response", - responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, + responses: Optional[dict[Union[int, str], dict[str, Any]]] = None, deprecated: Optional[bool] = None, - methods: Optional[List[str]] = None, + methods: Optional[list[str]] = None, operation_id: Optional[str] = None, response_model_include: Optional[IncEx] = None, response_model_exclude: Optional[IncEx] = None, @@ -1214,9 +1215,9 @@ class FastAPI(Starlette): response_model_exclude_defaults: bool = False, response_model_exclude_none: bool = False, include_in_schema: bool = True, - response_class: Type[Response] = Default(JSONResponse), + response_class: type[Response] = Default(JSONResponse), name: Optional[str] = None, - openapi_extra: Optional[Dict[str, Any]] = None, + openapi_extra: Optional[dict[str, Any]] = None, generate_unique_id_function: Callable[[routing.APIRoute], str] = Default( generate_unique_id ), @@ -1338,7 +1339,7 @@ class FastAPI(Starlette): *, prefix: Annotated[str, Doc("An optional path prefix for the router.")] = "", tags: Annotated[ - Optional[List[Union[str, Enum]]], + Optional[list[Union[str, Enum]]], Doc( """ A list of tags to be applied to all the *path operations* in this @@ -1380,7 +1381,7 @@ class FastAPI(Starlette): ), ] = None, responses: Annotated[ - Optional[Dict[Union[int, str], Dict[str, Any]]], + Optional[dict[Union[int, str], dict[str, Any]]], Doc( """ Additional responses to be shown in OpenAPI. @@ -1447,7 +1448,7 @@ class FastAPI(Starlette): ), ] = True, default_response_class: Annotated[ - Type[Response], + type[Response], Doc( """ Default response class to be used for the *path operations* in this @@ -1475,7 +1476,7 @@ class FastAPI(Starlette): ), ] = Default(JSONResponse), callbacks: Annotated[ - Optional[List[BaseRoute]], + Optional[list[BaseRoute]], Doc( """ List of *path operations* that will be used as OpenAPI callbacks. @@ -1598,7 +1599,7 @@ class FastAPI(Starlette): ), ] = None, tags: Annotated[ - Optional[List[Union[str, Enum]]], + Optional[list[Union[str, Enum]]], Doc( """ A list of tags to be applied to the *path operation*. @@ -1664,7 +1665,7 @@ class FastAPI(Starlette): ), ] = "Successful Response", responses: Annotated[ - Optional[Dict[Union[int, str], Dict[str, Any]]], + Optional[dict[Union[int, str], dict[str, Any]]], Doc( """ Additional responses that could be returned by this *path operation*. @@ -1805,7 +1806,7 @@ class FastAPI(Starlette): ), ] = True, response_class: Annotated[ - Type[Response], + type[Response], Doc( """ Response class to be used for this *path operation*. @@ -1826,7 +1827,7 @@ class FastAPI(Starlette): ), ] = None, callbacks: Annotated[ - Optional[List[BaseRoute]], + Optional[list[BaseRoute]], Doc( """ List of *path operations* that will be used as OpenAPI callbacks. @@ -1842,7 +1843,7 @@ class FastAPI(Starlette): ), ] = None, openapi_extra: Annotated[ - Optional[Dict[str, Any]], + Optional[dict[str, Any]], Doc( """ Extra metadata to be included in the OpenAPI schema for this *path @@ -1971,7 +1972,7 @@ class FastAPI(Starlette): ), ] = None, tags: Annotated[ - Optional[List[Union[str, Enum]]], + Optional[list[Union[str, Enum]]], Doc( """ A list of tags to be applied to the *path operation*. @@ -2037,7 +2038,7 @@ class FastAPI(Starlette): ), ] = "Successful Response", responses: Annotated[ - Optional[Dict[Union[int, str], Dict[str, Any]]], + Optional[dict[Union[int, str], dict[str, Any]]], Doc( """ Additional responses that could be returned by this *path operation*. @@ -2178,7 +2179,7 @@ class FastAPI(Starlette): ), ] = True, response_class: Annotated[ - Type[Response], + type[Response], Doc( """ Response class to be used for this *path operation*. @@ -2199,7 +2200,7 @@ class FastAPI(Starlette): ), ] = None, callbacks: Annotated[ - Optional[List[BaseRoute]], + Optional[list[BaseRoute]], Doc( """ List of *path operations* that will be used as OpenAPI callbacks. @@ -2215,7 +2216,7 @@ class FastAPI(Starlette): ), ] = None, openapi_extra: Annotated[ - Optional[Dict[str, Any]], + Optional[dict[str, Any]], Doc( """ Extra metadata to be included in the OpenAPI schema for this *path @@ -2349,7 +2350,7 @@ class FastAPI(Starlette): ), ] = None, tags: Annotated[ - Optional[List[Union[str, Enum]]], + Optional[list[Union[str, Enum]]], Doc( """ A list of tags to be applied to the *path operation*. @@ -2415,7 +2416,7 @@ class FastAPI(Starlette): ), ] = "Successful Response", responses: Annotated[ - Optional[Dict[Union[int, str], Dict[str, Any]]], + Optional[dict[Union[int, str], dict[str, Any]]], Doc( """ Additional responses that could be returned by this *path operation*. @@ -2556,7 +2557,7 @@ class FastAPI(Starlette): ), ] = True, response_class: Annotated[ - Type[Response], + type[Response], Doc( """ Response class to be used for this *path operation*. @@ -2577,7 +2578,7 @@ class FastAPI(Starlette): ), ] = None, callbacks: Annotated[ - Optional[List[BaseRoute]], + Optional[list[BaseRoute]], Doc( """ List of *path operations* that will be used as OpenAPI callbacks. @@ -2593,7 +2594,7 @@ class FastAPI(Starlette): ), ] = None, openapi_extra: Annotated[ - Optional[Dict[str, Any]], + Optional[dict[str, Any]], Doc( """ Extra metadata to be included in the OpenAPI schema for this *path @@ -2727,7 +2728,7 @@ class FastAPI(Starlette): ), ] = None, tags: Annotated[ - Optional[List[Union[str, Enum]]], + Optional[list[Union[str, Enum]]], Doc( """ A list of tags to be applied to the *path operation*. @@ -2793,7 +2794,7 @@ class FastAPI(Starlette): ), ] = "Successful Response", responses: Annotated[ - Optional[Dict[Union[int, str], Dict[str, Any]]], + Optional[dict[Union[int, str], dict[str, Any]]], Doc( """ Additional responses that could be returned by this *path operation*. @@ -2934,7 +2935,7 @@ class FastAPI(Starlette): ), ] = True, response_class: Annotated[ - Type[Response], + type[Response], Doc( """ Response class to be used for this *path operation*. @@ -2955,7 +2956,7 @@ class FastAPI(Starlette): ), ] = None, callbacks: Annotated[ - Optional[List[BaseRoute]], + Optional[list[BaseRoute]], Doc( """ List of *path operations* that will be used as OpenAPI callbacks. @@ -2971,7 +2972,7 @@ class FastAPI(Starlette): ), ] = None, openapi_extra: Annotated[ - Optional[Dict[str, Any]], + Optional[dict[str, Any]], Doc( """ Extra metadata to be included in the OpenAPI schema for this *path @@ -3100,7 +3101,7 @@ class FastAPI(Starlette): ), ] = None, tags: Annotated[ - Optional[List[Union[str, Enum]]], + Optional[list[Union[str, Enum]]], Doc( """ A list of tags to be applied to the *path operation*. @@ -3166,7 +3167,7 @@ class FastAPI(Starlette): ), ] = "Successful Response", responses: Annotated[ - Optional[Dict[Union[int, str], Dict[str, Any]]], + Optional[dict[Union[int, str], dict[str, Any]]], Doc( """ Additional responses that could be returned by this *path operation*. @@ -3307,7 +3308,7 @@ class FastAPI(Starlette): ), ] = True, response_class: Annotated[ - Type[Response], + type[Response], Doc( """ Response class to be used for this *path operation*. @@ -3328,7 +3329,7 @@ class FastAPI(Starlette): ), ] = None, callbacks: Annotated[ - Optional[List[BaseRoute]], + Optional[list[BaseRoute]], Doc( """ List of *path operations* that will be used as OpenAPI callbacks. @@ -3344,7 +3345,7 @@ class FastAPI(Starlette): ), ] = None, openapi_extra: Annotated[ - Optional[Dict[str, Any]], + Optional[dict[str, Any]], Doc( """ Extra metadata to be included in the OpenAPI schema for this *path @@ -3473,7 +3474,7 @@ class FastAPI(Starlette): ), ] = None, tags: Annotated[ - Optional[List[Union[str, Enum]]], + Optional[list[Union[str, Enum]]], Doc( """ A list of tags to be applied to the *path operation*. @@ -3539,7 +3540,7 @@ class FastAPI(Starlette): ), ] = "Successful Response", responses: Annotated[ - Optional[Dict[Union[int, str], Dict[str, Any]]], + Optional[dict[Union[int, str], dict[str, Any]]], Doc( """ Additional responses that could be returned by this *path operation*. @@ -3680,7 +3681,7 @@ class FastAPI(Starlette): ), ] = True, response_class: Annotated[ - Type[Response], + type[Response], Doc( """ Response class to be used for this *path operation*. @@ -3701,7 +3702,7 @@ class FastAPI(Starlette): ), ] = None, callbacks: Annotated[ - Optional[List[BaseRoute]], + Optional[list[BaseRoute]], Doc( """ List of *path operations* that will be used as OpenAPI callbacks. @@ -3717,7 +3718,7 @@ class FastAPI(Starlette): ), ] = None, openapi_extra: Annotated[ - Optional[Dict[str, Any]], + Optional[dict[str, Any]], Doc( """ Extra metadata to be included in the OpenAPI schema for this *path @@ -3846,7 +3847,7 @@ class FastAPI(Starlette): ), ] = None, tags: Annotated[ - Optional[List[Union[str, Enum]]], + Optional[list[Union[str, Enum]]], Doc( """ A list of tags to be applied to the *path operation*. @@ -3912,7 +3913,7 @@ class FastAPI(Starlette): ), ] = "Successful Response", responses: Annotated[ - Optional[Dict[Union[int, str], Dict[str, Any]]], + Optional[dict[Union[int, str], dict[str, Any]]], Doc( """ Additional responses that could be returned by this *path operation*. @@ -4053,7 +4054,7 @@ class FastAPI(Starlette): ), ] = True, response_class: Annotated[ - Type[Response], + type[Response], Doc( """ Response class to be used for this *path operation*. @@ -4074,7 +4075,7 @@ class FastAPI(Starlette): ), ] = None, callbacks: Annotated[ - Optional[List[BaseRoute]], + Optional[list[BaseRoute]], Doc( """ List of *path operations* that will be used as OpenAPI callbacks. @@ -4090,7 +4091,7 @@ class FastAPI(Starlette): ), ] = None, openapi_extra: Annotated[ - Optional[Dict[str, Any]], + Optional[dict[str, Any]], Doc( """ Extra metadata to be included in the OpenAPI schema for this *path @@ -4224,7 +4225,7 @@ class FastAPI(Starlette): ), ] = None, tags: Annotated[ - Optional[List[Union[str, Enum]]], + Optional[list[Union[str, Enum]]], Doc( """ A list of tags to be applied to the *path operation*. @@ -4290,7 +4291,7 @@ class FastAPI(Starlette): ), ] = "Successful Response", responses: Annotated[ - Optional[Dict[Union[int, str], Dict[str, Any]]], + Optional[dict[Union[int, str], dict[str, Any]]], Doc( """ Additional responses that could be returned by this *path operation*. @@ -4431,7 +4432,7 @@ class FastAPI(Starlette): ), ] = True, response_class: Annotated[ - Type[Response], + type[Response], Doc( """ Response class to be used for this *path operation*. @@ -4452,7 +4453,7 @@ class FastAPI(Starlette): ), ] = None, callbacks: Annotated[ - Optional[List[BaseRoute]], + Optional[list[BaseRoute]], Doc( """ List of *path operations* that will be used as OpenAPI callbacks. @@ -4468,7 +4469,7 @@ class FastAPI(Starlette): ), ] = None, openapi_extra: Annotated[ - Optional[Dict[str, Any]], + Optional[dict[str, Any]], Doc( """ Extra metadata to be included in the OpenAPI schema for this *path @@ -4623,7 +4624,7 @@ class FastAPI(Starlette): def exception_handler( self, exc_class_or_status_code: Annotated[ - Union[int, Type[Exception]], + Union[int, type[Exception]], Doc( """ The Exception class this would handle, or a status code. diff --git a/fastapi/background.py b/fastapi/background.py index 6d4a30d442..20803ba670 100644 --- a/fastapi/background.py +++ b/fastapi/background.py @@ -1,8 +1,8 @@ -from typing import Any, Callable +from typing import Annotated, Any, Callable from annotated_doc import Doc from starlette.background import BackgroundTasks as StarletteBackgroundTasks -from typing_extensions import Annotated, ParamSpec +from typing_extensions import ParamSpec P = ParamSpec("P") diff --git a/fastapi/concurrency.py b/fastapi/concurrency.py index 3202c70789..76a5a2eb12 100644 --- a/fastapi/concurrency.py +++ b/fastapi/concurrency.py @@ -1,5 +1,7 @@ +from collections.abc import AsyncGenerator +from contextlib import AbstractContextManager from contextlib import asynccontextmanager as asynccontextmanager -from typing import AsyncGenerator, ContextManager, TypeVar +from typing import TypeVar import anyio.to_thread from anyio import CapacityLimiter @@ -14,7 +16,7 @@ _T = TypeVar("_T") @asynccontextmanager async def contextmanager_in_threadpool( - cm: ContextManager[_T], + cm: AbstractContextManager[_T], ) -> AsyncGenerator[_T, None]: # blocking __exit__ from running waiting on a free thread # can create race conditions/deadlocks if the context manager itself diff --git a/fastapi/datastructures.py b/fastapi/datastructures.py index 8ad9aa11a6..2bf5fdb262 100644 --- a/fastapi/datastructures.py +++ b/fastapi/datastructures.py @@ -1,21 +1,16 @@ +from collections.abc import Mapping from typing import ( + Annotated, Any, BinaryIO, Callable, - Dict, - Iterable, Optional, - Type, TypeVar, cast, ) from annotated_doc import Doc -from fastapi._compat import ( - CoreSchema, - GetJsonSchemaHandler, - JsonSchemaValue, -) +from pydantic import GetJsonSchemaHandler from starlette.datastructures import URL as URL # noqa: F401 from starlette.datastructures import Address as Address # noqa: F401 from starlette.datastructures import FormData as FormData # noqa: F401 @@ -23,7 +18,6 @@ from starlette.datastructures import Headers as Headers # noqa: F401 from starlette.datastructures import QueryParams as QueryParams # noqa: F401 from starlette.datastructures import State as State # noqa: F401 from starlette.datastructures import UploadFile as StarletteUploadFile -from typing_extensions import Annotated class UploadFile(StarletteUploadFile): @@ -137,37 +131,22 @@ class UploadFile(StarletteUploadFile): """ return await super().close() - @classmethod - def __get_validators__(cls: Type["UploadFile"]) -> Iterable[Callable[..., Any]]: - yield cls.validate - - @classmethod - def validate(cls: Type["UploadFile"], v: Any) -> Any: - if not isinstance(v, StarletteUploadFile): - raise ValueError(f"Expected UploadFile, received: {type(v)}") - return v - @classmethod def _validate(cls, __input_value: Any, _: Any) -> "UploadFile": if not isinstance(__input_value, StarletteUploadFile): raise ValueError(f"Expected UploadFile, received: {type(__input_value)}") return cast(UploadFile, __input_value) - # TODO: remove when deprecating Pydantic v1 - @classmethod - def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: - field_schema.update({"type": "string", "format": "binary"}) - @classmethod def __get_pydantic_json_schema__( - cls, core_schema: CoreSchema, handler: GetJsonSchemaHandler - ) -> JsonSchemaValue: + cls, core_schema: Mapping[str, Any], handler: GetJsonSchemaHandler + ) -> dict[str, Any]: return {"type": "string", "format": "binary"} @classmethod def __get_pydantic_core_schema__( - cls, source: Type[Any], handler: Callable[[Any], CoreSchema] - ) -> CoreSchema: + cls, source: type[Any], handler: Callable[[Any], Mapping[str, Any]] + ) -> Mapping[str, Any]: from ._compat.v2 import with_info_plain_validator_function return with_info_plain_validator_function(cls._validate) diff --git a/fastapi/dependencies/models.py b/fastapi/dependencies/models.py index d6359c0f51..58392326d6 100644 --- a/fastapi/dependencies/models.py +++ b/fastapi/dependencies/models.py @@ -1,8 +1,8 @@ import inspect import sys from dataclasses import dataclass, field -from functools import cached_property -from typing import Any, Callable, List, Optional, Sequence, Union +from functools import cached_property, partial +from typing import Any, Callable, Optional, Union from fastapi._compat import ModelField from fastapi.security.base import SecurityBase @@ -15,21 +15,27 @@ else: # pragma: no cover from asyncio import iscoroutinefunction -@dataclass -class SecurityRequirement: - security_scheme: SecurityBase - scopes: Optional[Sequence[str]] = None +def _unwrapped_call(call: Optional[Callable[..., Any]]) -> Any: + if call is None: + return call # pragma: no cover + unwrapped = inspect.unwrap(_impartial(call)) + return unwrapped + + +def _impartial(func: Callable[..., Any]) -> Callable[..., Any]: + while isinstance(func, partial): + func = func.func + return func @dataclass class Dependant: - path_params: List[ModelField] = field(default_factory=list) - query_params: List[ModelField] = field(default_factory=list) - header_params: List[ModelField] = field(default_factory=list) - cookie_params: List[ModelField] = field(default_factory=list) - body_params: List[ModelField] = field(default_factory=list) - dependencies: List["Dependant"] = field(default_factory=list) - security_requirements: List[SecurityRequirement] = field(default_factory=list) + path_params: list[ModelField] = field(default_factory=list) + query_params: list[ModelField] = field(default_factory=list) + header_params: list[ModelField] = field(default_factory=list) + cookie_params: list[ModelField] = field(default_factory=list) + body_params: list[ModelField] = field(default_factory=list) + dependencies: list["Dependant"] = field(default_factory=list) name: Optional[str] = None call: Optional[Callable[..., Any]] = None request_param_name: Optional[str] = None @@ -38,41 +44,145 @@ class Dependant: response_param_name: Optional[str] = None background_tasks_param_name: Optional[str] = None security_scopes_param_name: Optional[str] = None - security_scopes: Optional[List[str]] = None + own_oauth_scopes: Optional[list[str]] = None + parent_oauth_scopes: Optional[list[str]] = None use_cache: bool = True path: Optional[str] = None scope: Union[Literal["function", "request"], None] = None + @cached_property + def oauth_scopes(self) -> list[str]: + scopes = self.parent_oauth_scopes.copy() if self.parent_oauth_scopes else [] + # This doesn't use a set to preserve order, just in case + for scope in self.own_oauth_scopes or []: + if scope not in scopes: + scopes.append(scope) + return scopes + @cached_property def cache_key(self) -> DependencyCacheKey: + scopes_for_cache = ( + tuple(sorted(set(self.oauth_scopes or []))) if self._uses_scopes else () + ) return ( self.call, - tuple(sorted(set(self.security_scopes or []))), + scopes_for_cache, self.computed_scope or "", ) @cached_property - def is_gen_callable(self) -> bool: - if inspect.isgeneratorfunction(self.call): + def _uses_scopes(self) -> bool: + if self.own_oauth_scopes: return True - dunder_call = getattr(self.call, "__call__", None) # noqa: B004 - return inspect.isgeneratorfunction(dunder_call) + if self.security_scopes_param_name is not None: + return True + if self._is_security_scheme: + return True + for sub_dep in self.dependencies: + if sub_dep._uses_scopes: + return True + return False + + @cached_property + def _is_security_scheme(self) -> bool: + if self.call is None: + return False # pragma: no cover + unwrapped = _unwrapped_call(self.call) + return isinstance(unwrapped, SecurityBase) + + # Mainly to get the type of SecurityBase, but it's the same self.call + @cached_property + def _security_scheme(self) -> SecurityBase: + unwrapped = _unwrapped_call(self.call) + assert isinstance(unwrapped, SecurityBase) + return unwrapped + + @cached_property + def _security_dependencies(self) -> list["Dependant"]: + security_deps = [dep for dep in self.dependencies if dep._is_security_scheme] + return security_deps + + @cached_property + def is_gen_callable(self) -> bool: + if self.call is None: + return False # pragma: no cover + if inspect.isgeneratorfunction( + _impartial(self.call) + ) or inspect.isgeneratorfunction(_unwrapped_call(self.call)): + return True + if inspect.isclass(_unwrapped_call(self.call)): + return False + dunder_call = getattr(_impartial(self.call), "__call__", None) # noqa: B004 + if dunder_call is None: + return False # pragma: no cover + if inspect.isgeneratorfunction( + _impartial(dunder_call) + ) or inspect.isgeneratorfunction(_unwrapped_call(dunder_call)): + return True + dunder_unwrapped_call = getattr(_unwrapped_call(self.call), "__call__", None) # noqa: B004 + if dunder_unwrapped_call is None: + return False # pragma: no cover + if inspect.isgeneratorfunction( + _impartial(dunder_unwrapped_call) + ) or inspect.isgeneratorfunction(_unwrapped_call(dunder_unwrapped_call)): + return True + return False @cached_property def is_async_gen_callable(self) -> bool: - if inspect.isasyncgenfunction(self.call): + if self.call is None: + return False # pragma: no cover + if inspect.isasyncgenfunction( + _impartial(self.call) + ) or inspect.isasyncgenfunction(_unwrapped_call(self.call)): return True - dunder_call = getattr(self.call, "__call__", None) # noqa: B004 - return inspect.isasyncgenfunction(dunder_call) + if inspect.isclass(_unwrapped_call(self.call)): + return False + dunder_call = getattr(_impartial(self.call), "__call__", None) # noqa: B004 + if dunder_call is None: + return False # pragma: no cover + if inspect.isasyncgenfunction( + _impartial(dunder_call) + ) or inspect.isasyncgenfunction(_unwrapped_call(dunder_call)): + return True + dunder_unwrapped_call = getattr(_unwrapped_call(self.call), "__call__", None) # noqa: B004 + if dunder_unwrapped_call is None: + return False # pragma: no cover + if inspect.isasyncgenfunction( + _impartial(dunder_unwrapped_call) + ) or inspect.isasyncgenfunction(_unwrapped_call(dunder_unwrapped_call)): + return True + return False @cached_property def is_coroutine_callable(self) -> bool: - if inspect.isroutine(self.call): - return iscoroutinefunction(self.call) - if inspect.isclass(self.call): + if self.call is None: + return False # pragma: no cover + if inspect.isroutine(_impartial(self.call)) and iscoroutinefunction( + _impartial(self.call) + ): + return True + if inspect.isroutine(_unwrapped_call(self.call)) and iscoroutinefunction( + _unwrapped_call(self.call) + ): + return True + if inspect.isclass(_unwrapped_call(self.call)): return False - dunder_call = getattr(self.call, "__call__", None) # noqa: B004 - return iscoroutinefunction(dunder_call) + dunder_call = getattr(_impartial(self.call), "__call__", None) # noqa: B004 + if dunder_call is None: + return False # pragma: no cover + if iscoroutinefunction(_impartial(dunder_call)) or iscoroutinefunction( + _unwrapped_call(dunder_call) + ): + return True + dunder_unwrapped_call = getattr(_unwrapped_call(self.call), "__call__", None) # noqa: B004 + if dunder_unwrapped_call is None: + return False # pragma: no cover + if iscoroutinefunction( + _impartial(dunder_unwrapped_call) + ) or iscoroutinefunction(_unwrapped_call(dunder_unwrapped_call)): + return True + return False @cached_property def computed_scope(self) -> Union[str, None]: diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index c5c6b69bbc..45e1ff3ed1 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -1,19 +1,16 @@ +import dataclasses import inspect +import sys +from collections.abc import Coroutine, Mapping, Sequence from contextlib import AsyncExitStack, contextmanager from copy import copy, deepcopy from dataclasses import dataclass from typing import ( + Annotated, Any, Callable, - Coroutine, - Dict, ForwardRef, - List, - Mapping, Optional, - Sequence, - Tuple, - Type, Union, cast, ) @@ -21,17 +18,14 @@ from typing import ( import anyio from fastapi import params from fastapi._compat import ( - PYDANTIC_V2, ModelField, RequiredParam, Undefined, - _is_error_wrapper, - _is_model_class, + _regenerate_error_with_loc, copy_field_info, create_body_model, evaluate_forwardref, field_annotation_is_scalar, - get_annotation_from_field_info, get_cached_model_fields, get_missing_field_error, is_bytes_field, @@ -42,23 +36,19 @@ from fastapi._compat import ( is_uploadfile_or_nonable_uploadfile_annotation, is_uploadfile_sequence_annotation, lenient_issubclass, - may_v1, sequence_types, serialize_sequence_value, value_is_sequence, ) -from fastapi._compat.shared import annotation_is_pydantic_v1 from fastapi.background import BackgroundTasks from fastapi.concurrency import ( asynccontextmanager, contextmanager_in_threadpool, ) -from fastapi.dependencies.models import Dependant, SecurityRequirement +from fastapi.dependencies.models import Dependant from fastapi.exceptions import DependencyScopeError from fastapi.logger import logger -from fastapi.security.base import SecurityBase -from fastapi.security.oauth2 import OAuth2, SecurityScopes -from fastapi.security.open_id_connect_url import OpenIdConnect +from fastapi.security.oauth2 import SecurityScopes from fastapi.types import DependencyCacheKey from fastapi.utils import create_model_field, get_path_param_names from pydantic import BaseModel @@ -75,9 +65,7 @@ from starlette.datastructures import ( from starlette.requests import HTTPConnection, Request from starlette.responses import Response from starlette.websockets import WebSocket -from typing_extensions import Annotated, Literal, get_args, get_origin - -from .. import temp_pydantic_v1_params +from typing_extensions import Literal, get_args, get_origin multipart_not_installed_error = ( 'Form data requires "python-multipart" to be installed. \n' @@ -125,11 +113,14 @@ def get_parameterless_sub_dependant(*, depends: params.Depends, path: str) -> De assert callable(depends.dependency), ( "A parameter-less dependency must have a callable dependency" ) - use_security_scopes: List[str] = [] + own_oauth_scopes: list[str] = [] if isinstance(depends, params.Security) and depends.scopes: - use_security_scopes.extend(depends.scopes) + own_oauth_scopes.extend(depends.scopes) return get_dependant( - path=path, call=depends.dependency, security_scopes=use_security_scopes + path=path, + call=depends.dependency, + scope=depends.scope, + own_oauth_scopes=own_oauth_scopes, ) @@ -137,11 +128,15 @@ def get_flat_dependant( dependant: Dependant, *, skip_repeats: bool = False, - visited: Optional[List[DependencyCacheKey]] = None, + visited: Optional[list[DependencyCacheKey]] = None, + parent_oauth_scopes: Optional[list[str]] = None, ) -> Dependant: if visited is None: visited = [] visited.append(dependant.cache_key) + use_parent_oauth_scopes = (parent_oauth_scopes or []) + ( + dependant.oauth_scopes or [] + ) flat_dependant = Dependant( path_params=dependant.path_params.copy(), @@ -149,36 +144,51 @@ def get_flat_dependant( header_params=dependant.header_params.copy(), cookie_params=dependant.cookie_params.copy(), body_params=dependant.body_params.copy(), - security_requirements=dependant.security_requirements.copy(), + name=dependant.name, + call=dependant.call, + request_param_name=dependant.request_param_name, + websocket_param_name=dependant.websocket_param_name, + http_connection_param_name=dependant.http_connection_param_name, + response_param_name=dependant.response_param_name, + background_tasks_param_name=dependant.background_tasks_param_name, + security_scopes_param_name=dependant.security_scopes_param_name, + own_oauth_scopes=dependant.own_oauth_scopes, + parent_oauth_scopes=use_parent_oauth_scopes, use_cache=dependant.use_cache, path=dependant.path, + scope=dependant.scope, ) for sub_dependant in dependant.dependencies: if skip_repeats and sub_dependant.cache_key in visited: continue flat_sub = get_flat_dependant( - sub_dependant, skip_repeats=skip_repeats, visited=visited + sub_dependant, + skip_repeats=skip_repeats, + visited=visited, + parent_oauth_scopes=flat_dependant.oauth_scopes, ) + flat_dependant.dependencies.append(flat_sub) flat_dependant.path_params.extend(flat_sub.path_params) flat_dependant.query_params.extend(flat_sub.query_params) flat_dependant.header_params.extend(flat_sub.header_params) flat_dependant.cookie_params.extend(flat_sub.cookie_params) flat_dependant.body_params.extend(flat_sub.body_params) - flat_dependant.security_requirements.extend(flat_sub.security_requirements) + flat_dependant.dependencies.extend(flat_sub.dependencies) + return flat_dependant -def _get_flat_fields_from_params(fields: List[ModelField]) -> List[ModelField]: +def _get_flat_fields_from_params(fields: list[ModelField]) -> list[ModelField]: if not fields: return fields first_field = fields[0] - if len(fields) == 1 and _is_model_class(first_field.type_): + if len(fields) == 1 and lenient_issubclass(first_field.type_, BaseModel): fields_to_extract = get_cached_model_fields(first_field.type_) return fields_to_extract return fields -def get_flat_params(dependant: Dependant) -> List[ModelField]: +def get_flat_params(dependant: Dependant) -> list[ModelField]: flat_dependant = get_flat_dependant(dependant, skip_repeats=True) path_params = _get_flat_fields_from_params(flat_dependant.path_params) query_params = _get_flat_fields_from_params(flat_dependant.query_params) @@ -187,9 +197,23 @@ def get_flat_params(dependant: Dependant) -> List[ModelField]: return path_params + query_params + header_params + cookie_params +def _get_signature(call: Callable[..., Any]) -> inspect.Signature: + if sys.version_info >= (3, 10): + try: + signature = inspect.signature(call, eval_str=True) + except NameError: + # Handle type annotations with if TYPE_CHECKING, not used by FastAPI + # e.g. dependency return types + signature = inspect.signature(call) + else: + signature = inspect.signature(call) + return signature + + def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature: - signature = inspect.signature(call) - globalns = getattr(call, "__globals__", {}) + signature = _get_signature(call) + unwrapped = inspect.unwrap(call) + globalns = getattr(unwrapped, "__globals__", {}) typed_params = [ inspect.Parameter( name=param.name, @@ -203,7 +227,7 @@ def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature: return typed_signature -def get_typed_annotation(annotation: Any, globalns: Dict[str, Any]) -> Any: +def get_typed_annotation(annotation: Any, globalns: dict[str, Any]) -> Any: if isinstance(annotation, str): annotation = ForwardRef(annotation) annotation = evaluate_forwardref(annotation, globalns, globalns) @@ -213,13 +237,14 @@ def get_typed_annotation(annotation: Any, globalns: Dict[str, Any]) -> Any: def get_typed_return_annotation(call: Callable[..., Any]) -> Any: - signature = inspect.signature(call) + signature = _get_signature(call) + unwrapped = inspect.unwrap(call) annotation = signature.return_annotation if annotation is inspect.Signature.empty: return None - globalns = getattr(call, "__globals__", {}) + globalns = getattr(unwrapped, "__globals__", {}) return get_typed_annotation(annotation, globalns) @@ -228,7 +253,8 @@ def get_dependant( path: str, call: Callable[..., Any], name: Optional[str] = None, - security_scopes: Optional[List[str]] = None, + own_oauth_scopes: Optional[list[str]] = None, + parent_oauth_scopes: Optional[list[str]] = None, use_cache: bool = True, scope: Union[Literal["function", "request"], None] = None, ) -> Dependant: @@ -236,21 +262,15 @@ def get_dependant( call=call, name=name, path=path, - security_scopes=security_scopes, use_cache=use_cache, scope=scope, + own_oauth_scopes=own_oauth_scopes, + parent_oauth_scopes=parent_oauth_scopes, ) + current_scopes = (parent_oauth_scopes or []) + (own_oauth_scopes or []) path_param_names = get_path_param_names(path) endpoint_signature = get_typed_signature(call) signature_params = endpoint_signature.parameters - if isinstance(call, SecurityBase): - use_scopes: List[str] = [] - if isinstance(call, (OAuth2, OpenIdConnect)): - use_scopes = security_scopes or use_scopes - security_requirement = SecurityRequirement( - security_scheme=call, scopes=use_scopes - ) - dependant.security_requirements.append(security_requirement) for param_name, param in signature_params.items(): is_path_param = param_name in path_param_names param_details = analyze_param( @@ -271,15 +291,16 @@ def get_dependant( f'The dependency "{dependant.call.__name__}" has a scope of ' '"request", it cannot depend on dependencies with scope "function".' ) - use_security_scopes = security_scopes or [] + sub_own_oauth_scopes: list[str] = [] if isinstance(param_details.depends, params.Security): if param_details.depends.scopes: - use_security_scopes.extend(param_details.depends.scopes) + sub_own_oauth_scopes = list(param_details.depends.scopes) sub_dependant = get_dependant( path=path, call=param_details.depends.dependency, name=param_name, - security_scopes=use_security_scopes, + own_oauth_scopes=sub_own_oauth_scopes, + parent_oauth_scopes=current_scopes, use_cache=param_details.depends.use_cache, scope=param_details.depends.scope, ) @@ -295,9 +316,7 @@ def get_dependant( ) continue assert param_details.field is not None - if isinstance( - param_details.field.field_info, (params.Body, temp_pydantic_v1_params.Body) - ): + if isinstance(param_details.field.field_info, params.Body): dependant.body_params.append(param_details.field) else: add_param_to_fields(field=param_details.field, dependant=dependant) @@ -356,7 +375,7 @@ def analyze_param( fastapi_annotations = [ arg for arg in annotated_args[1:] - if isinstance(arg, (FieldInfo, may_v1.FieldInfo, params.Depends)) + if isinstance(arg, (FieldInfo, params.Depends)) ] fastapi_specific_annotations = [ arg @@ -365,29 +384,27 @@ def analyze_param( arg, ( params.Param, - temp_pydantic_v1_params.Param, params.Body, - temp_pydantic_v1_params.Body, params.Depends, ), ) ] if fastapi_specific_annotations: - fastapi_annotation: Union[ - FieldInfo, may_v1.FieldInfo, params.Depends, None - ] = fastapi_specific_annotations[-1] + fastapi_annotation: Union[FieldInfo, params.Depends, None] = ( + fastapi_specific_annotations[-1] + ) else: fastapi_annotation = None # Set default for Annotated FieldInfo - if isinstance(fastapi_annotation, (FieldInfo, may_v1.FieldInfo)): + if isinstance(fastapi_annotation, FieldInfo): # Copy `field_info` because we mutate `field_info.default` below. field_info = copy_field_info( - field_info=fastapi_annotation, annotation=use_annotation + field_info=fastapi_annotation, # type: ignore[arg-type] + annotation=use_annotation, ) - assert field_info.default in { - Undefined, - may_v1.Undefined, - } or field_info.default in {RequiredParam, may_v1.RequiredParam}, ( + assert ( + field_info.default == Undefined or field_info.default == RequiredParam + ), ( f"`{field_info.__class__.__name__}` default value cannot be set in" f" `Annotated` for {param_name!r}. Set the default value with `=` instead." ) @@ -411,21 +428,20 @@ def analyze_param( ) depends = value # Get FieldInfo from default value - elif isinstance(value, (FieldInfo, may_v1.FieldInfo)): + elif isinstance(value, FieldInfo): assert field_info is None, ( "Cannot specify FastAPI annotations in `Annotated` and default value" f" together for {param_name!r}" ) - field_info = value - if PYDANTIC_V2: - if isinstance(field_info, FieldInfo): - field_info.annotation = type_annotation + field_info = value # type: ignore[assignment] + if isinstance(field_info, FieldInfo): + field_info.annotation = type_annotation # Get Depends from type annotation if depends is not None and depends.dependency is None: # Copy `depends` before mutating it depends = copy(depends) - depends.dependency = type_annotation + depends = dataclasses.replace(depends, dependency=type_annotation) # Handle non-param type annotations like Request if lenient_issubclass( @@ -456,14 +472,7 @@ def analyze_param( ) or is_uploadfile_sequence_annotation(type_annotation): field_info = params.File(annotation=use_annotation, default=default_value) elif not field_annotation_is_scalar(annotation=type_annotation): - if annotation_is_pydantic_v1(use_annotation): - field_info = temp_pydantic_v1_params.Body( - annotation=use_annotation, default=default_value - ) - else: - field_info = params.Body( - annotation=use_annotation, default=default_value - ) + field_info = params.Body(annotation=use_annotation, default=default_value) else: field_info = params.Query(annotation=use_annotation, default=default_value) @@ -472,23 +481,17 @@ def analyze_param( if field_info is not None: # Handle field_info.in_ if is_path_param: - assert isinstance( - field_info, (params.Path, temp_pydantic_v1_params.Path) - ), ( + assert isinstance(field_info, params.Path), ( f"Cannot use `{field_info.__class__.__name__}` for path param" f" {param_name!r}" ) elif ( - isinstance(field_info, (params.Param, temp_pydantic_v1_params.Param)) + isinstance(field_info, params.Param) and getattr(field_info, "in_", None) is None ): field_info.in_ = params.ParamTypes.query - use_annotation_from_field_info = get_annotation_from_field_info( - use_annotation, - field_info, - param_name, - ) - if isinstance(field_info, (params.Form, temp_pydantic_v1_params.Form)): + use_annotation_from_field_info = use_annotation + if isinstance(field_info, params.Form): ensure_multipart_is_installed() if not field_info.alias and getattr(field_info, "convert_underscores", None): alias = param_name.replace("_", "-") @@ -500,20 +503,19 @@ def analyze_param( type_=use_annotation_from_field_info, default=field_info.default, alias=alias, - required=field_info.default - in (RequiredParam, may_v1.RequiredParam, Undefined), + required=field_info.default in (RequiredParam, Undefined), field_info=field_info, ) if is_path_param: assert is_scalar_field(field=field), ( "Path params must be of one of the supported types" ) - elif isinstance(field_info, (params.Query, temp_pydantic_v1_params.Query)): + elif isinstance(field_info, params.Query): assert ( is_scalar_field(field) or is_scalar_sequence_field(field) or ( - _is_model_class(field.type_) + lenient_issubclass(field.type_, BaseModel) # For Pydantic v1 and getattr(field, "shape", 1) == 1 ) @@ -539,34 +541,34 @@ def add_param_to_fields(*, field: ModelField, dependant: Dependant) -> None: async def _solve_generator( - *, dependant: Dependant, stack: AsyncExitStack, sub_values: Dict[str, Any] + *, dependant: Dependant, stack: AsyncExitStack, sub_values: dict[str, Any] ) -> Any: assert dependant.call - if dependant.is_gen_callable: - cm = contextmanager_in_threadpool(contextmanager(dependant.call)(**sub_values)) - elif dependant.is_async_gen_callable: + if dependant.is_async_gen_callable: cm = asynccontextmanager(dependant.call)(**sub_values) + elif dependant.is_gen_callable: + cm = contextmanager_in_threadpool(contextmanager(dependant.call)(**sub_values)) return await stack.enter_async_context(cm) @dataclass class SolvedDependency: - values: Dict[str, Any] - errors: List[Any] + values: dict[str, Any] + errors: list[Any] background_tasks: Optional[StarletteBackgroundTasks] response: Response - dependency_cache: Dict[DependencyCacheKey, Any] + dependency_cache: dict[DependencyCacheKey, Any] async def solve_dependencies( *, request: Union[Request, WebSocket], dependant: Dependant, - body: Optional[Union[Dict[str, Any], FormData]] = None, + body: Optional[Union[dict[str, Any], FormData]] = None, background_tasks: Optional[StarletteBackgroundTasks] = None, response: Optional[Response] = None, dependency_overrides_provider: Optional[Any] = None, - dependency_cache: Optional[Dict[DependencyCacheKey, Any]] = None, + dependency_cache: Optional[dict[DependencyCacheKey, Any]] = None, # TODO: remove this parameter later, no longer used, not removing it yet as some # people might be monkey patching this function (although that's not supported) async_exit_stack: AsyncExitStack, @@ -580,8 +582,8 @@ async def solve_dependencies( assert isinstance(function_astack, AsyncExitStack), ( "fastapi_function_astack not found in request scope" ) - values: Dict[str, Any] = {} - errors: List[Any] = [] + values: dict[str, Any] = {} + errors: list[Any] = [] if response is None: response = Response() del response.headers["content-length"] @@ -605,7 +607,7 @@ async def solve_dependencies( path=use_path, call=call, name=sub_dependant.name, - security_scopes=sub_dependant.security_scopes, + parent_oauth_scopes=sub_dependant.oauth_scopes, scope=sub_dependant.scope, ) @@ -687,7 +689,7 @@ async def solve_dependencies( values[dependant.response_param_name] = response if dependant.security_scopes_param_name: values[dependant.security_scopes_param_name] = SecurityScopes( - scopes=dependant.security_scopes + scopes=dependant.oauth_scopes ) return SolvedDependency( values=values, @@ -699,18 +701,16 @@ async def solve_dependencies( def _validate_value_with_model_field( - *, field: ModelField, value: Any, values: Dict[str, Any], loc: Tuple[str, ...] -) -> Tuple[Any, List[Any]]: + *, field: ModelField, value: Any, values: dict[str, Any], loc: tuple[str, ...] +) -> tuple[Any, list[Any]]: if value is None: if field.required: return None, [get_missing_field_error(loc=loc)] else: return deepcopy(field.default), [] v_, errors_ = field.validate(value, values, loc=loc) - if _is_error_wrapper(errors_): # type: ignore[arg-type] - return None, [errors_] - elif isinstance(errors_, list): - new_errors = may_v1._regenerate_error_with_loc(errors=errors_, loc_prefix=()) + if isinstance(errors_, list): + new_errors = _regenerate_error_with_loc(errors=errors_, loc_prefix=()) return None, new_errors else: return v_, [] @@ -719,7 +719,7 @@ def _validate_value_with_model_field( def _get_multidict_value( field: ModelField, values: Mapping[str, Any], alias: Union[str, None] = None ) -> Any: - alias = alias or field.alias + alias = alias or get_validation_alias(field) if is_sequence_field(field) and isinstance(values, (ImmutableMultiDict, Headers)): value = values.getlist(alias) else: @@ -727,7 +727,7 @@ def _get_multidict_value( if ( value is None or ( - isinstance(field.field_info, (params.Form, temp_pydantic_v1_params.Form)) + isinstance(field.field_info, params.Form) and isinstance(value, str) # For type checks and value == "" ) @@ -743,9 +743,9 @@ def _get_multidict_value( def request_params_to_args( fields: Sequence[ModelField], received_params: Union[Mapping[str, Any], QueryParams, Headers], -) -> Tuple[Dict[str, Any], List[Any]]: - values: Dict[str, Any] = {} - errors: List[Dict[str, Any]] = [] +) -> tuple[dict[str, Any], list[Any]]: + values: dict[str, Any] = {} + errors: list[dict[str, Any]] = [] if not fields: return values, errors @@ -763,7 +763,7 @@ def request_params_to_args( first_field.field_info, "convert_underscores", True ) - params_to_process: Dict[str, Any] = {} + params_to_process: dict[str, Any] = {} processed_keys = set() @@ -776,27 +776,31 @@ def request_params_to_args( field.field_info, "convert_underscores", default_convert_underscores ) if convert_underscores: - alias = ( - field.alias - if field.alias != field.name - else field.name.replace("_", "-") - ) + alias = get_validation_alias(field) + if alias == field.name: + alias = alias.replace("_", "-") value = _get_multidict_value(field, received_params, alias=alias) if value is not None: - params_to_process[field.name] = value - processed_keys.add(alias or field.alias) - processed_keys.add(field.name) + params_to_process[get_validation_alias(field)] = value + processed_keys.add(alias or get_validation_alias(field)) - for key, value in received_params.items(): + for key in received_params.keys(): if key not in processed_keys: - params_to_process[key] = value + if hasattr(received_params, "getlist"): + value = received_params.getlist(key) + if isinstance(value, list) and (len(value) == 1): + params_to_process[key] = value[0] + else: + params_to_process[key] = value + else: + params_to_process[key] = received_params.get(key) if single_not_embedded_field: field_info = first_field.field_info - assert isinstance(field_info, (params.Param, temp_pydantic_v1_params.Param)), ( + assert isinstance(field_info, params.Param), ( "Params must be subclasses of Param" ) - loc: Tuple[str, ...] = (field_info.in_.value,) + loc: tuple[str, ...] = (field_info.in_.value,) v_, errors_ = _validate_value_with_model_field( field=first_field, value=params_to_process, values=values, loc=loc ) @@ -805,10 +809,10 @@ def request_params_to_args( for field in fields: value = _get_multidict_value(field, received_params) field_info = field.field_info - assert isinstance(field_info, (params.Param, temp_pydantic_v1_params.Param)), ( + assert isinstance(field_info, params.Param), ( "Params must be subclasses of Param" ) - loc = (field_info.in_.value, field.alias) + loc = (field_info.in_.value, get_validation_alias(field)) v_, errors_ = _validate_value_with_model_field( field=field, value=value, values=values, loc=loc ) @@ -832,13 +836,13 @@ def is_union_of_base_models(field_type: Any) -> bool: union_args = get_args(field_type) for arg in union_args: - if not _is_model_class(arg): + if not lenient_issubclass(arg, BaseModel): return False return True -def _should_embed_body_fields(fields: List[ModelField]) -> bool: +def _should_embed_body_fields(fields: list[ModelField]) -> bool: if not fields: return False # More than one dependency could have the same field, it would show up as multiple @@ -854,8 +858,8 @@ def _should_embed_body_fields(fields: List[ModelField]) -> bool: # If it's a Form (or File) field, it has to be a BaseModel (or a union of BaseModels) to be top level # otherwise it has to be embedded, so that the key value pair can be extracted if ( - isinstance(first_field.field_info, (params.Form, temp_pydantic_v1_params.Form)) - and not _is_model_class(first_field.type_) + isinstance(first_field.field_info, params.Form) + and not lenient_issubclass(first_field.type_, BaseModel) and not is_union_of_base_models(first_field.type_) ): return True @@ -863,28 +867,28 @@ def _should_embed_body_fields(fields: List[ModelField]) -> bool: async def _extract_form_body( - body_fields: List[ModelField], + body_fields: list[ModelField], received_body: FormData, -) -> Dict[str, Any]: +) -> dict[str, Any]: values = {} for field in body_fields: value = _get_multidict_value(field, received_body) field_info = field.field_info if ( - isinstance(field_info, (params.File, temp_pydantic_v1_params.File)) + isinstance(field_info, params.File) and is_bytes_field(field) and isinstance(value, UploadFile) ): value = await value.read() elif ( is_bytes_sequence_field(field) - and isinstance(field_info, (params.File, temp_pydantic_v1_params.File)) + and isinstance(field_info, params.File) and value_is_sequence(value) ): # For types - assert isinstance(value, sequence_types) # type: ignore[arg-type] - results: List[Union[bytes, str]] = [] + assert isinstance(value, sequence_types) + results: list[Union[bytes, str]] = [] async def process_fn( fn: Callable[[], Coroutine[Any, Any, Any]], @@ -897,30 +901,35 @@ async def _extract_form_body( tg.start_soon(process_fn, sub_value.read) value = serialize_sequence_value(field=field, value=results) if value is not None: - values[field.alias] = value - for key, value in received_body.items(): - if key not in values: - values[key] = value + values[get_validation_alias(field)] = value + field_aliases = {get_validation_alias(field) for field in body_fields} + for key in received_body.keys(): + if key not in field_aliases: + param_values = received_body.getlist(key) + if len(param_values) == 1: + values[key] = param_values[0] + else: + values[key] = param_values return values async def request_body_to_args( - body_fields: List[ModelField], - received_body: Optional[Union[Dict[str, Any], FormData]], + body_fields: list[ModelField], + received_body: Optional[Union[dict[str, Any], FormData]], embed_body_fields: bool, -) -> Tuple[Dict[str, Any], List[Dict[str, Any]]]: - values: Dict[str, Any] = {} - errors: List[Dict[str, Any]] = [] +) -> tuple[dict[str, Any], list[dict[str, Any]]]: + values: dict[str, Any] = {} + errors: list[dict[str, Any]] = [] assert body_fields, "request_body_to_args() should be called with fields" single_not_embedded_field = len(body_fields) == 1 and not embed_body_fields first_field = body_fields[0] body_to_process = received_body - fields_to_extract: List[ModelField] = body_fields + fields_to_extract: list[ModelField] = body_fields if ( single_not_embedded_field - and _is_model_class(first_field.type_) + and lenient_issubclass(first_field.type_, BaseModel) and isinstance(received_body, FormData) ): fields_to_extract = get_cached_model_fields(first_field.type_) @@ -929,17 +938,17 @@ async def request_body_to_args( body_to_process = await _extract_form_body(fields_to_extract, received_body) if single_not_embedded_field: - loc: Tuple[str, ...] = ("body",) + loc: tuple[str, ...] = ("body",) v_, errors_ = _validate_value_with_model_field( field=first_field, value=body_to_process, values=values, loc=loc ) return {first_field.name: v_}, errors_ for field in body_fields: - loc = ("body", field.alias) + loc = ("body", get_validation_alias(field)) value: Optional[Any] = None if body_to_process is not None: try: - value = body_to_process.get(field.alias) + value = body_to_process.get(get_validation_alias(field)) # If the received body is a list, not a dict except AttributeError: errors.append(get_missing_field_error(loc)) @@ -977,36 +986,23 @@ def get_body_field( fields=flat_dependant.body_params, model_name=model_name ) required = any(True for f in flat_dependant.body_params if f.required) - BodyFieldInfo_kwargs: Dict[str, Any] = { + BodyFieldInfo_kwargs: dict[str, Any] = { "annotation": BodyModel, "alias": "body", } if not required: BodyFieldInfo_kwargs["default"] = None if any(isinstance(f.field_info, params.File) for f in flat_dependant.body_params): - BodyFieldInfo: Type[params.Body] = params.File - elif any( - isinstance(f.field_info, temp_pydantic_v1_params.File) - for f in flat_dependant.body_params - ): - BodyFieldInfo: Type[temp_pydantic_v1_params.Body] = temp_pydantic_v1_params.File # type: ignore[no-redef] + BodyFieldInfo: type[params.Body] = params.File elif any(isinstance(f.field_info, params.Form) for f in flat_dependant.body_params): BodyFieldInfo = params.Form - elif any( - isinstance(f.field_info, temp_pydantic_v1_params.Form) - for f in flat_dependant.body_params - ): - BodyFieldInfo = temp_pydantic_v1_params.Form # type: ignore[assignment] else: - if annotation_is_pydantic_v1(BodyModel): - BodyFieldInfo = temp_pydantic_v1_params.Body # type: ignore[assignment] - else: - BodyFieldInfo = params.Body + BodyFieldInfo = params.Body body_param_media_types = [ f.field_info.media_type for f in flat_dependant.body_params - if isinstance(f.field_info, (params.Body, temp_pydantic_v1_params.Body)) + if isinstance(f.field_info, params.Body) ] if len(set(body_param_media_types)) == 1: BodyFieldInfo_kwargs["media_type"] = body_param_media_types[0] @@ -1018,3 +1014,8 @@ def get_body_field( field_info=BodyFieldInfo(**BodyFieldInfo_kwargs), ) return final_field + + +def get_validation_alias(field: ModelField) -> str: + va = getattr(field, "validation_alias", None) + return va or field.alias diff --git a/fastapi/encoders.py b/fastapi/encoders.py index 6fc6228e15..e8610c983b 100644 --- a/fastapi/encoders.py +++ b/fastapi/encoders.py @@ -14,19 +14,22 @@ from ipaddress import ( from pathlib import Path, PurePath from re import Pattern from types import GeneratorType -from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union +from typing import Annotated, Any, Callable, Optional, Union from uuid import UUID from annotated_doc import Doc -from fastapi._compat import may_v1 +from fastapi.exceptions import PydanticV1NotSupportedError from fastapi.types import IncEx from pydantic import BaseModel from pydantic.color import Color from pydantic.networks import AnyUrl, NameEmail from pydantic.types import SecretBytes, SecretStr -from typing_extensions import Annotated +from pydantic_core import PydanticUndefinedType -from ._compat import Url, _is_undefined, _model_dump +from ._compat import ( + Url, + is_pydantic_v1_model_instance, +) # Taken from Pydantic v1 as is @@ -34,14 +37,14 @@ def isoformat(o: Union[datetime.date, datetime.time]) -> str: return o.isoformat() -# Taken from Pydantic v1 as is +# Adapted from Pydantic v1 # TODO: pv2 should this return strings instead? def decimal_encoder(dec_value: Decimal) -> Union[int, float]: """ - Encodes a Decimal as int of there's no exponent, otherwise float + Encodes a Decimal as int if there's no exponent, otherwise float This is useful when we use ConstrainedDecimal to represent Numeric(x,0) - where a integer (but not int typed) is used. Encoding this as a float + where an integer (but not int typed) is used. Encoding this as a float results in failed round-tripping between encode and parse. Our Id type is a prime example of this. @@ -50,17 +53,20 @@ def decimal_encoder(dec_value: Decimal) -> Union[int, float]: >>> decimal_encoder(Decimal("1")) 1 + + >>> decimal_encoder(Decimal("NaN")) + nan """ - if dec_value.as_tuple().exponent >= 0: # type: ignore[operator] + exponent = dec_value.as_tuple().exponent + if isinstance(exponent, int) and exponent >= 0: return int(dec_value) else: return float(dec_value) -ENCODERS_BY_TYPE: Dict[Type[Any], Callable[[Any], Any]] = { +ENCODERS_BY_TYPE: dict[type[Any], Callable[[Any], Any]] = { bytes: lambda o: o.decode(), Color: str, - may_v1.Color: str, datetime.date: isoformat, datetime.datetime: isoformat, datetime.time: isoformat, @@ -77,26 +83,21 @@ ENCODERS_BY_TYPE: Dict[Type[Any], Callable[[Any], Any]] = { IPv6Interface: str, IPv6Network: str, NameEmail: str, - may_v1.NameEmail: str, Path: str, Pattern: lambda o: o.pattern, SecretBytes: str, - may_v1.SecretBytes: str, SecretStr: str, - may_v1.SecretStr: str, set: list, UUID: str, Url: str, - may_v1.Url: str, AnyUrl: str, - may_v1.AnyUrl: str, } def generate_encoders_by_class_tuples( - type_encoder_map: Dict[Any, Callable[[Any], Any]], -) -> Dict[Callable[[Any], Any], Tuple[Any, ...]]: - encoders_by_class_tuples: Dict[Callable[[Any], Any], Tuple[Any, ...]] = defaultdict( + type_encoder_map: dict[Any, Callable[[Any], Any]], +) -> dict[Callable[[Any], Any], tuple[Any, ...]]: + encoders_by_class_tuples: dict[Callable[[Any], Any], tuple[Any, ...]] = defaultdict( tuple ) for type_, encoder in type_encoder_map.items(): @@ -176,7 +177,7 @@ def jsonable_encoder( ), ] = False, custom_encoder: Annotated[ - Optional[Dict[Any, Callable[[Any], Any]]], + Optional[dict[Any, Callable[[Any], Any]]], Doc( """ Pydantic's `custom_encoder` parameter, passed to Pydantic models to define @@ -221,15 +222,8 @@ def jsonable_encoder( include = set(include) if exclude is not None and not isinstance(exclude, (set, dict)): exclude = set(exclude) - if isinstance(obj, (BaseModel, may_v1.BaseModel)): - # TODO: remove when deprecating Pydantic v1 - encoders: Dict[Any, Any] = {} - if isinstance(obj, may_v1.BaseModel): - encoders = getattr(obj.__config__, "json_encoders", {}) # type: ignore[attr-defined] - if custom_encoder: - encoders = {**encoders, **custom_encoder} - obj_dict = _model_dump( - obj, + if isinstance(obj, BaseModel): + obj_dict = obj.model_dump( mode="json", include=include, exclude=exclude, @@ -238,14 +232,10 @@ def jsonable_encoder( exclude_none=exclude_none, exclude_defaults=exclude_defaults, ) - if "__root__" in obj_dict: - obj_dict = obj_dict["__root__"] return jsonable_encoder( obj_dict, exclude_none=exclude_none, exclude_defaults=exclude_defaults, - # TODO: remove when deprecating Pydantic v1 - custom_encoder=encoders, sqlalchemy_safe=sqlalchemy_safe, ) if dataclasses.is_dataclass(obj): @@ -268,7 +258,7 @@ def jsonable_encoder( return str(obj) if isinstance(obj, (str, int, float, type(None))): return obj - if _is_undefined(obj): + if isinstance(obj, PydanticUndefinedType): return None if isinstance(obj, dict): encoded_dict = {} @@ -328,11 +318,15 @@ def jsonable_encoder( for encoder, classes_tuple in encoders_by_class_tuples.items(): if isinstance(obj, classes_tuple): return encoder(obj) - + if is_pydantic_v1_model_instance(obj): + raise PydanticV1NotSupportedError( + "pydantic.v1 models are no longer supported by FastAPI." + f" Please update the model {obj!r}." + ) try: data = dict(obj) except Exception as e: - errors: List[Exception] = [] + errors: list[Exception] = [] errors.append(e) try: data = vars(obj) diff --git a/fastapi/exceptions.py b/fastapi/exceptions.py index 0620428be7..1a3abd80c2 100644 --- a/fastapi/exceptions.py +++ b/fastapi/exceptions.py @@ -1,10 +1,17 @@ -from typing import Any, Dict, Optional, Sequence, Type, Union +from collections.abc import Sequence +from typing import Annotated, Any, Optional, TypedDict, Union from annotated_doc import Doc from pydantic import BaseModel, create_model from starlette.exceptions import HTTPException as StarletteHTTPException from starlette.exceptions import WebSocketException as StarletteWebSocketException -from typing_extensions import Annotated + + +class EndpointContext(TypedDict, total=False): + function: str + path: str + file: str + line: int class HTTPException(StarletteHTTPException): @@ -55,7 +62,7 @@ class HTTPException(StarletteHTTPException): ), ] = None, headers: Annotated[ - Optional[Dict[str, str]], + Optional[dict[str, str]], Doc( """ Any headers to send to the client in the response. @@ -137,8 +144,8 @@ class WebSocketException(StarletteWebSocketException): super().__init__(code=code, reason=reason) -RequestErrorModel: Type[BaseModel] = create_model("Request") -WebSocketErrorModel: Type[BaseModel] = create_model("WebSocket") +RequestErrorModel: type[BaseModel] = create_model("Request") +WebSocketErrorModel: type[BaseModel] = create_model("WebSocket") class FastAPIError(RuntimeError): @@ -155,30 +162,85 @@ class DependencyScopeError(FastAPIError): class ValidationException(Exception): - def __init__(self, errors: Sequence[Any]) -> None: + def __init__( + self, + errors: Sequence[Any], + *, + endpoint_ctx: Optional[EndpointContext] = None, + ) -> None: self._errors = errors + self.endpoint_ctx = endpoint_ctx + + ctx = endpoint_ctx or {} + self.endpoint_function = ctx.get("function") + self.endpoint_path = ctx.get("path") + self.endpoint_file = ctx.get("file") + self.endpoint_line = ctx.get("line") def errors(self) -> Sequence[Any]: return self._errors + def _format_endpoint_context(self) -> str: + if not (self.endpoint_file and self.endpoint_line and self.endpoint_function): + if self.endpoint_path: + return f"\n Endpoint: {self.endpoint_path}" + return "" + + context = f'\n File "{self.endpoint_file}", line {self.endpoint_line}, in {self.endpoint_function}' + if self.endpoint_path: + context += f"\n {self.endpoint_path}" + return context + + def __str__(self) -> str: + message = f"{len(self._errors)} validation error{'s' if len(self._errors) != 1 else ''}:\n" + for err in self._errors: + message += f" {err}\n" + message += self._format_endpoint_context() + return message.rstrip() + class RequestValidationError(ValidationException): - def __init__(self, errors: Sequence[Any], *, body: Any = None) -> None: - super().__init__(errors) + def __init__( + self, + errors: Sequence[Any], + *, + body: Any = None, + endpoint_ctx: Optional[EndpointContext] = None, + ) -> None: + super().__init__(errors, endpoint_ctx=endpoint_ctx) self.body = body class WebSocketRequestValidationError(ValidationException): - pass + def __init__( + self, + errors: Sequence[Any], + *, + endpoint_ctx: Optional[EndpointContext] = None, + ) -> None: + super().__init__(errors, endpoint_ctx=endpoint_ctx) class ResponseValidationError(ValidationException): - def __init__(self, errors: Sequence[Any], *, body: Any = None) -> None: - super().__init__(errors) + def __init__( + self, + errors: Sequence[Any], + *, + body: Any = None, + endpoint_ctx: Optional[EndpointContext] = None, + ) -> None: + super().__init__(errors, endpoint_ctx=endpoint_ctx) self.body = body - def __str__(self) -> str: - message = f"{len(self._errors)} validation errors:\n" - for err in self._errors: - message += f" {err}\n" - return message + +class PydanticV1NotSupportedError(FastAPIError): + """ + A pydantic.v1 model is used, which is no longer supported. + """ + + +class FastAPIDeprecationWarning(UserWarning): + """ + A custom deprecation warning as DeprecationWarning is ignored + Ref: https://sethmlarson.dev/deprecations-via-warnings-dont-work-for-python-libraries + """ diff --git a/fastapi/openapi/docs.py b/fastapi/openapi/docs.py index 74b23a3706..82380f85d9 100644 --- a/fastapi/openapi/docs.py +++ b/fastapi/openapi/docs.py @@ -1,13 +1,12 @@ import json -from typing import Any, Dict, Optional +from typing import Annotated, Any, Optional from annotated_doc import Doc from fastapi.encoders import jsonable_encoder from starlette.responses import HTMLResponse -from typing_extensions import Annotated swagger_ui_default_parameters: Annotated[ - Dict[str, Any], + dict[str, Any], Doc( """ Default configurations for Swagger UI. @@ -82,7 +81,7 @@ def get_swagger_ui_html( ), ] = None, init_oauth: Annotated[ - Optional[Dict[str, Any]], + Optional[dict[str, Any]], Doc( """ A dictionary with Swagger UI OAuth2 initialization configurations. @@ -90,7 +89,7 @@ def get_swagger_ui_html( ), ] = None, swagger_ui_parameters: Annotated[ - Optional[Dict[str, Any]], + Optional[dict[str, Any]], Doc( """ Configuration parameters for Swagger UI. diff --git a/fastapi/openapi/models.py b/fastapi/openapi/models.py index 81d276aed6..ac6a6d52c3 100644 --- a/fastapi/openapi/models.py +++ b/fastapi/openapi/models.py @@ -1,17 +1,16 @@ +from collections.abc import Iterable, Mapping from enum import Enum -from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Type, Union +from typing import Annotated, Any, Callable, Optional, Union -from fastapi._compat import ( - PYDANTIC_V2, - CoreSchema, - GetJsonSchemaHandler, - JsonSchemaValue, - _model_rebuild, - with_info_plain_validator_function, -) +from fastapi._compat import with_info_plain_validator_function from fastapi.logger import logger -from pydantic import AnyUrl, BaseModel, Field -from typing_extensions import Annotated, Literal, TypedDict +from pydantic import ( + AnyUrl, + BaseModel, + Field, + GetJsonSchemaHandler, +) +from typing_extensions import Literal, TypedDict from typing_extensions import deprecated as typing_deprecated try: @@ -44,25 +43,19 @@ except ImportError: # pragma: no cover @classmethod def __get_pydantic_json_schema__( - cls, core_schema: CoreSchema, handler: GetJsonSchemaHandler - ) -> JsonSchemaValue: + cls, core_schema: Mapping[str, Any], handler: GetJsonSchemaHandler + ) -> dict[str, Any]: return {"type": "string", "format": "email"} @classmethod def __get_pydantic_core_schema__( - cls, source: Type[Any], handler: Callable[[Any], CoreSchema] - ) -> CoreSchema: + cls, source: type[Any], handler: Callable[[Any], Mapping[str, Any]] + ) -> Mapping[str, Any]: return with_info_plain_validator_function(cls._validate) class BaseModelWithConfig(BaseModel): - if PYDANTIC_V2: - model_config = {"extra": "allow"} - - else: - - class Config: - extra = "allow" + model_config = {"extra": "allow"} class Contact(BaseModelWithConfig): @@ -88,7 +81,7 @@ class Info(BaseModelWithConfig): class ServerVariable(BaseModelWithConfig): - enum: Annotated[Optional[List[str]], Field(min_length=1)] = None + enum: Annotated[Optional[list[str]], Field(min_length=1)] = None default: str description: Optional[str] = None @@ -96,7 +89,7 @@ class ServerVariable(BaseModelWithConfig): class Server(BaseModelWithConfig): url: Union[AnyUrl, str] description: Optional[str] = None - variables: Optional[Dict[str, ServerVariable]] = None + variables: Optional[dict[str, ServerVariable]] = None class Reference(BaseModel): @@ -105,7 +98,7 @@ class Reference(BaseModel): class Discriminator(BaseModel): propertyName: str - mapping: Optional[Dict[str, str]] = None + mapping: Optional[dict[str, str]] = None class XML(BaseModelWithConfig): @@ -137,34 +130,34 @@ class Schema(BaseModelWithConfig): dynamicAnchor: Optional[str] = Field(default=None, alias="$dynamicAnchor") ref: Optional[str] = Field(default=None, alias="$ref") dynamicRef: Optional[str] = Field(default=None, alias="$dynamicRef") - defs: Optional[Dict[str, "SchemaOrBool"]] = Field(default=None, alias="$defs") + defs: Optional[dict[str, "SchemaOrBool"]] = Field(default=None, alias="$defs") comment: Optional[str] = Field(default=None, alias="$comment") # Ref: JSON Schema 2020-12: https://json-schema.org/draft/2020-12/json-schema-core.html#name-a-vocabulary-for-applying-s # A Vocabulary for Applying Subschemas - allOf: Optional[List["SchemaOrBool"]] = None - anyOf: Optional[List["SchemaOrBool"]] = None - oneOf: Optional[List["SchemaOrBool"]] = None + allOf: Optional[list["SchemaOrBool"]] = None + anyOf: Optional[list["SchemaOrBool"]] = None + oneOf: Optional[list["SchemaOrBool"]] = None not_: Optional["SchemaOrBool"] = Field(default=None, alias="not") if_: Optional["SchemaOrBool"] = Field(default=None, alias="if") then: Optional["SchemaOrBool"] = None else_: Optional["SchemaOrBool"] = Field(default=None, alias="else") - dependentSchemas: Optional[Dict[str, "SchemaOrBool"]] = None - prefixItems: Optional[List["SchemaOrBool"]] = None + dependentSchemas: Optional[dict[str, "SchemaOrBool"]] = None + prefixItems: Optional[list["SchemaOrBool"]] = None # TODO: uncomment and remove below when deprecating Pydantic v1 # It generates a list of schemas for tuples, before prefixItems was available # items: Optional["SchemaOrBool"] = None - items: Optional[Union["SchemaOrBool", List["SchemaOrBool"]]] = None + items: Optional[Union["SchemaOrBool", list["SchemaOrBool"]]] = None contains: Optional["SchemaOrBool"] = None - properties: Optional[Dict[str, "SchemaOrBool"]] = None - patternProperties: Optional[Dict[str, "SchemaOrBool"]] = None + properties: Optional[dict[str, "SchemaOrBool"]] = None + patternProperties: Optional[dict[str, "SchemaOrBool"]] = None additionalProperties: Optional["SchemaOrBool"] = None propertyNames: Optional["SchemaOrBool"] = None unevaluatedItems: Optional["SchemaOrBool"] = None unevaluatedProperties: Optional["SchemaOrBool"] = None # Ref: JSON Schema Validation 2020-12: https://json-schema.org/draft/2020-12/json-schema-validation.html#name-a-vocabulary-for-structural # A Vocabulary for Structural Validation - type: Optional[Union[SchemaType, List[SchemaType]]] = None - enum: Optional[List[Any]] = None + type: Optional[Union[SchemaType, list[SchemaType]]] = None + enum: Optional[list[Any]] = None const: Optional[Any] = None multipleOf: Optional[float] = Field(default=None, gt=0) maximum: Optional[float] = None @@ -181,8 +174,8 @@ class Schema(BaseModelWithConfig): minContains: Optional[int] = Field(default=None, ge=0) maxProperties: Optional[int] = Field(default=None, ge=0) minProperties: Optional[int] = Field(default=None, ge=0) - required: Optional[List[str]] = None - dependentRequired: Optional[Dict[str, Set[str]]] = None + required: Optional[list[str]] = None + dependentRequired: Optional[dict[str, set[str]]] = None # Ref: JSON Schema Validation 2020-12: https://json-schema.org/draft/2020-12/json-schema-validation.html#name-vocabularies-for-semantic-c # Vocabularies for Semantic Content With "format" format: Optional[str] = None @@ -199,7 +192,7 @@ class Schema(BaseModelWithConfig): deprecated: Optional[bool] = None readOnly: Optional[bool] = None writeOnly: Optional[bool] = None - examples: Optional[List[Any]] = None + examples: Optional[list[Any]] = None # Ref: OpenAPI 3.1.0: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#schema-object # Schema Object discriminator: Optional[Discriminator] = None @@ -225,13 +218,7 @@ class Example(TypedDict, total=False): value: Optional[Any] externalValue: Optional[AnyUrl] - if PYDANTIC_V2: # type: ignore [misc] - __pydantic_config__ = {"extra": "allow"} - - else: - - class Config: - extra = "allow" + __pydantic_config__ = {"extra": "allow"} # type: ignore[misc] class ParameterInType(Enum): @@ -243,7 +230,7 @@ class ParameterInType(Enum): class Encoding(BaseModelWithConfig): contentType: Optional[str] = None - headers: Optional[Dict[str, Union["Header", Reference]]] = None + headers: Optional[dict[str, Union["Header", Reference]]] = None style: Optional[str] = None explode: Optional[bool] = None allowReserved: Optional[bool] = None @@ -252,8 +239,8 @@ class Encoding(BaseModelWithConfig): class MediaType(BaseModelWithConfig): schema_: Optional[Union[Schema, Reference]] = Field(default=None, alias="schema") example: Optional[Any] = None - examples: Optional[Dict[str, Union[Example, Reference]]] = None - encoding: Optional[Dict[str, Encoding]] = None + examples: Optional[dict[str, Union[Example, Reference]]] = None + encoding: Optional[dict[str, Encoding]] = None class ParameterBase(BaseModelWithConfig): @@ -266,9 +253,9 @@ class ParameterBase(BaseModelWithConfig): allowReserved: Optional[bool] = None schema_: Optional[Union[Schema, Reference]] = Field(default=None, alias="schema") example: Optional[Any] = None - examples: Optional[Dict[str, Union[Example, Reference]]] = None + examples: Optional[dict[str, Union[Example, Reference]]] = None # Serialization rules for more complex scenarios - content: Optional[Dict[str, MediaType]] = None + content: Optional[dict[str, MediaType]] = None class Parameter(ParameterBase): @@ -282,14 +269,14 @@ class Header(ParameterBase): class RequestBody(BaseModelWithConfig): description: Optional[str] = None - content: Dict[str, MediaType] + content: dict[str, MediaType] required: Optional[bool] = None class Link(BaseModelWithConfig): operationRef: Optional[str] = None operationId: Optional[str] = None - parameters: Optional[Dict[str, Union[Any, str]]] = None + parameters: Optional[dict[str, Union[Any, str]]] = None requestBody: Optional[Union[Any, str]] = None description: Optional[str] = None server: Optional[Server] = None @@ -297,25 +284,25 @@ class Link(BaseModelWithConfig): class Response(BaseModelWithConfig): description: str - headers: Optional[Dict[str, Union[Header, Reference]]] = None - content: Optional[Dict[str, MediaType]] = None - links: Optional[Dict[str, Union[Link, Reference]]] = None + headers: Optional[dict[str, Union[Header, Reference]]] = None + content: Optional[dict[str, MediaType]] = None + links: Optional[dict[str, Union[Link, Reference]]] = None class Operation(BaseModelWithConfig): - tags: Optional[List[str]] = None + tags: Optional[list[str]] = None summary: Optional[str] = None description: Optional[str] = None externalDocs: Optional[ExternalDocumentation] = None operationId: Optional[str] = None - parameters: Optional[List[Union[Parameter, Reference]]] = None + parameters: Optional[list[Union[Parameter, Reference]]] = None requestBody: Optional[Union[RequestBody, Reference]] = None # Using Any for Specification Extensions - responses: Optional[Dict[str, Union[Response, Any]]] = None - callbacks: Optional[Dict[str, Union[Dict[str, "PathItem"], Reference]]] = None + responses: Optional[dict[str, Union[Response, Any]]] = None + callbacks: Optional[dict[str, Union[dict[str, "PathItem"], Reference]]] = None deprecated: Optional[bool] = None - security: Optional[List[Dict[str, List[str]]]] = None - servers: Optional[List[Server]] = None + security: Optional[list[dict[str, list[str]]]] = None + servers: Optional[list[Server]] = None class PathItem(BaseModelWithConfig): @@ -330,8 +317,8 @@ class PathItem(BaseModelWithConfig): head: Optional[Operation] = None patch: Optional[Operation] = None trace: Optional[Operation] = None - servers: Optional[List[Server]] = None - parameters: Optional[List[Union[Parameter, Reference]]] = None + servers: Optional[list[Server]] = None + parameters: Optional[list[Union[Parameter, Reference]]] = None class SecuritySchemeType(Enum): @@ -370,7 +357,7 @@ class HTTPBearer(HTTPBase): class OAuthFlow(BaseModelWithConfig): refreshUrl: Optional[str] = None - scopes: Dict[str, str] = {} + scopes: dict[str, str] = {} class OAuthFlowImplicit(OAuthFlow): @@ -413,17 +400,17 @@ SecurityScheme = Union[APIKey, HTTPBase, OAuth2, OpenIdConnect, HTTPBearer] class Components(BaseModelWithConfig): - schemas: Optional[Dict[str, Union[Schema, Reference]]] = None - responses: Optional[Dict[str, Union[Response, Reference]]] = None - parameters: Optional[Dict[str, Union[Parameter, Reference]]] = None - examples: Optional[Dict[str, Union[Example, Reference]]] = None - requestBodies: Optional[Dict[str, Union[RequestBody, Reference]]] = None - headers: Optional[Dict[str, Union[Header, Reference]]] = None - securitySchemes: Optional[Dict[str, Union[SecurityScheme, Reference]]] = None - links: Optional[Dict[str, Union[Link, Reference]]] = None + schemas: Optional[dict[str, Union[Schema, Reference]]] = None + responses: Optional[dict[str, Union[Response, Reference]]] = None + parameters: Optional[dict[str, Union[Parameter, Reference]]] = None + examples: Optional[dict[str, Union[Example, Reference]]] = None + requestBodies: Optional[dict[str, Union[RequestBody, Reference]]] = None + headers: Optional[dict[str, Union[Header, Reference]]] = None + securitySchemes: Optional[dict[str, Union[SecurityScheme, Reference]]] = None + links: Optional[dict[str, Union[Link, Reference]]] = None # Using Any for Specification Extensions - callbacks: Optional[Dict[str, Union[Dict[str, PathItem], Reference, Any]]] = None - pathItems: Optional[Dict[str, Union[PathItem, Reference]]] = None + callbacks: Optional[dict[str, Union[dict[str, PathItem], Reference, Any]]] = None + pathItems: Optional[dict[str, Union[PathItem, Reference]]] = None class Tag(BaseModelWithConfig): @@ -436,16 +423,16 @@ class OpenAPI(BaseModelWithConfig): openapi: str info: Info jsonSchemaDialect: Optional[str] = None - servers: Optional[List[Server]] = None + servers: Optional[list[Server]] = None # Using Any for Specification Extensions - paths: Optional[Dict[str, Union[PathItem, Any]]] = None - webhooks: Optional[Dict[str, Union[PathItem, Reference]]] = None + paths: Optional[dict[str, Union[PathItem, Any]]] = None + webhooks: Optional[dict[str, Union[PathItem, Reference]]] = None components: Optional[Components] = None - security: Optional[List[Dict[str, List[str]]]] = None - tags: Optional[List[Tag]] = None + security: Optional[list[dict[str, list[str]]]] = None + tags: Optional[list[Tag]] = None externalDocs: Optional[ExternalDocumentation] = None -_model_rebuild(Schema) -_model_rebuild(Operation) -_model_rebuild(Encoding) +Schema.model_rebuild() +Operation.model_rebuild() +Encoding.model_rebuild() diff --git a/fastapi/openapi/utils.py b/fastapi/openapi/utils.py index dbc93d2892..75ff261025 100644 --- a/fastapi/openapi/utils.py +++ b/fastapi/openapi/utils.py @@ -1,11 +1,11 @@ import http.client import inspect import warnings -from typing import Any, Dict, List, Optional, Sequence, Set, Tuple, Type, Union, cast +from collections.abc import Sequence +from typing import Any, Optional, Union, cast from fastapi import routing from fastapi._compat import ( - JsonSchemaValue, ModelField, Undefined, get_compat_model_name_map, @@ -19,8 +19,10 @@ from fastapi.dependencies.utils import ( _get_flat_fields_from_params, get_flat_dependant, get_flat_params, + get_validation_alias, ) from fastapi.encoders import jsonable_encoder +from fastapi.exceptions import FastAPIDeprecationWarning from fastapi.openapi.constants import METHODS_WITH_BODY, REF_PREFIX from fastapi.openapi.models import OpenAPI from fastapi.params import Body, ParamTypes @@ -36,8 +38,6 @@ from starlette.responses import JSONResponse from starlette.routing import BaseRoute from typing_extensions import Literal -from .._compat import _is_model_field - validation_error_definition = { "title": "ValidationError", "type": "object", @@ -65,7 +65,7 @@ validation_error_response_definition = { }, } -status_code_ranges: Dict[str, str] = { +status_code_ranges: dict[str, str] = { "1XX": "Information", "2XX": "Success", "3XX": "Redirection", @@ -77,18 +77,27 @@ status_code_ranges: Dict[str, str] = { def get_openapi_security_definitions( flat_dependant: Dependant, -) -> Tuple[Dict[str, Any], List[Dict[str, Any]]]: +) -> tuple[dict[str, Any], list[dict[str, Any]]]: security_definitions = {} - operation_security = [] - for security_requirement in flat_dependant.security_requirements: + # Use a dict to merge scopes for same security scheme + operation_security_dict: dict[str, list[str]] = {} + for security_dependency in flat_dependant._security_dependencies: security_definition = jsonable_encoder( - security_requirement.security_scheme.model, + security_dependency._security_scheme.model, by_alias=True, exclude_none=True, ) - security_name = security_requirement.security_scheme.scheme_name + security_name = security_dependency._security_scheme.scheme_name security_definitions[security_name] = security_definition - operation_security.append({security_name: security_requirement.scopes}) + # Merge scopes for the same security scheme + if security_name not in operation_security_dict: + operation_security_dict[security_name] = [] + for scope in security_dependency.oauth_scopes or []: + if scope not in operation_security_dict[security_name]: + operation_security_dict[security_name].append(scope) + operation_security = [ + {name: scopes} for name, scopes in operation_security_dict.items() + ] return security_definitions, operation_security @@ -96,11 +105,11 @@ def _get_openapi_operation_parameters( *, dependant: Dependant, model_name_map: ModelNameMap, - field_mapping: Dict[ - Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue + field_mapping: dict[ + tuple[ModelField, Literal["validation", "serialization"]], dict[str, Any] ], separate_input_output_schemas: bool = True, -) -> List[Dict[str, Any]]: +) -> list[dict[str, Any]]: parameters = [] flat_dependant = get_flat_dependant(dependant, skip_repeats=True) path_params = _get_flat_fields_from_params(flat_dependant.path_params) @@ -132,7 +141,7 @@ def _get_openapi_operation_parameters( field_mapping=field_mapping, separate_input_output_schemas=separate_input_output_schemas, ) - name = param.alias + name = get_validation_alias(param) convert_underscores = getattr( param.field_info, "convert_underscores", @@ -140,7 +149,7 @@ def _get_openapi_operation_parameters( ) if ( param_type == ParamTypes.header - and param.alias == param.name + and name == param.name and convert_underscores ): name = param.name.replace("_", "-") @@ -169,14 +178,14 @@ def get_openapi_operation_request_body( *, body_field: Optional[ModelField], model_name_map: ModelNameMap, - field_mapping: Dict[ - Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue + field_mapping: dict[ + tuple[ModelField, Literal["validation", "serialization"]], dict[str, Any] ], separate_input_output_schemas: bool = True, -) -> Optional[Dict[str, Any]]: +) -> Optional[dict[str, Any]]: if not body_field: return None - assert _is_model_field(body_field) + assert isinstance(body_field, ModelField) body_schema = get_schema_from_model_field( field=body_field, model_name_map=model_name_map, @@ -186,10 +195,10 @@ def get_openapi_operation_request_body( field_info = cast(Body, body_field.field_info) request_media_type = field_info.media_type required = body_field.required - request_body_oai: Dict[str, Any] = {} + request_body_oai: dict[str, Any] = {} if required: request_body_oai["required"] = required - request_media_content: Dict[str, Any] = {"schema": body_schema} + request_media_content: dict[str, Any] = {"schema": body_schema} if field_info.openapi_examples: request_media_content["examples"] = jsonable_encoder( field_info.openapi_examples @@ -204,9 +213,9 @@ def generate_operation_id( *, route: routing.APIRoute, method: str ) -> str: # pragma: nocover warnings.warn( - "fastapi.openapi.utils.generate_operation_id() was deprecated, " + message="fastapi.openapi.utils.generate_operation_id() was deprecated, " "it is not used internally, and will be removed soon", - DeprecationWarning, + category=FastAPIDeprecationWarning, stacklevel=2, ) if route.operation_id: @@ -222,9 +231,9 @@ def generate_operation_summary(*, route: routing.APIRoute, method: str) -> str: def get_openapi_operation_metadata( - *, route: routing.APIRoute, method: str, operation_ids: Set[str] -) -> Dict[str, Any]: - operation: Dict[str, Any] = {} + *, route: routing.APIRoute, method: str, operation_ids: set[str] +) -> dict[str, Any]: + operation: dict[str, Any] = {} if route.tags: operation["tags"] = route.tags operation["summary"] = generate_operation_summary(route=route, method=method) @@ -250,19 +259,19 @@ def get_openapi_operation_metadata( def get_openapi_path( *, route: routing.APIRoute, - operation_ids: Set[str], + operation_ids: set[str], model_name_map: ModelNameMap, - field_mapping: Dict[ - Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue + field_mapping: dict[ + tuple[ModelField, Literal["validation", "serialization"]], dict[str, Any] ], separate_input_output_schemas: bool = True, -) -> Tuple[Dict[str, Any], Dict[str, Any], Dict[str, Any]]: +) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any]]: path = {} - security_schemes: Dict[str, Any] = {} - definitions: Dict[str, Any] = {} + security_schemes: dict[str, Any] = {} + definitions: dict[str, Any] = {} assert route.methods is not None, "Methods must be a list" if isinstance(route.response_class, DefaultPlaceholder): - current_response_class: Type[Response] = route.response_class.value + current_response_class: type[Response] = route.response_class.value else: current_response_class = route.response_class assert current_response_class, "A response class is needed to generate OpenAPI" @@ -272,7 +281,7 @@ def get_openapi_path( operation = get_openapi_operation_metadata( route=route, method=method, operation_ids=operation_ids ) - parameters: List[Dict[str, Any]] = [] + parameters: list[dict[str, Any]] = [] flat_dependant = get_flat_dependant(route.dependant, skip_repeats=True) security_definitions, operation_security = get_openapi_security_definitions( flat_dependant=flat_dependant @@ -380,7 +389,7 @@ def get_openapi_path( "An additional response must be a dict" ) field = route.response_fields.get(additional_status_code) - additional_field_schema: Optional[Dict[str, Any]] = None + additional_field_schema: Optional[dict[str, Any]] = None if field: additional_field_schema = get_schema_from_model_field( field=field, @@ -435,17 +444,17 @@ def get_openapi_path( def get_fields_from_routes( routes: Sequence[BaseRoute], -) -> List[ModelField]: - body_fields_from_routes: List[ModelField] = [] - responses_from_routes: List[ModelField] = [] - request_fields_from_routes: List[ModelField] = [] - callback_flat_models: List[ModelField] = [] +) -> list[ModelField]: + body_fields_from_routes: list[ModelField] = [] + responses_from_routes: list[ModelField] = [] + request_fields_from_routes: list[ModelField] = [] + callback_flat_models: list[ModelField] = [] for route in routes: if getattr(route, "include_in_schema", None) and isinstance( route, routing.APIRoute ): if route.body_field: - assert _is_model_field(route.body_field), ( + assert isinstance(route.body_field, ModelField), ( "A request body must be a Pydantic Field" ) body_fields_from_routes.append(route.body_field) @@ -473,15 +482,15 @@ def get_openapi( description: Optional[str] = None, routes: Sequence[BaseRoute], webhooks: Optional[Sequence[BaseRoute]] = None, - tags: Optional[List[Dict[str, Any]]] = None, - servers: Optional[List[Dict[str, Union[str, Any]]]] = None, + tags: Optional[list[dict[str, Any]]] = None, + servers: Optional[list[dict[str, Union[str, Any]]]] = None, terms_of_service: Optional[str] = None, - contact: Optional[Dict[str, Union[str, Any]]] = None, - license_info: Optional[Dict[str, Union[str, Any]]] = None, + contact: Optional[dict[str, Union[str, Any]]] = None, + license_info: Optional[dict[str, Union[str, Any]]] = None, separate_input_output_schemas: bool = True, - external_docs: Optional[Dict[str, Any]] = None, -) -> Dict[str, Any]: - info: Dict[str, Any] = {"title": title, "version": version} + external_docs: Optional[dict[str, Any]] = None, +) -> dict[str, Any]: + info: dict[str, Any] = {"title": title, "version": version} if summary: info["summary"] = summary if description: @@ -492,13 +501,13 @@ def get_openapi( info["contact"] = contact if license_info: info["license"] = license_info - output: Dict[str, Any] = {"openapi": openapi_version, "info": info} + output: dict[str, Any] = {"openapi": openapi_version, "info": info} if servers: output["servers"] = servers - components: Dict[str, Dict[str, Any]] = {} - paths: Dict[str, Dict[str, Any]] = {} - webhook_paths: Dict[str, Dict[str, Any]] = {} - operation_ids: Set[str] = set() + components: dict[str, dict[str, Any]] = {} + paths: dict[str, dict[str, Any]] = {} + webhook_paths: dict[str, dict[str, Any]] = {} + operation_ids: set[str] = set() all_fields = get_fields_from_routes(list(routes or []) + list(webhooks or [])) model_name_map = get_compat_model_name_map(all_fields) field_mapping, definitions = get_definitions( diff --git a/fastapi/param_functions.py b/fastapi/param_functions.py index e32f755933..0834fd741a 100644 --- a/fastapi/param_functions.py +++ b/fastapi/param_functions.py @@ -1,10 +1,12 @@ -from typing import Any, Callable, Dict, List, Optional, Sequence, Union +from collections.abc import Sequence +from typing import Annotated, Any, Callable, Optional, Union from annotated_doc import Doc from fastapi import params from fastapi._compat import Undefined from fastapi.openapi.models import Example -from typing_extensions import Annotated, Literal, deprecated +from pydantic import AliasChoices, AliasPath +from typing_extensions import Literal, deprecated _Unset: Any = Undefined @@ -53,10 +55,8 @@ def Path( # noqa: N802 """ ), ] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None validation_alias: Annotated[ - Union[str, None], + Union[str, AliasPath, AliasChoices, None], Doc( """ 'Whitelist' validation step. The parameter field will be the single one @@ -209,7 +209,7 @@ def Path( # noqa: N802 ), ] = _Unset, examples: Annotated[ - Optional[List[Any]], + Optional[list[Any]], Doc( """ Example values for this field. @@ -224,7 +224,7 @@ def Path( # noqa: N802 ), ] = _Unset, openapi_examples: Annotated[ - Optional[Dict[str, Example]], + Optional[dict[str, Example]], Doc( """ OpenAPI-specific examples. @@ -262,7 +262,7 @@ def Path( # noqa: N802 ), ] = True, json_schema_extra: Annotated[ - Union[Dict[str, Any], None], + Union[dict[str, Any], None], Doc( """ Any additional JSON schema data. @@ -378,10 +378,8 @@ def Query( # noqa: N802 """ ), ] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None validation_alias: Annotated[ - Union[str, None], + Union[str, AliasPath, AliasChoices, None], Doc( """ 'Whitelist' validation step. The parameter field will be the single one @@ -534,7 +532,7 @@ def Query( # noqa: N802 ), ] = _Unset, examples: Annotated[ - Optional[List[Any]], + Optional[list[Any]], Doc( """ Example values for this field. @@ -549,7 +547,7 @@ def Query( # noqa: N802 ), ] = _Unset, openapi_examples: Annotated[ - Optional[Dict[str, Example]], + Optional[dict[str, Example]], Doc( """ OpenAPI-specific examples. @@ -587,7 +585,7 @@ def Query( # noqa: N802 ), ] = True, json_schema_extra: Annotated[ - Union[Dict[str, Any], None], + Union[dict[str, Any], None], Doc( """ Any additional JSON schema data. @@ -682,10 +680,8 @@ def Header( # noqa: N802 """ ), ] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None validation_alias: Annotated[ - Union[str, None], + Union[str, AliasPath, AliasChoices, None], Doc( """ 'Whitelist' validation step. The parameter field will be the single one @@ -849,7 +845,7 @@ def Header( # noqa: N802 ), ] = _Unset, examples: Annotated[ - Optional[List[Any]], + Optional[list[Any]], Doc( """ Example values for this field. @@ -864,7 +860,7 @@ def Header( # noqa: N802 ), ] = _Unset, openapi_examples: Annotated[ - Optional[Dict[str, Example]], + Optional[dict[str, Example]], Doc( """ OpenAPI-specific examples. @@ -902,7 +898,7 @@ def Header( # noqa: N802 ), ] = True, json_schema_extra: Annotated[ - Union[Dict[str, Any], None], + Union[dict[str, Any], None], Doc( """ Any additional JSON schema data. @@ -998,10 +994,8 @@ def Cookie( # noqa: N802 """ ), ] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None validation_alias: Annotated[ - Union[str, None], + Union[str, AliasPath, AliasChoices, None], Doc( """ 'Whitelist' validation step. The parameter field will be the single one @@ -1154,7 +1148,7 @@ def Cookie( # noqa: N802 ), ] = _Unset, examples: Annotated[ - Optional[List[Any]], + Optional[list[Any]], Doc( """ Example values for this field. @@ -1169,7 +1163,7 @@ def Cookie( # noqa: N802 ), ] = _Unset, openapi_examples: Annotated[ - Optional[Dict[str, Example]], + Optional[dict[str, Example]], Doc( """ OpenAPI-specific examples. @@ -1207,7 +1201,7 @@ def Cookie( # noqa: N802 ), ] = True, json_schema_extra: Annotated[ - Union[Dict[str, Any], None], + Union[dict[str, Any], None], Doc( """ Any additional JSON schema data. @@ -1325,10 +1319,8 @@ def Body( # noqa: N802 """ ), ] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None validation_alias: Annotated[ - Union[str, None], + Union[str, AliasPath, AliasChoices, None], Doc( """ 'Whitelist' validation step. The parameter field will be the single one @@ -1481,7 +1473,7 @@ def Body( # noqa: N802 ), ] = _Unset, examples: Annotated[ - Optional[List[Any]], + Optional[list[Any]], Doc( """ Example values for this field. @@ -1496,7 +1488,7 @@ def Body( # noqa: N802 ), ] = _Unset, openapi_examples: Annotated[ - Optional[Dict[str, Example]], + Optional[dict[str, Example]], Doc( """ OpenAPI-specific examples. @@ -1534,7 +1526,7 @@ def Body( # noqa: N802 ), ] = True, json_schema_extra: Annotated[ - Union[Dict[str, Any], None], + Union[dict[str, Any], None], Doc( """ Any additional JSON schema data. @@ -1640,10 +1632,8 @@ def Form( # noqa: N802 """ ), ] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None validation_alias: Annotated[ - Union[str, None], + Union[str, AliasPath, AliasChoices, None], Doc( """ 'Whitelist' validation step. The parameter field will be the single one @@ -1796,7 +1786,7 @@ def Form( # noqa: N802 ), ] = _Unset, examples: Annotated[ - Optional[List[Any]], + Optional[list[Any]], Doc( """ Example values for this field. @@ -1811,7 +1801,7 @@ def Form( # noqa: N802 ), ] = _Unset, openapi_examples: Annotated[ - Optional[Dict[str, Example]], + Optional[dict[str, Example]], Doc( """ OpenAPI-specific examples. @@ -1849,7 +1839,7 @@ def Form( # noqa: N802 ), ] = True, json_schema_extra: Annotated[ - Union[Dict[str, Any], None], + Union[dict[str, Any], None], Doc( """ Any additional JSON schema data. @@ -1954,10 +1944,8 @@ def File( # noqa: N802 """ ), ] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None validation_alias: Annotated[ - Union[str, None], + Union[str, AliasPath, AliasChoices, None], Doc( """ 'Whitelist' validation step. The parameter field will be the single one @@ -2110,7 +2098,7 @@ def File( # noqa: N802 ), ] = _Unset, examples: Annotated[ - Optional[List[Any]], + Optional[list[Any]], Doc( """ Example values for this field. @@ -2125,7 +2113,7 @@ def File( # noqa: N802 ), ] = _Unset, openapi_examples: Annotated[ - Optional[Dict[str, Example]], + Optional[dict[str, Example]], Doc( """ OpenAPI-specific examples. @@ -2163,7 +2151,7 @@ def File( # noqa: N802 ), ] = True, json_schema_extra: Annotated[ - Union[Dict[str, Any], None], + Union[dict[str, Any], None], Doc( """ Any additional JSON schema data. diff --git a/fastapi/params.py b/fastapi/params.py index 6a58d5808e..72e797f833 100644 --- a/fastapi/params.py +++ b/fastapi/params.py @@ -1,15 +1,16 @@ import warnings +from collections.abc import Sequence from dataclasses import dataclass from enum import Enum -from typing import Any, Callable, Dict, List, Optional, Sequence, Union +from typing import Annotated, Any, Callable, Optional, Union +from fastapi.exceptions import FastAPIDeprecationWarning from fastapi.openapi.models import Example +from pydantic import AliasChoices, AliasPath from pydantic.fields import FieldInfo -from typing_extensions import Annotated, Literal, deprecated +from typing_extensions import Literal, deprecated from ._compat import ( - PYDANTIC_V2, - PYDANTIC_VERSION_MINOR_TUPLE, Undefined, ) @@ -34,9 +35,7 @@ class Param(FieldInfo): # type: ignore[misc] annotation: Optional[Any] = None, alias: Optional[str] = None, alias_priority: Union[int, None] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None - validation_alias: Union[str, None] = None, + validation_alias: Union[str, AliasPath, AliasChoices, None] = None, serialization_alias: Union[str, None] = None, title: Optional[str] = None, description: Optional[str] = None, @@ -59,7 +58,7 @@ class Param(FieldInfo): # type: ignore[misc] allow_inf_nan: Union[bool, None] = _Unset, max_digits: Union[int, None] = _Unset, decimal_places: Union[int, None] = _Unset, - examples: Optional[List[Any]] = None, + examples: Optional[list[Any]] = None, example: Annotated[ Optional[Any], deprecated( @@ -67,16 +66,16 @@ class Param(FieldInfo): # type: ignore[misc] "although still supported. Use examples instead." ), ] = _Unset, - openapi_examples: Optional[Dict[str, Example]] = None, + openapi_examples: Optional[dict[str, Example]] = None, deprecated: Union[deprecated, str, bool, None] = None, include_in_schema: bool = True, - json_schema_extra: Union[Dict[str, Any], None] = None, + json_schema_extra: Union[dict[str, Any], None] = None, **extra: Any, ): if example is not _Unset: warnings.warn( "`example` has been deprecated, please use `examples` instead", - category=DeprecationWarning, + category=FastAPIDeprecationWarning, stacklevel=4, ) self.example = example @@ -106,29 +105,28 @@ class Param(FieldInfo): # type: ignore[misc] if regex is not None: warnings.warn( "`regex` has been deprecated, please use `pattern` instead", - category=DeprecationWarning, + category=FastAPIDeprecationWarning, stacklevel=4, ) current_json_schema_extra = json_schema_extra or extra - if PYDANTIC_VERSION_MINOR_TUPLE < (2, 7): - self.deprecated = deprecated - else: - kwargs["deprecated"] = deprecated - if PYDANTIC_V2: - kwargs.update( - { - "annotation": annotation, - "alias_priority": alias_priority, - "validation_alias": validation_alias, - "serialization_alias": serialization_alias, - "strict": strict, - "json_schema_extra": current_json_schema_extra, - } - ) - kwargs["pattern"] = pattern or regex - else: - kwargs["regex"] = pattern or regex - kwargs.update(**current_json_schema_extra) + kwargs["deprecated"] = deprecated + + if serialization_alias in (_Unset, None) and isinstance(alias, str): + serialization_alias = alias + if validation_alias in (_Unset, None): + validation_alias = alias + kwargs.update( + { + "annotation": annotation, + "alias_priority": alias_priority, + "validation_alias": validation_alias, + "serialization_alias": serialization_alias, + "strict": strict, + "json_schema_extra": current_json_schema_extra, + } + ) + kwargs["pattern"] = pattern or regex + use_kwargs = {k: v for k, v in kwargs.items() if v is not _Unset} super().__init__(**use_kwargs) @@ -148,9 +146,7 @@ class Path(Param): # type: ignore[misc] annotation: Optional[Any] = None, alias: Optional[str] = None, alias_priority: Union[int, None] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None - validation_alias: Union[str, None] = None, + validation_alias: Union[str, AliasPath, AliasChoices, None] = None, serialization_alias: Union[str, None] = None, title: Optional[str] = None, description: Optional[str] = None, @@ -173,7 +169,7 @@ class Path(Param): # type: ignore[misc] allow_inf_nan: Union[bool, None] = _Unset, max_digits: Union[int, None] = _Unset, decimal_places: Union[int, None] = _Unset, - examples: Optional[List[Any]] = None, + examples: Optional[list[Any]] = None, example: Annotated[ Optional[Any], deprecated( @@ -181,10 +177,10 @@ class Path(Param): # type: ignore[misc] "although still supported. Use examples instead." ), ] = _Unset, - openapi_examples: Optional[Dict[str, Example]] = None, + openapi_examples: Optional[dict[str, Example]] = None, deprecated: Union[deprecated, str, bool, None] = None, include_in_schema: bool = True, - json_schema_extra: Union[Dict[str, Any], None] = None, + json_schema_extra: Union[dict[str, Any], None] = None, **extra: Any, ): assert default is ..., "Path parameters cannot have a default value" @@ -234,9 +230,7 @@ class Query(Param): # type: ignore[misc] annotation: Optional[Any] = None, alias: Optional[str] = None, alias_priority: Union[int, None] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None - validation_alias: Union[str, None] = None, + validation_alias: Union[str, AliasPath, AliasChoices, None] = None, serialization_alias: Union[str, None] = None, title: Optional[str] = None, description: Optional[str] = None, @@ -259,7 +253,7 @@ class Query(Param): # type: ignore[misc] allow_inf_nan: Union[bool, None] = _Unset, max_digits: Union[int, None] = _Unset, decimal_places: Union[int, None] = _Unset, - examples: Optional[List[Any]] = None, + examples: Optional[list[Any]] = None, example: Annotated[ Optional[Any], deprecated( @@ -267,10 +261,10 @@ class Query(Param): # type: ignore[misc] "although still supported. Use examples instead." ), ] = _Unset, - openapi_examples: Optional[Dict[str, Example]] = None, + openapi_examples: Optional[dict[str, Example]] = None, deprecated: Union[deprecated, str, bool, None] = None, include_in_schema: bool = True, - json_schema_extra: Union[Dict[str, Any], None] = None, + json_schema_extra: Union[dict[str, Any], None] = None, **extra: Any, ): super().__init__( @@ -318,9 +312,7 @@ class Header(Param): # type: ignore[misc] annotation: Optional[Any] = None, alias: Optional[str] = None, alias_priority: Union[int, None] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None - validation_alias: Union[str, None] = None, + validation_alias: Union[str, AliasPath, AliasChoices, None] = None, serialization_alias: Union[str, None] = None, convert_underscores: bool = True, title: Optional[str] = None, @@ -344,7 +336,7 @@ class Header(Param): # type: ignore[misc] allow_inf_nan: Union[bool, None] = _Unset, max_digits: Union[int, None] = _Unset, decimal_places: Union[int, None] = _Unset, - examples: Optional[List[Any]] = None, + examples: Optional[list[Any]] = None, example: Annotated[ Optional[Any], deprecated( @@ -352,10 +344,10 @@ class Header(Param): # type: ignore[misc] "although still supported. Use examples instead." ), ] = _Unset, - openapi_examples: Optional[Dict[str, Example]] = None, + openapi_examples: Optional[dict[str, Example]] = None, deprecated: Union[deprecated, str, bool, None] = None, include_in_schema: bool = True, - json_schema_extra: Union[Dict[str, Any], None] = None, + json_schema_extra: Union[dict[str, Any], None] = None, **extra: Any, ): self.convert_underscores = convert_underscores @@ -404,9 +396,7 @@ class Cookie(Param): # type: ignore[misc] annotation: Optional[Any] = None, alias: Optional[str] = None, alias_priority: Union[int, None] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None - validation_alias: Union[str, None] = None, + validation_alias: Union[str, AliasPath, AliasChoices, None] = None, serialization_alias: Union[str, None] = None, title: Optional[str] = None, description: Optional[str] = None, @@ -429,7 +419,7 @@ class Cookie(Param): # type: ignore[misc] allow_inf_nan: Union[bool, None] = _Unset, max_digits: Union[int, None] = _Unset, decimal_places: Union[int, None] = _Unset, - examples: Optional[List[Any]] = None, + examples: Optional[list[Any]] = None, example: Annotated[ Optional[Any], deprecated( @@ -437,10 +427,10 @@ class Cookie(Param): # type: ignore[misc] "although still supported. Use examples instead." ), ] = _Unset, - openapi_examples: Optional[Dict[str, Example]] = None, + openapi_examples: Optional[dict[str, Example]] = None, deprecated: Union[deprecated, str, bool, None] = None, include_in_schema: bool = True, - json_schema_extra: Union[Dict[str, Any], None] = None, + json_schema_extra: Union[dict[str, Any], None] = None, **extra: Any, ): super().__init__( @@ -488,9 +478,7 @@ class Body(FieldInfo): # type: ignore[misc] media_type: str = "application/json", alias: Optional[str] = None, alias_priority: Union[int, None] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None - validation_alias: Union[str, None] = None, + validation_alias: Union[str, AliasPath, AliasChoices, None] = None, serialization_alias: Union[str, None] = None, title: Optional[str] = None, description: Optional[str] = None, @@ -513,7 +501,7 @@ class Body(FieldInfo): # type: ignore[misc] allow_inf_nan: Union[bool, None] = _Unset, max_digits: Union[int, None] = _Unset, decimal_places: Union[int, None] = _Unset, - examples: Optional[List[Any]] = None, + examples: Optional[list[Any]] = None, example: Annotated[ Optional[Any], deprecated( @@ -521,10 +509,10 @@ class Body(FieldInfo): # type: ignore[misc] "although still supported. Use examples instead." ), ] = _Unset, - openapi_examples: Optional[Dict[str, Example]] = None, + openapi_examples: Optional[dict[str, Example]] = None, deprecated: Union[deprecated, str, bool, None] = None, include_in_schema: bool = True, - json_schema_extra: Union[Dict[str, Any], None] = None, + json_schema_extra: Union[dict[str, Any], None] = None, **extra: Any, ): self.embed = embed @@ -532,7 +520,7 @@ class Body(FieldInfo): # type: ignore[misc] if example is not _Unset: warnings.warn( "`example` has been deprecated, please use `examples` instead", - category=DeprecationWarning, + category=FastAPIDeprecationWarning, stacklevel=4, ) self.example = example @@ -562,29 +550,26 @@ class Body(FieldInfo): # type: ignore[misc] if regex is not None: warnings.warn( "`regex` has been deprecated, please use `pattern` instead", - category=DeprecationWarning, + category=FastAPIDeprecationWarning, stacklevel=4, ) current_json_schema_extra = json_schema_extra or extra - if PYDANTIC_VERSION_MINOR_TUPLE < (2, 7): - self.deprecated = deprecated - else: - kwargs["deprecated"] = deprecated - if PYDANTIC_V2: - kwargs.update( - { - "annotation": annotation, - "alias_priority": alias_priority, - "validation_alias": validation_alias, - "serialization_alias": serialization_alias, - "strict": strict, - "json_schema_extra": current_json_schema_extra, - } - ) - kwargs["pattern"] = pattern or regex - else: - kwargs["regex"] = pattern or regex - kwargs.update(**current_json_schema_extra) + kwargs["deprecated"] = deprecated + if serialization_alias in (_Unset, None) and isinstance(alias, str): + serialization_alias = alias + if validation_alias in (_Unset, None): + validation_alias = alias + kwargs.update( + { + "annotation": annotation, + "alias_priority": alias_priority, + "validation_alias": validation_alias, + "serialization_alias": serialization_alias, + "strict": strict, + "json_schema_extra": current_json_schema_extra, + } + ) + kwargs["pattern"] = pattern or regex use_kwargs = {k: v for k, v in kwargs.items() if v is not _Unset} @@ -604,9 +589,7 @@ class Form(Body): # type: ignore[misc] media_type: str = "application/x-www-form-urlencoded", alias: Optional[str] = None, alias_priority: Union[int, None] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None - validation_alias: Union[str, None] = None, + validation_alias: Union[str, AliasPath, AliasChoices, None] = None, serialization_alias: Union[str, None] = None, title: Optional[str] = None, description: Optional[str] = None, @@ -629,7 +612,7 @@ class Form(Body): # type: ignore[misc] allow_inf_nan: Union[bool, None] = _Unset, max_digits: Union[int, None] = _Unset, decimal_places: Union[int, None] = _Unset, - examples: Optional[List[Any]] = None, + examples: Optional[list[Any]] = None, example: Annotated[ Optional[Any], deprecated( @@ -637,10 +620,10 @@ class Form(Body): # type: ignore[misc] "although still supported. Use examples instead." ), ] = _Unset, - openapi_examples: Optional[Dict[str, Example]] = None, + openapi_examples: Optional[dict[str, Example]] = None, deprecated: Union[deprecated, str, bool, None] = None, include_in_schema: bool = True, - json_schema_extra: Union[Dict[str, Any], None] = None, + json_schema_extra: Union[dict[str, Any], None] = None, **extra: Any, ): super().__init__( @@ -688,9 +671,7 @@ class File(Form): # type: ignore[misc] media_type: str = "multipart/form-data", alias: Optional[str] = None, alias_priority: Union[int, None] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None - validation_alias: Union[str, None] = None, + validation_alias: Union[str, AliasPath, AliasChoices, None] = None, serialization_alias: Union[str, None] = None, title: Optional[str] = None, description: Optional[str] = None, @@ -713,7 +694,7 @@ class File(Form): # type: ignore[misc] allow_inf_nan: Union[bool, None] = _Unset, max_digits: Union[int, None] = _Unset, decimal_places: Union[int, None] = _Unset, - examples: Optional[List[Any]] = None, + examples: Optional[list[Any]] = None, example: Annotated[ Optional[Any], deprecated( @@ -721,10 +702,10 @@ class File(Form): # type: ignore[misc] "although still supported. Use examples instead." ), ] = _Unset, - openapi_examples: Optional[Dict[str, Example]] = None, + openapi_examples: Optional[dict[str, Example]] = None, deprecated: Union[deprecated, str, bool, None] = None, include_in_schema: bool = True, - json_schema_extra: Union[Dict[str, Any], None] = None, + json_schema_extra: Union[dict[str, Any], None] = None, **extra: Any, ): super().__init__( @@ -762,13 +743,13 @@ class File(Form): # type: ignore[misc] ) -@dataclass +@dataclass(frozen=True) class Depends: dependency: Optional[Callable[..., Any]] = None use_cache: bool = True scope: Union[Literal["function", "request"], None] = None -@dataclass +@dataclass(frozen=True) class Security(Depends): scopes: Optional[Sequence[str]] = None diff --git a/fastapi/routing.py b/fastapi/routing.py index 59e86b7397..02e450d492 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -1,39 +1,33 @@ -import dataclasses import email.message import functools import inspect import json -import sys +from collections.abc import ( + AsyncIterator, + Awaitable, + Collection, + Coroutine, + Mapping, + Sequence, +) from contextlib import AsyncExitStack, asynccontextmanager from enum import Enum, IntEnum from typing import ( + Annotated, Any, - AsyncIterator, - Awaitable, Callable, - Collection, - Coroutine, - Dict, - List, - Mapping, Optional, - Sequence, - Set, - Tuple, - Type, Union, ) import anyio from annotated_doc import Doc from anyio import CapacityLimiter -from fastapi import params, temp_pydantic_v1_params +from fastapi import params from fastapi._compat import ( ModelField, Undefined, - _get_model_config, - _model_dump, - _normalize_errors, + annotation_is_pydantic_v1, lenient_issubclass, ) from fastapi.datastructures import Default, DefaultPlaceholder @@ -49,7 +43,9 @@ from fastapi.dependencies.utils import ( ) from fastapi.encoders import jsonable_encoder from fastapi.exceptions import ( + EndpointContext, FastAPIError, + PydanticV1NotSupportedError, RequestValidationError, ResponseValidationError, WebSocketRequestValidationError, @@ -62,7 +58,6 @@ from fastapi.utils import ( get_value_or_default, is_body_allowed_for_status_code, ) -from pydantic import BaseModel from starlette import routing from starlette._exception_handler import wrap_app_handling_exceptions from starlette._utils import is_async_callable @@ -79,12 +74,7 @@ from starlette.routing import ( from starlette.routing import Mount as Mount # noqa from starlette.types import AppType, ASGIApp, Lifespan, Receive, Scope, Send from starlette.websockets import WebSocket -from typing_extensions import Annotated, deprecated - -if sys.version_info >= (3, 13): # pragma: no cover - from inspect import iscoroutinefunction -else: # pragma: no cover - from asyncio import iscoroutinefunction +from typing_extensions import deprecated # Copy of starlette.routing.request_response modified to include the @@ -155,54 +145,6 @@ def websocket_session( return app -def _prepare_response_content( - res: Any, - *, - exclude_unset: bool, - exclude_defaults: bool = False, - exclude_none: bool = False, -) -> Any: - if isinstance(res, BaseModel): - read_with_orm_mode = getattr(_get_model_config(res), "read_with_orm_mode", None) - if read_with_orm_mode: - # Let from_orm extract the data from this model instead of converting - # it now to a dict. - # Otherwise, there's no way to extract lazy data that requires attribute - # access instead of dict iteration, e.g. lazy relationships. - return res - return _model_dump( - res, - by_alias=True, - exclude_unset=exclude_unset, - exclude_defaults=exclude_defaults, - exclude_none=exclude_none, - ) - elif isinstance(res, list): - return [ - _prepare_response_content( - item, - exclude_unset=exclude_unset, - exclude_defaults=exclude_defaults, - exclude_none=exclude_none, - ) - for item in res - ] - elif isinstance(res, dict): - return { - k: _prepare_response_content( - v, - exclude_unset=exclude_unset, - exclude_defaults=exclude_defaults, - exclude_none=exclude_none, - ) - for k, v in res.items() - } - elif dataclasses.is_dataclass(res): - assert not isinstance(res, type) - return dataclasses.asdict(res) - return res - - def _merge_lifespan_context( original_context: Lifespan[Any], nested_context: Lifespan[Any] ) -> Lifespan[Any]: @@ -220,6 +162,33 @@ def _merge_lifespan_context( return merged_lifespan # type: ignore[return-value] +# Cache for endpoint context to avoid re-extracting on every request +_endpoint_context_cache: dict[int, EndpointContext] = {} + + +def _extract_endpoint_context(func: Any) -> EndpointContext: + """Extract endpoint context with caching to avoid repeated file I/O.""" + func_id = id(func) + + if func_id in _endpoint_context_cache: + return _endpoint_context_cache[func_id] + + try: + ctx: EndpointContext = {} + + if (source_file := inspect.getsourcefile(func)) is not None: + ctx["file"] = source_file + if (line_number := inspect.getsourcelines(func)[1]) is not None: + ctx["line"] = line_number + if (func_name := getattr(func, "__name__", None)) is not None: + ctx["function"] = func_name + except Exception: + ctx = EndpointContext() + + _endpoint_context_cache[func_id] = ctx + return ctx + + async def serialize_response( *, field: Optional[ModelField] = None, @@ -231,17 +200,10 @@ async def serialize_response( exclude_defaults: bool = False, exclude_none: bool = False, is_coroutine: bool = True, + endpoint_ctx: Optional[EndpointContext] = None, ) -> Any: if field: errors = [] - if not hasattr(field, "serialize"): - # pydantic v1 - response_content = _prepare_response_content( - response_content, - exclude_unset=exclude_unset, - exclude_defaults=exclude_defaults, - exclude_none=exclude_none, - ) if is_coroutine: value, errors_ = field.validate(response_content, {}, loc=("response",)) else: @@ -253,25 +215,15 @@ async def serialize_response( ) if isinstance(errors_, list): errors.extend(errors_) - elif errors_: - errors.append(errors_) if errors: + ctx = endpoint_ctx or EndpointContext() raise ResponseValidationError( - errors=_normalize_errors(errors), body=response_content + errors=errors, + body=response_content, + endpoint_ctx=ctx, ) - if hasattr(field, "serialize"): - return field.serialize( - value, - include=include, - exclude=exclude, - by_alias=by_alias, - exclude_unset=exclude_unset, - exclude_defaults=exclude_defaults, - exclude_none=exclude_none, - ) - - return jsonable_encoder( + return field.serialize( value, include=include, exclude=exclude, @@ -280,12 +232,13 @@ async def serialize_response( exclude_defaults=exclude_defaults, exclude_none=exclude_none, ) + else: return jsonable_encoder(response_content) async def run_endpoint_function( - *, dependant: Dependant, values: Dict[str, Any], is_coroutine: bool + *, dependant: Dependant, values: dict[str, Any], is_coroutine: bool ) -> Any: # Only called by get_request_handler. Has been split into its own function to # facilitate profiling endpoints, since inner functions are harder to profile. @@ -301,7 +254,7 @@ def get_request_handler( dependant: Dependant, body_field: Optional[ModelField] = None, status_code: Optional[int] = None, - response_class: Union[Type[Response], DefaultPlaceholder] = Default(JSONResponse), + response_class: Union[type[Response], DefaultPlaceholder] = Default(JSONResponse), response_field: Optional[ModelField] = None, response_model_include: Optional[IncEx] = None, response_model_exclude: Optional[IncEx] = None, @@ -313,12 +266,10 @@ def get_request_handler( embed_body_fields: bool = False, ) -> Callable[[Request], Coroutine[Any, Any, Response]]: assert dependant.call is not None, "dependant.call must be a function" - is_coroutine = iscoroutinefunction(dependant.call) - is_body_form = body_field and isinstance( - body_field.field_info, (params.Form, temp_pydantic_v1_params.Form) - ) + is_coroutine = dependant.is_coroutine_callable + is_body_form = body_field and isinstance(body_field.field_info, params.Form) if isinstance(response_class, DefaultPlaceholder): - actual_response_class: Type[Response] = response_class.value + actual_response_class: type[Response] = response_class.value else: actual_response_class = response_class @@ -329,6 +280,18 @@ def get_request_handler( "fastapi_middleware_astack not found in request scope" ) + # Extract endpoint context for error messages + endpoint_ctx = ( + _extract_endpoint_context(dependant.call) + if dependant.call + else EndpointContext() + ) + + if dependant.path: + # For mounted sub-apps, include the mount path prefix + mount_path = request.scope.get("root_path", "").rstrip("/") + endpoint_ctx["path"] = f"{request.method} {mount_path}{dependant.path}" + # Read body and auto-close files try: body: Any = None @@ -366,6 +329,7 @@ def get_request_handler( } ], body=e.doc, + endpoint_ctx=endpoint_ctx, ) raise validation_error from e except HTTPException: @@ -378,7 +342,7 @@ def get_request_handler( raise http_error from e # Solve dependencies and run path operation function, auto-closing dependencies - errors: List[Any] = [] + errors: list[Any] = [] async_exit_stack = request.scope.get("fastapi_inner_astack") assert isinstance(async_exit_stack, AsyncExitStack), ( "fastapi_inner_astack not found in request scope" @@ -403,7 +367,7 @@ def get_request_handler( raw_response.background = solved_result.background_tasks response = raw_response else: - response_args: Dict[str, Any] = { + response_args: dict[str, Any] = { "background": solved_result.background_tasks } # If status_code was set, use it, otherwise use the default from the @@ -425,6 +389,7 @@ def get_request_handler( exclude_defaults=response_model_exclude_defaults, exclude_none=response_model_exclude_none, is_coroutine=is_coroutine, + endpoint_ctx=endpoint_ctx, ) response = actual_response_class(content, **response_args) if not is_body_allowed_for_status_code(response.status_code): @@ -432,7 +397,7 @@ def get_request_handler( response.headers.raw.extend(solved_result.response.headers.raw) if errors: validation_error = RequestValidationError( - _normalize_errors(errors), body=body + errors, body=body, endpoint_ctx=endpoint_ctx ) raise validation_error @@ -449,6 +414,15 @@ def get_websocket_app( embed_body_fields: bool = False, ) -> Callable[[WebSocket], Coroutine[Any, Any, Any]]: async def app(websocket: WebSocket) -> None: + endpoint_ctx = ( + _extract_endpoint_context(dependant.call) + if dependant.call + else EndpointContext() + ) + if dependant.path: + # For mounted sub-apps, include the mount path prefix + mount_path = websocket.scope.get("root_path", "").rstrip("/") + endpoint_ctx["path"] = f"WS {mount_path}{dependant.path}" async_exit_stack = websocket.scope.get("fastapi_inner_astack") assert isinstance(async_exit_stack, AsyncExitStack), ( "fastapi_inner_astack not found in request scope" @@ -462,7 +436,8 @@ def get_websocket_app( ) if solved_result.errors: raise WebSocketRequestValidationError( - _normalize_errors(solved_result.errors) + solved_result.errors, + endpoint_ctx=endpoint_ctx, ) assert dependant.call is not None, "dependant.call must be a function" await dependant.call(**solved_result.values) @@ -505,7 +480,7 @@ class APIWebSocketRoute(routing.WebSocketRoute): ) ) - def matches(self, scope: Scope) -> Tuple[Match, Scope]: + def matches(self, scope: Scope) -> tuple[Match, Scope]: match, child_scope = super().matches(scope) if match != Match.NONE: child_scope["route"] = self @@ -520,15 +495,15 @@ class APIRoute(routing.Route): *, response_model: Any = Default(None), status_code: Optional[int] = None, - tags: Optional[List[Union[str, Enum]]] = None, + tags: Optional[list[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, summary: Optional[str] = None, description: Optional[str] = None, response_description: str = "Successful Response", - responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, + responses: Optional[dict[Union[int, str], dict[str, Any]]] = None, deprecated: Optional[bool] = None, name: Optional[str] = None, - methods: Optional[Union[Set[str], List[str]]] = None, + methods: Optional[Union[set[str], list[str]]] = None, operation_id: Optional[str] = None, response_model_include: Optional[IncEx] = None, response_model_exclude: Optional[IncEx] = None, @@ -537,12 +512,12 @@ class APIRoute(routing.Route): response_model_exclude_defaults: bool = False, response_model_exclude_none: bool = False, include_in_schema: bool = True, - response_class: Union[Type[Response], DefaultPlaceholder] = Default( + response_class: Union[type[Response], DefaultPlaceholder] = Default( JSONResponse ), dependency_overrides_provider: Optional[Any] = None, - callbacks: Optional[List[BaseRoute]] = None, - openapi_extra: Optional[Dict[str, Any]] = None, + callbacks: Optional[list[BaseRoute]] = None, + openapi_extra: Optional[dict[str, Any]] = None, generate_unique_id_function: Union[ Callable[["APIRoute"], str], DefaultPlaceholder ] = Default(generate_unique_id), @@ -578,7 +553,7 @@ class APIRoute(routing.Route): self.path_regex, self.path_format, self.param_convertors = compile_path(path) if methods is None: methods = ["GET"] - self.methods: Set[str] = {method.upper() for method in methods} + self.methods: set[str] = {method.upper() for method in methods} if isinstance(generate_unique_id_function, DefaultPlaceholder): current_generate_unique_id: Callable[[APIRoute], str] = ( generate_unique_id_function.value @@ -595,6 +570,11 @@ class APIRoute(routing.Route): f"Status code {status_code} must not have a response body" ) response_name = "Response_" + self.unique_id + if annotation_is_pydantic_v1(self.response_model): + raise PydanticV1NotSupportedError( + "pydantic.v1 models are no longer supported by FastAPI." + f" Please update the response model {self.response_model!r}." + ) self.response_field = create_model_field( name=response_name, type_=self.response_model, @@ -628,12 +608,17 @@ class APIRoute(routing.Route): f"Status code {additional_status_code} must not have a response body" ) response_name = f"Response_{additional_status_code}_{self.unique_id}" + if annotation_is_pydantic_v1(model): + raise PydanticV1NotSupportedError( + "pydantic.v1 models are no longer supported by FastAPI." + f" In responses={{}}, please update {model}." + ) response_field = create_model_field( name=response_name, type_=model, mode="serialization" ) response_fields[additional_status_code] = response_field if response_fields: - self.response_fields: Dict[Union[int, str], ModelField] = response_fields + self.response_fields: dict[Union[int, str], ModelField] = response_fields else: self.response_fields = {} @@ -674,7 +659,7 @@ class APIRoute(routing.Route): embed_body_fields=self._embed_body_fields, ) - def matches(self, scope: Scope) -> Tuple[Match, Scope]: + def matches(self, scope: Scope) -> tuple[Match, Scope]: match, child_scope = super().matches(scope) if match != Match.NONE: child_scope["route"] = self @@ -713,7 +698,7 @@ class APIRouter(routing.Router): *, prefix: Annotated[str, Doc("An optional path prefix for the router.")] = "", tags: Annotated[ - Optional[List[Union[str, Enum]]], + Optional[list[Union[str, Enum]]], Doc( """ A list of tags to be applied to all the *path operations* in this @@ -739,7 +724,7 @@ class APIRouter(routing.Router): ), ] = None, default_response_class: Annotated[ - Type[Response], + type[Response], Doc( """ The default response class to be used. @@ -750,7 +735,7 @@ class APIRouter(routing.Router): ), ] = Default(JSONResponse), responses: Annotated[ - Optional[Dict[Union[int, str], Dict[str, Any]]], + Optional[dict[Union[int, str], dict[str, Any]]], Doc( """ Additional responses to be shown in OpenAPI. @@ -766,7 +751,7 @@ class APIRouter(routing.Router): ), ] = None, callbacks: Annotated[ - Optional[List[BaseRoute]], + Optional[list[BaseRoute]], Doc( """ OpenAPI callbacks that should apply to all *path operations* in this @@ -780,7 +765,7 @@ class APIRouter(routing.Router): ), ] = None, routes: Annotated[ - Optional[List[BaseRoute]], + Optional[list[BaseRoute]], Doc( """ **Note**: you probably shouldn't use this parameter, it is inherited @@ -831,7 +816,7 @@ class APIRouter(routing.Router): ), ] = None, route_class: Annotated[ - Type[APIRoute], + type[APIRoute], Doc( """ Custom route (*path operation*) class to be used by this router. @@ -937,7 +922,7 @@ class APIRouter(routing.Router): "A path prefix must not end with '/', as the routes will start with '/'" ) self.prefix = prefix - self.tags: List[Union[str, Enum]] = tags or [] + self.tags: list[Union[str, Enum]] = tags or [] self.dependencies = list(dependencies or []) self.deprecated = deprecated self.include_in_schema = include_in_schema @@ -974,14 +959,14 @@ class APIRouter(routing.Router): *, response_model: Any = Default(None), status_code: Optional[int] = None, - tags: Optional[List[Union[str, Enum]]] = None, + tags: Optional[list[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, summary: Optional[str] = None, description: Optional[str] = None, response_description: str = "Successful Response", - responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, + responses: Optional[dict[Union[int, str], dict[str, Any]]] = None, deprecated: Optional[bool] = None, - methods: Optional[Union[Set[str], List[str]]] = None, + methods: Optional[Union[set[str], list[str]]] = None, operation_id: Optional[str] = None, response_model_include: Optional[IncEx] = None, response_model_exclude: Optional[IncEx] = None, @@ -990,13 +975,13 @@ class APIRouter(routing.Router): response_model_exclude_defaults: bool = False, response_model_exclude_none: bool = False, include_in_schema: bool = True, - response_class: Union[Type[Response], DefaultPlaceholder] = Default( + response_class: Union[type[Response], DefaultPlaceholder] = Default( JSONResponse ), name: Optional[str] = None, - route_class_override: Optional[Type[APIRoute]] = None, - callbacks: Optional[List[BaseRoute]] = None, - openapi_extra: Optional[Dict[str, Any]] = None, + route_class_override: Optional[type[APIRoute]] = None, + callbacks: Optional[list[BaseRoute]] = None, + openapi_extra: Optional[dict[str, Any]] = None, generate_unique_id_function: Union[ Callable[[APIRoute], str], DefaultPlaceholder ] = Default(generate_unique_id), @@ -1055,14 +1040,14 @@ class APIRouter(routing.Router): *, response_model: Any = Default(None), status_code: Optional[int] = None, - tags: Optional[List[Union[str, Enum]]] = None, + tags: Optional[list[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, summary: Optional[str] = None, description: Optional[str] = None, response_description: str = "Successful Response", - responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, + responses: Optional[dict[Union[int, str], dict[str, Any]]] = None, deprecated: Optional[bool] = None, - methods: Optional[List[str]] = None, + methods: Optional[list[str]] = None, operation_id: Optional[str] = None, response_model_include: Optional[IncEx] = None, response_model_exclude: Optional[IncEx] = None, @@ -1071,10 +1056,10 @@ class APIRouter(routing.Router): response_model_exclude_defaults: bool = False, response_model_exclude_none: bool = False, include_in_schema: bool = True, - response_class: Type[Response] = Default(JSONResponse), + response_class: type[Response] = Default(JSONResponse), name: Optional[str] = None, - callbacks: Optional[List[BaseRoute]] = None, - openapi_extra: Optional[Dict[str, Any]] = None, + callbacks: Optional[list[BaseRoute]] = None, + openapi_extra: Optional[dict[str, Any]] = None, generate_unique_id_function: Callable[[APIRoute], str] = Default( generate_unique_id ), @@ -1214,7 +1199,7 @@ class APIRouter(routing.Router): *, prefix: Annotated[str, Doc("An optional path prefix for the router.")] = "", tags: Annotated[ - Optional[List[Union[str, Enum]]], + Optional[list[Union[str, Enum]]], Doc( """ A list of tags to be applied to all the *path operations* in this @@ -1240,7 +1225,7 @@ class APIRouter(routing.Router): ), ] = None, default_response_class: Annotated[ - Type[Response], + type[Response], Doc( """ The default response class to be used. @@ -1251,7 +1236,7 @@ class APIRouter(routing.Router): ), ] = Default(JSONResponse), responses: Annotated[ - Optional[Dict[Union[int, str], Dict[str, Any]]], + Optional[dict[Union[int, str], dict[str, Any]]], Doc( """ Additional responses to be shown in OpenAPI. @@ -1267,7 +1252,7 @@ class APIRouter(routing.Router): ), ] = None, callbacks: Annotated[ - Optional[List[BaseRoute]], + Optional[list[BaseRoute]], Doc( """ OpenAPI callbacks that should apply to all *path operations* in this @@ -1372,7 +1357,7 @@ class APIRouter(routing.Router): current_tags.extend(tags) if route.tags: current_tags.extend(route.tags) - current_dependencies: List[params.Depends] = [] + current_dependencies: list[params.Depends] = [] if dependencies: current_dependencies.extend(dependencies) if route.dependencies: @@ -1513,7 +1498,7 @@ class APIRouter(routing.Router): ), ] = None, tags: Annotated[ - Optional[List[Union[str, Enum]]], + Optional[list[Union[str, Enum]]], Doc( """ A list of tags to be applied to the *path operation*. @@ -1579,7 +1564,7 @@ class APIRouter(routing.Router): ), ] = "Successful Response", responses: Annotated[ - Optional[Dict[Union[int, str], Dict[str, Any]]], + Optional[dict[Union[int, str], dict[str, Any]]], Doc( """ Additional responses that could be returned by this *path operation*. @@ -1720,7 +1705,7 @@ class APIRouter(routing.Router): ), ] = True, response_class: Annotated[ - Type[Response], + type[Response], Doc( """ Response class to be used for this *path operation*. @@ -1741,7 +1726,7 @@ class APIRouter(routing.Router): ), ] = None, callbacks: Annotated[ - Optional[List[BaseRoute]], + Optional[list[BaseRoute]], Doc( """ List of *path operations* that will be used as OpenAPI callbacks. @@ -1757,7 +1742,7 @@ class APIRouter(routing.Router): ), ] = None, openapi_extra: Annotated[ - Optional[Dict[str, Any]], + Optional[dict[str, Any]], Doc( """ Extra metadata to be included in the OpenAPI schema for this *path @@ -1890,7 +1875,7 @@ class APIRouter(routing.Router): ), ] = None, tags: Annotated[ - Optional[List[Union[str, Enum]]], + Optional[list[Union[str, Enum]]], Doc( """ A list of tags to be applied to the *path operation*. @@ -1956,7 +1941,7 @@ class APIRouter(routing.Router): ), ] = "Successful Response", responses: Annotated[ - Optional[Dict[Union[int, str], Dict[str, Any]]], + Optional[dict[Union[int, str], dict[str, Any]]], Doc( """ Additional responses that could be returned by this *path operation*. @@ -2097,7 +2082,7 @@ class APIRouter(routing.Router): ), ] = True, response_class: Annotated[ - Type[Response], + type[Response], Doc( """ Response class to be used for this *path operation*. @@ -2118,7 +2103,7 @@ class APIRouter(routing.Router): ), ] = None, callbacks: Annotated[ - Optional[List[BaseRoute]], + Optional[list[BaseRoute]], Doc( """ List of *path operations* that will be used as OpenAPI callbacks. @@ -2134,7 +2119,7 @@ class APIRouter(routing.Router): ), ] = None, openapi_extra: Annotated[ - Optional[Dict[str, Any]], + Optional[dict[str, Any]], Doc( """ Extra metadata to be included in the OpenAPI schema for this *path @@ -2272,7 +2257,7 @@ class APIRouter(routing.Router): ), ] = None, tags: Annotated[ - Optional[List[Union[str, Enum]]], + Optional[list[Union[str, Enum]]], Doc( """ A list of tags to be applied to the *path operation*. @@ -2338,7 +2323,7 @@ class APIRouter(routing.Router): ), ] = "Successful Response", responses: Annotated[ - Optional[Dict[Union[int, str], Dict[str, Any]]], + Optional[dict[Union[int, str], dict[str, Any]]], Doc( """ Additional responses that could be returned by this *path operation*. @@ -2479,7 +2464,7 @@ class APIRouter(routing.Router): ), ] = True, response_class: Annotated[ - Type[Response], + type[Response], Doc( """ Response class to be used for this *path operation*. @@ -2500,7 +2485,7 @@ class APIRouter(routing.Router): ), ] = None, callbacks: Annotated[ - Optional[List[BaseRoute]], + Optional[list[BaseRoute]], Doc( """ List of *path operations* that will be used as OpenAPI callbacks. @@ -2516,7 +2501,7 @@ class APIRouter(routing.Router): ), ] = None, openapi_extra: Annotated[ - Optional[Dict[str, Any]], + Optional[dict[str, Any]], Doc( """ Extra metadata to be included in the OpenAPI schema for this *path @@ -2654,7 +2639,7 @@ class APIRouter(routing.Router): ), ] = None, tags: Annotated[ - Optional[List[Union[str, Enum]]], + Optional[list[Union[str, Enum]]], Doc( """ A list of tags to be applied to the *path operation*. @@ -2720,7 +2705,7 @@ class APIRouter(routing.Router): ), ] = "Successful Response", responses: Annotated[ - Optional[Dict[Union[int, str], Dict[str, Any]]], + Optional[dict[Union[int, str], dict[str, Any]]], Doc( """ Additional responses that could be returned by this *path operation*. @@ -2861,7 +2846,7 @@ class APIRouter(routing.Router): ), ] = True, response_class: Annotated[ - Type[Response], + type[Response], Doc( """ Response class to be used for this *path operation*. @@ -2882,7 +2867,7 @@ class APIRouter(routing.Router): ), ] = None, callbacks: Annotated[ - Optional[List[BaseRoute]], + Optional[list[BaseRoute]], Doc( """ List of *path operations* that will be used as OpenAPI callbacks. @@ -2898,7 +2883,7 @@ class APIRouter(routing.Router): ), ] = None, openapi_extra: Annotated[ - Optional[Dict[str, Any]], + Optional[dict[str, Any]], Doc( """ Extra metadata to be included in the OpenAPI schema for this *path @@ -3031,7 +3016,7 @@ class APIRouter(routing.Router): ), ] = None, tags: Annotated[ - Optional[List[Union[str, Enum]]], + Optional[list[Union[str, Enum]]], Doc( """ A list of tags to be applied to the *path operation*. @@ -3097,7 +3082,7 @@ class APIRouter(routing.Router): ), ] = "Successful Response", responses: Annotated[ - Optional[Dict[Union[int, str], Dict[str, Any]]], + Optional[dict[Union[int, str], dict[str, Any]]], Doc( """ Additional responses that could be returned by this *path operation*. @@ -3238,7 +3223,7 @@ class APIRouter(routing.Router): ), ] = True, response_class: Annotated[ - Type[Response], + type[Response], Doc( """ Response class to be used for this *path operation*. @@ -3259,7 +3244,7 @@ class APIRouter(routing.Router): ), ] = None, callbacks: Annotated[ - Optional[List[BaseRoute]], + Optional[list[BaseRoute]], Doc( """ List of *path operations* that will be used as OpenAPI callbacks. @@ -3275,7 +3260,7 @@ class APIRouter(routing.Router): ), ] = None, openapi_extra: Annotated[ - Optional[Dict[str, Any]], + Optional[dict[str, Any]], Doc( """ Extra metadata to be included in the OpenAPI schema for this *path @@ -3408,7 +3393,7 @@ class APIRouter(routing.Router): ), ] = None, tags: Annotated[ - Optional[List[Union[str, Enum]]], + Optional[list[Union[str, Enum]]], Doc( """ A list of tags to be applied to the *path operation*. @@ -3474,7 +3459,7 @@ class APIRouter(routing.Router): ), ] = "Successful Response", responses: Annotated[ - Optional[Dict[Union[int, str], Dict[str, Any]]], + Optional[dict[Union[int, str], dict[str, Any]]], Doc( """ Additional responses that could be returned by this *path operation*. @@ -3615,7 +3600,7 @@ class APIRouter(routing.Router): ), ] = True, response_class: Annotated[ - Type[Response], + type[Response], Doc( """ Response class to be used for this *path operation*. @@ -3636,7 +3621,7 @@ class APIRouter(routing.Router): ), ] = None, callbacks: Annotated[ - Optional[List[BaseRoute]], + Optional[list[BaseRoute]], Doc( """ List of *path operations* that will be used as OpenAPI callbacks. @@ -3652,7 +3637,7 @@ class APIRouter(routing.Router): ), ] = None, openapi_extra: Annotated[ - Optional[Dict[str, Any]], + Optional[dict[str, Any]], Doc( """ Extra metadata to be included in the OpenAPI schema for this *path @@ -3790,7 +3775,7 @@ class APIRouter(routing.Router): ), ] = None, tags: Annotated[ - Optional[List[Union[str, Enum]]], + Optional[list[Union[str, Enum]]], Doc( """ A list of tags to be applied to the *path operation*. @@ -3856,7 +3841,7 @@ class APIRouter(routing.Router): ), ] = "Successful Response", responses: Annotated[ - Optional[Dict[Union[int, str], Dict[str, Any]]], + Optional[dict[Union[int, str], dict[str, Any]]], Doc( """ Additional responses that could be returned by this *path operation*. @@ -3997,7 +3982,7 @@ class APIRouter(routing.Router): ), ] = True, response_class: Annotated[ - Type[Response], + type[Response], Doc( """ Response class to be used for this *path operation*. @@ -4018,7 +4003,7 @@ class APIRouter(routing.Router): ), ] = None, callbacks: Annotated[ - Optional[List[BaseRoute]], + Optional[list[BaseRoute]], Doc( """ List of *path operations* that will be used as OpenAPI callbacks. @@ -4034,7 +4019,7 @@ class APIRouter(routing.Router): ), ] = None, openapi_extra: Annotated[ - Optional[Dict[str, Any]], + Optional[dict[str, Any]], Doc( """ Extra metadata to be included in the OpenAPI schema for this *path @@ -4172,7 +4157,7 @@ class APIRouter(routing.Router): ), ] = None, tags: Annotated[ - Optional[List[Union[str, Enum]]], + Optional[list[Union[str, Enum]]], Doc( """ A list of tags to be applied to the *path operation*. @@ -4238,7 +4223,7 @@ class APIRouter(routing.Router): ), ] = "Successful Response", responses: Annotated[ - Optional[Dict[Union[int, str], Dict[str, Any]]], + Optional[dict[Union[int, str], dict[str, Any]]], Doc( """ Additional responses that could be returned by this *path operation*. @@ -4379,7 +4364,7 @@ class APIRouter(routing.Router): ), ] = True, response_class: Annotated[ - Type[Response], + type[Response], Doc( """ Response class to be used for this *path operation*. @@ -4400,7 +4385,7 @@ class APIRouter(routing.Router): ), ] = None, callbacks: Annotated[ - Optional[List[BaseRoute]], + Optional[list[BaseRoute]], Doc( """ List of *path operations* that will be used as OpenAPI callbacks. @@ -4416,7 +4401,7 @@ class APIRouter(routing.Router): ), ] = None, openapi_extra: Annotated[ - Optional[Dict[str, Any]], + Optional[dict[str, Any]], Doc( """ Extra metadata to be included in the OpenAPI schema for this *path diff --git a/fastapi/security/api_key.py b/fastapi/security/api_key.py index 496c815a77..18dfb8e618 100644 --- a/fastapi/security/api_key.py +++ b/fastapi/security/api_key.py @@ -1,22 +1,51 @@ -from typing import Optional +from typing import Annotated, Optional, Union from annotated_doc import Doc from fastapi.openapi.models import APIKey, APIKeyIn from fastapi.security.base import SecurityBase from starlette.exceptions import HTTPException from starlette.requests import Request -from starlette.status import HTTP_403_FORBIDDEN -from typing_extensions import Annotated +from starlette.status import HTTP_401_UNAUTHORIZED class APIKeyBase(SecurityBase): - @staticmethod - def check_api_key(api_key: Optional[str], auto_error: bool) -> Optional[str]: + def __init__( + self, + location: APIKeyIn, + name: str, + description: Union[str, None], + scheme_name: Union[str, None], + auto_error: bool, + ): + self.auto_error = auto_error + + self.model: APIKey = APIKey( + **{"in": location}, + name=name, + description=description, + ) + self.scheme_name = scheme_name or self.__class__.__name__ + + def make_not_authenticated_error(self) -> HTTPException: + """ + The WWW-Authenticate header is not standardized for API Key authentication but + the HTTP specification requires that an error of 401 "Unauthorized" must + include a WWW-Authenticate header. + + Ref: https://datatracker.ietf.org/doc/html/rfc9110#name-401-unauthorized + + For this, this method sends a custom challenge `APIKey`. + """ + return HTTPException( + status_code=HTTP_401_UNAUTHORIZED, + detail="Not authenticated", + headers={"WWW-Authenticate": "APIKey"}, + ) + + def check_api_key(self, api_key: Optional[str]) -> Optional[str]: if not api_key: - if auto_error: - raise HTTPException( - status_code=HTTP_403_FORBIDDEN, detail="Not authenticated" - ) + if self.auto_error: + raise self.make_not_authenticated_error() return None return api_key @@ -100,17 +129,17 @@ class APIKeyQuery(APIKeyBase): ), ] = True, ): - self.model: APIKey = APIKey( - **{"in": APIKeyIn.query}, + super().__init__( + location=APIKeyIn.query, name=name, + scheme_name=scheme_name, description=description, + auto_error=auto_error, ) - self.scheme_name = scheme_name or self.__class__.__name__ - self.auto_error = auto_error async def __call__(self, request: Request) -> Optional[str]: api_key = request.query_params.get(self.model.name) - return self.check_api_key(api_key, self.auto_error) + return self.check_api_key(api_key) class APIKeyHeader(APIKeyBase): @@ -188,17 +217,17 @@ class APIKeyHeader(APIKeyBase): ), ] = True, ): - self.model: APIKey = APIKey( - **{"in": APIKeyIn.header}, + super().__init__( + location=APIKeyIn.header, name=name, + scheme_name=scheme_name, description=description, + auto_error=auto_error, ) - self.scheme_name = scheme_name or self.__class__.__name__ - self.auto_error = auto_error async def __call__(self, request: Request) -> Optional[str]: api_key = request.headers.get(self.model.name) - return self.check_api_key(api_key, self.auto_error) + return self.check_api_key(api_key) class APIKeyCookie(APIKeyBase): @@ -276,14 +305,14 @@ class APIKeyCookie(APIKeyBase): ), ] = True, ): - self.model: APIKey = APIKey( - **{"in": APIKeyIn.cookie}, + super().__init__( + location=APIKeyIn.cookie, name=name, + scheme_name=scheme_name, description=description, + auto_error=auto_error, ) - self.scheme_name = scheme_name or self.__class__.__name__ - self.auto_error = auto_error async def __call__(self, request: Request) -> Optional[str]: api_key = request.cookies.get(self.model.name) - return self.check_api_key(api_key, self.auto_error) + return self.check_api_key(api_key) diff --git a/fastapi/security/http.py b/fastapi/security/http.py index 3a5985650a..b4c3bc6d85 100644 --- a/fastapi/security/http.py +++ b/fastapi/security/http.py @@ -1,6 +1,6 @@ import binascii from base64 import b64decode -from typing import Optional +from typing import Annotated, Optional from annotated_doc import Doc from fastapi.exceptions import HTTPException @@ -10,8 +10,7 @@ from fastapi.security.base import SecurityBase from fastapi.security.utils import get_authorization_scheme_param from pydantic import BaseModel from starlette.requests import Request -from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN -from typing_extensions import Annotated +from starlette.status import HTTP_401_UNAUTHORIZED class HTTPBasicCredentials(BaseModel): @@ -76,10 +75,22 @@ class HTTPBase(SecurityBase): description: Optional[str] = None, auto_error: bool = True, ): - self.model = HTTPBaseModel(scheme=scheme, description=description) + self.model: HTTPBaseModel = HTTPBaseModel( + scheme=scheme, description=description + ) self.scheme_name = scheme_name or self.__class__.__name__ self.auto_error = auto_error + def make_authenticate_headers(self) -> dict[str, str]: + return {"WWW-Authenticate": f"{self.model.scheme.title()}"} + + def make_not_authenticated_error(self) -> HTTPException: + return HTTPException( + status_code=HTTP_401_UNAUTHORIZED, + detail="Not authenticated", + headers=self.make_authenticate_headers(), + ) + async def __call__( self, request: Request ) -> Optional[HTTPAuthorizationCredentials]: @@ -87,9 +98,7 @@ class HTTPBase(SecurityBase): scheme, credentials = get_authorization_scheme_param(authorization) if not (authorization and scheme and credentials): if self.auto_error: - raise HTTPException( - status_code=HTTP_403_FORBIDDEN, detail="Not authenticated" - ) + raise self.make_not_authenticated_error() else: return None return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials) @@ -99,6 +108,8 @@ class HTTPBasic(HTTPBase): """ HTTP Basic authentication. + Ref: https://datatracker.ietf.org/doc/html/rfc7617 + ## Usage Create an instance object and use that object as the dependency in `Depends()`. @@ -185,36 +196,28 @@ class HTTPBasic(HTTPBase): self.realm = realm self.auto_error = auto_error + def make_authenticate_headers(self) -> dict[str, str]: + if self.realm: + return {"WWW-Authenticate": f'Basic realm="{self.realm}"'} + return {"WWW-Authenticate": "Basic"} + async def __call__( # type: ignore self, request: Request ) -> Optional[HTTPBasicCredentials]: authorization = request.headers.get("Authorization") scheme, param = get_authorization_scheme_param(authorization) - if self.realm: - unauthorized_headers = {"WWW-Authenticate": f'Basic realm="{self.realm}"'} - else: - unauthorized_headers = {"WWW-Authenticate": "Basic"} if not authorization or scheme.lower() != "basic": if self.auto_error: - raise HTTPException( - status_code=HTTP_401_UNAUTHORIZED, - detail="Not authenticated", - headers=unauthorized_headers, - ) + raise self.make_not_authenticated_error() else: return None - invalid_user_credentials_exc = HTTPException( - status_code=HTTP_401_UNAUTHORIZED, - detail="Invalid authentication credentials", - headers=unauthorized_headers, - ) try: data = b64decode(param).decode("ascii") - except (ValueError, UnicodeDecodeError, binascii.Error): - raise invalid_user_credentials_exc # noqa: B904 + except (ValueError, UnicodeDecodeError, binascii.Error) as e: + raise self.make_not_authenticated_error() from e username, separator, password = data.partition(":") if not separator: - raise invalid_user_credentials_exc + raise self.make_not_authenticated_error() return HTTPBasicCredentials(username=username, password=password) @@ -306,17 +309,12 @@ class HTTPBearer(HTTPBase): scheme, credentials = get_authorization_scheme_param(authorization) if not (authorization and scheme and credentials): if self.auto_error: - raise HTTPException( - status_code=HTTP_403_FORBIDDEN, detail="Not authenticated" - ) + raise self.make_not_authenticated_error() else: return None if scheme.lower() != "bearer": if self.auto_error: - raise HTTPException( - status_code=HTTP_403_FORBIDDEN, - detail="Invalid authentication credentials", - ) + raise self.make_not_authenticated_error() else: return None return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials) @@ -326,6 +324,12 @@ class HTTPDigest(HTTPBase): """ HTTP Digest authentication. + **Warning**: this is only a stub to connect the components with OpenAPI in FastAPI, + but it doesn't implement the full Digest scheme, you would need to to subclass it + and implement it in your code. + + Ref: https://datatracker.ietf.org/doc/html/rfc7616 + ## Usage Create an instance object and use that object as the dependency in `Depends()`. @@ -408,17 +412,12 @@ class HTTPDigest(HTTPBase): scheme, credentials = get_authorization_scheme_param(authorization) if not (authorization and scheme and credentials): if self.auto_error: - raise HTTPException( - status_code=HTTP_403_FORBIDDEN, detail="Not authenticated" - ) + raise self.make_not_authenticated_error() else: return None if scheme.lower() != "digest": if self.auto_error: - raise HTTPException( - status_code=HTTP_403_FORBIDDEN, - detail="Invalid authentication credentials", - ) + raise self.make_not_authenticated_error() else: return None return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials) diff --git a/fastapi/security/oauth2.py b/fastapi/security/oauth2.py index f8d97d7620..fc49ba1903 100644 --- a/fastapi/security/oauth2.py +++ b/fastapi/security/oauth2.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, List, Optional, Union, cast +from typing import Annotated, Any, Optional, Union, cast from annotated_doc import Doc from fastapi.exceptions import HTTPException @@ -8,10 +8,7 @@ from fastapi.param_functions import Form from fastapi.security.base import SecurityBase from fastapi.security.utils import get_authorization_scheme_param from starlette.requests import Request -from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN - -# TODO: import from typing when deprecating Python 3.9 -from typing_extensions import Annotated +from starlette.status import HTTP_401_UNAUTHORIZED class OAuth2PasswordRequestForm: @@ -323,7 +320,7 @@ class OAuth2(SecurityBase): self, *, flows: Annotated[ - Union[OAuthFlowsModel, Dict[str, Dict[str, Any]]], + Union[OAuthFlowsModel, dict[str, dict[str, Any]]], Doc( """ The dictionary of OAuth2 flows. @@ -377,13 +374,33 @@ class OAuth2(SecurityBase): self.scheme_name = scheme_name or self.__class__.__name__ self.auto_error = auto_error + def make_not_authenticated_error(self) -> HTTPException: + """ + The OAuth 2 specification doesn't define the challenge that should be used, + because a `Bearer` token is not really the only option to authenticate. + + But declaring any other authentication challenge would be application-specific + as it's not defined in the specification. + + For practical reasons, this method uses the `Bearer` challenge by default, as + it's probably the most common one. + + If you are implementing an OAuth2 authentication scheme other than the provided + ones in FastAPI (based on bearer tokens), you might want to override this. + + Ref: https://datatracker.ietf.org/doc/html/rfc6749 + """ + return HTTPException( + status_code=HTTP_401_UNAUTHORIZED, + detail="Not authenticated", + headers={"WWW-Authenticate": "Bearer"}, + ) + async def __call__(self, request: Request) -> Optional[str]: authorization = request.headers.get("Authorization") if not authorization: if self.auto_error: - raise HTTPException( - status_code=HTTP_403_FORBIDDEN, detail="Not authenticated" - ) + raise self.make_not_authenticated_error() else: return None return authorization @@ -420,7 +437,7 @@ class OAuth2PasswordBearer(OAuth2): ), ] = None, scopes: Annotated[ - Optional[Dict[str, str]], + Optional[dict[str, str]], Doc( """ The OAuth2 scopes that would be required by the *path operations* that @@ -491,11 +508,7 @@ class OAuth2PasswordBearer(OAuth2): scheme, param = get_authorization_scheme_param(authorization) if not authorization or scheme.lower() != "bearer": if self.auto_error: - raise HTTPException( - status_code=HTTP_401_UNAUTHORIZED, - detail="Not authenticated", - headers={"WWW-Authenticate": "Bearer"}, - ) + raise self.make_not_authenticated_error() else: return None return param @@ -537,7 +550,7 @@ class OAuth2AuthorizationCodeBearer(OAuth2): ), ] = None, scopes: Annotated[ - Optional[Dict[str, str]], + Optional[dict[str, str]], Doc( """ The OAuth2 scopes that would be required by the *path operations* that @@ -601,11 +614,7 @@ class OAuth2AuthorizationCodeBearer(OAuth2): scheme, param = get_authorization_scheme_param(authorization) if not authorization or scheme.lower() != "bearer": if self.auto_error: - raise HTTPException( - status_code=HTTP_401_UNAUTHORIZED, - detail="Not authenticated", - headers={"WWW-Authenticate": "Bearer"}, - ) + raise self.make_not_authenticated_error() else: return None # pragma: nocover return param @@ -627,7 +636,7 @@ class SecurityScopes: def __init__( self, scopes: Annotated[ - Optional[List[str]], + Optional[list[str]], Doc( """ This will be filled by FastAPI. @@ -636,7 +645,7 @@ class SecurityScopes: ] = None, ): self.scopes: Annotated[ - List[str], + list[str], Doc( """ The list of all the scopes required by dependencies. diff --git a/fastapi/security/open_id_connect_url.py b/fastapi/security/open_id_connect_url.py index 5e99798e63..f4d953351e 100644 --- a/fastapi/security/open_id_connect_url.py +++ b/fastapi/security/open_id_connect_url.py @@ -1,18 +1,22 @@ -from typing import Optional +from typing import Annotated, Optional from annotated_doc import Doc from fastapi.openapi.models import OpenIdConnect as OpenIdConnectModel from fastapi.security.base import SecurityBase from starlette.exceptions import HTTPException from starlette.requests import Request -from starlette.status import HTTP_403_FORBIDDEN -from typing_extensions import Annotated +from starlette.status import HTTP_401_UNAUTHORIZED class OpenIdConnect(SecurityBase): """ OpenID Connect authentication class. An instance of it would be used as a dependency. + + **Warning**: this is only a stub to connect the components with OpenAPI in FastAPI, + but it doesn't implement the full OpenIdConnect scheme, for example, it doesn't use + the OpenIDConnect URL. You would need to to subclass it and implement it in your + code. """ def __init__( @@ -73,13 +77,18 @@ class OpenIdConnect(SecurityBase): self.scheme_name = scheme_name or self.__class__.__name__ self.auto_error = auto_error + def make_not_authenticated_error(self) -> HTTPException: + return HTTPException( + status_code=HTTP_401_UNAUTHORIZED, + detail="Not authenticated", + headers={"WWW-Authenticate": "Bearer"}, + ) + async def __call__(self, request: Request) -> Optional[str]: authorization = request.headers.get("Authorization") if not authorization: if self.auto_error: - raise HTTPException( - status_code=HTTP_403_FORBIDDEN, detail="Not authenticated" - ) + raise self.make_not_authenticated_error() else: return None return authorization diff --git a/fastapi/security/utils.py b/fastapi/security/utils.py index fa7a450b74..002e68b445 100644 --- a/fastapi/security/utils.py +++ b/fastapi/security/utils.py @@ -1,9 +1,9 @@ -from typing import Optional, Tuple +from typing import Optional def get_authorization_scheme_param( authorization_header_value: Optional[str], -) -> Tuple[str, str]: +) -> tuple[str, str]: if not authorization_header_value: return "", "" scheme, _, param = authorization_header_value.partition(" ") diff --git a/fastapi/temp_pydantic_v1_params.py b/fastapi/temp_pydantic_v1_params.py deleted file mode 100644 index e41d712308..0000000000 --- a/fastapi/temp_pydantic_v1_params.py +++ /dev/null @@ -1,724 +0,0 @@ -import warnings -from typing import Any, Callable, Dict, List, Optional, Union - -from fastapi.openapi.models import Example -from fastapi.params import ParamTypes -from typing_extensions import Annotated, deprecated - -from ._compat.may_v1 import FieldInfo, Undefined -from ._compat.shared import PYDANTIC_VERSION_MINOR_TUPLE - -_Unset: Any = Undefined - - -class Param(FieldInfo): # type: ignore[misc] - in_: ParamTypes - - def __init__( - self, - default: Any = Undefined, - *, - default_factory: Union[Callable[[], Any], None] = _Unset, - annotation: Optional[Any] = None, - alias: Optional[str] = None, - alias_priority: Union[int, None] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None - validation_alias: Union[str, None] = None, - serialization_alias: Union[str, None] = None, - title: Optional[str] = None, - description: Optional[str] = None, - gt: Optional[float] = None, - ge: Optional[float] = None, - lt: Optional[float] = None, - le: Optional[float] = None, - min_length: Optional[int] = None, - max_length: Optional[int] = None, - pattern: Optional[str] = None, - regex: Annotated[ - Optional[str], - deprecated( - "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." - ), - ] = None, - discriminator: Union[str, None] = None, - strict: Union[bool, None] = _Unset, - multiple_of: Union[float, None] = _Unset, - allow_inf_nan: Union[bool, None] = _Unset, - max_digits: Union[int, None] = _Unset, - decimal_places: Union[int, None] = _Unset, - examples: Optional[List[Any]] = None, - example: Annotated[ - Optional[Any], - deprecated( - "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " - "although still supported. Use examples instead." - ), - ] = _Unset, - openapi_examples: Optional[Dict[str, Example]] = None, - deprecated: Union[deprecated, str, bool, None] = None, - include_in_schema: bool = True, - json_schema_extra: Union[Dict[str, Any], None] = None, - **extra: Any, - ): - if example is not _Unset: - warnings.warn( - "`example` has been deprecated, please use `examples` instead", - category=DeprecationWarning, - stacklevel=4, - ) - self.example = example - self.include_in_schema = include_in_schema - self.openapi_examples = openapi_examples - kwargs = dict( - default=default, - default_factory=default_factory, - alias=alias, - title=title, - description=description, - gt=gt, - ge=ge, - lt=lt, - le=le, - min_length=min_length, - max_length=max_length, - discriminator=discriminator, - multiple_of=multiple_of, - allow_inf_nan=allow_inf_nan, - max_digits=max_digits, - decimal_places=decimal_places, - **extra, - ) - if examples is not None: - kwargs["examples"] = examples - if regex is not None: - warnings.warn( - "`regex` has been deprecated, please use `pattern` instead", - category=DeprecationWarning, - stacklevel=4, - ) - current_json_schema_extra = json_schema_extra or extra - if PYDANTIC_VERSION_MINOR_TUPLE < (2, 7): - self.deprecated = deprecated - else: - kwargs["deprecated"] = deprecated - kwargs["regex"] = pattern or regex - kwargs.update(**current_json_schema_extra) - use_kwargs = {k: v for k, v in kwargs.items() if v is not _Unset} - - super().__init__(**use_kwargs) - - def __repr__(self) -> str: - return f"{self.__class__.__name__}({self.default})" - - -class Path(Param): # type: ignore[misc] - in_ = ParamTypes.path - - def __init__( - self, - default: Any = ..., - *, - default_factory: Union[Callable[[], Any], None] = _Unset, - annotation: Optional[Any] = None, - alias: Optional[str] = None, - alias_priority: Union[int, None] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None - validation_alias: Union[str, None] = None, - serialization_alias: Union[str, None] = None, - title: Optional[str] = None, - description: Optional[str] = None, - gt: Optional[float] = None, - ge: Optional[float] = None, - lt: Optional[float] = None, - le: Optional[float] = None, - min_length: Optional[int] = None, - max_length: Optional[int] = None, - pattern: Optional[str] = None, - regex: Annotated[ - Optional[str], - deprecated( - "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." - ), - ] = None, - discriminator: Union[str, None] = None, - strict: Union[bool, None] = _Unset, - multiple_of: Union[float, None] = _Unset, - allow_inf_nan: Union[bool, None] = _Unset, - max_digits: Union[int, None] = _Unset, - decimal_places: Union[int, None] = _Unset, - examples: Optional[List[Any]] = None, - example: Annotated[ - Optional[Any], - deprecated( - "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " - "although still supported. Use examples instead." - ), - ] = _Unset, - openapi_examples: Optional[Dict[str, Example]] = None, - deprecated: Union[deprecated, str, bool, None] = None, - include_in_schema: bool = True, - json_schema_extra: Union[Dict[str, Any], None] = None, - **extra: Any, - ): - assert default is ..., "Path parameters cannot have a default value" - self.in_ = self.in_ - super().__init__( - default=default, - default_factory=default_factory, - annotation=annotation, - alias=alias, - alias_priority=alias_priority, - validation_alias=validation_alias, - serialization_alias=serialization_alias, - title=title, - description=description, - gt=gt, - ge=ge, - lt=lt, - le=le, - min_length=min_length, - max_length=max_length, - pattern=pattern, - regex=regex, - discriminator=discriminator, - strict=strict, - multiple_of=multiple_of, - allow_inf_nan=allow_inf_nan, - max_digits=max_digits, - decimal_places=decimal_places, - deprecated=deprecated, - example=example, - examples=examples, - openapi_examples=openapi_examples, - include_in_schema=include_in_schema, - json_schema_extra=json_schema_extra, - **extra, - ) - - -class Query(Param): # type: ignore[misc] - in_ = ParamTypes.query - - def __init__( - self, - default: Any = Undefined, - *, - default_factory: Union[Callable[[], Any], None] = _Unset, - annotation: Optional[Any] = None, - alias: Optional[str] = None, - alias_priority: Union[int, None] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None - validation_alias: Union[str, None] = None, - serialization_alias: Union[str, None] = None, - title: Optional[str] = None, - description: Optional[str] = None, - gt: Optional[float] = None, - ge: Optional[float] = None, - lt: Optional[float] = None, - le: Optional[float] = None, - min_length: Optional[int] = None, - max_length: Optional[int] = None, - pattern: Optional[str] = None, - regex: Annotated[ - Optional[str], - deprecated( - "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." - ), - ] = None, - discriminator: Union[str, None] = None, - strict: Union[bool, None] = _Unset, - multiple_of: Union[float, None] = _Unset, - allow_inf_nan: Union[bool, None] = _Unset, - max_digits: Union[int, None] = _Unset, - decimal_places: Union[int, None] = _Unset, - examples: Optional[List[Any]] = None, - example: Annotated[ - Optional[Any], - deprecated( - "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " - "although still supported. Use examples instead." - ), - ] = _Unset, - openapi_examples: Optional[Dict[str, Example]] = None, - deprecated: Union[deprecated, str, bool, None] = None, - include_in_schema: bool = True, - json_schema_extra: Union[Dict[str, Any], None] = None, - **extra: Any, - ): - super().__init__( - default=default, - default_factory=default_factory, - annotation=annotation, - alias=alias, - alias_priority=alias_priority, - validation_alias=validation_alias, - serialization_alias=serialization_alias, - title=title, - description=description, - gt=gt, - ge=ge, - lt=lt, - le=le, - min_length=min_length, - max_length=max_length, - pattern=pattern, - regex=regex, - discriminator=discriminator, - strict=strict, - multiple_of=multiple_of, - allow_inf_nan=allow_inf_nan, - max_digits=max_digits, - decimal_places=decimal_places, - deprecated=deprecated, - example=example, - examples=examples, - openapi_examples=openapi_examples, - include_in_schema=include_in_schema, - json_schema_extra=json_schema_extra, - **extra, - ) - - -class Header(Param): # type: ignore[misc] - in_ = ParamTypes.header - - def __init__( - self, - default: Any = Undefined, - *, - default_factory: Union[Callable[[], Any], None] = _Unset, - annotation: Optional[Any] = None, - alias: Optional[str] = None, - alias_priority: Union[int, None] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None - validation_alias: Union[str, None] = None, - serialization_alias: Union[str, None] = None, - convert_underscores: bool = True, - title: Optional[str] = None, - description: Optional[str] = None, - gt: Optional[float] = None, - ge: Optional[float] = None, - lt: Optional[float] = None, - le: Optional[float] = None, - min_length: Optional[int] = None, - max_length: Optional[int] = None, - pattern: Optional[str] = None, - regex: Annotated[ - Optional[str], - deprecated( - "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." - ), - ] = None, - discriminator: Union[str, None] = None, - strict: Union[bool, None] = _Unset, - multiple_of: Union[float, None] = _Unset, - allow_inf_nan: Union[bool, None] = _Unset, - max_digits: Union[int, None] = _Unset, - decimal_places: Union[int, None] = _Unset, - examples: Optional[List[Any]] = None, - example: Annotated[ - Optional[Any], - deprecated( - "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " - "although still supported. Use examples instead." - ), - ] = _Unset, - openapi_examples: Optional[Dict[str, Example]] = None, - deprecated: Union[deprecated, str, bool, None] = None, - include_in_schema: bool = True, - json_schema_extra: Union[Dict[str, Any], None] = None, - **extra: Any, - ): - self.convert_underscores = convert_underscores - super().__init__( - default=default, - default_factory=default_factory, - annotation=annotation, - alias=alias, - alias_priority=alias_priority, - validation_alias=validation_alias, - serialization_alias=serialization_alias, - title=title, - description=description, - gt=gt, - ge=ge, - lt=lt, - le=le, - min_length=min_length, - max_length=max_length, - pattern=pattern, - regex=regex, - discriminator=discriminator, - strict=strict, - multiple_of=multiple_of, - allow_inf_nan=allow_inf_nan, - max_digits=max_digits, - decimal_places=decimal_places, - deprecated=deprecated, - example=example, - examples=examples, - openapi_examples=openapi_examples, - include_in_schema=include_in_schema, - json_schema_extra=json_schema_extra, - **extra, - ) - - -class Cookie(Param): # type: ignore[misc] - in_ = ParamTypes.cookie - - def __init__( - self, - default: Any = Undefined, - *, - default_factory: Union[Callable[[], Any], None] = _Unset, - annotation: Optional[Any] = None, - alias: Optional[str] = None, - alias_priority: Union[int, None] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None - validation_alias: Union[str, None] = None, - serialization_alias: Union[str, None] = None, - title: Optional[str] = None, - description: Optional[str] = None, - gt: Optional[float] = None, - ge: Optional[float] = None, - lt: Optional[float] = None, - le: Optional[float] = None, - min_length: Optional[int] = None, - max_length: Optional[int] = None, - pattern: Optional[str] = None, - regex: Annotated[ - Optional[str], - deprecated( - "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." - ), - ] = None, - discriminator: Union[str, None] = None, - strict: Union[bool, None] = _Unset, - multiple_of: Union[float, None] = _Unset, - allow_inf_nan: Union[bool, None] = _Unset, - max_digits: Union[int, None] = _Unset, - decimal_places: Union[int, None] = _Unset, - examples: Optional[List[Any]] = None, - example: Annotated[ - Optional[Any], - deprecated( - "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " - "although still supported. Use examples instead." - ), - ] = _Unset, - openapi_examples: Optional[Dict[str, Example]] = None, - deprecated: Union[deprecated, str, bool, None] = None, - include_in_schema: bool = True, - json_schema_extra: Union[Dict[str, Any], None] = None, - **extra: Any, - ): - super().__init__( - default=default, - default_factory=default_factory, - annotation=annotation, - alias=alias, - alias_priority=alias_priority, - validation_alias=validation_alias, - serialization_alias=serialization_alias, - title=title, - description=description, - gt=gt, - ge=ge, - lt=lt, - le=le, - min_length=min_length, - max_length=max_length, - pattern=pattern, - regex=regex, - discriminator=discriminator, - strict=strict, - multiple_of=multiple_of, - allow_inf_nan=allow_inf_nan, - max_digits=max_digits, - decimal_places=decimal_places, - deprecated=deprecated, - example=example, - examples=examples, - openapi_examples=openapi_examples, - include_in_schema=include_in_schema, - json_schema_extra=json_schema_extra, - **extra, - ) - - -class Body(FieldInfo): # type: ignore[misc] - def __init__( - self, - default: Any = Undefined, - *, - default_factory: Union[Callable[[], Any], None] = _Unset, - annotation: Optional[Any] = None, - embed: Union[bool, None] = None, - media_type: str = "application/json", - alias: Optional[str] = None, - alias_priority: Union[int, None] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None - validation_alias: Union[str, None] = None, - serialization_alias: Union[str, None] = None, - title: Optional[str] = None, - description: Optional[str] = None, - gt: Optional[float] = None, - ge: Optional[float] = None, - lt: Optional[float] = None, - le: Optional[float] = None, - min_length: Optional[int] = None, - max_length: Optional[int] = None, - pattern: Optional[str] = None, - regex: Annotated[ - Optional[str], - deprecated( - "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." - ), - ] = None, - discriminator: Union[str, None] = None, - strict: Union[bool, None] = _Unset, - multiple_of: Union[float, None] = _Unset, - allow_inf_nan: Union[bool, None] = _Unset, - max_digits: Union[int, None] = _Unset, - decimal_places: Union[int, None] = _Unset, - examples: Optional[List[Any]] = None, - example: Annotated[ - Optional[Any], - deprecated( - "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " - "although still supported. Use examples instead." - ), - ] = _Unset, - openapi_examples: Optional[Dict[str, Example]] = None, - deprecated: Union[deprecated, str, bool, None] = None, - include_in_schema: bool = True, - json_schema_extra: Union[Dict[str, Any], None] = None, - **extra: Any, - ): - self.embed = embed - self.media_type = media_type - if example is not _Unset: - warnings.warn( - "`example` has been deprecated, please use `examples` instead", - category=DeprecationWarning, - stacklevel=4, - ) - self.example = example - self.include_in_schema = include_in_schema - self.openapi_examples = openapi_examples - kwargs = dict( - default=default, - default_factory=default_factory, - alias=alias, - title=title, - description=description, - gt=gt, - ge=ge, - lt=lt, - le=le, - min_length=min_length, - max_length=max_length, - discriminator=discriminator, - multiple_of=multiple_of, - allow_inf_nan=allow_inf_nan, - max_digits=max_digits, - decimal_places=decimal_places, - **extra, - ) - if examples is not None: - kwargs["examples"] = examples - if regex is not None: - warnings.warn( - "`regex` has been deprecated, please use `pattern` instead", - category=DeprecationWarning, - stacklevel=4, - ) - current_json_schema_extra = json_schema_extra or extra - if PYDANTIC_VERSION_MINOR_TUPLE < (2, 7): - self.deprecated = deprecated - else: - kwargs["deprecated"] = deprecated - kwargs["regex"] = pattern or regex - kwargs.update(**current_json_schema_extra) - - use_kwargs = {k: v for k, v in kwargs.items() if v is not _Unset} - - super().__init__(**use_kwargs) - - def __repr__(self) -> str: - return f"{self.__class__.__name__}({self.default})" - - -class Form(Body): # type: ignore[misc] - def __init__( - self, - default: Any = Undefined, - *, - default_factory: Union[Callable[[], Any], None] = _Unset, - annotation: Optional[Any] = None, - media_type: str = "application/x-www-form-urlencoded", - alias: Optional[str] = None, - alias_priority: Union[int, None] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None - validation_alias: Union[str, None] = None, - serialization_alias: Union[str, None] = None, - title: Optional[str] = None, - description: Optional[str] = None, - gt: Optional[float] = None, - ge: Optional[float] = None, - lt: Optional[float] = None, - le: Optional[float] = None, - min_length: Optional[int] = None, - max_length: Optional[int] = None, - pattern: Optional[str] = None, - regex: Annotated[ - Optional[str], - deprecated( - "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." - ), - ] = None, - discriminator: Union[str, None] = None, - strict: Union[bool, None] = _Unset, - multiple_of: Union[float, None] = _Unset, - allow_inf_nan: Union[bool, None] = _Unset, - max_digits: Union[int, None] = _Unset, - decimal_places: Union[int, None] = _Unset, - examples: Optional[List[Any]] = None, - example: Annotated[ - Optional[Any], - deprecated( - "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " - "although still supported. Use examples instead." - ), - ] = _Unset, - openapi_examples: Optional[Dict[str, Example]] = None, - deprecated: Union[deprecated, str, bool, None] = None, - include_in_schema: bool = True, - json_schema_extra: Union[Dict[str, Any], None] = None, - **extra: Any, - ): - super().__init__( - default=default, - default_factory=default_factory, - annotation=annotation, - media_type=media_type, - alias=alias, - alias_priority=alias_priority, - validation_alias=validation_alias, - serialization_alias=serialization_alias, - title=title, - description=description, - gt=gt, - ge=ge, - lt=lt, - le=le, - min_length=min_length, - max_length=max_length, - pattern=pattern, - regex=regex, - discriminator=discriminator, - strict=strict, - multiple_of=multiple_of, - allow_inf_nan=allow_inf_nan, - max_digits=max_digits, - decimal_places=decimal_places, - deprecated=deprecated, - example=example, - examples=examples, - openapi_examples=openapi_examples, - include_in_schema=include_in_schema, - json_schema_extra=json_schema_extra, - **extra, - ) - - -class File(Form): # type: ignore[misc] - def __init__( - self, - default: Any = Undefined, - *, - default_factory: Union[Callable[[], Any], None] = _Unset, - annotation: Optional[Any] = None, - media_type: str = "multipart/form-data", - alias: Optional[str] = None, - alias_priority: Union[int, None] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None - validation_alias: Union[str, None] = None, - serialization_alias: Union[str, None] = None, - title: Optional[str] = None, - description: Optional[str] = None, - gt: Optional[float] = None, - ge: Optional[float] = None, - lt: Optional[float] = None, - le: Optional[float] = None, - min_length: Optional[int] = None, - max_length: Optional[int] = None, - pattern: Optional[str] = None, - regex: Annotated[ - Optional[str], - deprecated( - "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." - ), - ] = None, - discriminator: Union[str, None] = None, - strict: Union[bool, None] = _Unset, - multiple_of: Union[float, None] = _Unset, - allow_inf_nan: Union[bool, None] = _Unset, - max_digits: Union[int, None] = _Unset, - decimal_places: Union[int, None] = _Unset, - examples: Optional[List[Any]] = None, - example: Annotated[ - Optional[Any], - deprecated( - "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " - "although still supported. Use examples instead." - ), - ] = _Unset, - openapi_examples: Optional[Dict[str, Example]] = None, - deprecated: Union[deprecated, str, bool, None] = None, - include_in_schema: bool = True, - json_schema_extra: Union[Dict[str, Any], None] = None, - **extra: Any, - ): - super().__init__( - default=default, - default_factory=default_factory, - annotation=annotation, - media_type=media_type, - alias=alias, - alias_priority=alias_priority, - validation_alias=validation_alias, - serialization_alias=serialization_alias, - title=title, - description=description, - gt=gt, - ge=ge, - lt=lt, - le=le, - min_length=min_length, - max_length=max_length, - pattern=pattern, - regex=regex, - discriminator=discriminator, - strict=strict, - multiple_of=multiple_of, - allow_inf_nan=allow_inf_nan, - max_digits=max_digits, - decimal_places=decimal_places, - deprecated=deprecated, - example=example, - examples=examples, - openapi_examples=openapi_examples, - include_in_schema=include_in_schema, - json_schema_extra=json_schema_extra, - **extra, - ) diff --git a/fastapi/types.py b/fastapi/types.py index 3f4e81a7cc..d3e980cb43 100644 --- a/fastapi/types.py +++ b/fastapi/types.py @@ -1,11 +1,11 @@ import types from enum import Enum -from typing import Any, Callable, Dict, Optional, Set, Tuple, Type, TypeVar, Union +from typing import Any, Callable, Optional, TypeVar, Union from pydantic import BaseModel DecoratedCallable = TypeVar("DecoratedCallable", bound=Callable[..., Any]) UnionType = getattr(types, "UnionType", Union) -ModelNameMap = Dict[Union[Type[BaseModel], Type[Enum]], str] -IncEx = Union[Set[int], Set[str], Dict[int, Any], Dict[str, Any]] -DependencyCacheKey = Tuple[Optional[Callable[..., Any]], Tuple[str, ...], str] +ModelNameMap = dict[Union[type[BaseModel], type[Enum]], str] +IncEx = Union[set[int], set[str], dict[int, Any], dict[str, Any]] +DependencyCacheKey = tuple[Optional[Callable[..., Any]], tuple[str, ...], str] diff --git a/fastapi/utils.py b/fastapi/utils.py index 2e79ee6b19..78fdcbb5b4 100644 --- a/fastapi/utils.py +++ b/fastapi/utils.py @@ -1,22 +1,16 @@ import re import warnings -from dataclasses import is_dataclass +from collections.abc import MutableMapping from typing import ( TYPE_CHECKING, Any, - Dict, - MutableMapping, Optional, - Set, - Type, Union, - cast, ) from weakref import WeakKeyDictionary import fastapi from fastapi._compat import ( - PYDANTIC_V2, BaseConfig, ModelField, PydanticSchemaGenerationError, @@ -24,19 +18,20 @@ from fastapi._compat import ( UndefinedType, Validator, annotation_is_pydantic_v1, - lenient_issubclass, - may_v1, ) from fastapi.datastructures import DefaultPlaceholder, DefaultType +from fastapi.exceptions import FastAPIDeprecationWarning, PydanticV1NotSupportedError from pydantic import BaseModel from pydantic.fields import FieldInfo from typing_extensions import Literal +from ._compat import v2 + if TYPE_CHECKING: # pragma: nocover from .routing import APIRoute # Cache for `create_cloned_field` -_CLONED_TYPES_CACHE: MutableMapping[Type[BaseModel], Type[BaseModel]] = ( +_CLONED_TYPES_CACHE: MutableMapping[type[BaseModel], type[BaseModel]] = ( WeakKeyDictionary() ) @@ -58,7 +53,7 @@ def is_body_allowed_for_status_code(status_code: Union[int, str, None]) -> bool: return not (current_status_code < 200 or current_status_code in {204, 205, 304}) -def get_path_param_names(path: str) -> Set[str]: +def get_path_param_names(path: str) -> set[str]: return set(re.findall("{(.*?)}", path)) @@ -76,132 +71,47 @@ _invalid_args_message = ( def create_model_field( name: str, type_: Any, - class_validators: Optional[Dict[str, Validator]] = None, + class_validators: Optional[dict[str, Validator]] = None, default: Optional[Any] = Undefined, required: Union[bool, UndefinedType] = Undefined, - model_config: Union[Type[BaseConfig], None] = None, + model_config: Union[type[BaseConfig], None] = None, field_info: Optional[FieldInfo] = None, alias: Optional[str] = None, mode: Literal["validation", "serialization"] = "validation", version: Literal["1", "auto"] = "auto", ) -> ModelField: + if annotation_is_pydantic_v1(type_): + raise PydanticV1NotSupportedError( + "pydantic.v1 models are no longer supported by FastAPI." + f" Please update the response model {type_!r}." + ) class_validators = class_validators or {} - v1_model_config = may_v1.BaseConfig - v1_field_info = field_info or may_v1.FieldInfo() - v1_kwargs = { - "name": name, - "field_info": v1_field_info, - "type_": type_, - "class_validators": class_validators, - "default": default, - "required": required, - "model_config": v1_model_config, - "alias": alias, - } - - if ( - annotation_is_pydantic_v1(type_) - or isinstance(field_info, may_v1.FieldInfo) - or version == "1" - ): - from fastapi._compat import v1 - - try: - return v1.ModelField(**v1_kwargs) # type: ignore[no-any-return] - except RuntimeError: - raise fastapi.exceptions.FastAPIError(_invalid_args_message) from None - elif PYDANTIC_V2: - from ._compat import v2 - - field_info = field_info or FieldInfo( - annotation=type_, default=default, alias=alias - ) - kwargs = {"mode": mode, "name": name, "field_info": field_info} - try: - return v2.ModelField(**kwargs) # type: ignore[return-value,arg-type] - except PydanticSchemaGenerationError: - raise fastapi.exceptions.FastAPIError(_invalid_args_message) from None - # Pydantic v2 is not installed, but it's not a Pydantic v1 ModelField, it could be - # a Pydantic v1 type, like a constrained int - from fastapi._compat import v1 - + field_info = field_info or FieldInfo(annotation=type_, default=default, alias=alias) + kwargs = {"mode": mode, "name": name, "field_info": field_info} try: - return v1.ModelField(**v1_kwargs) # type: ignore[no-any-return] - except RuntimeError: - raise fastapi.exceptions.FastAPIError(_invalid_args_message) from None + return v2.ModelField(**kwargs) # type: ignore[return-value,arg-type] + except PydanticSchemaGenerationError: + raise fastapi.exceptions.FastAPIError( + _invalid_args_message.format(type_=type_) + ) from None def create_cloned_field( field: ModelField, *, - cloned_types: Optional[MutableMapping[Type[BaseModel], Type[BaseModel]]] = None, + cloned_types: Optional[MutableMapping[type[BaseModel], type[BaseModel]]] = None, ) -> ModelField: - if PYDANTIC_V2: - from ._compat import v2 - - if isinstance(field, v2.ModelField): - return field - - from fastapi._compat import v1 - - # cloned_types caches already cloned types to support recursive models and improve - # performance by avoiding unnecessary cloning - if cloned_types is None: - cloned_types = _CLONED_TYPES_CACHE - - original_type = field.type_ - if is_dataclass(original_type) and hasattr(original_type, "__pydantic_model__"): - original_type = original_type.__pydantic_model__ - use_type = original_type - if lenient_issubclass(original_type, v1.BaseModel): - original_type = cast(Type[v1.BaseModel], original_type) - use_type = cloned_types.get(original_type) - if use_type is None: - use_type = v1.create_model(original_type.__name__, __base__=original_type) - cloned_types[original_type] = use_type - for f in original_type.__fields__.values(): - use_type.__fields__[f.name] = create_cloned_field( - f, - cloned_types=cloned_types, - ) - new_field = create_model_field(name=field.name, type_=use_type, version="1") - new_field.has_alias = field.has_alias # type: ignore[attr-defined] - new_field.alias = field.alias # type: ignore[misc] - new_field.class_validators = field.class_validators # type: ignore[attr-defined] - new_field.default = field.default # type: ignore[misc] - new_field.default_factory = field.default_factory # type: ignore[attr-defined] - new_field.required = field.required # type: ignore[misc] - new_field.model_config = field.model_config # type: ignore[attr-defined] - new_field.field_info = field.field_info - new_field.allow_none = field.allow_none # type: ignore[attr-defined] - new_field.validate_always = field.validate_always # type: ignore[attr-defined] - if field.sub_fields: # type: ignore[attr-defined] - new_field.sub_fields = [ # type: ignore[attr-defined] - create_cloned_field(sub_field, cloned_types=cloned_types) - for sub_field in field.sub_fields # type: ignore[attr-defined] - ] - if field.key_field: # type: ignore[attr-defined] - new_field.key_field = create_cloned_field( # type: ignore[attr-defined] - field.key_field, # type: ignore[attr-defined] - cloned_types=cloned_types, - ) - new_field.validators = field.validators # type: ignore[attr-defined] - new_field.pre_validators = field.pre_validators # type: ignore[attr-defined] - new_field.post_validators = field.post_validators # type: ignore[attr-defined] - new_field.parse_json = field.parse_json # type: ignore[attr-defined] - new_field.shape = field.shape # type: ignore[attr-defined] - new_field.populate_validators() # type: ignore[attr-defined] - return new_field + return field def generate_operation_id_for_path( *, name: str, path: str, method: str ) -> str: # pragma: nocover warnings.warn( - "fastapi.utils.generate_operation_id_for_path() was deprecated, " + message="fastapi.utils.generate_operation_id_for_path() was deprecated, " "it is not used internally, and will be removed soon", - DeprecationWarning, + category=FastAPIDeprecationWarning, stacklevel=2, ) operation_id = f"{name}{path}" @@ -218,7 +128,7 @@ def generate_unique_id(route: "APIRoute") -> str: return operation_id -def deep_dict_update(main_dict: Dict[Any, Any], update_dict: Dict[Any, Any]) -> None: +def deep_dict_update(main_dict: dict[Any, Any], update_dict: dict[Any, Any]) -> None: for key, value in update_dict.items(): if ( key in main_dict diff --git a/pdm_build.py b/pdm_build.py index c83222b336..a0eb88eebb 100644 --- a/pdm_build.py +++ b/pdm_build.py @@ -1,5 +1,5 @@ import os -from typing import Any, Dict +from typing import Any from pdm.backend.hooks import Context @@ -9,12 +9,12 @@ TIANGOLO_BUILD_PACKAGE = os.getenv("TIANGOLO_BUILD_PACKAGE", "fastapi") def pdm_build_initialize(context: Context) -> None: metadata = context.config.metadata # Get custom config for the current package, from the env var - config: Dict[str, Any] = context.config.data["tool"]["tiangolo"][ + config: dict[str, Any] = context.config.data["tool"]["tiangolo"][ "_internal-slim-build" ]["packages"].get(TIANGOLO_BUILD_PACKAGE) if not config: return - project_config: Dict[str, Any] = config["project"] + project_config: dict[str, Any] = config["project"] # Override main [project] configs with custom configs for this package for key, value in project_config.items(): metadata[key] = value diff --git a/pyproject.toml b/pyproject.toml index 7d2be00744..fe9b3a7ea1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ description = "FastAPI framework, high performance, easy to learn, fast to code, readme = "README.md" license = "MIT" license-files = ["LICENSE"] -requires-python = ">=3.8" +requires-python = ">=3.9" authors = [ { name = "Sebastián Ramírez", email = "tiangolo@gmail.com" }, ] @@ -30,11 +30,9 @@ classifiers = [ "Framework :: AsyncIO", "Framework :: FastAPI", "Framework :: Pydantic", - "Framework :: Pydantic :: 1", "Framework :: Pydantic :: 2", "Intended Audience :: Developers", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", @@ -45,8 +43,8 @@ classifiers = [ "Topic :: Internet :: WWW/HTTP", ] dependencies = [ - "starlette>=0.40.0,<0.50.0", - "pydantic>=1.7.4,!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0", + "starlette>=0.40.0,<0.51.0", + "pydantic>=2.7.0", "typing-extensions>=4.8.0", "annotated-doc>=0.0.2", ] @@ -72,11 +70,10 @@ standard = [ "email-validator >=2.0.0", # Uvicorn with uvloop "uvicorn[standard] >=0.12.0", - # TODO: this should be part of some pydantic optional extra dependencies # # Settings management - # "pydantic-settings >=2.0.0", + "pydantic-settings >=2.0.0", # # Extra Pydantic data types - # "pydantic-extra-types >=2.0.0", + "pydantic-extra-types >=2.0.0", ] standard-no-fastapi-cloud-cli = [ @@ -91,11 +88,10 @@ standard-no-fastapi-cloud-cli = [ "email-validator >=2.0.0", # Uvicorn with uvloop "uvicorn[standard] >=0.12.0", - # TODO: this should be part of some pydantic optional extra dependencies # # Settings management - # "pydantic-settings >=2.0.0", + "pydantic-settings >=2.0.0", # # Extra Pydantic data types - # "pydantic-extra-types >=2.0.0", + "pydantic-extra-types >=2.0.0", ] all = [ @@ -127,6 +123,68 @@ all = [ [project.scripts] fastapi = "fastapi.cli:main" +[dependency-groups] +dev = [ + { include-group = "tests" }, + { include-group = "docs" }, + { include-group = "translations" }, + "playwright>=1.57.0", + "prek==0.2.22", +] +docs = [ + { include-group = "docs-tests" }, + "black==25.1.0", + "cairosvg==2.8.2", + "griffe-typingdoc==0.3.0", + "griffe-warnings-deprecated==1.1.0", + "jieba==0.42.1", + "markdown-include-variants==0.0.8", + "mdx-include>=1.4.1,<2.0.0", + "mkdocs-macros-plugin==1.4.1", + "mkdocs-material==9.7.0", + "mkdocs-redirects>=1.2.1,<1.3.0", + "mkdocstrings[python]==0.30.1", + "pillow==11.3.0", + "python-slugify==8.0.4", + "pyyaml>=5.3.1,<7.0.0", + "typer==0.16.0", +] +docs-tests = [ + "httpx>=0.23.0,<1.0.0", + "ruff==0.14.3", +] +github-actions = [ + "httpx>=0.27.0,<1.0.0", + "pydantic>=2.5.3,<3.0.0", + "pydantic-settings>=2.1.0,<3.0.0", + "pygithub>=2.3.0,<3.0.0", + "pyyaml>=5.3.1,<7.0.0", + "smokeshow>=0.5.0", +] +tests = [ + { include-group = "docs-tests" }, + "anyio[trio]>=3.2.1,<5.0.0", + "coverage[toml]>=6.5.0,<8.0", + "dirty-equals==0.9.0", + "flask>=1.1.2,<4.0.0", + "inline-snapshot>=0.21.1", + "mypy==1.14.1", + "pwdlib[argon2]>=0.2.1", + "pyjwt==2.9.0", + "pytest>=7.1.3,<9.0.0", + "pytest-codspeed==4.2.0", + "pyyaml>=5.3.1,<7.0.0", + "sqlmodel==0.0.27", + "strawberry-graphql>=0.200.0,<1.0.0", + "types-orjson==3.6.2", + "types-ujson==5.10.0.20240515", +] +translations = [ + "gitpython==3.1.45", + "pydantic-ai==0.4.10", + "pygithub==2.8.1", +] + [tool.pdm] version = { source = "file", path = "fastapi/__init__.py" } distribution = true @@ -135,11 +193,10 @@ distribution = true source-includes = [ "tests/", "docs_src/", - "requirements*.txt", "scripts/", # For a test "docs/en/docs/img/favicon.png", - ] +] [tool.tiangolo._internal-slim-build.packages.fastapi-slim.project] name = "fastapi-slim" @@ -184,8 +241,6 @@ filterwarnings = [ # Ref: https://github.com/python-trio/trio/pull/3054 # Remove once there's a new version of Trio 'ignore:The `hash` argument is deprecated*:DeprecationWarning:trio', - # Ignore flaky coverage / pytest warning about SQLite connection, only applies to Python 3.13 and Pydantic v1 - 'ignore:Exception ignored in. =0.23.0,<1.0.0 -# For linting and generating docs versions -ruff ==0.14.3 diff --git a/requirements-docs.txt b/requirements-docs.txt deleted file mode 100644 index 696eb2a334..0000000000 --- a/requirements-docs.txt +++ /dev/null @@ -1,19 +0,0 @@ --e . --r requirements-docs-tests.txt -mkdocs-material==9.6.16 -mdx-include >=1.4.1,<2.0.0 -mkdocs-redirects>=1.2.1,<1.3.0 -typer == 0.16.0 -pyyaml >=5.3.1,<7.0.0 -# For Material for MkDocs, Chinese search -jieba==0.42.1 -# For image processing by Material for MkDocs -pillow==11.3.0 -# For image processing by Material for MkDocs -cairosvg==2.8.2 -mkdocstrings[python]==0.30.1 -griffe-typingdoc==0.3.0 -# For griffe, it formats with black -black==25.1.0 -mkdocs-macros-plugin==1.4.1 -markdown-include-variants==0.0.5 diff --git a/requirements-github-actions.txt b/requirements-github-actions.txt deleted file mode 100644 index a6a733447d..0000000000 --- a/requirements-github-actions.txt +++ /dev/null @@ -1,6 +0,0 @@ -PyGithub>=2.3.0,<3.0.0 -pydantic>=2.5.3,<3.0.0 -pydantic-settings>=2.1.0,<3.0.0 -httpx>=0.27.0,<1.0.0 -pyyaml >=5.3.1,<7.0.0 -smokeshow diff --git a/requirements-tests.txt b/requirements-tests.txt deleted file mode 100644 index c5de4157e7..0000000000 --- a/requirements-tests.txt +++ /dev/null @@ -1,16 +0,0 @@ --e .[all] --r requirements-docs-tests.txt -pytest >=7.1.3,<9.0.0 -coverage[toml] >= 6.5.0,< 8.0 -mypy ==1.14.1 -dirty-equals ==0.9.0 -sqlmodel==0.0.27 -flask >=1.1.2,<4.0.0 -anyio[trio] >=3.2.1,<5.0.0 -PyJWT==2.9.0 -pyyaml >=5.3.1,<7.0.0 -pwdlib[argon2] >=0.2.1 -inline-snapshot>=0.21.1 -# types -types-ujson ==5.10.0.20240515 -types-orjson ==3.6.2 diff --git a/requirements-translations.txt b/requirements-translations.txt deleted file mode 100644 index a62ba3ac1c..0000000000 --- a/requirements-translations.txt +++ /dev/null @@ -1,2 +0,0 @@ -pydantic-ai==0.4.10 -GitPython==3.1.45 diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 9180bf1be5..0000000000 --- a/requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ --e .[all] --r requirements-tests.txt --r requirements-docs.txt -pre-commit >=2.17.0,<5.0.0 -# For generating screenshots -playwright diff --git a/scripts/contributors.py b/scripts/contributors.py index 251558de7f..af1434d795 100644 --- a/scripts/contributors.py +++ b/scripts/contributors.py @@ -282,9 +282,10 @@ def main() -> None: return logging.info("Setting up GitHub Actions git user") - subprocess.run(["git", "config", "user.name", "github-actions"], check=True) + subprocess.run(["git", "config", "user.name", "github-actions[bot]"], check=True) subprocess.run( - ["git", "config", "user.email", "github-actions@github.com"], check=True + ["git", "config", "user.email", "github-actions[bot]@users.noreply.github.com"], + check=True, ) branch_name = f"fastapi-people-contributors-{secrets.token_hex(4)}" logging.info(f"Creating a new branch {branch_name}") diff --git a/scripts/doc_parsing_utils.py b/scripts/doc_parsing_utils.py new file mode 100644 index 0000000000..79f2e9ec0a --- /dev/null +++ b/scripts/doc_parsing_utils.py @@ -0,0 +1,733 @@ +import re +from typing import TypedDict, Union + +CODE_INCLUDE_RE = re.compile(r"^\{\*\s*(\S+)\s*(.*)\*\}$") +CODE_INCLUDE_PLACEHOLDER = "" + +HEADER_WITH_PERMALINK_RE = re.compile(r"^(#{1,6}) (.+?)(\s*\{\s*#.*\s*\})?\s*$") +HEADER_LINE_RE = re.compile(r"^(#{1,6}) (.+?)(?:\s*\{\s*(#.*)\s*\})?\s*$") + +TIANGOLO_COM = "https://fastapi.tiangolo.com" +ASSETS_URL_PREFIXES = ("/img/", "/css/", "/js/") + +MARKDOWN_LINK_RE = re.compile( + r"(?.*?)\]" # link text (non-greedy) + r"\(" + r"(?P[^)\s]+)" # url (no spaces and `)`) + r'(?:\s+["\'](?P.*?)["\'])?' # optional title in "" or '' + r"\)" + r"(?:\s*\{(?P<attrs>[^}]*)\})?" # optional attributes in {} +) + +HTML_LINK_RE = re.compile(r"<a\s+[^>]*>.*?</a>") +HTML_LINK_TEXT_RE = re.compile(r"<a\b([^>]*)>(.*?)</a>") +HTML_LINK_OPEN_TAG_RE = re.compile(r"<a\b([^>]*)>") +HTML_ATTR_RE = re.compile(r'(\w+)\s*=\s*([\'"])(.*?)\2') + +CODE_BLOCK_LANG_RE = re.compile(r"^`{3,4}([\w-]*)", re.MULTILINE) + +SLASHES_COMMENT_RE = re.compile( + r"^(?P<code>.*?)(?P<comment>(?:(?<= )// .*)|(?:^// .*))?$" +) + +HASH_COMMENT_RE = re.compile(r"^(?P<code>.*?)(?P<comment>(?:(?<= )# .*)|(?:^# .*))?$") + + +class CodeIncludeInfo(TypedDict): + line_no: int + line: str + + +class HeaderPermalinkInfo(TypedDict): + line_no: int + hashes: str + title: str + permalink: str + + +class MarkdownLinkInfo(TypedDict): + line_no: int + url: str + text: str + title: Union[str, None] + attributes: Union[str, None] + full_match: str + + +class HTMLLinkAttribute(TypedDict): + name: str + quote: str + value: str + + +class HtmlLinkInfo(TypedDict): + line_no: int + full_tag: str + attributes: list[HTMLLinkAttribute] + text: str + + +class MultilineCodeBlockInfo(TypedDict): + lang: str + start_line_no: int + content: list[str] + + +# Code includes +# -------------------------------------------------------------------------------------- + + +def extract_code_includes(lines: list[str]) -> list[CodeIncludeInfo]: + """ + Extract lines that contain code includes. + + Return list of CodeIncludeInfo, where each dict contains: + - `line_no` - line number (1-based) + - `line` - text of the line + """ + + includes: list[CodeIncludeInfo] = [] + for line_no, line in enumerate(lines, start=1): + if CODE_INCLUDE_RE.match(line): + includes.append(CodeIncludeInfo(line_no=line_no, line=line)) + return includes + + +def replace_code_includes_with_placeholders(text: list[str]) -> list[str]: + """ + Replace code includes with placeholders. + """ + + modified_text = text.copy() + includes = extract_code_includes(text) + for include in includes: + modified_text[include["line_no"] - 1] = CODE_INCLUDE_PLACEHOLDER + return modified_text + + +def replace_placeholders_with_code_includes( + text: list[str], original_includes: list[CodeIncludeInfo] +) -> list[str]: + """ + Replace code includes placeholders with actual code includes from the original (English) document. + Fail if the number of placeholders does not match the number of original includes. + """ + + code_include_lines = [ + line_no + for line_no, line in enumerate(text) + if line.strip() == CODE_INCLUDE_PLACEHOLDER + ] + + if len(code_include_lines) != len(original_includes): + raise ValueError( + "Number of code include placeholders does not match the number of code includes " + "in the original document " + f"({len(code_include_lines)} vs {len(original_includes)})" + ) + + modified_text = text.copy() + for i, line_no in enumerate(code_include_lines): + modified_text[line_no] = original_includes[i]["line"] + + return modified_text + + +# Header permalinks +# -------------------------------------------------------------------------------------- + + +def extract_header_permalinks(lines: list[str]) -> list[HeaderPermalinkInfo]: + """ + Extract list of header permalinks from the given lines. + + Return list of HeaderPermalinkInfo, where each dict contains: + - `line_no` - line number (1-based) + - `hashes` - string of hashes representing header level (e.g., "###") + - `permalink` - permalink string (e.g., "{#permalink}") + """ + + headers: list[HeaderPermalinkInfo] = [] + in_code_block3 = False + in_code_block4 = False + + for line_no, line in enumerate(lines, start=1): + if not (in_code_block3 or in_code_block4): + if line.startswith("```"): + count = len(line) - len(line.lstrip("`")) + if count == 3: + in_code_block3 = True + continue + elif count >= 4: + in_code_block4 = True + continue + + header_match = HEADER_WITH_PERMALINK_RE.match(line) + if header_match: + hashes, title, permalink = header_match.groups() + headers.append( + HeaderPermalinkInfo( + hashes=hashes, line_no=line_no, permalink=permalink, title=title + ) + ) + + elif in_code_block3: + if line.startswith("```"): + count = len(line) - len(line.lstrip("`")) + if count == 3: + in_code_block3 = False + continue + + elif in_code_block4: + if line.startswith("````"): + count = len(line) - len(line.lstrip("`")) + if count >= 4: + in_code_block4 = False + continue + + return headers + + +def remove_header_permalinks(lines: list[str]) -> list[str]: + """ + Remove permalinks from headers in the given lines. + """ + + modified_lines: list[str] = [] + for line in lines: + header_match = HEADER_WITH_PERMALINK_RE.match(line) + if header_match: + hashes, title, _permalink = header_match.groups() + modified_line = f"{hashes} {title}" + modified_lines.append(modified_line) + else: + modified_lines.append(line) + return modified_lines + + +def replace_header_permalinks( + text: list[str], + header_permalinks: list[HeaderPermalinkInfo], + original_header_permalinks: list[HeaderPermalinkInfo], +) -> list[str]: + """ + Replace permalinks in the given text with the permalinks from the original document. + + Fail if the number or level of headers does not match the original. + """ + + modified_text: list[str] = text.copy() + + if len(header_permalinks) != len(original_header_permalinks): + raise ValueError( + "Number of headers with permalinks does not match the number in the " + "original document " + f"({len(header_permalinks)} vs {len(original_header_permalinks)})" + ) + + for header_no in range(len(header_permalinks)): + header_info = header_permalinks[header_no] + original_header_info = original_header_permalinks[header_no] + + if header_info["hashes"] != original_header_info["hashes"]: + raise ValueError( + "Header levels do not match between document and original document" + f" (found {header_info['hashes']}, expected {original_header_info['hashes']})" + f" for header №{header_no + 1} in line {header_info['line_no']}" + ) + line_no = header_info["line_no"] - 1 + hashes = header_info["hashes"] + title = header_info["title"] + permalink = original_header_info["permalink"] + modified_text[line_no] = f"{hashes} {title}{permalink}" + + return modified_text + + +# Markdown links +# -------------------------------------------------------------------------------------- + + +def extract_markdown_links(lines: list[str]) -> list[MarkdownLinkInfo]: + """ + Extract all markdown links from the given lines. + + Return list of MarkdownLinkInfo, where each dict contains: + - `line_no` - line number (1-based) + - `url` - link URL + - `text` - link text + - `title` - link title (if any) + """ + + links: list[MarkdownLinkInfo] = [] + for line_no, line in enumerate(lines, start=1): + for m in MARKDOWN_LINK_RE.finditer(line): + links.append( + MarkdownLinkInfo( + line_no=line_no, + url=m.group("url"), + text=m.group("text"), + title=m.group("title"), + attributes=m.group("attrs"), + full_match=m.group(0), + ) + ) + return links + + +def _add_lang_code_to_url(url: str, lang_code: str) -> str: + if url.startswith(TIANGOLO_COM): + rel_url = url[len(TIANGOLO_COM) :] + if not rel_url.startswith(ASSETS_URL_PREFIXES): + url = url.replace(TIANGOLO_COM, f"{TIANGOLO_COM}/{lang_code}") + return url + + +def _construct_markdown_link( + url: str, + text: str, + title: Union[str, None], + attributes: Union[str, None], + lang_code: str, +) -> str: + """ + Construct a markdown link, adjusting the URL for the given language code if needed. + """ + url = _add_lang_code_to_url(url, lang_code) + + if title: + link = f'[{text}]({url} "{title}")' + else: + link = f"[{text}]({url})" + + if attributes: + link += f"{{{attributes}}}" + + return link + + +def replace_markdown_links( + text: list[str], + links: list[MarkdownLinkInfo], + original_links: list[MarkdownLinkInfo], + lang_code: str, +) -> list[str]: + """ + Replace markdown links in the given text with the original links. + + Fail if the number of links does not match the original. + """ + + if len(links) != len(original_links): + raise ValueError( + "Number of markdown links does not match the number in the " + "original document " + f"({len(links)} vs {len(original_links)})" + ) + + modified_text = text.copy() + for i, link_info in enumerate(links): + link_text = link_info["text"] + link_title = link_info["title"] + original_link_info = original_links[i] + + # Replace + replacement_link = _construct_markdown_link( + url=original_link_info["url"], + text=link_text, + title=link_title, + attributes=original_link_info["attributes"], + lang_code=lang_code, + ) + line_no = link_info["line_no"] - 1 + modified_line = modified_text[line_no] + modified_line = modified_line.replace( + link_info["full_match"], replacement_link, 1 + ) + modified_text[line_no] = modified_line + + return modified_text + + +# HTML links +# -------------------------------------------------------------------------------------- + + +def extract_html_links(lines: list[str]) -> list[HtmlLinkInfo]: + """ + Extract all HTML links from the given lines. + + Return list of HtmlLinkInfo, where each dict contains: + - `line_no` - line number (1-based) + - `full_tag` - full HTML link tag + - `attributes` - list of HTMLLinkAttribute (name, quote, value) + - `text` - link text + """ + + links = [] + for line_no, line in enumerate(lines, start=1): + for html_link in HTML_LINK_RE.finditer(line): + link_str = html_link.group(0) + + link_text_match = HTML_LINK_TEXT_RE.match(link_str) + assert link_text_match is not None + link_text = link_text_match.group(2) + assert isinstance(link_text, str) + + link_open_tag_match = HTML_LINK_OPEN_TAG_RE.match(link_str) + assert link_open_tag_match is not None + link_open_tag = link_open_tag_match.group(1) + assert isinstance(link_open_tag, str) + + attributes: list[HTMLLinkAttribute] = [] + for attr_name, attr_quote, attr_value in re.findall( + HTML_ATTR_RE, link_open_tag + ): + assert isinstance(attr_name, str) + assert isinstance(attr_quote, str) + assert isinstance(attr_value, str) + attributes.append( + HTMLLinkAttribute( + name=attr_name, quote=attr_quote, value=attr_value + ) + ) + links.append( + HtmlLinkInfo( + line_no=line_no, + full_tag=link_str, + attributes=attributes, + text=link_text, + ) + ) + return links + + +def _construct_html_link( + link_text: str, + attributes: list[HTMLLinkAttribute], + lang_code: str, +) -> str: + """ + Reconstruct HTML link, adjusting the URL for the given language code if needed. + """ + + attributes_upd: list[HTMLLinkAttribute] = [] + for attribute in attributes: + if attribute["name"] == "href": + original_url = attribute["value"] + url = _add_lang_code_to_url(original_url, lang_code) + attributes_upd.append( + HTMLLinkAttribute(name="href", quote=attribute["quote"], value=url) + ) + else: + attributes_upd.append(attribute) + + attrs_str = " ".join( + f"{attribute['name']}={attribute['quote']}{attribute['value']}{attribute['quote']}" + for attribute in attributes_upd + ) + return f"<a {attrs_str}>{link_text}</a>" + + +def replace_html_links( + text: list[str], + links: list[HtmlLinkInfo], + original_links: list[HtmlLinkInfo], + lang_code: str, +) -> list[str]: + """ + Replace HTML links in the given text with the links from the original document. + + Adjust URLs for the given language code. + Fail if the number of links does not match the original. + """ + + if len(links) != len(original_links): + raise ValueError( + "Number of HTML links does not match the number in the " + "original document " + f"({len(links)} vs {len(original_links)})" + ) + + modified_text = text.copy() + for link_index, link in enumerate(links): + original_link_info = original_links[link_index] + + # Replace in the document text + replacement_link = _construct_html_link( + link_text=link["text"], + attributes=original_link_info["attributes"], + lang_code=lang_code, + ) + line_no = link["line_no"] - 1 + modified_text[line_no] = modified_text[line_no].replace( + link["full_tag"], replacement_link, 1 + ) + + return modified_text + + +# Multiline code blocks +# -------------------------------------------------------------------------------------- + + +def get_code_block_lang(line: str) -> str: + match = CODE_BLOCK_LANG_RE.match(line) + if match: + return match.group(1) + return "" + + +def extract_multiline_code_blocks(text: list[str]) -> list[MultilineCodeBlockInfo]: + blocks: list[MultilineCodeBlockInfo] = [] + + in_code_block3 = False + in_code_block4 = False + current_block_lang = "" + current_block_start_line = -1 + current_block_lines = [] + + for line_no, line in enumerate(text, start=1): + stripped = line.lstrip() + + # --- Detect opening fence --- + if not (in_code_block3 or in_code_block4): + if stripped.startswith("```"): + current_block_start_line = line_no + count = len(stripped) - len(stripped.lstrip("`")) + if count == 3: + in_code_block3 = True + current_block_lang = get_code_block_lang(stripped) + current_block_lines = [line] + continue + elif count >= 4: + in_code_block4 = True + current_block_lang = get_code_block_lang(stripped) + current_block_lines = [line] + continue + + # --- Detect closing fence --- + elif in_code_block3: + if stripped.startswith("```"): + count = len(stripped) - len(stripped.lstrip("`")) + if count == 3: + current_block_lines.append(line) + blocks.append( + MultilineCodeBlockInfo( + lang=current_block_lang, + start_line_no=current_block_start_line, + content=current_block_lines, + ) + ) + in_code_block3 = False + current_block_lang = "" + current_block_start_line = -1 + current_block_lines = [] + continue + current_block_lines.append(line) + + elif in_code_block4: + if stripped.startswith("````"): + count = len(stripped) - len(stripped.lstrip("`")) + if count >= 4: + current_block_lines.append(line) + blocks.append( + MultilineCodeBlockInfo( + lang=current_block_lang, + start_line_no=current_block_start_line, + content=current_block_lines, + ) + ) + in_code_block4 = False + current_block_lang = "" + current_block_start_line = -1 + current_block_lines = [] + continue + current_block_lines.append(line) + + return blocks + + +def _split_hash_comment(line: str) -> tuple[str, Union[str, None]]: + match = HASH_COMMENT_RE.match(line) + if match: + code = match.group("code").rstrip() + comment = match.group("comment") + return code, comment + return line.rstrip(), None + + +def _split_slashes_comment(line: str) -> tuple[str, Union[str, None]]: + match = SLASHES_COMMENT_RE.match(line) + if match: + code = match.group("code").rstrip() + comment = match.group("comment") + return code, comment + return line, None + + +def replace_multiline_code_block( + block_a: MultilineCodeBlockInfo, block_b: MultilineCodeBlockInfo +) -> list[str]: + """ + Replace multiline code block `a` with block `b` leaving comments intact. + + Syntax of comments depends on the language of the code block. + Raises ValueError if the blocks are not compatible (different languages or different number of lines). + """ + + start_line = block_a["start_line_no"] + end_line_no = start_line + len(block_a["content"]) - 1 + + if block_a["lang"] != block_b["lang"]: + raise ValueError( + f"Code block (lines {start_line}-{end_line_no}) " + "has different language than the original block " + f"('{block_a['lang']}' vs '{block_b['lang']}')" + ) + if len(block_a["content"]) != len(block_b["content"]): + raise ValueError( + f"Code block (lines {start_line}-{end_line_no}) " + "has different number of lines than the original block " + f"({len(block_a['content'])} vs {len(block_b['content'])})" + ) + + block_language = block_a["lang"].lower() + if block_language in {"mermaid"}: + if block_a != block_b: + print( + f"Skipping mermaid code block replacement (lines {start_line}-{end_line_no}). " + "This should be checked manually." + ) + return block_a["content"].copy() # We don't handle mermaid code blocks for now + + code_block: list[str] = [] + for line_a, line_b in zip(block_a["content"], block_b["content"]): + line_a_comment: Union[str, None] = None + line_b_comment: Union[str, None] = None + + # Handle comments based on language + if block_language in { + "python", + "py", + "sh", + "bash", + "dockerfile", + "requirements", + "gitignore", + "toml", + "yaml", + "yml", + "hash-style-comments", + }: + _line_a_code, line_a_comment = _split_hash_comment(line_a) + _line_b_code, line_b_comment = _split_hash_comment(line_b) + res_line = line_b + if line_b_comment: + res_line = res_line.replace(line_b_comment, line_a_comment, 1) + code_block.append(res_line) + elif block_language in {"console", "json", "slash-style-comments"}: + _line_a_code, line_a_comment = _split_slashes_comment(line_a) + _line_b_code, line_b_comment = _split_slashes_comment(line_b) + res_line = line_b + if line_b_comment: + res_line = res_line.replace(line_b_comment, line_a_comment, 1) + code_block.append(res_line) + else: + code_block.append(line_b) + + return code_block + + +def replace_multiline_code_blocks_in_text( + text: list[str], + code_blocks: list[MultilineCodeBlockInfo], + original_code_blocks: list[MultilineCodeBlockInfo], +) -> list[str]: + """ + Update each code block in `text` with the corresponding code block from + `original_code_blocks` with comments taken from `code_blocks`. + + Raises ValueError if the number, language, or shape of code blocks do not match. + """ + + if len(code_blocks) != len(original_code_blocks): + raise ValueError( + "Number of code blocks does not match the number in the original document " + f"({len(code_blocks)} vs {len(original_code_blocks)})" + ) + + modified_text = text.copy() + for block, original_block in zip(code_blocks, original_code_blocks): + updated_content = replace_multiline_code_block(block, original_block) + + start_line_index = block["start_line_no"] - 1 + for i, updated_line in enumerate(updated_content): + modified_text[start_line_index + i] = updated_line + + return modified_text + + +# All checks +# -------------------------------------------------------------------------------------- + + +def check_translation( + doc_lines: list[str], + en_doc_lines: list[str], + lang_code: str, + auto_fix: bool, + path: str, +) -> list[str]: + # Fix code includes + en_code_includes = extract_code_includes(en_doc_lines) + doc_lines_with_placeholders = replace_code_includes_with_placeholders(doc_lines) + fixed_doc_lines = replace_placeholders_with_code_includes( + doc_lines_with_placeholders, en_code_includes + ) + if auto_fix and (fixed_doc_lines != doc_lines): + print(f"Fixing code includes in: {path}") + doc_lines = fixed_doc_lines + + # Fix permalinks + en_permalinks = extract_header_permalinks(en_doc_lines) + doc_permalinks = extract_header_permalinks(doc_lines) + fixed_doc_lines = replace_header_permalinks( + doc_lines, doc_permalinks, en_permalinks + ) + if auto_fix and (fixed_doc_lines != doc_lines): + print(f"Fixing header permalinks in: {path}") + doc_lines = fixed_doc_lines + + # Fix markdown links + en_markdown_links = extract_markdown_links(en_doc_lines) + doc_markdown_links = extract_markdown_links(doc_lines) + fixed_doc_lines = replace_markdown_links( + doc_lines, doc_markdown_links, en_markdown_links, lang_code + ) + if auto_fix and (fixed_doc_lines != doc_lines): + print(f"Fixing markdown links in: {path}") + doc_lines = fixed_doc_lines + + # Fix HTML links + en_html_links = extract_html_links(en_doc_lines) + doc_html_links = extract_html_links(doc_lines) + fixed_doc_lines = replace_html_links( + doc_lines, doc_html_links, en_html_links, lang_code + ) + if auto_fix and (fixed_doc_lines != doc_lines): + print(f"Fixing HTML links in: {path}") + doc_lines = fixed_doc_lines + + # Fix multiline code blocks + en_code_blocks = extract_multiline_code_blocks(en_doc_lines) + doc_code_blocks = extract_multiline_code_blocks(doc_lines) + fixed_doc_lines = replace_multiline_code_blocks_in_text( + doc_lines, doc_code_blocks, en_code_blocks + ) + if auto_fix and (fixed_doc_lines != doc_lines): + print(f"Fixing multiline code blocks in: {path}") + doc_lines = fixed_doc_lines + + return doc_lines diff --git a/scripts/docs.py b/scripts/docs.py index 56ffb9d364..20bf1c1688 100644 --- a/scripts/docs.py +++ b/scripts/docs.py @@ -4,21 +4,32 @@ import os import re import shutil import subprocess -from functools import lru_cache +from html.parser import HTMLParser from http.server import HTTPServer, SimpleHTTPRequestHandler -from importlib import metadata from multiprocessing import Pool from pathlib import Path -from typing import Any, Dict, List, Optional, Union +from typing import Any, Optional, Union import mkdocs.utils import typer import yaml from jinja2 import Template from ruff.__main__ import find_ruff_bin +from slugify import slugify as py_slugify logging.basicConfig(level=logging.INFO) +SUPPORTED_LANGS = { + "en", + "de", + "es", + "ko", + "pt", + "ru", + "uk", +} + + app = typer.Typer() mkdocs_name = "mkdocs.yml" @@ -27,8 +38,8 @@ missing_translation_snippet = """ {!../../docs/missing-translation.md!} """ -non_translated_sections = [ - "reference/", +non_translated_sections = ( + f"reference{os.sep}", "release-notes.md", "fastapi-people.md", "external-links.md", @@ -36,7 +47,7 @@ non_translated_sections = [ "management-tasks.md", "management.md", "contributing.md", -] +) docs_path = Path("docs") en_docs_path = Path("docs/en") @@ -44,20 +55,46 @@ en_config_path: Path = en_docs_path / mkdocs_name site_path = Path("site").absolute() build_site_path = Path("site_build").absolute() +header_pattern = re.compile(r"^(#{1,6}) (.+?)(?:\s*\{\s*(#.*)\s*\})?\s*$") header_with_permalink_pattern = re.compile(r"^(#{1,6}) (.+?)(\s*\{\s*#.*\s*\})\s*$") +code_block3_pattern = re.compile(r"^\s*```") +code_block4_pattern = re.compile(r"^\s*````") -@lru_cache -def is_mkdocs_insiders() -> bool: - version = metadata.version("mkdocs-material") - return "insiders" in version +class VisibleTextExtractor(HTMLParser): + """Extract visible text from a string with HTML tags.""" + + def __init__(self): + super().__init__() + self.text_parts = [] + + def handle_data(self, data): + self.text_parts.append(data) + + def extract_visible_text(self, html: str) -> str: + self.reset() + self.text_parts = [] + self.feed(html) + return "".join(self.text_parts).strip() -def get_en_config() -> Dict[str, Any]: +def slugify(text: str) -> str: + return py_slugify( + text, + replacements=[ + ("`", ""), # `dict`s -> dicts + ("'s", "s"), # it's -> its + ("'t", "t"), # don't -> dont + ("**", ""), # **FastAPI**s -> FastAPIs + ], + ) + + +def get_en_config() -> dict[str, Any]: return mkdocs.utils.yaml_load(en_config_path.read_text(encoding="utf-8")) -def get_lang_paths() -> List[Path]: +def get_lang_paths() -> list[Path]: return sorted(docs_path.iterdir()) @@ -77,9 +114,7 @@ def complete_existing_lang(incomplete: str): @app.callback() def callback() -> None: - if is_mkdocs_insiders(): - os.environ["INSIDERS_FILE"] = "../en/mkdocs.insiders.yml" - # For MacOS with insiders and Cairo + # For MacOS with Cairo os.environ["DYLD_FALLBACK_LIBRARY_PATH"] = "/opt/homebrew/lib" @@ -95,14 +130,9 @@ def new_lang(lang: str = typer.Argument(..., callback=lang_callback)): new_path.mkdir() new_config_path: Path = Path(new_path) / mkdocs_name new_config_path.write_text("INHERIT: ../en/mkdocs.yml\n", encoding="utf-8") - new_config_docs_path: Path = new_path / "docs" - new_config_docs_path.mkdir() - en_index_path: Path = en_docs_path / "docs" / "index.md" - new_index_path: Path = new_config_docs_path / "index.md" - en_index_content = en_index_path.read_text(encoding="utf-8") - new_index_content = f"{missing_translation_snippet}\n\n{en_index_content}" - new_index_path.write_text(new_index_content, encoding="utf-8") - typer.secho(f"Successfully initialized: {new_path}", color=typer.colors.GREEN) + new_llm_prompt_path: Path = new_path / "llm-prompt.md" + new_llm_prompt_path.write_text("", encoding="utf-8") + print(f"Successfully initialized: {new_path}") update_languages() @@ -115,10 +145,6 @@ def build_lang( """ Build the docs for a language. """ - insiders_env_file = os.environ.get("INSIDERS_FILE") - print(f"Insiders file {insiders_env_file}") - if is_mkdocs_insiders(): - print("Using insiders") lang_path: Path = Path("docs") / lang if not lang_path.is_dir(): typer.echo(f"The language translation doesn't seem to exist yet: {lang}") @@ -145,14 +171,20 @@ def build_lang( index_sponsors_template = """ -{% if sponsors %} +### Keystone Sponsor + +{% for sponsor in sponsors.keystone -%} +<a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}"></a> +{% endfor %} +### Gold and Silver Sponsors + {% for sponsor in sponsors.gold -%} <a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}"></a> {% endfor -%} {%- for sponsor in sponsors.silver -%} <a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}"></a> {% endfor %} -{% endif %} + """ @@ -203,27 +235,15 @@ def generate_readme() -> None: """ Generate README.md content from main index.md """ - typer.echo("Generating README") readme_path = Path("README.md") + old_content = readme_path.read_text("utf-8") new_content = generate_readme_content() - readme_path.write_text(new_content, encoding="utf-8") - - -@app.command() -def verify_readme() -> None: - """ - Verify README.md content from main index.md - """ - typer.echo("Verifying README") - readme_path = Path("README.md") - generated_content = generate_readme_content() - readme_content = readme_path.read_text("utf-8") - if generated_content != readme_content: - typer.secho( - "README.md outdated from the latest index.md", color=typer.colors.RED - ) - raise typer.Abort() - typer.echo("Valid README ✅") + if new_content != old_content: + print("README.md outdated from the latest index.md") + print("Updating README.md") + readme_path.write_text(new_content, encoding="utf-8") + raise typer.Exit(1) + print("README.md is up to date ✅") @app.command() @@ -234,7 +254,11 @@ def build_all() -> None: """ update_languages() shutil.rmtree(site_path, ignore_errors=True) - langs = [lang.name for lang in get_lang_paths() if lang.is_dir()] + langs = [ + lang.name + for lang in get_lang_paths() + if (lang.is_dir() and lang.name in SUPPORTED_LANGS) + ] cpu_count = os.cpu_count() or 1 process_pool_size = cpu_count * 4 typer.echo(f"Using process pool size: {process_pool_size}") @@ -247,7 +271,17 @@ def update_languages() -> None: """ Update the mkdocs.yml file Languages section including all the available languages. """ - update_config() + old_config = get_en_config() + updated_config = get_updated_config_content() + if old_config != updated_config: + print("docs/en/mkdocs.yml outdated") + print("Updating docs/en/mkdocs.yml") + en_config_path.write_text( + yaml.dump(updated_config, sort_keys=False, width=200, allow_unicode=True), + encoding="utf-8", + ) + raise typer.Exit(1) + print("docs/en/mkdocs.yml is up to date ✅") @app.command() @@ -301,19 +335,22 @@ def live( ) -def get_updated_config_content() -> Dict[str, Any]: +def get_updated_config_content() -> dict[str, Any]: config = get_en_config() languages = [{"en": "/"}] - new_alternate: List[Dict[str, str]] = [] + new_alternate: list[dict[str, str]] = [] # Language names sourced from https://quickref.me/iso-639-1 # Contributors may wish to update or change these, e.g. to fix capitalization. language_names_path = Path(__file__).parent / "../docs/language_names.yml" - local_language_names: Dict[str, str] = mkdocs.utils.yaml_load( + local_language_names: dict[str, str] = mkdocs.utils.yaml_load( language_names_path.read_text(encoding="utf-8") ) for lang_path in get_lang_paths(): if lang_path.name in {"en", "em"} or not lang_path.is_dir(): continue + if lang_path.name not in SUPPORTED_LANGS: + # Skip languages that are not yet ready + continue code = lang_path.name languages.append({code: f"/{code}/"}) for lang_dict in languages: @@ -327,44 +364,16 @@ def get_updated_config_content() -> Dict[str, Any]: raise typer.Abort() use_name = f"{code} - {local_language_names[code]}" new_alternate.append({"link": url, "name": use_name}) - new_alternate.append({"link": "/em/", "name": "😉"}) config["extra"]["alternate"] = new_alternate return config -def update_config() -> None: - config = get_updated_config_content() - en_config_path.write_text( - yaml.dump(config, sort_keys=False, width=200, allow_unicode=True), - encoding="utf-8", - ) - - @app.command() -def verify_config() -> None: +def ensure_non_translated() -> None: """ - Verify main mkdocs.yml content to make sure it uses the latest language names. + Ensure there are no files in the non translatable pages. """ - typer.echo("Verifying mkdocs.yml") - config = get_en_config() - updated_config = get_updated_config_content() - if config != updated_config: - typer.secho( - "docs/en/mkdocs.yml outdated from docs/language_names.yml, " - "update language_names.yml and run " - "python ./scripts/docs.py update-languages", - color=typer.colors.RED, - ) - raise typer.Abort() - typer.echo("Valid mkdocs.yml ✅") - - -@app.command() -def verify_non_translated() -> None: - """ - Verify there are no files in the non translatable pages. - """ - print("Verifying non translated pages") + print("Ensuring no non translated pages") lang_paths = get_lang_paths() error_paths = [] for lang in lang_paths: @@ -375,25 +384,22 @@ def verify_non_translated() -> None: if non_translatable_path.exists(): error_paths.append(non_translatable_path) if error_paths: - print("Non-translated pages found, remove them:") + print("Non-translated pages found, removing them:") for error_path in error_paths: print(error_path) - raise typer.Abort() + if error_path.is_file(): + error_path.unlink() + else: + shutil.rmtree(error_path) + raise typer.Exit(1) print("No non-translated pages found ✅") -@app.command() -def verify_docs(): - verify_readme() - verify_config() - verify_non_translated() - - @app.command() def langs_json(): langs = [] for lang_path in get_lang_paths(): - if lang_path.is_dir(): + if lang_path.is_dir() and lang_path.name in SUPPORTED_LANGS: langs.append(lang_path.name) print(json.dumps(langs)) @@ -434,5 +440,83 @@ def generate_docs_src_versions_for_file(file_path: Path) -> None: version_file.write_text(content_format, encoding="utf-8") +@app.command() +def add_permalinks_page(path: Path, update_existing: bool = False): + """ + Add or update header permalinks in specific page of En docs. + """ + + if not path.is_relative_to(en_docs_path / "docs"): + raise RuntimeError(f"Path must be inside {en_docs_path}") + rel_path = path.relative_to(en_docs_path / "docs") + + # Skip excluded sections + if str(rel_path).startswith(non_translated_sections): + return + + visible_text_extractor = VisibleTextExtractor() + updated_lines = [] + in_code_block3 = False + in_code_block4 = False + permalinks = set() + + with path.open("r", encoding="utf-8") as f: + lines = f.readlines() + + for line in lines: + # Handle codeblocks start and end + if not (in_code_block3 or in_code_block4): + if code_block4_pattern.match(line): + in_code_block4 = True + elif code_block3_pattern.match(line): + in_code_block3 = True + else: + if in_code_block4 and code_block4_pattern.match(line): + in_code_block4 = False + elif in_code_block3 and code_block3_pattern.match(line): + in_code_block3 = False + + # Process Headers only outside codeblocks + if not (in_code_block3 or in_code_block4): + match = header_pattern.match(line) + if match: + hashes, title, _permalink = match.groups() + if (not _permalink) or update_existing: + slug = slugify(visible_text_extractor.extract_visible_text(title)) + if slug in permalinks: + # If the slug is already used, append a number to make it unique + count = 1 + original_slug = slug + while slug in permalinks: + slug = f"{original_slug}_{count}" + count += 1 + permalinks.add(slug) + + line = f"{hashes} {title} {{ #{slug} }}\n" + + updated_lines.append(line) + + with path.open("w", encoding="utf-8") as f: + f.writelines(updated_lines) + + +@app.command() +def add_permalinks_pages(pages: list[Path], update_existing: bool = False) -> None: + """ + Add or update header permalinks in specific pages of En docs. + """ + for md_file in pages: + add_permalinks_page(md_file, update_existing=update_existing) + + +@app.command() +def add_permalinks(update_existing: bool = False) -> None: + """ + Add or update header permalinks in all pages of En docs. + """ + for md_file in en_docs_path.rglob("*.md"): + add_permalinks_page(md_file, update_existing=update_existing) + + if __name__ == "__main__": app() diff --git a/scripts/general-llm-prompt.md b/scripts/general-llm-prompt.md new file mode 100644 index 0000000000..d45ab8eb07 --- /dev/null +++ b/scripts/general-llm-prompt.md @@ -0,0 +1,528 @@ +### Your task + +Translate an English original content to a target language. + +The original content is written in Markdown, write the translation in Markdown as well. + +The original content will be surrounded by triple percentage signs (%%%). Do not include the triple percentage signs in the translation. + +### Technical terms in English + +For technical terms in English that don't have a common translation term, use the original term in English. + +### Content of code snippets + +Do not translate the content of code snippets, keep the original in English. For example, `list`, `dict`, keep them as is. + +### Content of code blocks + +Do not translate the content of code blocks, except for comments in the language which the code block uses. + +Examples: + +Source (English) - The code block is a bash code example with one comment: + +```bash +# Print greeting +echo "Hello, World!" +``` + +Result (German): + +```bash +# Gruß ausgeben +echo "Hello, World!" +``` + +Source (English) - The code block is a console example containing HTML tags. No comments, so nothing to change here: + +```console +$ <font color="#4E9A06">fastapi</font> run <u style="text-decoration-style:solid">main.py</u> +<span style="background-color:#009485"><font color="#D3D7CF"> FastAPI </font></span> Starting server + Searching for package file structure +``` + +Result (German): + +```console +$ <font color="#4E9A06">fastapi</font> run <u style="text-decoration-style:solid">main.py</u> +<span style="background-color:#009485"><font color="#D3D7CF"> FastAPI </font></span> Starting server + Searching for package file structure +``` + +Source (English) - The code block is a console example containing 5 comments: + + +```console +// Go to the home directory +$ cd +// Create a directory for all your code projects +$ mkdir code +// Enter into that code directory +$ cd code +// Create a directory for this project +$ mkdir awesome-project +// Enter into that project directory +$ cd awesome-project +``` + +Result (German): + +```console +// Gehe zum Home-Verzeichnis +$ cd +// Erstelle ein Verzeichnis für alle Ihre Code-Projekte +$ mkdir code +// Gehe in dieses Code-Verzeichnis +$ cd code +// Erstelle ein Verzeichnis für dieses Projekt +$ mkdir awesome-project +// Gehe in dieses Projektverzeichnis +$ cd awesome-project +``` + +If there is an existing translation and its Mermaid diagram is in sync with the Mermaid diagram in the English source, except a few translated words, then use the Mermaid diagram of the existing translation. The human editor of the translation translated these words in the Mermaid diagram. Keep these translations, do not revert them back to the English source. + +Example: + +Source (English): + +```mermaid +flowchart LR + subgraph global[global env] + harry-1[harry v1] + end + subgraph stone-project[philosophers-stone project] + stone(philosophers-stone) -->|requires| harry-1 + end +``` + +Existing translation (German) - has three translations: + +```mermaid +flowchart LR + subgraph global[globale Umgebung] + harry-1[harry v1] + end + subgraph stone-project[philosophers-stone-Projekt] + stone(philosophers-stone) -->|benötigt| harry-1 + end +``` + +Result (German) - you change nothing: + +```mermaid +flowchart LR + subgraph global[globale Umgebung] + harry-1[harry v1] + end + subgraph stone-project[philosophers-stone-Projekt] + stone(philosophers-stone) -->|benötigt| harry-1 + end +``` + +### Special blocks + +There are special blocks of notes, tips and others that look like: + +/// note +Here goes a note +/// + +To translate it, keep the same line and add the translation after a vertical bar. + +For example, if you were translating to Spanish, you would write: + +/// note | Nota + +Some examples in Spanish: + +Source (English): + +/// tip + +Result (Spanish): + +/// tip | Consejo + +Source (English): + +/// details | Preview + +Result (Spanish): + +/// details | Vista previa + +### Tab blocks + +There are special blocks surrounded by four slashes (////). They mark text, which will be rendered as part of a tab in the final document. The scheme is: + +//// tab | {tab title} +{tab content, may span many lines} +//// + +Keep everything before the vertical bar (|) as is, including the vertical bar. Translate the tab title. Translate the tab content, applying the rules you know. Keep the four block closing slashes as is. + +Examples: + +Source (English): + +//// tab | Python 3.8+ non-Annotated +Hello +//// + +Result (German): + +//// tab | Python 3.8+ nicht annotiert +Hallo +//// + +Source (English) - Here there is nothing to translate in the tab title: + +//// tab | Linux, macOS, Windows Bash +Hello again +//// + +Result (German): + +//// tab | Linux, macOS, Windows Bash +Hallo wieder +//// + +### Headings + +Every Markdown heading in the English text (all levels) ends with a part inside curly brackets. This part denotes the hash of this heading, which is used in links to this heading. In translations, translate the heading, but do not translate this hash part, so that links do not break. + +Examples of how to translate a heading: + +Source (English): + +``` +## Alternative API docs { #alternative-api-docs } +``` + +Result (Spanish): + +``` +## Documentación de la API alternativa { #alternative-api-docs } +``` + +Source (English): + +``` +### Example { #example } +``` + +Result (German): + +``` +### Beispiel { #example } +``` + +### Links + +Use the following rules for links (apply both to Markdown-style links ([text](url)) and to HTML-style <a href="url">text</a> tags): + +- For relative URLs, only translate the link text. Do not translate the URL or its parts. + +Example: + +Source (English): + +``` +[One of the fastest Python frameworks available](#performance) +``` + +Result (German): + +``` +[Eines der schnellsten verfügbaren Python-Frameworks](#performance) +``` + +- For absolute URLs which DO NOT start EXACTLY with https://fastapi.tiangolo.com, only translate the link text and leave the URL unchanged. + +Example: + +Source (English): + +``` +<a href="https://sqlmodel.tiangolo.com/" class="external-link" target="_blank">SQLModel docs</a> +``` + +Result (German): + +``` +<a href="https://sqlmodel.tiangolo.com/" class="external-link" target="_blank">SQLModel-Dokumentation</a> +``` + +- For absolute URLs which DO start EXACTLY with https://fastapi.tiangolo.com, only translate the link text and change the URL by adding the language code (https://fastapi.tiangolo.com/{language_code}[rest part of the url]). + +Example: + +Source (English): + +``` +<a href="https://fastapi.tiangolo.com/tutorial/path-params/#documentation" class="external-link" target="_blank">Documentation</a> +``` + +Result (Spanish): + +``` +<a href="https://fastapi.tiangolo.com/es/tutorial/path-params/#documentation" class="external-link" target="_blank">Documentación</a> +``` + +- Do not add language codes for URLs that point to static assets (e.g., images, CSS, JavaScript). + +Example: + +Source (English): + +``` +<a href="https://fastapi.tiangolo.com/img/something.jpg" class="external-link" target="_blank">Something</a> +``` + +Result (Spanish): + +``` +<a href="https://fastapi.tiangolo.com/img/something.jpg" class="external-link" target="_blank">Algo</a> +``` + +- For internal links, only translate link text. + +Example: + +Source (English): + +``` +[Create Pull Requests](help-fastapi.md#create-a-pull-request){.internal-link target=_blank} +``` + +Result (German): + +``` +[Pull Requests erzeugen](help-fastapi.md#create-a-pull-request){.internal-link target=_blank} +``` + +- Do not translate anchor fragments in links (the part after `#`), as they must remain the same to work correctly. + +- If an existing translation has a link with an anchor fragment different to the anchor fragment in the English source, then this is an error. Fix this by using the anchor fragment of the English source. + +Example: + +Source (English): + +``` +[Body - Multiple Parameters: Singular values in body](body-multiple-params.md#singular-values-in-body){.internal-link target=_blank} +``` + +Existing wrong translation (German) - notice the wrongly translated anchor fragment: + +``` +[Body - Mehrere Parameter: Einfache Werte im Body](body-multiple-params.md#einzelne-werte-im-body){.internal-link target=_blank}. +``` + +Result (German) - you fix the anchor fragment: + +``` +[Body - Mehrere Parameter: Einfache Werte im Body](body-multiple-params.md#singular-values-in-body){.internal-link target=_blank}. +``` + +- Do not add anchor fragments at will, even if this makes sense. If the English source has no anchor, don't add one. + +Example: + +Source (English): + +``` +Create a [virtual environment](../virtual-environments.md){.internal-link target=_blank} +``` + +Wrong translation in German - Anchor added to the URL. + +``` +Erstelle eine [virtuelle Umgebung](../virtual-environments.md#create-a-virtual-environment){.internal-link target=_blank} +``` + +Good translation (German) - URL stays like in the English source. + +``` +Erstelle eine [Virtuelle Umgebung](../virtual-environments.md){.internal-link target=_blank} +``` + +### HTML abbr elements + +Translate HTML abbr elements (`<abbr title="description">text</abbr>`) as follows: + +- If the text surrounded by the abbr element is an abbreviation (the text may be surrounded by further HTML or Markdown markup or quotes, for example <code>text</code> or `text` or "text", ignore that further markup when deciding if the text is an abbreviation), and if the description (the text inside the title attribute) contains the full phrase for this abbreviation, then append a dash (-) to the full phrase, followed by the translation of the full phrase. + +Conversion scheme: + +Source (English): + +``` +<abbr title="{full phrase}">{abbreviation}</abbr> +``` + +Result: + +``` +<abbr title="{full phrase} - {translation of full phrase}">{abbreviation}</abbr> +``` + +Examples: + +Source (English): + +``` +<abbr title="Internet of Things">IoT</abbr> +<abbr title="Central Processing Unit">CPU</abbr> +<abbr title="too long; didn't read"><strong>TL;DR:</strong></abbr> +``` + +Result (German): + +``` +<abbr title="Internet of Things - Internet der Dinge">IoT</abbr> +<abbr title="Central Processing Unit - Zentrale Verarbeitungseinheit">CPU</abbr> +<abbr title="too long; didn't read - zu lang; hab's nicht gelesen"><strong>TL;DR:</strong></abbr> +``` + +- If the language to which you translate mostly uses the letters of the ASCII char set (for example Spanish, French, German, but not Russian, Chinese) and if the translation of the full phrase is identical to, or starts with the same letters as the original full phrase, then only give the translation of the full phrase. + +Conversion scheme: + +Source (English): + +``` +<abbr title="{full phrase}">{abbreviation}</abbr> +``` + +Result: + +``` +<abbr title="{translation of full phrase}">{abbreviation}</abbr> +``` + +Examples: + +Source (English): + +``` +<abbr title="JSON Web Tokens">JWT</abbr> +<abbr title="Enumeration">Enum</abbr> +<abbr title="Asynchronous Server Gateway Interface">ASGI</abbr> +``` + +Result (German): + +``` +<abbr title="JSON Web Tokens">JWT</abbr> +<abbr title="Enumeration">Enum</abbr> +<abbr title="Asynchrones Server-Gateway-Interface">ASGI</abbr> +``` + +- If the description is not a full phrase for an abbreviation which the abbr element surrounds, but some other information, then just translate the description. + +Conversion scheme: + +Source (English): + +``` +<abbr title="{description}">{text}</abbr> +``` + +Result: + +``` +<abbr title="{translation of description}">{translation of text}</abbr> +``` + +Examples: + + Source (English): + +``` +<abbr title="also known as: endpoints, routes">path</abbr> +<abbr title="a program that checks for code errors">linter</abbr> +<abbr title="converting the string that comes from an HTTP request into Python data">parsing</abbr> +<abbr title="before 2023-03">0.95.0</abbr> +<abbr title="2023-08-26">at the time of writing this</abbr> +``` + +Result (German): + +``` +<abbr title="auch bekannt als: Endpunkte, Routen">Pfad</abbr> +<abbr title="Programm das auf Fehler im Code prüft">Linter</abbr> +<abbr title="Konvertieren des Strings eines HTTP-Requests in Python-Daten">Parsen</abbr> +<abbr title="vor 2023-03">0.95.0</abbr> +<abbr title="2023-08-26">zum Zeitpunkt als das hier geschrieben wurde</abbr> +``` + +- If the text surrounded by the abbr element is an abbreviation and the description contains both the full phrase for that abbreviation, and other information, separated by a colon (`:`), then append a dash (`-`) and the translation of the full phrase to the original full phrase and translate the other information. + +Conversion scheme: + +Source (English): + +``` +<abbr title="{full phrase}: {other information}">{abbreviation}</abbr> +``` + +Result: + +``` +<abbr title="{full phrase} - {translation of full phrase}: {translation of other information}">{abbreviation}</abbr> +``` + +Examples: + +Source (English): + +``` +<abbr title="Input/Output: disk reading or writing, network communication.">I/O</abbr> +<abbr title="Content Delivery Network: service, that provides static files.">CDN</abbr> +<abbr title="Integrated Development Environment: similar to a code editor">IDE</abbr> +``` + +Result (German): + +``` +<abbr title="Input/Output - Eingabe/Ausgabe: Lesen oder Schreiben auf der Festplatte, Netzwerkkommunikation.">I/O</abbr> +<abbr title="Content Delivery Network - Inhalte auslieferndes Netzwerk: Dienst, der statische Dateien bereitstellt.">CDN</abbr> +<abbr title="Integrated Development Environment - Integrierte Entwicklungsumgebung: Ähnlich einem Code-Editor">IDE</abbr> +``` + +- You can leave the original full phrase away, if the translated full phrase is identical or starts with the same letters as the original full phrase. + +Conversion scheme: + +Source (English): + +``` +<abbr title="{full phrase}: {information}">{abbreviation}</abbr> +``` + +Result: + +``` +<abbr title="{translation of full phrase}: {translation of information}">{abbreviation}</abbr> +``` + +Example: + +Source (English): + +``` +<abbr title="Object Relational Mapper: a fancy term for a library where some classes represent SQL tables and instances represent rows in those tables">ORM</abbr> +``` + +Result (German): + +``` +<abbr title="Objektrelationaler Mapper: Ein Fachbegriff für eine Bibliothek, in der einige Klassen SQL-Tabellen und Instanzen Zeilen in diesen Tabellen darstellen">ORM</abbr> +``` + +- If there is an existing translation, and it has ADDITIONAL abbr elements in a sentence, and these additional abbr elements do not exist in the related sentence in the English text, then KEEP those additional abbr elements in the translation. Do not remove them. Except when you remove the whole sentence from the translation, because the whole sentence was removed from the English text, then also remove the abbr element. The reasoning for this rule is, that such additional abbr elements are manually added by the human editor of the translation, in order to translate or explain an English word to the human readers of the translation. These additional abbr elements would not make sense in the English text, but they do make sense in the translation. So keep them in the translation, even though they are not part of the English text. This rule only applies to abbr elements. + +- Apply above rules also when there is an existing translation! Make sure that all title attributes in abbr elements get properly translated or updated, using the schemes given above. However, leave the ADDITIONAL abbr's described above alone. Do not change their formatting or content. diff --git a/scripts/mkdocs_hooks.py b/scripts/mkdocs_hooks.py index b9e4ff59ed..4b781270a2 100644 --- a/scripts/mkdocs_hooks.py +++ b/scripts/mkdocs_hooks.py @@ -1,6 +1,6 @@ from functools import lru_cache from pathlib import Path -from typing import Any, List, Union +from typing import Any, Union import material from mkdocs.config.defaults import MkDocsConfig @@ -27,7 +27,7 @@ def get_missing_translation_content(docs_dir: str) -> str: @lru_cache -def get_mkdocs_material_langs() -> List[str]: +def get_mkdocs_material_langs() -> list[str]: material_path = Path(material.__file__).parent material_langs_path = material_path / "templates" / "partials" / "languages" langs = [file.stem for file in material_langs_path.glob("*.html")] @@ -65,7 +65,7 @@ def resolve_file(*, item: str, files: Files, config: MkDocsConfig) -> None: ) -def resolve_files(*, items: List[Any], files: Files, config: MkDocsConfig) -> None: +def resolve_files(*, items: list[Any], files: Files, config: MkDocsConfig) -> None: for item in items: if isinstance(item, str): resolve_file(item=item, files=files, config=config) @@ -94,9 +94,9 @@ def on_files(files: Files, *, config: MkDocsConfig) -> Files: def generate_renamed_section_items( - items: List[Union[Page, Section, Link]], *, config: MkDocsConfig -) -> List[Union[Page, Section, Link]]: - new_items: List[Union[Page, Section, Link]] = [] + items: list[Union[Page, Section, Link]], *, config: MkDocsConfig +) -> list[Union[Page, Section, Link]]: + new_items: list[Union[Page, Section, Link]] = [] for item in items: if isinstance(item, Section): new_title = item.title @@ -132,7 +132,7 @@ def on_pre_page(page: Page, *, config: MkDocsConfig, files: Files) -> Page: def on_page_markdown( markdown: str, *, page: Page, config: MkDocsConfig, files: Files ) -> str: - # Set matadata["social"]["cards_layout_options"]["title"] to clean title (without + # Set metadata["social"]["cards_layout_options"]["title"] to clean title (without # permalink) title = page.title clean_title = title.split("{ #")[0] diff --git a/scripts/notify_translations.py b/scripts/notify_translations.py index c300624db2..74cdf0dffe 100644 --- a/scripts/notify_translations.py +++ b/scripts/notify_translations.py @@ -3,7 +3,7 @@ import random import sys import time from pathlib import Path -from typing import Any, Dict, List, Union, cast +from typing import Any, Union, cast import httpx from github import Github @@ -120,7 +120,7 @@ class CommentsEdge(BaseModel): class Comments(BaseModel): - edges: List[CommentsEdge] + edges: list[CommentsEdge] class CommentsDiscussion(BaseModel): @@ -149,7 +149,7 @@ class AllDiscussionsLabelsEdge(BaseModel): class AllDiscussionsDiscussionLabels(BaseModel): - edges: List[AllDiscussionsLabelsEdge] + edges: list[AllDiscussionsLabelsEdge] class AllDiscussionsDiscussionNode(BaseModel): @@ -160,7 +160,7 @@ class AllDiscussionsDiscussionNode(BaseModel): class AllDiscussionsDiscussions(BaseModel): - nodes: List[AllDiscussionsDiscussionNode] + nodes: list[AllDiscussionsDiscussionNode] class AllDiscussionsRepository(BaseModel): @@ -205,7 +205,7 @@ def get_graphql_response( discussion_id: Union[str, None] = None, comment_id: Union[str, None] = None, body: Union[str, None] = None, -) -> Dict[str, Any]: +) -> dict[str, Any]: headers = {"Authorization": f"token {settings.github_token.get_secret_value()}"} variables = { "after": after, @@ -233,12 +233,12 @@ def get_graphql_response( logging.error(data["errors"]) logging.error(response.text) raise RuntimeError(response.text) - return cast(Dict[str, Any], data) + return cast(dict[str, Any], data) def get_graphql_translation_discussions( *, settings: Settings -) -> List[AllDiscussionsDiscussionNode]: +) -> list[AllDiscussionsDiscussionNode]: data = get_graphql_response( settings=settings, query=all_discussions_query, @@ -250,7 +250,7 @@ def get_graphql_translation_discussions( def get_graphql_translation_discussion_comments_edges( *, settings: Settings, discussion_number: int, after: Union[str, None] = None -) -> List[CommentsEdge]: +) -> list[CommentsEdge]: data = get_graphql_response( settings=settings, query=translation_discussion_query, @@ -264,7 +264,7 @@ def get_graphql_translation_discussion_comments_edges( def get_graphql_translation_discussion_comments( *, settings: Settings, discussion_number: int ) -> list[Comment]: - comment_nodes: List[Comment] = [] + comment_nodes: list[Comment] = [] discussion_edges = get_graphql_translation_discussion_comments_edges( settings=settings, discussion_number=discussion_number ) @@ -316,7 +316,7 @@ def main() -> None: raise RuntimeError( f"No github event file available at: {settings.github_event_path}" ) - contents = settings.github_event_path.read_text() + contents = settings.github_event_path.read_text("utf-8") github_event = PartialGitHubEvent.model_validate_json(contents) logging.info(f"Using GitHub event: {github_event}") number = ( @@ -348,7 +348,7 @@ def main() -> None: # Generate translation map, lang ID to discussion discussions = get_graphql_translation_discussions(settings=settings) - lang_to_discussion_map: Dict[str, AllDiscussionsDiscussionNode] = {} + lang_to_discussion_map: dict[str, AllDiscussionsDiscussionNode] = {} for discussion in discussions: for edge in discussion.labels.edges: label = edge.node.name diff --git a/scripts/people.py b/scripts/people.py index 7418b45956..207ab46493 100644 --- a/scripts/people.py +++ b/scripts/people.py @@ -3,9 +3,10 @@ import secrets import subprocess import time from collections import Counter +from collections.abc import Container from datetime import datetime, timedelta, timezone from pathlib import Path -from typing import Any, Container, Union +from typing import Any, Union import httpx import yaml @@ -378,9 +379,10 @@ def main() -> None: return logging.info("Setting up GitHub Actions git user") - subprocess.run(["git", "config", "user.name", "github-actions"], check=True) + subprocess.run(["git", "config", "user.name", "github-actions[bot]"], check=True) subprocess.run( - ["git", "config", "user.email", "github-actions@github.com"], check=True + ["git", "config", "user.email", "github-actions[bot]@users.noreply.github.com"], + check=True, ) branch_name = f"fastapi-people-experts-{secrets.token_hex(4)}" logging.info(f"Creating a new branch {branch_name}") diff --git a/scripts/sponsors.py b/scripts/sponsors.py index 45e02bd621..fdcabc737b 100644 --- a/scripts/sponsors.py +++ b/scripts/sponsors.py @@ -190,9 +190,10 @@ def main() -> None: return logging.info("Setting up GitHub Actions git user") - subprocess.run(["git", "config", "user.name", "github-actions"], check=True) + subprocess.run(["git", "config", "user.name", "github-actions[bot]"], check=True) subprocess.run( - ["git", "config", "user.email", "github-actions@github.com"], check=True + ["git", "config", "user.email", "github-actions[bot]@users.noreply.github.com"], + check=True, ) branch_name = f"fastapi-people-sponsors-{secrets.token_hex(4)}" logging.info(f"Creating a new branch {branch_name}") diff --git a/scripts/test.sh b/scripts/test.sh index 7d17add8fa..0bffcd1cdd 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -4,4 +4,4 @@ set -e set -x export PYTHONPATH=./docs_src -coverage run -m pytest tests ${@} +coverage run -m pytest tests scripts/tests/ ${@} diff --git a/scripts/tests/test_translation_fixer/conftest.py b/scripts/tests/test_translation_fixer/conftest.py new file mode 100644 index 0000000000..006f519f46 --- /dev/null +++ b/scripts/tests/test_translation_fixer/conftest.py @@ -0,0 +1,42 @@ +import shutil +import sys +from pathlib import Path + +import pytest +from typer.testing import CliRunner + +skip_on_windows = pytest.mark.skipif( + sys.platform == "win32", reason="Skipping on Windows" +) + + +def pytest_collection_modifyitems(items: list[pytest.Item]) -> None: + for item in items: + item.add_marker(skip_on_windows) + + +@pytest.fixture(name="runner") +def get_runner(): + runner = CliRunner() + with runner.isolated_filesystem(): + yield runner + + +@pytest.fixture(name="root_dir") +def prepare_paths(runner): + docs_dir = Path("docs") + en_docs_dir = docs_dir / "en" / "docs" + lang_docs_dir = docs_dir / "lang" / "docs" + en_docs_dir.mkdir(parents=True, exist_ok=True) + lang_docs_dir.mkdir(parents=True, exist_ok=True) + yield Path.cwd() + + +@pytest.fixture +def copy_test_files(root_dir: Path, request: pytest.FixtureRequest): + en_file_path = Path(request.param[0]) + translation_file_path = Path(request.param[1]) + shutil.copy(str(en_file_path), str(root_dir / "docs" / "en" / "docs" / "doc.md")) + shutil.copy( + str(translation_file_path), str(root_dir / "docs" / "lang" / "docs" / "doc.md") + ) diff --git a/scripts/tests/test_translation_fixer/test_code_blocks/data/en_doc.md b/scripts/tests/test_translation_fixer/test_code_blocks/data/en_doc.md new file mode 100644 index 0000000000..cad20e2c70 --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_code_blocks/data/en_doc.md @@ -0,0 +1,44 @@ +# Code blocks { #code-blocks } + +Some text + +```python +# This is a sample Python code block +def hello_world(): + # Comment with indentation + print("Hello, world!") # Print greeting +``` + +Some more text + +```toml +# This is a sample TOML code block +title = "TOML Example" # Title of the document +``` + +And more text + +```console +// Use the command "live" and pass the language code as a CLI argument +$ python ./scripts/docs.py live es + +<span style="color: green;">[INFO]</span> Serving on http://127.0.0.1:8008 +<span style="color: green;">[INFO]</span> Start watching changes +<span style="color: green;">[INFO]</span> Start detecting changes +``` + +And even more text + +```json +{ + // This is a sample JSON code block + "greeting": "Hello, world!" // Greeting +} +``` + +Mermaid diagram + +```mermaid +flowchart LR + stone(philosophers-stone) -->|requires| harry-1[harry v1] +``` diff --git a/scripts/tests/test_translation_fixer/test_code_blocks/data/translated_doc_lines_number_gt.md b/scripts/tests/test_translation_fixer/test_code_blocks/data/translated_doc_lines_number_gt.md new file mode 100644 index 0000000000..f46070156b --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_code_blocks/data/translated_doc_lines_number_gt.md @@ -0,0 +1,45 @@ +# Code blocks { #code-blocks } + +Some text + +```python +# This is a sample Python code block +def hello_world(): + # Comment with indentation + print("Hello, world!") # Print greeting +``` + +Some more text + +```toml +# Extra line +# This is a sample TOML code block +title = "TOML Example" # Title of the document +``` + +And more text + +```console +// Use the command "live" and pass the language code as a CLI argument +$ python ./scripts/docs.py live es + +<span style="color: green;">[INFO]</span> Serving on http://127.0.0.1:8008 +<span style="color: green;">[INFO]</span> Start watching changes +<span style="color: green;">[INFO]</span> Start detecting changes +``` + +And even more text + +```json +{ + // This is a sample JSON code block + "greeting": "Hello, world!" // Greeting +} +``` + +Диаграма Mermaid + +```mermaid +flowchart LR + stone(philosophers-stone) -->|requires| harry-1[harry v1] +``` diff --git a/scripts/tests/test_translation_fixer/test_code_blocks/data/translated_doc_lines_number_lt.md b/scripts/tests/test_translation_fixer/test_code_blocks/data/translated_doc_lines_number_lt.md new file mode 100644 index 0000000000..e08baa70b1 --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_code_blocks/data/translated_doc_lines_number_lt.md @@ -0,0 +1,45 @@ +# Code blocks { #code-blocks } + +Some text + +```python +# This is a sample Python code block +def hello_world(): + # Comment with indentation + print("Hello, world!") # Print greeting +``` + +Some more text + +The following block is missing first line: + +```toml +title = "TOML Example" # Title of the document +``` + +And more text + +```console +// Use the command "live" and pass the language code as a CLI argument +$ python ./scripts/docs.py live es + +<span style="color: green;">[INFO]</span> Serving on http://127.0.0.1:8008 +<span style="color: green;">[INFO]</span> Start watching changes +<span style="color: green;">[INFO]</span> Start detecting changes +``` + +And even more text + +```json +{ + // This is a sample JSON code block + "greeting": "Hello, world!" // Greeting +} +``` + +Диаграма Mermaid + +```mermaid +flowchart LR + stone(philosophers-stone) -->|requires| harry-1[harry v1] +``` diff --git a/scripts/tests/test_translation_fixer/test_code_blocks/data/translated_doc_mermaid_not_translated.md b/scripts/tests/test_translation_fixer/test_code_blocks/data/translated_doc_mermaid_not_translated.md new file mode 100644 index 0000000000..cacb9546d0 --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_code_blocks/data/translated_doc_mermaid_not_translated.md @@ -0,0 +1,44 @@ +# Code blocks { #code-blocks } + +Some text + +```python +# This is a sample Python code block +def hello_world(): + # Comment with indentation + print("Hello, world!") # Print greeting +``` + +Some more text + +```toml +# This is a sample TOML code block +title = "TOML Example" # Title of the document +``` + +And more text + +```console +// Use the command "live" and pass the language code as a CLI argument +$ python ./scripts/docs.py live es + +<span style="color: green;">[INFO]</span> Serving on http://127.0.0.1:8008 +<span style="color: green;">[INFO]</span> Start watching changes +<span style="color: green;">[INFO]</span> Start detecting changes +``` + +And even more text + +```json +{ + // This is a sample JSON code block + "greeting": "Hello, world!" // Greeting +} +``` + +Диаграма Mermaid + +```mermaid +flowchart LR + stone(philosophers-stone) -->|requires| harry-1[harry v1] +``` diff --git a/scripts/tests/test_translation_fixer/test_code_blocks/data/translated_doc_mermaid_translated.md b/scripts/tests/test_translation_fixer/test_code_blocks/data/translated_doc_mermaid_translated.md new file mode 100644 index 0000000000..d03dca53eb --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_code_blocks/data/translated_doc_mermaid_translated.md @@ -0,0 +1,44 @@ +# Code blocks { #code-blocks } + +Some text + +```python +# This is a sample Python code block +def hello_world(): + # Comment with indentation + print("Hello, world!") # Print greeting +``` + +Some more text + +```toml +# This is a sample TOML code block +title = "TOML Example" # Title of the document +``` + +And more text + +```console +// Use the command "live" and pass the language code as a CLI argument +$ python ./scripts/docs.py live es + +<span style="color: green;">[INFO]</span> Serving on http://127.0.0.1:8008 +<span style="color: green;">[INFO]</span> Start watching changes +<span style="color: green;">[INFO]</span> Start detecting changes +``` + +And even more text + +```json +{ + // This is a sample JSON code block + "greeting": "Hello, world!" // Greeting +} +``` + +Диаграма Mermaid + +```mermaid +flowchart LR + stone(philosophers-stone) -->|требует| harry-1[harry v1] +``` diff --git a/scripts/tests/test_translation_fixer/test_code_blocks/data/translated_doc_number_gt.md b/scripts/tests/test_translation_fixer/test_code_blocks/data/translated_doc_number_gt.md new file mode 100644 index 0000000000..e77050cc95 --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_code_blocks/data/translated_doc_number_gt.md @@ -0,0 +1,50 @@ +# Code blocks { #code-blocks } + +Some text + +```python +# This is a sample Python code block +def hello_world(): + # Comment with indentation + print("Hello, world!") # Print greeting +``` + +Some more text + +```toml +# This is a sample TOML code block +title = "TOML Example" # Title of the document +``` + +Extra code block + +``` +$ cd my_project +``` + +And more text + +```console +// Use the command "live" and pass the language code as a CLI argument +$ python ./scripts/docs.py live es + +<span style="color: green;">[INFO]</span> Serving on http://127.0.0.1:8008 +<span style="color: green;">[INFO]</span> Start watching changes +<span style="color: green;">[INFO]</span> Start detecting changes +``` + +And even more text + +```json +{ + // This is a sample JSON code block + "greeting": "Hello, world!" // Greeting +} +``` + +Диаграма Mermaid + +```mermaid +flowchart LR + stone(philosophers-stone) -->|requires| harry-1[harry v1] +``` diff --git a/scripts/tests/test_translation_fixer/test_code_blocks/data/translated_doc_number_lt.md b/scripts/tests/test_translation_fixer/test_code_blocks/data/translated_doc_number_lt.md new file mode 100644 index 0000000000..918cb883f4 --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_code_blocks/data/translated_doc_number_lt.md @@ -0,0 +1,41 @@ +# Code blocks { #code-blocks } + +Some text + +```python +# This is a sample Python code block +def hello_world(): + # Comment with indentation + print("Hello, world!") # Print greeting +``` + +Some more text + +Missing code block... + +And more text + +```console +// Use the command "live" and pass the language code as a CLI argument +$ python ./scripts/docs.py live es + +<span style="color: green;">[INFO]</span> Serving on http://127.0.0.1:8008 +<span style="color: green;">[INFO]</span> Start watching changes +<span style="color: green;">[INFO]</span> Start detecting changes +``` + +And even more text + +```json +{ + // This is a sample JSON code block + "greeting": "Hello, world!" // Greeting +} +``` + +Диаграма Mermaid + +```mermaid +flowchart LR + stone(philosophers-stone) -->|requires| harry-1[harry v1] +``` diff --git a/scripts/tests/test_translation_fixer/test_code_blocks/data/translated_doc_wrong_lang_code.md b/scripts/tests/test_translation_fixer/test_code_blocks/data/translated_doc_wrong_lang_code.md new file mode 100644 index 0000000000..88aed900d3 --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_code_blocks/data/translated_doc_wrong_lang_code.md @@ -0,0 +1,46 @@ +# Code blocks { #code-blocks } + +Some text + +```python +# This is a sample Python code block +def hello_world(): + # Comment with indentation + print("Hello, world!") # Print greeting +``` + +Some more text + +The following block has wrong language code (should be TOML): + +```yaml +# This is a sample TOML code block +title = "TOML Example" # Title of the document +``` + +And more text + +```console +// Use the command "live" and pass the language code as a CLI argument +$ python ./scripts/docs.py live es + +<span style="color: green;">[INFO]</span> Serving on http://127.0.0.1:8008 +<span style="color: green;">[INFO]</span> Start watching changes +<span style="color: green;">[INFO]</span> Start detecting changes +``` + +And even more text + +```json +{ + // This is a sample JSON code block + "greeting": "Hello, world!" // Greeting +} +``` + +Диаграма Mermaid + +```mermaid +flowchart LR + stone(philosophers-stone) -->|requires| harry-1[harry v1] +``` diff --git a/scripts/tests/test_translation_fixer/test_code_blocks/data/translated_doc_wrong_lang_code_2.md b/scripts/tests/test_translation_fixer/test_code_blocks/data/translated_doc_wrong_lang_code_2.md new file mode 100644 index 0000000000..a7fbb39f54 --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_code_blocks/data/translated_doc_wrong_lang_code_2.md @@ -0,0 +1,46 @@ +# Code blocks { #code-blocks } + +Some text + +```python +# This is a sample Python code block +def hello_world(): + # Comment with indentation + print("Hello, world!") # Print greeting +``` + +Some more text + +The following block has wrong language code (should be TOML): + +``` +# This is a sample TOML code block +title = "TOML Example" # Title of the document +``` + +And more text + +```console +// Use the command "live" and pass the language code as a CLI argument +$ python ./scripts/docs.py live es + +<span style="color: green;">[INFO]</span> Serving on http://127.0.0.1:8008 +<span style="color: green;">[INFO]</span> Start watching changes +<span style="color: green;">[INFO]</span> Start detecting changes +``` + +And even more text + +```json +{ + // This is a sample JSON code block + "greeting": "Hello, world!" // Greeting +} +``` + +Диаграма Mermaid + +```mermaid +flowchart LR + stone(philosophers-stone) -->|requires| harry-1[harry v1] +``` diff --git a/scripts/tests/test_translation_fixer/test_code_blocks/test_code_blocks_lines_number_mismatch.py b/scripts/tests/test_translation_fixer/test_code_blocks/test_code_blocks_lines_number_mismatch.py new file mode 100644 index 0000000000..9cdbe8323a --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_code_blocks/test_code_blocks_lines_number_mismatch.py @@ -0,0 +1,58 @@ +from pathlib import Path + +import pytest +from typer.testing import CliRunner + +from scripts.translation_fixer import cli + +data_path = Path( + "scripts/tests/test_translation_fixer/test_code_blocks/data" +).absolute() + + +@pytest.mark.parametrize( + "copy_test_files", + [(f"{data_path}/en_doc.md", f"{data_path}/translated_doc_lines_number_gt.md")], + indirect=True, +) +def test_gt(runner: CliRunner, root_dir: Path, copy_test_files): + result = runner.invoke( + cli, + ["fix-pages", "docs/lang/docs/doc.md"], + ) + assert result.exit_code == 1, result.output + + fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text("utf-8") + expected_content = Path(f"{data_path}/translated_doc_lines_number_gt.md").read_text( + "utf-8" + ) + + assert fixed_content == expected_content # Translated doc remains unchanged + assert "Error processing docs/lang/docs/doc.md" in result.output + assert ( + "Code block (lines 14-18) has different number of lines than the original block (5 vs 4)" + ) in result.output + + +@pytest.mark.parametrize( + "copy_test_files", + [(f"{data_path}/en_doc.md", f"{data_path}/translated_doc_lines_number_lt.md")], + indirect=True, +) +def test_lt(runner: CliRunner, root_dir: Path, copy_test_files): + result = runner.invoke( + cli, + ["fix-pages", "docs/lang/docs/doc.md"], + ) + # assert result.exit_code == 1, result.output + + fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text("utf-8") + expected_content = Path(f"{data_path}/translated_doc_lines_number_lt.md").read_text( + "utf-8" + ) + + assert fixed_content == expected_content # Translated doc remains unchanged + assert "Error processing docs/lang/docs/doc.md" in result.output + assert ( + "Code block (lines 16-18) has different number of lines than the original block (3 vs 4)" + ) in result.output diff --git a/scripts/tests/test_translation_fixer/test_code_blocks/test_code_blocks_mermaid.py b/scripts/tests/test_translation_fixer/test_code_blocks/test_code_blocks_mermaid.py new file mode 100644 index 0000000000..8b80c70f34 --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_code_blocks/test_code_blocks_mermaid.py @@ -0,0 +1,59 @@ +from pathlib import Path + +import pytest +from typer.testing import CliRunner + +from scripts.translation_fixer import cli + +data_path = Path( + "scripts/tests/test_translation_fixer/test_code_blocks/data" +).absolute() + + +@pytest.mark.parametrize( + "copy_test_files", + [(f"{data_path}/en_doc.md", f"{data_path}/translated_doc_mermaid_translated.md")], + indirect=True, +) +def test_translated(runner: CliRunner, root_dir: Path, copy_test_files): + result = runner.invoke( + cli, + ["fix-pages", "docs/lang/docs/doc.md"], + ) + assert result.exit_code == 0, result.output + + fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text("utf-8") + expected_content = Path( + f"{data_path}/translated_doc_mermaid_translated.md" + ).read_text("utf-8") + + assert fixed_content == expected_content # Translated doc remains unchanged + assert ( + "Skipping mermaid code block replacement (lines 41-44). This should be checked manually." + ) in result.output + + +@pytest.mark.parametrize( + "copy_test_files", + [ + ( + f"{data_path}/en_doc.md", + f"{data_path}/translated_doc_mermaid_not_translated.md", + ) + ], + indirect=True, +) +def test_not_translated(runner: CliRunner, root_dir: Path, copy_test_files): + result = runner.invoke( + cli, + ["fix-pages", "docs/lang/docs/doc.md"], + ) + assert result.exit_code == 0, result.output + + fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text("utf-8") + expected_content = Path( + f"{data_path}/translated_doc_mermaid_not_translated.md" + ).read_text("utf-8") + + assert fixed_content == expected_content # Translated doc remains unchanged + assert ("Skipping mermaid code block replacement") not in result.output diff --git a/scripts/tests/test_translation_fixer/test_code_blocks/test_code_blocks_number_mismatch.py b/scripts/tests/test_translation_fixer/test_code_blocks/test_code_blocks_number_mismatch.py new file mode 100644 index 0000000000..ad5767c1cb --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_code_blocks/test_code_blocks_number_mismatch.py @@ -0,0 +1,60 @@ +from pathlib import Path + +import pytest +from typer.testing import CliRunner + +from scripts.translation_fixer import cli + +data_path = Path( + "scripts/tests/test_translation_fixer/test_code_blocks/data" +).absolute() + + +@pytest.mark.parametrize( + "copy_test_files", + [(f"{data_path}/en_doc.md", f"{data_path}/translated_doc_number_gt.md")], + indirect=True, +) +def test_gt(runner: CliRunner, root_dir: Path, copy_test_files): + result = runner.invoke( + cli, + ["fix-pages", "docs/lang/docs/doc.md"], + ) + assert result.exit_code == 1, result.output + + fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text("utf-8") + expected_content = Path(f"{data_path}/translated_doc_number_gt.md").read_text( + "utf-8" + ) + + assert fixed_content == expected_content # Translated doc remains unchanged + assert "Error processing docs/lang/docs/doc.md" in result.output + assert ( + "Number of code blocks does not match the number " + "in the original document (6 vs 5)" + ) in result.output + + +@pytest.mark.parametrize( + "copy_test_files", + [(f"{data_path}/en_doc.md", f"{data_path}/translated_doc_number_lt.md")], + indirect=True, +) +def test_lt(runner: CliRunner, root_dir: Path, copy_test_files): + result = runner.invoke( + cli, + ["fix-pages", "docs/lang/docs/doc.md"], + ) + # assert result.exit_code == 1, result.output + + fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text("utf-8") + expected_content = Path(f"{data_path}/translated_doc_number_lt.md").read_text( + "utf-8" + ) + + assert fixed_content == expected_content # Translated doc remains unchanged + assert "Error processing docs/lang/docs/doc.md" in result.output + assert ( + "Number of code blocks does not match the number " + "in the original document (4 vs 5)" + ) in result.output diff --git a/scripts/tests/test_translation_fixer/test_code_blocks/test_code_blocks_wrong_lang_code.py b/scripts/tests/test_translation_fixer/test_code_blocks/test_code_blocks_wrong_lang_code.py new file mode 100644 index 0000000000..85d73e3b4a --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_code_blocks/test_code_blocks_wrong_lang_code.py @@ -0,0 +1,58 @@ +from pathlib import Path + +import pytest +from typer.testing import CliRunner + +from scripts.translation_fixer import cli + +data_path = Path( + "scripts/tests/test_translation_fixer/test_code_blocks/data" +).absolute() + + +@pytest.mark.parametrize( + "copy_test_files", + [(f"{data_path}/en_doc.md", f"{data_path}/translated_doc_wrong_lang_code.md")], + indirect=True, +) +def test_wrong_lang_code_1(runner: CliRunner, root_dir: Path, copy_test_files): + result = runner.invoke( + cli, + ["fix-pages", "docs/lang/docs/doc.md"], + ) + assert result.exit_code == 1, result.output + + fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text("utf-8") + expected_content = Path(f"{data_path}/translated_doc_wrong_lang_code.md").read_text( + "utf-8" + ) + + assert fixed_content == expected_content # Translated doc remains unchanged + assert "Error processing docs/lang/docs/doc.md" in result.output + assert ( + "Code block (lines 16-19) has different language than the original block ('yaml' vs 'toml')" + ) in result.output + + +@pytest.mark.parametrize( + "copy_test_files", + [(f"{data_path}/en_doc.md", f"{data_path}/translated_doc_wrong_lang_code_2.md")], + indirect=True, +) +def test_wrong_lang_code_2(runner: CliRunner, root_dir: Path, copy_test_files): + result = runner.invoke( + cli, + ["fix-pages", "docs/lang/docs/doc.md"], + ) + assert result.exit_code == 1, result.output + + fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text("utf-8") + expected_content = Path( + f"{data_path}/translated_doc_wrong_lang_code_2.md" + ).read_text("utf-8") + + assert fixed_content == expected_content # Translated doc remains unchanged + assert "Error processing docs/lang/docs/doc.md" in result.output + assert ( + "Code block (lines 16-19) has different language than the original block ('' vs 'toml')" + ) in result.output diff --git a/scripts/tests/test_translation_fixer/test_code_includes/data/en_doc.md b/scripts/tests/test_translation_fixer/test_code_includes/data/en_doc.md new file mode 100644 index 0000000000..593da0b327 --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_code_includes/data/en_doc.md @@ -0,0 +1,13 @@ +# Header + +{* ../../docs_src/dependencies/tutorial013_an_py310.py ln[19:21] *} + +Some text + +{* ../../docs_src/bigger_applications/app_an_py39/internal/admin.py hl[3] title["app/internal/admin.py"] *} + +Some more text + +{* ../../docs_src/dependencies/tutorial013_an_py310.py ln[30:38] hl[31:33] *} + +And even more text diff --git a/scripts/tests/test_translation_fixer/test_code_includes/data/translated_doc_number_gt.md b/scripts/tests/test_translation_fixer/test_code_includes/data/translated_doc_number_gt.md new file mode 100644 index 0000000000..c1ad94d276 --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_code_includes/data/translated_doc_number_gt.md @@ -0,0 +1,15 @@ +# Header + +{* ../../docs_src/dependencies/tutorial013_an_py310.py ln[19:21] *} + +Some text + +{* ../../docs_src/bigger_applications/app_an_py39/internal/admin.py hl[3] title["app/internal/admin.py"] *} + +Some more text + +{* ../../docs_src/dependencies/tutorial013_an_py310.py ln[30:38] hl[31:33] *} + +And even more text + +{* ../../docs_src/python_types/tutorial001_py39.py *} diff --git a/scripts/tests/test_translation_fixer/test_code_includes/data/translated_doc_number_lt.md b/scripts/tests/test_translation_fixer/test_code_includes/data/translated_doc_number_lt.md new file mode 100644 index 0000000000..07eaf2c23d --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_code_includes/data/translated_doc_number_lt.md @@ -0,0 +1,13 @@ +# Header + +{* ../../docs_src/dependencies/tutorial013_an_py310.py ln[19:21] *} + +Some text + +{* ../../docs_src/bigger_applications/app_an_py39/internal/admin.py hl[3] title["app/internal/admin.py"] *} + +Some more text + +... + +And even more text diff --git a/scripts/tests/test_translation_fixer/test_code_includes/test_number_mismatch.py b/scripts/tests/test_translation_fixer/test_code_includes/test_number_mismatch.py new file mode 100644 index 0000000000..1020b890cf --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_code_includes/test_number_mismatch.py @@ -0,0 +1,60 @@ +from pathlib import Path + +import pytest +from typer.testing import CliRunner + +from scripts.translation_fixer import cli + +data_path = Path( + "scripts/tests/test_translation_fixer/test_code_includes/data" +).absolute() + + +@pytest.mark.parametrize( + "copy_test_files", + [(f"{data_path}/en_doc.md", f"{data_path}/translated_doc_number_gt.md")], + indirect=True, +) +def test_gt(runner: CliRunner, root_dir: Path, copy_test_files): + result = runner.invoke( + cli, + ["fix-pages", "docs/lang/docs/doc.md"], + ) + assert result.exit_code == 1 + + fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text("utf-8") + expected_content = Path(f"{data_path}/translated_doc_number_gt.md").read_text( + "utf-8" + ) + + assert fixed_content == expected_content # Translated doc remains unchanged + assert "Error processing docs/lang/docs/doc.md" in result.output + assert ( + "Number of code include placeholders does not match the number of code includes " + "in the original document (4 vs 3)" + ) in result.output + + +@pytest.mark.parametrize( + "copy_test_files", + [(f"{data_path}/en_doc.md", f"{data_path}/translated_doc_number_lt.md")], + indirect=True, +) +def test_lt(runner: CliRunner, root_dir: Path, copy_test_files): + result = runner.invoke( + cli, + ["fix-pages", "docs/lang/docs/doc.md"], + ) + assert result.exit_code == 1 + + fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text("utf-8") + expected_content = Path(f"{data_path}/translated_doc_number_lt.md").read_text( + "utf-8" + ) + + assert fixed_content == expected_content # Translated doc remains unchanged + assert "Error processing docs/lang/docs/doc.md" in result.output + assert ( + "Number of code include placeholders does not match the number of code includes " + "in the original document (2 vs 3)" + ) in result.output diff --git a/scripts/tests/test_translation_fixer/test_complex_doc/data/en_doc.md b/scripts/tests/test_translation_fixer/test_complex_doc/data/en_doc.md new file mode 100644 index 0000000000..69cd3f3fd7 --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_complex_doc/data/en_doc.md @@ -0,0 +1,244 @@ +# Test translation fixer tool { #test-translation-fixer } + +## Code blocks with and without comments { #code-blocks-with-and-without-comments } + +This is a test page for the translation fixer tool. + +### Code blocks with comments { #code-blocks-with-comments } + +The following code blocks include comments in different styles. +Fixer tool should fix content, but preserve comments correctly. + +```python +# This is a sample Python code block +def hello_world(): + # Comment with indentation + print("Hello, world!") # Print greeting +``` + +```toml +# This is a sample TOML code block +title = "TOML Example" # Title of the document +``` + +```console +// Use the command "live" and pass the language code as a CLI argument +$ python ./scripts/docs.py live es + +<span style="color: green;">[INFO]</span> Serving on http://127.0.0.1:8008 +<span style="color: green;">[INFO]</span> Start watching changes +<span style="color: green;">[INFO]</span> Start detecting changes +``` + +```json +{ + // This is a sample JSON code block + "greeting": "Hello, world!" // Greeting +} +``` + + +### Code blocks with comments where language uses different comment styles { #code-blocks-with-different-comment-styles } + +The following code blocks include comments in different styles based on the language. +Fixer tool will not preserve comments in these blocks. + +```json +{ + # This is a sample JSON code block + "greeting": "Hello, world!" # Print greeting +} +``` + +```console +# This is a sample console code block +$ echo "Hello, world!" # Print greeting +``` + +```toml +// This is a sample TOML code block +title = "TOML Example" // Title of the document +``` + + +### Code blocks with comments with unsupported languages or without language specified { #code-blocks-with-unsupported-languages } + +The following code blocks use unsupported languages for comment preservation. +Fixer tool will not preserve comments in these blocks. + +```javascript +// This is a sample JavaScript code block +console.log("Hello, world!"); // Print greeting +``` + +``` +# This is a sample console code block +$ echo "Hello, world!" # Print greeting +``` + +``` +// This is a sample console code block +$ echo "Hello, world!" // Print greeting +``` + + +### Code blocks with comments that don't follow pattern { #code-blocks-with-comments-without-pattern } + +Fixer tool expects comments that follow specific pattern: + +- For hash-style comments: comment starts with `# ` (hash following by whitespace) in the beginning of the string or after a whitespace. +- For slash-style comments: comment starts with `// ` (two slashes following by whitespace) in the beginning of the string or after a whitespace. + +If comment doesn't follow this pattern, fixer tool will not preserve it. + +```python +#Function declaration +def hello_world():# Print greeting + print("Hello, world!") #Print greeting without space after hash +``` + +```console +//Function declaration +def hello_world():// Print greeting + print("Hello, world!") //Print greeting without space after slashes +``` + +## Code blocks with quadruple backticks { #code-blocks-with-quadruple-backticks } + +The following code block uses quadruple backticks. + +````python +# Hello world function +def hello_world(): + print("Hello, world!") # Print greeting +```` + +### Backticks number mismatch is fixable { #backticks-number-mismatch-is-fixable } + +The following code block has triple backticks in the original document, but quadruple backticks in the translated document. +It will be fixed by the fixer tool (will convert to triple backticks). + +```Python +# Some Python code +``` + +### Triple backticks inside quadruple backticks { #triple-backticks-inside-quadruple-backticks } + +Comments inside nested code block will NOT be preserved. + +```` +Here is a code block with quadruple backticks that contains triple backticks inside: + +```python +# This is a sample Python code block +def hello_world(): + print("Hello, world!") # Print greeting +``` + +```` + +# Code includes { #code-includes } + +## Simple code includes { #simple-code-includes } + +{* ../../docs_src/python_types/tutorial001_py39.py *} + +{* ../../docs_src/python_types/tutorial002_py39.py *} + + +## Code includes with highlighting { #code-includes-with-highlighting } + +{* ../../docs_src/python_types/tutorial002_py39.py hl[1] *} + +{* ../../docs_src/python_types/tutorial006_py39.py hl[10] *} + + +## Code includes with line ranges { #code-includes-with-line-ranges } + +{* ../../docs_src/dependencies/tutorial013_an_py310.py ln[19:21] *} + +{* ../../docs_src/dependencies/tutorial013_an_py310.py ln[30:38] *} + + +## Code includes with line ranges and highlighting { #code-includes-with-line-ranges-and-highlighting } + +{* ../../docs_src/dependencies/tutorial013_an_py310.py ln[30:38] hl[31:33] *} + +{* ../../docs_src/dependencies/tutorial015_an_py310.py ln[10:15] hl[12:14] *} + + +## Code includes qith title { #code-includes-with-title } + +{* ../../docs_src/bigger_applications/app_an_py39/routers/users.py hl[1,3] title["app/routers/users.py"] *} + +{* ../../docs_src/bigger_applications/app_an_py39/internal/admin.py hl[3] title["app/internal/admin.py"] *} + +## Code includes with unknown attributes { #code-includes-with-unknown-attributes } + +{* ../../docs_src/python_types/tutorial001_py39.py unknown[123] *} + +## Some more code includes to test fixing { #some-more-code-includes-to-test-fixing } + +{* ../../docs_src/dependencies/tutorial013_an_py310.py ln[19:21] *} + +{* ../../docs_src/bigger_applications/app_an_py39/internal/admin.py hl[3] title["app/internal/admin.py"] *} + +{* ../../docs_src/dependencies/tutorial013_an_py310.py ln[30:38] hl[31:33] *} + + + +# Links { #links } + +## Markdown-style links { #markdown-style-links } + +This is a [Markdown link](https://example.com) to an external site. + +This is a link with attributes: [**FastAPI** Project Generators](project-generation.md){.internal-link target=_blank} + +This is a link to the main FastAPI site: [FastAPI](https://fastapi.tiangolo.com) - tool should add language code to the URL. + +This is a link to one of the pages on FastAPI site: [How to](https://fastapi.tiangolo.com/how-to/) - tool should add language code to the URL. + +Link to test wrong attribute: [**FastAPI** Project Generators](project-generation.md){.internal-link} - tool should fix the attribute. + +Link with a title: [Example](https://example.com "Example site") - URL will be fixed, title preserved. + +### Markdown link to static assets { #markdown-link-to-static-assets } + +These are links to static assets: + +* [FastAPI Logo](https://fastapi.tiangolo.com/img/fastapi-logo.png) +* [FastAPI CSS](https://fastapi.tiangolo.com/css/fastapi.css) +* [FastAPI JS](https://fastapi.tiangolo.com/js/fastapi.js) + +Tool should NOT add language code to their URLs. + +## HTML-style links { #html-style-links } + +This is an <a href="https://example.com" target="_blank" class="external-link">HTML link</a> to an external site. + +This is an <a href="https://fastapi.tiangolo.com">link to the main FastAPI site</a> - tool should add language code to the URL. + +This is an <a href="https://fastapi.tiangolo.com/how-to/">link to one of the pages on FastAPI site</a> - tool should add language code to the URL. + +Link to test wrong attribute: <a href="project-generation.md" class="internal-link">**FastAPI** Project Generators</a> - tool should fix the attribute. + +### HTML links to static assets { #html-links-to-static-assets } + +These are links to static assets: + +* <a href="https://fastapi.tiangolo.com/img/fastapi-logo.png">FastAPI Logo</a> +* <a href="https://fastapi.tiangolo.com/css/fastapi.css">FastAPI CSS</a> +* <a href="https://fastapi.tiangolo.com/js/fastapi.js">FastAPI JS</a> + +Tool should NOT add language code to their URLs. + +# Header (with HTML link to <a href="https://tiangolo.com">tiangolo.com</a>) { #header-with-html-link-to-tiangolo-com } + +#Not a header + +```Python +# Also not a header +``` + +Some text diff --git a/scripts/tests/test_translation_fixer/test_complex_doc/data/translated_doc.md b/scripts/tests/test_translation_fixer/test_complex_doc/data/translated_doc.md new file mode 100644 index 0000000000..c922d7b133 --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_complex_doc/data/translated_doc.md @@ -0,0 +1,240 @@ +# Тестовый инструмент исправления переводов { #test-translation-fixer } + +## Блоки кода с комментариями и без комментариев { #code-blocks-with-and-without-comments } + +Это тестовая страница для инструмента исправления переводов. + +### Блоки кода с комментариями { #code-blocks-with-comments } + +Следующие блоки кода содержат комментарии в разных стилях. +Инструмент исправления должен исправлять содержимое, но корректно сохранять комментарии. + +```python +# Это пример блока кода на Python +def hello_world(): + # Комментарий с отступом + print("Hello, world!") # Печать приветствия +``` + +```toml +# Это пример блока кода на TOML +title = "TOML Example" # Заголовок документа +``` + +```console +// Используйте команду "live" и передайте код языка в качестве аргумента CLI +$ python ./scripts/docs.py live es + +<span style="color: green;">[INFO]</span> Serving on http://127.0.0.1:8008 +<span style="color: green;">[INFO]</span> Start watching changes +<span style="color: green;">[INFO]</span> Start detecting changes +``` + +```json +{ + // Это пример блока кода на JSON + "greeting": "Hello, world!" // Печать приветствия +} +``` + + +### Блоки кода с комментариями, где язык использует другие стили комментариев { #code-blocks-with-different-comment-styles } + +Следующие блоки кода содержат комментарии в разных стилях в зависимости от языка. +Инструмент исправления не будет сохранять комментарии в этих блоках. + +```json +{ + # Это пример блока кода на JSON + "greeting": "Hello, world!" # Печать приветствия +} +``` + +```console +# Это пример блока кода консоли +$ echo "Hello, world!" # Печать приветствия +``` + +```toml +// Это пример блока кода на TOML +title = "TOML Example" // Заголовок документа +``` + +### Блоки кода с комментариями на неподдерживаемых языках или без указания языка { #code-blocks-with-unsupported-languages } + +Следующие блоки кода используют неподдерживаемые языки для сохранения комментариев. +Инструмент исправления не будет сохранять комментарии в этих блоках. + +```javascript +// Это пример блока кода на JavaScript +console.log("Hello, world!"); // Печать приветствия +``` + +``` +# Это пример блока кода консоли +$ echo "Hello, world!" # Печать приветствия +``` + +``` +// Это пример блока кода консоли +$ echo "Hello, world!" // Печать приветствия +``` + +### Блоки кода с комментариями, которые не соответствуют шаблону { #code-blocks-with-comments-without-pattern } + +Инструмент исправления ожидает комментарии, которые соответствуют определённому шаблону: + +- Для комментариев в стиле с решёткой: комментарий начинается с `# ` (решётка, затем пробел) в начале строки или после пробела. +- Для комментариев в стиле со слешами: комментарий начинается с `// ` (два слеша, затем пробел) в начале строки или после пробела. + +Если комментарий не соответствует этому шаблону, инструмент исправления не будет его сохранять. + +```python +#Объявление функции +def hello_world():# Печать приветствия + print("Hello, world!") #Печать приветствия без пробела после решётки +``` + +```console +//Объявление функции +def hello_world():// Печать приветствия + print("Hello, world!") //Печать приветствия без пробела после слешей +``` + +## Блок кода с четырёхкратными обратными кавычками { #code-blocks-with-quadruple-backticks } + +Следующий блок кода содержит четырёхкратные обратные кавычки. + +````python +# Функция приветствия +def hello_world(): + print("Hello, world") # Печать приветствия +```` + +### Несоответствие обратных кавычек фиксится { #backticks-number-mismatch-is-fixable } + +Следующий блок кода имеет тройные обратные кавычки в оригинальном документе, но четырёхкратные обратные кавычки в переведённом документе. +Это будет исправлено инструментом исправления (будет преобразовано в тройные обратные кавычки). + +````Python +# Немного кода на Python +```` + +### Блок кода в тройных обратных кавычка внутри блока кода в четырёхкратных обратных кавычках { #triple-backticks-inside-quadruple-backticks } + +Комментарии внутри вложенного блока кода в тройных обратных кавычках НЕ БУДУТ сохранены. + +```` +Here is a code block with quadruple backticks that contains triple backticks inside: + +```python +# Этот комментарий НЕ будет сохранён +def hello_world(): + print("Hello, world") # Как и этот комментарий +``` + +```` + +# Включения кода { #code-includes } + +## Простые включения кода { #simple-code-includes } + +{* ../../docs_src/python_types/tutorial001_py39.py *} + +{* ../../docs_src/python_types/tutorial002_py39.py *} + + +## Включения кода с подсветкой { #code-includes-with-highlighting } + +{* ../../docs_src/python_types/tutorial002_py39.py hl[1] *} + +{* ../../docs_src/python_types/tutorial006_py39.py hl[10] *} + + +## Включения кода с диапазонами строк { #code-includes-with-line-ranges } + +{* ../../docs_src/dependencies/tutorial013_an_py310.py ln[19:21] *} + +{* ../../docs_src/dependencies/tutorial013_an_py310.py ln[30:38] *} + + +## Включения кода с диапазонами строк и подсветкой { #code-includes-with-line-ranges-and-highlighting } + +{* ../../docs_src/dependencies/tutorial013_an_py310.py ln[30:38] hl[31:33] *} + +{* ../../docs_src/dependencies/tutorial015_an_py310.py ln[10:15] hl[12:14] *} + + +## Включения кода с заголовком { #code-includes-with-title } + +{* ../../docs_src/bigger_applications/app_an_py39/routers/users.py hl[1,3] title["app/routers/users.py"] *} + +{* ../../docs_src/bigger_applications/app_an_py39/internal/admin.py hl[3] title["app/internal/admin.py"] *} + +## Включения кода с неизвестными атрибутами { #code-includes-with-unknown-attributes } + +{* ../../docs_src/python_types/tutorial001_py39.py unknown[123] *} + +## Ещё включения кода для тестирования исправления { #some-more-code-includes-to-test-fixing } + +{* ../../docs_src/dependencies/tutorial013_an_py310.py ln[19 : 21] *} + +{* ../../docs_src/bigger_applications/app_an_py39/wrong.py hl[3] title["app/internal/admin.py"] *} + +{* ../../docs_src/dependencies/tutorial013_an_py310.py ln[1:30] hl[1:10] *} + +# Ссылки { #links } + +## Ссылки в стиле Markdown { #markdown-style-links } + +Это [Markdown-ссылка](https://example.com) на внешний сайт. + +Это ссылка с атрибутами: [**FastAPI** генераторы проектов](project-generation.md){.internal-link target=_blank} + +Это ссылка на основной сайт FastAPI: [FastAPI](https://fastapi.tiangolo.com) — инструмент должен добавить код языка в URL. + +Это ссылка на одну из страниц на сайте FastAPI: [How to](https://fastapi.tiangolo.com/how-to) — инструмент должен добавить код языка в URL. + +Ссылка для тестирования неправильного атрибута: [**FastAPI** генераторы проектов](project-generation.md){.external-link} - инструмент должен исправить атрибут. + +Ссылка с заголовком: [Пример](http://example.com/ "Сайт для примера") - URL будет исправлен инструментом, заголовок сохранится. + +### Markdown ссылки на статические ресурсы { #markdown-link-to-static-assets } + +Это ссылки на статические ресурсы: + +* [FastAPI Logo](https://fastapi.tiangolo.com/img/fastapi-logo.png) +* [FastAPI CSS](https://fastapi.tiangolo.com/css/fastapi.css) +* [FastAPI JS](https://fastapi.tiangolo.com/js/fastapi.js) + +Инструмент НЕ должен добавлять код языка в их URL. + +## Ссылки в стиле HTML { #html-style-links } + +Это <a href="https://example.com" target="_blank" class="external-link">HTML-ссылка</a> на внешний сайт. + +Это <a href="https://fastapi.tiangolo.com">ссылка на основной сайт FastAPI</a> — инструмент должен добавить код языка в URL. + +Это <a href="https://fastapi.tiangolo.com/how-to/">ссылка на одну из страниц на сайте FastAPI</a> — инструмент должен добавить код языка в URL. + +Ссылка для тестирования неправильного атрибута: <a href="project-generation.md" class="external-link">**FastAPI** генераторы проектов</a> - инструмент должен исправить атрибут. + +### HTML ссылки на статические ресурсы { #html-links-to-static-assets } + +Это ссылки на статические ресурсы: + +* <a href="https://fastapi.tiangolo.com/img/fastapi-logo.png">FastAPI Logo</a> +* <a href="https://fastapi.tiangolo.com/css/fastapi.css">FastAPI CSS</a> +* <a href="https://fastapi.tiangolo.com/js/fastapi.js">FastAPI JS</a> + +Инструмент НЕ должен добавлять код языка в их URL. + +# Заголовок (с HTML ссылкой на <a href="https://tiangolo.com">tiangolo.com</a>) { #header-5 } + +#Не заголовок + +```Python +# Также не заголовок +``` + +Немного текста diff --git a/scripts/tests/test_translation_fixer/test_complex_doc/data/translated_doc_expected.md b/scripts/tests/test_translation_fixer/test_complex_doc/data/translated_doc_expected.md new file mode 100644 index 0000000000..b33f36e772 --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_complex_doc/data/translated_doc_expected.md @@ -0,0 +1,240 @@ +# Тестовый инструмент исправления переводов { #test-translation-fixer } + +## Блоки кода с комментариями и без комментариев { #code-blocks-with-and-without-comments } + +Это тестовая страница для инструмента исправления переводов. + +### Блоки кода с комментариями { #code-blocks-with-comments } + +Следующие блоки кода содержат комментарии в разных стилях. +Инструмент исправления должен исправлять содержимое, но корректно сохранять комментарии. + +```python +# Это пример блока кода на Python +def hello_world(): + # Комментарий с отступом + print("Hello, world!") # Печать приветствия +``` + +```toml +# Это пример блока кода на TOML +title = "TOML Example" # Заголовок документа +``` + +```console +// Используйте команду "live" и передайте код языка в качестве аргумента CLI +$ python ./scripts/docs.py live es + +<span style="color: green;">[INFO]</span> Serving on http://127.0.0.1:8008 +<span style="color: green;">[INFO]</span> Start watching changes +<span style="color: green;">[INFO]</span> Start detecting changes +``` + +```json +{ + // Это пример блока кода на JSON + "greeting": "Hello, world!" // Печать приветствия +} +``` + + +### Блоки кода с комментариями, где язык использует другие стили комментариев { #code-blocks-with-different-comment-styles } + +Следующие блоки кода содержат комментарии в разных стилях в зависимости от языка. +Инструмент исправления не будет сохранять комментарии в этих блоках. + +```json +{ + # This is a sample JSON code block + "greeting": "Hello, world!" # Print greeting +} +``` + +```console +# This is a sample console code block +$ echo "Hello, world!" # Print greeting +``` + +```toml +// This is a sample TOML code block +title = "TOML Example" // Title of the document +``` + +### Блоки кода с комментариями на неподдерживаемых языках или без указания языка { #code-blocks-with-unsupported-languages } + +Следующие блоки кода используют неподдерживаемые языки для сохранения комментариев. +Инструмент исправления не будет сохранять комментарии в этих блоках. + +```javascript +// This is a sample JavaScript code block +console.log("Hello, world!"); // Print greeting +``` + +``` +# This is a sample console code block +$ echo "Hello, world!" # Print greeting +``` + +``` +// This is a sample console code block +$ echo "Hello, world!" // Print greeting +``` + +### Блоки кода с комментариями, которые не соответствуют шаблону { #code-blocks-with-comments-without-pattern } + +Инструмент исправления ожидает комментарии, которые соответствуют определённому шаблону: + +- Для комментариев в стиле с решёткой: комментарий начинается с `# ` (решётка, затем пробел) в начале строки или после пробела. +- Для комментариев в стиле со слешами: комментарий начинается с `// ` (два слеша, затем пробел) в начале строки или после пробела. + +Если комментарий не соответствует этому шаблону, инструмент исправления не будет его сохранять. + +```python +#Function declaration +def hello_world():# Print greeting + print("Hello, world!") #Print greeting without space after hash +``` + +```console +//Function declaration +def hello_world():// Print greeting + print("Hello, world!") //Print greeting without space after slashes +``` + +## Блок кода с четырёхкратными обратными кавычками { #code-blocks-with-quadruple-backticks } + +Следующий блок кода содержит четырёхкратные обратные кавычки. + +````python +# Функция приветствия +def hello_world(): + print("Hello, world!") # Печать приветствия +```` + +### Несоответствие обратных кавычек фиксится { #backticks-number-mismatch-is-fixable } + +Следующий блок кода имеет тройные обратные кавычки в оригинальном документе, но четырёхкратные обратные кавычки в переведённом документе. +Это будет исправлено инструментом исправления (будет преобразовано в тройные обратные кавычки). + +```Python +# Немного кода на Python +``` + +### Блок кода в тройных обратных кавычка внутри блока кода в четырёхкратных обратных кавычках { #triple-backticks-inside-quadruple-backticks } + +Комментарии внутри вложенного блока кода в тройных обратных кавычках НЕ БУДУТ сохранены. + +```` +Here is a code block with quadruple backticks that contains triple backticks inside: + +```python +# This is a sample Python code block +def hello_world(): + print("Hello, world!") # Print greeting +``` + +```` + +# Включения кода { #code-includes } + +## Простые включения кода { #simple-code-includes } + +{* ../../docs_src/python_types/tutorial001_py39.py *} + +{* ../../docs_src/python_types/tutorial002_py39.py *} + + +## Включения кода с подсветкой { #code-includes-with-highlighting } + +{* ../../docs_src/python_types/tutorial002_py39.py hl[1] *} + +{* ../../docs_src/python_types/tutorial006_py39.py hl[10] *} + + +## Включения кода с диапазонами строк { #code-includes-with-line-ranges } + +{* ../../docs_src/dependencies/tutorial013_an_py310.py ln[19:21] *} + +{* ../../docs_src/dependencies/tutorial013_an_py310.py ln[30:38] *} + + +## Включения кода с диапазонами строк и подсветкой { #code-includes-with-line-ranges-and-highlighting } + +{* ../../docs_src/dependencies/tutorial013_an_py310.py ln[30:38] hl[31:33] *} + +{* ../../docs_src/dependencies/tutorial015_an_py310.py ln[10:15] hl[12:14] *} + + +## Включения кода с заголовком { #code-includes-with-title } + +{* ../../docs_src/bigger_applications/app_an_py39/routers/users.py hl[1,3] title["app/routers/users.py"] *} + +{* ../../docs_src/bigger_applications/app_an_py39/internal/admin.py hl[3] title["app/internal/admin.py"] *} + +## Включения кода с неизвестными атрибутами { #code-includes-with-unknown-attributes } + +{* ../../docs_src/python_types/tutorial001_py39.py unknown[123] *} + +## Ещё включения кода для тестирования исправления { #some-more-code-includes-to-test-fixing } + +{* ../../docs_src/dependencies/tutorial013_an_py310.py ln[19:21] *} + +{* ../../docs_src/bigger_applications/app_an_py39/internal/admin.py hl[3] title["app/internal/admin.py"] *} + +{* ../../docs_src/dependencies/tutorial013_an_py310.py ln[30:38] hl[31:33] *} + +# Ссылки { #links } + +## Ссылки в стиле Markdown { #markdown-style-links } + +Это [Markdown-ссылка](https://example.com) на внешний сайт. + +Это ссылка с атрибутами: [**FastAPI** генераторы проектов](project-generation.md){.internal-link target=_blank} + +Это ссылка на основной сайт FastAPI: [FastAPI](https://fastapi.tiangolo.com/lang) — инструмент должен добавить код языка в URL. + +Это ссылка на одну из страниц на сайте FastAPI: [How to](https://fastapi.tiangolo.com/lang/how-to/) — инструмент должен добавить код языка в URL. + +Ссылка для тестирования неправильного атрибута: [**FastAPI** генераторы проектов](project-generation.md){.internal-link} - инструмент должен исправить атрибут. + +Ссылка с заголовком: [Пример](https://example.com "Сайт для примера") - URL будет исправлен инструментом, заголовок сохранится. + +### Markdown ссылки на статические ресурсы { #markdown-link-to-static-assets } + +Это ссылки на статические ресурсы: + +* [FastAPI Logo](https://fastapi.tiangolo.com/img/fastapi-logo.png) +* [FastAPI CSS](https://fastapi.tiangolo.com/css/fastapi.css) +* [FastAPI JS](https://fastapi.tiangolo.com/js/fastapi.js) + +Инструмент НЕ должен добавлять код языка в их URL. + +## Ссылки в стиле HTML { #html-style-links } + +Это <a href="https://example.com" target="_blank" class="external-link">HTML-ссылка</a> на внешний сайт. + +Это <a href="https://fastapi.tiangolo.com/lang">ссылка на основной сайт FastAPI</a> — инструмент должен добавить код языка в URL. + +Это <a href="https://fastapi.tiangolo.com/lang/how-to/">ссылка на одну из страниц на сайте FastAPI</a> — инструмент должен добавить код языка в URL. + +Ссылка для тестирования неправильного атрибута: <a href="project-generation.md" class="internal-link">**FastAPI** генераторы проектов</a> - инструмент должен исправить атрибут. + +### HTML ссылки на статические ресурсы { #html-links-to-static-assets } + +Это ссылки на статические ресурсы: + +* <a href="https://fastapi.tiangolo.com/img/fastapi-logo.png">FastAPI Logo</a> +* <a href="https://fastapi.tiangolo.com/css/fastapi.css">FastAPI CSS</a> +* <a href="https://fastapi.tiangolo.com/js/fastapi.js">FastAPI JS</a> + +Инструмент НЕ должен добавлять код языка в их URL. + +# Заголовок (с HTML ссылкой на <a href="https://tiangolo.com">tiangolo.com</a>) { #header-with-html-link-to-tiangolo-com } + +#Не заголовок + +```Python +# Также не заголовок +``` + +Немного текста diff --git a/scripts/tests/test_translation_fixer/test_complex_doc/test_compex_doc.py b/scripts/tests/test_translation_fixer/test_complex_doc/test_compex_doc.py new file mode 100644 index 0000000000..cc7bcadda8 --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_complex_doc/test_compex_doc.py @@ -0,0 +1,30 @@ +from pathlib import Path + +import pytest +from typer.testing import CliRunner + +from scripts.translation_fixer import cli + +data_path = Path( + "scripts/tests/test_translation_fixer/test_complex_doc/data" +).absolute() + + +@pytest.mark.parametrize( + "copy_test_files", + [(f"{data_path}/en_doc.md", f"{data_path}/translated_doc.md")], + indirect=True, +) +def test_fix(runner: CliRunner, root_dir: Path, copy_test_files): + result = runner.invoke( + cli, + ["fix-pages", "docs/lang/docs/doc.md"], + ) + assert result.exit_code == 0, result.output + + fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text("utf-8") + expected_content = (data_path / "translated_doc_expected.md").read_text("utf-8") + assert fixed_content == expected_content + + assert "Fixing multiline code blocks in" in result.output + assert "Fixing markdown links in" in result.output diff --git a/scripts/tests/test_translation_fixer/test_header_permalinks/data/en_doc.md b/scripts/tests/test_translation_fixer/test_header_permalinks/data/en_doc.md new file mode 100644 index 0000000000..878e16fb5d --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_header_permalinks/data/en_doc.md @@ -0,0 +1,19 @@ +# Header 1 { #header-1 } + +Some text + +## Header 2 { #header-2 } + +Some more text + +### Header 3 { #header-3 } + +Even more text + +# Header 4 { #header-4 } + +A bit more text + +#Not a header + +Final portion of text diff --git a/scripts/tests/test_translation_fixer/test_header_permalinks/data/translated_doc_level_mismatch_1.md b/scripts/tests/test_translation_fixer/test_header_permalinks/data/translated_doc_level_mismatch_1.md new file mode 100644 index 0000000000..feefd77705 --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_header_permalinks/data/translated_doc_level_mismatch_1.md @@ -0,0 +1,19 @@ +# Header 1 { #header-1 } + +Some text + +# Header 2 { #header-2 } + +Some more text + +### Header 3 { #header-3 } + +Even more text + +# Header 4 { #header-4 } + +A bit more text + +#Not a header + +Final portion of text diff --git a/scripts/tests/test_translation_fixer/test_header_permalinks/data/translated_doc_level_mismatch_2.md b/scripts/tests/test_translation_fixer/test_header_permalinks/data/translated_doc_level_mismatch_2.md new file mode 100644 index 0000000000..ad53a660cd --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_header_permalinks/data/translated_doc_level_mismatch_2.md @@ -0,0 +1,19 @@ +# Header 1 { #header-1 } + +Some text + +## Header 2 { #header-2 } + +Some more text + +### Header 3 { #header-3 } + +Even more text + +## Header 4 { #header-4 } + +A bit more text + +#Not a header + +Final portion of text diff --git a/scripts/tests/test_translation_fixer/test_header_permalinks/data/translated_doc_number_gt.md b/scripts/tests/test_translation_fixer/test_header_permalinks/data/translated_doc_number_gt.md new file mode 100644 index 0000000000..9c517c9bb0 --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_header_permalinks/data/translated_doc_number_gt.md @@ -0,0 +1,19 @@ +# Header 1 { #header-1 } + +Some text + +## Header 2 { #header-2 } + +Some more text + +### Header 3 { #header-3 } + +Even more text + +# Header 4 { #header-4 } + +A bit more text + +# Extra header + +Final portion of text diff --git a/scripts/tests/test_translation_fixer/test_header_permalinks/data/translated_doc_number_lt.md b/scripts/tests/test_translation_fixer/test_header_permalinks/data/translated_doc_number_lt.md new file mode 100644 index 0000000000..8a308728b2 --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_header_permalinks/data/translated_doc_number_lt.md @@ -0,0 +1,19 @@ +# Header 1 { #header-1 } + +Some text + +## Header 2 { #header-2 } + +Some more text + +### Header 3 { #header-3 } + +Even more text + +Header 4 is missing + +A bit more text + +#Not a header + +Final portion of text diff --git a/scripts/tests/test_translation_fixer/test_header_permalinks/test_header_level_mismatch.py b/scripts/tests/test_translation_fixer/test_header_permalinks/test_header_level_mismatch.py new file mode 100644 index 0000000000..99d27db953 --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_header_permalinks/test_header_level_mismatch.py @@ -0,0 +1,60 @@ +from pathlib import Path + +import pytest +from typer.testing import CliRunner + +from scripts.translation_fixer import cli + +data_path = Path( + "scripts/tests/test_translation_fixer/test_header_permalinks/data" +).absolute() + + +@pytest.mark.parametrize( + "copy_test_files", + [(f"{data_path}/en_doc.md", f"{data_path}/translated_doc_level_mismatch_1.md")], + indirect=True, +) +def test_level_mismatch_1(runner: CliRunner, root_dir: Path, copy_test_files): + result = runner.invoke( + cli, + ["fix-pages", "docs/lang/docs/doc.md"], + ) + assert result.exit_code == 1 + + fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text("utf-8") + expected_content = Path( + f"{data_path}/translated_doc_level_mismatch_1.md" + ).read_text("utf-8") + + assert fixed_content == expected_content # Translated doc remains unchanged + assert "Error processing docs/lang/docs/doc.md" in result.output + assert ( + "Header levels do not match between document and original document" + " (found #, expected ##) for header №2 in line 5" + ) in result.output + + +@pytest.mark.parametrize( + "copy_test_files", + [(f"{data_path}/en_doc.md", f"{data_path}/translated_doc_level_mismatch_2.md")], + indirect=True, +) +def test_level_mismatch_2(runner: CliRunner, root_dir: Path, copy_test_files): + result = runner.invoke( + cli, + ["fix-pages", "docs/lang/docs/doc.md"], + ) + assert result.exit_code == 1 + + fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text("utf-8") + expected_content = Path( + f"{data_path}/translated_doc_level_mismatch_2.md" + ).read_text("utf-8") + + assert fixed_content == expected_content # Translated doc remains unchanged + assert "Error processing docs/lang/docs/doc.md" in result.output + assert ( + "Header levels do not match between document and original document" + " (found ##, expected #) for header №4 in line 13" + ) in result.output diff --git a/scripts/tests/test_translation_fixer/test_header_permalinks/test_header_number_mismatch.py b/scripts/tests/test_translation_fixer/test_header_permalinks/test_header_number_mismatch.py new file mode 100644 index 0000000000..c4d0346171 --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_header_permalinks/test_header_number_mismatch.py @@ -0,0 +1,60 @@ +from pathlib import Path + +import pytest +from typer.testing import CliRunner + +from scripts.translation_fixer import cli + +data_path = Path( + "scripts/tests/test_translation_fixer/test_header_permalinks/data" +).absolute() + + +@pytest.mark.parametrize( + "copy_test_files", + [(f"{data_path}/en_doc.md", f"{data_path}/translated_doc_number_gt.md")], + indirect=True, +) +def test_gt(runner: CliRunner, root_dir: Path, copy_test_files): + result = runner.invoke( + cli, + ["fix-pages", "docs/lang/docs/doc.md"], + ) + assert result.exit_code == 1 + + fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text("utf-8") + expected_content = Path(f"{data_path}/translated_doc_number_gt.md").read_text( + "utf-8" + ) + + assert fixed_content == expected_content # Translated doc remains unchanged + assert "Error processing docs/lang/docs/doc.md" in result.output + assert ( + "Number of headers with permalinks does not match the number " + "in the original document (5 vs 4)" + ) in result.output + + +@pytest.mark.parametrize( + "copy_test_files", + [(f"{data_path}/en_doc.md", f"{data_path}/translated_doc_number_lt.md")], + indirect=True, +) +def test_lt(runner: CliRunner, root_dir: Path, copy_test_files): + result = runner.invoke( + cli, + ["fix-pages", "docs/lang/docs/doc.md"], + ) + assert result.exit_code == 1 + + fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text("utf-8") + expected_content = Path(f"{data_path}/translated_doc_number_lt.md").read_text( + "utf-8" + ) + + assert fixed_content == expected_content # Translated doc remains unchanged + assert "Error processing docs/lang/docs/doc.md" in result.output + assert ( + "Number of headers with permalinks does not match the number " + "in the original document (3 vs 4)" + ) in result.output diff --git a/scripts/tests/test_translation_fixer/test_html_links/data/en_doc.md b/scripts/tests/test_translation_fixer/test_html_links/data/en_doc.md new file mode 100644 index 0000000000..4c4d104cf2 --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_html_links/data/en_doc.md @@ -0,0 +1,19 @@ +# Header 1 { #header-1 } + +Some text with a link to <a href="https://fastapi.tiangolo.com">FastAPI</a>. + +## Header 2 { #header-2 } + +Two links here: <a href="https://fastapi.tiangolo.com/how-to/">How to</a> and <a href="project-generation.md" class="internal-link" target="_blank">Project Generators</a>. + +### Header 3 { #header-3 } + +Another link: <a href="project-generation.md" class="internal-link" target="_blank" title="Link title">**FastAPI** Project Generators</a> with title. + +# Header 4 { #header-4 } + +Link to anchor: <a href="#header-2">Header 2</a> + +# Header with <a href="http://example.com">link</a> { #header-with-link } + +Some text diff --git a/scripts/tests/test_translation_fixer/test_html_links/data/translated_doc_number_gt.md b/scripts/tests/test_translation_fixer/test_html_links/data/translated_doc_number_gt.md new file mode 100644 index 0000000000..bac40242be --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_html_links/data/translated_doc_number_gt.md @@ -0,0 +1,21 @@ +# Заголовок 1 { #header-1 } + +Немного текста со ссылкой на <a href="https://fastapi.tiangolo.com">FastAPI</a>. + +## Заголовок 2 { #header-2 } + +Две ссылки здесь: <a href="https://fastapi.tiangolo.com/how-to/">How to</a> и <a href="project-generation.md" class="internal-link" target="_blank">Project Generators</a>. + +### Заголовок 3 { #header-3 } + +Ещё ссылка: <a href="project-generation.md" class="internal-link" target="_blank" title="Тайтл">**FastAPI** Генераторы Проектов</a> с тайтлом. + +И ещё одна <a href="https://github.com">экстра ссылка</a>. + +# Заголовок 4 { #header-4 } + +Ссылка на якорь: <a href="#header-2">Заголовок 2</a> + +# Заголовок со <a href="http://example.com">ссылкой</a> { #header-with-link } + +Немного текста diff --git a/scripts/tests/test_translation_fixer/test_html_links/data/translated_doc_number_lt.md b/scripts/tests/test_translation_fixer/test_html_links/data/translated_doc_number_lt.md new file mode 100644 index 0000000000..e2b36b6887 --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_html_links/data/translated_doc_number_lt.md @@ -0,0 +1,19 @@ +# Заголовок 1 { #header-1 } + +Немного текста со ссылкой на <a href="https://fastapi.tiangolo.com">FastAPI</a>. + +## Заголовок 2 { #header-2 } + +Две ссылки здесь: <a href="https://fastapi.tiangolo.com/how-to/">How to</a> и <a href="project-generation.md" class="internal-link" target="_blank">Project Generators</a>. + +### Заголовок 3 { #header-3 } + +Ещё ссылка: <a href="project-generation.md" class="internal-link" target="_blank" title="Тайтл">**FastAPI** Генераторы Проектов</a> с тайтлом. + +# Заголовок 4 { #header-4 } + +Ссылка на якорь: <a href="#header-2">Заголовок 2</a> + +# Заголовок с потерянной ссылкой { #header-with-link } + +Немного текста diff --git a/scripts/tests/test_translation_fixer/test_html_links/test_html_links_number_mismatch.py b/scripts/tests/test_translation_fixer/test_html_links/test_html_links_number_mismatch.py new file mode 100644 index 0000000000..11dcea1541 --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_html_links/test_html_links_number_mismatch.py @@ -0,0 +1,58 @@ +from pathlib import Path + +import pytest +from typer.testing import CliRunner + +from scripts.translation_fixer import cli + +data_path = Path("scripts/tests/test_translation_fixer/test_html_links/data").absolute() + + +@pytest.mark.parametrize( + "copy_test_files", + [(f"{data_path}/en_doc.md", f"{data_path}/translated_doc_number_gt.md")], + indirect=True, +) +def test_gt(runner: CliRunner, root_dir: Path, copy_test_files): + result = runner.invoke( + cli, + ["fix-pages", "docs/lang/docs/doc.md"], + ) + assert result.exit_code == 1, result.output + + fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text("utf-8") + expected_content = Path(f"{data_path}/translated_doc_number_gt.md").read_text( + "utf-8" + ) + + assert fixed_content == expected_content # Translated doc remains unchanged + assert "Error processing docs/lang/docs/doc.md" in result.output + assert ( + "Number of HTML links does not match the number " + "in the original document (7 vs 6)" + ) in result.output + + +@pytest.mark.parametrize( + "copy_test_files", + [(f"{data_path}/en_doc.md", f"{data_path}/translated_doc_number_lt.md")], + indirect=True, +) +def test_lt(runner: CliRunner, root_dir: Path, copy_test_files): + result = runner.invoke( + cli, + ["fix-pages", "docs/lang/docs/doc.md"], + ) + # assert result.exit_code == 1, result.output + + fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text("utf-8") + expected_content = Path(f"{data_path}/translated_doc_number_lt.md").read_text( + "utf-8" + ) + + assert fixed_content == expected_content # Translated doc remains unchanged + assert "Error processing docs/lang/docs/doc.md" in result.output + assert ( + "Number of HTML links does not match the number " + "in the original document (5 vs 6)" + ) in result.output diff --git a/scripts/tests/test_translation_fixer/test_markdown_links/data/en_doc.md b/scripts/tests/test_translation_fixer/test_markdown_links/data/en_doc.md new file mode 100644 index 0000000000..9d965b0d68 --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_markdown_links/data/en_doc.md @@ -0,0 +1,19 @@ +# Header 1 { #header-1 } + +Some text with a link to [FastAPI](https://fastapi.tiangolo.com). + +## Header 2 { #header-2 } + +Two links here: [How to](https://fastapi.tiangolo.com/how-to/) and [Project Generators](project-generation.md){.internal-link target=_blank}. + +### Header 3 { #header-3 } + +Another link: [**FastAPI** Project Generators](project-generation.md "Link title"){.internal-link target=_blank} with title. + +# Header 4 { #header-4 } + +Link to anchor: [Header 2](#header-2) + +# Header with [link](http://example.com) { #header-with-link } + +Some text diff --git a/scripts/tests/test_translation_fixer/test_markdown_links/data/translated_doc.md b/scripts/tests/test_translation_fixer/test_markdown_links/data/translated_doc.md new file mode 100644 index 0000000000..804a7e60a6 --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_markdown_links/data/translated_doc.md @@ -0,0 +1,19 @@ +# Заголовок 1 { #header-1 } + +Немного текста со ссылкой на [FastAPI](https://fastapi.tiangolo.com). + +## Заголовок 2 { #header-2 } + +Две ссылки здесь: [How to](https://fastapi.tiangolo.com/how-to/) и [Project Generators](project-generation.md){.internal-link target=_blank}. + +### Заголовок 3 { #header-3 } + +Ещё ссылка: [**FastAPI** Генераторы Проектов](project-generation.md "Тайтл"){.internal-link target=_blank} с тайтлом. + +# Заголовок 4 { #header-4 } + +Ссылка на якорь: [Заголовок 2](#header-2) + +# Заголовок со [ссылкой](http://example.com) { #header-with-link } + +Немного текста diff --git a/scripts/tests/test_translation_fixer/test_markdown_links/data/translated_doc_number_gt.md b/scripts/tests/test_translation_fixer/test_markdown_links/data/translated_doc_number_gt.md new file mode 100644 index 0000000000..9cbedb676e --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_markdown_links/data/translated_doc_number_gt.md @@ -0,0 +1,21 @@ +# Заголовок 1 { #header-1 } + +Немного текста со ссылкой на [FastAPI](https://fastapi.tiangolo.com). + +## Заголовок 2 { #header-2 } + +Две ссылки здесь: [How to](https://fastapi.tiangolo.com/how-to/) и [Project Generators](project-generation.md){.internal-link target=_blank}. + +### Заголовок 3 { #header-3 } + +Ещё ссылка: [**FastAPI** Генераторы Проектов](project-generation.md "Тайтл"){.internal-link target=_blank} с тайтлом. + +И ещё одна [экстра ссылка](https://github.com). + +# Заголовок 4 { #header-4 } + +Ссылка на якорь: [Заголовок 2](#header-2) + +# Заголовок со [ссылкой](http://example.com) { #header-with-link } + +Немного текста diff --git a/scripts/tests/test_translation_fixer/test_markdown_links/data/translated_doc_number_lt.md b/scripts/tests/test_translation_fixer/test_markdown_links/data/translated_doc_number_lt.md new file mode 100644 index 0000000000..4e9e6ccf78 --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_markdown_links/data/translated_doc_number_lt.md @@ -0,0 +1,19 @@ +# Заголовок 1 { #header-1 } + +Немного текста со ссылкой на [FastAPI](https://fastapi.tiangolo.com). + +## Заголовок 2 { #header-2 } + +Две ссылки здесь: [How to](https://fastapi.tiangolo.com/how-to/) и [Project Generators](project-generation.md){.internal-link target=_blank}. + +### Заголовок 3 { #header-3 } + +Ещё ссылка: [**FastAPI** Генераторы Проектов](project-generation.md "Тайтл"){.internal-link target=_blank} с тайтлом. + +# Заголовок 4 { #header-4 } + +Ссылка на якорь: [Заголовок 2](#header-2) + +# Заголовок с потерянной ссылкой { #header-with-link } + +Немного текста diff --git a/scripts/tests/test_translation_fixer/test_markdown_links/test_mkd_links_number_mismatch.py b/scripts/tests/test_translation_fixer/test_markdown_links/test_mkd_links_number_mismatch.py new file mode 100644 index 0000000000..c72e012543 --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_markdown_links/test_mkd_links_number_mismatch.py @@ -0,0 +1,60 @@ +from pathlib import Path + +import pytest +from typer.testing import CliRunner + +from scripts.translation_fixer import cli + +data_path = Path( + "scripts/tests/test_translation_fixer/test_markdown_links/data" +).absolute() + + +@pytest.mark.parametrize( + "copy_test_files", + [(f"{data_path}/en_doc.md", f"{data_path}/translated_doc_number_gt.md")], + indirect=True, +) +def test_gt(runner: CliRunner, root_dir: Path, copy_test_files): + result = runner.invoke( + cli, + ["fix-pages", "docs/lang/docs/doc.md"], + ) + assert result.exit_code == 1, result.output + + fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text("utf-8") + expected_content = Path(f"{data_path}/translated_doc_number_gt.md").read_text( + "utf-8" + ) + + assert fixed_content == expected_content # Translated doc remains unchanged + assert "Error processing docs/lang/docs/doc.md" in result.output + assert ( + "Number of markdown links does not match the number " + "in the original document (7 vs 6)" + ) in result.output + + +@pytest.mark.parametrize( + "copy_test_files", + [(f"{data_path}/en_doc.md", f"{data_path}/translated_doc_number_lt.md")], + indirect=True, +) +def test_lt(runner: CliRunner, root_dir: Path, copy_test_files): + result = runner.invoke( + cli, + ["fix-pages", "docs/lang/docs/doc.md"], + ) + # assert result.exit_code == 1, result.output + + fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text("utf-8") + expected_content = Path(f"{data_path}/translated_doc_number_lt.md").read_text( + "utf-8" + ) + + assert fixed_content == expected_content # Translated doc remains unchanged + assert "Error processing docs/lang/docs/doc.md" in result.output + assert ( + "Number of markdown links does not match the number " + "in the original document (5 vs 6)" + ) in result.output diff --git a/scripts/topic_repos.py b/scripts/topic_repos.py index bc14977513..b7afc0864a 100644 --- a/scripts/topic_repos.py +++ b/scripts/topic_repos.py @@ -56,9 +56,10 @@ def main() -> None: return repos_path.write_text(new_repos_content, encoding="utf-8") logging.info("Setting up GitHub Actions git user") - subprocess.run(["git", "config", "user.name", "github-actions"], check=True) + subprocess.run(["git", "config", "user.name", "github-actions[bot]"], check=True) subprocess.run( - ["git", "config", "user.email", "github-actions@github.com"], check=True + ["git", "config", "user.email", "github-actions[bot]@users.noreply.github.com"], + check=True, ) branch_name = f"fastapi-topic-repos-{secrets.token_hex(4)}" logging.info(f"Creating a new branch {branch_name}") diff --git a/scripts/translate.py b/scripts/translate.py index ede101e8fc..eba4ad6a2f 100644 --- a/scripts/translate.py +++ b/scripts/translate.py @@ -1,3 +1,4 @@ +import json import secrets import subprocess from collections.abc import Iterable @@ -24,650 +25,8 @@ non_translated_sections = ( "contributing.md", ) - -general_prompt = """ -### About literal text in this prompt - -1) In the following instructions (after I say: `The above rules are in effect now`) the two characters `«` and `»` will be used to surround LITERAL TEXT, which is text or characters you shall interpret literally. The `«` and the `»` are not part of the literal text, they are the meta characters denoting it. - -2) Furthermore, text surrounded by `«««` and `»»»` is a BLOCK OF LITERAL TEXT which spans multiple lines. To get its content, dedent all lines of the block until the `«««` and `»»»` are at column zero, then remove the newline (`\n`) after the `«««` and the newline before the `»»»`. The `«««` and the `»»»` are not part of the literal text block, they are the meta characters denoting it. - -3) If you see backticks or any other quotes inside literal text – inside `«` and `»` – or inside blocks of literal text – inside `«««` and `»»»` – then interpret them as literal characters, do NOT interpret them as meta characters. - -The above rules are in effect now. - - -### Definitions of terms used in this prompt - -"backtick" - - The character «`» - Unicode U+0060 (GRAVE ACCENT) - -"single backtick" - - A single backtick – «`» - -"triple backticks" - - Three backticks in a row – «```» - -"neutral double quote" - - The character «"» - Unicode U+0022 (QUOTATION MARK) - -"neutral single quote" - - The character «'» - Unicode U+0027 (APOSTROPHE) - -"English double typographic quotes" - - The characters «“» and «”» - Unicode U+201C (LEFT DOUBLE QUOTATION MARK) and Unicode U+201D (RIGHT DOUBLE QUOTATION MARK) - -"English single typographic quotes" - - The characters «‘» and «’» - Unicode U+2018 (LEFT SINGLE QUOTATION MARK) and Unicode U+2019 (RIGHT SINGLE QUOTATION MARK) - -"code snippet" - - Also called "inline code". Text in a Markdown document which is surrounded by single backticks. A paragraph in a Markdown document can have a more than one code snippet. - - Example: - - ««« - `i am a code snippet` - »»» - - Example: - - ««« - `first code snippet` `second code snippet` `third code snippet` - »»» - -"code block" - - Text in a Markdown document which is surrounded by triple backticks. Spreads multiple lines. - - Example: - - ««« - ``` - Hello - World - ``` - »»» - - Example: - - ««« - ```python - print("hello World") - ``` - »»» - -"HTML element" - - a HTML opening tag – e.g. «<div>» – and a HTML closing tag – e.g. «</div>» – surrounding text or other HTML elements. - - -### Your task - -Translate an English text – the original content – to a target language. - -The original content is written in Markdown, write the translation in Markdown as well. - -The original content will be surrounded by triple percentage signs («%%%»). Do not include the triple percentage signs in the translation. - - -### Technical terms in English - -For technical terms in English that don't have a common translation term, use the original term in English. - - -### Content of code snippets - -Do not translate the content of code snippets, keep the original in English. For example, «`list`», «`dict`», keep them as is. - - -### Content of code blocks - -Do not translate the content of code blocks, except for comments in the language which the code block uses. - -Examples: - - Source (English) – The code block is a bash code example with one comment: - - ««« - ```bash - # Print greeting - echo "Hello, World!" - ``` - »»» - - Result (German): - - ««« - ```bash - # Gruß ausgeben - echo "Hello, World!" - ``` - »»» - - Source (English) – The code block is a console example containing HTML tags. No comments, so nothing to change here: - - ««« - ```console - $ <font color="#4E9A06">fastapi</font> run <u style="text-decoration-style:solid">main.py</u> - <span style="background-color:#009485"><font color="#D3D7CF"> FastAPI </font></span> Starting server - Searching for package file structure - ``` - »»» - - Result (German): - - ««« - ```console - $ <font color="#4E9A06">fastapi</font> run <u style="text-decoration-style:solid">main.py</u> - <span style="background-color:#009485"><font color="#D3D7CF"> FastAPI </font></span> Starting server - Searching for package file structure - ``` - »»» - - Source (English) – The code block is a console example containing 5 comments: - - ««« - ```console - // Go to the home directory - $ cd - // Create a directory for all your code projects - $ mkdir code - // Enter into that code directory - $ cd code - // Create a directory for this project - $ mkdir awesome-project - // Enter into that project directory - $ cd awesome-project - ``` - »»» - - Result (German): - - ««« - ```console - // Gehe zum Home-Verzeichnis - $ cd - // Erstelle ein Verzeichnis für alle Ihre Code-Projekte - $ mkdir code - // Gehe in dieses Code-Verzeichnis - $ cd code - // Erstelle ein Verzeichnis für dieses Projekt - $ mkdir awesome-project - // Gehe in dieses Projektverzeichnis - $ cd awesome-project - ``` - »»» - -If there is an existing translation and its Mermaid diagram is in sync with the Mermaid diagram in the English source, except a few translated words, then use the Mermaid diagram of the existing translation. The human editor of the translation translated these words in the Mermaid diagram. Keep these translations, do not revert them back to the English source. - -Example: - - Source (English): - - ««« - ```mermaid - flowchart LR - subgraph global[global env] - harry-1[harry v1] - end - subgraph stone-project[philosophers-stone project] - stone(philosophers-stone) -->|requires| harry-1 - end - ``` - »»» - - Existing translation (German) – has three translations: - - ««« - ```mermaid - flowchart LR - subgraph global[globale Umgebung] - harry-1[harry v1] - end - subgraph stone-project[philosophers-stone-Projekt] - stone(philosophers-stone) -->|benötigt| harry-1 - end - ``` - »»» - - Result (German) – you change nothing: - - ««« - ```mermaid - flowchart LR - subgraph global[globale Umgebung] - harry-1[harry v1] - end - subgraph stone-project[philosophers-stone-Projekt] - stone(philosophers-stone) -->|benötigt| harry-1 - end - ``` - »»» - - -### Special blocks - -There are special blocks of notes, tips and others that look like: - - ««« - /// note - »»» - -To translate it, keep the same line and add the translation after a vertical bar. - -For example, if you were translating to Spanish, you would write: - - ««« - /// note | Nota - »»» - -Some examples in Spanish: - - Source: - - ««« - /// tip - »»» - - Result: - - ««« - /// tip | Consejo - »»» - - Source: - - ««« - /// details | Preview - »»» - - Result: - - ««« - /// details | Vista previa - »»» - - -### Tab blocks - -There are special blocks surrounded by four slashes («////»). They mark text, which will be rendered as part of a tab in the final document. The scheme is: - - //// tab | {tab title} - {tab content, may span many lines} - //// - -Keep everything before the vertical bar («|») as is, including the vertical bar. Translate the tab title. Translate the tab content, applying the rules you know. Keep the four block closing slashes as is. - -Examples: - - Source (English): - - ««« - //// tab | Python 3.8+ non-Annotated - Hello - //// - »»» - - Result (German): - - ««« - //// tab | Python 3.8+ nicht annotiert - Hallo - //// - »»» - - Source (English) – Here there is nothing to translate in the tab title: - - ««« - //// tab | Linux, macOS, Windows Bash - Hello again - //// - »»» - - Result (German): - - ««« - //// tab | Linux, macOS, Windows Bash - Hallo wieder - //// - »»» - - -### Headings - -Every Markdown heading in the English text (all levels) ends with a part inside curly brackets. This part denotes the hash of this heading, which is used in links to this heading. In translations, translate the heading, but do not translate this hash part, so that links do not break. - -Examples of how to translate a heading: - - Source (English): - - ««« - ## Alternative API docs { #alternative-api-docs } - »»» - - Result (Spanish): - - ««« - ## Documentación de la API alternativa { #alternative-api-docs } - »»» - - Source (English): - - ««« - ### Example { #example } - »»» - - Result (German): - - ««« - ### Beispiel { #example } - »»» - - -### Links - -Use the following rules for links (apply both to Markdown-style links ([text](url)) and to HTML-style <a> tags): - -1) For relative URLs, only translate link text. Do not translate the URL or its parts - -Example: - - Source (English): - - ««« - [One of the fastest Python frameworks available](#performance) - »»» - - Result (German): - - ««« - [Eines der schnellsten verfügbaren Python-Frameworks](#performance) - »»» - -2) For absolute URLs which DO NOT start EXACTLY with «https://fastapi.tiangolo.com», only translate link text and leave the URL unchanged. - -Example: - - Source (English): - - ««« - <a href="https://sqlmodel.tiangolo.com/" class="external-link" target="_blank">SQLModel docs</a> - »»» - - Result (German): - - ««« - <a href="https://sqlmodel.tiangolo.com/" class="external-link" target="_blank">SQLModel-Dokumentation</a> - »»» - -3) For absolute URLs which DO start EXACTLY with «https://fastapi.tiangolo.com», only translate link text and change the URL by adding language code («https://fastapi.tiangolo.com/{language_code}[rest part of the url]»). - -Example: - - Source (English): - - ««« - <a href="https://fastapi.tiangolo.com/tutorial/path-params/#documentation" class="external-link" target="_blank">Documentation</a> - »»» - - Result (Spanish): - - ««« - <a href="https://fastapi.tiangolo.com/es/tutorial/path-params/#documentation" class="external-link" target="_blank">Documentación</a> - »»» - -3.1) Do not add language codes for URLs that point to static assets (e.g., images, CSS, JavaScript). - -Example: - - Source (English): - - ««« - <a href="https://fastapi.tiangolo.com/img/something.jpg" class="external-link" target="_blank">Something</a> - »»» - - Result (Spanish): - - ««« - <a href="https://fastapi.tiangolo.com/img/something.jpg" class="external-link" target="_blank">Algo</a> - »»» - -4) For internal links, only translate link text. - -Example: - - Source (English): - - ««« - [Create Pull Requests](help-fastapi.md#create-a-pull-request){.internal-link target=_blank} - »»» - - Result (German): - - ««« - [Pull Requests erzeugen](help-fastapi.md#create-a-pull-request){.internal-link target=_blank} - »»» - -5) Do not translate anchor fragments in links (the part after «#»), as they must remain the same to work correctly. - -5.1) If an existing translation has a link with an anchor fragment different to the anchor fragment in the English source, then this is an error. Fix this by using the anchor fragment of the English source. - -Example: - - Source (English): - - ««« - [Body - Multiple Parameters: Singular values in body](body-multiple-params.md#singular-values-in-body){.internal-link target=_blank} - »»» - - Existing wrong translation (German) – notice the wrongly translated anchor fragment: - - ««« - [Body – Mehrere Parameter: Einfache Werte im Body](body-multiple-params.md#einzelne-werte-im-body){.internal-link target=_blank}. - »»» - - Result (German) – you fix the anchor fragment: - - ««« - [Body – Mehrere Parameter: Einfache Werte im Body](body-multiple-params.md#singular-values-in-body){.internal-link target=_blank}. - »»» - -5.2) Do not add anchor fragments at will, even if this makes sense. If the English source has no anchor, don't add one. - -Example: - - Source (English): - - ««« - Create a [virtual environment](../virtual-environments.md){.internal-link target=_blank} - »»» - - Wrong translation (German) – Anchor added to the URL. - - ««« - Erstelle eine [virtuelle Umgebung](../virtual-environments.md#create-a-virtual-environment){.internal-link target=_blank} - »»» - - Good translation (German) – URL stays like in the English source. - - ««« - Erstelle eine [Virtuelle Umgebung](../virtual-environments.md){.internal-link target=_blank} - »»» - - -### HTML abbr elements - -Translate HTML abbr elements («<abbr title="description">text</abbr>») as follows: - -1) If the text surrounded by the abbr element is an abbreviation (the text may be surrounded by further HTML or Markdown markup or quotes, for example «<code>text</code>» or «`text`» or «"text"», ignore that further markup when deciding if the text is an abbreviation), and if the description (the text inside the title attribute) contains the full phrase for this abbreviation, then append a dash («–») to the full phrase, followed by the translation of the full phrase. - -Conversion scheme: - - Source (English): - - <abbr title="{full phrase}">{abbreviation}</abbr> - - Result: - - <abbr title="{full phrase} – {translation of full phrase}">{abbreviation}</abbr> - -Examples: - - Source (English): - - ««« - <abbr title="Internet of Things">IoT</abbr> - <abbr title="Central Processing Unit">CPU</abbr> - <abbr title="too long; didn't read"><strong>TL;DR:</strong></abbr> - »»» - - Result (German): - - ««« - <abbr title="Internet of Things – Internet der Dinge">IoT</abbr> - <abbr title="Central Processing Unit – Zentrale Verarbeitungseinheit">CPU</abbr> - <abbr title="too long; didn't read – zu lang; hab's nicht gelesen"><strong>TL;DR:</strong></abbr> - »»» - -1.1) If the language to which you translate mostly uses the letters of the ASCII char set (for example Spanish, French, German, but not Russian, Chinese) and if the translation of the full phrase is identical to, or starts with the same letters as the original full phrase, then only give the translation of the full phrase. - -Conversion scheme: - - Source (English): - - <abbr title="{full phrase}">{abbreviation}</abbr> - - Result: - - <abbr title="{translation of full phrase}">{abbreviation}</abbr> - -Examples: - - Source (English): - - ««« - <abbr title="JSON Web Tokens">JWT</abbr> - <abbr title="Enumeration">Enum</abbr> - <abbr title="Asynchronous Server Gateway Interface">ASGI</abbr> - »»» - - Result (German): - - ««« - <abbr title="JSON Web Tokens">JWT</abbr> - <abbr title="Enumeration">Enum</abbr> - <abbr title="Asynchrones Server-Gateway-Interface">ASGI</abbr> - »»» - -2) If the description is not a full phrase for an abbreviation which the abbr element surrounds, but some other information, then just translate the description. - -Conversion scheme: - - Source (English): - - <abbr title="{description}">{text}</abbr> - - Result: - - <abbr title="{translation of description}">{translation of text}</abbr> - -Examples: - - Source (English): - - ««« - <abbr title="also known as: endpoints, routes">path</abbr> - <abbr title="a program that checks for code errors">linter</abbr> - <abbr title="converting the string that comes from an HTTP request into Python data">parsing</abbr> - <abbr title="before 2023-03">0.95.0</abbr> - <abbr title="2023-08-26">at the time of writing this</abbr> - »»» - - Result (German): - - ««« - <abbr title="auch bekannt als: Endpunkte, Routen">Pfad</abbr> - <abbr title="Programm das auf Fehler im Code prüft">Linter</abbr> - <abbr title="Konvertieren des Strings eines HTTP-Requests in Python-Daten">Parsen</abbr> - <abbr title="vor 2023-03">0.95.0</abbr> - <abbr title="2023-08-26">zum Zeitpunkt als das hier geschrieben wurde</abbr> - »»» - - -3) If the text surrounded by the abbr element is an abbreviation and the description contains both the full phrase for that abbreviation, and other information, separated by a colon («:»), then append a dash («–») and the translation of the full phrase to the original full phrase and translate the other information. - -Conversion scheme: - - Source (English): - - <abbr title="{full phrase}: {other information}">{abbreviation}</abbr> - - Result: - - <abbr title="{full phrase} – {translation of full phrase}: {translation of other information}">{abbreviation}</abbr> - -Examples: - - Source (English): - - ««« - <abbr title="Input/Output: disk reading or writing, network communication.">I/O</abbr> - <abbr title="Content Delivery Network: service, that provides static files.">CDN</abbr> - <abbr title="Integrated Development Environment: similar to a code editor">IDE</abbr> - »»» - - Result (German): - - ««« - <abbr title="Input/Output – Eingabe/Ausgabe: Lesen oder Schreiben auf der Festplatte, Netzwerkkommunikation.">I/O</abbr> - <abbr title="Content Delivery Network – Inhalte auslieferndes Netzwerk: Dienst, der statische Dateien bereitstellt.">CDN</abbr> - <abbr title="Integrated Development Environment – Integrierte Entwicklungsumgebung: Ähnlich einem Code-Editor">IDE</abbr> - »»» - -3.1) Like in rule 2.1, you can leave the original full phrase away, if the translated full phrase is identical or starts with the same letters as the original full phrase. - -Conversion scheme: - - Source (English): - - <abbr title="{full phrase}: {information}">{abbreviation}</abbr> - - Result: - - <abbr title="{translation of full phrase}: {translation of information}">{abbreviation}</abbr> - -Example: - - Source (English): - - ««« - <abbr title="Object Relational Mapper: a fancy term for a library where some classes represent SQL tables and instances represent rows in those tables">ORM</abbr> - »»» - - Result (German): - - ««« - <abbr title="Objektrelationaler Mapper: Ein Fachbegriff für eine Bibliothek, in der einige Klassen SQL-Tabellen und Instanzen Zeilen in diesen Tabellen darstellen">ORM</abbr> - »»» - -4) If there is an existing translation, and it has ADDITIONAL abbr elements in a sentence, and these additional abbr elements do not exist in the related sentence in the English text, then KEEP those additional abbr elements in the translation. Do not remove them. Except when you remove the whole sentence from the translation, because the whole sentence was removed from the English text, then also remove the abbr element. The reasoning for this rule is, that such additional abbr elements are manually added by the human editor of the translation, in order to translate or explain an English word to the human readers of the translation. These additional abbr elements would not make sense in the English text, but they do make sense in the translation. So keep them in the translation, even though they are not part of the English text. This rule only applies to abbr elements. - -5) Apply above rules also when there is an existing translation! Make sure that all title attributes in abbr elements get properly translated or updated, using the schemes given above. However, leave the ADDITIONAL abbr's from rule 4 alone. Do not change their formatting or content. - -""" +general_prompt_path = Path(__file__).absolute().parent / "general-llm-prompt.md" +general_prompt = general_prompt_path.read_text(encoding="utf-8") app = typer.Typer() @@ -726,7 +85,7 @@ def translate_page( print(f"Found existing translation: {out_path}") old_translation = out_path.read_text(encoding="utf-8") print(f"Translating {en_path} to {language} ({language_name})") - agent = Agent("openai:gpt-5") + agent = Agent("openai:gpt-5.2") prompt_segments = [ general_prompt, @@ -828,6 +187,65 @@ def translate_lang(language: Annotated[str, typer.Option(envvar="LANGUAGE")]) -> print(f"Done translating: {p}") +def get_llm_translatable() -> list[str]: + translatable_langs = [] + langs = get_langs() + for lang in langs: + if lang == "en": + continue + lang_prompt_path = Path(f"docs/{lang}/llm-prompt.md") + if lang_prompt_path.exists(): + translatable_langs.append(lang) + return translatable_langs + + +@app.command() +def list_llm_translatable() -> list[str]: + translatable_langs = get_llm_translatable() + print("LLM translatable languages:", translatable_langs) + return translatable_langs + + +@app.command() +def llm_translatable_json( + language: Annotated[str | None, typer.Option(envvar="LANGUAGE")] = None, +) -> None: + translatable_langs = get_llm_translatable() + if language: + if language in translatable_langs: + print(json.dumps([language])) + return + else: + raise typer.Exit(code=1) + print(json.dumps(translatable_langs)) + + +@app.command() +def commands_json( + command: Annotated[str | None, typer.Option(envvar="COMMAND")] = None, +) -> None: + available_commands = [ + "translate-page", + "translate-lang", + "update-outdated", + "add-missing", + "update-and-add", + "remove-removable", + ] + default_commands = [ + "remove-removable", + "update-outdated", + "add-missing", + ] + if command: + if command in available_commands: + print(json.dumps([command])) + return + else: + raise typer.Exit(code=1) + print(json.dumps(default_commands)) + + @app.command() def list_removable(language: str) -> list[Path]: removable_paths: list[Path] = [] @@ -854,7 +272,7 @@ def list_all_removable() -> list[Path]: @app.command() -def remove_removable(language: str) -> None: +def remove_removable(language: Annotated[str, typer.Option(envvar="LANGUAGE")]) -> None: removable_paths = list_removable(language) for path in removable_paths: path.unlink() @@ -939,24 +357,39 @@ def update_and_add(language: Annotated[str, typer.Option(envvar="LANGUAGE")]) -> def make_pr( *, language: Annotated[str | None, typer.Option(envvar="LANGUAGE")] = None, + command: Annotated[str | None, typer.Option(envvar="COMMAND")] = None, github_token: Annotated[str, typer.Option(envvar="GITHUB_TOKEN")], github_repository: Annotated[str, typer.Option(envvar="GITHUB_REPOSITORY")], + commit_in_place: Annotated[ + bool, typer.Option(envvar="COMMIT_IN_PLACE", show_default=True) + ] = False, ) -> None: print("Setting up GitHub Actions git user") repo = git.Repo(Path(__file__).absolute().parent.parent) if not repo.is_dirty(untracked_files=True): print("Repository is clean, no changes to commit") return - subprocess.run(["git", "config", "user.name", "github-actions"], check=True) + subprocess.run(["git", "config", "user.name", "github-actions[bot]"], check=True) subprocess.run( - ["git", "config", "user.email", "github-actions@github.com"], check=True + ["git", "config", "user.email", "github-actions[bot]@users.noreply.github.com"], + check=True, ) - branch_name = "translate" - if language: - branch_name += f"-{language}" - branch_name += f"-{secrets.token_hex(4)}" - print(f"Creating a new branch {branch_name}") - subprocess.run(["git", "checkout", "-b", branch_name], check=True) + current_branch = repo.active_branch.name + if current_branch == "master" and commit_in_place: + print("Can't commit directly to master") + raise typer.Exit(code=1) + + if not commit_in_place: + branch_name = "translate" + if language: + branch_name += f"-{language}" + if command: + branch_name += f"-{command}" + branch_name += f"-{secrets.token_hex(4)}" + print(f"Creating a new branch {branch_name}") + subprocess.run(["git", "checkout", "-b", branch_name], check=True) + else: + print(f"Committing in place on branch {current_branch}") print("Adding updated files") git_path = Path("docs") subprocess.run(["git", "add", str(git_path)], check=True) @@ -964,16 +397,25 @@ def make_pr( message = "🌐 Update translations" if language: message += f" for {language}" + if command: + message += f" ({command})" subprocess.run(["git", "commit", "-m", message], check=True) print("Pushing branch") subprocess.run(["git", "push", "origin", branch_name], check=True) - print("Creating PR") - g = Github(github_token) - gh_repo = g.get_repo(github_repository) - pr = gh_repo.create_pull( - title=message, body=message, base="master", head=branch_name - ) - print(f"Created PR: {pr.number}") + if not commit_in_place: + print("Creating PR") + g = Github(github_token) + gh_repo = g.get_repo(github_repository) + body = ( + message + + "\n\nThis PR was created automatically using LLMs." + + f"\n\nIt uses the prompt file https://github.com/fastapi/fastapi/blob/master/docs/{language}/llm-prompt.md." + + "\n\nIn most cases, it's better to make PRs updating that file so that the LLM can do a better job generating the translations than suggesting changes in this PR." + ) + pr = gh_repo.create_pull( + title=message, body=body, base="master", head=branch_name + ) + print(f"Created PR: {pr.number}") print("Finished") diff --git a/scripts/translation_fixer.py b/scripts/translation_fixer.py new file mode 100644 index 0000000000..3e1f42d514 --- /dev/null +++ b/scripts/translation_fixer.py @@ -0,0 +1,132 @@ +import os +from collections.abc import Iterable +from pathlib import Path +from typing import Annotated + +import typer + +from scripts.doc_parsing_utils import check_translation + +non_translated_sections = ( + f"reference{os.sep}", + "release-notes.md", + "fastapi-people.md", + "external-links.md", + "newsletter.md", + "management-tasks.md", + "management.md", + "contributing.md", +) + + +cli = typer.Typer() + + +@cli.callback() +def callback(): + pass + + +def iter_all_lang_paths(lang_path_root: Path) -> Iterable[Path]: + """ + Iterate on the markdown files to translate in order of priority. + """ + + first_dirs = [ + lang_path_root / "learn", + lang_path_root / "tutorial", + lang_path_root / "advanced", + lang_path_root / "about", + lang_path_root / "how-to", + ] + first_parent = lang_path_root + yield from first_parent.glob("*.md") + for dir_path in first_dirs: + yield from dir_path.rglob("*.md") + first_dirs_str = tuple(str(d) for d in first_dirs) + for path in lang_path_root.rglob("*.md"): + if str(path).startswith(first_dirs_str): + continue + if path.parent == first_parent: + continue + yield path + + +def get_all_paths(lang: str): + res: list[str] = [] + lang_docs_root = Path("docs") / lang / "docs" + for path in iter_all_lang_paths(lang_docs_root): + relpath = path.relative_to(lang_docs_root) + if not str(relpath).startswith(non_translated_sections): + res.append(str(relpath)) + return res + + +def process_one_page(path: Path) -> bool: + """ + Fix one translated document by comparing it to the English version. + + Returns True if processed successfully, False otherwise. + """ + + try: + lang_code = path.parts[1] + if lang_code == "en": + print(f"Skipping English document: {path}") + return True + + en_doc_path = Path("docs") / "en" / Path(*path.parts[2:]) + + doc_lines = path.read_text(encoding="utf-8").splitlines() + en_doc_lines = en_doc_path.read_text(encoding="utf-8").splitlines() + + doc_lines = check_translation( + doc_lines=doc_lines, + en_doc_lines=en_doc_lines, + lang_code=lang_code, + auto_fix=True, + path=str(path), + ) + + # Write back the fixed document + doc_lines.append("") # Ensure file ends with a newline + path.write_text("\n".join(doc_lines), encoding="utf-8") + + except ValueError as e: + print(f"Error processing {path}: {e}") + return False + return True + + +@cli.command() +def fix_all(ctx: typer.Context, language: str): + docs = get_all_paths(language) + + all_good = True + for page in docs: + doc_path = Path("docs") / language / "docs" / page + res = process_one_page(doc_path) + all_good = all_good and res + + if not all_good: + raise typer.Exit(code=1) + + +@cli.command() +def fix_pages( + doc_paths: Annotated[ + list[Path], + typer.Argument(help="List of paths to documents."), + ], +): + all_good = True + for path in doc_paths: + res = process_one_page(path) + all_good = all_good and res + + if not all_good: + raise typer.Exit(code=1) + + +if __name__ == "__main__": + cli() diff --git a/tests/benchmarks/__init__.py b/tests/benchmarks/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/benchmarks/test_general_performance.py b/tests/benchmarks/test_general_performance.py new file mode 100644 index 0000000000..87add6d174 --- /dev/null +++ b/tests/benchmarks/test_general_performance.py @@ -0,0 +1,399 @@ +import json +import sys +from collections.abc import Iterator +from typing import Annotated, Any + +import pytest +from fastapi import Depends, FastAPI +from fastapi.testclient import TestClient +from pydantic import BaseModel + +if "--codspeed" not in sys.argv: + pytest.skip( + "Benchmark tests are skipped by default; run with --codspeed.", + allow_module_level=True, + ) + +LARGE_ITEMS: list[dict[str, Any]] = [ + { + "id": i, + "name": f"item-{i}", + "values": list(range(25)), + "meta": { + "active": True, + "group": i % 10, + "tag": f"t{i % 5}", + }, + } + for i in range(300) +] + +LARGE_METADATA: dict[str, Any] = { + "source": "benchmark", + "version": 1, + "flags": {"a": True, "b": False, "c": True}, + "notes": ["x" * 50, "y" * 50, "z" * 50], +} + +LARGE_PAYLOAD: dict[str, Any] = {"items": LARGE_ITEMS, "metadata": LARGE_METADATA} + + +def dep_a(): + return 40 + + +def dep_b(a: Annotated[int, Depends(dep_a)]): + return a + 2 + + +class ItemIn(BaseModel): + name: str + value: int + + +class ItemOut(BaseModel): + name: str + value: int + dep: int + + +class LargeIn(BaseModel): + items: list[dict[str, Any]] + metadata: dict[str, Any] + + +class LargeOut(BaseModel): + items: list[dict[str, Any]] + metadata: dict[str, Any] + + +app = FastAPI() + + +@app.post("/sync/validated", response_model=ItemOut) +def sync_validated(item: ItemIn, dep: Annotated[int, Depends(dep_b)]): + return ItemOut(name=item.name, value=item.value, dep=dep) + + +@app.get("/sync/dict-no-response-model") +def sync_dict_no_response_model(): + return {"name": "foo", "value": 123} + + +@app.get("/sync/dict-with-response-model", response_model=ItemOut) +def sync_dict_with_response_model( + dep: Annotated[int, Depends(dep_b)], +): + return {"name": "foo", "value": 123, "dep": dep} + + +@app.get("/sync/model-no-response-model") +def sync_model_no_response_model(dep: Annotated[int, Depends(dep_b)]): + return ItemOut(name="foo", value=123, dep=dep) + + +@app.get("/sync/model-with-response-model", response_model=ItemOut) +def sync_model_with_response_model(dep: Annotated[int, Depends(dep_b)]): + return ItemOut(name="foo", value=123, dep=dep) + + +@app.post("/async/validated", response_model=ItemOut) +async def async_validated( + item: ItemIn, + dep: Annotated[int, Depends(dep_b)], +): + return ItemOut(name=item.name, value=item.value, dep=dep) + + +@app.post("/sync/large-receive") +def sync_large_receive(payload: LargeIn): + return {"received": len(payload.items)} + + +@app.post("/async/large-receive") +async def async_large_receive(payload: LargeIn): + return {"received": len(payload.items)} + + +@app.get("/sync/large-dict-no-response-model") +def sync_large_dict_no_response_model(): + return LARGE_PAYLOAD + + +@app.get("/sync/large-dict-with-response-model", response_model=LargeOut) +def sync_large_dict_with_response_model(): + return LARGE_PAYLOAD + + +@app.get("/sync/large-model-no-response-model") +def sync_large_model_no_response_model(): + return LargeOut(items=LARGE_ITEMS, metadata=LARGE_METADATA) + + +@app.get("/sync/large-model-with-response-model", response_model=LargeOut) +def sync_large_model_with_response_model(): + return LargeOut(items=LARGE_ITEMS, metadata=LARGE_METADATA) + + +@app.get("/async/large-dict-no-response-model") +async def async_large_dict_no_response_model(): + return LARGE_PAYLOAD + + +@app.get("/async/large-dict-with-response-model", response_model=LargeOut) +async def async_large_dict_with_response_model(): + return LARGE_PAYLOAD + + +@app.get("/async/large-model-no-response-model") +async def async_large_model_no_response_model(): + return LargeOut(items=LARGE_ITEMS, metadata=LARGE_METADATA) + + +@app.get("/async/large-model-with-response-model", response_model=LargeOut) +async def async_large_model_with_response_model(): + return LargeOut(items=LARGE_ITEMS, metadata=LARGE_METADATA) + + +@app.get("/async/dict-no-response-model") +async def async_dict_no_response_model(): + return {"name": "foo", "value": 123} + + +@app.get("/async/dict-with-response-model", response_model=ItemOut) +async def async_dict_with_response_model( + dep: Annotated[int, Depends(dep_b)], +): + return {"name": "foo", "value": 123, "dep": dep} + + +@app.get("/async/model-no-response-model") +async def async_model_no_response_model( + dep: Annotated[int, Depends(dep_b)], +): + return ItemOut(name="foo", value=123, dep=dep) + + +@app.get("/async/model-with-response-model", response_model=ItemOut) +async def async_model_with_response_model( + dep: Annotated[int, Depends(dep_b)], +): + return ItemOut(name="foo", value=123, dep=dep) + + +@pytest.fixture(scope="module") +def client() -> Iterator[TestClient]: + with TestClient(app) as client: + yield client + + +def _bench_get(benchmark, client: TestClient, path: str) -> tuple[int, bytes]: + warmup = client.get(path) + assert warmup.status_code == 200 + + def do_request() -> tuple[int, bytes]: + response = client.get(path) + return response.status_code, response.content + + return benchmark(do_request) + + +def _bench_post_json( + benchmark, client: TestClient, path: str, json: dict[str, Any] +) -> tuple[int, bytes]: + warmup = client.post(path, json=json) + assert warmup.status_code == 200 + + def do_request() -> tuple[int, bytes]: + response = client.post(path, json=json) + return response.status_code, response.content + + return benchmark(do_request) + + +def test_sync_receiving_validated_pydantic_model(benchmark, client: TestClient) -> None: + status_code, body = _bench_post_json( + benchmark, + client, + "/sync/validated", + json={"name": "foo", "value": 123}, + ) + assert status_code == 200 + assert body == b'{"name":"foo","value":123,"dep":42}' + + +def test_sync_return_dict_without_response_model(benchmark, client: TestClient) -> None: + status_code, body = _bench_get(benchmark, client, "/sync/dict-no-response-model") + assert status_code == 200 + assert body == b'{"name":"foo","value":123}' + + +def test_sync_return_dict_with_response_model(benchmark, client: TestClient) -> None: + status_code, body = _bench_get(benchmark, client, "/sync/dict-with-response-model") + assert status_code == 200 + assert body == b'{"name":"foo","value":123,"dep":42}' + + +def test_sync_return_model_without_response_model( + benchmark, client: TestClient +) -> None: + status_code, body = _bench_get(benchmark, client, "/sync/model-no-response-model") + assert status_code == 200 + assert body == b'{"name":"foo","value":123,"dep":42}' + + +def test_sync_return_model_with_response_model(benchmark, client: TestClient) -> None: + status_code, body = _bench_get(benchmark, client, "/sync/model-with-response-model") + assert status_code == 200 + assert body == b'{"name":"foo","value":123,"dep":42}' + + +def test_async_receiving_validated_pydantic_model( + benchmark, client: TestClient +) -> None: + status_code, body = _bench_post_json( + benchmark, client, "/async/validated", json={"name": "foo", "value": 123} + ) + assert status_code == 200 + assert body == b'{"name":"foo","value":123,"dep":42}' + + +def test_async_return_dict_without_response_model( + benchmark, client: TestClient +) -> None: + status_code, body = _bench_get(benchmark, client, "/async/dict-no-response-model") + assert status_code == 200 + assert body == b'{"name":"foo","value":123}' + + +def test_async_return_dict_with_response_model(benchmark, client: TestClient) -> None: + status_code, body = _bench_get(benchmark, client, "/async/dict-with-response-model") + assert status_code == 200 + assert body == b'{"name":"foo","value":123,"dep":42}' + + +def test_async_return_model_without_response_model( + benchmark, client: TestClient +) -> None: + status_code, body = _bench_get(benchmark, client, "/async/model-no-response-model") + assert status_code == 200 + assert body == b'{"name":"foo","value":123,"dep":42}' + + +def test_async_return_model_with_response_model(benchmark, client: TestClient) -> None: + status_code, body = _bench_get( + benchmark, client, "/async/model-with-response-model" + ) + assert status_code == 200 + assert body == b'{"name":"foo","value":123,"dep":42}' + + +def test_sync_receiving_large_payload(benchmark, client: TestClient) -> None: + status_code, body = _bench_post_json( + benchmark, + client, + "/sync/large-receive", + json=LARGE_PAYLOAD, + ) + assert status_code == 200 + assert body == b'{"received":300}' + + +def test_async_receiving_large_payload(benchmark, client: TestClient) -> None: + status_code, body = _bench_post_json( + benchmark, + client, + "/async/large-receive", + json=LARGE_PAYLOAD, + ) + assert status_code == 200 + assert body == b'{"received":300}' + + +def _expected_large_payload_json_bytes() -> bytes: + return json.dumps( + LARGE_PAYLOAD, + ensure_ascii=False, + allow_nan=False, + separators=(",", ":"), + ).encode("utf-8") + + +def test_sync_return_large_dict_without_response_model( + benchmark, client: TestClient +) -> None: + status_code, body = _bench_get( + benchmark, client, "/sync/large-dict-no-response-model" + ) + assert status_code == 200 + assert body == _expected_large_payload_json_bytes() + + +def test_sync_return_large_dict_with_response_model( + benchmark, client: TestClient +) -> None: + status_code, body = _bench_get( + benchmark, client, "/sync/large-dict-with-response-model" + ) + assert status_code == 200 + assert body == _expected_large_payload_json_bytes() + + +def test_sync_return_large_model_without_response_model( + benchmark, client: TestClient +) -> None: + status_code, body = _bench_get( + benchmark, client, "/sync/large-model-no-response-model" + ) + assert status_code == 200 + assert body == _expected_large_payload_json_bytes() + + +def test_sync_return_large_model_with_response_model( + benchmark, client: TestClient +) -> None: + status_code, body = _bench_get( + benchmark, client, "/sync/large-model-with-response-model" + ) + assert status_code == 200 + assert body == _expected_large_payload_json_bytes() + + +def test_async_return_large_dict_without_response_model( + benchmark, client: TestClient +) -> None: + status_code, body = _bench_get( + benchmark, client, "/async/large-dict-no-response-model" + ) + assert status_code == 200 + assert body == _expected_large_payload_json_bytes() + + +def test_async_return_large_dict_with_response_model( + benchmark, client: TestClient +) -> None: + status_code, body = _bench_get( + benchmark, client, "/async/large-dict-with-response-model" + ) + assert status_code == 200 + assert body == _expected_large_payload_json_bytes() + + +def test_async_return_large_model_without_response_model( + benchmark, client: TestClient +) -> None: + status_code, body = _bench_get( + benchmark, client, "/async/large-model-no-response-model" + ) + assert status_code == 200 + assert body == _expected_large_payload_json_bytes() + + +def test_async_return_large_model_with_response_model( + benchmark, client: TestClient +) -> None: + status_code, body = _bench_get( + benchmark, client, "/async/large-model-with-response-model" + ) + assert status_code == 200 + assert body == _expected_large_payload_json_bytes() diff --git a/tests/forward_reference_type.py b/tests/forward_reference_type.py new file mode 100644 index 0000000000..52a0d4a708 --- /dev/null +++ b/tests/forward_reference_type.py @@ -0,0 +1,9 @@ +from pydantic import BaseModel + + +def forwardref_method(input: "ForwardRefModel") -> "ForwardRefModel": + return ForwardRefModel(x=input.x + 1) + + +class ForwardRefModel(BaseModel): + x: int = 0 diff --git a/tests/main.py b/tests/main.py index 2f1d617115..7edb16c615 100644 --- a/tests/main.py +++ b/tests/main.py @@ -1,5 +1,5 @@ import http -from typing import FrozenSet, List, Optional +from typing import Optional from fastapi import FastAPI, Path, Query @@ -195,15 +195,15 @@ def get_enum_status_code(): @app.get("/query/frozenset") -def get_query_type_frozenset(query: FrozenSet[int] = Query(...)): +def get_query_type_frozenset(query: frozenset[int] = Query(...)): return ",".join(map(str, sorted(query))) @app.get("/query/list") -def get_query_list(device_ids: List[int] = Query()) -> List[int]: +def get_query_list(device_ids: list[int] = Query()) -> list[int]: return device_ids @app.get("/query/list-default") -def get_query_list_default(device_ids: List[int] = Query(default=[])) -> List[int]: +def get_query_list_default(device_ids: list[int] = Query(default=[])) -> list[int]: return device_ids diff --git a/tests/test_additional_properties.py b/tests/test_additional_properties.py index be14d10edc..2622366400 100644 --- a/tests/test_additional_properties.py +++ b/tests/test_additional_properties.py @@ -1,5 +1,3 @@ -from typing import Dict - from fastapi import FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel @@ -8,7 +6,7 @@ app = FastAPI() class Items(BaseModel): - items: Dict[str, int] + items: dict[str, int] @app.post("/foo") diff --git a/tests/test_additional_properties_bool.py b/tests/test_additional_properties_bool.py index de59e48ce7..063297a3f2 100644 --- a/tests/test_additional_properties_bool.py +++ b/tests/test_additional_properties_bool.py @@ -1,19 +1,12 @@ from typing import Union -from dirty_equals import IsDict from fastapi import FastAPI -from fastapi._compat import PYDANTIC_V2 from fastapi.testclient import TestClient from pydantic import BaseModel, ConfigDict class FooBaseModel(BaseModel): - if PYDANTIC_V2: - model_config = ConfigDict(extra="forbid") - else: - - class Config: - extra = "forbid" + model_config = ConfigDict(extra="forbid") class Foo(FooBaseModel): @@ -58,19 +51,13 @@ def test_openapi_schema(): "requestBody": { "content": { "application/json": { - "schema": IsDict( - { - "anyOf": [ - {"$ref": "#/components/schemas/Foo"}, - {"type": "null"}, - ], - "title": "Foo", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"$ref": "#/components/schemas/Foo"} - ) + "schema": { + "anyOf": [ + {"$ref": "#/components/schemas/Foo"}, + {"type": "null"}, + ], + "title": "Foo", + } } } }, diff --git a/tests/test_additional_responses_custom_model_in_callback.py b/tests/test_additional_responses_custom_model_in_callback.py index 2ad5754551..376d7714ed 100644 --- a/tests/test_additional_responses_custom_model_in_callback.py +++ b/tests/test_additional_responses_custom_model_in_callback.py @@ -1,6 +1,6 @@ -from dirty_equals import IsDict from fastapi import APIRouter, FastAPI from fastapi.testclient import TestClient +from inline_snapshot import snapshot from pydantic import BaseModel, HttpUrl from starlette.responses import JSONResponse @@ -32,121 +32,114 @@ client = TestClient(app) def test_openapi_schema(): 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": { - "/": { - "post": { - "summary": "Main Route", - "operationId": "main_route__post", - "parameters": [ - { - "required": True, - "schema": IsDict( - { - "title": "Callback Url", - "minLength": 1, - "type": "string", - "format": "uri", - } - ) - # TODO: remove when deprecating Pydantic v1 - | IsDict( - { + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/": { + "post": { + "summary": "Main Route", + "operationId": "main_route__post", + "parameters": [ + { + "required": True, + "schema": { "title": "Callback Url", "maxLength": 2083, "minLength": 1, "type": "string", "format": "uri", - } - ), - "name": "callback_url", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, + }, + "name": "callback_url", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "callbacks": { + "callback_route": { + "{$callback_url}/callback/": { + "get": { + "summary": "Callback Route", + "operationId": "callback_route__callback_url__callback__get", + "responses": { + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CustomModel" + } + } + }, + "description": "Bad Request", + }, + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + }, } } - }, + } + }, + } + } + }, + "components": { + "schemas": { + "CustomModel": { + "title": "CustomModel", + "required": ["a"], + "type": "object", + "properties": {"a": {"title": "A", "type": "integer"}}, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } }, }, - "callbacks": { - "callback_route": { - "{$callback_url}/callback/": { - "get": { - "summary": "Callback Route", - "operationId": "callback_route__callback_url__callback__get", - "responses": { - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CustomModel" - } - } - }, - "description": "Bad Request", - }, - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - }, - } - } - } + "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"}, + }, }, } - } - }, - "components": { - "schemas": { - "CustomModel": { - "title": "CustomModel", - "required": ["a"], - "type": "object", - "properties": {"a": {"title": "A", "type": "integer"}}, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "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_additional_responses_custom_validationerror.py b/tests/test_additional_responses_custom_validationerror.py index 9fec5c96d4..8724e5ecb8 100644 --- a/tests/test_additional_responses_custom_validationerror.py +++ b/tests/test_additional_responses_custom_validationerror.py @@ -1,5 +1,3 @@ -import typing - from fastapi import FastAPI from fastapi.responses import JSONResponse from fastapi.testclient import TestClient @@ -18,7 +16,7 @@ class Error(BaseModel): class JsonApiError(BaseModel): - errors: typing.List[Error] + errors: list[Error] @app.get( diff --git a/tests/test_additional_responses_response_class.py b/tests/test_additional_responses_response_class.py index 68753561c9..fecc3ee16b 100644 --- a/tests/test_additional_responses_response_class.py +++ b/tests/test_additional_responses_response_class.py @@ -1,5 +1,3 @@ -import typing - from fastapi import FastAPI from fastapi.responses import JSONResponse from fastapi.testclient import TestClient @@ -18,7 +16,7 @@ class Error(BaseModel): class JsonApiError(BaseModel): - errors: typing.List[Error] + errors: list[Error] @app.get( diff --git a/tests/test_allow_inf_nan_in_enforcing.py b/tests/test_allow_inf_nan_in_enforcing.py index 9e855fdf81..083a024af0 100644 --- a/tests/test_allow_inf_nan_in_enforcing.py +++ b/tests/test_allow_inf_nan_in_enforcing.py @@ -1,7 +1,8 @@ +from typing import Annotated + import pytest from fastapi import Body, FastAPI, Query from fastapi.testclient import TestClient -from typing_extensions import Annotated app = FastAPI() diff --git a/tests/test_ambiguous_params.py b/tests/test_ambiguous_params.py index 8a31442eb0..44d49b7818 100644 --- a/tests/test_ambiguous_params.py +++ b/tests/test_ambiguous_params.py @@ -1,9 +1,9 @@ +from typing import Annotated + import pytest from fastapi import Depends, FastAPI, Path from fastapi.param_functions import Query from fastapi.testclient import TestClient -from fastapi.utils import PYDANTIC_V2 -from typing_extensions import Annotated app = FastAPI() @@ -70,6 +70,5 @@ def test_multiple_annotations(): response = client.get("/multi-query", params={"foo": "123"}) assert response.status_code == 422 - if PYDANTIC_V2: - response = client.get("/multi-query", params={"foo": "1"}) - assert response.status_code == 422 + response = client.get("/multi-query", params={"foo": "1"}) + assert response.status_code == 422 diff --git a/tests/test_annotated.py b/tests/test_annotated.py index 473d33e52c..39f6f83b29 100644 --- a/tests/test_annotated.py +++ b/tests/test_annotated.py @@ -1,8 +1,8 @@ +from typing import Annotated + import pytest -from dirty_equals import IsDict from fastapi import APIRouter, FastAPI, Query from fastapi.testclient import TestClient -from typing_extensions import Annotated app = FastAPI() @@ -31,44 +31,23 @@ client = TestClient(app) foo_is_missing = { "detail": [ - IsDict( - { - "loc": ["query", "foo"], - "msg": "Field required", - "type": "missing", - "input": None, - } - ) - # TODO: remove when deprecating Pydantic v1 - | IsDict( - { - "loc": ["query", "foo"], - "msg": "field required", - "type": "value_error.missing", - } - ) + { + "loc": ["query", "foo"], + "msg": "Field required", + "type": "missing", + "input": None, + } ] } foo_is_short = { "detail": [ - IsDict( - { - "ctx": {"min_length": 1}, - "loc": ["query", "foo"], - "msg": "String should have at least 1 character", - "type": "string_too_short", - "input": "", - } - ) - # TODO: remove when deprecating Pydantic v1 - | IsDict( - { - "ctx": {"limit_value": 1}, - "loc": ["query", "foo"], - "msg": "ensure this value has at least 1 characters", - "type": "value_error.any_str.min_length", - } - ) + { + "ctx": {"min_length": 1}, + "loc": ["query", "foo"], + "msg": "String should have at least 1 character", + "type": "string_too_short", + "input": "", + } ] } diff --git a/tests/test_application.py b/tests/test_application.py index 8f1b0a18d3..001586ff78 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -1,5 +1,4 @@ import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from .main import app @@ -274,14 +273,10 @@ def test_openapi_schema(): "name": "item_id", "in": "path", "required": True, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Item Id", - } - ) - # TODO: remove when deprecating Pydantic v1 - | IsDict({"title": "Item Id", "type": "string"}), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Item Id", + }, } ], } @@ -984,14 +979,10 @@ def test_openapi_schema(): "name": "query", "in": "query", "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "integer"}, {"type": "null"}], - "title": "Query", - } - ) - # TODO: remove when deprecating Pydantic v1 - | IsDict({"title": "Query", "type": "integer"}), + "schema": { + "anyOf": [{"type": "integer"}, {"type": "null"}], + "title": "Query", + }, } ], } diff --git a/tests/test_arbitrary_types.py b/tests/test_arbitrary_types.py new file mode 100644 index 0000000000..481acc3bfc --- /dev/null +++ b/tests/test_arbitrary_types.py @@ -0,0 +1,135 @@ +from typing import Annotated + +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient +from inline_snapshot import snapshot + + +@pytest.fixture(name="client") +def get_client(): + from pydantic import ( + BaseModel, + ConfigDict, + PlainSerializer, + TypeAdapter, + WithJsonSchema, + ) + + class FakeNumpyArray: + def __init__(self): + self.data = [1.0, 2.0, 3.0] + + FakeNumpyArrayPydantic = Annotated[ + FakeNumpyArray, + WithJsonSchema(TypeAdapter(list[float]).json_schema()), + PlainSerializer(lambda v: v.data), + ] + + class MyModel(BaseModel): + model_config = ConfigDict(arbitrary_types_allowed=True) + custom_field: FakeNumpyArrayPydantic + + app = FastAPI() + + @app.get("/") + def test() -> MyModel: + return MyModel(custom_field=FakeNumpyArray()) + + client = TestClient(app) + return client + + +def test_get(client: TestClient): + response = client.get("/") + assert response.json() == {"custom_field": [1.0, 2.0, 3.0]} + + +def test_typeadapter(): + # This test is only to confirm that Pydantic alone is working as expected + from pydantic import ( + BaseModel, + ConfigDict, + PlainSerializer, + TypeAdapter, + WithJsonSchema, + ) + + class FakeNumpyArray: + def __init__(self): + self.data = [1.0, 2.0, 3.0] + + FakeNumpyArrayPydantic = Annotated[ + FakeNumpyArray, + WithJsonSchema(TypeAdapter(list[float]).json_schema()), + PlainSerializer(lambda v: v.data), + ] + + class MyModel(BaseModel): + model_config = ConfigDict(arbitrary_types_allowed=True) + custom_field: FakeNumpyArrayPydantic + + ta = TypeAdapter(MyModel) + assert ta.dump_python(MyModel(custom_field=FakeNumpyArray())) == { + "custom_field": [1.0, 2.0, 3.0] + } + assert ta.json_schema() == snapshot( + { + "properties": { + "custom_field": { + "items": {"type": "number"}, + "title": "Custom Field", + "type": "array", + } + }, + "required": ["custom_field"], + "title": "MyModel", + "type": "object", + } + ) + + +def test_openapi_schema(client: TestClient): + response = client.get("openapi.json") + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/": { + "get": { + "summary": "Test", + "operationId": "test__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MyModel" + } + } + }, + } + }, + } + } + }, + "components": { + "schemas": { + "MyModel": { + "properties": { + "custom_field": { + "items": {"type": "number"}, + "type": "array", + "title": "Custom Field", + } + }, + "type": "object", + "required": ["custom_field"], + "title": "MyModel", + } + } + }, + } + ) diff --git a/tests/test_compat.py b/tests/test_compat.py index 0184c9a2ee..0b5600f8f5 100644 --- a/tests/test_compat.py +++ b/tests/test_compat.py @@ -1,23 +1,18 @@ -from typing import Any, Dict, List, Union +from typing import Union from fastapi import FastAPI, UploadFile from fastapi._compat import ( Undefined, - _get_model_config, - get_cached_model_fields, - is_scalar_field, is_uploadfile_sequence_annotation, - may_v1, ) from fastapi._compat.shared import is_bytes_sequence_annotation from fastapi.testclient import TestClient from pydantic import BaseModel, ConfigDict from pydantic.fields import FieldInfo -from .utils import needs_py_lt_314, needs_pydanticv2 +from .utils import needs_py310 -@needs_pydanticv2 def test_model_field_default_required(): from fastapi._compat import v2 @@ -27,41 +22,11 @@ def test_model_field_default_required(): assert field.default is Undefined -@needs_py_lt_314 -def test_v1_plain_validator_function(): - from fastapi._compat import v1 - - # For coverage - def func(v): # pragma: no cover - return v - - result = v1.with_info_plain_validator_function(func) - assert result == {} - - -def test_is_model_field(): - # For coverage - from fastapi._compat import _is_model_field - - assert not _is_model_field(str) - - -@needs_pydanticv2 -def test_get_model_config(): - # For coverage in Pydantic v2 - class Foo(BaseModel): - model_config = ConfigDict(from_attributes=True) - - foo = Foo() - config = _get_model_config(foo) - assert config == {"from_attributes": True} - - def test_complex(): app = FastAPI() @app.post("/") - def foo(foo: Union[str, List[int]]): + def foo(foo: Union[str, list[int]]): return foo client = TestClient(app) @@ -75,7 +40,6 @@ def test_complex(): assert response2.json() == [1, 2] -@needs_pydanticv2 def test_propagates_pydantic2_model_config(): app = FastAPI() @@ -95,7 +59,7 @@ def test_propagates_pydantic2_model_config(): embedded_model: EmbeddedModel = EmbeddedModel() @app.post("/") - def foo(req: Model) -> Dict[str, Union[str, None]]: + def foo(req: Model) -> dict[str, Union[str, None]]: return { "value": req.value or None, "embedded_value": req.embedded_model.value or None, @@ -125,7 +89,7 @@ def test_is_bytes_sequence_annotation_union(): # TODO: in theory this would allow declaring types that could be lists of bytes # to be read from files and other types, but I'm not even sure it's a good idea # to support it as a first class "feature" - assert is_bytes_sequence_annotation(Union[List[str], List[bytes]]) + assert is_bytes_sequence_annotation(Union[list[str], list[bytes]]) def test_is_uploadfile_sequence_annotation(): @@ -133,34 +97,38 @@ def test_is_uploadfile_sequence_annotation(): # TODO: in theory this would allow declaring types that could be lists of UploadFile # and other types, but I'm not even sure it's a good idea to support it as a first # class "feature" - assert is_uploadfile_sequence_annotation(Union[List[str], List[UploadFile]]) + assert is_uploadfile_sequence_annotation(Union[list[str], list[UploadFile]]) -@needs_py_lt_314 -def test_is_pv1_scalar_field(): - from fastapi._compat import v1 +def test_serialize_sequence_value_with_optional_list(): + """Test that serialize_sequence_value handles optional lists correctly.""" + from fastapi._compat import v2 - # For coverage - class Model(v1.BaseModel): - foo: Union[str, Dict[str, Any]] - - fields = v1.get_model_fields(Model) - assert not is_scalar_field(fields[0]) + field_info = FieldInfo(annotation=Union[list[str], None]) + field = v2.ModelField(name="items", field_info=field_info) + result = v2.serialize_sequence_value(field=field, value=["a", "b", "c"]) + assert result == ["a", "b", "c"] + assert isinstance(result, list) -@needs_py_lt_314 -def test_get_model_fields_cached(): - from fastapi._compat import v1 +@needs_py310 +def test_serialize_sequence_value_with_optional_list_pipe_union(): + """Test that serialize_sequence_value handles optional lists correctly (with new syntax).""" + from fastapi._compat import v2 - class Model(may_v1.BaseModel): - foo: str + field_info = FieldInfo(annotation=list[str] | None) + field = v2.ModelField(name="items", field_info=field_info) + result = v2.serialize_sequence_value(field=field, value=["a", "b", "c"]) + assert result == ["a", "b", "c"] + assert isinstance(result, list) - non_cached_fields = v1.get_model_fields(Model) - non_cached_fields2 = v1.get_model_fields(Model) - cached_fields = get_cached_model_fields(Model) - cached_fields2 = get_cached_model_fields(Model) - for f1, f2 in zip(cached_fields, cached_fields2): - assert f1 is f2 - assert non_cached_fields is not non_cached_fields2 - assert cached_fields is cached_fields2 +def test_serialize_sequence_value_with_none_first_in_union(): + """Test that serialize_sequence_value handles Union[None, List[...]] correctly.""" + from fastapi._compat import v2 + + field_info = FieldInfo(annotation=Union[None, list[str]]) + field = v2.ModelField(name="items", field_info=field_info) + result = v2.serialize_sequence_value(field=field, value=["x", "y"]) + assert result == ["x", "y"] + assert isinstance(result, list) diff --git a/tests/test_compat_params_v1.py b/tests/test_compat_params_v1.py deleted file mode 100644 index 7064761cb5..0000000000 --- a/tests/test_compat_params_v1.py +++ /dev/null @@ -1,1122 +0,0 @@ -import sys -from typing import List, Optional - -import pytest - -from tests.utils import pydantic_snapshot, skip_module_if_py_gte_314 - -if sys.version_info >= (3, 14): - skip_module_if_py_gte_314() - -from fastapi import FastAPI -from fastapi._compat.v1 import BaseModel -from fastapi.temp_pydantic_v1_params import ( - Body, - Cookie, - File, - Form, - Header, - Path, - Query, -) -from fastapi.testclient import TestClient -from inline_snapshot import snapshot -from typing_extensions import Annotated - - -class Item(BaseModel): - name: str - price: float - description: Optional[str] = None - - -app = FastAPI() - - -@app.get("/items/{item_id}") -def get_item_with_path( - item_id: Annotated[int, Path(title="The ID of the item", ge=1, le=1000)], -): - return {"item_id": item_id} - - -@app.get("/items/") -def get_items_with_query( - q: Annotated[ - Optional[str], Query(min_length=3, max_length=50, pattern="^[a-zA-Z0-9 ]+$") - ] = None, - skip: Annotated[int, Query(ge=0)] = 0, - limit: Annotated[int, Query(ge=1, le=100, examples=[5])] = 10, -): - return {"q": q, "skip": skip, "limit": limit} - - -@app.get("/users/") -def get_user_with_header( - x_custom: Annotated[Optional[str], Header()] = None, - x_token: Annotated[Optional[str], Header(convert_underscores=True)] = None, -): - return {"x_custom": x_custom, "x_token": x_token} - - -@app.get("/cookies/") -def get_cookies( - session_id: Annotated[Optional[str], Cookie()] = None, - tracking_id: Annotated[Optional[str], Cookie(min_length=10)] = None, -): - return {"session_id": session_id, "tracking_id": tracking_id} - - -@app.post("/items/") -def create_item( - item: Annotated[ - Item, - Body(examples=[{"name": "Foo", "price": 35.4, "description": "The Foo item"}]), - ], -): - return {"item": item} - - -@app.post("/items-embed/") -def create_item_embed( - item: Annotated[Item, Body(embed=True)], -): - return {"item": item} - - -@app.put("/items/{item_id}") -def update_item( - item_id: Annotated[int, Path(ge=1)], - item: Annotated[Item, Body()], - importance: Annotated[int, Body(gt=0, le=10)], -): - return {"item": item, "importance": importance} - - -@app.post("/form-data/") -def submit_form( - username: Annotated[str, Form(min_length=3, max_length=50)], - password: Annotated[str, Form(min_length=8)], - email: Annotated[Optional[str], Form()] = None, -): - return {"username": username, "password": password, "email": email} - - -@app.post("/upload/") -def upload_file( - file: Annotated[bytes, File()], - description: Annotated[Optional[str], Form()] = None, -): - return {"file_size": len(file), "description": description} - - -@app.post("/upload-multiple/") -def upload_multiple_files( - files: Annotated[List[bytes], File()], - note: Annotated[str, Form()] = "", -): - return { - "file_count": len(files), - "total_size": sum(len(f) for f in files), - "note": note, - } - - -client = TestClient(app) - - -# Path parameter tests -def test_path_param_valid(): - response = client.get("/items/50") - assert response.status_code == 200 - assert response.json() == {"item_id": 50} - - -def test_path_param_too_large(): - response = client.get("/items/1001") - assert response.status_code == 422 - error = response.json()["detail"][0] - assert error["loc"] == ["path", "item_id"] - - -def test_path_param_too_small(): - response = client.get("/items/0") - assert response.status_code == 422 - error = response.json()["detail"][0] - assert error["loc"] == ["path", "item_id"] - - -# Query parameter tests -def test_query_params_valid(): - response = client.get("/items/?q=test search&skip=5&limit=20") - assert response.status_code == 200 - assert response.json() == {"q": "test search", "skip": 5, "limit": 20} - - -def test_query_params_defaults(): - response = client.get("/items/") - assert response.status_code == 200 - assert response.json() == {"q": None, "skip": 0, "limit": 10} - - -def test_query_param_too_short(): - response = client.get("/items/?q=ab") - assert response.status_code == 422 - error = response.json()["detail"][0] - assert error["loc"] == ["query", "q"] - - -def test_query_param_invalid_pattern(): - response = client.get("/items/?q=test@#$") - assert response.status_code == 422 - error = response.json()["detail"][0] - assert error["loc"] == ["query", "q"] - - -def test_query_param_limit_too_large(): - response = client.get("/items/?limit=101") - assert response.status_code == 422 - error = response.json()["detail"][0] - assert error["loc"] == ["query", "limit"] - - -# Header parameter tests -def test_header_params(): - response = client.get( - "/users/", - headers={"X-Custom": "Plumbus", "X-Token": "secret-token"}, - ) - assert response.status_code == 200 - assert response.json() == { - "x_custom": "Plumbus", - "x_token": "secret-token", - } - - -def test_header_underscore_conversion(): - response = client.get( - "/users/", - headers={"x-token": "secret-token-with-dash"}, - ) - assert response.status_code == 200 - assert response.json()["x_token"] == "secret-token-with-dash" - - -def test_header_params_none(): - response = client.get("/users/") - assert response.status_code == 200 - assert response.json() == {"x_custom": None, "x_token": None} - - -# Cookie parameter tests -def test_cookie_params(): - with TestClient(app) as client: - client.cookies.set("session_id", "abc123") - client.cookies.set("tracking_id", "1234567890abcdef") - response = client.get("/cookies/") - assert response.status_code == 200 - assert response.json() == { - "session_id": "abc123", - "tracking_id": "1234567890abcdef", - } - - -def test_cookie_tracking_id_too_short(): - with TestClient(app) as client: - client.cookies.set("tracking_id", "short") - response = client.get("/cookies/") - assert response.status_code == 422 - assert response.json() == snapshot( - { - "detail": [ - { - "loc": ["cookie", "tracking_id"], - "msg": "ensure this value has at least 10 characters", - "type": "value_error.any_str.min_length", - "ctx": {"limit_value": 10}, - } - ] - } - ) - - -def test_cookie_params_none(): - response = client.get("/cookies/") - assert response.status_code == 200 - assert response.json() == {"session_id": None, "tracking_id": None} - - -# Body parameter tests -def test_body_param(): - response = client.post( - "/items/", - json={"name": "Test Item", "price": 29.99, "description": "A test item"}, - ) - assert response.status_code == 200 - assert response.json() == { - "item": { - "name": "Test Item", - "price": 29.99, - "description": "A test item", - } - } - - -def test_body_param_minimal(): - response = client.post( - "/items/", - json={"name": "Minimal", "price": 9.99}, - ) - assert response.status_code == 200 - assert response.json() == { - "item": {"name": "Minimal", "price": 9.99, "description": None} - } - - -def test_body_param_missing_required(): - response = client.post( - "/items/", - json={"name": "Incomplete"}, - ) - assert response.status_code == 422 - error = response.json()["detail"][0] - assert error["loc"] == ["body", "price"] - - -def test_body_embed(): - response = client.post( - "/items-embed/", - json={"item": {"name": "Embedded", "price": 15.0}}, - ) - assert response.status_code == 200 - assert response.json() == { - "item": {"name": "Embedded", "price": 15.0, "description": None} - } - - -def test_body_embed_wrong_structure(): - response = client.post( - "/items-embed/", - json={"name": "Not Embedded", "price": 15.0}, - ) - assert response.status_code == 422 - - -# Multiple body parameters test -def test_multiple_body_params(): - response = client.put( - "/items/5", - json={ - "item": {"name": "Updated Item", "price": 49.99}, - "importance": 8, - }, - ) - assert response.status_code == 200 - assert response.json() == snapshot( - { - "item": {"name": "Updated Item", "price": 49.99, "description": None}, - "importance": 8, - } - ) - - -def test_multiple_body_params_importance_too_large(): - response = client.put( - "/items/5", - json={ - "item": {"name": "Item", "price": 10.0}, - "importance": 11, - }, - ) - assert response.status_code == 422 - assert response.json() == snapshot( - { - "detail": [ - { - "loc": ["body", "importance"], - "msg": "ensure this value is less than or equal to 10", - "type": "value_error.number.not_le", - "ctx": {"limit_value": 10}, - } - ] - } - ) - - -def test_multiple_body_params_importance_too_small(): - response = client.put( - "/items/5", - json={ - "item": {"name": "Item", "price": 10.0}, - "importance": 0, - }, - ) - assert response.status_code == 422 - assert response.json() == snapshot( - { - "detail": [ - { - "loc": ["body", "importance"], - "msg": "ensure this value is greater than 0", - "type": "value_error.number.not_gt", - "ctx": {"limit_value": 0}, - } - ] - } - ) - - -# Form parameter tests -def test_form_data_valid(): - response = client.post( - "/form-data/", - data={ - "username": "testuser", - "password": "password123", - "email": "test@example.com", - }, - ) - assert response.status_code == 200, response.text - assert response.json() == { - "username": "testuser", - "password": "password123", - "email": "test@example.com", - } - - -def test_form_data_optional_field(): - response = client.post( - "/form-data/", - data={"username": "testuser", "password": "password123"}, - ) - assert response.status_code == 200 - assert response.json() == { - "username": "testuser", - "password": "password123", - "email": None, - } - - -def test_form_data_username_too_short(): - response = client.post( - "/form-data/", - data={"username": "ab", "password": "password123"}, - ) - assert response.status_code == 422 - assert response.json() == snapshot( - { - "detail": [ - { - "loc": ["body", "username"], - "msg": "ensure this value has at least 3 characters", - "type": "value_error.any_str.min_length", - "ctx": {"limit_value": 3}, - } - ] - } - ) - - -def test_form_data_password_too_short(): - response = client.post( - "/form-data/", - data={"username": "testuser", "password": "short"}, - ) - assert response.status_code == 422 - assert response.json() == snapshot( - { - "detail": [ - { - "loc": ["body", "password"], - "msg": "ensure this value has at least 8 characters", - "type": "value_error.any_str.min_length", - "ctx": {"limit_value": 8}, - } - ] - } - ) - - -# File upload tests -def test_upload_file(): - response = client.post( - "/upload/", - files={"file": ("test.txt", b"Hello, World!", "text/plain")}, - data={"description": "A test file"}, - ) - assert response.status_code == 200 - assert response.json() == { - "file_size": 13, - "description": "A test file", - } - - -def test_upload_file_without_description(): - response = client.post( - "/upload/", - files={"file": ("test.txt", b"Hello!", "text/plain")}, - ) - assert response.status_code == 200 - assert response.json() == { - "file_size": 6, - "description": None, - } - - -def test_upload_multiple_files(): - response = client.post( - "/upload-multiple/", - files=[ - ("files", ("file1.txt", b"Content 1", "text/plain")), - ("files", ("file2.txt", b"Content 2", "text/plain")), - ("files", ("file3.txt", b"Content 3", "text/plain")), - ], - data={"note": "Multiple files uploaded"}, - ) - assert response.status_code == 200 - assert response.json() == { - "file_count": 3, - "total_size": 27, - "note": "Multiple files uploaded", - } - - -def test_upload_multiple_files_empty_note(): - response = client.post( - "/upload-multiple/", - files=[ - ("files", ("file1.txt", b"Test", "text/plain")), - ], - ) - assert response.status_code == 200 - assert response.json()["file_count"] == 1 - assert response.json()["note"] == "" - - -# __repr__ tests -def test_query_repr(): - query_param = Query(default=None, min_length=3) - assert repr(query_param) == "Query(None)" - - -def test_body_repr(): - body_param = Body(default=None) - assert repr(body_param) == "Body(None)" - - -# Deprecation warning tests for regex parameter -def test_query_regex_deprecation_warning(): - with pytest.warns(DeprecationWarning, match="`regex` has been deprecated"): - Query(regex="^test$") - - -def test_body_regex_deprecation_warning(): - with pytest.warns(DeprecationWarning, match="`regex` has been deprecated"): - Body(regex="^test$") - - -# Deprecation warning tests for example parameter -def test_query_example_deprecation_warning(): - with pytest.warns(DeprecationWarning, match="`example` has been deprecated"): - Query(example="test example") - - -def test_body_example_deprecation_warning(): - with pytest.warns(DeprecationWarning, match="`example` has been deprecated"): - Body(example={"test": "example"}) - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == snapshot( - { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "get": { - "summary": "Get Item With Path", - "operationId": "get_item_with_path_items__item_id__get", - "parameters": [ - { - "name": "item_id", - "in": "path", - "required": True, - "schema": { - "title": "The ID of the item", - "minimum": 1, - "maximum": 1000, - "type": "integer", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "put": { - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "name": "item_id", - "in": "path", - "required": True, - "schema": { - "title": "Item Id", - "minimum": 1, - "type": "integer", - }, - } - ], - "requestBody": { - "required": True, - "content": { - "application/json": { - "schema": pydantic_snapshot( - v1=snapshot( - { - "$ref": "#/components/schemas/Body_update_item_items__item_id__put" - } - ), - v2=snapshot( - { - "title": "Body", - "allOf": [ - { - "$ref": "#/components/schemas/Body_update_item_items__item_id__put" - } - ], - } - ), - ), - } - }, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/items/": { - "get": { - "summary": "Get Items With Query", - "operationId": "get_items_with_query_items__get", - "parameters": [ - { - "name": "q", - "in": "query", - "required": False, - "schema": { - "title": "Q", - "maxLength": 50, - "minLength": 3, - "pattern": "^[a-zA-Z0-9 ]+$", - "type": "string", - }, - }, - { - "name": "skip", - "in": "query", - "required": False, - "schema": { - "title": "Skip", - "default": 0, - "minimum": 0, - "type": "integer", - }, - }, - { - "name": "limit", - "in": "query", - "required": False, - "schema": { - "title": "Limit", - "default": 10, - "minimum": 1, - "maximum": 100, - "examples": [5], - "type": "integer", - }, - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "post": { - "summary": "Create Item", - "operationId": "create_item_items__post", - "requestBody": { - "required": True, - "content": { - "application/json": { - "schema": { - "title": "Item", - "examples": [ - { - "name": "Foo", - "price": 35.4, - "description": "The Foo item", - } - ], - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - } - } - }, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/users/": { - "get": { - "summary": "Get User With Header", - "operationId": "get_user_with_header_users__get", - "parameters": [ - { - "name": "x-custom", - "in": "header", - "required": False, - "schema": {"title": "X-Custom", "type": "string"}, - }, - { - "name": "x-token", - "in": "header", - "required": False, - "schema": {"title": "X-Token", "type": "string"}, - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/cookies/": { - "get": { - "summary": "Get Cookies", - "operationId": "get_cookies_cookies__get", - "parameters": [ - { - "name": "session_id", - "in": "cookie", - "required": False, - "schema": {"title": "Session Id", "type": "string"}, - }, - { - "name": "tracking_id", - "in": "cookie", - "required": False, - "schema": { - "title": "Tracking Id", - "minLength": 10, - "type": "string", - }, - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/items-embed/": { - "post": { - "summary": "Create Item Embed", - "operationId": "create_item_embed_items_embed__post", - "requestBody": { - "content": { - "application/json": { - "schema": pydantic_snapshot( - v1=snapshot( - { - "$ref": "#/components/schemas/Body_create_item_embed_items_embed__post" - } - ), - v2=snapshot( - { - "allOf": [ - { - "$ref": "#/components/schemas/Body_create_item_embed_items_embed__post" - } - ], - "title": "Body", - } - ), - ), - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/form-data/": { - "post": { - "summary": "Submit Form", - "operationId": "submit_form_form_data__post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": pydantic_snapshot( - v1=snapshot( - { - "$ref": "#/components/schemas/Body_submit_form_form_data__post" - } - ), - v2=snapshot( - { - "allOf": [ - { - "$ref": "#/components/schemas/Body_submit_form_form_data__post" - } - ], - "title": "Body", - } - ), - ), - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/upload/": { - "post": { - "summary": "Upload File", - "operationId": "upload_file_upload__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": pydantic_snapshot( - v1=snapshot( - { - "$ref": "#/components/schemas/Body_upload_file_upload__post" - } - ), - v2=snapshot( - { - "allOf": [ - { - "$ref": "#/components/schemas/Body_upload_file_upload__post" - } - ], - "title": "Body", - } - ), - ), - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/upload-multiple/": { - "post": { - "summary": "Upload Multiple Files", - "operationId": "upload_multiple_files_upload_multiple__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": pydantic_snapshot( - v1=snapshot( - { - "$ref": "#/components/schemas/Body_upload_multiple_files_upload_multiple__post" - } - ), - v2=snapshot( - { - "allOf": [ - { - "$ref": "#/components/schemas/Body_upload_multiple_files_upload_multiple__post" - } - ], - "title": "Body", - } - ), - ), - } - }, - "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": { - "Body_create_item_embed_items_embed__post": { - "properties": pydantic_snapshot( - v1=snapshot( - {"item": {"$ref": "#/components/schemas/Item"}} - ), - v2=snapshot( - { - "item": { - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - "title": "Item", - } - } - ), - ), - "type": "object", - "required": ["item"], - "title": "Body_create_item_embed_items_embed__post", - }, - "Body_submit_form_form_data__post": { - "properties": { - "username": { - "type": "string", - "maxLength": 50, - "minLength": 3, - "title": "Username", - }, - "password": { - "type": "string", - "minLength": 8, - "title": "Password", - }, - "email": {"type": "string", "title": "Email"}, - }, - "type": "object", - "required": ["username", "password"], - "title": "Body_submit_form_form_data__post", - }, - "Body_update_item_items__item_id__put": { - "properties": { - "item": pydantic_snapshot( - v1=snapshot({"$ref": "#/components/schemas/Item"}), - v2=snapshot( - { - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - "title": "Item", - } - ), - ), - "importance": { - "type": "integer", - "maximum": 10.0, - "exclusiveMinimum": 0.0, - "title": "Importance", - }, - }, - "type": "object", - "required": ["item", "importance"], - "title": "Body_update_item_items__item_id__put", - }, - "Body_upload_file_upload__post": { - "properties": { - "file": { - "type": "string", - "format": "binary", - "title": "File", - }, - "description": {"type": "string", "title": "Description"}, - }, - "type": "object", - "required": ["file"], - "title": "Body_upload_file_upload__post", - }, - "Body_upload_multiple_files_upload_multiple__post": { - "properties": { - "files": { - "items": {"type": "string", "format": "binary"}, - "type": "array", - "title": "Files", - }, - "note": {"type": "string", "title": "Note", "default": ""}, - }, - "type": "object", - "required": ["files"], - "title": "Body_upload_multiple_files_upload_multiple__post", - }, - "HTTPValidationError": { - "properties": { - "detail": { - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - "type": "array", - "title": "Detail", - } - }, - "type": "object", - "title": "HTTPValidationError", - }, - "Item": { - "properties": { - "name": {"type": "string", "title": "Name"}, - "price": {"type": "number", "title": "Price"}, - "description": {"type": "string", "title": "Description"}, - }, - "type": "object", - "required": ["name", "price"], - "title": "Item", - }, - "ValidationError": { - "properties": { - "loc": { - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - "type": "array", - "title": "Location", - }, - "msg": {"type": "string", "title": "Message"}, - "type": {"type": "string", "title": "Error Type"}, - }, - "type": "object", - "required": ["loc", "msg", "type"], - "title": "ValidationError", - }, - } - }, - } - ) diff --git a/tests/test_computed_fields.py b/tests/test_computed_fields.py index a1b4121688..e7f969f7cf 100644 --- a/tests/test_computed_fields.py +++ b/tests/test_computed_fields.py @@ -2,12 +2,11 @@ import pytest from fastapi import FastAPI from fastapi.testclient import TestClient -from .utils import needs_pydanticv2 - @pytest.fixture(name="client") -def get_client(): - app = FastAPI() +def get_client(request): + separate_input_output_schemas = request.param + app = FastAPI(separate_input_output_schemas=separate_input_output_schemas) from pydantic import BaseModel, computed_field @@ -32,15 +31,15 @@ def get_client(): return client +@pytest.mark.parametrize("client", [True, False], indirect=True) @pytest.mark.parametrize("path", ["/", "/responses"]) -@needs_pydanticv2 def test_get(client: TestClient, path: str): response = client.get(path) assert response.status_code == 200, response.text assert response.json() == {"width": 3, "length": 4, "area": 12} -@needs_pydanticv2 +@pytest.mark.parametrize("client", [True, False], indirect=True) def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text diff --git a/tests/test_custom_schema_fields.py b/tests/test_custom_schema_fields.py index d890291b19..60b795e9ba 100644 --- a/tests/test_custom_schema_fields.py +++ b/tests/test_custom_schema_fields.py @@ -1,13 +1,8 @@ -from typing import Optional +from typing import Annotated, Optional from fastapi import FastAPI -from fastapi._compat import PYDANTIC_V2 from fastapi.testclient import TestClient -from pydantic import BaseModel -from typing_extensions import Annotated - -if PYDANTIC_V2: - from pydantic import WithJsonSchema +from pydantic import BaseModel, WithJsonSchema app = FastAPI() @@ -15,23 +10,15 @@ app = FastAPI() class Item(BaseModel): name: str - if PYDANTIC_V2: - description: Annotated[ - Optional[str], WithJsonSchema({"type": ["string", "null"]}) - ] = None + description: Annotated[ + Optional[str], WithJsonSchema({"type": ["string", "null"]}) + ] = None - model_config = { - "json_schema_extra": { - "x-something-internal": {"level": 4}, - } + model_config = { + "json_schema_extra": { + "x-something-internal": {"level": 4}, } - else: - description: Optional[str] = None # type: ignore[no-redef] - - class Config: - schema_extra = { - "x-something-internal": {"level": 4}, - } + } @app.get("/foo", response_model=Item) @@ -56,7 +43,7 @@ item_schema = { }, "description": { "title": "Description", - "type": ["string", "null"] if PYDANTIC_V2 else "string", + "type": ["string", "null"], }, }, } diff --git a/tests/test_datastructures.py b/tests/test_datastructures.py index 7e57d525ce..29a70cae0c 100644 --- a/tests/test_datastructures.py +++ b/tests/test_datastructures.py @@ -1,6 +1,5 @@ import io from pathlib import Path -from typing import List import pytest from fastapi import FastAPI, UploadFile @@ -8,12 +7,6 @@ from fastapi.datastructures import Default from fastapi.testclient import TestClient -# TODO: remove when deprecating Pydantic v1 -def test_upload_file_invalid(): - with pytest.raises(ValueError): - UploadFile.validate("not a Starlette UploadFile") - - def test_upload_file_invalid_pydantic_v2(): with pytest.raises(ValueError): UploadFile._validate("not a Starlette UploadFile", {}) @@ -38,7 +31,7 @@ def test_upload_file_is_closed(tmp_path: Path): path.write_bytes(b"<file content>") app = FastAPI() - testing_file_store: List[UploadFile] = [] + testing_file_store: list[UploadFile] = [] @app.post("/uploadfile/") def create_upload_file(file: UploadFile): diff --git a/tests/test_datetime_custom_encoder.py b/tests/test_datetime_custom_encoder.py index 3aa77c0b1d..f154ede029 100644 --- a/tests/test_datetime_custom_encoder.py +++ b/tests/test_datetime_custom_encoder.py @@ -4,10 +4,7 @@ from fastapi import FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel -from .utils import needs_pydanticv1, needs_pydanticv2 - -@needs_pydanticv2 def test_pydanticv2(): from pydantic import field_serializer @@ -29,29 +26,3 @@ def test_pydanticv2(): with client: response = client.get("/model") assert response.json() == {"dt_field": "2019-01-01T08:00:00+00:00"} - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -def test_pydanticv1(): - class ModelWithDatetimeField(BaseModel): - dt_field: datetime - - class Config: - json_encoders = { - datetime: lambda dt: dt.replace( - microsecond=0, tzinfo=timezone.utc - ).isoformat() - } - - app = FastAPI() - model = ModelWithDatetimeField(dt_field=datetime(2019, 1, 1, 8)) - - @app.get("/model", response_model=ModelWithDatetimeField) - def get_model(): - return model - - client = TestClient(app) - with client: - response = client.get("/model") - assert response.json() == {"dt_field": "2019-01-01T08:00:00+00:00"} diff --git a/tests/test_dependency_after_yield_raise.py b/tests/test_dependency_after_yield_raise.py index b560dc36f9..b561402772 100644 --- a/tests/test_dependency_after_yield_raise.py +++ b/tests/test_dependency_after_yield_raise.py @@ -1,9 +1,8 @@ -from typing import Any +from typing import Annotated, Any import pytest from fastapi import Depends, FastAPI, HTTPException from fastapi.testclient import TestClient -from typing_extensions import Annotated class CustomError(Exception): diff --git a/tests/test_dependency_after_yield_streaming.py b/tests/test_dependency_after_yield_streaming.py index 7e1c8822b8..cbadff8f87 100644 --- a/tests/test_dependency_after_yield_streaming.py +++ b/tests/test_dependency_after_yield_streaming.py @@ -1,11 +1,11 @@ +from collections.abc import Generator from contextlib import contextmanager -from typing import Any, Generator +from typing import Annotated, Any import pytest from fastapi import Depends, FastAPI from fastapi.responses import StreamingResponse from fastapi.testclient import TestClient -from typing_extensions import Annotated class Session: diff --git a/tests/test_dependency_after_yield_websockets.py b/tests/test_dependency_after_yield_websockets.py index 7c323c338b..0fdf697b6c 100644 --- a/tests/test_dependency_after_yield_websockets.py +++ b/tests/test_dependency_after_yield_websockets.py @@ -1,10 +1,10 @@ +from collections.abc import Generator from contextlib import contextmanager -from typing import Any, Generator +from typing import Annotated, Any import pytest from fastapi import Depends, FastAPI, WebSocket from fastapi.testclient import TestClient -from typing_extensions import Annotated class Session: diff --git a/tests/test_dependency_class.py b/tests/test_dependency_class.py index 0233492e6c..95ff3e9d95 100644 --- a/tests/test_dependency_class.py +++ b/tests/test_dependency_class.py @@ -1,4 +1,4 @@ -from typing import AsyncGenerator, Generator +from collections.abc import AsyncGenerator, Generator import pytest from fastapi import Depends, FastAPI @@ -48,6 +48,34 @@ async_callable_gen_dependency = AsyncCallableGenDependency() methods_dependency = MethodsDependency() +@app.get("/callable-dependency-class") +async def get_callable_dependency_class( + value: str, instance: CallableDependency = Depends() +): + return instance(value) + + +@app.get("/callable-gen-dependency-class") +async def get_callable_gen_dependency_class( + value: str, instance: CallableGenDependency = Depends() +): + return next(instance(value)) + + +@app.get("/async-callable-dependency-class") +async def get_async_callable_dependency_class( + value: str, instance: AsyncCallableDependency = Depends() +): + return await instance(value) + + +@app.get("/async-callable-gen-dependency-class") +async def get_async_callable_gen_dependency_class( + value: str, instance: AsyncCallableGenDependency = Depends() +): + return await instance(value).__anext__() + + @app.get("/callable-dependency") async def get_callable_dependency(value: str = Depends(callable_dependency)): return value @@ -114,6 +142,10 @@ client = TestClient(app) ("/synchronous-method-gen-dependency", "synchronous-method-gen-dependency"), ("/asynchronous-method-dependency", "asynchronous-method-dependency"), ("/asynchronous-method-gen-dependency", "asynchronous-method-gen-dependency"), + ("/callable-dependency-class", "callable-dependency-class"), + ("/callable-gen-dependency-class", "callable-gen-dependency-class"), + ("/async-callable-dependency-class", "async-callable-dependency-class"), + ("/async-callable-gen-dependency-class", "async-callable-gen-dependency-class"), ], ) def test_class_dependency(route, value): diff --git a/tests/test_dependency_contextmanager.py b/tests/test_dependency_contextmanager.py index 02c10458cb..5a89934741 100644 --- a/tests/test_dependency_contextmanager.py +++ b/tests/test_dependency_contextmanager.py @@ -1,5 +1,4 @@ import json -from typing import Dict import pytest from fastapi import BackgroundTasks, Depends, FastAPI @@ -37,19 +36,19 @@ class OtherDependencyError(Exception): pass -async def asyncgen_state(state: Dict[str, str] = Depends(get_state)): +async def asyncgen_state(state: dict[str, str] = Depends(get_state)): state["/async"] = "asyncgen started" yield state["/async"] state["/async"] = "asyncgen completed" -def generator_state(state: Dict[str, str] = Depends(get_state)): +def generator_state(state: dict[str, str] = Depends(get_state)): state["/sync"] = "generator started" yield state["/sync"] state["/sync"] = "generator completed" -async def asyncgen_state_try(state: Dict[str, str] = Depends(get_state)): +async def asyncgen_state_try(state: dict[str, str] = Depends(get_state)): state["/async_raise"] = "asyncgen raise started" try: yield state["/async_raise"] @@ -60,7 +59,7 @@ async def asyncgen_state_try(state: Dict[str, str] = Depends(get_state)): state["/async_raise"] = "asyncgen raise finalized" -def generator_state_try(state: Dict[str, str] = Depends(get_state)): +def generator_state_try(state: dict[str, str] = Depends(get_state)): state["/sync_raise"] = "generator raise started" try: yield state["/sync_raise"] diff --git a/tests/test_dependency_contextvars.py b/tests/test_dependency_contextvars.py index 076802df84..0c2e5594b6 100644 --- a/tests/test_dependency_contextvars.py +++ b/tests/test_dependency_contextvars.py @@ -1,10 +1,11 @@ +from collections.abc import Awaitable from contextvars import ContextVar -from typing import Any, Awaitable, Callable, Dict, Optional +from typing import Any, Callable, Optional from fastapi import Depends, FastAPI, Request, Response from fastapi.testclient import TestClient -legacy_request_state_context_var: ContextVar[Optional[Dict[str, Any]]] = ContextVar( +legacy_request_state_context_var: ContextVar[Optional[dict[str, Any]]] = ContextVar( "legacy_request_state_context_var", default=None ) diff --git a/tests/test_dependency_duplicates.py b/tests/test_dependency_duplicates.py index 8e8d07c2d6..a8658e03bb 100644 --- a/tests/test_dependency_duplicates.py +++ b/tests/test_dependency_duplicates.py @@ -1,6 +1,3 @@ -from typing import List - -from dirty_equals import IsDict from fastapi import Depends, FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel @@ -40,7 +37,7 @@ async def no_duplicates(item: Item, item2: Item = Depends(dependency)): @app.post("/with-duplicates-sub") async def no_duplicates_sub( - item: Item, sub_items: List[Item] = Depends(sub_duplicate_dependency) + item: Item, sub_items: list[Item] = Depends(sub_duplicate_dependency) ): return [item, sub_items] @@ -48,29 +45,16 @@ async def no_duplicates_sub( def test_no_duplicates_invalid(): response = client.post("/no-duplicates", json={"item": {"data": "myitem"}}) assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "item2"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "item2"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "item2"], + "msg": "Field required", + "input": None, + } + ] + } def test_no_duplicates(): diff --git a/tests/test_dependency_overrides.py b/tests/test_dependency_overrides.py index 154937fa0b..e25db624d8 100644 --- a/tests/test_dependency_overrides.py +++ b/tests/test_dependency_overrides.py @@ -1,7 +1,6 @@ from typing import Optional import pytest -from dirty_equals import IsDict from fastapi import APIRouter, Depends, FastAPI from fastapi.testclient import TestClient @@ -54,29 +53,16 @@ async def overrider_dependency_with_sub(msg: dict = Depends(overrider_sub_depend def test_main_depends(): response = client.get("/main-depends/") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "q"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "q"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "q"], + "msg": "Field required", + "input": None, + } + ] + } def test_main_depends_q_foo(): @@ -100,29 +86,16 @@ def test_main_depends_q_foo_skip_100_limit_200(): def test_decorator_depends(): response = client.get("/decorator-depends/") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "q"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "q"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "q"], + "msg": "Field required", + "input": None, + } + ] + } def test_decorator_depends_q_foo(): @@ -140,29 +113,16 @@ def test_decorator_depends_q_foo_skip_100_limit_200(): def test_router_depends(): response = client.get("/router-depends/") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "q"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "q"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "q"], + "msg": "Field required", + "input": None, + } + ] + } def test_router_depends_q_foo(): @@ -186,29 +146,16 @@ def test_router_depends_q_foo_skip_100_limit_200(): def test_router_decorator_depends(): response = client.get("/router-decorator-depends/") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "q"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "q"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "q"], + "msg": "Field required", + "input": None, + } + ] + } def test_router_decorator_depends_q_foo(): @@ -272,29 +219,17 @@ def test_override_with_sub_main_depends(): app.dependency_overrides[common_parameters] = overrider_dependency_with_sub response = client.get("/main-depends/") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "k"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "k"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "k"], + "msg": "Field required", + "input": None, + } + ] + } + app.dependency_overrides = {} @@ -302,29 +237,17 @@ def test_override_with_sub__main_depends_q_foo(): app.dependency_overrides[common_parameters] = overrider_dependency_with_sub response = client.get("/main-depends/?q=foo") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "k"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "k"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "k"], + "msg": "Field required", + "input": None, + } + ] + } + app.dependency_overrides = {} @@ -340,29 +263,17 @@ def test_override_with_sub_decorator_depends(): app.dependency_overrides[common_parameters] = overrider_dependency_with_sub response = client.get("/decorator-depends/") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "k"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "k"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "k"], + "msg": "Field required", + "input": None, + } + ] + } + app.dependency_overrides = {} @@ -370,29 +281,17 @@ def test_override_with_sub_decorator_depends_q_foo(): app.dependency_overrides[common_parameters] = overrider_dependency_with_sub response = client.get("/decorator-depends/?q=foo") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "k"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "k"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "k"], + "msg": "Field required", + "input": None, + } + ] + } + app.dependency_overrides = {} @@ -408,29 +307,17 @@ def test_override_with_sub_router_depends(): app.dependency_overrides[common_parameters] = overrider_dependency_with_sub response = client.get("/router-depends/") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "k"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "k"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "k"], + "msg": "Field required", + "input": None, + } + ] + } + app.dependency_overrides = {} @@ -438,29 +325,17 @@ def test_override_with_sub_router_depends_q_foo(): app.dependency_overrides[common_parameters] = overrider_dependency_with_sub response = client.get("/router-depends/?q=foo") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "k"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "k"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "k"], + "msg": "Field required", + "input": None, + } + ] + } + app.dependency_overrides = {} @@ -476,29 +351,17 @@ def test_override_with_sub_router_decorator_depends(): app.dependency_overrides[common_parameters] = overrider_dependency_with_sub response = client.get("/router-decorator-depends/") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "k"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "k"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "k"], + "msg": "Field required", + "input": None, + } + ] + } + app.dependency_overrides = {} @@ -506,29 +369,17 @@ def test_override_with_sub_router_decorator_depends_q_foo(): app.dependency_overrides[common_parameters] = overrider_dependency_with_sub response = client.get("/router-decorator-depends/?q=foo") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "k"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "k"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "k"], + "msg": "Field required", + "input": None, + } + ] + } + app.dependency_overrides = {} diff --git a/tests/test_dependency_paramless.py b/tests/test_dependency_paramless.py index 9c3cc3878b..1774196fe4 100644 --- a/tests/test_dependency_paramless.py +++ b/tests/test_dependency_paramless.py @@ -1,4 +1,4 @@ -from typing import Union +from typing import Annotated, Union from fastapi import FastAPI, HTTPException, Security from fastapi.security import ( @@ -6,7 +6,6 @@ from fastapi.security import ( SecurityScopes, ) from fastapi.testclient import TestClient -from typing_extensions import Annotated app = FastAPI() diff --git a/tests/test_dependency_partial.py b/tests/test_dependency_partial.py new file mode 100644 index 0000000000..05a3cffa5a --- /dev/null +++ b/tests/test_dependency_partial.py @@ -0,0 +1,251 @@ +from collections.abc import AsyncGenerator, Generator +from functools import partial +from typing import Annotated + +import pytest +from fastapi import Depends, FastAPI +from fastapi.testclient import TestClient + +app = FastAPI() + + +def function_dependency(value: str) -> str: + return value + + +async def async_function_dependency(value: str) -> str: + return value + + +def gen_dependency(value: str) -> Generator[str, None, None]: + yield value + + +async def async_gen_dependency(value: str) -> AsyncGenerator[str, None]: + yield value + + +class CallableDependency: + def __call__(self, value: str) -> str: + return value + + +class CallableGenDependency: + def __call__(self, value: str) -> Generator[str, None, None]: + yield value + + +class AsyncCallableDependency: + async def __call__(self, value: str) -> str: + return value + + +class AsyncCallableGenDependency: + async def __call__(self, value: str) -> AsyncGenerator[str, None]: + yield value + + +class MethodsDependency: + def synchronous(self, value: str) -> str: + return value + + async def asynchronous(self, value: str) -> str: + return value + + def synchronous_gen(self, value: str) -> Generator[str, None, None]: + yield value + + async def asynchronous_gen(self, value: str) -> AsyncGenerator[str, None]: + yield value + + +callable_dependency = CallableDependency() +callable_gen_dependency = CallableGenDependency() +async_callable_dependency = AsyncCallableDependency() +async_callable_gen_dependency = AsyncCallableGenDependency() +methods_dependency = MethodsDependency() + + +@app.get("/partial-function-dependency") +async def get_partial_function_dependency( + value: Annotated[ + str, Depends(partial(function_dependency, "partial-function-dependency")) + ], +) -> str: + return value + + +@app.get("/partial-async-function-dependency") +async def get_partial_async_function_dependency( + value: Annotated[ + str, + Depends( + partial(async_function_dependency, "partial-async-function-dependency") + ), + ], +) -> str: + return value + + +@app.get("/partial-gen-dependency") +async def get_partial_gen_dependency( + value: Annotated[str, Depends(partial(gen_dependency, "partial-gen-dependency"))], +) -> str: + return value + + +@app.get("/partial-async-gen-dependency") +async def get_partial_async_gen_dependency( + value: Annotated[ + str, Depends(partial(async_gen_dependency, "partial-async-gen-dependency")) + ], +) -> str: + return value + + +@app.get("/partial-callable-dependency") +async def get_partial_callable_dependency( + value: Annotated[ + str, Depends(partial(callable_dependency, "partial-callable-dependency")) + ], +) -> str: + return value + + +@app.get("/partial-callable-gen-dependency") +async def get_partial_callable_gen_dependency( + value: Annotated[ + str, + Depends(partial(callable_gen_dependency, "partial-callable-gen-dependency")), + ], +) -> str: + return value + + +@app.get("/partial-async-callable-dependency") +async def get_partial_async_callable_dependency( + value: Annotated[ + str, + Depends( + partial(async_callable_dependency, "partial-async-callable-dependency") + ), + ], +) -> str: + return value + + +@app.get("/partial-async-callable-gen-dependency") +async def get_partial_async_callable_gen_dependency( + value: Annotated[ + str, + Depends( + partial( + async_callable_gen_dependency, "partial-async-callable-gen-dependency" + ) + ), + ], +) -> str: + return value + + +@app.get("/partial-synchronous-method-dependency") +async def get_partial_synchronous_method_dependency( + value: Annotated[ + str, + Depends( + partial( + methods_dependency.synchronous, "partial-synchronous-method-dependency" + ) + ), + ], +) -> str: + return value + + +@app.get("/partial-synchronous-method-gen-dependency") +async def get_partial_synchronous_method_gen_dependency( + value: Annotated[ + str, + Depends( + partial( + methods_dependency.synchronous_gen, + "partial-synchronous-method-gen-dependency", + ) + ), + ], +) -> str: + return value + + +@app.get("/partial-asynchronous-method-dependency") +async def get_partial_asynchronous_method_dependency( + value: Annotated[ + str, + Depends( + partial( + methods_dependency.asynchronous, + "partial-asynchronous-method-dependency", + ) + ), + ], +) -> str: + return value + + +@app.get("/partial-asynchronous-method-gen-dependency") +async def get_partial_asynchronous_method_gen_dependency( + value: Annotated[ + str, + Depends( + partial( + methods_dependency.asynchronous_gen, + "partial-asynchronous-method-gen-dependency", + ) + ), + ], +) -> str: + return value + + +client = TestClient(app) + + +@pytest.mark.parametrize( + "route,value", + [ + ("/partial-function-dependency", "partial-function-dependency"), + ( + "/partial-async-function-dependency", + "partial-async-function-dependency", + ), + ("/partial-gen-dependency", "partial-gen-dependency"), + ("/partial-async-gen-dependency", "partial-async-gen-dependency"), + ("/partial-callable-dependency", "partial-callable-dependency"), + ("/partial-callable-gen-dependency", "partial-callable-gen-dependency"), + ("/partial-async-callable-dependency", "partial-async-callable-dependency"), + ( + "/partial-async-callable-gen-dependency", + "partial-async-callable-gen-dependency", + ), + ( + "/partial-synchronous-method-dependency", + "partial-synchronous-method-dependency", + ), + ( + "/partial-synchronous-method-gen-dependency", + "partial-synchronous-method-gen-dependency", + ), + ( + "/partial-asynchronous-method-dependency", + "partial-asynchronous-method-dependency", + ), + ( + "/partial-asynchronous-method-gen-dependency", + "partial-asynchronous-method-gen-dependency", + ), + ], +) +def test_dependency_types_with_partial(route: str, value: str) -> None: + response = client.get(route) + assert response.status_code == 200, response.text + assert response.json() == value diff --git a/tests/test_dependency_security_overrides.py b/tests/test_dependency_security_overrides.py index b89d82db43..14b65c7770 100644 --- a/tests/test_dependency_security_overrides.py +++ b/tests/test_dependency_security_overrides.py @@ -1,5 +1,3 @@ -from typing import List, Tuple - from fastapi import Depends, FastAPI, Security from fastapi.security import SecurityScopes from fastapi.testclient import TestClient @@ -25,8 +23,8 @@ def get_data_override(): @app.get("/user") def read_user( - user_data: Tuple[str, List[str]] = Security(get_user, scopes=["foo", "bar"]), - data: List[int] = Depends(get_data), + user_data: tuple[str, list[str]] = Security(get_user, scopes=["foo", "bar"]), + data: list[int] = Depends(get_data), ): return {"user": user_data[0], "scopes": user_data[1], "data": data} diff --git a/tests/test_dependency_wrapped.py b/tests/test_dependency_wrapped.py new file mode 100644 index 0000000000..a4044112a2 --- /dev/null +++ b/tests/test_dependency_wrapped.py @@ -0,0 +1,449 @@ +import inspect +import sys +from collections.abc import AsyncGenerator, Generator +from functools import wraps + +import pytest +from fastapi import Depends, FastAPI +from fastapi.concurrency import iterate_in_threadpool, run_in_threadpool +from fastapi.testclient import TestClient + +if sys.version_info >= (3, 13): # pragma: no cover + from inspect import iscoroutinefunction +else: # pragma: no cover + from asyncio import iscoroutinefunction + + +def noop_wrap(func): + @wraps(func) + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + + return wrapper + + +def noop_wrap_async(func): + if inspect.isgeneratorfunction(func): + + @wraps(func) + async def gen_wrapper(*args, **kwargs): + async for item in iterate_in_threadpool(func(*args, **kwargs)): + yield item + + return gen_wrapper + + elif inspect.isasyncgenfunction(func): + + @wraps(func) + async def async_gen_wrapper(*args, **kwargs): + async for item in func(*args, **kwargs): + yield item + + return async_gen_wrapper + + @wraps(func) + async def wrapper(*args, **kwargs): + if inspect.isroutine(func) and iscoroutinefunction(func): + return await func(*args, **kwargs) + if inspect.isclass(func): + return await run_in_threadpool(func, *args, **kwargs) + dunder_call = getattr(func, "__call__", None) # noqa: B004 + if iscoroutinefunction(dunder_call): + return await dunder_call(*args, **kwargs) + return await run_in_threadpool(func, *args, **kwargs) + + return wrapper + + +class ClassInstanceDep: + def __call__(self): + return True + + +class_instance_dep = ClassInstanceDep() +wrapped_class_instance_dep = noop_wrap(class_instance_dep) +wrapped_class_instance_dep_async_wrapper = noop_wrap_async(class_instance_dep) + + +class ClassInstanceGenDep: + def __call__(self): + yield True + + +class_instance_gen_dep = ClassInstanceGenDep() +wrapped_class_instance_gen_dep = noop_wrap(class_instance_gen_dep) + + +class ClassInstanceWrappedDep: + @noop_wrap + def __call__(self): + return True + + +class_instance_wrapped_dep = ClassInstanceWrappedDep() + + +class ClassInstanceWrappedAsyncDep: + @noop_wrap_async + def __call__(self): + return True + + +class_instance_wrapped_async_dep = ClassInstanceWrappedAsyncDep() + + +class ClassInstanceWrappedGenDep: + @noop_wrap + def __call__(self): + yield True + + +class_instance_wrapped_gen_dep = ClassInstanceWrappedGenDep() + + +class ClassInstanceWrappedAsyncGenDep: + @noop_wrap_async + def __call__(self): + yield True + + +class_instance_wrapped_async_gen_dep = ClassInstanceWrappedAsyncGenDep() + + +class ClassDep: + def __init__(self): + self.value = True + + +wrapped_class_dep = noop_wrap(ClassDep) +wrapped_class_dep_async_wrapper = noop_wrap_async(ClassDep) + + +class ClassInstanceAsyncDep: + async def __call__(self): + return True + + +class_instance_async_dep = ClassInstanceAsyncDep() +wrapped_class_instance_async_dep = noop_wrap(class_instance_async_dep) +wrapped_class_instance_async_dep_async_wrapper = noop_wrap_async( + class_instance_async_dep +) + + +class ClassInstanceAsyncGenDep: + async def __call__(self): + yield True + + +class_instance_async_gen_dep = ClassInstanceAsyncGenDep() +wrapped_class_instance_async_gen_dep = noop_wrap(class_instance_async_gen_dep) + + +class ClassInstanceAsyncWrappedDep: + @noop_wrap + async def __call__(self): + return True + + +class_instance_async_wrapped_dep = ClassInstanceAsyncWrappedDep() + + +class ClassInstanceAsyncWrappedAsyncDep: + @noop_wrap_async + async def __call__(self): + return True + + +class_instance_async_wrapped_async_dep = ClassInstanceAsyncWrappedAsyncDep() + + +class ClassInstanceAsyncWrappedGenDep: + @noop_wrap + async def __call__(self): + yield True + + +class_instance_async_wrapped_gen_dep = ClassInstanceAsyncWrappedGenDep() + + +class ClassInstanceAsyncWrappedGenAsyncDep: + @noop_wrap_async + async def __call__(self): + yield True + + +class_instance_async_wrapped_gen_async_dep = ClassInstanceAsyncWrappedGenAsyncDep() + +app = FastAPI() + +# Sync wrapper + + +@noop_wrap +def wrapped_dependency() -> bool: + return True + + +@noop_wrap +def wrapped_gen_dependency() -> Generator[bool, None, None]: + yield True + + +@noop_wrap +async def async_wrapped_dependency() -> bool: + return True + + +@noop_wrap +async def async_wrapped_gen_dependency() -> AsyncGenerator[bool, None]: + yield True + + +@app.get("/wrapped-dependency/") +async def get_wrapped_dependency(value: bool = Depends(wrapped_dependency)): + return value + + +@app.get("/wrapped-gen-dependency/") +async def get_wrapped_gen_dependency(value: bool = Depends(wrapped_gen_dependency)): + return value + + +@app.get("/async-wrapped-dependency/") +async def get_async_wrapped_dependency(value: bool = Depends(async_wrapped_dependency)): + return value + + +@app.get("/async-wrapped-gen-dependency/") +async def get_async_wrapped_gen_dependency( + value: bool = Depends(async_wrapped_gen_dependency), +): + return value + + +@app.get("/wrapped-class-instance-dependency/") +async def get_wrapped_class_instance_dependency( + value: bool = Depends(wrapped_class_instance_dep), +): + return value + + +@app.get("/wrapped-class-instance-async-dependency/") +async def get_wrapped_class_instance_async_dependency( + value: bool = Depends(wrapped_class_instance_async_dep), +): + return value + + +@app.get("/wrapped-class-instance-gen-dependency/") +async def get_wrapped_class_instance_gen_dependency( + value: bool = Depends(wrapped_class_instance_gen_dep), +): + return value + + +@app.get("/wrapped-class-instance-async-gen-dependency/") +async def get_wrapped_class_instance_async_gen_dependency( + value: bool = Depends(wrapped_class_instance_async_gen_dep), +): + return value + + +@app.get("/class-instance-wrapped-dependency/") +async def get_class_instance_wrapped_dependency( + value: bool = Depends(class_instance_wrapped_dep), +): + return value + + +@app.get("/class-instance-wrapped-async-dependency/") +async def get_class_instance_wrapped_async_dependency( + value: bool = Depends(class_instance_wrapped_async_dep), +): + return value + + +@app.get("/class-instance-async-wrapped-dependency/") +async def get_class_instance_async_wrapped_dependency( + value: bool = Depends(class_instance_async_wrapped_dep), +): + return value + + +@app.get("/class-instance-async-wrapped-async-dependency/") +async def get_class_instance_async_wrapped_async_dependency( + value: bool = Depends(class_instance_async_wrapped_async_dep), +): + return value + + +@app.get("/class-instance-wrapped-gen-dependency/") +async def get_class_instance_wrapped_gen_dependency( + value: bool = Depends(class_instance_wrapped_gen_dep), +): + return value + + +@app.get("/class-instance-wrapped-async-gen-dependency/") +async def get_class_instance_wrapped_async_gen_dependency( + value: bool = Depends(class_instance_wrapped_async_gen_dep), +): + return value + + +@app.get("/class-instance-async-wrapped-gen-dependency/") +async def get_class_instance_async_wrapped_gen_dependency( + value: bool = Depends(class_instance_async_wrapped_gen_dep), +): + return value + + +@app.get("/class-instance-async-wrapped-gen-async-dependency/") +async def get_class_instance_async_wrapped_gen_async_dependency( + value: bool = Depends(class_instance_async_wrapped_gen_async_dep), +): + return value + + +@app.get("/wrapped-class-dependency/") +async def get_wrapped_class_dependency(value: ClassDep = Depends(wrapped_class_dep)): + return value.value + + +@app.get("/wrapped-endpoint/") +@noop_wrap +def get_wrapped_endpoint(): + return True + + +@app.get("/async-wrapped-endpoint/") +@noop_wrap +async def get_async_wrapped_endpoint(): + return True + + +# Async wrapper + + +@noop_wrap_async +def wrapped_dependency_async_wrapper() -> bool: + return True + + +@noop_wrap_async +def wrapped_gen_dependency_async_wrapper() -> Generator[bool, None, None]: + yield True + + +@noop_wrap_async +async def async_wrapped_dependency_async_wrapper() -> bool: + return True + + +@noop_wrap_async +async def async_wrapped_gen_dependency_async_wrapper() -> AsyncGenerator[bool, None]: + yield True + + +@app.get("/wrapped-dependency-async-wrapper/") +async def get_wrapped_dependency_async_wrapper( + value: bool = Depends(wrapped_dependency_async_wrapper), +): + return value + + +@app.get("/wrapped-gen-dependency-async-wrapper/") +async def get_wrapped_gen_dependency_async_wrapper( + value: bool = Depends(wrapped_gen_dependency_async_wrapper), +): + return value + + +@app.get("/async-wrapped-dependency-async-wrapper/") +async def get_async_wrapped_dependency_async_wrapper( + value: bool = Depends(async_wrapped_dependency_async_wrapper), +): + return value + + +@app.get("/async-wrapped-gen-dependency-async-wrapper/") +async def get_async_wrapped_gen_dependency_async_wrapper( + value: bool = Depends(async_wrapped_gen_dependency_async_wrapper), +): + return value + + +@app.get("/wrapped-class-instance-dependency-async-wrapper/") +async def get_wrapped_class_instance_dependency_async_wrapper( + value: bool = Depends(wrapped_class_instance_dep_async_wrapper), +): + return value + + +@app.get("/wrapped-class-instance-async-dependency-async-wrapper/") +async def get_wrapped_class_instance_async_dependency_async_wrapper( + value: bool = Depends(wrapped_class_instance_async_dep_async_wrapper), +): + return value + + +@app.get("/wrapped-class-dependency-async-wrapper/") +async def get_wrapped_class_dependency_async_wrapper( + value: ClassDep = Depends(wrapped_class_dep_async_wrapper), +): + return value.value + + +@app.get("/wrapped-endpoint-async-wrapper/") +@noop_wrap_async +def get_wrapped_endpoint_async_wrapper(): + return True + + +@app.get("/async-wrapped-endpoint-async-wrapper/") +@noop_wrap_async +async def get_async_wrapped_endpoint_async_wrapper(): + return True + + +client = TestClient(app) + + +@pytest.mark.parametrize( + "route", + [ + "/wrapped-dependency/", + "/wrapped-gen-dependency/", + "/async-wrapped-dependency/", + "/async-wrapped-gen-dependency/", + "/wrapped-class-instance-dependency/", + "/wrapped-class-instance-async-dependency/", + "/wrapped-class-instance-gen-dependency/", + "/wrapped-class-instance-async-gen-dependency/", + "/class-instance-wrapped-dependency/", + "/class-instance-wrapped-async-dependency/", + "/class-instance-async-wrapped-dependency/", + "/class-instance-async-wrapped-async-dependency/", + "/class-instance-wrapped-gen-dependency/", + "/class-instance-wrapped-async-gen-dependency/", + "/class-instance-async-wrapped-gen-dependency/", + "/class-instance-async-wrapped-gen-async-dependency/", + "/wrapped-class-dependency/", + "/wrapped-endpoint/", + "/async-wrapped-endpoint/", + "/wrapped-dependency-async-wrapper/", + "/wrapped-gen-dependency-async-wrapper/", + "/async-wrapped-dependency-async-wrapper/", + "/async-wrapped-gen-dependency-async-wrapper/", + "/wrapped-class-instance-dependency-async-wrapper/", + "/wrapped-class-instance-async-dependency-async-wrapper/", + "/wrapped-class-dependency-async-wrapper/", + "/wrapped-endpoint-async-wrapper/", + "/async-wrapped-endpoint-async-wrapper/", + ], +) +def test_class_dependency(route): + response = client.get(route) + assert response.status_code == 200, response.text + assert response.json() is True diff --git a/tests/test_dependency_yield_scope.py b/tests/test_dependency_yield_scope.py index a5227dd7ab..f3fc3cc94f 100644 --- a/tests/test_dependency_yield_scope.py +++ b/tests/test_dependency_yield_scope.py @@ -1,12 +1,11 @@ import json -from typing import Any, Tuple +from typing import Annotated, Any import pytest -from fastapi import Depends, FastAPI +from fastapi import APIRouter, Depends, FastAPI, HTTPException from fastapi.exceptions import FastAPIError from fastapi.responses import StreamingResponse from fastapi.testclient import TestClient -from typing_extensions import Annotated class Session: @@ -20,6 +19,11 @@ def dep_session() -> Any: s.open = False +def raise_after_yield() -> Any: + yield + raise HTTPException(status_code=503, detail="Exception after yield") + + SessionFuncDep = Annotated[Session, Depends(dep_session, scope="function")] SessionRequestDep = Annotated[Session, Depends(dep_session, scope="request")] SessionDefaultDep = Annotated[Session, Depends(dep_session)] @@ -38,7 +42,7 @@ def get_named_session(session: SessionRequestDep, session_b: SessionDefaultDep) named_session.open = False -NamedSessionsDep = Annotated[Tuple[NamedSession, Session], Depends(get_named_session)] +NamedSessionsDep = Annotated[tuple[NamedSession, Session], Depends(get_named_session)] def get_named_func_session(session: SessionFuncDep) -> Any: @@ -53,17 +57,23 @@ def get_named_regular_func_session(session: SessionFuncDep) -> Any: BrokenSessionsDep = Annotated[ - Tuple[NamedSession, Session], Depends(get_named_func_session) + tuple[NamedSession, Session], Depends(get_named_func_session) ] NamedSessionsFuncDep = Annotated[ - Tuple[NamedSession, Session], Depends(get_named_func_session, scope="function") + tuple[NamedSession, Session], Depends(get_named_func_session, scope="function") ] RegularSessionsDep = Annotated[ - Tuple[NamedSession, Session], Depends(get_named_regular_func_session) + tuple[NamedSession, Session], Depends(get_named_regular_func_session) ] app = FastAPI() +router = APIRouter() + + +@router.get("/") +def get_index(): + return {"status": "ok"} @app.get("/function-scope") @@ -124,6 +134,18 @@ def get_regular_function_scope(sessions: RegularSessionsDep) -> Any: return StreamingResponse(iter_data()) +app.include_router( + prefix="/router-scope-function", + router=router, + dependencies=[Depends(raise_after_yield, scope="function")], +) + +app.include_router( + prefix="/router-scope-request", + router=router, + dependencies=[Depends(raise_after_yield, scope="request")], +) + client = TestClient(app) @@ -182,3 +204,42 @@ def test_regular_function_scope() -> None: data = response.json() assert data["named_session_open"] is True assert data["session_open"] is False + + +def test_router_level_dep_scope_function() -> None: + response = client.get("/router-scope-function/") + assert response.status_code == 503 + assert response.json() == {"detail": "Exception after yield"} + + +def test_router_level_dep_scope_request() -> None: + with TestClient(app, raise_server_exceptions=False) as client: + response = client.get("/router-scope-request/") + assert response.status_code == 200 + assert response.json() == {"status": "ok"} + + +def test_app_level_dep_scope_function() -> None: + app = FastAPI(dependencies=[Depends(raise_after_yield, scope="function")]) + + @app.get("/app-scope-function") + def get_app_scope_function(): + return {"status": "ok"} + + with TestClient(app) as client: + response = client.get("/app-scope-function") + assert response.status_code == 503 + assert response.json() == {"detail": "Exception after yield"} + + +def test_app_level_dep_scope_request() -> None: + app = FastAPI(dependencies=[Depends(raise_after_yield, scope="request")]) + + @app.get("/app-scope-request") + def get_app_scope_request(): + return {"status": "ok"} + + with TestClient(app, raise_server_exceptions=False) as client: + response = client.get("/app-scope-request") + assert response.status_code == 200 + assert response.json() == {"status": "ok"} diff --git a/tests/test_dependency_yield_scope_websockets.py b/tests/test_dependency_yield_scope_websockets.py index 52a30ae7a7..dbe35e576c 100644 --- a/tests/test_dependency_yield_scope_websockets.py +++ b/tests/test_dependency_yield_scope_websockets.py @@ -1,13 +1,12 @@ from contextvars import ContextVar -from typing import Any, Dict, Tuple +from typing import Annotated, Any import pytest from fastapi import Depends, FastAPI, WebSocket from fastapi.exceptions import FastAPIError from fastapi.testclient import TestClient -from typing_extensions import Annotated -global_context: ContextVar[Dict[str, Any]] = ContextVar("global_context", default={}) # noqa: B039 +global_context: ContextVar[dict[str, Any]] = ContextVar("global_context", default={}) # noqa: B039 class Session: @@ -43,7 +42,7 @@ def get_named_session(session: SessionRequestDep, session_b: SessionDefaultDep) global_state["named_session_closed"] = True -NamedSessionsDep = Annotated[Tuple[NamedSession, Session], Depends(get_named_session)] +NamedSessionsDep = Annotated[tuple[NamedSession, Session], Depends(get_named_session)] def get_named_func_session(session: SessionFuncDep) -> Any: @@ -60,14 +59,14 @@ def get_named_regular_func_session(session: SessionFuncDep) -> Any: BrokenSessionsDep = Annotated[ - Tuple[NamedSession, Session], Depends(get_named_func_session) + tuple[NamedSession, Session], Depends(get_named_func_session) ] NamedSessionsFuncDep = Annotated[ - Tuple[NamedSession, Session], Depends(get_named_func_session, scope="function") + tuple[NamedSession, Session], Depends(get_named_func_session, scope="function") ] RegularSessionsDep = Annotated[ - Tuple[NamedSession, Session], Depends(get_named_regular_func_session) + tuple[NamedSession, Session], Depends(get_named_regular_func_session) ] app = FastAPI() diff --git a/tests/test_depends_hashable.py b/tests/test_depends_hashable.py new file mode 100644 index 0000000000..d57f2726ec --- /dev/null +++ b/tests/test_depends_hashable.py @@ -0,0 +1,25 @@ +# This is more or less a workaround to make Depends and Security hashable +# as other tools that use them depend on that +# Ref: https://github.com/fastapi/fastapi/pull/14320 + +from fastapi import Depends, Security + + +def dep(): + pass + + +def test_depends_hashable(): + dep() # just for coverage + d1 = Depends(dep) + d2 = Depends(dep) + d3 = Depends(dep, scope="function") + d4 = Depends(dep, scope="function") + + s1 = Security(dep) + s2 = Security(dep) + + assert hash(d1) == hash(d2) + assert hash(s1) == hash(s2) + assert hash(d1) != hash(d3) + assert hash(d3) == hash(d4) diff --git a/tests/test_extra_routes.py b/tests/test_extra_routes.py index bd16fe9254..45734ec28a 100644 --- a/tests/test_extra_routes.py +++ b/tests/test_extra_routes.py @@ -1,6 +1,5 @@ from typing import Optional -from dirty_equals import IsDict from fastapi import FastAPI from fastapi.responses import JSONResponse from fastapi.testclient import TestClient @@ -328,14 +327,10 @@ def test_openapi_schema(): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, - "price": IsDict( - { - "title": "Price", - "anyOf": [{"type": "number"}, {"type": "null"}], - } - ) - # TODO: remove when deprecating Pydantic v1 - | IsDict({"title": "Price", "type": "number"}), + "price": { + "title": "Price", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, }, }, "ValidationError": { diff --git a/tests/test_fastapi_cli.py b/tests/test_fastapi_cli.py index a5c10778ad..78a49a1fbe 100644 --- a/tests/test_fastapi_cli.py +++ b/tests/test_fastapi_cli.py @@ -1,3 +1,4 @@ +import os import subprocess import sys from unittest.mock import patch @@ -20,6 +21,7 @@ def test_fastapi_cli(): ], capture_output=True, encoding="utf-8", + env={**os.environ, "PYTHONIOENCODING": "utf-8"}, ) assert result.returncode == 1, result.stdout assert "Path does not exist non_existent_file.py" in result.stdout diff --git a/tests/test_file_and_form_order_issue_9116.py b/tests/test_file_and_form_order_issue_9116.py index cb9a31d314..75290b60cd 100644 --- a/tests/test_file_and_form_order_issue_9116.py +++ b/tests/test_file_and_form_order_issue_9116.py @@ -4,12 +4,11 @@ See https://github.com/tiangolo/fastapi/discussions/9116 """ from pathlib import Path -from typing import List +from typing import Annotated import pytest from fastapi import FastAPI, File, Form from fastapi.testclient import TestClient -from typing_extensions import Annotated app = FastAPI() @@ -32,7 +31,7 @@ def file_after_form( @app.post("/file_list_before_form") def file_list_before_form( - files: Annotated[List[bytes], File()], + files: Annotated[list[bytes], File()], city: Annotated[str, Form()], ): return {"file_contents": files, "city": city} @@ -41,7 +40,7 @@ def file_list_before_form( @app.post("/file_list_after_form") def file_list_after_form( city: Annotated[str, Form()], - files: Annotated[List[bytes], File()], + files: Annotated[list[bytes], File()], ): return {"file_contents": files, "city": city} diff --git a/tests/test_filter_pydantic_sub_model/app_pv1.py b/tests/test_filter_pydantic_sub_model/app_pv1.py deleted file mode 100644 index 657e8c5d12..0000000000 --- a/tests/test_filter_pydantic_sub_model/app_pv1.py +++ /dev/null @@ -1,35 +0,0 @@ -from typing import Optional - -from fastapi import Depends, FastAPI -from pydantic import BaseModel, validator - -app = FastAPI() - - -class ModelB(BaseModel): - username: str - - -class ModelC(ModelB): - password: str - - -class ModelA(BaseModel): - name: str - description: Optional[str] = None - model_b: ModelB - - @validator("name") - def lower_username(cls, name: str, values): - if not name.endswith("A"): - raise ValueError("name must end in A") - return name - - -async def get_model_c() -> ModelC: - return ModelC(username="test-user", password="test-password") - - -@app.get("/model/{name}", response_model=ModelA) -async def get_model_a(name: str, model_c=Depends(get_model_c)): - return {"name": name, "description": "model-a-desc", "model_b": model_c} diff --git a/tests/test_filter_pydantic_sub_model_pv2.py b/tests/test_filter_pydantic_sub_model_pv2.py index 2e2c26ddcb..fc5876410d 100644 --- a/tests/test_filter_pydantic_sub_model_pv2.py +++ b/tests/test_filter_pydantic_sub_model_pv2.py @@ -1,12 +1,11 @@ from typing import Optional import pytest -from dirty_equals import HasRepr, IsDict, IsOneOf +from dirty_equals import HasRepr from fastapi import Depends, FastAPI from fastapi.exceptions import ResponseValidationError from fastapi.testclient import TestClient - -from .utils import needs_pydanticv2 +from inline_snapshot import snapshot @pytest.fixture(name="client") @@ -25,6 +24,7 @@ def get_client(): name: str description: Optional[str] = None foo: ModelB + tags: dict[str, str] = {} @field_validator("name") def lower_username(cls, name: str, info: ValidationInfo): @@ -37,13 +37,17 @@ def get_client(): @app.get("/model/{name}", response_model=ModelA) async def get_model_a(name: str, model_c=Depends(get_model_c)): - return {"name": name, "description": "model-a-desc", "foo": model_c} + return { + "name": name, + "description": "model-a-desc", + "foo": model_c, + "tags": {"key1": "value1", "key2": "value2"}, + } client = TestClient(app) return client -@needs_pydanticv2 def test_filter_sub_model(client: TestClient): response = client.get("/model/modelA") assert response.status_code == 200, response.text @@ -51,134 +55,128 @@ def test_filter_sub_model(client: TestClient): "name": "modelA", "description": "model-a-desc", "foo": {"username": "test-user"}, + "tags": {"key1": "value1", "key2": "value2"}, } -@needs_pydanticv2 def test_validator_is_cloned(client: TestClient): with pytest.raises(ResponseValidationError) as err: client.get("/model/modelX") assert err.value.errors() == [ - IsDict( - { - "type": "value_error", - "loc": ("response", "name"), - "msg": "Value error, name must end in A", - "input": "modelX", - "ctx": {"error": HasRepr("ValueError('name must end in A')")}, - } - ) - | IsDict( - # TODO remove when deprecating Pydantic v1 - { - "loc": ("response", "name"), - "msg": "name must end in A", - "type": "value_error", - } - ) + { + "type": "value_error", + "loc": ("response", "name"), + "msg": "Value error, name must end in A", + "input": "modelX", + "ctx": {"error": HasRepr("ValueError('name must end in A')")}, + } ] -@needs_pydanticv2 def test_openapi_schema(client: TestClient): 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": { - "/model/{name}": { - "get": { - "summary": "Get Model A", - "operationId": "get_model_a_model__name__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Name", "type": "string"}, - "name": "name", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/ModelA"} - } + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/model/{name}": { + "get": { + "summary": "Get Model A", + "operationId": "get_model_a_model__name__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Name", "type": "string"}, + "name": "name", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ModelA" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, }, }, - "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" + }, + } + }, + }, + "ModelA": { + "title": "ModelA", + "required": ["name", "foo"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], }, + "foo": {"$ref": "#/components/schemas/ModelB"}, + "tags": { + "additionalProperties": {"type": "string"}, + "type": "object", + "title": "Tags", + "default": {}, + }, + }, + }, + "ModelB": { + "title": "ModelB", + "required": ["username"], + "type": "object", + "properties": { + "username": {"title": "Username", "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"}, }, }, } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ModelA": { - "title": "ModelA", - "required": IsOneOf( - ["name", "description", "foo"], - # TODO remove when deprecating Pydantic v1 - ["name", "foo"], - ), - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": IsDict( - { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | - # TODO remove when deprecating Pydantic v1 - IsDict({"title": "Description", "type": "string"}), - "foo": {"$ref": "#/components/schemas/ModelB"}, - }, - }, - "ModelB": { - "title": "ModelB", - "required": ["username"], - "type": "object", - "properties": {"username": {"title": "Username", "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_form_default.py b/tests/test_form_default.py new file mode 100644 index 0000000000..0b3eb8f2e2 --- /dev/null +++ b/tests/test_form_default.py @@ -0,0 +1,34 @@ +from typing import Annotated, Optional + +from fastapi import FastAPI, File, Form +from starlette.testclient import TestClient + +app = FastAPI() + + +@app.post("/urlencoded") +async def post_url_encoded(age: Annotated[Optional[int], Form()] = None): + return age + + +@app.post("/multipart") +async def post_multi_part( + age: Annotated[Optional[int], Form()] = None, + file: Annotated[Optional[bytes], File()] = None, +): + return {"file": file, "age": age} + + +client = TestClient(app) + + +def test_form_default_url_encoded(): + response = client.post("/urlencoded", data={"age": ""}) + assert response.status_code == 200 + assert response.text == "null" + + +def test_form_default_multi_part(): + response = client.post("/multipart", data={"age": ""}) + assert response.status_code == 200 + assert response.json() == {"file": None, "age": None} diff --git a/tests/test_forms_single_model.py b/tests/test_forms_single_model.py index 880ab38200..7d03d29572 100644 --- a/tests/test_forms_single_model.py +++ b/tests/test_forms_single_model.py @@ -1,10 +1,8 @@ -from typing import List, Optional +from typing import Annotated, Optional -from dirty_equals import IsDict from fastapi import FastAPI, Form from fastapi.testclient import TestClient from pydantic import BaseModel, Field -from typing_extensions import Annotated app = FastAPI() @@ -13,15 +11,26 @@ class FormModel(BaseModel): username: str lastname: str age: Optional[int] = None - tags: List[str] = ["foo", "bar"] + tags: list[str] = ["foo", "bar"] alias_with: str = Field(alias="with", default="nothing") +class FormModelExtraAllow(BaseModel): + param: str + + model_config = {"extra": "allow"} + + @app.post("/form/") def post_form(user: Annotated[FormModel, Form()]): return user +@app.post("/form-extra-allow/") +def post_form_extra_allow(params: Annotated[FormModelExtraAllow, Form()]): + return params + + client = TestClient(app) @@ -69,65 +78,64 @@ def test_invalid_data(): }, ) assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["body", "age"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "seventy", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "age"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "int_parsing", + "loc": ["body", "age"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "seventy", + } + ] + } def test_no_data(): response = client.post("/form/") assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": {"tags": ["foo", "bar"], "with": "nothing"}, - }, - { - "type": "missing", - "loc": ["body", "lastname"], - "msg": "Field required", - "input": {"tags": ["foo", "bar"], "with": "nothing"}, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "lastname"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {"tags": ["foo", "bar"], "with": "nothing"}, + }, + { + "type": "missing", + "loc": ["body", "lastname"], + "msg": "Field required", + "input": {"tags": ["foo", "bar"], "with": "nothing"}, + }, + ] + } + + +def test_extra_param_single(): + response = client.post( + "/form-extra-allow/", + data={ + "param": "123", + "extra_param": "456", + }, ) + assert response.status_code == 200, response.text + assert response.json() == { + "param": "123", + "extra_param": "456", + } + + +def test_extra_param_list(): + response = client.post( + "/form-extra-allow/", + data={ + "param": "123", + "extra_params": ["456", "789"], + }, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "param": "123", + "extra_params": ["456", "789"], + } diff --git a/tests/test_forms_single_param.py b/tests/test_forms_single_param.py index 3bb951441f..67f054b34e 100644 --- a/tests/test_forms_single_param.py +++ b/tests/test_forms_single_param.py @@ -1,6 +1,7 @@ +from typing import Annotated + from fastapi import FastAPI, Form from fastapi.testclient import TestClient -from typing_extensions import Annotated app = FastAPI() diff --git a/tests/test_generate_unique_id_function.py b/tests/test_generate_unique_id_function.py index 5aeec66367..62ebfbc964 100644 --- a/tests/test_generate_unique_id_function.py +++ b/tests/test_generate_unique_id_function.py @@ -1,5 +1,4 @@ import warnings -from typing import List from fastapi import APIRouter, FastAPI from fastapi.routing import APIRoute @@ -33,12 +32,12 @@ def test_top_level_generate_unique_id(): app = FastAPI(generate_unique_id_function=custom_generate_unique_id) router = APIRouter() - @app.post("/", response_model=List[Item], responses={404: {"model": List[Message]}}) + @app.post("/", response_model=list[Item], responses={404: {"model": list[Message]}}) def post_root(item1: Item, item2: Item): return item1, item2 # pragma: nocover @router.post( - "/router", response_model=List[Item], responses={404: {"model": List[Message]}} + "/router", response_model=list[Item], responses={404: {"model": list[Message]}} ) def post_router(item1: Item, item2: Item): return item1, item2 # pragma: nocover @@ -234,12 +233,12 @@ def test_router_overrides_generate_unique_id(): app = FastAPI(generate_unique_id_function=custom_generate_unique_id) router = APIRouter(generate_unique_id_function=custom_generate_unique_id2) - @app.post("/", response_model=List[Item], responses={404: {"model": List[Message]}}) + @app.post("/", response_model=list[Item], responses={404: {"model": list[Message]}}) def post_root(item1: Item, item2: Item): return item1, item2 # pragma: nocover @router.post( - "/router", response_model=List[Item], responses={404: {"model": List[Message]}} + "/router", response_model=list[Item], responses={404: {"model": list[Message]}} ) def post_router(item1: Item, item2: Item): return item1, item2 # pragma: nocover @@ -435,12 +434,12 @@ def test_router_include_overrides_generate_unique_id(): app = FastAPI(generate_unique_id_function=custom_generate_unique_id) router = APIRouter(generate_unique_id_function=custom_generate_unique_id2) - @app.post("/", response_model=List[Item], responses={404: {"model": List[Message]}}) + @app.post("/", response_model=list[Item], responses={404: {"model": list[Message]}}) def post_root(item1: Item, item2: Item): return item1, item2 # pragma: nocover @router.post( - "/router", response_model=List[Item], responses={404: {"model": List[Message]}} + "/router", response_model=list[Item], responses={404: {"model": list[Message]}} ) def post_router(item1: Item, item2: Item): return item1, item2 # pragma: nocover @@ -637,20 +636,20 @@ def test_subrouter_top_level_include_overrides_generate_unique_id(): router = APIRouter() sub_router = APIRouter(generate_unique_id_function=custom_generate_unique_id2) - @app.post("/", response_model=List[Item], responses={404: {"model": List[Message]}}) + @app.post("/", response_model=list[Item], responses={404: {"model": list[Message]}}) def post_root(item1: Item, item2: Item): return item1, item2 # pragma: nocover @router.post( - "/router", response_model=List[Item], responses={404: {"model": List[Message]}} + "/router", response_model=list[Item], responses={404: {"model": list[Message]}} ) def post_router(item1: Item, item2: Item): return item1, item2 # pragma: nocover @sub_router.post( "/subrouter", - response_model=List[Item], - responses={404: {"model": List[Message]}}, + response_model=list[Item], + responses={404: {"model": list[Message]}}, ) def post_subrouter(item1: Item, item2: Item): return item1, item2 # pragma: nocover @@ -910,14 +909,14 @@ def test_router_path_operation_overrides_generate_unique_id(): app = FastAPI(generate_unique_id_function=custom_generate_unique_id) router = APIRouter(generate_unique_id_function=custom_generate_unique_id2) - @app.post("/", response_model=List[Item], responses={404: {"model": List[Message]}}) + @app.post("/", response_model=list[Item], responses={404: {"model": list[Message]}}) def post_root(item1: Item, item2: Item): return item1, item2 # pragma: nocover @router.post( "/router", - response_model=List[Item], - responses={404: {"model": List[Message]}}, + response_model=list[Item], + responses={404: {"model": list[Message]}}, generate_unique_id_function=custom_generate_unique_id3, ) def post_router(item1: Item, item2: Item): @@ -1116,8 +1115,8 @@ def test_app_path_operation_overrides_generate_unique_id(): @app.post( "/", - response_model=List[Item], - responses={404: {"model": List[Message]}}, + response_model=list[Item], + responses={404: {"model": list[Message]}}, generate_unique_id_function=custom_generate_unique_id3, ) def post_root(item1: Item, item2: Item): @@ -1125,8 +1124,8 @@ def test_app_path_operation_overrides_generate_unique_id(): @router.post( "/router", - response_model=List[Item], - responses={404: {"model": List[Message]}}, + response_model=list[Item], + responses={404: {"model": list[Message]}}, ) def post_router(item1: Item, item2: Item): return item1, item2 # pragma: nocover @@ -1324,8 +1323,8 @@ def test_callback_override_generate_unique_id(): @callback_router.post( "/post-callback", - response_model=List[Item], - responses={404: {"model": List[Message]}}, + response_model=list[Item], + responses={404: {"model": list[Message]}}, generate_unique_id_function=custom_generate_unique_id3, ) def post_callback(item1: Item, item2: Item): @@ -1333,8 +1332,8 @@ def test_callback_override_generate_unique_id(): @app.post( "/", - response_model=List[Item], - responses={404: {"model": List[Message]}}, + response_model=list[Item], + responses={404: {"model": list[Message]}}, generate_unique_id_function=custom_generate_unique_id3, callbacks=callback_router.routes, ) @@ -1343,8 +1342,8 @@ def test_callback_override_generate_unique_id(): @app.post( "/tocallback", - response_model=List[Item], - responses={404: {"model": List[Message]}}, + response_model=list[Item], + responses={404: {"model": list[Message]}}, ) def post_with_callback(item1: Item, item2: Item): return item1, item2 # pragma: nocover diff --git a/tests/test_generic_parameterless_depends.py b/tests/test_generic_parameterless_depends.py index 5aa35320c9..93b72ad243 100644 --- a/tests/test_generic_parameterless_depends.py +++ b/tests/test_generic_parameterless_depends.py @@ -1,8 +1,7 @@ -from typing import TypeVar +from typing import Annotated, TypeVar from fastapi import Depends, FastAPI from fastapi.testclient import TestClient -from typing_extensions import Annotated app = FastAPI() diff --git a/tests/test_get_model_definitions_formfeed_escape.py b/tests/test_get_model_definitions_formfeed_escape.py index 6601585ef0..eb7939b69a 100644 --- a/tests/test_get_model_definitions_formfeed_escape.py +++ b/tests/test_get_model_definitions_formfeed_escape.py @@ -1,180 +1,158 @@ -from typing import Any, Iterator, Set, Type - -import fastapi._compat -import fastapi.openapi.utils -import pydantic.schema import pytest from fastapi import FastAPI -from pydantic import BaseModel -from starlette.testclient import TestClient - -from .utils import needs_pydanticv1 +from fastapi.testclient import TestClient +from inline_snapshot import snapshot -class Address(BaseModel): - """ - This is a public description of an Address - \f - You can't see this part of the docstring, it's private! - """ +@pytest.fixture(name="client") +def client_fixture() -> TestClient: + from pydantic import BaseModel - line_1: str - city: str - state_province: str + class Address(BaseModel): + """ + This is a public description of an Address + \f + You can't see this part of the docstring, it's private! + """ + + line_1: str + city: str + state_province: str + + class Facility(BaseModel): + id: str + address: Address + + app = FastAPI() + + @app.get("/facilities/{facility_id}") + def get_facility(facility_id: str) -> Facility: + return Facility( + id=facility_id, + address=Address(line_1="123 Main St", city="Anytown", state_province="CA"), + ) + + client = TestClient(app) + return client -class Facility(BaseModel): - id: str - address: Address +def test_get(client: TestClient): + response = client.get("/facilities/42") + assert response.status_code == 200, response.text + assert response.json() == { + "id": "42", + "address": { + "line_1": "123 Main St", + "city": "Anytown", + "state_province": "CA", + }, + } -app = FastAPI() - -client = TestClient(app) - - -@app.get("/facilities/{facility_id}") -def get_facility(facility_id: str) -> Facility: ... - - -openapi_schema = { - "components": { - "schemas": { - "Address": { - # NOTE: the description of this model shows only the public-facing text, before the `\f` in docstring - "description": "This is a public description of an Address\n", - "properties": { - "city": {"title": "City", "type": "string"}, - "line_1": {"title": "Line 1", "type": "string"}, - "state_province": {"title": "State Province", "type": "string"}, - }, - "required": ["line_1", "city", "state_province"], - "title": "Address", - "type": "object", - }, - "Facility": { - "properties": { - "address": {"$ref": "#/components/schemas/Address"}, - "id": {"title": "Id", "type": "string"}, - }, - "required": ["id", "address"], - "title": "Facility", - "type": "object", - }, - "HTTPValidationError": { - "properties": { - "detail": { - "items": {"$ref": "#/components/schemas/ValidationError"}, - "title": "Detail", - "type": "array", - } - }, - "title": "HTTPValidationError", - "type": "object", - }, - "ValidationError": { - "properties": { - "loc": { - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - "title": "Location", - "type": "array", - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - "required": ["loc", "msg", "type"], - "title": "ValidationError", - "type": "object", - }, - } - }, - "info": {"title": "FastAPI", "version": "0.1.0"}, - "openapi": "3.1.0", - "paths": { - "/facilities/{facility_id}": { - "get": { - "operationId": "get_facility_facilities__facility_id__get", - "parameters": [ - { - "in": "path", - "name": "facility_id", - "required": True, - "schema": {"title": "Facility Id", "type": "string"}, - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Facility"} - } - }, - "description": "Successful Response", - }, - "422": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - "description": "Validation Error", - }, - }, - "summary": "Get Facility", - } - } - }, -} - - -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): """ Sanity check to ensure our app's openapi schema renders as we expect """ response = client.get("/openapi.json") assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - -class SortedTypeSet(set): - """ - Set of Types whose `__iter__()` method yields results sorted by the type names - """ - - def __init__(self, seq: Set[Type[Any]], *, sort_reversed: bool): - super().__init__(seq) - self.sort_reversed = sort_reversed - - def __iter__(self) -> Iterator[Type[Any]]: - members_sorted = sorted( - super().__iter__(), - key=lambda type_: type_.__name__, - reverse=self.sort_reversed, - ) - yield from members_sorted - - -@needs_pydanticv1 -@pytest.mark.parametrize("sort_reversed", [True, False]) -def test_model_description_escaped_with_formfeed(sort_reversed: bool): - """ - Regression test for bug fixed by https://github.com/fastapi/fastapi/pull/6039. - - Test `get_model_definitions` with models passed in different order. - """ - from fastapi._compat import v1 - - all_fields = fastapi.openapi.utils.get_fields_from_routes(app.routes) - - flat_models = v1.get_flat_models_from_fields(all_fields, known_models=set()) - model_name_map = pydantic.schema.get_model_name_map(flat_models) - - expected_address_description = "This is a public description of an Address\n" - - models = v1.get_model_definitions( - flat_models=SortedTypeSet(flat_models, sort_reversed=sort_reversed), - model_name_map=model_name_map, + assert response.json() == snapshot( + { + "components": { + "schemas": { + "Address": { + # NOTE: the description of this model shows only the public-facing text, before the `\f` in docstring + "description": "This is a public description of an Address\n", + "properties": { + "city": {"title": "City", "type": "string"}, + "line_1": {"title": "Line 1", "type": "string"}, + "state_province": { + "title": "State Province", + "type": "string", + }, + }, + "required": ["line_1", "city", "state_province"], + "title": "Address", + "type": "object", + }, + "Facility": { + "properties": { + "address": {"$ref": "#/components/schemas/Address"}, + "id": {"title": "Id", "type": "string"}, + }, + "required": ["id", "address"], + "title": "Facility", + "type": "object", + }, + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "title": "Detail", + "type": "array", + } + }, + "title": "HTTPValidationError", + "type": "object", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "title": "Location", + "type": "array", + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + "required": ["loc", "msg", "type"], + "title": "ValidationError", + "type": "object", + }, + } + }, + "info": {"title": "FastAPI", "version": "0.1.0"}, + "openapi": "3.1.0", + "paths": { + "/facilities/{facility_id}": { + "get": { + "operationId": "get_facility_facilities__facility_id__get", + "parameters": [ + { + "in": "path", + "name": "facility_id", + "required": True, + "schema": {"title": "Facility Id", "type": "string"}, + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Facility" + } + } + }, + "description": "Successful Response", + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error", + }, + }, + "summary": "Get Facility", + } + } + }, + } ) - assert models["Address"]["description"] == expected_address_description diff --git a/tests/test_infer_param_optionality.py b/tests/test_infer_param_optionality.py index e3d57bb428..147018996e 100644 --- a/tests/test_infer_param_optionality.py +++ b/tests/test_infer_param_optionality.py @@ -1,6 +1,5 @@ from typing import Optional -from dirty_equals import IsDict from fastapi import APIRouter, FastAPI from fastapi.testclient import TestClient @@ -163,16 +162,10 @@ def test_openapi_schema(): "required": False, "name": "user_id", "in": "query", - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "User Id", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "User Id", "type": "string"} - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "User Id", + }, } ], "responses": { @@ -208,16 +201,10 @@ def test_openapi_schema(): "required": False, "name": "user_id", "in": "query", - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "User Id", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "User Id", "type": "string"} - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "User Id", + }, }, ], "responses": { @@ -247,16 +234,10 @@ def test_openapi_schema(): "required": True, "name": "user_id", "in": "path", - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "User Id", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "User Id", "type": "string"} - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "User Id", + }, } ], "responses": { @@ -292,16 +273,10 @@ def test_openapi_schema(): "required": True, "name": "user_id", "in": "path", - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "User Id", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "User Id", "type": "string"} - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "User Id", + }, }, ], "responses": { diff --git a/tests/test_inherited_custom_class.py b/tests/test_inherited_custom_class.py index fe9350f4ef..8cf8952f92 100644 --- a/tests/test_inherited_custom_class.py +++ b/tests/test_inherited_custom_class.py @@ -5,8 +5,6 @@ from fastapi import FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel -from .utils import needs_pydanticv1, needs_pydanticv2 - class MyUuid: def __init__(self, uuid_string: str): @@ -26,7 +24,6 @@ class MyUuid: raise TypeError("vars() argument must have __dict__ attribute") -@needs_pydanticv2 def test_pydanticv2(): from pydantic import field_serializer @@ -68,44 +65,3 @@ def test_pydanticv2(): assert response_pydantic.json() == { "a_uuid": "b8799909-f914-42de-91bc-95c819218d01" } - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -def test_pydanticv1(): - app = FastAPI() - - @app.get("/fast_uuid") - def return_fast_uuid(): - asyncpg_uuid = MyUuid("a10ff360-3b1e-4984-a26f-d3ab460bdb51") - assert isinstance(asyncpg_uuid, uuid.UUID) - assert type(asyncpg_uuid) is not uuid.UUID - with pytest.raises(TypeError): - vars(asyncpg_uuid) - return {"fast_uuid": asyncpg_uuid} - - class SomeCustomClass(BaseModel): - class Config: - arbitrary_types_allowed = True - json_encoders = {uuid.UUID: str} - - a_uuid: MyUuid - - @app.get("/get_custom_class") - def return_some_user(): - # Test that the fix also works for custom pydantic classes - return SomeCustomClass(a_uuid=MyUuid("b8799909-f914-42de-91bc-95c819218d01")) - - client = TestClient(app) - - with client: - response_simple = client.get("/fast_uuid") - response_pydantic = client.get("/get_custom_class") - - assert response_simple.json() == { - "fast_uuid": "a10ff360-3b1e-4984-a26f-d3ab460bdb51" - } - - assert response_pydantic.json() == { - "a_uuid": "b8799909-f914-42de-91bc-95c819218d01" - } diff --git a/tests/test_invalid_path_param.py b/tests/test_invalid_path_param.py index d5fa53c1fc..35c00363da 100644 --- a/tests/test_invalid_path_param.py +++ b/tests/test_invalid_path_param.py @@ -1,5 +1,3 @@ -from typing import Dict, List, Tuple - import pytest from fastapi import FastAPI from pydantic import BaseModel @@ -13,7 +11,7 @@ def test_invalid_sequence(): title: str @app.get("/items/{id}") - def read_items(id: List[Item]): + def read_items(id: list[Item]): pass # pragma: no cover @@ -25,7 +23,7 @@ def test_invalid_tuple(): title: str @app.get("/items/{id}") - def read_items(id: Tuple[Item, Item]): + def read_items(id: tuple[Item, Item]): pass # pragma: no cover @@ -37,7 +35,7 @@ def test_invalid_dict(): title: str @app.get("/items/{id}") - def read_items(id: Dict[str, Item]): + def read_items(id: dict[str, Item]): pass # pragma: no cover diff --git a/tests/test_invalid_sequence_param.py b/tests/test_invalid_sequence_param.py index 475786adbf..2b8fd059e1 100644 --- a/tests/test_invalid_sequence_param.py +++ b/tests/test_invalid_sequence_param.py @@ -1,4 +1,4 @@ -from typing import Dict, List, Optional, Tuple +from typing import Optional import pytest from fastapi import FastAPI, Query @@ -13,7 +13,7 @@ def test_invalid_sequence(): title: str @app.get("/items/") - def read_items(q: List[Item] = Query(default=None)): + def read_items(q: list[Item] = Query(default=None)): pass # pragma: no cover @@ -25,7 +25,7 @@ def test_invalid_tuple(): title: str @app.get("/items/") - def read_items(q: Tuple[Item, Item] = Query(default=None)): + def read_items(q: tuple[Item, Item] = Query(default=None)): pass # pragma: no cover @@ -37,7 +37,7 @@ def test_invalid_dict(): title: str @app.get("/items/") - def read_items(q: Dict[str, Item] = Query(default=None)): + def read_items(q: dict[str, Item] = Query(default=None)): pass # pragma: no cover diff --git a/tests/test_jsonable_encoder.py b/tests/test_jsonable_encoder.py index 447c5b4d6a..4528dff440 100644 --- a/tests/test_jsonable_encoder.py +++ b/tests/test_jsonable_encoder.py @@ -1,18 +1,19 @@ +import warnings from collections import deque from dataclasses import dataclass from datetime import datetime, timezone from decimal import Decimal from enum import Enum +from math import isinf, isnan from pathlib import PurePath, PurePosixPath, PureWindowsPath -from typing import Optional +from typing import Optional, TypedDict import pytest -from fastapi._compat import PYDANTIC_V2, Undefined +from fastapi._compat import Undefined from fastapi.encoders import jsonable_encoder +from fastapi.exceptions import PydanticV1NotSupportedError from pydantic import BaseModel, Field, ValidationError -from .utils import needs_pydanticv1, needs_pydanticv2 - class Person: def __init__(self, name: str): @@ -58,12 +59,7 @@ class RoleEnum(Enum): class ModelWithConfig(BaseModel): role: Optional[RoleEnum] = None - if PYDANTIC_V2: - model_config = {"use_enum_values": True} - else: - - class Config: - use_enum_values = True + model_config = {"use_enum_values": True} class ModelWithAlias(BaseModel): @@ -88,6 +84,18 @@ def test_encode_dict(): } +def test_encode_dict_include_exclude_list(): + pet = {"name": "Firulais", "owner": {"name": "Foo"}} + assert jsonable_encoder(pet) == {"name": "Firulais", "owner": {"name": "Foo"}} + assert jsonable_encoder(pet, include=["name"]) == {"name": "Firulais"} + assert jsonable_encoder(pet, exclude=["owner"]) == {"name": "Firulais"} + assert jsonable_encoder(pet, include=[]) == {} + assert jsonable_encoder(pet, exclude=[]) == { + "name": "Firulais", + "owner": {"name": "Foo"}, + } + + def test_encode_class(): person = Person(name="Foo") pet = Pet(owner=person, name="Firulais") @@ -129,7 +137,6 @@ def test_encode_unsupported(): jsonable_encoder(unserializable) -@needs_pydanticv2 def test_encode_custom_json_encoders_model_pydanticv2(): from pydantic import field_serializer @@ -149,27 +156,17 @@ def test_encode_custom_json_encoders_model_pydanticv2(): assert jsonable_encoder(subclass_model) == {"dt_field": "2019-01-01T08:00:00+00:00"} -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -def test_encode_custom_json_encoders_model_pydanticv1(): - class ModelWithCustomEncoder(BaseModel): - dt_field: datetime +def test_json_encoder_error_with_pydanticv1(): + with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + from pydantic import v1 - class Config: - json_encoders = { - datetime: lambda dt: dt.replace( - microsecond=0, tzinfo=timezone.utc - ).isoformat() - } + class ModelV1(v1.BaseModel): + name: str - class ModelWithCustomEncoderSubclass(ModelWithCustomEncoder): - class Config: - pass - - model = ModelWithCustomEncoder(dt_field=datetime(2019, 1, 1, 8)) - assert jsonable_encoder(model) == {"dt_field": "2019-01-01T08:00:00+00:00"} - subclass_model = ModelWithCustomEncoderSubclass(dt_field=datetime(2019, 1, 1, 8)) - assert jsonable_encoder(subclass_model) == {"dt_field": "2019-01-01T08:00:00+00:00"} + data = ModelV1(name="test") + with pytest.raises(PydanticV1NotSupportedError): + jsonable_encoder(data) def test_encode_model_with_config(): @@ -205,23 +202,27 @@ def test_encode_model_with_default(): } -@needs_pydanticv1 def test_custom_encoders(): class safe_datetime(datetime): pass - class MyModel(BaseModel): + class MyDict(TypedDict): dt_field: safe_datetime - instance = MyModel(dt_field=safe_datetime.now()) + instance = MyDict(dt_field=safe_datetime.now()) encoded_instance = jsonable_encoder( instance, custom_encoder={safe_datetime: lambda o: o.strftime("%H:%M:%S")} ) - assert encoded_instance["dt_field"] == instance.dt_field.strftime("%H:%M:%S") + assert encoded_instance["dt_field"] == instance["dt_field"].strftime("%H:%M:%S") + + encoded_instance = jsonable_encoder( + instance, custom_encoder={datetime: lambda o: o.strftime("%H:%M:%S")} + ) + assert encoded_instance["dt_field"] == instance["dt_field"].strftime("%H:%M:%S") encoded_instance2 = jsonable_encoder(instance) - assert encoded_instance2["dt_field"] == instance.dt_field.isoformat() + assert encoded_instance2["dt_field"] == instance["dt_field"].isoformat() def test_custom_enum_encoders(): @@ -243,12 +244,7 @@ def test_encode_model_with_pure_path(): class ModelWithPath(BaseModel): path: PurePath - if PYDANTIC_V2: - model_config = {"arbitrary_types_allowed": True} - else: - - class Config: - arbitrary_types_allowed = True + model_config = {"arbitrary_types_allowed": True} test_path = PurePath("/foo", "bar") obj = ModelWithPath(path=test_path) @@ -259,12 +255,7 @@ def test_encode_model_with_pure_posix_path(): class ModelWithPath(BaseModel): path: PurePosixPath - if PYDANTIC_V2: - model_config = {"arbitrary_types_allowed": True} - else: - - class Config: - arbitrary_types_allowed = True + model_config = {"arbitrary_types_allowed": True} obj = ModelWithPath(path=PurePosixPath("/foo", "bar")) assert jsonable_encoder(obj) == {"path": "/foo/bar"} @@ -274,38 +265,40 @@ def test_encode_model_with_pure_windows_path(): class ModelWithPath(BaseModel): path: PureWindowsPath - if PYDANTIC_V2: - model_config = {"arbitrary_types_allowed": True} - else: - - class Config: - arbitrary_types_allowed = True + model_config = {"arbitrary_types_allowed": True} obj = ModelWithPath(path=PureWindowsPath("/foo", "bar")) assert jsonable_encoder(obj) == {"path": "\\foo\\bar"} -@needs_pydanticv1 -def test_encode_root(): - class ModelWithRoot(BaseModel): - __root__: str +def test_encode_pure_path(): + test_path = PurePath("/foo", "bar") - model = ModelWithRoot(__root__="Foo") - assert jsonable_encoder(model) == "Foo" + assert jsonable_encoder({"path": test_path}) == {"path": str(test_path)} -@needs_pydanticv2 def test_decimal_encoder_float(): data = {"value": Decimal(1.23)} assert jsonable_encoder(data) == {"value": 1.23} -@needs_pydanticv2 def test_decimal_encoder_int(): data = {"value": Decimal(2)} assert jsonable_encoder(data) == {"value": 2} +def test_decimal_encoder_nan(): + data = {"value": Decimal("NaN")} + assert isnan(jsonable_encoder(data)["value"]) + + +def test_decimal_encoder_infinity(): + data = {"value": Decimal("Infinity")} + assert isinf(jsonable_encoder(data)["value"]) + data = {"value": Decimal("-Infinity")} + assert isinf(jsonable_encoder(data)["value"]) + + def test_encode_deque_encodes_child_models(): class Model(BaseModel): test: str @@ -315,7 +308,6 @@ def test_encode_deque_encodes_child_models(): assert jsonable_encoder(dq)[0]["test"] == "test" -@needs_pydanticv2 def test_encode_pydantic_undefined(): data = {"value": Undefined} assert jsonable_encoder(data) == {"value": None} diff --git a/tests/test_multi_body_errors.py b/tests/test_multi_body_errors.py index 33304827a9..4418c77cb0 100644 --- a/tests/test_multi_body_errors.py +++ b/tests/test_multi_body_errors.py @@ -1,9 +1,9 @@ from decimal import Decimal -from typing import List -from dirty_equals import IsDict, IsOneOf +from dirty_equals import IsOneOf from fastapi import FastAPI from fastapi.testclient import TestClient +from inline_snapshot import snapshot from pydantic import BaseModel, condecimal app = FastAPI() @@ -15,7 +15,7 @@ class Item(BaseModel): @app.post("/items/") -def save_item_no_body(item: List[Item]): +def save_item_no_body(item: list[Item]): return {"item": item} @@ -25,109 +25,65 @@ client = TestClient(app) def test_put_correct_body(): response = client.post("/items/", json=[{"name": "Foo", "age": 5}]) assert response.status_code == 200, response.text - assert response.json() == { - "item": [ - { - "name": "Foo", - "age": IsOneOf( - 5, - # TODO: remove when deprecating Pydantic v1 - "5", - ), - } - ] - } + assert response.json() == snapshot( + { + "item": [ + { + "name": "Foo", + "age": "5", + } + ] + } + ) def test_jsonable_encoder_requiring_error(): response = client.post("/items/", json=[{"name": "Foo", "age": -1.0}]) assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "greater_than", - "loc": ["body", 0, "age"], - "msg": "Input should be greater than 0", - "input": -1.0, - "ctx": {"gt": 0}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "ctx": {"limit_value": 0.0}, - "loc": ["body", 0, "age"], - "msg": "ensure this value is greater than 0", - "type": "value_error.number.not_gt", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "greater_than", + "loc": ["body", 0, "age"], + "msg": "Input should be greater than 0", + "input": -1.0, + "ctx": {"gt": 0}, + } + ] + } def test_put_incorrect_body_multiple(): response = client.post("/items/", json=[{"age": "five"}, {"age": "six"}]) assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", 0, "name"], - "msg": "Field required", - "input": {"age": "five"}, - }, - { - "type": "decimal_parsing", - "loc": ["body", 0, "age"], - "msg": "Input should be a valid decimal", - "input": "five", - }, - { - "type": "missing", - "loc": ["body", 1, "name"], - "msg": "Field required", - "input": {"age": "six"}, - }, - { - "type": "decimal_parsing", - "loc": ["body", 1, "age"], - "msg": "Input should be a valid decimal", - "input": "six", - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", 0, "name"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", 0, "age"], - "msg": "value is not a valid decimal", - "type": "type_error.decimal", - }, - { - "loc": ["body", 1, "name"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", 1, "age"], - "msg": "value is not a valid decimal", - "type": "type_error.decimal", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", 0, "name"], + "msg": "Field required", + "input": {"age": "five"}, + }, + { + "type": "decimal_parsing", + "loc": ["body", 0, "age"], + "msg": "Input should be a valid decimal", + "input": "five", + }, + { + "type": "missing", + "loc": ["body", 1, "name"], + "msg": "Field required", + "input": {"age": "six"}, + }, + { + "type": "decimal_parsing", + "loc": ["body", 1, "age"], + "msg": "Input should be a valid decimal", + "input": "six", + }, + ] + } def test_openapi_schema(): @@ -180,31 +136,21 @@ def test_openapi_schema(): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [ - {"exclusiveMinimum": 0.0, "type": "number"}, - IsOneOf( - # pydantic < 2.12.0 - {"type": "string"}, - # pydantic >= 2.12.0 - { - "type": "string", - "pattern": r"^(?!^[-+.]*$)[+-]?0*\d*\.?\d*$", - }, - ), - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Age", - "exclusiveMinimum": 0.0, - "type": "number", - } - ), + "age": { + "title": "Age", + "anyOf": [ + {"exclusiveMinimum": 0.0, "type": "number"}, + IsOneOf( + # pydantic < 2.12.0 + {"type": "string"}, + # pydantic >= 2.12.0 + { + "type": "string", + "pattern": r"^(?!^[-+.]*$)[+-]?0*\d*\.?\d*$", + }, + ), + ], + }, }, }, "ValidationError": { diff --git a/tests/test_multi_query_errors.py b/tests/test_multi_query_errors.py index 8162d986c5..5df51ba185 100644 --- a/tests/test_multi_query_errors.py +++ b/tests/test_multi_query_errors.py @@ -1,6 +1,3 @@ -from typing import List - -from dirty_equals import IsDict from fastapi import FastAPI, Query from fastapi.testclient import TestClient @@ -8,7 +5,7 @@ app = FastAPI() @app.get("/items/") -def read_items(q: List[int] = Query(default=None)): +def read_items(q: list[int] = Query(default=None)): return {"q": q} @@ -24,40 +21,22 @@ def test_multi_query(): def test_multi_query_incorrect(): response = client.get("/items/?q=five&q=six") assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["query", "q", 0], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "five", - }, - { - "type": "int_parsing", - "loc": ["query", "q", 1], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "six", - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "q", 0], - "msg": "value is not a valid integer", - "type": "type_error.integer", - }, - { - "loc": ["query", "q", 1], - "msg": "value is not a valid integer", - "type": "type_error.integer", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "int_parsing", + "loc": ["query", "q", 0], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "five", + }, + { + "type": "int_parsing", + "loc": ["query", "q", 1], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "six", + }, + ] + } def test_openapi_schema(): diff --git a/tests/test_no_schema_split.py b/tests/test_no_schema_split.py index b0b5958c1b..131a3755e7 100644 --- a/tests/test_no_schema_split.py +++ b/tests/test_no_schema_split.py @@ -3,15 +3,12 @@ # Made an issue in: # https://github.com/fastapi/fastapi/issues/14247 from enum import Enum -from typing import List from fastapi import FastAPI from fastapi.testclient import TestClient from inline_snapshot import snapshot from pydantic import BaseModel, Field -from tests.utils import pydantic_snapshot - class MessageEventType(str, Enum): alpha = "alpha" @@ -25,7 +22,7 @@ class MessageEvent(BaseModel): class MessageOutput(BaseModel): body: str = "" - events: List[MessageEvent] = [] + events: list[MessageEvent] = [] class Message(BaseModel): @@ -127,47 +124,21 @@ def test_openapi_schema(): }, "MessageEvent": { "properties": { - "event_type": pydantic_snapshot( - v2=snapshot( - { - "$ref": "#/components/schemas/MessageEventType", - "default": "alpha", - } - ), - v1=snapshot( - { - "allOf": [ - { - "$ref": "#/components/schemas/MessageEventType" - } - ], - "default": "alpha", - } - ), - ), + "event_type": { + "$ref": "#/components/schemas/MessageEventType", + "default": "alpha", + }, "output": {"type": "string", "title": "Output"}, }, "type": "object", "required": ["output"], "title": "MessageEvent", }, - "MessageEventType": pydantic_snapshot( - v2=snapshot( - { - "type": "string", - "enum": ["alpha", "beta"], - "title": "MessageEventType", - } - ), - v1=snapshot( - { - "type": "string", - "enum": ["alpha", "beta"], - "title": "MessageEventType", - "description": "An enumeration.", - } - ), - ), + "MessageEventType": { + "type": "string", + "enum": ["alpha", "beta"], + "title": "MessageEventType", + }, "MessageOutput": { "properties": { "body": {"type": "string", "title": "Body", "default": ""}, diff --git a/tests/test_openapi_examples.py b/tests/test_openapi_examples.py index b3f83ae237..bd0d55452e 100644 --- a/tests/test_openapi_examples.py +++ b/tests/test_openapi_examples.py @@ -1,6 +1,5 @@ from typing import Union -from dirty_equals import IsDict from fastapi import Body, Cookie, FastAPI, Header, Path, Query from fastapi.testclient import TestClient from pydantic import BaseModel @@ -155,26 +154,12 @@ def test_openapi_schema(): "requestBody": { "content": { "application/json": { - "schema": IsDict( - { - "$ref": "#/components/schemas/Item", - "examples": [ - {"data": "Data in Body examples, example1"} - ], - } - ) - | IsDict( - { - # TODO: remove when deprecating Pydantic v1 - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - "title": "Item", - "examples": [ - {"data": "Data in Body examples, example1"} - ], - } - ), + "schema": { + "$ref": "#/components/schemas/Item", + "examples": [ + {"data": "Data in Body examples, example1"} + ], + }, "examples": { "Example One": { "summary": "Example One Summary", @@ -265,27 +250,14 @@ def test_openapi_schema(): "name": "data", "in": "query", "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "examples": [ - "json_schema_query1", - "json_schema_query2", - ], - "title": "Data", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "examples": [ - "json_schema_query1", - "json_schema_query2", - ], - "type": "string", - "title": "Data", - } - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "examples": [ + "json_schema_query1", + "json_schema_query2", + ], + "title": "Data", + }, "examples": { "Query One": { "summary": "Query One Summary", @@ -323,27 +295,14 @@ def test_openapi_schema(): "name": "data", "in": "header", "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "examples": [ - "json_schema_header1", - "json_schema_header2", - ], - "title": "Data", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "string", - "examples": [ - "json_schema_header1", - "json_schema_header2", - ], - "title": "Data", - } - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "examples": [ + "json_schema_header1", + "json_schema_header2", + ], + "title": "Data", + }, "examples": { "Header One": { "summary": "Header One Summary", @@ -381,27 +340,14 @@ def test_openapi_schema(): "name": "data", "in": "cookie", "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "examples": [ - "json_schema_cookie1", - "json_schema_cookie2", - ], - "title": "Data", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "string", - "examples": [ - "json_schema_cookie1", - "json_schema_cookie2", - ], - "title": "Data", - } - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "examples": [ + "json_schema_cookie1", + "json_schema_cookie2", + ], + "title": "Data", + }, "examples": { "Cookie One": { "summary": "Cookie One Summary", diff --git a/tests/test_openapi_query_parameter_extension.py b/tests/test_openapi_query_parameter_extension.py index dc7147c712..084cb695d4 100644 --- a/tests/test_openapi_query_parameter_extension.py +++ b/tests/test_openapi_query_parameter_extension.py @@ -1,6 +1,5 @@ from typing import Optional -from dirty_equals import IsDict from fastapi import FastAPI from fastapi.testclient import TestClient @@ -53,21 +52,11 @@ def test_openapi(): "parameters": [ { "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "integer"}, {"type": "null"}], - "default": 50, - "title": "Standard Query Param", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Standard Query Param", - "type": "integer", - "default": 50, - } - ), + "schema": { + "anyOf": [{"type": "integer"}, {"type": "null"}], + "default": 50, + "title": "Standard Query Param", + }, "name": "standard_query_param", "in": "query", }, diff --git a/tests/test_openapi_schema_type.py b/tests/test_openapi_schema_type.py index a45ea20c8c..98d9787455 100644 --- a/tests/test_openapi_schema_type.py +++ b/tests/test_openapi_schema_type.py @@ -1,4 +1,4 @@ -from typing import List, Optional, Union +from typing import Optional, Union import pytest from fastapi.openapi.models import Schema, SchemaType @@ -13,7 +13,7 @@ from fastapi.openapi.models import Schema, SchemaType ], ) def test_allowed_schema_type( - type_value: Optional[Union[SchemaType, List[SchemaType]]], + type_value: Optional[Union[SchemaType, list[SchemaType]]], ) -> None: """Test that Schema accepts SchemaType, List[SchemaType] and None for type field.""" schema = Schema(type=type_value) diff --git a/tests/test_openapi_separate_input_output_schemas.py b/tests/test_openapi_separate_input_output_schemas.py index fa73620eae..1891f0bde0 100644 --- a/tests/test_openapi_separate_input_output_schemas.py +++ b/tests/test_openapi_separate_input_output_schemas.py @@ -1,27 +1,32 @@ -from typing import List, Optional +from typing import Optional from fastapi import FastAPI from fastapi.testclient import TestClient from inline_snapshot import snapshot -from pydantic import BaseModel - -from .utils import PYDANTIC_V2, needs_pydanticv2 +from pydantic import BaseModel, computed_field class SubItem(BaseModel): subname: str sub_description: Optional[str] = None - tags: List[str] = [] - if PYDANTIC_V2: - model_config = {"json_schema_serialization_defaults_required": True} + tags: list[str] = [] + model_config = {"json_schema_serialization_defaults_required": True} class Item(BaseModel): name: str description: Optional[str] = None sub: Optional[SubItem] = None - if PYDANTIC_V2: - model_config = {"json_schema_serialization_defaults_required": True} + model_config = {"json_schema_serialization_defaults_required": True} + + +class WithComputedField(BaseModel): + name: str + + @computed_field + @property + def computed_field(self) -> str: + return f"computed {self.name}" def get_app_client(separate_input_output_schemas: bool = True) -> TestClient: @@ -32,11 +37,11 @@ def get_app_client(separate_input_output_schemas: bool = True) -> TestClient: return item @app.post("/items-list/") - def create_item_list(item: List[Item]): + def create_item_list(item: list[Item]): return item @app.get("/items/") - def read_items() -> List[Item]: + def read_items() -> list[Item]: return [ Item( name="Portal Gun", @@ -46,6 +51,12 @@ def get_app_client(separate_input_output_schemas: bool = True) -> TestClient: Item(name="Plumbus"), ] + @app.post("/with-computed-field/") + def create_with_computed_field( + with_computed_field: WithComputedField, + ) -> WithComputedField: + return with_computed_field + client = TestClient(app) return client @@ -131,7 +142,22 @@ def test_read_items(): ) -@needs_pydanticv2 +def test_with_computed_field(): + client = get_app_client() + client_no = get_app_client(separate_input_output_schemas=False) + response = client.post("/with-computed-field/", json={"name": "example"}) + response2 = client_no.post("/with-computed-field/", json={"name": "example"}) + assert response.status_code == response2.status_code == 200, response.text + assert ( + response.json() + == response2.json() + == { + "name": "example", + "computed_field": "computed example", + } + ) + + def test_openapi_schema(): client = get_app_client() response = client.get("/openapi.json") @@ -245,6 +271,44 @@ def test_openapi_schema(): }, } }, + "/with-computed-field/": { + "post": { + "summary": "Create With Computed Field", + "operationId": "create_with_computed_field_with_computed_field__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WithComputedField-Input" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WithComputedField-Output" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, }, "components": { "schemas": { @@ -333,6 +397,25 @@ def test_openapi_schema(): "required": ["subname", "sub_description", "tags"], "title": "SubItem", }, + "WithComputedField-Input": { + "properties": {"name": {"type": "string", "title": "Name"}}, + "type": "object", + "required": ["name"], + "title": "WithComputedField", + }, + "WithComputedField-Output": { + "properties": { + "name": {"type": "string", "title": "Name"}, + "computed_field": { + "type": "string", + "title": "Computed Field", + "readOnly": True, + }, + }, + "type": "object", + "required": ["name", "computed_field"], + "title": "WithComputedField", + }, "ValidationError": { "properties": { "loc": { @@ -355,7 +438,6 @@ def test_openapi_schema(): ) -@needs_pydanticv2 def test_openapi_schema_no_separate(): client = get_app_client(separate_input_output_schemas=False) response = client.get("/openapi.json") @@ -458,6 +540,44 @@ def test_openapi_schema_no_separate(): }, } }, + "/with-computed-field/": { + "post": { + "summary": "Create With Computed Field", + "operationId": "create_with_computed_field_with_computed_field__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WithComputedField-Input" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WithComputedField-Output" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, }, "components": { "schemas": { @@ -508,6 +628,25 @@ def test_openapi_schema_no_separate(): "required": ["subname"], "title": "SubItem", }, + "WithComputedField-Input": { + "properties": {"name": {"type": "string", "title": "Name"}}, + "type": "object", + "required": ["name"], + "title": "WithComputedField", + }, + "WithComputedField-Output": { + "properties": { + "name": {"type": "string", "title": "Name"}, + "computed_field": { + "type": "string", + "title": "Computed Field", + "readOnly": True, + }, + }, + "type": "object", + "required": ["name", "computed_field"], + "title": "WithComputedField", + }, "ValidationError": { "properties": { "loc": { diff --git a/tests/test_openapi_servers.py b/tests/test_openapi_servers.py index 8697c8438b..33079e4b1d 100644 --- a/tests/test_openapi_servers.py +++ b/tests/test_openapi_servers.py @@ -1,6 +1,6 @@ -from dirty_equals import IsOneOf from fastapi import FastAPI from fastapi.testclient import TestClient +from inline_snapshot import snapshot app = FastAPI( servers=[ @@ -30,39 +30,31 @@ def test_app(): def test_openapi_schema(): 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"}, - "servers": [ - {"url": "/", "description": "Default, relative server"}, - { - "url": IsOneOf( - "http://staging.localhost.tiangolo.com:8000/", - # TODO: remove when deprecating Pydantic v1 - "http://staging.localhost.tiangolo.com:8000", - ), - "description": "Staging but actually localhost still", - }, - { - "url": IsOneOf( - "https://prod.example.com/", - # TODO: remove when deprecating Pydantic v1 - "https://prod.example.com", - ) - }, - ], - "paths": { - "/foo": { - "get": { - "summary": "Foo", - "operationId": "foo_foo_get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "servers": [ + {"url": "/", "description": "Default, relative server"}, + { + "url": "http://staging.localhost.tiangolo.com:8000", + "description": "Staging but actually localhost still", + }, + {"url": "https://prod.example.com"}, + ], + "paths": { + "/foo": { + "get": { + "summary": "Foo", + "operationId": "foo_foo_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } } - } - }, - } + }, + } + ) diff --git a/tests/test_optional_file_list.py b/tests/test_optional_file_list.py new file mode 100644 index 0000000000..6860258643 --- /dev/null +++ b/tests/test_optional_file_list.py @@ -0,0 +1,30 @@ +from typing import Optional + +from fastapi import FastAPI, File +from fastapi.testclient import TestClient + +app = FastAPI() + + +@app.post("/files") +async def upload_files(files: Optional[list[bytes]] = File(None)): + if files is None: + return {"files_count": 0} + return {"files_count": len(files), "sizes": [len(f) for f in files]} + + +def test_optional_bytes_list(): + client = TestClient(app) + response = client.post( + "/files", + files=[("files", b"content1"), ("files", b"content2")], + ) + assert response.status_code == 200 + assert response.json() == {"files_count": 2, "sizes": [8, 8]} + + +def test_optional_bytes_list_no_files(): + client = TestClient(app) + response = client.post("/files") + assert response.status_code == 200 + assert response.json() == {"files_count": 0} diff --git a/tests/test_params_repr.py b/tests/test_params_repr.py index baa172497d..670e4f5ddf 100644 --- a/tests/test_params_repr.py +++ b/tests/test_params_repr.py @@ -1,9 +1,8 @@ -from typing import Any, List +from typing import Any -from dirty_equals import IsOneOf from fastapi.params import Body, Cookie, Header, Param, Path, Query -test_data: List[Any] = ["teststr", None, ..., 1, []] +test_data: list[Any] = ["teststr", None, ..., 1, []] def get_user(): @@ -19,11 +18,7 @@ def test_param_repr_none(): def test_param_repr_ellipsis(): - assert repr(Param(...)) == IsOneOf( - "Param(PydanticUndefined)", - # TODO: remove when deprecating Pydantic v1 - "Param(Ellipsis)", - ) + assert repr(Param(...)) == "Param(PydanticUndefined)" def test_param_repr_number(): @@ -35,16 +30,8 @@ def test_param_repr_list(): def test_path_repr(): - assert repr(Path()) == IsOneOf( - "Path(PydanticUndefined)", - # TODO: remove when deprecating Pydantic v1 - "Path(Ellipsis)", - ) - assert repr(Path(...)) == IsOneOf( - "Path(PydanticUndefined)", - # TODO: remove when deprecating Pydantic v1 - "Path(Ellipsis)", - ) + assert repr(Path()) == "Path(PydanticUndefined)" + assert repr(Path(...)) == "Path(PydanticUndefined)" def test_query_repr_str(): @@ -56,11 +43,7 @@ def test_query_repr_none(): def test_query_repr_ellipsis(): - assert repr(Query(...)) == IsOneOf( - "Query(PydanticUndefined)", - # TODO: remove when deprecating Pydantic v1 - "Query(Ellipsis)", - ) + assert repr(Query(...)) == "Query(PydanticUndefined)" def test_query_repr_number(): @@ -80,11 +63,7 @@ def test_header_repr_none(): def test_header_repr_ellipsis(): - assert repr(Header(...)) == IsOneOf( - "Header(PydanticUndefined)", - # TODO: remove when deprecating Pydantic v1 - "Header(Ellipsis)", - ) + assert repr(Header(...)) == "Header(PydanticUndefined)" def test_header_repr_number(): @@ -104,11 +83,7 @@ def test_cookie_repr_none(): def test_cookie_repr_ellipsis(): - assert repr(Cookie(...)) == IsOneOf( - "Cookie(PydanticUndefined)", - # TODO: remove when deprecating Pydantic v1 - "Cookie(Ellipsis)", - ) + assert repr(Cookie(...)) == "Cookie(PydanticUndefined)" def test_cookie_repr_number(): @@ -128,11 +103,7 @@ def test_body_repr_none(): def test_body_repr_ellipsis(): - assert repr(Body(...)) == IsOneOf( - "Body(PydanticUndefined)", - # TODO: remove when deprecating Pydantic v1 - "Body(Ellipsis)", - ) + assert repr(Body(...)) == "Body(PydanticUndefined)" def test_body_repr_number(): diff --git a/tests/test_path.py b/tests/test_path.py index 09c1f13fb1..47051b927c 100644 --- a/tests/test_path.py +++ b/tests/test_path.py @@ -1,4 +1,3 @@ -from dirty_equals import IsDict from fastapi.testclient import TestClient from .main import app @@ -45,57 +44,31 @@ def test_path_str_True(): def test_path_int_foobar(): response = client.get("/path/int/foobar") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["path", "item_id"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "foobar", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "int_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "foobar", + } + ] + } def test_path_int_True(): response = client.get("/path/int/True") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["path", "item_id"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "True", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "int_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "True", + } + ] + } def test_path_int_42(): @@ -107,85 +80,46 @@ def test_path_int_42(): def test_path_int_42_5(): response = client.get("/path/int/42.5") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["path", "item_id"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "42.5", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "int_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "42.5", + } + ] + } def test_path_float_foobar(): response = client.get("/path/float/foobar") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "float_parsing", - "loc": ["path", "item_id"], - "msg": "Input should be a valid number, unable to parse string as a number", - "input": "foobar", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "value is not a valid float", - "type": "type_error.float", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "float_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid number, unable to parse string as a number", + "input": "foobar", + } + ] + } def test_path_float_True(): response = client.get("/path/float/True") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "float_parsing", - "loc": ["path", "item_id"], - "msg": "Input should be a valid number, unable to parse string as a number", - "input": "True", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "value is not a valid float", - "type": "type_error.float", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "float_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid number, unable to parse string as a number", + "input": "True", + } + ] + } def test_path_float_42(): @@ -203,29 +137,16 @@ def test_path_float_42_5(): def test_path_bool_foobar(): response = client.get("/path/bool/foobar") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "bool_parsing", - "loc": ["path", "item_id"], - "msg": "Input should be a valid boolean, unable to interpret input", - "input": "foobar", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "value could not be parsed to a boolean", - "type": "type_error.bool", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "bool_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid boolean, unable to interpret input", + "input": "foobar", + } + ] + } def test_path_bool_True(): @@ -237,57 +158,31 @@ def test_path_bool_True(): def test_path_bool_42(): response = client.get("/path/bool/42") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "bool_parsing", - "loc": ["path", "item_id"], - "msg": "Input should be a valid boolean, unable to interpret input", - "input": "42", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "value could not be parsed to a boolean", - "type": "type_error.bool", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "bool_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid boolean, unable to interpret input", + "input": "42", + } + ] + } def test_path_bool_42_5(): response = client.get("/path/bool/42.5") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "bool_parsing", - "loc": ["path", "item_id"], - "msg": "Input should be a valid boolean, unable to interpret input", - "input": "42.5", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "value could not be parsed to a boolean", - "type": "type_error.bool", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "bool_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid boolean, unable to interpret input", + "input": "42.5", + } + ] + } def test_path_bool_1(): @@ -335,31 +230,17 @@ def test_path_param_minlength_foo(): def test_path_param_minlength_fo(): response = client.get("/path/param-minlength/fo") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "string_too_short", - "loc": ["path", "item_id"], - "msg": "String should have at least 3 characters", - "input": "fo", - "ctx": {"min_length": 3}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "ensure this value has at least 3 characters", - "type": "value_error.any_str.min_length", - "ctx": {"limit_value": 3}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "string_too_short", + "loc": ["path", "item_id"], + "msg": "String should have at least 3 characters", + "input": "fo", + "ctx": {"min_length": 3}, + } + ] + } def test_path_param_maxlength_foo(): @@ -371,31 +252,17 @@ def test_path_param_maxlength_foo(): def test_path_param_maxlength_foobar(): response = client.get("/path/param-maxlength/foobar") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "string_too_long", - "loc": ["path", "item_id"], - "msg": "String should have at most 3 characters", - "input": "foobar", - "ctx": {"max_length": 3}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "ensure this value has at most 3 characters", - "type": "value_error.any_str.max_length", - "ctx": {"limit_value": 3}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "string_too_long", + "loc": ["path", "item_id"], + "msg": "String should have at most 3 characters", + "input": "foobar", + "ctx": {"max_length": 3}, + } + ] + } def test_path_param_min_maxlength_foo(): @@ -407,60 +274,33 @@ def test_path_param_min_maxlength_foo(): def test_path_param_min_maxlength_foobar(): response = client.get("/path/param-min_maxlength/foobar") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "string_too_long", - "loc": ["path", "item_id"], - "msg": "String should have at most 3 characters", - "input": "foobar", - "ctx": {"max_length": 3}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "ensure this value has at most 3 characters", - "type": "value_error.any_str.max_length", - "ctx": {"limit_value": 3}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "string_too_long", + "loc": ["path", "item_id"], + "msg": "String should have at most 3 characters", + "input": "foobar", + "ctx": {"max_length": 3}, + } + ] + } def test_path_param_min_maxlength_f(): response = client.get("/path/param-min_maxlength/f") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "string_too_short", - "loc": ["path", "item_id"], - "msg": "String should have at least 2 characters", - "input": "f", - "ctx": {"min_length": 2}, - } - ] - } - ) | IsDict( - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "ensure this value has at least 2 characters", - "type": "value_error.any_str.min_length", - "ctx": {"limit_value": 2}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "string_too_short", + "loc": ["path", "item_id"], + "msg": "String should have at least 2 characters", + "input": "f", + "ctx": {"min_length": 2}, + } + ] + } def test_path_param_gt_42(): @@ -472,31 +312,17 @@ def test_path_param_gt_42(): def test_path_param_gt_2(): response = client.get("/path/param-gt/2") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "greater_than", - "loc": ["path", "item_id"], - "msg": "Input should be greater than 3", - "input": "2", - "ctx": {"gt": 3.0}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "ensure this value is greater than 3", - "type": "value_error.number.not_gt", - "ctx": {"limit_value": 3}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "greater_than", + "loc": ["path", "item_id"], + "msg": "Input should be greater than 3", + "input": "2", + "ctx": {"gt": 3.0}, + } + ] + } def test_path_param_gt0_0_05(): @@ -508,31 +334,17 @@ def test_path_param_gt0_0_05(): def test_path_param_gt0_0(): response = client.get("/path/param-gt0/0") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "greater_than", - "loc": ["path", "item_id"], - "msg": "Input should be greater than 0", - "input": "0", - "ctx": {"gt": 0.0}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "ensure this value is greater than 0", - "type": "value_error.number.not_gt", - "ctx": {"limit_value": 0}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "greater_than", + "loc": ["path", "item_id"], + "msg": "Input should be greater than 0", + "input": "0", + "ctx": {"gt": 0.0}, + } + ] + } def test_path_param_ge_42(): @@ -550,61 +362,33 @@ def test_path_param_ge_3(): def test_path_param_ge_2(): response = client.get("/path/param-ge/2") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "greater_than_equal", - "loc": ["path", "item_id"], - "msg": "Input should be greater than or equal to 3", - "input": "2", - "ctx": {"ge": 3.0}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "ensure this value is greater than or equal to 3", - "type": "value_error.number.not_ge", - "ctx": {"limit_value": 3}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "greater_than_equal", + "loc": ["path", "item_id"], + "msg": "Input should be greater than or equal to 3", + "input": "2", + "ctx": {"ge": 3.0}, + } + ] + } def test_path_param_lt_42(): response = client.get("/path/param-lt/42") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "less_than", - "loc": ["path", "item_id"], - "msg": "Input should be less than 3", - "input": "42", - "ctx": {"lt": 3.0}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "ensure this value is less than 3", - "type": "value_error.number.not_lt", - "ctx": {"limit_value": 3}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "less_than", + "loc": ["path", "item_id"], + "msg": "Input should be less than 3", + "input": "42", + "ctx": {"lt": 3.0}, + } + ] + } def test_path_param_lt_2(): @@ -622,61 +406,33 @@ def test_path_param_lt0__1(): def test_path_param_lt0_0(): response = client.get("/path/param-lt0/0") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "less_than", - "loc": ["path", "item_id"], - "msg": "Input should be less than 0", - "input": "0", - "ctx": {"lt": 0.0}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "ensure this value is less than 0", - "type": "value_error.number.not_lt", - "ctx": {"limit_value": 0}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "less_than", + "loc": ["path", "item_id"], + "msg": "Input should be less than 0", + "input": "0", + "ctx": {"lt": 0.0}, + } + ] + } def test_path_param_le_42(): response = client.get("/path/param-le/42") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "less_than_equal", - "loc": ["path", "item_id"], - "msg": "Input should be less than or equal to 3", - "input": "42", - "ctx": {"le": 3.0}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "ensure this value is less than or equal to 3", - "type": "value_error.number.not_le", - "ctx": {"limit_value": 3}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "less_than_equal", + "loc": ["path", "item_id"], + "msg": "Input should be less than or equal to 3", + "input": "42", + "ctx": {"le": 3.0}, + } + ] + } def test_path_param_le_3(): @@ -700,61 +456,33 @@ def test_path_param_lt_gt_2(): def test_path_param_lt_gt_4(): response = client.get("/path/param-lt-gt/4") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "less_than", - "loc": ["path", "item_id"], - "msg": "Input should be less than 3", - "input": "4", - "ctx": {"lt": 3.0}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "ensure this value is less than 3", - "type": "value_error.number.not_lt", - "ctx": {"limit_value": 3}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "less_than", + "loc": ["path", "item_id"], + "msg": "Input should be less than 3", + "input": "4", + "ctx": {"lt": 3.0}, + } + ] + } def test_path_param_lt_gt_0(): response = client.get("/path/param-lt-gt/0") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "greater_than", - "loc": ["path", "item_id"], - "msg": "Input should be greater than 1", - "input": "0", - "ctx": {"gt": 1.0}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "ensure this value is greater than 1", - "type": "value_error.number.not_gt", - "ctx": {"limit_value": 1}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "greater_than", + "loc": ["path", "item_id"], + "msg": "Input should be greater than 1", + "input": "0", + "ctx": {"gt": 1.0}, + } + ] + } def test_path_param_le_ge_2(): @@ -777,31 +505,17 @@ def test_path_param_le_ge_3(): def test_path_param_le_ge_4(): response = client.get("/path/param-le-ge/4") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "less_than_equal", - "loc": ["path", "item_id"], - "msg": "Input should be less than or equal to 3", - "input": "4", - "ctx": {"le": 3.0}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "ensure this value is less than or equal to 3", - "type": "value_error.number.not_le", - "ctx": {"limit_value": 3}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "less_than_equal", + "loc": ["path", "item_id"], + "msg": "Input should be less than or equal to 3", + "input": "4", + "ctx": {"le": 3.0}, + } + ] + } def test_path_param_lt_int_2(): @@ -813,59 +527,32 @@ def test_path_param_lt_int_2(): def test_path_param_lt_int_42(): response = client.get("/path/param-lt-int/42") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "less_than", - "loc": ["path", "item_id"], - "msg": "Input should be less than 3", - "input": "42", - "ctx": {"lt": 3}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "ensure this value is less than 3", - "type": "value_error.number.not_lt", - "ctx": {"limit_value": 3}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "less_than", + "loc": ["path", "item_id"], + "msg": "Input should be less than 3", + "input": "42", + "ctx": {"lt": 3}, + } + ] + } def test_path_param_lt_int_2_7(): response = client.get("/path/param-lt-int/2.7") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["path", "item_id"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "2.7", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "int_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "2.7", + } + ] + } def test_path_param_gt_int_42(): @@ -877,89 +564,48 @@ def test_path_param_gt_int_42(): def test_path_param_gt_int_2(): response = client.get("/path/param-gt-int/2") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "greater_than", - "loc": ["path", "item_id"], - "msg": "Input should be greater than 3", - "input": "2", - "ctx": {"gt": 3}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "ensure this value is greater than 3", - "type": "value_error.number.not_gt", - "ctx": {"limit_value": 3}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "greater_than", + "loc": ["path", "item_id"], + "msg": "Input should be greater than 3", + "input": "2", + "ctx": {"gt": 3}, + } + ] + } def test_path_param_gt_int_2_7(): response = client.get("/path/param-gt-int/2.7") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["path", "item_id"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "2.7", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "int_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "2.7", + } + ] + } def test_path_param_le_int_42(): response = client.get("/path/param-le-int/42") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "less_than_equal", - "loc": ["path", "item_id"], - "msg": "Input should be less than or equal to 3", - "input": "42", - "ctx": {"le": 3}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "ensure this value is less than or equal to 3", - "type": "value_error.number.not_le", - "ctx": {"limit_value": 3}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "less_than_equal", + "loc": ["path", "item_id"], + "msg": "Input should be less than or equal to 3", + "input": "42", + "ctx": {"le": 3}, + } + ] + } def test_path_param_le_int_3(): @@ -977,29 +623,16 @@ def test_path_param_le_int_2(): def test_path_param_le_int_2_7(): response = client.get("/path/param-le-int/2.7") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["path", "item_id"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "2.7", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "int_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "2.7", + } + ] + } def test_path_param_ge_int_42(): @@ -1017,59 +650,32 @@ def test_path_param_ge_int_3(): def test_path_param_ge_int_2(): response = client.get("/path/param-ge-int/2") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "greater_than_equal", - "loc": ["path", "item_id"], - "msg": "Input should be greater than or equal to 3", - "input": "2", - "ctx": {"ge": 3}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "ensure this value is greater than or equal to 3", - "type": "value_error.number.not_ge", - "ctx": {"limit_value": 3}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "greater_than_equal", + "loc": ["path", "item_id"], + "msg": "Input should be greater than or equal to 3", + "input": "2", + "ctx": {"ge": 3}, + } + ] + } def test_path_param_ge_int_2_7(): response = client.get("/path/param-ge-int/2.7") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["path", "item_id"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "2.7", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "int_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "2.7", + } + ] + } def test_path_param_lt_gt_int_2(): @@ -1081,89 +687,48 @@ def test_path_param_lt_gt_int_2(): def test_path_param_lt_gt_int_4(): response = client.get("/path/param-lt-gt-int/4") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "less_than", - "loc": ["path", "item_id"], - "msg": "Input should be less than 3", - "input": "4", - "ctx": {"lt": 3}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "ensure this value is less than 3", - "type": "value_error.number.not_lt", - "ctx": {"limit_value": 3}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "less_than", + "loc": ["path", "item_id"], + "msg": "Input should be less than 3", + "input": "4", + "ctx": {"lt": 3}, + } + ] + } def test_path_param_lt_gt_int_0(): response = client.get("/path/param-lt-gt-int/0") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "greater_than", - "loc": ["path", "item_id"], - "msg": "Input should be greater than 1", - "input": "0", - "ctx": {"gt": 1}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "ensure this value is greater than 1", - "type": "value_error.number.not_gt", - "ctx": {"limit_value": 1}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "greater_than", + "loc": ["path", "item_id"], + "msg": "Input should be greater than 1", + "input": "0", + "ctx": {"gt": 1}, + } + ] + } def test_path_param_lt_gt_int_2_7(): response = client.get("/path/param-lt-gt-int/2.7") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["path", "item_id"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "2.7", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "int_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "2.7", + } + ] + } def test_path_param_le_ge_int_2(): @@ -1187,56 +752,29 @@ def test_path_param_le_ge_int_3(): def test_path_param_le_ge_int_4(): response = client.get("/path/param-le-ge-int/4") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "less_than_equal", - "loc": ["path", "item_id"], - "msg": "Input should be less than or equal to 3", - "input": "4", - "ctx": {"le": 3}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "ensure this value is less than or equal to 3", - "type": "value_error.number.not_le", - "ctx": {"limit_value": 3}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "less_than_equal", + "loc": ["path", "item_id"], + "msg": "Input should be less than or equal to 3", + "input": "4", + "ctx": {"le": 3}, + } + ] + } def test_path_param_le_ge_int_2_7(): response = client.get("/path/param-le-ge-int/2.7") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["path", "item_id"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "2.7", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "int_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "2.7", + } + ] + } diff --git a/tests/test_pydantic_v1_error.py b/tests/test_pydantic_v1_error.py new file mode 100644 index 0000000000..13229a3137 --- /dev/null +++ b/tests/test_pydantic_v1_error.py @@ -0,0 +1,97 @@ +import sys +import warnings +from typing import Union + +import pytest + +from tests.utils import skip_module_if_py_gte_314 + +if sys.version_info >= (3, 14): + skip_module_if_py_gte_314() + +from fastapi import FastAPI +from fastapi.exceptions import PydanticV1NotSupportedError + +with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + from pydantic.v1 import BaseModel + + +def test_raises_pydantic_v1_model_in_endpoint_param() -> None: + class ParamModelV1(BaseModel): + name: str + + app = FastAPI() + + with pytest.raises(PydanticV1NotSupportedError): + + @app.post("/param") + def endpoint(data: ParamModelV1): # pragma: no cover + return data + + +def test_raises_pydantic_v1_model_in_return_type() -> None: + class ReturnModelV1(BaseModel): + name: str + + app = FastAPI() + + with pytest.raises(PydanticV1NotSupportedError): + + @app.get("/return") + def endpoint() -> ReturnModelV1: # pragma: no cover + return ReturnModelV1(name="test") + + +def test_raises_pydantic_v1_model_in_response_model() -> None: + class ResponseModelV1(BaseModel): + name: str + + app = FastAPI() + + with pytest.raises(PydanticV1NotSupportedError): + + @app.get("/response-model", response_model=ResponseModelV1) + def endpoint(): # pragma: no cover + return {"name": "test"} + + +def test_raises_pydantic_v1_model_in_additional_responses_model() -> None: + class ErrorModelV1(BaseModel): + detail: str + + app = FastAPI() + + with pytest.raises(PydanticV1NotSupportedError): + + @app.get( + "/responses", response_model=None, responses={400: {"model": ErrorModelV1}} + ) + def endpoint(): # pragma: no cover + return {"ok": True} + + +def test_raises_pydantic_v1_model_in_union() -> None: + class ModelV1A(BaseModel): + name: str + + app = FastAPI() + + with pytest.raises(PydanticV1NotSupportedError): + + @app.post("/union") + def endpoint(data: Union[dict, ModelV1A]): # pragma: no cover + return data + + +def test_raises_pydantic_v1_model_in_sequence() -> None: + class ModelV1A(BaseModel): + name: str + + app = FastAPI() + + with pytest.raises(PydanticV1NotSupportedError): + + @app.post("/sequence") + def endpoint(data: list[ModelV1A]): # pragma: no cover + return data diff --git a/tests/test_pydantic_v1_v2_01.py b/tests/test_pydantic_v1_v2_01.py deleted file mode 100644 index 769e5fab62..0000000000 --- a/tests/test_pydantic_v1_v2_01.py +++ /dev/null @@ -1,475 +0,0 @@ -import sys -from typing import Any, List, Union - -from tests.utils import pydantic_snapshot, skip_module_if_py_gte_314 - -if sys.version_info >= (3, 14): - skip_module_if_py_gte_314() - -from fastapi import FastAPI -from fastapi._compat.v1 import BaseModel -from fastapi.testclient import TestClient -from inline_snapshot import snapshot - - -class SubItem(BaseModel): - name: str - - -class Item(BaseModel): - title: str - size: int - description: Union[str, None] = None - sub: SubItem - multi: List[SubItem] = [] - - -app = FastAPI() - - -@app.post("/simple-model") -def handle_simple_model(data: SubItem) -> SubItem: - return data - - -@app.post("/simple-model-filter", response_model=SubItem) -def handle_simple_model_filter(data: SubItem) -> Any: - extended_data = data.dict() - extended_data.update({"secret_price": 42}) - return extended_data - - -@app.post("/item") -def handle_item(data: Item) -> Item: - return data - - -@app.post("/item-filter", response_model=Item) -def handle_item_filter(data: Item) -> Any: - extended_data = data.dict() - extended_data.update({"secret_data": "classified", "internal_id": 12345}) - extended_data["sub"].update({"internal_id": 67890}) - return extended_data - - -client = TestClient(app) - - -def test_old_simple_model(): - response = client.post( - "/simple-model", - json={"name": "Foo"}, - ) - assert response.status_code == 200, response.text - assert response.json() == {"name": "Foo"} - - -def test_old_simple_model_validation_error(): - response = client.post( - "/simple-model", - json={"wrong_name": "Foo"}, - ) - assert response.status_code == 422, response.text - assert response.json() == snapshot( - { - "detail": [ - { - "loc": ["body", "name"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) - - -def test_old_simple_model_filter(): - response = client.post( - "/simple-model-filter", - json={"name": "Foo"}, - ) - assert response.status_code == 200, response.text - assert response.json() == {"name": "Foo"} - - -def test_item_model(): - response = client.post( - "/item", - json={ - "title": "Test Item", - "size": 100, - "description": "This is a test item", - "sub": {"name": "SubItem1"}, - "multi": [{"name": "Multi1"}, {"name": "Multi2"}], - }, - ) - assert response.status_code == 200, response.text - assert response.json() == { - "title": "Test Item", - "size": 100, - "description": "This is a test item", - "sub": {"name": "SubItem1"}, - "multi": [{"name": "Multi1"}, {"name": "Multi2"}], - } - - -def test_item_model_minimal(): - response = client.post( - "/item", - json={"title": "Minimal Item", "size": 50, "sub": {"name": "SubMin"}}, - ) - assert response.status_code == 200, response.text - assert response.json() == { - "title": "Minimal Item", - "size": 50, - "description": None, - "sub": {"name": "SubMin"}, - "multi": [], - } - - -def test_item_model_validation_errors(): - response = client.post( - "/item", - json={"title": "Missing fields"}, - ) - assert response.status_code == 422, response.text - error_detail = response.json()["detail"] - assert len(error_detail) == 2 - assert { - "loc": ["body", "size"], - "msg": "field required", - "type": "value_error.missing", - } in error_detail - assert { - "loc": ["body", "sub"], - "msg": "field required", - "type": "value_error.missing", - } in error_detail - - -def test_item_model_nested_validation_error(): - response = client.post( - "/item", - json={"title": "Test Item", "size": 100, "sub": {"wrong_field": "test"}}, - ) - assert response.status_code == 422, response.text - assert response.json() == snapshot( - { - "detail": [ - { - "loc": ["body", "sub", "name"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) - - -def test_item_model_invalid_type(): - response = client.post( - "/item", - json={"title": "Test Item", "size": "not_a_number", "sub": {"name": "SubItem"}}, - ) - assert response.status_code == 422, response.text - assert response.json() == snapshot( - { - "detail": [ - { - "loc": ["body", "size"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) - - -def test_item_filter(): - response = client.post( - "/item-filter", - json={ - "title": "Filtered Item", - "size": 200, - "description": "Test filtering", - "sub": {"name": "SubFiltered"}, - "multi": [], - }, - ) - assert response.status_code == 200, response.text - result = response.json() - assert result == { - "title": "Filtered Item", - "size": 200, - "description": "Test filtering", - "sub": {"name": "SubFiltered"}, - "multi": [], - } - assert "secret_data" not in result - assert "internal_id" not in result - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == snapshot( - { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/simple-model": { - "post": { - "summary": "Handle Simple Model", - "operationId": "handle_simple_model_simple_model_post", - "requestBody": { - "content": { - "application/json": { - "schema": pydantic_snapshot( - v2=snapshot( - { - "allOf": [ - { - "$ref": "#/components/schemas/SubItem" - } - ], - "title": "Data", - } - ), - v1=snapshot( - {"$ref": "#/components/schemas/SubItem"} - ), - ) - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubItem" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/simple-model-filter": { - "post": { - "summary": "Handle Simple Model Filter", - "operationId": "handle_simple_model_filter_simple_model_filter_post", - "requestBody": { - "content": { - "application/json": { - "schema": pydantic_snapshot( - v2=snapshot( - { - "allOf": [ - { - "$ref": "#/components/schemas/SubItem" - } - ], - "title": "Data", - } - ), - v1=snapshot( - {"$ref": "#/components/schemas/SubItem"} - ), - ) - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubItem" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/item": { - "post": { - "summary": "Handle Item", - "operationId": "handle_item_item_post", - "requestBody": { - "content": { - "application/json": { - "schema": pydantic_snapshot( - v2=snapshot( - { - "allOf": [ - { - "$ref": "#/components/schemas/Item" - } - ], - "title": "Data", - } - ), - v1=snapshot( - {"$ref": "#/components/schemas/Item"} - ), - ) - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/item-filter": { - "post": { - "summary": "Handle Item Filter", - "operationId": "handle_item_filter_item_filter_post", - "requestBody": { - "content": { - "application/json": { - "schema": pydantic_snapshot( - v2=snapshot( - { - "allOf": [ - { - "$ref": "#/components/schemas/Item" - } - ], - "title": "Data", - } - ), - v1=snapshot( - {"$ref": "#/components/schemas/Item"} - ), - ) - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "properties": { - "detail": { - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - "type": "array", - "title": "Detail", - } - }, - "type": "object", - "title": "HTTPValidationError", - }, - "Item": { - "properties": { - "title": {"type": "string", "title": "Title"}, - "size": {"type": "integer", "title": "Size"}, - "description": {"type": "string", "title": "Description"}, - "sub": {"$ref": "#/components/schemas/SubItem"}, - "multi": { - "items": {"$ref": "#/components/schemas/SubItem"}, - "type": "array", - "title": "Multi", - "default": [], - }, - }, - "type": "object", - "required": ["title", "size", "sub"], - "title": "Item", - }, - "SubItem": { - "properties": {"name": {"type": "string", "title": "Name"}}, - "type": "object", - "required": ["name"], - "title": "SubItem", - }, - "ValidationError": { - "properties": { - "loc": { - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - "type": "array", - "title": "Location", - }, - "msg": {"type": "string", "title": "Message"}, - "type": {"type": "string", "title": "Error Type"}, - }, - "type": "object", - "required": ["loc", "msg", "type"], - "title": "ValidationError", - }, - } - }, - } - ) diff --git a/tests/test_pydantic_v1_v2_list.py b/tests/test_pydantic_v1_v2_list.py deleted file mode 100644 index 64f3dd3446..0000000000 --- a/tests/test_pydantic_v1_v2_list.py +++ /dev/null @@ -1,701 +0,0 @@ -import sys -from typing import Any, List, Union - -from tests.utils import pydantic_snapshot, skip_module_if_py_gte_314 - -if sys.version_info >= (3, 14): - skip_module_if_py_gte_314() - -from fastapi import FastAPI -from fastapi._compat.v1 import BaseModel -from fastapi.testclient import TestClient -from inline_snapshot import snapshot - - -class SubItem(BaseModel): - name: str - - -class Item(BaseModel): - title: str - size: int - description: Union[str, None] = None - sub: SubItem - multi: List[SubItem] = [] - - -app = FastAPI() - - -@app.post("/item") -def handle_item(data: Item) -> List[Item]: - return [data, data] - - -@app.post("/item-filter", response_model=List[Item]) -def handle_item_filter(data: Item) -> Any: - extended_data = data.dict() - extended_data.update({"secret_data": "classified", "internal_id": 12345}) - extended_data["sub"].update({"internal_id": 67890}) - return [extended_data, extended_data] - - -@app.post("/item-list") -def handle_item_list(data: List[Item]) -> Item: - if data: - return data[0] - return Item(title="", size=0, sub=SubItem(name="")) - - -@app.post("/item-list-filter", response_model=Item) -def handle_item_list_filter(data: List[Item]) -> Any: - if data: - extended_data = data[0].dict() - extended_data.update({"secret_data": "classified", "internal_id": 12345}) - extended_data["sub"].update({"internal_id": 67890}) - return extended_data - return Item(title="", size=0, sub=SubItem(name="")) - - -@app.post("/item-list-to-list") -def handle_item_list_to_list(data: List[Item]) -> List[Item]: - return data - - -@app.post("/item-list-to-list-filter", response_model=List[Item]) -def handle_item_list_to_list_filter(data: List[Item]) -> Any: - if data: - extended_data = data[0].dict() - extended_data.update({"secret_data": "classified", "internal_id": 12345}) - extended_data["sub"].update({"internal_id": 67890}) - return [extended_data, extended_data] - return [] - - -client = TestClient(app) - - -def test_item_to_list(): - response = client.post( - "/item", - json={ - "title": "Test Item", - "size": 100, - "description": "This is a test item", - "sub": {"name": "SubItem1"}, - "multi": [{"name": "Multi1"}, {"name": "Multi2"}], - }, - ) - assert response.status_code == 200, response.text - result = response.json() - assert isinstance(result, list) - assert len(result) == 2 - for item in result: - assert item == { - "title": "Test Item", - "size": 100, - "description": "This is a test item", - "sub": {"name": "SubItem1"}, - "multi": [{"name": "Multi1"}, {"name": "Multi2"}], - } - - -def test_item_to_list_filter(): - response = client.post( - "/item-filter", - json={ - "title": "Filtered Item", - "size": 200, - "description": "Test filtering", - "sub": {"name": "SubFiltered"}, - "multi": [], - }, - ) - assert response.status_code == 200, response.text - result = response.json() - assert isinstance(result, list) - assert len(result) == 2 - for item in result: - assert item == { - "title": "Filtered Item", - "size": 200, - "description": "Test filtering", - "sub": {"name": "SubFiltered"}, - "multi": [], - } - # Verify secret fields are filtered out - assert "secret_data" not in item - assert "internal_id" not in item - assert "internal_id" not in item["sub"] - - -def test_list_to_item(): - response = client.post( - "/item-list", - json=[ - {"title": "First Item", "size": 50, "sub": {"name": "First Sub"}}, - {"title": "Second Item", "size": 75, "sub": {"name": "Second Sub"}}, - ], - ) - assert response.status_code == 200, response.text - assert response.json() == { - "title": "First Item", - "size": 50, - "description": None, - "sub": {"name": "First Sub"}, - "multi": [], - } - - -def test_list_to_item_empty(): - response = client.post( - "/item-list", - json=[], - ) - assert response.status_code == 200, response.text - assert response.json() == { - "title": "", - "size": 0, - "description": None, - "sub": {"name": ""}, - "multi": [], - } - - -def test_list_to_item_filter(): - response = client.post( - "/item-list-filter", - json=[ - { - "title": "First Item", - "size": 100, - "sub": {"name": "First Sub"}, - "multi": [{"name": "Multi1"}], - }, - {"title": "Second Item", "size": 200, "sub": {"name": "Second Sub"}}, - ], - ) - assert response.status_code == 200, response.text - result = response.json() - assert result == { - "title": "First Item", - "size": 100, - "description": None, - "sub": {"name": "First Sub"}, - "multi": [{"name": "Multi1"}], - } - # Verify secret fields are filtered out - assert "secret_data" not in result - assert "internal_id" not in result - - -def test_list_to_item_filter_no_data(): - response = client.post("/item-list-filter", json=[]) - assert response.status_code == 200, response.text - assert response.json() == { - "title": "", - "size": 0, - "description": None, - "sub": {"name": ""}, - "multi": [], - } - - -def test_list_to_list(): - input_items = [ - {"title": "Item 1", "size": 10, "sub": {"name": "Sub1"}}, - { - "title": "Item 2", - "size": 20, - "description": "Second item", - "sub": {"name": "Sub2"}, - "multi": [{"name": "M1"}, {"name": "M2"}], - }, - {"title": "Item 3", "size": 30, "sub": {"name": "Sub3"}}, - ] - response = client.post( - "/item-list-to-list", - json=input_items, - ) - assert response.status_code == 200, response.text - result = response.json() - assert isinstance(result, list) - assert len(result) == 3 - assert result[0] == { - "title": "Item 1", - "size": 10, - "description": None, - "sub": {"name": "Sub1"}, - "multi": [], - } - assert result[1] == { - "title": "Item 2", - "size": 20, - "description": "Second item", - "sub": {"name": "Sub2"}, - "multi": [{"name": "M1"}, {"name": "M2"}], - } - assert result[2] == { - "title": "Item 3", - "size": 30, - "description": None, - "sub": {"name": "Sub3"}, - "multi": [], - } - - -def test_list_to_list_filter(): - response = client.post( - "/item-list-to-list-filter", - json=[{"title": "Item 1", "size": 100, "sub": {"name": "Sub1"}}], - ) - assert response.status_code == 200, response.text - result = response.json() - assert isinstance(result, list) - assert len(result) == 2 - for item in result: - assert item == { - "title": "Item 1", - "size": 100, - "description": None, - "sub": {"name": "Sub1"}, - "multi": [], - } - # Verify secret fields are filtered out - assert "secret_data" not in item - assert "internal_id" not in item - - -def test_list_to_list_filter_no_data(): - response = client.post( - "/item-list-to-list-filter", - json=[], - ) - assert response.status_code == 200, response.text - assert response.json() == [] - - -def test_list_validation_error(): - response = client.post( - "/item-list", - json=[ - {"title": "Valid Item", "size": 100, "sub": {"name": "Sub1"}}, - { - "title": "Invalid Item" - # Missing required fields: size and sub - }, - ], - ) - assert response.status_code == 422, response.text - error_detail = response.json()["detail"] - assert len(error_detail) == 2 - assert { - "loc": ["body", 1, "size"], - "msg": "field required", - "type": "value_error.missing", - } in error_detail - assert { - "loc": ["body", 1, "sub"], - "msg": "field required", - "type": "value_error.missing", - } in error_detail - - -def test_list_nested_validation_error(): - response = client.post( - "/item-list", - json=[ - {"title": "Item with bad sub", "size": 100, "sub": {"wrong_field": "value"}} - ], - ) - assert response.status_code == 422, response.text - assert response.json() == snapshot( - { - "detail": [ - { - "loc": ["body", 0, "sub", "name"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) - - -def test_list_type_validation_error(): - response = client.post( - "/item-list", - json=[{"title": "Item", "size": "not_a_number", "sub": {"name": "Sub"}}], - ) - assert response.status_code == 422, response.text - assert response.json() == snapshot( - { - "detail": [ - { - "loc": ["body", 0, "size"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) - - -def test_invalid_list_structure(): - response = client.post( - "/item-list", - json={"title": "Not a list", "size": 100, "sub": {"name": "Sub"}}, - ) - assert response.status_code == 422, response.text - assert response.json() == snapshot( - { - "detail": [ - { - "loc": ["body"], - "msg": "value is not a valid list", - "type": "type_error.list", - } - ] - } - ) - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == snapshot( - { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/item": { - "post": { - "summary": "Handle Item", - "operationId": "handle_item_item_post", - "requestBody": { - "content": { - "application/json": { - "schema": pydantic_snapshot( - v2=snapshot( - { - "allOf": [ - { - "$ref": "#/components/schemas/Item" - } - ], - "title": "Data", - } - ), - v1=snapshot( - {"$ref": "#/components/schemas/Item"} - ), - ) - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/Item" - }, - "type": "array", - "title": "Response Handle Item Item Post", - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/item-filter": { - "post": { - "summary": "Handle Item Filter", - "operationId": "handle_item_filter_item_filter_post", - "requestBody": { - "content": { - "application/json": { - "schema": pydantic_snapshot( - v2=snapshot( - { - "allOf": [ - { - "$ref": "#/components/schemas/Item" - } - ], - "title": "Data", - } - ), - v1=snapshot( - {"$ref": "#/components/schemas/Item"} - ), - ) - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/Item" - }, - "type": "array", - "title": "Response Handle Item Filter Item Filter Post", - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/item-list": { - "post": { - "summary": "Handle Item List", - "operationId": "handle_item_list_item_list_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "items": {"$ref": "#/components/schemas/Item"}, - "type": "array", - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/item-list-filter": { - "post": { - "summary": "Handle Item List Filter", - "operationId": "handle_item_list_filter_item_list_filter_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "items": {"$ref": "#/components/schemas/Item"}, - "type": "array", - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/item-list-to-list": { - "post": { - "summary": "Handle Item List To List", - "operationId": "handle_item_list_to_list_item_list_to_list_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "items": {"$ref": "#/components/schemas/Item"}, - "type": "array", - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/Item" - }, - "type": "array", - "title": "Response Handle Item List To List Item List To List Post", - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/item-list-to-list-filter": { - "post": { - "summary": "Handle Item List To List Filter", - "operationId": "handle_item_list_to_list_filter_item_list_to_list_filter_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "items": {"$ref": "#/components/schemas/Item"}, - "type": "array", - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/Item" - }, - "type": "array", - "title": "Response Handle Item List To List Filter Item List To List Filter Post", - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "properties": { - "detail": { - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - "type": "array", - "title": "Detail", - } - }, - "type": "object", - "title": "HTTPValidationError", - }, - "Item": { - "properties": { - "title": {"type": "string", "title": "Title"}, - "size": {"type": "integer", "title": "Size"}, - "description": {"type": "string", "title": "Description"}, - "sub": {"$ref": "#/components/schemas/SubItem"}, - "multi": { - "items": {"$ref": "#/components/schemas/SubItem"}, - "type": "array", - "title": "Multi", - "default": [], - }, - }, - "type": "object", - "required": ["title", "size", "sub"], - "title": "Item", - }, - "SubItem": { - "properties": {"name": {"type": "string", "title": "Name"}}, - "type": "object", - "required": ["name"], - "title": "SubItem", - }, - "ValidationError": { - "properties": { - "loc": { - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - "type": "array", - "title": "Location", - }, - "msg": {"type": "string", "title": "Message"}, - "type": {"type": "string", "title": "Error Type"}, - }, - "type": "object", - "required": ["loc", "msg", "type"], - "title": "ValidationError", - }, - } - }, - } - ) diff --git a/tests/test_pydantic_v1_v2_mixed.py b/tests/test_pydantic_v1_v2_mixed.py deleted file mode 100644 index 54d408827f..0000000000 --- a/tests/test_pydantic_v1_v2_mixed.py +++ /dev/null @@ -1,1499 +0,0 @@ -import sys -from typing import Any, List, Union - -from tests.utils import pydantic_snapshot, skip_module_if_py_gte_314 - -if sys.version_info >= (3, 14): - skip_module_if_py_gte_314() - -from fastapi import FastAPI -from fastapi._compat.v1 import BaseModel -from fastapi.testclient import TestClient -from inline_snapshot import snapshot -from pydantic import BaseModel as NewBaseModel - - -class SubItem(BaseModel): - name: str - - -class Item(BaseModel): - title: str - size: int - description: Union[str, None] = None - sub: SubItem - multi: List[SubItem] = [] - - -class NewSubItem(NewBaseModel): - new_sub_name: str - - -class NewItem(NewBaseModel): - new_title: str - new_size: int - new_description: Union[str, None] = None - new_sub: NewSubItem - new_multi: List[NewSubItem] = [] - - -app = FastAPI() - - -@app.post("/v1-to-v2/item") -def handle_v1_item_to_v2(data: Item) -> NewItem: - return NewItem( - new_title=data.title, - new_size=data.size, - new_description=data.description, - new_sub=NewSubItem(new_sub_name=data.sub.name), - new_multi=[NewSubItem(new_sub_name=s.name) for s in data.multi], - ) - - -@app.post("/v1-to-v2/item-filter", response_model=NewItem) -def handle_v1_item_to_v2_filter(data: Item) -> Any: - result = { - "new_title": data.title, - "new_size": data.size, - "new_description": data.description, - "new_sub": {"new_sub_name": data.sub.name, "new_sub_secret": "sub_hidden"}, - "new_multi": [ - {"new_sub_name": s.name, "new_sub_secret": "sub_hidden"} for s in data.multi - ], - "secret": "hidden_v1_to_v2", - } - return result - - -@app.post("/v2-to-v1/item") -def handle_v2_item_to_v1(data: NewItem) -> Item: - return Item( - title=data.new_title, - size=data.new_size, - description=data.new_description, - sub=SubItem(name=data.new_sub.new_sub_name), - multi=[SubItem(name=s.new_sub_name) for s in data.new_multi], - ) - - -@app.post("/v2-to-v1/item-filter", response_model=Item) -def handle_v2_item_to_v1_filter(data: NewItem) -> Any: - result = { - "title": data.new_title, - "size": data.new_size, - "description": data.new_description, - "sub": {"name": data.new_sub.new_sub_name, "sub_secret": "sub_hidden"}, - "multi": [ - {"name": s.new_sub_name, "sub_secret": "sub_hidden"} for s in data.new_multi - ], - "secret": "hidden_v2_to_v1", - } - return result - - -@app.post("/v1-to-v2/item-to-list") -def handle_v1_item_to_v2_list(data: Item) -> List[NewItem]: - converted = NewItem( - new_title=data.title, - new_size=data.size, - new_description=data.description, - new_sub=NewSubItem(new_sub_name=data.sub.name), - new_multi=[NewSubItem(new_sub_name=s.name) for s in data.multi], - ) - return [converted, converted] - - -@app.post("/v1-to-v2/list-to-list") -def handle_v1_list_to_v2_list(data: List[Item]) -> List[NewItem]: - result = [] - for item in data: - result.append( - NewItem( - new_title=item.title, - new_size=item.size, - new_description=item.description, - new_sub=NewSubItem(new_sub_name=item.sub.name), - new_multi=[NewSubItem(new_sub_name=s.name) for s in item.multi], - ) - ) - return result - - -@app.post("/v1-to-v2/list-to-list-filter", response_model=List[NewItem]) -def handle_v1_list_to_v2_list_filter(data: List[Item]) -> Any: - result = [] - for item in data: - converted = { - "new_title": item.title, - "new_size": item.size, - "new_description": item.description, - "new_sub": {"new_sub_name": item.sub.name, "new_sub_secret": "sub_hidden"}, - "new_multi": [ - {"new_sub_name": s.name, "new_sub_secret": "sub_hidden"} - for s in item.multi - ], - "secret": "hidden_v2_to_v1", - } - result.append(converted) - return result - - -@app.post("/v1-to-v2/list-to-item") -def handle_v1_list_to_v2_item(data: List[Item]) -> NewItem: - if data: - item = data[0] - return NewItem( - new_title=item.title, - new_size=item.size, - new_description=item.description, - new_sub=NewSubItem(new_sub_name=item.sub.name), - new_multi=[NewSubItem(new_sub_name=s.name) for s in item.multi], - ) - return NewItem(new_title="", new_size=0, new_sub=NewSubItem(new_sub_name="")) - - -@app.post("/v2-to-v1/item-to-list") -def handle_v2_item_to_v1_list(data: NewItem) -> List[Item]: - converted = Item( - title=data.new_title, - size=data.new_size, - description=data.new_description, - sub=SubItem(name=data.new_sub.new_sub_name), - multi=[SubItem(name=s.new_sub_name) for s in data.new_multi], - ) - return [converted, converted] - - -@app.post("/v2-to-v1/list-to-list") -def handle_v2_list_to_v1_list(data: List[NewItem]) -> List[Item]: - result = [] - for item in data: - result.append( - Item( - title=item.new_title, - size=item.new_size, - description=item.new_description, - sub=SubItem(name=item.new_sub.new_sub_name), - multi=[SubItem(name=s.new_sub_name) for s in item.new_multi], - ) - ) - return result - - -@app.post("/v2-to-v1/list-to-list-filter", response_model=List[Item]) -def handle_v2_list_to_v1_list_filter(data: List[NewItem]) -> Any: - result = [] - for item in data: - converted = { - "title": item.new_title, - "size": item.new_size, - "description": item.new_description, - "sub": {"name": item.new_sub.new_sub_name, "sub_secret": "sub_hidden"}, - "multi": [ - {"name": s.new_sub_name, "sub_secret": "sub_hidden"} - for s in item.new_multi - ], - "secret": "hidden_v2_to_v1", - } - result.append(converted) - return result - - -@app.post("/v2-to-v1/list-to-item") -def handle_v2_list_to_v1_item(data: List[NewItem]) -> Item: - if data: - item = data[0] - return Item( - title=item.new_title, - size=item.new_size, - description=item.new_description, - sub=SubItem(name=item.new_sub.new_sub_name), - multi=[SubItem(name=s.new_sub_name) for s in item.new_multi], - ) - return Item(title="", size=0, sub=SubItem(name="")) - - -client = TestClient(app) - - -def test_v1_to_v2_item(): - response = client.post( - "/v1-to-v2/item", - json={ - "title": "Old Item", - "size": 100, - "description": "V1 description", - "sub": {"name": "V1 Sub"}, - "multi": [{"name": "M1"}, {"name": "M2"}], - }, - ) - assert response.status_code == 200, response.text - assert response.json() == { - "new_title": "Old Item", - "new_size": 100, - "new_description": "V1 description", - "new_sub": {"new_sub_name": "V1 Sub"}, - "new_multi": [{"new_sub_name": "M1"}, {"new_sub_name": "M2"}], - } - - -def test_v1_to_v2_item_minimal(): - response = client.post( - "/v1-to-v2/item", - json={"title": "Minimal", "size": 50, "sub": {"name": "MinSub"}}, - ) - assert response.status_code == 200, response.text - assert response.json() == { - "new_title": "Minimal", - "new_size": 50, - "new_description": None, - "new_sub": {"new_sub_name": "MinSub"}, - "new_multi": [], - } - - -def test_v1_to_v2_item_filter(): - response = client.post( - "/v1-to-v2/item-filter", - json={ - "title": "Filtered Item", - "size": 50, - "sub": {"name": "Sub"}, - "multi": [{"name": "Multi1"}], - }, - ) - assert response.status_code == 200, response.text - result = response.json() - assert result == snapshot( - { - "new_title": "Filtered Item", - "new_size": 50, - "new_description": None, - "new_sub": {"new_sub_name": "Sub"}, - "new_multi": [{"new_sub_name": "Multi1"}], - } - ) - # Verify secret fields are filtered out - assert "secret" not in result - assert "new_sub_secret" not in result["new_sub"] - assert "new_sub_secret" not in result["new_multi"][0] - - -def test_v2_to_v1_item(): - response = client.post( - "/v2-to-v1/item", - json={ - "new_title": "New Item", - "new_size": 200, - "new_description": "V2 description", - "new_sub": {"new_sub_name": "V2 Sub"}, - "new_multi": [{"new_sub_name": "N1"}, {"new_sub_name": "N2"}], - }, - ) - assert response.status_code == 200, response.text - assert response.json() == { - "title": "New Item", - "size": 200, - "description": "V2 description", - "sub": {"name": "V2 Sub"}, - "multi": [{"name": "N1"}, {"name": "N2"}], - } - - -def test_v2_to_v1_item_minimal(): - response = client.post( - "/v2-to-v1/item", - json={ - "new_title": "MinimalNew", - "new_size": 75, - "new_sub": {"new_sub_name": "MinNewSub"}, - }, - ) - assert response.status_code == 200, response.text - assert response.json() == { - "title": "MinimalNew", - "size": 75, - "description": None, - "sub": {"name": "MinNewSub"}, - "multi": [], - } - - -def test_v2_to_v1_item_filter(): - response = client.post( - "/v2-to-v1/item-filter", - json={ - "new_title": "Filtered New", - "new_size": 75, - "new_sub": {"new_sub_name": "NewSub"}, - "new_multi": [], - }, - ) - assert response.status_code == 200, response.text - result = response.json() - assert result == snapshot( - { - "title": "Filtered New", - "size": 75, - "description": None, - "sub": {"name": "NewSub"}, - "multi": [], - } - ) - # Verify secret fields are filtered out - assert "secret" not in result - assert "sub_secret" not in result["sub"] - - -def test_v1_item_to_v2_list(): - response = client.post( - "/v1-to-v2/item-to-list", - json={ - "title": "Single to List", - "size": 150, - "description": "Convert to list", - "sub": {"name": "Sub1"}, - "multi": [], - }, - ) - assert response.status_code == 200, response.text - result = response.json() - assert result == [ - { - "new_title": "Single to List", - "new_size": 150, - "new_description": "Convert to list", - "new_sub": {"new_sub_name": "Sub1"}, - "new_multi": [], - }, - { - "new_title": "Single to List", - "new_size": 150, - "new_description": "Convert to list", - "new_sub": {"new_sub_name": "Sub1"}, - "new_multi": [], - }, - ] - - -def test_v1_list_to_v2_list(): - response = client.post( - "/v1-to-v2/list-to-list", - json=[ - {"title": "Item1", "size": 10, "sub": {"name": "Sub1"}}, - { - "title": "Item2", - "size": 20, - "description": "Second item", - "sub": {"name": "Sub2"}, - "multi": [{"name": "M1"}, {"name": "M2"}], - }, - {"title": "Item3", "size": 30, "sub": {"name": "Sub3"}}, - ], - ) - assert response.status_code == 200, response.text - assert response.json() == [ - { - "new_title": "Item1", - "new_size": 10, - "new_description": None, - "new_sub": {"new_sub_name": "Sub1"}, - "new_multi": [], - }, - { - "new_title": "Item2", - "new_size": 20, - "new_description": "Second item", - "new_sub": {"new_sub_name": "Sub2"}, - "new_multi": [{"new_sub_name": "M1"}, {"new_sub_name": "M2"}], - }, - { - "new_title": "Item3", - "new_size": 30, - "new_description": None, - "new_sub": {"new_sub_name": "Sub3"}, - "new_multi": [], - }, - ] - - -def test_v1_list_to_v2_list_filter(): - response = client.post( - "/v1-to-v2/list-to-list-filter", - json=[{"title": "FilterMe", "size": 30, "sub": {"name": "SubF"}}], - ) - assert response.status_code == 200, response.text - result = response.json() - assert result == snapshot( - [ - { - "new_title": "FilterMe", - "new_size": 30, - "new_description": None, - "new_sub": {"new_sub_name": "SubF"}, - "new_multi": [], - } - ] - ) - # Verify secret fields are filtered out - assert "secret" not in result[0] - assert "new_sub_secret" not in result[0]["new_sub"] - - -def test_v1_list_to_v2_item(): - response = client.post( - "/v1-to-v2/list-to-item", - json=[ - {"title": "First", "size": 100, "sub": {"name": "FirstSub"}}, - {"title": "Second", "size": 200, "sub": {"name": "SecondSub"}}, - ], - ) - assert response.status_code == 200, response.text - assert response.json() == { - "new_title": "First", - "new_size": 100, - "new_description": None, - "new_sub": {"new_sub_name": "FirstSub"}, - "new_multi": [], - } - - -def test_v1_list_to_v2_item_empty(): - response = client.post("/v1-to-v2/list-to-item", json=[]) - assert response.status_code == 200, response.text - assert response.json() == { - "new_title": "", - "new_size": 0, - "new_description": None, - "new_sub": {"new_sub_name": ""}, - "new_multi": [], - } - - -def test_v2_item_to_v1_list(): - response = client.post( - "/v2-to-v1/item-to-list", - json={ - "new_title": "Single New", - "new_size": 250, - "new_description": "New to list", - "new_sub": {"new_sub_name": "NewSub"}, - "new_multi": [], - }, - ) - assert response.status_code == 200, response.text - assert response.json() == [ - { - "title": "Single New", - "size": 250, - "description": "New to list", - "sub": {"name": "NewSub"}, - "multi": [], - }, - { - "title": "Single New", - "size": 250, - "description": "New to list", - "sub": {"name": "NewSub"}, - "multi": [], - }, - ] - - -def test_v2_list_to_v1_list(): - response = client.post( - "/v2-to-v1/list-to-list", - json=[ - {"new_title": "New1", "new_size": 15, "new_sub": {"new_sub_name": "NS1"}}, - { - "new_title": "New2", - "new_size": 25, - "new_description": "Second new", - "new_sub": {"new_sub_name": "NS2"}, - "new_multi": [{"new_sub_name": "NM1"}], - }, - ], - ) - assert response.status_code == 200, response.text - assert response.json() == [ - { - "title": "New1", - "size": 15, - "description": None, - "sub": {"name": "NS1"}, - "multi": [], - }, - { - "title": "New2", - "size": 25, - "description": "Second new", - "sub": {"name": "NS2"}, - "multi": [{"name": "NM1"}], - }, - ] - - -def test_v2_list_to_v1_list_filter(): - response = client.post( - "/v2-to-v1/list-to-list-filter", - json=[ - { - "new_title": "FilterNew", - "new_size": 35, - "new_sub": {"new_sub_name": "NSF"}, - } - ], - ) - assert response.status_code == 200, response.text - result = response.json() - assert result == snapshot( - [ - { - "title": "FilterNew", - "size": 35, - "description": None, - "sub": {"name": "NSF"}, - "multi": [], - } - ] - ) - # Verify secret fields are filtered out - assert "secret" not in result[0] - assert "sub_secret" not in result[0]["sub"] - - -def test_v2_list_to_v1_item(): - response = client.post( - "/v2-to-v1/list-to-item", - json=[ - { - "new_title": "FirstNew", - "new_size": 300, - "new_sub": {"new_sub_name": "FNS"}, - }, - { - "new_title": "SecondNew", - "new_size": 400, - "new_sub": {"new_sub_name": "SNS"}, - }, - ], - ) - assert response.status_code == 200, response.text - assert response.json() == { - "title": "FirstNew", - "size": 300, - "description": None, - "sub": {"name": "FNS"}, - "multi": [], - } - - -def test_v2_list_to_v1_item_empty(): - response = client.post("/v2-to-v1/list-to-item", json=[]) - assert response.status_code == 200, response.text - assert response.json() == { - "title": "", - "size": 0, - "description": None, - "sub": {"name": ""}, - "multi": [], - } - - -def test_v1_to_v2_validation_error(): - response = client.post("/v1-to-v2/item", json={"title": "Missing fields"}) - assert response.status_code == 422, response.text - assert response.json() == snapshot( - { - "detail": [ - { - "loc": ["body", "size"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "sub"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -def test_v1_to_v2_nested_validation_error(): - response = client.post( - "/v1-to-v2/item", - json={"title": "Bad sub", "size": 100, "sub": {"wrong_field": "value"}}, - ) - assert response.status_code == 422, response.text - assert response.json() == snapshot( - { - "detail": [ - { - "loc": ["body", "sub", "name"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) - - -def test_v1_to_v2_type_validation_error(): - response = client.post( - "/v1-to-v2/item", - json={"title": "Bad type", "size": "not_a_number", "sub": {"name": "Sub"}}, - ) - assert response.status_code == 422, response.text - assert response.json() == snapshot( - { - "detail": [ - { - "loc": ["body", "size"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) - - -def test_v2_to_v1_validation_error(): - response = client.post( - "/v2-to-v1/item", - json={"new_title": "Missing fields"}, - ) - assert response.status_code == 422, response.text - assert response.json() == snapshot( - { - "detail": pydantic_snapshot( - v2=snapshot( - [ - { - "type": "missing", - "loc": ["body", "new_size"], - "msg": "Field required", - "input": {"new_title": "Missing fields"}, - }, - { - "type": "missing", - "loc": ["body", "new_sub"], - "msg": "Field required", - "input": {"new_title": "Missing fields"}, - }, - ] - ), - v1=snapshot( - [ - { - "loc": ["body", "new_size"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "new_sub"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - ), - ) - } - ) - - -def test_v2_to_v1_nested_validation_error(): - response = client.post( - "/v2-to-v1/item", - json={ - "new_title": "Bad sub", - "new_size": 200, - "new_sub": {"wrong_field": "value"}, - }, - ) - assert response.status_code == 422, response.text - assert response.json() == snapshot( - { - "detail": [ - pydantic_snapshot( - v2=snapshot( - { - "type": "missing", - "loc": ["body", "new_sub", "new_sub_name"], - "msg": "Field required", - "input": {"wrong_field": "value"}, - } - ), - v1=snapshot( - { - "loc": ["body", "new_sub", "new_sub_name"], - "msg": "field required", - "type": "value_error.missing", - } - ), - ) - ] - } - ) - - -def test_v1_list_validation_error(): - response = client.post( - "/v1-to-v2/list-to-list", - json=[ - {"title": "Valid", "size": 10, "sub": {"name": "S"}}, - {"title": "Invalid"}, - ], - ) - assert response.status_code == 422, response.text - assert response.json() == snapshot( - { - "detail": [ - { - "loc": ["body", 1, "size"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", 1, "sub"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -def test_v2_list_validation_error(): - response = client.post( - "/v2-to-v1/list-to-list", - json=[ - {"new_title": "Valid", "new_size": 10, "new_sub": {"new_sub_name": "NS"}}, - {"new_title": "Invalid"}, - ], - ) - assert response.status_code == 422, response.text - assert response.json() == snapshot( - { - "detail": pydantic_snapshot( - v2=snapshot( - [ - { - "type": "missing", - "loc": ["body", 1, "new_size"], - "msg": "Field required", - "input": {"new_title": "Invalid"}, - }, - { - "type": "missing", - "loc": ["body", 1, "new_sub"], - "msg": "Field required", - "input": {"new_title": "Invalid"}, - }, - ] - ), - v1=snapshot( - [ - { - "loc": ["body", 1, "new_size"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", 1, "new_sub"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - ), - ) - } - ) - - -def test_invalid_list_structure_v1(): - response = client.post( - "/v1-to-v2/list-to-list", - json={"title": "Not a list", "size": 100, "sub": {"name": "Sub"}}, - ) - assert response.status_code == 422, response.text - assert response.json() == snapshot( - { - "detail": [ - { - "loc": ["body"], - "msg": "value is not a valid list", - "type": "type_error.list", - } - ] - } - ) - - -def test_invalid_list_structure_v2(): - response = client.post( - "/v2-to-v1/list-to-list", - json={ - "new_title": "Not a list", - "new_size": 100, - "new_sub": {"new_sub_name": "Sub"}, - }, - ) - assert response.status_code == 422, response.text - assert response.json() == snapshot( - { - "detail": pydantic_snapshot( - v2=snapshot( - [ - { - "type": "list_type", - "loc": ["body"], - "msg": "Input should be a valid list", - "input": { - "new_title": "Not a list", - "new_size": 100, - "new_sub": {"new_sub_name": "Sub"}, - }, - } - ] - ), - v1=snapshot( - [ - { - "loc": ["body"], - "msg": "value is not a valid list", - "type": "type_error.list", - } - ] - ), - ) - } - ) - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == snapshot( - { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/v1-to-v2/item": { - "post": { - "summary": "Handle V1 Item To V2", - "operationId": "handle_v1_item_to_v2_v1_to_v2_item_post", - "requestBody": { - "content": { - "application/json": { - "schema": pydantic_snapshot( - v2=snapshot( - { - "allOf": [ - { - "$ref": "#/components/schemas/Item" - } - ], - "title": "Data", - } - ), - v1=snapshot( - {"$ref": "#/components/schemas/Item"} - ), - ) - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NewItem" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v1-to-v2/item-filter": { - "post": { - "summary": "Handle V1 Item To V2 Filter", - "operationId": "handle_v1_item_to_v2_filter_v1_to_v2_item_filter_post", - "requestBody": { - "content": { - "application/json": { - "schema": pydantic_snapshot( - v2=snapshot( - { - "allOf": [ - { - "$ref": "#/components/schemas/Item" - } - ], - "title": "Data", - } - ), - v1=snapshot( - {"$ref": "#/components/schemas/Item"} - ), - ) - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NewItem" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v2-to-v1/item": { - "post": { - "summary": "Handle V2 Item To V1", - "operationId": "handle_v2_item_to_v1_v2_to_v1_item_post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/NewItem"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v2-to-v1/item-filter": { - "post": { - "summary": "Handle V2 Item To V1 Filter", - "operationId": "handle_v2_item_to_v1_filter_v2_to_v1_item_filter_post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/NewItem"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v1-to-v2/item-to-list": { - "post": { - "summary": "Handle V1 Item To V2 List", - "operationId": "handle_v1_item_to_v2_list_v1_to_v2_item_to_list_post", - "requestBody": { - "content": { - "application/json": { - "schema": pydantic_snapshot( - v2=snapshot( - { - "allOf": [ - { - "$ref": "#/components/schemas/Item" - } - ], - "title": "Data", - } - ), - v1=snapshot( - {"$ref": "#/components/schemas/Item"} - ), - ) - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/NewItem" - }, - "type": "array", - "title": "Response Handle V1 Item To V2 List V1 To V2 Item To List Post", - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v1-to-v2/list-to-list": { - "post": { - "summary": "Handle V1 List To V2 List", - "operationId": "handle_v1_list_to_v2_list_v1_to_v2_list_to_list_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "items": {"$ref": "#/components/schemas/Item"}, - "type": "array", - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/NewItem" - }, - "type": "array", - "title": "Response Handle V1 List To V2 List V1 To V2 List To List Post", - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v1-to-v2/list-to-list-filter": { - "post": { - "summary": "Handle V1 List To V2 List Filter", - "operationId": "handle_v1_list_to_v2_list_filter_v1_to_v2_list_to_list_filter_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "items": {"$ref": "#/components/schemas/Item"}, - "type": "array", - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/NewItem" - }, - "type": "array", - "title": "Response Handle V1 List To V2 List Filter V1 To V2 List To List Filter Post", - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v1-to-v2/list-to-item": { - "post": { - "summary": "Handle V1 List To V2 Item", - "operationId": "handle_v1_list_to_v2_item_v1_to_v2_list_to_item_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "items": {"$ref": "#/components/schemas/Item"}, - "type": "array", - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NewItem" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v2-to-v1/item-to-list": { - "post": { - "summary": "Handle V2 Item To V1 List", - "operationId": "handle_v2_item_to_v1_list_v2_to_v1_item_to_list_post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/NewItem"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/Item" - }, - "type": "array", - "title": "Response Handle V2 Item To V1 List V2 To V1 Item To List Post", - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v2-to-v1/list-to-list": { - "post": { - "summary": "Handle V2 List To V1 List", - "operationId": "handle_v2_list_to_v1_list_v2_to_v1_list_to_list_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/NewItem" - }, - "type": "array", - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/Item" - }, - "type": "array", - "title": "Response Handle V2 List To V1 List V2 To V1 List To List Post", - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v2-to-v1/list-to-list-filter": { - "post": { - "summary": "Handle V2 List To V1 List Filter", - "operationId": "handle_v2_list_to_v1_list_filter_v2_to_v1_list_to_list_filter_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/NewItem" - }, - "type": "array", - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/Item" - }, - "type": "array", - "title": "Response Handle V2 List To V1 List Filter V2 To V1 List To List Filter Post", - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v2-to-v1/list-to-item": { - "post": { - "summary": "Handle V2 List To V1 Item", - "operationId": "handle_v2_list_to_v1_item_v2_to_v1_list_to_item_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/NewItem" - }, - "type": "array", - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "properties": { - "detail": { - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - "type": "array", - "title": "Detail", - } - }, - "type": "object", - "title": "HTTPValidationError", - }, - "Item": { - "properties": { - "title": {"type": "string", "title": "Title"}, - "size": {"type": "integer", "title": "Size"}, - "description": {"type": "string", "title": "Description"}, - "sub": {"$ref": "#/components/schemas/SubItem"}, - "multi": { - "items": {"$ref": "#/components/schemas/SubItem"}, - "type": "array", - "title": "Multi", - "default": [], - }, - }, - "type": "object", - "required": ["title", "size", "sub"], - "title": "Item", - }, - "NewItem": { - "properties": { - "new_title": {"type": "string", "title": "New Title"}, - "new_size": {"type": "integer", "title": "New Size"}, - "new_description": pydantic_snapshot( - v2=snapshot( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "New Description", - } - ), - v1=snapshot( - {"type": "string", "title": "New Description"} - ), - ), - "new_sub": {"$ref": "#/components/schemas/NewSubItem"}, - "new_multi": { - "items": {"$ref": "#/components/schemas/NewSubItem"}, - "type": "array", - "title": "New Multi", - "default": [], - }, - }, - "type": "object", - "required": ["new_title", "new_size", "new_sub"], - "title": "NewItem", - }, - "NewSubItem": { - "properties": { - "new_sub_name": {"type": "string", "title": "New Sub Name"} - }, - "type": "object", - "required": ["new_sub_name"], - "title": "NewSubItem", - }, - "SubItem": { - "properties": {"name": {"type": "string", "title": "Name"}}, - "type": "object", - "required": ["name"], - "title": "SubItem", - }, - "ValidationError": { - "properties": { - "loc": { - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - "type": "array", - "title": "Location", - }, - "msg": {"type": "string", "title": "Message"}, - "type": {"type": "string", "title": "Error Type"}, - }, - "type": "object", - "required": ["loc", "msg", "type"], - "title": "ValidationError", - }, - } - }, - } - ) diff --git a/tests/test_pydantic_v1_v2_multifile/main.py b/tests/test_pydantic_v1_v2_multifile/main.py deleted file mode 100644 index 8985cb7b4c..0000000000 --- a/tests/test_pydantic_v1_v2_multifile/main.py +++ /dev/null @@ -1,142 +0,0 @@ -from typing import List - -from fastapi import FastAPI - -from . import modelsv1, modelsv2, modelsv2b - -app = FastAPI() - - -@app.post("/v1-to-v2/item") -def handle_v1_item_to_v2(data: modelsv1.Item) -> modelsv2.Item: - return modelsv2.Item( - new_title=data.title, - new_size=data.size, - new_description=data.description, - new_sub=modelsv2.SubItem(new_sub_name=data.sub.name), - new_multi=[modelsv2.SubItem(new_sub_name=s.name) for s in data.multi], - ) - - -@app.post("/v2-to-v1/item") -def handle_v2_item_to_v1(data: modelsv2.Item) -> modelsv1.Item: - return modelsv1.Item( - title=data.new_title, - size=data.new_size, - description=data.new_description, - sub=modelsv1.SubItem(name=data.new_sub.new_sub_name), - multi=[modelsv1.SubItem(name=s.new_sub_name) for s in data.new_multi], - ) - - -@app.post("/v1-to-v2/item-to-list") -def handle_v1_item_to_v2_list(data: modelsv1.Item) -> List[modelsv2.Item]: - converted = modelsv2.Item( - new_title=data.title, - new_size=data.size, - new_description=data.description, - new_sub=modelsv2.SubItem(new_sub_name=data.sub.name), - new_multi=[modelsv2.SubItem(new_sub_name=s.name) for s in data.multi], - ) - return [converted, converted] - - -@app.post("/v1-to-v2/list-to-list") -def handle_v1_list_to_v2_list(data: List[modelsv1.Item]) -> List[modelsv2.Item]: - result = [] - for item in data: - result.append( - modelsv2.Item( - new_title=item.title, - new_size=item.size, - new_description=item.description, - new_sub=modelsv2.SubItem(new_sub_name=item.sub.name), - new_multi=[modelsv2.SubItem(new_sub_name=s.name) for s in item.multi], - ) - ) - return result - - -@app.post("/v1-to-v2/list-to-item") -def handle_v1_list_to_v2_item(data: List[modelsv1.Item]) -> modelsv2.Item: - if data: - item = data[0] - return modelsv2.Item( - new_title=item.title, - new_size=item.size, - new_description=item.description, - new_sub=modelsv2.SubItem(new_sub_name=item.sub.name), - new_multi=[modelsv2.SubItem(new_sub_name=s.name) for s in item.multi], - ) - return modelsv2.Item( - new_title="", new_size=0, new_sub=modelsv2.SubItem(new_sub_name="") - ) - - -@app.post("/v2-to-v1/item-to-list") -def handle_v2_item_to_v1_list(data: modelsv2.Item) -> List[modelsv1.Item]: - converted = modelsv1.Item( - title=data.new_title, - size=data.new_size, - description=data.new_description, - sub=modelsv1.SubItem(name=data.new_sub.new_sub_name), - multi=[modelsv1.SubItem(name=s.new_sub_name) for s in data.new_multi], - ) - return [converted, converted] - - -@app.post("/v2-to-v1/list-to-list") -def handle_v2_list_to_v1_list(data: List[modelsv2.Item]) -> List[modelsv1.Item]: - result = [] - for item in data: - result.append( - modelsv1.Item( - title=item.new_title, - size=item.new_size, - description=item.new_description, - sub=modelsv1.SubItem(name=item.new_sub.new_sub_name), - multi=[modelsv1.SubItem(name=s.new_sub_name) for s in item.new_multi], - ) - ) - return result - - -@app.post("/v2-to-v1/list-to-item") -def handle_v2_list_to_v1_item(data: List[modelsv2.Item]) -> modelsv1.Item: - if data: - item = data[0] - return modelsv1.Item( - title=item.new_title, - size=item.new_size, - description=item.new_description, - sub=modelsv1.SubItem(name=item.new_sub.new_sub_name), - multi=[modelsv1.SubItem(name=s.new_sub_name) for s in item.new_multi], - ) - return modelsv1.Item(title="", size=0, sub=modelsv1.SubItem(name="")) - - -@app.post("/v2-to-v1/same-name") -def handle_v2_same_name_to_v1( - item1: modelsv2.Item, item2: modelsv2b.Item -) -> modelsv1.Item: - return modelsv1.Item( - title=item1.new_title, - size=item2.dup_size, - description=item1.new_description, - sub=modelsv1.SubItem(name=item1.new_sub.new_sub_name), - multi=[modelsv1.SubItem(name=s.dup_sub_name) for s in item2.dup_multi], - ) - - -@app.post("/v2-to-v1/list-of-items-to-list-of-items") -def handle_v2_items_in_list_to_v1_item_in_list( - data1: List[modelsv2.ItemInList], data2: List[modelsv2b.ItemInList] -) -> List[modelsv1.ItemInList]: - result = [] - item1 = data1[0] - item2 = data2[0] - result = [ - modelsv1.ItemInList(name1=item1.name2), - modelsv1.ItemInList(name1=item2.dup_name2), - ] - return result diff --git a/tests/test_pydantic_v1_v2_multifile/modelsv1.py b/tests/test_pydantic_v1_v2_multifile/modelsv1.py deleted file mode 100644 index 889291a1a7..0000000000 --- a/tests/test_pydantic_v1_v2_multifile/modelsv1.py +++ /dev/null @@ -1,19 +0,0 @@ -from typing import List, Union - -from fastapi._compat.v1 import BaseModel - - -class SubItem(BaseModel): - name: str - - -class Item(BaseModel): - title: str - size: int - description: Union[str, None] = None - sub: SubItem - multi: List[SubItem] = [] - - -class ItemInList(BaseModel): - name1: str diff --git a/tests/test_pydantic_v1_v2_multifile/modelsv2.py b/tests/test_pydantic_v1_v2_multifile/modelsv2.py deleted file mode 100644 index 2c8c6ea356..0000000000 --- a/tests/test_pydantic_v1_v2_multifile/modelsv2.py +++ /dev/null @@ -1,19 +0,0 @@ -from typing import List, Union - -from pydantic import BaseModel - - -class SubItem(BaseModel): - new_sub_name: str - - -class Item(BaseModel): - new_title: str - new_size: int - new_description: Union[str, None] = None - new_sub: SubItem - new_multi: List[SubItem] = [] - - -class ItemInList(BaseModel): - name2: str diff --git a/tests/test_pydantic_v1_v2_multifile/modelsv2b.py b/tests/test_pydantic_v1_v2_multifile/modelsv2b.py deleted file mode 100644 index dc0c06c669..0000000000 --- a/tests/test_pydantic_v1_v2_multifile/modelsv2b.py +++ /dev/null @@ -1,19 +0,0 @@ -from typing import List, Union - -from pydantic import BaseModel - - -class SubItem(BaseModel): - dup_sub_name: str - - -class Item(BaseModel): - dup_title: str - dup_size: int - dup_description: Union[str, None] = None - dup_sub: SubItem - dup_multi: List[SubItem] = [] - - -class ItemInList(BaseModel): - dup_name2: str diff --git a/tests/test_pydantic_v1_v2_multifile/test_multifile.py b/tests/test_pydantic_v1_v2_multifile/test_multifile.py deleted file mode 100644 index e66d102fb3..0000000000 --- a/tests/test_pydantic_v1_v2_multifile/test_multifile.py +++ /dev/null @@ -1,1226 +0,0 @@ -import sys - -from tests.utils import pydantic_snapshot, skip_module_if_py_gte_314 - -if sys.version_info >= (3, 14): - skip_module_if_py_gte_314() - -from fastapi.testclient import TestClient -from inline_snapshot import snapshot - -from .main import app - -client = TestClient(app) - - -def test_v1_to_v2_item(): - response = client.post( - "/v1-to-v2/item", - json={"title": "Test", "size": 10, "sub": {"name": "SubTest"}}, - ) - assert response.status_code == 200 - assert response.json() == { - "new_title": "Test", - "new_size": 10, - "new_description": None, - "new_sub": {"new_sub_name": "SubTest"}, - "new_multi": [], - } - - -def test_v2_to_v1_item(): - response = client.post( - "/v2-to-v1/item", - json={ - "new_title": "NewTest", - "new_size": 20, - "new_sub": {"new_sub_name": "NewSubTest"}, - }, - ) - assert response.status_code == 200 - assert response.json() == { - "title": "NewTest", - "size": 20, - "description": None, - "sub": {"name": "NewSubTest"}, - "multi": [], - } - - -def test_v1_to_v2_item_to_list(): - response = client.post( - "/v1-to-v2/item-to-list", - json={"title": "ListTest", "size": 30, "sub": {"name": "SubListTest"}}, - ) - assert response.status_code == 200 - assert response.json() == [ - { - "new_title": "ListTest", - "new_size": 30, - "new_description": None, - "new_sub": {"new_sub_name": "SubListTest"}, - "new_multi": [], - }, - { - "new_title": "ListTest", - "new_size": 30, - "new_description": None, - "new_sub": {"new_sub_name": "SubListTest"}, - "new_multi": [], - }, - ] - - -def test_v1_to_v2_list_to_list(): - response = client.post( - "/v1-to-v2/list-to-list", - json=[ - {"title": "Item1", "size": 40, "sub": {"name": "Sub1"}}, - {"title": "Item2", "size": 50, "sub": {"name": "Sub2"}}, - ], - ) - assert response.status_code == 200 - assert response.json() == [ - { - "new_title": "Item1", - "new_size": 40, - "new_description": None, - "new_sub": {"new_sub_name": "Sub1"}, - "new_multi": [], - }, - { - "new_title": "Item2", - "new_size": 50, - "new_description": None, - "new_sub": {"new_sub_name": "Sub2"}, - "new_multi": [], - }, - ] - - -def test_v1_to_v2_list_to_item(): - response = client.post( - "/v1-to-v2/list-to-item", - json=[ - {"title": "FirstItem", "size": 60, "sub": {"name": "FirstSub"}}, - {"title": "SecondItem", "size": 70, "sub": {"name": "SecondSub"}}, - ], - ) - assert response.status_code == 200 - assert response.json() == { - "new_title": "FirstItem", - "new_size": 60, - "new_description": None, - "new_sub": {"new_sub_name": "FirstSub"}, - "new_multi": [], - } - - -def test_v2_to_v1_item_to_list(): - response = client.post( - "/v2-to-v1/item-to-list", - json={ - "new_title": "ListNew", - "new_size": 80, - "new_sub": {"new_sub_name": "SubListNew"}, - }, - ) - assert response.status_code == 200 - assert response.json() == [ - { - "title": "ListNew", - "size": 80, - "description": None, - "sub": {"name": "SubListNew"}, - "multi": [], - }, - { - "title": "ListNew", - "size": 80, - "description": None, - "sub": {"name": "SubListNew"}, - "multi": [], - }, - ] - - -def test_v2_to_v1_list_to_list(): - response = client.post( - "/v2-to-v1/list-to-list", - json=[ - { - "new_title": "New1", - "new_size": 90, - "new_sub": {"new_sub_name": "NewSub1"}, - }, - { - "new_title": "New2", - "new_size": 100, - "new_sub": {"new_sub_name": "NewSub2"}, - }, - ], - ) - assert response.status_code == 200 - assert response.json() == [ - { - "title": "New1", - "size": 90, - "description": None, - "sub": {"name": "NewSub1"}, - "multi": [], - }, - { - "title": "New2", - "size": 100, - "description": None, - "sub": {"name": "NewSub2"}, - "multi": [], - }, - ] - - -def test_v2_to_v1_list_to_item(): - response = client.post( - "/v2-to-v1/list-to-item", - json=[ - { - "new_title": "FirstNew", - "new_size": 110, - "new_sub": {"new_sub_name": "FirstNewSub"}, - }, - { - "new_title": "SecondNew", - "new_size": 120, - "new_sub": {"new_sub_name": "SecondNewSub"}, - }, - ], - ) - assert response.status_code == 200 - assert response.json() == { - "title": "FirstNew", - "size": 110, - "description": None, - "sub": {"name": "FirstNewSub"}, - "multi": [], - } - - -def test_v1_to_v2_list_to_item_empty(): - response = client.post("/v1-to-v2/list-to-item", json=[]) - assert response.status_code == 200 - assert response.json() == { - "new_title": "", - "new_size": 0, - "new_description": None, - "new_sub": {"new_sub_name": ""}, - "new_multi": [], - } - - -def test_v2_to_v1_list_to_item_empty(): - response = client.post("/v2-to-v1/list-to-item", json=[]) - assert response.status_code == 200 - assert response.json() == { - "title": "", - "size": 0, - "description": None, - "sub": {"name": ""}, - "multi": [], - } - - -def test_v2_same_name_to_v1(): - response = client.post( - "/v2-to-v1/same-name", - json={ - "item1": { - "new_title": "Title1", - "new_size": 100, - "new_description": "Description1", - "new_sub": {"new_sub_name": "Sub1"}, - "new_multi": [{"new_sub_name": "Multi1"}], - }, - "item2": { - "dup_title": "Title2", - "dup_size": 200, - "dup_description": "Description2", - "dup_sub": {"dup_sub_name": "Sub2"}, - "dup_multi": [ - {"dup_sub_name": "Multi2a"}, - {"dup_sub_name": "Multi2b"}, - ], - }, - }, - ) - assert response.status_code == 200 - assert response.json() == { - "title": "Title1", - "size": 200, - "description": "Description1", - "sub": {"name": "Sub1"}, - "multi": [{"name": "Multi2a"}, {"name": "Multi2b"}], - } - - -def test_v2_items_in_list_to_v1_item_in_list(): - response = client.post( - "/v2-to-v1/list-of-items-to-list-of-items", - json={ - "data1": [{"name2": "Item1"}, {"name2": "Item2"}], - "data2": [{"dup_name2": "Item3"}, {"dup_name2": "Item4"}], - }, - ) - assert response.status_code == 200, response.text - assert response.json() == [ - {"name1": "Item1"}, - {"name1": "Item3"}, - ] - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200 - assert response.json() == snapshot( - { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/v1-to-v2/item": { - "post": { - "summary": "Handle V1 Item To V2", - "operationId": "handle_v1_item_to_v2_v1_to_v2_item_post", - "requestBody": { - "content": { - "application/json": { - "schema": pydantic_snapshot( - v2=snapshot( - { - "allOf": [ - { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item" - } - ], - "title": "Data", - } - ), - v1=snapshot( - { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item" - } - ), - ) - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v2-to-v1/item": { - "post": { - "summary": "Handle V2 Item To V1", - "operationId": "handle_v2_item_to_v1_v2_to_v1_item_post", - "requestBody": { - "content": { - "application/json": { - "schema": pydantic_snapshot( - v2=snapshot( - { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item-Input" - } - ), - v1=snapshot( - { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item" - } - ), - ), - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v1-to-v2/item-to-list": { - "post": { - "summary": "Handle V1 Item To V2 List", - "operationId": "handle_v1_item_to_v2_list_v1_to_v2_item_to_list_post", - "requestBody": { - "content": { - "application/json": { - "schema": pydantic_snapshot( - v2=snapshot( - { - "allOf": [ - { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item" - } - ], - "title": "Data", - } - ), - v1=snapshot( - { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item" - } - ), - ) - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item" - }, - "type": "array", - "title": "Response Handle V1 Item To V2 List V1 To V2 Item To List Post", - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v1-to-v2/list-to-list": { - "post": { - "summary": "Handle V1 List To V2 List", - "operationId": "handle_v1_list_to_v2_list_v1_to_v2_list_to_list_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item" - }, - "type": "array", - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item" - }, - "type": "array", - "title": "Response Handle V1 List To V2 List V1 To V2 List To List Post", - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v1-to-v2/list-to-item": { - "post": { - "summary": "Handle V1 List To V2 Item", - "operationId": "handle_v1_list_to_v2_item_v1_to_v2_list_to_item_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item" - }, - "type": "array", - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v2-to-v1/item-to-list": { - "post": { - "summary": "Handle V2 Item To V1 List", - "operationId": "handle_v2_item_to_v1_list_v2_to_v1_item_to_list_post", - "requestBody": { - "content": { - "application/json": { - "schema": pydantic_snapshot( - v2=snapshot( - { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item-Input" - } - ), - v1=snapshot( - { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item" - } - ), - ), - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item" - }, - "type": "array", - "title": "Response Handle V2 Item To V1 List V2 To V1 Item To List Post", - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v2-to-v1/list-to-list": { - "post": { - "summary": "Handle V2 List To V1 List", - "operationId": "handle_v2_list_to_v1_list_v2_to_v1_list_to_list_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "items": pydantic_snapshot( - v2=snapshot( - { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item-Input" - } - ), - v1=snapshot( - { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item" - } - ), - ), - "type": "array", - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item" - }, - "type": "array", - "title": "Response Handle V2 List To V1 List V2 To V1 List To List Post", - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v2-to-v1/list-to-item": { - "post": { - "summary": "Handle V2 List To V1 Item", - "operationId": "handle_v2_list_to_v1_item_v2_to_v1_list_to_item_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "items": pydantic_snapshot( - v2=snapshot( - { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item-Input" - } - ), - v1=snapshot( - { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item" - } - ), - ), - "type": "array", - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v2-to-v1/same-name": { - "post": { - "summary": "Handle V2 Same Name To V1", - "operationId": "handle_v2_same_name_to_v1_v2_to_v1_same_name_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_handle_v2_same_name_to_v1_v2_to_v1_same_name_post" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v2-to-v1/list-of-items-to-list-of-items": { - "post": { - "summary": "Handle V2 Items In List To V1 Item In List", - "operationId": "handle_v2_items_in_list_to_v1_item_in_list_v2_to_v1_list_of_items_to_list_of_items_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_handle_v2_items_in_list_to_v1_item_in_list_v2_to_v1_list_of_items_to_list_of_items_post" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__ItemInList" - }, - "type": "array", - "title": "Response Handle V2 Items In List To V1 Item In List V2 To V1 List Of Items To List Of Items Post", - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": pydantic_snapshot( - v1=snapshot( - { - "Body_handle_v2_items_in_list_to_v1_item_in_list_v2_to_v1_list_of_items_to_list_of_items_post": { - "properties": { - "data1": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__ItemInList" - }, - "type": "array", - "title": "Data1", - }, - "data2": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2b__ItemInList" - }, - "type": "array", - "title": "Data2", - }, - }, - "type": "object", - "required": ["data1", "data2"], - "title": "Body_handle_v2_items_in_list_to_v1_item_in_list_v2_to_v1_list_of_items_to_list_of_items_post", - }, - "Body_handle_v2_same_name_to_v1_v2_to_v1_same_name_post": { - "properties": { - "item1": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item" - }, - "item2": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2b__Item" - }, - }, - "type": "object", - "required": ["item1", "item2"], - "title": "Body_handle_v2_same_name_to_v1_v2_to_v1_same_name_post", - }, - "HTTPValidationError": { - "properties": { - "detail": { - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - "type": "array", - "title": "Detail", - } - }, - "type": "object", - "title": "HTTPValidationError", - }, - "ValidationError": { - "properties": { - "loc": { - "items": { - "anyOf": [ - {"type": "string"}, - {"type": "integer"}, - ] - }, - "type": "array", - "title": "Location", - }, - "msg": {"type": "string", "title": "Message"}, - "type": {"type": "string", "title": "Error Type"}, - }, - "type": "object", - "required": ["loc", "msg", "type"], - "title": "ValidationError", - }, - "tests__test_pydantic_v1_v2_multifile__modelsv1__Item": { - "properties": { - "title": {"type": "string", "title": "Title"}, - "size": {"type": "integer", "title": "Size"}, - "description": { - "type": "string", - "title": "Description", - }, - "sub": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__SubItem" - }, - "multi": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__SubItem" - }, - "type": "array", - "title": "Multi", - "default": [], - }, - }, - "type": "object", - "required": ["title", "size", "sub"], - "title": "Item", - }, - "tests__test_pydantic_v1_v2_multifile__modelsv1__ItemInList": { - "properties": { - "name1": {"type": "string", "title": "Name1"} - }, - "type": "object", - "required": ["name1"], - "title": "ItemInList", - }, - "tests__test_pydantic_v1_v2_multifile__modelsv1__SubItem": { - "properties": { - "name": {"type": "string", "title": "Name"} - }, - "type": "object", - "required": ["name"], - "title": "SubItem", - }, - "tests__test_pydantic_v1_v2_multifile__modelsv2__Item": { - "properties": { - "new_title": { - "type": "string", - "title": "New Title", - }, - "new_size": { - "type": "integer", - "title": "New Size", - }, - "new_description": { - "type": "string", - "title": "New Description", - }, - "new_sub": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__SubItem" - }, - "new_multi": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__SubItem" - }, - "type": "array", - "title": "New Multi", - "default": [], - }, - }, - "type": "object", - "required": ["new_title", "new_size", "new_sub"], - "title": "Item", - }, - "tests__test_pydantic_v1_v2_multifile__modelsv2__ItemInList": { - "properties": { - "name2": {"type": "string", "title": "Name2"} - }, - "type": "object", - "required": ["name2"], - "title": "ItemInList", - }, - "tests__test_pydantic_v1_v2_multifile__modelsv2__SubItem": { - "properties": { - "new_sub_name": { - "type": "string", - "title": "New Sub Name", - } - }, - "type": "object", - "required": ["new_sub_name"], - "title": "SubItem", - }, - "tests__test_pydantic_v1_v2_multifile__modelsv2b__Item": { - "properties": { - "dup_title": { - "type": "string", - "title": "Dup Title", - }, - "dup_size": { - "type": "integer", - "title": "Dup Size", - }, - "dup_description": { - "type": "string", - "title": "Dup Description", - }, - "dup_sub": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2b__SubItem" - }, - "dup_multi": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2b__SubItem" - }, - "type": "array", - "title": "Dup Multi", - "default": [], - }, - }, - "type": "object", - "required": ["dup_title", "dup_size", "dup_sub"], - "title": "Item", - }, - "tests__test_pydantic_v1_v2_multifile__modelsv2b__ItemInList": { - "properties": { - "dup_name2": { - "type": "string", - "title": "Dup Name2", - } - }, - "type": "object", - "required": ["dup_name2"], - "title": "ItemInList", - }, - "tests__test_pydantic_v1_v2_multifile__modelsv2b__SubItem": { - "properties": { - "dup_sub_name": { - "type": "string", - "title": "Dup Sub Name", - } - }, - "type": "object", - "required": ["dup_sub_name"], - "title": "SubItem", - }, - } - ), - v2=snapshot( - { - "Body_handle_v2_items_in_list_to_v1_item_in_list_v2_to_v1_list_of_items_to_list_of_items_post": { - "properties": { - "data1": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__ItemInList" - }, - "type": "array", - "title": "Data1", - }, - "data2": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2b__ItemInList" - }, - "type": "array", - "title": "Data2", - }, - }, - "type": "object", - "required": ["data1", "data2"], - "title": "Body_handle_v2_items_in_list_to_v1_item_in_list_v2_to_v1_list_of_items_to_list_of_items_post", - }, - "Body_handle_v2_same_name_to_v1_v2_to_v1_same_name_post": { - "properties": { - "item1": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item-Input" - }, - "item2": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2b__Item" - }, - }, - "type": "object", - "required": ["item1", "item2"], - "title": "Body_handle_v2_same_name_to_v1_v2_to_v1_same_name_post", - }, - "HTTPValidationError": { - "properties": { - "detail": { - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - "type": "array", - "title": "Detail", - } - }, - "type": "object", - "title": "HTTPValidationError", - }, - "ValidationError": { - "properties": { - "loc": { - "items": { - "anyOf": [ - {"type": "string"}, - {"type": "integer"}, - ] - }, - "type": "array", - "title": "Location", - }, - "msg": {"type": "string", "title": "Message"}, - "type": {"type": "string", "title": "Error Type"}, - }, - "type": "object", - "required": ["loc", "msg", "type"], - "title": "ValidationError", - }, - "tests__test_pydantic_v1_v2_multifile__modelsv1__Item": { - "properties": { - "title": {"type": "string", "title": "Title"}, - "size": {"type": "integer", "title": "Size"}, - "description": { - "type": "string", - "title": "Description", - }, - "sub": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__SubItem" - }, - "multi": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__SubItem" - }, - "type": "array", - "title": "Multi", - "default": [], - }, - }, - "type": "object", - "required": ["title", "size", "sub"], - "title": "Item", - }, - "tests__test_pydantic_v1_v2_multifile__modelsv1__ItemInList": { - "properties": { - "name1": {"type": "string", "title": "Name1"} - }, - "type": "object", - "required": ["name1"], - "title": "ItemInList", - }, - "tests__test_pydantic_v1_v2_multifile__modelsv1__SubItem": { - "properties": { - "name": {"type": "string", "title": "Name"} - }, - "type": "object", - "required": ["name"], - "title": "SubItem", - }, - "tests__test_pydantic_v1_v2_multifile__modelsv2__Item": { - "properties": { - "new_title": { - "type": "string", - "title": "New Title", - }, - "new_size": { - "type": "integer", - "title": "New Size", - }, - "new_description": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "New Description", - }, - "new_sub": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__SubItem" - }, - "new_multi": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__SubItem" - }, - "type": "array", - "title": "New Multi", - "default": [], - }, - }, - "type": "object", - "required": ["new_title", "new_size", "new_sub"], - "title": "Item", - }, - "tests__test_pydantic_v1_v2_multifile__modelsv2__Item-Input": { - "properties": { - "new_title": { - "type": "string", - "title": "New Title", - }, - "new_size": { - "type": "integer", - "title": "New Size", - }, - "new_description": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "New Description", - }, - "new_sub": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__SubItem" - }, - "new_multi": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__SubItem" - }, - "type": "array", - "title": "New Multi", - "default": [], - }, - }, - "type": "object", - "required": ["new_title", "new_size", "new_sub"], - "title": "Item", - }, - "tests__test_pydantic_v1_v2_multifile__modelsv2__ItemInList": { - "properties": { - "name2": {"type": "string", "title": "Name2"} - }, - "type": "object", - "required": ["name2"], - "title": "ItemInList", - }, - "tests__test_pydantic_v1_v2_multifile__modelsv2__SubItem": { - "properties": { - "new_sub_name": { - "type": "string", - "title": "New Sub Name", - } - }, - "type": "object", - "required": ["new_sub_name"], - "title": "SubItem", - }, - "tests__test_pydantic_v1_v2_multifile__modelsv2b__Item": { - "properties": { - "dup_title": { - "type": "string", - "title": "Dup Title", - }, - "dup_size": { - "type": "integer", - "title": "Dup Size", - }, - "dup_description": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Dup Description", - }, - "dup_sub": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2b__SubItem" - }, - "dup_multi": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2b__SubItem" - }, - "type": "array", - "title": "Dup Multi", - "default": [], - }, - }, - "type": "object", - "required": ["dup_title", "dup_size", "dup_sub"], - "title": "Item", - }, - "tests__test_pydantic_v1_v2_multifile__modelsv2b__ItemInList": { - "properties": { - "dup_name2": { - "type": "string", - "title": "Dup Name2", - } - }, - "type": "object", - "required": ["dup_name2"], - "title": "ItemInList", - }, - "tests__test_pydantic_v1_v2_multifile__modelsv2b__SubItem": { - "properties": { - "dup_sub_name": { - "type": "string", - "title": "Dup Sub Name", - } - }, - "type": "object", - "required": ["dup_sub_name"], - "title": "SubItem", - }, - } - ), - ), - }, - } - ) diff --git a/tests/test_pydantic_v1_v2_noneable.py b/tests/test_pydantic_v1_v2_noneable.py deleted file mode 100644 index d2d6f6635b..0000000000 --- a/tests/test_pydantic_v1_v2_noneable.py +++ /dev/null @@ -1,766 +0,0 @@ -import sys -from typing import Any, List, Union - -from tests.utils import pydantic_snapshot, skip_module_if_py_gte_314 - -if sys.version_info >= (3, 14): - skip_module_if_py_gte_314() - -from fastapi import FastAPI -from fastapi._compat.v1 import BaseModel -from fastapi.testclient import TestClient -from inline_snapshot import snapshot -from pydantic import BaseModel as NewBaseModel - - -class SubItem(BaseModel): - name: str - - -class Item(BaseModel): - title: str - size: int - description: Union[str, None] = None - sub: SubItem - multi: List[SubItem] = [] - - -class NewSubItem(NewBaseModel): - new_sub_name: str - - -class NewItem(NewBaseModel): - new_title: str - new_size: int - new_description: Union[str, None] = None - new_sub: NewSubItem - new_multi: List[NewSubItem] = [] - - -app = FastAPI() - - -@app.post("/v1-to-v2/") -def handle_v1_item_to_v2(data: Item) -> Union[NewItem, None]: - if data.size < 0: - return None - return NewItem( - new_title=data.title, - new_size=data.size, - new_description=data.description, - new_sub=NewSubItem(new_sub_name=data.sub.name), - new_multi=[NewSubItem(new_sub_name=s.name) for s in data.multi], - ) - - -@app.post("/v1-to-v2/item-filter", response_model=Union[NewItem, None]) -def handle_v1_item_to_v2_filter(data: Item) -> Any: - if data.size < 0: - return None - result = { - "new_title": data.title, - "new_size": data.size, - "new_description": data.description, - "new_sub": {"new_sub_name": data.sub.name, "new_sub_secret": "sub_hidden"}, - "new_multi": [ - {"new_sub_name": s.name, "new_sub_secret": "sub_hidden"} for s in data.multi - ], - "secret": "hidden_v1_to_v2", - } - return result - - -@app.post("/v2-to-v1/item") -def handle_v2_item_to_v1(data: NewItem) -> Union[Item, None]: - if data.new_size < 0: - return None - return Item( - title=data.new_title, - size=data.new_size, - description=data.new_description, - sub=SubItem(name=data.new_sub.new_sub_name), - multi=[SubItem(name=s.new_sub_name) for s in data.new_multi], - ) - - -@app.post("/v2-to-v1/item-filter", response_model=Union[Item, None]) -def handle_v2_item_to_v1_filter(data: NewItem) -> Any: - if data.new_size < 0: - return None - result = { - "title": data.new_title, - "size": data.new_size, - "description": data.new_description, - "sub": {"name": data.new_sub.new_sub_name, "sub_secret": "sub_hidden"}, - "multi": [ - {"name": s.new_sub_name, "sub_secret": "sub_hidden"} for s in data.new_multi - ], - "secret": "hidden_v2_to_v1", - } - return result - - -client = TestClient(app) - - -def test_v1_to_v2_item_success(): - response = client.post( - "/v1-to-v2/", - json={ - "title": "Old Item", - "size": 100, - "description": "V1 description", - "sub": {"name": "V1 Sub"}, - "multi": [{"name": "M1"}, {"name": "M2"}], - }, - ) - assert response.status_code == 200, response.text - assert response.json() == { - "new_title": "Old Item", - "new_size": 100, - "new_description": "V1 description", - "new_sub": {"new_sub_name": "V1 Sub"}, - "new_multi": [{"new_sub_name": "M1"}, {"new_sub_name": "M2"}], - } - - -def test_v1_to_v2_item_returns_none(): - response = client.post( - "/v1-to-v2/", - json={"title": "Invalid Item", "size": -10, "sub": {"name": "Sub"}}, - ) - assert response.status_code == 200, response.text - assert response.json() is None - - -def test_v1_to_v2_item_minimal(): - response = client.post( - "/v1-to-v2/", json={"title": "Minimal", "size": 50, "sub": {"name": "MinSub"}} - ) - assert response.status_code == 200, response.text - assert response.json() == { - "new_title": "Minimal", - "new_size": 50, - "new_description": None, - "new_sub": {"new_sub_name": "MinSub"}, - "new_multi": [], - } - - -def test_v1_to_v2_item_filter_success(): - response = client.post( - "/v1-to-v2/item-filter", - json={ - "title": "Filtered Item", - "size": 50, - "sub": {"name": "Sub"}, - "multi": [{"name": "Multi1"}], - }, - ) - assert response.status_code == 200, response.text - result = response.json() - assert result["new_title"] == "Filtered Item" - assert result["new_size"] == 50 - assert result["new_sub"]["new_sub_name"] == "Sub" - assert result["new_multi"][0]["new_sub_name"] == "Multi1" - # Verify secret fields are filtered out - assert "secret" not in result - assert "new_sub_secret" not in result["new_sub"] - assert "new_sub_secret" not in result["new_multi"][0] - - -def test_v1_to_v2_item_filter_returns_none(): - response = client.post( - "/v1-to-v2/item-filter", - json={"title": "Invalid", "size": -1, "sub": {"name": "Sub"}}, - ) - assert response.status_code == 200, response.text - assert response.json() is None - - -def test_v2_to_v1_item_success(): - response = client.post( - "/v2-to-v1/item", - json={ - "new_title": "New Item", - "new_size": 200, - "new_description": "V2 description", - "new_sub": {"new_sub_name": "V2 Sub"}, - "new_multi": [{"new_sub_name": "N1"}, {"new_sub_name": "N2"}], - }, - ) - assert response.status_code == 200, response.text - assert response.json() == { - "title": "New Item", - "size": 200, - "description": "V2 description", - "sub": {"name": "V2 Sub"}, - "multi": [{"name": "N1"}, {"name": "N2"}], - } - - -def test_v2_to_v1_item_returns_none(): - response = client.post( - "/v2-to-v1/item", - json={ - "new_title": "Invalid New", - "new_size": -5, - "new_sub": {"new_sub_name": "NewSub"}, - }, - ) - assert response.status_code == 200, response.text - assert response.json() is None - - -def test_v2_to_v1_item_minimal(): - response = client.post( - "/v2-to-v1/item", - json={ - "new_title": "MinimalNew", - "new_size": 75, - "new_sub": {"new_sub_name": "MinNewSub"}, - }, - ) - assert response.status_code == 200, response.text - assert response.json() == { - "title": "MinimalNew", - "size": 75, - "description": None, - "sub": {"name": "MinNewSub"}, - "multi": [], - } - - -def test_v2_to_v1_item_filter_success(): - response = client.post( - "/v2-to-v1/item-filter", - json={ - "new_title": "Filtered New", - "new_size": 75, - "new_sub": {"new_sub_name": "NewSub"}, - "new_multi": [], - }, - ) - assert response.status_code == 200, response.text - result = response.json() - assert result["title"] == "Filtered New" - assert result["size"] == 75 - assert result["sub"]["name"] == "NewSub" - # Verify secret fields are filtered out - assert "secret" not in result - assert "sub_secret" not in result["sub"] - - -def test_v2_to_v1_item_filter_returns_none(): - response = client.post( - "/v2-to-v1/item-filter", - json={ - "new_title": "Invalid Filtered", - "new_size": -100, - "new_sub": {"new_sub_name": "Sub"}, - }, - ) - assert response.status_code == 200, response.text - assert response.json() is None - - -def test_v1_to_v2_validation_error(): - response = client.post("/v1-to-v2/", json={"title": "Missing fields"}) - assert response.status_code == 422, response.text - assert response.json() == snapshot( - { - "detail": [ - { - "loc": ["body", "size"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "sub"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -def test_v1_to_v2_nested_validation_error(): - response = client.post( - "/v1-to-v2/", - json={"title": "Bad sub", "size": 100, "sub": {"wrong_field": "value"}}, - ) - assert response.status_code == 422, response.text - error_detail = response.json()["detail"] - assert len(error_detail) == 1 - assert error_detail[0]["loc"] == ["body", "sub", "name"] - - -def test_v1_to_v2_type_validation_error(): - response = client.post( - "/v1-to-v2/", - json={"title": "Bad type", "size": "not_a_number", "sub": {"name": "Sub"}}, - ) - assert response.status_code == 422, response.text - error_detail = response.json()["detail"] - assert len(error_detail) == 1 - assert error_detail[0]["loc"] == ["body", "size"] - - -def test_v2_to_v1_validation_error(): - response = client.post("/v2-to-v1/item", json={"new_title": "Missing fields"}) - assert response.status_code == 422, response.text - assert response.json() == snapshot( - { - "detail": pydantic_snapshot( - v2=snapshot( - [ - { - "type": "missing", - "loc": ["body", "new_size"], - "msg": "Field required", - "input": {"new_title": "Missing fields"}, - }, - { - "type": "missing", - "loc": ["body", "new_sub"], - "msg": "Field required", - "input": {"new_title": "Missing fields"}, - }, - ] - ), - v1=snapshot( - [ - { - "loc": ["body", "new_size"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "new_sub"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - ), - ) - } - ) - - -def test_v2_to_v1_nested_validation_error(): - response = client.post( - "/v2-to-v1/item", - json={ - "new_title": "Bad sub", - "new_size": 200, - "new_sub": {"wrong_field": "value"}, - }, - ) - assert response.status_code == 422, response.text - assert response.json() == snapshot( - { - "detail": [ - pydantic_snapshot( - v2=snapshot( - { - "type": "missing", - "loc": ["body", "new_sub", "new_sub_name"], - "msg": "Field required", - "input": {"wrong_field": "value"}, - } - ), - v1=snapshot( - { - "loc": ["body", "new_sub", "new_sub_name"], - "msg": "field required", - "type": "value_error.missing", - } - ), - ) - ] - } - ) - - -def test_v2_to_v1_type_validation_error(): - response = client.post( - "/v2-to-v1/item", - json={ - "new_title": "Bad type", - "new_size": "not_a_number", - "new_sub": {"new_sub_name": "Sub"}, - }, - ) - assert response.status_code == 422, response.text - assert response.json() == snapshot( - { - "detail": [ - pydantic_snapshot( - v2=snapshot( - { - "type": "int_parsing", - "loc": ["body", "new_size"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "not_a_number", - } - ), - v1=snapshot( - { - "loc": ["body", "new_size"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ), - ) - ] - } - ) - - -def test_v1_to_v2_with_multi_items(): - response = client.post( - "/v1-to-v2/", - json={ - "title": "Complex Item", - "size": 300, - "description": "Item with multiple sub-items", - "sub": {"name": "Main Sub"}, - "multi": [{"name": "Sub1"}, {"name": "Sub2"}, {"name": "Sub3"}], - }, - ) - assert response.status_code == 200, response.text - assert response.json() == snapshot( - { - "new_title": "Complex Item", - "new_size": 300, - "new_description": "Item with multiple sub-items", - "new_sub": {"new_sub_name": "Main Sub"}, - "new_multi": [ - {"new_sub_name": "Sub1"}, - {"new_sub_name": "Sub2"}, - {"new_sub_name": "Sub3"}, - ], - } - ) - - -def test_v2_to_v1_with_multi_items(): - response = client.post( - "/v2-to-v1/item", - json={ - "new_title": "Complex New Item", - "new_size": 400, - "new_description": "New item with multiple sub-items", - "new_sub": {"new_sub_name": "Main New Sub"}, - "new_multi": [{"new_sub_name": "NewSub1"}, {"new_sub_name": "NewSub2"}], - }, - ) - assert response.status_code == 200, response.text - assert response.json() == snapshot( - { - "title": "Complex New Item", - "size": 400, - "description": "New item with multiple sub-items", - "sub": {"name": "Main New Sub"}, - "multi": [{"name": "NewSub1"}, {"name": "NewSub2"}], - } - ) - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == snapshot( - { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/v1-to-v2/": { - "post": { - "summary": "Handle V1 Item To V2", - "operationId": "handle_v1_item_to_v2_v1_to_v2__post", - "requestBody": { - "content": { - "application/json": { - "schema": pydantic_snapshot( - v2=snapshot( - { - "allOf": [ - { - "$ref": "#/components/schemas/Item" - } - ], - "title": "Data", - } - ), - v1=snapshot( - {"$ref": "#/components/schemas/Item"} - ), - ) - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": pydantic_snapshot( - v2=snapshot( - { - "anyOf": [ - { - "$ref": "#/components/schemas/NewItem" - }, - {"type": "null"}, - ], - "title": "Response Handle V1 Item To V2 V1 To V2 Post", - } - ), - v1=snapshot( - {"$ref": "#/components/schemas/NewItem"} - ), - ) - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v1-to-v2/item-filter": { - "post": { - "summary": "Handle V1 Item To V2 Filter", - "operationId": "handle_v1_item_to_v2_filter_v1_to_v2_item_filter_post", - "requestBody": { - "content": { - "application/json": { - "schema": pydantic_snapshot( - v2=snapshot( - { - "allOf": [ - { - "$ref": "#/components/schemas/Item" - } - ], - "title": "Data", - } - ), - v1=snapshot( - {"$ref": "#/components/schemas/Item"} - ), - ) - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": pydantic_snapshot( - v2=snapshot( - { - "anyOf": [ - { - "$ref": "#/components/schemas/NewItem" - }, - {"type": "null"}, - ], - "title": "Response Handle V1 Item To V2 Filter V1 To V2 Item Filter Post", - } - ), - v1=snapshot( - {"$ref": "#/components/schemas/NewItem"} - ), - ) - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v2-to-v1/item": { - "post": { - "summary": "Handle V2 Item To V1", - "operationId": "handle_v2_item_to_v1_v2_to_v1_item_post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/NewItem"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v2-to-v1/item-filter": { - "post": { - "summary": "Handle V2 Item To V1 Filter", - "operationId": "handle_v2_item_to_v1_filter_v2_to_v1_item_filter_post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/NewItem"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "properties": { - "detail": { - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - "type": "array", - "title": "Detail", - } - }, - "type": "object", - "title": "HTTPValidationError", - }, - "Item": { - "properties": { - "title": {"type": "string", "title": "Title"}, - "size": {"type": "integer", "title": "Size"}, - "description": {"type": "string", "title": "Description"}, - "sub": {"$ref": "#/components/schemas/SubItem"}, - "multi": { - "items": {"$ref": "#/components/schemas/SubItem"}, - "type": "array", - "title": "Multi", - "default": [], - }, - }, - "type": "object", - "required": ["title", "size", "sub"], - "title": "Item", - }, - "NewItem": { - "properties": { - "new_title": {"type": "string", "title": "New Title"}, - "new_size": {"type": "integer", "title": "New Size"}, - "new_description": pydantic_snapshot( - v2=snapshot( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "New Description", - } - ), - v1=snapshot( - {"type": "string", "title": "New Description"} - ), - ), - "new_sub": {"$ref": "#/components/schemas/NewSubItem"}, - "new_multi": { - "items": {"$ref": "#/components/schemas/NewSubItem"}, - "type": "array", - "title": "New Multi", - "default": [], - }, - }, - "type": "object", - "required": ["new_title", "new_size", "new_sub"], - "title": "NewItem", - }, - "NewSubItem": { - "properties": { - "new_sub_name": {"type": "string", "title": "New Sub Name"} - }, - "type": "object", - "required": ["new_sub_name"], - "title": "NewSubItem", - }, - "SubItem": { - "properties": {"name": {"type": "string", "title": "Name"}}, - "type": "object", - "required": ["name"], - "title": "SubItem", - }, - "ValidationError": { - "properties": { - "loc": { - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - "type": "array", - "title": "Location", - }, - "msg": {"type": "string", "title": "Message"}, - "type": {"type": "string", "title": "Error Type"}, - }, - "type": "object", - "required": ["loc", "msg", "type"], - "title": "ValidationError", - }, - } - }, - } - ) diff --git a/tests/test_pydanticv2_dataclasses_uuid_stringified_annotations.py b/tests/test_pydanticv2_dataclasses_uuid_stringified_annotations.py new file mode 100644 index 0000000000..b72b0518a1 --- /dev/null +++ b/tests/test_pydanticv2_dataclasses_uuid_stringified_annotations.py @@ -0,0 +1,52 @@ +from __future__ import annotations + +import uuid +from dataclasses import dataclass, field +from typing import Union + +from dirty_equals import IsUUID +from fastapi import FastAPI +from fastapi.testclient import TestClient +from inline_snapshot import snapshot + + +@dataclass +class Item: + id: uuid.UUID + name: str + price: float + tags: list[str] = field(default_factory=list) + description: Union[str, None] = None + tax: Union[float, None] = None + + +app = FastAPI() + + +@app.get("/item", response_model=Item) +async def read_item(): + return { + "id": uuid.uuid4(), + "name": "Island In The Moon", + "price": 12.99, + "description": "A place to be be playin' and havin' fun", + "tags": ["breater"], + } + + +client = TestClient(app) + + +def test_annotations(): + response = client.get("/item") + assert response.status_code == 200, response.text + assert response.json() == snapshot( + { + "id": IsUUID(), + "name": "Island In The Moon", + "price": 12.99, + "tags": ["breater"], + "description": "A place to be be playin' and havin' fun", + "tax": None, + } + ) diff --git a/tests/test_query.py b/tests/test_query.py index 57f551d2ab..c25960caca 100644 --- a/tests/test_query.py +++ b/tests/test_query.py @@ -1,4 +1,3 @@ -from dirty_equals import IsDict from fastapi.testclient import TestClient from .main import app @@ -9,29 +8,16 @@ client = TestClient(app) def test_query(): response = client.get("/query") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "query"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "query"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "query"], + "msg": "Field required", + "input": None, + } + ] + } def test_query_query_baz(): @@ -43,29 +29,16 @@ def test_query_query_baz(): def test_query_not_declared_baz(): response = client.get("/query?not_declared=baz") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "query"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "query"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "query"], + "msg": "Field required", + "input": None, + } + ] + } def test_query_optional(): @@ -89,29 +62,16 @@ def test_query_optional_not_declared_baz(): def test_query_int(): response = client.get("/query/int") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "query"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "query"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "query"], + "msg": "Field required", + "input": None, + } + ] + } def test_query_int_query_42(): @@ -123,85 +83,46 @@ def test_query_int_query_42(): def test_query_int_query_42_5(): response = client.get("/query/int?query=42.5") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["query", "query"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "42.5", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "query"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "int_parsing", + "loc": ["query", "query"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "42.5", + } + ] + } def test_query_int_query_baz(): response = client.get("/query/int?query=baz") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["query", "query"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "baz", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "query"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "int_parsing", + "loc": ["query", "query"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "baz", + } + ] + } def test_query_int_not_declared_baz(): response = client.get("/query/int?not_declared=baz") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "query"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "query"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "query"], + "msg": "Field required", + "input": None, + } + ] + } def test_query_int_optional(): @@ -219,29 +140,16 @@ def test_query_int_optional_query_50(): def test_query_int_optional_query_foo(): response = client.get("/query/int/optional?query=foo") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["query", "query"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "foo", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "query"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "int_parsing", + "loc": ["query", "query"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "foo", + } + ] + } def test_query_int_default(): @@ -259,29 +167,16 @@ def test_query_int_default_query_50(): def test_query_int_default_query_foo(): response = client.get("/query/int/default?query=foo") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["query", "query"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "foo", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "query"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "int_parsing", + "loc": ["query", "query"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "foo", + } + ] + } def test_query_param(): @@ -299,29 +194,16 @@ def test_query_param_query_50(): def test_query_param_required(): response = client.get("/query/param-required") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "query"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "query"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "query"], + "msg": "Field required", + "input": None, + } + ] + } def test_query_param_required_query_50(): @@ -333,29 +215,16 @@ def test_query_param_required_query_50(): def test_query_param_required_int(): response = client.get("/query/param-required/int") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "query"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "query"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "query"], + "msg": "Field required", + "input": None, + } + ] + } def test_query_param_required_int_query_50(): @@ -367,29 +236,16 @@ def test_query_param_required_int_query_50(): def test_query_param_required_int_query_foo(): response = client.get("/query/param-required/int?query=foo") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["query", "query"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "foo", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "query"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "int_parsing", + "loc": ["query", "query"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "foo", + } + ] + } def test_query_frozenset_query_1_query_1_query_2(): diff --git a/tests/test_query_cookie_header_model_extra_params.py b/tests/test_query_cookie_header_model_extra_params.py new file mode 100644 index 0000000000..d361e1e533 --- /dev/null +++ b/tests/test_query_cookie_header_model_extra_params.py @@ -0,0 +1,105 @@ +from fastapi import Cookie, FastAPI, Header, Query +from fastapi.testclient import TestClient +from pydantic import BaseModel + +app = FastAPI() + + +class Model(BaseModel): + param: str + + model_config = {"extra": "allow"} + + +@app.get("/query") +async def query_model_with_extra(data: Model = Query()): + return data + + +@app.get("/header") +async def header_model_with_extra(data: Model = Header()): + return data + + +@app.get("/cookie") +async def cookies_model_with_extra(data: Model = Cookie()): + return data + + +def test_query_pass_extra_list(): + client = TestClient(app) + resp = client.get( + "/query", + params={ + "param": "123", + "param2": ["456", "789"], # Pass a list of values as extra parameter + }, + ) + assert resp.status_code == 200 + assert resp.json() == { + "param": "123", + "param2": ["456", "789"], + } + + +def test_query_pass_extra_single(): + client = TestClient(app) + resp = client.get( + "/query", + params={ + "param": "123", + "param2": "456", + }, + ) + assert resp.status_code == 200 + assert resp.json() == { + "param": "123", + "param2": "456", + } + + +def test_header_pass_extra_list(): + client = TestClient(app) + + resp = client.get( + "/header", + headers=[ + ("param", "123"), + ("param2", "456"), # Pass a list of values as extra parameter + ("param2", "789"), + ], + ) + assert resp.status_code == 200 + resp_json = resp.json() + assert "param2" in resp_json + assert resp_json["param2"] == ["456", "789"] + + +def test_header_pass_extra_single(): + client = TestClient(app) + + resp = client.get( + "/header", + headers=[ + ("param", "123"), + ("param2", "456"), + ], + ) + assert resp.status_code == 200 + resp_json = resp.json() + assert "param2" in resp_json + assert resp_json["param2"] == "456" + + +def test_cookie_pass_extra_list(): + client = TestClient(app) + client.cookies = [ + ("param", "123"), + ("param2", "456"), # Pass a list of values as extra parameter + ("param2", "789"), + ] + resp = client.get("/cookie") + assert resp.status_code == 200 + resp_json = resp.json() + assert "param2" in resp_json + assert resp_json["param2"] == "789" # Cookies only keep the last value diff --git a/tests/test_read_with_orm_mode.py b/tests/test_read_with_orm_mode.py index b359874439..cd7389252a 100644 --- a/tests/test_read_with_orm_mode.py +++ b/tests/test_read_with_orm_mode.py @@ -4,10 +4,7 @@ from fastapi import FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel, ConfigDict -from .utils import needs_pydanticv1, needs_pydanticv2 - -@needs_pydanticv2 def test_read_with_orm_mode() -> None: class PersonBase(BaseModel): name: str @@ -44,45 +41,3 @@ def test_read_with_orm_mode() -> None: assert data["name"] == person_data["name"] assert data["lastname"] == person_data["lastname"] assert data["full_name"] == person_data["name"] + " " + person_data["lastname"] - - -@needs_pydanticv1 -def test_read_with_orm_mode_pv1() -> None: - class PersonBase(BaseModel): - name: str - lastname: str - - class Person(PersonBase): - @property - def full_name(self) -> str: - return f"{self.name} {self.lastname}" - - class Config: - orm_mode = True - read_with_orm_mode = True - - class PersonCreate(PersonBase): - pass - - class PersonRead(PersonBase): - full_name: str - - class Config: - orm_mode = True - - app = FastAPI() - - @app.post("/people/", response_model=PersonRead) - def create_person(person: PersonCreate) -> Any: - db_person = Person.from_orm(person) - return db_person - - client = TestClient(app) - - person_data = {"name": "Dive", "lastname": "Wilson"} - response = client.post("/people/", json=person_data) - data = response.json() - assert response.status_code == 200, response.text - assert data["name"] == person_data["name"] - assert data["lastname"] == person_data["lastname"] - assert data["full_name"] == person_data["name"] + " " + person_data["lastname"] diff --git a/tests/test_regex_deprecated_body.py b/tests/test_regex_deprecated_body.py index 74654ff3ce..5b4daa450f 100644 --- a/tests/test_regex_deprecated_body.py +++ b/tests/test_regex_deprecated_body.py @@ -1,15 +1,17 @@ +from typing import Annotated + import pytest -from dirty_equals import IsDict from fastapi import FastAPI, Form +from fastapi.exceptions import FastAPIDeprecationWarning from fastapi.testclient import TestClient -from typing_extensions import Annotated +from inline_snapshot import snapshot from .utils import needs_py310 def get_client(): app = FastAPI() - with pytest.warns(DeprecationWarning): + with pytest.warns(FastAPIDeprecationWarning): @app.post("/items/") async def read_items( @@ -45,31 +47,17 @@ def test_query_nonregexquery(): client = get_client() response = client.post("/items/", data={"q": "nonregexquery"}) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "string_pattern_mismatch", - "loc": ["body", "q"], - "msg": "String should match pattern '^fixedquery$'", - "input": "nonregexquery", - "ctx": {"pattern": "^fixedquery$"}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "ctx": {"pattern": "^fixedquery$"}, - "loc": ["body", "q"], - "msg": 'string does not match regex "^fixedquery$"', - "type": "value_error.str.regex", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "string_pattern_mismatch", + "loc": ["body", "q"], + "msg": "String should match pattern '^fixedquery$'", + "input": "nonregexquery", + "ctx": {"pattern": "^fixedquery$"}, + } + ] + } @needs_py310 @@ -77,104 +65,88 @@ def test_openapi_schema(): client = get_client() response = client.get("/openapi.json") assert response.status_code == 200, response.text - # insert_assert(response.json()) - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "post": { - "summary": "Read Items", - "operationId": "read_items_items__post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": IsDict( - { - "allOf": [ - { - "$ref": "#/components/schemas/Body_read_items_items__post" - } - ], - "title": "Body", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "post": { + "summary": "Read Items", + "operationId": "read_items_items__post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { "$ref": "#/components/schemas/Body_read_items_items__post" } - ) - } - } - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, }, }, - }, + } } - } - }, - "components": { - "schemas": { - "Body_read_items_items__post": { - "properties": { - "q": IsDict( - { + }, + "components": { + "schemas": { + "Body_read_items_items__post": { + "properties": { + "q": { "anyOf": [ {"type": "string", "pattern": "^fixedquery$"}, {"type": "null"}, ], "title": "Q", } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"type": "string", "pattern": "^fixedquery$", "title": "Q"} - ) - }, - "type": "object", - "title": "Body_read_items_items__post", - }, - "HTTPValidationError": { - "properties": { - "detail": { - "items": {"$ref": "#/components/schemas/ValidationError"}, - "type": "array", - "title": "Detail", - } - }, - "type": "object", - "title": "HTTPValidationError", - }, - "ValidationError": { - "properties": { - "loc": { - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - "type": "array", - "title": "Location", }, - "msg": {"type": "string", "title": "Message"}, - "type": {"type": "string", "title": "Error Type"}, + "type": "object", + "title": "Body_read_items_items__post", }, - "type": "object", - "required": ["loc", "msg", "type"], - "title": "ValidationError", - }, - } - }, - } + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } + ) diff --git a/tests/test_regex_deprecated_params.py b/tests/test_regex_deprecated_params.py index 2ce64c6862..d6eaa45fb1 100644 --- a/tests/test_regex_deprecated_params.py +++ b/tests/test_regex_deprecated_params.py @@ -1,15 +1,16 @@ +from typing import Annotated + import pytest -from dirty_equals import IsDict from fastapi import FastAPI, Query +from fastapi.exceptions import FastAPIDeprecationWarning from fastapi.testclient import TestClient -from typing_extensions import Annotated from .utils import needs_py310 def get_client(): app = FastAPI() - with pytest.warns(DeprecationWarning): + with pytest.warns(FastAPIDeprecationWarning): @app.get("/items/") async def read_items( @@ -45,31 +46,17 @@ def test_query_params_str_validations_item_query_nonregexquery(): client = get_client() response = client.get("/items/", params={"q": "nonregexquery"}) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "string_pattern_mismatch", - "loc": ["query", "q"], - "msg": "String should match pattern '^fixedquery$'", - "input": "nonregexquery", - "ctx": {"pattern": "^fixedquery$"}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "ctx": {"pattern": "^fixedquery$"}, - "loc": ["query", "q"], - "msg": 'string does not match regex "^fixedquery$"', - "type": "value_error.str.regex", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "string_pattern_mismatch", + "loc": ["query", "q"], + "msg": "String should match pattern '^fixedquery$'", + "input": "nonregexquery", + "ctx": {"pattern": "^fixedquery$"}, + } + ] + } @needs_py310 @@ -91,23 +78,13 @@ def test_openapi_schema(): "name": "q", "in": "query", "required": False, - "schema": IsDict( - { - "anyOf": [ - {"type": "string", "pattern": "^fixedquery$"}, - {"type": "null"}, - ], - "title": "Q", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "string", - "pattern": "^fixedquery$", - "title": "Q", - } - ), + "schema": { + "anyOf": [ + {"type": "string", "pattern": "^fixedquery$"}, + {"type": "null"}, + ], + "title": "Q", + }, } ], "responses": { diff --git a/tests/test_request_body_parameters_media_type.py b/tests/test_request_body_parameters_media_type.py index 8c72fee54a..d1bff9ddf4 100644 --- a/tests/test_request_body_parameters_media_type.py +++ b/tests/test_request_body_parameters_media_type.py @@ -1,5 +1,3 @@ -import typing - from fastapi import Body, FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel @@ -28,7 +26,7 @@ async def create_product(data: Product = Body(media_type=media_type, embed=True) @app.post("/shops") async def create_shop( data: Shop = Body(media_type=media_type), - included: typing.List[Product] = Body(default=[], media_type=media_type), + included: list[Product] = Body(default=[], media_type=media_type), ): pass # pragma: no cover diff --git a/tests/test_request_param_model_by_alias.py b/tests/test_request_param_model_by_alias.py new file mode 100644 index 0000000000..c29130d7a6 --- /dev/null +++ b/tests/test_request_param_model_by_alias.py @@ -0,0 +1,72 @@ +from dirty_equals import IsPartialDict +from fastapi import Cookie, FastAPI, Header, Query +from fastapi.testclient import TestClient +from pydantic import BaseModel, Field + +app = FastAPI() + + +class Model(BaseModel): + param: str = Field(alias="param_alias") + + +@app.get("/query") +async def query_model(data: Model = Query()): + return {"param": data.param} + + +@app.get("/header") +async def header_model(data: Model = Header()): + return {"param": data.param} + + +@app.get("/cookie") +async def cookie_model(data: Model = Cookie()): + return {"param": data.param} + + +def test_query_model_with_alias(): + client = TestClient(app) + response = client.get("/query", params={"param_alias": "value"}) + assert response.status_code == 200, response.text + assert response.json() == {"param": "value"} + + +def test_header_model_with_alias(): + client = TestClient(app) + response = client.get("/header", headers={"param_alias": "value"}) + assert response.status_code == 200, response.text + assert response.json() == {"param": "value"} + + +def test_cookie_model_with_alias(): + client = TestClient(app) + client.cookies.set("param_alias", "value") + response = client.get("/cookie") + assert response.status_code == 200, response.text + assert response.json() == {"param": "value"} + + +def test_query_model_with_alias_by_name(): + client = TestClient(app) + response = client.get("/query", params={"param": "value"}) + assert response.status_code == 422, response.text + details = response.json() + assert details["detail"][0]["input"] == {"param": "value"} + + +def test_header_model_with_alias_by_name(): + client = TestClient(app) + response = client.get("/header", headers={"param": "value"}) + assert response.status_code == 422, response.text + details = response.json() + assert details["detail"][0]["input"] == IsPartialDict({"param": "value"}) + + +def test_cookie_model_with_alias_by_name(): + client = TestClient(app) + client.cookies.set("param", "value") + response = client.get("/cookie") + assert response.status_code == 422, response.text + details = response.json() + assert details["detail"][0]["input"] == {"param": "value"} diff --git a/tests/test_request_params/__init__.py b/tests/test_request_params/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/test_request_params/test_body/__init__.py b/tests/test_request_params/test_body/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/test_request_params/test_body/test_list.py b/tests/test_request_params/test_body/test_list.py new file mode 100644 index 0000000000..970e6a6607 --- /dev/null +++ b/tests/test_request_params/test_body/test_list.py @@ -0,0 +1,433 @@ +from typing import Annotated, Union + +import pytest +from dirty_equals import IsOneOf, IsPartialDict +from fastapi import Body, FastAPI +from fastapi.testclient import TestClient +from pydantic import BaseModel, Field + +from .utils import get_body_model_name + +app = FastAPI() + +# ===================================================================================== +# Without aliases + + +@app.post("/required-list-str", operation_id="required_list_str") +async def read_required_list_str(p: Annotated[list[str], Body(embed=True)]): + return {"p": p} + + +class BodyModelRequiredListStr(BaseModel): + p: list[str] + + +@app.post("/model-required-list-str", operation_id="model_required_list_str") +def read_model_required_list_str(p: BodyModelRequiredListStr): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/required-list-str", "/model-required-list-str"], +) +def test_required_list_str_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p": { + "items": {"type": "string"}, + "title": "P", + "type": "array", + }, + }, + "required": ["p"], + "title": body_model_name, + "type": "object", + } + + +@pytest.mark.parametrize("json", [None, {}]) +@pytest.mark.parametrize( + "path", + ["/required-list-str", "/model-required-list-str"], +) +def test_required_list_str_missing(path: str, json: Union[dict, None]): + client = TestClient(app) + response = client.post(path, json=json) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": IsOneOf(["body", "p"], ["body"]), + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } + + +@pytest.mark.parametrize( + "path", + ["/required-list-str", "/model-required-list-str"], +) +def test_required_list_str(path: str): + client = TestClient(app) + response = client.post(path, json={"p": ["hello", "world"]}) + assert response.status_code == 200 + assert response.json() == {"p": ["hello", "world"]} + + +# ===================================================================================== +# Alias + + +@app.post("/required-list-alias", operation_id="required_list_alias") +async def read_required_list_alias( + p: Annotated[list[str], Body(embed=True, alias="p_alias")], +): + return {"p": p} + + +class BodyModelRequiredListAlias(BaseModel): + p: list[str] = Field(alias="p_alias") + + +@app.post("/model-required-list-alias", operation_id="model_required_list_alias") +async def read_model_required_list_alias(p: BodyModelRequiredListAlias): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + [ + "/required-list-alias", + "/model-required-list-alias", + ], +) +def test_required_list_str_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_alias": { + "items": {"type": "string"}, + "title": "P Alias", + "type": "array", + }, + }, + "required": ["p_alias"], + "title": body_model_name, + "type": "object", + } + + +@pytest.mark.parametrize("json", [None, {}]) +@pytest.mark.parametrize( + "path", + ["/required-list-alias", "/model-required-list-alias"], +) +def test_required_list_alias_missing(path: str, json: Union[dict, None]): + client = TestClient(app) + response = client.post(path, json=json) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": IsOneOf(["body", "p_alias"], ["body"]), + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } + + +@pytest.mark.parametrize( + "path", + ["/required-list-alias", "/model-required-list-alias"], +) +def test_required_list_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, json={"p": ["hello", "world"]}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "p_alias"], + "msg": "Field required", + "input": IsOneOf(None, {"p": ["hello", "world"]}), + } + ] + } + + +@pytest.mark.parametrize( + "path", + ["/required-list-alias", "/model-required-list-alias"], +) +def test_required_list_alias_by_alias(path: str): + client = TestClient(app) + response = client.post(path, json={"p_alias": ["hello", "world"]}) + assert response.status_code == 200, response.text + assert response.json() == {"p": ["hello", "world"]} + + +# ===================================================================================== +# Validation alias + + +@app.post( + "/required-list-validation-alias", operation_id="required_list_validation_alias" +) +def read_required_list_validation_alias( + p: Annotated[list[str], Body(embed=True, validation_alias="p_val_alias")], +): + return {"p": p} + + +class BodyModelRequiredListValidationAlias(BaseModel): + p: list[str] = Field(validation_alias="p_val_alias") + + +@app.post( + "/model-required-list-validation-alias", + operation_id="model_required_list_validation_alias", +) +async def read_model_required_list_validation_alias( + p: BodyModelRequiredListValidationAlias, +): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/required-list-validation-alias", "/model-required-list-validation-alias"], +) +def test_required_list_validation_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_val_alias": { + "items": {"type": "string"}, + "title": "P Val Alias", + "type": "array", + }, + }, + "required": ["p_val_alias"], + "title": body_model_name, + "type": "object", + } + + +@pytest.mark.parametrize("json", [None, {}]) +@pytest.mark.parametrize( + "path", + [ + "/required-list-validation-alias", + "/model-required-list-validation-alias", + ], +) +def test_required_list_validation_alias_missing(path: str, json: Union[dict, None]): + client = TestClient(app) + response = client.post(path, json=json) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": IsOneOf(["body"], ["body", "p_val_alias"]), + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-list-validation-alias", + "/model-required-list-validation-alias", + ], +) +def test_required_list_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, json={"p": ["hello", "world"]}) + assert response.status_code == 422, response.text + + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "p_val_alias"], + "msg": "Field required", + "input": IsOneOf(None, IsPartialDict({"p": ["hello", "world"]})), + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-list-validation-alias", + "/model-required-list-validation-alias", + ], +) +def test_required_list_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.post(path, json={"p_val_alias": ["hello", "world"]}) + assert response.status_code == 200, response.text + assert response.json() == {"p": ["hello", "world"]} + + +# ===================================================================================== +# Alias and validation alias + + +@app.post( + "/required-list-alias-and-validation-alias", + operation_id="required_list_alias_and_validation_alias", +) +def read_required_list_alias_and_validation_alias( + p: Annotated[ + list[str], Body(embed=True, alias="p_alias", validation_alias="p_val_alias") + ], +): + return {"p": p} + + +class BodyModelRequiredListAliasAndValidationAlias(BaseModel): + p: list[str] = Field(alias="p_alias", validation_alias="p_val_alias") + + +@app.post( + "/model-required-list-alias-and-validation-alias", + operation_id="model_required_list_alias_and_validation_alias", +) +def read_model_required_list_alias_and_validation_alias( + p: BodyModelRequiredListAliasAndValidationAlias, +): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + [ + "/required-list-alias-and-validation-alias", + "/model-required-list-alias-and-validation-alias", + ], +) +def test_required_list_alias_and_validation_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_val_alias": { + "items": {"type": "string"}, + "title": "P Val Alias", + "type": "array", + }, + }, + "required": ["p_val_alias"], + "title": body_model_name, + "type": "object", + } + + +@pytest.mark.parametrize("json", [None, {}]) +@pytest.mark.parametrize( + "path", + [ + "/required-list-alias-and-validation-alias", + "/model-required-list-alias-and-validation-alias", + ], +) +def test_required_list_alias_and_validation_alias_missing(path: str, json): + client = TestClient(app) + response = client.post(path, json=json) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": IsOneOf(["body"], ["body", "p_val_alias"]), + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-list-alias-and-validation-alias", + "/model-required-list-alias-and-validation-alias", + ], +) +def test_required_list_alias_and_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, json={"p": ["hello", "world"]}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": [ + "body", + "p_val_alias", + ], + "msg": "Field required", + "input": IsOneOf(None, {"p": ["hello", "world"]}), + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-list-alias-and-validation-alias", + "/model-required-list-alias-and-validation-alias", + ], +) +def test_required_list_alias_and_validation_alias_by_alias(path: str): + client = TestClient(app) + response = client.post(path, json={"p_alias": ["hello", "world"]}) + assert response.status_code == 422, response.text + + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "p_val_alias"], + "msg": "Field required", + "input": IsOneOf(None, {"p_alias": ["hello", "world"]}), + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-list-alias-and-validation-alias", + "/model-required-list-alias-and-validation-alias", + ], +) +def test_required_list_alias_and_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.post(path, json={"p_val_alias": ["hello", "world"]}) + assert response.status_code == 200, response.text + assert response.json() == {"p": ["hello", "world"]} diff --git a/tests/test_request_params/test_body/test_optional_list.py b/tests/test_request_params/test_body/test_optional_list.py new file mode 100644 index 0000000000..ba8ba9092e --- /dev/null +++ b/tests/test_request_params/test_body/test_optional_list.py @@ -0,0 +1,456 @@ +from typing import Annotated, Optional + +import pytest +from fastapi import Body, FastAPI +from fastapi.testclient import TestClient +from pydantic import BaseModel, Field + +from .utils import get_body_model_name + +app = FastAPI() + +# ===================================================================================== +# Without aliases + + +@app.post("/optional-list-str", operation_id="optional_list_str") +async def read_optional_list_str( + p: Annotated[Optional[list[str]], Body(embed=True)] = None, +): + return {"p": p} + + +class BodyModelOptionalListStr(BaseModel): + p: Optional[list[str]] = None + + +@app.post("/model-optional-list-str", operation_id="model_optional_list_str") +async def read_model_optional_list_str(p: BodyModelOptionalListStr): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/optional-list-str", "/model-optional-list-str"], +) +def test_optional_list_str_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p": { + "anyOf": [ + {"items": {"type": "string"}, "type": "array"}, + {"type": "null"}, + ], + "title": "P", + }, + }, + "title": body_model_name, + "type": "object", + } + + +def test_optional_list_str_missing(): + client = TestClient(app) + response = client.post("/optional-list-str") + assert response.status_code == 200, response.text + assert response.json() == {"p": None} + + +def test_model_optional_list_str_missing(): + client = TestClient(app) + response = client.post("/model-optional-list-str") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "input": None, + "loc": ["body"], + "msg": "Field required", + "type": "missing", + }, + ], + } + + +@pytest.mark.parametrize( + "path", + ["/optional-list-str", "/model-optional-list-str"], +) +def test_optional_list_str_missing_empty_dict(path: str): + client = TestClient(app) + response = client.post(path, json={}) + assert response.status_code == 200, response.text + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + ["/optional-list-str", "/model-optional-list-str"], +) +def test_optional_list_str(path: str): + client = TestClient(app) + response = client.post(path, json={"p": ["hello", "world"]}) + assert response.status_code == 200 + assert response.json() == {"p": ["hello", "world"]} + + +# ===================================================================================== +# Alias + + +@app.post("/optional-list-alias", operation_id="optional_list_alias") +async def read_optional_list_alias( + p: Annotated[Optional[list[str]], Body(embed=True, alias="p_alias")] = None, +): + return {"p": p} + + +class BodyModelOptionalListAlias(BaseModel): + p: Optional[list[str]] = Field(None, alias="p_alias") + + +@app.post("/model-optional-list-alias", operation_id="model_optional_list_alias") +async def read_model_optional_list_alias(p: BodyModelOptionalListAlias): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-list-alias", + "/model-optional-list-alias", + ], +) +def test_optional_list_str_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_alias": { + "anyOf": [ + {"items": {"type": "string"}, "type": "array"}, + {"type": "null"}, + ], + "title": "P Alias", + }, + }, + "title": body_model_name, + "type": "object", + } + + +def test_optional_list_alias_missing(): + client = TestClient(app) + response = client.post("/optional-list-alias") + assert response.status_code == 200, response.text + assert response.json() == {"p": None} + + +def test_model_optional_list_alias_missing(): + client = TestClient(app) + response = client.post("/model-optional-list-alias") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "input": None, + "loc": ["body"], + "msg": "Field required", + "type": "missing", + }, + ], + } + + +@pytest.mark.parametrize( + "path", + ["/optional-list-alias", "/model-optional-list-alias"], +) +def test_optional_list_alias_missing_empty_dict(path: str): + client = TestClient(app) + response = client.post(path, json={}) + assert response.status_code == 200, response.text + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + ["/optional-list-alias", "/model-optional-list-alias"], +) +def test_optional_list_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, json={"p": ["hello", "world"]}) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + ["/optional-list-alias", "/model-optional-list-alias"], +) +def test_optional_list_alias_by_alias(path: str): + client = TestClient(app) + response = client.post(path, json={"p_alias": ["hello", "world"]}) + assert response.status_code == 200 + assert response.json() == {"p": ["hello", "world"]} + + +# ===================================================================================== +# Validation alias + + +@app.post( + "/optional-list-validation-alias", operation_id="optional_list_validation_alias" +) +def read_optional_list_validation_alias( + p: Annotated[ + Optional[list[str]], Body(embed=True, validation_alias="p_val_alias") + ] = None, +): + return {"p": p} + + +class BodyModelOptionalListValidationAlias(BaseModel): + p: Optional[list[str]] = Field(None, validation_alias="p_val_alias") + + +@app.post( + "/model-optional-list-validation-alias", + operation_id="model_optional_list_validation_alias", +) +def read_model_optional_list_validation_alias( + p: BodyModelOptionalListValidationAlias, +): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/optional-list-validation-alias", "/model-optional-list-validation-alias"], +) +def test_optional_list_validation_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_val_alias": { + "anyOf": [ + {"items": {"type": "string"}, "type": "array"}, + {"type": "null"}, + ], + "title": "P Val Alias", + }, + }, + "title": body_model_name, + "type": "object", + } + + +def test_optional_list_validation_alias_missing(): + client = TestClient(app) + response = client.post("/optional-list-validation-alias") + assert response.status_code == 200, response.text + assert response.json() == {"p": None} + + +def test_model_optional_list_validation_alias_missing(): + client = TestClient(app) + response = client.post("/model-optional-list-validation-alias") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "input": None, + "loc": ["body"], + "msg": "Field required", + "type": "missing", + }, + ], + } + + +@pytest.mark.parametrize( + "path", + ["/optional-list-validation-alias", "/model-optional-list-validation-alias"], +) +def test_optional_list_validation_alias_missing_empty_dict(path: str): + client = TestClient(app) + response = client.post(path, json={}) + assert response.status_code == 200, response.text + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-list-validation-alias", + "/model-optional-list-validation-alias", + ], +) +def test_optional_list_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, json={"p": ["hello", "world"]}) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-list-validation-alias", + "/model-optional-list-validation-alias", + ], +) +def test_optional_list_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.post(path, json={"p_val_alias": ["hello", "world"]}) + assert response.status_code == 200, response.text + assert response.json() == {"p": ["hello", "world"]} + + +# ===================================================================================== +# Alias and validation alias + + +@app.post( + "/optional-list-alias-and-validation-alias", + operation_id="optional_list_alias_and_validation_alias", +) +def read_optional_list_alias_and_validation_alias( + p: Annotated[ + Optional[list[str]], + Body(embed=True, alias="p_alias", validation_alias="p_val_alias"), + ] = None, +): + return {"p": p} + + +class BodyModelOptionalListAliasAndValidationAlias(BaseModel): + p: Optional[list[str]] = Field( + None, alias="p_alias", validation_alias="p_val_alias" + ) + + +@app.post( + "/model-optional-list-alias-and-validation-alias", + operation_id="model_optional_list_alias_and_validation_alias", +) +def read_model_optional_list_alias_and_validation_alias( + p: BodyModelOptionalListAliasAndValidationAlias, +): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-list-alias-and-validation-alias", + "/model-optional-list-alias-and-validation-alias", + ], +) +def test_optional_list_alias_and_validation_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_val_alias": { + "anyOf": [ + {"items": {"type": "string"}, "type": "array"}, + {"type": "null"}, + ], + "title": "P Val Alias", + }, + }, + "title": body_model_name, + "type": "object", + } + + +def test_optional_list_alias_and_validation_alias_missing(): + client = TestClient(app) + response = client.post("/optional-list-alias-and-validation-alias") + assert response.status_code == 200, response.text + assert response.json() == {"p": None} + + +def test_model_optional_list_alias_and_validation_alias_missing(): + client = TestClient(app) + response = client.post("/model-optional-list-alias-and-validation-alias") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "input": None, + "loc": ["body"], + "msg": "Field required", + "type": "missing", + }, + ], + } + + +@pytest.mark.parametrize( + "path", + [ + "/optional-list-alias-and-validation-alias", + "/model-optional-list-alias-and-validation-alias", + ], +) +def test_optional_list_alias_and_validation_alias_missing_empty_dict(path: str): + client = TestClient(app) + response = client.post(path, json={}) + assert response.status_code == 200, response.text + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-list-alias-and-validation-alias", + "/model-optional-list-alias-and-validation-alias", + ], +) +def test_optional_list_alias_and_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, json={"p": ["hello", "world"]}) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-list-alias-and-validation-alias", + "/model-optional-list-alias-and-validation-alias", + ], +) +def test_optional_list_alias_and_validation_alias_by_alias(path: str): + client = TestClient(app) + response = client.post(path, json={"p_alias": ["hello", "world"]}) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-list-alias-and-validation-alias", + "/model-optional-list-alias-and-validation-alias", + ], +) +def test_optional_list_alias_and_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.post(path, json={"p_val_alias": ["hello", "world"]}) + assert response.status_code == 200, response.text + assert response.json() == { + "p": [ + "hello", + "world", + ] + } diff --git a/tests/test_request_params/test_body/test_optional_str.py b/tests/test_request_params/test_body/test_optional_str.py new file mode 100644 index 0000000000..b9c18034da --- /dev/null +++ b/tests/test_request_params/test_body/test_optional_str.py @@ -0,0 +1,431 @@ +from typing import Annotated, Optional + +import pytest +from fastapi import Body, FastAPI +from fastapi.testclient import TestClient +from pydantic import BaseModel, Field + +from .utils import get_body_model_name + +app = FastAPI() + +# ===================================================================================== +# Without aliases + + +@app.post("/optional-str", operation_id="optional_str") +async def read_optional_str(p: Annotated[Optional[str], Body(embed=True)] = None): + return {"p": p} + + +class BodyModelOptionalStr(BaseModel): + p: Optional[str] = None + + +@app.post("/model-optional-str", operation_id="model_optional_str") +async def read_model_optional_str(p: BodyModelOptionalStr): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/optional-str", "/model-optional-str"], +) +def test_optional_str_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P", + }, + }, + "title": body_model_name, + "type": "object", + } + + +def test_optional_str_missing(): + client = TestClient(app) + response = client.post("/optional-str") + assert response.status_code == 200, response.text + assert response.json() == {"p": None} + + +def test_model_optional_str_missing(): + client = TestClient(app) + response = client.post("/model-optional-str") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "input": None, + "loc": ["body"], + "msg": "Field required", + "type": "missing", + }, + ], + } + + +@pytest.mark.parametrize( + "path", + ["/optional-str", "/model-optional-str"], +) +def test_optional_str_missing_empty_dict(path: str): + client = TestClient(app) + response = client.post(path, json={}) + assert response.status_code == 200, response.text + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + ["/optional-str", "/model-optional-str"], +) +def test_optional_str(path: str): + client = TestClient(app) + response = client.post(path, json={"p": "hello"}) + assert response.status_code == 200 + assert response.json() == {"p": "hello"} + + +# ===================================================================================== +# Alias + + +@app.post("/optional-alias", operation_id="optional_alias") +async def read_optional_alias( + p: Annotated[Optional[str], Body(embed=True, alias="p_alias")] = None, +): + return {"p": p} + + +class BodyModelOptionalAlias(BaseModel): + p: Optional[str] = Field(None, alias="p_alias") + + +@app.post("/model-optional-alias", operation_id="model_optional_alias") +async def read_model_optional_alias(p: BodyModelOptionalAlias): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-alias", + "/model-optional-alias", + ], +) +def test_optional_str_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_alias": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P Alias", + }, + }, + "title": body_model_name, + "type": "object", + } + + +def test_optional_alias_missing(): + client = TestClient(app) + response = client.post("/optional-alias") + assert response.status_code == 200 + assert response.json() == {"p": None} + + +def test_model_optional_alias_missing(): + client = TestClient(app) + response = client.post("/model-optional-alias") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "input": None, + "loc": ["body"], + "msg": "Field required", + "type": "missing", + }, + ], + } + + +@pytest.mark.parametrize( + "path", + ["/optional-alias", "/model-optional-alias"], +) +def test_model_optional_alias_missing_empty_dict(path: str): + client = TestClient(app) + response = client.post(path, json={}) + assert response.status_code == 200, response.text + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + ["/optional-alias", "/model-optional-alias"], +) +def test_optional_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, json={"p": "hello"}) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + ["/optional-alias", "/model-optional-alias"], +) +def test_optional_alias_by_alias(path: str): + client = TestClient(app) + response = client.post(path, json={"p_alias": "hello"}) + assert response.status_code == 200 + assert response.json() == {"p": "hello"} + + +# ===================================================================================== +# Validation alias + + +@app.post("/optional-validation-alias", operation_id="optional_validation_alias") +def read_optional_validation_alias( + p: Annotated[ + Optional[str], Body(embed=True, validation_alias="p_val_alias") + ] = None, +): + return {"p": p} + + +class BodyModelOptionalValidationAlias(BaseModel): + p: Optional[str] = Field(None, validation_alias="p_val_alias") + + +@app.post( + "/model-optional-validation-alias", operation_id="model_optional_validation_alias" +) +def read_model_optional_validation_alias( + p: BodyModelOptionalValidationAlias, +): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/optional-validation-alias", "/model-optional-validation-alias"], +) +def test_optional_validation_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_val_alias": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P Val Alias", + }, + }, + "title": body_model_name, + "type": "object", + } + + +def test_optional_validation_alias_missing(): + client = TestClient(app) + response = client.post("/optional-validation-alias") + assert response.status_code == 200 + assert response.json() == {"p": None} + + +def test_model_optional_validation_alias_missing(): + client = TestClient(app) + response = client.post("/model-optional-validation-alias") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "input": None, + "loc": ["body"], + "msg": "Field required", + "type": "missing", + }, + ], + } + + +@pytest.mark.parametrize( + "path", + ["/optional-validation-alias", "/model-optional-validation-alias"], +) +def test_model_optional_validation_alias_missing_empty_dict(path: str): + client = TestClient(app) + response = client.post(path, json={}) + assert response.status_code == 200, response.text + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-validation-alias", + "/model-optional-validation-alias", + ], +) +def test_optional_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, json={"p": "hello"}) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-validation-alias", + "/model-optional-validation-alias", + ], +) +def test_optional_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.post(path, json={"p_val_alias": "hello"}) + assert response.status_code == 200 + assert response.json() == {"p": "hello"} + + +# ===================================================================================== +# Alias and validation alias + + +@app.post( + "/optional-alias-and-validation-alias", + operation_id="optional_alias_and_validation_alias", +) +def read_optional_alias_and_validation_alias( + p: Annotated[ + Optional[str], Body(embed=True, alias="p_alias", validation_alias="p_val_alias") + ] = None, +): + return {"p": p} + + +class BodyModelOptionalAliasAndValidationAlias(BaseModel): + p: Optional[str] = Field(None, alias="p_alias", validation_alias="p_val_alias") + + +@app.post( + "/model-optional-alias-and-validation-alias", + operation_id="model_optional_alias_and_validation_alias", +) +def read_model_optional_alias_and_validation_alias( + p: BodyModelOptionalAliasAndValidationAlias, +): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-alias-and-validation-alias", + "/model-optional-alias-and-validation-alias", + ], +) +def test_optional_alias_and_validation_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_val_alias": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P Val Alias", + }, + }, + "title": body_model_name, + "type": "object", + } + + +def test_optional_alias_and_validation_alias_missing(): + client = TestClient(app) + response = client.post("/optional-alias-and-validation-alias") + assert response.status_code == 200 + assert response.json() == {"p": None} + + +def test_model_optional_alias_and_validation_alias_missing(): + client = TestClient(app) + response = client.post("/model-optional-alias-and-validation-alias") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "input": None, + "loc": ["body"], + "msg": "Field required", + "type": "missing", + }, + ], + } + + +@pytest.mark.parametrize( + "path", + [ + "/optional-alias-and-validation-alias", + "/model-optional-alias-and-validation-alias", + ], +) +def test_model_optional_alias_and_validation_alias_missing_empty_dict(path: str): + client = TestClient(app) + response = client.post(path, json={}) + assert response.status_code == 200, response.text + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-alias-and-validation-alias", + "/model-optional-alias-and-validation-alias", + ], +) +def test_optional_alias_and_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, json={"p": "hello"}) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-alias-and-validation-alias", + "/model-optional-alias-and-validation-alias", + ], +) +def test_optional_alias_and_validation_alias_by_alias(path: str): + client = TestClient(app) + response = client.post(path, json={"p_alias": "hello"}) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-alias-and-validation-alias", + "/model-optional-alias-and-validation-alias", + ], +) +def test_optional_alias_and_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.post(path, json={"p_val_alias": "hello"}) + assert response.status_code == 200 + assert response.json() == {"p": "hello"} diff --git a/tests/test_request_params/test_body/test_required_str.py b/tests/test_request_params/test_body/test_required_str.py new file mode 100644 index 0000000000..5b434fa1db --- /dev/null +++ b/tests/test_request_params/test_body/test_required_str.py @@ -0,0 +1,421 @@ +from typing import Annotated, Any, Union + +import pytest +from dirty_equals import IsOneOf +from fastapi import Body, FastAPI +from fastapi.testclient import TestClient +from pydantic import BaseModel, Field + +from .utils import get_body_model_name + +app = FastAPI() + +# ===================================================================================== +# Without aliases + + +@app.post("/required-str", operation_id="required_str") +async def read_required_str(p: Annotated[str, Body(embed=True)]): + return {"p": p} + + +class BodyModelRequiredStr(BaseModel): + p: str + + +@app.post("/model-required-str", operation_id="model_required_str") +async def read_model_required_str(p: BodyModelRequiredStr): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/required-str", "/model-required-str"], +) +def test_required_str_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p": {"title": "P", "type": "string"}, + }, + "required": ["p"], + "title": body_model_name, + "type": "object", + } + + +@pytest.mark.parametrize("json", [None, {}]) +@pytest.mark.parametrize( + "path", + ["/required-str", "/model-required-str"], +) +def test_required_str_missing(path: str, json: Union[dict[str, Any], None]): + client = TestClient(app) + response = client.post(path, json=json) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": IsOneOf(["body"], ["body", "p"]), + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } + + +@pytest.mark.parametrize( + "path", + ["/required-str", "/model-required-str"], +) +def test_required_str(path: str): + client = TestClient(app) + response = client.post(path, json={"p": "hello"}) + assert response.status_code == 200 + assert response.json() == {"p": "hello"} + + +# ===================================================================================== +# Alias + + +@app.post("/required-alias", operation_id="required_alias") +async def read_required_alias( + p: Annotated[str, Body(embed=True, alias="p_alias")], +): + return {"p": p} + + +class BodyModelRequiredAlias(BaseModel): + p: str = Field(alias="p_alias") + + +@app.post("/model-required-alias", operation_id="model_required_alias") +async def read_model_required_alias(p: BodyModelRequiredAlias): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + [ + "/required-alias", + "/model-required-alias", + ], +) +def test_required_str_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_alias": {"title": "P Alias", "type": "string"}, + }, + "required": ["p_alias"], + "title": body_model_name, + "type": "object", + } + + +@pytest.mark.parametrize("json", [None, {}]) +@pytest.mark.parametrize( + "path", + ["/required-alias", "/model-required-alias"], +) +def test_required_alias_missing(path: str, json: Union[dict[str, Any], None]): + client = TestClient(app) + response = client.post(path, json=json) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": IsOneOf(["body", "p_alias"], ["body"]), + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } + + +@pytest.mark.parametrize( + "path", + ["/required-alias", "/model-required-alias"], +) +def test_required_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, json={"p": "hello"}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "p_alias"], + "msg": "Field required", + "input": IsOneOf(None, {"p": "hello"}), + } + ] + } + + +@pytest.mark.parametrize( + "path", + ["/required-alias", "/model-required-alias"], +) +def test_required_alias_by_alias(path: str): + client = TestClient(app) + response = client.post(path, json={"p_alias": "hello"}) + assert response.status_code == 200, response.text + assert response.json() == {"p": "hello"} + + +# ===================================================================================== +# Validation alias + + +@app.post("/required-validation-alias", operation_id="required_validation_alias") +def read_required_validation_alias( + p: Annotated[str, Body(embed=True, validation_alias="p_val_alias")], +): + return {"p": p} + + +class BodyModelRequiredValidationAlias(BaseModel): + p: str = Field(validation_alias="p_val_alias") + + +@app.post( + "/model-required-validation-alias", operation_id="model_required_validation_alias" +) +def read_model_required_validation_alias( + p: BodyModelRequiredValidationAlias, +): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/required-validation-alias", "/model-required-validation-alias"], +) +def test_required_validation_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_val_alias": {"title": "P Val Alias", "type": "string"}, + }, + "required": ["p_val_alias"], + "title": body_model_name, + "type": "object", + } + + +@pytest.mark.parametrize("json", [None, {}]) +@pytest.mark.parametrize( + "path", + [ + "/required-validation-alias", + "/model-required-validation-alias", + ], +) +def test_required_validation_alias_missing( + path: str, json: Union[dict[str, Any], None] +): + client = TestClient(app) + response = client.post(path, json=json) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": IsOneOf(["body", "p_val_alias"], ["body"]), + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-validation-alias", + "/model-required-validation-alias", + ], +) +def test_required_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, json={"p": "hello"}) + assert response.status_code == 422, response.text + + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "p_val_alias"], + "msg": "Field required", + "input": IsOneOf(None, {"p": "hello"}), + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-validation-alias", + "/model-required-validation-alias", + ], +) +def test_required_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.post(path, json={"p_val_alias": "hello"}) + assert response.status_code == 200, response.text + + assert response.json() == {"p": "hello"} + + +# ===================================================================================== +# Alias and validation alias + + +@app.post( + "/required-alias-and-validation-alias", + operation_id="required_alias_and_validation_alias", +) +def read_required_alias_and_validation_alias( + p: Annotated[ + str, Body(embed=True, alias="p_alias", validation_alias="p_val_alias") + ], +): + return {"p": p} + + +class BodyModelRequiredAliasAndValidationAlias(BaseModel): + p: str = Field(alias="p_alias", validation_alias="p_val_alias") + + +@app.post( + "/model-required-alias-and-validation-alias", + operation_id="model_required_alias_and_validation_alias", +) +def read_model_required_alias_and_validation_alias( + p: BodyModelRequiredAliasAndValidationAlias, +): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + [ + "/required-alias-and-validation-alias", + "/model-required-alias-and-validation-alias", + ], +) +def test_required_alias_and_validation_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_val_alias": {"title": "P Val Alias", "type": "string"}, + }, + "required": ["p_val_alias"], + "title": body_model_name, + "type": "object", + } + + +@pytest.mark.parametrize("json", [None, {}]) +@pytest.mark.parametrize( + "path", + [ + "/required-alias-and-validation-alias", + "/model-required-alias-and-validation-alias", + ], +) +def test_required_alias_and_validation_alias_missing( + path: str, json: Union[dict[str, Any], None] +): + client = TestClient(app) + response = client.post(path, json=json) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": IsOneOf(["body"], ["body", "p_val_alias"]), + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-alias-and-validation-alias", + "/model-required-alias-and-validation-alias", + ], +) +def test_required_alias_and_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, json={"p": "hello"}) + assert response.status_code == 422 + + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": [ + "body", + "p_val_alias", + ], + "msg": "Field required", + "input": IsOneOf(None, {"p": "hello"}), + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-alias-and-validation-alias", + "/model-required-alias-and-validation-alias", + ], +) +def test_required_alias_and_validation_alias_by_alias(path: str): + client = TestClient(app) + response = client.post(path, json={"p_alias": "hello"}) + assert response.status_code == 422, response.text + + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "p_val_alias"], + "msg": "Field required", + "input": IsOneOf(None, {"p_alias": "hello"}), + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-alias-and-validation-alias", + "/model-required-alias-and-validation-alias", + ], +) +def test_required_alias_and_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.post(path, json={"p_val_alias": "hello"}) + assert response.status_code == 200, response.text + + assert response.json() == {"p": "hello"} diff --git a/tests/test_request_params/test_body/utils.py b/tests/test_request_params/test_body/utils.py new file mode 100644 index 0000000000..bf07394f90 --- /dev/null +++ b/tests/test_request_params/test_body/utils.py @@ -0,0 +1,7 @@ +from typing import Any + + +def get_body_model_name(openapi: dict[str, Any], path: str) -> str: + body = openapi["paths"][path]["post"]["requestBody"] + body_schema = body["content"]["application/json"]["schema"] + return body_schema.get("$ref", "").split("/")[-1] diff --git a/tests/test_request_params/test_cookie/__init__.py b/tests/test_request_params/test_cookie/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/test_request_params/test_cookie/test_list.py b/tests/test_request_params/test_cookie/test_list.py new file mode 100644 index 0000000000..4ae80e0015 --- /dev/null +++ b/tests/test_request_params/test_cookie/test_list.py @@ -0,0 +1,3 @@ +# Currently, there is no way to pass multiple cookies with the same name. +# The only way to pass multiple values for cookie params is to serialize them using +# a comma as a delimiter, but this is not currently supported by Starlette. diff --git a/tests/test_request_params/test_cookie/test_optional_list.py b/tests/test_request_params/test_cookie/test_optional_list.py new file mode 100644 index 0000000000..4ae80e0015 --- /dev/null +++ b/tests/test_request_params/test_cookie/test_optional_list.py @@ -0,0 +1,3 @@ +# Currently, there is no way to pass multiple cookies with the same name. +# The only way to pass multiple values for cookie params is to serialize them using +# a comma as a delimiter, but this is not currently supported by Starlette. diff --git a/tests/test_request_params/test_cookie/test_optional_str.py b/tests/test_request_params/test_cookie/test_optional_str.py new file mode 100644 index 0000000000..6f381c8b86 --- /dev/null +++ b/tests/test_request_params/test_cookie/test_optional_str.py @@ -0,0 +1,327 @@ +from typing import Annotated, Optional + +import pytest +from fastapi import Cookie, FastAPI +from fastapi.testclient import TestClient +from pydantic import BaseModel, Field + +app = FastAPI() + +# ===================================================================================== +# Without aliases + + +@app.get("/optional-str") +async def read_optional_str(p: Annotated[Optional[str], Cookie()] = None): + return {"p": p} + + +class CookieModelOptionalStr(BaseModel): + p: Optional[str] = None + + +@app.get("/model-optional-str") +async def read_model_optional_str(p: Annotated[CookieModelOptionalStr, Cookie()]): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/optional-str", "/model-optional-str"], +) +def test_optional_str_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P", + }, + "name": "p", + "in": "cookie", + } + ] + + +@pytest.mark.parametrize( + "path", + ["/optional-str", "/model-optional-str"], +) +def test_optional_str_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + ["/optional-str", "/model-optional-str"], +) +def test_optional_str(path: str): + client = TestClient(app) + client.cookies.set("p", "hello") + response = client.get(path) + assert response.status_code == 200 + assert response.json() == {"p": "hello"} + + +# ===================================================================================== +# Alias + + +@app.get("/optional-alias") +async def read_optional_alias( + p: Annotated[Optional[str], Cookie(alias="p_alias")] = None, +): + return {"p": p} + + +class CookieModelOptionalAlias(BaseModel): + p: Optional[str] = Field(None, alias="p_alias") + + +@app.get("/model-optional-alias") +async def read_model_optional_alias(p: Annotated[CookieModelOptionalAlias, Cookie()]): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/optional-alias", "/model-optional-alias"], +) +def test_optional_str_alias_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P Alias", + }, + "name": "p_alias", + "in": "cookie", + } + ] + + +@pytest.mark.parametrize( + "path", + ["/optional-alias", "/model-optional-alias"], +) +def test_optional_alias_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + ["/optional-alias", "/model-optional-alias"], +) +def test_optional_alias_by_name(path: str): + client = TestClient(app) + client.cookies.set("p", "hello") + response = client.get(path) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-alias", + "/model-optional-alias", + ], +) +def test_optional_alias_by_alias(path: str): + client = TestClient(app) + client.cookies.set("p_alias", "hello") + response = client.get(path) + assert response.status_code == 200 + assert response.json() == {"p": "hello"} + + +# ===================================================================================== +# Validation alias + + +@app.get("/optional-validation-alias") +def read_optional_validation_alias( + p: Annotated[Optional[str], Cookie(validation_alias="p_val_alias")] = None, +): + return {"p": p} + + +class CookieModelOptionalValidationAlias(BaseModel): + p: Optional[str] = Field(None, validation_alias="p_val_alias") + + +@app.get("/model-optional-validation-alias") +def read_model_optional_validation_alias( + p: Annotated[CookieModelOptionalValidationAlias, Cookie()], +): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/optional-validation-alias", "/model-optional-validation-alias"], +) +def test_optional_validation_alias_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P Val Alias", + }, + "name": "p_val_alias", + "in": "cookie", + } + ] + + +@pytest.mark.parametrize( + "path", + ["/optional-validation-alias", "/model-optional-validation-alias"], +) +def test_optional_validation_alias_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-validation-alias", + "/model-optional-validation-alias", + ], +) +def test_optional_validation_alias_by_name(path: str): + client = TestClient(app) + client.cookies.set("p", "hello") + response = client.get(path) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-validation-alias", + "/model-optional-validation-alias", + ], +) +def test_optional_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + client.cookies.set("p_val_alias", "hello") + response = client.get(path) + assert response.status_code == 200 + assert response.json() == {"p": "hello"} + + +# ===================================================================================== +# Alias and validation alias + + +@app.get("/optional-alias-and-validation-alias") +def read_optional_alias_and_validation_alias( + p: Annotated[ + Optional[str], Cookie(alias="p_alias", validation_alias="p_val_alias") + ] = None, +): + return {"p": p} + + +class CookieModelOptionalAliasAndValidationAlias(BaseModel): + p: Optional[str] = Field(None, alias="p_alias", validation_alias="p_val_alias") + + +@app.get("/model-optional-alias-and-validation-alias") +def read_model_optional_alias_and_validation_alias( + p: Annotated[CookieModelOptionalAliasAndValidationAlias, Cookie()], +): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-alias-and-validation-alias", + "/model-optional-alias-and-validation-alias", + ], +) +def test_optional_alias_and_validation_alias_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P Val Alias", + }, + "name": "p_val_alias", + "in": "cookie", + } + ] + + +@pytest.mark.parametrize( + "path", + [ + "/optional-alias-and-validation-alias", + "/model-optional-alias-and-validation-alias", + ], +) +def test_optional_alias_and_validation_alias_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-alias-and-validation-alias", + "/model-optional-alias-and-validation-alias", + ], +) +def test_optional_alias_and_validation_alias_by_name(path: str): + client = TestClient(app) + client.cookies.set("p", "hello") + response = client.get(path) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-alias-and-validation-alias", + "/model-optional-alias-and-validation-alias", + ], +) +def test_optional_alias_and_validation_alias_by_alias(path: str): + client = TestClient(app) + client.cookies.set("p_alias", "hello") + response = client.get(path) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-alias-and-validation-alias", + "/model-optional-alias-and-validation-alias", + ], +) +def test_optional_alias_and_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + client.cookies.set("p_val_alias", "hello") + response = client.get(path) + assert response.status_code == 200 + assert response.json() == {"p": "hello"} diff --git a/tests/test_request_params/test_cookie/test_required_str.py b/tests/test_request_params/test_cookie/test_required_str.py new file mode 100644 index 0000000000..3e877b3e3d --- /dev/null +++ b/tests/test_request_params/test_cookie/test_required_str.py @@ -0,0 +1,413 @@ +from typing import Annotated + +import pytest +from dirty_equals import IsOneOf +from fastapi import Cookie, FastAPI +from fastapi.testclient import TestClient +from pydantic import BaseModel, Field + +app = FastAPI() + +# ===================================================================================== +# Without aliases + + +@app.get("/required-str") +async def read_required_str(p: Annotated[str, Cookie()]): + return {"p": p} + + +class CookieModelRequiredStr(BaseModel): + p: str + + +@app.get("/model-required-str") +async def read_model_required_str(p: Annotated[CookieModelRequiredStr, Cookie()]): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/required-str", "/model-required-str"], +) +def test_required_str_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": True, + "schema": {"title": "P", "type": "string"}, + "name": "p", + "in": "cookie", + } + ] + + +@pytest.mark.parametrize( + "path", + ["/required-str", "/model-required-str"], +) +def test_required_str_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["cookie", "p"], + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } + + +@pytest.mark.parametrize( + "path", + ["/required-str", "/model-required-str"], +) +def test_required_str(path: str): + client = TestClient(app) + client.cookies.set("p", "hello") + response = client.get(path) + assert response.status_code == 200 + assert response.json() == {"p": "hello"} + + +# ===================================================================================== +# Alias + + +@app.get("/required-alias") +async def read_required_alias(p: Annotated[str, Cookie(alias="p_alias")]): + return {"p": p} + + +class CookieModelRequiredAlias(BaseModel): + p: str = Field(alias="p_alias") + + +@app.get("/model-required-alias") +async def read_model_required_alias(p: Annotated[CookieModelRequiredAlias, Cookie()]): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/required-alias", "/model-required-alias"], +) +def test_required_str_alias_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": True, + "schema": {"title": "P Alias", "type": "string"}, + "name": "p_alias", + "in": "cookie", + } + ] + + +@pytest.mark.parametrize( + "path", + ["/required-alias", "/model-required-alias"], +) +def test_required_alias_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["cookie", "p_alias"], + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-alias", + "/model-required-alias", + ], +) +def test_required_alias_by_name(path: str): + client = TestClient(app) + client.cookies.set("p", "hello") + response = client.get(path) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["cookie", "p_alias"], + "msg": "Field required", + "input": IsOneOf( + None, + {"p": "hello"}, + ), + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-alias", + "/model-required-alias", + ], +) +def test_required_alias_by_alias(path: str): + client = TestClient(app) + client.cookies.set("p_alias", "hello") + response = client.get(path) + assert response.status_code == 200, response.text + assert response.json() == {"p": "hello"} + + +# ===================================================================================== +# Validation alias + + +@app.get("/required-validation-alias") +def read_required_validation_alias( + p: Annotated[str, Cookie(validation_alias="p_val_alias")], +): + return {"p": p} + + +class CookieModelRequiredValidationAlias(BaseModel): + p: str = Field(validation_alias="p_val_alias") + + +@app.get("/model-required-validation-alias") +def read_model_required_validation_alias( + p: Annotated[CookieModelRequiredValidationAlias, Cookie()], +): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/required-validation-alias", "/model-required-validation-alias"], +) +def test_required_validation_alias_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": True, + "schema": {"title": "P Val Alias", "type": "string"}, + "name": "p_val_alias", + "in": "cookie", + } + ] + + +@pytest.mark.parametrize( + "path", + [ + "/required-validation-alias", + "/model-required-validation-alias", + ], +) +def test_required_validation_alias_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": [ + "cookie", + "p_val_alias", + ], + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-validation-alias", + "/model-required-validation-alias", + ], +) +def test_required_validation_alias_by_name(path: str): + client = TestClient(app) + client.cookies.set("p", "hello") + response = client.get(path) + assert response.status_code == 422, response.text + + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["cookie", "p_val_alias"], + "msg": "Field required", + "input": IsOneOf(None, {"p": "hello"}), + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-validation-alias", + "/model-required-validation-alias", + ], +) +def test_required_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + client.cookies.set("p_val_alias", "hello") + response = client.get(path) + assert response.status_code == 200, response.text + + assert response.json() == {"p": "hello"} + + +# ===================================================================================== +# Alias and validation alias + + +@app.get("/required-alias-and-validation-alias") +def read_required_alias_and_validation_alias( + p: Annotated[str, Cookie(alias="p_alias", validation_alias="p_val_alias")], +): + return {"p": p} + + +class CookieModelRequiredAliasAndValidationAlias(BaseModel): + p: str = Field(alias="p_alias", validation_alias="p_val_alias") + + +@app.get("/model-required-alias-and-validation-alias") +def read_model_required_alias_and_validation_alias( + p: Annotated[CookieModelRequiredAliasAndValidationAlias, Cookie()], +): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + [ + "/required-alias-and-validation-alias", + "/model-required-alias-and-validation-alias", + ], +) +def test_required_alias_and_validation_alias_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": True, + "schema": {"title": "P Val Alias", "type": "string"}, + "name": "p_val_alias", + "in": "cookie", + } + ] + + +@pytest.mark.parametrize( + "path", + [ + "/required-alias-and-validation-alias", + "/model-required-alias-and-validation-alias", + ], +) +def test_required_alias_and_validation_alias_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": [ + "cookie", + "p_val_alias", + ], + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-alias-and-validation-alias", + "/model-required-alias-and-validation-alias", + ], +) +def test_required_alias_and_validation_alias_by_name(path: str): + client = TestClient(app) + client.cookies.set("p", "hello") + response = client.get(path) + assert response.status_code == 422 + + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": [ + "cookie", + "p_val_alias", + ], + "msg": "Field required", + "input": IsOneOf( + None, + {"p": "hello"}, + ), + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-alias-and-validation-alias", + "/model-required-alias-and-validation-alias", + ], +) +def test_required_alias_and_validation_alias_by_alias(path: str): + client = TestClient(app) + client.cookies.set("p_alias", "hello") + response = client.get(path) + assert response.status_code == 422 + + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["cookie", "p_val_alias"], + "msg": "Field required", + "input": IsOneOf( + None, + {"p_alias": "hello"}, + ), + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-alias-and-validation-alias", + "/model-required-alias-and-validation-alias", + ], +) +def test_required_alias_and_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + client.cookies.set("p_val_alias", "hello") + response = client.get(path) + assert response.status_code == 200, response.text + + assert response.json() == {"p": "hello"} diff --git a/tests/test_request_params/test_file/__init__.py b/tests/test_request_params/test_file/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/test_request_params/test_file/test_list.py b/tests/test_request_params/test_file/test_list.py new file mode 100644 index 0000000000..68280fcf32 --- /dev/null +++ b/tests/test_request_params/test_file/test_list.py @@ -0,0 +1,441 @@ +from typing import Annotated + +import pytest +from fastapi import FastAPI, File, UploadFile +from fastapi.testclient import TestClient + +from .utils import get_body_model_name + +app = FastAPI() + +# ===================================================================================== +# Without aliases + + +@app.post("/list-bytes", operation_id="list_bytes") +async def read_list_bytes(p: Annotated[list[bytes], File()]): + return {"file_size": [len(file) for file in p]} + + +@app.post("/list-uploadfile", operation_id="list_uploadfile") +async def read_list_uploadfile(p: Annotated[list[UploadFile], File()]): + return {"file_size": [file.size for file in p]} + + +@pytest.mark.parametrize( + "path", + [ + "/list-bytes", + "/list-uploadfile", + ], +) +def test_list_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p": { + "type": "array", + "items": {"type": "string", "format": "binary"}, + "title": "P", + }, + }, + "required": ["p"], + "title": body_model_name, + "type": "object", + } + + +@pytest.mark.parametrize( + "path", + [ + "/list-bytes", + "/list-uploadfile", + ], +) +def test_list_missing(path: str): + client = TestClient(app) + response = client.post(path) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "p"], + "msg": "Field required", + "input": None, + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/list-bytes", + "/list-uploadfile", + ], +) +def test_list(path: str): + client = TestClient(app) + response = client.post(path, files=[("p", b"hello"), ("p", b"world")]) + assert response.status_code == 200 + assert response.json() == {"file_size": [5, 5]} + + +# ===================================================================================== +# Alias + + +@app.post("/list-bytes-alias", operation_id="list_bytes_alias") +async def read_list_bytes_alias(p: Annotated[list[bytes], File(alias="p_alias")]): + return {"file_size": [len(file) for file in p]} + + +@app.post("/list-uploadfile-alias", operation_id="list_uploadfile_alias") +async def read_list_uploadfile_alias( + p: Annotated[list[UploadFile], File(alias="p_alias")], +): + return {"file_size": [file.size for file in p]} + + +@pytest.mark.parametrize( + "path", + [ + "/list-bytes-alias", + "/list-uploadfile-alias", + ], +) +def test_list_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_alias": { + "type": "array", + "items": {"type": "string", "format": "binary"}, + "title": "P Alias", + }, + }, + "required": ["p_alias"], + "title": body_model_name, + "type": "object", + } + + +@pytest.mark.parametrize( + "path", + [ + "/list-bytes-alias", + "/list-uploadfile-alias", + ], +) +def test_list_alias_missing(path: str): + client = TestClient(app) + response = client.post(path) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "p_alias"], + "msg": "Field required", + "input": None, + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/list-bytes-alias", + "/list-uploadfile-alias", + ], +) +def test_list_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, files=[("p", b"hello"), ("p", b"world")]) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "p_alias"], + "msg": "Field required", + "input": None, + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/list-bytes-alias", + "/list-uploadfile-alias", + ], +) +def test_list_alias_by_alias(path: str): + client = TestClient(app) + response = client.post(path, files=[("p_alias", b"hello"), ("p_alias", b"world")]) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": [5, 5]} + + +# ===================================================================================== +# Validation alias + + +@app.post("/list-bytes-validation-alias", operation_id="list_bytes_validation_alias") +def read_list_bytes_validation_alias( + p: Annotated[list[bytes], File(validation_alias="p_val_alias")], +): + return {"file_size": [len(file) for file in p]} + + +@app.post( + "/list-uploadfile-validation-alias", + operation_id="list_uploadfile_validation_alias", +) +def read_list_uploadfile_validation_alias( + p: Annotated[list[UploadFile], File(validation_alias="p_val_alias")], +): + return {"file_size": [file.size for file in p]} + + +@pytest.mark.parametrize( + "path", + [ + "/list-bytes-validation-alias", + "/list-uploadfile-validation-alias", + ], +) +def test_list_validation_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_val_alias": { + "type": "array", + "items": {"type": "string", "format": "binary"}, + "title": "P Val Alias", + }, + }, + "required": ["p_val_alias"], + "title": body_model_name, + "type": "object", + } + + +@pytest.mark.parametrize( + "path", + [ + "/list-bytes-validation-alias", + "/list-uploadfile-validation-alias", + ], +) +def test_list_validation_alias_missing(path: str): + client = TestClient(app) + response = client.post(path) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": [ + "body", + "p_val_alias", + ], + "msg": "Field required", + "input": None, + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/list-bytes-validation-alias", + "/list-uploadfile-validation-alias", + ], +) +def test_list_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, files=[("p", b"hello"), ("p", b"world")]) + assert response.status_code == 422, response.text + + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "p_val_alias"], + "msg": "Field required", + "input": None, + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/list-bytes-validation-alias", + "/list-uploadfile-validation-alias", + ], +) +def test_list_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.post( + path, files=[("p_val_alias", b"hello"), ("p_val_alias", b"world")] + ) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": [5, 5]} + + +# ===================================================================================== +# Alias and validation alias + + +@app.post( + "/list-bytes-alias-and-validation-alias", + operation_id="list_bytes_alias_and_validation_alias", +) +def read_list_bytes_alias_and_validation_alias( + p: Annotated[list[bytes], File(alias="p_alias", validation_alias="p_val_alias")], +): + return {"file_size": [len(file) for file in p]} + + +@app.post( + "/list-uploadfile-alias-and-validation-alias", + operation_id="list_uploadfile_alias_and_validation_alias", +) +def read_list_uploadfile_alias_and_validation_alias( + p: Annotated[ + list[UploadFile], File(alias="p_alias", validation_alias="p_val_alias") + ], +): + return {"file_size": [file.size for file in p]} + + +@pytest.mark.parametrize( + "path", + [ + "/list-bytes-alias-and-validation-alias", + "/list-uploadfile-alias-and-validation-alias", + ], +) +def test_list_alias_and_validation_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_val_alias": { + "type": "array", + "items": {"type": "string", "format": "binary"}, + "title": "P Val Alias", + }, + }, + "required": ["p_val_alias"], + "title": body_model_name, + "type": "object", + } + + +@pytest.mark.parametrize( + "path", + [ + "/list-bytes-alias-and-validation-alias", + "/list-uploadfile-alias-and-validation-alias", + ], +) +def test_list_alias_and_validation_alias_missing(path: str): + client = TestClient(app) + response = client.post(path) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": [ + "body", + "p_val_alias", + ], + "msg": "Field required", + "input": None, + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/list-bytes-alias-and-validation-alias", + "/list-uploadfile-alias-and-validation-alias", + ], +) +def test_list_alias_and_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, files=[("p", "hello"), ("p", "world")]) + assert response.status_code == 422 + + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": [ + "body", + "p_val_alias", + ], + "msg": "Field required", + "input": None, + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/list-bytes-alias-and-validation-alias", + "/list-uploadfile-alias-and-validation-alias", + ], +) +def test_list_alias_and_validation_alias_by_alias(path: str): + client = TestClient(app) + response = client.post(path, files=[("p_alias", b"hello"), ("p_alias", b"world")]) + assert response.status_code == 422, response.text + + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "p_val_alias"], + "msg": "Field required", + "input": None, + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/list-bytes-alias-and-validation-alias", + "/list-uploadfile-alias-and-validation-alias", + ], +) +def test_list_alias_and_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.post( + path, files=[("p_val_alias", b"hello"), ("p_val_alias", b"world")] + ) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": [5, 5]} diff --git a/tests/test_request_params/test_file/test_optional.py b/tests/test_request_params/test_file/test_optional.py new file mode 100644 index 0000000000..45ef7bdec4 --- /dev/null +++ b/tests/test_request_params/test_file/test_optional.py @@ -0,0 +1,363 @@ +from typing import Annotated, Optional + +import pytest +from fastapi import FastAPI, File, UploadFile +from fastapi.testclient import TestClient + +from .utils import get_body_model_name + +app = FastAPI() + +# ===================================================================================== +# Without aliases + + +@app.post("/optional-bytes", operation_id="optional_bytes") +async def read_optional_bytes(p: Annotated[Optional[bytes], File()] = None): + return {"file_size": len(p) if p else None} + + +@app.post("/optional-uploadfile", operation_id="optional_uploadfile") +async def read_optional_uploadfile(p: Annotated[Optional[UploadFile], File()] = None): + return {"file_size": p.size if p else None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-bytes", + "/optional-uploadfile", + ], +) +def test_optional_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p": { + "anyOf": [ + {"type": "string", "format": "binary"}, + {"type": "null"}, + ], + "title": "P", + } + }, + "title": body_model_name, + "type": "object", + } + + +@pytest.mark.parametrize( + "path", + [ + "/optional-bytes", + "/optional-uploadfile", + ], +) +def test_optional_missing(path: str): + client = TestClient(app) + response = client.post(path) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-bytes", + "/optional-uploadfile", + ], +) +def test_optional(path: str): + client = TestClient(app) + response = client.post(path, files=[("p", b"hello")]) + assert response.status_code == 200 + assert response.json() == {"file_size": 5} + + +# ===================================================================================== +# Alias + + +@app.post("/optional-bytes-alias", operation_id="optional_bytes_alias") +async def read_optional_bytes_alias( + p: Annotated[Optional[bytes], File(alias="p_alias")] = None, +): + return {"file_size": len(p) if p else None} + + +@app.post("/optional-uploadfile-alias", operation_id="optional_uploadfile_alias") +async def read_optional_uploadfile_alias( + p: Annotated[Optional[UploadFile], File(alias="p_alias")] = None, +): + return {"file_size": p.size if p else None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-bytes-alias", + "/optional-uploadfile-alias", + ], +) +def test_optional_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_alias": { + "anyOf": [ + {"type": "string", "format": "binary"}, + {"type": "null"}, + ], + "title": "P Alias", + } + }, + "title": body_model_name, + "type": "object", + } + + +@pytest.mark.parametrize( + "path", + [ + "/optional-bytes-alias", + "/optional-uploadfile-alias", + ], +) +def test_optional_alias_missing(path: str): + client = TestClient(app) + response = client.post(path) + assert response.status_code == 200 + assert response.json() == {"file_size": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-bytes-alias", + "/optional-uploadfile-alias", + ], +) +def test_optional_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, files=[("p", b"hello")]) + assert response.status_code == 200 + assert response.json() == {"file_size": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-bytes-alias", + "/optional-uploadfile-alias", + ], +) +def test_optional_alias_by_alias(path: str): + client = TestClient(app) + response = client.post(path, files=[("p_alias", b"hello")]) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": 5} + + +# ===================================================================================== +# Validation alias + + +@app.post( + "/optional-bytes-validation-alias", operation_id="optional_bytes_validation_alias" +) +def read_optional_bytes_validation_alias( + p: Annotated[Optional[bytes], File(validation_alias="p_val_alias")] = None, +): + return {"file_size": len(p) if p else None} + + +@app.post( + "/optional-uploadfile-validation-alias", + operation_id="optional_uploadfile_validation_alias", +) +def read_optional_uploadfile_validation_alias( + p: Annotated[Optional[UploadFile], File(validation_alias="p_val_alias")] = None, +): + return {"file_size": p.size if p else None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-bytes-validation-alias", + "/optional-uploadfile-validation-alias", + ], +) +def test_optional_validation_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_val_alias": { + "anyOf": [ + {"type": "string", "format": "binary"}, + {"type": "null"}, + ], + "title": "P Val Alias", + } + }, + "title": body_model_name, + "type": "object", + } + + +@pytest.mark.parametrize( + "path", + [ + "/optional-bytes-validation-alias", + "/optional-uploadfile-validation-alias", + ], +) +def test_optional_validation_alias_missing(path: str): + client = TestClient(app) + response = client.post(path) + assert response.status_code == 200 + assert response.json() == {"file_size": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-bytes-validation-alias", + "/optional-uploadfile-validation-alias", + ], +) +def test_optional_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, files=[("p", b"hello")]) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-bytes-validation-alias", + "/optional-uploadfile-validation-alias", + ], +) +def test_optional_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.post(path, files=[("p_val_alias", b"hello")]) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": 5} + + +# ===================================================================================== +# Alias and validation alias + + +@app.post( + "/optional-bytes-alias-and-validation-alias", + operation_id="optional_bytes_alias_and_validation_alias", +) +def read_optional_bytes_alias_and_validation_alias( + p: Annotated[ + Optional[bytes], File(alias="p_alias", validation_alias="p_val_alias") + ] = None, +): + return {"file_size": len(p) if p else None} + + +@app.post( + "/optional-uploadfile-alias-and-validation-alias", + operation_id="optional_uploadfile_alias_and_validation_alias", +) +def read_optional_uploadfile_alias_and_validation_alias( + p: Annotated[ + Optional[UploadFile], File(alias="p_alias", validation_alias="p_val_alias") + ] = None, +): + return {"file_size": p.size if p else None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-bytes-alias-and-validation-alias", + "/optional-uploadfile-alias-and-validation-alias", + ], +) +def test_optional_alias_and_validation_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_val_alias": { + "anyOf": [ + {"type": "string", "format": "binary"}, + {"type": "null"}, + ], + "title": "P Val Alias", + } + }, + "title": body_model_name, + "type": "object", + } + + +@pytest.mark.parametrize( + "path", + [ + "/optional-bytes-alias-and-validation-alias", + "/optional-uploadfile-alias-and-validation-alias", + ], +) +def test_optional_alias_and_validation_alias_missing(path: str): + client = TestClient(app) + response = client.post(path) + assert response.status_code == 200 + assert response.json() == {"file_size": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-bytes-alias-and-validation-alias", + "/optional-uploadfile-alias-and-validation-alias", + ], +) +def test_optional_alias_and_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, files={"p": "hello"}) + assert response.status_code == 200 + assert response.json() == {"file_size": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-bytes-alias-and-validation-alias", + "/optional-uploadfile-alias-and-validation-alias", + ], +) +def test_optional_alias_and_validation_alias_by_alias(path: str): + client = TestClient(app) + response = client.post(path, files=[("p_alias", b"hello")]) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-bytes-alias-and-validation-alias", + "/optional-uploadfile-alias-and-validation-alias", + ], +) +def test_optional_alias_and_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.post(path, files=[("p_val_alias", b"hello")]) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": 5} diff --git a/tests/test_request_params/test_file/test_optional_list.py b/tests/test_request_params/test_file/test_optional_list.py new file mode 100644 index 0000000000..162fbe08ae --- /dev/null +++ b/tests/test_request_params/test_file/test_optional_list.py @@ -0,0 +1,373 @@ +from typing import Annotated, Optional + +import pytest +from fastapi import FastAPI, File, UploadFile +from fastapi.testclient import TestClient + +from .utils import get_body_model_name + +app = FastAPI() + +# ===================================================================================== +# Without aliases + + +@app.post("/optional-list-bytes") +async def read_optional_list_bytes(p: Annotated[Optional[list[bytes]], File()] = None): + return {"file_size": [len(file) for file in p] if p else None} + + +@app.post("/optional-list-uploadfile") +async def read_optional_list_uploadfile( + p: Annotated[Optional[list[UploadFile]], File()] = None, +): + return {"file_size": [file.size for file in p] if p else None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-list-bytes", + "/optional-list-uploadfile", + ], +) +def test_optional_list_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p": { + "anyOf": [ + { + "type": "array", + "items": {"type": "string", "format": "binary"}, + }, + {"type": "null"}, + ], + "title": "P", + } + }, + "title": body_model_name, + "type": "object", + } + + +@pytest.mark.parametrize( + "path", + [ + "/optional-list-bytes", + "/optional-list-uploadfile", + ], +) +def test_optional_list_missing(path: str): + client = TestClient(app) + response = client.post(path) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-list-bytes", + "/optional-list-uploadfile", + ], +) +def test_optional_list(path: str): + client = TestClient(app) + response = client.post(path, files=[("p", b"hello"), ("p", b"world")]) + assert response.status_code == 200 + assert response.json() == {"file_size": [5, 5]} + + +# ===================================================================================== +# Alias + + +@app.post("/optional-list-bytes-alias") +async def read_optional_list_bytes_alias( + p: Annotated[Optional[list[bytes]], File(alias="p_alias")] = None, +): + return {"file_size": [len(file) for file in p] if p else None} + + +@app.post("/optional-list-uploadfile-alias") +async def read_optional_list_uploadfile_alias( + p: Annotated[Optional[list[UploadFile]], File(alias="p_alias")] = None, +): + return {"file_size": [file.size for file in p] if p else None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-list-bytes-alias", + "/optional-list-uploadfile-alias", + ], +) +def test_optional_list_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_alias": { + "anyOf": [ + { + "type": "array", + "items": {"type": "string", "format": "binary"}, + }, + {"type": "null"}, + ], + "title": "P Alias", + } + }, + "title": body_model_name, + "type": "object", + } + + +@pytest.mark.parametrize( + "path", + [ + "/optional-list-bytes-alias", + "/optional-list-uploadfile-alias", + ], +) +def test_optional_list_alias_missing(path: str): + client = TestClient(app) + response = client.post(path) + assert response.status_code == 200 + assert response.json() == {"file_size": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-list-bytes-alias", + "/optional-list-uploadfile-alias", + ], +) +def test_optional_list_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, files=[("p", b"hello"), ("p", b"world")]) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-list-bytes-alias", + "/optional-list-uploadfile-alias", + ], +) +def test_optional_list_alias_by_alias(path: str): + client = TestClient(app) + response = client.post(path, files=[("p_alias", b"hello"), ("p_alias", b"world")]) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": [5, 5]} + + +# ===================================================================================== +# Validation alias + + +@app.post("/optional-list-bytes-validation-alias") +def read_optional_list_bytes_validation_alias( + p: Annotated[Optional[list[bytes]], File(validation_alias="p_val_alias")] = None, +): + return {"file_size": [len(file) for file in p] if p else None} + + +@app.post("/optional-list-uploadfile-validation-alias") +def read_optional_list_uploadfile_validation_alias( + p: Annotated[ + Optional[list[UploadFile]], File(validation_alias="p_val_alias") + ] = None, +): + return {"file_size": [file.size for file in p] if p else None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-list-bytes-validation-alias", + "/optional-list-uploadfile-validation-alias", + ], +) +def test_optional_validation_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_val_alias": { + "anyOf": [ + { + "type": "array", + "items": {"type": "string", "format": "binary"}, + }, + {"type": "null"}, + ], + "title": "P Val Alias", + } + }, + "title": body_model_name, + "type": "object", + } + + +@pytest.mark.parametrize( + "path", + [ + "/optional-list-bytes-validation-alias", + "/optional-list-uploadfile-validation-alias", + ], +) +def test_optional_validation_alias_missing(path: str): + client = TestClient(app) + response = client.post(path) + assert response.status_code == 200 + assert response.json() == {"file_size": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-list-bytes-validation-alias", + "/optional-list-uploadfile-validation-alias", + ], +) +def test_optional_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, files=[("p", b"hello"), ("p", b"world")]) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-list-bytes-validation-alias", + "/optional-list-uploadfile-validation-alias", + ], +) +def test_optional_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.post( + path, files=[("p_val_alias", b"hello"), ("p_val_alias", b"world")] + ) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": [5, 5]} + + +# ===================================================================================== +# Alias and validation alias + + +@app.post("/optional-list-bytes-alias-and-validation-alias") +def read_optional_list_bytes_alias_and_validation_alias( + p: Annotated[ + Optional[list[bytes]], File(alias="p_alias", validation_alias="p_val_alias") + ] = None, +): + return {"file_size": [len(file) for file in p] if p else None} + + +@app.post("/optional-list-uploadfile-alias-and-validation-alias") +def read_optional_list_uploadfile_alias_and_validation_alias( + p: Annotated[ + Optional[list[UploadFile]], + File(alias="p_alias", validation_alias="p_val_alias"), + ] = None, +): + return {"file_size": [file.size for file in p] if p else None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-list-bytes-alias-and-validation-alias", + "/optional-list-uploadfile-alias-and-validation-alias", + ], +) +def test_optional_list_alias_and_validation_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_val_alias": { + "anyOf": [ + { + "type": "array", + "items": {"type": "string", "format": "binary"}, + }, + {"type": "null"}, + ], + "title": "P Val Alias", + } + }, + "title": body_model_name, + "type": "object", + } + + +@pytest.mark.parametrize( + "path", + [ + "/optional-list-bytes-alias-and-validation-alias", + "/optional-list-uploadfile-alias-and-validation-alias", + ], +) +def test_optional_list_alias_and_validation_alias_missing(path: str): + client = TestClient(app) + response = client.post(path) + assert response.status_code == 200 + assert response.json() == {"file_size": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-list-bytes-alias-and-validation-alias", + "/optional-list-uploadfile-alias-and-validation-alias", + ], +) +def test_optional_list_alias_and_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, files={"p": "hello"}) + assert response.status_code == 200 + assert response.json() == {"file_size": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-list-bytes-alias-and-validation-alias", + "/optional-list-uploadfile-alias-and-validation-alias", + ], +) +def test_optional_list_alias_and_validation_alias_by_alias(path: str): + client = TestClient(app) + response = client.post(path, files=[("p_alias", b"hello"), ("p_alias", b"world")]) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-list-bytes-alias-and-validation-alias", + "/optional-list-uploadfile-alias-and-validation-alias", + ], +) +def test_optional_list_alias_and_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.post( + path, files=[("p_val_alias", b"hello"), ("p_val_alias", b"world")] + ) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": [5, 5]} diff --git a/tests/test_request_params/test_file/test_required.py b/tests/test_request_params/test_file/test_required.py new file mode 100644 index 0000000000..a0f9d23a6b --- /dev/null +++ b/tests/test_request_params/test_file/test_required.py @@ -0,0 +1,429 @@ +from typing import Annotated + +import pytest +from fastapi import FastAPI, File, UploadFile +from fastapi.testclient import TestClient + +from .utils import get_body_model_name + +app = FastAPI() + +# ===================================================================================== +# Without aliases + + +@app.post("/required-bytes", operation_id="required_bytes") +async def read_required_bytes(p: Annotated[bytes, File()]): + return {"file_size": len(p)} + + +@app.post("/required-uploadfile", operation_id="required_uploadfile") +async def read_required_uploadfile(p: Annotated[UploadFile, File()]): + return {"file_size": p.size} + + +@pytest.mark.parametrize( + "path", + [ + "/required-bytes", + "/required-uploadfile", + ], +) +def test_required_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p": {"title": "P", "type": "string", "format": "binary"}, + }, + "required": ["p"], + "title": body_model_name, + "type": "object", + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-bytes", + "/required-uploadfile", + ], +) +def test_required_missing(path: str): + client = TestClient(app) + response = client.post(path) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "p"], + "msg": "Field required", + "input": None, + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-bytes", + "/required-uploadfile", + ], +) +def test_required(path: str): + client = TestClient(app) + response = client.post(path, files=[("p", b"hello")]) + assert response.status_code == 200 + assert response.json() == {"file_size": 5} + + +# ===================================================================================== +# Alias + + +@app.post("/required-bytes-alias", operation_id="required_bytes_alias") +async def read_required_bytes_alias(p: Annotated[bytes, File(alias="p_alias")]): + return {"file_size": len(p)} + + +@app.post("/required-uploadfile-alias", operation_id="required_uploadfile_alias") +async def read_required_uploadfile_alias( + p: Annotated[UploadFile, File(alias="p_alias")], +): + return {"file_size": p.size} + + +@pytest.mark.parametrize( + "path", + [ + "/required-bytes-alias", + "/required-uploadfile-alias", + ], +) +def test_required_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_alias": {"title": "P Alias", "type": "string", "format": "binary"}, + }, + "required": ["p_alias"], + "title": body_model_name, + "type": "object", + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-bytes-alias", + "/required-uploadfile-alias", + ], +) +def test_required_alias_missing(path: str): + client = TestClient(app) + response = client.post(path) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "p_alias"], + "msg": "Field required", + "input": None, + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-bytes-alias", + "/required-uploadfile-alias", + ], +) +def test_required_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, files=[("p", b"hello")]) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "p_alias"], + "msg": "Field required", + "input": None, + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-bytes-alias", + "/required-uploadfile-alias", + ], +) +def test_required_alias_by_alias(path: str): + client = TestClient(app) + response = client.post(path, files=[("p_alias", b"hello")]) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": 5} + + +# ===================================================================================== +# Validation alias + + +@app.post( + "/required-bytes-validation-alias", operation_id="required_bytes_validation_alias" +) +def read_required_bytes_validation_alias( + p: Annotated[bytes, File(validation_alias="p_val_alias")], +): + return {"file_size": len(p)} + + +@app.post( + "/required-uploadfile-validation-alias", + operation_id="required_uploadfile_validation_alias", +) +def read_required_uploadfile_validation_alias( + p: Annotated[UploadFile, File(validation_alias="p_val_alias")], +): + return {"file_size": p.size} + + +@pytest.mark.parametrize( + "path", + [ + "/required-bytes-validation-alias", + "/required-uploadfile-validation-alias", + ], +) +def test_required_validation_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_val_alias": { + "title": "P Val Alias", + "type": "string", + "format": "binary", + }, + }, + "required": ["p_val_alias"], + "title": body_model_name, + "type": "object", + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-bytes-validation-alias", + "/required-uploadfile-validation-alias", + ], +) +def test_required_validation_alias_missing(path: str): + client = TestClient(app) + response = client.post(path) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": [ + "body", + "p_val_alias", + ], + "msg": "Field required", + "input": None, + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-bytes-validation-alias", + "/required-uploadfile-validation-alias", + ], +) +def test_required_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, files=[("p", b"hello")]) + assert response.status_code == 422, response.text + + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "p_val_alias"], + "msg": "Field required", + "input": None, + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-bytes-validation-alias", + "/required-uploadfile-validation-alias", + ], +) +def test_required_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.post(path, files=[("p_val_alias", b"hello")]) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": 5} + + +# ===================================================================================== +# Alias and validation alias + + +@app.post( + "/required-bytes-alias-and-validation-alias", + operation_id="required_bytes_alias_and_validation_alias", +) +def read_required_bytes_alias_and_validation_alias( + p: Annotated[bytes, File(alias="p_alias", validation_alias="p_val_alias")], +): + return {"file_size": len(p)} + + +@app.post( + "/required-uploadfile-alias-and-validation-alias", + operation_id="required_uploadfile_alias_and_validation_alias", +) +def read_required_uploadfile_alias_and_validation_alias( + p: Annotated[UploadFile, File(alias="p_alias", validation_alias="p_val_alias")], +): + return {"file_size": p.size} + + +@pytest.mark.parametrize( + "path", + [ + "/required-bytes-alias-and-validation-alias", + "/required-uploadfile-alias-and-validation-alias", + ], +) +def test_required_alias_and_validation_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_val_alias": { + "title": "P Val Alias", + "type": "string", + "format": "binary", + }, + }, + "required": ["p_val_alias"], + "title": body_model_name, + "type": "object", + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-bytes-alias-and-validation-alias", + "/required-uploadfile-alias-and-validation-alias", + ], +) +def test_required_alias_and_validation_alias_missing(path: str): + client = TestClient(app) + response = client.post(path) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": [ + "body", + "p_val_alias", + ], + "msg": "Field required", + "input": None, + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-bytes-alias-and-validation-alias", + "/required-uploadfile-alias-and-validation-alias", + ], +) +def test_required_alias_and_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, files={"p": "hello"}) + assert response.status_code == 422 + + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": [ + "body", + "p_val_alias", + ], + "msg": "Field required", + "input": None, + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-bytes-alias-and-validation-alias", + "/required-uploadfile-alias-and-validation-alias", + ], +) +def test_required_alias_and_validation_alias_by_alias(path: str): + client = TestClient(app) + response = client.post(path, files=[("p_alias", b"hello")]) + assert response.status_code == 422, response.text + + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "p_val_alias"], + "msg": "Field required", + "input": None, + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-bytes-alias-and-validation-alias", + "/required-uploadfile-alias-and-validation-alias", + ], +) +def test_required_alias_and_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.post(path, files=[("p_val_alias", b"hello")]) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": 5} diff --git a/tests/test_request_params/test_file/utils.py b/tests/test_request_params/test_file/utils.py new file mode 100644 index 0000000000..e2a97ccd9b --- /dev/null +++ b/tests/test_request_params/test_file/utils.py @@ -0,0 +1,7 @@ +from typing import Any + + +def get_body_model_name(openapi: dict[str, Any], path: str) -> str: + body = openapi["paths"][path]["post"]["requestBody"] + body_schema = body["content"]["multipart/form-data"]["schema"] + return body_schema.get("$ref", "").split("/")[-1] diff --git a/tests/test_request_params/test_form/__init__.py b/tests/test_request_params/test_form/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/test_request_params/test_form/test_list.py b/tests/test_request_params/test_form/test_list.py new file mode 100644 index 0000000000..abe781c945 --- /dev/null +++ b/tests/test_request_params/test_form/test_list.py @@ -0,0 +1,436 @@ +from typing import Annotated + +import pytest +from dirty_equals import IsOneOf, IsPartialDict +from fastapi import FastAPI, Form +from fastapi.testclient import TestClient +from pydantic import BaseModel, Field + +from .utils import get_body_model_name + +app = FastAPI() + +# ===================================================================================== +# Without aliases + + +@app.post("/required-list-str", operation_id="required_list_str") +async def read_required_list_str(p: Annotated[list[str], Form()]): + return {"p": p} + + +class FormModelRequiredListStr(BaseModel): + p: list[str] + + +@app.post("/model-required-list-str", operation_id="model_required_list_str") +def read_model_required_list_str(p: Annotated[FormModelRequiredListStr, Form()]): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/required-list-str", "/model-required-list-str"], +) +def test_required_list_str_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p": { + "items": {"type": "string"}, + "title": "P", + "type": "array", + }, + }, + "required": ["p"], + "title": body_model_name, + "type": "object", + } + + +@pytest.mark.parametrize( + "path", + ["/required-list-str", "/model-required-list-str"], +) +def test_required_list_str_missing(path: str): + client = TestClient(app) + response = client.post(path) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "p"], + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } + + +@pytest.mark.parametrize( + "path", + ["/required-list-str", "/model-required-list-str"], +) +def test_required_list_str(path: str): + client = TestClient(app) + response = client.post(path, data={"p": ["hello", "world"]}) + assert response.status_code == 200 + assert response.json() == {"p": ["hello", "world"]} + + +# ===================================================================================== +# Alias + + +@app.post("/required-list-alias", operation_id="required_list_alias") +async def read_required_list_alias(p: Annotated[list[str], Form(alias="p_alias")]): + return {"p": p} + + +class FormModelRequiredListAlias(BaseModel): + p: list[str] = Field(alias="p_alias") + + +@app.post("/model-required-list-alias", operation_id="model_required_list_alias") +async def read_model_required_list_alias( + p: Annotated[FormModelRequiredListAlias, Form()], +): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + [ + "/required-list-alias", + "/model-required-list-alias", + ], +) +def test_required_list_str_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_alias": { + "items": {"type": "string"}, + "title": "P Alias", + "type": "array", + }, + }, + "required": ["p_alias"], + "title": body_model_name, + "type": "object", + } + + +@pytest.mark.parametrize( + "path", + ["/required-list-alias", "/model-required-list-alias"], +) +def test_required_list_alias_missing(path: str): + client = TestClient(app) + response = client.post(path) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "p_alias"], + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-list-alias", + "/model-required-list-alias", + ], +) +def test_required_list_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, data={"p": ["hello", "world"]}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "p_alias"], + "msg": "Field required", + "input": IsOneOf(None, {"p": ["hello", "world"]}), + } + ] + } + + +@pytest.mark.parametrize( + "path", + ["/required-list-alias", "/model-required-list-alias"], +) +def test_required_list_alias_by_alias(path: str): + client = TestClient(app) + response = client.post(path, data={"p_alias": ["hello", "world"]}) + assert response.status_code == 200, response.text + assert response.json() == {"p": ["hello", "world"]} + + +# ===================================================================================== +# Validation alias + + +@app.post( + "/required-list-validation-alias", operation_id="required_list_validation_alias" +) +def read_required_list_validation_alias( + p: Annotated[list[str], Form(validation_alias="p_val_alias")], +): + return {"p": p} + + +class FormModelRequiredListValidationAlias(BaseModel): + p: list[str] = Field(validation_alias="p_val_alias") + + +@app.post( + "/model-required-list-validation-alias", + operation_id="model_required_list_validation_alias", +) +async def read_model_required_list_validation_alias( + p: Annotated[FormModelRequiredListValidationAlias, Form()], +): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/required-list-validation-alias", "/model-required-list-validation-alias"], +) +def test_required_list_validation_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_val_alias": { + "items": {"type": "string"}, + "title": "P Val Alias", + "type": "array", + }, + }, + "required": ["p_val_alias"], + "title": body_model_name, + "type": "object", + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-list-validation-alias", + "/model-required-list-validation-alias", + ], +) +def test_required_list_validation_alias_missing(path: str): + client = TestClient(app) + response = client.post(path) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": [ + "body", + "p_val_alias", + ], + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-list-validation-alias", + "/model-required-list-validation-alias", + ], +) +def test_required_list_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, data={"p": ["hello", "world"]}) + assert response.status_code == 422, response.text + + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "p_val_alias"], + "msg": "Field required", + "input": IsOneOf(None, IsPartialDict({"p": ["hello", "world"]})), + } + ] + } + + +@pytest.mark.parametrize( + "path", + ["/required-list-validation-alias", "/model-required-list-validation-alias"], +) +def test_required_list_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.post(path, data={"p_val_alias": ["hello", "world"]}) + assert response.status_code == 200, response.text + + assert response.json() == {"p": ["hello", "world"]} + + +# ===================================================================================== +# Alias and validation alias + + +@app.post( + "/required-list-alias-and-validation-alias", + operation_id="required_list_alias_and_validation_alias", +) +def read_required_list_alias_and_validation_alias( + p: Annotated[list[str], Form(alias="p_alias", validation_alias="p_val_alias")], +): + return {"p": p} + + +class FormModelRequiredListAliasAndValidationAlias(BaseModel): + p: list[str] = Field(alias="p_alias", validation_alias="p_val_alias") + + +@app.post( + "/model-required-list-alias-and-validation-alias", + operation_id="model_required_list_alias_and_validation_alias", +) +def read_model_required_list_alias_and_validation_alias( + p: Annotated[FormModelRequiredListAliasAndValidationAlias, Form()], +): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + [ + "/required-list-alias-and-validation-alias", + "/model-required-list-alias-and-validation-alias", + ], +) +def test_required_list_alias_and_validation_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_val_alias": { + "items": {"type": "string"}, + "title": "P Val Alias", + "type": "array", + }, + }, + "required": ["p_val_alias"], + "title": body_model_name, + "type": "object", + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-list-alias-and-validation-alias", + "/model-required-list-alias-and-validation-alias", + ], +) +def test_required_list_alias_and_validation_alias_missing(path: str): + client = TestClient(app) + response = client.post(path) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": [ + "body", + "p_val_alias", + ], + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-list-alias-and-validation-alias", + "/model-required-list-alias-and-validation-alias", + ], +) +def test_required_list_alias_and_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, data={"p": ["hello", "world"]}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": [ + "body", + "p_val_alias", + ], + "msg": "Field required", + "input": IsOneOf( + None, + {"p": ["hello", "world"]}, + ), + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-list-alias-and-validation-alias", + "/model-required-list-alias-and-validation-alias", + ], +) +def test_required_list_alias_and_validation_alias_by_alias(path: str): + client = TestClient(app) + response = client.post(path, data={"p_alias": ["hello", "world"]}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "p_val_alias"], + "msg": "Field required", + "input": IsOneOf(None, {"p_alias": ["hello", "world"]}), + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-list-alias-and-validation-alias", + "/model-required-list-alias-and-validation-alias", + ], +) +def test_required_list_alias_and_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.post(path, data={"p_val_alias": ["hello", "world"]}) + assert response.status_code == 200, response.text + assert response.json() == {"p": ["hello", "world"]} diff --git a/tests/test_request_params/test_form/test_optional_list.py b/tests/test_request_params/test_form/test_optional_list.py new file mode 100644 index 0000000000..6d1957a18c --- /dev/null +++ b/tests/test_request_params/test_form/test_optional_list.py @@ -0,0 +1,360 @@ +from typing import Annotated, Optional + +import pytest +from fastapi import FastAPI, Form +from fastapi.testclient import TestClient +from pydantic import BaseModel, Field + +from .utils import get_body_model_name + +app = FastAPI() + +# ===================================================================================== +# Without aliases + + +@app.post("/optional-list-str", operation_id="optional_list_str") +async def read_optional_list_str( + p: Annotated[Optional[list[str]], Form()] = None, +): + return {"p": p} + + +class FormModelOptionalListStr(BaseModel): + p: Optional[list[str]] = None + + +@app.post("/model-optional-list-str", operation_id="model_optional_list_str") +async def read_model_optional_list_str(p: Annotated[FormModelOptionalListStr, Form()]): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/optional-list-str", "/model-optional-list-str"], +) +def test_optional_list_str_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p": { + "anyOf": [ + {"items": {"type": "string"}, "type": "array"}, + {"type": "null"}, + ], + "title": "P", + }, + }, + "title": body_model_name, + "type": "object", + } + + +@pytest.mark.parametrize( + "path", + ["/optional-list-str", "/model-optional-list-str"], +) +def test_optional_list_str_missing(path: str): + client = TestClient(app) + response = client.post(path) + assert response.status_code == 200, response.text + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + ["/optional-list-str", "/model-optional-list-str"], +) +def test_optional_list_str(path: str): + client = TestClient(app) + response = client.post(path, data={"p": ["hello", "world"]}) + assert response.status_code == 200 + assert response.json() == {"p": ["hello", "world"]} + + +# ===================================================================================== +# Alias + + +@app.post("/optional-list-alias", operation_id="optional_list_alias") +async def read_optional_list_alias( + p: Annotated[Optional[list[str]], Form(alias="p_alias")] = None, +): + return {"p": p} + + +class FormModelOptionalListAlias(BaseModel): + p: Optional[list[str]] = Field(None, alias="p_alias") + + +@app.post("/model-optional-list-alias", operation_id="model_optional_list_alias") +async def read_model_optional_list_alias( + p: Annotated[FormModelOptionalListAlias, Form()], +): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-list-alias", + "/model-optional-list-alias", + ], +) +def test_optional_list_str_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_alias": { + "anyOf": [ + {"items": {"type": "string"}, "type": "array"}, + {"type": "null"}, + ], + "title": "P Alias", + }, + }, + "title": body_model_name, + "type": "object", + } + + +@pytest.mark.parametrize( + "path", + ["/optional-list-alias", "/model-optional-list-alias"], +) +def test_optional_list_alias_missing(path: str): + client = TestClient(app) + response = client.post(path) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + ["/optional-list-alias", "/model-optional-list-alias"], +) +def test_optional_list_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, data={"p": ["hello", "world"]}) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + ["/optional-list-alias", "/model-optional-list-alias"], +) +def test_optional_list_alias_by_alias(path: str): + client = TestClient(app) + response = client.post(path, data={"p_alias": ["hello", "world"]}) + assert response.status_code == 200 + assert response.json() == {"p": ["hello", "world"]} + + +# ===================================================================================== +# Validation alias + + +@app.post( + "/optional-list-validation-alias", operation_id="optional_list_validation_alias" +) +def read_optional_list_validation_alias( + p: Annotated[Optional[list[str]], Form(validation_alias="p_val_alias")] = None, +): + return {"p": p} + + +class FormModelOptionalListValidationAlias(BaseModel): + p: Optional[list[str]] = Field(None, validation_alias="p_val_alias") + + +@app.post( + "/model-optional-list-validation-alias", + operation_id="model_optional_list_validation_alias", +) +def read_model_optional_list_validation_alias( + p: Annotated[FormModelOptionalListValidationAlias, Form()], +): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/optional-list-validation-alias", "/model-optional-list-validation-alias"], +) +def test_optional_list_validation_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_val_alias": { + "anyOf": [ + {"items": {"type": "string"}, "type": "array"}, + {"type": "null"}, + ], + "title": "P Val Alias", + }, + }, + "title": body_model_name, + "type": "object", + } + + +@pytest.mark.parametrize( + "path", + ["/optional-list-validation-alias", "/model-optional-list-validation-alias"], +) +def test_optional_list_validation_alias_missing(path: str): + client = TestClient(app) + response = client.post(path) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-list-validation-alias", + "/model-optional-list-validation-alias", + ], +) +def test_optional_list_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, data={"p": ["hello", "world"]}) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + ["/optional-list-validation-alias", "/model-optional-list-validation-alias"], +) +def test_optional_list_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.post(path, data={"p_val_alias": ["hello", "world"]}) + assert response.status_code == 200, response.text + assert response.json() == {"p": ["hello", "world"]} + + +# ===================================================================================== +# Alias and validation alias + + +@app.post( + "/optional-list-alias-and-validation-alias", + operation_id="optional_list_alias_and_validation_alias", +) +def read_optional_list_alias_and_validation_alias( + p: Annotated[ + Optional[list[str]], Form(alias="p_alias", validation_alias="p_val_alias") + ] = None, +): + return {"p": p} + + +class FormModelOptionalListAliasAndValidationAlias(BaseModel): + p: Optional[list[str]] = Field( + None, alias="p_alias", validation_alias="p_val_alias" + ) + + +@app.post( + "/model-optional-list-alias-and-validation-alias", + operation_id="model_optional_list_alias_and_validation_alias", +) +def read_model_optional_list_alias_and_validation_alias( + p: Annotated[FormModelOptionalListAliasAndValidationAlias, Form()], +): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-list-alias-and-validation-alias", + "/model-optional-list-alias-and-validation-alias", + ], +) +def test_optional_list_alias_and_validation_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_val_alias": { + "anyOf": [ + {"items": {"type": "string"}, "type": "array"}, + {"type": "null"}, + ], + "title": "P Val Alias", + }, + }, + "title": body_model_name, + "type": "object", + } + + +@pytest.mark.parametrize( + "path", + [ + "/optional-list-alias-and-validation-alias", + "/model-optional-list-alias-and-validation-alias", + ], +) +def test_optional_list_alias_and_validation_alias_missing(path: str): + client = TestClient(app) + response = client.post(path) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-list-alias-and-validation-alias", + "/model-optional-list-alias-and-validation-alias", + ], +) +def test_optional_list_alias_and_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, data={"p": ["hello", "world"]}) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-list-alias-and-validation-alias", + "/model-optional-list-alias-and-validation-alias", + ], +) +def test_optional_list_alias_and_validation_alias_by_alias(path: str): + client = TestClient(app) + response = client.post(path, data={"p_alias": ["hello", "world"]}) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-list-alias-and-validation-alias", + "/model-optional-list-alias-and-validation-alias", + ], +) +def test_optional_list_alias_and_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.post(path, data={"p_val_alias": ["hello", "world"]}) + assert response.status_code == 200, response.text + assert response.json() == { + "p": [ + "hello", + "world", + ] + } diff --git a/tests/test_request_params/test_form/test_optional_str.py b/tests/test_request_params/test_form/test_optional_str.py new file mode 100644 index 0000000000..810e83caa3 --- /dev/null +++ b/tests/test_request_params/test_form/test_optional_str.py @@ -0,0 +1,337 @@ +from typing import Annotated, Optional + +import pytest +from fastapi import FastAPI, Form +from fastapi.testclient import TestClient +from pydantic import BaseModel, Field + +from .utils import get_body_model_name + +app = FastAPI() + +# ===================================================================================== +# Without aliases + + +@app.post("/optional-str", operation_id="optional_str") +async def read_optional_str(p: Annotated[Optional[str], Form()] = None): + return {"p": p} + + +class FormModelOptionalStr(BaseModel): + p: Optional[str] = None + + +@app.post("/model-optional-str", operation_id="model_optional_str") +async def read_model_optional_str(p: Annotated[FormModelOptionalStr, Form()]): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/optional-str", "/model-optional-str"], +) +def test_optional_str_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P", + }, + }, + "title": body_model_name, + "type": "object", + } + + +@pytest.mark.parametrize( + "path", + ["/optional-str", "/model-optional-str"], +) +def test_optional_str_missing(path: str): + client = TestClient(app) + response = client.post(path) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + ["/optional-str", "/model-optional-str"], +) +def test_optional_str(path: str): + client = TestClient(app) + response = client.post(path, data={"p": "hello"}) + assert response.status_code == 200 + assert response.json() == {"p": "hello"} + + +# ===================================================================================== +# Alias + + +@app.post("/optional-alias", operation_id="optional_alias") +async def read_optional_alias( + p: Annotated[Optional[str], Form(alias="p_alias")] = None, +): + return {"p": p} + + +class FormModelOptionalAlias(BaseModel): + p: Optional[str] = Field(None, alias="p_alias") + + +@app.post("/model-optional-alias", operation_id="model_optional_alias") +async def read_model_optional_alias(p: Annotated[FormModelOptionalAlias, Form()]): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-alias", + "/model-optional-alias", + ], +) +def test_optional_str_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_alias": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P Alias", + }, + }, + "title": body_model_name, + "type": "object", + } + + +@pytest.mark.parametrize( + "path", + ["/optional-alias", "/model-optional-alias"], +) +def test_optional_alias_missing(path: str): + client = TestClient(app) + response = client.post(path) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + ["/optional-alias", "/model-optional-alias"], +) +def test_optional_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, data={"p": "hello"}) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + ["/optional-alias", "/model-optional-alias"], +) +def test_optional_alias_by_alias(path: str): + client = TestClient(app) + response = client.post(path, data={"p_alias": "hello"}) + assert response.status_code == 200 + assert response.json() == {"p": "hello"} + + +# ===================================================================================== +# Validation alias + + +@app.post("/optional-validation-alias", operation_id="optional_validation_alias") +def read_optional_validation_alias( + p: Annotated[Optional[str], Form(validation_alias="p_val_alias")] = None, +): + return {"p": p} + + +class FormModelOptionalValidationAlias(BaseModel): + p: Optional[str] = Field(None, validation_alias="p_val_alias") + + +@app.post( + "/model-optional-validation-alias", operation_id="model_optional_validation_alias" +) +def read_model_optional_validation_alias( + p: Annotated[FormModelOptionalValidationAlias, Form()], +): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/optional-validation-alias", "/model-optional-validation-alias"], +) +def test_optional_validation_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_val_alias": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P Val Alias", + }, + }, + "title": body_model_name, + "type": "object", + } + + +@pytest.mark.parametrize( + "path", + ["/optional-validation-alias", "/model-optional-validation-alias"], +) +def test_optional_validation_alias_missing(path: str): + client = TestClient(app) + response = client.post(path) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-validation-alias", + "/model-optional-validation-alias", + ], +) +def test_optional_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, data={"p": "hello"}) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-validation-alias", + "/model-optional-validation-alias", + ], +) +def test_optional_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.post(path, data={"p_val_alias": "hello"}) + assert response.status_code == 200 + assert response.json() == {"p": "hello"} + + +# ===================================================================================== +# Alias and validation alias + + +@app.post( + "/optional-alias-and-validation-alias", + operation_id="optional_alias_and_validation_alias", +) +def read_optional_alias_and_validation_alias( + p: Annotated[ + Optional[str], Form(alias="p_alias", validation_alias="p_val_alias") + ] = None, +): + return {"p": p} + + +class FormModelOptionalAliasAndValidationAlias(BaseModel): + p: Optional[str] = Field(None, alias="p_alias", validation_alias="p_val_alias") + + +@app.post( + "/model-optional-alias-and-validation-alias", + operation_id="model_optional_alias_and_validation_alias", +) +def read_model_optional_alias_and_validation_alias( + p: Annotated[FormModelOptionalAliasAndValidationAlias, Form()], +): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-alias-and-validation-alias", + "/model-optional-alias-and-validation-alias", + ], +) +def test_optional_alias_and_validation_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_val_alias": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P Val Alias", + }, + }, + "title": body_model_name, + "type": "object", + } + + +@pytest.mark.parametrize( + "path", + [ + "/optional-alias-and-validation-alias", + "/model-optional-alias-and-validation-alias", + ], +) +def test_optional_alias_and_validation_alias_missing(path: str): + client = TestClient(app) + response = client.post(path) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-alias-and-validation-alias", + "/model-optional-alias-and-validation-alias", + ], +) +def test_optional_alias_and_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, data={"p": "hello"}) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-alias-and-validation-alias", + "/model-optional-alias-and-validation-alias", + ], +) +def test_optional_alias_and_validation_alias_by_alias(path: str): + client = TestClient(app) + response = client.post(path, data={"p_alias": "hello"}) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-alias-and-validation-alias", + "/model-optional-alias-and-validation-alias", + ], +) +def test_optional_alias_and_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.post(path, data={"p_val_alias": "hello"}) + assert response.status_code == 200 + assert response.json() == {"p": "hello"} diff --git a/tests/test_request_params/test_form/test_required_str.py b/tests/test_request_params/test_form/test_required_str.py new file mode 100644 index 0000000000..7c9523b308 --- /dev/null +++ b/tests/test_request_params/test_form/test_required_str.py @@ -0,0 +1,415 @@ +from typing import Annotated + +import pytest +from dirty_equals import IsOneOf +from fastapi import FastAPI, Form +from fastapi.testclient import TestClient +from pydantic import BaseModel, Field + +from .utils import get_body_model_name + +app = FastAPI() + +# ===================================================================================== +# Without aliases + + +@app.post("/required-str", operation_id="required_str") +async def read_required_str(p: Annotated[str, Form()]): + return {"p": p} + + +class FormModelRequiredStr(BaseModel): + p: str + + +@app.post("/model-required-str", operation_id="model_required_str") +async def read_model_required_str(p: Annotated[FormModelRequiredStr, Form()]): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/required-str", "/model-required-str"], +) +def test_required_str_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p": {"title": "P", "type": "string"}, + }, + "required": ["p"], + "title": body_model_name, + "type": "object", + } + + +@pytest.mark.parametrize( + "path", + ["/required-str", "/model-required-str"], +) +def test_required_str_missing(path: str): + client = TestClient(app) + response = client.post(path) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "p"], + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } + + +@pytest.mark.parametrize( + "path", + ["/required-str", "/model-required-str"], +) +def test_required_str(path: str): + client = TestClient(app) + response = client.post(path, data={"p": "hello"}) + assert response.status_code == 200 + assert response.json() == {"p": "hello"} + + +# ===================================================================================== +# Alias + + +@app.post("/required-alias", operation_id="required_alias") +async def read_required_alias(p: Annotated[str, Form(alias="p_alias")]): + return {"p": p} + + +class FormModelRequiredAlias(BaseModel): + p: str = Field(alias="p_alias") + + +@app.post("/model-required-alias", operation_id="model_required_alias") +async def read_model_required_alias(p: Annotated[FormModelRequiredAlias, Form()]): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + [ + "/required-alias", + "/model-required-alias", + ], +) +def test_required_str_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_alias": {"title": "P Alias", "type": "string"}, + }, + "required": ["p_alias"], + "title": body_model_name, + "type": "object", + } + + +@pytest.mark.parametrize( + "path", + ["/required-alias", "/model-required-alias"], +) +def test_required_alias_missing(path: str): + client = TestClient(app) + response = client.post(path) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "p_alias"], + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } + + +@pytest.mark.parametrize( + "path", + ["/required-alias", "/model-required-alias"], +) +def test_required_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, data={"p": "hello"}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "p_alias"], + "msg": "Field required", + "input": IsOneOf(None, {"p": "hello"}), + } + ] + } + + +@pytest.mark.parametrize( + "path", + ["/required-alias", "/model-required-alias"], +) +def test_required_alias_by_alias(path: str): + client = TestClient(app) + response = client.post(path, data={"p_alias": "hello"}) + assert response.status_code == 200, response.text + assert response.json() == {"p": "hello"} + + +# ===================================================================================== +# Validation alias + + +@app.post("/required-validation-alias", operation_id="required_validation_alias") +def read_required_validation_alias( + p: Annotated[str, Form(validation_alias="p_val_alias")], +): + return {"p": p} + + +class FormModelRequiredValidationAlias(BaseModel): + p: str = Field(validation_alias="p_val_alias") + + +@app.post( + "/model-required-validation-alias", operation_id="model_required_validation_alias" +) +def read_model_required_validation_alias( + p: Annotated[FormModelRequiredValidationAlias, Form()], +): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/required-validation-alias", "/model-required-validation-alias"], +) +def test_required_validation_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_val_alias": {"title": "P Val Alias", "type": "string"}, + }, + "required": ["p_val_alias"], + "title": body_model_name, + "type": "object", + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-validation-alias", + "/model-required-validation-alias", + ], +) +def test_required_validation_alias_missing(path: str): + client = TestClient(app) + response = client.post(path) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": [ + "body", + "p_val_alias", + ], + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-validation-alias", + "/model-required-validation-alias", + ], +) +def test_required_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, data={"p": "hello"}) + assert response.status_code == 422, response.text + + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "p_val_alias"], + "msg": "Field required", + "input": IsOneOf(None, {"p": "hello"}), + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-validation-alias", + "/model-required-validation-alias", + ], +) +def test_required_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.post(path, data={"p_val_alias": "hello"}) + assert response.status_code == 200, response.text + + assert response.json() == {"p": "hello"} + + +# ===================================================================================== +# Alias and validation alias + + +@app.post( + "/required-alias-and-validation-alias", + operation_id="required_alias_and_validation_alias", +) +def read_required_alias_and_validation_alias( + p: Annotated[str, Form(alias="p_alias", validation_alias="p_val_alias")], +): + return {"p": p} + + +class FormModelRequiredAliasAndValidationAlias(BaseModel): + p: str = Field(alias="p_alias", validation_alias="p_val_alias") + + +@app.post( + "/model-required-alias-and-validation-alias", + operation_id="model_required_alias_and_validation_alias", +) +def read_model_required_alias_and_validation_alias( + p: Annotated[FormModelRequiredAliasAndValidationAlias, Form()], +): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + [ + "/required-alias-and-validation-alias", + "/model-required-alias-and-validation-alias", + ], +) +def test_required_alias_and_validation_alias_schema(path: str): + openapi = app.openapi() + body_model_name = get_body_model_name(openapi, path) + + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_val_alias": {"title": "P Val Alias", "type": "string"}, + }, + "required": ["p_val_alias"], + "title": body_model_name, + "type": "object", + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-alias-and-validation-alias", + "/model-required-alias-and-validation-alias", + ], +) +def test_required_alias_and_validation_alias_missing(path: str): + client = TestClient(app) + response = client.post(path) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": [ + "body", + "p_val_alias", + ], + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-alias-and-validation-alias", + "/model-required-alias-and-validation-alias", + ], +) +def test_required_alias_and_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.post(path, data={"p": "hello"}) + assert response.status_code == 422 + + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": [ + "body", + "p_val_alias", + ], + "msg": "Field required", + "input": IsOneOf(None, {"p": "hello"}), + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-alias-and-validation-alias", + "/model-required-alias-and-validation-alias", + ], +) +def test_required_alias_and_validation_alias_by_alias(path: str): + client = TestClient(app) + response = client.post(path, data={"p_alias": "hello"}) + assert response.status_code == 422, response.text + + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "p_val_alias"], + "msg": "Field required", + "input": IsOneOf(None, {"p_alias": "hello"}), + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-alias-and-validation-alias", + "/model-required-alias-and-validation-alias", + ], +) +def test_required_alias_and_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.post(path, data={"p_val_alias": "hello"}) + assert response.status_code == 200, response.text + + assert response.json() == {"p": "hello"} diff --git a/tests/test_request_params/test_form/utils.py b/tests/test_request_params/test_form/utils.py new file mode 100644 index 0000000000..9132173111 --- /dev/null +++ b/tests/test_request_params/test_form/utils.py @@ -0,0 +1,7 @@ +from typing import Any + + +def get_body_model_name(openapi: dict[str, Any], path: str) -> str: + body = openapi["paths"][path]["post"]["requestBody"] + body_schema = body["content"]["application/x-www-form-urlencoded"]["schema"] + return body_schema.get("$ref", "").split("/")[-1] diff --git a/tests/test_request_params/test_header/__init__.py b/tests/test_request_params/test_header/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/test_request_params/test_header/test_list.py b/tests/test_request_params/test_header/test_list.py new file mode 100644 index 0000000000..489a6b3e7d --- /dev/null +++ b/tests/test_request_params/test_header/test_list.py @@ -0,0 +1,418 @@ +from typing import Annotated + +import pytest +from dirty_equals import AnyThing, IsOneOf, IsPartialDict +from fastapi import FastAPI, Header +from fastapi.testclient import TestClient +from pydantic import BaseModel, Field + +app = FastAPI() + +# ===================================================================================== +# Without aliases + + +@app.get("/required-list-str") +async def read_required_list_str(p: Annotated[list[str], Header()]): + return {"p": p} + + +class HeaderModelRequiredListStr(BaseModel): + p: list[str] + + +@app.get("/model-required-list-str") +def read_model_required_list_str(p: Annotated[HeaderModelRequiredListStr, Header()]): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/required-list-str", "/model-required-list-str"], +) +def test_required_list_str_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": True, + "schema": { + "title": "P", + "type": "array", + "items": {"type": "string"}, + }, + "name": "p", + "in": "header", + } + ] + + +@pytest.mark.parametrize( + "path", + ["/required-list-str", "/model-required-list-str"], +) +def test_required_list_str_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["header", "p"], + "msg": "Field required", + "input": AnyThing, + } + ] + } + + +@pytest.mark.parametrize( + "path", + ["/required-list-str", "/model-required-list-str"], +) +def test_required_list_str(path: str): + client = TestClient(app) + response = client.get(path, headers=[("p", "hello"), ("p", "world")]) + assert response.status_code == 200 + assert response.json() == {"p": ["hello", "world"]} + + +# ===================================================================================== +# Alias + + +@app.get("/required-list-alias") +async def read_required_list_alias(p: Annotated[list[str], Header(alias="p_alias")]): + return {"p": p} + + +class HeaderModelRequiredListAlias(BaseModel): + p: list[str] = Field(alias="p_alias") + + +@app.get("/model-required-list-alias") +async def read_model_required_list_alias( + p: Annotated[HeaderModelRequiredListAlias, Header()], +): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/required-list-alias", "/model-required-list-alias"], +) +def test_required_list_str_alias_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": True, + "schema": { + "title": "P Alias", + "type": "array", + "items": {"type": "string"}, + }, + "name": "p_alias", + "in": "header", + } + ] + + +@pytest.mark.parametrize( + "path", + ["/required-list-alias", "/model-required-list-alias"], +) +def test_required_list_alias_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["header", "p_alias"], + "msg": "Field required", + "input": AnyThing, + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-list-alias", + "/model-required-list-alias", + ], +) +def test_required_list_alias_by_name(path: str): + client = TestClient(app) + response = client.get(path, headers=[("p", "hello"), ("p", "world")]) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["header", "p_alias"], + "msg": "Field required", + "input": IsOneOf(None, IsPartialDict({"p": ["hello", "world"]})), + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-list-alias", + "/model-required-list-alias", + ], +) +def test_required_list_alias_by_alias(path: str): + client = TestClient(app) + response = client.get(path, headers=[("p_alias", "hello"), ("p_alias", "world")]) + assert response.status_code == 200, response.text + assert response.json() == {"p": ["hello", "world"]} + + +# ===================================================================================== +# Validation alias + + +@app.get("/required-list-validation-alias") +def read_required_list_validation_alias( + p: Annotated[list[str], Header(validation_alias="p_val_alias")], +): + return {"p": p} + + +class HeaderModelRequiredListValidationAlias(BaseModel): + p: list[str] = Field(validation_alias="p_val_alias") + + +@app.get("/model-required-list-validation-alias") +async def read_model_required_list_validation_alias( + p: Annotated[HeaderModelRequiredListValidationAlias, Header()], +): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/required-list-validation-alias", "/model-required-list-validation-alias"], +) +def test_required_list_validation_alias_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": True, + "schema": { + "title": "P Val Alias", + "type": "array", + "items": {"type": "string"}, + }, + "name": "p_val_alias", + "in": "header", + } + ] + + +@pytest.mark.parametrize( + "path", + [ + "/required-list-validation-alias", + "/model-required-list-validation-alias", + ], +) +def test_required_list_validation_alias_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": [ + "header", + "p_val_alias", + ], + "msg": "Field required", + "input": AnyThing, + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-list-validation-alias", + "/model-required-list-validation-alias", + ], +) +def test_required_list_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.get(path, headers=[("p", "hello"), ("p", "world")]) + assert response.status_code == 422 + + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["header", "p_val_alias"], + "msg": "Field required", + "input": IsOneOf(None, IsPartialDict({"p": ["hello", "world"]})), + } + ] + } + + +@pytest.mark.parametrize( + "path", + ["/required-list-validation-alias", "/model-required-list-validation-alias"], +) +def test_required_list_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.get( + path, headers=[("p_val_alias", "hello"), ("p_val_alias", "world")] + ) + assert response.status_code == 200, response.text + + assert response.json() == {"p": ["hello", "world"]} + + +# ===================================================================================== +# Alias and validation alias + + +@app.get("/required-list-alias-and-validation-alias") +def read_required_list_alias_and_validation_alias( + p: Annotated[list[str], Header(alias="p_alias", validation_alias="p_val_alias")], +): + return {"p": p} + + +class HeaderModelRequiredListAliasAndValidationAlias(BaseModel): + p: list[str] = Field(alias="p_alias", validation_alias="p_val_alias") + + +@app.get("/model-required-list-alias-and-validation-alias") +def read_model_required_list_alias_and_validation_alias( + p: Annotated[HeaderModelRequiredListAliasAndValidationAlias, Header()], +): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + [ + "/required-list-alias-and-validation-alias", + "/model-required-list-alias-and-validation-alias", + ], +) +def test_required_list_alias_and_validation_alias_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": True, + "schema": { + "title": "P Val Alias", + "type": "array", + "items": {"type": "string"}, + }, + "name": "p_val_alias", + "in": "header", + } + ] + + +@pytest.mark.parametrize( + "path", + [ + "/required-list-alias-and-validation-alias", + "/model-required-list-alias-and-validation-alias", + ], +) +def test_required_list_alias_and_validation_alias_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": [ + "header", + "p_val_alias", + ], + "msg": "Field required", + "input": AnyThing, + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-list-alias-and-validation-alias", + "/model-required-list-alias-and-validation-alias", + ], +) +def test_required_list_alias_and_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.get(path, headers=[("p", "hello"), ("p", "world")]) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": [ + "header", + "p_val_alias", + ], + "msg": "Field required", + "input": IsOneOf( + None, + IsPartialDict({"p": ["hello", "world"]}), + ), + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-list-alias-and-validation-alias", + "/model-required-list-alias-and-validation-alias", + ], +) +def test_required_list_alias_and_validation_alias_by_alias(path: str): + client = TestClient(app) + response = client.get(path, headers=[("p_alias", "hello"), ("p_alias", "world")]) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["header", "p_val_alias"], + "msg": "Field required", + "input": IsOneOf( + None, + IsPartialDict({"p_alias": ["hello", "world"]}), + ), + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-list-alias-and-validation-alias", + "/model-required-list-alias-and-validation-alias", + ], +) +def test_required_list_alias_and_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.get( + path, headers=[("p_val_alias", "hello"), ("p_val_alias", "world")] + ) + assert response.status_code == 200, response.text + assert response.json() == {"p": ["hello", "world"]} diff --git a/tests/test_request_params/test_header/test_optional_list.py b/tests/test_request_params/test_header/test_optional_list.py new file mode 100644 index 0000000000..5dd4ea9ade --- /dev/null +++ b/tests/test_request_params/test_header/test_optional_list.py @@ -0,0 +1,345 @@ +from typing import Annotated, Optional + +import pytest +from fastapi import FastAPI, Header +from fastapi.testclient import TestClient +from pydantic import BaseModel, Field + +app = FastAPI() + +# ===================================================================================== +# Without aliases + + +@app.get("/optional-list-str") +async def read_optional_list_str( + p: Annotated[Optional[list[str]], Header()] = None, +): + return {"p": p} + + +class HeaderModelOptionalListStr(BaseModel): + p: Optional[list[str]] = None + + +@app.get("/model-optional-list-str") +async def read_model_optional_list_str( + p: Annotated[HeaderModelOptionalListStr, Header()], +): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/optional-list-str", "/model-optional-list-str"], +) +def test_optional_list_str_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": False, + "schema": { + "anyOf": [ + {"items": {"type": "string"}, "type": "array"}, + {"type": "null"}, + ], + "title": "P", + }, + "name": "p", + "in": "header", + } + ] + + +@pytest.mark.parametrize( + "path", + ["/optional-list-str", "/model-optional-list-str"], +) +def test_optional_list_str_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 200, response.text + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + ["/optional-list-str", "/model-optional-list-str"], +) +def test_optional_list_str(path: str): + client = TestClient(app) + response = client.get(path, headers=[("p", "hello"), ("p", "world")]) + assert response.status_code == 200 + assert response.json() == {"p": ["hello", "world"]} + + +# ===================================================================================== +# Alias + + +@app.get("/optional-list-alias") +async def read_optional_list_alias( + p: Annotated[Optional[list[str]], Header(alias="p_alias")] = None, +): + return {"p": p} + + +class HeaderModelOptionalListAlias(BaseModel): + p: Optional[list[str]] = Field(None, alias="p_alias") + + +@app.get("/model-optional-list-alias") +async def read_model_optional_list_alias( + p: Annotated[HeaderModelOptionalListAlias, Header()], +): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/optional-list-alias", "/model-optional-list-alias"], +) +def test_optional_list_str_alias_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": False, + "schema": { + "anyOf": [ + {"items": {"type": "string"}, "type": "array"}, + {"type": "null"}, + ], + "title": "P Alias", + }, + "name": "p_alias", + "in": "header", + } + ] + + +@pytest.mark.parametrize( + "path", + ["/optional-list-alias", "/model-optional-list-alias"], +) +def test_optional_list_alias_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + ["/optional-list-alias", "/model-optional-list-alias"], +) +def test_optional_list_alias_by_name(path: str): + client = TestClient(app) + response = client.get(path, headers=[("p", "hello"), ("p", "world")]) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-list-alias", + "/model-optional-list-alias", + ], +) +def test_optional_list_alias_by_alias(path: str): + client = TestClient(app) + response = client.get(path, headers=[("p_alias", "hello"), ("p_alias", "world")]) + assert response.status_code == 200 + assert response.json() == {"p": ["hello", "world"]} + + +# ===================================================================================== +# Validation alias + + +@app.get("/optional-list-validation-alias") +def read_optional_list_validation_alias( + p: Annotated[Optional[list[str]], Header(validation_alias="p_val_alias")] = None, +): + return {"p": p} + + +class HeaderModelOptionalListValidationAlias(BaseModel): + p: Optional[list[str]] = Field(None, validation_alias="p_val_alias") + + +@app.get("/model-optional-list-validation-alias") +def read_model_optional_list_validation_alias( + p: Annotated[HeaderModelOptionalListValidationAlias, Header()], +): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/optional-list-validation-alias", "/model-optional-list-validation-alias"], +) +def test_optional_list_validation_alias_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": False, + "schema": { + "anyOf": [ + {"items": {"type": "string"}, "type": "array"}, + {"type": "null"}, + ], + "title": "P Val Alias", + }, + "name": "p_val_alias", + "in": "header", + } + ] + + +@pytest.mark.parametrize( + "path", + ["/optional-list-validation-alias", "/model-optional-list-validation-alias"], +) +def test_optional_list_validation_alias_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-list-validation-alias", + "/model-optional-list-validation-alias", + ], +) +def test_optional_list_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.get(path, headers=[("p", "hello"), ("p", "world")]) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + ["/optional-list-validation-alias", "/model-optional-list-validation-alias"], +) +def test_optional_list_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.get( + path, headers=[("p_val_alias", "hello"), ("p_val_alias", "world")] + ) + assert response.status_code == 200, response.text + assert response.json() == {"p": ["hello", "world"]} + + +# ===================================================================================== +# Alias and validation alias + + +@app.get("/optional-list-alias-and-validation-alias") +def read_optional_list_alias_and_validation_alias( + p: Annotated[ + Optional[list[str]], Header(alias="p_alias", validation_alias="p_val_alias") + ] = None, +): + return {"p": p} + + +class HeaderModelOptionalListAliasAndValidationAlias(BaseModel): + p: Optional[list[str]] = Field( + None, alias="p_alias", validation_alias="p_val_alias" + ) + + +@app.get("/model-optional-list-alias-and-validation-alias") +def read_model_optional_list_alias_and_validation_alias( + p: Annotated[HeaderModelOptionalListAliasAndValidationAlias, Header()], +): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-list-alias-and-validation-alias", + "/model-optional-list-alias-and-validation-alias", + ], +) +def test_optional_list_alias_and_validation_alias_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": False, + "schema": { + "anyOf": [ + {"items": {"type": "string"}, "type": "array"}, + {"type": "null"}, + ], + "title": "P Val Alias", + }, + "name": "p_val_alias", + "in": "header", + } + ] + + +@pytest.mark.parametrize( + "path", + [ + "/optional-list-alias-and-validation-alias", + "/model-optional-list-alias-and-validation-alias", + ], +) +def test_optional_list_alias_and_validation_alias_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-list-alias-and-validation-alias", + "/model-optional-list-alias-and-validation-alias", + ], +) +def test_optional_list_alias_and_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.get(path, headers=[("p", "hello"), ("p", "world")]) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-list-alias-and-validation-alias", + "/model-optional-list-alias-and-validation-alias", + ], +) +def test_optional_list_alias_and_validation_alias_by_alias(path: str): + client = TestClient(app) + response = client.get(path, headers=[("p_alias", "hello"), ("p_alias", "world")]) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-list-alias-and-validation-alias", + "/model-optional-list-alias-and-validation-alias", + ], +) +def test_optional_list_alias_and_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.get( + path, headers=[("p_val_alias", "hello"), ("p_val_alias", "world")] + ) + assert response.status_code == 200, response.text + assert response.json() == { + "p": [ + "hello", + "world", + ] + } diff --git a/tests/test_request_params/test_header/test_optional_str.py b/tests/test_request_params/test_header/test_optional_str.py new file mode 100644 index 0000000000..0bd0eddc1b --- /dev/null +++ b/tests/test_request_params/test_header/test_optional_str.py @@ -0,0 +1,319 @@ +from typing import Annotated, Optional + +import pytest +from fastapi import FastAPI, Header +from fastapi.testclient import TestClient +from pydantic import BaseModel, Field + +app = FastAPI() + +# ===================================================================================== +# Without aliases + + +@app.get("/optional-str") +async def read_optional_str(p: Annotated[Optional[str], Header()] = None): + return {"p": p} + + +class HeaderModelOptionalStr(BaseModel): + p: Optional[str] = None + + +@app.get("/model-optional-str") +async def read_model_optional_str(p: Annotated[HeaderModelOptionalStr, Header()]): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/optional-str", "/model-optional-str"], +) +def test_optional_str_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P", + }, + "name": "p", + "in": "header", + } + ] + + +@pytest.mark.parametrize( + "path", + ["/optional-str", "/model-optional-str"], +) +def test_optional_str_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + ["/optional-str", "/model-optional-str"], +) +def test_optional_str(path: str): + client = TestClient(app) + response = client.get(path, headers={"p": "hello"}) + assert response.status_code == 200 + assert response.json() == {"p": "hello"} + + +# ===================================================================================== +# Alias + + +@app.get("/optional-alias") +async def read_optional_alias( + p: Annotated[Optional[str], Header(alias="p_alias")] = None, +): + return {"p": p} + + +class HeaderModelOptionalAlias(BaseModel): + p: Optional[str] = Field(None, alias="p_alias") + + +@app.get("/model-optional-alias") +async def read_model_optional_alias(p: Annotated[HeaderModelOptionalAlias, Header()]): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/optional-alias", "/model-optional-alias"], +) +def test_optional_str_alias_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P Alias", + }, + "name": "p_alias", + "in": "header", + } + ] + + +@pytest.mark.parametrize( + "path", + ["/optional-alias", "/model-optional-alias"], +) +def test_optional_alias_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + ["/optional-alias", "/model-optional-alias"], +) +def test_optional_alias_by_name(path: str): + client = TestClient(app) + response = client.get(path, headers={"p": "hello"}) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-alias", + "/model-optional-alias", + ], +) +def test_optional_alias_by_alias(path: str): + client = TestClient(app) + response = client.get(path, headers={"p_alias": "hello"}) + assert response.status_code == 200 + assert response.json() == {"p": "hello"} + + +# ===================================================================================== +# Validation alias + + +@app.get("/optional-validation-alias") +def read_optional_validation_alias( + p: Annotated[Optional[str], Header(validation_alias="p_val_alias")] = None, +): + return {"p": p} + + +class HeaderModelOptionalValidationAlias(BaseModel): + p: Optional[str] = Field(None, validation_alias="p_val_alias") + + +@app.get("/model-optional-validation-alias") +def read_model_optional_validation_alias( + p: Annotated[HeaderModelOptionalValidationAlias, Header()], +): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/optional-validation-alias", "/model-optional-validation-alias"], +) +def test_optional_validation_alias_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P Val Alias", + }, + "name": "p_val_alias", + "in": "header", + } + ] + + +@pytest.mark.parametrize( + "path", + ["/optional-validation-alias", "/model-optional-validation-alias"], +) +def test_optional_validation_alias_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-validation-alias", + "/model-optional-validation-alias", + ], +) +def test_optional_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.get(path, headers={"p": "hello"}) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-validation-alias", + "/model-optional-validation-alias", + ], +) +def test_optional_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.get(path, headers={"p_val_alias": "hello"}) + assert response.status_code == 200 + assert response.json() == {"p": "hello"} + + +# ===================================================================================== +# Alias and validation alias + + +@app.get("/optional-alias-and-validation-alias") +def read_optional_alias_and_validation_alias( + p: Annotated[ + Optional[str], Header(alias="p_alias", validation_alias="p_val_alias") + ] = None, +): + return {"p": p} + + +class HeaderModelOptionalAliasAndValidationAlias(BaseModel): + p: Optional[str] = Field(None, alias="p_alias", validation_alias="p_val_alias") + + +@app.get("/model-optional-alias-and-validation-alias") +def read_model_optional_alias_and_validation_alias( + p: Annotated[HeaderModelOptionalAliasAndValidationAlias, Header()], +): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-alias-and-validation-alias", + "/model-optional-alias-and-validation-alias", + ], +) +def test_optional_alias_and_validation_alias_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P Val Alias", + }, + "name": "p_val_alias", + "in": "header", + } + ] + + +@pytest.mark.parametrize( + "path", + [ + "/optional-alias-and-validation-alias", + "/model-optional-alias-and-validation-alias", + ], +) +def test_optional_alias_and_validation_alias_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-alias-and-validation-alias", + "/model-optional-alias-and-validation-alias", + ], +) +def test_optional_alias_and_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.get(path, headers={"p": "hello"}) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-alias-and-validation-alias", + "/model-optional-alias-and-validation-alias", + ], +) +def test_optional_alias_and_validation_alias_by_alias(path: str): + client = TestClient(app) + response = client.get(path, headers={"p_alias": "hello"}) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-alias-and-validation-alias", + "/model-optional-alias-and-validation-alias", + ], +) +def test_optional_alias_and_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.get(path, headers={"p_val_alias": "hello"}) + assert response.status_code == 200 + assert response.json() == {"p": "hello"} diff --git a/tests/test_request_params/test_header/test_required_str.py b/tests/test_request_params/test_header/test_required_str.py new file mode 100644 index 0000000000..20dd296570 --- /dev/null +++ b/tests/test_request_params/test_header/test_required_str.py @@ -0,0 +1,402 @@ +from typing import Annotated + +import pytest +from dirty_equals import AnyThing, IsOneOf, IsPartialDict +from fastapi import FastAPI, Header +from fastapi.testclient import TestClient +from pydantic import BaseModel, Field + +app = FastAPI() + +# ===================================================================================== +# Without aliases + + +@app.get("/required-str") +async def read_required_str(p: Annotated[str, Header()]): + return {"p": p} + + +class HeaderModelRequiredStr(BaseModel): + p: str + + +@app.get("/model-required-str") +async def read_model_required_str(p: Annotated[HeaderModelRequiredStr, Header()]): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/required-str", "/model-required-str"], +) +def test_required_str_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": True, + "schema": {"title": "P", "type": "string"}, + "name": "p", + "in": "header", + } + ] + + +@pytest.mark.parametrize( + "path", + ["/required-str", "/model-required-str"], +) +def test_required_str_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["header", "p"], + "msg": "Field required", + "input": AnyThing, + } + ] + } + + +@pytest.mark.parametrize( + "path", + ["/required-str", "/model-required-str"], +) +def test_required_str(path: str): + client = TestClient(app) + response = client.get(path, headers={"p": "hello"}) + assert response.status_code == 200 + assert response.json() == {"p": "hello"} + + +# ===================================================================================== +# Alias + + +@app.get("/required-alias") +async def read_required_alias(p: Annotated[str, Header(alias="p_alias")]): + return {"p": p} + + +class HeaderModelRequiredAlias(BaseModel): + p: str = Field(alias="p_alias") + + +@app.get("/model-required-alias") +async def read_model_required_alias(p: Annotated[HeaderModelRequiredAlias, Header()]): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/required-alias", "/model-required-alias"], +) +def test_required_str_alias_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": True, + "schema": {"title": "P Alias", "type": "string"}, + "name": "p_alias", + "in": "header", + } + ] + + +@pytest.mark.parametrize( + "path", + ["/required-alias", "/model-required-alias"], +) +def test_required_alias_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["header", "p_alias"], + "msg": "Field required", + "input": AnyThing, + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-alias", + "/model-required-alias", + ], +) +def test_required_alias_by_name(path: str): + client = TestClient(app) + response = client.get(path, headers={"p": "hello"}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["header", "p_alias"], + "msg": "Field required", + "input": IsOneOf(None, IsPartialDict({"p": "hello"})), + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-alias", + "/model-required-alias", + ], +) +def test_required_alias_by_alias(path: str): + client = TestClient(app) + response = client.get(path, headers={"p_alias": "hello"}) + assert response.status_code == 200, response.text + assert response.json() == {"p": "hello"} + + +# ===================================================================================== +# Validation alias + + +@app.get("/required-validation-alias") +def read_required_validation_alias( + p: Annotated[str, Header(validation_alias="p_val_alias")], +): + return {"p": p} + + +class HeaderModelRequiredValidationAlias(BaseModel): + p: str = Field(validation_alias="p_val_alias") + + +@app.get("/model-required-validation-alias") +def read_model_required_validation_alias( + p: Annotated[HeaderModelRequiredValidationAlias, Header()], +): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/required-validation-alias", "/model-required-validation-alias"], +) +def test_required_validation_alias_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": True, + "schema": {"title": "P Val Alias", "type": "string"}, + "name": "p_val_alias", + "in": "header", + } + ] + + +@pytest.mark.parametrize( + "path", + [ + "/required-validation-alias", + "/model-required-validation-alias", + ], +) +def test_required_validation_alias_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": [ + "header", + "p_val_alias", + ], + "msg": "Field required", + "input": AnyThing, + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-validation-alias", + "/model-required-validation-alias", + ], +) +def test_required_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.get(path, headers={"p": "hello"}) + assert response.status_code == 422, response.text + + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["header", "p_val_alias"], + "msg": "Field required", + "input": IsOneOf(None, IsPartialDict({"p": "hello"})), + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-validation-alias", + "/model-required-validation-alias", + ], +) +def test_required_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.get(path, headers={"p_val_alias": "hello"}) + assert response.status_code == 200, response.text + + assert response.json() == {"p": "hello"} + + +# ===================================================================================== +# Alias and validation alias + + +@app.get("/required-alias-and-validation-alias") +def read_required_alias_and_validation_alias( + p: Annotated[str, Header(alias="p_alias", validation_alias="p_val_alias")], +): + return {"p": p} + + +class HeaderModelRequiredAliasAndValidationAlias(BaseModel): + p: str = Field(alias="p_alias", validation_alias="p_val_alias") + + +@app.get("/model-required-alias-and-validation-alias") +def read_model_required_alias_and_validation_alias( + p: Annotated[HeaderModelRequiredAliasAndValidationAlias, Header()], +): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + [ + "/required-alias-and-validation-alias", + "/model-required-alias-and-validation-alias", + ], +) +def test_required_alias_and_validation_alias_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": True, + "schema": {"title": "P Val Alias", "type": "string"}, + "name": "p_val_alias", + "in": "header", + } + ] + + +@pytest.mark.parametrize( + "path", + [ + "/required-alias-and-validation-alias", + "/model-required-alias-and-validation-alias", + ], +) +def test_required_alias_and_validation_alias_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": [ + "header", + "p_val_alias", + ], + "msg": "Field required", + "input": AnyThing, + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-alias-and-validation-alias", + "/model-required-alias-and-validation-alias", + ], +) +def test_required_alias_and_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.get(path, headers={"p": "hello"}) + assert response.status_code == 422 + + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": [ + "header", + "p_val_alias", + ], + "msg": "Field required", + "input": IsOneOf( + None, + IsPartialDict({"p": "hello"}), + ), + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-alias-and-validation-alias", + "/model-required-alias-and-validation-alias", + ], +) +def test_required_alias_and_validation_alias_by_alias(path: str): + client = TestClient(app) + response = client.get(path, headers={"p_alias": "hello"}) + assert response.status_code == 422 + + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["header", "p_val_alias"], + "msg": "Field required", + "input": IsOneOf( + None, + IsPartialDict({"p_alias": "hello"}), + ), + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-alias-and-validation-alias", + "/model-required-alias-and-validation-alias", + ], +) +def test_required_alias_and_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.get(path, headers={"p_val_alias": "hello"}) + assert response.status_code == 200, response.text + + assert response.json() == {"p": "hello"} diff --git a/tests/test_request_params/test_path/__init__.py b/tests/test_request_params/test_path/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/test_request_params/test_path/test_list.py b/tests/test_request_params/test_path/test_list.py new file mode 100644 index 0000000000..bba055d9a9 --- /dev/null +++ b/tests/test_request_params/test_path/test_list.py @@ -0,0 +1 @@ +# FastAPI doesn't currently support non-scalar Path parameters diff --git a/tests/test_request_params/test_path/test_optional_list.py b/tests/test_request_params/test_path/test_optional_list.py new file mode 100644 index 0000000000..0719430ac7 --- /dev/null +++ b/tests/test_request_params/test_path/test_optional_list.py @@ -0,0 +1 @@ +# Optional Path parameters are not supported diff --git a/tests/test_request_params/test_path/test_optional_str.py b/tests/test_request_params/test_path/test_optional_str.py new file mode 100644 index 0000000000..0719430ac7 --- /dev/null +++ b/tests/test_request_params/test_path/test_optional_str.py @@ -0,0 +1 @@ +# Optional Path parameters are not supported diff --git a/tests/test_request_params/test_path/test_required_str.py b/tests/test_request_params/test_path/test_required_str.py new file mode 100644 index 0000000000..b2d63667e2 --- /dev/null +++ b/tests/test_request_params/test_path/test_required_str.py @@ -0,0 +1,85 @@ +from typing import Annotated + +import pytest +from fastapi import FastAPI, Path +from fastapi.testclient import TestClient + +app = FastAPI() + + +@app.get("/required-str/{p}") +async def read_required_str(p: Annotated[str, Path()]): + return {"p": p} + + +@app.get("/required-alias/{p_alias}") +async def read_required_alias(p: Annotated[str, Path(alias="p_alias")]): + return {"p": p} + + +@app.get("/required-validation-alias/{p_val_alias}") +def read_required_validation_alias( + p: Annotated[str, Path(validation_alias="p_val_alias")], +): + return {"p": p} + + +@app.get("/required-alias-and-validation-alias/{p_val_alias}") +def read_required_alias_and_validation_alias( + p: Annotated[str, Path(alias="p_alias", validation_alias="p_val_alias")], +): + return {"p": p} + + +@pytest.mark.parametrize( + ("path", "expected_name", "expected_title"), + [ + pytest.param("/required-str/{p}", "p", "P", id="required-str"), + pytest.param( + "/required-alias/{p_alias}", "p_alias", "P Alias", id="required-alias" + ), + pytest.param( + "/required-validation-alias/{p_val_alias}", + "p_val_alias", + "P Val Alias", + id="required-validation-alias", + ), + pytest.param( + "/required-alias-and-validation-alias/{p_val_alias}", + "p_val_alias", + "P Val Alias", + id="required-alias-and-validation-alias", + ), + ], +) +def test_schema(path: str, expected_name: str, expected_title: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": True, + "schema": {"title": expected_title, "type": "string"}, + "name": expected_name, + "in": "path", + } + ] + + +@pytest.mark.parametrize( + "path", + [ + pytest.param("/required-str", id="required-str"), + pytest.param("/required-alias", id="required-alias"), + pytest.param( + "/required-validation-alias", + id="required-validation-alias", + ), + pytest.param( + "/required-alias-and-validation-alias", + id="required-alias-and-validation-alias", + ), + ], +) +def test_success(path: str): + client = TestClient(app) + response = client.get(f"{path}/hello") + assert response.status_code == 200, response.text + assert response.json() == {"p": "hello"} diff --git a/tests/test_request_params/test_query/__init__.py b/tests/test_request_params/test_query/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/test_request_params/test_query/test_list.py b/tests/test_request_params/test_query/test_list.py new file mode 100644 index 0000000000..e933da214d --- /dev/null +++ b/tests/test_request_params/test_query/test_list.py @@ -0,0 +1,419 @@ +from typing import Annotated + +import pytest +from dirty_equals import IsOneOf +from fastapi import FastAPI, Query +from fastapi.testclient import TestClient +from pydantic import BaseModel, Field + +app = FastAPI() + +# ===================================================================================== +# Without aliases + + +@app.get("/required-list-str") +async def read_required_list_str(p: Annotated[list[str], Query()]): + return {"p": p} + + +class QueryModelRequiredListStr(BaseModel): + p: list[str] + + +@app.get("/model-required-list-str") +def read_model_required_list_str(p: Annotated[QueryModelRequiredListStr, Query()]): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/required-list-str", "/model-required-list-str"], +) +def test_required_list_str_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": True, + "schema": { + "title": "P", + "type": "array", + "items": {"type": "string"}, + }, + "name": "p", + "in": "query", + } + ] + + +@pytest.mark.parametrize( + "path", + ["/required-list-str", "/model-required-list-str"], +) +def test_required_list_str_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "p"], + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } + + +@pytest.mark.parametrize( + "path", + ["/required-list-str", "/model-required-list-str"], +) +def test_required_list_str(path: str): + client = TestClient(app) + response = client.get(f"{path}?p=hello&p=world") + assert response.status_code == 200 + assert response.json() == {"p": ["hello", "world"]} + + +# ===================================================================================== +# Alias + + +@app.get("/required-list-alias") +async def read_required_list_alias(p: Annotated[list[str], Query(alias="p_alias")]): + return {"p": p} + + +class QueryModelRequiredListAlias(BaseModel): + p: list[str] = Field(alias="p_alias") + + +@app.get("/model-required-list-alias") +async def read_model_required_list_alias( + p: Annotated[QueryModelRequiredListAlias, Query()], +): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/required-list-alias", "/model-required-list-alias"], +) +def test_required_list_str_alias_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": True, + "schema": { + "title": "P Alias", + "type": "array", + "items": {"type": "string"}, + }, + "name": "p_alias", + "in": "query", + } + ] + + +@pytest.mark.parametrize( + "path", + ["/required-list-alias", "/model-required-list-alias"], +) +def test_required_list_alias_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "p_alias"], + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-list-alias", + "/model-required-list-alias", + ], +) +def test_required_list_alias_by_name(path: str): + client = TestClient(app) + response = client.get(f"{path}?p=hello&p=world") + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "p_alias"], + "msg": "Field required", + "input": IsOneOf(None, {"p": ["hello", "world"]}), + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-list-alias", + "/model-required-list-alias", + ], +) +def test_required_list_alias_by_alias(path: str): + client = TestClient(app) + response = client.get(f"{path}?p_alias=hello&p_alias=world") + assert response.status_code == 200, response.text + assert response.json() == {"p": ["hello", "world"]} + + +# ===================================================================================== +# Validation alias + + +@app.get("/required-list-validation-alias") +def read_required_list_validation_alias( + p: Annotated[list[str], Query(validation_alias="p_val_alias")], +): + return {"p": p} + + +class QueryModelRequiredListValidationAlias(BaseModel): + p: list[str] = Field(validation_alias="p_val_alias") + + +@app.get("/model-required-list-validation-alias") +async def read_model_required_list_validation_alias( + p: Annotated[QueryModelRequiredListValidationAlias, Query()], +): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/required-list-validation-alias", "/model-required-list-validation-alias"], +) +def test_required_list_validation_alias_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": True, + "schema": { + "title": "P Val Alias", + "type": "array", + "items": {"type": "string"}, + }, + "name": "p_val_alias", + "in": "query", + } + ] + + +@pytest.mark.parametrize( + "path", + [ + "/required-list-validation-alias", + "/model-required-list-validation-alias", + ], +) +def test_required_list_validation_alias_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": [ + "query", + "p_val_alias", + ], + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-list-validation-alias", + "/model-required-list-validation-alias", + ], +) +def test_required_list_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.get(f"{path}?p=hello&p=world") + assert response.status_code == 422 + + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "p_val_alias"], + "msg": "Field required", + "input": IsOneOf(None, {"p": ["hello", "world"]}), + } + ] + } + + +@pytest.mark.parametrize( + "path", + ["/required-list-validation-alias", "/model-required-list-validation-alias"], +) +def test_required_list_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.get(f"{path}?p_val_alias=hello&p_val_alias=world") + assert response.status_code == 200, response.text + + assert response.json() == {"p": ["hello", "world"]} + + +# ===================================================================================== +# Alias and validation alias + + +@app.get("/required-list-alias-and-validation-alias") +def read_required_list_alias_and_validation_alias( + p: Annotated[list[str], Query(alias="p_alias", validation_alias="p_val_alias")], +): + return {"p": p} + + +class QueryModelRequiredListAliasAndValidationAlias(BaseModel): + p: list[str] = Field(alias="p_alias", validation_alias="p_val_alias") + + +@app.get("/model-required-list-alias-and-validation-alias") +def read_model_required_list_alias_and_validation_alias( + p: Annotated[QueryModelRequiredListAliasAndValidationAlias, Query()], +): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + [ + "/required-list-alias-and-validation-alias", + "/model-required-list-alias-and-validation-alias", + ], +) +def test_required_list_alias_and_validation_alias_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": True, + "schema": { + "title": "P Val Alias", + "type": "array", + "items": {"type": "string"}, + }, + "name": "p_val_alias", + "in": "query", + } + ] + + +@pytest.mark.parametrize( + "path", + [ + "/required-list-alias-and-validation-alias", + "/model-required-list-alias-and-validation-alias", + ], +) +def test_required_list_alias_and_validation_alias_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": [ + "query", + "p_val_alias", + ], + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-list-alias-and-validation-alias", + "/model-required-list-alias-and-validation-alias", + ], +) +def test_required_list_alias_and_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.get(f"{path}?p=hello&p=world") + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": [ + "query", + "p_val_alias", + ], + "msg": "Field required", + "input": IsOneOf( + None, + { + "p": [ + "hello", + "world", + ] + }, + ), + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-list-alias-and-validation-alias", + "/model-required-list-alias-and-validation-alias", + ], +) +def test_required_list_alias_and_validation_alias_by_alias(path: str): + client = TestClient(app) + response = client.get(f"{path}?p_alias=hello&p_alias=world") + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "p_val_alias"], + "msg": "Field required", + "input": IsOneOf( + None, + {"p_alias": ["hello", "world"]}, + ), + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-list-alias-and-validation-alias", + "/model-required-list-alias-and-validation-alias", + ], +) +def test_required_list_alias_and_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.get(f"{path}?p_val_alias=hello&p_val_alias=world") + assert response.status_code == 200, response.text + assert response.json() == {"p": ["hello", "world"]} diff --git a/tests/test_request_params/test_query/test_optional_list.py b/tests/test_request_params/test_query/test_optional_list.py new file mode 100644 index 0000000000..351e03a713 --- /dev/null +++ b/tests/test_request_params/test_query/test_optional_list.py @@ -0,0 +1,341 @@ +from typing import Annotated, Optional + +import pytest +from fastapi import FastAPI, Query +from fastapi.testclient import TestClient +from pydantic import BaseModel, Field + +app = FastAPI() + +# ===================================================================================== +# Without aliases + + +@app.get("/optional-list-str") +async def read_optional_list_str( + p: Annotated[Optional[list[str]], Query()] = None, +): + return {"p": p} + + +class QueryModelOptionalListStr(BaseModel): + p: Optional[list[str]] = None + + +@app.get("/model-optional-list-str") +async def read_model_optional_list_str( + p: Annotated[QueryModelOptionalListStr, Query()], +): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/optional-list-str", "/model-optional-list-str"], +) +def test_optional_list_str_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": False, + "schema": { + "anyOf": [ + {"items": {"type": "string"}, "type": "array"}, + {"type": "null"}, + ], + "title": "P", + }, + "name": "p", + "in": "query", + } + ] + + +@pytest.mark.parametrize( + "path", + ["/optional-list-str", "/model-optional-list-str"], +) +def test_optional_list_str_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 200, response.text + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + ["/optional-list-str", "/model-optional-list-str"], +) +def test_optional_list_str(path: str): + client = TestClient(app) + response = client.get(f"{path}?p=hello&p=world") + assert response.status_code == 200 + assert response.json() == {"p": ["hello", "world"]} + + +# ===================================================================================== +# Alias + + +@app.get("/optional-list-alias") +async def read_optional_list_alias( + p: Annotated[Optional[list[str]], Query(alias="p_alias")] = None, +): + return {"p": p} + + +class QueryModelOptionalListAlias(BaseModel): + p: Optional[list[str]] = Field(None, alias="p_alias") + + +@app.get("/model-optional-list-alias") +async def read_model_optional_list_alias( + p: Annotated[QueryModelOptionalListAlias, Query()], +): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/optional-list-alias", "/model-optional-list-alias"], +) +def test_optional_list_str_alias_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": False, + "schema": { + "anyOf": [ + {"items": {"type": "string"}, "type": "array"}, + {"type": "null"}, + ], + "title": "P Alias", + }, + "name": "p_alias", + "in": "query", + } + ] + + +@pytest.mark.parametrize( + "path", + ["/optional-list-alias", "/model-optional-list-alias"], +) +def test_optional_list_alias_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + ["/optional-list-alias", "/model-optional-list-alias"], +) +def test_optional_list_alias_by_name(path: str): + client = TestClient(app) + response = client.get(f"{path}?p=hello&p=world") + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-list-alias", + "/model-optional-list-alias", + ], +) +def test_optional_list_alias_by_alias(path: str): + client = TestClient(app) + response = client.get(f"{path}?p_alias=hello&p_alias=world") + assert response.status_code == 200 + assert response.json() == {"p": ["hello", "world"]} + + +# ===================================================================================== +# Validation alias + + +@app.get("/optional-list-validation-alias") +def read_optional_list_validation_alias( + p: Annotated[Optional[list[str]], Query(validation_alias="p_val_alias")] = None, +): + return {"p": p} + + +class QueryModelOptionalListValidationAlias(BaseModel): + p: Optional[list[str]] = Field(None, validation_alias="p_val_alias") + + +@app.get("/model-optional-list-validation-alias") +def read_model_optional_list_validation_alias( + p: Annotated[QueryModelOptionalListValidationAlias, Query()], +): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/optional-list-validation-alias", "/model-optional-list-validation-alias"], +) +def test_optional_list_validation_alias_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": False, + "schema": { + "anyOf": [ + {"items": {"type": "string"}, "type": "array"}, + {"type": "null"}, + ], + "title": "P Val Alias", + }, + "name": "p_val_alias", + "in": "query", + } + ] + + +@pytest.mark.parametrize( + "path", + ["/optional-list-validation-alias", "/model-optional-list-validation-alias"], +) +def test_optional_list_validation_alias_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-list-validation-alias", + "/model-optional-list-validation-alias", + ], +) +def test_optional_list_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.get(f"{path}?p=hello&p=world") + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + ["/optional-list-validation-alias", "/model-optional-list-validation-alias"], +) +def test_optional_list_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.get(f"{path}?p_val_alias=hello&p_val_alias=world") + assert response.status_code == 200, response.text + assert response.json() == {"p": ["hello", "world"]} + + +# ===================================================================================== +# Alias and validation alias + + +@app.get("/optional-list-alias-and-validation-alias") +def read_optional_list_alias_and_validation_alias( + p: Annotated[ + Optional[list[str]], Query(alias="p_alias", validation_alias="p_val_alias") + ] = None, +): + return {"p": p} + + +class QueryModelOptionalListAliasAndValidationAlias(BaseModel): + p: Optional[list[str]] = Field( + None, alias="p_alias", validation_alias="p_val_alias" + ) + + +@app.get("/model-optional-list-alias-and-validation-alias") +def read_model_optional_list_alias_and_validation_alias( + p: Annotated[QueryModelOptionalListAliasAndValidationAlias, Query()], +): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-list-alias-and-validation-alias", + "/model-optional-list-alias-and-validation-alias", + ], +) +def test_optional_list_alias_and_validation_alias_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": False, + "schema": { + "anyOf": [ + {"items": {"type": "string"}, "type": "array"}, + {"type": "null"}, + ], + "title": "P Val Alias", + }, + "name": "p_val_alias", + "in": "query", + } + ] + + +@pytest.mark.parametrize( + "path", + [ + "/optional-list-alias-and-validation-alias", + "/model-optional-list-alias-and-validation-alias", + ], +) +def test_optional_list_alias_and_validation_alias_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-list-alias-and-validation-alias", + "/model-optional-list-alias-and-validation-alias", + ], +) +def test_optional_list_alias_and_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.get(f"{path}?p=hello&p=world") + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-list-alias-and-validation-alias", + "/model-optional-list-alias-and-validation-alias", + ], +) +def test_optional_list_alias_and_validation_alias_by_alias(path: str): + client = TestClient(app) + response = client.get(f"{path}?p_alias=hello&p_alias=world") + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-list-alias-and-validation-alias", + "/model-optional-list-alias-and-validation-alias", + ], +) +def test_optional_list_alias_and_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.get(f"{path}?p_val_alias=hello&p_val_alias=world") + assert response.status_code == 200, response.text + assert response.json() == { + "p": [ + "hello", + "world", + ] + } diff --git a/tests/test_request_params/test_query/test_optional_str.py b/tests/test_request_params/test_query/test_optional_str.py new file mode 100644 index 0000000000..12e1b465a7 --- /dev/null +++ b/tests/test_request_params/test_query/test_optional_str.py @@ -0,0 +1,319 @@ +from typing import Annotated, Optional + +import pytest +from fastapi import FastAPI, Query +from fastapi.testclient import TestClient +from pydantic import BaseModel, Field + +app = FastAPI() + +# ===================================================================================== +# Without aliases + + +@app.get("/optional-str") +async def read_optional_str(p: Optional[str] = None): + return {"p": p} + + +class QueryModelOptionalStr(BaseModel): + p: Optional[str] = None + + +@app.get("/model-optional-str") +async def read_model_optional_str(p: Annotated[QueryModelOptionalStr, Query()]): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/optional-str", "/model-optional-str"], +) +def test_optional_str_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P", + }, + "name": "p", + "in": "query", + } + ] + + +@pytest.mark.parametrize( + "path", + ["/optional-str", "/model-optional-str"], +) +def test_optional_str_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + ["/optional-str", "/model-optional-str"], +) +def test_optional_str(path: str): + client = TestClient(app) + response = client.get(f"{path}?p=hello") + assert response.status_code == 200 + assert response.json() == {"p": "hello"} + + +# ===================================================================================== +# Alias + + +@app.get("/optional-alias") +async def read_optional_alias( + p: Annotated[Optional[str], Query(alias="p_alias")] = None, +): + return {"p": p} + + +class QueryModelOptionalAlias(BaseModel): + p: Optional[str] = Field(None, alias="p_alias") + + +@app.get("/model-optional-alias") +async def read_model_optional_alias(p: Annotated[QueryModelOptionalAlias, Query()]): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/optional-alias", "/model-optional-alias"], +) +def test_optional_str_alias_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P Alias", + }, + "name": "p_alias", + "in": "query", + } + ] + + +@pytest.mark.parametrize( + "path", + ["/optional-alias", "/model-optional-alias"], +) +def test_optional_alias_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + ["/optional-alias", "/model-optional-alias"], +) +def test_optional_alias_by_name(path: str): + client = TestClient(app) + response = client.get(f"{path}?p=hello") + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-alias", + "/model-optional-alias", + ], +) +def test_optional_alias_by_alias(path: str): + client = TestClient(app) + response = client.get(f"{path}?p_alias=hello") + assert response.status_code == 200 + assert response.json() == {"p": "hello"} + + +# ===================================================================================== +# Validation alias + + +@app.get("/optional-validation-alias") +def read_optional_validation_alias( + p: Annotated[Optional[str], Query(validation_alias="p_val_alias")] = None, +): + return {"p": p} + + +class QueryModelOptionalValidationAlias(BaseModel): + p: Optional[str] = Field(None, validation_alias="p_val_alias") + + +@app.get("/model-optional-validation-alias") +def read_model_optional_validation_alias( + p: Annotated[QueryModelOptionalValidationAlias, Query()], +): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/optional-validation-alias", "/model-optional-validation-alias"], +) +def test_optional_validation_alias_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P Val Alias", + }, + "name": "p_val_alias", + "in": "query", + } + ] + + +@pytest.mark.parametrize( + "path", + ["/optional-validation-alias", "/model-optional-validation-alias"], +) +def test_optional_validation_alias_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-validation-alias", + "/model-optional-validation-alias", + ], +) +def test_optional_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.get(f"{path}?p=hello") + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-validation-alias", + "/model-optional-validation-alias", + ], +) +def test_optional_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.get(f"{path}?p_val_alias=hello") + assert response.status_code == 200 + assert response.json() == {"p": "hello"} + + +# ===================================================================================== +# Alias and validation alias + + +@app.get("/optional-alias-and-validation-alias") +def read_optional_alias_and_validation_alias( + p: Annotated[ + Optional[str], Query(alias="p_alias", validation_alias="p_val_alias") + ] = None, +): + return {"p": p} + + +class QueryModelOptionalAliasAndValidationAlias(BaseModel): + p: Optional[str] = Field(None, alias="p_alias", validation_alias="p_val_alias") + + +@app.get("/model-optional-alias-and-validation-alias") +def read_model_optional_alias_and_validation_alias( + p: Annotated[QueryModelOptionalAliasAndValidationAlias, Query()], +): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-alias-and-validation-alias", + "/model-optional-alias-and-validation-alias", + ], +) +def test_optional_alias_and_validation_alias_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P Val Alias", + }, + "name": "p_val_alias", + "in": "query", + } + ] + + +@pytest.mark.parametrize( + "path", + [ + "/optional-alias-and-validation-alias", + "/model-optional-alias-and-validation-alias", + ], +) +def test_optional_alias_and_validation_alias_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-alias-and-validation-alias", + "/model-optional-alias-and-validation-alias", + ], +) +def test_optional_alias_and_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.get(f"{path}?p=hello") + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-alias-and-validation-alias", + "/model-optional-alias-and-validation-alias", + ], +) +def test_optional_alias_and_validation_alias_by_alias(path: str): + client = TestClient(app) + response = client.get(f"{path}?p_alias=hello") + assert response.status_code == 200 + assert response.json() == {"p": None} + + +@pytest.mark.parametrize( + "path", + [ + "/optional-alias-and-validation-alias", + "/model-optional-alias-and-validation-alias", + ], +) +def test_optional_alias_and_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.get(f"{path}?p_val_alias=hello") + assert response.status_code == 200 + assert response.json() == {"p": "hello"} diff --git a/tests/test_request_params/test_query/test_required_str.py b/tests/test_request_params/test_query/test_required_str.py new file mode 100644 index 0000000000..9e7b961453 --- /dev/null +++ b/tests/test_request_params/test_query/test_required_str.py @@ -0,0 +1,405 @@ +from typing import Annotated + +import pytest +from dirty_equals import IsOneOf +from fastapi import FastAPI, Query +from fastapi.testclient import TestClient +from pydantic import BaseModel, Field + +app = FastAPI() + +# ===================================================================================== +# Without aliases + + +@app.get("/required-str") +async def read_required_str(p: str): + return {"p": p} + + +class QueryModelRequiredStr(BaseModel): + p: str + + +@app.get("/model-required-str") +async def read_model_required_str(p: Annotated[QueryModelRequiredStr, Query()]): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/required-str", "/model-required-str"], +) +def test_required_str_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": True, + "schema": {"title": "P", "type": "string"}, + "name": "p", + "in": "query", + } + ] + + +@pytest.mark.parametrize( + "path", + ["/required-str", "/model-required-str"], +) +def test_required_str_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "p"], + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } + + +@pytest.mark.parametrize( + "path", + ["/required-str", "/model-required-str"], +) +def test_required_str(path: str): + client = TestClient(app) + response = client.get(f"{path}?p=hello") + assert response.status_code == 200 + assert response.json() == {"p": "hello"} + + +# ===================================================================================== +# Alias + + +@app.get("/required-alias") +async def read_required_alias(p: Annotated[str, Query(alias="p_alias")]): + return {"p": p} + + +class QueryModelRequiredAlias(BaseModel): + p: str = Field(alias="p_alias") + + +@app.get("/model-required-alias") +async def read_model_required_alias(p: Annotated[QueryModelRequiredAlias, Query()]): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/required-alias", "/model-required-alias"], +) +def test_required_str_alias_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": True, + "schema": {"title": "P Alias", "type": "string"}, + "name": "p_alias", + "in": "query", + } + ] + + +@pytest.mark.parametrize( + "path", + ["/required-alias", "/model-required-alias"], +) +def test_required_alias_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "p_alias"], + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-alias", + "/model-required-alias", + ], +) +def test_required_alias_by_name(path: str): + client = TestClient(app) + response = client.get(f"{path}?p=hello") + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "p_alias"], + "msg": "Field required", + "input": IsOneOf( + None, + {"p": "hello"}, + ), + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-alias", + "/model-required-alias", + ], +) +def test_required_alias_by_alias(path: str): + client = TestClient(app) + response = client.get(f"{path}?p_alias=hello") + assert response.status_code == 200, response.text + assert response.json() == {"p": "hello"} + + +# ===================================================================================== +# Validation alias + + +@app.get("/required-validation-alias") +def read_required_validation_alias( + p: Annotated[str, Query(validation_alias="p_val_alias")], +): + return {"p": p} + + +class QueryModelRequiredValidationAlias(BaseModel): + p: str = Field(validation_alias="p_val_alias") + + +@app.get("/model-required-validation-alias") +def read_model_required_validation_alias( + p: Annotated[QueryModelRequiredValidationAlias, Query()], +): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + ["/required-validation-alias", "/model-required-validation-alias"], +) +def test_required_validation_alias_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": True, + "schema": {"title": "P Val Alias", "type": "string"}, + "name": "p_val_alias", + "in": "query", + } + ] + + +@pytest.mark.parametrize( + "path", + [ + "/required-validation-alias", + "/model-required-validation-alias", + ], +) +def test_required_validation_alias_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": [ + "query", + "p_val_alias", + ], + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-validation-alias", + "/model-required-validation-alias", + ], +) +def test_required_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.get(f"{path}?p=hello") + assert response.status_code == 422, response.text + + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "p_val_alias"], + "msg": "Field required", + "input": IsOneOf(None, {"p": "hello"}), + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-validation-alias", + "/model-required-validation-alias", + ], +) +def test_required_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.get(f"{path}?p_val_alias=hello") + assert response.status_code == 200, response.text + + assert response.json() == {"p": "hello"} + + +# ===================================================================================== +# Alias and validation alias + + +@app.get("/required-alias-and-validation-alias") +def read_required_alias_and_validation_alias( + p: Annotated[str, Query(alias="p_alias", validation_alias="p_val_alias")], +): + return {"p": p} + + +class QueryModelRequiredAliasAndValidationAlias(BaseModel): + p: str = Field(alias="p_alias", validation_alias="p_val_alias") + + +@app.get("/model-required-alias-and-validation-alias") +def read_model_required_alias_and_validation_alias( + p: Annotated[QueryModelRequiredAliasAndValidationAlias, Query()], +): + return {"p": p.p} + + +@pytest.mark.parametrize( + "path", + [ + "/required-alias-and-validation-alias", + "/model-required-alias-and-validation-alias", + ], +) +def test_required_alias_and_validation_alias_schema(path: str): + assert app.openapi()["paths"][path]["get"]["parameters"] == [ + { + "required": True, + "schema": {"title": "P Val Alias", "type": "string"}, + "name": "p_val_alias", + "in": "query", + } + ] + + +@pytest.mark.parametrize( + "path", + [ + "/required-alias-and-validation-alias", + "/model-required-alias-and-validation-alias", + ], +) +def test_required_alias_and_validation_alias_missing(path: str): + client = TestClient(app) + response = client.get(path) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": [ + "query", + "p_val_alias", + ], + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-alias-and-validation-alias", + "/model-required-alias-and-validation-alias", + ], +) +def test_required_alias_and_validation_alias_by_name(path: str): + client = TestClient(app) + response = client.get(f"{path}?p=hello") + assert response.status_code == 422 + + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": [ + "query", + "p_val_alias", + ], + "msg": "Field required", + "input": IsOneOf( + None, + {"p": "hello"}, + ), + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-alias-and-validation-alias", + "/model-required-alias-and-validation-alias", + ], +) +def test_required_alias_and_validation_alias_by_alias(path: str): + client = TestClient(app) + response = client.get(f"{path}?p_alias=hello") + assert response.status_code == 422 + + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "p_val_alias"], + "msg": "Field required", + "input": IsOneOf( + None, + {"p_alias": "hello"}, + ), + } + ] + } + + +@pytest.mark.parametrize( + "path", + [ + "/required-alias-and-validation-alias", + "/model-required-alias-and-validation-alias", + ], +) +def test_required_alias_and_validation_alias_by_validation_alias(path: str): + client = TestClient(app) + response = client.get(f"{path}?p_val_alias=hello") + assert response.status_code == 200, response.text + + assert response.json() == {"p": "hello"} diff --git a/tests/test_response_by_alias.py b/tests/test_response_by_alias.py index e162cd39b5..807d2600b9 100644 --- a/tests/test_response_by_alias.py +++ b/tests/test_response_by_alias.py @@ -1,7 +1,4 @@ -from typing import List - from fastapi import FastAPI -from fastapi._compat import PYDANTIC_V2 from fastapi.testclient import TestClient from pydantic import BaseModel, ConfigDict, Field @@ -15,24 +12,14 @@ class Model(BaseModel): class ModelNoAlias(BaseModel): name: str - if PYDANTIC_V2: - model_config = ConfigDict( - json_schema_extra={ - "description": ( - "response_model_by_alias=False is basically a quick hack, to support " - "proper OpenAPI use another model with the correct field names" - ) - } - ) - else: - - class Config: - schema_extra = { - "description": ( - "response_model_by_alias=False is basically a quick hack, to support " - "proper OpenAPI use another model with the correct field names" - ) - } + model_config = ConfigDict( + json_schema_extra={ + "description": ( + "response_model_by_alias=False is basically a quick hack, to support " + "proper OpenAPI use another model with the correct field names" + ) + } + ) @app.get("/dict", response_model=Model, response_model_by_alias=False) @@ -45,7 +32,7 @@ def read_model(): return Model(alias="Foo") -@app.get("/list", response_model=List[Model], response_model_by_alias=False) +@app.get("/list", response_model=list[Model], response_model_by_alias=False) def read_list(): return [{"alias": "Foo"}, {"alias": "Bar"}] @@ -60,7 +47,7 @@ def by_alias_model(): return Model(alias="Foo") -@app.get("/by-alias/list", response_model=List[Model]) +@app.get("/by-alias/list", response_model=list[Model]) def by_alias_list(): return [{"alias": "Foo"}, {"alias": "Bar"}] @@ -75,7 +62,7 @@ def no_alias_model(): return ModelNoAlias(name="Foo") -@app.get("/no-alias/list", response_model=List[ModelNoAlias]) +@app.get("/no-alias/list", response_model=list[ModelNoAlias]) def no_alias_list(): return [{"name": "Foo"}, {"name": "Bar"}] diff --git a/tests/test_response_class_no_mediatype.py b/tests/test_response_class_no_mediatype.py index 706929ac36..4dc164bf92 100644 --- a/tests/test_response_class_no_mediatype.py +++ b/tests/test_response_class_no_mediatype.py @@ -1,5 +1,3 @@ -import typing - from fastapi import FastAPI, Response from fastapi.responses import JSONResponse from fastapi.testclient import TestClient @@ -18,7 +16,7 @@ class Error(BaseModel): class JsonApiError(BaseModel): - errors: typing.List[Error] + errors: list[Error] @app.get( diff --git a/tests/test_response_code_no_body.py b/tests/test_response_code_no_body.py index 3ca8708f12..70456a7462 100644 --- a/tests/test_response_code_no_body.py +++ b/tests/test_response_code_no_body.py @@ -1,5 +1,3 @@ -import typing - from fastapi import FastAPI from fastapi.responses import JSONResponse from fastapi.testclient import TestClient @@ -18,7 +16,7 @@ class Error(BaseModel): class JsonApiError(BaseModel): - errors: typing.List[Error] + errors: list[Error] @app.get( diff --git a/tests/test_response_model_as_return_annotation.py b/tests/test_response_model_as_return_annotation.py index 1745c69b60..58fba89f1a 100644 --- a/tests/test_response_model_as_return_annotation.py +++ b/tests/test_response_model_as_return_annotation.py @@ -1,4 +1,4 @@ -from typing import List, Union +from typing import Union import pytest from fastapi import FastAPI @@ -7,8 +7,6 @@ from fastapi.responses import JSONResponse, Response from fastapi.testclient import TestClient from pydantic import BaseModel -from tests.utils import needs_pydanticv1 - class BaseUser(BaseModel): name: str @@ -191,7 +189,7 @@ def response_model_filtering_model_annotation_submodel_return_submodel() -> DBUs return DBUser(name="John", surname="Doe", password_hash="secret") -@app.get("/response_model_list_of_model-no_annotation", response_model=List[User]) +@app.get("/response_model_list_of_model-no_annotation", response_model=list[User]) def response_model_list_of_model_no_annotation(): return [ DBUser(name="John", surname="Doe", password_hash="secret"), @@ -200,7 +198,7 @@ def response_model_list_of_model_no_annotation(): @app.get("/no_response_model-annotation_list_of_model") -def no_response_model_annotation_list_of_model() -> List[User]: +def no_response_model_annotation_list_of_model() -> list[User]: return [ DBUser(name="John", surname="Doe", password_hash="secret"), DBUser(name="Jane", surname="Does", password_hash="secret2"), @@ -208,7 +206,7 @@ def no_response_model_annotation_list_of_model() -> List[User]: @app.get("/no_response_model-annotation_forward_ref_list_of_model") -def no_response_model_annotation_forward_ref_list_of_model() -> "List[User]": +def no_response_model_annotation_forward_ref_list_of_model() -> "list[User]": return [ DBUser(name="John", surname="Doe", password_hash="secret"), DBUser(name="Jane", surname="Does", password_hash="secret2"), @@ -511,26 +509,6 @@ def test_invalid_response_model_field(): assert "parameter response_model=None" in e.value.args[0] -# TODO: remove when dropping Pydantic v1 support -@needs_pydanticv1 -def test_invalid_response_model_field_pv1(): - from fastapi._compat import v1 - - app = FastAPI() - - class Model(v1.BaseModel): - foo: str - - with pytest.raises(FastAPIError) as e: - - @app.get("/") - def read_root() -> Union[Response, Model, None]: - return Response(content="Foo") # pragma: no cover - - assert "valid Pydantic field type" in e.value.args[0] - assert "parameter response_model=None" in e.value.args[0] - - def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text diff --git a/tests/test_response_model_data_filter.py b/tests/test_response_model_data_filter.py index a3e0f95f04..358697d6df 100644 --- a/tests/test_response_model_data_filter.py +++ b/tests/test_response_model_data_filter.py @@ -1,5 +1,3 @@ -from typing import List - from fastapi import FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel @@ -44,7 +42,7 @@ async def read_pet(pet_id: int): return pet -@app.get("/pets/", response_model=List[PetOut]) +@app.get("/pets/", response_model=list[PetOut]) async def read_pets(): user = UserDB( email="johndoe@example.com", diff --git a/tests/test_response_model_data_filter_no_inheritance.py b/tests/test_response_model_data_filter_no_inheritance.py index 64003a8417..c0c2f3a9dc 100644 --- a/tests/test_response_model_data_filter_no_inheritance.py +++ b/tests/test_response_model_data_filter_no_inheritance.py @@ -1,5 +1,3 @@ -from typing import List - from fastapi import FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel @@ -46,7 +44,7 @@ async def read_pet(pet_id: int): return pet -@app.get("/pets/", response_model=List[PetOut]) +@app.get("/pets/", response_model=list[PetOut]) async def read_pets(): user = UserDB( email="johndoe@example.com", diff --git a/tests/test_response_model_invalid.py b/tests/test_response_model_invalid.py index 88b55a436d..f884b5c74a 100644 --- a/tests/test_response_model_invalid.py +++ b/tests/test_response_model_invalid.py @@ -1,5 +1,3 @@ -from typing import List - import pytest from fastapi import FastAPI from fastapi.exceptions import FastAPIError @@ -22,7 +20,7 @@ def test_invalid_response_model_sub_type_raises(): with pytest.raises(FastAPIError): app = FastAPI() - @app.get("/", response_model=List[NonPydanticModel]) + @app.get("/", response_model=list[NonPydanticModel]) def read_root(): pass # pragma: nocover @@ -40,6 +38,6 @@ def test_invalid_response_model_sub_type_in_responses_raises(): with pytest.raises(FastAPIError): app = FastAPI() - @app.get("/", responses={"500": {"model": List[NonPydanticModel]}}) + @app.get("/", responses={"500": {"model": list[NonPydanticModel]}}) def read_root(): pass # pragma: nocover diff --git a/tests/test_response_model_sub_types.py b/tests/test_response_model_sub_types.py index 660bcee1bb..8036bcb07d 100644 --- a/tests/test_response_model_sub_types.py +++ b/tests/test_response_model_sub_types.py @@ -1,5 +1,3 @@ -from typing import List - from fastapi import FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel @@ -17,7 +15,7 @@ def valid1(): pass -@app.get("/valid2", responses={"500": {"model": List[int]}}) +@app.get("/valid2", responses={"500": {"model": list[int]}}) def valid2(): pass @@ -27,7 +25,7 @@ def valid3(): pass -@app.get("/valid4", responses={"500": {"model": List[Model]}}) +@app.get("/valid4", responses={"500": {"model": list[Model]}}) def valid4(): pass diff --git a/tests/test_router_events.py b/tests/test_router_events.py index dd7ff3314b..9df299cdaa 100644 --- a/tests/test_router_events.py +++ b/tests/test_router_events.py @@ -1,5 +1,6 @@ +from collections.abc import AsyncGenerator from contextlib import asynccontextmanager -from typing import AsyncGenerator, Dict, Union +from typing import Union import pytest from fastapi import APIRouter, FastAPI, Request @@ -28,7 +29,7 @@ def test_router_events(state: State) -> None: app = FastAPI() @app.get("/") - def main() -> Dict[str, str]: + def main() -> dict[str, str]: return {"message": "Hello World"} @app.on_event("startup") @@ -96,7 +97,7 @@ def test_app_lifespan_state(state: State) -> None: app = FastAPI(lifespan=lifespan) @app.get("/") - def main() -> Dict[str, str]: + def main() -> dict[str, str]: return {"message": "Hello World"} assert state.app_startup is False @@ -113,19 +114,19 @@ def test_app_lifespan_state(state: State) -> None: def test_router_nested_lifespan_state(state: State) -> None: @asynccontextmanager - async def lifespan(app: FastAPI) -> AsyncGenerator[Dict[str, bool], None]: + async def lifespan(app: FastAPI) -> AsyncGenerator[dict[str, bool], None]: state.app_startup = True yield {"app": True} state.app_shutdown = True @asynccontextmanager - async def router_lifespan(app: FastAPI) -> AsyncGenerator[Dict[str, bool], None]: + async def router_lifespan(app: FastAPI) -> AsyncGenerator[dict[str, bool], None]: state.router_startup = True yield {"router": True} state.router_shutdown = True @asynccontextmanager - async def subrouter_lifespan(app: FastAPI) -> AsyncGenerator[Dict[str, bool], None]: + async def subrouter_lifespan(app: FastAPI) -> AsyncGenerator[dict[str, bool], None]: state.sub_router_startup = True yield {"sub_router": True} state.sub_router_shutdown = True @@ -139,7 +140,7 @@ def test_router_nested_lifespan_state(state: State) -> None: app.include_router(router) @app.get("/") - def main(request: Request) -> Dict[str, str]: + def main(request: Request) -> dict[str, str]: assert request.state.app assert request.state.router assert request.state.sub_router @@ -175,7 +176,7 @@ def test_router_nested_lifespan_state_overriding_by_parent() -> None: @asynccontextmanager async def lifespan( app: FastAPI, - ) -> AsyncGenerator[Dict[str, Union[str, bool]], None]: + ) -> AsyncGenerator[dict[str, Union[str, bool]], None]: yield { "app_specific": True, "overridden": "app", @@ -184,7 +185,7 @@ def test_router_nested_lifespan_state_overriding_by_parent() -> None: @asynccontextmanager async def router_lifespan( app: FastAPI, - ) -> AsyncGenerator[Dict[str, Union[str, bool]], None]: + ) -> AsyncGenerator[dict[str, Union[str, bool]], None]: yield { "router_specific": True, "overridden": "router", # should override parent @@ -225,7 +226,7 @@ def test_merged_mixed_state_lifespans() -> None: yield @asynccontextmanager - async def router_lifespan(app: FastAPI) -> AsyncGenerator[Dict[str, bool], None]: + async def router_lifespan(app: FastAPI) -> AsyncGenerator[dict[str, bool], None]: yield {"router": True} @asynccontextmanager diff --git a/tests/test_schema_compat_pydantic_v2.py b/tests/test_schema_compat_pydantic_v2.py new file mode 100644 index 0000000000..737687f256 --- /dev/null +++ b/tests/test_schema_compat_pydantic_v2.py @@ -0,0 +1,90 @@ +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient +from inline_snapshot import snapshot +from pydantic import BaseModel + +from tests.utils import needs_py310 + + +@pytest.fixture(name="client") +def get_client(): + from enum import Enum + + app = FastAPI() + + class PlatformRole(str, Enum): + admin = "admin" + user = "user" + + class OtherRole(str, Enum): ... + + class User(BaseModel): + username: str + role: PlatformRole | OtherRole + + @app.get("/users") + async def get_user() -> User: + return {"username": "alice", "role": "admin"} + + client = TestClient(app) + return client + + +@needs_py310 +def test_get(client: TestClient): + response = client.get("/users") + assert response.json() == {"username": "alice", "role": "admin"} + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("openapi.json") + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users": { + "get": { + "summary": "Get User", + "operationId": "get_user_users_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + } + }, + "components": { + "schemas": { + "PlatformRole": { + "type": "string", + "enum": ["admin", "user"], + "title": "PlatformRole", + }, + "User": { + "properties": { + "username": {"type": "string", "title": "Username"}, + "role": { + "anyOf": [ + {"$ref": "#/components/schemas/PlatformRole"}, + {"enum": [], "title": "OtherRole"}, + ], + "title": "Role", + }, + }, + "type": "object", + "required": ["username", "role"], + "title": "User", + }, + } + }, + } + ) diff --git a/tests/test_schema_extra_examples.py b/tests/test_schema_extra_examples.py index b313f47e90..ac8999c90a 100644 --- a/tests/test_schema_extra_examples.py +++ b/tests/test_schema_extra_examples.py @@ -1,9 +1,8 @@ from typing import Union import pytest -from dirty_equals import IsDict from fastapi import Body, Cookie, FastAPI, Header, Path, Query -from fastapi._compat import PYDANTIC_V2 +from fastapi.exceptions import FastAPIDeprecationWarning from fastapi.testclient import TestClient from pydantic import BaseModel, ConfigDict @@ -14,20 +13,15 @@ def create_app(): class Item(BaseModel): data: str - if PYDANTIC_V2: - model_config = ConfigDict( - json_schema_extra={"example": {"data": "Data in schema_extra"}} - ) - else: - - class Config: - schema_extra = {"example": {"data": "Data in schema_extra"}} + model_config = ConfigDict( + json_schema_extra={"example": {"data": "Data in schema_extra"}} + ) @app.post("/schema_extra/") def schema_extra(item: Item): return item - with pytest.warns(DeprecationWarning): + with pytest.warns(FastAPIDeprecationWarning): @app.post("/example/") def example(item: Item = Body(example={"data": "Data in Body example"})): @@ -44,7 +38,7 @@ def create_app(): ): return item - with pytest.warns(DeprecationWarning): + with pytest.warns(FastAPIDeprecationWarning): @app.post("/example_examples/") def example_examples( @@ -89,7 +83,7 @@ def create_app(): # ): # return lastname - with pytest.warns(DeprecationWarning): + with pytest.warns(FastAPIDeprecationWarning): @app.get("/path_example/{item_id}") def path_example( @@ -107,7 +101,7 @@ def create_app(): ): return item_id - with pytest.warns(DeprecationWarning): + with pytest.warns(FastAPIDeprecationWarning): @app.get("/path_example_examples/{item_id}") def path_example_examples( @@ -118,7 +112,7 @@ def create_app(): ): return item_id - with pytest.warns(DeprecationWarning): + with pytest.warns(FastAPIDeprecationWarning): @app.get("/query_example/") def query_example( @@ -138,7 +132,7 @@ def create_app(): ): return data - with pytest.warns(DeprecationWarning): + with pytest.warns(FastAPIDeprecationWarning): @app.get("/query_example_examples/") def query_example_examples( @@ -150,7 +144,7 @@ def create_app(): ): return data - with pytest.warns(DeprecationWarning): + with pytest.warns(FastAPIDeprecationWarning): @app.get("/header_example/") def header_example( @@ -173,7 +167,7 @@ def create_app(): ): return data - with pytest.warns(DeprecationWarning): + with pytest.warns(FastAPIDeprecationWarning): @app.get("/header_example_examples/") def header_example_examples( @@ -185,7 +179,7 @@ def create_app(): ): return data - with pytest.warns(DeprecationWarning): + with pytest.warns(FastAPIDeprecationWarning): @app.get("/cookie_example/") def cookie_example( @@ -205,7 +199,7 @@ def create_app(): ): return data - with pytest.warns(DeprecationWarning): + with pytest.warns(FastAPIDeprecationWarning): @app.get("/cookie_example_examples/") def cookie_example_examples( @@ -341,28 +335,13 @@ def test_openapi_schema(): "requestBody": { "content": { "application/json": { - "schema": IsDict( - { - "$ref": "#/components/schemas/Item", - "examples": [ - {"data": "Data in Body examples, example1"}, - {"data": "Data in Body examples, example2"}, - ], - } - ) - | IsDict( - # TODO: remove this when deprecating Pydantic v1 - { - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - "title": "Item", - "examples": [ - {"data": "Data in Body examples, example1"}, - {"data": "Data in Body examples, example2"}, - ], - } - ) + "schema": { + "$ref": "#/components/schemas/Item", + "examples": [ + {"data": "Data in Body examples, example1"}, + {"data": "Data in Body examples, example2"}, + ], + } } }, "required": True, @@ -392,28 +371,13 @@ def test_openapi_schema(): "requestBody": { "content": { "application/json": { - "schema": IsDict( - { - "$ref": "#/components/schemas/Item", - "examples": [ - {"data": "examples example_examples 1"}, - {"data": "examples example_examples 2"}, - ], - } - ) - | IsDict( - # TODO: remove this when deprecating Pydantic v1 - { - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - "title": "Item", - "examples": [ - {"data": "examples example_examples 1"}, - {"data": "examples example_examples 2"}, - ], - }, - ), + "schema": { + "$ref": "#/components/schemas/Item", + "examples": [ + {"data": "examples example_examples 1"}, + {"data": "examples example_examples 2"}, + ], + }, "example": {"data": "Overridden example"}, } }, @@ -544,16 +508,10 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Data", - } - ) - | IsDict( - # TODO: Remove this when deprecating Pydantic v1 - {"title": "Data", "type": "string"} - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Data", + }, "example": "query1", "name": "data", "in": "query", @@ -584,21 +542,11 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Data", - "examples": ["query1", "query2"], - } - ) - | IsDict( - # TODO: Remove this when deprecating Pydantic v1 - { - "type": "string", - "title": "Data", - "examples": ["query1", "query2"], - } - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Data", + "examples": ["query1", "query2"], + }, "name": "data", "in": "query", } @@ -628,21 +576,11 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Data", - "examples": ["query1", "query2"], - } - ) - | IsDict( - # TODO: Remove this when deprecating Pydantic v1 - { - "type": "string", - "title": "Data", - "examples": ["query1", "query2"], - } - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Data", + "examples": ["query1", "query2"], + }, "example": "query_overridden", "name": "data", "in": "query", @@ -673,16 +611,10 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Data", - } - ) - | IsDict( - # TODO: Remove this when deprecating Pydantic v1 - {"title": "Data", "type": "string"} - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Data", + }, "example": "header1", "name": "data", "in": "header", @@ -713,21 +645,11 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Data", - "examples": ["header1", "header2"], - } - ) - | IsDict( - # TODO: Remove this when deprecating Pydantic v1 - { - "type": "string", - "title": "Data", - "examples": ["header1", "header2"], - } - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Data", + "examples": ["header1", "header2"], + }, "name": "data", "in": "header", } @@ -757,21 +679,11 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Data", - "examples": ["header1", "header2"], - } - ) - | IsDict( - # TODO: Remove this when deprecating Pydantic v1 - { - "title": "Data", - "type": "string", - "examples": ["header1", "header2"], - } - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Data", + "examples": ["header1", "header2"], + }, "example": "header_overridden", "name": "data", "in": "header", @@ -802,16 +714,10 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Data", - } - ) - | IsDict( - # TODO: Remove this when deprecating Pydantic v1 - {"title": "Data", "type": "string"} - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Data", + }, "example": "cookie1", "name": "data", "in": "cookie", @@ -842,21 +748,11 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Data", - "examples": ["cookie1", "cookie2"], - } - ) - | IsDict( - # TODO: Remove this when deprecating Pydantic v1 - { - "title": "Data", - "type": "string", - "examples": ["cookie1", "cookie2"], - } - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Data", + "examples": ["cookie1", "cookie2"], + }, "name": "data", "in": "cookie", } @@ -886,21 +782,11 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Data", - "examples": ["cookie1", "cookie2"], - } - ) - | IsDict( - # TODO: Remove this when deprecating Pydantic v1 - { - "title": "Data", - "type": "string", - "examples": ["cookie1", "cookie2"], - } - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Data", + "examples": ["cookie1", "cookie2"], + }, "example": "cookie_overridden", "name": "data", "in": "cookie", diff --git a/tests/test_schema_ref_pydantic_v2.py b/tests/test_schema_ref_pydantic_v2.py new file mode 100644 index 0000000000..69cb82a356 --- /dev/null +++ b/tests/test_schema_ref_pydantic_v2.py @@ -0,0 +1,68 @@ +from typing import Any + +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient +from inline_snapshot import snapshot +from pydantic import BaseModel, ConfigDict, Field + + +@pytest.fixture(name="client") +def get_client(): + app = FastAPI() + + class ModelWithRef(BaseModel): + ref: str = Field(validation_alias="$ref", serialization_alias="$ref") + model_config = ConfigDict(validate_by_alias=True, serialize_by_alias=True) + + @app.get("/", response_model=ModelWithRef) + async def read_root() -> Any: + return {"$ref": "some-ref"} + + client = TestClient(app) + return client + + +def test_get(client: TestClient): + response = client.get("/") + assert response.json() == {"$ref": "some-ref"} + + +def test_openapi_schema(client: TestClient): + response = client.get("openapi.json") + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/": { + "get": { + "summary": "Read Root", + "operationId": "read_root__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ModelWithRef" + } + } + }, + } + }, + } + } + }, + "components": { + "schemas": { + "ModelWithRef": { + "properties": {"$ref": {"type": "string", "title": "$Ref"}}, + "type": "object", + "required": ["$ref"], + "title": "ModelWithRef", + } + } + }, + } + ) diff --git a/tests/test_security_api_key_cookie.py b/tests/test_security_api_key_cookie.py index 4ddb8e2eeb..9bacfc56ec 100644 --- a/tests/test_security_api_key_cookie.py +++ b/tests/test_security_api_key_cookie.py @@ -32,8 +32,9 @@ def test_security_api_key(): def test_security_api_key_no_key(): client = TestClient(app) response = client.get("/users/me") - assert response.status_code == 403, response.text + assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "APIKey" def test_openapi_schema(): diff --git a/tests/test_security_api_key_cookie_description.py b/tests/test_security_api_key_cookie_description.py index d99d616e06..d0cab324eb 100644 --- a/tests/test_security_api_key_cookie_description.py +++ b/tests/test_security_api_key_cookie_description.py @@ -32,8 +32,9 @@ def test_security_api_key(): def test_security_api_key_no_key(): client = TestClient(app) response = client.get("/users/me") - assert response.status_code == 403, response.text + assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "APIKey" def test_openapi_schema(): diff --git a/tests/test_security_api_key_header.py b/tests/test_security_api_key_header.py index 1ff883703d..3e761b150b 100644 --- a/tests/test_security_api_key_header.py +++ b/tests/test_security_api_key_header.py @@ -33,8 +33,9 @@ def test_security_api_key(): def test_security_api_key_no_key(): response = client.get("/users/me") - assert response.status_code == 403, response.text + assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "APIKey" def test_openapi_schema(): diff --git a/tests/test_security_api_key_header_description.py b/tests/test_security_api_key_header_description.py index 27f9d0f29e..38a1a88814 100644 --- a/tests/test_security_api_key_header_description.py +++ b/tests/test_security_api_key_header_description.py @@ -33,8 +33,9 @@ def test_security_api_key(): def test_security_api_key_no_key(): response = client.get("/users/me") - assert response.status_code == 403, response.text + assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "APIKey" def test_openapi_schema(): diff --git a/tests/test_security_api_key_query.py b/tests/test_security_api_key_query.py index dc7a0a621a..11ed194689 100644 --- a/tests/test_security_api_key_query.py +++ b/tests/test_security_api_key_query.py @@ -33,8 +33,9 @@ def test_security_api_key(): def test_security_api_key_no_key(): response = client.get("/users/me") - assert response.status_code == 403, response.text + assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "APIKey" def test_openapi_schema(): diff --git a/tests/test_security_api_key_query_description.py b/tests/test_security_api_key_query_description.py index 35dc7743a2..6587983261 100644 --- a/tests/test_security_api_key_query_description.py +++ b/tests/test_security_api_key_query_description.py @@ -33,8 +33,9 @@ def test_security_api_key(): def test_security_api_key_no_key(): response = client.get("/users/me") - assert response.status_code == 403, response.text + assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "APIKey" def test_openapi_schema(): diff --git a/tests/test_security_http_base.py b/tests/test_security_http_base.py index 51928bafd5..8cf259a750 100644 --- a/tests/test_security_http_base.py +++ b/tests/test_security_http_base.py @@ -23,8 +23,9 @@ def test_security_http_base(): def test_security_http_base_no_credentials(): response = client.get("/users/me") - assert response.status_code == 403, response.text + assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Other" def test_openapi_schema(): diff --git a/tests/test_security_http_base_description.py b/tests/test_security_http_base_description.py index bc79f32424..791ea59f4d 100644 --- a/tests/test_security_http_base_description.py +++ b/tests/test_security_http_base_description.py @@ -23,8 +23,9 @@ def test_security_http_base(): def test_security_http_base_no_credentials(): response = client.get("/users/me") - assert response.status_code == 403, response.text + assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Other" def test_openapi_schema(): diff --git a/tests/test_security_http_basic_optional.py b/tests/test_security_http_basic_optional.py index 9b6cb6c455..7071f381a1 100644 --- a/tests/test_security_http_basic_optional.py +++ b/tests/test_security_http_basic_optional.py @@ -38,7 +38,7 @@ def test_security_http_basic_invalid_credentials(): ) assert response.status_code == 401, response.text assert response.headers["WWW-Authenticate"] == "Basic" - assert response.json() == {"detail": "Invalid authentication credentials"} + assert response.json() == {"detail": "Not authenticated"} def test_security_http_basic_non_basic_credentials(): @@ -47,7 +47,7 @@ def test_security_http_basic_non_basic_credentials(): response = client.get("/users/me", headers={"Authorization": auth_header}) assert response.status_code == 401, response.text assert response.headers["WWW-Authenticate"] == "Basic" - assert response.json() == {"detail": "Invalid authentication credentials"} + assert response.json() == {"detail": "Not authenticated"} def test_openapi_schema(): diff --git a/tests/test_security_http_basic_realm.py b/tests/test_security_http_basic_realm.py index 9fc33971ae..ec7371f90f 100644 --- a/tests/test_security_http_basic_realm.py +++ b/tests/test_security_http_basic_realm.py @@ -36,7 +36,7 @@ def test_security_http_basic_invalid_credentials(): ) assert response.status_code == 401, response.text assert response.headers["WWW-Authenticate"] == 'Basic realm="simple"' - assert response.json() == {"detail": "Invalid authentication credentials"} + assert response.json() == {"detail": "Not authenticated"} def test_security_http_basic_non_basic_credentials(): @@ -45,7 +45,7 @@ def test_security_http_basic_non_basic_credentials(): response = client.get("/users/me", headers={"Authorization": auth_header}) assert response.status_code == 401, response.text assert response.headers["WWW-Authenticate"] == 'Basic realm="simple"' - assert response.json() == {"detail": "Invalid authentication credentials"} + assert response.json() == {"detail": "Not authenticated"} def test_openapi_schema(): diff --git a/tests/test_security_http_basic_realm_description.py b/tests/test_security_http_basic_realm_description.py index 02122442eb..a93d5fc86b 100644 --- a/tests/test_security_http_basic_realm_description.py +++ b/tests/test_security_http_basic_realm_description.py @@ -36,7 +36,7 @@ def test_security_http_basic_invalid_credentials(): ) assert response.status_code == 401, response.text assert response.headers["WWW-Authenticate"] == 'Basic realm="simple"' - assert response.json() == {"detail": "Invalid authentication credentials"} + assert response.json() == {"detail": "Not authenticated"} def test_security_http_basic_non_basic_credentials(): @@ -45,7 +45,7 @@ def test_security_http_basic_non_basic_credentials(): response = client.get("/users/me", headers={"Authorization": auth_header}) assert response.status_code == 401, response.text assert response.headers["WWW-Authenticate"] == 'Basic realm="simple"' - assert response.json() == {"detail": "Invalid authentication credentials"} + assert response.json() == {"detail": "Not authenticated"} def test_openapi_schema(): diff --git a/tests/test_security_http_bearer.py b/tests/test_security_http_bearer.py index 5b9e2d6919..961b42f4db 100644 --- a/tests/test_security_http_bearer.py +++ b/tests/test_security_http_bearer.py @@ -23,14 +23,16 @@ def test_security_http_bearer(): def test_security_http_bearer_no_credentials(): response = client.get("/users/me") - assert response.status_code == 403, response.text + assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" def test_security_http_bearer_incorrect_scheme_credentials(): response = client.get("/users/me", headers={"Authorization": "Basic notreally"}) - assert response.status_code == 403, response.text - assert response.json() == {"detail": "Invalid authentication credentials"} + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" def test_openapi_schema(): diff --git a/tests/test_security_http_bearer_description.py b/tests/test_security_http_bearer_description.py index 2f11c3a148..e16994abce 100644 --- a/tests/test_security_http_bearer_description.py +++ b/tests/test_security_http_bearer_description.py @@ -23,14 +23,16 @@ def test_security_http_bearer(): def test_security_http_bearer_no_credentials(): response = client.get("/users/me") - assert response.status_code == 403, response.text + assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" def test_security_http_bearer_incorrect_scheme_credentials(): response = client.get("/users/me", headers={"Authorization": "Basic notreally"}) - assert response.status_code == 403, response.text - assert response.json() == {"detail": "Invalid authentication credentials"} + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" def test_openapi_schema(): diff --git a/tests/test_security_http_digest.py b/tests/test_security_http_digest.py index 133d35763c..3fad4c7a56 100644 --- a/tests/test_security_http_digest.py +++ b/tests/test_security_http_digest.py @@ -23,16 +23,18 @@ def test_security_http_digest(): def test_security_http_digest_no_credentials(): response = client.get("/users/me") - assert response.status_code == 403, response.text + assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Digest" def test_security_http_digest_incorrect_scheme_credentials(): response = client.get( "/users/me", headers={"Authorization": "Other invalidauthorization"} ) - assert response.status_code == 403, response.text - assert response.json() == {"detail": "Invalid authentication credentials"} + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Digest" def test_openapi_schema(): diff --git a/tests/test_security_http_digest_description.py b/tests/test_security_http_digest_description.py index 4e31a0c008..319416a071 100644 --- a/tests/test_security_http_digest_description.py +++ b/tests/test_security_http_digest_description.py @@ -23,16 +23,18 @@ def test_security_http_digest(): def test_security_http_digest_no_credentials(): response = client.get("/users/me") - assert response.status_code == 403, response.text + assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Digest" def test_security_http_digest_incorrect_scheme_credentials(): response = client.get( "/users/me", headers={"Authorization": "Other invalidauthorization"} ) - assert response.status_code == 403, response.text - assert response.json() == {"detail": "Invalid authentication credentials"} + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Digest" def test_openapi_schema(): diff --git a/tests/test_security_oauth2.py b/tests/test_security_oauth2.py index 2b7e3457a9..7ad9369956 100644 --- a/tests/test_security_oauth2.py +++ b/tests/test_security_oauth2.py @@ -1,5 +1,4 @@ import pytest -from dirty_equals import IsDict from fastapi import Depends, FastAPI, Security from fastapi.security import OAuth2, OAuth2PasswordRequestFormStrict from fastapi.testclient import TestClient @@ -56,86 +55,51 @@ def test_security_oauth2_password_other_header(): def test_security_oauth2_password_bearer_no_header(): response = client.get("/users/me") - assert response.status_code == 403, response.text + assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" def test_strict_login_no_data(): response = client.post("/login") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "grant_type"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "password"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "grant_type"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "grant_type"], + "msg": "Field required", + "input": None, + }, + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": None, + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": None, + }, + ] + } def test_strict_login_no_grant_type(): response = client.post("/login", data={"username": "johndoe", "password": "secret"}) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "grant_type"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "grant_type"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "grant_type"], + "msg": "Field required", + "input": None, + } + ] + } @pytest.mark.parametrize( @@ -152,31 +116,17 @@ def test_strict_login_incorrect_grant_type(grant_type: str): data={"username": "johndoe", "password": "secret", "grant_type": grant_type}, ) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "string_pattern_mismatch", - "loc": ["body", "grant_type"], - "msg": "String should match pattern '^password$'", - "input": grant_type, - "ctx": {"pattern": "^password$"}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "grant_type"], - "msg": 'string does not match regex "^password$"', - "type": "value_error.str.regex", - "ctx": {"pattern": "^password$"}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "string_pattern_mismatch", + "loc": ["body", "grant_type"], + "msg": "String should match pattern '^password$'", + "input": grant_type, + "ctx": {"pattern": "^password$"}, + } + ] + } def test_strict_login_correct_grant_type(): @@ -263,26 +213,14 @@ def test_openapi_schema(): "username": {"title": "Username", "type": "string"}, "password": {"title": "Password", "type": "string"}, "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": IsDict( - { - "title": "Client Id", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Client Id", "type": "string"} - ), - "client_secret": IsDict( - { - "title": "Client Secret", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Client Secret", "type": "string"} - ), + "client_id": { + "title": "Client Id", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "client_secret": { + "title": "Client Secret", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, }, }, "ValidationError": { diff --git a/tests/test_security_oauth2_authorization_code_bearer_scopes_openapi.py b/tests/test_security_oauth2_authorization_code_bearer_scopes_openapi.py new file mode 100644 index 0000000000..583007c8b7 --- /dev/null +++ b/tests/test_security_oauth2_authorization_code_bearer_scopes_openapi.py @@ -0,0 +1,197 @@ +# Ref: https://github.com/fastapi/fastapi/issues/14454 + +from typing import Annotated, Optional + +from fastapi import APIRouter, Depends, FastAPI, Security +from fastapi.security import OAuth2AuthorizationCodeBearer +from fastapi.testclient import TestClient +from inline_snapshot import snapshot + +oauth2_scheme = OAuth2AuthorizationCodeBearer( + authorizationUrl="authorize", + tokenUrl="token", + auto_error=True, + scopes={"read": "Read access", "write": "Write access"}, +) + + +async def get_token(token: Annotated[str, Depends(oauth2_scheme)]) -> str: + return token + + +app = FastAPI(dependencies=[Depends(get_token)]) + + +@app.get("/") +async def root(): + return {"message": "Hello World"} + + +@app.get( + "/with-oauth2-scheme", + dependencies=[Security(oauth2_scheme, scopes=["read", "write"])], +) +async def read_with_oauth2_scheme(): + return {"message": "Admin Access"} + + +@app.get( + "/with-get-token", dependencies=[Security(get_token, scopes=["read", "write"])] +) +async def read_with_get_token(): + return {"message": "Admin Access"} + + +router = APIRouter(dependencies=[Security(oauth2_scheme, scopes=["read"])]) + + +@router.get("/items/") +async def read_items(token: Optional[str] = Depends(oauth2_scheme)): + return {"token": token} + + +@router.post("/items/") +async def create_item( + token: Optional[str] = Security(oauth2_scheme, scopes=["read", "write"]), +): + return {"token": token} + + +app.include_router(router) + +client = TestClient(app) + + +def test_root(): + response = client.get("/", headers={"Authorization": "Bearer testtoken"}) + assert response.status_code == 200, response.text + assert response.json() == {"message": "Hello World"} + + +def test_read_with_oauth2_scheme(): + response = client.get( + "/with-oauth2-scheme", headers={"Authorization": "Bearer testtoken"} + ) + assert response.status_code == 200, response.text + assert response.json() == {"message": "Admin Access"} + + +def test_read_with_get_token(): + response = client.get( + "/with-get-token", headers={"Authorization": "Bearer testtoken"} + ) + assert response.status_code == 200, response.text + assert response.json() == {"message": "Admin Access"} + + +def test_read_token(): + response = client.get("/items/", headers={"Authorization": "Bearer testtoken"}) + assert response.status_code == 200, response.text + assert response.json() == {"token": "testtoken"} + + +def test_create_token(): + response = client.post("/items/", headers={"Authorization": "Bearer testtoken"}) + assert response.status_code == 200, response.text + assert response.json() == {"token": "testtoken"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/": { + "get": { + "summary": "Root", + "operationId": "root__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "security": [{"OAuth2AuthorizationCodeBearer": []}], + } + }, + "/with-oauth2-scheme": { + "get": { + "summary": "Read With Oauth2 Scheme", + "operationId": "read_with_oauth2_scheme_with_oauth2_scheme_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "security": [ + {"OAuth2AuthorizationCodeBearer": ["read", "write"]} + ], + } + }, + "/with-get-token": { + "get": { + "summary": "Read With Get Token", + "operationId": "read_with_get_token_with_get_token_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "security": [ + {"OAuth2AuthorizationCodeBearer": ["read", "write"]} + ], + } + }, + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "security": [ + {"OAuth2AuthorizationCodeBearer": ["read"]}, + ], + }, + "post": { + "summary": "Create Item", + "operationId": "create_item_items__post", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "security": [ + {"OAuth2AuthorizationCodeBearer": ["read", "write"]}, + ], + }, + }, + }, + "components": { + "securitySchemes": { + "OAuth2AuthorizationCodeBearer": { + "type": "oauth2", + "flows": { + "authorizationCode": { + "scopes": { + "read": "Read access", + "write": "Write access", + }, + "authorizationUrl": "authorize", + "tokenUrl": "token", + } + }, + } + } + }, + } + ) diff --git a/tests/test_security_oauth2_authorization_code_bearer_scopes_openapi_simple.py b/tests/test_security_oauth2_authorization_code_bearer_scopes_openapi_simple.py new file mode 100644 index 0000000000..1c21369d33 --- /dev/null +++ b/tests/test_security_oauth2_authorization_code_bearer_scopes_openapi_simple.py @@ -0,0 +1,80 @@ +# Ref: https://github.com/fastapi/fastapi/issues/14454 + +from typing import Annotated + +from fastapi import Depends, FastAPI, Security +from fastapi.security import OAuth2AuthorizationCodeBearer +from fastapi.testclient import TestClient +from inline_snapshot import snapshot + +oauth2_scheme = OAuth2AuthorizationCodeBearer( + authorizationUrl="api/oauth/authorize", + tokenUrl="/api/oauth/token", + scopes={"read": "Read access", "write": "Write access"}, +) + + +async def get_token(token: Annotated[str, Depends(oauth2_scheme)]) -> str: + return token + + +app = FastAPI(dependencies=[Depends(get_token)]) + + +@app.get("/admin", dependencies=[Security(get_token, scopes=["read", "write"])]) +async def read_admin(): + return {"message": "Admin Access"} + + +client = TestClient(app) + + +def test_read_admin(): + response = client.get("/admin", headers={"Authorization": "Bearer faketoken"}) + assert response.status_code == 200, response.text + assert response.json() == {"message": "Admin Access"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/admin": { + "get": { + "summary": "Read Admin", + "operationId": "read_admin_admin_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "security": [ + {"OAuth2AuthorizationCodeBearer": ["read", "write"]} + ], + } + } + }, + "components": { + "securitySchemes": { + "OAuth2AuthorizationCodeBearer": { + "type": "oauth2", + "flows": { + "authorizationCode": { + "scopes": { + "read": "Read access", + "write": "Write access", + }, + "authorizationUrl": "api/oauth/authorize", + "tokenUrl": "/api/oauth/token", + } + }, + } + } + }, + } + ) diff --git a/tests/test_security_oauth2_optional.py b/tests/test_security_oauth2_optional.py index 046ac57637..57c16058af 100644 --- a/tests/test_security_oauth2_optional.py +++ b/tests/test_security_oauth2_optional.py @@ -1,7 +1,6 @@ from typing import Optional import pytest -from dirty_equals import IsDict from fastapi import Depends, FastAPI, Security from fastapi.security import OAuth2, OAuth2PasswordRequestFormStrict from fastapi.testclient import TestClient @@ -67,79 +66,43 @@ def test_security_oauth2_password_bearer_no_header(): def test_strict_login_no_data(): response = client.post("/login") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "grant_type"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "password"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "grant_type"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "grant_type"], + "msg": "Field required", + "input": None, + }, + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": None, + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": None, + }, + ] + } def test_strict_login_no_grant_type(): response = client.post("/login", data={"username": "johndoe", "password": "secret"}) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "grant_type"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "grant_type"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "grant_type"], + "msg": "Field required", + "input": None, + } + ] + } @pytest.mark.parametrize( @@ -156,31 +119,17 @@ def test_strict_login_incorrect_grant_type(grant_type: str): data={"username": "johndoe", "password": "secret", "grant_type": grant_type}, ) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "string_pattern_mismatch", - "loc": ["body", "grant_type"], - "msg": "String should match pattern '^password$'", - "input": grant_type, - "ctx": {"pattern": "^password$"}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "grant_type"], - "msg": 'string does not match regex "^password$"', - "type": "value_error.str.regex", - "ctx": {"pattern": "^password$"}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "string_pattern_mismatch", + "loc": ["body", "grant_type"], + "msg": "String should match pattern '^password$'", + "input": grant_type, + "ctx": {"pattern": "^password$"}, + } + ] + } def test_strict_login_correct_data(): @@ -267,26 +216,14 @@ def test_openapi_schema(): "username": {"title": "Username", "type": "string"}, "password": {"title": "Password", "type": "string"}, "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": IsDict( - { - "title": "Client Id", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Client Id", "type": "string"} - ), - "client_secret": IsDict( - { - "title": "Client Secret", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Client Secret", "type": "string"} - ), + "client_id": { + "title": "Client Id", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "client_secret": { + "title": "Client Secret", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, }, }, "ValidationError": { diff --git a/tests/test_security_oauth2_optional_description.py b/tests/test_security_oauth2_optional_description.py index 629cddca2f..60c6c242e0 100644 --- a/tests/test_security_oauth2_optional_description.py +++ b/tests/test_security_oauth2_optional_description.py @@ -1,7 +1,6 @@ from typing import Optional import pytest -from dirty_equals import IsDict from fastapi import Depends, FastAPI, Security from fastapi.security import OAuth2, OAuth2PasswordRequestFormStrict from fastapi.testclient import TestClient @@ -68,79 +67,43 @@ def test_security_oauth2_password_bearer_no_header(): def test_strict_login_None(): response = client.post("/login", data=None) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "grant_type"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "password"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "grant_type"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "grant_type"], + "msg": "Field required", + "input": None, + }, + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": None, + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": None, + }, + ] + } def test_strict_login_no_grant_type(): response = client.post("/login", data={"username": "johndoe", "password": "secret"}) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "grant_type"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "grant_type"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "grant_type"], + "msg": "Field required", + "input": None, + } + ] + } @pytest.mark.parametrize( @@ -157,31 +120,17 @@ def test_strict_login_incorrect_grant_type(grant_type: str): data={"username": "johndoe", "password": "secret", "grant_type": grant_type}, ) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "string_pattern_mismatch", - "loc": ["body", "grant_type"], - "msg": "String should match pattern '^password$'", - "input": grant_type, - "ctx": {"pattern": "^password$"}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "grant_type"], - "msg": 'string does not match regex "^password$"', - "type": "value_error.str.regex", - "ctx": {"pattern": "^password$"}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "string_pattern_mismatch", + "loc": ["body", "grant_type"], + "msg": "String should match pattern '^password$'", + "input": grant_type, + "ctx": {"pattern": "^password$"}, + } + ] + } def test_strict_login_correct_correct_grant_type(): @@ -268,26 +217,14 @@ def test_openapi_schema(): "username": {"title": "Username", "type": "string"}, "password": {"title": "Password", "type": "string"}, "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": IsDict( - { - "title": "Client Id", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Client Id", "type": "string"} - ), - "client_secret": IsDict( - { - "title": "Client Secret", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Client Secret", "type": "string"} - ), + "client_id": { + "title": "Client Id", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "client_secret": { + "title": "Client Secret", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, }, }, "ValidationError": { diff --git a/tests/test_security_openid_connect.py b/tests/test_security_openid_connect.py index 1e322e640e..c9a0a8db76 100644 --- a/tests/test_security_openid_connect.py +++ b/tests/test_security_openid_connect.py @@ -39,8 +39,9 @@ def test_security_oauth2_password_other_header(): def test_security_oauth2_password_bearer_no_header(): response = client.get("/users/me") - assert response.status_code == 403, response.text + assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" def test_openapi_schema(): diff --git a/tests/test_security_openid_connect_description.py b/tests/test_security_openid_connect_description.py index 44cf57f862..d008cbc630 100644 --- a/tests/test_security_openid_connect_description.py +++ b/tests/test_security_openid_connect_description.py @@ -41,8 +41,9 @@ def test_security_oauth2_password_other_header(): def test_security_oauth2_password_bearer_no_header(): response = client.get("/users/me") - assert response.status_code == 403, response.text + assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" def test_openapi_schema(): diff --git a/tests/test_security_scopes.py b/tests/test_security_scopes.py new file mode 100644 index 0000000000..fccb026fe2 --- /dev/null +++ b/tests/test_security_scopes.py @@ -0,0 +1,45 @@ +from typing import Annotated + +import pytest +from fastapi import Depends, FastAPI, Security +from fastapi.testclient import TestClient + + +@pytest.fixture(name="call_counter") +def call_counter_fixture(): + return {"count": 0} + + +@pytest.fixture(name="app") +def app_fixture(call_counter: dict[str, int]): + def get_db(): + call_counter["count"] += 1 + return f"db_{call_counter['count']}" + + def get_user(db: Annotated[str, Depends(get_db)]): + return "user" + + app = FastAPI() + + @app.get("/") + def endpoint( + db: Annotated[str, Depends(get_db)], + user: Annotated[str, Security(get_user, scopes=["read"])], + ): + return {"db": db} + + return app + + +@pytest.fixture(name="client") +def client_fixture(app: FastAPI): + return TestClient(app) + + +def test_security_scopes_dependency_called_once( + client: TestClient, call_counter: dict[str, int] +): + response = client.get("/") + + assert response.status_code == 200 + assert call_counter["count"] == 1 diff --git a/tests/test_security_scopes_dont_propagate.py b/tests/test_security_scopes_dont_propagate.py new file mode 100644 index 0000000000..c306ed0590 --- /dev/null +++ b/tests/test_security_scopes_dont_propagate.py @@ -0,0 +1,44 @@ +# Ref: https://github.com/tiangolo/fastapi/issues/5623 + +from typing import Annotated, Any + +from fastapi import FastAPI, Security +from fastapi.security import SecurityScopes +from fastapi.testclient import TestClient + + +async def security1(scopes: SecurityScopes): + return scopes.scopes + + +async def security2(scopes: SecurityScopes): + return scopes.scopes + + +async def dep3( + dep1: Annotated[list[str], Security(security1, scopes=["scope1"])], + dep2: Annotated[list[str], Security(security2, scopes=["scope2"])], +): + return {"dep1": dep1, "dep2": dep2} + + +app = FastAPI() + + +@app.get("/scopes") +def get_scopes( + dep3: Annotated[dict[str, Any], Security(dep3, scopes=["scope3"])], +): + return dep3 + + +client = TestClient(app) + + +def test_security_scopes_dont_propagate(): + response = client.get("/scopes") + assert response.status_code == 200 + assert response.json() == { + "dep1": ["scope3", "scope1"], + "dep2": ["scope3", "scope2"], + } diff --git a/tests/test_security_scopes_sub_dependency.py b/tests/test_security_scopes_sub_dependency.py new file mode 100644 index 0000000000..2c64d5f3d5 --- /dev/null +++ b/tests/test_security_scopes_sub_dependency.py @@ -0,0 +1,107 @@ +# Ref: https://github.com/fastapi/fastapi/discussions/6024#discussioncomment-8541913 + + +from typing import Annotated + +import pytest +from fastapi import Depends, FastAPI, Security +from fastapi.security import SecurityScopes +from fastapi.testclient import TestClient + + +@pytest.fixture(name="call_counts") +def call_counts_fixture(): + return { + "get_db_session": 0, + "get_current_user": 0, + "get_user_me": 0, + "get_user_items": 0, + } + + +@pytest.fixture(name="app") +def app_fixture(call_counts: dict[str, int]): + def get_db_session(): + call_counts["get_db_session"] += 1 + return f"db_session_{call_counts['get_db_session']}" + + def get_current_user( + security_scopes: SecurityScopes, + db_session: Annotated[str, Depends(get_db_session)], + ): + call_counts["get_current_user"] += 1 + return { + "user": f"user_{call_counts['get_current_user']}", + "scopes": security_scopes.scopes, + "db_session": db_session, + } + + def get_user_me( + current_user: Annotated[dict, Security(get_current_user, scopes=["me"])], + ): + call_counts["get_user_me"] += 1 + return { + "user_me": f"user_me_{call_counts['get_user_me']}", + "current_user": current_user, + } + + def get_user_items( + user_me: Annotated[dict, Depends(get_user_me)], + ): + call_counts["get_user_items"] += 1 + return { + "user_items": f"user_items_{call_counts['get_user_items']}", + "user_me": user_me, + } + + app = FastAPI() + + @app.get("/") + def path_operation( + user_me: Annotated[dict, Depends(get_user_me)], + user_items: Annotated[dict, Security(get_user_items, scopes=["items"])], + ): + return { + "user_me": user_me, + "user_items": user_items, + } + + return app + + +@pytest.fixture(name="client") +def client_fixture(app: FastAPI): + return TestClient(app) + + +def test_security_scopes_sub_dependency_caching( + client: TestClient, call_counts: dict[str, int] +): + response = client.get("/") + + assert response.status_code == 200 + assert call_counts["get_db_session"] == 1 + assert call_counts["get_current_user"] == 2 + assert call_counts["get_user_me"] == 2 + assert call_counts["get_user_items"] == 1 + assert response.json() == { + "user_me": { + "user_me": "user_me_1", + "current_user": { + "user": "user_1", + "scopes": ["me"], + "db_session": "db_session_1", + }, + }, + "user_items": { + "user_items": "user_items_1", + "user_me": { + "user_me": "user_me_2", + "current_user": { + "user": "user_2", + "scopes": ["items", "me"], + "db_session": "db_session_1", + }, + }, + }, + } diff --git a/tests/test_serialize_response.py b/tests/test_serialize_response.py index d823e5e04a..14f88dd931 100644 --- a/tests/test_serialize_response.py +++ b/tests/test_serialize_response.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import Optional from fastapi import FastAPI from fastapi.testclient import TestClient @@ -10,7 +10,7 @@ app = FastAPI() class Item(BaseModel): name: str price: Optional[float] = None - owner_ids: Optional[List[int]] = None + owner_ids: Optional[list[int]] = None @app.get("/items/valid", response_model=Item) @@ -23,7 +23,7 @@ def get_coerce(): return {"name": "coerce", "price": "1.0"} -@app.get("/items/validlist", response_model=List[Item]) +@app.get("/items/validlist", response_model=list[Item]) def get_validlist(): return [ {"name": "foo"}, diff --git a/tests/test_serialize_response_dataclass.py b/tests/test_serialize_response_dataclass.py index 1e3bf3b28b..ee695368b8 100644 --- a/tests/test_serialize_response_dataclass.py +++ b/tests/test_serialize_response_dataclass.py @@ -1,6 +1,6 @@ from dataclasses import dataclass from datetime import datetime -from typing import List, Optional +from typing import Optional from fastapi import FastAPI from fastapi.testclient import TestClient @@ -13,7 +13,7 @@ class Item: name: str date: datetime price: Optional[float] = None - owner_ids: Optional[List[int]] = None + owner_ids: Optional[list[int]] = None @app.get("/items/valid", response_model=Item) @@ -33,7 +33,7 @@ def get_coerce(): return {"name": "coerce", "date": datetime(2021, 7, 26).isoformat(), "price": "1.0"} -@app.get("/items/validlist", response_model=List[Item]) +@app.get("/items/validlist", response_model=list[Item]) def get_validlist(): return [ {"name": "foo", "date": datetime(2021, 7, 26)}, @@ -47,7 +47,7 @@ def get_validlist(): ] -@app.get("/items/objectlist", response_model=List[Item]) +@app.get("/items/objectlist", response_model=list[Item]) def get_objectlist(): return [ Item(name="foo", date=datetime(2021, 7, 26)), diff --git a/tests/test_serialize_response_model.py b/tests/test_serialize_response_model.py index 3bb46b2e9b..79c90c9c29 100644 --- a/tests/test_serialize_response_model.py +++ b/tests/test_serialize_response_model.py @@ -1,4 +1,4 @@ -from typing import Dict, List, Optional +from typing import Optional from fastapi import FastAPI from pydantic import BaseModel, Field @@ -10,7 +10,7 @@ app = FastAPI() class Item(BaseModel): name: str = Field(alias="aliased_name") price: Optional[float] = None - owner_ids: Optional[List[int]] = None + owner_ids: Optional[list[int]] = None @app.get("/items/valid", response_model=Item) @@ -23,7 +23,7 @@ def get_coerce(): return Item(aliased_name="coerce", price="1.0") -@app.get("/items/validlist", response_model=List[Item]) +@app.get("/items/validlist", response_model=list[Item]) def get_validlist(): return [ Item(aliased_name="foo"), @@ -32,7 +32,7 @@ def get_validlist(): ] -@app.get("/items/validdict", response_model=Dict[str, Item]) +@app.get("/items/validdict", response_model=dict[str, Item]) def get_validdict(): return { "k1": Item(aliased_name="foo"), @@ -59,7 +59,7 @@ def get_coerce_exclude_unset(): @app.get( "/items/validlist-exclude-unset", - response_model=List[Item], + response_model=list[Item], response_model_exclude_unset=True, ) def get_validlist_exclude_unset(): @@ -72,7 +72,7 @@ def get_validlist_exclude_unset(): @app.get( "/items/validdict-exclude-unset", - response_model=Dict[str, Item], + response_model=dict[str, Item], response_model_exclude_unset=True, ) def get_validdict_exclude_unset(): diff --git a/tests/test_stringified_annotation_dependency.py b/tests/test_stringified_annotation_dependency.py new file mode 100644 index 0000000000..ce88074957 --- /dev/null +++ b/tests/test_stringified_annotation_dependency.py @@ -0,0 +1,79 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Annotated + +import pytest +from fastapi import Depends, FastAPI +from fastapi.testclient import TestClient +from inline_snapshot import snapshot + +if TYPE_CHECKING: # pragma: no cover + from collections.abc import AsyncGenerator + + +class DummyClient: + async def get_people(self) -> list: + return ["John Doe", "Jane Doe"] + + async def close(self) -> None: + pass + + +async def get_client() -> AsyncGenerator[DummyClient, None]: + client = DummyClient() + yield client + await client.close() + + +Client = Annotated[DummyClient, Depends(get_client)] + + +@pytest.fixture(name="client") +def client_fixture() -> TestClient: + app = FastAPI() + + @app.get("/") + async def get_people(client: Client) -> list: + return await client.get_people() + + client = TestClient(app) + return client + + +def test_get(client: TestClient): + response = client.get("/") + assert response.status_code == 200, response.text + assert response.json() == ["John Doe", "Jane Doe"] + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/": { + "get": { + "summary": "Get People", + "operationId": "get_people__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": {}, + "type": "array", + "title": "Response Get People Get", + } + } + }, + } + }, + } + } + }, + } + ) diff --git a/tests/test_stringified_annotations_simple.py b/tests/test_stringified_annotations_simple.py new file mode 100644 index 0000000000..6e037fb21f --- /dev/null +++ b/tests/test_stringified_annotations_simple.py @@ -0,0 +1,27 @@ +from __future__ import annotations + +from typing import Annotated + +from fastapi import Depends, FastAPI, Request +from fastapi.testclient import TestClient + +from .utils import needs_py310 + + +class Dep: + def __call__(self, request: Request): + return "test" + + +@needs_py310 +def test_stringified_annotations(): + app = FastAPI() + + client = TestClient(app) + + @app.get("/test/") + def call(test: Annotated[str, Depends(Dep())]): + return {"test": test} + + response = client.get("/test") + assert response.status_code == 200 diff --git a/tests/test_sub_callbacks.py b/tests/test_sub_callbacks.py index ed7f4efe8a..cc7e5f5c6a 100644 --- a/tests/test_sub_callbacks.py +++ b/tests/test_sub_callbacks.py @@ -1,6 +1,5 @@ from typing import Optional -from dirty_equals import IsDict from fastapi import APIRouter, FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel, HttpUrl @@ -99,30 +98,18 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": IsDict( - { - "title": "Callback Url", - "anyOf": [ - { - "type": "string", - "format": "uri", - "minLength": 1, - "maxLength": 2083, - }, - {"type": "null"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Callback Url", - "maxLength": 2083, - "minLength": 1, - "type": "string", - "format": "uri", - } - ), + "schema": { + "title": "Callback Url", + "anyOf": [ + { + "type": "string", + "format": "uri", + "minLength": 1, + "maxLength": 2083, + }, + {"type": "null"}, + ], + }, "name": "callback_url", "in": "query", } @@ -262,16 +249,10 @@ def test_openapi_schema(): "type": "object", "properties": { "id": {"title": "Id", "type": "string"}, - "title": IsDict( - { - "title": "Title", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Title", "type": "string"} - ), + "title": { + "title": "Title", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, "customer": {"title": "Customer", "type": "string"}, "total": {"title": "Total", "type": "number"}, }, diff --git a/tests/test_top_level_security_scheme_in_openapi.py b/tests/test_top_level_security_scheme_in_openapi.py index e2de31af53..a36c66d1ac 100644 --- a/tests/test_top_level_security_scheme_in_openapi.py +++ b/tests/test_top_level_security_scheme_in_openapi.py @@ -27,7 +27,7 @@ def test_get_root(): def test_get_root_no_token(): response = client.get("/") - assert response.status_code == 403, response.text + assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} diff --git a/tests/test_tuples.py b/tests/test_tuples.py index ca33d2580c..d3c89045b4 100644 --- a/tests/test_tuples.py +++ b/tests/test_tuples.py @@ -1,6 +1,3 @@ -from typing import List, Tuple - -from dirty_equals import IsDict from fastapi import FastAPI, Form from fastapi.testclient import TestClient from pydantic import BaseModel @@ -9,7 +6,7 @@ app = FastAPI() class ItemGroup(BaseModel): - items: List[Tuple[str, str]] + items: list[tuple[str, str]] class Coordinate(BaseModel): @@ -23,12 +20,12 @@ def post_model_with_tuple(item_group: ItemGroup): @app.post("/tuple-of-models/") -def post_tuple_of_models(square: Tuple[Coordinate, Coordinate]): +def post_tuple_of_models(square: tuple[Coordinate, Coordinate]): return square @app.post("/tuple-form/") -def hello(values: Tuple[int, int] = Form()): +def hello(values: tuple[int, int] = Form()): return values @@ -127,31 +124,16 @@ def test_openapi_schema(): "requestBody": { "content": { "application/json": { - "schema": IsDict( - { - "title": "Square", - "maxItems": 2, - "minItems": 2, - "type": "array", - "prefixItems": [ - {"$ref": "#/components/schemas/Coordinate"}, - {"$ref": "#/components/schemas/Coordinate"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Square", - "maxItems": 2, - "minItems": 2, - "type": "array", - "items": [ - {"$ref": "#/components/schemas/Coordinate"}, - {"$ref": "#/components/schemas/Coordinate"}, - ], - } - ) + "schema": { + "title": "Square", + "maxItems": 2, + "minItems": 2, + "type": "array", + "prefixItems": [ + {"$ref": "#/components/schemas/Coordinate"}, + {"$ref": "#/components/schemas/Coordinate"}, + ], + } } }, "required": True, @@ -214,28 +196,16 @@ def test_openapi_schema(): "required": ["values"], "type": "object", "properties": { - "values": IsDict( - { - "title": "Values", - "maxItems": 2, - "minItems": 2, - "type": "array", - "prefixItems": [ - {"type": "integer"}, - {"type": "integer"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Values", - "maxItems": 2, - "minItems": 2, - "type": "array", - "items": [{"type": "integer"}, {"type": "integer"}], - } - ) + "values": { + "title": "Values", + "maxItems": 2, + "minItems": 2, + "type": "array", + "prefixItems": [ + {"type": "integer"}, + {"type": "integer"}, + ], + } }, }, "Coordinate": { @@ -266,26 +236,15 @@ def test_openapi_schema(): "items": { "title": "Items", "type": "array", - "items": IsDict( - { - "maxItems": 2, - "minItems": 2, - "type": "array", - "prefixItems": [ - {"type": "string"}, - {"type": "string"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "maxItems": 2, - "minItems": 2, - "type": "array", - "items": [{"type": "string"}, {"type": "string"}], - } - ), + "items": { + "maxItems": 2, + "minItems": 2, + "type": "array", + "prefixItems": [ + {"type": "string"}, + {"type": "string"}, + ], + }, } }, }, diff --git a/tests/test_tutorial/test_additional_responses/test_tutorial001.py b/tests/test_tutorial/test_additional_responses/test_tutorial001.py index 3afeaff840..1a18db75ce 100644 --- a/tests/test_tutorial/test_additional_responses/test_tutorial001.py +++ b/tests/test_tutorial/test_additional_responses/test_tutorial001.py @@ -1,6 +1,6 @@ from fastapi.testclient import TestClient -from docs_src.additional_responses.tutorial001 import app +from docs_src.additional_responses.tutorial001_py39 import app client = TestClient(app) diff --git a/tests/test_tutorial/test_additional_responses/test_tutorial002.py b/tests/test_tutorial/test_additional_responses/test_tutorial002.py index 588a3160a9..8208605956 100644 --- a/tests/test_tutorial/test_additional_responses/test_tutorial002.py +++ b/tests/test_tutorial/test_additional_responses/test_tutorial002.py @@ -1,21 +1,35 @@ +import importlib import os import shutil -from dirty_equals import IsDict +import pytest from fastapi.testclient import TestClient -from docs_src.additional_responses.tutorial002 import app - -client = TestClient(app) +from tests.utils import needs_py310 -def test_path_operation(): +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial002_py39"), + pytest.param("tutorial002_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.additional_responses.{request.param}") + + client = TestClient(mod.app) + client.headers.clear() + return client + + +def test_path_operation(client: TestClient): response = client.get("/items/foo") assert response.status_code == 200, response.text assert response.json() == {"id": "foo", "value": "there goes my hero"} -def test_path_operation_img(): +def test_path_operation_img(client: TestClient): shutil.copy("./docs/en/docs/img/favicon.png", "./image.png") response = client.get("/items/foo?img=1") assert response.status_code == 200, response.text @@ -24,7 +38,7 @@ def test_path_operation_img(): os.remove("./image.png") -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { @@ -65,16 +79,10 @@ def test_openapi_schema(): }, { "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "boolean"}, {"type": "null"}], - "title": "Img", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Img", "type": "boolean"} - ), + "schema": { + "anyOf": [{"type": "boolean"}, {"type": "null"}], + "title": "Img", + }, "name": "img", "in": "query", }, diff --git a/tests/test_tutorial/test_additional_responses/test_tutorial003.py b/tests/test_tutorial/test_additional_responses/test_tutorial003.py index bd34d2938c..90dc4e371e 100644 --- a/tests/test_tutorial/test_additional_responses/test_tutorial003.py +++ b/tests/test_tutorial/test_additional_responses/test_tutorial003.py @@ -1,6 +1,6 @@ from fastapi.testclient import TestClient -from docs_src.additional_responses.tutorial003 import app +from docs_src.additional_responses.tutorial003_py39 import app client = TestClient(app) diff --git a/tests/test_tutorial/test_additional_responses/test_tutorial004.py b/tests/test_tutorial/test_additional_responses/test_tutorial004.py index 55b556d8e1..c6abf5e466 100644 --- a/tests/test_tutorial/test_additional_responses/test_tutorial004.py +++ b/tests/test_tutorial/test_additional_responses/test_tutorial004.py @@ -1,21 +1,35 @@ +import importlib import os import shutil -from dirty_equals import IsDict +import pytest from fastapi.testclient import TestClient -from docs_src.additional_responses.tutorial004 import app - -client = TestClient(app) +from tests.utils import needs_py310 -def test_path_operation(): +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial004_py39"), + pytest.param("tutorial004_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.additional_responses.{request.param}") + + client = TestClient(mod.app) + client.headers.clear() + return client + + +def test_path_operation(client: TestClient): response = client.get("/items/foo") assert response.status_code == 200, response.text assert response.json() == {"id": "foo", "value": "there goes my hero"} -def test_path_operation_img(): +def test_path_operation_img(client: TestClient): shutil.copy("./docs/en/docs/img/favicon.png", "./image.png") response = client.get("/items/foo?img=1") assert response.status_code == 200, response.text @@ -24,7 +38,7 @@ def test_path_operation_img(): os.remove("./image.png") -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { @@ -68,16 +82,10 @@ def test_openapi_schema(): }, { "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "boolean"}, {"type": "null"}], - "title": "Img", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Img", "type": "boolean"} - ), + "schema": { + "anyOf": [{"type": "boolean"}, {"type": "null"}], + "title": "Img", + }, "name": "img", "in": "query", }, diff --git a/tests/test_tutorial/test_additional_status_codes/test_tutorial001.py b/tests/test_tutorial/test_additional_status_codes/test_tutorial001.py index b304f70153..bced1f6dfe 100644 --- a/tests/test_tutorial/test_additional_status_codes/test_tutorial001.py +++ b/tests/test_tutorial/test_additional_status_codes/test_tutorial001.py @@ -3,16 +3,15 @@ import importlib import pytest from fastapi.testclient import TestClient -from ...utils import needs_py39, needs_py310 +from ...utils import needs_py310 @pytest.fixture( name="client", params=[ - "tutorial001", + "tutorial001_py39", pytest.param("tutorial001_py310", marks=needs_py310), - "tutorial001_an", - pytest.param("tutorial001_an_py39", marks=needs_py39), + "tutorial001_an_py39", pytest.param("tutorial001_an_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_advanced_middleware/test_tutorial001.py b/tests/test_tutorial/test_advanced_middleware/test_tutorial001.py index 157fa5caf1..f17391956e 100644 --- a/tests/test_tutorial/test_advanced_middleware/test_tutorial001.py +++ b/tests/test_tutorial/test_advanced_middleware/test_tutorial001.py @@ -1,6 +1,6 @@ from fastapi.testclient import TestClient -from docs_src.advanced_middleware.tutorial001 import app +from docs_src.advanced_middleware.tutorial001_py39 import app def test_middleware(): diff --git a/tests/test_tutorial/test_advanced_middleware/test_tutorial002.py b/tests/test_tutorial/test_advanced_middleware/test_tutorial002.py index 79be52f4db..bae915406e 100644 --- a/tests/test_tutorial/test_advanced_middleware/test_tutorial002.py +++ b/tests/test_tutorial/test_advanced_middleware/test_tutorial002.py @@ -1,6 +1,6 @@ from fastapi.testclient import TestClient -from docs_src.advanced_middleware.tutorial002 import app +from docs_src.advanced_middleware.tutorial002_py39 import app def test_middleware(): diff --git a/tests/test_tutorial/test_advanced_middleware/test_tutorial003.py b/tests/test_tutorial/test_advanced_middleware/test_tutorial003.py index 04a922ff7b..66697997c8 100644 --- a/tests/test_tutorial/test_advanced_middleware/test_tutorial003.py +++ b/tests/test_tutorial/test_advanced_middleware/test_tutorial003.py @@ -1,7 +1,7 @@ from fastapi.responses import PlainTextResponse from fastapi.testclient import TestClient -from docs_src.advanced_middleware.tutorial003 import app +from docs_src.advanced_middleware.tutorial003_py39 import app @app.get("/large") diff --git a/tests/test_tutorial/test_async_tests/test_main.py b/tests/test_tutorial/test_async_tests/test_main_a.py similarity index 58% rename from tests/test_tutorial/test_async_tests/test_main.py rename to tests/test_tutorial/test_async_tests/test_main_a.py index 1f5d7186cc..f29acaa9a0 100644 --- a/tests/test_tutorial/test_async_tests/test_main.py +++ b/tests/test_tutorial/test_async_tests/test_main_a.py @@ -1,6 +1,6 @@ import pytest -from docs_src.async_tests.test_main import test_root +from docs_src.async_tests.app_a_py39.test_main import test_root @pytest.mark.anyio diff --git a/tests/test_tutorial/test_authentication_error_status_code/__init__.py b/tests/test_tutorial/test_authentication_error_status_code/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/test_tutorial/test_authentication_error_status_code/test_tutorial001.py b/tests/test_tutorial/test_authentication_error_status_code/test_tutorial001.py new file mode 100644 index 0000000000..6f58116313 --- /dev/null +++ b/tests/test_tutorial/test_authentication_error_status_code/test_tutorial001.py @@ -0,0 +1,66 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient +from inline_snapshot import snapshot + + +@pytest.fixture( + name="client", + params=[ + "tutorial001_an_py39", + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module( + f"docs_src.authentication_error_status_code.{request.param}" + ) + + client = TestClient(mod.app) + return client + + +def test_get_me(client: TestClient): + response = client.get("/me", headers={"Authorization": "Bearer secrettoken"}) + assert response.status_code == 200 + assert response.json() == { + "message": "You are authenticated", + "token": "secrettoken", + } + + +def test_get_me_no_credentials(client: TestClient): + response = client.get("/me") + assert response.status_code == 403 + assert response.json() == {"detail": "Not authenticated"} + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/me": { + "get": { + "summary": "Read Me", + "operationId": "read_me_me_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "security": [{"HTTPBearer403": []}], + } + } + }, + "components": { + "securitySchemes": { + "HTTPBearer403": {"type": "http", "scheme": "bearer"} + } + }, + } + ) diff --git a/tests/test_tutorial/test_background_tasks/test_tutorial001.py b/tests/test_tutorial/test_background_tasks/test_tutorial001.py index 0602cd8aa4..c0ad27a6f5 100644 --- a/tests/test_tutorial/test_background_tasks/test_tutorial001.py +++ b/tests/test_tutorial/test_background_tasks/test_tutorial001.py @@ -3,7 +3,7 @@ from pathlib import Path from fastapi.testclient import TestClient -from docs_src.background_tasks.tutorial001 import app +from docs_src.background_tasks.tutorial001_py39 import app client = TestClient(app) diff --git a/tests/test_tutorial/test_background_tasks/test_tutorial002.py b/tests/test_tutorial/test_background_tasks/test_tutorial002.py index d5ef51ee2b..288a1c244e 100644 --- a/tests/test_tutorial/test_background_tasks/test_tutorial002.py +++ b/tests/test_tutorial/test_background_tasks/test_tutorial002.py @@ -5,16 +5,15 @@ from pathlib import Path import pytest from fastapi.testclient import TestClient -from ...utils import needs_py39, needs_py310 +from ...utils import needs_py310 @pytest.fixture( name="client", params=[ - "tutorial002", + "tutorial002_py39", pytest.param("tutorial002_py310", marks=needs_py310), - "tutorial002_an", - pytest.param("tutorial002_an_py39", marks=needs_py39), + "tutorial002_an_py39", pytest.param("tutorial002_an_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_behind_a_proxy/test_tutorial001.py b/tests/test_tutorial/test_behind_a_proxy/test_tutorial001.py index a070f850f7..00574b5b0f 100644 --- a/tests/test_tutorial/test_behind_a_proxy/test_tutorial001.py +++ b/tests/test_tutorial/test_behind_a_proxy/test_tutorial001.py @@ -1,6 +1,6 @@ from fastapi.testclient import TestClient -from docs_src.behind_a_proxy.tutorial001 import app +from docs_src.behind_a_proxy.tutorial001_py39 import app client = TestClient(app, root_path="/api/v1") diff --git a/tests/test_tutorial/test_behind_a_proxy/test_tutorial001_01.py b/tests/test_tutorial/test_behind_a_proxy/test_tutorial001_01.py index f13046e018..da4acb28cc 100644 --- a/tests/test_tutorial/test_behind_a_proxy/test_tutorial001_01.py +++ b/tests/test_tutorial/test_behind_a_proxy/test_tutorial001_01.py @@ -1,6 +1,6 @@ from fastapi.testclient import TestClient -from docs_src.behind_a_proxy.tutorial001_01 import app +from docs_src.behind_a_proxy.tutorial001_01_py39 import app client = TestClient( app, diff --git a/tests/test_tutorial/test_behind_a_proxy/test_tutorial002.py b/tests/test_tutorial/test_behind_a_proxy/test_tutorial002.py index ce791e2157..1a49c0dfeb 100644 --- a/tests/test_tutorial/test_behind_a_proxy/test_tutorial002.py +++ b/tests/test_tutorial/test_behind_a_proxy/test_tutorial002.py @@ -1,6 +1,6 @@ from fastapi.testclient import TestClient -from docs_src.behind_a_proxy.tutorial002 import app +from docs_src.behind_a_proxy.tutorial002_py39 import app client = TestClient(app) diff --git a/tests/test_tutorial/test_behind_a_proxy/test_tutorial003.py b/tests/test_tutorial/test_behind_a_proxy/test_tutorial003.py index ec17b41790..a164bb80b5 100644 --- a/tests/test_tutorial/test_behind_a_proxy/test_tutorial003.py +++ b/tests/test_tutorial/test_behind_a_proxy/test_tutorial003.py @@ -1,7 +1,7 @@ -from dirty_equals import IsOneOf from fastapi.testclient import TestClient +from inline_snapshot import snapshot -from docs_src.behind_a_proxy.tutorial003 import app +from docs_src.behind_a_proxy.tutorial003_py39 import app client = TestClient(app) @@ -15,40 +15,34 @@ def test_main(): def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200 - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "servers": [ - {"url": "/api/v1"}, - { - "url": IsOneOf( - "https://stag.example.com/", - # TODO: remove when deprecating Pydantic v1 - "https://stag.example.com", - ), - "description": "Staging environment", - }, - { - "url": IsOneOf( - "https://prod.example.com/", - # TODO: remove when deprecating Pydantic v1 - "https://prod.example.com", - ), - "description": "Production environment", - }, - ], - "paths": { - "/app": { - "get": { - "summary": "Read Main", - "operationId": "read_main_app_get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "servers": [ + {"url": "/api/v1"}, + { + "url": "https://stag.example.com", + "description": "Staging environment", + }, + { + "url": "https://prod.example.com", + "description": "Production environment", + }, + ], + "paths": { + "/app": { + "get": { + "summary": "Read Main", + "operationId": "read_main_app_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } } - } - }, - } + }, + } + ) diff --git a/tests/test_tutorial/test_behind_a_proxy/test_tutorial004.py b/tests/test_tutorial/test_behind_a_proxy/test_tutorial004.py index 2f8eb46995..01bba9fedd 100644 --- a/tests/test_tutorial/test_behind_a_proxy/test_tutorial004.py +++ b/tests/test_tutorial/test_behind_a_proxy/test_tutorial004.py @@ -1,7 +1,7 @@ -from dirty_equals import IsOneOf from fastapi.testclient import TestClient +from inline_snapshot import snapshot -from docs_src.behind_a_proxy.tutorial004 import app +from docs_src.behind_a_proxy.tutorial004_py39 import app client = TestClient(app) @@ -15,39 +15,33 @@ def test_main(): def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200 - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "servers": [ - { - "url": IsOneOf( - "https://stag.example.com/", - # TODO: remove when deprecating Pydantic v1 - "https://stag.example.com", - ), - "description": "Staging environment", - }, - { - "url": IsOneOf( - "https://prod.example.com/", - # TODO: remove when deprecating Pydantic v1 - "https://prod.example.com", - ), - "description": "Production environment", - }, - ], - "paths": { - "/app": { - "get": { - "summary": "Read Main", - "operationId": "read_main_app_get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "servers": [ + { + "url": "https://stag.example.com", + "description": "Staging environment", + }, + { + "url": "https://prod.example.com", + "description": "Production environment", + }, + ], + "paths": { + "/app": { + "get": { + "summary": "Read Main", + "operationId": "read_main_app_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } } - } - }, - } + }, + } + ) diff --git a/tests/test_tutorial/test_bigger_applications/test_main.py b/tests/test_tutorial/test_bigger_applications/test_main.py index fe40fad7d0..f5e243b95a 100644 --- a/tests/test_tutorial/test_bigger_applications/test_main.py +++ b/tests/test_tutorial/test_bigger_applications/test_main.py @@ -1,18 +1,14 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient -from ...utils import needs_py39 - @pytest.fixture( name="client", params=[ - "app_an.main", - pytest.param("app_an_py39.main", marks=needs_py39), - "app.main", + "app_py39.main", + "app_an_py39.main", ], ) def get_client(request: pytest.FixtureRequest): @@ -31,29 +27,16 @@ def test_users_token_jessica(client: TestClient): def test_users_with_no_token(client: TestClient): response = client.get("/users") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "token"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + } + ] + } def test_users_foo_token_jessica(client: TestClient): @@ -65,29 +48,16 @@ def test_users_foo_token_jessica(client: TestClient): def test_users_foo_with_no_token(client: TestClient): response = client.get("/users/foo") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "token"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + } + ] + } def test_users_me_token_jessica(client: TestClient): @@ -99,29 +69,16 @@ def test_users_me_token_jessica(client: TestClient): def test_users_me_with_no_token(client: TestClient): response = client.get("/users/me") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "token"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + } + ] + } def test_users_token_monica_with_no_jessica(client: TestClient): @@ -144,29 +101,16 @@ def test_items_token_jessica(client: TestClient): def test_items_with_no_token_jessica(client: TestClient): response = client.get("/items", headers={"X-Token": "fake-super-secret-token"}) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "token"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + } + ] + } def test_items_plumbus_token_jessica(client: TestClient): @@ -190,29 +134,16 @@ def test_items_plumbus_with_no_token(client: TestClient): "/items/plumbus", headers={"X-Token": "fake-super-secret-token"} ) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "token"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + } + ] + } def test_items_with_invalid_token(client: TestClient): @@ -230,57 +161,31 @@ def test_items_bar_with_invalid_token(client: TestClient): def test_items_with_missing_x_token_header(client: TestClient): response = client.get("/items?token=jessica") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["header", "x-token"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + } + ] + } def test_items_plumbus_with_missing_x_token_header(client: TestClient): response = client.get("/items/plumbus?token=jessica") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["header", "x-token"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + } + ] + } def test_root_token_jessica(client: TestClient): @@ -292,68 +197,37 @@ def test_root_token_jessica(client: TestClient): def test_root_with_no_token(client: TestClient): response = client.get("/") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "token"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + } + ] + } def test_put_no_header(client: TestClient): response = client.put("/items/foo") assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "token"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["header", "x-token"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + }, + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + }, + ] + } def test_put_invalid_header(client: TestClient): diff --git a/tests/test_tutorial/test_body/test_tutorial001.py b/tests/test_tutorial/test_body/test_tutorial001.py index f8b5aee8d2..5a7cae1603 100644 --- a/tests/test_tutorial/test_body/test_tutorial001.py +++ b/tests/test_tutorial/test_body/test_tutorial001.py @@ -2,7 +2,6 @@ import importlib from unittest.mock import patch import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -11,7 +10,7 @@ from ...utils import needs_py310 @pytest.fixture( name="client", params=[ - "tutorial001", + "tutorial001_py39", pytest.param("tutorial001_py310", marks=needs_py310), ], ) @@ -74,124 +73,67 @@ def test_post_with_str_float_description_tax(client: TestClient): def test_post_with_only_name(client: TestClient): response = client.post("/items/", json={"name": "Foo"}) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "price"], - "msg": "Field required", - "input": {"name": "Foo"}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "price"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "price"], + "msg": "Field required", + "input": {"name": "Foo"}, + } + ] + } def test_post_with_only_name_price(client: TestClient): response = client.post("/items/", json={"name": "Foo", "price": "twenty"}) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "float_parsing", - "loc": ["body", "price"], - "msg": "Input should be a valid number, unable to parse string as a number", - "input": "twenty", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "price"], - "msg": "value is not a valid float", - "type": "type_error.float", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "float_parsing", + "loc": ["body", "price"], + "msg": "Input should be a valid number, unable to parse string as a number", + "input": "twenty", + } + ] + } def test_post_with_no_data(client: TestClient): response = client.post("/items/", json={}) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "name"], - "msg": "Field required", - "input": {}, - }, - { - "type": "missing", - "loc": ["body", "price"], - "msg": "Field required", - "input": {}, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "name"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "price"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "name"], + "msg": "Field required", + "input": {}, + }, + { + "type": "missing", + "loc": ["body", "price"], + "msg": "Field required", + "input": {}, + }, + ] + } def test_post_with_none(client: TestClient): response = client.post("/items/", json=None) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body"], + "msg": "Field required", + "input": None, + } + ] + } def test_post_broken_body(client: TestClient): @@ -201,67 +143,32 @@ def test_post_broken_body(client: TestClient): content="{some broken json}", ) assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "json_invalid", - "loc": ["body", 1], - "msg": "JSON decode error", - "input": {}, - "ctx": { - "error": "Expecting property name enclosed in double quotes" - }, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", 1], - "msg": "Expecting property name enclosed in double quotes: line 1 column 2 (char 1)", - "type": "value_error.jsondecode", - "ctx": { - "msg": "Expecting property name enclosed in double quotes", - "doc": "{some broken json}", - "pos": 1, - "lineno": 1, - "colno": 2, - }, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "json_invalid", + "loc": ["body", 1], + "msg": "JSON decode error", + "input": {}, + "ctx": {"error": "Expecting property name enclosed in double quotes"}, + } + ] + } def test_post_form_for_json(client: TestClient): response = client.post("/items/", data={"name": "Foo", "price": 50.5}) assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "model_attributes_type", - "loc": ["body"], - "msg": "Input should be a valid dictionary or object to extract fields from", - "input": "name=Foo&price=50.5", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body"], - "msg": "value is not a valid dict", - "type": "type_error.dict", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "model_attributes_type", + "loc": ["body"], + "msg": "Input should be a valid dictionary or object to extract fields from", + "input": "name=Foo&price=50.5", + } + ] + } def test_explicit_content_type(client: TestClient): @@ -302,84 +209,46 @@ def test_wrong_headers(client: TestClient): "/items/", content=data, headers={"Content-Type": "text/plain"} ) assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "model_attributes_type", - "loc": ["body"], - "msg": "Input should be a valid dictionary or object to extract fields from", - "input": '{"name": "Foo", "price": 50.5}', - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body"], - "msg": "value is not a valid dict", - "type": "type_error.dict", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "model_attributes_type", + "loc": ["body"], + "msg": "Input should be a valid dictionary or object to extract fields from", + "input": '{"name": "Foo", "price": 50.5}', + } + ] + } response = client.post( "/items/", content=data, headers={"Content-Type": "application/geo+json-seq"} ) assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "model_attributes_type", - "loc": ["body"], - "msg": "Input should be a valid dictionary or object to extract fields from", - "input": '{"name": "Foo", "price": 50.5}', - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body"], - "msg": "value is not a valid dict", - "type": "type_error.dict", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "model_attributes_type", + "loc": ["body"], + "msg": "Input should be a valid dictionary or object to extract fields from", + "input": '{"name": "Foo", "price": 50.5}', + } + ] + } + response = client.post( "/items/", content=data, headers={"Content-Type": "application/not-really-json"} ) assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "model_attributes_type", - "loc": ["body"], - "msg": "Input should be a valid dictionary or object to extract fields from", - "input": '{"name": "Foo", "price": 50.5}', - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body"], - "msg": "value is not a valid dict", - "type": "type_error.dict", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "model_attributes_type", + "loc": ["body"], + "msg": "Input should be a valid dictionary or object to extract fields from", + "input": '{"name": "Foo", "price": 50.5}', + } + ] + } def test_other_exceptions(client: TestClient): @@ -435,26 +304,14 @@ def test_openapi_schema(client: TestClient): "properties": { "name": {"title": "Name", "type": "string"}, "price": {"title": "Price", "type": "number"}, - "description": IsDict( - { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), - "tax": IsDict( - { - "title": "Tax", - "anyOf": [{"type": "number"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Tax", "type": "number"} - ), + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_body/test_tutorial002.py b/tests/test_tutorial/test_body/test_tutorial002.py new file mode 100644 index 0000000000..b6d51d5235 --- /dev/null +++ b/tests/test_tutorial/test_body/test_tutorial002.py @@ -0,0 +1,161 @@ +import importlib +from typing import Union + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial002_py39"), + pytest.param("tutorial002_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.body.{request.param}") + + client = TestClient(mod.app) + return client + + +@pytest.mark.parametrize("price", ["50.5", 50.5]) +def test_post_with_tax(client: TestClient, price: Union[str, float]): + response = client.post( + "/items/", + json={"name": "Foo", "price": price, "description": "Some Foo", "tax": 0.3}, + ) + assert response.status_code == 200 + assert response.json() == { + "name": "Foo", + "price": 50.5, + "description": "Some Foo", + "tax": 0.3, + "price_with_tax": 50.8, + } + + +@pytest.mark.parametrize("price", ["50.5", 50.5]) +def test_post_without_tax(client: TestClient, price: Union[str, float]): + response = client.post( + "/items/", json={"name": "Foo", "price": price, "description": "Some Foo"} + ) + assert response.status_code == 200 + assert response.json() == { + "name": "Foo", + "price": 50.5, + "description": "Some Foo", + "tax": None, + } + + +def test_post_with_no_data(client: TestClient): + response = client.post("/items/", json={}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "name"], + "msg": "Field required", + "input": {}, + }, + { + "type": "missing", + "loc": ["body", "price"], + "msg": "Field required", + "input": {}, + }, + ] + } + + +def test_openapi_schema(client: TestClient): + 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": { + "/items/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create Item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, + }, + }, + "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"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_body/test_tutorial003.py b/tests/test_tutorial/test_body/test_tutorial003.py new file mode 100644 index 0000000000..227a125e78 --- /dev/null +++ b/tests/test_tutorial/test_body/test_tutorial003.py @@ -0,0 +1,171 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial003_py39"), + pytest.param("tutorial003_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.body.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_put_all(client: TestClient): + response = client.put( + "/items/123", + json={"name": "Foo", "price": 50.1, "description": "Some Foo", "tax": 0.3}, + ) + assert response.status_code == 200 + assert response.json() == { + "item_id": 123, + "name": "Foo", + "price": 50.1, + "description": "Some Foo", + "tax": 0.3, + } + + +def test_put_only_required(client: TestClient): + response = client.put( + "/items/123", + json={"name": "Foo", "price": 50.1}, + ) + assert response.status_code == 200 + assert response.json() == { + "item_id": 123, + "name": "Foo", + "price": 50.1, + "description": None, + "tax": None, + } + + +def test_put_with_no_data(client: TestClient): + response = client.put("/items/123", json={}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "name"], + "msg": "Field required", + "input": {}, + }, + { + "type": "missing", + "loc": ["body", "price"], + "msg": "Field required", + "input": {}, + }, + ] + } + + +def test_openapi_schema(client: TestClient): + 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": { + "/items/{item_id}": { + "put": { + "parameters": [ + { + "in": "path", + "name": "item_id", + "required": True, + "schema": { + "title": "Item Id", + "type": "integer", + }, + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, + }, + }, + "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"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_body/test_tutorial004.py b/tests/test_tutorial/test_body/test_tutorial004.py new file mode 100644 index 0000000000..10212843ee --- /dev/null +++ b/tests/test_tutorial/test_body/test_tutorial004.py @@ -0,0 +1,182 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial004_py39"), + pytest.param("tutorial004_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.body.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_put_all(client: TestClient): + response = client.put( + "/items/123", + json={"name": "Foo", "price": 50.1, "description": "Some Foo", "tax": 0.3}, + params={"q": "somequery"}, + ) + assert response.status_code == 200 + assert response.json() == { + "item_id": 123, + "name": "Foo", + "price": 50.1, + "description": "Some Foo", + "tax": 0.3, + "q": "somequery", + } + + +def test_put_only_required(client: TestClient): + response = client.put( + "/items/123", + json={"name": "Foo", "price": 50.1}, + ) + assert response.status_code == 200 + assert response.json() == { + "item_id": 123, + "name": "Foo", + "price": 50.1, + "description": None, + "tax": None, + } + + +def test_put_with_no_data(client: TestClient): + response = client.put("/items/123", json={}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "name"], + "msg": "Field required", + "input": {}, + }, + { + "type": "missing", + "loc": ["body", "price"], + "msg": "Field required", + "input": {}, + }, + ] + } + + +def test_openapi_schema(client: TestClient): + 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": { + "/items/{item_id}": { + "put": { + "parameters": [ + { + "in": "path", + "name": "item_id", + "required": True, + "schema": { + "title": "Item Id", + "type": "integer", + }, + }, + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + }, + "name": "q", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, + }, + }, + "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"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_body_fields/test_tutorial001.py b/tests/test_tutorial/test_body_fields/test_tutorial001.py index fb68f28689..0ecadbb660 100644 --- a/tests/test_tutorial/test_body_fields/test_tutorial001.py +++ b/tests/test_tutorial/test_body_fields/test_tutorial001.py @@ -1,19 +1,17 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient -from ...utils import needs_py39, needs_py310 +from ...utils import needs_py310 @pytest.fixture( name="client", params=[ - "tutorial001", + "tutorial001_py39", pytest.param("tutorial001_py310", marks=needs_py310), - "tutorial001_an", - pytest.param("tutorial001_an_py39", marks=needs_py39), + "tutorial001_an_py39", pytest.param("tutorial001_an_py310", marks=needs_py310), ], ) @@ -60,31 +58,17 @@ def test_items_6(client: TestClient): def test_invalid_price(client: TestClient): response = client.put("/items/5", json={"item": {"name": "Foo", "price": -3.0}}) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "greater_than", - "loc": ["body", "item", "price"], - "msg": "Input should be greater than 0", - "input": -3.0, - "ctx": {"gt": 0.0}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "ctx": {"limit_value": 0}, - "loc": ["body", "item", "price"], - "msg": "ensure this value is greater than 0", - "type": "value_error.number.not_gt", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "greater_than", + "loc": ["body", "item", "price"], + "msg": "Input should be greater than 0", + "input": -3.0, + "ctx": {"gt": 0.0}, + } + ] + } def test_openapi_schema(client: TestClient): @@ -143,39 +127,23 @@ def test_openapi_schema(client: TestClient): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, - "description": IsDict( - { - "title": "The description of the item", - "anyOf": [ - {"maxLength": 300, "type": "string"}, - {"type": "null"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "The description of the item", - "maxLength": 300, - "type": "string", - } - ), + "description": { + "title": "The description of the item", + "anyOf": [ + {"maxLength": 300, "type": "string"}, + {"type": "null"}, + ], + }, "price": { "title": "Price", "exclusiveMinimum": 0.0, "type": "number", "description": "The price must be greater than zero", }, - "tax": IsDict( - { - "title": "Tax", - "anyOf": [{"type": "number"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Tax", "type": "number"} - ), + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, }, }, "Body_update_item_items__item_id__put": { diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial001.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial001.py index 1424055952..63c9c16d62 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial001.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial001.py @@ -1,19 +1,17 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient -from ...utils import needs_py39, needs_py310 +from ...utils import needs_py310 @pytest.fixture( name="client", params=[ - "tutorial001", + "tutorial001_py39", pytest.param("tutorial001_py310", marks=needs_py310), - "tutorial001_an", - pytest.param("tutorial001_an_py39", marks=needs_py39), + "tutorial001_an_py39", pytest.param("tutorial001_an_py310", marks=needs_py310), ], ) @@ -54,29 +52,16 @@ def test_post_no_body(client: TestClient): def test_post_id_foo(client: TestClient): response = client.put("/items/foo", json=None) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["path", "item_id"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "foo", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "int_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "foo", + } + ] + } def test_openapi_schema(client: TestClient): @@ -120,16 +105,10 @@ def test_openapi_schema(client: TestClient): }, { "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Q", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Q", "type": "string"} - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + }, "name": "q", "in": "query", }, @@ -137,19 +116,13 @@ def test_openapi_schema(client: TestClient): "requestBody": { "content": { "application/json": { - "schema": IsDict( - { - "anyOf": [ - {"$ref": "#/components/schemas/Item"}, - {"type": "null"}, - ], - "title": "Item", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"$ref": "#/components/schemas/Item"} - ) + "schema": { + "anyOf": [ + {"$ref": "#/components/schemas/Item"}, + {"type": "null"}, + ], + "title": "Item", + } } } }, @@ -164,27 +137,15 @@ def test_openapi_schema(client: TestClient): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, - "description": IsDict( - { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, "price": {"title": "Price", "type": "number"}, - "tax": IsDict( - { - "title": "Tax", - "anyOf": [{"type": "number"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Tax", "type": "number"} - ), + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial002.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial002.py new file mode 100644 index 0000000000..e98d5860fe --- /dev/null +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial002.py @@ -0,0 +1,361 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial002_py39"), + pytest.param("tutorial002_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.body_multiple_params.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_post_all(client: TestClient): + response = client.put( + "/items/5", + json={ + "item": { + "name": "Foo", + "price": 50.5, + "description": "Some Foo", + "tax": 0.1, + }, + "user": {"username": "johndoe", "full_name": "John Doe"}, + }, + ) + assert response.status_code == 200 + assert response.json() == { + "item_id": 5, + "item": { + "name": "Foo", + "price": 50.5, + "description": "Some Foo", + "tax": 0.1, + }, + "user": {"username": "johndoe", "full_name": "John Doe"}, + } + + +def test_post_required(client: TestClient): + response = client.put( + "/items/5", + json={ + "item": {"name": "Foo", "price": 50.5}, + "user": {"username": "johndoe"}, + }, + ) + assert response.status_code == 200 + assert response.json() == { + "item_id": 5, + "item": { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + }, + "user": {"username": "johndoe", "full_name": None}, + } + + +def test_post_no_body(client: TestClient): + response = client.put("/items/5", json=None) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "input": None, + "loc": [ + "body", + "item", + ], + "msg": "Field required", + "type": "missing", + }, + { + "input": None, + "loc": [ + "body", + "user", + ], + "msg": "Field required", + "type": "missing", + }, + ], + } + + +def test_post_no_item(client: TestClient): + response = client.put("/items/5", json={"user": {"username": "johndoe"}}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "input": None, + "loc": [ + "body", + "item", + ], + "msg": "Field required", + "type": "missing", + }, + ], + } + + +def test_post_no_user(client: TestClient): + response = client.put("/items/5", json={"item": {"name": "Foo", "price": 50.5}}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "input": None, + "loc": [ + "body", + "user", + ], + "msg": "Field required", + "type": "missing", + }, + ], + } + + +def test_post_missing_required_field_in_item(client: TestClient): + response = client.put( + "/items/5", json={"item": {"name": "Foo"}, "user": {"username": "johndoe"}} + ) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "input": {"name": "Foo"}, + "loc": [ + "body", + "item", + "price", + ], + "msg": "Field required", + "type": "missing", + }, + ], + } + + +def test_post_missing_required_field_in_user(client: TestClient): + response = client.put( + "/items/5", + json={"item": {"name": "Foo", "price": 50.5}, "user": {"ful_name": "John Doe"}}, + ) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "input": {"ful_name": "John Doe"}, + "loc": [ + "body", + "user", + "username", + ], + "msg": "Field required", + "type": "missing", + }, + ], + } + + +def test_post_id_foo(client: TestClient): + response = client.put( + "/items/foo", + json={ + "item": {"name": "Foo", "price": 50.5}, + "user": {"username": "johndoe"}, + }, + ) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "int_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "foo", + } + ] + } + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "info": { + "title": "FastAPI", + "version": "0.1.0", + }, + "openapi": "3.1.0", + "paths": { + "/items/{item_id}": { + "put": { + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "in": "path", + "name": "item_id", + "required": True, + "schema": { + "title": "Item Id", + "type": "integer", + }, + }, + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_update_item_items__item_id__put", + }, + }, + }, + "required": True, + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": {}, + }, + }, + "description": "Successful Response", + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError", + }, + }, + }, + "description": "Validation Error", + }, + }, + "summary": "Update Item", + }, + }, + }, + "components": { + "schemas": { + "Body_update_item_items__item_id__put": { + "properties": { + "item": { + "$ref": "#/components/schemas/Item", + }, + "user": { + "$ref": "#/components/schemas/User", + }, + }, + "required": [ + "item", + "user", + ], + "title": "Body_update_item_items__item_id__put", + "type": "object", + }, + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError", + }, + "title": "Detail", + "type": "array", + }, + }, + "title": "HTTPValidationError", + "type": "object", + }, + "Item": { + "properties": { + "name": { + "title": "Name", + "type": "string", + }, + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "price": {"title": "Price", "type": "number"}, + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, + }, + "required": [ + "name", + "price", + ], + "title": "Item", + "type": "object", + }, + "User": { + "properties": { + "username": { + "title": "Username", + "type": "string", + }, + "full_name": { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + }, + "required": [ + "username", + ], + "title": "User", + "type": "object", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "integer", + }, + ], + }, + "title": "Location", + "type": "array", + }, + "msg": { + "title": "Message", + "type": "string", + }, + "type": { + "title": "Error Type", + "type": "string", + }, + }, + "required": [ + "loc", + "msg", + "type", + ], + "title": "ValidationError", + "type": "object", + }, + }, + }, + } diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial003.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial003.py index d18ceae486..76b7ff7099 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial003.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial003.py @@ -1,19 +1,17 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient -from ...utils import needs_py39, needs_py310 +from ...utils import needs_py310 @pytest.fixture( name="client", params=[ - "tutorial003", + "tutorial003_py39", pytest.param("tutorial003_py310", marks=needs_py310), - "tutorial003_an", - pytest.param("tutorial003_an_py39", marks=needs_py39), + "tutorial003_an_py39", pytest.param("tutorial003_an_py310", marks=needs_py310), ], ) @@ -50,101 +48,55 @@ def test_post_body_valid(client: TestClient): def test_post_body_no_data(client: TestClient): response = client.put("/items/5", json=None) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "item"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "user"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "importance"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "item"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "user"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "importance"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "item"], + "msg": "Field required", + "input": None, + }, + { + "type": "missing", + "loc": ["body", "user"], + "msg": "Field required", + "input": None, + }, + { + "type": "missing", + "loc": ["body", "importance"], + "msg": "Field required", + "input": None, + }, + ] + } def test_post_body_empty_list(client: TestClient): response = client.put("/items/5", json=[]) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "item"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "user"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "importance"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "item"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "user"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "importance"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "item"], + "msg": "Field required", + "input": None, + }, + { + "type": "missing", + "loc": ["body", "user"], + "msg": "Field required", + "input": None, + }, + { + "type": "missing", + "loc": ["body", "importance"], + "msg": "Field required", + "input": None, + }, + ] + } def test_openapi_schema(client: TestClient): @@ -203,27 +155,15 @@ def test_openapi_schema(client: TestClient): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, - "description": IsDict( - { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, "price": {"title": "Price", "type": "number"}, - "tax": IsDict( - { - "title": "Tax", - "anyOf": [{"type": "number"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Tax", "type": "number"} - ), + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, }, }, "User": { @@ -232,16 +172,10 @@ def test_openapi_schema(client: TestClient): "type": "object", "properties": { "username": {"title": "Username", "type": "string"}, - "full_name": IsDict( - { - "title": "Full Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Full Name", "type": "string"} - ), + "full_name": { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, }, }, "Body_update_item_items__item_id__put": { diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial004.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial004.py new file mode 100644 index 0000000000..979c054cd0 --- /dev/null +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial004.py @@ -0,0 +1,290 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial004_py39"), + pytest.param("tutorial004_py310", marks=needs_py310), + pytest.param("tutorial004_an_py39"), + pytest.param("tutorial004_an_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.body_multiple_params.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_put_all(client: TestClient): + response = client.put( + "/items/5", + json={ + "importance": 2, + "item": {"name": "Foo", "price": 50.5}, + "user": {"username": "Dave"}, + }, + params={"q": "somequery"}, + ) + assert response.status_code == 200 + assert response.json() == { + "item_id": 5, + "importance": 2, + "item": { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + }, + "user": {"username": "Dave", "full_name": None}, + "q": "somequery", + } + + +def test_put_only_required(client: TestClient): + response = client.put( + "/items/5", + json={ + "importance": 2, + "item": {"name": "Foo", "price": 50.5}, + "user": {"username": "Dave"}, + }, + ) + assert response.status_code == 200 + assert response.json() == { + "item_id": 5, + "importance": 2, + "item": { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + }, + "user": {"username": "Dave", "full_name": None}, + } + + +def test_put_missing_body(client: TestClient): + response = client.put("/items/5") + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "input": None, + "loc": [ + "body", + "item", + ], + "msg": "Field required", + "type": "missing", + }, + { + "input": None, + "loc": [ + "body", + "user", + ], + "msg": "Field required", + "type": "missing", + }, + { + "input": None, + "loc": [ + "body", + "importance", + ], + "msg": "Field required", + "type": "missing", + }, + ], + } + + +def test_put_empty_body(client: TestClient): + response = client.put("/items/5", json={}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "item"], + "msg": "Field required", + "input": None, + }, + { + "type": "missing", + "loc": ["body", "user"], + "msg": "Field required", + "input": None, + }, + { + "type": "missing", + "loc": ["body", "importance"], + "msg": "Field required", + "input": None, + }, + ] + } + + +def test_put_invalid_importance(client: TestClient): + response = client.put( + "/items/5", + json={ + "importance": 0, + "item": {"name": "Foo", "price": 50.5}, + "user": {"username": "Dave"}, + }, + ) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "loc": ["body", "importance"], + "msg": "Input should be greater than 0", + "type": "greater_than", + "input": 0, + "ctx": {"gt": 0}, + }, + ], + } + + +def test_openapi_schema(client: TestClient): + 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": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + }, + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + }, + "name": "q", + "in": "query", + }, + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_update_item_items__item_id__put" + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "price": {"title": "Price", "type": "number"}, + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, + }, + }, + "User": { + "title": "User", + "required": ["username"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "full_name": { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + }, + }, + "Body_update_item_items__item_id__put": { + "title": "Body_update_item_items__item_id__put", + "required": ["item", "user", "importance"], + "type": "object", + "properties": { + "item": {"$ref": "#/components/schemas/Item"}, + "user": {"$ref": "#/components/schemas/User"}, + "importance": { + "title": "Importance", + "type": "integer", + "exclusiveMinimum": 0.0, + }, + }, + }, + "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"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial005.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial005.py new file mode 100644 index 0000000000..d47aa1b4f9 --- /dev/null +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial005.py @@ -0,0 +1,272 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial005_py39"), + pytest.param("tutorial005_py310", marks=needs_py310), + pytest.param("tutorial005_an_py39"), + pytest.param("tutorial005_an_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.body_multiple_params.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_post_all(client: TestClient): + response = client.put( + "/items/5", + json={ + "item": { + "name": "Foo", + "price": 50.5, + "description": "Some Foo", + "tax": 0.1, + }, + }, + ) + assert response.status_code == 200 + assert response.json() == { + "item_id": 5, + "item": { + "name": "Foo", + "price": 50.5, + "description": "Some Foo", + "tax": 0.1, + }, + } + + +def test_post_required(client: TestClient): + response = client.put( + "/items/5", + json={ + "item": {"name": "Foo", "price": 50.5}, + }, + ) + assert response.status_code == 200 + assert response.json() == { + "item_id": 5, + "item": { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + }, + } + + +def test_post_no_body(client: TestClient): + response = client.put("/items/5", json=None) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "input": None, + "loc": [ + "body", + "item", + ], + "msg": "Field required", + "type": "missing", + }, + ], + } + + +def test_post_like_not_embeded(client: TestClient): + response = client.put( + "/items/5", + json={ + "name": "Foo", + "price": 50.5, + }, + ) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "input": None, + "loc": [ + "body", + "item", + ], + "msg": "Field required", + "type": "missing", + }, + ], + } + + +def test_post_missing_required_field_in_item(client: TestClient): + response = client.put( + "/items/5", json={"item": {"name": "Foo"}, "user": {"username": "johndoe"}} + ) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "input": {"name": "Foo"}, + "loc": [ + "body", + "item", + "price", + ], + "msg": "Field required", + "type": "missing", + }, + ], + } + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "info": { + "title": "FastAPI", + "version": "0.1.0", + }, + "openapi": "3.1.0", + "paths": { + "/items/{item_id}": { + "put": { + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "in": "path", + "name": "item_id", + "required": True, + "schema": { + "title": "Item Id", + "type": "integer", + }, + }, + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_update_item_items__item_id__put", + }, + }, + }, + "required": True, + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": {}, + }, + }, + "description": "Successful Response", + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError", + }, + }, + }, + "description": "Validation Error", + }, + }, + "summary": "Update Item", + }, + }, + }, + "components": { + "schemas": { + "Body_update_item_items__item_id__put": { + "properties": { + "item": { + "$ref": "#/components/schemas/Item", + }, + }, + "required": ["item"], + "title": "Body_update_item_items__item_id__put", + "type": "object", + }, + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError", + }, + "title": "Detail", + "type": "array", + }, + }, + "title": "HTTPValidationError", + "type": "object", + }, + "Item": { + "properties": { + "name": { + "title": "Name", + "type": "string", + }, + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "price": {"title": "Price", "type": "number"}, + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, + }, + "required": [ + "name", + "price", + ], + "title": "Item", + "type": "object", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "integer", + }, + ], + }, + "title": "Location", + "type": "array", + }, + "msg": { + "title": "Message", + "type": "string", + }, + "type": { + "title": "Error Type", + "type": "string", + }, + }, + "required": [ + "loc", + "msg", + "type", + ], + "title": "ValidationError", + "type": "object", + }, + }, + }, + } diff --git a/tests/test_tutorial/test_body_nested_models/test_tutorial001_tutorial002_tutorial003.py b/tests/test_tutorial/test_body_nested_models/test_tutorial001_tutorial002_tutorial003.py new file mode 100644 index 0000000000..d452929c38 --- /dev/null +++ b/tests/test_tutorial/test_body_nested_models/test_tutorial001_tutorial002_tutorial003.py @@ -0,0 +1,251 @@ +import importlib + +import pytest +from dirty_equals import IsList +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +UNTYPED_LIST_SCHEMA = {"type": "array", "items": {}} + +LIST_OF_STR_SCHEMA = {"type": "array", "items": {"type": "string"}} + +SET_OF_STR_SCHEMA = {"type": "array", "items": {"type": "string"}, "uniqueItems": True} + + +@pytest.fixture( + name="mod_name", + params=[ + pytest.param("tutorial001_py39"), + pytest.param("tutorial001_py310", marks=needs_py310), + pytest.param("tutorial002_py39"), + pytest.param("tutorial002_py310", marks=needs_py310), + pytest.param("tutorial003_py39"), + pytest.param("tutorial003_py310", marks=needs_py310), + ], +) +def get_mod_name(request: pytest.FixtureRequest): + return request.param + + +@pytest.fixture(name="client") +def get_client(mod_name: str): + mod = importlib.import_module(f"docs_src.body_nested_models.{mod_name}") + + client = TestClient(mod.app) + return client + + +def test_put_all(client: TestClient, mod_name: str): + if mod_name.startswith("tutorial003"): + tags_expected = IsList("foo", "bar", check_order=False) + else: + tags_expected = ["foo", "bar", "foo"] + + response = client.put( + "/items/123", + json={ + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + "tags": ["foo", "bar", "foo"], + }, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "item_id": 123, + "item": { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + "tags": tags_expected, + }, + } + + +def test_put_only_required(client: TestClient): + response = client.put( + "/items/5", + json={"name": "Foo", "price": 35.4}, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "item_id": 5, + "item": { + "name": "Foo", + "description": None, + "price": 35.4, + "tax": None, + "tags": [], + }, + } + + +def test_put_empty_body(client: TestClient): + response = client.put( + "/items/5", + json={}, + ) + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["body", "name"], + "input": {}, + "msg": "Field required", + "type": "missing", + }, + { + "loc": ["body", "price"], + "input": {}, + "msg": "Field required", + "type": "missing", + }, + ] + } + + +def test_put_missing_required(client: TestClient): + response = client.put( + "/items/5", + json={"description": "A very nice Item"}, + ) + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["body", "name"], + "input": {"description": "A very nice Item"}, + "msg": "Field required", + "type": "missing", + }, + { + "loc": ["body", "price"], + "input": {"description": "A very nice Item"}, + "msg": "Field required", + "type": "missing", + }, + ] + } + + +def test_openapi_schema(client: TestClient, mod_name: str): + tags_schema = {"default": [], "title": "Tags"} + if mod_name.startswith("tutorial001"): + tags_schema.update(UNTYPED_LIST_SCHEMA) + elif mod_name.startswith("tutorial002"): + tags_schema.update(LIST_OF_STR_SCHEMA) + elif mod_name.startswith("tutorial003"): + tags_schema.update(SET_OF_STR_SCHEMA) + + 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": { + "/items/{item_id}": { + "put": { + "parameters": [ + { + "in": "path", + "name": "item_id", + "required": True, + "schema": { + "title": "Item Id", + "type": "integer", + }, + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Item", + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "properties": { + "name": { + "title": "Name", + "type": "string", + }, + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "price": { + "title": "Price", + "type": "number", + }, + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, + "tags": tags_schema, + }, + "required": [ + "name", + "price", + ], + "title": "Item", + "type": "object", + }, + "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"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_body_nested_models/test_tutorial004.py b/tests/test_tutorial/test_body_nested_models/test_tutorial004.py new file mode 100644 index 0000000000..ff9596943d --- /dev/null +++ b/tests/test_tutorial/test_body_nested_models/test_tutorial004.py @@ -0,0 +1,275 @@ +import importlib + +import pytest +from dirty_equals import IsList +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial004_py39"), + pytest.param("tutorial004_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.body_nested_models.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_put_all(client: TestClient): + response = client.put( + "/items/123", + json={ + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + "tags": ["foo", "bar", "foo"], + "image": {"url": "http://example.com/image.png", "name": "example image"}, + }, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "item_id": 123, + "item": { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + "tags": IsList("foo", "bar", check_order=False), + "image": {"url": "http://example.com/image.png", "name": "example image"}, + }, + } + + +def test_put_only_required(client: TestClient): + response = client.put( + "/items/5", + json={"name": "Foo", "price": 35.4}, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "item_id": 5, + "item": { + "name": "Foo", + "description": None, + "price": 35.4, + "tax": None, + "tags": [], + "image": None, + }, + } + + +def test_put_empty_body(client: TestClient): + response = client.put( + "/items/5", + json={}, + ) + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["body", "name"], + "input": {}, + "msg": "Field required", + "type": "missing", + }, + { + "loc": ["body", "price"], + "input": {}, + "msg": "Field required", + "type": "missing", + }, + ] + } + + +def test_put_missing_required_in_item(client: TestClient): + response = client.put( + "/items/5", + json={"description": "A very nice Item"}, + ) + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["body", "name"], + "input": {"description": "A very nice Item"}, + "msg": "Field required", + "type": "missing", + }, + { + "loc": ["body", "price"], + "input": {"description": "A very nice Item"}, + "msg": "Field required", + "type": "missing", + }, + ] + } + + +def test_put_missing_required_in_image(client: TestClient): + response = client.put( + "/items/5", + json={ + "name": "Foo", + "price": 35.4, + "image": {"url": "http://example.com/image.png"}, + }, + ) + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["body", "image", "name"], + "input": {"url": "http://example.com/image.png"}, + "msg": "Field required", + "type": "missing", + }, + ] + } + + +def test_openapi_schema(client: TestClient): + 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": { + "/items/{item_id}": { + "put": { + "parameters": [ + { + "in": "path", + "name": "item_id", + "required": True, + "schema": { + "title": "Item Id", + "type": "integer", + }, + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Item", + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Image": { + "properties": { + "url": { + "title": "Url", + "type": "string", + }, + "name": { + "title": "Name", + "type": "string", + }, + }, + "required": ["url", "name"], + "title": "Image", + "type": "object", + }, + "Item": { + "properties": { + "name": { + "title": "Name", + "type": "string", + }, + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "price": { + "title": "Price", + "type": "number", + }, + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, + "tags": { + "title": "Tags", + "default": [], + "type": "array", + "items": {"type": "string"}, + "uniqueItems": True, + }, + "image": { + "anyOf": [ + {"$ref": "#/components/schemas/Image"}, + {"type": "null"}, + ], + }, + }, + "required": [ + "name", + "price", + ], + "title": "Item", + "type": "object", + }, + "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"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_body_nested_models/test_tutorial005.py b/tests/test_tutorial/test_body_nested_models/test_tutorial005.py new file mode 100644 index 0000000000..9a07a904e6 --- /dev/null +++ b/tests/test_tutorial/test_body_nested_models/test_tutorial005.py @@ -0,0 +1,301 @@ +import importlib + +import pytest +from dirty_equals import IsList +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial005_py39"), + pytest.param("tutorial005_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.body_nested_models.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_put_all(client: TestClient): + response = client.put( + "/items/123", + json={ + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + "tags": ["foo", "bar", "foo"], + "image": {"url": "http://example.com/image.png", "name": "example image"}, + }, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "item_id": 123, + "item": { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + "tags": IsList("foo", "bar", check_order=False), + "image": {"url": "http://example.com/image.png", "name": "example image"}, + }, + } + + +def test_put_only_required(client: TestClient): + response = client.put( + "/items/5", + json={"name": "Foo", "price": 35.4}, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "item_id": 5, + "item": { + "name": "Foo", + "description": None, + "price": 35.4, + "tax": None, + "tags": [], + "image": None, + }, + } + + +def test_put_empty_body(client: TestClient): + response = client.put( + "/items/5", + json={}, + ) + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["body", "name"], + "input": {}, + "msg": "Field required", + "type": "missing", + }, + { + "loc": ["body", "price"], + "input": {}, + "msg": "Field required", + "type": "missing", + }, + ] + } + + +def test_put_missing_required_in_item(client: TestClient): + response = client.put( + "/items/5", + json={"description": "A very nice Item"}, + ) + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["body", "name"], + "input": {"description": "A very nice Item"}, + "msg": "Field required", + "type": "missing", + }, + { + "loc": ["body", "price"], + "input": {"description": "A very nice Item"}, + "msg": "Field required", + "type": "missing", + }, + ] + } + + +def test_put_missing_required_in_image(client: TestClient): + response = client.put( + "/items/5", + json={ + "name": "Foo", + "price": 35.4, + "image": {"url": "http://example.com/image.png"}, + }, + ) + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["body", "image", "name"], + "input": {"url": "http://example.com/image.png"}, + "msg": "Field required", + "type": "missing", + }, + ] + } + + +def test_put_wrong_url(client: TestClient): + response = client.put( + "/items/5", + json={ + "name": "Foo", + "price": 35.4, + "image": {"url": "not a valid url", "name": "example image"}, + }, + ) + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["body", "image", "url"], + "input": "not a valid url", + "msg": "Input should be a valid URL, relative URL without a base", + "type": "url_parsing", + "ctx": {"error": "relative URL without a base"}, + }, + ] + } + + +def test_openapi_schema(client: TestClient): + 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": { + "/items/{item_id}": { + "put": { + "parameters": [ + { + "in": "path", + "name": "item_id", + "required": True, + "schema": { + "title": "Item Id", + "type": "integer", + }, + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Item", + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Image": { + "properties": { + "url": { + "title": "Url", + "type": "string", + "format": "uri", + "maxLength": 2083, + "minLength": 1, + }, + "name": { + "title": "Name", + "type": "string", + }, + }, + "required": ["url", "name"], + "title": "Image", + "type": "object", + }, + "Item": { + "properties": { + "name": { + "title": "Name", + "type": "string", + }, + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "price": { + "title": "Price", + "type": "number", + }, + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, + "tags": { + "title": "Tags", + "default": [], + "type": "array", + "items": {"type": "string"}, + "uniqueItems": True, + }, + "image": { + "anyOf": [ + {"$ref": "#/components/schemas/Image"}, + {"type": "null"}, + ], + }, + }, + "required": [ + "name", + "price", + ], + "title": "Item", + "type": "object", + }, + "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"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_body_nested_models/test_tutorial006.py b/tests/test_tutorial/test_body_nested_models/test_tutorial006.py new file mode 100644 index 0000000000..088177cb95 --- /dev/null +++ b/tests/test_tutorial/test_body_nested_models/test_tutorial006.py @@ -0,0 +1,269 @@ +import importlib + +import pytest +from dirty_equals import IsList +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial006_py39"), + pytest.param("tutorial006_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.body_nested_models.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_put_all(client: TestClient): + response = client.put( + "/items/123", + json={ + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + "tags": ["foo", "bar", "foo"], + "images": [ + {"url": "http://example.com/image.png", "name": "example image"} + ], + }, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "item_id": 123, + "item": { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + "tags": IsList("foo", "bar", check_order=False), + "images": [ + {"url": "http://example.com/image.png", "name": "example image"} + ], + }, + } + + +def test_put_only_required(client: TestClient): + response = client.put( + "/items/5", + json={"name": "Foo", "price": 35.4}, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "item_id": 5, + "item": { + "name": "Foo", + "description": None, + "price": 35.4, + "tax": None, + "tags": [], + "images": None, + }, + } + + +def test_put_empty_body(client: TestClient): + response = client.put( + "/items/5", + json={}, + ) + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["body", "name"], + "input": {}, + "msg": "Field required", + "type": "missing", + }, + { + "loc": ["body", "price"], + "input": {}, + "msg": "Field required", + "type": "missing", + }, + ] + } + + +def test_put_images_not_list(client: TestClient): + response = client.put( + "/items/5", + json={ + "name": "Foo", + "price": 35.4, + "images": {"url": "http://example.com/image.png", "name": "example image"}, + }, + ) + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["body", "images"], + "input": { + "url": "http://example.com/image.png", + "name": "example image", + }, + "msg": "Input should be a valid list", + "type": "list_type", + }, + ] + } + + +def test_openapi_schema(client: TestClient): + 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": { + "/items/{item_id}": { + "put": { + "parameters": [ + { + "in": "path", + "name": "item_id", + "required": True, + "schema": { + "title": "Item Id", + "type": "integer", + }, + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Item", + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Image": { + "properties": { + "url": { + "title": "Url", + "type": "string", + "format": "uri", + "maxLength": 2083, + "minLength": 1, + }, + "name": { + "title": "Name", + "type": "string", + }, + }, + "required": ["url", "name"], + "title": "Image", + "type": "object", + }, + "Item": { + "properties": { + "name": { + "title": "Name", + "type": "string", + }, + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "price": { + "title": "Price", + "type": "number", + }, + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, + "tags": { + "title": "Tags", + "default": [], + "type": "array", + "items": {"type": "string"}, + "uniqueItems": True, + }, + "images": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/Image", + }, + "type": "array", + }, + { + "type": "null", + }, + ], + "title": "Images", + }, + }, + "required": [ + "name", + "price", + ], + "title": "Item", + "type": "object", + }, + "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"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_body_nested_models/test_tutorial007.py b/tests/test_tutorial/test_body_nested_models/test_tutorial007.py new file mode 100644 index 0000000000..a302819505 --- /dev/null +++ b/tests/test_tutorial/test_body_nested_models/test_tutorial007.py @@ -0,0 +1,344 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial007_py39"), + pytest.param("tutorial007_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.body_nested_models.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_post_all(client: TestClient): + data = { + "name": "Special Offer", + "description": "This is a special offer", + "price": 38.6, + "items": [ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + "tags": ["foo"], + "images": [ + { + "url": "http://example.com/image.png", + "name": "example image", + } + ], + } + ], + } + + response = client.post( + "/offers/", + json=data, + ) + assert response.status_code == 200, response.text + assert response.json() == data + + +def test_put_only_required(client: TestClient): + response = client.post( + "/offers/", + json={ + "name": "Special Offer", + "price": 38.6, + "items": [ + { + "name": "Foo", + "price": 35.4, + "images": [ + { + "url": "http://example.com/image.png", + "name": "example image", + } + ], + } + ], + }, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "name": "Special Offer", + "description": None, + "price": 38.6, + "items": [ + { + "name": "Foo", + "description": None, + "price": 35.4, + "tax": None, + "tags": [], + "images": [ + { + "url": "http://example.com/image.png", + "name": "example image", + } + ], + } + ], + } + + +def test_put_empty_body(client: TestClient): + response = client.post( + "/offers/", + json={}, + ) + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["body", "name"], + "input": {}, + "msg": "Field required", + "type": "missing", + }, + { + "loc": ["body", "price"], + "input": {}, + "msg": "Field required", + "type": "missing", + }, + { + "loc": ["body", "items"], + "input": {}, + "msg": "Field required", + "type": "missing", + }, + ] + } + + +def test_put_missing_required_in_items(client: TestClient): + response = client.post( + "/offers/", + json={ + "name": "Special Offer", + "price": 38.6, + "items": [{}], + }, + ) + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["body", "items", 0, "name"], + "input": {}, + "msg": "Field required", + "type": "missing", + }, + { + "loc": ["body", "items", 0, "price"], + "input": {}, + "msg": "Field required", + "type": "missing", + }, + ] + } + + +def test_put_missing_required_in_images(client: TestClient): + response = client.post( + "/offers/", + json={ + "name": "Special Offer", + "price": 38.6, + "items": [ + {"name": "Foo", "price": 35.4, "images": [{}]}, + ], + }, + ) + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["body", "items", 0, "images", 0, "url"], + "input": {}, + "msg": "Field required", + "type": "missing", + }, + { + "loc": ["body", "items", 0, "images", 0, "name"], + "input": {}, + "msg": "Field required", + "type": "missing", + }, + ] + } + + +def test_openapi_schema(client: TestClient): + 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": { + "/offers/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create Offer", + "operationId": "create_offer_offers__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Offer", + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Image": { + "properties": { + "url": { + "title": "Url", + "type": "string", + "format": "uri", + "maxLength": 2083, + "minLength": 1, + }, + "name": { + "title": "Name", + "type": "string", + }, + }, + "required": ["url", "name"], + "title": "Image", + "type": "object", + }, + "Item": { + "properties": { + "name": { + "title": "Name", + "type": "string", + }, + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "price": { + "title": "Price", + "type": "number", + }, + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, + "tags": { + "title": "Tags", + "default": [], + "type": "array", + "items": {"type": "string"}, + "uniqueItems": True, + }, + "images": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/Image", + }, + "type": "array", + }, + { + "type": "null", + }, + ], + "title": "Images", + }, + }, + "required": [ + "name", + "price", + ], + "title": "Item", + "type": "object", + }, + "Offer": { + "properties": { + "name": { + "title": "Name", + "type": "string", + }, + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "price": { + "title": "Price", + "type": "number", + }, + "items": { + "title": "Items", + "type": "array", + "items": {"$ref": "#/components/schemas/Item"}, + }, + }, + "required": ["name", "price", "items"], + "title": "Offer", + "type": "object", + }, + "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"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_body_nested_models/test_tutorial008.py b/tests/test_tutorial/test_body_nested_models/test_tutorial008.py new file mode 100644 index 0000000000..32eb8ee75c --- /dev/null +++ b/tests/test_tutorial/test_body_nested_models/test_tutorial008.py @@ -0,0 +1,157 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial008_py39"), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.body_nested_models.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_post_body(client: TestClient): + data = [ + {"url": "http://example.com/", "name": "Example"}, + {"url": "http://fastapi.tiangolo.com/", "name": "FastAPI"}, + ] + response = client.post("/images/multiple", json=data) + assert response.status_code == 200, response.text + assert response.json() == data + + +def test_post_invalid_list_item(client: TestClient): + data = [{"url": "not a valid url", "name": "Example"}] + response = client.post("/images/multiple", json=data) + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["body", 0, "url"], + "input": "not a valid url", + "msg": "Input should be a valid URL, relative URL without a base", + "type": "url_parsing", + "ctx": {"error": "relative URL without a base"}, + }, + ] + } + + +def test_post_not_a_list(client: TestClient): + data = {"url": "http://example.com/", "name": "Example"} + response = client.post("/images/multiple", json=data) + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["body"], + "input": { + "name": "Example", + "url": "http://example.com/", + }, + "msg": "Input should be a valid list", + "type": "list_type", + } + ] + } + + +def test_openapi_schema(client: TestClient): + 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": { + "/images/multiple/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create Multiple Images", + "operationId": "create_multiple_images_images_multiple__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "title": "Images", + "type": "array", + "items": {"$ref": "#/components/schemas/Image"}, + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Image": { + "properties": { + "url": { + "title": "Url", + "type": "string", + "format": "uri", + "maxLength": 2083, + "minLength": 1, + }, + "name": { + "title": "Name", + "type": "string", + }, + }, + "required": ["url", "name"], + "title": "Image", + "type": "object", + }, + "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"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_body_nested_models/test_tutorial009.py b/tests/test_tutorial/test_body_nested_models/test_tutorial009.py index 38ba3c8875..f2e56d40fb 100644 --- a/tests/test_tutorial/test_body_nested_models/test_tutorial009.py +++ b/tests/test_tutorial/test_body_nested_models/test_tutorial009.py @@ -1,17 +1,13 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient -from ...utils import needs_py39 - @pytest.fixture( name="client", params=[ - "tutorial009", - pytest.param("tutorial009_py39", marks=needs_py39), + "tutorial009_py39", ], ) def get_client(request: pytest.FixtureRequest): @@ -32,29 +28,16 @@ def test_post_invalid_body(client: TestClient): data = {"foo": 2.2, "3": 3.3} response = client.post("/index-weights/", json=data) assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["body", "foo", "[key]"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "foo", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "__key__"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "int_parsing", + "loc": ["body", "foo", "[key]"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "foo", + } + ] + } def test_openapi_schema(client: TestClient): diff --git a/tests/test_tutorial/test_body_updates/test_tutorial001.py b/tests/test_tutorial/test_body_updates/test_tutorial001.py index f874dc9bd6..0401eb7d0d 100644 --- a/tests/test_tutorial/test_body_updates/test_tutorial001.py +++ b/tests/test_tutorial/test_body_updates/test_tutorial001.py @@ -3,15 +3,14 @@ import importlib import pytest from fastapi.testclient import TestClient -from ...utils import needs_py39, needs_py310, needs_pydanticv1, needs_pydanticv2 +from ...utils import needs_py310 @pytest.fixture( name="client", params=[ - "tutorial001", + "tutorial001_py39", pytest.param("tutorial001_py310", marks=needs_py310), - pytest.param("tutorial001_py39", marks=needs_py39), ], ) def get_client(request: pytest.FixtureRequest): @@ -46,7 +45,6 @@ def test_put(client: TestClient): } -@needs_pydanticv2 def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text @@ -186,137 +184,3 @@ def test_openapi_schema(client: TestClient): } }, } - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -def test_openapi_schema_pv1(client: TestClient): - 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": { - "/items/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Item", - "operationId": "read_item_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - }, - "put": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - "required": True, - }, - }, - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": {"title": "Description", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "tax": {"title": "Tax", "type": "number", "default": 10.5}, - "tags": { - "title": "Tags", - "type": "array", - "items": {"type": "string"}, - "default": [], - }, - }, - }, - "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"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_body_updates/test_tutorial002.py b/tests/test_tutorial/test_body_updates/test_tutorial002.py new file mode 100644 index 0000000000..466e6af8fd --- /dev/null +++ b/tests/test_tutorial/test_body_updates/test_tutorial002.py @@ -0,0 +1,207 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial002_py39"), + pytest.param("tutorial002_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.body_updates.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_get(client: TestClient): + response = client.get("/items/baz") + assert response.status_code == 200, response.text + assert response.json() == { + "name": "Baz", + "description": None, + "price": 50.2, + "tax": 10.5, + "tags": [], + } + + +def test_patch_all(client: TestClient): + response = client.patch( + "/items/foo", + json={ + "name": "Fooz", + "description": "Item description", + "price": 3, + "tax": 10.5, + "tags": ["tag1", "tag2"], + }, + ) + assert response.json() == { + "name": "Fooz", + "description": "Item description", + "price": 3, + "tax": 10.5, + "tags": ["tag1", "tag2"], + } + + +def test_patch_name(client: TestClient): + response = client.patch( + "/items/bar", + json={"name": "Barz"}, + ) + assert response.json() == { + "name": "Barz", + "description": "The bartenders", + "price": 62, + "tax": 20.2, + "tags": [], + } + + +def test_openapi_schema(client: TestClient): + 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": { + "/items/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + }, + "patch": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__patch", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + }, + } + }, + "components": { + "schemas": { + "Item": { + "type": "object", + "title": "Item", + "properties": { + "name": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Name", + }, + "description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Description", + }, + "price": { + "anyOf": [{"type": "number"}, {"type": "null"}], + "title": "Price", + }, + "tax": {"title": "Tax", "type": "number", "default": 10.5}, + "tags": { + "title": "Tags", + "type": "array", + "items": {"type": "string"}, + "default": [], + }, + }, + }, + "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"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_conditional_openapi/test_tutorial001.py b/tests/test_tutorial/test_conditional_openapi/test_tutorial001.py index b098f259c2..ddc282d85e 100644 --- a/tests/test_tutorial/test_conditional_openapi/test_tutorial001.py +++ b/tests/test_tutorial/test_conditional_openapi/test_tutorial001.py @@ -2,19 +2,16 @@ import importlib from fastapi.testclient import TestClient -from ...utils import needs_pydanticv2 - def get_client() -> TestClient: - from docs_src.conditional_openapi import tutorial001 + from docs_src.conditional_openapi import tutorial001_py39 - importlib.reload(tutorial001) + importlib.reload(tutorial001_py39) - client = TestClient(tutorial001.app) + client = TestClient(tutorial001_py39.app) return client -@needs_pydanticv2 def test_disable_openapi(monkeypatch): monkeypatch.setenv("OPENAPI_URL", "") # Load the client after setting the env var @@ -27,7 +24,6 @@ def test_disable_openapi(monkeypatch): assert response.status_code == 404, response.text -@needs_pydanticv2 def test_root(): client = get_client() response = client.get("/") @@ -35,7 +31,6 @@ def test_root(): assert response.json() == {"message": "Hello World"} -@needs_pydanticv2 def test_default_openapi(): client = get_client() response = client.get("/docs") diff --git a/tests/test_tutorial/test_configure_swagger_ui/test_tutorial001.py b/tests/test_tutorial/test_configure_swagger_ui/test_tutorial001.py index a04dba2197..1fa9419a76 100644 --- a/tests/test_tutorial/test_configure_swagger_ui/test_tutorial001.py +++ b/tests/test_tutorial/test_configure_swagger_ui/test_tutorial001.py @@ -1,6 +1,6 @@ from fastapi.testclient import TestClient -from docs_src.configure_swagger_ui.tutorial001 import app +from docs_src.configure_swagger_ui.tutorial001_py39 import app client = TestClient(app) diff --git a/tests/test_tutorial/test_configure_swagger_ui/test_tutorial002.py b/tests/test_tutorial/test_configure_swagger_ui/test_tutorial002.py index ea56b6f21c..c218cc858c 100644 --- a/tests/test_tutorial/test_configure_swagger_ui/test_tutorial002.py +++ b/tests/test_tutorial/test_configure_swagger_ui/test_tutorial002.py @@ -1,6 +1,6 @@ from fastapi.testclient import TestClient -from docs_src.configure_swagger_ui.tutorial002 import app +from docs_src.configure_swagger_ui.tutorial002_py39 import app client = TestClient(app) diff --git a/tests/test_tutorial/test_configure_swagger_ui/test_tutorial003.py b/tests/test_tutorial/test_configure_swagger_ui/test_tutorial003.py index 926bbb14f0..8b73685499 100644 --- a/tests/test_tutorial/test_configure_swagger_ui/test_tutorial003.py +++ b/tests/test_tutorial/test_configure_swagger_ui/test_tutorial003.py @@ -1,6 +1,6 @@ from fastapi.testclient import TestClient -from docs_src.configure_swagger_ui.tutorial003 import app +from docs_src.configure_swagger_ui.tutorial003_py39 import app client = TestClient(app) diff --git a/tests/test_tutorial/test_cookie_param_models/test_tutorial001.py b/tests/test_tutorial/test_cookie_param_models/test_tutorial001.py index 60643185a4..ac8e7bdae1 100644 --- a/tests/test_tutorial/test_cookie_param_models/test_tutorial001.py +++ b/tests/test_tutorial/test_cookie_param_models/test_tutorial001.py @@ -1,20 +1,18 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from inline_snapshot import snapshot -from tests.utils import needs_py39, needs_py310 +from tests.utils import needs_py310 @pytest.fixture( name="client", params=[ - "tutorial001", + "tutorial001_py39", pytest.param("tutorial001_py310", marks=needs_py310), - "tutorial001_an", - pytest.param("tutorial001_an_py39", marks=needs_py39), + "tutorial001_an_py39", pytest.param("tutorial001_an_py310", marks=needs_py310), ], ) @@ -55,30 +53,16 @@ def test_cookie_param_model_invalid(client: TestClient): response = client.get("/items/") assert response.status_code == 422 assert response.json() == snapshot( - IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["cookie", "session_id"], - "msg": "Field required", - "input": {}, - } - ] - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "type": "value_error.missing", - "loc": ["cookie", "session_id"], - "msg": "field required", - } - ] - } - ) + { + "detail": [ + { + "type": "missing", + "loc": ["cookie", "session_id"], + "msg": "Field required", + "input": {}, + } + ] + } ) @@ -116,37 +100,19 @@ def test_openapi_schema(client: TestClient): "name": "fatebook_tracker", "in": "cookie", "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Fatebook Tracker", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "string", - "title": "Fatebook Tracker", - } - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Fatebook Tracker", + }, }, { "name": "googall_tracker", "in": "cookie", "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Googall Tracker", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "string", - "title": "Googall Tracker", - } - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Googall Tracker", + }, }, ], "responses": { diff --git a/tests/test_tutorial/test_cookie_param_models/test_tutorial002.py b/tests/test_tutorial/test_cookie_param_models/test_tutorial002.py index cef6f66309..d7c3d15f1b 100644 --- a/tests/test_tutorial/test_cookie_param_models/test_tutorial002.py +++ b/tests/test_tutorial/test_cookie_param_models/test_tutorial002.py @@ -1,32 +1,19 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from inline_snapshot import snapshot -from tests.utils import ( - needs_py39, - needs_py310, - needs_pydanticv1, - needs_pydanticv2, - pydantic_snapshot, -) +from tests.utils import needs_py310 @pytest.fixture( name="client", params=[ - pytest.param("tutorial002", marks=needs_pydanticv2), - pytest.param("tutorial002_py310", marks=[needs_py310, needs_pydanticv2]), - pytest.param("tutorial002_an", marks=needs_pydanticv2), - pytest.param("tutorial002_an_py39", marks=[needs_py39, needs_pydanticv2]), - pytest.param("tutorial002_an_py310", marks=[needs_py310, needs_pydanticv2]), - pytest.param("tutorial002_pv1", marks=[needs_pydanticv1, needs_pydanticv1]), - pytest.param("tutorial002_pv1_py310", marks=[needs_py310, needs_pydanticv1]), - pytest.param("tutorial002_pv1_an", marks=[needs_pydanticv1]), - pytest.param("tutorial002_pv1_an_py39", marks=[needs_py39, needs_pydanticv1]), - pytest.param("tutorial002_pv1_an_py310", marks=[needs_py310, needs_pydanticv1]), + pytest.param("tutorial002_py39"), + pytest.param("tutorial002_py310", marks=[needs_py310]), + pytest.param("tutorial002_an_py39"), + pytest.param("tutorial002_an_py310", marks=[needs_py310]), ], ) def get_client(request: pytest.FixtureRequest): @@ -65,31 +52,16 @@ def test_cookie_param_model_defaults(client: TestClient): def test_cookie_param_model_invalid(client: TestClient): response = client.get("/items/") assert response.status_code == 422 - assert response.json() == pydantic_snapshot( - v2=snapshot( + assert response.json() == { + "detail": [ { - "detail": [ - { - "type": "missing", - "loc": ["cookie", "session_id"], - "msg": "Field required", - "input": {}, - } - ] + "type": "missing", + "loc": ["cookie", "session_id"], + "msg": "Field required", + "input": {}, } - ), - v1=snapshot( - { - "detail": [ - { - "type": "value_error.missing", - "loc": ["cookie", "session_id"], - "msg": "field required", - } - ] - } - ), - ) + ] + } def test_cookie_param_model_extra(client: TestClient): @@ -99,30 +71,16 @@ def test_cookie_param_model_extra(client: TestClient): response = c.get("/items/") assert response.status_code == 422 assert response.json() == snapshot( - IsDict( - { - "detail": [ - { - "type": "extra_forbidden", - "loc": ["cookie", "extra"], - "msg": "Extra inputs are not permitted", - "input": "track-me-here-too", - } - ] - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "type": "value_error.extra", - "loc": ["cookie", "extra"], - "msg": "extra fields not permitted", - } - ] - } - ) + { + "detail": [ + { + "type": "extra_forbidden", + "loc": ["cookie", "extra"], + "msg": "Extra inputs are not permitted", + "input": "track-me-here-too", + } + ] + } ) @@ -149,42 +107,22 @@ def test_openapi_schema(client: TestClient): "name": "fatebook_tracker", "in": "cookie", "required": False, - "schema": pydantic_snapshot( - v2=snapshot( - { - "anyOf": [ - {"type": "string"}, - {"type": "null"}, - ], - "title": "Fatebook Tracker", - } - ), - v1=snapshot( - # TODO: remove when deprecating Pydantic v1 - { - "type": "string", - "title": "Fatebook Tracker", - } - ), - ), + "schema": { + "anyOf": [ + {"type": "string"}, + {"type": "null"}, + ], + "title": "Fatebook Tracker", + }, }, { "name": "googall_tracker", "in": "cookie", "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Googall Tracker", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "string", - "title": "Googall Tracker", - } - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Googall Tracker", + }, }, ], "responses": { diff --git a/tests/test_tutorial/test_cookie_params/test_tutorial001.py b/tests/test_tutorial/test_cookie_params/test_tutorial001.py index 90e8dfd37c..9b47cbc67a 100644 --- a/tests/test_tutorial/test_cookie_params/test_tutorial001.py +++ b/tests/test_tutorial/test_cookie_params/test_tutorial001.py @@ -2,19 +2,17 @@ import importlib from types import ModuleType import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient -from ...utils import needs_py39, needs_py310 +from ...utils import needs_py310 @pytest.fixture( name="mod", params=[ - "tutorial001", + "tutorial001_py39", pytest.param("tutorial001_py310", marks=needs_py310), - "tutorial001_an", - pytest.param("tutorial001_an_py39", marks=needs_py39), + "tutorial001_an_py39", pytest.param("tutorial001_an_py310", marks=needs_py310), ], ) @@ -76,16 +74,10 @@ def test_openapi_schema(mod: ModuleType): "parameters": [ { "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Ads Id", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Ads Id", "type": "string"} - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Ads Id", + }, "name": "ads_id", "in": "cookie", } diff --git a/tests/test_tutorial/test_cors/test_tutorial001.py b/tests/test_tutorial/test_cors/test_tutorial001.py index f62c9df4f9..6a733693ae 100644 --- a/tests/test_tutorial/test_cors/test_tutorial001.py +++ b/tests/test_tutorial/test_cors/test_tutorial001.py @@ -1,6 +1,6 @@ from fastapi.testclient import TestClient -from docs_src.cors.tutorial001 import app +from docs_src.cors.tutorial001_py39 import app def test_cors(): diff --git a/tests/test_tutorial/test_custom_docs_ui/test_tutorial001.py b/tests/test_tutorial/test_custom_docs_ui/test_tutorial001.py index cb8e8c2248..1816e5d975 100644 --- a/tests/test_tutorial/test_custom_docs_ui/test_tutorial001.py +++ b/tests/test_tutorial/test_custom_docs_ui/test_tutorial001.py @@ -10,7 +10,7 @@ def client(): static_dir: Path = Path(os.getcwd()) / "static" print(static_dir) static_dir.mkdir(exist_ok=True) - from docs_src.custom_docs_ui.tutorial001 import app + from docs_src.custom_docs_ui.tutorial001_py39 import app with TestClient(app) as client: yield client diff --git a/tests/test_tutorial/test_custom_docs_ui/test_tutorial002.py b/tests/test_tutorial/test_custom_docs_ui/test_tutorial002.py index 712618807c..e8b7eb7aa3 100644 --- a/tests/test_tutorial/test_custom_docs_ui/test_tutorial002.py +++ b/tests/test_tutorial/test_custom_docs_ui/test_tutorial002.py @@ -10,7 +10,7 @@ def client(): static_dir: Path = Path(os.getcwd()) / "static" print(static_dir) static_dir.mkdir(exist_ok=True) - from docs_src.custom_docs_ui.tutorial002 import app + from docs_src.custom_docs_ui.tutorial002_py39 import app with TestClient(app) as client: yield client diff --git a/tests/test_tutorial/test_custom_request_and_route/test_tutorial001.py b/tests/test_tutorial/test_custom_request_and_route/test_tutorial001.py index e6da630e88..b4ea537846 100644 --- a/tests/test_tutorial/test_custom_request_and_route/test_tutorial001.py +++ b/tests/test_tutorial/test_custom_request_and_route/test_tutorial001.py @@ -1,23 +1,36 @@ import gzip +import importlib import json import pytest from fastapi import Request from fastapi.testclient import TestClient -from docs_src.custom_request_and_route.tutorial001 import app +from tests.utils import needs_py310 -@app.get("/check-class") -async def check_gzip_request(request: Request): - return {"request_class": type(request).__name__} +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial001_py39"), + pytest.param("tutorial001_py310", marks=needs_py310), + pytest.param("tutorial001_an_py39"), + pytest.param("tutorial001_an_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.custom_request_and_route.{request.param}") + @mod.app.get("/check-class") + async def check_gzip_request(request: Request): + return {"request_class": type(request).__name__} -client = TestClient(app) + client = TestClient(mod.app) + return client @pytest.mark.parametrize("compress", [True, False]) -def test_gzip_request(compress): +def test_gzip_request(client: TestClient, compress): n = 1000 headers = {} body = [1] * n @@ -30,6 +43,6 @@ def test_gzip_request(compress): assert response.json() == {"sum": n} -def test_request_class(): +def test_request_class(client: TestClient): response = client.get("/check-class") assert response.json() == {"request_class": "GzipRequest"} diff --git a/tests/test_tutorial/test_custom_request_and_route/test_tutorial002.py b/tests/test_tutorial/test_custom_request_and_route/test_tutorial002.py index 647f1c5ddf..a9c7ae638b 100644 --- a/tests/test_tutorial/test_custom_request_and_route/test_tutorial002.py +++ b/tests/test_tutorial/test_custom_request_and_route/test_tutorial002.py @@ -1,46 +1,46 @@ -from dirty_equals import IsDict, IsOneOf +import importlib + +import pytest +from dirty_equals import IsOneOf from fastapi.testclient import TestClient -from docs_src.custom_request_and_route.tutorial002 import app - -client = TestClient(app) +from tests.utils import needs_py310 -def test_endpoint_works(): +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial002_py39"), + pytest.param("tutorial002_py310", marks=needs_py310), + pytest.param("tutorial002_an_py39"), + pytest.param("tutorial002_an_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.custom_request_and_route.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_endpoint_works(client: TestClient): response = client.post("/", json=[1, 2, 3]) assert response.json() == 6 -def test_exception_handler_body_access(): +def test_exception_handler_body_access(client: TestClient): response = client.post("/", json={"numbers": [1, 2, 3]}) - assert response.json() == IsDict( - { - "detail": { - "errors": [ - { - "type": "list_type", - "loc": ["body"], - "msg": "Input should be a valid list", - "input": {"numbers": [1, 2, 3]}, - } - ], - # httpx 0.28.0 switches to compact JSON https://github.com/encode/httpx/issues/3363 - "body": IsOneOf('{"numbers": [1, 2, 3]}', '{"numbers":[1,2,3]}'), - } + assert response.json() == { + "detail": { + "errors": [ + { + "type": "list_type", + "loc": ["body"], + "msg": "Input should be a valid list", + "input": {"numbers": [1, 2, 3]}, + } + ], + # httpx 0.28.0 switches to compact JSON https://github.com/encode/httpx/issues/3363 + "body": IsOneOf('{"numbers": [1, 2, 3]}', '{"numbers":[1,2,3]}'), } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": { - # httpx 0.28.0 switches to compact JSON https://github.com/encode/httpx/issues/3363 - "body": IsOneOf('{"numbers": [1, 2, 3]}', '{"numbers":[1,2,3]}'), - "errors": [ - { - "loc": ["body"], - "msg": "value is not a valid list", - "type": "type_error.list", - } - ], - } - } - ) + } diff --git a/tests/test_tutorial/test_custom_request_and_route/test_tutorial003.py b/tests/test_tutorial/test_custom_request_and_route/test_tutorial003.py index db5dad7cf6..6cad7bd3f0 100644 --- a/tests/test_tutorial/test_custom_request_and_route/test_tutorial003.py +++ b/tests/test_tutorial/test_custom_request_and_route/test_tutorial003.py @@ -1,17 +1,32 @@ +import importlib + +import pytest from fastapi.testclient import TestClient -from docs_src.custom_request_and_route.tutorial003 import app - -client = TestClient(app) +from tests.utils import needs_py310 -def test_get(): +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial003_py39"), + pytest.param("tutorial003_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.custom_request_and_route.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_get(client: TestClient): response = client.get("/") assert response.json() == {"message": "Not timed"} assert "X-Response-Time" not in response.headers -def test_get_timed(): +def test_get_timed(client: TestClient): response = client.get("/timed") assert response.json() == {"message": "It's the time of my life"} assert "X-Response-Time" in response.headers diff --git a/tests/test_tutorial/test_custom_response/test_tutorial001.py b/tests/test_tutorial/test_custom_response/test_tutorial001.py index fc83624673..f1d2accef2 100644 --- a/tests/test_tutorial/test_custom_response/test_tutorial001.py +++ b/tests/test_tutorial/test_custom_response/test_tutorial001.py @@ -1,17 +1,29 @@ +import importlib + +import pytest from fastapi.testclient import TestClient -from docs_src.custom_response.tutorial001 import app -client = TestClient(app) +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial001_py39"), + pytest.param("tutorial010_py39"), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.custom_response.{request.param}") + client = TestClient(mod.app) + return client -def test_get_custom_response(): +def test_get_custom_response(client: TestClient): response = client.get("/items/") assert response.status_code == 200, response.text assert response.json() == [{"item_id": "Foo"}] -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { diff --git a/tests/test_tutorial/test_custom_response/test_tutorial001b.py b/tests/test_tutorial/test_custom_response/test_tutorial001b.py index 91e5c501e3..3337f47d5f 100644 --- a/tests/test_tutorial/test_custom_response/test_tutorial001b.py +++ b/tests/test_tutorial/test_custom_response/test_tutorial001b.py @@ -1,6 +1,6 @@ from fastapi.testclient import TestClient -from docs_src.custom_response.tutorial001b import app +from docs_src.custom_response.tutorial001b_py39 import app client = TestClient(app) diff --git a/tests/test_tutorial/test_custom_response/test_tutorial002_tutorial003_tutorial004.py b/tests/test_tutorial/test_custom_response/test_tutorial002_tutorial003_tutorial004.py new file mode 100644 index 0000000000..22e2e02540 --- /dev/null +++ b/tests/test_tutorial/test_custom_response/test_tutorial002_tutorial003_tutorial004.py @@ -0,0 +1,68 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + + +@pytest.fixture( + name="mod_name", + params=[ + pytest.param("tutorial002_py39"), + pytest.param("tutorial003_py39"), + pytest.param("tutorial004_py39"), + ], +) +def get_mod_name(request: pytest.FixtureRequest) -> str: + return request.param + + +@pytest.fixture(name="client") +def get_client(mod_name: str) -> TestClient: + mod = importlib.import_module(f"docs_src.custom_response.{mod_name}") + return TestClient(mod.app) + + +html_contents = """ + <html> + <head> + <title>Some HTML in here + + +

Look ma! HTML!

+ + + """ + + +def test_get_custom_response(client: TestClient): + response = client.get("/items/") + assert response.status_code == 200, response.text + assert response.text == html_contents + + +def test_openapi_schema(client: TestClient, mod_name: str): + if mod_name.startswith("tutorial003"): + response_content = {"application/json": {"schema": {}}} + else: + response_content = {"text/html": {"schema": {"type": "string"}}} + + 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": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": response_content, + } + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + } + } + }, + } diff --git a/tests/test_tutorial/test_custom_response/test_tutorial005.py b/tests/test_tutorial/test_custom_response/test_tutorial005.py index 889bf3e929..fea105c28e 100644 --- a/tests/test_tutorial/test_custom_response/test_tutorial005.py +++ b/tests/test_tutorial/test_custom_response/test_tutorial005.py @@ -1,6 +1,6 @@ from fastapi.testclient import TestClient -from docs_src.custom_response.tutorial005 import app +from docs_src.custom_response.tutorial005_py39 import app client = TestClient(app) diff --git a/tests/test_tutorial/test_custom_response/test_tutorial006.py b/tests/test_tutorial/test_custom_response/test_tutorial006.py index 2d0a2cd3f6..e9e6ca1086 100644 --- a/tests/test_tutorial/test_custom_response/test_tutorial006.py +++ b/tests/test_tutorial/test_custom_response/test_tutorial006.py @@ -1,6 +1,6 @@ from fastapi.testclient import TestClient -from docs_src.custom_response.tutorial006 import app +from docs_src.custom_response.tutorial006_py39 import app client = TestClient(app) diff --git a/tests/test_tutorial/test_custom_response/test_tutorial006b.py b/tests/test_tutorial/test_custom_response/test_tutorial006b.py index 1739fd4570..7ca727d2cd 100644 --- a/tests/test_tutorial/test_custom_response/test_tutorial006b.py +++ b/tests/test_tutorial/test_custom_response/test_tutorial006b.py @@ -1,6 +1,6 @@ from fastapi.testclient import TestClient -from docs_src.custom_response.tutorial006b import app +from docs_src.custom_response.tutorial006b_py39 import app client = TestClient(app) diff --git a/tests/test_tutorial/test_custom_response/test_tutorial006c.py b/tests/test_tutorial/test_custom_response/test_tutorial006c.py index 2675f2a93c..e3f76c8403 100644 --- a/tests/test_tutorial/test_custom_response/test_tutorial006c.py +++ b/tests/test_tutorial/test_custom_response/test_tutorial006c.py @@ -1,6 +1,6 @@ from fastapi.testclient import TestClient -from docs_src.custom_response.tutorial006c import app +from docs_src.custom_response.tutorial006c_py39 import app client = TestClient(app) diff --git a/tests/test_tutorial/test_custom_response/test_tutorial007.py b/tests/test_tutorial/test_custom_response/test_tutorial007.py index 4ede820b94..a62476ec14 100644 --- a/tests/test_tutorial/test_custom_response/test_tutorial007.py +++ b/tests/test_tutorial/test_custom_response/test_tutorial007.py @@ -1,6 +1,6 @@ from fastapi.testclient import TestClient -from docs_src.custom_response.tutorial007 import app +from docs_src.custom_response.tutorial007_py39 import app client = TestClient(app) diff --git a/tests/test_tutorial/test_custom_response/test_tutorial008.py b/tests/test_tutorial/test_custom_response/test_tutorial008.py index 10d88a5948..d9fe61f539 100644 --- a/tests/test_tutorial/test_custom_response/test_tutorial008.py +++ b/tests/test_tutorial/test_custom_response/test_tutorial008.py @@ -2,15 +2,15 @@ from pathlib import Path from fastapi.testclient import TestClient -from docs_src.custom_response import tutorial008 -from docs_src.custom_response.tutorial008 import app +from docs_src.custom_response import tutorial008_py39 +from docs_src.custom_response.tutorial008_py39 import app client = TestClient(app) def test_get(tmp_path: Path): file_path: Path = tmp_path / "large-video-file.mp4" - tutorial008.some_file_path = str(file_path) + tutorial008_py39.some_file_path = str(file_path) test_content = b"Fake video bytes" file_path.write_bytes(test_content) response = client.get("/") diff --git a/tests/test_tutorial/test_custom_response/test_tutorial009.py b/tests/test_tutorial/test_custom_response/test_tutorial009.py index ac20f89e67..cb6a514be6 100644 --- a/tests/test_tutorial/test_custom_response/test_tutorial009.py +++ b/tests/test_tutorial/test_custom_response/test_tutorial009.py @@ -2,15 +2,15 @@ from pathlib import Path from fastapi.testclient import TestClient -from docs_src.custom_response import tutorial009 -from docs_src.custom_response.tutorial009 import app +from docs_src.custom_response import tutorial009_py39 +from docs_src.custom_response.tutorial009_py39 import app client = TestClient(app) def test_get(tmp_path: Path): file_path: Path = tmp_path / "large-video-file.mp4" - tutorial009.some_file_path = str(file_path) + tutorial009_py39.some_file_path = str(file_path) test_content = b"Fake video bytes" file_path.write_bytes(test_content) response = client.get("/") diff --git a/tests/test_tutorial/test_custom_response/test_tutorial009b.py b/tests/test_tutorial/test_custom_response/test_tutorial009b.py index 4f56e2f3fe..9918bdb1ac 100644 --- a/tests/test_tutorial/test_custom_response/test_tutorial009b.py +++ b/tests/test_tutorial/test_custom_response/test_tutorial009b.py @@ -2,15 +2,15 @@ from pathlib import Path from fastapi.testclient import TestClient -from docs_src.custom_response import tutorial009b -from docs_src.custom_response.tutorial009b import app +from docs_src.custom_response import tutorial009b_py39 +from docs_src.custom_response.tutorial009b_py39 import app client = TestClient(app) def test_get(tmp_path: Path): file_path: Path = tmp_path / "large-video-file.mp4" - tutorial009b.some_file_path = str(file_path) + tutorial009b_py39.some_file_path = str(file_path) test_content = b"Fake video bytes" file_path.write_bytes(test_content) response = client.get("/") diff --git a/tests/test_tutorial/test_custom_response/test_tutorial009c.py b/tests/test_tutorial/test_custom_response/test_tutorial009c.py index 23c711abe9..efc3a6b4a0 100644 --- a/tests/test_tutorial/test_custom_response/test_tutorial009c.py +++ b/tests/test_tutorial/test_custom_response/test_tutorial009c.py @@ -1,6 +1,6 @@ from fastapi.testclient import TestClient -from docs_src.custom_response.tutorial009c import app +from docs_src.custom_response.tutorial009c_py39 import app client = TestClient(app) diff --git a/tests/test_tutorial/test_dataclasses/test_tutorial001.py b/tests/test_tutorial/test_dataclasses/test_tutorial001.py index 762654d29d..4683062f59 100644 --- a/tests/test_tutorial/test_dataclasses/test_tutorial001.py +++ b/tests/test_tutorial/test_dataclasses/test_tutorial001.py @@ -1,12 +1,27 @@ -from dirty_equals import IsDict +import importlib + +import pytest from fastapi.testclient import TestClient -from docs_src.dataclasses.tutorial001 import app - -client = TestClient(app) +from tests.utils import needs_py310 -def test_post_item(): +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial001_py39"), + pytest.param("tutorial001_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.dataclasses_.{request.param}") + + client = TestClient(mod.app) + client.headers.clear() + return client + + +def test_post_item(client: TestClient): response = client.post("/items/", json={"name": "Foo", "price": 3}) assert response.status_code == 200 assert response.json() == { @@ -17,35 +32,22 @@ def test_post_item(): } -def test_post_invalid_item(): +def test_post_invalid_item(client: TestClient): response = client.post("/items/", json={"name": "Foo", "price": "invalid price"}) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "float_parsing", - "loc": ["body", "price"], - "msg": "Input should be a valid number, unable to parse string as a number", - "input": "invalid price", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "price"], - "msg": "value is not a valid float", - "type": "type_error.float", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "float_parsing", + "loc": ["body", "price"], + "msg": "Input should be a valid number, unable to parse string as a number", + "input": "invalid price", + } + ] + } -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { @@ -103,26 +105,14 @@ def test_openapi_schema(): "properties": { "name": {"title": "Name", "type": "string"}, "price": {"title": "Price", "type": "number"}, - "description": IsDict( - { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), - "tax": IsDict( - { - "title": "Tax", - "anyOf": [{"type": "number"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Tax", "type": "number"} - ), + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_dataclasses/test_tutorial002.py b/tests/test_tutorial/test_dataclasses/test_tutorial002.py index e6d303cfc1..210d743bb8 100644 --- a/tests/test_tutorial/test_dataclasses/test_tutorial002.py +++ b/tests/test_tutorial/test_dataclasses/test_tutorial002.py @@ -1,12 +1,28 @@ -from dirty_equals import IsDict, IsOneOf +import importlib + +import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot -from docs_src.dataclasses.tutorial002 import app - -client = TestClient(app) +from tests.utils import needs_py310 -def test_get_item(): +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial002_py39"), + pytest.param("tutorial002_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.dataclasses_.{request.param}") + + client = TestClient(mod.app) + client.headers.clear() + return client + + +def test_get_item(client: TestClient): response = client.get("/items/next") assert response.status_code == 200 assert response.json() == { @@ -18,80 +34,56 @@ def test_get_item(): } -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200 - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/next": { - "get": { - "summary": "Read Next Item", - "operationId": "read_next_item_items_next_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - } - }, + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/next": { + "get": { + "summary": "Read Next Item", + "operationId": "read_next_item_items_next_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + } + }, + } } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": IsOneOf( - ["name", "price", "tags", "description", "tax"], - # TODO: remove when deprecating Pydantic v1 - ["name", "price"], - ), - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "tags": IsDict( - { + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "tags": { "title": "Tags", "type": "array", "items": {"type": "string"}, - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Tags", - "type": "array", - "items": {"type": "string"}, - } - ), - "description": IsDict( - { + }, + "description": { "title": "Description", "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), - "tax": IsDict( - { + }, + "tax": { "title": "Tax", "anyOf": [{"type": "number"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Tax", "type": "number"} - ), - }, + }, + }, + } } - } - }, - } + }, + } + ) diff --git a/tests/test_tutorial/test_dataclasses/test_tutorial003.py b/tests/test_tutorial/test_dataclasses/test_tutorial003.py index e1fa45201f..a6a9fc1c7e 100644 --- a/tests/test_tutorial/test_dataclasses/test_tutorial003.py +++ b/tests/test_tutorial/test_dataclasses/test_tutorial003.py @@ -1,13 +1,27 @@ +import importlib + +import pytest from fastapi.testclient import TestClient -from docs_src.dataclasses.tutorial003 import app - -from ...utils import needs_pydanticv1, needs_pydanticv2 - -client = TestClient(app) +from ...utils import needs_py310 -def test_post_authors_item(): +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial003_py39"), + pytest.param("tutorial003_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.dataclasses_.{request.param}") + + client = TestClient(mod.app) + client.headers.clear() + return client + + +def test_post_authors_item(client: TestClient): response = client.post( "/authors/foo/items/", json=[{"name": "Bar"}, {"name": "Baz", "description": "Drop the Baz"}], @@ -22,7 +36,7 @@ def test_post_authors_item(): } -def test_get_authors(): +def test_get_authors(client: TestClient): response = client.get("/authors/") assert response.status_code == 200 assert response.json() == [ @@ -53,8 +67,7 @@ def test_get_authors(): ] -@needs_pydanticv2 -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { @@ -187,137 +200,3 @@ def test_openapi_schema(): } }, } - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -def test_openapi_schema_pv1(): - response = client.get("/openapi.json") - assert response.status_code == 200 - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/authors/{author_id}/items/": { - "post": { - "summary": "Create Author Items", - "operationId": "create_author_items_authors__author_id__items__post", - "parameters": [ - { - "required": True, - "schema": {"title": "Author Id", "type": "string"}, - "name": "author_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "title": "Items", - "type": "array", - "items": {"$ref": "#/components/schemas/Item"}, - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Author"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/authors/": { - "get": { - "summary": "Get Authors", - "operationId": "get_authors_authors__get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Get Authors Authors Get", - "type": "array", - "items": { - "$ref": "#/components/schemas/Author" - }, - } - } - }, - } - }, - } - }, - }, - "components": { - "schemas": { - "Author": { - "title": "Author", - "required": ["name"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "items": { - "title": "Items", - "type": "array", - "items": {"$ref": "#/components/schemas/Item"}, - }, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "Item": { - "title": "Item", - "required": ["name"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": {"title": "Description", "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_debugging/__init__.py b/tests/test_tutorial/test_debugging/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/test_tutorial/test_debugging/test_tutorial001.py b/tests/test_tutorial/test_debugging/test_tutorial001.py new file mode 100644 index 0000000000..cf62c3b194 --- /dev/null +++ b/tests/test_tutorial/test_debugging/test_tutorial001.py @@ -0,0 +1,64 @@ +import importlib +import runpy +import sys +import unittest + +import pytest +from fastapi.testclient import TestClient + +MOD_NAME = "docs_src.debugging.tutorial001_py39" + + +@pytest.fixture(name="client") +def get_client(): + mod = importlib.import_module(MOD_NAME) + client = TestClient(mod.app) + return client + + +def test_uvicorn_run_is_not_called_on_import(): + if sys.modules.get(MOD_NAME): + del sys.modules[MOD_NAME] # pragma: no cover + with unittest.mock.patch("uvicorn.run") as uvicorn_run_mock: + importlib.import_module(MOD_NAME) + uvicorn_run_mock.assert_not_called() + + +def test_get_root(client: TestClient): + response = client.get("/") + assert response.status_code == 200 + assert response.json() == {"hello world": "ba"} + + +def test_uvicorn_run_called_when_run_as_main(): # Just for coverage + if sys.modules.get(MOD_NAME): + del sys.modules[MOD_NAME] + with unittest.mock.patch("uvicorn.run") as uvicorn_run_mock: + runpy.run_module(MOD_NAME, run_name="__main__") + + uvicorn_run_mock.assert_called_once_with( + unittest.mock.ANY, host="0.0.0.0", port=8000 + ) + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/": { + "get": { + "summary": "Root", + "operationId": "root__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + }, + } + } + }, + } diff --git a/tests/test_tutorial/test_dependencies/test_tutorial001.py b/tests/test_tutorial/test_dependencies/test_tutorial001_tutorial001_02.py similarity index 84% rename from tests/test_tutorial/test_dependencies/test_tutorial001.py rename to tests/test_tutorial/test_dependencies/test_tutorial001_tutorial001_02.py index ed9944912e..50d7c4108c 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial001.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial001_tutorial001_02.py @@ -1,20 +1,20 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient -from ...utils import needs_py39, needs_py310 +from ...utils import needs_py310 @pytest.fixture( name="client", params=[ - "tutorial001", + pytest.param("tutorial001_py39"), pytest.param("tutorial001_py310", marks=needs_py310), - "tutorial001_an", - pytest.param("tutorial001_an_py39", marks=needs_py39), + pytest.param("tutorial001_an_py39"), pytest.param("tutorial001_an_py310", marks=needs_py310), + pytest.param("tutorial001_02_an_py39"), + pytest.param("tutorial001_02_an_py310", marks=needs_py310), ], ) def get_client(request: pytest.FixtureRequest): @@ -70,16 +70,10 @@ def test_openapi_schema(client: TestClient): "parameters": [ { "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Q", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Q", "type": "string"} - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + }, "name": "q", "in": "query", }, @@ -129,16 +123,10 @@ def test_openapi_schema(client: TestClient): "parameters": [ { "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Q", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Q", "type": "string"} - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + }, "name": "q", "in": "query", }, diff --git a/tests/test_tutorial/test_dependencies/test_tutorial004.py b/tests/test_tutorial/test_dependencies/test_tutorial002_tutorial003_tutorial004.py similarity index 87% rename from tests/test_tutorial/test_dependencies/test_tutorial004.py rename to tests/test_tutorial/test_dependencies/test_tutorial002_tutorial003_tutorial004.py index 8221c83d44..f09d6f268d 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial004.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial002_tutorial003_tutorial004.py @@ -1,19 +1,25 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient -from ...utils import needs_py39, needs_py310 +from ...utils import needs_py310 @pytest.fixture( name="client", params=[ - "tutorial004", + pytest.param("tutorial002_py39"), + pytest.param("tutorial002_py310", marks=needs_py310), + pytest.param("tutorial002_an_py39"), + pytest.param("tutorial002_an_py310", marks=needs_py310), + pytest.param("tutorial003_py39"), + pytest.param("tutorial003_py310", marks=needs_py310), + pytest.param("tutorial003_an_py39"), + pytest.param("tutorial003_an_py310", marks=needs_py310), + pytest.param("tutorial004_py39"), pytest.param("tutorial004_py310", marks=needs_py310), - "tutorial004_an", - pytest.param("tutorial004_an_py39", marks=needs_py39), + pytest.param("tutorial004_an_py39"), pytest.param("tutorial004_an_py310", marks=needs_py310), ], ) @@ -108,16 +114,10 @@ def test_openapi_schema(client: TestClient): "parameters": [ { "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Q", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Q", "type": "string"} - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + }, "name": "q", "in": "query", }, diff --git a/tests/test_tutorial/test_dependencies/test_tutorial005.py b/tests/test_tutorial/test_dependencies/test_tutorial005.py new file mode 100644 index 0000000000..a914936ba1 --- /dev/null +++ b/tests/test_tutorial/test_dependencies/test_tutorial005.py @@ -0,0 +1,139 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial005_py39"), + pytest.param("tutorial005_py310", marks=needs_py310), + pytest.param("tutorial005_an_py39"), + pytest.param("tutorial005_an_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.dependencies.{request.param}") + + client = TestClient(mod.app) + return client + + +@pytest.mark.parametrize( + "path,cookie,expected_status,expected_response", + [ + ( + "/items", + "from_cookie", + 200, + {"q_or_cookie": "from_cookie"}, + ), + ( + "/items?q=foo", + "from_cookie", + 200, + {"q_or_cookie": "foo"}, + ), + ( + "/items", + None, + 200, + {"q_or_cookie": None}, + ), + ], +) +def test_get(path, cookie, expected_status, expected_response, client: TestClient): + if cookie is not None: + client.cookies.set("last_query", cookie) + else: + client.cookies.clear() + response = client.get(path) + assert response.status_code == expected_status + assert response.json() == expected_response + + +def test_openapi_schema(client: TestClient): + 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": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Query", + "operationId": "read_query_items__get", + "parameters": [ + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + }, + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Last Query", + }, + "name": "last_query", + "in": "cookie", + }, + ], + } + } + }, + "components": { + "schemas": { + "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"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_dependencies/test_tutorial006.py b/tests/test_tutorial/test_dependencies/test_tutorial006.py index 4530762f76..59202df3bf 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial006.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial006.py @@ -1,18 +1,14 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient -from ...utils import needs_py39 - @pytest.fixture( name="client", params=[ - "tutorial006", - "tutorial006_an", - pytest.param("tutorial006_an_py39", marks=needs_py39), + pytest.param("tutorial006_py39"), + pytest.param("tutorial006_an_py39"), ], ) def get_client(request: pytest.FixtureRequest): @@ -25,40 +21,22 @@ def get_client(request: pytest.FixtureRequest): def test_get_no_headers(client: TestClient): response = client.get("/items/") assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["header", "x-token"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["header", "x-key"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["header", "x-key"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + }, + { + "type": "missing", + "loc": ["header", "x-key"], + "msg": "Field required", + "input": None, + }, + ] + } def test_get_invalid_one_header(client: TestClient): diff --git a/tests/test_tutorial/test_dependencies/test_tutorial007.py b/tests/test_tutorial/test_dependencies/test_tutorial007.py new file mode 100644 index 0000000000..3e188abcf6 --- /dev/null +++ b/tests/test_tutorial/test_dependencies/test_tutorial007.py @@ -0,0 +1,24 @@ +import asyncio +from contextlib import asynccontextmanager +from unittest.mock import Mock, patch + +from docs_src.dependencies.tutorial007_py39 import get_db + + +def test_get_db(): # Just for coverage + async def test_async_gen(): + cm = asynccontextmanager(get_db) + async with cm() as db_session: + return db_session + + dbsession_moock = Mock() + + with patch( + "docs_src.dependencies.tutorial007_py39.DBSession", + return_value=dbsession_moock, + create=True, + ): + value = asyncio.run(test_async_gen()) + + assert value is dbsession_moock + dbsession_moock.close.assert_called_once() diff --git a/tests/test_tutorial/test_dependencies/test_tutorial008.py b/tests/test_tutorial/test_dependencies/test_tutorial008.py new file mode 100644 index 0000000000..9d7377ebe4 --- /dev/null +++ b/tests/test_tutorial/test_dependencies/test_tutorial008.py @@ -0,0 +1,58 @@ +import importlib +from types import ModuleType +from typing import Annotated, Any +from unittest.mock import Mock, patch + +import pytest +from fastapi import Depends, FastAPI +from fastapi.testclient import TestClient + + +@pytest.fixture( + name="module", + params=[ + "tutorial008_py39", + # Fails with `NameError: name 'DepA' is not defined` + pytest.param("tutorial008_an_py39", marks=pytest.mark.xfail), + ], +) +def get_module(request: pytest.FixtureRequest): + mod_name = f"docs_src.dependencies.{request.param}" + mod = importlib.import_module(mod_name) + return mod + + +def test_get_db(module: ModuleType): + app = FastAPI() + + @app.get("/") + def read_root(c: Annotated[Any, Depends(module.dependency_c)]): + return {"c": str(c)} + + client = TestClient(app) + + a_mock = Mock() + b_mock = Mock() + c_mock = Mock() + + with ( + patch( + f"{module.__name__}.generate_dep_a", + return_value=a_mock, + create=True, + ), + patch( + f"{module.__name__}.generate_dep_b", + return_value=b_mock, + create=True, + ), + patch( + f"{module.__name__}.generate_dep_c", + return_value=c_mock, + create=True, + ), + ): + response = client.get("/") + + assert response.status_code == 200 + assert response.json() == {"c": str(c_mock)} diff --git a/tests/test_tutorial/test_dependencies/test_tutorial008b.py b/tests/test_tutorial/test_dependencies/test_tutorial008b.py index 4d70922657..91e00b3705 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial008b.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial008b.py @@ -3,15 +3,12 @@ import importlib import pytest from fastapi.testclient import TestClient -from ...utils import needs_py39 - @pytest.fixture( name="client", params=[ - "tutorial008b", - "tutorial008b_an", - pytest.param("tutorial008b_an_py39", marks=needs_py39), + pytest.param("tutorial008b_py39"), + pytest.param("tutorial008b_an_py39"), ], ) def get_client(request: pytest.FixtureRequest): diff --git a/tests/test_tutorial/test_dependencies/test_tutorial008c.py b/tests/test_tutorial/test_dependencies/test_tutorial008c.py index 369b0a221d..aede6f8d25 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial008c.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial008c.py @@ -5,15 +5,12 @@ import pytest from fastapi.exceptions import FastAPIError from fastapi.testclient import TestClient -from ...utils import needs_py39 - @pytest.fixture( name="mod", params=[ - "tutorial008c", - "tutorial008c_an", - pytest.param("tutorial008c_an_py39", marks=needs_py39), + pytest.param("tutorial008c_py39"), + pytest.param("tutorial008c_an_py39"), ], ) def get_mod(request: pytest.FixtureRequest): diff --git a/tests/test_tutorial/test_dependencies/test_tutorial008d.py b/tests/test_tutorial/test_dependencies/test_tutorial008d.py index bc99bb3835..5477f8b953 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial008d.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial008d.py @@ -4,15 +4,12 @@ from types import ModuleType import pytest from fastapi.testclient import TestClient -from ...utils import needs_py39 - @pytest.fixture( name="mod", params=[ - "tutorial008d", - "tutorial008d_an", - pytest.param("tutorial008d_an_py39", marks=needs_py39), + pytest.param("tutorial008d_py39"), + pytest.param("tutorial008d_an_py39"), ], ) def get_mod(request: pytest.FixtureRequest): diff --git a/tests/test_tutorial/test_dependencies/test_tutorial008e.py b/tests/test_tutorial/test_dependencies/test_tutorial008e.py index 1ae9ab2cd1..c433157ea1 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial008e.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial008e.py @@ -3,15 +3,12 @@ import importlib import pytest from fastapi.testclient import TestClient -from ...utils import needs_py39 - @pytest.fixture( name="client", params=[ - "tutorial008e", - "tutorial008e_an", - pytest.param("tutorial008e_an_py39", marks=needs_py39), + pytest.param("tutorial008e_py39"), + pytest.param("tutorial008e_an_py39"), ], ) def get_client(request: pytest.FixtureRequest): diff --git a/tests/test_tutorial/test_dependencies/test_tutorial010.py b/tests/test_tutorial/test_dependencies/test_tutorial010.py new file mode 100644 index 0000000000..6d3815ada2 --- /dev/null +++ b/tests/test_tutorial/test_dependencies/test_tutorial010.py @@ -0,0 +1,29 @@ +from typing import Annotated, Any +from unittest.mock import Mock, patch + +from fastapi import Depends, FastAPI +from fastapi.testclient import TestClient + +from docs_src.dependencies.tutorial010_py39 import get_db + + +def test_get_db(): + app = FastAPI() + + @app.get("/") + def read_root(c: Annotated[Any, Depends(get_db)]): + return {"c": str(c)} + + client = TestClient(app) + + dbsession_mock = Mock() + + with patch( + "docs_src.dependencies.tutorial010_py39.DBSession", + return_value=dbsession_mock, + create=True, + ): + response = client.get("/") + + assert response.status_code == 200 + assert response.json() == {"c": str(dbsession_mock)} diff --git a/tests/test_tutorial/test_dependencies/test_tutorial011.py b/tests/test_tutorial/test_dependencies/test_tutorial011.py new file mode 100644 index 0000000000..4868254c0b --- /dev/null +++ b/tests/test_tutorial/test_dependencies/test_tutorial011.py @@ -0,0 +1,120 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + + +@pytest.fixture( + name="client", + params=[ + "tutorial011_py39", + pytest.param("tutorial011_an_py39"), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.dependencies.{request.param}") + + client = TestClient(mod.app) + return client + + +@pytest.mark.parametrize( + "path,expected_status,expected_response", + [ + ( + "/query-checker/", + 200, + {"fixed_content_in_query": False}, + ), + ( + "/query-checker/?q=qwerty", + 200, + {"fixed_content_in_query": False}, + ), + ( + "/query-checker/?q=foobar", + 200, + {"fixed_content_in_query": True}, + ), + ], +) +def test_get(path, expected_status, expected_response, client: TestClient): + response = client.get(path) + assert response.status_code == expected_status + assert response.json() == expected_response + + +def test_openapi_schema(client: TestClient): + 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": { + "/query-checker/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Query Check", + "operationId": "read_query_check_query_checker__get", + "parameters": [ + { + "required": False, + "schema": { + "type": "string", + "default": "", + "title": "Q", + }, + "name": "q", + "in": "query", + }, + ], + } + } + }, + "components": { + "schemas": { + "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"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_dependencies/test_tutorial012.py b/tests/test_tutorial/test_dependencies/test_tutorial012.py index 0af17e9bc5..d5599ac73a 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial012.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial012.py @@ -1,18 +1,14 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient -from ...utils import needs_py39 - @pytest.fixture( name="client", params=[ - "tutorial012", - "tutorial012_an", - pytest.param("tutorial012_an_py39", marks=needs_py39), + pytest.param("tutorial012_py39"), + pytest.param("tutorial012_an_py39"), ], ) def get_client(request: pytest.FixtureRequest): @@ -25,79 +21,43 @@ def get_client(request: pytest.FixtureRequest): def test_get_no_headers_items(client: TestClient): response = client.get("/items/") assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["header", "x-token"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["header", "x-key"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["header", "x-key"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + }, + { + "type": "missing", + "loc": ["header", "x-key"], + "msg": "Field required", + "input": None, + }, + ] + } def test_get_no_headers_users(client: TestClient): response = client.get("/users/") assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["header", "x-token"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["header", "x-key"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["header", "x-key"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + }, + { + "type": "missing", + "loc": ["header", "x-key"], + "msg": "Field required", + "input": None, + }, + ] + } def test_get_invalid_one_header_items(client: TestClient): diff --git a/tests/test_tutorial/test_encoder/__init__.py b/tests/test_tutorial/test_encoder/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/test_tutorial/test_encoder/test_tutorial001.py b/tests/test_tutorial/test_encoder/test_tutorial001.py new file mode 100644 index 0000000000..5c8ee054d8 --- /dev/null +++ b/tests/test_tutorial/test_encoder/test_tutorial001.py @@ -0,0 +1,208 @@ +import importlib +from types import ModuleType + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial001_py39"), + pytest.param("tutorial001_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest): + module = importlib.import_module(f"docs_src.encoder.{request.param}") + return module + + +@pytest.fixture(name="client") +def get_client(mod: ModuleType): + client = TestClient(mod.app) + return client + + +def test_put(client: TestClient, mod: ModuleType): + fake_db = mod.fake_db + + response = client.put( + "/items/123", + json={ + "title": "Foo", + "timestamp": "2023-01-01T12:00:00", + "description": "An optional description", + }, + ) + assert response.status_code == 200 + assert "123" in fake_db + assert fake_db["123"] == { + "title": "Foo", + "timestamp": "2023-01-01T12:00:00", + "description": "An optional description", + } + + +def test_put_invalid_data(client: TestClient, mod: ModuleType): + fake_db = mod.fake_db + + response = client.put( + "/items/345", + json={ + "title": "Foo", + "timestamp": "not a date", + }, + ) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "loc": ["body", "timestamp"], + "msg": "Input should be a valid datetime or date, invalid character in year", + "type": "datetime_from_date_parsing", + "input": "not a date", + "ctx": {"error": "invalid character in year"}, + } + ] + } + assert "345" not in fake_db + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{id}": { + "put": { + "operationId": "update_item_items__id__put", + "parameters": [ + { + "in": "path", + "name": "id", + "required": True, + "schema": { + "title": "Id", + "type": "string", + }, + }, + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Item", + }, + }, + }, + "required": True, + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": {}, + }, + }, + "description": "Successful Response", + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError", + }, + }, + }, + "description": "Validation Error", + }, + }, + "summary": "Update Item", + }, + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError", + }, + "title": "Detail", + "type": "array", + }, + }, + "title": "HTTPValidationError", + "type": "object", + }, + "Item": { + "properties": { + "description": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "null", + }, + ], + "title": "Description", + }, + "timestamp": { + "format": "date-time", + "title": "Timestamp", + "type": "string", + }, + "title": { + "title": "Title", + "type": "string", + }, + }, + "required": [ + "title", + "timestamp", + ], + "title": "Item", + "type": "object", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "integer", + }, + ], + }, + "title": "Location", + "type": "array", + }, + "msg": { + "title": "Message", + "type": "string", + }, + "type": { + "title": "Error Type", + "type": "string", + }, + }, + "required": [ + "loc", + "msg", + "type", + ], + "title": "ValidationError", + "type": "object", + }, + }, + }, + } diff --git a/tests/test_tutorial/test_events/test_tutorial001.py b/tests/test_tutorial/test_events/test_tutorial001.py index f65b92d127..5fe99d50df 100644 --- a/tests/test_tutorial/test_events/test_tutorial001.py +++ b/tests/test_tutorial/test_events/test_tutorial001.py @@ -6,7 +6,7 @@ from fastapi.testclient import TestClient @pytest.fixture(name="app", scope="module") def get_app(): with pytest.warns(DeprecationWarning): - from docs_src.events.tutorial001 import app + from docs_src.events.tutorial001_py39 import app yield app diff --git a/tests/test_tutorial/test_events/test_tutorial002.py b/tests/test_tutorial/test_events/test_tutorial002.py index 137294d737..0612899b55 100644 --- a/tests/test_tutorial/test_events/test_tutorial002.py +++ b/tests/test_tutorial/test_events/test_tutorial002.py @@ -6,7 +6,7 @@ from fastapi.testclient import TestClient @pytest.fixture(name="app", scope="module") def get_app(): with pytest.warns(DeprecationWarning): - from docs_src.events.tutorial002 import app + from docs_src.events.tutorial002_py39 import app yield app diff --git a/tests/test_tutorial/test_events/test_tutorial003.py b/tests/test_tutorial/test_events/test_tutorial003.py index 0ad1a1f8b2..38710edfea 100644 --- a/tests/test_tutorial/test_events/test_tutorial003.py +++ b/tests/test_tutorial/test_events/test_tutorial003.py @@ -1,6 +1,6 @@ from fastapi.testclient import TestClient -from docs_src.events.tutorial003 import ( +from docs_src.events.tutorial003_py39 import ( app, fake_answer_to_everything_ml_model, ml_models, diff --git a/tests/test_tutorial/test_extending_openapi/test_tutorial001.py b/tests/test_tutorial/test_extending_openapi/test_tutorial001.py index a85a313501..83ecb300ee 100644 --- a/tests/test_tutorial/test_extending_openapi/test_tutorial001.py +++ b/tests/test_tutorial/test_extending_openapi/test_tutorial001.py @@ -1,6 +1,6 @@ from fastapi.testclient import TestClient -from docs_src.extending_openapi.tutorial001 import app +from docs_src.extending_openapi.tutorial001_py39 import app client = TestClient(app) diff --git a/tests/test_tutorial/test_extra_data_types/test_tutorial001.py b/tests/test_tutorial/test_extra_data_types/test_tutorial001.py index b816c9cab5..5479e29252 100644 --- a/tests/test_tutorial/test_extra_data_types/test_tutorial001.py +++ b/tests/test_tutorial/test_extra_data_types/test_tutorial001.py @@ -1,19 +1,18 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient +from inline_snapshot import snapshot -from ...utils import needs_py39, needs_py310 +from ...utils import needs_py310 @pytest.fixture( name="client", params=[ - "tutorial001", + pytest.param("tutorial001_py39"), pytest.param("tutorial001_py310", marks=needs_py310), - "tutorial001_an", - pytest.param("tutorial001_an_py39", marks=needs_py39), + pytest.param("tutorial001_an_py39"), pytest.param("tutorial001_an_py310", marks=needs_py310), ], ) @@ -48,146 +47,117 @@ def test_extra_types(client: TestClient): def test_openapi_schema(client: TestClient): 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": { - "/items/{item_id}": { - "put": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, }, - "422": { - "description": "Validation Error", + "summary": "Read Items", + "operationId": "read_items_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "type": "string", + "format": "uuid", + }, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "required": True, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "$ref": "#/components/schemas/Body_read_items_items__item_id__put" } } }, }, - }, - "summary": "Read Items", - "operationId": "read_items_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "type": "string", - "format": "uuid", - }, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "required": True, - "content": { - "application/json": { - "schema": IsDict( - { - "allOf": [ - { - "$ref": "#/components/schemas/Body_read_items_items__item_id__put" - } - ], - "title": "Body", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "$ref": "#/components/schemas/Body_read_items_items__item_id__put" - } - ) - } - }, - }, + } } - } - }, - "components": { - "schemas": { - "Body_read_items_items__item_id__put": { - "title": "Body_read_items_items__item_id__put", - "type": "object", - "properties": { - "start_datetime": { - "title": "Start Datetime", - "type": "string", - "format": "date-time", - }, - "end_datetime": { - "title": "End Datetime", - "type": "string", - "format": "date-time", - }, - "repeat_at": IsDict( - { + }, + "components": { + "schemas": { + "Body_read_items_items__item_id__put": { + "title": "Body_read_items_items__item_id__put", + "type": "object", + "properties": { + "start_datetime": { + "title": "Start Datetime", + "type": "string", + "format": "date-time", + }, + "end_datetime": { + "title": "End Datetime", + "type": "string", + "format": "date-time", + }, + "repeat_at": { "title": "Repeat At", "anyOf": [ {"type": "string", "format": "time"}, {"type": "null"}, ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Repeat At", - "type": "string", - "format": "time", - } - ), - "process_after": IsDict( - { + }, + "process_after": { "title": "Process After", "type": "string", "format": "duration", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Process After", - "type": "number", - "format": "time-delta", - } - ), - }, - "required": ["start_datetime", "end_datetime", "process_after"], - }, - "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"}, + "required": ["start_datetime", "end_datetime", "process_after"], }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } + "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"}, + }, }, - }, - } - }, - } + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + } + }, + } + ) diff --git a/tests/test_tutorial/test_extra_models/test_tutorial001_tutorial002.py b/tests/test_tutorial/test_extra_models/test_tutorial001_tutorial002.py new file mode 100644 index 0000000000..3f2f508a11 --- /dev/null +++ b/tests/test_tutorial/test_extra_models/test_tutorial001_tutorial002.py @@ -0,0 +1,156 @@ +import importlib + +import pytest +from dirty_equals import IsList +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial001_py39"), + pytest.param("tutorial001_py310", marks=needs_py310), + pytest.param("tutorial002_py39"), + pytest.param("tutorial002_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.extra_models.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_post(client: TestClient): + response = client.post( + "/user/", + json={ + "username": "johndoe", + "password": "secret", + "email": "johndoe@example.com", + "full_name": "John Doe", + }, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "username": "johndoe", + "email": "johndoe@example.com", + "full_name": "John Doe", + } + + +def test_openapi_schema(client: TestClient): + 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": { + "/user/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserOut", + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create User", + "operationId": "create_user_user__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/UserIn"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "UserIn": { + "title": "UserIn", + "required": IsList( + "username", "password", "email", check_order=False + ), + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "password": {"title": "Password", "type": "string"}, + "email": { + "title": "Email", + "type": "string", + "format": "email", + }, + "full_name": { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + }, + }, + "UserOut": { + "title": "UserOut", + "required": ["username", "email"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "email": { + "title": "Email", + "type": "string", + "format": "email", + }, + "full_name": { + "title": "Full Name", + "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"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_extra_models/test_tutorial003.py b/tests/test_tutorial/test_extra_models/test_tutorial003.py index 73aa299039..872af53830 100644 --- a/tests/test_tutorial/test_extra_models/test_tutorial003.py +++ b/tests/test_tutorial/test_extra_models/test_tutorial003.py @@ -1,8 +1,8 @@ import importlib import pytest -from dirty_equals import IsOneOf from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -10,7 +10,7 @@ from ...utils import needs_py310 @pytest.fixture( name="client", params=[ - "tutorial003", + pytest.param("tutorial003_py39"), pytest.param("tutorial003_py310", marks=needs_py310), ], ) @@ -43,107 +43,115 @@ def test_get_plane(client: TestClient): def test_openapi_schema(client: TestClient): 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": { - "/items/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Item Items Item Id Get", - "anyOf": [ - {"$ref": "#/components/schemas/PlaneItem"}, - {"$ref": "#/components/schemas/CarItem"}, - ], + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Item Items Item Id Get", + "anyOf": [ + { + "$ref": "#/components/schemas/PlaneItem" + }, + { + "$ref": "#/components/schemas/CarItem" + }, + ], + } } - } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, }, }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Item", - "operationId": "read_item_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + } } - } - }, - "components": { - "schemas": { - "PlaneItem": { - "title": "PlaneItem", - "required": IsOneOf( - ["description", "type", "size"], - # TODO: remove when deprecating Pydantic v1 - ["description", "size"], - ), - "type": "object", - "properties": { - "description": {"title": "Description", "type": "string"}, - "type": {"title": "Type", "type": "string", "default": "plane"}, - "size": {"title": "Size", "type": "integer"}, + }, + "components": { + "schemas": { + "PlaneItem": { + "title": "PlaneItem", + "required": ["description", "size"], + "type": "object", + "properties": { + "description": {"title": "Description", "type": "string"}, + "type": { + "title": "Type", + "type": "string", + "default": "plane", + }, + "size": {"title": "Size", "type": "integer"}, + }, }, - }, - "CarItem": { - "title": "CarItem", - "required": IsOneOf( - ["description", "type"], - # TODO: remove when deprecating Pydantic v1 - ["description"], - ), - "type": "object", - "properties": { - "description": {"title": "Description", "type": "string"}, - "type": {"title": "Type", "type": "string", "default": "car"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] + "CarItem": { + "title": "CarItem", + "required": ["description"], + "type": "object", + "properties": { + "description": {"title": "Description", "type": "string"}, + "type": { + "title": "Type", + "type": "string", + "default": "car", }, }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } + "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"}, + }, }, - }, - } - }, - } + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + } + }, + } + ) diff --git a/tests/test_tutorial/test_extra_models/test_tutorial004.py b/tests/test_tutorial/test_extra_models/test_tutorial004.py index 7628db30c7..073375ccc9 100644 --- a/tests/test_tutorial/test_extra_models/test_tutorial004.py +++ b/tests/test_tutorial/test_extra_models/test_tutorial004.py @@ -3,14 +3,11 @@ import importlib import pytest from fastapi.testclient import TestClient -from ...utils import needs_py39 - @pytest.fixture( name="client", params=[ - "tutorial004", - pytest.param("tutorial004_py39", marks=needs_py39), + pytest.param("tutorial004_py39"), ], ) def get_client(request: pytest.FixtureRequest): diff --git a/tests/test_tutorial/test_extra_models/test_tutorial005.py b/tests/test_tutorial/test_extra_models/test_tutorial005.py index 553e442385..8e52d8d4ba 100644 --- a/tests/test_tutorial/test_extra_models/test_tutorial005.py +++ b/tests/test_tutorial/test_extra_models/test_tutorial005.py @@ -3,14 +3,11 @@ import importlib import pytest from fastapi.testclient import TestClient -from ...utils import needs_py39 - @pytest.fixture( name="client", params=[ - "tutorial005", - pytest.param("tutorial005_py39", marks=needs_py39), + pytest.param("tutorial005_py39"), ], ) def get_client(request: pytest.FixtureRequest): diff --git a/tests/test_tutorial/test_first_steps/test_tutorial001.py b/tests/test_tutorial/test_first_steps/test_tutorial001_tutorial002_tutorial003.py similarity index 70% rename from tests/test_tutorial/test_first_steps/test_tutorial001.py rename to tests/test_tutorial/test_first_steps/test_tutorial001_tutorial002_tutorial003.py index 6cc9fc2282..aa65218cde 100644 --- a/tests/test_tutorial/test_first_steps/test_tutorial001.py +++ b/tests/test_tutorial/test_first_steps/test_tutorial001_tutorial002_tutorial003.py @@ -1,9 +1,20 @@ +import importlib + import pytest from fastapi.testclient import TestClient -from docs_src.first_steps.tutorial001 import app -client = TestClient(app) +@pytest.fixture( + name="client", + params=[ + "tutorial001_py39", + "tutorial003_py39", + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.first_steps.{request.param}") + client = TestClient(mod.app) + return client @pytest.mark.parametrize( @@ -13,13 +24,13 @@ client = TestClient(app) ("/nonexistent", 404, {"detail": "Not Found"}), ], ) -def test_get_path(path, expected_status, expected_response): +def test_get_path(client: TestClient, path, expected_status, expected_response): response = client.get(path) assert response.status_code == expected_status assert response.json() == expected_response -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { diff --git a/tests/test_tutorial/test_generate_clients/test_tutorial001.py b/tests/test_tutorial/test_generate_clients/test_tutorial001.py new file mode 100644 index 0000000000..bbb66b4516 --- /dev/null +++ b/tests/test_tutorial/test_generate_clients/test_tutorial001.py @@ -0,0 +1,142 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial001_py39"), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.generate_clients.{request.param}") + client = TestClient(mod.app) + return client + + +def test_post_items(client: TestClient): + response = client.post("/items/", json={"name": "Foo", "price": 5}) + assert response.status_code == 200, response.text + assert response.json() == {"message": "item received"} + + +def test_get_items(client: TestClient): + response = client.get("/items/") + assert response.status_code == 200, response.text + assert response.json() == [ + {"name": "Plumbus", "price": 3}, + {"name": "Portal Gun", "price": 9001}, + ] + + +def test_openapi_schema(client: TestClient): + 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": { + "/items/": { + "get": { + "summary": "Get Items", + "operationId": "get_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Get Items Items Get", + "type": "array", + "items": {"$ref": "#/components/schemas/Item"}, + } + } + }, + } + }, + }, + "post": { + "summary": "Create Item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResponseMessage" + } + } + }, + }, + "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"}, + } + }, + }, + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + }, + }, + "ResponseMessage": { + "title": "ResponseMessage", + "required": ["message"], + "type": "object", + "properties": {"message": {"title": "Message", "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_generate_clients/test_tutorial002.py b/tests/test_tutorial/test_generate_clients/test_tutorial002.py new file mode 100644 index 0000000000..ab8bc4c11c --- /dev/null +++ b/tests/test_tutorial/test_generate_clients/test_tutorial002.py @@ -0,0 +1,187 @@ +from fastapi.testclient import TestClient + +from docs_src.generate_clients.tutorial002_py39 import app + +client = TestClient(app) + + +def test_post_items(): + response = client.post("/items/", json={"name": "Foo", "price": 5}) + assert response.status_code == 200, response.text + assert response.json() == {"message": "Item received"} + + +def test_post_users(): + response = client.post( + "/users/", json={"username": "Foo", "email": "foo@example.com"} + ) + assert response.status_code == 200, response.text + assert response.json() == {"message": "User received"} + + +def test_get_items(): + response = client.get("/items/") + assert response.status_code == 200, response.text + assert response.json() == [ + {"name": "Plumbus", "price": 3}, + {"name": "Portal Gun", "price": 9001}, + ] + + +def test_openapi_schema(): + 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": { + "/items/": { + "get": { + "tags": ["items"], + "summary": "Get Items", + "operationId": "get_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Get Items Items Get", + "type": "array", + "items": {"$ref": "#/components/schemas/Item"}, + } + } + }, + } + }, + }, + "post": { + "tags": ["items"], + "summary": "Create Item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResponseMessage" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + "/users/": { + "post": { + "tags": ["users"], + "summary": "Create User", + "operationId": "create_user_users__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResponseMessage" + } + } + }, + }, + "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"}, + } + }, + }, + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + }, + }, + "ResponseMessage": { + "title": "ResponseMessage", + "required": ["message"], + "type": "object", + "properties": {"message": {"title": "Message", "type": "string"}}, + }, + "User": { + "title": "User", + "required": ["username", "email"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "email": {"title": "Email", "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_generate_clients/test_tutorial003.py b/tests/test_tutorial/test_generate_clients/test_tutorial003.py index 1cd9678a1c..bac52e4fd6 100644 --- a/tests/test_tutorial/test_generate_clients/test_tutorial003.py +++ b/tests/test_tutorial/test_generate_clients/test_tutorial003.py @@ -1,6 +1,6 @@ from fastapi.testclient import TestClient -from docs_src.generate_clients.tutorial003 import app +from docs_src.generate_clients.tutorial003_py39 import app client = TestClient(app) diff --git a/tests/test_tutorial/test_generate_clients/test_tutorial004.py b/tests/test_tutorial/test_generate_clients/test_tutorial004.py new file mode 100644 index 0000000000..e66f6d2a12 --- /dev/null +++ b/tests/test_tutorial/test_generate_clients/test_tutorial004.py @@ -0,0 +1,230 @@ +import importlib +import json +import pathlib +from unittest.mock import patch + +from docs_src.generate_clients import tutorial003_py39 + + +def test_remove_tags(tmp_path: pathlib.Path): + tmp_file = tmp_path / "openapi.json" + openapi_json = tutorial003_py39.app.openapi() + tmp_file.write_text(json.dumps(openapi_json)) + + with patch("pathlib.Path", return_value=tmp_file): + importlib.import_module("docs_src.generate_clients.tutorial004_py39") + + modified_openapi = json.loads(tmp_file.read_text()) + assert modified_openapi == { + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError", + }, + "title": "Detail", + "type": "array", + }, + }, + "title": "HTTPValidationError", + "type": "object", + }, + "Item": { + "properties": { + "name": { + "title": "Name", + "type": "string", + }, + "price": { + "title": "Price", + "type": "number", + }, + }, + "required": [ + "name", + "price", + ], + "title": "Item", + "type": "object", + }, + "ResponseMessage": { + "properties": { + "message": { + "title": "Message", + "type": "string", + }, + }, + "required": [ + "message", + ], + "title": "ResponseMessage", + "type": "object", + }, + "User": { + "properties": { + "email": { + "title": "Email", + "type": "string", + }, + "username": { + "title": "Username", + "type": "string", + }, + }, + "required": [ + "username", + "email", + ], + "title": "User", + "type": "object", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "integer", + }, + ], + }, + "title": "Location", + "type": "array", + }, + "msg": { + "title": "Message", + "type": "string", + }, + "type": { + "title": "Error Type", + "type": "string", + }, + }, + "required": [ + "loc", + "msg", + "type", + ], + "title": "ValidationError", + "type": "object", + }, + }, + }, + "info": { + "title": "FastAPI", + "version": "0.1.0", + }, + "openapi": "3.1.0", + "paths": { + "/items/": { + "get": { + "operationId": "get_items", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/Item", + }, + "title": "Response Items-Get Items", + "type": "array", + }, + }, + }, + "description": "Successful Response", + }, + }, + "summary": "Get Items", + "tags": [ + "items", + ], + }, + "post": { + "operationId": "create_item", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Item", + }, + }, + }, + "required": True, + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResponseMessage", + }, + }, + }, + "description": "Successful Response", + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError", + }, + }, + }, + "description": "Validation Error", + }, + }, + "summary": "Create Item", + "tags": [ + "items", + ], + }, + }, + "/users/": { + "post": { + "operationId": "create_user", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User", + }, + }, + }, + "required": True, + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResponseMessage", + }, + }, + }, + "description": "Successful Response", + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError", + }, + }, + }, + "description": "Validation Error", + }, + }, + "summary": "Create User", + "tags": [ + "users", + ], + }, + }, + }, + } diff --git a/tests/test_tutorial/test_graphql/__init__.py b/tests/test_tutorial/test_graphql/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/test_tutorial/test_graphql/test_tutorial001.py b/tests/test_tutorial/test_graphql/test_tutorial001.py new file mode 100644 index 0000000000..9ba7147b55 --- /dev/null +++ b/tests/test_tutorial/test_graphql/test_tutorial001.py @@ -0,0 +1,70 @@ +import warnings + +import pytest +from starlette.testclient import TestClient + +warnings.filterwarnings( + "ignore", + message=r"The 'lia' package has been renamed to 'cross_web'\..*", + category=DeprecationWarning, +) + +from docs_src.graphql_.tutorial001_py39 import app # noqa: E402 + + +@pytest.fixture(name="client") +def get_client() -> TestClient: + return TestClient(app) + + +def test_query(client: TestClient): + response = client.post("/graphql", json={"query": "{ user { name, age } }"}) + assert response.status_code == 200 + assert response.json() == {"data": {"user": {"name": "Patrick", "age": 100}}} + + +def test_openapi(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "info": { + "title": "FastAPI", + "version": "0.1.0", + }, + "openapi": "3.1.0", + "paths": { + "/graphql": { + "get": { + "operationId": "handle_http_get_graphql_get", + "responses": { + "200": { + "content": { + "application/json": { + "schema": {}, + }, + }, + "description": "The GraphiQL integrated development environment.", + }, + "404": { + "description": "Not found if GraphiQL or query via GET are not enabled.", + }, + }, + "summary": "Handle Http Get", + }, + "post": { + "operationId": "handle_http_post_graphql_post", + "responses": { + "200": { + "content": { + "application/json": { + "schema": {}, + }, + }, + "description": "Successful Response", + }, + }, + "summary": "Handle Http Post", + }, + }, + }, + } diff --git a/tests/test_tutorial/test_handling_errors/test_tutorial001.py b/tests/test_tutorial/test_handling_errors/test_tutorial001.py index 8809c135bd..c01850fae6 100644 --- a/tests/test_tutorial/test_handling_errors/test_tutorial001.py +++ b/tests/test_tutorial/test_handling_errors/test_tutorial001.py @@ -1,6 +1,6 @@ from fastapi.testclient import TestClient -from docs_src.handling_errors.tutorial001 import app +from docs_src.handling_errors.tutorial001_py39 import app client = TestClient(app) diff --git a/tests/test_tutorial/test_handling_errors/test_tutorial002.py b/tests/test_tutorial/test_handling_errors/test_tutorial002.py index efd86ebdec..09366a86fa 100644 --- a/tests/test_tutorial/test_handling_errors/test_tutorial002.py +++ b/tests/test_tutorial/test_handling_errors/test_tutorial002.py @@ -1,6 +1,6 @@ from fastapi.testclient import TestClient -from docs_src.handling_errors.tutorial002 import app +from docs_src.handling_errors.tutorial002_py39 import app client = TestClient(app) diff --git a/tests/test_tutorial/test_handling_errors/test_tutorial003.py b/tests/test_tutorial/test_handling_errors/test_tutorial003.py index 4763f68f3f..51ac3e7b28 100644 --- a/tests/test_tutorial/test_handling_errors/test_tutorial003.py +++ b/tests/test_tutorial/test_handling_errors/test_tutorial003.py @@ -1,6 +1,6 @@ from fastapi.testclient import TestClient -from docs_src.handling_errors.tutorial003 import app +from docs_src.handling_errors.tutorial003_py39 import app client = TestClient(app) diff --git a/tests/test_tutorial/test_handling_errors/test_tutorial004.py b/tests/test_tutorial/test_handling_errors/test_tutorial004.py index 217159a596..376bc8266f 100644 --- a/tests/test_tutorial/test_handling_errors/test_tutorial004.py +++ b/tests/test_tutorial/test_handling_errors/test_tutorial004.py @@ -1,6 +1,6 @@ from fastapi.testclient import TestClient -from docs_src.handling_errors.tutorial004 import app +from docs_src.handling_errors.tutorial004_py39 import app client = TestClient(app) @@ -8,18 +8,8 @@ client = TestClient(app) def test_get_validation_error(): response = client.get("/items/foo") assert response.status_code == 400, response.text - # TODO: remove when deprecating Pydantic v1 - assert ( - # TODO: remove when deprecating Pydantic v1 - "path -> item_id" in response.text - or "'loc': ('path', 'item_id')" in response.text - ) - assert ( - # TODO: remove when deprecating Pydantic v1 - "value is not a valid integer" in response.text - or "Input should be a valid integer, unable to parse string as an integer" - in response.text - ) + assert "Validation errors:" in response.text + assert "Field: ('path', 'item_id')" in response.text def test_get_http_error(): diff --git a/tests/test_tutorial/test_handling_errors/test_tutorial005.py b/tests/test_tutorial/test_handling_errors/test_tutorial005.py index 581b2e4c75..7bd947f194 100644 --- a/tests/test_tutorial/test_handling_errors/test_tutorial005.py +++ b/tests/test_tutorial/test_handling_errors/test_tutorial005.py @@ -1,7 +1,6 @@ -from dirty_equals import IsDict from fastapi.testclient import TestClient -from docs_src.handling_errors.tutorial005 import app +from docs_src.handling_errors.tutorial005_py39 import app client = TestClient(app) @@ -9,31 +8,17 @@ client = TestClient(app) def test_post_validation_error(): response = client.post("/items/", json={"title": "towel", "size": "XL"}) assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["body", "size"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "XL", - } - ], - "body": {"title": "towel", "size": "XL"}, - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "size"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ], - "body": {"title": "towel", "size": "XL"}, - } - ) + assert response.json() == { + "detail": [ + { + "type": "int_parsing", + "loc": ["body", "size"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "XL", + } + ], + "body": {"title": "towel", "size": "XL"}, + } def test_post(): diff --git a/tests/test_tutorial/test_handling_errors/test_tutorial006.py b/tests/test_tutorial/test_handling_errors/test_tutorial006.py index 7d2f553aac..e95e53d5ed 100644 --- a/tests/test_tutorial/test_handling_errors/test_tutorial006.py +++ b/tests/test_tutorial/test_handling_errors/test_tutorial006.py @@ -1,7 +1,6 @@ -from dirty_equals import IsDict from fastapi.testclient import TestClient -from docs_src.handling_errors.tutorial006 import app +from docs_src.handling_errors.tutorial006_py39 import app client = TestClient(app) @@ -9,29 +8,16 @@ client = TestClient(app) def test_get_validation_error(): response = client.get("/items/foo") assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["path", "item_id"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "foo", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "int_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "foo", + } + ] + } def test_get_http_error(): diff --git a/tests/test_tutorial/test_header_param_models/test_tutorial001.py b/tests/test_tutorial/test_header_param_models/test_tutorial001.py index bc876897b2..1fa8aee461 100644 --- a/tests/test_tutorial/test_header_param_models/test_tutorial001.py +++ b/tests/test_tutorial/test_header_param_models/test_tutorial001.py @@ -1,21 +1,18 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from inline_snapshot import snapshot -from tests.utils import needs_py39, needs_py310 +from tests.utils import needs_py310 @pytest.fixture( name="client", params=[ - "tutorial001", - pytest.param("tutorial001_py39", marks=needs_py39), + pytest.param("tutorial001_py39"), pytest.param("tutorial001_py310", marks=needs_py310), - "tutorial001_an", - pytest.param("tutorial001_an_py39", marks=needs_py39), + pytest.param("tutorial001_an_py39"), pytest.param("tutorial001_an_py310", marks=needs_py310), ], ) @@ -65,29 +62,19 @@ def test_header_param_model_invalid(client: TestClient): assert response.json() == snapshot( { "detail": [ - IsDict( - { - "type": "missing", - "loc": ["header", "save_data"], - "msg": "Field required", - "input": { - "x_tag": [], - "host": "testserver", - "accept": "*/*", - "accept-encoding": "gzip, deflate", - "connection": "keep-alive", - "user-agent": "testclient", - }, - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "value_error.missing", - "loc": ["header", "save_data"], - "msg": "field required", - } - ) + { + "type": "missing", + "loc": ["header", "save_data"], + "msg": "Field required", + "input": { + "x_tag": [], + "host": "testserver", + "accept": "*/*", + "accept-encoding": "gzip, deflate", + "connection": "keep-alive", + "user-agent": "testclient", + }, + } ] } ) @@ -138,37 +125,19 @@ def test_openapi_schema(client: TestClient): "name": "if-modified-since", "in": "header", "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "If Modified Since", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "string", - "title": "If Modified Since", - } - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "If Modified Since", + }, }, { "name": "traceparent", "in": "header", "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Traceparent", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "string", - "title": "Traceparent", - } - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Traceparent", + }, }, { "name": "x-tag", diff --git a/tests/test_tutorial/test_header_param_models/test_tutorial002.py b/tests/test_tutorial/test_header_param_models/test_tutorial002.py index 0615521c43..079a8f5402 100644 --- a/tests/test_tutorial/test_header_param_models/test_tutorial002.py +++ b/tests/test_tutorial/test_header_param_models/test_tutorial002.py @@ -1,26 +1,19 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from inline_snapshot import snapshot -from tests.utils import needs_py39, needs_py310, needs_pydanticv1, needs_pydanticv2 +from tests.utils import needs_py310 @pytest.fixture( name="client", params=[ - pytest.param("tutorial002", marks=needs_pydanticv2), - pytest.param("tutorial002_py310", marks=[needs_py310, needs_pydanticv2]), - pytest.param("tutorial002_an", marks=needs_pydanticv2), - pytest.param("tutorial002_an_py39", marks=[needs_py39, needs_pydanticv2]), - pytest.param("tutorial002_an_py310", marks=[needs_py310, needs_pydanticv2]), - pytest.param("tutorial002_pv1", marks=[needs_pydanticv1, needs_pydanticv1]), - pytest.param("tutorial002_pv1_py310", marks=[needs_py310, needs_pydanticv1]), - pytest.param("tutorial002_pv1_an", marks=[needs_pydanticv1]), - pytest.param("tutorial002_pv1_an_py39", marks=[needs_py39, needs_pydanticv1]), - pytest.param("tutorial002_pv1_an_py310", marks=[needs_py310, needs_pydanticv1]), + pytest.param("tutorial002_py39"), + pytest.param("tutorial002_py310", marks=[needs_py310]), + pytest.param("tutorial002_an_py39"), + pytest.param("tutorial002_an_py310", marks=[needs_py310]), ], ) def get_client(request: pytest.FixtureRequest): @@ -70,22 +63,12 @@ def test_header_param_model_invalid(client: TestClient): assert response.json() == snapshot( { "detail": [ - IsDict( - { - "type": "missing", - "loc": ["header", "save_data"], - "msg": "Field required", - "input": {"x_tag": [], "host": "testserver"}, - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "value_error.missing", - "loc": ["header", "save_data"], - "msg": "field required", - } - ) + { + "type": "missing", + "loc": ["header", "save_data"], + "msg": "Field required", + "input": {"x_tag": [], "host": "testserver"}, + } ] } ) @@ -99,22 +82,12 @@ def test_header_param_model_extra(client: TestClient): assert response.json() == snapshot( { "detail": [ - IsDict( - { - "type": "extra_forbidden", - "loc": ["header", "tool"], - "msg": "Extra inputs are not permitted", - "input": "plumbus", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "value_error.extra", - "loc": ["header", "tool"], - "msg": "extra fields not permitted", - } - ) + { + "type": "extra_forbidden", + "loc": ["header", "tool"], + "msg": "Extra inputs are not permitted", + "input": "plumbus", + } ] } ) @@ -149,37 +122,19 @@ def test_openapi_schema(client: TestClient): "name": "if-modified-since", "in": "header", "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "If Modified Since", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "string", - "title": "If Modified Since", - } - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "If Modified Since", + }, }, { "name": "traceparent", "in": "header", "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Traceparent", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "string", - "title": "Traceparent", - } - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Traceparent", + }, }, { "name": "x-tag", diff --git a/tests/test_tutorial/test_header_param_models/test_tutorial003.py b/tests/test_tutorial/test_header_param_models/test_tutorial003.py index 60940e1da2..4c89d80ee2 100644 --- a/tests/test_tutorial/test_header_param_models/test_tutorial003.py +++ b/tests/test_tutorial/test_header_param_models/test_tutorial003.py @@ -1,21 +1,18 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from inline_snapshot import snapshot -from tests.utils import needs_py39, needs_py310 +from tests.utils import needs_py310 @pytest.fixture( name="client", params=[ - "tutorial003", - pytest.param("tutorial003_py39", marks=needs_py39), + pytest.param("tutorial003_py39"), pytest.param("tutorial003_py310", marks=needs_py310), - "tutorial003_an", - pytest.param("tutorial003_an_py39", marks=needs_py39), + pytest.param("tutorial003_an_py39"), pytest.param("tutorial003_an_py310", marks=needs_py310), ], ) @@ -62,33 +59,23 @@ def test_header_param_model_no_underscore(client: TestClient): assert response.json() == snapshot( { "detail": [ - IsDict( - { - "type": "missing", - "loc": ["header", "save_data"], - "msg": "Field required", - "input": { - "host": "testserver", - "traceparent": "123", - "x_tag": [], - "accept": "*/*", - "accept-encoding": "gzip, deflate", - "connection": "keep-alive", - "user-agent": "testclient", - "save-data": "true", - "if-modified-since": "yesterday", - "x-tag": "two", - }, - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "value_error.missing", - "loc": ["header", "save_data"], - "msg": "field required", - } - ) + { + "type": "missing", + "loc": ["header", "save_data"], + "msg": "Field required", + "input": { + "host": "testserver", + "traceparent": "123", + "x_tag": [], + "accept": "*/*", + "accept-encoding": "gzip, deflate", + "connection": "keep-alive", + "user-agent": "testclient", + "save-data": "true", + "if-modified-since": "yesterday", + "x-tag": ["one", "two"], + }, + } ] } ) @@ -112,29 +99,19 @@ def test_header_param_model_invalid(client: TestClient): assert response.json() == snapshot( { "detail": [ - IsDict( - { - "type": "missing", - "loc": ["header", "save_data"], - "msg": "Field required", - "input": { - "x_tag": [], - "host": "testserver", - "accept": "*/*", - "accept-encoding": "gzip, deflate", - "connection": "keep-alive", - "user-agent": "testclient", - }, - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "value_error.missing", - "loc": ["header", "save_data"], - "msg": "field required", - } - ) + { + "type": "missing", + "loc": ["header", "save_data"], + "msg": "Field required", + "input": { + "x_tag": [], + "host": "testserver", + "accept": "*/*", + "accept-encoding": "gzip, deflate", + "connection": "keep-alive", + "user-agent": "testclient", + }, + } ] } ) @@ -185,37 +162,19 @@ def test_openapi_schema(client: TestClient): "name": "if_modified_since", "in": "header", "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "If Modified Since", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "string", - "title": "If Modified Since", - } - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "If Modified Since", + }, }, { "name": "traceparent", "in": "header", "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Traceparent", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "string", - "title": "Traceparent", - } - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Traceparent", + }, }, { "name": "x_tag", diff --git a/tests/test_tutorial/test_header_params/test_tutorial001.py b/tests/test_tutorial/test_header_params/test_tutorial001.py index d6f7fe6189..88591b8225 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial001.py +++ b/tests/test_tutorial/test_header_params/test_tutorial001.py @@ -1,7 +1,6 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -10,9 +9,9 @@ from ...utils import needs_py310 @pytest.fixture( name="client", params=[ - "tutorial001", + pytest.param("tutorial001_py39"), pytest.param("tutorial001_py310", marks=needs_py310), - "tutorial001_an", + pytest.param("tutorial001_an_py39"), pytest.param("tutorial001_an_py310", marks=needs_py310), ], ) @@ -67,16 +66,10 @@ def test_openapi_schema(client: TestClient): "parameters": [ { "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "User-Agent", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "User-Agent", "type": "string"} - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "User-Agent", + }, "name": "user-agent", "in": "header", } diff --git a/tests/test_tutorial/test_header_params/test_tutorial002.py b/tests/test_tutorial/test_header_params/test_tutorial002.py index 7158f8651b..229f96c1f8 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial002.py +++ b/tests/test_tutorial/test_header_params/test_tutorial002.py @@ -1,19 +1,17 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient -from ...utils import needs_py39, needs_py310 +from ...utils import needs_py310 @pytest.fixture( name="client", params=[ - "tutorial002", + pytest.param("tutorial002_py39"), pytest.param("tutorial002_py310", marks=needs_py310), - "tutorial002_an", - pytest.param("tutorial002_an_py39", marks=needs_py39), + pytest.param("tutorial002_an_py39"), pytest.param("tutorial002_an_py310", marks=needs_py310), ], ) @@ -79,16 +77,10 @@ def test_openapi_schema(client: TestClient): "parameters": [ { "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Strange Header", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Strange Header", "type": "string"} - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Strange Header", + }, "name": "strange_header", "in": "header", } diff --git a/tests/test_tutorial/test_header_params/test_tutorial003.py b/tests/test_tutorial/test_header_params/test_tutorial003.py index 473b961236..cf067ccf9e 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial003.py +++ b/tests/test_tutorial/test_header_params/test_tutorial003.py @@ -1,19 +1,17 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient -from ...utils import needs_py39, needs_py310 +from ...utils import needs_py310 @pytest.fixture( name="client", params=[ - "tutorial003", + pytest.param("tutorial003_py39"), pytest.param("tutorial003_py310", marks=needs_py310), - "tutorial003_an", - pytest.param("tutorial003_an_py39", marks=needs_py39), + pytest.param("tutorial003_an_py39"), pytest.param("tutorial003_an_py310", marks=needs_py310), ], ) @@ -57,23 +55,13 @@ def test_openapi_schema(client: TestClient): "parameters": [ { "required": False, - "schema": IsDict( - { - "title": "X-Token", - "anyOf": [ - {"type": "array", "items": {"type": "string"}}, - {"type": "null"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "X-Token", - "type": "array", - "items": {"type": "string"}, - } - ), + "schema": { + "title": "X-Token", + "anyOf": [ + {"type": "array", "items": {"type": "string"}}, + {"type": "null"}, + ], + }, "name": "x-token", "in": "header", } diff --git a/tests/test_tutorial/test_metadata/test_tutorial001.py b/tests/test_tutorial/test_metadata/test_tutorial001.py index 04e8ff82b0..ead48577d0 100644 --- a/tests/test_tutorial/test_metadata/test_tutorial001.py +++ b/tests/test_tutorial/test_metadata/test_tutorial001.py @@ -1,6 +1,6 @@ from fastapi.testclient import TestClient -from docs_src.metadata.tutorial001 import app +from docs_src.metadata.tutorial001_py39 import app client = TestClient(app) diff --git a/tests/test_tutorial/test_metadata/test_tutorial001_1.py b/tests/test_tutorial/test_metadata/test_tutorial001_1.py index 3efb1c4329..40878ccfd4 100644 --- a/tests/test_tutorial/test_metadata/test_tutorial001_1.py +++ b/tests/test_tutorial/test_metadata/test_tutorial001_1.py @@ -1,6 +1,6 @@ from fastapi.testclient import TestClient -from docs_src.metadata.tutorial001_1 import app +from docs_src.metadata.tutorial001_1_py39 import app client = TestClient(app) diff --git a/tests/test_tutorial/test_custom_response/test_tutorial004.py b/tests/test_tutorial/test_metadata/test_tutorial002.py similarity index 61% rename from tests/test_tutorial/test_custom_response/test_tutorial004.py rename to tests/test_tutorial/test_metadata/test_tutorial002.py index de60574f5e..e2814c88f9 100644 --- a/tests/test_tutorial/test_custom_response/test_tutorial004.py +++ b/tests/test_tutorial/test_metadata/test_tutorial002.py @@ -1,45 +1,41 @@ from fastapi.testclient import TestClient -from docs_src.custom_response.tutorial004 import app +from docs_src.metadata.tutorial002_py39 import app client = TestClient(app) -html_contents = """ - - - Some HTML in here - - -

Look ma! HTML!

- - - """ - - -def test_get_custom_response(): +def test_items(): response = client.get("/items/") assert response.status_code == 200, response.text - assert response.text == html_contents + assert response.json() == [{"name": "Foo"}] + + +def test_get_openapi_json_default_url(): + response = client.get("/openapi.json") + assert response.status_code == 404, response.text def test_openapi_schema(): - response = client.get("/openapi.json") + response = client.get("/api/v1/openapi.json") assert response.status_code == 200, response.text assert response.json() == { "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, + "info": { + "title": "FastAPI", + "version": "0.1.0", + }, "paths": { "/items/": { "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", "responses": { "200": { "description": "Successful Response", - "content": {"text/html": {"schema": {"type": "string"}}}, + "content": {"application/json": {"schema": {}}}, } }, - "summary": "Read Items", - "operationId": "read_items_items__get", } } }, diff --git a/tests/test_tutorial/test_metadata/test_tutorial003.py b/tests/test_tutorial/test_metadata/test_tutorial003.py new file mode 100644 index 0000000000..085c271cdb --- /dev/null +++ b/tests/test_tutorial/test_metadata/test_tutorial003.py @@ -0,0 +1,53 @@ +from fastapi.testclient import TestClient + +from docs_src.metadata.tutorial003_py39 import app + +client = TestClient(app) + + +def test_items(): + response = client.get("/items/") + assert response.status_code == 200, response.text + assert response.json() == [{"name": "Foo"}] + + +def test_openapi_schema(): + 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": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + } + }, + } + + +def test_swagger_ui_default_url(): + response = client.get("/docs") + assert response.status_code == 404, response.text + + +def test_swagger_ui_custom_url(): + response = client.get("/documentation") + assert response.status_code == 200, response.text + assert "FastAPI - Swagger UI" in response.text + + +def test_redoc_ui_default_url(): + response = client.get("/redoc") + assert response.status_code == 404, response.text diff --git a/tests/test_tutorial/test_metadata/test_tutorial004.py b/tests/test_tutorial/test_metadata/test_tutorial004.py index 5072203712..4ef93fd5e3 100644 --- a/tests/test_tutorial/test_metadata/test_tutorial004.py +++ b/tests/test_tutorial/test_metadata/test_tutorial004.py @@ -1,6 +1,6 @@ from fastapi.testclient import TestClient -from docs_src.metadata.tutorial004 import app +from docs_src.metadata.tutorial004_py39 import app client = TestClient(app) diff --git a/tests/test_tutorial/test_middleware/__init__.py b/tests/test_tutorial/test_middleware/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/test_tutorial/test_middleware/test_tutorial001.py b/tests/test_tutorial/test_middleware/test_tutorial001.py new file mode 100644 index 0000000000..cbcfd4146f --- /dev/null +++ b/tests/test_tutorial/test_middleware/test_tutorial001.py @@ -0,0 +1,24 @@ +from fastapi.testclient import TestClient + +from docs_src.middleware.tutorial001_py39 import app + +client = TestClient(app) + + +def test_response_headers(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert "X-Process-Time" in response.headers + + +def test_openapi_schema(): + 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": {}, + } diff --git a/tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py b/tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py index 73af420ae1..6fde96cb5b 100644 --- a/tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py +++ b/tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py @@ -1,12 +1,32 @@ -from dirty_equals import IsDict +import importlib +from types import ModuleType + +import pytest from fastapi.testclient import TestClient -from docs_src.openapi_callbacks.tutorial001 import app, invoice_notification - -client = TestClient(app) +from tests.utils import needs_py310 -def test_get(): +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial001_py39"), + pytest.param("tutorial001_py310", marks=needs_py310), + ], +) +def get_mod(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.openapi_callbacks.{request.param}") + return mod + + +@pytest.fixture(name="client") +def get_client(mod: ModuleType): + client = TestClient(mod.app) + client.headers.clear() + return client + + +def test_get(client: TestClient): response = client.post( "/invoices/", json={"id": "fooinvoice", "customer": "John", "total": 5.3} ) @@ -14,12 +34,12 @@ def test_get(): assert response.json() == {"msg": "Invoice received"} -def test_dummy_callback(): +def test_dummy_callback(mod: ModuleType): # Just for coverage - invoice_notification({}) + mod.invoice_notification({}) -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { @@ -34,30 +54,18 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": IsDict( - { - "anyOf": [ - { - "type": "string", - "format": "uri", - "minLength": 1, - "maxLength": 2083, - }, - {"type": "null"}, - ], - "title": "Callback Url", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Callback Url", - "maxLength": 2083, - "minLength": 1, - "type": "string", - "format": "uri", - } - ), + "schema": { + "anyOf": [ + { + "type": "string", + "format": "uri", + "minLength": 1, + "maxLength": 2083, + }, + {"type": "null"}, + ], + "title": "Callback Url", + }, "name": "callback_url", "in": "query", } @@ -150,16 +158,10 @@ def test_openapi_schema(): "type": "object", "properties": { "id": {"title": "Id", "type": "string"}, - "title": IsDict( - { - "title": "Title", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Title", "type": "string"} - ), + "title": { + "title": "Title", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, "customer": {"title": "Customer", "type": "string"}, "total": {"title": "Total", "type": "number"}, }, diff --git a/tests/test_tutorial/test_openapi_webhooks/test_tutorial001.py b/tests/test_tutorial/test_openapi_webhooks/test_tutorial001.py index dc67ec401d..27619489fa 100644 --- a/tests/test_tutorial/test_openapi_webhooks/test_tutorial001.py +++ b/tests/test_tutorial/test_openapi_webhooks/test_tutorial001.py @@ -1,6 +1,6 @@ from fastapi.testclient import TestClient -from docs_src.openapi_webhooks.tutorial001 import app +from docs_src.openapi_webhooks.tutorial001_py39 import app client = TestClient(app) diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial001.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial001.py index 95542398e4..ee0b707108 100644 --- a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial001.py +++ b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial001.py @@ -1,6 +1,6 @@ from fastapi.testclient import TestClient -from docs_src.path_operation_advanced_configuration.tutorial001 import app +from docs_src.path_operation_advanced_configuration.tutorial001_py39 import app client = TestClient(app) diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial002.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial002.py index d1388c3670..f6580d72e3 100644 --- a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial002.py +++ b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial002.py @@ -1,6 +1,6 @@ from fastapi.testclient import TestClient -from docs_src.path_operation_advanced_configuration.tutorial002 import app +from docs_src.path_operation_advanced_configuration.tutorial002_py39 import app client = TestClient(app) diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial003.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial003.py index 313bb2a04a..104554fce3 100644 --- a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial003.py +++ b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial003.py @@ -1,6 +1,6 @@ from fastapi.testclient import TestClient -from docs_src.path_operation_advanced_configuration.tutorial003 import app +from docs_src.path_operation_advanced_configuration.tutorial003_py39 import app client = TestClient(app) diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py index 4f69e4646c..a95540731d 100644 --- a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py +++ b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py @@ -1,13 +1,29 @@ +import importlib + +import pytest from fastapi.testclient import TestClient -from docs_src.path_operation_advanced_configuration.tutorial004 import app - -from ...utils import needs_pydanticv1, needs_pydanticv2 - -client = TestClient(app) +from ...utils import needs_py310 -def test_query_params_str_validations(): +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial004_py39"), + pytest.param("tutorial004_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module( + f"docs_src.path_operation_advanced_configuration.{request.param}" + ) + + client = TestClient(mod.app) + client.headers.clear() + return client + + +def test_query_params_str_validations(client: TestClient): response = client.post("/items/", json={"name": "Foo", "price": 42}) assert response.status_code == 200, response.text assert response.json() == { @@ -19,8 +35,7 @@ def test_query_params_str_validations(): } -@needs_pydanticv2 -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { @@ -119,100 +134,3 @@ def test_openapi_schema(): } }, } - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -def test_openapi_schema_pv1(): - 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": { - "/items/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Create an item", - "description": "Create an item with all the information:\n\n- **name**: each item must have a name\n- **description**: a long description\n- **price**: required\n- **tax**: if the item doesn't have tax, you can omit this\n- **tags**: a set of unique tag strings for this item", - "operationId": "create_item_items__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": {"title": "Description", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "tax": {"title": "Tax", "type": "number"}, - "tags": { - "title": "Tags", - "uniqueItems": True, - "type": "array", - "items": {"type": "string"}, - "default": [], - }, - }, - }, - "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"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial005.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial005.py index 07e2d7d202..e2a71236ff 100644 --- a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial005.py +++ b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial005.py @@ -1,6 +1,6 @@ from fastapi.testclient import TestClient -from docs_src.path_operation_advanced_configuration.tutorial005 import app +from docs_src.path_operation_advanced_configuration.tutorial005_py39 import app client = TestClient(app) diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial006.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial006.py index f92c59015e..9484f7f573 100644 --- a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial006.py +++ b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial006.py @@ -1,6 +1,6 @@ from fastapi.testclient import TestClient -from docs_src.path_operation_advanced_configuration.tutorial006 import app +from docs_src.path_operation_advanced_configuration.tutorial006_py39 import app client = TestClient(app) diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007.py index 8240b60a62..d5f284e3bd 100644 --- a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007.py +++ b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007.py @@ -1,18 +1,24 @@ +import importlib + import pytest from fastapi.testclient import TestClient -from ...utils import needs_pydanticv2 +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial007_py39"), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module( + f"docs_src.path_operation_advanced_configuration.{request.param}" + ) -@pytest.fixture(name="client") -def get_client(): - from docs_src.path_operation_advanced_configuration.tutorial007 import app - - client = TestClient(app) + client = TestClient(mod.app) return client -@needs_pydanticv2 def test_post(client: TestClient): yaml_data = """ name: Deadpoolio @@ -29,7 +35,6 @@ def test_post(client: TestClient): } -@needs_pydanticv2 def test_post_broken_yaml(client: TestClient): yaml_data = """ name: Deadpoolio @@ -43,7 +48,6 @@ def test_post_broken_yaml(client: TestClient): assert response.json() == {"detail": "Invalid YAML"} -@needs_pydanticv2 def test_post_invalid(client: TestClient): yaml_data = """ name: Deadpoolio @@ -68,7 +72,6 @@ def test_post_invalid(client: TestClient): } -@needs_pydanticv2 def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007_pv1.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007_pv1.py deleted file mode 100644 index ef012f8a6c..0000000000 --- a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007_pv1.py +++ /dev/null @@ -1,106 +0,0 @@ -import pytest -from fastapi.testclient import TestClient - -from ...utils import needs_pydanticv1 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.path_operation_advanced_configuration.tutorial007_pv1 import app - - client = TestClient(app) - return client - - -@needs_pydanticv1 -def test_post(client: TestClient): - yaml_data = """ - name: Deadpoolio - tags: - - x-force - - x-men - - x-avengers - """ - response = client.post("/items/", content=yaml_data) - assert response.status_code == 200, response.text - assert response.json() == { - "name": "Deadpoolio", - "tags": ["x-force", "x-men", "x-avengers"], - } - - -@needs_pydanticv1 -def test_post_broken_yaml(client: TestClient): - yaml_data = """ - name: Deadpoolio - tags: - x - x-force - x - x-men - x - x-avengers - """ - response = client.post("/items/", content=yaml_data) - assert response.status_code == 422, response.text - assert response.json() == {"detail": "Invalid YAML"} - - -@needs_pydanticv1 -def test_post_invalid(client: TestClient): - yaml_data = """ - name: Deadpoolio - tags: - - x-force - - x-men - - x-avengers - - sneaky: object - """ - response = client.post("/items/", content=yaml_data) - assert response.status_code == 422, response.text - assert response.json() == { - "detail": [ - {"loc": ["tags", 3], "msg": "str type expected", "type": "type_error.str"} - ] - } - - -@needs_pydanticv1 -def test_openapi_schema(client: TestClient): - 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": { - "/items/": { - "post": { - "summary": "Create Item", - "operationId": "create_item_items__post", - "requestBody": { - "content": { - "application/x-yaml": { - "schema": { - "title": "Item", - "required": ["name", "tags"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "tags": { - "title": "Tags", - "type": "array", - "items": {"type": "string"}, - }, - }, - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - } - } - }, - } diff --git a/tests/test_tutorial/test_path_operation_configurations/test_tutorial001.py b/tests/test_tutorial/test_path_operation_configurations/test_tutorial001.py new file mode 100644 index 0000000000..085d1f5e19 --- /dev/null +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial001.py @@ -0,0 +1,186 @@ +import importlib + +import pytest +from dirty_equals import IsList +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial001_py39"), + pytest.param("tutorial001_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest) -> TestClient: + mod = importlib.import_module( + f"docs_src.path_operation_configuration.{request.param}" + ) + return TestClient(mod.app) + + +def test_post_items(client: TestClient): + response = client.post( + "/items/", + json={ + "name": "Foo", + "description": "Item description", + "price": 42.0, + "tax": 3.2, + "tags": ["bar", "baz"], + }, + ) + assert response.status_code == 201, response.text + assert response.json() == { + "name": "Foo", + "description": "Item description", + "price": 42.0, + "tax": 3.2, + "tags": IsList("bar", "baz", check_order=False), + } + + +def test_openapi_schema(client: TestClient): + 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": { + "/items/": { + "post": { + "summary": "Create Item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError", + }, + "title": "Detail", + "type": "array", + }, + }, + "title": "HTTPValidationError", + "type": "object", + }, + "Item": { + "properties": { + "description": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "null", + }, + ], + "title": "Description", + }, + "name": { + "title": "Name", + "type": "string", + }, + "price": { + "title": "Price", + "type": "number", + }, + "tags": { + "default": [], + "items": { + "type": "string", + }, + "title": "Tags", + "type": "array", + "uniqueItems": True, + }, + "tax": { + "anyOf": [ + { + "type": "number", + }, + { + "type": "null", + }, + ], + "title": "Tax", + }, + }, + "required": [ + "name", + "price", + ], + "title": "Item", + "type": "object", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "integer", + }, + ], + }, + "title": "Location", + "type": "array", + }, + "msg": { + "title": "Message", + "type": "string", + }, + "type": { + "title": "Error Type", + "type": "string", + }, + }, + "required": [ + "loc", + "msg", + "type", + ], + "title": "ValidationError", + "type": "object", + }, + }, + }, + } diff --git a/tests/test_tutorial/test_path_operation_configurations/test_tutorial002.py b/tests/test_tutorial/test_path_operation_configurations/test_tutorial002.py new file mode 100644 index 0000000000..c7414d756a --- /dev/null +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial002.py @@ -0,0 +1,223 @@ +import importlib + +import pytest +from dirty_equals import IsList +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial002_py39"), + pytest.param("tutorial002_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest) -> TestClient: + mod = importlib.import_module( + f"docs_src.path_operation_configuration.{request.param}" + ) + return TestClient(mod.app) + + +def test_post_items(client: TestClient): + response = client.post( + "/items/", + json={ + "name": "Foo", + "description": "Item description", + "price": 42.0, + "tax": 3.2, + "tags": ["bar", "baz"], + }, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "name": "Foo", + "description": "Item description", + "price": 42.0, + "tax": 3.2, + "tags": IsList("bar", "baz", check_order=False), + } + + +def test_get_items(client: TestClient): + response = client.get("/items/") + assert response.status_code == 200, response.text + assert response.json() == [{"name": "Foo", "price": 42}] + + +def test_get_users(client: TestClient): + response = client.get("/users/") + assert response.status_code == 200, response.text + assert response.json() == [{"username": "johndoe"}] + + +def test_openapi_schema(client: TestClient): + 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": { + "/items/": { + "get": { + "tags": ["items"], + "summary": "Read Items", + "operationId": "read_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + }, + "post": { + "tags": ["items"], + "summary": "Create Item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + "/users/": { + "get": { + "tags": ["users"], + "summary": "Read Users", + "operationId": "read_users_users__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError", + }, + "title": "Detail", + "type": "array", + }, + }, + "title": "HTTPValidationError", + "type": "object", + }, + "Item": { + "properties": { + "description": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "null", + }, + ], + "title": "Description", + }, + "name": { + "title": "Name", + "type": "string", + }, + "price": { + "title": "Price", + "type": "number", + }, + "tags": { + "default": [], + "items": { + "type": "string", + }, + "title": "Tags", + "type": "array", + "uniqueItems": True, + }, + "tax": { + "anyOf": [ + { + "type": "number", + }, + { + "type": "null", + }, + ], + "title": "Tax", + }, + }, + "required": [ + "name", + "price", + ], + "title": "Item", + "type": "object", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "integer", + }, + ], + }, + "title": "Location", + "type": "array", + }, + "msg": { + "title": "Message", + "type": "string", + }, + "type": { + "title": "Error Type", + "type": "string", + }, + }, + "required": [ + "loc", + "msg", + "type", + ], + "title": "ValidationError", + "type": "object", + }, + }, + }, + } diff --git a/tests/test_tutorial/test_path_operation_configurations/test_tutorial002b.py b/tests/test_tutorial/test_path_operation_configurations/test_tutorial002b.py index 58dec5769d..5a0193adfa 100644 --- a/tests/test_tutorial/test_path_operation_configurations/test_tutorial002b.py +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial002b.py @@ -1,6 +1,6 @@ from fastapi.testclient import TestClient -from docs_src.path_operation_configuration.tutorial002b import app +from docs_src.path_operation_configuration.tutorial002b_py39 import app client = TestClient(app) diff --git a/tests/test_tutorial/test_path_operation_configurations/test_tutorial003_tutorial004.py b/tests/test_tutorial/test_path_operation_configurations/test_tutorial003_tutorial004.py new file mode 100644 index 0000000000..791db24625 --- /dev/null +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial003_tutorial004.py @@ -0,0 +1,208 @@ +import importlib +from textwrap import dedent + +import pytest +from dirty_equals import IsList +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +DESCRIPTIONS = { + "tutorial003": "Create an item with all the information, name, description, price, tax and a set of unique tags", + "tutorial004": dedent(""" + Create an item with all the information: + + - **name**: each item must have a name + - **description**: a long description + - **price**: required + - **tax**: if the item doesn't have tax, you can omit this + - **tags**: a set of unique tag strings for this item + """).strip(), +} + + +@pytest.fixture( + name="mod_name", + params=[ + pytest.param("tutorial003_py39"), + pytest.param("tutorial003_py310", marks=needs_py310), + pytest.param("tutorial004_py39"), + pytest.param("tutorial004_py310", marks=needs_py310), + ], +) +def get_mod_name(request: pytest.FixtureRequest) -> str: + return request.param + + +@pytest.fixture(name="client") +def get_client(mod_name: str) -> TestClient: + mod = importlib.import_module(f"docs_src.path_operation_configuration.{mod_name}") + return TestClient(mod.app) + + +def test_post_items(client: TestClient): + response = client.post( + "/items/", + json={ + "name": "Foo", + "description": "Item description", + "price": 42.0, + "tax": 3.2, + "tags": ["bar", "baz"], + }, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "name": "Foo", + "description": "Item description", + "price": 42.0, + "tax": 3.2, + "tags": IsList("bar", "baz", check_order=False), + } + + +def test_openapi_schema(client: TestClient, mod_name: str): + mod_name = mod_name[:11] + + 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": { + "/items/": { + "post": { + "summary": "Create an item", + "description": DESCRIPTIONS[mod_name], + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError", + }, + "title": "Detail", + "type": "array", + }, + }, + "title": "HTTPValidationError", + "type": "object", + }, + "Item": { + "properties": { + "description": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "null", + }, + ], + "title": "Description", + }, + "name": { + "title": "Name", + "type": "string", + }, + "price": { + "title": "Price", + "type": "number", + }, + "tags": { + "default": [], + "items": { + "type": "string", + }, + "title": "Tags", + "type": "array", + "uniqueItems": True, + }, + "tax": { + "anyOf": [ + { + "type": "number", + }, + { + "type": "null", + }, + ], + "title": "Tax", + }, + }, + "required": [ + "name", + "price", + ], + "title": "Item", + "type": "object", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "integer", + }, + ], + }, + "title": "Location", + "type": "array", + }, + "msg": { + "title": "Message", + "type": "string", + }, + "type": { + "title": "Error Type", + "type": "string", + }, + }, + "required": [ + "loc", + "msg", + "type", + ], + "title": "ValidationError", + "type": "object", + }, + }, + }, + } diff --git a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005.py b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005.py index 0742f5d0e8..c5a3aec1d9 100644 --- a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005.py +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005.py @@ -3,14 +3,13 @@ import importlib import pytest from fastapi.testclient import TestClient -from ...utils import needs_py39, needs_py310, needs_pydanticv1, needs_pydanticv2 +from ...utils import needs_py310 @pytest.fixture( name="client", params=[ - "tutorial005", - pytest.param("tutorial005_py39", marks=needs_py39), + pytest.param("tutorial005_py39"), pytest.param("tutorial005_py310", marks=needs_py310), ], ) @@ -35,7 +34,6 @@ def test_query_params_str_validations(client: TestClient): } -@needs_pydanticv2 def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text @@ -135,100 +133,3 @@ def test_openapi_schema(client: TestClient): } }, } - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -def test_openapi_schema_pv1(client: TestClient): - 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": { - "/items/": { - "post": { - "responses": { - "200": { - "description": "The created item", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Create an item", - "description": "Create an item with all the information:\n\n- **name**: each item must have a name\n- **description**: a long description\n- **price**: required\n- **tax**: if the item doesn't have tax, you can omit this\n- **tags**: a set of unique tag strings for this item", - "operationId": "create_item_items__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": {"title": "Description", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "tax": {"title": "Tax", "type": "number"}, - "tags": { - "title": "Tags", - "uniqueItems": True, - "type": "array", - "items": {"type": "string"}, - "default": [], - }, - }, - }, - "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"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_path_operation_configurations/test_tutorial006.py b/tests/test_tutorial/test_path_operation_configurations/test_tutorial006.py index 91180d1097..5d9c55675f 100644 --- a/tests/test_tutorial/test_path_operation_configurations/test_tutorial006.py +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial006.py @@ -1,7 +1,7 @@ import pytest from fastapi.testclient import TestClient -from docs_src.path_operation_configuration.tutorial006 import app +from docs_src.path_operation_configuration.tutorial006_py39 import app client = TestClient(app) diff --git a/tests/test_tutorial/test_path_params/test_tutorial001.py b/tests/test_tutorial/test_path_params/test_tutorial001.py new file mode 100644 index 0000000000..a898e386fb --- /dev/null +++ b/tests/test_tutorial/test_path_params/test_tutorial001.py @@ -0,0 +1,116 @@ +import pytest +from fastapi.testclient import TestClient + +from docs_src.path_params.tutorial001_py39 import app + +client = TestClient(app) + + +@pytest.mark.parametrize( + ("item_id", "expected_response"), + [ + (1, {"item_id": "1"}), + ("alice", {"item_id": "alice"}), + ], +) +def test_get_items(item_id, expected_response): + response = client.get(f"/items/{item_id}") + assert response.status_code == 200, response.text + assert response.json() == expected_response + + +def test_openapi_schema(): + 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": { + "/items/{item_id}": { + "get": { + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "in": "path", + "name": "item_id", + "required": True, + "schema": { + "title": "Item Id", + }, + }, + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": {}, + }, + }, + "description": "Successful Response", + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError", + }, + }, + }, + "description": "Validation Error", + }, + }, + "summary": "Read Item", + }, + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError", + }, + "title": "Detail", + "type": "array", + }, + }, + "title": "HTTPValidationError", + "type": "object", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "integer", + }, + ], + }, + "title": "Location", + "type": "array", + }, + "msg": { + "title": "Message", + "type": "string", + }, + "type": { + "title": "Error Type", + "type": "string", + }, + }, + "required": [ + "loc", + "msg", + "type", + ], + "title": "ValidationError", + "type": "object", + }, + }, + }, + } diff --git a/tests/test_tutorial/test_path_params/test_tutorial002.py b/tests/test_tutorial/test_path_params/test_tutorial002.py new file mode 100644 index 0000000000..0bfc9f807e --- /dev/null +++ b/tests/test_tutorial/test_path_params/test_tutorial002.py @@ -0,0 +1,124 @@ +from fastapi.testclient import TestClient + +from docs_src.path_params.tutorial002_py39 import app + +client = TestClient(app) + + +def test_get_items(): + response = client.get("/items/1") + assert response.status_code == 200, response.text + assert response.json() == {"item_id": 1} + + +def test_get_items_invalid_id(): + response = client.get("/items/item1") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "input": "item1", + "loc": ["path", "item_id"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "type": "int_parsing", + } + ] + } + + +def test_openapi_schema(): + 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": { + "/items/{item_id}": { + "get": { + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "in": "path", + "name": "item_id", + "required": True, + "schema": { + "title": "Item Id", + "type": "integer", + }, + }, + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": {}, + }, + }, + "description": "Successful Response", + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError", + }, + }, + }, + "description": "Validation Error", + }, + }, + "summary": "Read Item", + }, + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError", + }, + "title": "Detail", + "type": "array", + }, + }, + "title": "HTTPValidationError", + "type": "object", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "integer", + }, + ], + }, + "title": "Location", + "type": "array", + }, + "msg": { + "title": "Message", + "type": "string", + }, + "type": { + "title": "Error Type", + "type": "string", + }, + }, + "required": [ + "loc", + "msg", + "type", + ], + "title": "ValidationError", + "type": "object", + }, + }, + }, + } diff --git a/tests/test_tutorial/test_path_params/test_tutorial003.py b/tests/test_tutorial/test_path_params/test_tutorial003.py new file mode 100644 index 0000000000..cd2c39ab06 --- /dev/null +++ b/tests/test_tutorial/test_path_params/test_tutorial003.py @@ -0,0 +1,133 @@ +import pytest +from fastapi.testclient import TestClient + +from docs_src.path_params.tutorial003_py39 import app + +client = TestClient(app) + + +@pytest.mark.parametrize( + ("user_id", "expected_response"), + [ + ("me", {"user_id": "the current user"}), + ("alice", {"user_id": "alice"}), + ], +) +def test_get_users(user_id: str, expected_response: dict): + response = client.get(f"/users/{user_id}") + assert response.status_code == 200, response.text + assert response.json() == expected_response + + +def test_openapi_schema(): + 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": { + "/users/me": { + "get": { + "operationId": "read_user_me_users_me_get", + "responses": { + "200": { + "content": { + "application/json": { + "schema": {}, + }, + }, + "description": "Successful Response", + }, + }, + "summary": "Read User Me", + }, + }, + "/users/{user_id}": { + "get": { + "operationId": "read_user_users__user_id__get", + "parameters": [ + { + "in": "path", + "name": "user_id", + "required": True, + "schema": { + "title": "User Id", + "type": "string", + }, + }, + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": {}, + }, + }, + "description": "Successful Response", + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError", + }, + }, + }, + "description": "Validation Error", + }, + }, + "summary": "Read User", + }, + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError", + }, + "title": "Detail", + "type": "array", + }, + }, + "title": "HTTPValidationError", + "type": "object", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "integer", + }, + ], + }, + "title": "Location", + "type": "array", + }, + "msg": { + "title": "Message", + "type": "string", + }, + "type": { + "title": "Error Type", + "type": "string", + }, + }, + "required": [ + "loc", + "msg", + "type", + ], + "title": "ValidationError", + "type": "object", + }, + }, + }, + } diff --git a/tests/test_tutorial/test_path_params/test_tutorial003b.py b/tests/test_tutorial/test_path_params/test_tutorial003b.py new file mode 100644 index 0000000000..8e4a26a1ca --- /dev/null +++ b/tests/test_tutorial/test_path_params/test_tutorial003b.py @@ -0,0 +1,44 @@ +import asyncio + +from fastapi.testclient import TestClient + +from docs_src.path_params.tutorial003b_py39 import app, read_users2 + +client = TestClient(app) + + +def test_get_users(): + response = client.get("/users") + assert response.status_code == 200, response.text + assert response.json() == ["Rick", "Morty"] + + +def test_read_users2(): # Just for coverage + assert asyncio.run(read_users2()) == ["Bean", "Elfo"] + + +def test_openapi_schema(): + 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": { + "/users": { + "get": { + "operationId": "read_users2_users_get", + "responses": { + "200": { + "content": { + "application/json": { + "schema": {}, + }, + }, + "description": "Successful Response", + }, + }, + "summary": "Read Users2", + }, + }, + }, + } diff --git a/tests/test_tutorial/test_path_params/test_tutorial004.py b/tests/test_tutorial/test_path_params/test_tutorial004.py index acbeaca769..f7f233ccff 100644 --- a/tests/test_tutorial/test_path_params/test_tutorial004.py +++ b/tests/test_tutorial/test_path_params/test_tutorial004.py @@ -1,6 +1,6 @@ from fastapi.testclient import TestClient -from docs_src.path_params.tutorial004 import app +from docs_src.path_params.tutorial004_py39 import app client = TestClient(app) diff --git a/tests/test_tutorial/test_path_params/test_tutorial005.py b/tests/test_tutorial/test_path_params/test_tutorial005.py index 2e4b0146be..86ccce7b6d 100644 --- a/tests/test_tutorial/test_path_params/test_tutorial005.py +++ b/tests/test_tutorial/test_path_params/test_tutorial005.py @@ -1,7 +1,6 @@ -from dirty_equals import IsDict from fastapi.testclient import TestClient -from docs_src.path_params.tutorial005 import app +from docs_src.path_params.tutorial005_py39 import app client = TestClient(app) @@ -27,31 +26,17 @@ def test_get_enums_resnet(): def test_get_enums_invalid(): response = client.get("/models/foo") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "enum", - "loc": ["path", "model_name"], - "msg": "Input should be 'alexnet', 'resnet' or 'lenet'", - "input": "foo", - "ctx": {"expected": "'alexnet', 'resnet' or 'lenet'"}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "ctx": {"enum_values": ["alexnet", "resnet", "lenet"]}, - "loc": ["path", "model_name"], - "msg": "value is not a valid enumeration member; permitted: 'alexnet', 'resnet', 'lenet'", - "type": "type_error.enum", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "enum", + "loc": ["path", "model_name"], + "msg": "Input should be 'alexnet', 'resnet' or 'lenet'", + "input": "foo", + "ctx": {"expected": "'alexnet', 'resnet' or 'lenet'"}, + } + ] + } def test_openapi_schema(): @@ -106,22 +91,11 @@ def test_openapi_schema(): } }, }, - "ModelName": IsDict( - { - "title": "ModelName", - "enum": ["alexnet", "resnet", "lenet"], - "type": "string", - } - ) - | IsDict( - { - # TODO: remove when deprecating Pydantic v1 - "title": "ModelName", - "enum": ["alexnet", "resnet", "lenet"], - "type": "string", - "description": "An enumeration.", - } - ), + "ModelName": { + "title": "ModelName", + "enum": ["alexnet", "resnet", "lenet"], + "type": "string", + }, "ValidationError": { "title": "ValidationError", "required": ["loc", "msg", "type"], diff --git a/tests/test_tutorial/test_path_params_numeric_validations/__init__.py b/tests/test_tutorial/test_path_params_numeric_validations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial001.py b/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial001.py new file mode 100644 index 0000000000..f1e3041030 --- /dev/null +++ b/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial001.py @@ -0,0 +1,164 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial001_py39"), + pytest.param("tutorial001_py310", marks=needs_py310), + pytest.param("tutorial001_an_py39"), + pytest.param("tutorial001_an_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest) -> TestClient: + mod = importlib.import_module( + f"docs_src.path_params_numeric_validations.{request.param}" + ) + return TestClient(mod.app) + + +@pytest.mark.parametrize( + "path,expected_response", + [ + ("/items/42", {"item_id": 42}), + ("/items/123?item-query=somequery", {"item_id": 123, "q": "somequery"}), + ], +) +def test_read_items(client: TestClient, path, expected_response): + response = client.get(path) + assert response.status_code == 200, response.text + assert response.json() == expected_response + + +def test_read_items_invalid_item_id(client: TestClient): + response = client.get("/items/invalid_id") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["path", "item_id"], + "input": "invalid_id", + "msg": "Input should be a valid integer, unable to parse string as an integer", + "type": "int_parsing", + } + ] + } + + +def test_openapi_schema(client: TestClient): + 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": { + "/items/{item_id}": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "The ID of the item to get", + "type": "integer", + }, + "name": "item_id", + "in": "path", + }, + { + "required": False, + "schema": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "null", + }, + ], + "title": "Item-Query", + }, + "name": "item-query", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {}, + } + }, + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError", + }, + }, + }, + "description": "Validation Error", + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError", + }, + "title": "Detail", + "type": "array", + }, + }, + "title": "HTTPValidationError", + "type": "object", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "integer", + }, + ], + }, + "title": "Location", + "type": "array", + }, + "msg": { + "title": "Message", + "type": "string", + }, + "type": { + "title": "Error Type", + "type": "string", + }, + }, + "required": [ + "loc", + "msg", + "type", + ], + "title": "ValidationError", + "type": "object", + }, + }, + }, + } diff --git a/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial002_tutorial003.py b/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial002_tutorial003.py new file mode 100644 index 0000000000..467c915dcd --- /dev/null +++ b/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial002_tutorial003.py @@ -0,0 +1,170 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial002_py39"), + pytest.param("tutorial002_an_py39"), + pytest.param("tutorial003_py39"), + pytest.param("tutorial003_an_py39"), + ], +) +def get_client(request: pytest.FixtureRequest) -> TestClient: + mod = importlib.import_module( + f"docs_src.path_params_numeric_validations.{request.param}" + ) + return TestClient(mod.app) + + +@pytest.mark.parametrize( + "path,expected_response", + [ + ("/items/42?q=", {"item_id": 42}), + ("/items/123?q=somequery", {"item_id": 123, "q": "somequery"}), + ], +) +def test_read_items(client: TestClient, path, expected_response): + response = client.get(path) + assert response.status_code == 200, response.text + assert response.json() == expected_response + + +def test_read_items_invalid_item_id(client: TestClient): + response = client.get("/items/invalid_id?q=somequery") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["path", "item_id"], + "input": "invalid_id", + "msg": "Input should be a valid integer, unable to parse string as an integer", + "type": "int_parsing", + } + ] + } + + +def test_read_items_missing_q(client: TestClient): + response = client.get("/items/42") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["query", "q"], + "input": None, + "msg": "Field required", + "type": "missing", + } + ] + } + + +def test_openapi_schema(client: TestClient): + 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": { + "/items/{item_id}": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "The ID of the item to get", + "type": "integer", + }, + "name": "item_id", + "in": "path", + }, + { + "required": True, + "schema": { + "type": "string", + "title": "Q", + }, + "name": "q", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {}, + } + }, + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError", + }, + }, + }, + "description": "Validation Error", + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError", + }, + "title": "Detail", + "type": "array", + }, + }, + "title": "HTTPValidationError", + "type": "object", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "integer", + }, + ], + }, + "title": "Location", + "type": "array", + }, + "msg": { + "title": "Message", + "type": "string", + }, + "type": { + "title": "Error Type", + "type": "string", + }, + }, + "required": [ + "loc", + "msg", + "type", + ], + "title": "ValidationError", + "type": "object", + }, + }, + }, + } diff --git a/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial004.py b/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial004.py new file mode 100644 index 0000000000..d3593c984c --- /dev/null +++ b/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial004.py @@ -0,0 +1,185 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial004_py39"), + pytest.param("tutorial004_an_py39"), + ], +) +def get_client(request: pytest.FixtureRequest) -> TestClient: + mod = importlib.import_module( + f"docs_src.path_params_numeric_validations.{request.param}" + ) + return TestClient(mod.app) + + +@pytest.mark.parametrize( + "path,expected_response", + [ + ("/items/42?q=", {"item_id": 42}), + ("/items/1?q=somequery", {"item_id": 1, "q": "somequery"}), + ], +) +def test_read_items(client: TestClient, path, expected_response): + response = client.get(path) + assert response.status_code == 200, response.text + assert response.json() == expected_response + + +def test_read_items_non_int_item_id(client: TestClient): + response = client.get("/items/invalid_id?q=somequery") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["path", "item_id"], + "input": "invalid_id", + "msg": "Input should be a valid integer, unable to parse string as an integer", + "type": "int_parsing", + } + ] + } + + +def test_read_items_item_id_less_than_one(client: TestClient): + response = client.get("/items/0?q=somequery") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["path", "item_id"], + "input": "0", + "msg": "Input should be greater than or equal to 1", + "type": "greater_than_equal", + "ctx": {"ge": 1}, + } + ] + } + + +def test_read_items_missing_q(client: TestClient): + response = client.get("/items/42") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["query", "q"], + "input": None, + "msg": "Field required", + "type": "missing", + } + ] + } + + +def test_openapi_schema(client: TestClient): + 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": { + "/items/{item_id}": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "The ID of the item to get", + "type": "integer", + "minimum": 1, + }, + "name": "item_id", + "in": "path", + }, + { + "required": True, + "schema": { + "type": "string", + "title": "Q", + }, + "name": "q", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {}, + } + }, + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError", + }, + }, + }, + "description": "Validation Error", + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError", + }, + "title": "Detail", + "type": "array", + }, + }, + "title": "HTTPValidationError", + "type": "object", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "integer", + }, + ], + }, + "title": "Location", + "type": "array", + }, + "msg": { + "title": "Message", + "type": "string", + }, + "type": { + "title": "Error Type", + "type": "string", + }, + }, + "required": [ + "loc", + "msg", + "type", + ], + "title": "ValidationError", + "type": "object", + }, + }, + }, + } diff --git a/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial005.py b/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial005.py new file mode 100644 index 0000000000..296192593b --- /dev/null +++ b/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial005.py @@ -0,0 +1,202 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial005_py39"), + pytest.param("tutorial005_an_py39"), + ], +) +def get_client(request: pytest.FixtureRequest) -> TestClient: + mod = importlib.import_module( + f"docs_src.path_params_numeric_validations.{request.param}" + ) + return TestClient(mod.app) + + +@pytest.mark.parametrize( + "path,expected_response", + [ + ("/items/1?q=", {"item_id": 1}), + ("/items/1000?q=somequery", {"item_id": 1000, "q": "somequery"}), + ], +) +def test_read_items(client: TestClient, path, expected_response): + response = client.get(path) + assert response.status_code == 200, response.text + assert response.json() == expected_response + + +def test_read_items_non_int_item_id(client: TestClient): + response = client.get("/items/invalid_id?q=somequery") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["path", "item_id"], + "input": "invalid_id", + "msg": "Input should be a valid integer, unable to parse string as an integer", + "type": "int_parsing", + } + ] + } + + +def test_read_items_item_id_less_than_one(client: TestClient): + response = client.get("/items/0?q=somequery") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["path", "item_id"], + "input": "0", + "msg": "Input should be greater than 0", + "type": "greater_than", + "ctx": {"gt": 0}, + } + ] + } + + +def test_read_items_item_id_greater_than_one_thousand(client: TestClient): + response = client.get("/items/1001?q=somequery") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["path", "item_id"], + "input": "1001", + "msg": "Input should be less than or equal to 1000", + "type": "less_than_equal", + "ctx": {"le": 1000}, + } + ] + } + + +def test_read_items_missing_q(client: TestClient): + response = client.get("/items/42") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["query", "q"], + "input": None, + "msg": "Field required", + "type": "missing", + } + ] + } + + +def test_openapi_schema(client: TestClient): + 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": { + "/items/{item_id}": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "The ID of the item to get", + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 1000, + }, + "name": "item_id", + "in": "path", + }, + { + "required": True, + "schema": { + "type": "string", + "title": "Q", + }, + "name": "q", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {}, + } + }, + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError", + }, + }, + }, + "description": "Validation Error", + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError", + }, + "title": "Detail", + "type": "array", + }, + }, + "title": "HTTPValidationError", + "type": "object", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "integer", + }, + ], + }, + "title": "Location", + "type": "array", + }, + "msg": { + "title": "Message", + "type": "string", + }, + "type": { + "title": "Error Type", + "type": "string", + }, + }, + "required": [ + "loc", + "msg", + "type", + ], + "title": "ValidationError", + "type": "object", + }, + }, + }, + } diff --git a/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial006.py b/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial006.py new file mode 100644 index 0000000000..9dc7d7aac2 --- /dev/null +++ b/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial006.py @@ -0,0 +1,221 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial006_py39"), + pytest.param("tutorial006_an_py39"), + ], +) +def get_client(request: pytest.FixtureRequest) -> TestClient: + mod = importlib.import_module( + f"docs_src.path_params_numeric_validations.{request.param}" + ) + return TestClient(mod.app) + + +@pytest.mark.parametrize( + "path,expected_response", + [ + ( + "/items/0?q=&size=0.1", + {"item_id": 0, "size": 0.1}, + ), + ( + "/items/1000?q=somequery&size=10.4", + {"item_id": 1000, "q": "somequery", "size": 10.4}, + ), + ], +) +def test_read_items(client: TestClient, path, expected_response): + response = client.get(path) + assert response.status_code == 200, response.text + assert response.json() == expected_response + + +def test_read_items_item_id_less_than_zero(client: TestClient): + response = client.get("/items/-1?q=somequery&size=5") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["path", "item_id"], + "input": "-1", + "msg": "Input should be greater than or equal to 0", + "type": "greater_than_equal", + "ctx": {"ge": 0}, + } + ] + } + + +def test_read_items_item_id_greater_than_one_thousand(client: TestClient): + response = client.get("/items/1001?q=somequery&size=5") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["path", "item_id"], + "input": "1001", + "msg": "Input should be less than or equal to 1000", + "type": "less_than_equal", + "ctx": {"le": 1000}, + } + ] + } + + +def test_read_items_size_too_small(client: TestClient): + response = client.get("/items/1?q=somequery&size=0.0") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["query", "size"], + "input": "0.0", + "msg": "Input should be greater than 0", + "type": "greater_than", + "ctx": {"gt": 0.0}, + } + ] + } + + +def test_read_items_size_too_large(client: TestClient): + response = client.get("/items/1?q=somequery&size=10.5") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["query", "size"], + "input": "10.5", + "msg": "Input should be less than 10.5", + "type": "less_than", + "ctx": {"lt": 10.5}, + } + ] + } + + +def test_openapi_schema(client: TestClient): + 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": { + "/items/{item_id}": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "The ID of the item to get", + "type": "integer", + "minimum": 0, + "maximum": 1000, + }, + "name": "item_id", + "in": "path", + }, + { + "required": True, + "schema": { + "type": "string", + "title": "Q", + }, + "name": "q", + "in": "query", + }, + { + "in": "query", + "name": "size", + "required": True, + "schema": { + "exclusiveMaximum": 10.5, + "exclusiveMinimum": 0, + "title": "Size", + "type": "number", + }, + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {}, + } + }, + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError", + }, + }, + }, + "description": "Validation Error", + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError", + }, + "title": "Detail", + "type": "array", + }, + }, + "title": "HTTPValidationError", + "type": "object", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "integer", + }, + ], + }, + "title": "Location", + "type": "array", + }, + "msg": { + "title": "Message", + "type": "string", + }, + "type": { + "title": "Error Type", + "type": "string", + }, + }, + "required": [ + "loc", + "msg", + "type", + ], + "title": "ValidationError", + "type": "object", + }, + }, + }, + } diff --git a/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial001.py b/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial001.py deleted file mode 100644 index 3075a05f51..0000000000 --- a/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial001.py +++ /dev/null @@ -1,37 +0,0 @@ -import sys -from typing import Any - -import pytest -from fastapi._compat import PYDANTIC_V2 - -from tests.utils import skip_module_if_py_gte_314 - -if sys.version_info >= (3, 14): - skip_module_if_py_gte_314() - - -if not PYDANTIC_V2: - pytest.skip("This test is only for Pydantic v2", allow_module_level=True) - -import importlib - -import pytest - -from ...utils import needs_py310 - - -@pytest.fixture( - name="mod", - params=[ - "tutorial001_an", - pytest.param("tutorial001_an_py310", marks=needs_py310), - ], -) -def get_mod(request: pytest.FixtureRequest): - mod = importlib.import_module(f"docs_src.pydantic_v1_in_v2.{request.param}") - return mod - - -def test_model(mod: Any): - item = mod.Item(name="Foo", size=3.4) - assert item.dict() == {"name": "Foo", "description": None, "size": 3.4} diff --git a/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial002.py b/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial002.py deleted file mode 100644 index a402c663d1..0000000000 --- a/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial002.py +++ /dev/null @@ -1,140 +0,0 @@ -import sys - -import pytest -from fastapi._compat import PYDANTIC_V2 -from inline_snapshot import snapshot - -from tests.utils import skip_module_if_py_gte_314 - -if sys.version_info >= (3, 14): - skip_module_if_py_gte_314() - - -if not PYDANTIC_V2: - pytest.skip("This test is only for Pydantic v2", allow_module_level=True) - -import importlib - -import pytest -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@pytest.fixture( - name="client", - params=[ - "tutorial002_an", - pytest.param("tutorial002_an_py310", marks=needs_py310), - ], -) -def get_client(request: pytest.FixtureRequest): - mod = importlib.import_module(f"docs_src.pydantic_v1_in_v2.{request.param}") - - c = TestClient(mod.app) - return c - - -def test_call(client: TestClient): - response = client.post("/items/", json={"name": "Foo", "size": 3.4}) - assert response.status_code == 200, response.text - assert response.json() == { - "name": "Foo", - "description": None, - "size": 3.4, - } - - -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == snapshot( - { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "post": { - "summary": "Create Item", - "operationId": "create_item_items__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - "title": "Item", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "properties": { - "detail": { - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - "type": "array", - "title": "Detail", - } - }, - "type": "object", - "title": "HTTPValidationError", - }, - "Item": { - "properties": { - "name": {"type": "string", "title": "Name"}, - "description": {"type": "string", "title": "Description"}, - "size": {"type": "number", "title": "Size"}, - }, - "type": "object", - "required": ["name", "size"], - "title": "Item", - }, - "ValidationError": { - "properties": { - "loc": { - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - "type": "array", - "title": "Location", - }, - "msg": {"type": "string", "title": "Message"}, - "type": {"type": "string", "title": "Error Type"}, - }, - "type": "object", - "required": ["loc", "msg", "type"], - "title": "ValidationError", - }, - } - }, - } - ) diff --git a/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial003.py b/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial003.py deleted file mode 100644 index 03155c9249..0000000000 --- a/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial003.py +++ /dev/null @@ -1,154 +0,0 @@ -import sys - -import pytest -from fastapi._compat import PYDANTIC_V2 -from inline_snapshot import snapshot - -from tests.utils import skip_module_if_py_gte_314 - -if sys.version_info >= (3, 14): - skip_module_if_py_gte_314() - -if not PYDANTIC_V2: - pytest.skip("This test is only for Pydantic v2", allow_module_level=True) - - -import importlib - -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@pytest.fixture( - name="client", - params=[ - "tutorial003_an", - pytest.param("tutorial003_an_py310", marks=needs_py310), - ], -) -def get_client(request: pytest.FixtureRequest): - mod = importlib.import_module(f"docs_src.pydantic_v1_in_v2.{request.param}") - - c = TestClient(mod.app) - return c - - -def test_call(client: TestClient): - response = client.post("/items/", json={"name": "Foo", "size": 3.4}) - assert response.status_code == 200, response.text - assert response.json() == { - "name": "Foo", - "description": None, - "size": 3.4, - } - - -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == snapshot( - { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "post": { - "summary": "Create Item", - "operationId": "create_item_items__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - "title": "Item", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ItemV2" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "properties": { - "detail": { - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - "type": "array", - "title": "Detail", - } - }, - "type": "object", - "title": "HTTPValidationError", - }, - "Item": { - "properties": { - "name": {"type": "string", "title": "Name"}, - "description": {"type": "string", "title": "Description"}, - "size": {"type": "number", "title": "Size"}, - }, - "type": "object", - "required": ["name", "size"], - "title": "Item", - }, - "ItemV2": { - "properties": { - "name": {"type": "string", "title": "Name"}, - "description": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Description", - }, - "size": {"type": "number", "title": "Size"}, - }, - "type": "object", - "required": ["name", "size"], - "title": "ItemV2", - }, - "ValidationError": { - "properties": { - "loc": { - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - "type": "array", - "title": "Location", - }, - "msg": {"type": "string", "title": "Message"}, - "type": {"type": "string", "title": "Error Type"}, - }, - "type": "object", - "required": ["loc", "msg", "type"], - "title": "ValidationError", - }, - } - }, - } - ) diff --git a/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial004.py b/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial004.py deleted file mode 100644 index d2e204ddaf..0000000000 --- a/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial004.py +++ /dev/null @@ -1,153 +0,0 @@ -import sys - -import pytest -from fastapi._compat import PYDANTIC_V2 -from inline_snapshot import snapshot - -from tests.utils import skip_module_if_py_gte_314 - -if sys.version_info >= (3, 14): - skip_module_if_py_gte_314() - -if not PYDANTIC_V2: - pytest.skip("This test is only for Pydantic v2", allow_module_level=True) - - -import importlib - -from fastapi.testclient import TestClient - -from ...utils import needs_py39, needs_py310 - - -@pytest.fixture( - name="client", - params=[ - "tutorial004_an", - pytest.param("tutorial004_an_py39", marks=needs_py39), - pytest.param("tutorial004_an_py310", marks=needs_py310), - ], -) -def get_client(request: pytest.FixtureRequest): - mod = importlib.import_module(f"docs_src.pydantic_v1_in_v2.{request.param}") - - c = TestClient(mod.app) - return c - - -def test_call(client: TestClient): - response = client.post("/items/", json={"item": {"name": "Foo", "size": 3.4}}) - assert response.status_code == 200, response.text - assert response.json() == { - "name": "Foo", - "description": None, - "size": 3.4, - } - - -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == snapshot( - { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "post": { - "summary": "Create Item", - "operationId": "create_item_items__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Body_create_item_items__post" - } - ], - "title": "Body", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "Body_create_item_items__post": { - "properties": { - "item": { - "allOf": [{"$ref": "#/components/schemas/Item"}], - "title": "Item", - } - }, - "type": "object", - "required": ["item"], - "title": "Body_create_item_items__post", - }, - "HTTPValidationError": { - "properties": { - "detail": { - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - "type": "array", - "title": "Detail", - } - }, - "type": "object", - "title": "HTTPValidationError", - }, - "Item": { - "properties": { - "name": {"type": "string", "title": "Name"}, - "description": {"type": "string", "title": "Description"}, - "size": {"type": "number", "title": "Size"}, - }, - "type": "object", - "required": ["name", "size"], - "title": "Item", - }, - "ValidationError": { - "properties": { - "loc": { - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - "type": "array", - "title": "Location", - }, - "msg": {"type": "string", "title": "Message"}, - "type": {"type": "string", "title": "Error Type"}, - }, - "type": "object", - "required": ["loc", "msg", "type"], - "title": "ValidationError", - }, - } - }, - } - ) diff --git a/tests/test_tutorial/test_python_types/__init__.py b/tests/test_tutorial/test_python_types/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/test_tutorial/test_python_types/test_tutorial001_tutorial002.py b/tests/test_tutorial/test_python_types/test_tutorial001_tutorial002.py new file mode 100644 index 0000000000..ccb0968576 --- /dev/null +++ b/tests/test_tutorial/test_python_types/test_tutorial001_tutorial002.py @@ -0,0 +1,18 @@ +import runpy +from unittest.mock import patch + +import pytest + + +@pytest.mark.parametrize( + "module_name", + [ + "tutorial001_py39", + "tutorial002_py39", + ], +) +def test_run_module(module_name: str): + with patch("builtins.print") as mock_print: + runpy.run_module(f"docs_src.python_types.{module_name}", run_name="__main__") + + mock_print.assert_called_with("John Doe") diff --git a/tests/test_tutorial/test_python_types/test_tutorial003.py b/tests/test_tutorial/test_python_types/test_tutorial003.py new file mode 100644 index 0000000000..34d2649171 --- /dev/null +++ b/tests/test_tutorial/test_python_types/test_tutorial003.py @@ -0,0 +1,12 @@ +import pytest + +from docs_src.python_types.tutorial003_py39 import get_name_with_age + + +def test_get_name_with_age_pass_int(): + with pytest.raises(TypeError): + get_name_with_age("John", 30) + + +def test_get_name_with_age_pass_str(): + assert get_name_with_age("John", "30") == "John is this old: 30" diff --git a/tests/test_tutorial/test_python_types/test_tutorial004.py b/tests/test_tutorial/test_python_types/test_tutorial004.py new file mode 100644 index 0000000000..24af32883e --- /dev/null +++ b/tests/test_tutorial/test_python_types/test_tutorial004.py @@ -0,0 +1,5 @@ +from docs_src.python_types.tutorial004_py39 import get_name_with_age + + +def test_get_name_with_age_pass_int(): + assert get_name_with_age("John", 30) == "John is this old: 30" diff --git a/tests/test_tutorial/test_python_types/test_tutorial005.py b/tests/test_tutorial/test_python_types/test_tutorial005.py new file mode 100644 index 0000000000..6d67ec4716 --- /dev/null +++ b/tests/test_tutorial/test_python_types/test_tutorial005.py @@ -0,0 +1,12 @@ +from docs_src.python_types.tutorial005_py39 import get_items + + +def test_get_items(): + res = get_items( + "item_a", + "item_b", + "item_c", + "item_d", + "item_e", + ) + assert res == ("item_a", "item_b", "item_c", "item_d", "item_e") diff --git a/tests/test_tutorial/test_python_types/test_tutorial006.py b/tests/test_tutorial/test_python_types/test_tutorial006.py new file mode 100644 index 0000000000..50976926e7 --- /dev/null +++ b/tests/test_tutorial/test_python_types/test_tutorial006.py @@ -0,0 +1,16 @@ +from unittest.mock import patch + +from docs_src.python_types.tutorial006_py39 import process_items + + +def test_process_items(): + with patch("builtins.print") as mock_print: + process_items(["item_a", "item_b", "item_c"]) + + assert mock_print.call_count == 3 + call_args = [arg.args for arg in mock_print.call_args_list] + assert call_args == [ + ("item_a",), + ("item_b",), + ("item_c",), + ] diff --git a/tests/test_tutorial/test_python_types/test_tutorial007.py b/tests/test_tutorial/test_python_types/test_tutorial007.py new file mode 100644 index 0000000000..c045294652 --- /dev/null +++ b/tests/test_tutorial/test_python_types/test_tutorial007.py @@ -0,0 +1,8 @@ +from docs_src.python_types.tutorial007_py39 import process_items + + +def test_process_items(): + items_t = (1, 2, "foo") + items_s = {b"a", b"b", b"c"} + + assert process_items(items_t, items_s) == (items_t, items_s) diff --git a/tests/test_tutorial/test_python_types/test_tutorial008.py b/tests/test_tutorial/test_python_types/test_tutorial008.py new file mode 100644 index 0000000000..33cf6cbfbc --- /dev/null +++ b/tests/test_tutorial/test_python_types/test_tutorial008.py @@ -0,0 +1,17 @@ +from unittest.mock import patch + +from docs_src.python_types.tutorial008_py39 import process_items + + +def test_process_items(): + with patch("builtins.print") as mock_print: + process_items({"a": 1.0, "b": 2.5}) + + assert mock_print.call_count == 4 + call_args = [arg.args for arg in mock_print.call_args_list] + assert call_args == [ + ("a",), + (1.0,), + ("b",), + (2.5,), + ] diff --git a/tests/test_tutorial/test_python_types/test_tutorial008b.py b/tests/test_tutorial/test_python_types/test_tutorial008b.py new file mode 100644 index 0000000000..1ef0d4ea16 --- /dev/null +++ b/tests/test_tutorial/test_python_types/test_tutorial008b.py @@ -0,0 +1,27 @@ +import importlib +from types import ModuleType +from unittest.mock import patch + +import pytest + +from ...utils import needs_py310 + + +@pytest.fixture( + name="module", + params=[ + pytest.param("tutorial008b_py39"), + pytest.param("tutorial008b_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.python_types.{request.param}") + return mod + + +def test_process_items(module: ModuleType): + with patch("builtins.print") as mock_print: + module.process_item("a") + + assert mock_print.call_count == 1 + mock_print.assert_called_with("a") diff --git a/tests/test_tutorial/test_python_types/test_tutorial009_tutorial009b.py b/tests/test_tutorial/test_python_types/test_tutorial009_tutorial009b.py new file mode 100644 index 0000000000..34046c5c48 --- /dev/null +++ b/tests/test_tutorial/test_python_types/test_tutorial009_tutorial009b.py @@ -0,0 +1,33 @@ +import importlib +from types import ModuleType +from unittest.mock import patch + +import pytest + +from ...utils import needs_py310 + + +@pytest.fixture( + name="module", + params=[ + pytest.param("tutorial009_py39"), + pytest.param("tutorial009_py310", marks=needs_py310), + pytest.param("tutorial009b_py39"), + ], +) +def get_module(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.python_types.{request.param}") + return mod + + +def test_say_hi(module: ModuleType): + with patch("builtins.print") as mock_print: + module.say_hi("FastAPI") + module.say_hi() + + assert mock_print.call_count == 2 + call_args = [arg.args for arg in mock_print.call_args_list] + assert call_args == [ + ("Hey FastAPI!",), + ("Hello World",), + ] diff --git a/tests/test_tutorial/test_python_types/test_tutorial009c.py b/tests/test_tutorial/test_python_types/test_tutorial009c.py new file mode 100644 index 0000000000..7bd4049113 --- /dev/null +++ b/tests/test_tutorial/test_python_types/test_tutorial009c.py @@ -0,0 +1,33 @@ +import importlib +import re +from types import ModuleType +from unittest.mock import patch + +import pytest + +from ...utils import needs_py310 + + +@pytest.fixture( + name="module", + params=[ + pytest.param("tutorial009c_py39"), + pytest.param("tutorial009c_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.python_types.{request.param}") + return mod + + +def test_say_hi(module: ModuleType): + with patch("builtins.print") as mock_print: + module.say_hi("FastAPI") + + mock_print.assert_called_once_with("Hey FastAPI!") + + with pytest.raises( + TypeError, + match=re.escape("say_hi() missing 1 required positional argument: 'name'"), + ): + module.say_hi() diff --git a/tests/test_tutorial/test_python_types/test_tutorial010.py b/tests/test_tutorial/test_python_types/test_tutorial010.py new file mode 100644 index 0000000000..9e4d2e36bf --- /dev/null +++ b/tests/test_tutorial/test_python_types/test_tutorial010.py @@ -0,0 +1,5 @@ +from docs_src.python_types.tutorial010_py39 import Person, get_person_name + + +def test_get_person_name(): + assert get_person_name(Person("John Doe")) == "John Doe" diff --git a/tests/test_tutorial/test_python_types/test_tutorial011.py b/tests/test_tutorial/test_python_types/test_tutorial011.py new file mode 100644 index 0000000000..a05751b974 --- /dev/null +++ b/tests/test_tutorial/test_python_types/test_tutorial011.py @@ -0,0 +1,25 @@ +import runpy +from unittest.mock import patch + +import pytest + +from ...utils import needs_py310 + + +@pytest.mark.parametrize( + "module_name", + [ + pytest.param("tutorial011_py39"), + pytest.param("tutorial011_py310", marks=needs_py310), + ], +) +def test_run_module(module_name: str): + with patch("builtins.print") as mock_print: + runpy.run_module(f"docs_src.python_types.{module_name}", run_name="__main__") + + assert mock_print.call_count == 2 + call_args = [str(arg.args[0]) for arg in mock_print.call_args_list] + assert call_args == [ + "id=123 name='John Doe' signup_ts=datetime.datetime(2017, 6, 1, 12, 22) friends=[1, 2, 3]", + "123", + ] diff --git a/tests/test_tutorial/test_python_types/test_tutorial012.py b/tests/test_tutorial/test_python_types/test_tutorial012.py new file mode 100644 index 0000000000..e578048204 --- /dev/null +++ b/tests/test_tutorial/test_python_types/test_tutorial012.py @@ -0,0 +1,7 @@ +from docs_src.python_types.tutorial012_py39 import User + + +def test_user(): + user = User(name="John Doe", age=30) + assert user.name == "John Doe" + assert user.age == 30 diff --git a/tests/test_tutorial/test_python_types/test_tutorial013.py b/tests/test_tutorial/test_python_types/test_tutorial013.py new file mode 100644 index 0000000000..5602ef76f8 --- /dev/null +++ b/tests/test_tutorial/test_python_types/test_tutorial013.py @@ -0,0 +1,5 @@ +from docs_src.python_types.tutorial013_py39 import say_hello + + +def test_say_hello(): + assert say_hello("FastAPI") == "Hello FastAPI" diff --git a/tests/test_tutorial/test_query_param_models/test_tutorial001.py b/tests/test_tutorial/test_query_param_models/test_tutorial001.py index 5b7bc7b424..d3ce57121d 100644 --- a/tests/test_tutorial/test_query_param_models/test_tutorial001.py +++ b/tests/test_tutorial/test_query_param_models/test_tutorial001.py @@ -1,21 +1,18 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from inline_snapshot import snapshot -from tests.utils import needs_py39, needs_py310 +from tests.utils import needs_py310 @pytest.fixture( name="client", params=[ - "tutorial001", - pytest.param("tutorial001_py39", marks=needs_py39), + pytest.param("tutorial001_py39"), pytest.param("tutorial001_py310", marks=needs_py310), - "tutorial001_an", - pytest.param("tutorial001_an_py39", marks=needs_py39), + pytest.param("tutorial001_an_py39"), pytest.param("tutorial001_an_py310", marks=needs_py310), ], ) @@ -67,61 +64,31 @@ def test_query_param_model_invalid(client: TestClient): ) assert response.status_code == 422 assert response.json() == snapshot( - IsDict( - { - "detail": [ - { - "type": "less_than_equal", - "loc": ["query", "limit"], - "msg": "Input should be less than or equal to 100", - "input": "150", - "ctx": {"le": 100}, - }, - { - "type": "greater_than_equal", - "loc": ["query", "offset"], - "msg": "Input should be greater than or equal to 0", - "input": "-1", - "ctx": {"ge": 0}, - }, - { - "type": "literal_error", - "loc": ["query", "order_by"], - "msg": "Input should be 'created_at' or 'updated_at'", - "input": "invalid", - "ctx": {"expected": "'created_at' or 'updated_at'"}, - }, - ] - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "type": "value_error.number.not_le", - "loc": ["query", "limit"], - "msg": "ensure this value is less than or equal to 100", - "ctx": {"limit_value": 100}, - }, - { - "type": "value_error.number.not_ge", - "loc": ["query", "offset"], - "msg": "ensure this value is greater than or equal to 0", - "ctx": {"limit_value": 0}, - }, - { - "type": "value_error.const", - "loc": ["query", "order_by"], - "msg": "unexpected value; permitted: 'created_at', 'updated_at'", - "ctx": { - "given": "invalid", - "permitted": ["created_at", "updated_at"], - }, - }, - ] - } - ) + { + "detail": [ + { + "type": "less_than_equal", + "loc": ["query", "limit"], + "msg": "Input should be less than or equal to 100", + "input": "150", + "ctx": {"le": 100}, + }, + { + "type": "greater_than_equal", + "loc": ["query", "offset"], + "msg": "Input should be greater than or equal to 0", + "input": "-1", + "ctx": {"ge": 0}, + }, + { + "type": "literal_error", + "loc": ["query", "order_by"], + "msg": "Input should be 'created_at' or 'updated_at'", + "input": "invalid", + "ctx": {"expected": "'created_at' or 'updated_at'"}, + }, + ] + } ) diff --git a/tests/test_tutorial/test_query_param_models/test_tutorial002.py b/tests/test_tutorial/test_query_param_models/test_tutorial002.py index 4432c9d8a3..96abce6ab9 100644 --- a/tests/test_tutorial/test_query_param_models/test_tutorial002.py +++ b/tests/test_tutorial/test_query_param_models/test_tutorial002.py @@ -1,28 +1,19 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from inline_snapshot import snapshot -from tests.utils import needs_py39, needs_py310, needs_pydanticv1, needs_pydanticv2 +from tests.utils import needs_py310 @pytest.fixture( name="client", params=[ - pytest.param("tutorial002", marks=needs_pydanticv2), - pytest.param("tutorial002_py39", marks=[needs_py39, needs_pydanticv2]), - pytest.param("tutorial002_py310", marks=[needs_py310, needs_pydanticv2]), - pytest.param("tutorial002_an", marks=needs_pydanticv2), - pytest.param("tutorial002_an_py39", marks=[needs_py39, needs_pydanticv2]), - pytest.param("tutorial002_an_py310", marks=[needs_py310, needs_pydanticv2]), - pytest.param("tutorial002_pv1", marks=[needs_pydanticv1, needs_pydanticv1]), - pytest.param("tutorial002_pv1_py39", marks=[needs_py39, needs_pydanticv1]), - pytest.param("tutorial002_pv1_py310", marks=[needs_py310, needs_pydanticv1]), - pytest.param("tutorial002_pv1_an", marks=[needs_pydanticv1]), - pytest.param("tutorial002_pv1_an_py39", marks=[needs_py39, needs_pydanticv1]), - pytest.param("tutorial002_pv1_an_py310", marks=[needs_py310, needs_pydanticv1]), + pytest.param("tutorial002_py39"), + pytest.param("tutorial002_py310", marks=[needs_py310]), + pytest.param("tutorial002_an_py39"), + pytest.param("tutorial002_an_py310", marks=[needs_py310]), ], ) def get_client(request: pytest.FixtureRequest): @@ -73,61 +64,31 @@ def test_query_param_model_invalid(client: TestClient): ) assert response.status_code == 422 assert response.json() == snapshot( - IsDict( - { - "detail": [ - { - "type": "less_than_equal", - "loc": ["query", "limit"], - "msg": "Input should be less than or equal to 100", - "input": "150", - "ctx": {"le": 100}, - }, - { - "type": "greater_than_equal", - "loc": ["query", "offset"], - "msg": "Input should be greater than or equal to 0", - "input": "-1", - "ctx": {"ge": 0}, - }, - { - "type": "literal_error", - "loc": ["query", "order_by"], - "msg": "Input should be 'created_at' or 'updated_at'", - "input": "invalid", - "ctx": {"expected": "'created_at' or 'updated_at'"}, - }, - ] - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "type": "value_error.number.not_le", - "loc": ["query", "limit"], - "msg": "ensure this value is less than or equal to 100", - "ctx": {"limit_value": 100}, - }, - { - "type": "value_error.number.not_ge", - "loc": ["query", "offset"], - "msg": "ensure this value is greater than or equal to 0", - "ctx": {"limit_value": 0}, - }, - { - "type": "value_error.const", - "loc": ["query", "order_by"], - "msg": "unexpected value; permitted: 'created_at', 'updated_at'", - "ctx": { - "given": "invalid", - "permitted": ["created_at", "updated_at"], - }, - }, - ] - } - ) + { + "detail": [ + { + "type": "less_than_equal", + "loc": ["query", "limit"], + "msg": "Input should be less than or equal to 100", + "input": "150", + "ctx": {"le": 100}, + }, + { + "type": "greater_than_equal", + "loc": ["query", "offset"], + "msg": "Input should be greater than or equal to 0", + "input": "-1", + "ctx": {"ge": 0}, + }, + { + "type": "literal_error", + "loc": ["query", "order_by"], + "msg": "Input should be 'created_at' or 'updated_at'", + "input": "invalid", + "ctx": {"expected": "'created_at' or 'updated_at'"}, + }, + ] + } ) @@ -146,22 +107,12 @@ def test_query_param_model_extra(client: TestClient): assert response.json() == snapshot( { "detail": [ - IsDict( - { - "type": "extra_forbidden", - "loc": ["query", "tool"], - "msg": "Extra inputs are not permitted", - "input": "plumbus", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "value_error.extra", - "loc": ["query", "tool"], - "msg": "extra fields not permitted", - } - ) + { + "type": "extra_forbidden", + "loc": ["query", "tool"], + "msg": "Extra inputs are not permitted", + "input": "plumbus", + } ] } ) diff --git a/tests/test_tutorial/test_query_params/test_tutorial001.py b/tests/test_tutorial/test_query_params/test_tutorial001.py new file mode 100644 index 0000000000..4c92b57b8d --- /dev/null +++ b/tests/test_tutorial/test_query_params/test_tutorial001.py @@ -0,0 +1,126 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial001_py39"), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.query_params.{request.param}") + + client = TestClient(mod.app) + return client + + +@pytest.mark.parametrize( + ("path", "expected_json"), + [ + ( + "/items/", + [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}], + ), + ( + "/items/?skip=1", + [{"item_name": "Bar"}, {"item_name": "Baz"}], + ), + ( + "/items/?skip=1&limit=1", + [{"item_name": "Bar"}], + ), + ], +) +def test_read_user_item(client: TestClient, path, expected_json): + response = client.get(path) + assert response.status_code == 200 + assert response.json() == expected_json + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Item", + "operationId": "read_item_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Skip", + "type": "integer", + "default": 0, + }, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "type": "integer", + "default": 10, + }, + "name": "limit", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError", + }, + }, + }, + "description": "Validation Error", + }, + }, + } + } + }, + "components": { + "schemas": { + "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"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params/test_tutorial002.py b/tests/test_tutorial/test_query_params/test_tutorial002.py new file mode 100644 index 0000000000..ae3ee7613d --- /dev/null +++ b/tests/test_tutorial/test_query_params/test_tutorial002.py @@ -0,0 +1,127 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial002_py39"), + pytest.param("tutorial002_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.query_params.{request.param}") + + client = TestClient(mod.app) + return client + + +@pytest.mark.parametrize( + ("path", "expected_json"), + [ + ( + "/items/foo", + {"item_id": "foo"}, + ), + ( + "/items/bar?q=somequery", + {"item_id": "bar", "q": "somequery"}, + ), + ], +) +def test_read_user_item(client: TestClient, path, expected_json): + response = client.get(path) + assert response.status_code == 200 + assert response.json() == expected_json + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + }, + { + "required": False, + "schema": { + "title": "Q", + "anyOf": [ + { + "type": "string", + }, + { + "type": "null", + }, + ], + }, + "name": "q", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError", + }, + }, + }, + "description": "Validation Error", + }, + }, + } + } + }, + "components": { + "schemas": { + "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"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params/test_tutorial003.py b/tests/test_tutorial/test_query_params/test_tutorial003.py new file mode 100644 index 0000000000..c0b7e3b133 --- /dev/null +++ b/tests/test_tutorial/test_query_params/test_tutorial003.py @@ -0,0 +1,148 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial003_py39"), + pytest.param("tutorial003_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.query_params.{request.param}") + + client = TestClient(mod.app) + return client + + +@pytest.mark.parametrize( + ("path", "expected_json"), + [ + ( + "/items/foo", + { + "item_id": "foo", + "description": "This is an amazing item that has a long description", + }, + ), + ( + "/items/bar?q=somequery", + { + "item_id": "bar", + "q": "somequery", + "description": "This is an amazing item that has a long description", + }, + ), + ( + "/items/baz?short=true", + {"item_id": "baz"}, + ), + ], +) +def test_read_user_item(client: TestClient, path, expected_json): + response = client.get(path) + assert response.status_code == 200 + assert response.json() == expected_json + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + }, + { + "required": False, + "schema": { + "title": "Q", + "anyOf": [ + { + "type": "string", + }, + { + "type": "null", + }, + ], + }, + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Short", + "type": "boolean", + "default": False, + }, + "name": "short", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError", + }, + }, + }, + "description": "Validation Error", + }, + }, + } + } + }, + "components": { + "schemas": { + "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"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params/test_tutorial004.py b/tests/test_tutorial/test_query_params/test_tutorial004.py new file mode 100644 index 0000000000..9be18b74df --- /dev/null +++ b/tests/test_tutorial/test_query_params/test_tutorial004.py @@ -0,0 +1,156 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial004_py39"), + pytest.param("tutorial004_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.query_params.{request.param}") + + client = TestClient(mod.app) + return client + + +@pytest.mark.parametrize( + ("path", "expected_json"), + [ + ( + "/users/123/items/foo", + { + "item_id": "foo", + "owner_id": 123, + "description": "This is an amazing item that has a long description", + }, + ), + ( + "/users/1/items/bar?q=somequery", + { + "item_id": "bar", + "owner_id": 1, + "q": "somequery", + "description": "This is an amazing item that has a long description", + }, + ), + ( + "/users/42/items/baz?short=true", + {"item_id": "baz", "owner_id": 42}, + ), + ], +) +def test_read_user_item(client: TestClient, path, expected_json): + response = client.get(path) + assert response.status_code == 200 + assert response.json() == expected_json + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/{user_id}/items/{item_id}": { + "get": { + "summary": "Read User Item", + "operationId": "read_user_item_users__user_id__items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "User Id", "type": "integer"}, + "name": "user_id", + "in": "path", + }, + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + }, + { + "required": False, + "schema": { + "title": "Q", + "anyOf": [ + { + "type": "string", + }, + { + "type": "null", + }, + ], + }, + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Short", + "type": "boolean", + "default": False, + }, + "name": "short", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError", + }, + }, + }, + "description": "Validation Error", + }, + }, + } + } + }, + "components": { + "schemas": { + "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"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params/test_tutorial005.py b/tests/test_tutorial/test_query_params/test_tutorial005.py index 05ae85b451..1030781472 100644 --- a/tests/test_tutorial/test_query_params/test_tutorial005.py +++ b/tests/test_tutorial/test_query_params/test_tutorial005.py @@ -1,7 +1,6 @@ -from dirty_equals import IsDict from fastapi.testclient import TestClient -from docs_src.query_params.tutorial005 import app +from docs_src.query_params.tutorial005_py39 import app client = TestClient(app) @@ -15,29 +14,16 @@ def test_foo_needy_very(): def test_foo_no_needy(): response = client.get("/items/foo") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "needy"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "needy"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "needy"], + "msg": "Field required", + "input": None, + } + ] + } def test_openapi_schema(): diff --git a/tests/test_tutorial/test_query_params/test_tutorial006.py b/tests/test_tutorial/test_query_params/test_tutorial006.py index a0b5ef4943..157322c7e3 100644 --- a/tests/test_tutorial/test_query_params/test_tutorial006.py +++ b/tests/test_tutorial/test_query_params/test_tutorial006.py @@ -1,7 +1,6 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -10,7 +9,7 @@ from ...utils import needs_py310 @pytest.fixture( name="client", params=[ - "tutorial006", + pytest.param("tutorial006_py39"), pytest.param("tutorial006_py310", marks=needs_py310), ], ) @@ -35,51 +34,28 @@ def test_foo_needy_very(client: TestClient): def test_foo_no_needy(client: TestClient): response = client.get("/items/foo?skip=a&limit=b") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "needy"], - "msg": "Field required", - "input": None, - }, - { - "type": "int_parsing", - "loc": ["query", "skip"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "a", - }, - { - "type": "int_parsing", - "loc": ["query", "limit"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "b", - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "needy"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["query", "skip"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - }, - { - "loc": ["query", "limit"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "needy"], + "msg": "Field required", + "input": None, + }, + { + "type": "int_parsing", + "loc": ["query", "skip"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "a", + }, + { + "type": "int_parsing", + "loc": ["query", "limit"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "b", + }, + ] + } def test_openapi_schema(client: TestClient): @@ -134,16 +110,10 @@ def test_openapi_schema(client: TestClient): }, { "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "integer"}, {"type": "null"}], - "title": "Limit", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Limit", "type": "integer"} - ), + "schema": { + "anyOf": [{"type": "integer"}, {"type": "null"}], + "title": "Limit", + }, "name": "limit", "in": "query", }, diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial001.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial001.py new file mode 100644 index 0000000000..f1af7e08c1 --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial001.py @@ -0,0 +1,121 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial001_py39"), + pytest.param("tutorial001_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module( + f"docs_src.query_params_str_validations.{request.param}" + ) + + client = TestClient(mod.app) + return client + + +def test_query_params_str_validations_no_query(client: TestClient): + response = client.get("/items/") + assert response.status_code == 200 + assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + + +def test_query_params_str_validations_q_empty_str(client: TestClient): + response = client.get("/items/", params={"q": ""}) + assert response.status_code == 200 + assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + + +def test_query_params_str_validations_q_query(client: TestClient): + response = client.get("/items/", params={"q": "query"}) + assert response.status_code == 200 + assert response.json() == { + "items": [{"item_id": "Foo"}, {"item_id": "Bar"}], + "q": "query", + } + + +def test_openapi_schema(client: TestClient): + 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": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "anyOf": [ + {"type": "string"}, + {"type": "null"}, + ], + "title": "Q", + }, + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "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"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial002.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial002.py new file mode 100644 index 0000000000..62018b80b5 --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial002.py @@ -0,0 +1,142 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial002_py39"), + pytest.param("tutorial002_py310", marks=needs_py310), + pytest.param("tutorial002_an_py39"), + pytest.param("tutorial002_an_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module( + f"docs_src.query_params_str_validations.{request.param}" + ) + + client = TestClient(mod.app) + return client + + +def test_query_params_str_validations_no_query(client: TestClient): + response = client.get("/items/") + assert response.status_code == 200 + assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + + +def test_query_params_str_validations_q_empty_str(client: TestClient): + response = client.get("/items/", params={"q": ""}) + assert response.status_code == 200 + assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + + +def test_query_params_str_validations_q_query(client: TestClient): + response = client.get("/items/", params={"q": "query"}) + assert response.status_code == 200 + assert response.json() == { + "items": [{"item_id": "Foo"}, {"item_id": "Bar"}], + "q": "query", + } + + +def test_query_params_str_validations_q_too_long(client: TestClient): + response = client.get("/items/", params={"q": "q" * 51}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "string_too_long", + "loc": ["query", "q"], + "msg": "String should have at most 50 characters", + "input": "q" * 51, + "ctx": {"max_length": 50}, + } + ] + } + + +def test_openapi_schema(client: TestClient): + 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": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "anyOf": [ + { + "type": "string", + "maxLength": 50, + }, + {"type": "null"}, + ], + "title": "Q", + }, + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "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"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial003.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial003.py new file mode 100644 index 0000000000..a4ad7a63ba --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial003.py @@ -0,0 +1,153 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial003_py39"), + pytest.param("tutorial003_py310", marks=needs_py310), + pytest.param("tutorial003_an_py39"), + pytest.param("tutorial003_an_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module( + f"docs_src.query_params_str_validations.{request.param}" + ) + + client = TestClient(mod.app) + return client + + +def test_query_params_str_validations_no_query(client: TestClient): + response = client.get("/items/") + assert response.status_code == 200 + assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + + +def test_query_params_str_validations_q_query(client: TestClient): + response = client.get("/items/", params={"q": "query"}) + assert response.status_code == 200 + assert response.json() == { + "items": [{"item_id": "Foo"}, {"item_id": "Bar"}], + "q": "query", + } + + +def test_query_params_str_validations_q_too_short(client: TestClient): + response = client.get("/items/", params={"q": "qu"}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "string_too_short", + "loc": ["query", "q"], + "msg": "String should have at least 3 characters", + "input": "qu", + "ctx": {"min_length": 3}, + } + ] + } + + +def test_query_params_str_validations_q_too_long(client: TestClient): + response = client.get("/items/", params={"q": "q" * 51}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "string_too_long", + "loc": ["query", "q"], + "msg": "String should have at most 50 characters", + "input": "q" * 51, + "ctx": {"max_length": 50}, + } + ] + } + + +def test_openapi_schema(client: TestClient): + 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": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "anyOf": [ + { + "type": "string", + "minLength": 3, + "maxLength": 50, + }, + {"type": "null"}, + ], + "title": "Q", + }, + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "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"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial004.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial004.py new file mode 100644 index 0000000000..585989a827 --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial004.py @@ -0,0 +1,147 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial004_py39"), + pytest.param("tutorial004_py310", marks=needs_py310), + pytest.param("tutorial004_an_py39"), + pytest.param("tutorial004_an_py310", marks=needs_py310), + pytest.param( + "tutorial004_regex_an_py310", + marks=( + needs_py310, + pytest.mark.filterwarnings( + "ignore:`regex` has been deprecated, please use `pattern` instead:fastapi.exceptions.FastAPIDeprecationWarning" + ), + ), + ), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module( + f"docs_src.query_params_str_validations.{request.param}" + ) + + client = TestClient(mod.app) + return client + + +def test_query_params_str_validations_no_query(client: TestClient): + response = client.get("/items/") + assert response.status_code == 200 + assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + + +def test_query_params_str_validations_q_fixedquery(client: TestClient): + response = client.get("/items/", params={"q": "fixedquery"}) + assert response.status_code == 200 + assert response.json() == { + "items": [{"item_id": "Foo"}, {"item_id": "Bar"}], + "q": "fixedquery", + } + + +def test_query_params_str_validations_q_nonregexquery(client: TestClient): + response = client.get("/items/", params={"q": "nonregexquery"}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "string_pattern_mismatch", + "loc": ["query", "q"], + "msg": "String should match pattern '^fixedquery$'", + "input": "nonregexquery", + "ctx": {"pattern": "^fixedquery$"}, + } + ] + } + + +def test_openapi_schema(client: TestClient): + 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": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "anyOf": [ + { + "type": "string", + "minLength": 3, + "maxLength": 50, + "pattern": "^fixedquery$", + }, + {"type": "null"}, + ], + "title": "Q", + }, + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "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"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial005.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial005.py new file mode 100644 index 0000000000..52462fe33b --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial005.py @@ -0,0 +1,131 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial005_py39"), + pytest.param("tutorial005_an_py39"), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module( + f"docs_src.query_params_str_validations.{request.param}" + ) + + client = TestClient(mod.app) + return client + + +def test_query_params_str_validations_no_query(client: TestClient): + response = client.get("/items/") + assert response.status_code == 200 + assert response.json() == { + "items": [{"item_id": "Foo"}, {"item_id": "Bar"}], + "q": "fixedquery", + } + + +def test_query_params_str_validations_q_query(client: TestClient): + response = client.get("/items/", params={"q": "query"}) + assert response.status_code == 200 + assert response.json() == { + "items": [{"item_id": "Foo"}, {"item_id": "Bar"}], + "q": "query", + } + + +def test_query_params_str_validations_q_short(client: TestClient): + response = client.get("/items/", params={"q": "fa"}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "string_too_short", + "loc": ["query", "q"], + "msg": "String should have at least 3 characters", + "input": "fa", + "ctx": {"min_length": 3}, + } + ] + } + + +def test_openapi_schema(client: TestClient): + 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": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "type": "string", + "default": "fixedquery", + "minLength": 3, + "title": "Q", + }, + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "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"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial006.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial006.py new file mode 100644 index 0000000000..640cedce19 --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial006.py @@ -0,0 +1,136 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial006_py39"), + pytest.param("tutorial006_an_py39"), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module( + f"docs_src.query_params_str_validations.{request.param}" + ) + + client = TestClient(mod.app) + return client + + +def test_query_params_str_validations_no_query(client: TestClient): + response = client.get("/items/") + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "q"], + "msg": "Field required", + "input": None, + } + ] + } + + +def test_query_params_str_validations_q_fixedquery(client: TestClient): + response = client.get("/items/", params={"q": "fixedquery"}) + assert response.status_code == 200 + assert response.json() == { + "items": [{"item_id": "Foo"}, {"item_id": "Bar"}], + "q": "fixedquery", + } + + +def test_query_params_str_validations_q_fixedquery_too_short(client: TestClient): + response = client.get("/items/", params={"q": "fa"}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "string_too_short", + "loc": ["query", "q"], + "msg": "String should have at least 3 characters", + "input": "fa", + "ctx": {"min_length": 3}, + } + ] + } + + +def test_openapi_schema(client: TestClient): + 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": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": True, + "schema": { + "type": "string", + "minLength": 3, + "title": "Q", + }, + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "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"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial006c.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial006c.py new file mode 100644 index 0000000000..f287b5dcd8 --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial006c.py @@ -0,0 +1,148 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial006c_py39"), + pytest.param("tutorial006c_py310", marks=needs_py310), + pytest.param("tutorial006c_an_py39"), + pytest.param("tutorial006c_an_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module( + f"docs_src.query_params_str_validations.{request.param}" + ) + client = TestClient(mod.app) + return client + + +@pytest.mark.xfail( + reason="Code example is not valid. See https://github.com/fastapi/fastapi/issues/12419" +) +def test_query_params_str_validations_no_query(client: TestClient): + response = client.get("/items/") + assert response.status_code == 200 + assert response.json() == { # pragma: no cover + "items": [{"item_id": "Foo"}, {"item_id": "Bar"}], + } + + +@pytest.mark.xfail( + reason="Code example is not valid. See https://github.com/fastapi/fastapi/issues/12419" +) +def test_query_params_str_validations_empty_str(client: TestClient): + response = client.get("/items/?q=") + assert response.status_code == 200 + assert response.json() == { # pragma: no cover + "items": [{"item_id": "Foo"}, {"item_id": "Bar"}], + } + + +def test_query_params_str_validations_q_query(client: TestClient): + response = client.get("/items/", params={"q": "query"}) + assert response.status_code == 200 + assert response.json() == { + "items": [{"item_id": "Foo"}, {"item_id": "Bar"}], + "q": "query", + } + + +def test_query_params_str_validations_q_short(client: TestClient): + response = client.get("/items/", params={"q": "fa"}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "string_too_short", + "loc": ["query", "q"], + "msg": "String should have at least 3 characters", + "input": "fa", + "ctx": {"min_length": 3}, + } + ] + } + + +def test_openapi_schema(client: TestClient): + 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": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": True, + "schema": { + "anyOf": [ + {"type": "string", "minLength": 3}, + {"type": "null"}, + ], + "title": "Q", + }, + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "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"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial007.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial007.py new file mode 100644 index 0000000000..b17bc27719 --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial007.py @@ -0,0 +1,136 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial007_py39"), + pytest.param("tutorial007_py310", marks=needs_py310), + pytest.param("tutorial007_an_py39"), + pytest.param("tutorial007_an_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module( + f"docs_src.query_params_str_validations.{request.param}" + ) + + client = TestClient(mod.app) + return client + + +def test_query_params_str_validations_no_query(client: TestClient): + response = client.get("/items/") + assert response.status_code == 200 + assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + + +def test_query_params_str_validations_q_fixedquery(client: TestClient): + response = client.get("/items/", params={"q": "fixedquery"}) + assert response.status_code == 200 + assert response.json() == { + "items": [{"item_id": "Foo"}, {"item_id": "Bar"}], + "q": "fixedquery", + } + + +def test_query_params_str_validations_q_fixedquery_too_short(client: TestClient): + response = client.get("/items/", params={"q": "fa"}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "string_too_short", + "loc": ["query", "q"], + "msg": "String should have at least 3 characters", + "input": "fa", + "ctx": {"min_length": 3}, + } + ] + } + + +def test_openapi_schema(client: TestClient): + 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": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "anyOf": [ + { + "type": "string", + "minLength": 3, + }, + {"type": "null"}, + ], + "title": "Query string", + }, + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "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"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial008.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial008.py new file mode 100644 index 0000000000..c631115744 --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial008.py @@ -0,0 +1,138 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial008_py39"), + pytest.param("tutorial008_py310", marks=needs_py310), + pytest.param("tutorial008_an_py39"), + pytest.param("tutorial008_an_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module( + f"docs_src.query_params_str_validations.{request.param}" + ) + + client = TestClient(mod.app) + return client + + +def test_query_params_str_validations_no_query(client: TestClient): + response = client.get("/items/") + assert response.status_code == 200 + assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + + +def test_query_params_str_validations_q_fixedquery(client: TestClient): + response = client.get("/items/", params={"q": "fixedquery"}) + assert response.status_code == 200 + assert response.json() == { + "items": [{"item_id": "Foo"}, {"item_id": "Bar"}], + "q": "fixedquery", + } + + +def test_query_params_str_validations_q_fixedquery_too_short(client: TestClient): + response = client.get("/items/", params={"q": "fa"}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "string_too_short", + "loc": ["query", "q"], + "msg": "String should have at least 3 characters", + "input": "fa", + "ctx": {"min_length": 3}, + } + ] + } + + +def test_openapi_schema(client: TestClient): + 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": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "description": "Query string for the items to search in the database that have a good match", + "required": False, + "schema": { + "anyOf": [ + { + "type": "string", + "minLength": 3, + }, + {"type": "null"}, + ], + "title": "Query string", + "description": "Query string for the items to search in the database that have a good match", + }, + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "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"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial009.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial009.py new file mode 100644 index 0000000000..7e9d69d41c --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial009.py @@ -0,0 +1,123 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial009_py39"), + pytest.param("tutorial009_py310", marks=needs_py310), + pytest.param("tutorial009_an_py39"), + pytest.param("tutorial009_an_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module( + f"docs_src.query_params_str_validations.{request.param}" + ) + + client = TestClient(mod.app) + return client + + +def test_query_params_str_validations_no_query(client: TestClient): + response = client.get("/items/") + assert response.status_code == 200 + assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + + +def test_query_params_str_validations_item_query_fixedquery(client: TestClient): + response = client.get("/items/", params={"item-query": "fixedquery"}) + assert response.status_code == 200 + assert response.json() == { + "items": [{"item_id": "Foo"}, {"item_id": "Bar"}], + "q": "fixedquery", + } + + +def test_query_params_str_validations_q_fixedquery(client: TestClient): + response = client.get("/items/", params={"q": "fixedquery"}) + assert response.status_code == 200 + assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + + +def test_openapi_schema(client: TestClient): + 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": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "schema": { + "anyOf": [ + {"type": "string"}, + {"type": "null"}, + ], + "title": "Item-Query", + }, + "required": False, + "name": "item-query", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "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"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010.py index e08e169633..00889c5bf7 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010.py @@ -1,20 +1,18 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi._compat import PYDANTIC_VERSION_MINOR_TUPLE from fastapi.testclient import TestClient -from ...utils import needs_py39, needs_py310 +from ...utils import needs_py310 @pytest.fixture( name="client", params=[ - "tutorial010", + pytest.param("tutorial010_py39"), pytest.param("tutorial010_py310", marks=needs_py310), - "tutorial010_an", - pytest.param("tutorial010_an_py39", marks=needs_py39), + pytest.param("tutorial010_an_py39"), pytest.param("tutorial010_an_py310", marks=needs_py310), ], ) @@ -51,31 +49,17 @@ def test_query_params_str_validations_q_fixedquery(client: TestClient): def test_query_params_str_validations_item_query_nonregexquery(client: TestClient): response = client.get("/items/", params={"item-query": "nonregexquery"}) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "string_pattern_mismatch", - "loc": ["query", "item-query"], - "msg": "String should match pattern '^fixedquery$'", - "input": "nonregexquery", - "ctx": {"pattern": "^fixedquery$"}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "ctx": {"pattern": "^fixedquery$"}, - "loc": ["query", "item-query"], - "msg": 'string does not match regex "^fixedquery$"', - "type": "value_error.str.regex", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "string_pattern_mismatch", + "loc": ["query", "item-query"], + "msg": "String should match pattern '^fixedquery$'", + "input": "nonregexquery", + "ctx": {"pattern": "^fixedquery$"}, + } + ] + } def test_openapi_schema(client: TestClient): @@ -110,38 +94,25 @@ def test_openapi_schema(client: TestClient): "description": "Query string for the items to search in the database that have a good match", "required": False, "deprecated": True, - "schema": IsDict( - { - "anyOf": [ - { - "type": "string", - "minLength": 3, - "maxLength": 50, - "pattern": "^fixedquery$", - }, - {"type": "null"}, - ], - "title": "Query string", - "description": "Query string for the items to search in the database that have a good match", - # See https://github.com/pydantic/pydantic/blob/80353c29a824c55dea4667b328ba8f329879ac9f/tests/test_fastapi.sh#L25-L34. - **( - {"deprecated": True} - if PYDANTIC_VERSION_MINOR_TUPLE >= (2, 10) - else {} - ), - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Query string", - "maxLength": 50, - "minLength": 3, - "pattern": "^fixedquery$", - "type": "string", - "description": "Query string for the items to search in the database that have a good match", - } - ), + "schema": { + "anyOf": [ + { + "type": "string", + "minLength": 3, + "maxLength": 50, + "pattern": "^fixedquery$", + }, + {"type": "null"}, + ], + "title": "Query string", + "description": "Query string for the items to search in the database that have a good match", + # See https://github.com/pydantic/pydantic/blob/80353c29a824c55dea4667b328ba8f329879ac9f/tests/test_fastapi.sh#L25-L34. + **( + {"deprecated": True} + if PYDANTIC_VERSION_MINOR_TUPLE >= (2, 10) + else {} + ), + }, "name": "item-query", "in": "query", } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011.py index f4da25752b..11de33ae14 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011.py @@ -1,20 +1,17 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient -from ...utils import needs_py39, needs_py310 +from ...utils import needs_py310 @pytest.fixture( name="client", params=[ - "tutorial011", - pytest.param("tutorial011_py39", marks=needs_py310), + pytest.param("tutorial011_py39"), pytest.param("tutorial011_py310", marks=needs_py310), - "tutorial011_an", - pytest.param("tutorial011_an_py39", marks=needs_py39), + pytest.param("tutorial011_an_py39"), pytest.param("tutorial011_an_py310", marks=needs_py310), ], ) @@ -71,23 +68,13 @@ def test_openapi_schema(client: TestClient): "parameters": [ { "required": False, - "schema": IsDict( - { - "anyOf": [ - {"type": "array", "items": {"type": "string"}}, - {"type": "null"}, - ], - "title": "Q", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Q", - "type": "array", - "items": {"type": "string"}, - } - ), + "schema": { + "anyOf": [ + {"type": "array", "items": {"type": "string"}}, + {"type": "null"}, + ], + "title": "Q", + }, "name": "q", "in": "query", } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial012.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial012.py index 549a90519e..1826928611 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial012.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial012.py @@ -3,16 +3,12 @@ import importlib import pytest from fastapi.testclient import TestClient -from ...utils import needs_py39 - @pytest.fixture( name="client", params=[ - "tutorial012", - pytest.param("tutorial012_py39", marks=needs_py39), - "tutorial012_an", - pytest.param("tutorial012_an_py39", marks=needs_py39), + pytest.param("tutorial012_py39"), + pytest.param("tutorial012_an_py39"), ], ) def get_client(request: pytest.FixtureRequest): diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial013.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial013.py index f2f5f7a858..46c367c86b 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial013.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial013.py @@ -3,15 +3,12 @@ import importlib import pytest from fastapi.testclient import TestClient -from ...utils import needs_py39 - @pytest.fixture( name="client", params=[ - "tutorial013", - "tutorial013_an", - pytest.param("tutorial013_an_py39", marks=needs_py39), + "tutorial013_py39", + "tutorial013_an_py39", ], ) def get_client(request: pytest.FixtureRequest): diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial014.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014.py index edd40bb1ab..0feaccfa44 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial014.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014.py @@ -3,16 +3,15 @@ import importlib import pytest from fastapi.testclient import TestClient -from ...utils import needs_py39, needs_py310 +from ...utils import needs_py310 @pytest.fixture( name="client", params=[ - "tutorial014", + pytest.param("tutorial014_py39"), pytest.param("tutorial014_py310", marks=needs_py310), - "tutorial014_an", - pytest.param("tutorial014_an_py39", marks=needs_py39), + pytest.param("tutorial014_an_py39"), pytest.param("tutorial014_an_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial015.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial015.py index ae1c40286d..82bb606a9b 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial015.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial015.py @@ -5,15 +5,14 @@ from dirty_equals import IsStr from fastapi.testclient import TestClient from inline_snapshot import snapshot -from ...utils import needs_py39, needs_py310, needs_pydanticv2 +from ...utils import needs_py310 @pytest.fixture( name="client", params=[ - pytest.param("tutorial015_an", marks=needs_pydanticv2), - pytest.param("tutorial015_an_py310", marks=(needs_py310, needs_pydanticv2)), - pytest.param("tutorial015_an_py39", marks=(needs_py39, needs_pydanticv2)), + pytest.param("tutorial015_an_py39"), + pytest.param("tutorial015_an_py310", marks=[needs_py310]), ], ) def get_client(request: pytest.FixtureRequest): diff --git a/tests/test_tutorial/test_request_files/test_tutorial001.py b/tests/test_tutorial/test_request_files/test_tutorial001.py index b069199612..e0e1bbe639 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001.py @@ -1,18 +1,14 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient -from ...utils import needs_py39 - @pytest.fixture( name="client", params=[ - "tutorial001", - "tutorial001_an", - pytest.param("tutorial001_an_py39", marks=needs_py39), + "tutorial001_py39", + "tutorial001_an_py39", ], ) def get_client(request: pytest.FixtureRequest): @@ -25,57 +21,31 @@ def get_client(request: pytest.FixtureRequest): def test_post_form_no_body(client: TestClient): response = client.post("/files/") assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "file"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "file"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "file"], + "msg": "Field required", + "input": None, + } + ] + } def test_post_body_json(client: TestClient): response = client.post("/files/", json={"file": "Foo"}) assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "file"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "file"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "file"], + "msg": "Field required", + "input": None, + } + ] + } def test_post_file(tmp_path, client: TestClient): diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_02.py b/tests/test_tutorial/test_request_files/test_tutorial001_02.py index 9075a17561..18948c5444 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001_02.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001_02.py @@ -2,19 +2,18 @@ import importlib from pathlib import Path import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient +from inline_snapshot import snapshot -from ...utils import needs_py39, needs_py310 +from ...utils import needs_py310 @pytest.fixture( name="client", params=[ - "tutorial001_02", + pytest.param("tutorial001_02_py39"), pytest.param("tutorial001_02_py310", marks=needs_py310), - "tutorial001_02_an", - pytest.param("tutorial001_02_an_py39", marks=needs_py39), + pytest.param("tutorial001_02_an_py39"), pytest.param("tutorial001_02_an_py310", marks=needs_py310), ], ) @@ -60,166 +59,132 @@ def test_post_upload_file(tmp_path: Path, client: TestClient): def test_openapi_schema(client: TestClient): 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": { - "/files/": { - "post": { - "summary": "Create File", - "operationId": "create_file_files__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": IsDict( - { - "allOf": [ - { - "$ref": "#/components/schemas/Body_create_file_files__post" - } - ], - "title": "Body", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "summary": "Create File", + "operationId": "create_file_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { "$ref": "#/components/schemas/Body_create_file_files__post" } - ) - } - } - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, }, }, - }, - } - }, - "/uploadfile/": { - "post": { - "summary": "Create Upload File", - "operationId": "create_upload_file_uploadfile__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": IsDict( - { - "allOf": [ - { - "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" - } - ], - "title": "Body", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { + } + }, + "/uploadfile/": { + "post": { + "summary": "Create Upload File", + "operationId": "create_upload_file_uploadfile__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" } - ) - } - } - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "Body_create_file_files__post": { + "title": "Body_create_file_files__post", + "type": "object", + "properties": { + "file": { + "title": "File", + "anyOf": [ + {"type": "string", "format": "binary"}, + {"type": "null"}, + ], + } + }, + }, + "Body_create_upload_file_uploadfile__post": { + "title": "Body_create_upload_file_uploadfile__post", + "type": "object", + "properties": { + "file": { + "title": "File", + "anyOf": [ + {"type": "string", "format": "binary"}, + {"type": "null"}, + ], + } + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + "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"}, }, }, } }, - }, - "components": { - "schemas": { - "Body_create_file_files__post": { - "title": "Body_create_file_files__post", - "type": "object", - "properties": { - "file": IsDict( - { - "title": "File", - "anyOf": [ - {"type": "string", "format": "binary"}, - {"type": "null"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "File", "type": "string", "format": "binary"} - ) - }, - }, - "Body_create_upload_file_uploadfile__post": { - "title": "Body_create_upload_file_uploadfile__post", - "type": "object", - "properties": { - "file": IsDict( - { - "title": "File", - "anyOf": [ - {"type": "string", "format": "binary"}, - {"type": "null"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "File", "type": "string", "format": "binary"} - ) - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "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_request_files/test_tutorial001_03.py b/tests/test_tutorial/test_request_files/test_tutorial001_03.py index 9fbe2166c1..53a7a0cf85 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001_03.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001_03.py @@ -3,15 +3,12 @@ import importlib import pytest from fastapi.testclient import TestClient -from ...utils import needs_py39 - @pytest.fixture( name="client", params=[ - "tutorial001_03", - "tutorial001_03_an", - pytest.param("tutorial001_03_an_py39", marks=needs_py39), + "tutorial001_03_py39", + "tutorial001_03_an_py39", ], ) def get_client(request: pytest.FixtureRequest): diff --git a/tests/test_tutorial/test_request_files/test_tutorial002.py b/tests/test_tutorial/test_request_files/test_tutorial002.py index 446a876578..03772419ad 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial002.py +++ b/tests/test_tutorial/test_request_files/test_tutorial002.py @@ -1,20 +1,15 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi import FastAPI from fastapi.testclient import TestClient -from ...utils import needs_py39 - @pytest.fixture( name="app", params=[ - "tutorial002", - "tutorial002_an", - pytest.param("tutorial002_py39", marks=needs_py39), - pytest.param("tutorial002_an_py39", marks=needs_py39), + "tutorial002_py39", + "tutorial002_an_py39", ], ) def get_app(request: pytest.FixtureRequest): @@ -32,57 +27,31 @@ def get_client(app: FastAPI): def test_post_form_no_body(client: TestClient): response = client.post("/files/") assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "files"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "files"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "files"], + "msg": "Field required", + "input": None, + } + ] + } def test_post_body_json(client: TestClient): response = client.post("/files/", json={"file": "Foo"}) assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "files"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "files"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "files"], + "msg": "Field required", + "input": None, + } + ] + } def test_post_files(tmp_path, app: FastAPI): diff --git a/tests/test_tutorial/test_request_files/test_tutorial003.py b/tests/test_tutorial/test_request_files/test_tutorial003.py index 8534ba3e9a..fa4bfd5695 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial003.py +++ b/tests/test_tutorial/test_request_files/test_tutorial003.py @@ -4,16 +4,12 @@ import pytest from fastapi import FastAPI from fastapi.testclient import TestClient -from ...utils import needs_py39 - @pytest.fixture( name="app", params=[ - "tutorial003", - "tutorial003_an", - pytest.param("tutorial003_py39", marks=needs_py39), - pytest.param("tutorial003_an_py39", marks=needs_py39), + "tutorial003_py39", + "tutorial003_an_py39", ], ) def get_app(request: pytest.FixtureRequest): diff --git a/tests/test_tutorial/test_request_form_models/test_tutorial001.py b/tests/test_tutorial/test_request_form_models/test_tutorial001.py index 1ca3c96d33..0c43dd7b21 100644 --- a/tests/test_tutorial/test_request_form_models/test_tutorial001.py +++ b/tests/test_tutorial/test_request_form_models/test_tutorial001.py @@ -1,18 +1,14 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient -from ...utils import needs_py39 - @pytest.fixture( name="client", params=[ - "tutorial001", - "tutorial001_an", - pytest.param("tutorial001_an_py39", marks=needs_py39), + "tutorial001_py39", + "tutorial001_an_py39", ], ) def get_client(request: pytest.FixtureRequest): @@ -31,135 +27,73 @@ def test_post_body_form(client: TestClient): def test_post_body_form_no_password(client: TestClient): response = client.post("/login/", data={"username": "Foo"}) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "password"], - "msg": "Field required", - "input": {"username": "Foo"}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": {"username": "Foo"}, + } + ] + } def test_post_body_form_no_username(client: TestClient): response = client.post("/login/", data={"password": "secret"}) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": {"password": "secret"}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {"password": "secret"}, + } + ] + } def test_post_body_form_no_data(client: TestClient): response = client.post("/login/") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": {}, - }, - { - "type": "missing", - "loc": ["body", "password"], - "msg": "Field required", - "input": {}, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {}, + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": {}, + }, + ] + } def test_post_body_json(client: TestClient): response = client.post("/login/", json={"username": "Foo", "password": "secret"}) assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": {}, - }, - { - "type": "missing", - "loc": ["body", "password"], - "msg": "Field required", - "input": {}, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {}, + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": {}, + }, + ] + } def test_openapi_schema(client: TestClient): diff --git a/tests/test_tutorial/test_request_form_models/test_tutorial002.py b/tests/test_tutorial/test_request_form_models/test_tutorial002.py index b3f6be63ab..238f8fa2ef 100644 --- a/tests/test_tutorial/test_request_form_models/test_tutorial002.py +++ b/tests/test_tutorial/test_request_form_models/test_tutorial002.py @@ -3,15 +3,12 @@ import importlib import pytest from fastapi.testclient import TestClient -from ...utils import needs_py39, needs_pydanticv2 - @pytest.fixture( name="client", params=[ - "tutorial002", - "tutorial002_an", - pytest.param("tutorial002_an_py39", marks=needs_py39), + "tutorial002_py39", + "tutorial002_an_py39", ], ) def get_client(request: pytest.FixtureRequest): @@ -21,14 +18,12 @@ def get_client(request: pytest.FixtureRequest): return client -@needs_pydanticv2 def test_post_body_form(client: TestClient): response = client.post("/login/", data={"username": "Foo", "password": "secret"}) assert response.status_code == 200 assert response.json() == {"username": "Foo", "password": "secret"} -@needs_pydanticv2 def test_post_body_extra_form(client: TestClient): response = client.post( "/login/", data={"username": "Foo", "password": "secret", "extra": "extra"} @@ -46,7 +41,6 @@ def test_post_body_extra_form(client: TestClient): } -@needs_pydanticv2 def test_post_body_form_no_password(client: TestClient): response = client.post("/login/", data={"username": "Foo"}) assert response.status_code == 422 @@ -62,7 +56,6 @@ def test_post_body_form_no_password(client: TestClient): } -@needs_pydanticv2 def test_post_body_form_no_username(client: TestClient): response = client.post("/login/", data={"password": "secret"}) assert response.status_code == 422 @@ -78,7 +71,6 @@ def test_post_body_form_no_username(client: TestClient): } -@needs_pydanticv2 def test_post_body_form_no_data(client: TestClient): response = client.post("/login/") assert response.status_code == 422 @@ -100,7 +92,6 @@ def test_post_body_form_no_data(client: TestClient): } -@needs_pydanticv2 def test_post_body_json(client: TestClient): response = client.post("/login/", json={"username": "Foo", "password": "secret"}) assert response.status_code == 422, response.text @@ -122,7 +113,6 @@ def test_post_body_json(client: TestClient): } -@needs_pydanticv2 def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text diff --git a/tests/test_tutorial/test_request_form_models/test_tutorial002_pv1.py b/tests/test_tutorial/test_request_form_models/test_tutorial002_pv1.py deleted file mode 100644 index b503f23a53..0000000000 --- a/tests/test_tutorial/test_request_form_models/test_tutorial002_pv1.py +++ /dev/null @@ -1,205 +0,0 @@ -import importlib - -import pytest -from fastapi.testclient import TestClient - -from ...utils import needs_py39, needs_pydanticv1 - - -@pytest.fixture( - name="client", - params=[ - "tutorial002_pv1", - "tutorial002_pv1_an", - pytest.param("tutorial002_pv1_an_py39", marks=needs_py39), - ], -) -def get_client(request: pytest.FixtureRequest): - mod = importlib.import_module(f"docs_src.request_form_models.{request.param}") - - client = TestClient(mod.app) - return client - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -def test_post_body_form(client: TestClient): - response = client.post("/login/", data={"username": "Foo", "password": "secret"}) - assert response.status_code == 200 - assert response.json() == {"username": "Foo", "password": "secret"} - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -def test_post_body_extra_form(client: TestClient): - response = client.post( - "/login/", data={"username": "Foo", "password": "secret", "extra": "extra"} - ) - assert response.status_code == 422 - assert response.json() == { - "detail": [ - { - "type": "value_error.extra", - "loc": ["body", "extra"], - "msg": "extra fields not permitted", - } - ] - } - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -def test_post_body_form_no_password(client: TestClient): - response = client.post("/login/", data={"username": "Foo"}) - assert response.status_code == 422 - assert response.json() == { - "detail": [ - { - "type": "value_error.missing", - "loc": ["body", "password"], - "msg": "field required", - } - ] - } - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -def test_post_body_form_no_username(client: TestClient): - response = client.post("/login/", data={"password": "secret"}) - assert response.status_code == 422 - assert response.json() == { - "detail": [ - { - "type": "value_error.missing", - "loc": ["body", "username"], - "msg": "field required", - } - ] - } - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -def test_post_body_form_no_data(client: TestClient): - response = client.post("/login/") - assert response.status_code == 422 - assert response.json() == { - "detail": [ - { - "type": "value_error.missing", - "loc": ["body", "username"], - "msg": "field required", - }, - { - "type": "value_error.missing", - "loc": ["body", "password"], - "msg": "field required", - }, - ] - } - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -def test_post_body_json(client: TestClient): - response = client.post("/login/", json={"username": "Foo", "password": "secret"}) - assert response.status_code == 422, response.text - assert response.json() == { - "detail": [ - { - "type": "value_error.missing", - "loc": ["body", "username"], - "msg": "field required", - }, - { - "type": "value_error.missing", - "loc": ["body", "password"], - "msg": "field required", - }, - ] - } - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -def test_openapi_schema(client: TestClient): - 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": { - "/login/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Login", - "operationId": "login_login__post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": {"$ref": "#/components/schemas/FormData"} - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "FormData": { - "properties": { - "username": {"type": "string", "title": "Username"}, - "password": {"type": "string", "title": "Password"}, - }, - "additionalProperties": False, - "type": "object", - "required": ["username", "password"], - "title": "FormData", - }, - "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"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_request_forms/test_tutorial001.py b/tests/test_tutorial/test_request_forms/test_tutorial001.py index 321f8022b7..4276414fc2 100644 --- a/tests/test_tutorial/test_request_forms/test_tutorial001.py +++ b/tests/test_tutorial/test_request_forms/test_tutorial001.py @@ -1,18 +1,14 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient -from ...utils import needs_py39 - @pytest.fixture( name="client", params=[ - "tutorial001", - "tutorial001_an", - pytest.param("tutorial001_an_py39", marks=needs_py39), + "tutorial001_py39", + "tutorial001_an_py39", ], ) def get_client(request: pytest.FixtureRequest): @@ -31,135 +27,73 @@ def test_post_body_form(client: TestClient): def test_post_body_form_no_password(client: TestClient): response = client.post("/login/", data={"username": "Foo"}) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "password"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": None, + } + ] + } def test_post_body_form_no_username(client: TestClient): response = client.post("/login/", data={"password": "secret"}) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": None, + } + ] + } def test_post_body_form_no_data(client: TestClient): response = client.post("/login/") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "password"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": None, + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": None, + }, + ] + } def test_post_body_json(client: TestClient): response = client.post("/login/", json={"username": "Foo", "password": "secret"}) assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "password"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": None, + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": None, + }, + ] + } def test_openapi_schema(client: TestClient): diff --git a/tests/test_tutorial/test_request_forms_and_files/test_tutorial001.py b/tests/test_tutorial/test_request_forms_and_files/test_tutorial001.py index d12219245e..7fa4c3de57 100644 --- a/tests/test_tutorial/test_request_forms_and_files/test_tutorial001.py +++ b/tests/test_tutorial/test_request_forms_and_files/test_tutorial001.py @@ -1,19 +1,15 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi import FastAPI from fastapi.testclient import TestClient -from ...utils import needs_py39 - @pytest.fixture( name="app", params=[ - "tutorial001", - "tutorial001_an", - pytest.param("tutorial001_an_py39", marks=needs_py39), + "tutorial001_py39", + "tutorial001_an_py39", ], ) def get_app(request: pytest.FixtureRequest): @@ -31,140 +27,76 @@ def get_client(app: FastAPI): def test_post_form_no_body(client: TestClient): response = client.post("/files/") assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "file"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "fileb"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "token"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "file"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "fileb"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "file"], + "msg": "Field required", + "input": None, + }, + { + "type": "missing", + "loc": ["body", "fileb"], + "msg": "Field required", + "input": None, + }, + { + "type": "missing", + "loc": ["body", "token"], + "msg": "Field required", + "input": None, + }, + ] + } def test_post_form_no_file(client: TestClient): response = client.post("/files/", data={"token": "foo"}) assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "file"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "fileb"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "file"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "fileb"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "file"], + "msg": "Field required", + "input": None, + }, + { + "type": "missing", + "loc": ["body", "fileb"], + "msg": "Field required", + "input": None, + }, + ] + } def test_post_body_json(client: TestClient): response = client.post("/files/", json={"file": "Foo", "token": "Bar"}) assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "file"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "fileb"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "token"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "file"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "fileb"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "file"], + "msg": "Field required", + "input": None, + }, + { + "type": "missing", + "loc": ["body", "fileb"], + "msg": "Field required", + "input": None, + }, + { + "type": "missing", + "loc": ["body", "token"], + "msg": "Field required", + "input": None, + }, + ] + } def test_post_file_no_token(tmp_path, app: FastAPI): @@ -175,40 +107,22 @@ def test_post_file_no_token(tmp_path, app: FastAPI): with path.open("rb") as file: response = client.post("/files/", files={"file": file}) assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "fileb"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "token"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "fileb"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "fileb"], + "msg": "Field required", + "input": None, + }, + { + "type": "missing", + "loc": ["body", "token"], + "msg": "Field required", + "input": None, + }, + ] + } def test_post_files_and_token(tmp_path, app: FastAPI): diff --git a/tests/test_tutorial/test_response_change_status_code/test_tutorial001.py b/tests/test_tutorial/test_response_change_status_code/test_tutorial001.py index 8ce3dcf1ab..05d5ca619f 100644 --- a/tests/test_tutorial/test_response_change_status_code/test_tutorial001.py +++ b/tests/test_tutorial/test_response_change_status_code/test_tutorial001.py @@ -1,6 +1,6 @@ from fastapi.testclient import TestClient -from docs_src.response_change_status_code.tutorial001 import app +from docs_src.response_change_status_code.tutorial001_py39 import app client = TestClient(app) diff --git a/tests/test_tutorial/test_response_cookies/test_tutorial001.py b/tests/test_tutorial/test_response_cookies/test_tutorial001.py index eecd1a24c9..6b931c8bda 100644 --- a/tests/test_tutorial/test_response_cookies/test_tutorial001.py +++ b/tests/test_tutorial/test_response_cookies/test_tutorial001.py @@ -1,6 +1,6 @@ from fastapi.testclient import TestClient -from docs_src.response_cookies.tutorial001 import app +from docs_src.response_cookies.tutorial001_py39 import app client = TestClient(app) diff --git a/tests/test_tutorial/test_response_cookies/test_tutorial002.py b/tests/test_tutorial/test_response_cookies/test_tutorial002.py index 3e390025fa..3ee5f500c5 100644 --- a/tests/test_tutorial/test_response_cookies/test_tutorial002.py +++ b/tests/test_tutorial/test_response_cookies/test_tutorial002.py @@ -1,6 +1,6 @@ from fastapi.testclient import TestClient -from docs_src.response_cookies.tutorial002 import app +from docs_src.response_cookies.tutorial002_py39 import app client = TestClient(app) diff --git a/tests/test_tutorial/test_response_directly/__init__.py b/tests/test_tutorial/test_response_directly/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/test_tutorial/test_response_directly/test_tutorial001.py b/tests/test_tutorial/test_response_directly/test_tutorial001.py new file mode 100644 index 0000000000..2d0c387195 --- /dev/null +++ b/tests/test_tutorial/test_response_directly/test_tutorial001.py @@ -0,0 +1,152 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial001_py39"), + pytest.param("tutorial001_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.response_directly.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_path_operation(client: TestClient): + response = client.put( + "/items/1", + json={ + "title": "Foo", + "timestamp": "2023-01-01T12:00:00", + "description": "A test item", + }, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "description": "A test item", + "timestamp": "2023-01-01T12:00:00", + "title": "Foo", + } + + +def test_openapi_schema_pv2(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "info": { + "title": "FastAPI", + "version": "0.1.0", + }, + "openapi": "3.1.0", + "paths": { + "/items/{id}": { + "put": { + "operationId": "update_item_items__id__put", + "parameters": [ + { + "in": "path", + "name": "id", + "required": True, + "schema": {"title": "Id", "type": "string"}, + }, + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Item", + }, + }, + }, + "required": True, + }, + "responses": { + "200": { + "content": { + "application/json": {"schema": {}}, + }, + "description": "Successful Response", + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError", + }, + }, + }, + "description": "Validation Error", + }, + }, + "summary": "Update Item", + }, + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError", + }, + "title": "Detail", + "type": "array", + }, + }, + "title": "HTTPValidationError", + "type": "object", + }, + "Item": { + "properties": { + "description": { + "anyOf": [ + {"type": "string"}, + {"type": "null"}, + ], + "title": "Description", + }, + "timestamp": { + "format": "date-time", + "title": "Timestamp", + "type": "string", + }, + "title": {"title": "Title", "type": "string"}, + }, + "required": [ + "title", + "timestamp", + ], + "title": "Item", + "type": "object", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + {"type": "string"}, + {"type": "integer"}, + ], + }, + "title": "Location", + "type": "array", + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + "required": ["loc", "msg", "type"], + "title": "ValidationError", + "type": "object", + }, + }, + }, + } diff --git a/tests/test_tutorial/test_response_directly/test_tutorial002.py b/tests/test_tutorial/test_response_directly/test_tutorial002.py new file mode 100644 index 0000000000..ef84575723 --- /dev/null +++ b/tests/test_tutorial/test_response_directly/test_tutorial002.py @@ -0,0 +1,65 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial002_py39"), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.response_directly.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_path_operation(client: TestClient): + expected_content = """ + +
+ Apply shampoo here. +
+ + You'll have to use soap here. + +
+ """ + + response = client.get("/legacy/") + assert response.status_code == 200, response.text + assert response.headers["content-type"] == "application/xml" + assert response.text == expected_content + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "info": { + "title": "FastAPI", + "version": "0.1.0", + }, + "openapi": "3.1.0", + "paths": { + "/legacy/": { + "get": { + "operationId": "get_legacy_data_legacy__get", + "responses": { + "200": { + "content": { + "application/json": { + "schema": {}, + }, + }, + "description": "Successful Response", + }, + }, + "summary": "Get Legacy Data", + }, + }, + }, + } diff --git a/tests/test_tutorial/test_response_headers/test_tutorial001.py b/tests/test_tutorial/test_response_headers/test_tutorial001.py index 1549d6b5b1..6232aad23e 100644 --- a/tests/test_tutorial/test_response_headers/test_tutorial001.py +++ b/tests/test_tutorial/test_response_headers/test_tutorial001.py @@ -1,6 +1,6 @@ from fastapi.testclient import TestClient -from docs_src.response_headers.tutorial001 import app +from docs_src.response_headers.tutorial001_py39 import app client = TestClient(app) diff --git a/tests/test_tutorial/test_response_headers/test_tutorial002.py b/tests/test_tutorial/test_response_headers/test_tutorial002.py index 2826833f84..2ac2226cb1 100644 --- a/tests/test_tutorial/test_response_headers/test_tutorial002.py +++ b/tests/test_tutorial/test_response_headers/test_tutorial002.py @@ -1,6 +1,6 @@ from fastapi.testclient import TestClient -from docs_src.response_headers.tutorial002 import app +from docs_src.response_headers.tutorial002_py39 import app client = TestClient(app) diff --git a/tests/test_tutorial/test_response_model/test_tutorial001_tutorial001_01.py b/tests/test_tutorial/test_response_model/test_tutorial001_tutorial001_01.py new file mode 100644 index 0000000000..10692f9904 --- /dev/null +++ b/tests/test_tutorial/test_response_model/test_tutorial001_tutorial001_01.py @@ -0,0 +1,193 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial001_py39"), + pytest.param("tutorial001_py310", marks=needs_py310), + pytest.param("tutorial001_01_py39"), + pytest.param("tutorial001_01_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.response_model.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_read_items(client: TestClient): + response = client.get("/items/") + assert response.status_code == 200, response.text + assert response.json() == [ + { + "name": "Portal Gun", + "description": None, + "price": 42.0, + "tags": [], + "tax": None, + }, + { + "name": "Plumbus", + "description": None, + "price": 32.0, + "tags": [], + "tax": None, + }, + ] + + +def test_create_item(client: TestClient): + item_data = { + "name": "Test Item", + "description": "A test item", + "price": 10.5, + "tax": 1.5, + "tags": ["test", "item"], + } + response = client.post("/items/", json=item_data) + assert response.status_code == 200, response.text + assert response.json() == item_data + + +def test_create_item_only_required(client: TestClient): + response = client.post( + "/items/", + json={ + "name": "Test Item", + "price": 10.5, + }, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "name": "Test Item", + "price": 10.5, + "description": None, + "tax": None, + "tags": [], + } + + +def test_openapi_schema(client: TestClient): + 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": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": {"$ref": "#/components/schemas/Item"}, + "title": "Response Read Items Items Get", + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + }, + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Item", + }, + }, + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"}, + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create Item", + "operationId": "create_item_items__post", + }, + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, + "tags": { + "title": "Tags", + "type": "array", + "items": {"type": "string"}, + "default": [], + }, + }, + }, + "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"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_filter_pydantic_sub_model/test_filter_pydantic_sub_model_pv1.py b/tests/test_tutorial/test_response_model/test_tutorial002.py similarity index 58% rename from tests/test_filter_pydantic_sub_model/test_filter_pydantic_sub_model_pv1.py rename to tests/test_tutorial/test_response_model/test_tutorial002.py index 48732dbf06..216d4c420c 100644 --- a/tests/test_filter_pydantic_sub_model/test_filter_pydantic_sub_model_pv1.py +++ b/tests/test_tutorial/test_response_model/test_tutorial002.py @@ -1,43 +1,40 @@ +import importlib + import pytest -from fastapi.exceptions import ResponseValidationError from fastapi.testclient import TestClient -from ..utils import needs_pydanticv1 +from ...utils import needs_py310 -@pytest.fixture(name="client") -def get_client(): - from .app_pv1 import app +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial002_py39"), + pytest.param("tutorial002_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.response_model.{request.param}") - client = TestClient(app) + client = TestClient(mod.app) return client -@needs_pydanticv1 -def test_filter_sub_model(client: TestClient): - response = client.get("/model/modelA") - assert response.status_code == 200, response.text - assert response.json() == { - "name": "modelA", - "description": "model-a-desc", - "model_b": {"username": "test-user"}, +def test_post_user(client: TestClient): + user_data = { + "username": "foo", + "password": "fighter", + "email": "foo@example.com", + "full_name": "Grave Dohl", } + response = client.post( + "/user/", + json=user_data, + ) + assert response.status_code == 200, response.text + assert response.json() == user_data -@needs_pydanticv1 -def test_validator_is_cloned(client: TestClient): - with pytest.raises(ResponseValidationError) as err: - client.get("/model/modelX") - assert err.value.errors() == [ - { - "loc": ("response", "name"), - "msg": "name must end in A", - "type": "value_error", - } - ] - - -@needs_pydanticv1 def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text @@ -45,24 +42,14 @@ def test_openapi_schema(client: TestClient): "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { - "/model/{name}": { - "get": { - "summary": "Get Model A", - "operationId": "get_model_a_model__name__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Name", "type": "string"}, - "name": "name", - "in": "path", - } - ], + "/user/": { + "post": { "responses": { "200": { "description": "Successful Response", "content": { "application/json": { - "schema": {"$ref": "#/components/schemas/ModelA"} + "schema": {"$ref": "#/components/schemas/UserIn"} } }, }, @@ -77,38 +64,39 @@ def test_openapi_schema(client: TestClient): }, }, }, + "summary": "Create User", + "operationId": "create_user_user__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/UserIn"} + } + }, + "required": True, + }, } } }, "components": { "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", + "UserIn": { + "title": "UserIn", + "required": ["username", "password", "email"], "type": "object", "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } + "username": {"title": "Username", "type": "string"}, + "password": {"title": "Password", "type": "string"}, + "email": { + "title": "Email", + "type": "string", + "format": "email", + }, + "full_name": { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, }, }, - "ModelA": { - "title": "ModelA", - "required": ["name", "model_b"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": {"title": "Description", "type": "string"}, - "model_b": {"$ref": "#/components/schemas/ModelB"}, - }, - }, - "ModelB": { - "title": "ModelB", - "required": ["username"], - "type": "object", - "properties": {"username": {"title": "Username", "type": "string"}}, - }, "ValidationError": { "title": "ValidationError", "required": ["loc", "msg", "type"], @@ -125,6 +113,17 @@ def test_openapi_schema(client: TestClient): "type": {"title": "Error Type", "type": "string"}, }, }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, } }, } diff --git a/tests/test_tutorial/test_response_model/test_tutorial003.py b/tests/test_tutorial/test_response_model/test_tutorial003.py index 70cfd6e4cc..35ed5572dd 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial003.py +++ b/tests/test_tutorial/test_response_model/test_tutorial003.py @@ -1,8 +1,8 @@ import importlib import pytest -from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -10,7 +10,7 @@ from ...utils import needs_py310 @pytest.fixture( name="client", params=[ - "tutorial003", + pytest.param("tutorial003_py39"), pytest.param("tutorial003_py310", marks=needs_py310), ], ) @@ -42,125 +42,115 @@ def test_post_user(client: TestClient): def test_openapi_schema(client: TestClient): 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": { - "/user/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/UserOut"} - } + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/user/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserOut" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, }, }, - "422": { - "description": "Validation Error", + "summary": "Create User", + "operationId": "create_user_user__post", + "requestBody": { "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } + "schema": {"$ref": "#/components/schemas/UserIn"} } }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "UserOut": { + "title": "UserOut", + "required": ["username", "email"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "email": { + "title": "Email", + "type": "string", + "format": "email", + }, + "full_name": { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, }, }, - "summary": "Create User", - "operationId": "create_user_user__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/UserIn"} + "UserIn": { + "title": "UserIn", + "required": ["username", "password", "email"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "password": {"title": "Password", "type": "string"}, + "email": { + "title": "Email", + "type": "string", + "format": "email", + }, + "full_name": { + "title": "Full Name", + "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"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, } }, - "required": True, }, } - } - }, - "components": { - "schemas": { - "UserOut": { - "title": "UserOut", - "required": IsOneOf( - ["username", "email", "full_name"], - # TODO: remove when deprecating Pydantic v1 - ["username", "email"], - ), - "type": "object", - "properties": { - "username": {"title": "Username", "type": "string"}, - "email": { - "title": "Email", - "type": "string", - "format": "email", - }, - "full_name": IsDict( - { - "title": "Full Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Full Name", "type": "string"} - ), - }, - }, - "UserIn": { - "title": "UserIn", - "required": ["username", "password", "email"], - "type": "object", - "properties": { - "username": {"title": "Username", "type": "string"}, - "password": {"title": "Password", "type": "string"}, - "email": { - "title": "Email", - "type": "string", - "format": "email", - }, - "full_name": IsDict( - { - "title": "Full Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Full Name", "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"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } + }, + } + ) diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_01.py b/tests/test_tutorial/test_response_model/test_tutorial003_01.py index 3975856b6c..fa1eb62770 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial003_01.py +++ b/tests/test_tutorial/test_response_model/test_tutorial003_01.py @@ -1,8 +1,8 @@ import importlib import pytest -from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -10,7 +10,7 @@ from ...utils import needs_py310 @pytest.fixture( name="client", params=[ - "tutorial003_01", + pytest.param("tutorial003_01_py39"), pytest.param("tutorial003_01_py310", marks=needs_py310), ], ) @@ -42,125 +42,115 @@ def test_post_user(client: TestClient): def test_openapi_schema(client: TestClient): 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": { - "/user/": { - "post": { - "summary": "Create User", - "operationId": "create_user_user__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/UserIn"} + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/user/": { + "post": { + "summary": "Create User", + "operationId": "create_user_user__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/UserIn"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BaseUser" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "BaseUser": { + "title": "BaseUser", + "required": ["username", "email"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "email": { + "title": "Email", + "type": "string", + "format": "email", + }, + "full_name": { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, } }, - "required": True, }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/BaseUser"} - } + "UserIn": { + "title": "UserIn", + "required": ["username", "email", "password"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "email": { + "title": "Email", + "type": "string", + "format": "email", }, + "full_name": { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "password": {"title": "Password", "type": "string"}, }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } + }, + "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"}, }, }, } - } - }, - "components": { - "schemas": { - "BaseUser": { - "title": "BaseUser", - "required": IsOneOf( - ["username", "email", "full_name"], - # TODO: remove when deprecating Pydantic v1 - ["username", "email"], - ), - "type": "object", - "properties": { - "username": {"title": "Username", "type": "string"}, - "email": { - "title": "Email", - "type": "string", - "format": "email", - }, - "full_name": IsDict( - { - "title": "Full Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Full Name", "type": "string"} - ), - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "UserIn": { - "title": "UserIn", - "required": ["username", "email", "password"], - "type": "object", - "properties": { - "username": {"title": "Username", "type": "string"}, - "email": { - "title": "Email", - "type": "string", - "format": "email", - }, - "full_name": IsDict( - { - "title": "Full Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Full Name", "type": "string"} - ), - "password": {"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_response_model/test_tutorial003_02.py b/tests/test_tutorial/test_response_model/test_tutorial003_02.py index eabd203456..b7507b7110 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial003_02.py +++ b/tests/test_tutorial/test_response_model/test_tutorial003_02.py @@ -1,6 +1,6 @@ from fastapi.testclient import TestClient -from docs_src.response_model.tutorial003_02 import app +from docs_src.response_model.tutorial003_02_py39 import app client = TestClient(app) diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_03.py b/tests/test_tutorial/test_response_model/test_tutorial003_03.py index 970ff58450..ea3c733b24 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial003_03.py +++ b/tests/test_tutorial/test_response_model/test_tutorial003_03.py @@ -1,6 +1,6 @@ from fastapi.testclient import TestClient -from docs_src.response_model.tutorial003_03 import app +from docs_src.response_model.tutorial003_03_py39 import app client = TestClient(app) diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_04.py b/tests/test_tutorial/test_response_model/test_tutorial003_04.py index f32e93ddcb..145af126fd 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial003_04.py +++ b/tests/test_tutorial/test_response_model/test_tutorial003_04.py @@ -9,7 +9,7 @@ from ...utils import needs_py310 @pytest.mark.parametrize( "module_name", [ - "tutorial003_04", + pytest.param("tutorial003_04_py39"), pytest.param("tutorial003_04_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_05.py b/tests/test_tutorial/test_response_model/test_tutorial003_05.py index 9500852e15..19a7c601bb 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial003_05.py +++ b/tests/test_tutorial/test_response_model/test_tutorial003_05.py @@ -9,7 +9,7 @@ from ...utils import needs_py310 @pytest.fixture( name="client", params=[ - "tutorial003_05", + pytest.param("tutorial003_05_py39"), pytest.param("tutorial003_05_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_response_model/test_tutorial004.py b/tests/test_tutorial/test_response_model/test_tutorial004.py index 449a52b813..9c0d95ebd0 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial004.py +++ b/tests/test_tutorial/test_response_model/test_tutorial004.py @@ -1,17 +1,16 @@ import importlib import pytest -from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient +from inline_snapshot import snapshot -from ...utils import needs_py39, needs_py310 +from ...utils import needs_py310 @pytest.fixture( name="client", params=[ - "tutorial004", - pytest.param("tutorial004_py39", marks=needs_py39), + pytest.param("tutorial004_py39"), pytest.param("tutorial004_py310", marks=needs_py310), ], ) @@ -51,104 +50,98 @@ def test_get(url, data, client: TestClient): def test_openapi_schema(client: TestClient): 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": { - "/items/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} } - } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, }, }, - }, - "summary": "Read Item", - "operationId": "read_item_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": IsOneOf( - ["name", "description", "price", "tax", "tags"], - # TODO: remove when deprecating Pydantic v1 - ["name", "price"], - ), - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "description": IsDict( + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": { "title": "Description", "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), - "tax": {"title": "Tax", "type": "number", "default": 10.5}, - "tags": { - "title": "Tags", - "type": "array", - "items": {"type": "string"}, - "default": [], - }, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "tax": {"title": "Tax", "type": "number", "default": 10.5}, + "tags": { + "title": "Tags", + "type": "array", + "items": {"type": "string"}, + "default": [], }, }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } + "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"}, + }, }, - }, - } - }, - } + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + } + }, + } + ) diff --git a/tests/test_tutorial/test_response_model/test_tutorial005.py b/tests/test_tutorial/test_response_model/test_tutorial005.py index a633a3fddd..63e8535db0 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial005.py +++ b/tests/test_tutorial/test_response_model/test_tutorial005.py @@ -1,8 +1,8 @@ import importlib import pytest -from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -10,7 +10,7 @@ from ...utils import needs_py310 @pytest.fixture( name="client", params=[ - "tutorial005", + pytest.param("tutorial005_py39"), pytest.param("tutorial005_py310", marks=needs_py310), ], ) @@ -40,132 +40,126 @@ def test_read_item_public_data(client: TestClient): def test_openapi_schema(client: TestClient): 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": { - "/items/{item_id}/name": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}/name": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} } - } + }, }, - }, - }, - "summary": "Read Item Name", - "operationId": "read_item_name_items__item_id__name_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/items/{item_id}/public": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } } - } + }, }, }, - }, - "summary": "Read Item Public Data", - "operationId": "read_item_public_data_items__item_id__public_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - } - }, - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": IsOneOf( - ["name", "description", "price", "tax"], - # TODO: remove when deprecating Pydantic v1 - ["name", "price"], - ), - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "description": IsDict( + "summary": "Read Item Name", + "operationId": "read_item_name_items__item_id__name_get", + "parameters": [ { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/items/{item_id}/public": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Item Public Data", + "operationId": "read_item_public_data_items__item_id__public_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + } + }, + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": { "title": "Description", "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), - "tax": {"title": "Tax", "type": "number", "default": 10.5}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] }, + "tax": {"title": "Tax", "type": "number", "default": 10.5}, }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } + "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"}, + }, }, - }, - } - }, - } + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + } + }, + } + ) diff --git a/tests/test_tutorial/test_response_model/test_tutorial006.py b/tests/test_tutorial/test_response_model/test_tutorial006.py index 863522d1b4..08ab659527 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial006.py +++ b/tests/test_tutorial/test_response_model/test_tutorial006.py @@ -1,8 +1,8 @@ import importlib import pytest -from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -10,7 +10,7 @@ from ...utils import needs_py310 @pytest.fixture( name="client", params=[ - "tutorial006", + pytest.param("tutorial006_py39"), pytest.param("tutorial006_py310", marks=needs_py310), ], ) @@ -40,132 +40,126 @@ def test_read_item_public_data(client: TestClient): def test_openapi_schema(client: TestClient): 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": { - "/items/{item_id}/name": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}/name": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} } - } + }, }, - }, - }, - "summary": "Read Item Name", - "operationId": "read_item_name_items__item_id__name_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/items/{item_id}/public": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } } - } + }, }, }, - }, - "summary": "Read Item Public Data", - "operationId": "read_item_public_data_items__item_id__public_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - } - }, - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": IsOneOf( - ["name", "description", "price", "tax"], - # TODO: remove when deprecating Pydantic v1 - ["name", "price"], - ), - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "description": IsDict( + "summary": "Read Item Name", + "operationId": "read_item_name_items__item_id__name_get", + "parameters": [ { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/items/{item_id}/public": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Item Public Data", + "operationId": "read_item_public_data_items__item_id__public_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + } + }, + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": { "title": "Description", "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), - "tax": {"title": "Tax", "type": "number", "default": 10.5}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] }, + "tax": {"title": "Tax", "type": "number", "default": 10.5}, }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } + "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"}, + }, }, - }, - } - }, - } + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + } + }, + } + ) diff --git a/tests/test_tutorial/test_response_status_code/__init__.py b/tests/test_tutorial/test_response_status_code/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/test_tutorial/test_response_status_code/test_tutorial001_tutorial002.py b/tests/test_tutorial/test_response_status_code/test_tutorial001_tutorial002.py new file mode 100644 index 0000000000..ddf55a045d --- /dev/null +++ b/tests/test_tutorial/test_response_status_code/test_tutorial001_tutorial002.py @@ -0,0 +1,96 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial001_py39"), + pytest.param("tutorial002_py39"), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.response_status_code.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_create_item(client: TestClient): + response = client.post("/items/", params={"name": "Test Item"}) + assert response.status_code == 201, response.text + assert response.json() == {"name": "Test Item"} + + +def test_openapi_schema(client: TestClient): + 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": { + "/items/": { + "post": { + "parameters": [ + { + "name": "name", + "in": "query", + "required": True, + "schema": {"title": "Name", "type": "string"}, + } + ], + "summary": "Create Item", + "operationId": "create_item_items__post", + "responses": { + "201": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "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"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial001.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial001.py index c21cbb4bc2..82f69fd463 100644 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial001.py +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial001.py @@ -3,13 +3,13 @@ import importlib import pytest from fastapi.testclient import TestClient -from ...utils import needs_py310, needs_pydanticv2 +from ...utils import needs_py310 @pytest.fixture( name="client", params=[ - "tutorial001", + pytest.param("tutorial001_py39"), pytest.param("tutorial001_py310", marks=needs_py310), ], ) @@ -20,7 +20,6 @@ def get_client(request: pytest.FixtureRequest): return client -@needs_pydanticv2 def test_post_body_example(client: TestClient): response = client.put( "/items/5", @@ -34,7 +33,6 @@ def test_post_body_example(client: TestClient): assert response.status_code == 200 -@needs_pydanticv2 def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial001_pv1.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial002.py similarity index 79% rename from tests/test_tutorial/test_schema_extra_example/test_tutorial001_pv1.py rename to tests/test_tutorial/test_schema_extra_example/test_tutorial002.py index b79f42e642..4f52408605 100644 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial001_pv1.py +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial002.py @@ -3,14 +3,14 @@ import importlib import pytest from fastapi.testclient import TestClient -from ...utils import needs_py310, needs_pydanticv1 +from ...utils import needs_py310 @pytest.fixture( name="client", params=[ - "tutorial001_pv1", - pytest.param("tutorial001_pv1_py310", marks=needs_py310), + pytest.param("tutorial002_py39"), + pytest.param("tutorial002_py310", marks=needs_py310), ], ) def get_client(request: pytest.FixtureRequest): @@ -20,7 +20,6 @@ def get_client(request: pytest.FixtureRequest): return client -@needs_pydanticv1 def test_post_body_example(client: TestClient): response = client.put( "/items/5", @@ -34,7 +33,6 @@ def test_post_body_example(client: TestClient): assert response.status_code == 200 -@needs_pydanticv1 def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text @@ -49,19 +47,19 @@ def test_openapi_schema(client: TestClient): "operationId": "update_item_items__item_id__put", "parameters": [ { - "required": True, - "schema": {"type": "integer", "title": "Item Id"}, "name": "item_id", "in": "path", + "required": True, + "schema": {"type": "integer", "title": "Item Id"}, } ], "requestBody": { + "required": True, "content": { "application/json": { "schema": {"$ref": "#/components/schemas/Item"} } }, - "required": True, }, "responses": { "200": { @@ -97,22 +95,30 @@ def test_openapi_schema(client: TestClient): }, "Item": { "properties": { - "name": {"type": "string", "title": "Name"}, - "description": {"type": "string", "title": "Description"}, - "price": {"type": "number", "title": "Price"}, - "tax": {"type": "number", "title": "Tax"}, + "name": { + "type": "string", + "title": "Name", + "examples": ["Foo"], + }, + "description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Description", + "examples": ["A very nice Item"], + }, + "price": { + "type": "number", + "title": "Price", + "examples": [35.4], + }, + "tax": { + "anyOf": [{"type": "number"}, {"type": "null"}], + "title": "Tax", + "examples": [3.2], + }, }, "type": "object", "required": ["name", "price"], "title": "Item", - "examples": [ - { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - } - ], }, "ValidationError": { "properties": { diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial003.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial003.py new file mode 100644 index 0000000000..3529a9bf02 --- /dev/null +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial003.py @@ -0,0 +1,143 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial003_py39"), + pytest.param("tutorial003_py310", marks=needs_py310), + pytest.param("tutorial003_an_py39"), + pytest.param("tutorial003_an_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.schema_extra_example.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_post_body_example(client: TestClient): + response = client.put( + "/items/5", + json={ + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + ) + assert response.status_code == 200 + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + # insert_assert(response.json()) + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "name": "item_id", + "in": "path", + "required": True, + "schema": {"type": "integer", "title": "Item Id"}, + } + ], + "requestBody": { + "required": True, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Item", + "examples": [ + { + "description": "A very nice Item", + "name": "Foo", + "price": 35.4, + "tax": 3.2, + } + ], + }, + } + }, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": {"$ref": "#/components/schemas/ValidationError"}, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "Item": { + "properties": { + "name": {"type": "string", "title": "Name"}, + "description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Description", + }, + "price": {"type": "number", "title": "Price"}, + "tax": { + "anyOf": [{"type": "number"}, {"type": "null"}], + "title": "Tax", + }, + }, + "type": "object", + "required": ["name", "price"], + "title": "Item", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial004.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial004.py index 61aefd12a8..9326e06290 100644 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial004.py +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial004.py @@ -1,19 +1,17 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient -from ...utils import needs_py39, needs_py310 +from ...utils import needs_py310 @pytest.fixture( name="client", params=[ - "tutorial004", + pytest.param("tutorial004_py39"), pytest.param("tutorial004_py310", marks=needs_py310), - "tutorial004_an", - pytest.param("tutorial004_an_py39", marks=needs_py39), + pytest.param("tutorial004_an_py39"), pytest.param("tutorial004_an_py310", marks=needs_py310), ], ) @@ -59,46 +57,22 @@ def test_openapi_schema(client: TestClient): "requestBody": { "content": { "application/json": { - "schema": IsDict( - { - "$ref": "#/components/schemas/Item", - "examples": [ - { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, - {"name": "Bar", "price": "35.4"}, - { - "name": "Baz", - "price": "thirty five point four", - }, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - "title": "Item", - "examples": [ - { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, - {"name": "Bar", "price": "35.4"}, - { - "name": "Baz", - "price": "thirty five point four", - }, - ], - } - ) + "schema": { + "$ref": "#/components/schemas/Item", + "examples": [ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + {"name": "Bar", "price": "35.4"}, + { + "name": "Baz", + "price": "thirty five point four", + }, + ], + } } }, "required": True, @@ -141,27 +115,15 @@ def test_openapi_schema(client: TestClient): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, - "description": IsDict( - { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, "price": {"title": "Price", "type": "number"}, - "tax": IsDict( - { - "title": "Tax", - "anyOf": [{"type": "number"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Tax", "type": "number"} - ), + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial005.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial005.py index 12859227b1..2d0dee48ca 100644 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial005.py +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial005.py @@ -1,19 +1,17 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient -from ...utils import needs_py39, needs_py310 +from ...utils import needs_py310 @pytest.fixture( name="client", params=[ - "tutorial005", + pytest.param("tutorial005_py39"), pytest.param("tutorial005_py310", marks=needs_py310), - "tutorial005_an", - pytest.param("tutorial005_an_py39", marks=needs_py39), + pytest.param("tutorial005_an_py39"), pytest.param("tutorial005_an_py310", marks=needs_py310), ], ) @@ -59,16 +57,7 @@ def test_openapi_schema(client: TestClient) -> None: "requestBody": { "content": { "application/json": { - "schema": IsDict({"$ref": "#/components/schemas/Item"}) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - "title": "Item", - } - ), + "schema": {"$ref": "#/components/schemas/Item"}, "examples": { "normal": { "summary": "A normal example", @@ -135,27 +124,15 @@ def test_openapi_schema(client: TestClient) -> None: "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, - "description": IsDict( - { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, "price": {"title": "Price", "type": "number"}, - "tax": IsDict( - { - "title": "Tax", - "anyOf": [{"type": "number"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Tax", "type": "number"} - ), + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_security/test_tutorial001.py b/tests/test_tutorial/test_security/test_tutorial001.py index f572d6e3e1..cdaa50b191 100644 --- a/tests/test_tutorial/test_security/test_tutorial001.py +++ b/tests/test_tutorial/test_security/test_tutorial001.py @@ -3,15 +3,12 @@ import importlib import pytest from fastapi.testclient import TestClient -from ...utils import needs_py39 - @pytest.fixture( name="client", params=[ - "tutorial001", - "tutorial001_an", - pytest.param("tutorial001_an_py39", marks=needs_py39), + pytest.param("tutorial001_py39"), + pytest.param("tutorial001_an_py39"), ], ) def get_client(request: pytest.FixtureRequest): diff --git a/tests/test_tutorial/test_security/test_tutorial002.py b/tests/test_tutorial/test_security/test_tutorial002.py new file mode 100644 index 0000000000..85c076b1d2 --- /dev/null +++ b/tests/test_tutorial/test_security/test_tutorial002.py @@ -0,0 +1,71 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial002_py39"), + pytest.param("tutorial002_py310", marks=needs_py310), + pytest.param("tutorial002_an_py39"), + pytest.param("tutorial002_an_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.security.{request.param}") + client = TestClient(mod.app) + return client + + +def test_no_token(client: TestClient): + response = client.get("/users/me") + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +def test_token(client: TestClient): + response = client.get("/users/me", headers={"Authorization": "Bearer testtoken"}) + assert response.status_code == 200, response.text + assert response.json() == { + "username": "testtokenfakedecoded", + "email": "john@example.com", + "full_name": "John Doe", + "disabled": None, + } + + +def test_openapi_schema(client: TestClient): + 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": { + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Users Me", + "operationId": "read_users_me_users_me_get", + "security": [{"OAuth2PasswordBearer": []}], + } + } + }, + "components": { + "securitySchemes": { + "OAuth2PasswordBearer": { + "type": "oauth2", + "flows": {"password": {"scopes": {}, "tokenUrl": "token"}}, + } + }, + }, + } diff --git a/tests/test_tutorial/test_security/test_tutorial003.py b/tests/test_tutorial/test_security/test_tutorial003.py index 2bbb2e8510..6a786348cf 100644 --- a/tests/test_tutorial/test_security/test_tutorial003.py +++ b/tests/test_tutorial/test_security/test_tutorial003.py @@ -1,19 +1,17 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient -from ...utils import needs_py39, needs_py310 +from ...utils import needs_py310 @pytest.fixture( name="client", params=[ - "tutorial003", + pytest.param("tutorial003_py39"), pytest.param("tutorial003_py310", marks=needs_py310), - "tutorial003_an", - pytest.param("tutorial003_an_py39", marks=needs_py39), + pytest.param("tutorial003_an_py39"), pytest.param("tutorial003_an_py310", marks=needs_py310), ], ) @@ -66,7 +64,7 @@ def test_token(client: TestClient): def test_incorrect_token(client: TestClient): response = client.get("/users/me", headers={"Authorization": "Bearer nonexistent"}) assert response.status_code == 401, response.text - assert response.json() == {"detail": "Invalid authentication credentials"} + assert response.json() == {"detail": "Not authenticated"} assert response.headers["WWW-Authenticate"] == "Bearer" @@ -145,23 +143,13 @@ def test_openapi_schema(client: TestClient): "required": ["username", "password"], "type": "object", "properties": { - "grant_type": IsDict( - { - "title": "Grant Type", - "anyOf": [ - {"pattern": "^password$", "type": "string"}, - {"type": "null"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Grant Type", - "pattern": "^password$", - "type": "string", - } - ), + "grant_type": { + "title": "Grant Type", + "anyOf": [ + {"pattern": "^password$", "type": "string"}, + {"type": "null"}, + ], + }, "username": {"title": "Username", "type": "string"}, "password": { "title": "Password", @@ -169,31 +157,15 @@ def test_openapi_schema(client: TestClient): "format": "password", }, "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": IsDict( - { - "title": "Client Id", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Client Id", "type": "string"} - ), - "client_secret": IsDict( - { - "title": "Client Secret", - "anyOf": [{"type": "string"}, {"type": "null"}], - "format": "password", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Client Secret", - "type": "string", - "format": "password", - } - ), + "client_id": { + "title": "Client Id", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "client_secret": { + "title": "Client Secret", + "anyOf": [{"type": "string"}, {"type": "null"}], + "format": "password", + }, }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_security/test_tutorial004.py b/tests/test_tutorial/test_security/test_tutorial004.py new file mode 100644 index 0000000000..b5e3d39ef7 --- /dev/null +++ b/tests/test_tutorial/test_security/test_tutorial004.py @@ -0,0 +1,363 @@ +import importlib +from types import ModuleType +from unittest.mock import patch + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial004_py39"), + pytest.param("tutorial004_py310", marks=needs_py310), + pytest.param("tutorial004_an_py39"), + pytest.param("tutorial004_an_py310", marks=needs_py310), + ], +) +def get_mod(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.security.{request.param}") + + return mod + + +def get_access_token(*, username="johndoe", password="secret", client: TestClient): + data = {"username": username, "password": password} + response = client.post("/token", data=data) + content = response.json() + access_token = content.get("access_token") + return access_token + + +def test_login(mod: ModuleType): + client = TestClient(mod.app) + response = client.post("/token", data={"username": "johndoe", "password": "secret"}) + assert response.status_code == 200, response.text + content = response.json() + assert "access_token" in content + assert content["token_type"] == "bearer" + + +def test_login_incorrect_password(mod: ModuleType): + client = TestClient(mod.app) + response = client.post( + "/token", data={"username": "johndoe", "password": "incorrect"} + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Incorrect username or password"} + + +def test_login_incorrect_username(mod: ModuleType): + client = TestClient(mod.app) + response = client.post("/token", data={"username": "foo", "password": "secret"}) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Incorrect username or password"} + + +def test_no_token(mod: ModuleType): + client = TestClient(mod.app) + response = client.get("/users/me") + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +def test_token(mod: ModuleType): + client = TestClient(mod.app) + access_token = get_access_token(client=client) + response = client.get( + "/users/me", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 200, response.text + assert response.json() == { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "disabled": False, + } + + +def test_incorrect_token(mod: ModuleType): + client = TestClient(mod.app) + response = client.get("/users/me", headers={"Authorization": "Bearer nonexistent"}) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Could not validate credentials"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +def test_incorrect_token_type(mod: ModuleType): + client = TestClient(mod.app) + response = client.get( + "/users/me", headers={"Authorization": "Notexistent testtoken"} + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +def test_verify_password(mod: ModuleType): + assert mod.verify_password( + "secret", mod.fake_users_db["johndoe"]["hashed_password"] + ) + + +def test_get_password_hash(mod: ModuleType): + assert mod.get_password_hash("johndoe") + + +def test_create_access_token(mod: ModuleType): + access_token = mod.create_access_token(data={"data": "foo"}) + assert access_token + + +def test_token_no_sub(mod: ModuleType): + client = TestClient(mod.app) + + response = client.get( + "/users/me", + headers={ + "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjoiZm9vIn0.9ynBhuYb4e6aW3oJr_K_TBgwcMTDpRToQIE25L57rOE" + }, + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Could not validate credentials"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +def test_token_no_username(mod: ModuleType): + client = TestClient(mod.app) + + response = client.get( + "/users/me", + headers={ + "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmb28ifQ.NnExK_dlNAYyzACrXtXDrcWOgGY2JuPbI4eDaHdfK5Y" + }, + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Could not validate credentials"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +def test_token_nonexistent_user(mod: ModuleType): + client = TestClient(mod.app) + + response = client.get( + "/users/me", + headers={ + "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VybmFtZTpib2IifQ.HcfCW67Uda-0gz54ZWTqmtgJnZeNem0Q757eTa9EZuw" + }, + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Could not validate credentials"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +def test_token_inactive_user(mod: ModuleType): + client = TestClient(mod.app) + alice_user_data = { + "username": "alice", + "full_name": "Alice Wonderson", + "email": "alice@example.com", + "hashed_password": mod.get_password_hash("secretalice"), + "disabled": True, + } + with patch.dict(f"{mod.__name__}.fake_users_db", {"alice": alice_user_data}): + access_token = get_access_token( + username="alice", password="secretalice", client=client + ) + response = client.get( + "/users/me", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Inactive user"} + + +def test_read_items(mod: ModuleType): + client = TestClient(mod.app) + access_token = get_access_token(client=client) + response = client.get( + "/users/me/items/", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 200, response.text + assert response.json() == [{"item_id": "Foo", "owner": "johndoe"}] + + +def test_openapi_schema(mod: ModuleType): + client = TestClient(mod.app) + 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": { + "/token": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Token"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Login For Access Token", + "operationId": "login_for_access_token_token_post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Body_login_for_access_token_token_post" + } + } + }, + "required": True, + }, + } + }, + "/users/me/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + "summary": "Read Users Me", + "operationId": "read_users_me_users_me__get", + "security": [{"OAuth2PasswordBearer": []}], + } + }, + "/users/me/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Own Items", + "operationId": "read_own_items_users_me_items__get", + "security": [{"OAuth2PasswordBearer": []}], + } + }, + }, + "components": { + "schemas": { + "User": { + "title": "User", + "required": ["username"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "email": { + "title": "Email", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "full_name": { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "disabled": { + "title": "Disabled", + "anyOf": [{"type": "boolean"}, {"type": "null"}], + }, + }, + }, + "Token": { + "title": "Token", + "required": ["access_token", "token_type"], + "type": "object", + "properties": { + "access_token": {"title": "Access Token", "type": "string"}, + "token_type": {"title": "Token Type", "type": "string"}, + }, + }, + "Body_login_for_access_token_token_post": { + "title": "Body_login_for_access_token_token_post", + "required": ["username", "password"], + "type": "object", + "properties": { + "grant_type": { + "title": "Grant Type", + "anyOf": [ + {"pattern": "^password$", "type": "string"}, + {"type": "null"}, + ], + }, + "username": {"title": "Username", "type": "string"}, + "password": { + "title": "Password", + "type": "string", + "format": "password", + }, + "scope": {"title": "Scope", "type": "string", "default": ""}, + "client_id": { + "title": "Client Id", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "client_secret": { + "title": "Client Secret", + "anyOf": [{"type": "string"}, {"type": "null"}], + "format": "password", + }, + }, + }, + "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"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + }, + "securitySchemes": { + "OAuth2PasswordBearer": { + "type": "oauth2", + "flows": { + "password": { + "scopes": {}, + "tokenUrl": "token", + } + }, + } + }, + }, + } diff --git a/tests/test_tutorial/test_security/test_tutorial005.py b/tests/test_tutorial/test_security/test_tutorial005.py index ad644d61bb..25b47f0adc 100644 --- a/tests/test_tutorial/test_security/test_tutorial005.py +++ b/tests/test_tutorial/test_security/test_tutorial005.py @@ -2,20 +2,18 @@ import importlib from types import ModuleType import pytest -from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient +from inline_snapshot import snapshot -from ...utils import needs_py39, needs_py310 +from ...utils import needs_py310 @pytest.fixture( name="mod", params=[ - "tutorial005", + pytest.param("tutorial005_py39"), pytest.param("tutorial005_py310", marks=needs_py310), - "tutorial005_an", - pytest.param("tutorial005_py39", marks=needs_py39), - pytest.param("tutorial005_an_py39", marks=needs_py39), + pytest.param("tutorial005_an_py39"), pytest.param("tutorial005_an_py310", marks=needs_py310), ], ) @@ -217,240 +215,200 @@ def test_openapi_schema(mod: ModuleType): client = TestClient(mod.app) 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": { - "/token": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Token"} - } + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/token": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Token"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, }, }, - "422": { - "description": "Validation Error", + "summary": "Login For Access Token", + "operationId": "login_for_access_token_token_post", + "requestBody": { "content": { - "application/json": { + "application/x-www-form-urlencoded": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "$ref": "#/components/schemas/Body_login_for_access_token_token_post" } } }, + "required": True, }, - }, - "summary": "Login For Access Token", - "operationId": "login_for_access_token_token_post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/Body_login_for_access_token_token_post" - } + } + }, + "/users/me/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, } }, - "required": True, - }, - } + "summary": "Read Users Me", + "operationId": "read_users_me_users_me__get", + "security": [{"OAuth2PasswordBearer": ["me"]}], + } + }, + "/users/me/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Own Items", + "operationId": "read_own_items_users_me_items__get", + "security": [{"OAuth2PasswordBearer": ["items", "me"]}], + } + }, + "/status/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read System Status", + "operationId": "read_system_status_status__get", + "security": [{"OAuth2PasswordBearer": []}], + } + }, }, - "/users/me/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - "summary": "Read Users Me", - "operationId": "read_users_me_users_me__get", - "security": [{"OAuth2PasswordBearer": ["me"]}], - } - }, - "/users/me/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Own Items", - "operationId": "read_own_items_users_me_items__get", - "security": [{"OAuth2PasswordBearer": ["items", "me"]}], - } - }, - "/status/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read System Status", - "operationId": "read_system_status_status__get", - "security": [{"OAuth2PasswordBearer": []}], - } - }, - }, - "components": { - "schemas": { - "User": { - "title": "User", - "required": IsOneOf( - ["username", "email", "full_name", "disabled"], - # TODO: remove when deprecating Pydantic v1 - ["username"], - ), - "type": "object", - "properties": { - "username": {"title": "Username", "type": "string"}, - "email": IsDict( - { + "components": { + "schemas": { + "User": { + "title": "User", + "required": ["username"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "email": { "title": "Email", "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Email", "type": "string"} - ), - "full_name": IsDict( - { + }, + "full_name": { "title": "Full Name", "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Full Name", "type": "string"} - ), - "disabled": IsDict( - { + }, + "disabled": { "title": "Disabled", "anyOf": [{"type": "boolean"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Disabled", "type": "boolean"} - ), + }, + }, }, - }, - "Token": { - "title": "Token", - "required": ["access_token", "token_type"], - "type": "object", - "properties": { - "access_token": {"title": "Access Token", "type": "string"}, - "token_type": {"title": "Token Type", "type": "string"}, + "Token": { + "title": "Token", + "required": ["access_token", "token_type"], + "type": "object", + "properties": { + "access_token": {"title": "Access Token", "type": "string"}, + "token_type": {"title": "Token Type", "type": "string"}, + }, }, - }, - "Body_login_for_access_token_token_post": { - "title": "Body_login_for_access_token_token_post", - "required": ["username", "password"], - "type": "object", - "properties": { - "grant_type": IsDict( - { + "Body_login_for_access_token_token_post": { + "title": "Body_login_for_access_token_token_post", + "required": ["username", "password"], + "type": "object", + "properties": { + "grant_type": { "title": "Grant Type", "anyOf": [ {"pattern": "^password$", "type": "string"}, {"type": "null"}, ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Grant Type", - "pattern": "^password$", + }, + "username": {"title": "Username", "type": "string"}, + "password": { + "title": "Password", "type": "string", - } - ), - "username": {"title": "Username", "type": "string"}, - "password": { - "title": "Password", - "type": "string", - "format": "password", - }, - "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": IsDict( - { + "format": "password", + }, + "scope": { + "title": "Scope", + "type": "string", + "default": "", + }, + "client_id": { "title": "Client Id", "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Client Id", "type": "string"} - ), - "client_secret": IsDict( - { + }, + "client_secret": { "title": "Client Secret", "anyOf": [{"type": "string"}, {"type": "null"}], "format": "password", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Client Secret", - "type": "string", - "format": "password", - } - ), - }, - }, - "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"}, }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - }, - "securitySchemes": { - "OAuth2PasswordBearer": { - "type": "oauth2", - "flows": { - "password": { - "scopes": { - "me": "Read information about the current user.", - "items": "Read items.", + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, }, - "tokenUrl": "token", - } + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, }, - } + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + }, + "securitySchemes": { + "OAuth2PasswordBearer": { + "type": "oauth2", + "flows": { + "password": { + "scopes": { + "me": "Read information about the current user.", + "items": "Read items.", + }, + "tokenUrl": "token", + } + }, + } + }, }, - }, - } + } + ) diff --git a/tests/test_tutorial/test_security/test_tutorial006.py b/tests/test_tutorial/test_security/test_tutorial006.py index 40b4138062..a4b3104bbc 100644 --- a/tests/test_tutorial/test_security/test_tutorial006.py +++ b/tests/test_tutorial/test_security/test_tutorial006.py @@ -4,15 +4,12 @@ from base64 import b64encode import pytest from fastapi.testclient import TestClient -from ...utils import needs_py39 - @pytest.fixture( name="client", params=[ - "tutorial006", - "tutorial006_an", - pytest.param("tutorial006_an_py39", marks=needs_py39), + pytest.param("tutorial006_py39"), + pytest.param("tutorial006_an_py39"), ], ) def get_client(request: pytest.FixtureRequest): @@ -41,7 +38,7 @@ def test_security_http_basic_invalid_credentials(client: TestClient): ) assert response.status_code == 401, response.text assert response.headers["WWW-Authenticate"] == "Basic" - assert response.json() == {"detail": "Invalid authentication credentials"} + assert response.json() == {"detail": "Not authenticated"} def test_security_http_basic_non_basic_credentials(client: TestClient): @@ -50,7 +47,7 @@ def test_security_http_basic_non_basic_credentials(client: TestClient): response = client.get("/users/me", headers={"Authorization": auth_header}) assert response.status_code == 401, response.text assert response.headers["WWW-Authenticate"] == "Basic" - assert response.json() == {"detail": "Invalid authentication credentials"} + assert response.json() == {"detail": "Not authenticated"} def test_openapi_schema(client: TestClient): diff --git a/tests/test_tutorial/test_security/test_tutorial007.py b/tests/test_tutorial/test_security/test_tutorial007.py new file mode 100644 index 0000000000..28b70a2d43 --- /dev/null +++ b/tests/test_tutorial/test_security/test_tutorial007.py @@ -0,0 +1,89 @@ +import importlib +from base64 import b64encode + +import pytest +from fastapi.testclient import TestClient + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial007_py39"), + pytest.param("tutorial007_an_py39"), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.security.{request.param}") + return TestClient(mod.app) + + +def test_security_http_basic(client: TestClient): + response = client.get("/users/me", auth=("stanleyjobson", "swordfish")) + assert response.status_code == 200, response.text + assert response.json() == {"username": "stanleyjobson"} + + +def test_security_http_basic_no_credentials(client: TestClient): + response = client.get("/users/me") + assert response.json() == {"detail": "Not authenticated"} + assert response.status_code == 401, response.text + assert response.headers["WWW-Authenticate"] == "Basic" + + +def test_security_http_basic_invalid_credentials(client: TestClient): + response = client.get( + "/users/me", headers={"Authorization": "Basic notabase64token"} + ) + assert response.status_code == 401, response.text + assert response.headers["WWW-Authenticate"] == "Basic" + assert response.json() == {"detail": "Not authenticated"} + + +def test_security_http_basic_non_basic_credentials(client: TestClient): + payload = b64encode(b"johnsecret").decode("ascii") + auth_header = f"Basic {payload}" + response = client.get("/users/me", headers={"Authorization": auth_header}) + assert response.status_code == 401, response.text + assert response.headers["WWW-Authenticate"] == "Basic" + assert response.json() == {"detail": "Not authenticated"} + + +def test_security_http_basic_invalid_username(client: TestClient): + response = client.get("/users/me", auth=("alice", "swordfish")) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Incorrect username or password"} + assert response.headers["WWW-Authenticate"] == "Basic" + + +def test_security_http_basic_invalid_password(client: TestClient): + response = client.get("/users/me", auth=("stanleyjobson", "wrongpassword")) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Incorrect username or password"} + assert response.headers["WWW-Authenticate"] == "Basic" + + +def test_openapi_schema(client: TestClient): + 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": { + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Current User", + "operationId": "read_current_user_users_me_get", + "security": [{"HTTPBasic": []}], + } + } + }, + "components": { + "securitySchemes": {"HTTPBasic": {"type": "http", "scheme": "basic"}} + }, + } diff --git a/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001.py b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001.py index 059fb889b3..275b234877 100644 --- a/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001.py +++ b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001.py @@ -3,15 +3,14 @@ import importlib import pytest from fastapi.testclient import TestClient -from ...utils import needs_py39, needs_py310, needs_pydanticv2 +from ...utils import needs_py310 @pytest.fixture( name="client", params=[ - "tutorial001", + pytest.param("tutorial001_py39"), pytest.param("tutorial001_py310", marks=needs_py310), - pytest.param("tutorial001_py39", marks=needs_py39), ], ) def get_client(request: pytest.FixtureRequest) -> TestClient: @@ -39,7 +38,6 @@ def test_read_items(client: TestClient) -> None: ] -@needs_pydanticv2 def test_openapi_schema(client: TestClient) -> None: response = client.get("/openapi.json") assert response.status_code == 200, response.text diff --git a/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial002.py b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial002.py index cc9afeab75..8230e39226 100644 --- a/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial002.py +++ b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial002.py @@ -3,15 +3,14 @@ import importlib import pytest from fastapi.testclient import TestClient -from ...utils import needs_py39, needs_py310, needs_pydanticv2 +from ...utils import needs_py310 @pytest.fixture( name="client", params=[ - "tutorial002", + pytest.param("tutorial002_py39"), pytest.param("tutorial002_py310", marks=needs_py310), - pytest.param("tutorial002_py39", marks=needs_py39), ], ) def get_client(request: pytest.FixtureRequest) -> TestClient: @@ -39,7 +38,6 @@ def test_read_items(client: TestClient) -> None: ] -@needs_pydanticv2 def test_openapi_schema(client: TestClient) -> None: response = client.get("/openapi.json") assert response.status_code == 200, response.text diff --git a/tests/test_tutorial/test_settings/test_app01.py b/tests/test_tutorial/test_settings/test_app01.py new file mode 100644 index 0000000000..0c5e440f1a --- /dev/null +++ b/tests/test_tutorial/test_settings/test_app01.py @@ -0,0 +1,78 @@ +import importlib +import sys + +import pytest +from dirty_equals import IsAnyStr +from fastapi.testclient import TestClient +from pydantic import ValidationError +from pytest import MonkeyPatch + + +@pytest.fixture( + name="mod_name", + params=[ + pytest.param("app01_py39"), + ], +) +def get_mod_name(request: pytest.FixtureRequest): + return f"docs_src.settings.{request.param}.main" + + +@pytest.fixture(name="client") +def get_test_client(mod_name: str, monkeypatch: MonkeyPatch) -> TestClient: + if mod_name in sys.modules: + del sys.modules[mod_name] + monkeypatch.setenv("ADMIN_EMAIL", "admin@example.com") + main_mod = importlib.import_module(mod_name) + return TestClient(main_mod.app) + + +def test_settings_validation_error(mod_name: str, monkeypatch: MonkeyPatch): + monkeypatch.delenv("ADMIN_EMAIL", raising=False) + if mod_name in sys.modules: + del sys.modules[mod_name] # pragma: no cover + + with pytest.raises(ValidationError) as exc_info: + importlib.import_module(mod_name) + assert exc_info.value.errors() == [ + { + "loc": ("admin_email",), + "msg": "Field required", + "type": "missing", + "input": {}, + "url": IsAnyStr, + } + ] + + +def test_app(client: TestClient): + response = client.get("/info") + data = response.json() + assert data == { + "app_name": "Awesome API", + "admin_email": "admin@example.com", + "items_per_user": 50, + } + + +def test_openapi_schema(client: TestClient): + 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": { + "/info": { + "get": { + "operationId": "info_info_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Info", + } + } + }, + } diff --git a/tests/test_tutorial/test_settings/test_app02.py b/tests/test_tutorial/test_settings/test_app02.py index eced88c044..9cbed4fd1b 100644 --- a/tests/test_tutorial/test_settings/test_app02.py +++ b/tests/test_tutorial/test_settings/test_app02.py @@ -1,20 +1,40 @@ +import importlib +from types import ModuleType + +import pytest from pytest import MonkeyPatch -from ...utils import needs_pydanticv2 + +@pytest.fixture( + name="mod_path", + params=[ + pytest.param("app02_py39"), + pytest.param("app02_an_py39"), + ], +) +def get_mod_path(request: pytest.FixtureRequest): + mod_path = f"docs_src.settings.{request.param}" + return mod_path -@needs_pydanticv2 -def test_settings(monkeypatch: MonkeyPatch): - from docs_src.settings.app02 import main +@pytest.fixture(name="main_mod") +def get_main_mod(mod_path: str) -> ModuleType: + main_mod = importlib.import_module(f"{mod_path}.main") + return main_mod + +@pytest.fixture(name="test_main_mod") +def get_test_main_mod(mod_path: str) -> ModuleType: + test_main_mod = importlib.import_module(f"{mod_path}.test_main") + return test_main_mod + + +def test_settings(main_mod: ModuleType, monkeypatch: MonkeyPatch): monkeypatch.setenv("ADMIN_EMAIL", "admin@example.com") - settings = main.get_settings() + settings = main_mod.get_settings() assert settings.app_name == "Awesome API" assert settings.items_per_user == 50 -@needs_pydanticv2 -def test_override_settings(): - from docs_src.settings.app02 import test_main - - test_main.test_app() +def test_override_settings(test_main_mod: ModuleType): + test_main_mod.test_app() diff --git a/tests/test_tutorial/test_settings/test_app03.py b/tests/test_tutorial/test_settings/test_app03.py new file mode 100644 index 0000000000..72de497967 --- /dev/null +++ b/tests/test_tutorial/test_settings/test_app03.py @@ -0,0 +1,44 @@ +import importlib +from types import ModuleType + +import pytest +from fastapi.testclient import TestClient +from pytest import MonkeyPatch + + +@pytest.fixture( + name="mod_path", + params=[ + pytest.param("app03_py39"), + pytest.param("app03_an_py39"), + ], +) +def get_mod_path(request: pytest.FixtureRequest): + mod_path = f"docs_src.settings.{request.param}" + return mod_path + + +@pytest.fixture(name="main_mod") +def get_main_mod(mod_path: str) -> ModuleType: + main_mod = importlib.import_module(f"{mod_path}.main") + return main_mod + + +def test_settings(main_mod: ModuleType, monkeypatch: MonkeyPatch): + monkeypatch.setenv("ADMIN_EMAIL", "admin@example.com") + settings = main_mod.get_settings() + assert settings.app_name == "Awesome API" + assert settings.admin_email == "admin@example.com" + assert settings.items_per_user == 50 + + +def test_endpoint(main_mod: ModuleType, monkeypatch: MonkeyPatch): + monkeypatch.setenv("ADMIN_EMAIL", "admin@example.com") + client = TestClient(main_mod.app) + response = client.get("/info") + assert response.status_code == 200 + assert response.json() == { + "app_name": "Awesome API", + "admin_email": "admin@example.com", + "items_per_user": 50, + } diff --git a/tests/test_tutorial/test_settings/test_tutorial001.py b/tests/test_tutorial/test_settings/test_tutorial001.py index 92a5782d4d..f4576a0d21 100644 --- a/tests/test_tutorial/test_settings/test_tutorial001.py +++ b/tests/test_tutorial/test_settings/test_tutorial001.py @@ -4,16 +4,8 @@ import pytest from fastapi.testclient import TestClient from pytest import MonkeyPatch -from ...utils import needs_pydanticv1, needs_pydanticv2 - -@pytest.fixture( - name="app", - params=[ - pytest.param("tutorial001", marks=needs_pydanticv2), - pytest.param("tutorial001_pv1", marks=needs_pydanticv1), - ], -) +@pytest.fixture(name="app", params=[pytest.param("tutorial001_py39")]) def get_app(request: pytest.FixtureRequest, monkeypatch: MonkeyPatch): monkeypatch.setenv("ADMIN_EMAIL", "admin@example.com") mod = importlib.import_module(f"docs_src.settings.{request.param}") diff --git a/tests/test_tutorial/test_sql_databases/test_tutorial001.py b/tests/test_tutorial/test_sql_databases/test_tutorial001.py index 6604a2fd38..2c628f5257 100644 --- a/tests/test_tutorial/test_sql_databases/test_tutorial001.py +++ b/tests/test_tutorial/test_sql_databases/test_tutorial001.py @@ -2,14 +2,14 @@ import importlib import warnings import pytest -from dirty_equals import IsDict, IsInt +from dirty_equals import IsInt from fastapi.testclient import TestClient from inline_snapshot import snapshot from sqlalchemy import StaticPool from sqlmodel import SQLModel, create_engine from sqlmodel.main import default_registry -from tests.utils import needs_py39, needs_py310 +from tests.utils import needs_py310 def clear_sqlmodel(): @@ -22,11 +22,9 @@ def clear_sqlmodel(): @pytest.fixture( name="client", params=[ - "tutorial001", - pytest.param("tutorial001_py39", marks=needs_py39), + pytest.param("tutorial001_py39"), pytest.param("tutorial001_py310", marks=needs_py310), - "tutorial001_an", - pytest.param("tutorial001_an_py39", marks=needs_py39), + pytest.param("tutorial001_an_py39"), pytest.param("tutorial001_an_py310", marks=needs_py310), ], ) @@ -45,7 +43,7 @@ def get_client(request: pytest.FixtureRequest): with TestClient(mod.app) as c: yield c - # Clean up connection explicitely to avoid resource warning + # Clean up connection explicitly to avoid resource warning mod.engine.dispose() @@ -320,33 +318,15 @@ def test_openapi_schema(client: TestClient): }, "Hero": { "properties": { - "id": IsDict( - { - "anyOf": [{"type": "integer"}, {"type": "null"}], - "title": "Id", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "integer", - "title": "Id", - } - ), + "id": { + "anyOf": [{"type": "integer"}, {"type": "null"}], + "title": "Id", + }, "name": {"type": "string", "title": "Name"}, - "age": IsDict( - { - "anyOf": [{"type": "integer"}, {"type": "null"}], - "title": "Age", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "integer", - "title": "Age", - } - ), + "age": { + "anyOf": [{"type": "integer"}, {"type": "null"}], + "title": "Age", + }, "secret_name": {"type": "string", "title": "Secret Name"}, }, "type": "object", diff --git a/tests/test_tutorial/test_sql_databases/test_tutorial002.py b/tests/test_tutorial/test_sql_databases/test_tutorial002.py index 2c4e0988ce..c72c16e9ae 100644 --- a/tests/test_tutorial/test_sql_databases/test_tutorial002.py +++ b/tests/test_tutorial/test_sql_databases/test_tutorial002.py @@ -2,14 +2,14 @@ import importlib import warnings import pytest -from dirty_equals import IsDict, IsInt +from dirty_equals import IsInt from fastapi.testclient import TestClient from inline_snapshot import Is, snapshot from sqlalchemy import StaticPool from sqlmodel import SQLModel, create_engine from sqlmodel.main import default_registry -from tests.utils import needs_py39, needs_py310 +from tests.utils import needs_py310 def clear_sqlmodel(): @@ -22,11 +22,9 @@ def clear_sqlmodel(): @pytest.fixture( name="client", params=[ - "tutorial002", - pytest.param("tutorial002_py39", marks=needs_py39), + pytest.param("tutorial002_py39"), pytest.param("tutorial002_py310", marks=needs_py310), - "tutorial002_an", - pytest.param("tutorial002_an_py39", marks=needs_py39), + pytest.param("tutorial002_an_py39"), pytest.param("tutorial002_an_py310", marks=needs_py310), ], ) @@ -45,7 +43,7 @@ def get_client(request: pytest.FixtureRequest): with TestClient(mod.app) as c: yield c - # Clean up connection explicitely to avoid resource warning + # Clean up connection explicitly to avoid resource warning mod.engine.dispose() @@ -375,19 +373,10 @@ def test_openapi_schema(client: TestClient): "HeroCreate": { "properties": { "name": {"type": "string", "title": "Name"}, - "age": IsDict( - { - "anyOf": [{"type": "integer"}, {"type": "null"}], - "title": "Age", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "integer", - "title": "Age", - } - ), + "age": { + "anyOf": [{"type": "integer"}, {"type": "null"}], + "title": "Age", + }, "secret_name": {"type": "string", "title": "Secret Name"}, }, "type": "object", @@ -397,19 +386,10 @@ def test_openapi_schema(client: TestClient): "HeroPublic": { "properties": { "name": {"type": "string", "title": "Name"}, - "age": IsDict( - { - "anyOf": [{"type": "integer"}, {"type": "null"}], - "title": "Age", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "integer", - "title": "Age", - } - ), + "age": { + "anyOf": [{"type": "integer"}, {"type": "null"}], + "title": "Age", + }, "id": {"type": "integer", "title": "Id"}, }, "type": "object", @@ -418,45 +398,18 @@ def test_openapi_schema(client: TestClient): }, "HeroUpdate": { "properties": { - "name": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Name", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "string", - "title": "Name", - } - ), - "age": IsDict( - { - "anyOf": [{"type": "integer"}, {"type": "null"}], - "title": "Age", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "integer", - "title": "Age", - } - ), - "secret_name": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Secret Name", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "string", - "title": "Secret Name", - } - ), + "name": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Name", + }, + "age": { + "anyOf": [{"type": "integer"}, {"type": "null"}], + "title": "Age", + }, + "secret_name": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Secret Name", + }, }, "type": "object", "title": "HeroUpdate", diff --git a/tests/test_tutorial/test_static_files/__init__.py b/tests/test_tutorial/test_static_files/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/test_tutorial/test_static_files/test_tutorial001.py b/tests/test_tutorial/test_static_files/test_tutorial001.py new file mode 100644 index 0000000000..4fbf19ae82 --- /dev/null +++ b/tests/test_tutorial/test_static_files/test_tutorial001.py @@ -0,0 +1,40 @@ +import os +from pathlib import Path + +import pytest +from fastapi.testclient import TestClient + + +@pytest.fixture(scope="module") +def client(): + static_dir: Path = Path(os.getcwd()) / "static" + static_dir.mkdir(exist_ok=True) + sample_file = static_dir / "sample.txt" + sample_file.write_text("This is a sample static file.") + from docs_src.static_files.tutorial001_py39 import app + + with TestClient(app) as client: + yield client + sample_file.unlink() + static_dir.rmdir() + + +def test_static_files(client: TestClient): + response = client.get("/static/sample.txt") + assert response.status_code == 200, response.text + assert response.text == "This is a sample static file." + + +def test_static_files_not_found(client: TestClient): + response = client.get("/static/non_existent_file.txt") + assert response.status_code == 404, response.text + + +def test_openapi_schema(client: TestClient): + 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": {}, + } diff --git a/tests/test_tutorial/test_sub_applications/test_tutorial001.py b/tests/test_tutorial/test_sub_applications/test_tutorial001.py index 0790d207be..ef1f80164b 100644 --- a/tests/test_tutorial/test_sub_applications/test_tutorial001.py +++ b/tests/test_tutorial/test_sub_applications/test_tutorial001.py @@ -1,6 +1,6 @@ from fastapi.testclient import TestClient -from docs_src.sub_applications.tutorial001 import app +from docs_src.sub_applications.tutorial001_py39 import app client = TestClient(app) diff --git a/tests/test_tutorial/test_templates/test_tutorial001.py b/tests/test_tutorial/test_templates/test_tutorial001.py index 4d4729425e..818508037d 100644 --- a/tests/test_tutorial/test_templates/test_tutorial001.py +++ b/tests/test_tutorial/test_templates/test_tutorial001.py @@ -11,7 +11,7 @@ def test_main(): shutil.rmtree("./templates") shutil.copytree("./docs_src/templates/templates/", "./templates") shutil.copytree("./docs_src/templates/static/", "./static") - from docs_src.templates.tutorial001 import app + from docs_src.templates.tutorial001_py39 import app client = TestClient(app) response = client.get("/items/foo") diff --git a/tests/test_tutorial/test_testing/test_main.py b/tests/test_tutorial/test_testing/test_main_a.py similarity index 90% rename from tests/test_tutorial/test_testing/test_main.py rename to tests/test_tutorial/test_testing/test_main_a.py index fe34980813..9b3c796bdc 100644 --- a/tests/test_tutorial/test_testing/test_main.py +++ b/tests/test_tutorial/test_testing/test_main_a.py @@ -1,4 +1,4 @@ -from docs_src.app_testing.test_main import client, test_read_main +from docs_src.app_testing.app_a_py39.test_main import client, test_read_main def test_main(): diff --git a/tests/test_tutorial/test_testing/test_main_b.py b/tests/test_tutorial/test_testing/test_main_b.py index aa7f969c6d..3d679cd5a6 100644 --- a/tests/test_tutorial/test_testing/test_main_b.py +++ b/tests/test_tutorial/test_testing/test_main_b.py @@ -3,16 +3,15 @@ from types import ModuleType import pytest -from ...utils import needs_py39, needs_py310 +from ...utils import needs_py310 @pytest.fixture( name="test_module", params=[ - "app_b.test_main", + "app_b_py39.test_main", pytest.param("app_b_py310.test_main", marks=needs_py310), - "app_b_an.test_main", - pytest.param("app_b_an_py39.test_main", marks=needs_py39), + "app_b_an_py39.test_main", pytest.param("app_b_an_py310.test_main", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_testing/test_tutorial001.py b/tests/test_tutorial/test_testing/test_tutorial001.py index 471e896c93..5f6533306e 100644 --- a/tests/test_tutorial/test_testing/test_tutorial001.py +++ b/tests/test_tutorial/test_testing/test_tutorial001.py @@ -1,4 +1,4 @@ -from docs_src.app_testing.tutorial001 import client, test_read_main +from docs_src.app_testing.tutorial001_py39 import client, test_read_main def test_main(): diff --git a/tests/test_tutorial/test_testing/test_tutorial002.py b/tests/test_tutorial/test_testing/test_tutorial002.py index ec4f91ee7f..cc9b5ba27c 100644 --- a/tests/test_tutorial/test_testing/test_tutorial002.py +++ b/tests/test_tutorial/test_testing/test_tutorial002.py @@ -1,4 +1,4 @@ -from docs_src.app_testing.tutorial002 import test_read_main, test_websocket +from docs_src.app_testing.tutorial002_py39 import test_read_main, test_websocket def test_main(): diff --git a/tests/test_tutorial/test_testing/test_tutorial003.py b/tests/test_tutorial/test_testing/test_tutorial003.py index 2a5d670712..4faa820e98 100644 --- a/tests/test_tutorial/test_testing/test_tutorial003.py +++ b/tests/test_tutorial/test_testing/test_tutorial003.py @@ -3,5 +3,5 @@ import pytest def test_main(): with pytest.warns(DeprecationWarning): - from docs_src.app_testing.tutorial003 import test_read_items + from docs_src.app_testing.tutorial003_py39 import test_read_items test_read_items() diff --git a/tests/test_tutorial/test_testing/test_tutorial004.py b/tests/test_tutorial/test_testing/test_tutorial004.py index 812ee44c1f..c95214ffe3 100644 --- a/tests/test_tutorial/test_testing/test_tutorial004.py +++ b/tests/test_tutorial/test_testing/test_tutorial004.py @@ -1,4 +1,4 @@ -from docs_src.app_testing.tutorial004 import test_read_items +from docs_src.app_testing.tutorial004_py39 import test_read_items def test_main(): diff --git a/tests/test_tutorial/test_testing_dependencies/test_tutorial001.py b/tests/test_tutorial/test_testing_dependencies/test_tutorial001.py index 00ee6ab1ea..6e9656bf55 100644 --- a/tests/test_tutorial/test_testing_dependencies/test_tutorial001.py +++ b/tests/test_tutorial/test_testing_dependencies/test_tutorial001.py @@ -3,16 +3,15 @@ from types import ModuleType import pytest -from ...utils import needs_py39, needs_py310 +from ...utils import needs_py310 @pytest.fixture( name="test_module", params=[ - "tutorial001", + pytest.param("tutorial001_py39"), pytest.param("tutorial001_py310", marks=needs_py310), - "tutorial001_an", - pytest.param("tutorial001_an_py39", marks=needs_py39), + pytest.param("tutorial001_an_py39"), pytest.param("tutorial001_an_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_using_request_directly/__init__.py b/tests/test_tutorial/test_using_request_directly/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/test_tutorial/test_using_request_directly/test_tutorial001.py b/tests/test_tutorial/test_using_request_directly/test_tutorial001.py new file mode 100644 index 0000000000..33e661b164 --- /dev/null +++ b/tests/test_tutorial/test_using_request_directly/test_tutorial001.py @@ -0,0 +1,112 @@ +from fastapi.testclient import TestClient + +from docs_src.using_request_directly.tutorial001_py39 import app + +client = TestClient(app) + + +def test_path_operation(): + response = client.get("/items/foo") + assert response.status_code == 200 + assert response.json() == {"client_host": "testclient", "item_id": "foo"} + + +def test_openapi(): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "info": { + "title": "FastAPI", + "version": "0.1.0", + }, + "openapi": "3.1.0", + "paths": { + "/items/{item_id}": { + "get": { + "operationId": "read_root_items__item_id__get", + "parameters": [ + { + "in": "path", + "name": "item_id", + "required": True, + "schema": { + "title": "Item Id", + "type": "string", + }, + }, + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": {}, + }, + }, + "description": "Successful Response", + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError", + }, + }, + }, + "description": "Validation Error", + }, + }, + "summary": "Read Root", + }, + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError", + }, + "title": "Detail", + "type": "array", + }, + }, + "title": "HTTPValidationError", + "type": "object", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "integer", + }, + ], + }, + "title": "Location", + "type": "array", + }, + "msg": { + "title": "Message", + "type": "string", + }, + "type": { + "title": "Error Type", + "type": "string", + }, + }, + "required": [ + "loc", + "msg", + "type", + ], + "title": "ValidationError", + "type": "object", + }, + }, + }, + } diff --git a/tests/test_tutorial/test_websockets/test_tutorial001.py b/tests/test_tutorial/test_websockets/test_tutorial001.py index 7dbecb2650..4f8368db28 100644 --- a/tests/test_tutorial/test_websockets/test_tutorial001.py +++ b/tests/test_tutorial/test_websockets/test_tutorial001.py @@ -2,7 +2,7 @@ import pytest from fastapi.testclient import TestClient from fastapi.websockets import WebSocketDisconnect -from docs_src.websockets.tutorial001 import app +from docs_src.websockets.tutorial001_py39 import app client = TestClient(app) diff --git a/tests/test_tutorial/test_websockets/test_tutorial002.py b/tests/test_tutorial/test_websockets/test_tutorial002.py index 51aa5752a6..ebf1fc8e81 100644 --- a/tests/test_tutorial/test_websockets/test_tutorial002.py +++ b/tests/test_tutorial/test_websockets/test_tutorial002.py @@ -5,16 +5,15 @@ from fastapi import FastAPI from fastapi.testclient import TestClient from fastapi.websockets import WebSocketDisconnect -from ...utils import needs_py39, needs_py310 +from ...utils import needs_py310 @pytest.fixture( name="app", params=[ - "tutorial002", + pytest.param("tutorial002_py39"), pytest.param("tutorial002_py310", marks=needs_py310), - "tutorial002_an", - pytest.param("tutorial002_an_py39", marks=needs_py39), + pytest.param("tutorial002_an_py39"), pytest.param("tutorial002_an_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_websockets/test_tutorial003.py b/tests/test_tutorial/test_websockets/test_tutorial003.py index dbcad3b02c..0be1fc81d6 100644 --- a/tests/test_tutorial/test_websockets/test_tutorial003.py +++ b/tests/test_tutorial/test_websockets/test_tutorial003.py @@ -1,19 +1,44 @@ +import importlib +from types import ModuleType + +import pytest from fastapi.testclient import TestClient -from docs_src.websockets.tutorial003 import app, html -client = TestClient(app) +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial003_py39"), + ], +) +def get_mod(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.websockets.{request.param}") + + return mod -def test_get(): +@pytest.fixture(name="html") +def get_html(mod: ModuleType): + return mod.html + + +@pytest.fixture(name="client") +def get_client(mod: ModuleType): + client = TestClient(mod.app) + + return client + + +def test_get(client: TestClient, html: str): response = client.get("/") assert response.text == html -def test_websocket_handle_disconnection(): - with client.websocket_connect("/ws/1234") as connection, client.websocket_connect( - "/ws/5678" - ) as connection_two: +def test_websocket_handle_disconnection(client: TestClient): + with ( + client.websocket_connect("/ws/1234") as connection, + client.websocket_connect("/ws/5678") as connection_two, + ): connection.send_text("Hello from 1234") data1 = connection.receive_text() assert data1 == "You wrote: Hello from 1234" diff --git a/tests/test_tutorial/test_websockets/test_tutorial003_py39.py b/tests/test_tutorial/test_websockets/test_tutorial003_py39.py deleted file mode 100644 index 06c4a92796..0000000000 --- a/tests/test_tutorial/test_websockets/test_tutorial003_py39.py +++ /dev/null @@ -1,50 +0,0 @@ -import pytest -from fastapi import FastAPI -from fastapi.testclient import TestClient - -from ...utils import needs_py39 - - -@pytest.fixture(name="app") -def get_app(): - from docs_src.websockets.tutorial003_py39 import app - - return app - - -@pytest.fixture(name="html") -def get_html(): - from docs_src.websockets.tutorial003_py39 import html - - return html - - -@pytest.fixture(name="client") -def get_client(app: FastAPI): - client = TestClient(app) - - return client - - -@needs_py39 -def test_get(client: TestClient, html: str): - response = client.get("/") - assert response.text == html - - -@needs_py39 -def test_websocket_handle_disconnection(client: TestClient): - with client.websocket_connect("/ws/1234") as connection, client.websocket_connect( - "/ws/5678" - ) as connection_two: - connection.send_text("Hello from 1234") - data1 = connection.receive_text() - assert data1 == "You wrote: Hello from 1234" - data2 = connection_two.receive_text() - client1_says = "Client #1234 says: Hello from 1234" - assert data2 == client1_says - data1 = connection.receive_text() - assert data1 == client1_says - connection_two.close() - data1 = connection.receive_text() - assert data1 == "Client #5678 left the chat" diff --git a/tests/test_tutorial/test_wsgi/test_tutorial001.py b/tests/test_tutorial/test_wsgi/test_tutorial001.py index 4f82252739..9fe8c2a4b8 100644 --- a/tests/test_tutorial/test_wsgi/test_tutorial001.py +++ b/tests/test_tutorial/test_wsgi/test_tutorial001.py @@ -1,6 +1,6 @@ from fastapi.testclient import TestClient -from docs_src.wsgi.tutorial001 import app +from docs_src.wsgi.tutorial001_py39 import app client = TestClient(app) diff --git a/tests/test_union_body.py b/tests/test_union_body.py index c15acacd18..ee7fcc4231 100644 --- a/tests/test_union_body.py +++ b/tests/test_union_body.py @@ -1,6 +1,5 @@ from typing import Optional, Union -from dirty_equals import IsDict from fastapi import FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel @@ -91,18 +90,12 @@ def test_openapi_schema(): "Item": { "title": "Item", "type": "object", - "properties": IsDict( - { - "name": { - "title": "Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } + "properties": { + "name": { + "title": "Name", + "anyOf": [{"type": "string"}, {"type": "null"}], } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"name": {"title": "Name", "type": "string"}} - ), + }, }, "ValidationError": { "title": "ValidationError", diff --git a/tests/test_union_body_discriminator.py b/tests/test_union_body_discriminator.py index 6af9e1d226..6c31649bcc 100644 --- a/tests/test_union_body_discriminator.py +++ b/tests/test_union_body_discriminator.py @@ -1,16 +1,12 @@ -from typing import Any, Dict, Union +from typing import Annotated, Any, Union -from dirty_equals import IsDict from fastapi import FastAPI from fastapi.testclient import TestClient from inline_snapshot import snapshot from pydantic import BaseModel, Field -from typing_extensions import Annotated, Literal - -from .utils import needs_pydanticv2 +from typing_extensions import Literal -@needs_pydanticv2 def test_discriminator_pydantic_v2() -> None: from pydantic import Tag @@ -32,7 +28,7 @@ def test_discriminator_pydantic_v2() -> None: @app.post("/items/") def save_union_body_discriminator( item: Item, q: Annotated[str, Field(description="Query string")] - ) -> Dict[str, Any]: + ) -> dict[str, Any]: return {"item": item} client = TestClient(app) @@ -93,21 +89,11 @@ def test_discriminator_pydantic_v2() -> None: "description": "Successful Response", "content": { "application/json": { - "schema": IsDict( - { - # Pydantic 2.10, in Python 3.8 - # TODO: remove when dropping support for Python 3.8 - "type": "object", - "title": "Response Save Union Body Discriminator Items Post", - } - ) - | IsDict( - { - "type": "object", - "additionalProperties": True, - "title": "Response Save Union Body Discriminator Items Post", - } - ) + "schema": { + "type": "object", + "additionalProperties": True, + "title": "Response Save Union Body Discriminator Items Post", + } } }, }, diff --git a/tests/test_union_body_discriminator_annotated.py b/tests/test_union_body_discriminator_annotated.py new file mode 100644 index 0000000000..42a6aed24c --- /dev/null +++ b/tests/test_union_body_discriminator_annotated.py @@ -0,0 +1,201 @@ +# Ref: https://github.com/fastapi/fastapi/discussions/14495 + +from typing import Annotated, Union + +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient +from inline_snapshot import snapshot +from pydantic import BaseModel + + +@pytest.fixture(name="client") +def client_fixture() -> TestClient: + from fastapi import Body + from pydantic import Discriminator, Tag + + class Cat(BaseModel): + pet_type: str = "cat" + meows: int + + class Dog(BaseModel): + pet_type: str = "dog" + barks: float + + def get_pet_type(v): + assert isinstance(v, dict) + return v.get("pet_type", "") + + Pet = Annotated[ + Union[Annotated[Cat, Tag("cat")], Annotated[Dog, Tag("dog")]], + Discriminator(get_pet_type), + ] + + app = FastAPI() + + @app.post("/pet/assignment") + async def create_pet_assignment(pet: Pet = Body()): + return pet + + @app.post("/pet/annotated") + async def create_pet_annotated(pet: Annotated[Pet, Body()]): + return pet + + client = TestClient(app) + return client + + +def test_union_body_discriminator_assignment(client: TestClient) -> None: + response = client.post("/pet/assignment", json={"pet_type": "cat", "meows": 5}) + assert response.status_code == 200, response.text + assert response.json() == {"pet_type": "cat", "meows": 5} + + +def test_union_body_discriminator_annotated(client: TestClient) -> None: + response = client.post("/pet/annotated", json={"pet_type": "dog", "barks": 3.5}) + assert response.status_code == 200, response.text + assert response.json() == {"pet_type": "dog", "barks": 3.5} + + +def test_openapi_schema(client: TestClient) -> None: + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/pet/assignment": { + "post": { + "summary": "Create Pet Assignment", + "operationId": "create_pet_assignment_pet_assignment_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "anyOf": [ + {"$ref": "#/components/schemas/Cat"}, + {"$ref": "#/components/schemas/Dog"}, + ], + "title": "Pet", + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/pet/annotated": { + "post": { + "summary": "Create Pet Annotated", + "operationId": "create_pet_annotated_pet_annotated_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + {"$ref": "#/components/schemas/Cat"}, + {"$ref": "#/components/schemas/Dog"}, + ], + "title": "Pet", + } + } + }, + "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": { + "Cat": { + "properties": { + "pet_type": { + "type": "string", + "title": "Pet Type", + "default": "cat", + }, + "meows": {"type": "integer", "title": "Meows"}, + }, + "type": "object", + "required": ["meows"], + "title": "Cat", + }, + "Dog": { + "properties": { + "pet_type": { + "type": "string", + "title": "Pet Type", + "default": "dog", + }, + "barks": {"type": "number", "title": "Barks"}, + }, + "type": "object", + "required": ["barks"], + "title": "Dog", + }, + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } + ) diff --git a/tests/test_union_forms.py b/tests/test_union_forms.py index cbe98ea825..018949f0c7 100644 --- a/tests/test_union_forms.py +++ b/tests/test_union_forms.py @@ -1,9 +1,8 @@ -from typing import Union +from typing import Annotated, Union from fastapi import FastAPI, Form from fastapi.testclient import TestClient from pydantic import BaseModel -from typing_extensions import Annotated app = FastAPI() diff --git a/tests/test_union_inherited_body.py b/tests/test_union_inherited_body.py index ef75d459ea..3c062e7f5a 100644 --- a/tests/test_union_inherited_body.py +++ b/tests/test_union_inherited_body.py @@ -1,6 +1,5 @@ from typing import Optional, Union -from dirty_equals import IsDict from fastapi import FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel @@ -86,16 +85,10 @@ def test_openapi_schema(): "title": "Item", "type": "object", "properties": { - "name": IsDict( - { - "title": "Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Name", "type": "string"} - ) + "name": { + "title": "Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } }, }, "ExtendedItem": { @@ -103,16 +96,10 @@ def test_openapi_schema(): "required": ["age"], "type": "object", "properties": { - "name": IsDict( - { - "title": "Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Name", "type": "string"} - ), + "name": { + "title": "Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, "age": {"title": "Age", "type": "integer"}, }, }, diff --git a/tests/test_validate_response.py b/tests/test_validate_response.py index cd97007a44..938d419566 100644 --- a/tests/test_validate_response.py +++ b/tests/test_validate_response.py @@ -1,4 +1,4 @@ -from typing import List, Optional, Union +from typing import Optional, Union import pytest from fastapi import FastAPI @@ -12,7 +12,7 @@ app = FastAPI() class Item(BaseModel): name: str price: Optional[float] = None - owner_ids: Optional[List[int]] = None + owner_ids: Optional[list[int]] = None @app.get("/items/invalid", response_model=Item) @@ -38,7 +38,7 @@ def get_innerinvalid(): return {"name": "double invalid", "price": "foo", "owner_ids": ["foo", "bar"]} -@app.get("/items/invalidlist", response_model=List[Item]) +@app.get("/items/invalidlist", response_model=list[Item]) def get_invalidlist(): return [ {"name": "foo"}, diff --git a/tests/test_validate_response_dataclass.py b/tests/test_validate_response_dataclass.py index 0415988a0b..67282bcde1 100644 --- a/tests/test_validate_response_dataclass.py +++ b/tests/test_validate_response_dataclass.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import Optional import pytest from fastapi import FastAPI @@ -13,7 +13,7 @@ app = FastAPI() class Item: name: str price: Optional[float] = None - owner_ids: Optional[List[int]] = None + owner_ids: Optional[list[int]] = None @app.get("/items/invalid", response_model=Item) @@ -26,7 +26,7 @@ def get_innerinvalid(): return {"name": "double invalid", "price": "foo", "owner_ids": ["foo", "bar"]} -@app.get("/items/invalidlist", response_model=List[Item]) +@app.get("/items/invalidlist", response_model=list[Item]) def get_invalidlist(): return [ {"name": "foo"}, diff --git a/tests/test_validate_response_recursive/app.py b/tests/test_validate_response_recursive/app.py index d23d279808..d51aa848d0 100644 --- a/tests/test_validate_response_recursive/app.py +++ b/tests/test_validate_response_recursive/app.py @@ -1,34 +1,27 @@ -from typing import List - from fastapi import FastAPI -from fastapi._compat import PYDANTIC_V2 from pydantic import BaseModel app = FastAPI() class RecursiveItem(BaseModel): - sub_items: List["RecursiveItem"] = [] + sub_items: list["RecursiveItem"] = [] name: str class RecursiveSubitemInSubmodel(BaseModel): - sub_items2: List["RecursiveItemViaSubmodel"] = [] + sub_items2: list["RecursiveItemViaSubmodel"] = [] name: str class RecursiveItemViaSubmodel(BaseModel): - sub_items1: List[RecursiveSubitemInSubmodel] = [] + sub_items1: list[RecursiveSubitemInSubmodel] = [] name: str -if PYDANTIC_V2: - RecursiveItem.model_rebuild() - RecursiveSubitemInSubmodel.model_rebuild() - RecursiveItemViaSubmodel.model_rebuild() -else: - RecursiveItem.update_forward_refs() - RecursiveSubitemInSubmodel.update_forward_refs() +RecursiveItem.model_rebuild() +RecursiveSubitemInSubmodel.model_rebuild() +RecursiveItemViaSubmodel.model_rebuild() @app.get("/items/recursive", response_model=RecursiveItem) diff --git a/tests/test_validation_error_context.py b/tests/test_validation_error_context.py new file mode 100644 index 0000000000..844b8a64f9 --- /dev/null +++ b/tests/test_validation_error_context.py @@ -0,0 +1,168 @@ +from fastapi import FastAPI, Request, WebSocket +from fastapi.exceptions import ( + RequestValidationError, + ResponseValidationError, + WebSocketRequestValidationError, +) +from fastapi.testclient import TestClient +from pydantic import BaseModel + + +class Item(BaseModel): + id: int + name: str + + +class ExceptionCapture: + def __init__(self): + self.exception = None + + def capture(self, exc): + self.exception = exc + return exc + + +app = FastAPI() +sub_app = FastAPI() +captured_exception = ExceptionCapture() + +app.mount(path="/sub", app=sub_app) + + +@app.exception_handler(RequestValidationError) +@sub_app.exception_handler(RequestValidationError) +async def request_validation_handler(request: Request, exc: RequestValidationError): + captured_exception.capture(exc) + raise exc + + +@app.exception_handler(ResponseValidationError) +@sub_app.exception_handler(ResponseValidationError) +async def response_validation_handler(_: Request, exc: ResponseValidationError): + captured_exception.capture(exc) + raise exc + + +@app.exception_handler(WebSocketRequestValidationError) +@sub_app.exception_handler(WebSocketRequestValidationError) +async def websocket_validation_handler( + websocket: WebSocket, exc: WebSocketRequestValidationError +): + captured_exception.capture(exc) + raise exc + + +@app.get("/users/{user_id}") +def get_user(user_id: int): + return {"user_id": user_id} # pragma: no cover + + +@app.get("/items/", response_model=Item) +def get_item(): + return {"name": "Widget"} + + +@sub_app.get("/items/", response_model=Item) +def get_sub_item(): + return {"name": "Widget"} # pragma: no cover + + +@app.websocket("/ws/{item_id}") +async def websocket_endpoint(websocket: WebSocket, item_id: int): + await websocket.accept() # pragma: no cover + await websocket.send_text(f"Item: {item_id}") # pragma: no cover + await websocket.close() # pragma: no cover + + +@sub_app.websocket("/ws/{item_id}") +async def subapp_websocket_endpoint(websocket: WebSocket, item_id: int): + await websocket.accept() # pragma: no cover + await websocket.send_text(f"Item: {item_id}") # pragma: no cover + await websocket.close() # pragma: no cover + + +client = TestClient(app) + + +def test_request_validation_error_includes_endpoint_context(): + captured_exception.exception = None + try: + client.get("/users/invalid") + except Exception: + pass + + assert captured_exception.exception is not None + error_str = str(captured_exception.exception) + assert "get_user" in error_str + assert "/users/" in error_str + + +def test_response_validation_error_includes_endpoint_context(): + captured_exception.exception = None + try: + client.get("/items/") + except Exception: + pass + + assert captured_exception.exception is not None + error_str = str(captured_exception.exception) + assert "get_item" in error_str + assert "/items/" in error_str + + +def test_websocket_validation_error_includes_endpoint_context(): + captured_exception.exception = None + try: + with client.websocket_connect("/ws/invalid"): + pass # pragma: no cover + except Exception: + pass + + assert captured_exception.exception is not None + error_str = str(captured_exception.exception) + assert "websocket_endpoint" in error_str + assert "/ws/" in error_str + + +def test_subapp_request_validation_error_includes_endpoint_context(): + captured_exception.exception = None + try: + client.get("/sub/items/") + except Exception: + pass + + assert captured_exception.exception is not None + error_str = str(captured_exception.exception) + assert "get_sub_item" in error_str + assert "/sub/items/" in error_str + + +def test_subapp_websocket_validation_error_includes_endpoint_context(): + captured_exception.exception = None + try: + with client.websocket_connect("/sub/ws/invalid"): + pass # pragma: no cover + except Exception: + pass + + assert captured_exception.exception is not None + error_str = str(captured_exception.exception) + assert "subapp_websocket_endpoint" in error_str + assert "/sub/ws/" in error_str + + +def test_validation_error_with_only_path(): + errors = [{"type": "missing", "loc": ("body", "name"), "msg": "Field required"}] + exc = RequestValidationError(errors, endpoint_ctx={"path": "GET /api/test"}) + error_str = str(exc) + assert "Endpoint: GET /api/test" in error_str + assert 'File "' not in error_str + + +def test_validation_error_with_no_context(): + errors = [{"type": "missing", "loc": ("body", "name"), "msg": "Field required"}] + exc = RequestValidationError(errors, endpoint_ctx={}) + error_str = str(exc) + assert "1 validation error:" in error_str + assert "Endpoint" not in error_str + assert 'File "' not in error_str diff --git a/tests/test_webhooks_security.py b/tests/test_webhooks_security.py index 21a694cb54..982ae1e21d 100644 --- a/tests/test_webhooks_security.py +++ b/tests/test_webhooks_security.py @@ -1,10 +1,10 @@ from datetime import datetime +from typing import Annotated from fastapi import FastAPI, Security from fastapi.security import HTTPBearer from fastapi.testclient import TestClient from pydantic import BaseModel -from typing_extensions import Annotated app = FastAPI() diff --git a/tests/test_wrapped_method_forward_reference.py b/tests/test_wrapped_method_forward_reference.py new file mode 100644 index 0000000000..7403f6002c --- /dev/null +++ b/tests/test_wrapped_method_forward_reference.py @@ -0,0 +1,31 @@ +import functools + +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from .forward_reference_type import forwardref_method + + +def passthrough(f): + @functools.wraps(f) + def method(*args, **kwargs): + return f(*args, **kwargs) + + return method + + +def test_wrapped_method_type_inference(): + """ + Regression test ensuring that when a method imported from another module + is decorated with something that sets the __wrapped__ attribute (functools.wraps), + then the types are still processed correctly, including dereferencing of forward + references. + """ + app = FastAPI() + client = TestClient(app) + app.post("/endpoint")(passthrough(forwardref_method)) + app.post("/endpoint2")(passthrough(passthrough(forwardref_method))) + with client: + response = client.post("/endpoint", json={"input": {"x": 0}}) + response2 = client.post("/endpoint2", json={"input": {"x": 0}}) + assert response.json() == response2.json() == {"x": 1} diff --git a/tests/test_ws_dependencies.py b/tests/test_ws_dependencies.py index ccb1c4b7da..51b982c00e 100644 --- a/tests/test_ws_dependencies.py +++ b/tests/test_ws_dependencies.py @@ -1,16 +1,15 @@ import json -from typing import List +from typing import Annotated from fastapi import APIRouter, Depends, FastAPI, WebSocket from fastapi.testclient import TestClient -from typing_extensions import Annotated -def dependency_list() -> List[str]: +def dependency_list() -> list[str]: return [] -DepList = Annotated[List[str], Depends(dependency_list)] +DepList = Annotated[list[str], Depends(dependency_list)] def create_dependency(name: str): diff --git a/tests/utils.py b/tests/utils.py index 691e92bbfb..efa0bfd52b 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,43 +1,17 @@ import sys import pytest -from fastapi._compat import PYDANTIC_V2 -from inline_snapshot import Snapshot 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+" ) needs_py_lt_314 = pytest.mark.skipif( - sys.version_info > (3, 13), reason="requires python3.13-" + sys.version_info >= (3, 14), reason="requires python3.13-" ) -needs_pydanticv2 = pytest.mark.skipif(not PYDANTIC_V2, reason="requires Pydantic v2") -needs_pydanticv1 = pytest.mark.skipif(PYDANTIC_V2, reason="requires Pydantic v1") def skip_module_if_py_gte_314(): """Skip entire module on Python 3.14+ at import time.""" if sys.version_info >= (3, 14): pytest.skip("requires python3.13-", allow_module_level=True) - - -def pydantic_snapshot( - *, - v2: Snapshot, - v1: Snapshot, # TODO: remove v1 argument when deprecating Pydantic v1 -): - """ - This function should be used like this: - - >>> assert value == pydantic_snapshot(v2=snapshot(),v1=snapshot()) - - inline-snapshot will create the snapshots when pytest is executed for each versions of pydantic. - - It is also possible to use the function inside snapshots for version-specific values. - - >>> assert value == snapshot({ - "data": "some data", - "version_specific": pydantic_snapshot(v2=snapshot(),v1=snapshot()), - }) - """ - return v2 if PYDANTIC_V2 else v1 diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000000..9ae2220e0c --- /dev/null +++ b/uv.lock @@ -0,0 +1,5339 @@ +version = 1 +revision = 3 +requires-python = ">=3.9" +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.10' and python_full_version < '3.14'", + "python_full_version < '3.10'", +] + +[[package]] +name = "ag-ui-protocol" +version = "0.1.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/bb/5a5ec893eea5805fb9a3db76a9888c3429710dfb6f24bbb37568f2cf7320/ag_ui_protocol-0.1.10.tar.gz", hash = "sha256:3213991c6b2eb24bb1a8c362ee270c16705a07a4c5962267a083d0959ed894f4", size = 6945, upload-time = "2025-11-06T15:17:17.068Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/78/eb55fabaab41abc53f52c0918a9a8c0f747807e5306273f51120fd695957/ag_ui_protocol-0.1.10-py3-none-any.whl", hash = "sha256:c81e6981f30aabdf97a7ee312bfd4df0cd38e718d9fc10019c7d438128b93ab5", size = 7889, upload-time = "2025-11-06T15:17:15.325Z" }, +] + +[[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 = "anthropic" +version = "0.75.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "docstring-parser" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/1f/08e95f4b7e2d35205ae5dcbb4ae97e7d477fc521c275c02609e2931ece2d/anthropic-0.75.0.tar.gz", hash = "sha256:e8607422f4ab616db2ea5baacc215dd5f028da99ce2f022e33c7c535b29f3dfb", size = 439565, upload-time = "2025-11-24T20:41:45.28Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/1c/1cd02b7ae64302a6e06724bf80a96401d5313708651d277b1458504a1730/anthropic-0.75.0-py3-none-any.whl", hash = "sha256:ea8317271b6c15d80225a9f3c670152746e88805a7a61e14d4a374577164965b", size = 388164, upload-time = "2025-11-24T20:41:43.587Z" }, +] + +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] + +[package.optional-dependencies] +trio = [ + { name = "trio", version = "0.31.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "trio", version = "0.32.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] + +[[package]] +name = "argcomplete" +version = "3.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/38/61/0b9ae6399dd4a58d8c1b1dc5a27d6f2808023d0b5dd3104bb99f45a33ff6/argcomplete-3.6.3.tar.gz", hash = "sha256:62e8ed4fd6a45864acc8235409461b72c9a28ee785a2011cc5eb78318786c89c", size = 73754, upload-time = "2025-10-20T03:33:34.741Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/f5/9373290775639cb67a2fce7f629a1c240dce9f12fe927bc32b2736e16dfc/argcomplete-3.6.3-py3-none-any.whl", hash = "sha256:f5007b3a600ccac5d25bbce33089211dfd49eab4a7718da3f10e3082525a92ce", size = 43846, upload-time = "2025-10-20T03:33:33.021Z" }, +] + +[[package]] +name = "argon2-cffi" +version = "23.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "argon2-cffi-bindings", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/31/fa/57ec2c6d16ecd2ba0cf15f3c7d1c3c2e7b5fcb83555ff56d7ab10888ec8f/argon2_cffi-23.1.0.tar.gz", hash = "sha256:879c3e79a2729ce768ebb7d36d4609e3a78a4ca2ec3a9f12286ca057e3d0db08", size = 42798, upload-time = "2023-08-15T14:13:12.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/6a/e8a041599e78b6b3752da48000b14c8d1e8a04ded09c88c714ba047f34f5/argon2_cffi-23.1.0-py3-none-any.whl", hash = "sha256:c670642b78ba29641818ab2e68bd4e6a78ba53b7eff7b4c3815ae16abf91c7ea", size = 15124, upload-time = "2023-08-15T14:13:10.752Z" }, +] + +[[package]] +name = "argon2-cffi" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.10' and python_full_version < '3.14'", +] +dependencies = [ + { name = "argon2-cffi-bindings", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/89/ce5af8a7d472a67cc819d5d998aa8c82c5d860608c4db9f46f1162d7dab9/argon2_cffi-25.1.0.tar.gz", hash = "sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1", size = 45706, upload-time = "2025-06-03T06:55:32.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl", hash = "sha256:fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741", size = 14657, upload-time = "2025-06-03T06:55:30.804Z" }, +] + +[[package]] +name = "argon2-cffi-bindings" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/2d/db8af0df73c1cf454f71b2bbe5e356b8c1f8041c979f505b3d3186e520a9/argon2_cffi_bindings-25.1.0.tar.gz", hash = "sha256:b957f3e6ea4d55d820e40ff76f450952807013d361a65d7f28acc0acbf29229d", size = 1783441, upload-time = "2025-07-30T10:02:05.147Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/97/3c0a35f46e52108d4707c44b95cfe2afcafc50800b5450c197454569b776/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:3d3f05610594151994ca9ccb3c771115bdb4daef161976a266f0dd8aa9996b8f", size = 54393, upload-time = "2025-07-30T10:01:40.97Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f4/98bbd6ee89febd4f212696f13c03ca302b8552e7dbf9c8efa11ea4a388c3/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8b8efee945193e667a396cbc7b4fb7d357297d6234d30a489905d96caabde56b", size = 29328, upload-time = "2025-07-30T10:01:41.916Z" }, + { url = "https://files.pythonhosted.org/packages/43/24/90a01c0ef12ac91a6be05969f29944643bc1e5e461155ae6559befa8f00b/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3c6702abc36bf3ccba3f802b799505def420a1b7039862014a65db3205967f5a", size = 31269, upload-time = "2025-07-30T10:01:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/d4/d3/942aa10782b2697eee7af5e12eeff5ebb325ccfb86dd8abda54174e377e4/argon2_cffi_bindings-25.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1c70058c6ab1e352304ac7e3b52554daadacd8d453c1752e547c76e9c99ac44", size = 86558, upload-time = "2025-07-30T10:01:43.943Z" }, + { url = "https://files.pythonhosted.org/packages/0d/82/b484f702fec5536e71836fc2dbc8c5267b3f6e78d2d539b4eaa6f0db8bf8/argon2_cffi_bindings-25.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2fd3bfbff3c5d74fef31a722f729bf93500910db650c925c2d6ef879a7e51cb", size = 92364, upload-time = "2025-07-30T10:01:44.887Z" }, + { url = "https://files.pythonhosted.org/packages/c9/c1/a606ff83b3f1735f3759ad0f2cd9e038a0ad11a3de3b6c673aa41c24bb7b/argon2_cffi_bindings-25.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4f9665de60b1b0e99bcd6be4f17d90339698ce954cfd8d9cf4f91c995165a92", size = 85637, upload-time = "2025-07-30T10:01:46.225Z" }, + { url = "https://files.pythonhosted.org/packages/44/b4/678503f12aceb0262f84fa201f6027ed77d71c5019ae03b399b97caa2f19/argon2_cffi_bindings-25.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ba92837e4a9aa6a508c8d2d7883ed5a8f6c308c89a4790e1e447a220deb79a85", size = 91934, upload-time = "2025-07-30T10:01:47.203Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c7/f36bd08ef9bd9f0a9cff9428406651f5937ce27b6c5b07b92d41f91ae541/argon2_cffi_bindings-25.1.0-cp314-cp314t-win32.whl", hash = "sha256:84a461d4d84ae1295871329b346a97f68eade8c53b6ed9a7ca2d7467f3c8ff6f", size = 28158, upload-time = "2025-07-30T10:01:48.341Z" }, + { url = "https://files.pythonhosted.org/packages/b3/80/0106a7448abb24a2c467bf7d527fe5413b7fdfa4ad6d6a96a43a62ef3988/argon2_cffi_bindings-25.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b55aec3565b65f56455eebc9b9f34130440404f27fe21c3b375bf1ea4d8fbae6", size = 32597, upload-time = "2025-07-30T10:01:49.112Z" }, + { url = "https://files.pythonhosted.org/packages/05/b8/d663c9caea07e9180b2cb662772865230715cbd573ba3b5e81793d580316/argon2_cffi_bindings-25.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:87c33a52407e4c41f3b70a9c2d3f6056d88b10dad7695be708c5021673f55623", size = 28231, upload-time = "2025-07-30T10:01:49.92Z" }, + { url = "https://files.pythonhosted.org/packages/1d/57/96b8b9f93166147826da5f90376e784a10582dd39a393c99bb62cfcf52f0/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:aecba1723ae35330a008418a91ea6cfcedf6d31e5fbaa056a166462ff066d500", size = 54121, upload-time = "2025-07-30T10:01:50.815Z" }, + { url = "https://files.pythonhosted.org/packages/0a/08/a9bebdb2e0e602dde230bdde8021b29f71f7841bd54801bcfd514acb5dcf/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2630b6240b495dfab90aebe159ff784d08ea999aa4b0d17efa734055a07d2f44", size = 29177, upload-time = "2025-07-30T10:01:51.681Z" }, + { url = "https://files.pythonhosted.org/packages/b6/02/d297943bcacf05e4f2a94ab6f462831dc20158614e5d067c35d4e63b9acb/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:7aef0c91e2c0fbca6fc68e7555aa60ef7008a739cbe045541e438373bc54d2b0", size = 31090, upload-time = "2025-07-30T10:01:53.184Z" }, + { url = "https://files.pythonhosted.org/packages/c1/93/44365f3d75053e53893ec6d733e4a5e3147502663554b4d864587c7828a7/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e021e87faa76ae0d413b619fe2b65ab9a037f24c60a1e6cc43457ae20de6dc6", size = 81246, upload-time = "2025-07-30T10:01:54.145Z" }, + { url = "https://files.pythonhosted.org/packages/09/52/94108adfdd6e2ddf58be64f959a0b9c7d4ef2fa71086c38356d22dc501ea/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e924cfc503018a714f94a49a149fdc0b644eaead5d1f089330399134fa028a", size = 87126, upload-time = "2025-07-30T10:01:55.074Z" }, + { url = "https://files.pythonhosted.org/packages/72/70/7a2993a12b0ffa2a9271259b79cc616e2389ed1a4d93842fac5a1f923ffd/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87b72589133f0346a1cb8d5ecca4b933e3c9b64656c9d175270a000e73b288d", size = 80343, upload-time = "2025-07-30T10:01:56.007Z" }, + { url = "https://files.pythonhosted.org/packages/78/9a/4e5157d893ffc712b74dbd868c7f62365618266982b64accab26bab01edc/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1db89609c06afa1a214a69a462ea741cf735b29a57530478c06eb81dd403de99", size = 86777, upload-time = "2025-07-30T10:01:56.943Z" }, + { url = "https://files.pythonhosted.org/packages/74/cd/15777dfde1c29d96de7f18edf4cc94c385646852e7c7b0320aa91ccca583/argon2_cffi_bindings-25.1.0-cp39-abi3-win32.whl", hash = "sha256:473bcb5f82924b1becbb637b63303ec8d10e84c8d241119419897a26116515d2", size = 27180, upload-time = "2025-07-30T10:01:57.759Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c6/a759ece8f1829d1f162261226fbfd2c6832b3ff7657384045286d2afa384/argon2_cffi_bindings-25.1.0-cp39-abi3-win_amd64.whl", hash = "sha256:a98cd7d17e9f7ce244c0803cad3c23a7d379c301ba618a5fa76a67d116618b98", size = 31715, upload-time = "2025-07-30T10:01:58.56Z" }, + { url = "https://files.pythonhosted.org/packages/42/b9/f8d6fa329ab25128b7e98fd83a3cb34d9db5b059a9847eddb840a0af45dd/argon2_cffi_bindings-25.1.0-cp39-abi3-win_arm64.whl", hash = "sha256:b0fdbcf513833809c882823f98dc2f931cf659d9a1429616ac3adebb49f5db94", size = 27149, upload-time = "2025-07-30T10:01:59.329Z" }, + { url = "https://files.pythonhosted.org/packages/11/2d/ba4e4ca8d149f8dcc0d952ac0967089e1d759c7e5fcf0865a317eb680fbb/argon2_cffi_bindings-25.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6dca33a9859abf613e22733131fc9194091c1fa7cb3e131c143056b4856aa47e", size = 24549, upload-time = "2025-07-30T10:02:00.101Z" }, + { url = "https://files.pythonhosted.org/packages/5c/82/9b2386cc75ac0bd3210e12a44bfc7fd1632065ed8b80d573036eecb10442/argon2_cffi_bindings-25.1.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:21378b40e1b8d1655dd5310c84a40fc19a9aa5e6366e835ceb8576bf0fea716d", size = 25539, upload-time = "2025-07-30T10:02:00.929Z" }, + { url = "https://files.pythonhosted.org/packages/31/db/740de99a37aa727623730c90d92c22c9e12585b3c98c54b7960f7810289f/argon2_cffi_bindings-25.1.0-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d588dec224e2a83edbdc785a5e6f3c6cd736f46bfd4b441bbb5aa1f5085e584", size = 28467, upload-time = "2025-07-30T10:02:02.08Z" }, + { url = "https://files.pythonhosted.org/packages/71/7a/47c4509ea18d755f44e2b92b7178914f0c113946d11e16e626df8eaa2b0b/argon2_cffi_bindings-25.1.0-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5acb4e41090d53f17ca1110c3427f0a130f944b896fc8c83973219c97f57b690", size = 27355, upload-time = "2025-07-30T10:02:02.867Z" }, + { url = "https://files.pythonhosted.org/packages/ee/82/82745642d3c46e7cea25e1885b014b033f4693346ce46b7f47483cf5d448/argon2_cffi_bindings-25.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:da0c79c23a63723aa5d782250fbf51b768abca630285262fb5144ba5ae01e520", size = 29187, upload-time = "2025-07-30T10:02:03.674Z" }, +] + +[[package]] +name = "asttokens" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/a5/8e3f9b6771b0b408517c82d97aed8f2036509bc247d46114925e32fe33f0/asttokens-3.0.1.tar.gz", hash = "sha256:71a4ee5de0bde6a31d64f6b13f2293ac190344478f081c3d1bccfcf5eacb0cb7", size = 62308, upload-time = "2025-11-15T16:43:48.578Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl", hash = "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a", size = 27047, upload-time = "2025-11-15T16:43:16.109Z" }, +] + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, +] + +[[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 = "25.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "click", version = "8.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "mypy-extensions" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "platformdirs", version = "4.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "platformdirs", version = "4.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/94/49/26a7b0f3f35da4b5a65f081943b7bcd22d7002f5f0fb8098ec1ff21cb6ef/black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666", size = 649449, upload-time = "2025-01-29T04:15:40.373Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/3b/4ba3f93ac8d90410423fdd31d7541ada9bcee1df32fb90d26de41ed40e1d/black-25.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:759e7ec1e050a15f89b770cefbf91ebee8917aac5c20483bc2d80a6c3a04df32", size = 1629419, upload-time = "2025-01-29T05:37:06.642Z" }, + { url = "https://files.pythonhosted.org/packages/b4/02/0bde0485146a8a5e694daed47561785e8b77a0466ccc1f3e485d5ef2925e/black-25.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e519ecf93120f34243e6b0054db49c00a35f84f195d5bce7e9f5cfc578fc2da", size = 1461080, upload-time = "2025-01-29T05:37:09.321Z" }, + { url = "https://files.pythonhosted.org/packages/52/0e/abdf75183c830eaca7589144ff96d49bce73d7ec6ad12ef62185cc0f79a2/black-25.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:055e59b198df7ac0b7efca5ad7ff2516bca343276c466be72eb04a3bcc1f82d7", size = 1766886, upload-time = "2025-01-29T04:18:24.432Z" }, + { url = "https://files.pythonhosted.org/packages/dc/a6/97d8bb65b1d8a41f8a6736222ba0a334db7b7b77b8023ab4568288f23973/black-25.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:db8ea9917d6f8fc62abd90d944920d95e73c83a5ee3383493e35d271aca872e9", size = 1419404, upload-time = "2025-01-29T04:19:04.296Z" }, + { url = "https://files.pythonhosted.org/packages/7e/4f/87f596aca05c3ce5b94b8663dbfe242a12843caaa82dd3f85f1ffdc3f177/black-25.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0", size = 1614372, upload-time = "2025-01-29T05:37:11.71Z" }, + { url = "https://files.pythonhosted.org/packages/e7/d0/2c34c36190b741c59c901e56ab7f6e54dad8df05a6272a9747ecef7c6036/black-25.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299", size = 1442865, upload-time = "2025-01-29T05:37:14.309Z" }, + { url = "https://files.pythonhosted.org/packages/21/d4/7518c72262468430ead45cf22bd86c883a6448b9eb43672765d69a8f1248/black-25.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096", size = 1749699, upload-time = "2025-01-29T04:18:17.688Z" }, + { url = "https://files.pythonhosted.org/packages/58/db/4f5beb989b547f79096e035c4981ceb36ac2b552d0ac5f2620e941501c99/black-25.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2", size = 1428028, upload-time = "2025-01-29T04:18:51.711Z" }, + { url = "https://files.pythonhosted.org/packages/83/71/3fe4741df7adf015ad8dfa082dd36c94ca86bb21f25608eb247b4afb15b2/black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b", size = 1650988, upload-time = "2025-01-29T05:37:16.707Z" }, + { url = "https://files.pythonhosted.org/packages/13/f3/89aac8a83d73937ccd39bbe8fc6ac8860c11cfa0af5b1c96d081facac844/black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc", size = 1453985, upload-time = "2025-01-29T05:37:18.273Z" }, + { url = "https://files.pythonhosted.org/packages/6f/22/b99efca33f1f3a1d2552c714b1e1b5ae92efac6c43e790ad539a163d1754/black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f", size = 1783816, upload-time = "2025-01-29T04:18:33.823Z" }, + { url = "https://files.pythonhosted.org/packages/18/7e/a27c3ad3822b6f2e0e00d63d58ff6299a99a5b3aee69fa77cd4b0076b261/black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba", size = 1440860, upload-time = "2025-01-29T04:19:12.944Z" }, + { url = "https://files.pythonhosted.org/packages/98/87/0edf98916640efa5d0696e1abb0a8357b52e69e82322628f25bf14d263d1/black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f", size = 1650673, upload-time = "2025-01-29T05:37:20.574Z" }, + { url = "https://files.pythonhosted.org/packages/52/e5/f7bf17207cf87fa6e9b676576749c6b6ed0d70f179a3d812c997870291c3/black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3", size = 1453190, upload-time = "2025-01-29T05:37:22.106Z" }, + { url = "https://files.pythonhosted.org/packages/e3/ee/adda3d46d4a9120772fae6de454c8495603c37c4c3b9c60f25b1ab6401fe/black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171", size = 1782926, upload-time = "2025-01-29T04:18:58.564Z" }, + { url = "https://files.pythonhosted.org/packages/cc/64/94eb5f45dcb997d2082f097a3944cfc7fe87e071907f677e80788a2d7b7a/black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18", size = 1442613, upload-time = "2025-01-29T04:19:27.63Z" }, + { url = "https://files.pythonhosted.org/packages/d3/b6/ae7507470a4830dbbfe875c701e84a4a5fb9183d1497834871a715716a92/black-25.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1ee0a0c330f7b5130ce0caed9936a904793576ef4d2b98c40835d6a65afa6a0", size = 1628593, upload-time = "2025-01-29T05:37:23.672Z" }, + { url = "https://files.pythonhosted.org/packages/24/c1/ae36fa59a59f9363017ed397750a0cd79a470490860bc7713967d89cdd31/black-25.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3df5f1bf91d36002b0a75389ca8663510cf0531cca8aa5c1ef695b46d98655f", size = 1460000, upload-time = "2025-01-29T05:37:25.829Z" }, + { url = "https://files.pythonhosted.org/packages/ac/b6/98f832e7a6c49aa3a464760c67c7856363aa644f2f3c74cf7d624168607e/black-25.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9e6827d563a2c820772b32ce8a42828dc6790f095f441beef18f96aa6f8294e", size = 1765963, upload-time = "2025-01-29T04:18:38.116Z" }, + { url = "https://files.pythonhosted.org/packages/ce/e9/2cb0a017eb7024f70e0d2e9bdb8c5a5b078c5740c7f8816065d06f04c557/black-25.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:bacabb307dca5ebaf9c118d2d2f6903da0d62c9faa82bd21a33eecc319559355", size = 1419419, upload-time = "2025-01-29T04:18:30.191Z" }, + { url = "https://files.pythonhosted.org/packages/09/71/54e999902aed72baf26bca0d50781b01838251a462612966e9fc4891eadd/black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717", size = 207646, upload-time = "2025-01-29T04:15:38.082Z" }, +] + +[[package]] +name = "blinker" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" }, +] + +[[package]] +name = "boto3" +version = "1.42.24" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, + { name = "jmespath" }, + { name = "s3transfer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ee/21/8be0e3685c3a4868be48d8d2f6e5b4641727e1d8a5d396b8b401d2b5f06e/boto3-1.42.24.tar.gz", hash = "sha256:c47a2f40df933e3861fc66fd8d6b87ee36d4361663a7e7ba39a87f5a78b2eae1", size = 112788, upload-time = "2026-01-07T20:30:51.019Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/75/bbfccb268f9faa4f59030888e859dca9797a980b77d6a074113af73bd4bf/boto3-1.42.24-py3-none-any.whl", hash = "sha256:8ed6ad670a5a2d7f66c1b0d3362791b48392c7a08f78479f5d8ab319a4d9118f", size = 140572, upload-time = "2026-01-07T20:30:49.431Z" }, +] + +[[package]] +name = "botocore" +version = "1.42.24" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jmespath" }, + { name = "python-dateutil" }, + { name = "urllib3", version = "1.26.20", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "urllib3", version = "2.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/12/d7/bb4a4e839b238ffb67b002d7326b328ebe5eb23ed5180f2ca10399a802de/botocore-1.42.24.tar.gz", hash = "sha256:be8d1bea64fb91eea08254a1e5fea057e4428d08e61f4e11083a02cafc1f8cc6", size = 14878455, upload-time = "2026-01-07T20:30:40.379Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/d4/f2655d777eed8b069ecab3761454cb83f830f8be8b5b0d292e4b3a980d00/botocore-1.42.24-py3-none-any.whl", hash = "sha256:8fca9781d7c84f7ad070fceffaff7179c4aa7a5ffb27b43df9d1d957801e0a8d", size = 14551806, upload-time = "2026-01-07T20:30:38.103Z" }, +] + +[[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.8.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cairocffi" }, + { name = "cssselect2" }, + { name = "defusedxml" }, + { name = "pillow" }, + { name = "tinycss2", version = "1.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "tinycss2", version = "1.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/b9/5106168bd43d7cd8b7cc2a2ee465b385f14b63f4c092bb89eee2d48c8e67/cairosvg-2.8.2.tar.gz", hash = "sha256:07cbf4e86317b27a92318a4cac2a4bb37a5e9c1b8a27355d06874b22f85bef9f", size = 8398590, upload-time = "2025-05-15T06:56:32.653Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/48/816bd4aaae93dbf9e408c58598bc32f4a8c65f4b86ab560864cb3ee60adb/cairosvg-2.8.2-py3-none-any.whl", hash = "sha256:eab46dad4674f33267a671dce39b64be245911c901c70d65d2b7b0821e852bf5", size = 45773, upload-time = "2025-05-15T06:56:28.552Z" }, +] + +[[package]] +name = "certifi" +version = "2026.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, +] + +[[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" }, + { url = "https://files.pythonhosted.org/packages/c0/cc/08ed5a43f2996a16b462f64a7055c6e962803534924b9b2f1371d8c00b7b/cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf", size = 184288, upload-time = "2025-09-08T23:23:48.404Z" }, + { url = "https://files.pythonhosted.org/packages/3d/de/38d9726324e127f727b4ecc376bc85e505bfe61ef130eaf3f290c6847dd4/cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7", size = 180509, upload-time = "2025-09-08T23:23:49.73Z" }, + { url = "https://files.pythonhosted.org/packages/9b/13/c92e36358fbcc39cf0962e83223c9522154ee8630e1df7c0b3a39a8124e2/cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c", size = 208813, upload-time = "2025-09-08T23:23:51.263Z" }, + { url = "https://files.pythonhosted.org/packages/15/12/a7a79bd0df4c3bff744b2d7e52cc1b68d5e7e427b384252c42366dc1ecbc/cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165", size = 216498, upload-time = "2025-09-08T23:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ad/5c51c1c7600bdd7ed9a24a203ec255dccdd0ebf4527f7b922a0bde2fb6ed/cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534", size = 203243, upload-time = "2025-09-08T23:23:53.836Z" }, + { url = "https://files.pythonhosted.org/packages/32/f2/81b63e288295928739d715d00952c8c6034cb6c6a516b17d37e0c8be5600/cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f", size = 203158, upload-time = "2025-09-08T23:23:55.169Z" }, + { url = "https://files.pythonhosted.org/packages/1f/74/cc4096ce66f5939042ae094e2e96f53426a979864aa1f96a621ad128be27/cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63", size = 216548, upload-time = "2025-09-08T23:23:56.506Z" }, + { url = "https://files.pythonhosted.org/packages/e8/be/f6424d1dc46b1091ffcc8964fa7c0ab0cd36839dd2761b49c90481a6ba1b/cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2", size = 218897, upload-time = "2025-09-08T23:23:57.825Z" }, + { url = "https://files.pythonhosted.org/packages/f7/e0/dda537c2309817edf60109e39265f24f24aa7f050767e22c98c53fe7f48b/cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65", size = 211249, upload-time = "2025-09-08T23:23:59.139Z" }, + { url = "https://files.pythonhosted.org/packages/2b/e7/7c769804eb75e4c4b35e658dba01de1640a351a9653c3d49ca89d16ccc91/cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322", size = 218041, upload-time = "2025-09-08T23:24:00.496Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d9/6218d78f920dcd7507fc16a766b5ef8f3b913cc7aa938e7fc80b9978d089/cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a", size = 172138, upload-time = "2025-09-08T23:24:01.7Z" }, + { url = "https://files.pythonhosted.org/packages/54/8f/a1e836f82d8e32a97e6b29cc8f641779181ac7363734f12df27db803ebda/cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9", size = 182794, upload-time = "2025-09-08T23:24:02.943Z" }, +] + +[[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/46/7c/0c4760bccf082737ca7ab84a4c2034fcc06b1f21cf3032ea98bd6feb1725/charset_normalizer-3.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9", size = 209609, upload-time = "2025-10-14T04:42:10.922Z" }, + { url = "https://files.pythonhosted.org/packages/bb/a4/69719daef2f3d7f1819de60c9a6be981b8eeead7542d5ec4440f3c80e111/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d", size = 149029, upload-time = "2025-10-14T04:42:12.38Z" }, + { url = "https://files.pythonhosted.org/packages/e6/21/8d4e1d6c1e6070d3672908b8e4533a71b5b53e71d16828cc24d0efec564c/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608", size = 144580, upload-time = "2025-10-14T04:42:13.549Z" }, + { url = "https://files.pythonhosted.org/packages/a7/0a/a616d001b3f25647a9068e0b9199f697ce507ec898cacb06a0d5a1617c99/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc", size = 162340, upload-time = "2025-10-14T04:42:14.892Z" }, + { url = "https://files.pythonhosted.org/packages/85/93/060b52deb249a5450460e0585c88a904a83aec474ab8e7aba787f45e79f2/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e", size = 159619, upload-time = "2025-10-14T04:42:16.676Z" }, + { url = "https://files.pythonhosted.org/packages/dd/21/0274deb1cc0632cd587a9a0ec6b4674d9108e461cb4cd40d457adaeb0564/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1", size = 153980, upload-time = "2025-10-14T04:42:17.917Z" }, + { url = "https://files.pythonhosted.org/packages/28/2b/e3d7d982858dccc11b31906976323d790dded2017a0572f093ff982d692f/charset_normalizer-3.4.4-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3", size = 152174, upload-time = "2025-10-14T04:42:19.018Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ff/4a269f8e35f1e58b2df52c131a1fa019acb7ef3f8697b7d464b07e9b492d/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6", size = 151666, upload-time = "2025-10-14T04:42:20.171Z" }, + { url = "https://files.pythonhosted.org/packages/da/c9/ec39870f0b330d58486001dd8e532c6b9a905f5765f58a6f8204926b4a93/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88", size = 145550, upload-time = "2025-10-14T04:42:21.324Z" }, + { url = "https://files.pythonhosted.org/packages/75/8f/d186ab99e40e0ed9f82f033d6e49001701c81244d01905dd4a6924191a30/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1", size = 163721, upload-time = "2025-10-14T04:42:22.46Z" }, + { url = "https://files.pythonhosted.org/packages/96/b1/6047663b9744df26a7e479ac1e77af7134b1fcf9026243bb48ee2d18810f/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf", size = 152127, upload-time = "2025-10-14T04:42:23.712Z" }, + { url = "https://files.pythonhosted.org/packages/59/78/e5a6eac9179f24f704d1be67d08704c3c6ab9f00963963524be27c18ed87/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318", size = 161175, upload-time = "2025-10-14T04:42:24.87Z" }, + { url = "https://files.pythonhosted.org/packages/e5/43/0e626e42d54dd2f8dd6fc5e1c5ff00f05fbca17cb699bedead2cae69c62f/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c", size = 155375, upload-time = "2025-10-14T04:42:27.246Z" }, + { url = "https://files.pythonhosted.org/packages/e9/91/d9615bf2e06f35e4997616ff31248c3657ed649c5ab9d35ea12fce54e380/charset_normalizer-3.4.4-cp39-cp39-win32.whl", hash = "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505", size = 99692, upload-time = "2025-10-14T04:42:28.425Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a9/6c040053909d9d1ef4fcab45fddec083aedc9052c10078339b47c8573ea8/charset_normalizer-3.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966", size = 107192, upload-time = "2025-10-14T04:42:29.482Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c6/4fa536b2c0cd3edfb7ccf8469fa0f363ea67b7213a842b90909ca33dd851/charset_normalizer-3.4.4-cp39-cp39-win_arm64.whl", hash = "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50", size = 100220, upload-time = "2025-10-14T04:42:30.632Z" }, + { 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.1.8" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.10' and python_full_version < '3.14'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.10' and 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 = "cohere" +version = "5.20.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastavro" }, + { name = "httpx" }, + { name = "pydantic" }, + { name = "pydantic-core" }, + { name = "requests" }, + { name = "tokenizers" }, + { name = "types-requests", version = "2.31.0.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "types-requests", version = "2.32.4.20260107", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4b/ed/bb02083654bdc089ae4ef1cd7691fd2233f1fd9f32bcbfacc80ff57d9775/cohere-5.20.1.tar.gz", hash = "sha256:50973f63d2c6138ff52ce37d8d6f78ccc539af4e8c43865e960d68e0bf835b6f", size = 180820, upload-time = "2025-12-18T16:39:50.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/e3/94eb11ac3ebaaa3a6afb5d2ff23db95d58bc468ae538c388edf49f2f20b5/cohere-5.20.1-py3-none-any.whl", hash = "sha256:d230fd13d95ba92ae927fce3dd497599b169883afc7954fe29b39fb8d5df5fc7", size = 318973, upload-time = "2025-12-18T16:39:49.504Z" }, +] + +[[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.10.7" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/51/26/d22c300112504f5f9a9fd2297ce33c35f3d353e4aeb987c8419453b2a7c2/coverage-7.10.7.tar.gz", hash = "sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239", size = 827704, upload-time = "2025-09-21T20:03:56.815Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/6c/3a3f7a46888e69d18abe3ccc6fe4cb16cccb1e6a2f99698931dafca489e6/coverage-7.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fc04cc7a3db33664e0c2d10eb8990ff6b3536f6842c9590ae8da4c614b9ed05a", size = 217987, upload-time = "2025-09-21T20:00:57.218Z" }, + { url = "https://files.pythonhosted.org/packages/03/94/952d30f180b1a916c11a56f5c22d3535e943aa22430e9e3322447e520e1c/coverage-7.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e201e015644e207139f7e2351980feb7040e6f4b2c2978892f3e3789d1c125e5", size = 218388, upload-time = "2025-09-21T20:01:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/50/2b/9e0cf8ded1e114bcd8b2fd42792b57f1c4e9e4ea1824cde2af93a67305be/coverage-7.10.7-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:240af60539987ced2c399809bd34f7c78e8abe0736af91c3d7d0e795df633d17", size = 245148, upload-time = "2025-09-21T20:01:01.768Z" }, + { url = "https://files.pythonhosted.org/packages/19/20/d0384ac06a6f908783d9b6aa6135e41b093971499ec488e47279f5b846e6/coverage-7.10.7-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8421e088bc051361b01c4b3a50fd39a4b9133079a2229978d9d30511fd05231b", size = 246958, upload-time = "2025-09-21T20:01:03.355Z" }, + { url = "https://files.pythonhosted.org/packages/60/83/5c283cff3d41285f8eab897651585db908a909c572bdc014bcfaf8a8b6ae/coverage-7.10.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6be8ed3039ae7f7ac5ce058c308484787c86e8437e72b30bf5e88b8ea10f3c87", size = 248819, upload-time = "2025-09-21T20:01:04.968Z" }, + { url = "https://files.pythonhosted.org/packages/60/22/02eb98fdc5ff79f423e990d877693e5310ae1eab6cb20ae0b0b9ac45b23b/coverage-7.10.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e28299d9f2e889e6d51b1f043f58d5f997c373cc12e6403b90df95b8b047c13e", size = 245754, upload-time = "2025-09-21T20:01:06.321Z" }, + { url = "https://files.pythonhosted.org/packages/b4/bc/25c83bcf3ad141b32cd7dc45485ef3c01a776ca3aa8ef0a93e77e8b5bc43/coverage-7.10.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c4e16bd7761c5e454f4efd36f345286d6f7c5fa111623c355691e2755cae3b9e", size = 246860, upload-time = "2025-09-21T20:01:07.605Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b7/95574702888b58c0928a6e982038c596f9c34d52c5e5107f1eef729399b5/coverage-7.10.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b1c81d0e5e160651879755c9c675b974276f135558cf4ba79fee7b8413a515df", size = 244877, upload-time = "2025-09-21T20:01:08.829Z" }, + { url = "https://files.pythonhosted.org/packages/47/b6/40095c185f235e085df0e0b158f6bd68cc6e1d80ba6c7721dc81d97ec318/coverage-7.10.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:606cc265adc9aaedcc84f1f064f0e8736bc45814f15a357e30fca7ecc01504e0", size = 245108, upload-time = "2025-09-21T20:01:10.527Z" }, + { url = "https://files.pythonhosted.org/packages/c8/50/4aea0556da7a4b93ec9168420d170b55e2eb50ae21b25062513d020c6861/coverage-7.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:10b24412692df990dbc34f8fb1b6b13d236ace9dfdd68df5b28c2e39cafbba13", size = 245752, upload-time = "2025-09-21T20:01:11.857Z" }, + { url = "https://files.pythonhosted.org/packages/6a/28/ea1a84a60828177ae3b100cb6723838523369a44ec5742313ed7db3da160/coverage-7.10.7-cp310-cp310-win32.whl", hash = "sha256:b51dcd060f18c19290d9b8a9dd1e0181538df2ce0717f562fff6cf74d9fc0b5b", size = 220497, upload-time = "2025-09-21T20:01:13.459Z" }, + { url = "https://files.pythonhosted.org/packages/fc/1a/a81d46bbeb3c3fd97b9602ebaa411e076219a150489bcc2c025f151bd52d/coverage-7.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:3a622ac801b17198020f09af3eaf45666b344a0d69fc2a6ffe2ea83aeef1d807", size = 221392, upload-time = "2025-09-21T20:01:14.722Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5d/c1a17867b0456f2e9ce2d8d4708a4c3a089947d0bec9c66cdf60c9e7739f/coverage-7.10.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a609f9c93113be646f44c2a0256d6ea375ad047005d7f57a5c15f614dc1b2f59", size = 218102, upload-time = "2025-09-21T20:01:16.089Z" }, + { url = "https://files.pythonhosted.org/packages/54/f0/514dcf4b4e3698b9a9077f084429681bf3aad2b4a72578f89d7f643eb506/coverage-7.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:65646bb0359386e07639c367a22cf9b5bf6304e8630b565d0626e2bdf329227a", size = 218505, upload-time = "2025-09-21T20:01:17.788Z" }, + { url = "https://files.pythonhosted.org/packages/20/f6/9626b81d17e2a4b25c63ac1b425ff307ecdeef03d67c9a147673ae40dc36/coverage-7.10.7-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5f33166f0dfcce728191f520bd2692914ec70fac2713f6bf3ce59c3deacb4699", size = 248898, upload-time = "2025-09-21T20:01:19.488Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ef/bd8e719c2f7417ba03239052e099b76ea1130ac0cbb183ee1fcaa58aaff3/coverage-7.10.7-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35f5e3f9e455bb17831876048355dca0f758b6df22f49258cb5a91da23ef437d", size = 250831, upload-time = "2025-09-21T20:01:20.817Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b6/bf054de41ec948b151ae2b79a55c107f5760979538f5fb80c195f2517718/coverage-7.10.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da86b6d62a496e908ac2898243920c7992499c1712ff7c2b6d837cc69d9467e", size = 252937, upload-time = "2025-09-21T20:01:22.171Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e5/3860756aa6f9318227443c6ce4ed7bf9e70bb7f1447a0353f45ac5c7974b/coverage-7.10.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6b8b09c1fad947c84bbbc95eca841350fad9cbfa5a2d7ca88ac9f8d836c92e23", size = 249021, upload-time = "2025-09-21T20:01:23.907Z" }, + { url = "https://files.pythonhosted.org/packages/26/0f/bd08bd042854f7fd07b45808927ebcce99a7ed0f2f412d11629883517ac2/coverage-7.10.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4376538f36b533b46f8971d3a3e63464f2c7905c9800db97361c43a2b14792ab", size = 250626, upload-time = "2025-09-21T20:01:25.721Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a7/4777b14de4abcc2e80c6b1d430f5d51eb18ed1d75fca56cbce5f2db9b36e/coverage-7.10.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:121da30abb574f6ce6ae09840dae322bef734480ceafe410117627aa54f76d82", size = 248682, upload-time = "2025-09-21T20:01:27.105Z" }, + { url = "https://files.pythonhosted.org/packages/34/72/17d082b00b53cd45679bad682fac058b87f011fd8b9fe31d77f5f8d3a4e4/coverage-7.10.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:88127d40df529336a9836870436fc2751c339fbaed3a836d42c93f3e4bd1d0a2", size = 248402, upload-time = "2025-09-21T20:01:28.629Z" }, + { url = "https://files.pythonhosted.org/packages/81/7a/92367572eb5bdd6a84bfa278cc7e97db192f9f45b28c94a9ca1a921c3577/coverage-7.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ba58bbcd1b72f136080c0bccc2400d66cc6115f3f906c499013d065ac33a4b61", size = 249320, upload-time = "2025-09-21T20:01:30.004Z" }, + { url = "https://files.pythonhosted.org/packages/2f/88/a23cc185f6a805dfc4fdf14a94016835eeb85e22ac3a0e66d5e89acd6462/coverage-7.10.7-cp311-cp311-win32.whl", hash = "sha256:972b9e3a4094b053a4e46832b4bc829fc8a8d347160eb39d03f1690316a99c14", size = 220536, upload-time = "2025-09-21T20:01:32.184Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ef/0b510a399dfca17cec7bc2f05ad8bd78cf55f15c8bc9a73ab20c5c913c2e/coverage-7.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:a7b55a944a7f43892e28ad4bc0561dfd5f0d73e605d1aa5c3c976b52aea121d2", size = 221425, upload-time = "2025-09-21T20:01:33.557Z" }, + { url = "https://files.pythonhosted.org/packages/51/7f/023657f301a276e4ba1850f82749bc136f5a7e8768060c2e5d9744a22951/coverage-7.10.7-cp311-cp311-win_arm64.whl", hash = "sha256:736f227fb490f03c6488f9b6d45855f8e0fd749c007f9303ad30efab0e73c05a", size = 220103, upload-time = "2025-09-21T20:01:34.929Z" }, + { url = "https://files.pythonhosted.org/packages/13/e4/eb12450f71b542a53972d19117ea5a5cea1cab3ac9e31b0b5d498df1bd5a/coverage-7.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7bb3b9ddb87ef7725056572368040c32775036472d5a033679d1fa6c8dc08417", size = 218290, upload-time = "2025-09-21T20:01:36.455Z" }, + { url = "https://files.pythonhosted.org/packages/37/66/593f9be12fc19fb36711f19a5371af79a718537204d16ea1d36f16bd78d2/coverage-7.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:18afb24843cbc175687225cab1138c95d262337f5473512010e46831aa0c2973", size = 218515, upload-time = "2025-09-21T20:01:37.982Z" }, + { url = "https://files.pythonhosted.org/packages/66/80/4c49f7ae09cafdacc73fbc30949ffe77359635c168f4e9ff33c9ebb07838/coverage-7.10.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:399a0b6347bcd3822be369392932884b8216d0944049ae22925631a9b3d4ba4c", size = 250020, upload-time = "2025-09-21T20:01:39.617Z" }, + { url = "https://files.pythonhosted.org/packages/a6/90/a64aaacab3b37a17aaedd83e8000142561a29eb262cede42d94a67f7556b/coverage-7.10.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314f2c326ded3f4b09be11bc282eb2fc861184bc95748ae67b360ac962770be7", size = 252769, upload-time = "2025-09-21T20:01:41.341Z" }, + { url = "https://files.pythonhosted.org/packages/98/2e/2dda59afd6103b342e096f246ebc5f87a3363b5412609946c120f4e7750d/coverage-7.10.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c41e71c9cfb854789dee6fc51e46743a6d138b1803fab6cb860af43265b42ea6", size = 253901, upload-time = "2025-09-21T20:01:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/53/dc/8d8119c9051d50f3119bb4a75f29f1e4a6ab9415cd1fa8bf22fcc3fb3b5f/coverage-7.10.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc01f57ca26269c2c706e838f6422e2a8788e41b3e3c65e2f41148212e57cd59", size = 250413, upload-time = "2025-09-21T20:01:44.469Z" }, + { url = "https://files.pythonhosted.org/packages/98/b3/edaff9c5d79ee4d4b6d3fe046f2b1d799850425695b789d491a64225d493/coverage-7.10.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a6442c59a8ac8b85812ce33bc4d05bde3fb22321fa8294e2a5b487c3505f611b", size = 251820, upload-time = "2025-09-21T20:01:45.915Z" }, + { url = "https://files.pythonhosted.org/packages/11/25/9a0728564bb05863f7e513e5a594fe5ffef091b325437f5430e8cfb0d530/coverage-7.10.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:78a384e49f46b80fb4c901d52d92abe098e78768ed829c673fbb53c498bef73a", size = 249941, upload-time = "2025-09-21T20:01:47.296Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fd/ca2650443bfbef5b0e74373aac4df67b08180d2f184b482c41499668e258/coverage-7.10.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5e1e9802121405ede4b0133aa4340ad8186a1d2526de5b7c3eca519db7bb89fb", size = 249519, upload-time = "2025-09-21T20:01:48.73Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/f692f125fb4299b6f963b0745124998ebb8e73ecdfce4ceceb06a8c6bec5/coverage-7.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d41213ea25a86f69efd1575073d34ea11aabe075604ddf3d148ecfec9e1e96a1", size = 251375, upload-time = "2025-09-21T20:01:50.529Z" }, + { url = "https://files.pythonhosted.org/packages/5e/75/61b9bbd6c7d24d896bfeec57acba78e0f8deac68e6baf2d4804f7aae1f88/coverage-7.10.7-cp312-cp312-win32.whl", hash = "sha256:77eb4c747061a6af8d0f7bdb31f1e108d172762ef579166ec84542f711d90256", size = 220699, upload-time = "2025-09-21T20:01:51.941Z" }, + { url = "https://files.pythonhosted.org/packages/ca/f3/3bf7905288b45b075918d372498f1cf845b5b579b723c8fd17168018d5f5/coverage-7.10.7-cp312-cp312-win_amd64.whl", hash = "sha256:f51328ffe987aecf6d09f3cd9d979face89a617eacdaea43e7b3080777f647ba", size = 221512, upload-time = "2025-09-21T20:01:53.481Z" }, + { url = "https://files.pythonhosted.org/packages/5c/44/3e32dbe933979d05cf2dac5e697c8599cfe038aaf51223ab901e208d5a62/coverage-7.10.7-cp312-cp312-win_arm64.whl", hash = "sha256:bda5e34f8a75721c96085903c6f2197dc398c20ffd98df33f866a9c8fd95f4bf", size = 220147, upload-time = "2025-09-21T20:01:55.2Z" }, + { url = "https://files.pythonhosted.org/packages/9a/94/b765c1abcb613d103b64fcf10395f54d69b0ef8be6a0dd9c524384892cc7/coverage-7.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d", size = 218320, upload-time = "2025-09-21T20:01:56.629Z" }, + { url = "https://files.pythonhosted.org/packages/72/4f/732fff31c119bb73b35236dd333030f32c4bfe909f445b423e6c7594f9a2/coverage-7.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b", size = 218575, upload-time = "2025-09-21T20:01:58.203Z" }, + { url = "https://files.pythonhosted.org/packages/87/02/ae7e0af4b674be47566707777db1aa375474f02a1d64b9323e5813a6cdd5/coverage-7.10.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e", size = 249568, upload-time = "2025-09-21T20:01:59.748Z" }, + { url = "https://files.pythonhosted.org/packages/a2/77/8c6d22bf61921a59bce5471c2f1f7ac30cd4ac50aadde72b8c48d5727902/coverage-7.10.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b", size = 252174, upload-time = "2025-09-21T20:02:01.192Z" }, + { url = "https://files.pythonhosted.org/packages/b1/20/b6ea4f69bbb52dac0aebd62157ba6a9dddbfe664f5af8122dac296c3ee15/coverage-7.10.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49", size = 253447, upload-time = "2025-09-21T20:02:02.701Z" }, + { url = "https://files.pythonhosted.org/packages/f9/28/4831523ba483a7f90f7b259d2018fef02cb4d5b90bc7c1505d6e5a84883c/coverage-7.10.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911", size = 249779, upload-time = "2025-09-21T20:02:04.185Z" }, + { url = "https://files.pythonhosted.org/packages/a7/9f/4331142bc98c10ca6436d2d620c3e165f31e6c58d43479985afce6f3191c/coverage-7.10.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0", size = 251604, upload-time = "2025-09-21T20:02:06.034Z" }, + { url = "https://files.pythonhosted.org/packages/ce/60/bda83b96602036b77ecf34e6393a3836365481b69f7ed7079ab85048202b/coverage-7.10.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f", size = 249497, upload-time = "2025-09-21T20:02:07.619Z" }, + { url = "https://files.pythonhosted.org/packages/5f/af/152633ff35b2af63977edd835d8e6430f0caef27d171edf2fc76c270ef31/coverage-7.10.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c", size = 249350, upload-time = "2025-09-21T20:02:10.34Z" }, + { url = "https://files.pythonhosted.org/packages/9d/71/d92105d122bd21cebba877228990e1646d862e34a98bb3374d3fece5a794/coverage-7.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f", size = 251111, upload-time = "2025-09-21T20:02:12.122Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9e/9fdb08f4bf476c912f0c3ca292e019aab6712c93c9344a1653986c3fd305/coverage-7.10.7-cp313-cp313-win32.whl", hash = "sha256:dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698", size = 220746, upload-time = "2025-09-21T20:02:13.919Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b1/a75fd25df44eab52d1931e89980d1ada46824c7a3210be0d3c88a44aaa99/coverage-7.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843", size = 221541, upload-time = "2025-09-21T20:02:15.57Z" }, + { url = "https://files.pythonhosted.org/packages/14/3a/d720d7c989562a6e9a14b2c9f5f2876bdb38e9367126d118495b89c99c37/coverage-7.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546", size = 220170, upload-time = "2025-09-21T20:02:17.395Z" }, + { url = "https://files.pythonhosted.org/packages/bb/22/e04514bf2a735d8b0add31d2b4ab636fc02370730787c576bb995390d2d5/coverage-7.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c", size = 219029, upload-time = "2025-09-21T20:02:18.936Z" }, + { url = "https://files.pythonhosted.org/packages/11/0b/91128e099035ece15da3445d9015e4b4153a6059403452d324cbb0a575fa/coverage-7.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15", size = 219259, upload-time = "2025-09-21T20:02:20.44Z" }, + { url = "https://files.pythonhosted.org/packages/8b/51/66420081e72801536a091a0c8f8c1f88a5c4bf7b9b1bdc6222c7afe6dc9b/coverage-7.10.7-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4", size = 260592, upload-time = "2025-09-21T20:02:22.313Z" }, + { url = "https://files.pythonhosted.org/packages/5d/22/9b8d458c2881b22df3db5bb3e7369e63d527d986decb6c11a591ba2364f7/coverage-7.10.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0", size = 262768, upload-time = "2025-09-21T20:02:24.287Z" }, + { url = "https://files.pythonhosted.org/packages/f7/08/16bee2c433e60913c610ea200b276e8eeef084b0d200bdcff69920bd5828/coverage-7.10.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0", size = 264995, upload-time = "2025-09-21T20:02:26.133Z" }, + { url = "https://files.pythonhosted.org/packages/20/9d/e53eb9771d154859b084b90201e5221bca7674ba449a17c101a5031d4054/coverage-7.10.7-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65", size = 259546, upload-time = "2025-09-21T20:02:27.716Z" }, + { url = "https://files.pythonhosted.org/packages/ad/b0/69bc7050f8d4e56a89fb550a1577d5d0d1db2278106f6f626464067b3817/coverage-7.10.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541", size = 262544, upload-time = "2025-09-21T20:02:29.216Z" }, + { url = "https://files.pythonhosted.org/packages/ef/4b/2514b060dbd1bc0aaf23b852c14bb5818f244c664cb16517feff6bb3a5ab/coverage-7.10.7-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6", size = 260308, upload-time = "2025-09-21T20:02:31.226Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/7ba2175007c246d75e496f64c06e94122bdb914790a1285d627a918bd271/coverage-7.10.7-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999", size = 258920, upload-time = "2025-09-21T20:02:32.823Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b3/fac9f7abbc841409b9a410309d73bfa6cfb2e51c3fada738cb607ce174f8/coverage-7.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2", size = 261434, upload-time = "2025-09-21T20:02:34.86Z" }, + { url = "https://files.pythonhosted.org/packages/ee/51/a03bec00d37faaa891b3ff7387192cef20f01604e5283a5fabc95346befa/coverage-7.10.7-cp313-cp313t-win32.whl", hash = "sha256:2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a", size = 221403, upload-time = "2025-09-21T20:02:37.034Z" }, + { url = "https://files.pythonhosted.org/packages/53/22/3cf25d614e64bf6d8e59c7c669b20d6d940bb337bdee5900b9ca41c820bb/coverage-7.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb", size = 222469, upload-time = "2025-09-21T20:02:39.011Z" }, + { url = "https://files.pythonhosted.org/packages/49/a1/00164f6d30d8a01c3c9c48418a7a5be394de5349b421b9ee019f380df2a0/coverage-7.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb", size = 220731, upload-time = "2025-09-21T20:02:40.939Z" }, + { url = "https://files.pythonhosted.org/packages/23/9c/5844ab4ca6a4dd97a1850e030a15ec7d292b5c5cb93082979225126e35dd/coverage-7.10.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520", size = 218302, upload-time = "2025-09-21T20:02:42.527Z" }, + { url = "https://files.pythonhosted.org/packages/f0/89/673f6514b0961d1f0e20ddc242e9342f6da21eaba3489901b565c0689f34/coverage-7.10.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32", size = 218578, upload-time = "2025-09-21T20:02:44.468Z" }, + { url = "https://files.pythonhosted.org/packages/05/e8/261cae479e85232828fb17ad536765c88dd818c8470aca690b0ac6feeaa3/coverage-7.10.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3445258bcded7d4aa630ab8296dea4d3f15a255588dd535f980c193ab6b95f3f", size = 249629, upload-time = "2025-09-21T20:02:46.503Z" }, + { url = "https://files.pythonhosted.org/packages/82/62/14ed6546d0207e6eda876434e3e8475a3e9adbe32110ce896c9e0c06bb9a/coverage-7.10.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a", size = 252162, upload-time = "2025-09-21T20:02:48.689Z" }, + { url = "https://files.pythonhosted.org/packages/ff/49/07f00db9ac6478e4358165a08fb41b469a1b053212e8a00cb02f0d27a05f/coverage-7.10.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360", size = 253517, upload-time = "2025-09-21T20:02:50.31Z" }, + { url = "https://files.pythonhosted.org/packages/a2/59/c5201c62dbf165dfbc91460f6dbbaa85a8b82cfa6131ac45d6c1bfb52deb/coverage-7.10.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:93c1b03552081b2a4423091d6fb3787265b8f86af404cff98d1b5342713bdd69", size = 249632, upload-time = "2025-09-21T20:02:51.971Z" }, + { url = "https://files.pythonhosted.org/packages/07/ae/5920097195291a51fb00b3a70b9bbd2edbfe3c84876a1762bd1ef1565ebc/coverage-7.10.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cc87dd1b6eaf0b848eebb1c86469b9f72a1891cb42ac7adcfbce75eadb13dd14", size = 251520, upload-time = "2025-09-21T20:02:53.858Z" }, + { url = "https://files.pythonhosted.org/packages/b9/3c/a815dde77a2981f5743a60b63df31cb322c944843e57dbd579326625a413/coverage-7.10.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:39508ffda4f343c35f3236fe8d1a6634a51f4581226a1262769d7f970e73bffe", size = 249455, upload-time = "2025-09-21T20:02:55.807Z" }, + { url = "https://files.pythonhosted.org/packages/aa/99/f5cdd8421ea656abefb6c0ce92556709db2265c41e8f9fc6c8ae0f7824c9/coverage-7.10.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:925a1edf3d810537c5a3abe78ec5530160c5f9a26b1f4270b40e62cc79304a1e", size = 249287, upload-time = "2025-09-21T20:02:57.784Z" }, + { url = "https://files.pythonhosted.org/packages/c3/7a/e9a2da6a1fc5d007dd51fca083a663ab930a8c4d149c087732a5dbaa0029/coverage-7.10.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2c8b9a0636f94c43cd3576811e05b89aa9bc2d0a85137affc544ae5cb0e4bfbd", size = 250946, upload-time = "2025-09-21T20:02:59.431Z" }, + { url = "https://files.pythonhosted.org/packages/ef/5b/0b5799aa30380a949005a353715095d6d1da81927d6dbed5def2200a4e25/coverage-7.10.7-cp314-cp314-win32.whl", hash = "sha256:b7b8288eb7cdd268b0304632da8cb0bb93fadcfec2fe5712f7b9cc8f4d487be2", size = 221009, upload-time = "2025-09-21T20:03:01.324Z" }, + { url = "https://files.pythonhosted.org/packages/da/b0/e802fbb6eb746de006490abc9bb554b708918b6774b722bb3a0e6aa1b7de/coverage-7.10.7-cp314-cp314-win_amd64.whl", hash = "sha256:1ca6db7c8807fb9e755d0379ccc39017ce0a84dcd26d14b5a03b78563776f681", size = 221804, upload-time = "2025-09-21T20:03:03.4Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e8/71d0c8e374e31f39e3389bb0bd19e527d46f00ea8571ec7ec8fd261d8b44/coverage-7.10.7-cp314-cp314-win_arm64.whl", hash = "sha256:097c1591f5af4496226d5783d036bf6fd6cd0cbc132e071b33861de756efb880", size = 220384, upload-time = "2025-09-21T20:03:05.111Z" }, + { url = "https://files.pythonhosted.org/packages/62/09/9a5608d319fa3eba7a2019addeacb8c746fb50872b57a724c9f79f146969/coverage-7.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63", size = 219047, upload-time = "2025-09-21T20:03:06.795Z" }, + { url = "https://files.pythonhosted.org/packages/f5/6f/f58d46f33db9f2e3647b2d0764704548c184e6f5e014bef528b7f979ef84/coverage-7.10.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2", size = 219266, upload-time = "2025-09-21T20:03:08.495Z" }, + { url = "https://files.pythonhosted.org/packages/74/5c/183ffc817ba68e0b443b8c934c8795553eb0c14573813415bd59941ee165/coverage-7.10.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8fb190658865565c549b6b4706856d6a7b09302c797eb2cf8e7fe9dabb043f0d", size = 260767, upload-time = "2025-09-21T20:03:10.172Z" }, + { url = "https://files.pythonhosted.org/packages/0f/48/71a8abe9c1ad7e97548835e3cc1adbf361e743e9d60310c5f75c9e7bf847/coverage-7.10.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0", size = 262931, upload-time = "2025-09-21T20:03:11.861Z" }, + { url = "https://files.pythonhosted.org/packages/84/fd/193a8fb132acfc0a901f72020e54be5e48021e1575bb327d8ee1097a28fd/coverage-7.10.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699", size = 265186, upload-time = "2025-09-21T20:03:13.539Z" }, + { url = "https://files.pythonhosted.org/packages/b1/8f/74ecc30607dd95ad50e3034221113ccb1c6d4e8085cc761134782995daae/coverage-7.10.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03ffc58aacdf65d2a82bbeb1ffe4d01ead4017a21bfd0454983b88ca73af94b9", size = 259470, upload-time = "2025-09-21T20:03:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/0f/55/79ff53a769f20d71b07023ea115c9167c0bb56f281320520cf64c5298a96/coverage-7.10.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1b4fd784344d4e52647fd7857b2af5b3fbe6c239b0b5fa63e94eb67320770e0f", size = 262626, upload-time = "2025-09-21T20:03:17.673Z" }, + { url = "https://files.pythonhosted.org/packages/88/e2/dac66c140009b61ac3fc13af673a574b00c16efdf04f9b5c740703e953c0/coverage-7.10.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0ebbaddb2c19b71912c6f2518e791aa8b9f054985a0769bdb3a53ebbc765c6a1", size = 260386, upload-time = "2025-09-21T20:03:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/a2/f1/f48f645e3f33bb9ca8a496bc4a9671b52f2f353146233ebd7c1df6160440/coverage-7.10.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a2d9a3b260cc1d1dbdb1c582e63ddcf5363426a1a68faa0f5da28d8ee3c722a0", size = 258852, upload-time = "2025-09-21T20:03:21.007Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3b/8442618972c51a7affeead957995cfa8323c0c9bcf8fa5a027421f720ff4/coverage-7.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a3cc8638b2480865eaa3926d192e64ce6c51e3d29c849e09d5b4ad95efae5399", size = 261534, upload-time = "2025-09-21T20:03:23.12Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dc/101f3fa3a45146db0cb03f5b4376e24c0aac818309da23e2de0c75295a91/coverage-7.10.7-cp314-cp314t-win32.whl", hash = "sha256:67f8c5cbcd3deb7a60b3345dffc89a961a484ed0af1f6f73de91705cc6e31235", size = 221784, upload-time = "2025-09-21T20:03:24.769Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a1/74c51803fc70a8a40d7346660379e144be772bab4ac7bb6e6b905152345c/coverage-7.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e1ed71194ef6dea7ed2d5cb5f7243d4bcd334bfb63e59878519be558078f848d", size = 222905, upload-time = "2025-09-21T20:03:26.93Z" }, + { url = "https://files.pythonhosted.org/packages/12/65/f116a6d2127df30bcafbceef0302d8a64ba87488bf6f73a6d8eebf060873/coverage-7.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:7fe650342addd8524ca63d77b2362b02345e5f1a093266787d210c70a50b471a", size = 220922, upload-time = "2025-09-21T20:03:28.672Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ad/d1c25053764b4c42eb294aae92ab617d2e4f803397f9c7c8295caa77a260/coverage-7.10.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fff7b9c3f19957020cac546c70025331113d2e61537f6e2441bc7657913de7d3", size = 217978, upload-time = "2025-09-21T20:03:30.362Z" }, + { url = "https://files.pythonhosted.org/packages/52/2f/b9f9daa39b80ece0b9548bbb723381e29bc664822d9a12c2135f8922c22b/coverage-7.10.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bc91b314cef27742da486d6839b677b3f2793dfe52b51bbbb7cf736d5c29281c", size = 218370, upload-time = "2025-09-21T20:03:32.147Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6e/30d006c3b469e58449650642383dddf1c8fb63d44fdf92994bfd46570695/coverage-7.10.7-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:567f5c155eda8df1d3d439d40a45a6a5f029b429b06648235f1e7e51b522b396", size = 244802, upload-time = "2025-09-21T20:03:33.919Z" }, + { url = "https://files.pythonhosted.org/packages/b0/49/8a070782ce7e6b94ff6a0b6d7c65ba6bc3091d92a92cef4cd4eb0767965c/coverage-7.10.7-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2af88deffcc8a4d5974cf2d502251bc3b2db8461f0b66d80a449c33757aa9f40", size = 246625, upload-time = "2025-09-21T20:03:36.09Z" }, + { url = "https://files.pythonhosted.org/packages/6a/92/1c1c5a9e8677ce56d42b97bdaca337b2d4d9ebe703d8c174ede52dbabd5f/coverage-7.10.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7315339eae3b24c2d2fa1ed7d7a38654cba34a13ef19fbcb9425da46d3dc594", size = 248399, upload-time = "2025-09-21T20:03:38.342Z" }, + { url = "https://files.pythonhosted.org/packages/c0/54/b140edee7257e815de7426d5d9846b58505dffc29795fff2dfb7f8a1c5a0/coverage-7.10.7-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:912e6ebc7a6e4adfdbb1aec371ad04c68854cd3bf3608b3514e7ff9062931d8a", size = 245142, upload-time = "2025-09-21T20:03:40.591Z" }, + { url = "https://files.pythonhosted.org/packages/e4/9e/6d6b8295940b118e8b7083b29226c71f6154f7ff41e9ca431f03de2eac0d/coverage-7.10.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f49a05acd3dfe1ce9715b657e28d138578bc40126760efb962322c56e9ca344b", size = 246284, upload-time = "2025-09-21T20:03:42.355Z" }, + { url = "https://files.pythonhosted.org/packages/db/e5/5e957ca747d43dbe4d9714358375c7546cb3cb533007b6813fc20fce37ad/coverage-7.10.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:cce2109b6219f22ece99db7644b9622f54a4e915dad65660ec435e89a3ea7cc3", size = 244353, upload-time = "2025-09-21T20:03:44.218Z" }, + { url = "https://files.pythonhosted.org/packages/9a/45/540fc5cc92536a1b783b7ef99450bd55a4b3af234aae35a18a339973ce30/coverage-7.10.7-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:f3c887f96407cea3916294046fc7dab611c2552beadbed4ea901cbc6a40cc7a0", size = 244430, upload-time = "2025-09-21T20:03:46.065Z" }, + { url = "https://files.pythonhosted.org/packages/75/0b/8287b2e5b38c8fe15d7e3398849bb58d382aedc0864ea0fa1820e8630491/coverage-7.10.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:635adb9a4507c9fd2ed65f39693fa31c9a3ee3a8e6dc64df033e8fdf52a7003f", size = 245311, upload-time = "2025-09-21T20:03:48.19Z" }, + { url = "https://files.pythonhosted.org/packages/0c/1d/29724999984740f0c86d03e6420b942439bf5bd7f54d4382cae386a9d1e9/coverage-7.10.7-cp39-cp39-win32.whl", hash = "sha256:5a02d5a850e2979b0a014c412573953995174743a3f7fa4ea5a6e9a3c5617431", size = 220500, upload-time = "2025-09-21T20:03:50.024Z" }, + { url = "https://files.pythonhosted.org/packages/43/11/4b1e6b129943f905ca54c339f343877b55b365ae2558806c1be4f7476ed5/coverage-7.10.7-cp39-cp39-win_amd64.whl", hash = "sha256:c134869d5ffe34547d14e174c866fd8fe2254918cc0a95e99052903bc1543e07", size = 221408, upload-time = "2025-09-21T20:03:51.803Z" }, + { url = "https://files.pythonhosted.org/packages/ec/16/114df1c291c22cac3b0c127a73e0af5c12ed7bbb6558d310429a0ae24023/coverage-7.10.7-py3-none-any.whl", hash = "sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260", size = 209952, upload-time = "2025-09-21T20:03:53.918Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version < '3.10'" }, +] + +[[package]] +name = "coverage" +version = "7.13.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.10' and python_full_version < '3.14'", +] +sdist = { url = "https://files.pythonhosted.org/packages/23/f9/e92df5e07f3fc8d4c7f9a0f146ef75446bf870351cd37b788cf5897f8079/coverage-7.13.1.tar.gz", hash = "sha256:b7593fe7eb5feaa3fbb461ac79aac9f9fc0387a5ca8080b0c6fe2ca27b091afd", size = 825862, upload-time = "2025-12-28T15:42:56.969Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/9a/3742e58fd04b233df95c012ee9f3dfe04708a5e1d32613bd2d47d4e1be0d/coverage-7.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e1fa280b3ad78eea5be86f94f461c04943d942697e0dac889fa18fff8f5f9147", size = 218633, upload-time = "2025-12-28T15:40:10.165Z" }, + { url = "https://files.pythonhosted.org/packages/7e/45/7e6bdc94d89cd7c8017ce735cf50478ddfe765d4fbf0c24d71d30ea33d7a/coverage-7.13.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c3d8c679607220979434f494b139dfb00131ebf70bb406553d69c1ff01a5c33d", size = 219147, upload-time = "2025-12-28T15:40:12.069Z" }, + { url = "https://files.pythonhosted.org/packages/f7/38/0d6a258625fd7f10773fe94097dc16937a5f0e3e0cdf3adef67d3ac6baef/coverage-7.13.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:339dc63b3eba969067b00f41f15ad161bf2946613156fb131266d8debc8e44d0", size = 245894, upload-time = "2025-12-28T15:40:13.556Z" }, + { url = "https://files.pythonhosted.org/packages/27/58/409d15ea487986994cbd4d06376e9860e9b157cfbfd402b1236770ab8dd2/coverage-7.13.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:db622b999ffe49cb891f2fff3b340cdc2f9797d01a0a202a0973ba2562501d90", size = 247721, upload-time = "2025-12-28T15:40:15.37Z" }, + { url = "https://files.pythonhosted.org/packages/da/bf/6e8056a83fd7a96c93341f1ffe10df636dd89f26d5e7b9ca511ce3bcf0df/coverage-7.13.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1443ba9acbb593fa7c1c29e011d7c9761545fe35e7652e85ce7f51a16f7e08d", size = 249585, upload-time = "2025-12-28T15:40:17.226Z" }, + { url = "https://files.pythonhosted.org/packages/f4/15/e1daff723f9f5959acb63cbe35b11203a9df77ee4b95b45fffd38b318390/coverage-7.13.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c832ec92c4499ac463186af72f9ed4d8daec15499b16f0a879b0d1c8e5cf4a3b", size = 246597, upload-time = "2025-12-28T15:40:19.028Z" }, + { url = "https://files.pythonhosted.org/packages/74/a6/1efd31c5433743a6ddbc9d37ac30c196bb07c7eab3d74fbb99b924c93174/coverage-7.13.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:562ec27dfa3f311e0db1ba243ec6e5f6ab96b1edfcfc6cf86f28038bc4961ce6", size = 247626, upload-time = "2025-12-28T15:40:20.846Z" }, + { url = "https://files.pythonhosted.org/packages/6d/9f/1609267dd3e749f57fdd66ca6752567d1c13b58a20a809dc409b263d0b5f/coverage-7.13.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4de84e71173d4dada2897e5a0e1b7877e5eefbfe0d6a44edee6ce31d9b8ec09e", size = 245629, upload-time = "2025-12-28T15:40:22.397Z" }, + { url = "https://files.pythonhosted.org/packages/e2/f6/6815a220d5ec2466383d7cc36131b9fa6ecbe95c50ec52a631ba733f306a/coverage-7.13.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:a5a68357f686f8c4d527a2dc04f52e669c2fc1cbde38f6f7eb6a0e58cbd17cae", size = 245901, upload-time = "2025-12-28T15:40:23.836Z" }, + { url = "https://files.pythonhosted.org/packages/ac/58/40576554cd12e0872faf6d2c0eb3bc85f71d78427946ddd19ad65201e2c0/coverage-7.13.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:77cc258aeb29a3417062758975521eae60af6f79e930d6993555eeac6a8eac29", size = 246505, upload-time = "2025-12-28T15:40:25.421Z" }, + { url = "https://files.pythonhosted.org/packages/3b/77/9233a90253fba576b0eee81707b5781d0e21d97478e5377b226c5b096c0f/coverage-7.13.1-cp310-cp310-win32.whl", hash = "sha256:bb4f8c3c9a9f34423dba193f241f617b08ffc63e27f67159f60ae6baf2dcfe0f", size = 221257, upload-time = "2025-12-28T15:40:27.217Z" }, + { url = "https://files.pythonhosted.org/packages/e0/43/e842ff30c1a0a623ec80db89befb84a3a7aad7bfe44a6ea77d5a3e61fedd/coverage-7.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:c8e2706ceb622bc63bac98ebb10ef5da80ed70fbd8a7999a5076de3afaef0fb1", size = 222191, upload-time = "2025-12-28T15:40:28.916Z" }, + { url = "https://files.pythonhosted.org/packages/b4/9b/77baf488516e9ced25fc215a6f75d803493fc3f6a1a1227ac35697910c2a/coverage-7.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a55d509a1dc5a5b708b5dad3b5334e07a16ad4c2185e27b40e4dba796ab7f88", size = 218755, upload-time = "2025-12-28T15:40:30.812Z" }, + { url = "https://files.pythonhosted.org/packages/d7/cd/7ab01154e6eb79ee2fab76bf4d89e94c6648116557307ee4ebbb85e5c1bf/coverage-7.13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4d010d080c4888371033baab27e47c9df7d6fb28d0b7b7adf85a4a49be9298b3", size = 219257, upload-time = "2025-12-28T15:40:32.333Z" }, + { url = "https://files.pythonhosted.org/packages/01/d5/b11ef7863ffbbdb509da0023fad1e9eda1c0eaea61a6d2ea5b17d4ac706e/coverage-7.13.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d938b4a840fb1523b9dfbbb454f652967f18e197569c32266d4d13f37244c3d9", size = 249657, upload-time = "2025-12-28T15:40:34.1Z" }, + { url = "https://files.pythonhosted.org/packages/f7/7c/347280982982383621d29b8c544cf497ae07ac41e44b1ca4903024131f55/coverage-7.13.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bf100a3288f9bb7f919b87eb84f87101e197535b9bd0e2c2b5b3179633324fee", size = 251581, upload-time = "2025-12-28T15:40:36.131Z" }, + { url = "https://files.pythonhosted.org/packages/82/f6/ebcfed11036ade4c0d75fa4453a6282bdd225bc073862766eec184a4c643/coverage-7.13.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef6688db9bf91ba111ae734ba6ef1a063304a881749726e0d3575f5c10a9facf", size = 253691, upload-time = "2025-12-28T15:40:37.626Z" }, + { url = "https://files.pythonhosted.org/packages/02/92/af8f5582787f5d1a8b130b2dcba785fa5e9a7a8e121a0bb2220a6fdbdb8a/coverage-7.13.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0b609fc9cdbd1f02e51f67f51e5aee60a841ef58a68d00d5ee2c0faf357481a3", size = 249799, upload-time = "2025-12-28T15:40:39.47Z" }, + { url = "https://files.pythonhosted.org/packages/24/aa/0e39a2a3b16eebf7f193863323edbff38b6daba711abaaf807d4290cf61a/coverage-7.13.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c43257717611ff5e9a1d79dce8e47566235ebda63328718d9b65dd640bc832ef", size = 251389, upload-time = "2025-12-28T15:40:40.954Z" }, + { url = "https://files.pythonhosted.org/packages/73/46/7f0c13111154dc5b978900c0ccee2e2ca239b910890e674a77f1363d483e/coverage-7.13.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e09fbecc007f7b6afdfb3b07ce5bd9f8494b6856dd4f577d26c66c391b829851", size = 249450, upload-time = "2025-12-28T15:40:42.489Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ca/e80da6769e8b669ec3695598c58eef7ad98b0e26e66333996aee6316db23/coverage-7.13.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:a03a4f3a19a189919c7055098790285cc5c5b0b3976f8d227aea39dbf9f8bfdb", size = 249170, upload-time = "2025-12-28T15:40:44.279Z" }, + { url = "https://files.pythonhosted.org/packages/af/18/9e29baabdec1a8644157f572541079b4658199cfd372a578f84228e860de/coverage-7.13.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3820778ea1387c2b6a818caec01c63adc5b3750211af6447e8dcfb9b6f08dbba", size = 250081, upload-time = "2025-12-28T15:40:45.748Z" }, + { url = "https://files.pythonhosted.org/packages/00/f8/c3021625a71c3b2f516464d322e41636aea381018319050a8114105872ee/coverage-7.13.1-cp311-cp311-win32.whl", hash = "sha256:ff10896fa55167371960c5908150b434b71c876dfab97b69478f22c8b445ea19", size = 221281, upload-time = "2025-12-28T15:40:47.232Z" }, + { url = "https://files.pythonhosted.org/packages/27/56/c216625f453df6e0559ed666d246fcbaaa93f3aa99eaa5080cea1229aa3d/coverage-7.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:a998cc0aeeea4c6d5622a3754da5a493055d2d95186bad877b0a34ea6e6dbe0a", size = 222215, upload-time = "2025-12-28T15:40:49.19Z" }, + { url = "https://files.pythonhosted.org/packages/5c/9a/be342e76f6e531cae6406dc46af0d350586f24d9b67fdfa6daee02df71af/coverage-7.13.1-cp311-cp311-win_arm64.whl", hash = "sha256:fea07c1a39a22614acb762e3fbbb4011f65eedafcb2948feeef641ac78b4ee5c", size = 220886, upload-time = "2025-12-28T15:40:51.067Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8a/87af46cccdfa78f53db747b09f5f9a21d5fc38d796834adac09b30a8ce74/coverage-7.13.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6f34591000f06e62085b1865c9bc5f7858df748834662a51edadfd2c3bfe0dd3", size = 218927, upload-time = "2025-12-28T15:40:52.814Z" }, + { url = "https://files.pythonhosted.org/packages/82/a8/6e22fdc67242a4a5a153f9438d05944553121c8f4ba70cb072af4c41362e/coverage-7.13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b67e47c5595b9224599016e333f5ec25392597a89d5744658f837d204e16c63e", size = 219288, upload-time = "2025-12-28T15:40:54.262Z" }, + { url = "https://files.pythonhosted.org/packages/d0/0a/853a76e03b0f7c4375e2ca025df45c918beb367f3e20a0a8e91967f6e96c/coverage-7.13.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e7b8bd70c48ffb28461ebe092c2345536fb18bbbf19d287c8913699735f505c", size = 250786, upload-time = "2025-12-28T15:40:56.059Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b4/694159c15c52b9f7ec7adf49d50e5f8ee71d3e9ef38adb4445d13dd56c20/coverage-7.13.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c223d078112e90dc0e5c4e35b98b9584164bea9fbbd221c0b21c5241f6d51b62", size = 253543, upload-time = "2025-12-28T15:40:57.585Z" }, + { url = "https://files.pythonhosted.org/packages/96/b2/7f1f0437a5c855f87e17cf5d0dc35920b6440ff2b58b1ba9788c059c26c8/coverage-7.13.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:794f7c05af0763b1bbd1b9e6eff0e52ad068be3b12cd96c87de037b01390c968", size = 254635, upload-time = "2025-12-28T15:40:59.443Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d1/73c3fdb8d7d3bddd9473c9c6a2e0682f09fc3dfbcb9c3f36412a7368bcab/coverage-7.13.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0642eae483cc8c2902e4af7298bf886d605e80f26382124cddc3967c2a3df09e", size = 251202, upload-time = "2025-12-28T15:41:01.328Z" }, + { url = "https://files.pythonhosted.org/packages/66/3c/f0edf75dcc152f145d5598329e864bbbe04ab78660fe3e8e395f9fff010f/coverage-7.13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f5e772ed5fef25b3de9f2008fe67b92d46831bd2bc5bdc5dd6bfd06b83b316f", size = 252566, upload-time = "2025-12-28T15:41:03.319Z" }, + { url = "https://files.pythonhosted.org/packages/17/b3/e64206d3c5f7dcbceafd14941345a754d3dbc78a823a6ed526e23b9cdaab/coverage-7.13.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:45980ea19277dc0a579e432aef6a504fe098ef3a9032ead15e446eb0f1191aee", size = 250711, upload-time = "2025-12-28T15:41:06.411Z" }, + { url = "https://files.pythonhosted.org/packages/dc/ad/28a3eb970a8ef5b479ee7f0c484a19c34e277479a5b70269dc652b730733/coverage-7.13.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f18eca6028ffa62adbd185a8f1e1dd242f2e68164dba5c2b74a5204850b4cf", size = 250278, upload-time = "2025-12-28T15:41:08.285Z" }, + { url = "https://files.pythonhosted.org/packages/54/e3/c8f0f1a93133e3e1291ca76cbb63565bd4b5c5df63b141f539d747fff348/coverage-7.13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8dca5590fec7a89ed6826fce625595279e586ead52e9e958d3237821fbc750c", size = 252154, upload-time = "2025-12-28T15:41:09.969Z" }, + { url = "https://files.pythonhosted.org/packages/d0/bf/9939c5d6859c380e405b19e736321f1c7d402728792f4c752ad1adcce005/coverage-7.13.1-cp312-cp312-win32.whl", hash = "sha256:ff86d4e85188bba72cfb876df3e11fa243439882c55957184af44a35bd5880b7", size = 221487, upload-time = "2025-12-28T15:41:11.468Z" }, + { url = "https://files.pythonhosted.org/packages/fa/dc/7282856a407c621c2aad74021680a01b23010bb8ebf427cf5eacda2e876f/coverage-7.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:16cc1da46c04fb0fb128b4dc430b78fa2aba8a6c0c9f8eb391fd5103409a6ac6", size = 222299, upload-time = "2025-12-28T15:41:13.386Z" }, + { url = "https://files.pythonhosted.org/packages/10/79/176a11203412c350b3e9578620013af35bcdb79b651eb976f4a4b32044fa/coverage-7.13.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d9bc218650022a768f3775dd7fdac1886437325d8d295d923ebcfef4892ad5c", size = 220941, upload-time = "2025-12-28T15:41:14.975Z" }, + { url = "https://files.pythonhosted.org/packages/a3/a4/e98e689347a1ff1a7f67932ab535cef82eb5e78f32a9e4132e114bbb3a0a/coverage-7.13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cb237bfd0ef4d5eb6a19e29f9e528ac67ac3be932ea6b44fb6cc09b9f3ecff78", size = 218951, upload-time = "2025-12-28T15:41:16.653Z" }, + { url = "https://files.pythonhosted.org/packages/32/33/7cbfe2bdc6e2f03d6b240d23dc45fdaf3fd270aaf2d640be77b7f16989ab/coverage-7.13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1dcb645d7e34dcbcc96cd7c132b1fc55c39263ca62eb961c064eb3928997363b", size = 219325, upload-time = "2025-12-28T15:41:18.609Z" }, + { url = "https://files.pythonhosted.org/packages/59/f6/efdabdb4929487baeb7cb2a9f7dac457d9356f6ad1b255be283d58b16316/coverage-7.13.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3d42df8201e00384736f0df9be2ced39324c3907607d17d50d50116c989d84cd", size = 250309, upload-time = "2025-12-28T15:41:20.629Z" }, + { url = "https://files.pythonhosted.org/packages/12/da/91a52516e9d5aea87d32d1523f9cdcf7a35a3b298e6be05d6509ba3cfab2/coverage-7.13.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa3edde1aa8807de1d05934982416cb3ec46d1d4d91e280bcce7cca01c507992", size = 252907, upload-time = "2025-12-28T15:41:22.257Z" }, + { url = "https://files.pythonhosted.org/packages/75/38/f1ea837e3dc1231e086db1638947e00d264e7e8c41aa8ecacf6e1e0c05f4/coverage-7.13.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9edd0e01a343766add6817bc448408858ba6b489039eaaa2018474e4001651a4", size = 254148, upload-time = "2025-12-28T15:41:23.87Z" }, + { url = "https://files.pythonhosted.org/packages/7f/43/f4f16b881aaa34954ba446318dea6b9ed5405dd725dd8daac2358eda869a/coverage-7.13.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:985b7836931d033570b94c94713c6dba5f9d3ff26045f72c3e5dbc5fe3361e5a", size = 250515, upload-time = "2025-12-28T15:41:25.437Z" }, + { url = "https://files.pythonhosted.org/packages/84/34/8cba7f00078bd468ea914134e0144263194ce849ec3baad187ffb6203d1c/coverage-7.13.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ffed1e4980889765c84a5d1a566159e363b71d6b6fbaf0bebc9d3c30bc016766", size = 252292, upload-time = "2025-12-28T15:41:28.459Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a4/cffac66c7652d84ee4ac52d3ccb94c015687d3b513f9db04bfcac2ac800d/coverage-7.13.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8842af7f175078456b8b17f1b73a0d16a65dcbdc653ecefeb00a56b3c8c298c4", size = 250242, upload-time = "2025-12-28T15:41:30.02Z" }, + { url = "https://files.pythonhosted.org/packages/f4/78/9a64d462263dde416f3c0067efade7b52b52796f489b1037a95b0dc389c9/coverage-7.13.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ccd7a6fca48ca9c131d9b0a2972a581e28b13416fc313fb98b6d24a03ce9a398", size = 250068, upload-time = "2025-12-28T15:41:32.007Z" }, + { url = "https://files.pythonhosted.org/packages/69/c8/a8994f5fece06db7c4a97c8fc1973684e178599b42e66280dded0524ef00/coverage-7.13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0403f647055de2609be776965108447deb8e384fe4a553c119e3ff6bfbab4784", size = 251846, upload-time = "2025-12-28T15:41:33.946Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f7/91fa73c4b80305c86598a2d4e54ba22df6bf7d0d97500944af7ef155d9f7/coverage-7.13.1-cp313-cp313-win32.whl", hash = "sha256:549d195116a1ba1e1ae2f5ca143f9777800f6636eab917d4f02b5310d6d73461", size = 221512, upload-time = "2025-12-28T15:41:35.519Z" }, + { url = "https://files.pythonhosted.org/packages/45/0b/0768b4231d5a044da8f75e097a8714ae1041246bb765d6b5563bab456735/coverage-7.13.1-cp313-cp313-win_amd64.whl", hash = "sha256:5899d28b5276f536fcf840b18b61a9fce23cc3aec1d114c44c07fe94ebeaa500", size = 222321, upload-time = "2025-12-28T15:41:37.371Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b8/bdcb7253b7e85157282450262008f1366aa04663f3e3e4c30436f596c3e2/coverage-7.13.1-cp313-cp313-win_arm64.whl", hash = "sha256:868a2fae76dfb06e87291bcbd4dcbcc778a8500510b618d50496e520bd94d9b9", size = 220949, upload-time = "2025-12-28T15:41:39.553Z" }, + { url = "https://files.pythonhosted.org/packages/70/52/f2be52cc445ff75ea8397948c96c1b4ee14f7f9086ea62fc929c5ae7b717/coverage-7.13.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:67170979de0dacac3f3097d02b0ad188d8edcea44ccc44aaa0550af49150c7dc", size = 219643, upload-time = "2025-12-28T15:41:41.567Z" }, + { url = "https://files.pythonhosted.org/packages/47/79/c85e378eaa239e2edec0c5523f71542c7793fe3340954eafb0bc3904d32d/coverage-7.13.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f80e2bb21bfab56ed7405c2d79d34b5dc0bc96c2c1d2a067b643a09fb756c43a", size = 219997, upload-time = "2025-12-28T15:41:43.418Z" }, + { url = "https://files.pythonhosted.org/packages/fe/9b/b1ade8bfb653c0bbce2d6d6e90cc6c254cbb99b7248531cc76253cb4da6d/coverage-7.13.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f83351e0f7dcdb14d7326c3d8d8c4e915fa685cbfdc6281f9470d97a04e9dfe4", size = 261296, upload-time = "2025-12-28T15:41:45.207Z" }, + { url = "https://files.pythonhosted.org/packages/1f/af/ebf91e3e1a2473d523e87e87fd8581e0aa08741b96265730e2d79ce78d8d/coverage-7.13.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb3f6562e89bad0110afbe64e485aac2462efdce6232cdec7862a095dc3412f6", size = 263363, upload-time = "2025-12-28T15:41:47.163Z" }, + { url = "https://files.pythonhosted.org/packages/c4/8b/fb2423526d446596624ac7fde12ea4262e66f86f5120114c3cfd0bb2befa/coverage-7.13.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77545b5dcda13b70f872c3b5974ac64c21d05e65b1590b441c8560115dc3a0d1", size = 265783, upload-time = "2025-12-28T15:41:49.03Z" }, + { url = "https://files.pythonhosted.org/packages/9b/26/ef2adb1e22674913b89f0fe7490ecadcef4a71fa96f5ced90c60ec358789/coverage-7.13.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4d240d260a1aed814790bbe1f10a5ff31ce6c21bc78f0da4a1e8268d6c80dbd", size = 260508, upload-time = "2025-12-28T15:41:51.035Z" }, + { url = "https://files.pythonhosted.org/packages/ce/7d/f0f59b3404caf662e7b5346247883887687c074ce67ba453ea08c612b1d5/coverage-7.13.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d2287ac9360dec3837bfdad969963a5d073a09a85d898bd86bea82aa8876ef3c", size = 263357, upload-time = "2025-12-28T15:41:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b1/29896492b0b1a047604d35d6fa804f12818fa30cdad660763a5f3159e158/coverage-7.13.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0d2c11f3ea4db66b5cbded23b20185c35066892c67d80ec4be4bab257b9ad1e0", size = 260978, upload-time = "2025-12-28T15:41:54.589Z" }, + { url = "https://files.pythonhosted.org/packages/48/f2/971de1238a62e6f0a4128d37adadc8bb882ee96afbe03ff1570291754629/coverage-7.13.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:3fc6a169517ca0d7ca6846c3c5392ef2b9e38896f61d615cb75b9e7134d4ee1e", size = 259877, upload-time = "2025-12-28T15:41:56.263Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fc/0474efcbb590ff8628830e9aaec5f1831594874360e3251f1fdec31d07a3/coverage-7.13.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d10a2ed46386e850bb3de503a54f9fe8192e5917fcbb143bfef653a9355e9a53", size = 262069, upload-time = "2025-12-28T15:41:58.093Z" }, + { url = "https://files.pythonhosted.org/packages/88/4f/3c159b7953db37a7b44c0eab8a95c37d1aa4257c47b4602c04022d5cb975/coverage-7.13.1-cp313-cp313t-win32.whl", hash = "sha256:75a6f4aa904301dab8022397a22c0039edc1f51e90b83dbd4464b8a38dc87842", size = 222184, upload-time = "2025-12-28T15:41:59.763Z" }, + { url = "https://files.pythonhosted.org/packages/58/a5/6b57d28f81417f9335774f20679d9d13b9a8fb90cd6160957aa3b54a2379/coverage-7.13.1-cp313-cp313t-win_amd64.whl", hash = "sha256:309ef5706e95e62578cda256b97f5e097916a2c26247c287bbe74794e7150df2", size = 223250, upload-time = "2025-12-28T15:42:01.52Z" }, + { url = "https://files.pythonhosted.org/packages/81/7c/160796f3b035acfbb58be80e02e484548595aa67e16a6345e7910ace0a38/coverage-7.13.1-cp313-cp313t-win_arm64.whl", hash = "sha256:92f980729e79b5d16d221038dbf2e8f9a9136afa072f9d5d6ed4cb984b126a09", size = 221521, upload-time = "2025-12-28T15:42:03.275Z" }, + { url = "https://files.pythonhosted.org/packages/aa/8e/ba0e597560c6563fc0adb902fda6526df5d4aa73bb10adf0574d03bd2206/coverage-7.13.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:97ab3647280d458a1f9adb85244e81587505a43c0c7cff851f5116cd2814b894", size = 218996, upload-time = "2025-12-28T15:42:04.978Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8e/764c6e116f4221dc7aa26c4061181ff92edb9c799adae6433d18eeba7a14/coverage-7.13.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8f572d989142e0908e6acf57ad1b9b86989ff057c006d13b76c146ec6a20216a", size = 219326, upload-time = "2025-12-28T15:42:06.691Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a6/6130dc6d8da28cdcbb0f2bf8865aeca9b157622f7c0031e48c6cf9a0e591/coverage-7.13.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d72140ccf8a147e94274024ff6fd8fb7811354cf7ef88b1f0a988ebaa5bc774f", size = 250374, upload-time = "2025-12-28T15:42:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/82/2b/783ded568f7cd6b677762f780ad338bf4b4750205860c17c25f7c708995e/coverage-7.13.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3c9f051b028810f5a87c88e5d6e9af3c0ff32ef62763bf15d29f740453ca909", size = 252882, upload-time = "2025-12-28T15:42:10.515Z" }, + { url = "https://files.pythonhosted.org/packages/cd/b2/9808766d082e6a4d59eb0cc881a57fc1600eb2c5882813eefff8254f71b5/coverage-7.13.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f398ba4df52d30b1763f62eed9de5620dcde96e6f491f4c62686736b155aa6e4", size = 254218, upload-time = "2025-12-28T15:42:12.208Z" }, + { url = "https://files.pythonhosted.org/packages/44/ea/52a985bb447c871cb4d2e376e401116520991b597c85afdde1ea9ef54f2c/coverage-7.13.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:132718176cc723026d201e347f800cd1a9e4b62ccd3f82476950834dad501c75", size = 250391, upload-time = "2025-12-28T15:42:14.21Z" }, + { url = "https://files.pythonhosted.org/packages/7f/1d/125b36cc12310718873cfc8209ecfbc1008f14f4f5fa0662aa608e579353/coverage-7.13.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e549d642426e3579b3f4b92d0431543b012dcb6e825c91619d4e93b7363c3f9", size = 252239, upload-time = "2025-12-28T15:42:16.292Z" }, + { url = "https://files.pythonhosted.org/packages/6a/16/10c1c164950cade470107f9f14bbac8485f8fb8515f515fca53d337e4a7f/coverage-7.13.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:90480b2134999301eea795b3a9dbf606c6fbab1b489150c501da84a959442465", size = 250196, upload-time = "2025-12-28T15:42:18.54Z" }, + { url = "https://files.pythonhosted.org/packages/2a/c6/cd860fac08780c6fd659732f6ced1b40b79c35977c1356344e44d72ba6c4/coverage-7.13.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e825dbb7f84dfa24663dd75835e7257f8882629fc11f03ecf77d84a75134b864", size = 250008, upload-time = "2025-12-28T15:42:20.365Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/a8c58d3d38f82a5711e1e0a67268362af48e1a03df27c03072ac30feefcf/coverage-7.13.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:623dcc6d7a7ba450bbdbeedbaa0c42b329bdae16491af2282f12a7e809be7eb9", size = 251671, upload-time = "2025-12-28T15:42:22.114Z" }, + { url = "https://files.pythonhosted.org/packages/f0/bc/fd4c1da651d037a1e3d53e8cb3f8182f4b53271ffa9a95a2e211bacc0349/coverage-7.13.1-cp314-cp314-win32.whl", hash = "sha256:6e73ebb44dca5f708dc871fe0b90cf4cff1a13f9956f747cc87b535a840386f5", size = 221777, upload-time = "2025-12-28T15:42:23.919Z" }, + { url = "https://files.pythonhosted.org/packages/4b/50/71acabdc8948464c17e90b5ffd92358579bd0910732c2a1c9537d7536aa6/coverage-7.13.1-cp314-cp314-win_amd64.whl", hash = "sha256:be753b225d159feb397bd0bf91ae86f689bad0da09d3b301478cd39b878ab31a", size = 222592, upload-time = "2025-12-28T15:42:25.619Z" }, + { url = "https://files.pythonhosted.org/packages/f7/c8/a6fb943081bb0cc926499c7907731a6dc9efc2cbdc76d738c0ab752f1a32/coverage-7.13.1-cp314-cp314-win_arm64.whl", hash = "sha256:228b90f613b25ba0019361e4ab81520b343b622fc657daf7e501c4ed6a2366c0", size = 221169, upload-time = "2025-12-28T15:42:27.629Z" }, + { url = "https://files.pythonhosted.org/packages/16/61/d5b7a0a0e0e40d62e59bc8c7aa1afbd86280d82728ba97f0673b746b78e2/coverage-7.13.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:60cfb538fe9ef86e5b2ab0ca8fc8d62524777f6c611dcaf76dc16fbe9b8e698a", size = 219730, upload-time = "2025-12-28T15:42:29.306Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2c/8881326445fd071bb49514d1ce97d18a46a980712b51fee84f9ab42845b4/coverage-7.13.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:57dfc8048c72ba48a8c45e188d811e5efd7e49b387effc8fb17e97936dde5bf6", size = 220001, upload-time = "2025-12-28T15:42:31.319Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d7/50de63af51dfa3a7f91cc37ad8fcc1e244b734232fbc8b9ab0f3c834a5cd/coverage-7.13.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3f2f725aa3e909b3c5fdb8192490bdd8e1495e85906af74fe6e34a2a77ba0673", size = 261370, upload-time = "2025-12-28T15:42:32.992Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2c/d31722f0ec918fd7453b2758312729f645978d212b410cd0f7c2aed88a94/coverage-7.13.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ee68b21909686eeb21dfcba2c3b81fee70dcf38b140dcd5aa70680995fa3aa5", size = 263485, upload-time = "2025-12-28T15:42:34.759Z" }, + { url = "https://files.pythonhosted.org/packages/fa/7a/2c114fa5c5fc08ba0777e4aec4c97e0b4a1afcb69c75f1f54cff78b073ab/coverage-7.13.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:724b1b270cb13ea2e6503476e34541a0b1f62280bc997eab443f87790202033d", size = 265890, upload-time = "2025-12-28T15:42:36.517Z" }, + { url = "https://files.pythonhosted.org/packages/65/d9/f0794aa1c74ceabc780fe17f6c338456bbc4e96bd950f2e969f48ac6fb20/coverage-7.13.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:916abf1ac5cf7eb16bc540a5bf75c71c43a676f5c52fcb9fe75a2bd75fb944e8", size = 260445, upload-time = "2025-12-28T15:42:38.646Z" }, + { url = "https://files.pythonhosted.org/packages/49/23/184b22a00d9bb97488863ced9454068c79e413cb23f472da6cbddc6cfc52/coverage-7.13.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:776483fd35b58d8afe3acbd9988d5de592ab6da2d2a865edfdbc9fdb43e7c486", size = 263357, upload-time = "2025-12-28T15:42:40.788Z" }, + { url = "https://files.pythonhosted.org/packages/7d/bd/58af54c0c9199ea4190284f389005779d7daf7bf3ce40dcd2d2b2f96da69/coverage-7.13.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b6f3b96617e9852703f5b633ea01315ca45c77e879584f283c44127f0f1ec564", size = 260959, upload-time = "2025-12-28T15:42:42.808Z" }, + { url = "https://files.pythonhosted.org/packages/4b/2a/6839294e8f78a4891bf1df79d69c536880ba2f970d0ff09e7513d6e352e9/coverage-7.13.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd63e7b74661fed317212fab774e2a648bc4bb09b35f25474f8e3325d2945cd7", size = 259792, upload-time = "2025-12-28T15:42:44.818Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c3/528674d4623283310ad676c5af7414b9850ab6d55c2300e8aa4b945ec554/coverage-7.13.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:933082f161bbb3e9f90d00990dc956120f608cdbcaeea15c4d897f56ef4fe416", size = 262123, upload-time = "2025-12-28T15:42:47.108Z" }, + { url = "https://files.pythonhosted.org/packages/06/c5/8c0515692fb4c73ac379d8dc09b18eaf0214ecb76ea6e62467ba7a1556ff/coverage-7.13.1-cp314-cp314t-win32.whl", hash = "sha256:18be793c4c87de2965e1c0f060f03d9e5aff66cfeae8e1dbe6e5b88056ec153f", size = 222562, upload-time = "2025-12-28T15:42:49.144Z" }, + { url = "https://files.pythonhosted.org/packages/05/0e/c0a0c4678cb30dac735811db529b321d7e1c9120b79bd728d4f4d6b010e9/coverage-7.13.1-cp314-cp314t-win_amd64.whl", hash = "sha256:0e42e0ec0cd3e0d851cb3c91f770c9301f48647cb2877cb78f74bdaa07639a79", size = 223670, upload-time = "2025-12-28T15:42:51.218Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5f/b177aa0011f354abf03a8f30a85032686d290fdeed4222b27d36b4372a50/coverage-7.13.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eaecf47ef10c72ece9a2a92118257da87e460e113b83cc0d2905cbbe931792b4", size = 221707, upload-time = "2025-12-28T15:42:53.034Z" }, + { url = "https://files.pythonhosted.org/packages/cc/48/d9f421cb8da5afaa1a64570d9989e00fb7955e6acddc5a12979f7666ef60/coverage-7.13.1-py3-none-any.whl", hash = "sha256:2016745cb3ba554469d02819d78958b571792bb68e31302610e898f80dd3a573", size = 210722, upload-time = "2025-12-28T15:42:54.901Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version >= '3.10' and python_full_version <= '3.11'" }, +] + +[[package]] +name = "cross-web" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/93/4f/bdb62e969649ee76d4741ef8eee34384ec2bc21cc66eb7fd244e6ad62be8/cross_web-0.4.0.tar.gz", hash = "sha256:4ae65619ddfcd06d6803432c0366342d7e8aeba10194b4e144d73a662e75370c", size = 157111, upload-time = "2025-12-25T20:45:21.989Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/d6/6c6a036655e5091b26b9f350dcf43821895325aa4727396b14c67679a957/cross_web-0.4.0-py3-none-any.whl", hash = "sha256:0c675bd26e91428cab31e3e927929b42da94aa96da92974e57c78f9a732d0e9b", size = 14200, upload-time = "2025-12-25T20:45:23.075Z" }, +] + +[[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", version = "1.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "tinycss2", version = "1.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { 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.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/99/133892f401ced5a27e641a473c547d5fbdb39af8f85dac8a9d633ea3e7a7/dirty_equals-0.9.0.tar.gz", hash = "sha256:17f515970b04ed7900b733c95fd8091f4f85e52f1fb5f268757f25c858eb1f7b", size = 50412, upload-time = "2025-01-11T23:23:40.491Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/0c/03cc99bf3b6328604b10829de3460f2b2ad3373200c45665c38508e550c6/dirty_equals-0.9.0-py3-none-any.whl", hash = "sha256:ff4d027f5cfa1b69573af00f7ba9043ea652dbdce3fe5cbe828e478c7346db9c", size = 28226, upload-time = "2025-01-11T23:23:37.489Z" }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + +[[package]] +name = "dnspython" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/b5/4a/263763cb2ba3816dd94b08ad3a33d5fdae34ecb856678773cc40a3605829/dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1", size = 345197, upload-time = "2024-10-05T20:14:59.362Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/1b/e0a87d256e40e8c888847551b20a017a6b98139178505dc7ffb96f04e954/dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86", size = 313632, upload-time = "2024-10-05T20:14:57.687Z" }, +] + +[[package]] +name = "dnspython" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.10' and python_full_version < '3.14'", +] +sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, +] + +[[package]] +name = "docstring-parser" +version = "0.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442, upload-time = "2025-07-21T07:35:01.868Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" }, +] + +[[package]] +name = "email-validator" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython", version = "2.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "dnspython", version = "2.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238, upload-time = "2025-08-26T13:09:06.831Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, +] + +[[package]] +name = "eval-type-backport" +version = "0.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/a3/cafafb4558fd638aadfe4121dc6cefb8d743368c085acb2f521df0f3d9d7/eval_type_backport-0.3.1.tar.gz", hash = "sha256:57e993f7b5b69d271e37482e62f74e76a0276c82490cf8e4f0dffeb6b332d5ed", size = 9445, upload-time = "2025-12-02T11:51:42.987Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/22/fdc2e30d43ff853720042fa15baa3e6122722be1a7950a98233ebb55cd71/eval_type_backport-0.3.1-py3-none-any.whl", hash = "sha256:279ab641905e9f11129f56a8a78f493518515b83402b860f6f06dd7c011fdfa8", size = 6063, upload-time = "2025-12-02T11:51:41.665Z" }, +] + +[[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 = "executing" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, +] + +[[package]] +name = "fastapi" +source = { editable = "." } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette", version = "0.49.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "starlette", version = "0.50.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions" }, +] + +[package.optional-dependencies] +all = [ + { name = "email-validator" }, + { name = "fastapi-cli", extra = ["standard"] }, + { name = "httpx" }, + { name = "itsdangerous" }, + { name = "jinja2" }, + { name = "orjson" }, + { name = "pydantic-extra-types" }, + { name = "pydantic-settings", version = "2.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pydantic-settings", version = "2.12.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "python-multipart", version = "0.0.20", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "python-multipart", version = "0.0.21", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pyyaml" }, + { name = "ujson" }, + { name = "uvicorn", version = "0.39.0", source = { registry = "https://pypi.org/simple" }, extra = ["standard"], marker = "python_full_version < '3.10'" }, + { name = "uvicorn", version = "0.40.0", source = { registry = "https://pypi.org/simple" }, extra = ["standard"], marker = "python_full_version >= '3.10'" }, +] +standard = [ + { name = "email-validator" }, + { name = "fastapi-cli", extra = ["standard"] }, + { name = "httpx" }, + { name = "jinja2" }, + { name = "pydantic-extra-types" }, + { name = "pydantic-settings", version = "2.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pydantic-settings", version = "2.12.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "python-multipart", version = "0.0.20", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "python-multipart", version = "0.0.21", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "uvicorn", version = "0.39.0", source = { registry = "https://pypi.org/simple" }, extra = ["standard"], marker = "python_full_version < '3.10'" }, + { name = "uvicorn", version = "0.40.0", source = { registry = "https://pypi.org/simple" }, extra = ["standard"], marker = "python_full_version >= '3.10'" }, +] +standard-no-fastapi-cloud-cli = [ + { name = "email-validator" }, + { name = "fastapi-cli", extra = ["standard-no-fastapi-cloud-cli"] }, + { name = "httpx" }, + { name = "jinja2" }, + { name = "pydantic-extra-types" }, + { name = "pydantic-settings", version = "2.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pydantic-settings", version = "2.12.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "python-multipart", version = "0.0.20", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "python-multipart", version = "0.0.21", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "uvicorn", version = "0.39.0", source = { registry = "https://pypi.org/simple" }, extra = ["standard"], marker = "python_full_version < '3.10'" }, + { name = "uvicorn", version = "0.40.0", source = { registry = "https://pypi.org/simple" }, extra = ["standard"], marker = "python_full_version >= '3.10'" }, +] + +[package.dev-dependencies] +dev = [ + { name = "anyio", extra = ["trio"] }, + { name = "black" }, + { name = "cairosvg" }, + { name = "coverage", version = "7.10.7", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version < '3.10'" }, + { name = "coverage", version = "7.13.1", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version >= '3.10'" }, + { name = "dirty-equals" }, + { name = "flask" }, + { name = "gitpython" }, + { name = "griffe-typingdoc" }, + { name = "griffe-warnings-deprecated" }, + { name = "httpx" }, + { name = "inline-snapshot" }, + { name = "jieba" }, + { name = "markdown-include-variants" }, + { name = "mdx-include" }, + { name = "mkdocs-macros-plugin" }, + { name = "mkdocs-material" }, + { name = "mkdocs-redirects" }, + { name = "mkdocstrings", extra = ["python"] }, + { name = "mypy" }, + { name = "pillow" }, + { name = "playwright" }, + { name = "prek" }, + { name = "pwdlib", version = "0.2.1", source = { registry = "https://pypi.org/simple" }, extra = ["argon2"], marker = "python_full_version < '3.10'" }, + { name = "pwdlib", version = "0.3.0", source = { registry = "https://pypi.org/simple" }, extra = ["argon2"], marker = "python_full_version >= '3.10'" }, + { name = "pydantic-ai" }, + { name = "pygithub" }, + { name = "pyjwt" }, + { name = "pytest" }, + { name = "pytest-codspeed" }, + { name = "python-slugify" }, + { name = "pyyaml" }, + { name = "ruff" }, + { name = "sqlmodel" }, + { name = "strawberry-graphql", version = "0.283.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "strawberry-graphql", version = "0.288.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "typer" }, + { name = "types-orjson" }, + { name = "types-ujson" }, +] +docs = [ + { name = "black" }, + { name = "cairosvg" }, + { name = "griffe-typingdoc" }, + { name = "griffe-warnings-deprecated" }, + { name = "httpx" }, + { name = "jieba" }, + { name = "markdown-include-variants" }, + { name = "mdx-include" }, + { name = "mkdocs-macros-plugin" }, + { name = "mkdocs-material" }, + { name = "mkdocs-redirects" }, + { name = "mkdocstrings", extra = ["python"] }, + { name = "pillow" }, + { name = "python-slugify" }, + { name = "pyyaml" }, + { name = "ruff" }, + { name = "typer" }, +] +docs-tests = [ + { name = "httpx" }, + { name = "ruff" }, +] +github-actions = [ + { name = "httpx" }, + { name = "pydantic" }, + { name = "pydantic-settings", version = "2.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pydantic-settings", version = "2.12.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pygithub" }, + { name = "pyyaml" }, + { name = "smokeshow" }, +] +tests = [ + { name = "anyio", extra = ["trio"] }, + { name = "coverage", version = "7.10.7", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version < '3.10'" }, + { name = "coverage", version = "7.13.1", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version >= '3.10'" }, + { name = "dirty-equals" }, + { name = "flask" }, + { name = "httpx" }, + { name = "inline-snapshot" }, + { name = "mypy" }, + { name = "pwdlib", version = "0.2.1", source = { registry = "https://pypi.org/simple" }, extra = ["argon2"], marker = "python_full_version < '3.10'" }, + { name = "pwdlib", version = "0.3.0", source = { registry = "https://pypi.org/simple" }, extra = ["argon2"], marker = "python_full_version >= '3.10'" }, + { name = "pyjwt" }, + { name = "pytest" }, + { name = "pytest-codspeed" }, + { name = "pyyaml" }, + { name = "ruff" }, + { name = "sqlmodel" }, + { name = "strawberry-graphql", version = "0.283.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "strawberry-graphql", version = "0.288.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "types-orjson" }, + { name = "types-ujson" }, +] +translations = [ + { name = "gitpython" }, + { name = "pydantic-ai" }, + { name = "pygithub" }, +] + +[package.metadata] +requires-dist = [ + { name = "annotated-doc", specifier = ">=0.0.2" }, + { name = "email-validator", marker = "extra == 'all'", specifier = ">=2.0.0" }, + { name = "email-validator", marker = "extra == 'standard'", specifier = ">=2.0.0" }, + { name = "email-validator", marker = "extra == 'standard-no-fastapi-cloud-cli'", specifier = ">=2.0.0" }, + { name = "fastapi-cli", extras = ["standard"], marker = "extra == 'all'", specifier = ">=0.0.8" }, + { name = "fastapi-cli", extras = ["standard"], marker = "extra == 'standard'", specifier = ">=0.0.8" }, + { name = "fastapi-cli", extras = ["standard-no-fastapi-cloud-cli"], marker = "extra == 'standard-no-fastapi-cloud-cli'", specifier = ">=0.0.8" }, + { name = "httpx", marker = "extra == 'all'", specifier = ">=0.23.0,<1.0.0" }, + { name = "httpx", marker = "extra == 'standard'", specifier = ">=0.23.0,<1.0.0" }, + { name = "httpx", marker = "extra == 'standard-no-fastapi-cloud-cli'", specifier = ">=0.23.0,<1.0.0" }, + { name = "itsdangerous", marker = "extra == 'all'", specifier = ">=1.1.0" }, + { name = "jinja2", marker = "extra == 'all'", specifier = ">=3.1.5" }, + { name = "jinja2", marker = "extra == 'standard'", specifier = ">=3.1.5" }, + { name = "jinja2", marker = "extra == 'standard-no-fastapi-cloud-cli'", specifier = ">=3.1.5" }, + { name = "orjson", marker = "extra == 'all'", specifier = ">=3.2.1" }, + { name = "pydantic", specifier = ">=2.7.0" }, + { name = "pydantic-extra-types", marker = "extra == 'all'", specifier = ">=2.0.0" }, + { name = "pydantic-extra-types", marker = "extra == 'standard'", specifier = ">=2.0.0" }, + { name = "pydantic-extra-types", marker = "extra == 'standard-no-fastapi-cloud-cli'", specifier = ">=2.0.0" }, + { name = "pydantic-settings", marker = "extra == 'all'", specifier = ">=2.0.0" }, + { name = "pydantic-settings", marker = "extra == 'standard'", specifier = ">=2.0.0" }, + { name = "pydantic-settings", marker = "extra == 'standard-no-fastapi-cloud-cli'", specifier = ">=2.0.0" }, + { name = "python-multipart", marker = "extra == 'all'", specifier = ">=0.0.18" }, + { name = "python-multipart", marker = "extra == 'standard'", specifier = ">=0.0.18" }, + { name = "python-multipart", marker = "extra == 'standard-no-fastapi-cloud-cli'", specifier = ">=0.0.18" }, + { name = "pyyaml", marker = "extra == 'all'", specifier = ">=5.3.1" }, + { name = "starlette", specifier = ">=0.40.0,<0.51.0" }, + { name = "typing-extensions", specifier = ">=4.8.0" }, + { name = "ujson", marker = "extra == 'all'", specifier = ">=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0" }, + { name = "uvicorn", extras = ["standard"], marker = "extra == 'all'", specifier = ">=0.12.0" }, + { name = "uvicorn", extras = ["standard"], marker = "extra == 'standard'", specifier = ">=0.12.0" }, + { name = "uvicorn", extras = ["standard"], marker = "extra == 'standard-no-fastapi-cloud-cli'", specifier = ">=0.12.0" }, +] +provides-extras = ["standard", "standard-no-fastapi-cloud-cli", "all"] + +[package.metadata.requires-dev] +dev = [ + { name = "anyio", extras = ["trio"], specifier = ">=3.2.1,<5.0.0" }, + { name = "black", specifier = "==25.1.0" }, + { name = "cairosvg", specifier = "==2.8.2" }, + { name = "coverage", extras = ["toml"], specifier = ">=6.5.0,<8.0" }, + { name = "dirty-equals", specifier = "==0.9.0" }, + { name = "flask", specifier = ">=1.1.2,<4.0.0" }, + { name = "gitpython", specifier = "==3.1.45" }, + { name = "griffe-typingdoc", specifier = "==0.3.0" }, + { name = "griffe-warnings-deprecated", specifier = "==1.1.0" }, + { name = "httpx", specifier = ">=0.23.0,<1.0.0" }, + { name = "inline-snapshot", specifier = ">=0.21.1" }, + { name = "jieba", specifier = "==0.42.1" }, + { name = "markdown-include-variants", specifier = "==0.0.8" }, + { name = "mdx-include", specifier = ">=1.4.1,<2.0.0" }, + { name = "mkdocs-macros-plugin", specifier = "==1.4.1" }, + { name = "mkdocs-material", specifier = "==9.7.0" }, + { name = "mkdocs-redirects", specifier = ">=1.2.1,<1.3.0" }, + { name = "mkdocstrings", extras = ["python"], specifier = "==0.30.1" }, + { name = "mypy", specifier = "==1.14.1" }, + { name = "pillow", specifier = "==11.3.0" }, + { name = "playwright", specifier = ">=1.57.0" }, + { name = "prek", specifier = "==0.2.22" }, + { name = "pwdlib", extras = ["argon2"], specifier = ">=0.2.1" }, + { name = "pydantic-ai", specifier = "==0.4.10" }, + { name = "pygithub", specifier = "==2.8.1" }, + { name = "pyjwt", specifier = "==2.9.0" }, + { name = "pytest", specifier = ">=7.1.3,<9.0.0" }, + { name = "pytest-codspeed", specifier = "==4.2.0" }, + { name = "python-slugify", specifier = "==8.0.4" }, + { name = "pyyaml", specifier = ">=5.3.1,<7.0.0" }, + { name = "ruff", specifier = "==0.14.3" }, + { name = "sqlmodel", specifier = "==0.0.27" }, + { name = "strawberry-graphql", specifier = ">=0.200.0,<1.0.0" }, + { name = "typer", specifier = "==0.16.0" }, + { name = "types-orjson", specifier = "==3.6.2" }, + { name = "types-ujson", specifier = "==5.10.0.20240515" }, +] +docs = [ + { name = "black", specifier = "==25.1.0" }, + { name = "cairosvg", specifier = "==2.8.2" }, + { name = "griffe-typingdoc", specifier = "==0.3.0" }, + { name = "griffe-warnings-deprecated", specifier = "==1.1.0" }, + { name = "httpx", specifier = ">=0.23.0,<1.0.0" }, + { name = "jieba", specifier = "==0.42.1" }, + { name = "markdown-include-variants", specifier = "==0.0.8" }, + { name = "mdx-include", specifier = ">=1.4.1,<2.0.0" }, + { name = "mkdocs-macros-plugin", specifier = "==1.4.1" }, + { name = "mkdocs-material", specifier = "==9.7.0" }, + { name = "mkdocs-redirects", specifier = ">=1.2.1,<1.3.0" }, + { name = "mkdocstrings", extras = ["python"], specifier = "==0.30.1" }, + { name = "pillow", specifier = "==11.3.0" }, + { name = "python-slugify", specifier = "==8.0.4" }, + { name = "pyyaml", specifier = ">=5.3.1,<7.0.0" }, + { name = "ruff", specifier = "==0.14.3" }, + { name = "typer", specifier = "==0.16.0" }, +] +docs-tests = [ + { name = "httpx", specifier = ">=0.23.0,<1.0.0" }, + { name = "ruff", specifier = "==0.14.3" }, +] +github-actions = [ + { name = "httpx", specifier = ">=0.27.0,<1.0.0" }, + { name = "pydantic", specifier = ">=2.5.3,<3.0.0" }, + { name = "pydantic-settings", specifier = ">=2.1.0,<3.0.0" }, + { name = "pygithub", specifier = ">=2.3.0,<3.0.0" }, + { name = "pyyaml", specifier = ">=5.3.1,<7.0.0" }, + { name = "smokeshow", specifier = ">=0.5.0" }, +] +tests = [ + { name = "anyio", extras = ["trio"], specifier = ">=3.2.1,<5.0.0" }, + { name = "coverage", extras = ["toml"], specifier = ">=6.5.0,<8.0" }, + { name = "dirty-equals", specifier = "==0.9.0" }, + { name = "flask", specifier = ">=1.1.2,<4.0.0" }, + { name = "httpx", specifier = ">=0.23.0,<1.0.0" }, + { name = "inline-snapshot", specifier = ">=0.21.1" }, + { name = "mypy", specifier = "==1.14.1" }, + { name = "pwdlib", extras = ["argon2"], specifier = ">=0.2.1" }, + { name = "pyjwt", specifier = "==2.9.0" }, + { name = "pytest", specifier = ">=7.1.3,<9.0.0" }, + { name = "pytest-codspeed", specifier = "==4.2.0" }, + { name = "pyyaml", specifier = ">=5.3.1,<7.0.0" }, + { name = "ruff", specifier = "==0.14.3" }, + { name = "sqlmodel", specifier = "==0.0.27" }, + { name = "strawberry-graphql", specifier = ">=0.200.0,<1.0.0" }, + { name = "types-orjson", specifier = "==3.6.2" }, + { name = "types-ujson", specifier = "==5.10.0.20240515" }, +] +translations = [ + { name = "gitpython", specifier = "==3.1.45" }, + { name = "pydantic-ai", specifier = "==0.4.10" }, + { name = "pygithub", specifier = "==2.8.1" }, +] + +[[package]] +name = "fastapi-cli" +version = "0.0.20" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "rich-toolkit" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typer" }, + { name = "uvicorn", version = "0.39.0", source = { registry = "https://pypi.org/simple" }, extra = ["standard"], marker = "python_full_version < '3.10'" }, + { name = "uvicorn", version = "0.40.0", source = { registry = "https://pypi.org/simple" }, extra = ["standard"], marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d3/ca/d90fb3bfbcbd6e56c77afd9d114dd6ce8955d8bb90094399d1c70e659e40/fastapi_cli-0.0.20.tar.gz", hash = "sha256:d17c2634f7b96b6b560bc16b0035ed047d523c912011395f49f00a421692bc3a", size = 19786, upload-time = "2025-12-22T17:13:33.794Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/89/5c4eef60524d0fd704eb0706885b82cd5623a43396b94e4a5b17d3a3f516/fastapi_cli-0.0.20-py3-none-any.whl", hash = "sha256:e58b6a0038c0b1532b7a0af690656093dee666201b6b19d3c87175b358e9f783", size = 12390, upload-time = "2025-12-22T17:13:31.708Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "fastapi-cloud-cli" }, + { name = "uvicorn", version = "0.39.0", source = { registry = "https://pypi.org/simple" }, extra = ["standard"], marker = "python_full_version < '3.10'" }, + { name = "uvicorn", version = "0.40.0", source = { registry = "https://pypi.org/simple" }, extra = ["standard"], marker = "python_full_version >= '3.10'" }, +] +standard-no-fastapi-cloud-cli = [ + { name = "uvicorn", version = "0.39.0", source = { registry = "https://pypi.org/simple" }, extra = ["standard"], marker = "python_full_version < '3.10'" }, + { name = "uvicorn", version = "0.40.0", source = { registry = "https://pypi.org/simple" }, extra = ["standard"], marker = "python_full_version >= '3.10'" }, +] + +[[package]] +name = "fastapi-cloud-cli" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastar" }, + { name = "httpx" }, + { name = "pydantic", extra = ["email"] }, + { name = "rich-toolkit" }, + { name = "rignore" }, + { name = "sentry-sdk" }, + { name = "typer" }, + { name = "uvicorn", version = "0.39.0", source = { registry = "https://pypi.org/simple" }, extra = ["standard"], marker = "python_full_version < '3.10'" }, + { name = "uvicorn", version = "0.40.0", source = { registry = "https://pypi.org/simple" }, extra = ["standard"], marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/51/5d/3b33438de35521fab4968b232caa9a4bd568a5078f2b2dfb7bb8a4528603/fastapi_cloud_cli-0.8.0.tar.gz", hash = "sha256:cf07c502528bfd9e6b184776659f05d9212811d76bbec9fbb6bf34bed4c7456f", size = 30257, upload-time = "2025-12-23T12:08:33.904Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dd/8e/abb95ef59e91bb5adaa2d18fbf9ea70fd524010bb03f406a2dd2a4775ef9/fastapi_cloud_cli-0.8.0-py3-none-any.whl", hash = "sha256:e9f40bee671d985fd25d7a5409b56d4f103777bf8a0c6d746ea5fbf97a8186d9", size = 22306, upload-time = "2025-12-23T12:08:32.68Z" }, +] + +[[package]] +name = "fastar" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/69/e7/f89d54fb04104114dd0552836dc2b47914f416cc0e200b409dd04a33de5e/fastar-0.8.0.tar.gz", hash = "sha256:f4d4d68dbf1c4c2808f0e730fac5843493fc849f70fe3ad3af60dfbaf68b9a12", size = 68524, upload-time = "2025-11-26T02:36:00.72Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/e2/51d9ee443aabcd5aa581d45b18b6198ced364b5cd97e5504c5d782ceb82c/fastar-0.8.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:c9f930cff014cf79d396d0541bd9f3a3f170c9b5e45d10d634d98f9ed08788c3", size = 708536, upload-time = "2025-11-26T02:34:35.236Z" }, + { url = "https://files.pythonhosted.org/packages/07/2a/edfc6274768b8a3859a5ca4f8c29cb7f614d7f27d2378e2c88aa91cda54e/fastar-0.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07b70f712d20622346531a4b46bb332569bea621f61314c0b7e80903a16d14cf", size = 632235, upload-time = "2025-11-26T02:34:19.367Z" }, + { url = "https://files.pythonhosted.org/packages/ef/1e/3cfbaaec464caef196700ee2ffae1c03f94f7c5e2a85d0ec0ea9cdd1da81/fastar-0.8.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:330639db3bfba4c6d132421a2a4aeb81e7bea8ce9159cdb6e247fbc5fae97686", size = 871386, upload-time = "2025-11-26T02:33:47.613Z" }, + { url = "https://files.pythonhosted.org/packages/82/50/224a674ad541054179e4e6e0b54bb6e162f04f698a2512b42a8085fc6b6f/fastar-0.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98ea7ceb6231e48d7bb0d7dc13e946baa29c7f6873eaf4afb69725d6da349033", size = 764955, upload-time = "2025-11-26T02:32:44.279Z" }, + { url = "https://files.pythonhosted.org/packages/4d/5e/4608184aa57cb6a54f62c1eb3e5133ba8d461fc7f13193c0255effbec12a/fastar-0.8.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a90695a601a78bbca910fdf2efcdf3103c55d0de5a5c6e93556d707bf886250b", size = 765987, upload-time = "2025-11-26T02:32:59.701Z" }, + { url = "https://files.pythonhosted.org/packages/e0/53/6afd2b680dddfa10df9a16bbcf6cabfee0d92435d5c7e3f4cfe3b1712662/fastar-0.8.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d0bf655ff4c9320b0ca8a5b128063d5093c0c8c1645a2b5f7167143fd8531aa", size = 930900, upload-time = "2025-11-26T02:33:16.059Z" }, + { url = "https://files.pythonhosted.org/packages/ef/1e/b7a304bfcc1d06845cbfa4b464516f6fff9c8c6692f6ef80a3a86b04e199/fastar-0.8.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d8df22cdd8d58e7689aa89b2e4a07e8e5fa4f88d2d9c2621f0e88a49be97ccea", size = 821523, upload-time = "2025-11-26T02:33:30.897Z" }, + { url = "https://files.pythonhosted.org/packages/1d/da/9ef8605c6d233cd6ca3a95f7f518ac22aa064903afe6afa57733bfb7c31b/fastar-0.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8a5e6ad722685128521c8fb44cf25bd38669650ba3a4b466b8903e5aa28e1a0", size = 821268, upload-time = "2025-11-26T02:34:04.003Z" }, + { url = "https://files.pythonhosted.org/packages/7e/22/ed37c78a6b4420de1677d82e79742787975c34847229c33dc376334c7283/fastar-0.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:31cd541231a2456e32104da891cf9962c3b40234d0465cbf9322a6bc8a1b05d5", size = 986286, upload-time = "2025-11-26T02:34:50.279Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a6/366b15f432d85d4089e6e4b52a09cc2a2bcf4d7a1f0771e3d3194deccb1e/fastar-0.8.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:175db2a98d67ced106468e8987975484f8bbbd5ad99201da823b38bafb565ed5", size = 1041921, upload-time = "2025-11-26T02:35:07.292Z" }, + { url = "https://files.pythonhosted.org/packages/f4/45/45f8e6991e3ce9f8aeefdc8d4c200daada41097a36808643d1703464c3e2/fastar-0.8.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ada877ab1c65197d772ce1b1c2e244d4799680d8b3f136a4308360f3d8661b23", size = 1047302, upload-time = "2025-11-26T02:35:24.995Z" }, + { url = "https://files.pythonhosted.org/packages/c2/e2/a587796111a3cd4b78cd61ec3fc1252d8517d81f763f4164ed5680f84810/fastar-0.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:01084cb75f13ca6a8e80bd41584322523189f8e81b472053743d6e6c3062b5a6", size = 995141, upload-time = "2025-11-26T02:35:42.449Z" }, + { url = "https://files.pythonhosted.org/packages/89/c0/7a8ec86695b0b77168e220cf2af1aa30592f5ecdbd0ce6d641d29c4a8bae/fastar-0.8.0-cp310-cp310-win32.whl", hash = "sha256:ca639b9909805e44364ea13cca2682b487e74826e4ad75957115ec693228d6b6", size = 456544, upload-time = "2025-11-26T02:36:23.801Z" }, + { url = "https://files.pythonhosted.org/packages/be/a9/8da4deb840121c59deabd939ce2dca3d6beec85576f3743d1144441938b5/fastar-0.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:fbc0f2ed0f4add7fb58034c576584d44d7eaaf93dee721dfb26dbed6e222dbac", size = 490701, upload-time = "2025-11-26T02:36:09.625Z" }, + { url = "https://files.pythonhosted.org/packages/cd/15/1c764530b81b266f6d27d78d49b6bef22a73b3300cd83a280bfd244908c5/fastar-0.8.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:cd9c0d3ebf7a0a6f642f771cf41b79f7c98d40a3072a8abe1174fbd9bd615bd3", size = 708427, upload-time = "2025-11-26T02:34:36.502Z" }, + { url = "https://files.pythonhosted.org/packages/41/fc/75d42c008516543219e4293e4d8ac55da57a5c63147484f10468bd1bc24e/fastar-0.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2875a077340fe4f8099bd3ed8fa90d9595e1ac3cd62ae19ab690d5bf550eeb35", size = 631740, upload-time = "2025-11-26T02:34:20.718Z" }, + { url = "https://files.pythonhosted.org/packages/50/8d/9632984f7824ed2210157dcebd8e9821ef6d4f2b28510d0516db6625ff9b/fastar-0.8.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a999263d9f87184bf2801833b2ecf105e03c0dd91cac78685673b70da564fd64", size = 871628, upload-time = "2025-11-26T02:33:49.279Z" }, + { url = "https://files.pythonhosted.org/packages/05/97/3eb6ea71b7544d45cd29cacb764ca23cde8ce0aed1a6a02251caa4c0a818/fastar-0.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c41111da56430f638cbfc498ebdcc7d30f63416e904b27b7695c29bd4889cb8", size = 765005, upload-time = "2025-11-26T02:32:45.833Z" }, + { url = "https://files.pythonhosted.org/packages/d6/45/3eb0ee945a0b5d5f9df7e7c25c037ce7fa441cd0b4d44f76d286e2f4396a/fastar-0.8.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3719541a12bb09ab1eae91d2c987a9b2b7d7149c52e7109ba6e15b74aabc49b1", size = 765587, upload-time = "2025-11-26T02:33:01.174Z" }, + { url = "https://files.pythonhosted.org/packages/51/bb/7defd6ec0d9570b1987d8ebde52d07d97f3f26e10b592fb3e12738eba39a/fastar-0.8.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7a9b0fff8079b18acdface7ef1b7f522fd9a589f65ca4a1a0dd7c92a0886c2a2", size = 931150, upload-time = "2025-11-26T02:33:17.374Z" }, + { url = "https://files.pythonhosted.org/packages/28/54/62e51e684dab347c61878afbf09e177029c1a91eb1e39ef244e6b3ef9efa/fastar-0.8.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ac073576c1931959191cb20df38bab21dd152f66c940aa3ca8b22e39f753b2f3", size = 821354, upload-time = "2025-11-26T02:33:32.083Z" }, + { url = "https://files.pythonhosted.org/packages/53/a8/12708ea4d21e3cf9f485b2a67d44ce84d949a6eddcc9aa5b3d324585ab43/fastar-0.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:003b59a7c3e405b6a7bff8fab17d31e0ccbc7f06730a8f8ca1694eeea75f3c76", size = 821626, upload-time = "2025-11-26T02:34:05.685Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c4/1b4d3347c7a759853f963410bf6baf42fe014d587c50c39c8e145f4bf1a0/fastar-0.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a7b96748425efd9fc155cd920d65088a1b0d754421962418ea73413d02ff515a", size = 986187, upload-time = "2025-11-26T02:34:52.047Z" }, + { url = "https://files.pythonhosted.org/packages/dc/59/2dbe0dc2570764475e60030403738faa261a9d3bff16b08629c378ab939a/fastar-0.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:90957a30e64418b02df5b4d525bea50403d98a4b1f29143ce5914ddfa7e54ee4", size = 1041536, upload-time = "2025-11-26T02:35:08.926Z" }, + { url = "https://files.pythonhosted.org/packages/d9/0f/639b295669c7ca6fbc2b4be2a7832aaeac1a5e06923f15a8a6d6daecbc7d/fastar-0.8.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f6e784a8015623fbb7ccca1af372fd82cb511b408ddd2348dc929fc6e415df73", size = 1047149, upload-time = "2025-11-26T02:35:26.597Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e7/23e3a19e06d261d1894f98eca9458f98c090c505a0c712dafc0ff1fc2965/fastar-0.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a03eaf287bbc93064688a1220580ce261e7557c8898f687f4d0b281c85b28d3c", size = 994992, upload-time = "2025-11-26T02:35:44.009Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7a/3ea4726bae3ac9358d02107ae48f3e10ee186dbed554af79e00b7b498c44/fastar-0.8.0-cp311-cp311-win32.whl", hash = "sha256:661a47ed90762f419406c47e802f46af63a08254ba96abd1c8191e4ce967b665", size = 456449, upload-time = "2025-11-26T02:36:25.291Z" }, + { url = "https://files.pythonhosted.org/packages/cb/3c/0142bee993c431ee91cf5535e6e4b079ad491f620c215fcd79b7e5ffeb2b/fastar-0.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:b48abd6056fef7bc3d414aafb453c5b07fdf06d2df5a2841d650288a3aa1e9d3", size = 490863, upload-time = "2025-11-26T02:36:11.114Z" }, + { url = "https://files.pythonhosted.org/packages/3b/18/d119944f6bdbf6e722e204e36db86390ea45684a1bf6be6e3aa42abd471f/fastar-0.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:50c18788b3c6ffb85e176dcb8548bb8e54616a0519dcdbbfba66f6bbc4316933", size = 462230, upload-time = "2025-11-26T02:36:01.917Z" }, + { url = "https://files.pythonhosted.org/packages/58/f1/5b2ff898abac7f1a418284aad285e3a4f68d189c572ab2db0f6c9079dd16/fastar-0.8.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0f10d2adfe40f47ff228f4efaa32d409d732ded98580e03ed37c9535b5fc923d", size = 706369, upload-time = "2025-11-26T02:34:37.783Z" }, + { url = "https://files.pythonhosted.org/packages/23/60/8046a386dca39154f80c927cbbeeb4b1c1267a3271bffe61552eb9995757/fastar-0.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b930da9d598e3bc69513d131f397e6d6be4643926ef3de5d33d1e826631eb036", size = 629097, upload-time = "2025-11-26T02:34:21.888Z" }, + { url = "https://files.pythonhosted.org/packages/22/7e/1ae005addc789924a9268da2394d3bb5c6f96836f7e37b7e3d23c2362675/fastar-0.8.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9d210da2de733ca801de83e931012349d209f38b92d9630ccaa94bd445bdc9b8", size = 868938, upload-time = "2025-11-26T02:33:51.119Z" }, + { url = "https://files.pythonhosted.org/packages/a6/77/290a892b073b84bf82e6b2259708dfe79c54f356e252c2dd40180b16fe07/fastar-0.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa02270721517078a5bd61a38719070ac2537a4aa6b6c48cf369cf2abc59174a", size = 765204, upload-time = "2025-11-26T02:32:47.02Z" }, + { url = "https://files.pythonhosted.org/packages/d0/00/c3155171b976003af3281f5258189f1935b15d1221bfc7467b478c631216/fastar-0.8.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:83c391e5b789a720e4d0029b9559f5d6dee3226693c5b39c0eab8eaece997e0f", size = 764717, upload-time = "2025-11-26T02:33:02.453Z" }, + { url = "https://files.pythonhosted.org/packages/b7/43/405b7ad76207b2c11b7b59335b70eac19e4a2653977f5588a1ac8fed54f4/fastar-0.8.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3258d7a78a72793cdd081545da61cabe85b1f37634a1d0b97ffee0ff11d105ef", size = 931502, upload-time = "2025-11-26T02:33:18.619Z" }, + { url = "https://files.pythonhosted.org/packages/da/8a/a3dde6d37cc3da4453f2845cdf16675b5686b73b164f37e2cc579b057c2c/fastar-0.8.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e6eab95dd985cdb6a50666cbeb9e4814676e59cfe52039c880b69d67cfd44767", size = 821454, upload-time = "2025-11-26T02:33:33.427Z" }, + { url = "https://files.pythonhosted.org/packages/da/c1/904fe2468609c8990dce9fe654df3fbc7324a8d8e80d8240ae2c89757064/fastar-0.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:829b1854166141860887273c116c94e31357213fa8e9fe8baeb18bd6c38aa8d9", size = 821647, upload-time = "2025-11-26T02:34:07Z" }, + { url = "https://files.pythonhosted.org/packages/c8/73/a0642ab7a400bc07528091785e868ace598fde06fcd139b8f865ec1b6f3c/fastar-0.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b1667eae13f9457a3c737f4376d68e8c3e548353538b28f7e4273a30cb3965cd", size = 986342, upload-time = "2025-11-26T02:34:53.371Z" }, + { url = "https://files.pythonhosted.org/packages/af/af/60c1bfa6edab72366461a95f053d0f5f7ab1825fe65ca2ca367432cd8629/fastar-0.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b864a95229a7db0814cd9ef7987cb713fd43dce1b0d809dd17d9cd6f02fdde3e", size = 1040207, upload-time = "2025-11-26T02:35:10.65Z" }, + { url = "https://files.pythonhosted.org/packages/f6/a0/0d624290dec622e7fa084b6881f456809f68777d54a314f5dde932714506/fastar-0.8.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c05fbc5618ce17675a42576fa49858d79734627f0a0c74c0875ab45ee8de340c", size = 1045031, upload-time = "2025-11-26T02:35:28.108Z" }, + { url = "https://files.pythonhosted.org/packages/a7/74/cf663af53c4706ba88e6b4af44a6b0c3bd7d7ca09f079dc40647a8f06585/fastar-0.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7f41c51ee96f338662ee3c3df4840511ba3f9969606840f1b10b7cb633a3c716", size = 994877, upload-time = "2025-11-26T02:35:45.797Z" }, + { url = "https://files.pythonhosted.org/packages/52/17/444c8be6e77206050e350da7c338102b6cab384be937fa0b1d6d1f9ede73/fastar-0.8.0-cp312-cp312-win32.whl", hash = "sha256:d949a1a2ea7968b734632c009df0571c94636a5e1622c87a6e2bf712a7334f47", size = 455996, upload-time = "2025-11-26T02:36:26.938Z" }, + { url = "https://files.pythonhosted.org/packages/dc/34/fc3b5e56d71a17b1904800003d9251716e8fd65f662e1b10a26881698a74/fastar-0.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:fc645994d5b927d769121094e8a649b09923b3c13a8b0b98696d8f853f23c532", size = 490429, upload-time = "2025-11-26T02:36:12.707Z" }, + { url = "https://files.pythonhosted.org/packages/35/a8/5608cc837417107c594e2e7be850b9365bcb05e99645966a5d6a156285fe/fastar-0.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:d81ee82e8dc78a0adb81728383bd39611177d642a8fa2d601d4ad5ad59e5f3bd", size = 461297, upload-time = "2025-11-26T02:36:03.546Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a5/79ecba3646e22d03eef1a66fb7fc156567213e2e4ab9faab3bbd4489e483/fastar-0.8.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:a3253a06845462ca2196024c7a18f5c0ba4de1532ab1c4bad23a40b332a06a6a", size = 706112, upload-time = "2025-11-26T02:34:39.237Z" }, + { url = "https://files.pythonhosted.org/packages/0a/03/4f883bce878218a8676c2d7ca09b50c856a5470bb3b7f63baf9521ea6995/fastar-0.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5cbeb3ebfa0980c68ff8b126295cc6b208ccd81b638aebc5a723d810a7a0e5d2", size = 628954, upload-time = "2025-11-26T02:34:23.705Z" }, + { url = "https://files.pythonhosted.org/packages/4f/f1/892e471f156b03d10ba48ace9384f5a896702a54506137462545f38e40b8/fastar-0.8.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1c0d5956b917daac77d333d48b3f0f3ff927b8039d5b32d8125462782369f761", size = 868685, upload-time = "2025-11-26T02:33:53.077Z" }, + { url = "https://files.pythonhosted.org/packages/39/ba/e24915045852e30014ec6840446975c03f4234d1c9270394b51d3ad18394/fastar-0.8.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27b404db2b786b65912927ce7f3790964a4bcbde42cdd13091b82a89cd655e1c", size = 765044, upload-time = "2025-11-26T02:32:48.187Z" }, + { url = "https://files.pythonhosted.org/packages/14/2c/1aa11ac21a99984864c2fca4994e094319ff3a2046e7a0343c39317bd5b9/fastar-0.8.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0902fc89dcf1e7f07b8563032a4159fe2b835e4c16942c76fd63451d0e5f76a3", size = 764322, upload-time = "2025-11-26T02:33:03.859Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f0/4b91902af39fe2d3bae7c85c6d789586b9fbcf618d7fdb3d37323915906d/fastar-0.8.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:069347e2f0f7a8b99bbac8cd1bc0e06c7b4a31dc964fc60d84b95eab3d869dc1", size = 931016, upload-time = "2025-11-26T02:33:19.902Z" }, + { url = "https://files.pythonhosted.org/packages/c9/97/8fc43a5a9c0a2dc195730f6f7a0f367d171282cd8be2511d0e87c6d2dad0/fastar-0.8.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7fd135306f6bfe9a835918280e0eb440b70ab303e0187d90ab51ca86e143f70d", size = 821308, upload-time = "2025-11-26T02:33:34.664Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e9/058615b63a7fd27965e8c5966f393ed0c169f7ff5012e1674f21684de3ba/fastar-0.8.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78d06d6897f43c27154b5f2d0eb930a43a81b7eec73f6f0b0114814d4a10ab38", size = 821171, upload-time = "2025-11-26T02:34:08.498Z" }, + { url = "https://files.pythonhosted.org/packages/ca/cf/69e16a17961570a755c37ffb5b5aa7610d2e77807625f537989da66f2a9d/fastar-0.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a922f8439231fa0c32b15e8d70ff6d415619b9d40492029dabbc14a0c53b5f18", size = 986227, upload-time = "2025-11-26T02:34:55.06Z" }, + { url = "https://files.pythonhosted.org/packages/fb/83/2100192372e59b56f4ace37d7d9cabda511afd71b5febad1643d1c334271/fastar-0.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a739abd51eb766384b4caff83050888e80cd75bbcfec61e6d1e64875f94e4a40", size = 1039395, upload-time = "2025-11-26T02:35:12.166Z" }, + { url = "https://files.pythonhosted.org/packages/75/15/cdd03aca972f55872efbb7cf7540c3fa7b97a75d626303a3ea46932163dc/fastar-0.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5a65f419d808b23ac89d5cd1b13a2f340f15bc5d1d9af79f39fdb77bba48ff1b", size = 1044766, upload-time = "2025-11-26T02:35:29.62Z" }, + { url = "https://files.pythonhosted.org/packages/3d/29/945e69e4e2652329ace545999334ec31f1431fbae3abb0105587e11af2ae/fastar-0.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7bb2ae6c0cce58f0db1c9f20495e7557cca2c1ee9c69bbd90eafd54f139171c5", size = 994740, upload-time = "2025-11-26T02:35:47.887Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5d/dbfe28f8cd1eb484bba0c62e5259b2cf6fea229d6ef43e05c06b5a78c034/fastar-0.8.0-cp313-cp313-win32.whl", hash = "sha256:b28753e0d18a643272597cb16d39f1053842aa43131ad3e260c03a2417d38401", size = 455990, upload-time = "2025-11-26T02:36:28.502Z" }, + { url = "https://files.pythonhosted.org/packages/e1/01/e965740bd36e60ef4c5aa2cbe42b6c4eb1dc3551009238a97c2e5e96bd23/fastar-0.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:620e5d737dce8321d49a5ebb7997f1fd0047cde3512082c27dc66d6ac8c1927a", size = 490227, upload-time = "2025-11-26T02:36:14.363Z" }, + { url = "https://files.pythonhosted.org/packages/dd/10/c99202719b83e5249f26902ae53a05aea67d840eeb242019322f20fc171c/fastar-0.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:c4c4bd08df563120cd33e854fe0a93b81579e8571b11f9b7da9e84c37da2d6b6", size = 461078, upload-time = "2025-11-26T02:36:04.94Z" }, + { url = "https://files.pythonhosted.org/packages/96/4a/9573b87a0ef07580ed111e7230259aec31bb33ca3667963ebee77022ec61/fastar-0.8.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:50b36ce654ba44b0e13fae607ae17ee6e1597b69f71df1bee64bb8328d881dfc", size = 706041, upload-time = "2025-11-26T02:34:40.638Z" }, + { url = "https://files.pythonhosted.org/packages/4a/19/f95444a1d4f375333af49300aa75ee93afa3335c0e40fda528e460ed859c/fastar-0.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:63a892762683d7ab00df0227d5ea9677c62ff2cde9b875e666c0be569ed940f3", size = 628617, upload-time = "2025-11-26T02:34:24.893Z" }, + { url = "https://files.pythonhosted.org/packages/b3/c9/b51481b38b7e3f16ef2b9e233b1a3623386c939d745d6e41bbd389eaae30/fastar-0.8.0-cp314-cp314-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4ae6a145c1bff592644bde13f2115e0239f4b7babaf506d14e7d208483cf01a5", size = 869299, upload-time = "2025-11-26T02:33:54.274Z" }, + { url = "https://files.pythonhosted.org/packages/bf/02/3ba1267ee5ba7314e29c431cf82eaa68586f2c40cdfa08be3632b7d07619/fastar-0.8.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ae0ff7c0a1c7e1428404b81faee8aebef466bfd0be25bfe4dabf5d535c68741", size = 764667, upload-time = "2025-11-26T02:32:49.606Z" }, + { url = "https://files.pythonhosted.org/packages/1b/84/bf33530fd015b5d7c2cc69e0bce4a38d736754a6955487005aab1af6adcd/fastar-0.8.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dbfd87dbd217b45c898b2dbcd0169aae534b2c1c5cbe3119510881f6a5ac8ef5", size = 763993, upload-time = "2025-11-26T02:33:05.782Z" }, + { url = "https://files.pythonhosted.org/packages/da/e0/9564d24e7cea6321a8d921c6d2a457044a476ef197aa4708e179d3d97f0d/fastar-0.8.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a5abd99fcba83ef28c8fe6ae2927edc79053db43a0457a962ed85c9bf150d37", size = 930153, upload-time = "2025-11-26T02:33:21.53Z" }, + { url = "https://files.pythonhosted.org/packages/35/b1/6f57fcd8d6e192cfebf97e58eb27751640ad93784c857b79039e84387b51/fastar-0.8.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:91d4c685620c3a9d6b5ae091dbabab4f98b20049b7ecc7976e19cc9016c0d5d6", size = 821177, upload-time = "2025-11-26T02:33:35.839Z" }, + { url = "https://files.pythonhosted.org/packages/b3/78/9e004ea9f3aa7466f5ddb6f9518780e1d2f0ed3ca55f093632982598bace/fastar-0.8.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f77c2f2cad76e9dc7b6701297adb1eba87d0485944b416fc2ccf5516c01219a3", size = 820652, upload-time = "2025-11-26T02:34:09.776Z" }, + { url = "https://files.pythonhosted.org/packages/42/95/b604ed536544005c9f1aee7c4c74b00150db3d8d535cd8232dc20f947063/fastar-0.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e7f07c4a3dada7757a8fc430a5b4a29e6ef696d2212747213f57086ffd970316", size = 985961, upload-time = "2025-11-26T02:34:56.401Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7b/fa9d4d96a5d494bdb8699363bb9de8178c0c21a02e1d89cd6f913d127018/fastar-0.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:90c0c3fe55105c0aed8a83135dbdeb31e683455dbd326a1c48fa44c378b85616", size = 1039316, upload-time = "2025-11-26T02:35:13.807Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f9/8462789243bc3f33e8401378ec6d54de4e20cfa60c96a0e15e3e9d1389bb/fastar-0.8.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:fb9ee51e5bffe0dab3d3126d3a4fac8d8f7235cedcb4b8e74936087ce1c157f3", size = 1045028, upload-time = "2025-11-26T02:35:31.079Z" }, + { url = "https://files.pythonhosted.org/packages/a5/71/9abb128777e616127194b509e98fcda3db797d76288c1a8c23dd22afc14f/fastar-0.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e380b1e8d30317f52406c43b11e98d11e1d68723bbd031e18049ea3497b59a6d", size = 994677, upload-time = "2025-11-26T02:35:49.391Z" }, + { url = "https://files.pythonhosted.org/packages/de/c1/b81b3f194853d7ad232a67a1d768f5f51a016f165cfb56cb31b31bbc6177/fastar-0.8.0-cp314-cp314-win32.whl", hash = "sha256:1c4ffc06e9c4a8ca498c07e094670d8d8c0d25b17ca6465b9774da44ea997ab1", size = 456687, upload-time = "2025-11-26T02:36:30.205Z" }, + { url = "https://files.pythonhosted.org/packages/cb/87/9e0cd4768a98181d56f0cdbab2363404cc15deb93f4aad3b99cd2761bbaa/fastar-0.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:5517a8ad4726267c57a3e0e2a44430b782e00b230bf51c55b5728e758bb3a692", size = 490578, upload-time = "2025-11-26T02:36:16.218Z" }, + { url = "https://files.pythonhosted.org/packages/aa/1e/580a76cf91847654f2ad6520e956e93218f778540975bc4190d363f709e2/fastar-0.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:58030551046ff4a8616931e52a36c83545ff05996db5beb6e0cd2b7e748aa309", size = 461473, upload-time = "2025-11-26T02:36:06.373Z" }, + { url = "https://files.pythonhosted.org/packages/58/4c/bdb5c6efe934f68708529c8c9d4055ebef5c4be370621966438f658b29bd/fastar-0.8.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:1e7d29b6bfecb29db126a08baf3c04a5ab667f6cea2b7067d3e623a67729c4a6", size = 705570, upload-time = "2025-11-26T02:34:42.01Z" }, + { url = "https://files.pythonhosted.org/packages/6d/78/f01ac7e71d5a37621bd13598a26e948a12b85ca8042f7ee1a0a8c9f59cda/fastar-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:05eb7b96940f9526b485f1d0b02393839f0f61cac4b1f60024984f8b326d2640", size = 627761, upload-time = "2025-11-26T02:34:26.152Z" }, + { url = "https://files.pythonhosted.org/packages/06/45/6df0ecda86ea9d2e95053c1a655d153dee55fc121b6e13ea6d1e246a50b6/fastar-0.8.0-cp314-cp314t-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:619352d8ac011794e2345c462189dc02ba634750d23cd9d86a9267dd71b1f278", size = 869414, upload-time = "2025-11-26T02:33:55.618Z" }, + { url = "https://files.pythonhosted.org/packages/b2/72/486421f5a8c0c377cc82e7a50c8a8ea899a6ec2aa72bde8f09fb667a2dc8/fastar-0.8.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74ebfecef3fe6d7a90355fac1402fd30636988332a1d33f3e80019a10782bb24", size = 763863, upload-time = "2025-11-26T02:32:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/d4/64/39f654dbb41a3867fb1f2c8081c014d8f1d32ea10585d84cacbef0b32995/fastar-0.8.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2975aca5a639e26a3ab0d23b4b0628d6dd6d521146c3c11486d782be621a35aa", size = 763065, upload-time = "2025-11-26T02:33:07.274Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bd/c011a34fb3534c4c3301f7c87c4ffd7e47f6113c904c092ddc8a59a303ea/fastar-0.8.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:afc438eaed8ff0dcdd9308268be5cb38c1db7e94c3ccca7c498ca13a4a4535a3", size = 930530, upload-time = "2025-11-26T02:33:23.117Z" }, + { url = "https://files.pythonhosted.org/packages/55/9d/aa6e887a7033c571b1064429222bbe09adc9a3c1e04f3d1788ba5838ebd5/fastar-0.8.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6ced0a5399cc0a84a858ef0a31ca2d0c24d3bbec4bcda506a9192d8119f3590a", size = 820572, upload-time = "2025-11-26T02:33:37.542Z" }, + { url = "https://files.pythonhosted.org/packages/ad/9c/7a3a2278a1052e1a5d98646de7c095a00cffd2492b3b84ce730e2f1cd93a/fastar-0.8.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec9b23da8c4c039da3fe2e358973c66976a0c8508aa06d6626b4403cb5666c19", size = 820649, upload-time = "2025-11-26T02:34:11.108Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/d38edc1f4438cd047e56137c26d94783ffade42e1b3bde620ccf17b771ef/fastar-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:dfba078fcd53478032fd0ceed56960ec6b7ff0511cfc013a8a3a4307e3a7bac4", size = 985653, upload-time = "2025-11-26T02:34:57.884Z" }, + { url = "https://files.pythonhosted.org/packages/69/d9/2147d0c19757e165cd62d41cec3f7b38fad2ad68ab784978b5f81716c7ea/fastar-0.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:ade56c94c14be356d295fecb47a3fcd473dd43a8803ead2e2b5b9e58feb6dcfa", size = 1038140, upload-time = "2025-11-26T02:35:15.778Z" }, + { url = "https://files.pythonhosted.org/packages/7f/1d/ec4c717ffb8a308871e9602ec3197d957e238dc0227127ac573ec9bca952/fastar-0.8.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e48d938f9366db5e59441728f70b7f6c1ccfab7eff84f96f9b7e689b07786c52", size = 1045195, upload-time = "2025-11-26T02:35:32.865Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/637334dc8c8f3bb391388b064ae13f0ad9402bc5a6c3e77b8887d0c31921/fastar-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:79c441dc1482ff51a54fb3f57ae6f7bb3d2cff88fa2cc5d196c519f8aab64a56", size = 994686, upload-time = "2025-11-26T02:35:51.392Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e2/dfa19a4b260b8ab3581b7484dcb80c09b25324f4daa6b6ae1c7640d1607a/fastar-0.8.0-cp314-cp314t-win32.whl", hash = "sha256:187f61dc739afe45ac8e47ed7fd1adc45d52eac110cf27d579155720507d6fbe", size = 455767, upload-time = "2025-11-26T02:36:34.758Z" }, + { url = "https://files.pythonhosted.org/packages/51/47/df65c72afc1297797b255f90c4778b5d6f1f0f80282a134d5ab610310ed9/fastar-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:40e9d763cf8bf85ce2fa256e010aa795c0fe3d3bd1326d5c3084e6ce7857127e", size = 489971, upload-time = "2025-11-26T02:36:22.081Z" }, + { url = "https://files.pythonhosted.org/packages/85/11/0aa8455af26f0ae89e42be67f3a874255ee5d7f0f026fc86e8d56f76b428/fastar-0.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:e59673307b6a08210987059a2bdea2614fe26e3335d0e5d1a3d95f49a05b1418", size = 460467, upload-time = "2025-11-26T02:36:07.978Z" }, + { url = "https://files.pythonhosted.org/packages/fd/0d/462ce073b1f87cc6bf33b656c340c103eeb67487d118b761a697f1ee454f/fastar-0.8.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:923afc2db5192e56e71952a88e3fe5965c7c9c910d385d2db7573136f064f2fa", size = 709519, upload-time = "2025-11-26T02:34:44.783Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f1/ceca1e04fd3266f866efdbb5d343fee1d8ff8fe94c64c8d1aab68b483ad0/fastar-0.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4fbe775356930f3aab0ce709fdf8ecf90c10882f5bbdcea215c89a3b14090c50", size = 632509, upload-time = "2025-11-26T02:34:29.428Z" }, + { url = "https://files.pythonhosted.org/packages/4f/db/fdb07e9dce80ebb38138f166a30dc482c82cc8dfcfda1024e8b100a53d1c/fastar-0.8.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2ff516154e77f4bf78c31a0c11aa78a8a80e11b6964ec6f28982e42ffcbb543c", size = 871400, upload-time = "2025-11-26T02:33:58.461Z" }, + { url = "https://files.pythonhosted.org/packages/54/6d/d0a342528002cf7527339fd275bdee5e5cc90c2f193e8418d3510ffedcd7/fastar-0.8.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d2fdd1c987ff2300bdf39baed556f8e155f8577018775e794a268ecf1707610", size = 765781, upload-time = "2025-11-26T02:32:53.929Z" }, + { url = "https://files.pythonhosted.org/packages/bc/96/dd2721e7160eed7bc9c3b695e942868c7ba2fab41947278fa380c18569be/fastar-0.8.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d80e4dad8ee2362a71870b1e735800bb5e97f12ebbee4bd0cf15a81ad2428b5a", size = 766126, upload-time = "2025-11-26T02:33:10.075Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ac/6533d2608254def722cd8c852f4ac8fb413fc43ad3251b7fb99863ffaa83/fastar-0.8.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a17abee1febf5363ed2633f5e13de4be481ba1ab5f77860d39470eccdc4b65af", size = 932549, upload-time = "2025-11-26T02:33:25.866Z" }, + { url = "https://files.pythonhosted.org/packages/97/dc/aac042d0d0017b8d75ff34381cc28b482ace0d78a26d4eef9a8674f850c2/fastar-0.8.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64cbde8e0ece3d799090a4727f936f66c5990d3ac59416f3de76a2c676e8e568", size = 821860, upload-time = "2025-11-26T02:33:41.423Z" }, + { url = "https://files.pythonhosted.org/packages/b2/ef/c31057572e3a5a2c90da4986d8594d0ff33097b4a44058f9f52103c33677/fastar-0.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63d98b26590d293a9d9a379bae88367a8f3a6137c28819ed6dd6e11aca4a5c6e", size = 821660, upload-time = "2025-11-26T02:34:13.84Z" }, + { url = "https://files.pythonhosted.org/packages/39/00/d42fc84e8ce8587eb746406aa74ec231f97ce659a48bc064d5ff5cd78889/fastar-0.8.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:bf440983d4d64582bddf2f0bd3c43ea1db93a8c31cf7c20e473bffaf6d9c0b6d", size = 987026, upload-time = "2025-11-26T02:35:01.181Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d9/5e4d37faca6f2df6a0bf1efc340192a871356162a7cc53626dc117645ad6/fastar-0.8.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:1d90cbf984a39afda27afe08e40c2d8eddc49c5e80590af641610c7b6dc20161", size = 1042165, upload-time = "2025-11-26T02:35:18.729Z" }, + { url = "https://files.pythonhosted.org/packages/77/fd/bb4731243e42f385e8db9da824971f5d0380e6b31466c36eb089d631c589/fastar-0.8.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ca0db5e563d84b639fe15385eeca940777b6d2f0a1f3bb7cd5b55ab7124f0554", size = 1046990, upload-time = "2025-11-26T02:35:36.158Z" }, + { url = "https://files.pythonhosted.org/packages/c4/50/2ac066f771858ea45d885cfde7262b286a223af9538d3f85d29d5159b4fb/fastar-0.8.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:42ff3052d74684a636423d4f040db88eebd4caf20842fa5f06020e0130c01f69", size = 995637, upload-time = "2025-11-26T02:35:54.714Z" }, + { url = "https://files.pythonhosted.org/packages/d3/04/c884ea3fa7c154ac0e89219a337c9d6e0d52d0eb5547b3f99b896eca417e/fastar-0.8.0-cp39-cp39-win32.whl", hash = "sha256:15e3dfaa769d2117ef707e5f47c62126d1b63f8e9c85133112f33f1fbdf8942f", size = 456817, upload-time = "2025-11-26T02:36:33.198Z" }, + { url = "https://files.pythonhosted.org/packages/aa/0f/3a5758cca852bb221e78950fbfece0b932c6de6981ee36fb0ac57cf66c13/fastar-0.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:5153aa1c194316d0f67b6884a62d122d51fce4196263e92e4bca2a6c47cd44c0", size = 490633, upload-time = "2025-11-26T02:36:19.976Z" }, + { url = "https://files.pythonhosted.org/packages/25/9f/6eaa810c240236eff2edf736cd50a17c97dbab1693cda4f7bcea09d13418/fastar-0.8.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2127cf2e80ffd49744a160201e0e2f55198af6c028a7b3f750026e0b1f1caa4e", size = 710544, upload-time = "2025-11-26T02:34:46.195Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a5/58ff9e49a1cd5fbfc8f1238226cbf83b905376a391a6622cdd396b2cfa29/fastar-0.8.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:ff85094f10003801339ac4fa9b20a3410c2d8f284d4cba2dc99de6e98c877812", size = 634020, upload-time = "2025-11-26T02:34:31.085Z" }, + { url = "https://files.pythonhosted.org/packages/80/94/f839257c6600a83fbdb5a7fcc06319599086137b25ba38ca3d2c0fe14562/fastar-0.8.0-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:3dbca235f0bd804cca6602fe055d3892bebf95fb802e6c6c7d872fb10f7abc6c", size = 871735, upload-time = "2025-11-26T02:34:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/eb/79/4124c54260f7ee5cb7034bfe499eff2f8512b052d54be4671e59d4f25a4f/fastar-0.8.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:722e54bfdee6c81a0005e147319e93d8797f442308032c92fa28d03ef8fda076", size = 766779, upload-time = "2025-11-26T02:32:55.109Z" }, + { url = "https://files.pythonhosted.org/packages/36/b6/043b263c4126bf6557c942d099503989af9c5c7ee5cca9a04e00f754816f/fastar-0.8.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a78e5221b94a80800930b7fd0d0e797ae73aadf7044c05ed46cb9bdf870f022", size = 766755, upload-time = "2025-11-26T02:33:11.595Z" }, + { url = "https://files.pythonhosted.org/packages/57/ff/29a5dc06f2940439ebf98661ecc98d48d3f22fed8d6a2d5dc985d1e8da24/fastar-0.8.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:997092d31ff451de8d0568f6773f3517cb87dcd0bc76184edb65d7154390a6f8", size = 932732, upload-time = "2025-11-26T02:33:27.122Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e8/2218830f422b37aad52c24b53cb84b5d88bd6fd6ad411bd6689b1a32500d/fastar-0.8.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:558e8fcf8fe574541df5db14a46cd98bfbed14a811b7014a54f2b714c0cfac42", size = 822571, upload-time = "2025-11-26T02:33:42.986Z" }, + { url = "https://files.pythonhosted.org/packages/6e/fd/ba6dfeff77cddfe58d85c490b1735c002b81c0d6f826916a8b6c4f8818bc/fastar-0.8.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1d2a54f87e2908cc19e1a6ee249620174fbefc54a219aba1eaa6f31657683c3", size = 822440, upload-time = "2025-11-26T02:34:15.439Z" }, + { url = "https://files.pythonhosted.org/packages/a7/57/54d5740c84b35de0eb12975397ecc16785b5ad8bed2dbac38b8c8a7c1edd/fastar-0.8.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:ef94901537be277f9ec59db939eb817960496c6351afede5b102699b5098604d", size = 987424, upload-time = "2025-11-26T02:35:02.742Z" }, + { url = "https://files.pythonhosted.org/packages/ee/c7/18115927f16deb1ddffdbd4ae992e7e33064bc6defa2b92a147948f8bc0c/fastar-0.8.0-pp310-pypy310_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:0afbb92f78bf29d5e9db76fb46cbabc429e49015cddf72ab9e761afbe88ac100", size = 1042675, upload-time = "2025-11-26T02:35:20.252Z" }, + { url = "https://files.pythonhosted.org/packages/d7/1a/ca884fc7973ec6d765e87af23a4dd25784fb0a36ac2df825f18c3630bbab/fastar-0.8.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:fb59c7925e7710ad178d9e1a3e65edf295d9a042a0cdcb673b4040949eb8ad0a", size = 1047098, upload-time = "2025-11-26T02:35:37.643Z" }, + { url = "https://files.pythonhosted.org/packages/44/ee/25cd645db749b206bb95e1512e57e75d56ccbbb8ec3536f52a7979deab6b/fastar-0.8.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:e6c4d6329da568ec36b1347b0c09c4d27f9dfdeddf9f438ddb16799ecf170098", size = 997397, upload-time = "2025-11-26T02:35:56.215Z" }, + { url = "https://files.pythonhosted.org/packages/98/6e/6c46aa7f8c8734e7f96ee5141acd3877667ce66f34eea10703aa7571d191/fastar-0.8.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:998e3fa4b555b63eb134e6758437ed739ad1652fdd2a61dfe1dacbfddc35fe66", size = 710662, upload-time = "2025-11-26T02:34:47.593Z" }, + { url = "https://files.pythonhosted.org/packages/70/27/fd622442f2fbd4ff5459677987481ef1c60e077cb4e63a2ed4d8dce6f869/fastar-0.8.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:5f83e60d845091f3a12bc37f412774264d161576eaf810ed8b43567eb934b7e5", size = 634049, upload-time = "2025-11-26T02:34:32.365Z" }, + { url = "https://files.pythonhosted.org/packages/8f/ee/aa4d08aea25b5419a7277132e738ab1cd775f26aebddce11413b07e2fdff/fastar-0.8.0-pp311-pypy311_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:299672e1c74d8b73c61684fac9159cfc063d35f4b165996a88facb0e26862cb5", size = 872055, upload-time = "2025-11-26T02:34:01.377Z" }, + { url = "https://files.pythonhosted.org/packages/92/9a/2bf2f77aade575e67997e0c759fd55cb1c66b7a5b437b1cd0e97d8b241bc/fastar-0.8.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3d3a27066b84d015deab5faee78565509bb33b137896443e4144cb1be1a5f90", size = 766787, upload-time = "2025-11-26T02:32:57.161Z" }, + { url = "https://files.pythonhosted.org/packages/0b/90/23a3f6c252f11b10c70f854bce09abc61f71b5a0e6a4b0eac2bcb9a2c583/fastar-0.8.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ef0bcf4385bbdd3c1acecce2d9ea7dab7cc9b8ee0581bbccb7ab11908a7ce288", size = 766861, upload-time = "2025-11-26T02:33:12.824Z" }, + { url = "https://files.pythonhosted.org/packages/76/bb/beeb9078380acd4484db5c957d066171695d9340e3526398eb230127b0c2/fastar-0.8.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f10ef62b6eda6cb6fd9ba8e1fe08a07d7b2bdcc8eaa00eb91566143b92ed7eee", size = 932667, upload-time = "2025-11-26T02:33:28.405Z" }, + { url = "https://files.pythonhosted.org/packages/f4/6d/b034cc637bd0ee638d5a85d08e941b0b8ffd44cf391fb751ba98233734f7/fastar-0.8.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c4f6c82a8ee98c17aa48585ee73b51c89c1b010e5c951af83e07c3436180e3fc", size = 822712, upload-time = "2025-11-26T02:33:44.27Z" }, + { url = "https://files.pythonhosted.org/packages/e2/2b/7d183c63f59227c4689792042d6647f2586a5e7273b55e81745063088d81/fastar-0.8.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6129067fcb86276635b5857010f4e9b9c7d5d15dd571bb03c6c1ed73c40fd92", size = 822659, upload-time = "2025-11-26T02:34:16.815Z" }, + { url = "https://files.pythonhosted.org/packages/3e/f9/716e0cd9de2427fdf766bc68176f76226cd01fffef3a56c5046fa863f5f0/fastar-0.8.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:4cc9e77019e489f1ddac446b6a5b9dfb5c3d9abd142652c22a1d9415dbcc0e47", size = 987412, upload-time = "2025-11-26T02:35:04.259Z" }, + { url = "https://files.pythonhosted.org/packages/a4/b9/9a8c3fd59958c1c8027bc075af11722cdc62c4968bb277e841d131232289/fastar-0.8.0-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:382bfe82c026086487cb17fee12f4c1e2b4e67ce230f2e04487d3e7ddfd69031", size = 1042911, upload-time = "2025-11-26T02:35:21.857Z" }, + { url = "https://files.pythonhosted.org/packages/e2/2f/c3f30963b47022134b8a231c12845f4d7cfba520f59bbc1a82468aea77c7/fastar-0.8.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:908d2b9a1ff3d549cc304b32f95706a536da8f0bcb0bc0f9e4c1cce39b80e218", size = 1047464, upload-time = "2025-11-26T02:35:39.376Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8a/218ab6d9a2bab3b07718e6cd8405529600edc1e9c266320e8524c8f63251/fastar-0.8.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:1aa7dbde2d2d73eb5b6203d0f74875cb66350f0f1b4325b4839fc8fbbf5d074e", size = 997309, upload-time = "2025-11-26T02:35:57.722Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ef/da0cbac6da78b22ddb6ec0bbf3ad1813f8dd991a911342fc20e5feabaa15/fastar-0.8.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:284036bae786a520456ad3f58e72aaf1bd5d74e309132e568343564daa4ae383", size = 710367, upload-time = "2025-11-26T02:34:48.853Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f0/951d368b4538477fe44651aaa7436318c22bf89e8e18086a68bd89e14a82/fastar-0.8.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:5aba0942b4f56acdb8fa8aa7cb506f70c1a17bf13dcab318a17ffb467cb2e7ec", size = 633744, upload-time = "2025-11-26T02:34:33.723Z" }, + { url = "https://files.pythonhosted.org/packages/1e/d2/5f37b634aff1e27191874b7f0fb7306e60566d984df06ff5c2f901ef123b/fastar-0.8.0-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:52eda6230799db7bbd44461c622161e9bcd43603399da19b0daab2782e0030b0", size = 871412, upload-time = "2025-11-26T02:34:02.738Z" }, + { url = "https://files.pythonhosted.org/packages/b4/cd/41148c74cc164aacb8b5e7cb938a7ecc5a4e595f95e312aafcc8dbe36e48/fastar-0.8.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f860566b9f3cb1900980f46a4c3f003990c0009c11730f988f758542c17a2364", size = 766470, upload-time = "2025-11-26T02:32:58.503Z" }, + { url = "https://files.pythonhosted.org/packages/5f/89/b5715d4f969f15fe78b184ae42295e71f0ad2e0e083f0625212271cbda55/fastar-0.8.0-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:78f3fe5f45437c66d1dbece5f31aa487e48ef46d76b2082b873d5fa18013ebe1", size = 765285, upload-time = "2025-11-26T02:33:14.148Z" }, + { url = "https://files.pythonhosted.org/packages/39/4a/88223721278a663684c4fa6cb95ca296e0ba385362de4809a5e85cacfb36/fastar-0.8.0-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:82bc445202bbc53f067bb15e3b8639f01fd54d3096a0f9601240690cfd7c9684", size = 933881, upload-time = "2025-11-26T02:33:29.637Z" }, + { url = "https://files.pythonhosted.org/packages/ac/73/a13d6d57029378477c4d9232eddfb81bb2f74e0e91bdbca864c74223d877/fastar-0.8.0-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b1208b5453cfe7192e54765f73844b80d684bd8dc6d6acbbb60ead42590b13e", size = 822511, upload-time = "2025-11-26T02:33:45.882Z" }, + { url = "https://files.pythonhosted.org/packages/a4/96/79000571a10d93e2110e72b5e7155e6bf138c8cb896117cfb768c74d01ff/fastar-0.8.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8922754c66699e27d4f1ce07c9c256228054cdc9bb36363e8bb5b503453a6da", size = 822204, upload-time = "2025-11-26T02:34:18.164Z" }, + { url = "https://files.pythonhosted.org/packages/35/e4/c2066024f336349ce36f85b0df9d082981d453cfdb5c139af2c5eebcc49a/fastar-0.8.0-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:92cad46dfbb9969359823c9f61165ec32d5d675d86e863889416e9b64efea95c", size = 987454, upload-time = "2025-11-26T02:35:05.827Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b7/e3a062534af02610dea226d2c14e70beb90b91a23562fed8f76bfa35e6cd/fastar-0.8.0-pp39-pypy39_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:f4eb9560a447ff6a4b377f08b6e5d3a31909a612b028f2c57810ffaf570eceb8", size = 1041310, upload-time = "2025-11-26T02:35:23.305Z" }, + { url = "https://files.pythonhosted.org/packages/91/73/1640476553705ee29ff9350a3b6c707ac150be53094d326e0d81d62a65ec/fastar-0.8.0-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:52455794e6cc2b6a6dbf141a1c4312a1a1215d75e8849a35fcff694454da880f", size = 1047020, upload-time = "2025-11-26T02:35:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/b7/79/9b492c4da885a6699bc76032dbc285c565429ca1b6dc6b5aa5c908111d22/fastar-0.8.0-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:8de5decfa18a03807ae26ba5af095c2c04ac31ae915e9a849363a4495463171f", size = 996379, upload-time = "2025-11-26T02:35:59.297Z" }, +] + +[[package]] +name = "fastavro" +version = "1.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/8b/fa2d3287fd2267be6261d0177c6809a7fa12c5600ddb33490c8dc29e77b2/fastavro-1.12.1.tar.gz", hash = "sha256:2f285be49e45bc047ab2f6bed040bb349da85db3f3c87880e4b92595ea093b2b", size = 1025661, upload-time = "2025-10-10T15:40:55.41Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/a0/077fd7cbfc143152cb96780cb592ed6cb6696667d8bc1b977745eb2255a8/fastavro-1.12.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:00650ca533907361edda22e6ffe8cf87ab2091c5d8aee5c8000b0f2dcdda7ed3", size = 1000335, upload-time = "2025-10-10T15:40:59.834Z" }, + { url = "https://files.pythonhosted.org/packages/a0/ae/a115e027f3a75df237609701b03ecba0b7f0aa3d77fe0161df533fde1eb7/fastavro-1.12.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac76d6d95f909c72ee70d314b460b7e711d928845771531d823eb96a10952d26", size = 3221067, upload-time = "2025-10-10T15:41:04.399Z" }, + { url = "https://files.pythonhosted.org/packages/94/4e/c4991c3eec0175af9a8a0c161b88089cb7bf7fe353b3e3be1bc4cf9036b2/fastavro-1.12.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f55eef18c41d4476bd32a82ed5dd86aabc3f614e1b66bdb09ffa291612e1670", size = 3228979, upload-time = "2025-10-10T15:41:06.738Z" }, + { url = "https://files.pythonhosted.org/packages/21/0c/f2afb8eaea38799ccb1ed07d68bf2659f2e313f1902bbd36774cf6a1bef9/fastavro-1.12.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:81563e1f93570e6565487cdb01ba241a36a00e58cff9c5a0614af819d1155d8f", size = 3160740, upload-time = "2025-10-10T15:41:08.731Z" }, + { url = "https://files.pythonhosted.org/packages/0d/1a/f4d367924b40b86857862c1fa65f2afba94ddadf298b611e610a676a29e5/fastavro-1.12.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bec207360f76f0b3de540758a297193c5390e8e081c43c3317f610b1414d8c8f", size = 3235787, upload-time = "2025-10-10T15:41:10.869Z" }, + { url = "https://files.pythonhosted.org/packages/90/ec/8db9331896e3dfe4f71b2b3c23f2e97fbbfd90129777467ca9f8bafccb74/fastavro-1.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:c0390bfe4a9f8056a75ac6785fbbff8f5e317f5356481d2e29ec980877d2314b", size = 449350, upload-time = "2025-10-10T15:41:12.104Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e9/31c64b47cefc0951099e7c0c8c8ea1c931edd1350f34d55c27cbfbb08df1/fastavro-1.12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6b632b713bc5d03928a87d811fa4a11d5f25cd43e79c161e291c7d3f7aa740fd", size = 1016585, upload-time = "2025-10-10T15:41:13.717Z" }, + { url = "https://files.pythonhosted.org/packages/10/76/111560775b548f5d8d828c1b5285ff90e2d2745643fb80ecbf115344eea4/fastavro-1.12.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa7ab3769beadcebb60f0539054c7755f63bd9cf7666e2c15e615ab605f89a8", size = 3404629, upload-time = "2025-10-10T15:41:15.642Z" }, + { url = "https://files.pythonhosted.org/packages/b0/07/6bb93cb963932146c2b6c5c765903a0a547ad9f0f8b769a4a9aad8c06369/fastavro-1.12.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:123fb221df3164abd93f2d042c82f538a1d5a43ce41375f12c91ce1355a9141e", size = 3428594, upload-time = "2025-10-10T15:41:17.779Z" }, + { url = "https://files.pythonhosted.org/packages/d1/67/8115ec36b584197ea737ec79e3499e1f1b640b288d6c6ee295edd13b80f6/fastavro-1.12.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:632a4e3ff223f834ddb746baae0cc7cee1068eb12c32e4d982c2fee8a5b483d0", size = 3344145, upload-time = "2025-10-10T15:41:19.89Z" }, + { url = "https://files.pythonhosted.org/packages/9e/9e/a7cebb3af967e62539539897c10138fa0821668ec92525d1be88a9cd3ee6/fastavro-1.12.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:83e6caf4e7a8717d932a3b1ff31595ad169289bbe1128a216be070d3a8391671", size = 3431942, upload-time = "2025-10-10T15:41:22.076Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d1/7774ddfb8781c5224294c01a593ebce2ad3289b948061c9701bd1903264d/fastavro-1.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:b91a0fe5a173679a6c02d53ca22dcaad0a2c726b74507e0c1c2e71a7c3f79ef9", size = 450542, upload-time = "2025-10-10T15:41:23.333Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f0/10bd1a3d08667fa0739e2b451fe90e06df575ec8b8ba5d3135c70555c9bd/fastavro-1.12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:509818cb24b98a804fc80be9c5fed90f660310ae3d59382fc811bfa187122167", size = 1009057, upload-time = "2025-10-10T15:41:24.556Z" }, + { url = "https://files.pythonhosted.org/packages/78/ad/0d985bc99e1fa9e74c636658000ba38a5cd7f5ab2708e9c62eaf736ecf1a/fastavro-1.12.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:089e155c0c76e0d418d7e79144ce000524dd345eab3bc1e9c5ae69d500f71b14", size = 3391866, upload-time = "2025-10-10T15:41:26.882Z" }, + { url = "https://files.pythonhosted.org/packages/0d/9e/b4951dc84ebc34aac69afcbfbb22ea4a91080422ec2bfd2c06076ff1d419/fastavro-1.12.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44cbff7518901c91a82aab476fcab13d102e4999499df219d481b9e15f61af34", size = 3458005, upload-time = "2025-10-10T15:41:29.017Z" }, + { url = "https://files.pythonhosted.org/packages/af/f8/5a8df450a9f55ca8441f22ea0351d8c77809fc121498b6970daaaf667a21/fastavro-1.12.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a275e48df0b1701bb764b18a8a21900b24cf882263cb03d35ecdba636bbc830b", size = 3295258, upload-time = "2025-10-10T15:41:31.564Z" }, + { url = "https://files.pythonhosted.org/packages/99/b2/40f25299111d737e58b85696e91138a66c25b7334f5357e7ac2b0e8966f8/fastavro-1.12.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2de72d786eb38be6b16d556b27232b1bf1b2797ea09599507938cdb7a9fe3e7c", size = 3430328, upload-time = "2025-10-10T15:41:33.689Z" }, + { url = "https://files.pythonhosted.org/packages/e0/07/85157a7c57c5f8b95507d7829b5946561e5ee656ff80e9dd9a757f53ddaf/fastavro-1.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:9090f0dee63fe022ee9cc5147483366cc4171c821644c22da020d6b48f576b4f", size = 444140, upload-time = "2025-10-10T15:41:34.902Z" }, + { url = "https://files.pythonhosted.org/packages/bb/57/26d5efef9182392d5ac9f253953c856ccb66e4c549fd3176a1e94efb05c9/fastavro-1.12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:78df838351e4dff9edd10a1c41d1324131ffecbadefb9c297d612ef5363c049a", size = 1000599, upload-time = "2025-10-10T15:41:36.554Z" }, + { url = "https://files.pythonhosted.org/packages/33/cb/8ab55b21d018178eb126007a56bde14fd01c0afc11d20b5f2624fe01e698/fastavro-1.12.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:780476c23175d2ae457c52f45b9ffa9d504593499a36cd3c1929662bf5b7b14b", size = 3335933, upload-time = "2025-10-10T15:41:39.07Z" }, + { url = "https://files.pythonhosted.org/packages/fe/03/9c94ec9bf873eb1ffb0aa694f4e71940154e6e9728ddfdc46046d7e8ced4/fastavro-1.12.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0714b285160fcd515eb0455540f40dd6dac93bdeacdb03f24e8eac3d8aa51f8d", size = 3402066, upload-time = "2025-10-10T15:41:41.608Z" }, + { url = "https://files.pythonhosted.org/packages/75/c8/cb472347c5a584ccb8777a649ebb28278fccea39d005fc7df19996f41df8/fastavro-1.12.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a8bc2dcec5843d499f2489bfe0747999108f78c5b29295d877379f1972a3d41a", size = 3240038, upload-time = "2025-10-10T15:41:43.743Z" }, + { url = "https://files.pythonhosted.org/packages/e1/77/569ce9474c40304b3a09e109494e020462b83e405545b78069ddba5f614e/fastavro-1.12.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3b1921ac35f3d89090a5816b626cf46e67dbecf3f054131f84d56b4e70496f45", size = 3369398, upload-time = "2025-10-10T15:41:45.719Z" }, + { url = "https://files.pythonhosted.org/packages/4a/1f/9589e35e9ea68035385db7bdbf500d36b8891db474063fb1ccc8215ee37c/fastavro-1.12.1-cp313-cp313-win_amd64.whl", hash = "sha256:5aa777b8ee595b50aa084104cd70670bf25a7bbb9fd8bb5d07524b0785ee1699", size = 444220, upload-time = "2025-10-10T15:41:47.39Z" }, + { url = "https://files.pythonhosted.org/packages/6c/d2/78435fe737df94bd8db2234b2100f5453737cffd29adee2504a2b013de84/fastavro-1.12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c3d67c47f177e486640404a56f2f50b165fe892cc343ac3a34673b80cc7f1dd6", size = 1086611, upload-time = "2025-10-10T15:41:48.818Z" }, + { url = "https://files.pythonhosted.org/packages/b6/be/428f99b10157230ddac77ec8cc167005b29e2bd5cbe228345192bb645f30/fastavro-1.12.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5217f773492bac43dae15ff2931432bce2d7a80be7039685a78d3fab7df910bd", size = 3541001, upload-time = "2025-10-10T15:41:50.871Z" }, + { url = "https://files.pythonhosted.org/packages/16/08/a2eea4f20b85897740efe44887e1ac08f30dfa4bfc3de8962bdcbb21a5a1/fastavro-1.12.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:469fecb25cba07f2e1bfa4c8d008477cd6b5b34a59d48715e1b1a73f6160097d", size = 3432217, upload-time = "2025-10-10T15:41:53.149Z" }, + { url = "https://files.pythonhosted.org/packages/87/bb/b4c620b9eb6e9838c7f7e4b7be0762834443adf9daeb252a214e9ad3178c/fastavro-1.12.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d71c8aa841ef65cfab709a22bb887955f42934bced3ddb571e98fdbdade4c609", size = 3366742, upload-time = "2025-10-10T15:41:55.237Z" }, + { url = "https://files.pythonhosted.org/packages/3d/d1/e69534ccdd5368350646fea7d93be39e5f77c614cca825c990bd9ca58f67/fastavro-1.12.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:b81fc04e85dfccf7c028e0580c606e33aa8472370b767ef058aae2c674a90746", size = 3383743, upload-time = "2025-10-10T15:41:57.68Z" }, + { url = "https://files.pythonhosted.org/packages/58/54/b7b4a0c3fb5fcba38128542da1b26c4e6d69933c923f493548bdfd63ab6a/fastavro-1.12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:9445da127751ba65975d8e4bdabf36bfcfdad70fc35b2d988e3950cce0ec0e7c", size = 1001377, upload-time = "2025-10-10T15:41:59.241Z" }, + { url = "https://files.pythonhosted.org/packages/1e/4f/0e589089c7df0d8f57d7e5293fdc34efec9a3b758a0d4d0c99a7937e2492/fastavro-1.12.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ed924233272719b5d5a6a0b4d80ef3345fc7e84fc7a382b6232192a9112d38a6", size = 3320401, upload-time = "2025-10-10T15:42:01.682Z" }, + { url = "https://files.pythonhosted.org/packages/f9/19/260110d56194ae29d7e423a336fccea8bcd103196d00f0b364b732bdb84e/fastavro-1.12.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3616e2f0e1c9265e92954fa099db79c6e7817356d3ff34f4bcc92699ae99697c", size = 3350894, upload-time = "2025-10-10T15:42:04.073Z" }, + { url = "https://files.pythonhosted.org/packages/d0/96/58b0411e8be9694d5972bee3167d6c1fd1fdfdf7ce253c1a19a327208f4f/fastavro-1.12.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cb0337b42fd3c047fcf0e9b7597bd6ad25868de719f29da81eabb6343f08d399", size = 3229644, upload-time = "2025-10-10T15:42:06.221Z" }, + { url = "https://files.pythonhosted.org/packages/5b/db/38660660eac82c30471d9101f45b3acfdcbadfe42d8f7cdb129459a45050/fastavro-1.12.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:64961ab15b74b7c168717bbece5660e0f3d457837c3cc9d9145181d011199fa7", size = 3329704, upload-time = "2025-10-10T15:42:08.384Z" }, + { url = "https://files.pythonhosted.org/packages/9d/a9/1672910f458ecb30b596c9e59e41b7c00309b602a0494341451e92e62747/fastavro-1.12.1-cp314-cp314-win_amd64.whl", hash = "sha256:792356d320f6e757e89f7ac9c22f481e546c886454a6709247f43c0dd7058004", size = 452911, upload-time = "2025-10-10T15:42:09.795Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8d/2e15d0938ded1891b33eff252e8500605508b799c2e57188a933f0bd744c/fastavro-1.12.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:120aaf82ac19d60a1016afe410935fe94728752d9c2d684e267e5b7f0e70f6d9", size = 3541999, upload-time = "2025-10-10T15:42:11.794Z" }, + { url = "https://files.pythonhosted.org/packages/a7/1c/6dfd082a205be4510543221b734b1191299e6a1810c452b6bc76dfa6968e/fastavro-1.12.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6a3462934b20a74f9ece1daa49c2e4e749bd9a35fa2657b53bf62898fba80f5", size = 3433972, upload-time = "2025-10-10T15:42:14.485Z" }, + { url = "https://files.pythonhosted.org/packages/24/90/9de694625a1a4b727b1ad0958d220cab25a9b6cf7f16a5c7faa9ea7b2261/fastavro-1.12.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1f81011d54dd47b12437b51dd93a70a9aa17b61307abf26542fc3c13efbc6c51", size = 3368752, upload-time = "2025-10-10T15:42:16.618Z" }, + { url = "https://files.pythonhosted.org/packages/fa/93/b44f67589e4d439913dab6720f7e3507b0fa8b8e56d06f6fc875ced26afb/fastavro-1.12.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:43ded16b3f4a9f1a42f5970c2aa618acb23ea59c4fcaa06680bdf470b255e5a8", size = 3386636, upload-time = "2025-10-10T15:42:18.974Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b8/67f5682cd59cb59ec6eecb5479b132caaf69709d1e1ceda4f92d8c69d7f1/fastavro-1.12.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:02281432dcb11c78b3280da996eff61ee0eff39c5de06c6e0fbf19275093e6d4", size = 1002311, upload-time = "2025-10-10T15:42:20.415Z" }, + { url = "https://files.pythonhosted.org/packages/93/38/2f15a7ad24e4b6e0239016c068f142358732bf8ead0315ee926b88634bec/fastavro-1.12.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4128978b930aaf930332db4b3acc290783183f3be06a241ae4a482f3ed8ce892", size = 3195871, upload-time = "2025-10-10T15:42:22.675Z" }, + { url = "https://files.pythonhosted.org/packages/c1/e5/6fc0250b3006b1b42c1ab9f0511ccd44e1aeb15a63a77fc780ee97f58797/fastavro-1.12.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:546ffffda6610fca672f0ed41149808e106d8272bb246aa7539fa8bb6f117f17", size = 3204127, upload-time = "2025-10-10T15:42:24.855Z" }, + { url = "https://files.pythonhosted.org/packages/c6/43/1f3909eb096eb1066e416f0875abe783b73fabe823ad616f6956b3e80e84/fastavro-1.12.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a7d840ccd9aacada3ddc80fbcc4ea079b658107fe62e9d289a0de9d54e95d366", size = 3135604, upload-time = "2025-10-10T15:42:28.899Z" }, + { url = "https://files.pythonhosted.org/packages/e6/61/ec083a3a5d7c2b97d0e2c9e137a6e667583afe884af0e49318f7ee7eaa5a/fastavro-1.12.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3100ad643e7fa658469a2a2db229981c1a000ff16b8037c0b58ce3ec4d2107e8", size = 3210932, upload-time = "2025-10-10T15:42:30.891Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ba/b814fb09b32c8f3059451c33bb11d55eb23188e6a499f5dde628d13bc076/fastavro-1.12.1-cp39-cp39-win_amd64.whl", hash = "sha256:a38607444281619eda3a9c1be9f5397634012d1b237142eee1540e810b30ac8b", size = 510328, upload-time = "2025-10-10T15:42:32.415Z" }, +] + +[[package]] +name = "filelock" +version = "3.19.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/40/bb/0ab3e58d22305b6f5440629d20683af28959bf793d98d11950e305c1c326/filelock-3.19.1.tar.gz", hash = "sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58", size = 17687, upload-time = "2025-08-14T16:56:03.016Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/14/42b2651a2f46b022ccd948bca9f2d5af0fd8929c4eec235b8d6d844fbe67/filelock-3.19.1-py3-none-any.whl", hash = "sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d", size = 15988, upload-time = "2025-08-14T16:56:01.633Z" }, +] + +[[package]] +name = "filelock" +version = "3.20.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.10' and python_full_version < '3.14'", +] +sdist = { url = "https://files.pythonhosted.org/packages/c1/e0/a75dbe4bca1e7d41307323dad5ea2efdd95408f74ab2de8bd7dba9b51a1a/filelock-3.20.2.tar.gz", hash = "sha256:a2241ff4ddde2a7cebddf78e39832509cb045d18ec1a09d7248d6bfc6bfbbe64", size = 19510, upload-time = "2026-01-02T15:33:32.582Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/30/ab407e2ec752aa541704ed8f93c11e2a5d92c168b8a755d818b74a3c5c2d/filelock-3.20.2-py3-none-any.whl", hash = "sha256:fbba7237d6ea277175a32c54bb71ef814a8546d8601269e1bfc388de333974e8", size = 16697, upload-time = "2026-01-02T15:33:31.133Z" }, +] + +[[package]] +name = "flask" +version = "3.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "blinker" }, + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "click", version = "8.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "itsdangerous" }, + { name = "jinja2" }, + { name = "markupsafe" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/6d/cfe3c0fcc5e477df242b98bfe186a4c34357b4847e87ecaef04507332dab/flask-3.1.2.tar.gz", hash = "sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87", size = 720160, upload-time = "2025-08-19T21:03:21.205Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/f9/7f9263c5695f4bd0023734af91bedb2ff8209e8de6ead162f35d8dc762fd/flask-3.1.2-py3-none-any.whl", hash = "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c", size = 103308, upload-time = "2025-08-19T21:03:19.499Z" }, +] + +[[package]] +name = "fsspec" +version = "2025.10.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/24/7f/2747c0d332b9acfa75dc84447a066fdf812b5a6b8d30472b74d309bfe8cb/fsspec-2025.10.0.tar.gz", hash = "sha256:b6789427626f068f9a83ca4e8a3cc050850b6c0f71f99ddb4f542b8266a26a59", size = 309285, upload-time = "2025-10-30T14:58:44.036Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/02/a6b21098b1d5d6249b7c5ab69dde30108a71e4e819d4a9778f1de1d5b70d/fsspec-2025.10.0-py3-none-any.whl", hash = "sha256:7c7712353ae7d875407f97715f0e1ffcc21e33d5b24556cb1e090ae9409ec61d", size = 200966, upload-time = "2025-10-30T14:58:42.53Z" }, +] + +[[package]] +name = "fsspec" +version = "2025.12.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.10' and python_full_version < '3.14'", +] +sdist = { url = "https://files.pythonhosted.org/packages/b6/27/954057b0d1f53f086f681755207dda6de6c660ce133c829158e8e8fe7895/fsspec-2025.12.0.tar.gz", hash = "sha256:c505de011584597b1060ff778bb664c1bc022e87921b0e4f10cc9c44f9635973", size = 309748, upload-time = "2025-12-03T15:23:42.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/c7/b64cae5dba3a1b138d7123ec36bb5ccd39d39939f18454407e5468f4763f/fsspec-2025.12.0-py3-none-any.whl", hash = "sha256:8bf1fe301b7d8acfa6e8571e3b1c3d158f909666642431cc78a1b7b4dbc5ec5b", size = 201422, upload-time = "2025-12-03T15:23:41.434Z" }, +] + +[[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 = "gitdb" +version = "4.0.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "smmap" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" }, +] + +[[package]] +name = "gitpython" +version = "3.1.45" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "gitdb" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9a/c8/dd58967d119baab745caec2f9d853297cec1989ec1d63f677d3880632b88/gitpython-3.1.45.tar.gz", hash = "sha256:85b0ee964ceddf211c41b9f27a49086010a190fd8132a24e21f362a4b36a791c", size = 215076, upload-time = "2025-07-24T03:45:54.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/61/d4b89fec821f72385526e1b9d9a3a0385dda4a72b206d28049e2c7cd39b8/gitpython-3.1.45-py3-none-any.whl", hash = "sha256:8908cb2e02fb3b93b7eb0f2827125cb699869470432cc885f019b8fd0fccff77", size = 208168, upload-time = "2025-07-24T03:45:52.517Z" }, +] + +[[package]] +name = "google-auth" +version = "2.47.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1-modules" }, + { name = "rsa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/3c/ec64b9a275ca22fa1cd3b6e77fefcf837b0732c890aa32d2bd21313d9b33/google_auth-2.47.0.tar.gz", hash = "sha256:833229070a9dfee1a353ae9877dcd2dec069a8281a4e72e72f77d4a70ff945da", size = 323719, upload-time = "2026-01-06T21:55:31.045Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/18/79e9008530b79527e0d5f79e7eef08d3b179b7f851cfd3a2f27822fbdfa9/google_auth-2.47.0-py3-none-any.whl", hash = "sha256:c516d68336bfde7cf0da26aab674a36fedcf04b37ac4edd59c597178760c3498", size = 234867, upload-time = "2026-01-06T21:55:28.6Z" }, +] + +[package.optional-dependencies] +requests = [ + { name = "requests", marker = "python_full_version >= '3.10'" }, +] + +[[package]] +name = "google-genai" +version = "1.47.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "anyio", marker = "python_full_version < '3.10'" }, + { name = "google-auth", marker = "python_full_version < '3.10'" }, + { name = "httpx", marker = "python_full_version < '3.10'" }, + { name = "pydantic", marker = "python_full_version < '3.10'" }, + { name = "requests", marker = "python_full_version < '3.10'" }, + { name = "tenacity", marker = "python_full_version < '3.10'" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, + { name = "websockets", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/97/784fba9bc6c41263ff90cb9063eadfdd755dde79cfa5a8d0e397b067dcf9/google_genai-1.47.0.tar.gz", hash = "sha256:ecece00d0a04e6739ea76cc8dad82ec9593d9380aaabef078990e60574e5bf59", size = 241471, upload-time = "2025-10-29T22:01:02.88Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/ef/e080e8d67c270ea320956bb911a9359664fc46d3b87d1f029decd33e5c4c/google_genai-1.47.0-py3-none-any.whl", hash = "sha256:e3851237556cbdec96007d8028b4b1f2425cdc5c099a8dc36b72a57e42821b60", size = 241506, upload-time = "2025-10-29T22:01:00.982Z" }, +] + +[[package]] +name = "google-genai" +version = "1.57.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.10' and python_full_version < '3.14'", +] +dependencies = [ + { name = "anyio", marker = "python_full_version >= '3.10'" }, + { name = "distro", marker = "python_full_version >= '3.10'" }, + { name = "google-auth", extra = ["requests"], marker = "python_full_version >= '3.10'" }, + { name = "httpx", marker = "python_full_version >= '3.10'" }, + { name = "pydantic", marker = "python_full_version >= '3.10'" }, + { name = "requests", marker = "python_full_version >= '3.10'" }, + { name = "sniffio", marker = "python_full_version >= '3.10'" }, + { name = "tenacity", marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.10'" }, + { name = "websockets", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2b/b4/8251c2d2576224a4b51a8ab6159820f9200b8da28ff555c78ee15607096e/google_genai-1.57.0.tar.gz", hash = "sha256:0ff9c36b8d68abfbdbd13b703ece926de5f3e67955666b36315ecf669b94a826", size = 485648, upload-time = "2026-01-07T20:38:20.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/02/858bdae08e2184b6afe0b18bc3113318522c9cf326a5a1698055edd31f88/google_genai-1.57.0-py3-none-any.whl", hash = "sha256:d63c7a89a1f549c4d14032f41a0cdb4b6fe3f565e2eee6b5e0907a0aeceabefd", size = 713323, upload-time = "2026-01-07T20:38:18.051Z" }, +] + +[[package]] +name = "graphql-core" +version = "3.2.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/9b/037a640a2983b09aed4a823f9cf1729e6d780b0671f854efa4727a7affbe/graphql_core-3.2.7.tar.gz", hash = "sha256:27b6904bdd3b43f2a0556dad5d579bdfdeab1f38e8e8788e555bdcb586a6f62c", size = 513484, upload-time = "2025-11-01T22:30:40.436Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/14/933037032608787fb92e365883ad6a741c235e0ff992865ec5d904a38f1e/graphql_core-3.2.7-py3-none-any.whl", hash = "sha256:17fc8f3ca4a42913d8e24d9ac9f08deddf0a0b2483076575757f6c412ead2ec0", size = 207262, upload-time = "2025-11-01T22:30:38.912Z" }, +] + +[[package]] +name = "greenlet" +version = "3.2.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/03/b8/704d753a5a45507a7aab61f18db9509302ed3d0a27ac7e0359ec2905b1a6/greenlet-3.2.4.tar.gz", hash = "sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d", size = 188260, upload-time = "2025-08-07T13:24:33.51Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/ed/6bfa4109fcb23a58819600392564fea69cdc6551ffd5e69ccf1d52a40cbc/greenlet-3.2.4-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:8c68325b0d0acf8d91dde4e6f930967dd52a5302cd4062932a6b2e7c2969f47c", size = 271061, upload-time = "2025-08-07T13:17:15.373Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fc/102ec1a2fc015b3a7652abab7acf3541d58c04d3d17a8d3d6a44adae1eb1/greenlet-3.2.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:94385f101946790ae13da500603491f04a76b6e4c059dab271b3ce2e283b2590", size = 629475, upload-time = "2025-08-07T13:42:54.009Z" }, + { url = "https://files.pythonhosted.org/packages/c5/26/80383131d55a4ac0fb08d71660fd77e7660b9db6bdb4e8884f46d9f2cc04/greenlet-3.2.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f10fd42b5ee276335863712fa3da6608e93f70629c631bf77145021600abc23c", size = 640802, upload-time = "2025-08-07T13:45:25.52Z" }, + { url = "https://files.pythonhosted.org/packages/9f/7c/e7833dbcd8f376f3326bd728c845d31dcde4c84268d3921afcae77d90d08/greenlet-3.2.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c8c9e331e58180d0d83c5b7999255721b725913ff6bc6cf39fa2a45841a4fd4b", size = 636703, upload-time = "2025-08-07T13:53:12.622Z" }, + { url = "https://files.pythonhosted.org/packages/e9/49/547b93b7c0428ede7b3f309bc965986874759f7d89e4e04aeddbc9699acb/greenlet-3.2.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:58b97143c9cc7b86fc458f215bd0932f1757ce649e05b640fea2e79b54cedb31", size = 635417, upload-time = "2025-08-07T13:18:25.189Z" }, + { url = "https://files.pythonhosted.org/packages/7f/91/ae2eb6b7979e2f9b035a9f612cf70f1bf54aad4e1d125129bef1eae96f19/greenlet-3.2.4-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c2ca18a03a8cfb5b25bc1cbe20f3d9a4c80d8c3b13ba3df49ac3961af0b1018d", size = 584358, upload-time = "2025-08-07T13:18:23.708Z" }, + { url = "https://files.pythonhosted.org/packages/f7/85/433de0c9c0252b22b16d413c9407e6cb3b41df7389afc366ca204dbc1393/greenlet-3.2.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9fe0a28a7b952a21e2c062cd5756d34354117796c6d9215a87f55e38d15402c5", size = 1113550, upload-time = "2025-08-07T13:42:37.467Z" }, + { url = "https://files.pythonhosted.org/packages/a1/8d/88f3ebd2bc96bf7747093696f4335a0a8a4c5acfcf1b757717c0d2474ba3/greenlet-3.2.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8854167e06950ca75b898b104b63cc646573aa5fef1353d4508ecdd1ee76254f", size = 1137126, upload-time = "2025-08-07T13:18:20.239Z" }, + { url = "https://files.pythonhosted.org/packages/f1/29/74242b7d72385e29bcc5563fba67dad94943d7cd03552bac320d597f29b2/greenlet-3.2.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f47617f698838ba98f4ff4189aef02e7343952df3a615f847bb575c3feb177a7", size = 1544904, upload-time = "2025-11-04T12:42:04.763Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e2/1572b8eeab0f77df5f6729d6ab6b141e4a84ee8eb9bc8c1e7918f94eda6d/greenlet-3.2.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af41be48a4f60429d5cad9d22175217805098a9ef7c40bfef44f7669fb9d74d8", size = 1611228, upload-time = "2025-11-04T12:42:08.423Z" }, + { url = "https://files.pythonhosted.org/packages/d6/6f/b60b0291d9623c496638c582297ead61f43c4b72eef5e9c926ef4565ec13/greenlet-3.2.4-cp310-cp310-win_amd64.whl", hash = "sha256:73f49b5368b5359d04e18d15828eecc1806033db5233397748f4ca813ff1056c", size = 298654, upload-time = "2025-08-07T13:50:00.469Z" }, + { url = "https://files.pythonhosted.org/packages/a4/de/f28ced0a67749cac23fecb02b694f6473f47686dff6afaa211d186e2ef9c/greenlet-3.2.4-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:96378df1de302bc38e99c3a9aa311967b7dc80ced1dcc6f171e99842987882a2", size = 272305, upload-time = "2025-08-07T13:15:41.288Z" }, + { url = "https://files.pythonhosted.org/packages/09/16/2c3792cba130000bf2a31c5272999113f4764fd9d874fb257ff588ac779a/greenlet-3.2.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1ee8fae0519a337f2329cb78bd7a8e128ec0f881073d43f023c7b8d4831d5246", size = 632472, upload-time = "2025-08-07T13:42:55.044Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/95d48d7e3d433e6dae5b1682e4292242a53f22df82e6d3dda81b1701a960/greenlet-3.2.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:94abf90142c2a18151632371140b3dba4dee031633fe614cb592dbb6c9e17bc3", size = 644646, upload-time = "2025-08-07T13:45:26.523Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5e/405965351aef8c76b8ef7ad370e5da58d57ef6068df197548b015464001a/greenlet-3.2.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:4d1378601b85e2e5171b99be8d2dc85f594c79967599328f95c1dc1a40f1c633", size = 640519, upload-time = "2025-08-07T13:53:13.928Z" }, + { url = "https://files.pythonhosted.org/packages/25/5d/382753b52006ce0218297ec1b628e048c4e64b155379331f25a7316eb749/greenlet-3.2.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0db5594dce18db94f7d1650d7489909b57afde4c580806b8d9203b6e79cdc079", size = 639707, upload-time = "2025-08-07T13:18:27.146Z" }, + { url = "https://files.pythonhosted.org/packages/1f/8e/abdd3f14d735b2929290a018ecf133c901be4874b858dd1c604b9319f064/greenlet-3.2.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2523e5246274f54fdadbce8494458a2ebdcdbc7b802318466ac5606d3cded1f8", size = 587684, upload-time = "2025-08-07T13:18:25.164Z" }, + { url = "https://files.pythonhosted.org/packages/5d/65/deb2a69c3e5996439b0176f6651e0052542bb6c8f8ec2e3fba97c9768805/greenlet-3.2.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1987de92fec508535687fb807a5cea1560f6196285a4cde35c100b8cd632cc52", size = 1116647, upload-time = "2025-08-07T13:42:38.655Z" }, + { url = "https://files.pythonhosted.org/packages/3f/cc/b07000438a29ac5cfb2194bfc128151d52f333cee74dd7dfe3fb733fc16c/greenlet-3.2.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:55e9c5affaa6775e2c6b67659f3a71684de4c549b3dd9afca3bc773533d284fa", size = 1142073, upload-time = "2025-08-07T13:18:21.737Z" }, + { url = "https://files.pythonhosted.org/packages/67/24/28a5b2fa42d12b3d7e5614145f0bd89714c34c08be6aabe39c14dd52db34/greenlet-3.2.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c9c6de1940a7d828635fbd254d69db79e54619f165ee7ce32fda763a9cb6a58c", size = 1548385, upload-time = "2025-11-04T12:42:11.067Z" }, + { url = "https://files.pythonhosted.org/packages/6a/05/03f2f0bdd0b0ff9a4f7b99333d57b53a7709c27723ec8123056b084e69cd/greenlet-3.2.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03c5136e7be905045160b1b9fdca93dd6727b180feeafda6818e6496434ed8c5", size = 1613329, upload-time = "2025-11-04T12:42:12.928Z" }, + { url = "https://files.pythonhosted.org/packages/d8/0f/30aef242fcab550b0b3520b8e3561156857c94288f0332a79928c31a52cf/greenlet-3.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:9c40adce87eaa9ddb593ccb0fa6a07caf34015a29bf8d344811665b573138db9", size = 299100, upload-time = "2025-08-07T13:44:12.287Z" }, + { url = "https://files.pythonhosted.org/packages/44/69/9b804adb5fd0671f367781560eb5eb586c4d495277c93bde4307b9e28068/greenlet-3.2.4-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3b67ca49f54cede0186854a008109d6ee71f66bd57bb36abd6d0a0267b540cdd", size = 274079, upload-time = "2025-08-07T13:15:45.033Z" }, + { url = "https://files.pythonhosted.org/packages/46/e9/d2a80c99f19a153eff70bc451ab78615583b8dac0754cfb942223d2c1a0d/greenlet-3.2.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddf9164e7a5b08e9d22511526865780a576f19ddd00d62f8a665949327fde8bb", size = 640997, upload-time = "2025-08-07T13:42:56.234Z" }, + { url = "https://files.pythonhosted.org/packages/3b/16/035dcfcc48715ccd345f3a93183267167cdd162ad123cd93067d86f27ce4/greenlet-3.2.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f28588772bb5fb869a8eb331374ec06f24a83a9c25bfa1f38b6993afe9c1e968", size = 655185, upload-time = "2025-08-07T13:45:27.624Z" }, + { url = "https://files.pythonhosted.org/packages/31/da/0386695eef69ffae1ad726881571dfe28b41970173947e7c558d9998de0f/greenlet-3.2.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:5c9320971821a7cb77cfab8d956fa8e39cd07ca44b6070db358ceb7f8797c8c9", size = 649926, upload-time = "2025-08-07T13:53:15.251Z" }, + { url = "https://files.pythonhosted.org/packages/68/88/69bf19fd4dc19981928ceacbc5fd4bb6bc2215d53199e367832e98d1d8fe/greenlet-3.2.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c60a6d84229b271d44b70fb6e5fa23781abb5d742af7b808ae3f6efd7c9c60f6", size = 651839, upload-time = "2025-08-07T13:18:30.281Z" }, + { url = "https://files.pythonhosted.org/packages/19/0d/6660d55f7373b2ff8152401a83e02084956da23ae58cddbfb0b330978fe9/greenlet-3.2.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b3812d8d0c9579967815af437d96623f45c0f2ae5f04e366de62a12d83a8fb0", size = 607586, upload-time = "2025-08-07T13:18:28.544Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1a/c953fdedd22d81ee4629afbb38d2f9d71e37d23caace44775a3a969147d4/greenlet-3.2.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:abbf57b5a870d30c4675928c37278493044d7c14378350b3aa5d484fa65575f0", size = 1123281, upload-time = "2025-08-07T13:42:39.858Z" }, + { url = "https://files.pythonhosted.org/packages/3f/c7/12381b18e21aef2c6bd3a636da1088b888b97b7a0362fac2e4de92405f97/greenlet-3.2.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:20fb936b4652b6e307b8f347665e2c615540d4b42b3b4c8a321d8286da7e520f", size = 1151142, upload-time = "2025-08-07T13:18:22.981Z" }, + { url = "https://files.pythonhosted.org/packages/27/45/80935968b53cfd3f33cf99ea5f08227f2646e044568c9b1555b58ffd61c2/greenlet-3.2.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ee7a6ec486883397d70eec05059353b8e83eca9168b9f3f9a361971e77e0bcd0", size = 1564846, upload-time = "2025-11-04T12:42:15.191Z" }, + { url = "https://files.pythonhosted.org/packages/69/02/b7c30e5e04752cb4db6202a3858b149c0710e5453b71a3b2aec5d78a1aab/greenlet-3.2.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:326d234cbf337c9c3def0676412eb7040a35a768efc92504b947b3e9cfc7543d", size = 1633814, upload-time = "2025-11-04T12:42:17.175Z" }, + { url = "https://files.pythonhosted.org/packages/e9/08/b0814846b79399e585f974bbeebf5580fbe59e258ea7be64d9dfb253c84f/greenlet-3.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:a7d4e128405eea3814a12cc2605e0e6aedb4035bf32697f72deca74de4105e02", size = 299899, upload-time = "2025-08-07T13:38:53.448Z" }, + { url = "https://files.pythonhosted.org/packages/49/e8/58c7f85958bda41dafea50497cbd59738c5c43dbbea5ee83d651234398f4/greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31", size = 272814, upload-time = "2025-08-07T13:15:50.011Z" }, + { url = "https://files.pythonhosted.org/packages/62/dd/b9f59862e9e257a16e4e610480cfffd29e3fae018a68c2332090b53aac3d/greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945", size = 641073, upload-time = "2025-08-07T13:42:57.23Z" }, + { url = "https://files.pythonhosted.org/packages/f7/0b/bc13f787394920b23073ca3b6c4a7a21396301ed75a655bcb47196b50e6e/greenlet-3.2.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:710638eb93b1fa52823aa91bf75326f9ecdfd5e0466f00789246a5280f4ba0fc", size = 655191, upload-time = "2025-08-07T13:45:29.752Z" }, + { url = "https://files.pythonhosted.org/packages/f2/d6/6adde57d1345a8d0f14d31e4ab9c23cfe8e2cd39c3baf7674b4b0338d266/greenlet-3.2.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c5111ccdc9c88f423426df3fd1811bfc40ed66264d35aa373420a34377efc98a", size = 649516, upload-time = "2025-08-07T13:53:16.314Z" }, + { url = "https://files.pythonhosted.org/packages/7f/3b/3a3328a788d4a473889a2d403199932be55b1b0060f4ddd96ee7cdfcad10/greenlet-3.2.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76383238584e9711e20ebe14db6c88ddcedc1829a9ad31a584389463b5aa504", size = 652169, upload-time = "2025-08-07T13:18:32.861Z" }, + { url = "https://files.pythonhosted.org/packages/ee/43/3cecdc0349359e1a527cbf2e3e28e5f8f06d3343aaf82ca13437a9aa290f/greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671", size = 610497, upload-time = "2025-08-07T13:18:31.636Z" }, + { url = "https://files.pythonhosted.org/packages/b8/19/06b6cf5d604e2c382a6f31cafafd6f33d5dea706f4db7bdab184bad2b21d/greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b", size = 1121662, upload-time = "2025-08-07T13:42:41.117Z" }, + { url = "https://files.pythonhosted.org/packages/a2/15/0d5e4e1a66fab130d98168fe984c509249c833c1a3c16806b90f253ce7b9/greenlet-3.2.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae", size = 1149210, upload-time = "2025-08-07T13:18:24.072Z" }, + { url = "https://files.pythonhosted.org/packages/1c/53/f9c440463b3057485b8594d7a638bed53ba531165ef0ca0e6c364b5cc807/greenlet-3.2.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e343822feb58ac4d0a1211bd9399de2b3a04963ddeec21530fc426cc121f19b", size = 1564759, upload-time = "2025-11-04T12:42:19.395Z" }, + { url = "https://files.pythonhosted.org/packages/47/e4/3bb4240abdd0a8d23f4f88adec746a3099f0d86bfedb623f063b2e3b4df0/greenlet-3.2.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca7f6f1f2649b89ce02f6f229d7c19f680a6238af656f61e0115b24857917929", size = 1634288, upload-time = "2025-11-04T12:42:21.174Z" }, + { url = "https://files.pythonhosted.org/packages/0b/55/2321e43595e6801e105fcfdee02b34c0f996eb71e6ddffca6b10b7e1d771/greenlet-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b", size = 299685, upload-time = "2025-08-07T13:24:38.824Z" }, + { url = "https://files.pythonhosted.org/packages/22/5c/85273fd7cc388285632b0498dbbab97596e04b154933dfe0f3e68156c68c/greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0", size = 273586, upload-time = "2025-08-07T13:16:08.004Z" }, + { url = "https://files.pythonhosted.org/packages/d1/75/10aeeaa3da9332c2e761e4c50d4c3556c21113ee3f0afa2cf5769946f7a3/greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f", size = 686346, upload-time = "2025-08-07T13:42:59.944Z" }, + { url = "https://files.pythonhosted.org/packages/c0/aa/687d6b12ffb505a4447567d1f3abea23bd20e73a5bed63871178e0831b7a/greenlet-3.2.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c17b6b34111ea72fc5a4e4beec9711d2226285f0386ea83477cbb97c30a3f3a5", size = 699218, upload-time = "2025-08-07T13:45:30.969Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8b/29aae55436521f1d6f8ff4e12fb676f3400de7fcf27fccd1d4d17fd8fecd/greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1", size = 694659, upload-time = "2025-08-07T13:53:17.759Z" }, + { url = "https://files.pythonhosted.org/packages/92/2e/ea25914b1ebfde93b6fc4ff46d6864564fba59024e928bdc7de475affc25/greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735", size = 695355, upload-time = "2025-08-07T13:18:34.517Z" }, + { url = "https://files.pythonhosted.org/packages/72/60/fc56c62046ec17f6b0d3060564562c64c862948c9d4bc8aa807cf5bd74f4/greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337", size = 657512, upload-time = "2025-08-07T13:18:33.969Z" }, + { url = "https://files.pythonhosted.org/packages/23/6e/74407aed965a4ab6ddd93a7ded3180b730d281c77b765788419484cdfeef/greenlet-3.2.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2917bdf657f5859fbf3386b12d68ede4cf1f04c90c3a6bc1f013dd68a22e2269", size = 1612508, upload-time = "2025-11-04T12:42:23.427Z" }, + { url = "https://files.pythonhosted.org/packages/0d/da/343cd760ab2f92bac1845ca07ee3faea9fe52bee65f7bcb19f16ad7de08b/greenlet-3.2.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:015d48959d4add5d6c9f6c5210ee3803a830dce46356e3bc326d6776bde54681", size = 1680760, upload-time = "2025-11-04T12:42:25.341Z" }, + { url = "https://files.pythonhosted.org/packages/e3/a5/6ddab2b4c112be95601c13428db1d8b6608a8b6039816f2ba09c346c08fc/greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01", size = 303425, upload-time = "2025-08-07T13:32:27.59Z" }, + { url = "https://files.pythonhosted.org/packages/f7/c0/93885c4106d2626bf51fdec377d6aef740dfa5c4877461889a7cf8e565cc/greenlet-3.2.4-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:b6a7c19cf0d2742d0809a4c05975db036fdff50cd294a93632d6a310bf9ac02c", size = 269859, upload-time = "2025-08-07T13:16:16.003Z" }, + { url = "https://files.pythonhosted.org/packages/4d/f5/33f05dc3ba10a02dedb1485870cf81c109227d3d3aa280f0e48486cac248/greenlet-3.2.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:27890167f55d2387576d1f41d9487ef171849ea0359ce1510ca6e06c8bece11d", size = 627610, upload-time = "2025-08-07T13:43:01.345Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a7/9476decef51a0844195f99ed5dc611d212e9b3515512ecdf7321543a7225/greenlet-3.2.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:18d9260df2b5fbf41ae5139e1be4e796d99655f023a636cd0e11e6406cca7d58", size = 639417, upload-time = "2025-08-07T13:45:32.094Z" }, + { url = "https://files.pythonhosted.org/packages/bd/e0/849b9159cbb176f8c0af5caaff1faffdece7a8417fcc6fe1869770e33e21/greenlet-3.2.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:671df96c1f23c4a0d4077a325483c1503c96a1b7d9db26592ae770daa41233d4", size = 634751, upload-time = "2025-08-07T13:53:18.848Z" }, + { url = "https://files.pythonhosted.org/packages/5f/d3/844e714a9bbd39034144dca8b658dcd01839b72bb0ec7d8014e33e3705f0/greenlet-3.2.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:16458c245a38991aa19676900d48bd1a6f2ce3e16595051a4db9d012154e8433", size = 634020, upload-time = "2025-08-07T13:18:36.841Z" }, + { url = "https://files.pythonhosted.org/packages/6b/4c/f3de2a8de0e840ecb0253ad0dc7e2bb3747348e798ec7e397d783a3cb380/greenlet-3.2.4-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9913f1a30e4526f432991f89ae263459b1c64d1608c0d22a5c79c287b3c70df", size = 582817, upload-time = "2025-08-07T13:18:35.48Z" }, + { url = "https://files.pythonhosted.org/packages/89/80/7332915adc766035c8980b161c2e5d50b2f941f453af232c164cff5e0aeb/greenlet-3.2.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b90654e092f928f110e0007f572007c9727b5265f7632c2fa7415b4689351594", size = 1111985, upload-time = "2025-08-07T13:42:42.425Z" }, + { url = "https://files.pythonhosted.org/packages/66/71/1928e2c80197353bcb9b50aa19c4d8e26ee6d7a900c564907665cf4b9a41/greenlet-3.2.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:81701fd84f26330f0d5f4944d4e92e61afe6319dcd9775e39396e39d7c3e5f98", size = 1136137, upload-time = "2025-08-07T13:18:26.168Z" }, + { url = "https://files.pythonhosted.org/packages/4b/bf/7bd33643e48ed45dcc0e22572f650767832bd4e1287f97434943cc402148/greenlet-3.2.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:28a3c6b7cd72a96f61b0e4b2a36f681025b60ae4779cc73c1535eb5f29560b10", size = 1542941, upload-time = "2025-11-04T12:42:27.427Z" }, + { url = "https://files.pythonhosted.org/packages/9b/74/4bc433f91d0d09a1c22954a371f9df928cb85e72640870158853a83415e5/greenlet-3.2.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:52206cd642670b0b320a1fd1cbfd95bca0e043179c1d8a045f2c6109dfe973be", size = 1609685, upload-time = "2025-11-04T12:42:29.242Z" }, + { url = "https://files.pythonhosted.org/packages/89/48/a5dc74dde38aeb2b15d418cec76ed50e1dd3d620ccda84d8199703248968/greenlet-3.2.4-cp39-cp39-win32.whl", hash = "sha256:65458b409c1ed459ea899e939f0e1cdb14f58dbc803f2f93c5eab5694d32671b", size = 281400, upload-time = "2025-08-07T14:02:20.263Z" }, + { url = "https://files.pythonhosted.org/packages/e5/44/342c4591db50db1076b8bda86ed0ad59240e3e1da17806a4cf10a6d0e447/greenlet-3.2.4-cp39-cp39-win_amd64.whl", hash = "sha256:d2e685ade4dafd447ede19c31277a224a239a0a1a4eca4e6390efedf20260cfb", size = 298533, upload-time = "2025-08-07T13:56:34.168Z" }, +] + +[[package]] +name = "greenlet" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.10' and python_full_version < '3.14'", +] +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/f0/d0/0ae86792fb212e4384041e0ef8e7bc66f59a54912ce407d26a966ed2914d/greenlet-3.3.0-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b299a0cb979f5d7197442dccc3aee67fce53500cd88951b7e6c35575701c980b", size = 597403, upload-time = "2025-12-04T15:07:10.831Z" }, + { 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/80/d7/db0a5085035d05134f8c089643da2b44cc9b80647c39e93129c5ef170d8f/greenlet-3.3.0-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:670d0f94cd302d81796e37299bcd04b95d62403883b24225c6b5271466612f45", size = 601098, upload-time = "2025-12-04T15:07:11.898Z" }, + { 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/77/cb/43692bcd5f7a0da6ec0ec6d58ee7cddb606d055ce94a62ac9b1aa481e969/greenlet-3.3.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c024b1e5696626890038e34f76140ed1daf858e37496d33f2af57f06189e70d7", size = 622297, upload-time = "2025-12-04T15:07:13.552Z" }, + { 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/79/07/c47a82d881319ec18a4510bb30463ed6891f2ad2c1901ed5ec23d3de351f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30a6e28487a790417d036088b3bcb3f3ac7d8babaa7d0139edbaddebf3af9492", size = 624311, upload-time = "2025-12-04T15:07:14.697Z" }, + { 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/93/79/d2c70cae6e823fac36c3bbc9077962105052b7ef81db2f01ec3b9bf17e2b/greenlet-3.3.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dcd2bdbd444ff340e8d6bdf54d2f206ccddbb3ccfdcd3c25bf4afaa7b8f0cf45", size = 671388, upload-time = "2025-12-04T15:07:15.789Z" }, + { 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/69/cc/1e4bae2e45ca2fa55299f4e85854606a78ecc37fead20d69322f96000504/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2662433acbca297c9153a4023fe2161c8dcfdcc91f10433171cf7e7d94ba2221", size = 662506, upload-time = "2025-12-04T15:07:16.906Z" }, + { 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" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ec/d7/6c09dd7ce4c7837e4cdb11dce980cb45ae3cd87677298dc3b781b6bce7d3/griffe-1.14.0.tar.gz", hash = "sha256:9d2a15c1eca966d68e00517de5d69dd1bc5c9f2335ef6c1775362ba5b8651a13", size = 424684, upload-time = "2025-09-05T15:02:29.167Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/b1/9ff6578d789a89812ff21e4e0f80ffae20a65d5dd84e7a17873fe3b365be/griffe-1.14.0-py3-none-any.whl", hash = "sha256:0e9d52832cccf0f7188cfe585ba962d2674b241c01916d780925df34873bceb0", size = 144439, upload-time = "2025-09-05T15:02:27.511Z" }, +] + +[[package]] +name = "griffe" +version = "1.15.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.10' and python_full_version < '3.14'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0d/0c/3a471b6e31951dce2360477420d0a8d1e00dea6cf33b70f3e8c3ab6e28e1/griffe-1.15.0.tar.gz", hash = "sha256:7726e3afd6f298fbc3696e67958803e7ac843c1cfe59734b6251a40cdbfb5eea", size = 424112, upload-time = "2025-11-10T15:03:15.52Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/83/3b1d03d36f224edded98e9affd0467630fc09d766c0e56fb1498cbb04a9b/griffe-1.15.0-py3-none-any.whl", hash = "sha256:6f6762661949411031f5fcda9593f586e6ce8340f0ba88921a0f2ef7a81eb9a3", size = 150705, upload-time = "2025-11-10T15:03:13.549Z" }, +] + +[[package]] +name = "griffe-typingdoc" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "griffe", version = "1.14.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "griffe", version = "1.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/77/d5e5fa0a8391bc2890ae45255847197299739833108dd76ee3c9b2ff0bba/griffe_typingdoc-0.3.0.tar.gz", hash = "sha256:59d9ef98d02caa7aed88d8df1119c9e48c02ed049ea50ce4018ace9331d20f8b", size = 33169, upload-time = "2025-10-23T12:01:39.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/af/aa32c13f753e2625ec895b1f56eee3c9380a2088a88a2c028955e223856e/griffe_typingdoc-0.3.0-py3-none-any.whl", hash = "sha256:4f6483fff7733a679d1dce142fb029f314125f3caaf0d620eb82e7390c8564bb", size = 9923, upload-time = "2025-10-23T12:01:37.601Z" }, +] + +[[package]] +name = "griffe-warnings-deprecated" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "griffe", version = "1.14.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "griffe", version = "1.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7e/0e/f034e1714eb2c694d6196c75f77a02f9c69d19f9961c4804a016397bf3e5/griffe_warnings_deprecated-1.1.0.tar.gz", hash = "sha256:7bf21de327d59c66c7ce08d0166aa4292ce0577ff113de5878f428d102b6f7c5", size = 33260, upload-time = "2024-12-10T21:02:18.395Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/4c/b7241f03ad1f22ec2eed33b0f90c4f8c949e3395c4b7488670b07225a20b/griffe_warnings_deprecated-1.1.0-py3-none-any.whl", hash = "sha256:e7b0e8bfd6e5add3945d4d9805b2a41c72409e456733965be276d55f01e8a7a2", size = 5854, upload-time = "2024-12-10T21:02:16.96Z" }, +] + +[[package]] +name = "groq" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3f/12/f4099a141677fcd2ed79dcc1fcec431e60c52e0e90c9c5d935f0ffaf8c0e/groq-1.0.0.tar.gz", hash = "sha256:66cb7bb729e6eb644daac7ce8efe945e99e4eb33657f733ee6f13059ef0c25a9", size = 146068, upload-time = "2025-12-17T23:34:23.115Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/88/3175759d2ef30406ea721f4d837bfa1ba4339fde3b81ba8c5640a96ed231/groq-1.0.0-py3-none-any.whl", hash = "sha256:6e22bf92ffad988f01d2d4df7729add66b8fd5dbfb2154b5bbf3af245b72c731", size = 138292, upload-time = "2025-12-17T23:34:21.957Z" }, +] + +[[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 = "hf-xet" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/6e/0f11bacf08a67f7fb5ee09740f2ca54163863b07b70d579356e9222ce5d8/hf_xet-1.2.0.tar.gz", hash = "sha256:a8c27070ca547293b6890c4bf389f713f80e8c478631432962bb7f4bc0bd7d7f", size = 506020, upload-time = "2025-10-24T19:04:32.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/a5/85ef910a0aa034a2abcfadc360ab5ac6f6bc4e9112349bd40ca97551cff0/hf_xet-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:ceeefcd1b7aed4956ae8499e2199607765fbd1c60510752003b6cc0b8413b649", size = 2861870, upload-time = "2025-10-24T19:04:11.422Z" }, + { url = "https://files.pythonhosted.org/packages/ea/40/e2e0a7eb9a51fe8828ba2d47fe22a7e74914ea8a0db68a18c3aa7449c767/hf_xet-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b70218dd548e9840224df5638fdc94bd033552963cfa97f9170829381179c813", size = 2717584, upload-time = "2025-10-24T19:04:09.586Z" }, + { url = "https://files.pythonhosted.org/packages/a5/7d/daf7f8bc4594fdd59a8a596f9e3886133fdc68e675292218a5e4c1b7e834/hf_xet-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d40b18769bb9a8bc82a9ede575ce1a44c75eb80e7375a01d76259089529b5dc", size = 3315004, upload-time = "2025-10-24T19:04:00.314Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ba/45ea2f605fbf6d81c8b21e4d970b168b18a53515923010c312c06cd83164/hf_xet-1.2.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:cd3a6027d59cfb60177c12d6424e31f4b5ff13d8e3a1247b3a584bf8977e6df5", size = 3222636, upload-time = "2025-10-24T19:03:58.111Z" }, + { url = "https://files.pythonhosted.org/packages/4a/1d/04513e3cab8f29ab8c109d309ddd21a2705afab9d52f2ba1151e0c14f086/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6de1fc44f58f6dd937956c8d304d8c2dea264c80680bcfa61ca4a15e7b76780f", size = 3408448, upload-time = "2025-10-24T19:04:20.951Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7c/60a2756d7feec7387db3a1176c632357632fbe7849fce576c5559d4520c7/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f182f264ed2acd566c514e45da9f2119110e48a87a327ca271027904c70c5832", size = 3503401, upload-time = "2025-10-24T19:04:22.549Z" }, + { url = "https://files.pythonhosted.org/packages/4e/64/48fffbd67fb418ab07451e4ce641a70de1c40c10a13e25325e24858ebe5a/hf_xet-1.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:293a7a3787e5c95d7be1857358a9130694a9c6021de3f27fa233f37267174382", size = 2900866, upload-time = "2025-10-24T19:04:33.461Z" }, + { url = "https://files.pythonhosted.org/packages/e2/51/f7e2caae42f80af886db414d4e9885fac959330509089f97cccb339c6b87/hf_xet-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:10bfab528b968c70e062607f663e21e34e2bba349e8038db546646875495179e", size = 2861861, upload-time = "2025-10-24T19:04:19.01Z" }, + { url = "https://files.pythonhosted.org/packages/6e/1d/a641a88b69994f9371bd347f1dd35e5d1e2e2460a2e350c8d5165fc62005/hf_xet-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a212e842647b02eb6a911187dc878e79c4aa0aa397e88dd3b26761676e8c1f8", size = 2717699, upload-time = "2025-10-24T19:04:17.306Z" }, + { url = "https://files.pythonhosted.org/packages/df/e0/e5e9bba7d15f0318955f7ec3f4af13f92e773fbb368c0b8008a5acbcb12f/hf_xet-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30e06daccb3a7d4c065f34fc26c14c74f4653069bb2b194e7f18f17cbe9939c0", size = 3314885, upload-time = "2025-10-24T19:04:07.642Z" }, + { url = "https://files.pythonhosted.org/packages/21/90/b7fe5ff6f2b7b8cbdf1bd56145f863c90a5807d9758a549bf3d916aa4dec/hf_xet-1.2.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:29c8fc913a529ec0a91867ce3d119ac1aac966e098cf49501800c870328cc090", size = 3221550, upload-time = "2025-10-24T19:04:05.55Z" }, + { url = "https://files.pythonhosted.org/packages/6f/cb/73f276f0a7ce46cc6a6ec7d6c7d61cbfe5f2e107123d9bbd0193c355f106/hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e159cbfcfbb29f920db2c09ed8b660eb894640d284f102ada929b6e3dc410a", size = 3408010, upload-time = "2025-10-24T19:04:28.598Z" }, + { url = "https://files.pythonhosted.org/packages/b8/1e/d642a12caa78171f4be64f7cd9c40e3ca5279d055d0873188a58c0f5fbb9/hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9c91d5ae931510107f148874e9e2de8a16052b6f1b3ca3c1b12f15ccb491390f", size = 3503264, upload-time = "2025-10-24T19:04:30.397Z" }, + { url = "https://files.pythonhosted.org/packages/17/b5/33764714923fa1ff922770f7ed18c2daae034d21ae6e10dbf4347c854154/hf_xet-1.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:210d577732b519ac6ede149d2f2f34049d44e8622bf14eb3d63bbcd2d4b332dc", size = 2901071, upload-time = "2025-10-24T19:04:37.463Z" }, + { url = "https://files.pythonhosted.org/packages/96/2d/22338486473df5923a9ab7107d375dbef9173c338ebef5098ef593d2b560/hf_xet-1.2.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:46740d4ac024a7ca9b22bebf77460ff43332868b661186a8e46c227fdae01848", size = 2866099, upload-time = "2025-10-24T19:04:15.366Z" }, + { url = "https://files.pythonhosted.org/packages/7f/8c/c5becfa53234299bc2210ba314eaaae36c2875e0045809b82e40a9544f0c/hf_xet-1.2.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:27df617a076420d8845bea087f59303da8be17ed7ec0cd7ee3b9b9f579dff0e4", size = 2722178, upload-time = "2025-10-24T19:04:13.695Z" }, + { url = "https://files.pythonhosted.org/packages/9a/92/cf3ab0b652b082e66876d08da57fcc6fa2f0e6c70dfbbafbd470bb73eb47/hf_xet-1.2.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3651fd5bfe0281951b988c0facbe726aa5e347b103a675f49a3fa8144c7968fd", size = 3320214, upload-time = "2025-10-24T19:04:03.596Z" }, + { url = "https://files.pythonhosted.org/packages/46/92/3f7ec4a1b6a65bf45b059b6d4a5d38988f63e193056de2f420137e3c3244/hf_xet-1.2.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d06fa97c8562fb3ee7a378dd9b51e343bc5bc8190254202c9771029152f5e08c", size = 3229054, upload-time = "2025-10-24T19:04:01.949Z" }, + { url = "https://files.pythonhosted.org/packages/0b/dd/7ac658d54b9fb7999a0ccb07ad863b413cbaf5cf172f48ebcd9497ec7263/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4c1428c9ae73ec0939410ec73023c4f842927f39db09b063b9482dac5a3bb737", size = 3413812, upload-time = "2025-10-24T19:04:24.585Z" }, + { url = "https://files.pythonhosted.org/packages/92/68/89ac4e5b12a9ff6286a12174c8538a5930e2ed662091dd2572bbe0a18c8a/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a55558084c16b09b5ed32ab9ed38421e2d87cf3f1f89815764d1177081b99865", size = 3508920, upload-time = "2025-10-24T19:04:26.927Z" }, + { url = "https://files.pythonhosted.org/packages/cb/44/870d44b30e1dcfb6a65932e3e1506c103a8a5aea9103c337e7a53180322c/hf_xet-1.2.0-cp37-abi3-win_amd64.whl", hash = "sha256:e6584a52253f72c9f52f9e549d5895ca7a471608495c4ecaa6cc73dba2b24d69", size = 2905735, upload-time = "2025-10-24T19:04:35.928Z" }, +] + +[[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 = "httptools" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961, upload-time = "2025-10-10T03:55:08.559Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/e5/c07e0bcf4ec8db8164e9f6738c048b2e66aabf30e7506f440c4cc6953f60/httptools-0.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:11d01b0ff1fe02c4c32d60af61a4d613b74fad069e47e06e9067758c01e9ac78", size = 204531, upload-time = "2025-10-10T03:54:20.887Z" }, + { url = "https://files.pythonhosted.org/packages/7e/4f/35e3a63f863a659f92ffd92bef131f3e81cf849af26e6435b49bd9f6f751/httptools-0.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:84d86c1e5afdc479a6fdabf570be0d3eb791df0ae727e8dbc0259ed1249998d4", size = 109408, upload-time = "2025-10-10T03:54:22.455Z" }, + { url = "https://files.pythonhosted.org/packages/f5/71/b0a9193641d9e2471ac541d3b1b869538a5fb6419d52fd2669fa9c79e4b8/httptools-0.7.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c8c751014e13d88d2be5f5f14fc8b89612fcfa92a9cc480f2bc1598357a23a05", size = 440889, upload-time = "2025-10-10T03:54:23.753Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d9/2e34811397b76718750fea44658cb0205b84566e895192115252e008b152/httptools-0.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:654968cb6b6c77e37b832a9be3d3ecabb243bbe7a0b8f65fbc5b6b04c8fcabed", size = 440460, upload-time = "2025-10-10T03:54:25.313Z" }, + { url = "https://files.pythonhosted.org/packages/01/3f/a04626ebeacc489866bb4d82362c0657b2262bef381d68310134be7f40bb/httptools-0.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b580968316348b474b020edf3988eecd5d6eec4634ee6561e72ae3a2a0e00a8a", size = 425267, upload-time = "2025-10-10T03:54:26.81Z" }, + { url = "https://files.pythonhosted.org/packages/a5/99/adcd4f66614db627b587627c8ad6f4c55f18881549bab10ecf180562e7b9/httptools-0.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d496e2f5245319da9d764296e86c5bb6fcf0cf7a8806d3d000717a889c8c0b7b", size = 424429, upload-time = "2025-10-10T03:54:28.174Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/ec8fc904a8fd30ba022dfa85f3bbc64c3c7cd75b669e24242c0658e22f3c/httptools-0.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:cbf8317bfccf0fed3b5680c559d3459cccf1abe9039bfa159e62e391c7270568", size = 86173, upload-time = "2025-10-10T03:54:29.5Z" }, + { url = "https://files.pythonhosted.org/packages/9c/08/17e07e8d89ab8f343c134616d72eebfe03798835058e2ab579dcc8353c06/httptools-0.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:474d3b7ab469fefcca3697a10d11a32ee2b9573250206ba1e50d5980910da657", size = 206521, upload-time = "2025-10-10T03:54:31.002Z" }, + { url = "https://files.pythonhosted.org/packages/aa/06/c9c1b41ff52f16aee526fd10fbda99fa4787938aa776858ddc4a1ea825ec/httptools-0.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3c3b7366bb6c7b96bd72d0dbe7f7d5eead261361f013be5f6d9590465ea1c70", size = 110375, upload-time = "2025-10-10T03:54:31.941Z" }, + { url = "https://files.pythonhosted.org/packages/cc/cc/10935db22fda0ee34c76f047590ca0a8bd9de531406a3ccb10a90e12ea21/httptools-0.7.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:379b479408b8747f47f3b253326183d7c009a3936518cdb70db58cffd369d9df", size = 456621, upload-time = "2025-10-10T03:54:33.176Z" }, + { url = "https://files.pythonhosted.org/packages/0e/84/875382b10d271b0c11aa5d414b44f92f8dd53e9b658aec338a79164fa548/httptools-0.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cad6b591a682dcc6cf1397c3900527f9affef1e55a06c4547264796bbd17cf5e", size = 454954, upload-time = "2025-10-10T03:54:34.226Z" }, + { url = "https://files.pythonhosted.org/packages/30/e1/44f89b280f7e46c0b1b2ccee5737d46b3bb13136383958f20b580a821ca0/httptools-0.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eb844698d11433d2139bbeeb56499102143beb582bd6c194e3ba69c22f25c274", size = 440175, upload-time = "2025-10-10T03:54:35.942Z" }, + { url = "https://files.pythonhosted.org/packages/6f/7e/b9287763159e700e335028bc1824359dc736fa9b829dacedace91a39b37e/httptools-0.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f65744d7a8bdb4bda5e1fa23e4ba16832860606fcc09d674d56e425e991539ec", size = 440310, upload-time = "2025-10-10T03:54:37.1Z" }, + { url = "https://files.pythonhosted.org/packages/b3/07/5b614f592868e07f5c94b1f301b5e14a21df4e8076215a3bccb830a687d8/httptools-0.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:135fbe974b3718eada677229312e97f3b31f8a9c8ffa3ae6f565bf808d5b6bcb", size = 86875, upload-time = "2025-10-10T03:54:38.421Z" }, + { url = "https://files.pythonhosted.org/packages/53/7f/403e5d787dc4942316e515e949b0c8a013d84078a915910e9f391ba9b3ed/httptools-0.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5", size = 206280, upload-time = "2025-10-10T03:54:39.274Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0d/7f3fd28e2ce311ccc998c388dd1c53b18120fda3b70ebb022b135dc9839b/httptools-0.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5", size = 110004, upload-time = "2025-10-10T03:54:40.403Z" }, + { url = "https://files.pythonhosted.org/packages/84/a6/b3965e1e146ef5762870bbe76117876ceba51a201e18cc31f5703e454596/httptools-0.7.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03", size = 517655, upload-time = "2025-10-10T03:54:41.347Z" }, + { url = "https://files.pythonhosted.org/packages/11/7d/71fee6f1844e6fa378f2eddde6c3e41ce3a1fb4b2d81118dd544e3441ec0/httptools-0.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2", size = 511440, upload-time = "2025-10-10T03:54:42.452Z" }, + { url = "https://files.pythonhosted.org/packages/22/a5/079d216712a4f3ffa24af4a0381b108aa9c45b7a5cc6eb141f81726b1823/httptools-0.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362", size = 495186, upload-time = "2025-10-10T03:54:43.937Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9e/025ad7b65278745dee3bd0ebf9314934c4592560878308a6121f7f812084/httptools-0.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c", size = 499192, upload-time = "2025-10-10T03:54:45.003Z" }, + { url = "https://files.pythonhosted.org/packages/6d/de/40a8f202b987d43afc4d54689600ff03ce65680ede2f31df348d7f368b8f/httptools-0.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321", size = 86694, upload-time = "2025-10-10T03:54:45.923Z" }, + { url = "https://files.pythonhosted.org/packages/09/8f/c77b1fcbfd262d422f12da02feb0d218fa228d52485b77b953832105bb90/httptools-0.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3", size = 202889, upload-time = "2025-10-10T03:54:47.089Z" }, + { url = "https://files.pythonhosted.org/packages/0a/1a/22887f53602feaa066354867bc49a68fc295c2293433177ee90870a7d517/httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca", size = 108180, upload-time = "2025-10-10T03:54:48.052Z" }, + { url = "https://files.pythonhosted.org/packages/32/6a/6aaa91937f0010d288d3d124ca2946d48d60c3a5ee7ca62afe870e3ea011/httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c", size = 478596, upload-time = "2025-10-10T03:54:48.919Z" }, + { url = "https://files.pythonhosted.org/packages/6d/70/023d7ce117993107be88d2cbca566a7c1323ccbaf0af7eabf2064fe356f6/httptools-0.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66", size = 473268, upload-time = "2025-10-10T03:54:49.993Z" }, + { url = "https://files.pythonhosted.org/packages/32/4d/9dd616c38da088e3f436e9a616e1d0cc66544b8cdac405cc4e81c8679fc7/httptools-0.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346", size = 455517, upload-time = "2025-10-10T03:54:51.066Z" }, + { url = "https://files.pythonhosted.org/packages/1d/3a/a6c595c310b7df958e739aae88724e24f9246a514d909547778d776799be/httptools-0.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650", size = 458337, upload-time = "2025-10-10T03:54:52.196Z" }, + { url = "https://files.pythonhosted.org/packages/fd/82/88e8d6d2c51edc1cc391b6e044c6c435b6aebe97b1abc33db1b0b24cd582/httptools-0.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6", size = 85743, upload-time = "2025-10-10T03:54:53.448Z" }, + { url = "https://files.pythonhosted.org/packages/34/50/9d095fcbb6de2d523e027a2f304d4551855c2f46e0b82befd718b8b20056/httptools-0.7.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270", size = 203619, upload-time = "2025-10-10T03:54:54.321Z" }, + { url = "https://files.pythonhosted.org/packages/07/f0/89720dc5139ae54b03f861b5e2c55a37dba9a5da7d51e1e824a1f343627f/httptools-0.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3", size = 108714, upload-time = "2025-10-10T03:54:55.163Z" }, + { url = "https://files.pythonhosted.org/packages/b3/cb/eea88506f191fb552c11787c23f9a405f4c7b0c5799bf73f2249cd4f5228/httptools-0.7.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1", size = 472909, upload-time = "2025-10-10T03:54:56.056Z" }, + { url = "https://files.pythonhosted.org/packages/e0/4a/a548bdfae6369c0d078bab5769f7b66f17f1bfaa6fa28f81d6be6959066b/httptools-0.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b", size = 470831, upload-time = "2025-10-10T03:54:57.219Z" }, + { url = "https://files.pythonhosted.org/packages/4d/31/14df99e1c43bd132eec921c2e7e11cda7852f65619bc0fc5bdc2d0cb126c/httptools-0.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60", size = 452631, upload-time = "2025-10-10T03:54:58.219Z" }, + { url = "https://files.pythonhosted.org/packages/22/d2/b7e131f7be8d854d48cb6d048113c30f9a46dca0c9a8b08fcb3fcd588cdc/httptools-0.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca", size = 452910, upload-time = "2025-10-10T03:54:59.366Z" }, + { url = "https://files.pythonhosted.org/packages/53/cf/878f3b91e4e6e011eff6d1fa9ca39f7eb17d19c9d7971b04873734112f30/httptools-0.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96", size = 88205, upload-time = "2025-10-10T03:55:00.389Z" }, + { url = "https://files.pythonhosted.org/packages/90/de/b1fe0e8890f0292c266117d4cd268186758a9c34e576fbd573fdf3beacff/httptools-0.7.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ac50afa68945df63ec7a2707c506bd02239272288add34539a2ef527254626a4", size = 206454, upload-time = "2025-10-10T03:55:01.528Z" }, + { url = "https://files.pythonhosted.org/packages/57/a7/a675c90b49e550c7635ce209c01bc61daa5b08aef17da27ef4e0e78fcf3f/httptools-0.7.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de987bb4e7ac95b99b805b99e0aae0ad51ae61df4263459d36e07cf4052d8b3a", size = 110260, upload-time = "2025-10-10T03:55:02.418Z" }, + { url = "https://files.pythonhosted.org/packages/03/44/fb5ef8136e6e97f7b020e97e40c03a999f97e68574d4998fa52b0a62b01b/httptools-0.7.1-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d169162803a24425eb5e4d51d79cbf429fd7a491b9e570a55f495ea55b26f0bf", size = 441524, upload-time = "2025-10-10T03:55:03.292Z" }, + { url = "https://files.pythonhosted.org/packages/b4/62/8496a5425341867796d7e2419695f74a74607054e227bbaeabec8323e87f/httptools-0.7.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49794f9250188a57fa73c706b46cb21a313edb00d337ca4ce1a011fe3c760b28", size = 440877, upload-time = "2025-10-10T03:55:04.282Z" }, + { url = "https://files.pythonhosted.org/packages/e8/f1/26c2e5214106bf6ed04d03e518ff28ca0c6b5390c5da7b12bbf94b40ae43/httptools-0.7.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:aeefa0648362bb97a7d6b5ff770bfb774930a327d7f65f8208394856862de517", size = 425775, upload-time = "2025-10-10T03:55:05.341Z" }, + { url = "https://files.pythonhosted.org/packages/3a/34/7500a19257139725281f7939a7d1aa3701cf1ac4601a1690f9ab6f510e15/httptools-0.7.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0d92b10dbf0b3da4823cde6a96d18e6ae358a9daa741c71448975f6a2c339cad", size = 425001, upload-time = "2025-10-10T03:55:06.389Z" }, + { url = "https://files.pythonhosted.org/packages/71/04/31a7949d645ebf33a67f56a0024109444a52a271735e0647a210264f3e61/httptools-0.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:5ddbd045cfcb073db2449563dd479057f2c2b681ebc232380e63ef15edc9c023", size = 86818, upload-time = "2025-10-10T03:55:07.316Z" }, +] + +[[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 = "httpx-sse" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, +] + +[[package]] +name = "huggingface-hub" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock", version = "3.19.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "filelock", version = "3.20.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "fsspec", version = "2025.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "fsspec", version = "2025.12.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "httpx" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "shellingham" }, + { name = "tqdm" }, + { name = "typer-slim" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c1/c9/d42b5cfa0a50b77cf9165e13edfaf2e3bd4e0def9cb67b6b8a07224a52ab/huggingface_hub-1.3.0.tar.gz", hash = "sha256:289e2a3586fdf01e35882944eaa06fbd57436de24b6e653d1fab248584acd66b", size = 622092, upload-time = "2026-01-09T09:54:44.663Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/5b/c5fde1f56b1f072b3028ec5413f3f5bf472c5891ebb34589cddb1689609f/huggingface_hub-1.3.0-py3-none-any.whl", hash = "sha256:763f450169bb05ea3867990e9d3ba9464eb617b874791301dc81be2c6ffb0bf5", size = 533092, upload-time = "2026-01-09T09:54:43.228Z" }, +] + +[[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 = "importlib-metadata" +version = "8.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.10' and python_full_version < '3.14'", +] +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 = "inline-snapshot" +version = "0.31.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asttokens" }, + { name = "executing" }, + { name = "pytest" }, + { name = "rich" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/b1/52b5ee59f73ed31d5fe21b10881bf2d121d07d54b23c0b6b74186792e620/inline_snapshot-0.31.1.tar.gz", hash = "sha256:4ea5ed70aa1d652713bbfd750606b94bd8a42483f7d3680433b3e92994495f64", size = 2606338, upload-time = "2025-11-07T07:36:18.932Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/52/945db420380efbda8c69a7a4a16c53df9d7ac50d8217286b9d41e5d825ff/inline_snapshot-0.31.1-py3-none-any.whl", hash = "sha256:7875a73c986a03388c7e758fb5cb8a43d2c3a20328aa1d851bfb4ed536c4496f", size = 71965, upload-time = "2025-11-07T07:36:16.836Z" }, +] + +[[package]] +name = "invoke" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/de/bd/b461d3424a24c80490313fd77feeb666ca4f6a28c7e72713e3d9095719b4/invoke-2.2.1.tar.gz", hash = "sha256:515bf49b4a48932b79b024590348da22f39c4942dff991ad1fb8b8baea1be707", size = 304762, upload-time = "2025-10-11T00:36:35.172Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/4b/b99e37f88336009971405cbb7630610322ed6fbfa31e1d7ab3fbf3049a2d/invoke-2.2.1-py3-none-any.whl", hash = "sha256:2413bc441b376e5cd3f55bb5d364f973ad8bdd7bf87e53c79de3c11bf3feecc8", size = 160287, upload-time = "2025-10-11T00:36:33.703Z" }, +] + +[[package]] +name = "itsdangerous" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, +] + +[[package]] +name = "jieba" +version = "0.42.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c6/cb/18eeb235f833b726522d7ebed54f2278ce28ba9438e3135ab0278d9792a2/jieba-0.42.1.tar.gz", hash = "sha256:055ca12f62674fafed09427f176506079bc135638a14e23e25be909131928db2", size = 19214172, upload-time = "2020-01-20T14:27:23.5Z" } + +[[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 = "jiter" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/45/9d/e0660989c1370e25848bb4c52d061c71837239738ad937e83edca174c273/jiter-0.12.0.tar.gz", hash = "sha256:64dfcd7d5c168b38d3f9f8bba7fc639edb3418abcc74f22fdbe6b8938293f30b", size = 168294, upload-time = "2025-11-09T20:49:23.302Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/91/13cb9505f7be74a933f37da3af22e029f6ba64f5669416cb8b2774bc9682/jiter-0.12.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:e7acbaba9703d5de82a2c98ae6a0f59ab9770ab5af5fa35e43a303aee962cf65", size = 316652, upload-time = "2025-11-09T20:46:41.021Z" }, + { url = "https://files.pythonhosted.org/packages/4e/76/4e9185e5d9bb4e482cf6dec6410d5f78dfeb374cfcecbbe9888d07c52daa/jiter-0.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:364f1a7294c91281260364222f535bc427f56d4de1d8ffd718162d21fbbd602e", size = 319829, upload-time = "2025-11-09T20:46:43.281Z" }, + { url = "https://files.pythonhosted.org/packages/86/af/727de50995d3a153138139f259baae2379d8cb0522c0c00419957bc478a6/jiter-0.12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85ee4d25805d4fb23f0a5167a962ef8e002dbfb29c0989378488e32cf2744b62", size = 350568, upload-time = "2025-11-09T20:46:45.075Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c1/d6e9f4b7a3d5ac63bcbdfddeb50b2dcfbdc512c86cffc008584fdc350233/jiter-0.12.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:796f466b7942107eb889c08433b6e31b9a7ed31daceaecf8af1be26fb26c0ca8", size = 369052, upload-time = "2025-11-09T20:46:46.818Z" }, + { url = "https://files.pythonhosted.org/packages/eb/be/00824cd530f30ed73fa8a4f9f3890a705519e31ccb9e929f1e22062e7c76/jiter-0.12.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:35506cb71f47dba416694e67af996bbdefb8e3608f1f78799c2e1f9058b01ceb", size = 481585, upload-time = "2025-11-09T20:46:48.319Z" }, + { url = "https://files.pythonhosted.org/packages/74/b6/2ad7990dff9504d4b5052eef64aa9574bd03d722dc7edced97aad0d47be7/jiter-0.12.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:726c764a90c9218ec9e4f99a33d6bf5ec169163f2ca0fc21b654e88c2abc0abc", size = 380541, upload-time = "2025-11-09T20:46:49.643Z" }, + { url = "https://files.pythonhosted.org/packages/b5/c7/f3c26ecbc1adbf1db0d6bba99192143d8fe8504729d9594542ecc4445784/jiter-0.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa47810c5565274810b726b0dc86d18dce5fd17b190ebdc3890851d7b2a0e74", size = 364423, upload-time = "2025-11-09T20:46:51.731Z" }, + { url = "https://files.pythonhosted.org/packages/18/51/eac547bf3a2d7f7e556927278e14c56a0604b8cddae75815d5739f65f81d/jiter-0.12.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8ec0259d3f26c62aed4d73b198c53e316ae11f0f69c8fbe6682c6dcfa0fcce2", size = 389958, upload-time = "2025-11-09T20:46:53.432Z" }, + { url = "https://files.pythonhosted.org/packages/2c/1f/9ca592e67175f2db156cff035e0d817d6004e293ee0c1d73692d38fcb596/jiter-0.12.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:79307d74ea83465b0152fa23e5e297149506435535282f979f18b9033c0bb025", size = 522084, upload-time = "2025-11-09T20:46:54.848Z" }, + { url = "https://files.pythonhosted.org/packages/83/ff/597d9cdc3028f28224f53e1a9d063628e28b7a5601433e3196edda578cdd/jiter-0.12.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cf6e6dd18927121fec86739f1a8906944703941d000f0639f3eb6281cc601dca", size = 513054, upload-time = "2025-11-09T20:46:56.487Z" }, + { url = "https://files.pythonhosted.org/packages/24/6d/1970bce1351bd02e3afcc5f49e4f7ef3dabd7fb688f42be7e8091a5b809a/jiter-0.12.0-cp310-cp310-win32.whl", hash = "sha256:b6ae2aec8217327d872cbfb2c1694489057b9433afce447955763e6ab015b4c4", size = 206368, upload-time = "2025-11-09T20:46:58.638Z" }, + { url = "https://files.pythonhosted.org/packages/e3/6b/eb1eb505b2d86709b59ec06681a2b14a94d0941db091f044b9f0e16badc0/jiter-0.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:c7f49ce90a71e44f7e1aa9e7ec415b9686bbc6a5961e57eab511015e6759bc11", size = 204847, upload-time = "2025-11-09T20:47:00.295Z" }, + { url = "https://files.pythonhosted.org/packages/32/f9/eaca4633486b527ebe7e681c431f529b63fe2709e7c5242fc0f43f77ce63/jiter-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d8f8a7e317190b2c2d60eb2e8aa835270b008139562d70fe732e1c0020ec53c9", size = 316435, upload-time = "2025-11-09T20:47:02.087Z" }, + { url = "https://files.pythonhosted.org/packages/10/c1/40c9f7c22f5e6ff715f28113ebaba27ab85f9af2660ad6e1dd6425d14c19/jiter-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2218228a077e784c6c8f1a8e5d6b8cb1dea62ce25811c356364848554b2056cd", size = 320548, upload-time = "2025-11-09T20:47:03.409Z" }, + { url = "https://files.pythonhosted.org/packages/6b/1b/efbb68fe87e7711b00d2cfd1f26bb4bfc25a10539aefeaa7727329ffb9cb/jiter-0.12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9354ccaa2982bf2188fd5f57f79f800ef622ec67beb8329903abf6b10da7d423", size = 351915, upload-time = "2025-11-09T20:47:05.171Z" }, + { url = "https://files.pythonhosted.org/packages/15/2d/c06e659888c128ad1e838123d0638f0efad90cc30860cb5f74dd3f2fc0b3/jiter-0.12.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2607185ea89b4af9a604d4c7ec40e45d3ad03ee66998b031134bc510232bb7", size = 368966, upload-time = "2025-11-09T20:47:06.508Z" }, + { url = "https://files.pythonhosted.org/packages/6b/20/058db4ae5fb07cf6a4ab2e9b9294416f606d8e467fb74c2184b2a1eeacba/jiter-0.12.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a585a5e42d25f2e71db5f10b171f5e5ea641d3aa44f7df745aa965606111cc2", size = 482047, upload-time = "2025-11-09T20:47:08.382Z" }, + { url = "https://files.pythonhosted.org/packages/49/bb/dc2b1c122275e1de2eb12905015d61e8316b2f888bdaac34221c301495d6/jiter-0.12.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd9e21d34edff5a663c631f850edcb786719c960ce887a5661e9c828a53a95d9", size = 380835, upload-time = "2025-11-09T20:47:09.81Z" }, + { url = "https://files.pythonhosted.org/packages/23/7d/38f9cd337575349de16da575ee57ddb2d5a64d425c9367f5ef9e4612e32e/jiter-0.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a612534770470686cd5431478dc5a1b660eceb410abade6b1b74e320ca98de6", size = 364587, upload-time = "2025-11-09T20:47:11.529Z" }, + { url = "https://files.pythonhosted.org/packages/f0/a3/b13e8e61e70f0bb06085099c4e2462647f53cc2ca97614f7fedcaa2bb9f3/jiter-0.12.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3985aea37d40a908f887b34d05111e0aae822943796ebf8338877fee2ab67725", size = 390492, upload-time = "2025-11-09T20:47:12.993Z" }, + { url = "https://files.pythonhosted.org/packages/07/71/e0d11422ed027e21422f7bc1883c61deba2d9752b720538430c1deadfbca/jiter-0.12.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b1207af186495f48f72529f8d86671903c8c10127cac6381b11dddc4aaa52df6", size = 522046, upload-time = "2025-11-09T20:47:14.6Z" }, + { url = "https://files.pythonhosted.org/packages/9f/59/b968a9aa7102a8375dbbdfbd2aeebe563c7e5dddf0f47c9ef1588a97e224/jiter-0.12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ef2fb241de583934c9915a33120ecc06d94aa3381a134570f59eed784e87001e", size = 513392, upload-time = "2025-11-09T20:47:16.011Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e4/7df62002499080dbd61b505c5cb351aa09e9959d176cac2aa8da6f93b13b/jiter-0.12.0-cp311-cp311-win32.whl", hash = "sha256:453b6035672fecce8007465896a25b28a6b59cfe8fbc974b2563a92f5a92a67c", size = 206096, upload-time = "2025-11-09T20:47:17.344Z" }, + { url = "https://files.pythonhosted.org/packages/bb/60/1032b30ae0572196b0de0e87dce3b6c26a1eff71aad5fe43dee3082d32e0/jiter-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:ca264b9603973c2ad9435c71a8ec8b49f8f715ab5ba421c85a51cde9887e421f", size = 204899, upload-time = "2025-11-09T20:47:19.365Z" }, + { url = "https://files.pythonhosted.org/packages/49/d5/c145e526fccdb834063fb45c071df78b0cc426bbaf6de38b0781f45d956f/jiter-0.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:cb00ef392e7d684f2754598c02c409f376ddcef857aae796d559e6cacc2d78a5", size = 188070, upload-time = "2025-11-09T20:47:20.75Z" }, + { url = "https://files.pythonhosted.org/packages/92/c9/5b9f7b4983f1b542c64e84165075335e8a236fa9e2ea03a0c79780062be8/jiter-0.12.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:305e061fa82f4680607a775b2e8e0bcb071cd2205ac38e6ef48c8dd5ebe1cf37", size = 314449, upload-time = "2025-11-09T20:47:22.999Z" }, + { url = "https://files.pythonhosted.org/packages/98/6e/e8efa0e78de00db0aee82c0cf9e8b3f2027efd7f8a71f859d8f4be8e98ef/jiter-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5c1860627048e302a528333c9307c818c547f214d8659b0705d2195e1a94b274", size = 319855, upload-time = "2025-11-09T20:47:24.779Z" }, + { url = "https://files.pythonhosted.org/packages/20/26/894cd88e60b5d58af53bec5c6759d1292bd0b37a8b5f60f07abf7a63ae5f/jiter-0.12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df37577a4f8408f7e0ec3205d2a8f87672af8f17008358063a4d6425b6081ce3", size = 350171, upload-time = "2025-11-09T20:47:26.469Z" }, + { url = "https://files.pythonhosted.org/packages/f5/27/a7b818b9979ac31b3763d25f3653ec3a954044d5e9f5d87f2f247d679fd1/jiter-0.12.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75fdd787356c1c13a4f40b43c2156276ef7a71eb487d98472476476d803fb2cf", size = 365590, upload-time = "2025-11-09T20:47:27.918Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7e/e46195801a97673a83746170b17984aa8ac4a455746354516d02ca5541b4/jiter-0.12.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1eb5db8d9c65b112aacf14fcd0faae9913d07a8afea5ed06ccdd12b724e966a1", size = 479462, upload-time = "2025-11-09T20:47:29.654Z" }, + { url = "https://files.pythonhosted.org/packages/ca/75/f833bfb009ab4bd11b1c9406d333e3b4357709ed0570bb48c7c06d78c7dd/jiter-0.12.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:73c568cc27c473f82480abc15d1301adf333a7ea4f2e813d6a2c7d8b6ba8d0df", size = 378983, upload-time = "2025-11-09T20:47:31.026Z" }, + { url = "https://files.pythonhosted.org/packages/71/b3/7a69d77943cc837d30165643db753471aff5df39692d598da880a6e51c24/jiter-0.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4321e8a3d868919bcb1abb1db550d41f2b5b326f72df29e53b2df8b006eb9403", size = 361328, upload-time = "2025-11-09T20:47:33.286Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ac/a78f90caf48d65ba70d8c6efc6f23150bc39dc3389d65bbec2a95c7bc628/jiter-0.12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0a51bad79f8cc9cac2b4b705039f814049142e0050f30d91695a2d9a6611f126", size = 386740, upload-time = "2025-11-09T20:47:34.703Z" }, + { url = "https://files.pythonhosted.org/packages/39/b6/5d31c2cc8e1b6a6bcf3c5721e4ca0a3633d1ab4754b09bc7084f6c4f5327/jiter-0.12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2a67b678f6a5f1dd6c36d642d7db83e456bc8b104788262aaefc11a22339f5a9", size = 520875, upload-time = "2025-11-09T20:47:36.058Z" }, + { url = "https://files.pythonhosted.org/packages/30/b5/4df540fae4e9f68c54b8dab004bd8c943a752f0b00efd6e7d64aa3850339/jiter-0.12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efe1a211fe1fd14762adea941e3cfd6c611a136e28da6c39272dbb7a1bbe6a86", size = 511457, upload-time = "2025-11-09T20:47:37.932Z" }, + { url = "https://files.pythonhosted.org/packages/07/65/86b74010e450a1a77b2c1aabb91d4a91dd3cd5afce99f34d75fd1ac64b19/jiter-0.12.0-cp312-cp312-win32.whl", hash = "sha256:d779d97c834b4278276ec703dc3fc1735fca50af63eb7262f05bdb4e62203d44", size = 204546, upload-time = "2025-11-09T20:47:40.47Z" }, + { url = "https://files.pythonhosted.org/packages/1c/c7/6659f537f9562d963488e3e55573498a442503ced01f7e169e96a6110383/jiter-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:e8269062060212b373316fe69236096aaf4c49022d267c6736eebd66bbbc60bb", size = 205196, upload-time = "2025-11-09T20:47:41.794Z" }, + { url = "https://files.pythonhosted.org/packages/21/f4/935304f5169edadfec7f9c01eacbce4c90bb9a82035ac1de1f3bd2d40be6/jiter-0.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:06cb970936c65de926d648af0ed3d21857f026b1cf5525cb2947aa5e01e05789", size = 186100, upload-time = "2025-11-09T20:47:43.007Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a6/97209693b177716e22576ee1161674d1d58029eb178e01866a0422b69224/jiter-0.12.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:6cc49d5130a14b732e0612bc76ae8db3b49898732223ef8b7599aa8d9810683e", size = 313658, upload-time = "2025-11-09T20:47:44.424Z" }, + { url = "https://files.pythonhosted.org/packages/06/4d/125c5c1537c7d8ee73ad3d530a442d6c619714b95027143f1b61c0b4dfe0/jiter-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:37f27a32ce36364d2fa4f7fdc507279db604d27d239ea2e044c8f148410defe1", size = 318605, upload-time = "2025-11-09T20:47:45.973Z" }, + { url = "https://files.pythonhosted.org/packages/99/bf/a840b89847885064c41a5f52de6e312e91fa84a520848ee56c97e4fa0205/jiter-0.12.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbc0944aa3d4b4773e348cda635252824a78f4ba44328e042ef1ff3f6080d1cf", size = 349803, upload-time = "2025-11-09T20:47:47.535Z" }, + { url = "https://files.pythonhosted.org/packages/8a/88/e63441c28e0db50e305ae23e19c1d8fae012d78ed55365da392c1f34b09c/jiter-0.12.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:da25c62d4ee1ffbacb97fac6dfe4dcd6759ebdc9015991e92a6eae5816287f44", size = 365120, upload-time = "2025-11-09T20:47:49.284Z" }, + { url = "https://files.pythonhosted.org/packages/0a/7c/49b02714af4343970eb8aca63396bc1c82fa01197dbb1e9b0d274b550d4e/jiter-0.12.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:048485c654b838140b007390b8182ba9774621103bd4d77c9c3f6f117474ba45", size = 479918, upload-time = "2025-11-09T20:47:50.807Z" }, + { url = "https://files.pythonhosted.org/packages/69/ba/0a809817fdd5a1db80490b9150645f3aae16afad166960bcd562be194f3b/jiter-0.12.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:635e737fbb7315bef0037c19b88b799143d2d7d3507e61a76751025226b3ac87", size = 379008, upload-time = "2025-11-09T20:47:52.211Z" }, + { url = "https://files.pythonhosted.org/packages/5f/c3/c9fc0232e736c8877d9e6d83d6eeb0ba4e90c6c073835cc2e8f73fdeef51/jiter-0.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e017c417b1ebda911bd13b1e40612704b1f5420e30695112efdbed8a4b389ed", size = 361785, upload-time = "2025-11-09T20:47:53.512Z" }, + { url = "https://files.pythonhosted.org/packages/96/61/61f69b7e442e97ca6cd53086ddc1cf59fb830549bc72c0a293713a60c525/jiter-0.12.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:89b0bfb8b2bf2351fba36bb211ef8bfceba73ef58e7f0c68fb67b5a2795ca2f9", size = 386108, upload-time = "2025-11-09T20:47:54.893Z" }, + { url = "https://files.pythonhosted.org/packages/e9/2e/76bb3332f28550c8f1eba3bf6e5efe211efda0ddbbaf24976bc7078d42a5/jiter-0.12.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:f5aa5427a629a824a543672778c9ce0c5e556550d1569bb6ea28a85015287626", size = 519937, upload-time = "2025-11-09T20:47:56.253Z" }, + { url = "https://files.pythonhosted.org/packages/84/d6/fa96efa87dc8bff2094fb947f51f66368fa56d8d4fc9e77b25d7fbb23375/jiter-0.12.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed53b3d6acbcb0fd0b90f20c7cb3b24c357fe82a3518934d4edfa8c6898e498c", size = 510853, upload-time = "2025-11-09T20:47:58.32Z" }, + { url = "https://files.pythonhosted.org/packages/8a/28/93f67fdb4d5904a708119a6ab58a8f1ec226ff10a94a282e0215402a8462/jiter-0.12.0-cp313-cp313-win32.whl", hash = "sha256:4747de73d6b8c78f2e253a2787930f4fffc68da7fa319739f57437f95963c4de", size = 204699, upload-time = "2025-11-09T20:47:59.686Z" }, + { url = "https://files.pythonhosted.org/packages/c4/1f/30b0eb087045a0abe2a5c9c0c0c8da110875a1d3be83afd4a9a4e548be3c/jiter-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:e25012eb0c456fcc13354255d0338cd5397cce26c77b2832b3c4e2e255ea5d9a", size = 204258, upload-time = "2025-11-09T20:48:01.01Z" }, + { url = "https://files.pythonhosted.org/packages/2c/f4/2b4daf99b96bce6fc47971890b14b2a36aef88d7beb9f057fafa032c6141/jiter-0.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:c97b92c54fe6110138c872add030a1f99aea2401ddcdaa21edf74705a646dd60", size = 185503, upload-time = "2025-11-09T20:48:02.35Z" }, + { url = "https://files.pythonhosted.org/packages/39/ca/67bb15a7061d6fe20b9b2a2fd783e296a1e0f93468252c093481a2f00efa/jiter-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:53839b35a38f56b8be26a7851a48b89bc47e5d88e900929df10ed93b95fea3d6", size = 317965, upload-time = "2025-11-09T20:48:03.783Z" }, + { url = "https://files.pythonhosted.org/packages/18/af/1788031cd22e29c3b14bc6ca80b16a39a0b10e611367ffd480c06a259831/jiter-0.12.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94f669548e55c91ab47fef8bddd9c954dab1938644e715ea49d7e117015110a4", size = 345831, upload-time = "2025-11-09T20:48:05.55Z" }, + { url = "https://files.pythonhosted.org/packages/05/17/710bf8472d1dff0d3caf4ced6031060091c1320f84ee7d5dcbed1f352417/jiter-0.12.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:351d54f2b09a41600ffea43d081522d792e81dcfb915f6d2d242744c1cc48beb", size = 361272, upload-time = "2025-11-09T20:48:06.951Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f1/1dcc4618b59761fef92d10bcbb0b038b5160be653b003651566a185f1a5c/jiter-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2a5e90604620f94bf62264e7c2c038704d38217b7465b863896c6d7c902b06c7", size = 204604, upload-time = "2025-11-09T20:48:08.328Z" }, + { url = "https://files.pythonhosted.org/packages/d9/32/63cb1d9f1c5c6632a783c0052cde9ef7ba82688f7065e2f0d5f10a7e3edb/jiter-0.12.0-cp313-cp313t-win_arm64.whl", hash = "sha256:88ef757017e78d2860f96250f9393b7b577b06a956ad102c29c8237554380db3", size = 185628, upload-time = "2025-11-09T20:48:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/a8/99/45c9f0dbe4a1416b2b9a8a6d1236459540f43d7fb8883cff769a8db0612d/jiter-0.12.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c46d927acd09c67a9fb1416df45c5a04c27e83aae969267e98fba35b74e99525", size = 312478, upload-time = "2025-11-09T20:48:10.898Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a7/54ae75613ba9e0f55fcb0bc5d1f807823b5167cc944e9333ff322e9f07dd/jiter-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:774ff60b27a84a85b27b88cd5583899c59940bcc126caca97eb2a9df6aa00c49", size = 318706, upload-time = "2025-11-09T20:48:12.266Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/2aa241ad2c10774baf6c37f8b8e1f39c07db358f1329f4eb40eba179c2a2/jiter-0.12.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5433fab222fb072237df3f637d01b81f040a07dcac1cb4a5c75c7aa9ed0bef1", size = 351894, upload-time = "2025-11-09T20:48:13.673Z" }, + { url = "https://files.pythonhosted.org/packages/54/4f/0f2759522719133a9042781b18cc94e335b6d290f5e2d3e6899d6af933e3/jiter-0.12.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8c593c6e71c07866ec6bfb790e202a833eeec885022296aff6b9e0b92d6a70e", size = 365714, upload-time = "2025-11-09T20:48:15.083Z" }, + { url = "https://files.pythonhosted.org/packages/dc/6f/806b895f476582c62a2f52c453151edd8a0fde5411b0497baaa41018e878/jiter-0.12.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:90d32894d4c6877a87ae00c6b915b609406819dce8bc0d4e962e4de2784e567e", size = 478989, upload-time = "2025-11-09T20:48:16.706Z" }, + { url = "https://files.pythonhosted.org/packages/86/6c/012d894dc6e1033acd8db2b8346add33e413ec1c7c002598915278a37f79/jiter-0.12.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:798e46eed9eb10c3adbbacbd3bdb5ecd4cf7064e453d00dbef08802dae6937ff", size = 378615, upload-time = "2025-11-09T20:48:18.614Z" }, + { url = "https://files.pythonhosted.org/packages/87/30/d718d599f6700163e28e2c71c0bbaf6dace692e7df2592fd793ac9276717/jiter-0.12.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3f1368f0a6719ea80013a4eb90ba72e75d7ea67cfc7846db2ca504f3df0169a", size = 364745, upload-time = "2025-11-09T20:48:20.117Z" }, + { url = "https://files.pythonhosted.org/packages/8f/85/315b45ce4b6ddc7d7fceca24068543b02bdc8782942f4ee49d652e2cc89f/jiter-0.12.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:65f04a9d0b4406f7e51279710b27484af411896246200e461d80d3ba0caa901a", size = 386502, upload-time = "2025-11-09T20:48:21.543Z" }, + { url = "https://files.pythonhosted.org/packages/74/0b/ce0434fb40c5b24b368fe81b17074d2840748b4952256bab451b72290a49/jiter-0.12.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:fd990541982a24281d12b67a335e44f117e4c6cbad3c3b75c7dea68bf4ce3a67", size = 519845, upload-time = "2025-11-09T20:48:22.964Z" }, + { url = "https://files.pythonhosted.org/packages/e8/a3/7a7a4488ba052767846b9c916d208b3ed114e3eb670ee984e4c565b9cf0d/jiter-0.12.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:b111b0e9152fa7df870ecaebb0bd30240d9f7fff1f2003bcb4ed0f519941820b", size = 510701, upload-time = "2025-11-09T20:48:24.483Z" }, + { url = "https://files.pythonhosted.org/packages/c3/16/052ffbf9d0467b70af24e30f91e0579e13ded0c17bb4a8eb2aed3cb60131/jiter-0.12.0-cp314-cp314-win32.whl", hash = "sha256:a78befb9cc0a45b5a5a0d537b06f8544c2ebb60d19d02c41ff15da28a9e22d42", size = 205029, upload-time = "2025-11-09T20:48:25.749Z" }, + { url = "https://files.pythonhosted.org/packages/e4/18/3cf1f3f0ccc789f76b9a754bdb7a6977e5d1d671ee97a9e14f7eb728d80e/jiter-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:e1fe01c082f6aafbe5c8faf0ff074f38dfb911d53f07ec333ca03f8f6226debf", size = 204960, upload-time = "2025-11-09T20:48:27.415Z" }, + { url = "https://files.pythonhosted.org/packages/02/68/736821e52ecfdeeb0f024b8ab01b5a229f6b9293bbdb444c27efade50b0f/jiter-0.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:d72f3b5a432a4c546ea4bedc84cce0c3404874f1d1676260b9c7f048a9855451", size = 185529, upload-time = "2025-11-09T20:48:29.125Z" }, + { url = "https://files.pythonhosted.org/packages/30/61/12ed8ee7a643cce29ac97c2281f9ce3956eb76b037e88d290f4ed0d41480/jiter-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e6ded41aeba3603f9728ed2b6196e4df875348ab97b28fc8afff115ed42ba7a7", size = 318974, upload-time = "2025-11-09T20:48:30.87Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c6/f3041ede6d0ed5e0e79ff0de4c8f14f401bbf196f2ef3971cdbe5fd08d1d/jiter-0.12.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a947920902420a6ada6ad51892082521978e9dd44a802663b001436e4b771684", size = 345932, upload-time = "2025-11-09T20:48:32.658Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5d/4d94835889edd01ad0e2dbfc05f7bdfaed46292e7b504a6ac7839aa00edb/jiter-0.12.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:add5e227e0554d3a52cf390a7635edaffdf4f8fce4fdbcef3cc2055bb396a30c", size = 367243, upload-time = "2025-11-09T20:48:34.093Z" }, + { url = "https://files.pythonhosted.org/packages/fd/76/0051b0ac2816253a99d27baf3dda198663aff882fa6ea7deeb94046da24e/jiter-0.12.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f9b1cda8fcb736250d7e8711d4580ebf004a46771432be0ae4796944b5dfa5d", size = 479315, upload-time = "2025-11-09T20:48:35.507Z" }, + { url = "https://files.pythonhosted.org/packages/70/ae/83f793acd68e5cb24e483f44f482a1a15601848b9b6f199dacb970098f77/jiter-0.12.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:deeb12a2223fe0135c7ff1356a143d57f95bbf1f4a66584f1fc74df21d86b993", size = 380714, upload-time = "2025-11-09T20:48:40.014Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/4808a88338ad2c228b1126b93fcd8ba145e919e886fe910d578230dabe3b/jiter-0.12.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c596cc0f4cb574877550ce4ecd51f8037469146addd676d7c1a30ebe6391923f", size = 365168, upload-time = "2025-11-09T20:48:41.462Z" }, + { url = "https://files.pythonhosted.org/packages/0c/d4/04619a9e8095b42aef436b5aeb4c0282b4ff1b27d1db1508df9f5dc82750/jiter-0.12.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ab4c823b216a4aeab3fdbf579c5843165756bd9ad87cc6b1c65919c4715f783", size = 387893, upload-time = "2025-11-09T20:48:42.921Z" }, + { url = "https://files.pythonhosted.org/packages/17/ea/d3c7e62e4546fdc39197fa4a4315a563a89b95b6d54c0d25373842a59cbe/jiter-0.12.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e427eee51149edf962203ff8db75a7514ab89be5cb623fb9cea1f20b54f1107b", size = 520828, upload-time = "2025-11-09T20:48:44.278Z" }, + { url = "https://files.pythonhosted.org/packages/cc/0b/c6d3562a03fd767e31cb119d9041ea7958c3c80cb3d753eafb19b3b18349/jiter-0.12.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:edb868841f84c111255ba5e80339d386d937ec1fdce419518ce1bd9370fac5b6", size = 511009, upload-time = "2025-11-09T20:48:45.726Z" }, + { url = "https://files.pythonhosted.org/packages/aa/51/2cb4468b3448a8385ebcd15059d325c9ce67df4e2758d133ab9442b19834/jiter-0.12.0-cp314-cp314t-win32.whl", hash = "sha256:8bbcfe2791dfdb7c5e48baf646d37a6a3dcb5a97a032017741dea9f817dca183", size = 205110, upload-time = "2025-11-09T20:48:47.033Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c5/ae5ec83dec9c2d1af805fd5fe8f74ebded9c8670c5210ec7820ce0dbeb1e/jiter-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2fa940963bf02e1d8226027ef461e36af472dea85d36054ff835aeed944dd873", size = 205223, upload-time = "2025-11-09T20:48:49.076Z" }, + { url = "https://files.pythonhosted.org/packages/97/9a/3c5391907277f0e55195550cf3fa8e293ae9ee0c00fb402fec1e38c0c82f/jiter-0.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:506c9708dd29b27288f9f8f1140c3cb0e3d8ddb045956d7757b1fa0e0f39a473", size = 185564, upload-time = "2025-11-09T20:48:50.376Z" }, + { url = "https://files.pythonhosted.org/packages/7d/da/3e1fbd1f03f89ff0b4469d481be0b5cf2880c8e7b56fd80303b3ab5ae52d/jiter-0.12.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c9d28b218d5f9e5f69a0787a196322a5056540cb378cac8ff542b4fa7219966c", size = 319378, upload-time = "2025-11-09T20:48:51.761Z" }, + { url = "https://files.pythonhosted.org/packages/c7/4e/e07d69285e9e19a153050a6d281d2f0968600753a8fed8a3a141d6ffc140/jiter-0.12.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d0ee12028daf8cfcf880dd492349a122a64f42c059b6c62a2b0c96a83a8da820", size = 312195, upload-time = "2025-11-09T20:48:53.547Z" }, + { url = "https://files.pythonhosted.org/packages/2d/82/1f1cb5231b36af9f3d6d5b6030e70110faf14fd143419fc5fe7d852e691a/jiter-0.12.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b135ebe757a82d67ed2821526e72d0acf87dd61f6013e20d3c45b8048af927b", size = 352777, upload-time = "2025-11-09T20:48:55.058Z" }, + { url = "https://files.pythonhosted.org/packages/6a/5e/728393bbbc99b31e8f7a4fdd8fa55e455a0a9648f79097d9088baf1f676f/jiter-0.12.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:15d7fafb81af8a9e3039fc305529a61cd933eecee33b4251878a1c89859552a3", size = 370738, upload-time = "2025-11-09T20:48:56.632Z" }, + { url = "https://files.pythonhosted.org/packages/30/08/ac92f0df7b14ac82f2fe0a382a8000e600ab90af95798d4a7db0c1bd0736/jiter-0.12.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92d1f41211d8a8fe412faad962d424d334764c01dac6691c44691c2e4d3eedaf", size = 483744, upload-time = "2025-11-09T20:48:58.006Z" }, + { url = "https://files.pythonhosted.org/packages/7e/f4/dbfa4e759a2b82e969a14c3d0a91b176f1ed94717183a2f495cf94a651b9/jiter-0.12.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a64a48d7c917b8f32f25c176df8749ecf08cec17c466114727efe7441e17f6d", size = 382888, upload-time = "2025-11-09T20:48:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/6c/d9/b86fff7f748b0bb54222a8f132ffaf4d1be56b4591fa76d3cfdd701a33e5/jiter-0.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:122046f3b3710b85de99d9aa2f3f0492a8233a2f54a64902b096efc27ea747b5", size = 366465, upload-time = "2025-11-09T20:49:01.408Z" }, + { url = "https://files.pythonhosted.org/packages/93/3c/1152d8b433317a568927e13c1b125c680e6c058ff5d304833be8469bd4f2/jiter-0.12.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:27ec39225e03c32c6b863ba879deb427882f243ae46f0d82d68b695fa5b48b40", size = 392603, upload-time = "2025-11-09T20:49:02.784Z" }, + { url = "https://files.pythonhosted.org/packages/6e/92/ff19d8fb87f3f9438eb7464862c8d0126455bc046b345d59b21443640c62/jiter-0.12.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:26b9e155ddc132225a39b1995b3b9f0fe0f79a6d5cbbeacf103271e7d309b404", size = 523780, upload-time = "2025-11-09T20:49:04.42Z" }, + { url = "https://files.pythonhosted.org/packages/87/3a/4260e2d84e4a293c36d2a8e8b8dcd69609c671f3bd310e4625359217c517/jiter-0.12.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9ab05b7c58e29bb9e60b70c2e0094c98df79a1e42e397b9bb6eaa989b7a66dd0", size = 514874, upload-time = "2025-11-09T20:49:05.844Z" }, + { url = "https://files.pythonhosted.org/packages/2e/f7/574d2cb79e86feb035ade18c2254da71d04417555907c9df51dd6b183426/jiter-0.12.0-cp39-cp39-win32.whl", hash = "sha256:59f9f9df87ed499136db1c2b6c9efb902f964bed42a582ab7af413b6a293e7b0", size = 208329, upload-time = "2025-11-09T20:49:07.444Z" }, + { url = "https://files.pythonhosted.org/packages/05/ce/50725ec39782d8c973f19ae2d7dd3d192d01332c7cbde48c75e16a3e85a9/jiter-0.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:d3719596a1ebe7a48a498e8d5d0c4bf7553321d4c3eee1d620628d51351a3928", size = 206557, upload-time = "2025-11-09T20:49:08.888Z" }, + { url = "https://files.pythonhosted.org/packages/fe/54/5339ef1ecaa881c6948669956567a64d2670941925f245c434f494ffb0e5/jiter-0.12.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:4739a4657179ebf08f85914ce50332495811004cc1747852e8b2041ed2aab9b8", size = 311144, upload-time = "2025-11-09T20:49:10.503Z" }, + { url = "https://files.pythonhosted.org/packages/27/74/3446c652bffbd5e81ab354e388b1b5fc1d20daac34ee0ed11ff096b1b01a/jiter-0.12.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:41da8def934bf7bec16cb24bd33c0ca62126d2d45d81d17b864bd5ad721393c3", size = 305877, upload-time = "2025-11-09T20:49:12.269Z" }, + { url = "https://files.pythonhosted.org/packages/a1/f4/ed76ef9043450f57aac2d4fbeb27175aa0eb9c38f833be6ef6379b3b9a86/jiter-0.12.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c44ee814f499c082e69872d426b624987dbc5943ab06e9bbaa4f81989fdb79e", size = 340419, upload-time = "2025-11-09T20:49:13.803Z" }, + { url = "https://files.pythonhosted.org/packages/21/01/857d4608f5edb0664aa791a3d45702e1a5bcfff9934da74035e7b9803846/jiter-0.12.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd2097de91cf03eaa27b3cbdb969addf83f0179c6afc41bbc4513705e013c65d", size = 347212, upload-time = "2025-11-09T20:49:15.643Z" }, + { url = "https://files.pythonhosted.org/packages/cb/f5/12efb8ada5f5c9edc1d4555fe383c1fb2eac05ac5859258a72d61981d999/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:e8547883d7b96ef2e5fe22b88f8a4c8725a56e7f4abafff20fd5272d634c7ecb", size = 309974, upload-time = "2025-11-09T20:49:17.187Z" }, + { url = "https://files.pythonhosted.org/packages/85/15/d6eb3b770f6a0d332675141ab3962fd4a7c270ede3515d9f3583e1d28276/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:89163163c0934854a668ed783a2546a0617f71706a2551a4a0666d91ab365d6b", size = 304233, upload-time = "2025-11-09T20:49:18.734Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3e/e7e06743294eea2cf02ced6aa0ff2ad237367394e37a0e2b4a1108c67a36/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d96b264ab7d34bbb2312dedc47ce07cd53f06835eacbc16dde3761f47c3a9e7f", size = 338537, upload-time = "2025-11-09T20:49:20.317Z" }, + { url = "https://files.pythonhosted.org/packages/2f/9c/6753e6522b8d0ef07d3a3d239426669e984fb0eba15a315cdbc1253904e4/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c24e864cb30ab82311c6425655b0cdab0a98c5d973b065c66a3f020740c2324c", size = 346110, upload-time = "2025-11-09T20:49:21.817Z" }, +] + +[[package]] +name = "jmespath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/00/2a/e867e8531cf3e36b41201936b7fa7ba7b5702dbef42922193f05c8976cd6/jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe", size = 25843, upload-time = "2022-06-17T18:00:12.224Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", size = 20256, upload-time = "2022-06-17T18:00:10.251Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs", marker = "python_full_version >= '3.10'" }, + { name = "jsonschema-specifications", marker = "python_full_version >= '3.10'" }, + { name = "referencing", marker = "python_full_version >= '3.10'" }, + { name = "rpds-py", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "lia-web" +version = "0.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cross-web", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/3d/7d574a7a5cf5fbc5fc09c07ea3696dd400353b7702bc009cf596b8c12035/lia_web-0.3.1.tar.gz", hash = "sha256:7f551269eddd729f1437e9341ad21622a849eb0c0975d9232ccbbaadbdc74c06", size = 2021, upload-time = "2025-12-25T20:41:51.195Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/8b/b628fc18658f94b3d094708a18b71083cf47628e85cbc6b9edba54d5b2d7/lia_web-0.3.1-py3-none-any.whl", hash = "sha256:e4e6e7a9381e228aca60a6f3d67dbae9a5f4638eced242d931f95797ddba3f8b", size = 5933, upload-time = "2025-12-25T20:41:52.289Z" }, +] + +[[package]] +name = "logfire-api" +version = "4.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/2a/76d8fbafa881cb03d5ad6e1d67d537e8c308ae7145812b8891f7b8751224/logfire_api-4.17.0.tar.gz", hash = "sha256:4647dad05146a68af441d59a7746a966df4c2581b316616f1210f8cf74931353", size = 58305, upload-time = "2026-01-07T10:52:17.768Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/bc/3844e103dca998dcc195d6ef09e0f29d9000bac870117db1dd59a29bfeef/logfire_api-4.17.0-py3-none-any.whl", hash = "sha256:80a4b79cd9918934cdf2043d944cfb04182708178d846273484d47f3619a5a39", size = 96146, upload-time = "2026-01-07T10:52:15.088Z" }, +] + +[[package]] +name = "markdown" +version = "3.9" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8d/37/02347f6d6d8279247a5837082ebc26fc0d5aaeaf75aa013fcbb433c777ab/markdown-3.9.tar.gz", hash = "sha256:d2900fe1782bd33bdbbd56859defef70c2e78fc46668f8eb9df3128138f2cb6a", size = 364585, upload-time = "2025-09-04T20:25:22.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/ae/44c4a6a4cbb496d93c6257954260fe3a6e91b7bed2240e5dad2a717f5111/markdown-3.9-py3-none-any.whl", hash = "sha256:9f4d91ed810864ea88a6f32c07ba8bee1346c0cc1f6b1f9f6c822f2a9667d280", size = 107441, upload-time = "2025-09-04T20:25:21.784Z" }, +] + +[[package]] +name = "markdown" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.10' and python_full_version < '3.14'", +] +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", version = "3.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "markdown", version = "3.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +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 = "3.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "mdurl", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.10' and python_full_version < '3.14'", +] +dependencies = [ + { name = "mdurl", marker = "python_full_version >= '3.10'" }, +] +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" }, + { url = "https://files.pythonhosted.org/packages/56/23/0d8c13a44bde9154821586520840643467aee574d8ce79a17da539ee7fed/markupsafe-3.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26", size = 11623, upload-time = "2025-09-27T18:37:29.296Z" }, + { url = "https://files.pythonhosted.org/packages/fd/23/07a2cb9a8045d5f3f0890a8c3bc0859d7a47bfd9a560b563899bec7b72ed/markupsafe-3.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc", size = 12049, upload-time = "2025-09-27T18:37:30.234Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e4/6be85eb81503f8e11b61c0b6369b6e077dcf0a74adbd9ebf6b349937b4e9/markupsafe-3.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c", size = 21923, upload-time = "2025-09-27T18:37:31.177Z" }, + { url = "https://files.pythonhosted.org/packages/6f/bc/4dc914ead3fe6ddaef035341fee0fc956949bbd27335b611829292b89ee2/markupsafe-3.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42", size = 20543, upload-time = "2025-09-27T18:37:32.168Z" }, + { url = "https://files.pythonhosted.org/packages/89/6e/5fe81fbcfba4aef4093d5f856e5c774ec2057946052d18d168219b7bd9f9/markupsafe-3.0.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b", size = 20585, upload-time = "2025-09-27T18:37:33.166Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f6/e0e5a3d3ae9c4020f696cd055f940ef86b64fe88de26f3a0308b9d3d048c/markupsafe-3.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758", size = 21387, upload-time = "2025-09-27T18:37:34.185Z" }, + { url = "https://files.pythonhosted.org/packages/c8/25/651753ef4dea08ea790f4fbb65146a9a44a014986996ca40102e237aa49a/markupsafe-3.0.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2", size = 20133, upload-time = "2025-09-27T18:37:35.138Z" }, + { url = "https://files.pythonhosted.org/packages/dc/0a/c3cf2b4fef5f0426e8a6d7fce3cb966a17817c568ce59d76b92a233fdbec/markupsafe-3.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d", size = 20588, upload-time = "2025-09-27T18:37:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/cd/1b/a7782984844bd519ad4ffdbebbba2671ec5d0ebbeac34736c15fb86399e8/markupsafe-3.0.3-cp39-cp39-win32.whl", hash = "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7", size = 14566, upload-time = "2025-09-27T18:37:37.09Z" }, + { url = "https://files.pythonhosted.org/packages/18/1f/8d9c20e1c9440e215a44be5ab64359e207fcb4f675543f1cf9a2a7f648d0/markupsafe-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e", size = 15053, upload-time = "2025-09-27T18:37:38.054Z" }, + { url = "https://files.pythonhosted.org/packages/4e/d3/fe08482b5cd995033556d45041a4f4e76e7f0521112a9c9991d40d39825f/markupsafe-3.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8", size = 13928, upload-time = "2025-09-27T18:37:39.037Z" }, +] + +[[package]] +name = "mcp" +version = "1.19.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", marker = "python_full_version >= '3.10'" }, + { name = "httpx", marker = "python_full_version >= '3.10'" }, + { name = "httpx-sse", marker = "python_full_version >= '3.10'" }, + { name = "jsonschema", marker = "python_full_version >= '3.10'" }, + { name = "pydantic", marker = "python_full_version >= '3.10'" }, + { name = "pydantic-settings", version = "2.12.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "python-multipart", version = "0.0.21", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pywin32", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, + { name = "sse-starlette", marker = "python_full_version >= '3.10'" }, + { name = "starlette", version = "0.50.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "uvicorn", version = "0.40.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' and sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/2b/916852a5668f45d8787378461eaa1244876d77575ffef024483c94c0649c/mcp-1.19.0.tar.gz", hash = "sha256:213de0d3cd63f71bc08ffe9cc8d4409cc87acffd383f6195d2ce0457c021b5c1", size = 444163, upload-time = "2025-10-24T01:11:15.839Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/a3/3e71a875a08b6a830b88c40bc413bff01f1650f1efe8a054b5e90a9d4f56/mcp-1.19.0-py3-none-any.whl", hash = "sha256:f5907fe1c0167255f916718f376d05f09a830a215327a3ccdd5ec8a519f2e572", size = 170105, upload-time = "2025-10-24T01:11:14.151Z" }, +] + +[[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", version = "3.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "markdown", version = "3.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { 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 = "mistralai" +version = "1.9.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "eval-type-backport" }, + { name = "httpx" }, + { name = "invoke" }, + { name = "pydantic" }, + { name = "python-dateutil" }, + { name = "pyyaml" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/8d/d8b7af67a966b6f227024e1cb7287fc19901a434f87a5a391dcfe635d338/mistralai-1.9.11.tar.gz", hash = "sha256:3df9e403c31a756ec79e78df25ee73cea3eb15f86693773e16b16adaf59c9b8a", size = 208051, upload-time = "2025-10-02T15:53:40.473Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/76/4ce12563aea5a76016f8643eff30ab731e6656c845e9e4d090ef10c7b925/mistralai-1.9.11-py3-none-any.whl", hash = "sha256:7a3dc2b8ef3fceaa3582220234261b5c4e3e03a972563b07afa150e44a25a6d3", size = 442796, upload-time = "2025-10-02T15:53:39.134Z" }, +] + +[[package]] +name = "mkdocs" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "click", version = "8.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "ghp-import" }, + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "jinja2" }, + { name = "markdown", version = "3.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "markdown", version = "3.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { 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", version = "3.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "markdown", version = "3.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { 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 = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "mergedeep" }, + { name = "platformdirs", version = "4.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "platformdirs", version = "4.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { 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.4.1" +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", version = "3.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "termcolor", version = "3.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1b/42/bb2ceed148c77f82b57c6d3b7f584f0f34ababf7a9a8ff85809380d1f400/mkdocs_macros_plugin-1.4.1.tar.gz", hash = "sha256:55a9c93871e3744cdeb0736316783d60830a6d5d97b1132364e6b491607f2332", size = 35094, upload-time = "2025-10-25T12:37:20.689Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/ec/e6e96a7ae8df414f03f43681821234b0d3b86666f7b91f70ab26775a8809/mkdocs_macros_plugin-1.4.1-py3-none-any.whl", hash = "sha256:5a9e483f6056fe7ad0923802affe699233ca468672e20a9640dba237165b3240", size = 40155, upload-time = "2025-10-25T12:37:19.417Z" }, +] + +[[package]] +name = "mkdocs-material" +version = "9.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "backrefs" }, + { name = "colorama" }, + { name = "jinja2" }, + { name = "markdown", version = "3.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "markdown", version = "3.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "mkdocs" }, + { name = "mkdocs-material-extensions" }, + { name = "paginate" }, + { name = "pygments" }, + { name = "pymdown-extensions" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9c/3b/111b84cd6ff28d9e955b5f799ef217a17bc1684ac346af333e6100e413cb/mkdocs_material-9.7.0.tar.gz", hash = "sha256:602b359844e906ee402b7ed9640340cf8a474420d02d8891451733b6b02314ec", size = 4094546, upload-time = "2025-11-11T08:49:09.73Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/87/eefe8d5e764f4cf50ed91b943f8e8f96b5efd65489d8303b7a36e2e79834/mkdocs_material-9.7.0-py3-none-any.whl", hash = "sha256:da2866ea53601125ff5baa8aa06404c6e07af3c5ce3d5de95e3b52b80b442887", size = 9283770, upload-time = "2025-11-11T08:49:06.26Z" }, +] + +[[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 = "0.30.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "jinja2" }, + { name = "markdown", version = "3.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "markdown", version = "3.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "markupsafe" }, + { name = "mkdocs" }, + { name = "mkdocs-autorefs" }, + { name = "pymdown-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c5/33/2fa3243439f794e685d3e694590d28469a9b8ea733af4b48c250a3ffc9a0/mkdocstrings-0.30.1.tar.gz", hash = "sha256:84a007aae9b707fb0aebfc9da23db4b26fc9ab562eb56e335e9ec480cb19744f", size = 106350, upload-time = "2025-09-19T10:49:26.446Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/2c/f0dc4e1ee7f618f5bff7e05898d20bf8b6e7fa612038f768bfa295f136a4/mkdocstrings-0.30.1-py3-none-any.whl", hash = "sha256:41bd71f284ca4d44a668816193e4025c950b002252081e387433656ae9a70a82", size = 36704, upload-time = "2025-09-19T10:49:24.805Z" }, +] + +[package.optional-dependencies] +python = [ + { name = "mkdocstrings-python", version = "1.18.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "mkdocstrings-python", version = "2.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] + +[[package]] +name = "mkdocstrings-python" +version = "1.18.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "griffe", version = "1.14.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "mkdocs-autorefs", marker = "python_full_version < '3.10'" }, + { name = "mkdocstrings", marker = "python_full_version < '3.10'" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/ae/58ab2bfbee2792e92a98b97e872f7c003deb903071f75d8d83aa55db28fa/mkdocstrings_python-1.18.2.tar.gz", hash = "sha256:4ad536920a07b6336f50d4c6d5603316fafb1172c5c882370cbbc954770ad323", size = 207972, upload-time = "2025-08-28T16:11:19.847Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/8f/ce008599d9adebf33ed144e7736914385e8537f5fc686fdb7cceb8c22431/mkdocstrings_python-1.18.2-py3-none-any.whl", hash = "sha256:944fe6deb8f08f33fa936d538233c4036e9f53e840994f6146e8e94eb71b600d", size = 138215, upload-time = "2025-08-28T16:11:18.176Z" }, +] + +[[package]] +name = "mkdocstrings-python" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.10' and python_full_version < '3.14'", +] +dependencies = [ + { name = "griffe", version = "1.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "mkdocs-autorefs", marker = "python_full_version >= '3.10'" }, + { name = "mkdocstrings", marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/24/75/d30af27a2906f00eb90143470272376d728521997800f5dce5b340ba35bc/mkdocstrings_python-2.0.1.tar.gz", hash = "sha256:843a562221e6a471fefdd4b45cc6c22d2607ccbad632879234fa9692e9cf7732", size = 199345, upload-time = "2025-12-03T14:26:11.755Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/06/c5f8deba7d2cbdfa7967a716ae801aa9ca5f734b8f54fd473ef77a088dbe/mkdocstrings_python-2.0.1-py3-none-any.whl", hash = "sha256:66ecff45c5f8b71bf174e11d49afc845c2dfc7fc0ab17a86b6b337e0f24d8d90", size = 105055, upload-time = "2025-12-03T14:26:10.184Z" }, +] + +[[package]] +name = "mypy" +version = "1.14.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/eb/2c92d8ea1e684440f54fa49ac5d9a5f19967b7b472a281f419e69a8d228e/mypy-1.14.1.tar.gz", hash = "sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6", size = 3216051, upload-time = "2024-12-30T16:39:07.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/7a/87ae2adb31d68402da6da1e5f30c07ea6063e9f09b5e7cfc9dfa44075e74/mypy-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52686e37cf13d559f668aa398dd7ddf1f92c5d613e4f8cb262be2fb4fedb0fcb", size = 11211002, upload-time = "2024-12-30T16:37:22.435Z" }, + { url = "https://files.pythonhosted.org/packages/e1/23/eada4c38608b444618a132be0d199b280049ded278b24cbb9d3fc59658e4/mypy-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fb545ca340537d4b45d3eecdb3def05e913299ca72c290326be19b3804b39c0", size = 10358400, upload-time = "2024-12-30T16:37:53.526Z" }, + { url = "https://files.pythonhosted.org/packages/43/c9/d6785c6f66241c62fd2992b05057f404237deaad1566545e9f144ced07f5/mypy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90716d8b2d1f4cd503309788e51366f07c56635a3309b0f6a32547eaaa36a64d", size = 12095172, upload-time = "2024-12-30T16:37:50.332Z" }, + { url = "https://files.pythonhosted.org/packages/c3/62/daa7e787770c83c52ce2aaf1a111eae5893de9e004743f51bfcad9e487ec/mypy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae753f5c9fef278bcf12e1a564351764f2a6da579d4a81347e1d5a15819997b", size = 12828732, upload-time = "2024-12-30T16:37:29.96Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a2/5fb18318a3637f29f16f4e41340b795da14f4751ef4f51c99ff39ab62e52/mypy-1.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0fe0f5feaafcb04505bcf439e991c6d8f1bf8b15f12b05feeed96e9e7bf1427", size = 13012197, upload-time = "2024-12-30T16:38:05.037Z" }, + { url = "https://files.pythonhosted.org/packages/28/99/e153ce39105d164b5f02c06c35c7ba958aaff50a2babba7d080988b03fe7/mypy-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:7d54bd85b925e501c555a3227f3ec0cfc54ee8b6930bd6141ec872d1c572f81f", size = 9780836, upload-time = "2024-12-30T16:37:19.726Z" }, + { url = "https://files.pythonhosted.org/packages/da/11/a9422850fd506edbcdc7f6090682ecceaf1f87b9dd847f9df79942da8506/mypy-1.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f995e511de847791c3b11ed90084a7a0aafdc074ab88c5a9711622fe4751138c", size = 11120432, upload-time = "2024-12-30T16:37:11.533Z" }, + { url = "https://files.pythonhosted.org/packages/b6/9e/47e450fd39078d9c02d620545b2cb37993a8a8bdf7db3652ace2f80521ca/mypy-1.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d64169ec3b8461311f8ce2fd2eb5d33e2d0f2c7b49116259c51d0d96edee48d1", size = 10279515, upload-time = "2024-12-30T16:37:40.724Z" }, + { url = "https://files.pythonhosted.org/packages/01/b5/6c8d33bd0f851a7692a8bfe4ee75eb82b6983a3cf39e5e32a5d2a723f0c1/mypy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba24549de7b89b6381b91fbc068d798192b1b5201987070319889e93038967a8", size = 12025791, upload-time = "2024-12-30T16:36:58.73Z" }, + { url = "https://files.pythonhosted.org/packages/f0/4c/e10e2c46ea37cab5c471d0ddaaa9a434dc1d28650078ac1b56c2d7b9b2e4/mypy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:183cf0a45457d28ff9d758730cd0210419ac27d4d3f285beda038c9083363b1f", size = 12749203, upload-time = "2024-12-30T16:37:03.741Z" }, + { url = "https://files.pythonhosted.org/packages/88/55/beacb0c69beab2153a0f57671ec07861d27d735a0faff135a494cd4f5020/mypy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f2a0ecc86378f45347f586e4163d1769dd81c5a223d577fe351f26b179e148b1", size = 12885900, upload-time = "2024-12-30T16:37:57.948Z" }, + { url = "https://files.pythonhosted.org/packages/a2/75/8c93ff7f315c4d086a2dfcde02f713004357d70a163eddb6c56a6a5eff40/mypy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:ad3301ebebec9e8ee7135d8e3109ca76c23752bac1e717bc84cd3836b4bf3eae", size = 9777869, upload-time = "2024-12-30T16:37:33.428Z" }, + { url = "https://files.pythonhosted.org/packages/43/1b/b38c079609bb4627905b74fc6a49849835acf68547ac33d8ceb707de5f52/mypy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30ff5ef8519bbc2e18b3b54521ec319513a26f1bba19a7582e7b1f58a6e69f14", size = 11266668, upload-time = "2024-12-30T16:38:02.211Z" }, + { url = "https://files.pythonhosted.org/packages/6b/75/2ed0d2964c1ffc9971c729f7a544e9cd34b2cdabbe2d11afd148d7838aa2/mypy-1.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb9f255c18052343c70234907e2e532bc7e55a62565d64536dbc7706a20b78b9", size = 10254060, upload-time = "2024-12-30T16:37:46.131Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5f/7b8051552d4da3c51bbe8fcafffd76a6823779101a2b198d80886cd8f08e/mypy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b4e3413e0bddea671012b063e27591b953d653209e7a4fa5e48759cda77ca11", size = 11933167, upload-time = "2024-12-30T16:37:43.534Z" }, + { url = "https://files.pythonhosted.org/packages/04/90/f53971d3ac39d8b68bbaab9a4c6c58c8caa4d5fd3d587d16f5927eeeabe1/mypy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:553c293b1fbdebb6c3c4030589dab9fafb6dfa768995a453d8a5d3b23784af2e", size = 12864341, upload-time = "2024-12-30T16:37:36.249Z" }, + { url = "https://files.pythonhosted.org/packages/03/d2/8bc0aeaaf2e88c977db41583559319f1821c069e943ada2701e86d0430b7/mypy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fad79bfe3b65fe6a1efaed97b445c3d37f7be9fdc348bdb2d7cac75579607c89", size = 12972991, upload-time = "2024-12-30T16:37:06.743Z" }, + { url = "https://files.pythonhosted.org/packages/6f/17/07815114b903b49b0f2cf7499f1c130e5aa459411596668267535fe9243c/mypy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:8fa2220e54d2946e94ab6dbb3ba0a992795bd68b16dc852db33028df2b00191b", size = 9879016, upload-time = "2024-12-30T16:37:15.02Z" }, + { url = "https://files.pythonhosted.org/packages/9e/15/bb6a686901f59222275ab228453de741185f9d54fecbaacec041679496c6/mypy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:92c3ed5afb06c3a8e188cb5da4984cab9ec9a77ba956ee419c68a388b4595255", size = 11252097, upload-time = "2024-12-30T16:37:25.144Z" }, + { url = "https://files.pythonhosted.org/packages/f8/b3/8b0f74dfd072c802b7fa368829defdf3ee1566ba74c32a2cb2403f68024c/mypy-1.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dbec574648b3e25f43d23577309b16534431db4ddc09fda50841f1e34e64ed34", size = 10239728, upload-time = "2024-12-30T16:38:08.634Z" }, + { url = "https://files.pythonhosted.org/packages/c5/9b/4fd95ab20c52bb5b8c03cc49169be5905d931de17edfe4d9d2986800b52e/mypy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c6d94b16d62eb3e947281aa7347d78236688e21081f11de976376cf010eb31a", size = 11924965, upload-time = "2024-12-30T16:38:12.132Z" }, + { url = "https://files.pythonhosted.org/packages/56/9d/4a236b9c57f5d8f08ed346914b3f091a62dd7e19336b2b2a0d85485f82ff/mypy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4b19b03fdf54f3c5b2fa474c56b4c13c9dbfb9a2db4370ede7ec11a2c5927d9", size = 12867660, upload-time = "2024-12-30T16:38:17.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/88/a61a5497e2f68d9027de2bb139c7bb9abaeb1be1584649fa9d807f80a338/mypy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0c911fde686394753fff899c409fd4e16e9b294c24bfd5e1ea4675deae1ac6fd", size = 12969198, upload-time = "2024-12-30T16:38:32.839Z" }, + { url = "https://files.pythonhosted.org/packages/54/da/3d6fc5d92d324701b0c23fb413c853892bfe0e1dbe06c9138037d459756b/mypy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8b21525cb51671219f5307be85f7e646a153e5acc656e5cebf64bfa076c50107", size = 9885276, upload-time = "2024-12-30T16:38:20.828Z" }, + { url = "https://files.pythonhosted.org/packages/ca/1f/186d133ae2514633f8558e78cd658070ba686c0e9275c5a5c24a1e1f0d67/mypy-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3888a1816d69f7ab92092f785a462944b3ca16d7c470d564165fe703b0970c35", size = 11200493, upload-time = "2024-12-30T16:38:26.935Z" }, + { url = "https://files.pythonhosted.org/packages/af/fc/4842485d034e38a4646cccd1369f6b1ccd7bc86989c52770d75d719a9941/mypy-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46c756a444117c43ee984bd055db99e498bc613a70bbbc120272bd13ca579fbc", size = 10357702, upload-time = "2024-12-30T16:38:50.623Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e6/457b83f2d701e23869cfec013a48a12638f75b9d37612a9ddf99072c1051/mypy-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:27fc248022907e72abfd8e22ab1f10e903915ff69961174784a3900a8cba9ad9", size = 12091104, upload-time = "2024-12-30T16:38:53.735Z" }, + { url = "https://files.pythonhosted.org/packages/f1/bf/76a569158db678fee59f4fd30b8e7a0d75bcbaeef49edd882a0d63af6d66/mypy-1.14.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:499d6a72fb7e5de92218db961f1a66d5f11783f9ae549d214617edab5d4dbdbb", size = 12830167, upload-time = "2024-12-30T16:38:56.437Z" }, + { url = "https://files.pythonhosted.org/packages/43/bc/0bc6b694b3103de9fed61867f1c8bd33336b913d16831431e7cb48ef1c92/mypy-1.14.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:57961db9795eb566dc1d1b4e9139ebc4c6b0cb6e7254ecde69d1552bf7613f60", size = 13013834, upload-time = "2024-12-30T16:38:59.204Z" }, + { url = "https://files.pythonhosted.org/packages/b0/79/5f5ec47849b6df1e6943d5fd8e6632fbfc04b4fd4acfa5a5a9535d11b4e2/mypy-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:07ba89fdcc9451f2ebb02853deb6aaaa3d2239a236669a63ab3801bbf923ef5c", size = 9781231, upload-time = "2024-12-30T16:39:05.124Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b5/32dd67b69a16d088e533962e5044e51004176a9952419de0370cdaead0f8/mypy-1.14.1-py3-none-any.whl", hash = "sha256:b66a60cc4073aeb8ae00057f9c1f64d49e90f918fbcef9a977eb121da8b8f1d1", size = 2752905, upload-time = "2024-12-30T16:38:42.021Z" }, +] + +[[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 = "openai" +version = "2.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d8/b1/12fe1c196bea326261718eb037307c1c1fe1dedc2d2d4de777df822e6238/openai-2.14.0.tar.gz", hash = "sha256:419357bedde9402d23bf8f2ee372fca1985a73348debba94bddff06f19459952", size = 626938, upload-time = "2025-12-19T03:28:45.742Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/4b/7c1a00c2c3fbd004253937f7520f692a9650767aa73894d7a34f0d65d3f4/openai-2.14.0-py3-none-any.whl", hash = "sha256:7ea40aca4ffc4c4a776e77679021b47eec1160e341f42ae086ba949c9dcc9183", size = 1067558, upload-time = "2025-12-19T03:28:43.727Z" }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.39.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/97/b9/3161be15bb8e3ad01be8be5a968a9237c3027c5be504362ff800fca3e442/opentelemetry_api-1.39.1.tar.gz", hash = "sha256:fbde8c80e1b937a2c61f20347e91c0c18a1940cecf012d62e65a7caf08967c9c", size = 65767, upload-time = "2025-12-11T13:32:39.182Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/df/d3f1ddf4bb4cb50ed9b1139cc7b1c54c34a1e7ce8fd1b9a37c0d1551a6bd/opentelemetry_api-1.39.1-py3-none-any.whl", hash = "sha256:2edd8463432a7f8443edce90972169b195e7d6a05500cd29e6d13898187c9950", size = 66356, upload-time = "2025-12-11T13:32:17.304Z" }, +] + +[[package]] +name = "orjson" +version = "3.11.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/b8/333fdb27840f3bf04022d21b654a35f58e15407183aeb16f3b41aa053446/orjson-3.11.5.tar.gz", hash = "sha256:82393ab47b4fe44ffd0a7659fa9cfaacc717eb617c93cde83795f14af5c2e9d5", size = 5972347, upload-time = "2025-12-06T15:55:39.458Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/19/b22cf9dad4db20c8737041046054cbd4f38bb5a2d0e4bb60487832ce3d76/orjson-3.11.5-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:df9eadb2a6386d5ea2bfd81309c505e125cfc9ba2b1b99a97e60985b0b3665d1", size = 245719, upload-time = "2025-12-06T15:53:43.877Z" }, + { url = "https://files.pythonhosted.org/packages/03/2e/b136dd6bf30ef5143fbe76a4c142828b55ccc618be490201e9073ad954a1/orjson-3.11.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ccc70da619744467d8f1f49a8cadae5ec7bbe054e5232d95f92ed8737f8c5870", size = 132467, upload-time = "2025-12-06T15:53:45.379Z" }, + { url = "https://files.pythonhosted.org/packages/ae/fc/ae99bfc1e1887d20a0268f0e2686eb5b13d0ea7bbe01de2b566febcd2130/orjson-3.11.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:073aab025294c2f6fc0807201c76fdaed86f8fc4be52c440fb78fbb759a1ac09", size = 130702, upload-time = "2025-12-06T15:53:46.659Z" }, + { url = "https://files.pythonhosted.org/packages/6e/43/ef7912144097765997170aca59249725c3ab8ef6079f93f9d708dd058df5/orjson-3.11.5-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:835f26fa24ba0bb8c53ae2a9328d1706135b74ec653ed933869b74b6909e63fd", size = 135907, upload-time = "2025-12-06T15:53:48.487Z" }, + { url = "https://files.pythonhosted.org/packages/3f/da/24d50e2d7f4092ddd4d784e37a3fa41f22ce8ed97abc9edd222901a96e74/orjson-3.11.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667c132f1f3651c14522a119e4dd631fad98761fa960c55e8e7430bb2a1ba4ac", size = 139935, upload-time = "2025-12-06T15:53:49.88Z" }, + { url = "https://files.pythonhosted.org/packages/02/4a/b4cb6fcbfff5b95a3a019a8648255a0fac9b221fbf6b6e72be8df2361feb/orjson-3.11.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42e8961196af655bb5e63ce6c60d25e8798cd4dfbc04f4203457fa3869322c2e", size = 137541, upload-time = "2025-12-06T15:53:51.226Z" }, + { url = "https://files.pythonhosted.org/packages/a5/99/a11bd129f18c2377c27b2846a9d9be04acec981f770d711ba0aaea563984/orjson-3.11.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75412ca06e20904c19170f8a24486c4e6c7887dea591ba18a1ab572f1300ee9f", size = 139031, upload-time = "2025-12-06T15:53:52.309Z" }, + { url = "https://files.pythonhosted.org/packages/64/29/d7b77d7911574733a036bb3e8ad7053ceb2b7d6ea42208b9dbc55b23b9ed/orjson-3.11.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6af8680328c69e15324b5af3ae38abbfcf9cbec37b5346ebfd52339c3d7e8a18", size = 141622, upload-time = "2025-12-06T15:53:53.606Z" }, + { url = "https://files.pythonhosted.org/packages/93/41/332db96c1de76b2feda4f453e91c27202cd092835936ce2b70828212f726/orjson-3.11.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a86fe4ff4ea523eac8f4b57fdac319faf037d3c1be12405e6a7e86b3fbc4756a", size = 413800, upload-time = "2025-12-06T15:53:54.866Z" }, + { url = "https://files.pythonhosted.org/packages/76/e1/5a0d148dd1f89ad2f9651df67835b209ab7fcb1118658cf353425d7563e9/orjson-3.11.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e607b49b1a106ee2086633167033afbd63f76f2999e9236f638b06b112b24ea7", size = 151198, upload-time = "2025-12-06T15:53:56.383Z" }, + { url = "https://files.pythonhosted.org/packages/0d/96/8db67430d317a01ae5cf7971914f6775affdcfe99f5bff9ef3da32492ecc/orjson-3.11.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7339f41c244d0eea251637727f016b3d20050636695bc78345cce9029b189401", size = 141984, upload-time = "2025-12-06T15:53:57.746Z" }, + { url = "https://files.pythonhosted.org/packages/71/49/40d21e1aa1ac569e521069228bb29c9b5a350344ccf922a0227d93c2ed44/orjson-3.11.5-cp310-cp310-win32.whl", hash = "sha256:8be318da8413cdbbce77b8c5fac8d13f6eb0f0db41b30bb598631412619572e8", size = 135272, upload-time = "2025-12-06T15:53:59.769Z" }, + { url = "https://files.pythonhosted.org/packages/c4/7e/d0e31e78be0c100e08be64f48d2850b23bcb4d4c70d114f4e43b39f6895a/orjson-3.11.5-cp310-cp310-win_amd64.whl", hash = "sha256:b9f86d69ae822cabc2a0f6c099b43e8733dda788405cba2665595b7e8dd8d167", size = 133360, upload-time = "2025-12-06T15:54:01.25Z" }, + { url = "https://files.pythonhosted.org/packages/fd/68/6b3659daec3a81aed5ab47700adb1a577c76a5452d35b91c88efee89987f/orjson-3.11.5-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9c8494625ad60a923af6b2b0bd74107146efe9b55099e20d7740d995f338fcd8", size = 245318, upload-time = "2025-12-06T15:54:02.355Z" }, + { url = "https://files.pythonhosted.org/packages/e9/00/92db122261425f61803ccf0830699ea5567439d966cbc35856fe711bfe6b/orjson-3.11.5-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:7bb2ce0b82bc9fd1168a513ddae7a857994b780b2945a8c51db4ab1c4b751ebc", size = 129491, upload-time = "2025-12-06T15:54:03.877Z" }, + { url = "https://files.pythonhosted.org/packages/94/4f/ffdcb18356518809d944e1e1f77589845c278a1ebbb5a8297dfefcc4b4cb/orjson-3.11.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67394d3becd50b954c4ecd24ac90b5051ee7c903d167459f93e77fc6f5b4c968", size = 132167, upload-time = "2025-12-06T15:54:04.944Z" }, + { url = "https://files.pythonhosted.org/packages/97/c6/0a8caff96f4503f4f7dd44e40e90f4d14acf80d3b7a97cb88747bb712d3e/orjson-3.11.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:298d2451f375e5f17b897794bcc3e7b821c0f32b4788b9bcae47ada24d7f3cf7", size = 130516, upload-time = "2025-12-06T15:54:06.274Z" }, + { url = "https://files.pythonhosted.org/packages/4d/63/43d4dc9bd9954bff7052f700fdb501067f6fb134a003ddcea2a0bb3854ed/orjson-3.11.5-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa5e4244063db8e1d87e0f54c3f7522f14b2dc937e65d5241ef0076a096409fd", size = 135695, upload-time = "2025-12-06T15:54:07.702Z" }, + { url = "https://files.pythonhosted.org/packages/87/6f/27e2e76d110919cb7fcb72b26166ee676480a701bcf8fc53ac5d0edce32f/orjson-3.11.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1db2088b490761976c1b2e956d5d4e6409f3732e9d79cfa69f876c5248d1baf9", size = 139664, upload-time = "2025-12-06T15:54:08.828Z" }, + { url = "https://files.pythonhosted.org/packages/d4/f8/5966153a5f1be49b5fbb8ca619a529fde7bc71aa0a376f2bb83fed248bcd/orjson-3.11.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c2ed66358f32c24e10ceea518e16eb3549e34f33a9d51f99ce23b0251776a1ef", size = 137289, upload-time = "2025-12-06T15:54:09.898Z" }, + { url = "https://files.pythonhosted.org/packages/a7/34/8acb12ff0299385c8bbcbb19fbe40030f23f15a6de57a9c587ebf71483fb/orjson-3.11.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2021afda46c1ed64d74b555065dbd4c2558d510d8cec5ea6a53001b3e5e82a9", size = 138784, upload-time = "2025-12-06T15:54:11.022Z" }, + { url = "https://files.pythonhosted.org/packages/ee/27/910421ea6e34a527f73d8f4ee7bdffa48357ff79c7b8d6eb6f7b82dd1176/orjson-3.11.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b42ffbed9128e547a1647a3e50bc88ab28ae9daa61713962e0d3dd35e820c125", size = 141322, upload-time = "2025-12-06T15:54:12.427Z" }, + { url = "https://files.pythonhosted.org/packages/87/a3/4b703edd1a05555d4bb1753d6ce44e1a05b7a6d7c164d5b332c795c63d70/orjson-3.11.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:8d5f16195bb671a5dd3d1dbea758918bada8f6cc27de72bd64adfbd748770814", size = 413612, upload-time = "2025-12-06T15:54:13.858Z" }, + { url = "https://files.pythonhosted.org/packages/1b/36/034177f11d7eeea16d3d2c42a1883b0373978e08bc9dad387f5074c786d8/orjson-3.11.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c0e5d9f7a0227df2927d343a6e3859bebf9208b427c79bd31949abcc2fa32fa5", size = 150993, upload-time = "2025-12-06T15:54:15.189Z" }, + { url = "https://files.pythonhosted.org/packages/44/2f/ea8b24ee046a50a7d141c0227c4496b1180b215e728e3b640684f0ea448d/orjson-3.11.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:23d04c4543e78f724c4dfe656b3791b5f98e4c9253e13b2636f1af5d90e4a880", size = 141774, upload-time = "2025-12-06T15:54:16.451Z" }, + { url = "https://files.pythonhosted.org/packages/8a/12/cc440554bf8200eb23348a5744a575a342497b65261cd65ef3b28332510a/orjson-3.11.5-cp311-cp311-win32.whl", hash = "sha256:c404603df4865f8e0afe981aa3c4b62b406e6d06049564d58934860b62b7f91d", size = 135109, upload-time = "2025-12-06T15:54:17.73Z" }, + { url = "https://files.pythonhosted.org/packages/a3/83/e0c5aa06ba73a6760134b169f11fb970caa1525fa4461f94d76e692299d9/orjson-3.11.5-cp311-cp311-win_amd64.whl", hash = "sha256:9645ef655735a74da4990c24ffbd6894828fbfa117bc97c1edd98c282ecb52e1", size = 133193, upload-time = "2025-12-06T15:54:19.426Z" }, + { url = "https://files.pythonhosted.org/packages/cb/35/5b77eaebc60d735e832c5b1a20b155667645d123f09d471db0a78280fb49/orjson-3.11.5-cp311-cp311-win_arm64.whl", hash = "sha256:1cbf2735722623fcdee8e712cbaaab9e372bbcb0c7924ad711b261c2eccf4a5c", size = 126830, upload-time = "2025-12-06T15:54:20.836Z" }, + { url = "https://files.pythonhosted.org/packages/ef/a4/8052a029029b096a78955eadd68ab594ce2197e24ec50e6b6d2ab3f4e33b/orjson-3.11.5-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:334e5b4bff9ad101237c2d799d9fd45737752929753bf4faf4b207335a416b7d", size = 245347, upload-time = "2025-12-06T15:54:22.061Z" }, + { url = "https://files.pythonhosted.org/packages/64/67/574a7732bd9d9d79ac620c8790b4cfe0717a3d5a6eb2b539e6e8995e24a0/orjson-3.11.5-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:ff770589960a86eae279f5d8aa536196ebda8273a2a07db2a54e82b93bc86626", size = 129435, upload-time = "2025-12-06T15:54:23.615Z" }, + { url = "https://files.pythonhosted.org/packages/52/8d/544e77d7a29d90cf4d9eecd0ae801c688e7f3d1adfa2ebae5e1e94d38ab9/orjson-3.11.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed24250e55efbcb0b35bed7caaec8cedf858ab2f9f2201f17b8938c618c8ca6f", size = 132074, upload-time = "2025-12-06T15:54:24.694Z" }, + { url = "https://files.pythonhosted.org/packages/6e/57/b9f5b5b6fbff9c26f77e785baf56ae8460ef74acdb3eae4931c25b8f5ba9/orjson-3.11.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a66d7769e98a08a12a139049aac2f0ca3adae989817f8c43337455fbc7669b85", size = 130520, upload-time = "2025-12-06T15:54:26.185Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6d/d34970bf9eb33f9ec7c979a262cad86076814859e54eb9a059a52f6dc13d/orjson-3.11.5-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:86cfc555bfd5794d24c6a1903e558b50644e5e68e6471d66502ce5cb5fdef3f9", size = 136209, upload-time = "2025-12-06T15:54:27.264Z" }, + { url = "https://files.pythonhosted.org/packages/e7/39/bc373b63cc0e117a105ea12e57280f83ae52fdee426890d57412432d63b3/orjson-3.11.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a230065027bc2a025e944f9d4714976a81e7ecfa940923283bca7bbc1f10f626", size = 139837, upload-time = "2025-12-06T15:54:28.75Z" }, + { url = "https://files.pythonhosted.org/packages/cb/aa/7c4818c8d7d324da220f4f1af55c343956003aa4d1ce1857bdc1d396ba69/orjson-3.11.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b29d36b60e606df01959c4b982729c8845c69d1963f88686608be9ced96dbfaa", size = 137307, upload-time = "2025-12-06T15:54:29.856Z" }, + { url = "https://files.pythonhosted.org/packages/46/bf/0993b5a056759ba65145effe3a79dd5a939d4a070eaa5da2ee3180fbb13f/orjson-3.11.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c74099c6b230d4261fdc3169d50efc09abf38ace1a42ea2f9994b1d79153d477", size = 139020, upload-time = "2025-12-06T15:54:31.024Z" }, + { url = "https://files.pythonhosted.org/packages/65/e8/83a6c95db3039e504eda60fc388f9faedbb4f6472f5aba7084e06552d9aa/orjson-3.11.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e697d06ad57dd0c7a737771d470eedc18e68dfdefcdd3b7de7f33dfda5b6212e", size = 141099, upload-time = "2025-12-06T15:54:32.196Z" }, + { url = "https://files.pythonhosted.org/packages/b9/b4/24fdc024abfce31c2f6812973b0a693688037ece5dc64b7a60c1ce69e2f2/orjson-3.11.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e08ca8a6c851e95aaecc32bc44a5aa75d0ad26af8cdac7c77e4ed93acf3d5b69", size = 413540, upload-time = "2025-12-06T15:54:33.361Z" }, + { url = "https://files.pythonhosted.org/packages/d9/37/01c0ec95d55ed0c11e4cae3e10427e479bba40c77312b63e1f9665e0737d/orjson-3.11.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e8b5f96c05fce7d0218df3fdfeb962d6b8cfff7e3e20264306b46dd8b217c0f3", size = 151530, upload-time = "2025-12-06T15:54:34.6Z" }, + { url = "https://files.pythonhosted.org/packages/f9/d4/f9ebc57182705bb4bbe63f5bbe14af43722a2533135e1d2fb7affa0c355d/orjson-3.11.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ddbfdb5099b3e6ba6d6ea818f61997bb66de14b411357d24c4612cf1ebad08ca", size = 141863, upload-time = "2025-12-06T15:54:35.801Z" }, + { url = "https://files.pythonhosted.org/packages/0d/04/02102b8d19fdcb009d72d622bb5781e8f3fae1646bf3e18c53d1bc8115b5/orjson-3.11.5-cp312-cp312-win32.whl", hash = "sha256:9172578c4eb09dbfcf1657d43198de59b6cef4054de385365060ed50c458ac98", size = 135255, upload-time = "2025-12-06T15:54:37.209Z" }, + { url = "https://files.pythonhosted.org/packages/d4/fb/f05646c43d5450492cb387de5549f6de90a71001682c17882d9f66476af5/orjson-3.11.5-cp312-cp312-win_amd64.whl", hash = "sha256:2b91126e7b470ff2e75746f6f6ee32b9ab67b7a93c8ba1d15d3a0caaf16ec875", size = 133252, upload-time = "2025-12-06T15:54:38.401Z" }, + { url = "https://files.pythonhosted.org/packages/dc/a6/7b8c0b26ba18c793533ac1cd145e131e46fcf43952aa94c109b5b913c1f0/orjson-3.11.5-cp312-cp312-win_arm64.whl", hash = "sha256:acbc5fac7e06777555b0722b8ad5f574739e99ffe99467ed63da98f97f9ca0fe", size = 126777, upload-time = "2025-12-06T15:54:39.515Z" }, + { url = "https://files.pythonhosted.org/packages/10/43/61a77040ce59f1569edf38f0b9faadc90c8cf7e9bec2e0df51d0132c6bb7/orjson-3.11.5-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:3b01799262081a4c47c035dd77c1301d40f568f77cc7ec1bb7db5d63b0a01629", size = 245271, upload-time = "2025-12-06T15:54:40.878Z" }, + { url = "https://files.pythonhosted.org/packages/55/f9/0f79be617388227866d50edd2fd320cb8fb94dc1501184bb1620981a0aba/orjson-3.11.5-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:61de247948108484779f57a9f406e4c84d636fa5a59e411e6352484985e8a7c3", size = 129422, upload-time = "2025-12-06T15:54:42.403Z" }, + { url = "https://files.pythonhosted.org/packages/77/42/f1bf1549b432d4a78bfa95735b79b5dac75b65b5bb815bba86ad406ead0a/orjson-3.11.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:894aea2e63d4f24a7f04a1908307c738d0dce992e9249e744b8f4e8dd9197f39", size = 132060, upload-time = "2025-12-06T15:54:43.531Z" }, + { url = "https://files.pythonhosted.org/packages/25/49/825aa6b929f1a6ed244c78acd7b22c1481fd7e5fda047dc8bf4c1a807eb6/orjson-3.11.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ddc21521598dbe369d83d4d40338e23d4101dad21dae0e79fa20465dbace019f", size = 130391, upload-time = "2025-12-06T15:54:45.059Z" }, + { url = "https://files.pythonhosted.org/packages/42/ec/de55391858b49e16e1aa8f0bbbb7e5997b7345d8e984a2dec3746d13065b/orjson-3.11.5-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7cce16ae2f5fb2c53c3eafdd1706cb7b6530a67cc1c17abe8ec747f5cd7c0c51", size = 135964, upload-time = "2025-12-06T15:54:46.576Z" }, + { url = "https://files.pythonhosted.org/packages/1c/40/820bc63121d2d28818556a2d0a09384a9f0262407cf9fa305e091a8048df/orjson-3.11.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e46c762d9f0e1cfb4ccc8515de7f349abbc95b59cb5a2bd68df5973fdef913f8", size = 139817, upload-time = "2025-12-06T15:54:48.084Z" }, + { url = "https://files.pythonhosted.org/packages/09/c7/3a445ca9a84a0d59d26365fd8898ff52bdfcdcb825bcc6519830371d2364/orjson-3.11.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d7345c759276b798ccd6d77a87136029e71e66a8bbf2d2755cbdde1d82e78706", size = 137336, upload-time = "2025-12-06T15:54:49.426Z" }, + { url = "https://files.pythonhosted.org/packages/9a/b3/dc0d3771f2e5d1f13368f56b339c6782f955c6a20b50465a91acb79fe961/orjson-3.11.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75bc2e59e6a2ac1dd28901d07115abdebc4563b5b07dd612bf64260a201b1c7f", size = 138993, upload-time = "2025-12-06T15:54:50.939Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a2/65267e959de6abe23444659b6e19c888f242bf7725ff927e2292776f6b89/orjson-3.11.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:54aae9b654554c3b4edd61896b978568c6daa16af96fa4681c9b5babd469f863", size = 141070, upload-time = "2025-12-06T15:54:52.414Z" }, + { url = "https://files.pythonhosted.org/packages/63/c9/da44a321b288727a322c6ab17e1754195708786a04f4f9d2220a5076a649/orjson-3.11.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:4bdd8d164a871c4ec773f9de0f6fe8769c2d6727879c37a9666ba4183b7f8228", size = 413505, upload-time = "2025-12-06T15:54:53.67Z" }, + { url = "https://files.pythonhosted.org/packages/7f/17/68dc14fa7000eefb3d4d6d7326a190c99bb65e319f02747ef3ebf2452f12/orjson-3.11.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a261fef929bcf98a60713bf5e95ad067cea16ae345d9a35034e73c3990e927d2", size = 151342, upload-time = "2025-12-06T15:54:55.113Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c5/ccee774b67225bed630a57478529fc026eda33d94fe4c0eac8fe58d4aa52/orjson-3.11.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c028a394c766693c5c9909dec76b24f37e6a1b91999e8d0c0d5feecbe93c3e05", size = 141823, upload-time = "2025-12-06T15:54:56.331Z" }, + { url = "https://files.pythonhosted.org/packages/67/80/5d00e4155d0cd7390ae2087130637671da713959bb558db9bac5e6f6b042/orjson-3.11.5-cp313-cp313-win32.whl", hash = "sha256:2cc79aaad1dfabe1bd2d50ee09814a1253164b3da4c00a78c458d82d04b3bdef", size = 135236, upload-time = "2025-12-06T15:54:57.507Z" }, + { url = "https://files.pythonhosted.org/packages/95/fe/792cc06a84808dbdc20ac6eab6811c53091b42f8e51ecebf14b540e9cfe4/orjson-3.11.5-cp313-cp313-win_amd64.whl", hash = "sha256:ff7877d376add4e16b274e35a3f58b7f37b362abf4aa31863dadacdd20e3a583", size = 133167, upload-time = "2025-12-06T15:54:58.71Z" }, + { url = "https://files.pythonhosted.org/packages/46/2c/d158bd8b50e3b1cfdcf406a7e463f6ffe3f0d167b99634717acdaf5e299f/orjson-3.11.5-cp313-cp313-win_arm64.whl", hash = "sha256:59ac72ea775c88b163ba8d21b0177628bd015c5dd060647bbab6e22da3aad287", size = 126712, upload-time = "2025-12-06T15:54:59.892Z" }, + { url = "https://files.pythonhosted.org/packages/c2/60/77d7b839e317ead7bb225d55bb50f7ea75f47afc489c81199befc5435b50/orjson-3.11.5-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e446a8ea0a4c366ceafc7d97067bfd55292969143b57e3c846d87fc701e797a0", size = 245252, upload-time = "2025-12-06T15:55:01.127Z" }, + { url = "https://files.pythonhosted.org/packages/f1/aa/d4639163b400f8044cef0fb9aa51b0337be0da3a27187a20d1166e742370/orjson-3.11.5-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:53deb5addae9c22bbe3739298f5f2196afa881ea75944e7720681c7080909a81", size = 129419, upload-time = "2025-12-06T15:55:02.723Z" }, + { url = "https://files.pythonhosted.org/packages/30/94/9eabf94f2e11c671111139edf5ec410d2f21e6feee717804f7e8872d883f/orjson-3.11.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82cd00d49d6063d2b8791da5d4f9d20539c5951f965e45ccf4e96d33505ce68f", size = 132050, upload-time = "2025-12-06T15:55:03.918Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c8/ca10f5c5322f341ea9a9f1097e140be17a88f88d1cfdd29df522970d9744/orjson-3.11.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3fd15f9fc8c203aeceff4fda211157fad114dde66e92e24097b3647a08f4ee9e", size = 130370, upload-time = "2025-12-06T15:55:05.173Z" }, + { url = "https://files.pythonhosted.org/packages/25/d4/e96824476d361ee2edd5c6290ceb8d7edf88d81148a6ce172fc00278ca7f/orjson-3.11.5-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9df95000fbe6777bf9820ae82ab7578e8662051bb5f83d71a28992f539d2cda7", size = 136012, upload-time = "2025-12-06T15:55:06.402Z" }, + { url = "https://files.pythonhosted.org/packages/85/8e/9bc3423308c425c588903f2d103cfcfe2539e07a25d6522900645a6f257f/orjson-3.11.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92a8d676748fca47ade5bc3da7430ed7767afe51b2f8100e3cd65e151c0eaceb", size = 139809, upload-time = "2025-12-06T15:55:07.656Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3c/b404e94e0b02a232b957c54643ce68d0268dacb67ac33ffdee24008c8b27/orjson-3.11.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aa0f513be38b40234c77975e68805506cad5d57b3dfd8fe3baa7f4f4051e15b4", size = 137332, upload-time = "2025-12-06T15:55:08.961Z" }, + { url = "https://files.pythonhosted.org/packages/51/30/cc2d69d5ce0ad9b84811cdf4a0cd5362ac27205a921da524ff42f26d65e0/orjson-3.11.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa1863e75b92891f553b7922ce4ee10ed06db061e104f2b7815de80cdcb135ad", size = 138983, upload-time = "2025-12-06T15:55:10.595Z" }, + { url = "https://files.pythonhosted.org/packages/0e/87/de3223944a3e297d4707d2fe3b1ffb71437550e165eaf0ca8bbe43ccbcb1/orjson-3.11.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4be86b58e9ea262617b8ca6251a2f0d63cc132a6da4b5fcc8e0a4128782c829", size = 141069, upload-time = "2025-12-06T15:55:11.832Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/81d5087ae74be33bcae3ff2d80f5ccaa4a8fedc6d39bf65a427a95b8977f/orjson-3.11.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:b923c1c13fa02084eb38c9c065afd860a5cff58026813319a06949c3af5732ac", size = 413491, upload-time = "2025-12-06T15:55:13.314Z" }, + { url = "https://files.pythonhosted.org/packages/d0/6f/f6058c21e2fc1efaf918986dbc2da5cd38044f1a2d4b7b91ad17c4acf786/orjson-3.11.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:1b6bd351202b2cd987f35a13b5e16471cf4d952b42a73c391cc537974c43ef6d", size = 151375, upload-time = "2025-12-06T15:55:14.715Z" }, + { url = "https://files.pythonhosted.org/packages/54/92/c6921f17d45e110892899a7a563a925b2273d929959ce2ad89e2525b885b/orjson-3.11.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bb150d529637d541e6af06bbe3d02f5498d628b7f98267ff87647584293ab439", size = 141850, upload-time = "2025-12-06T15:55:15.94Z" }, + { url = "https://files.pythonhosted.org/packages/88/86/cdecb0140a05e1a477b81f24739da93b25070ee01ce7f7242f44a6437594/orjson-3.11.5-cp314-cp314-win32.whl", hash = "sha256:9cc1e55c884921434a84a0c3dd2699eb9f92e7b441d7f53f3941079ec6ce7499", size = 135278, upload-time = "2025-12-06T15:55:17.202Z" }, + { url = "https://files.pythonhosted.org/packages/e4/97/b638d69b1e947d24f6109216997e38922d54dcdcdb1b11c18d7efd2d3c59/orjson-3.11.5-cp314-cp314-win_amd64.whl", hash = "sha256:a4f3cb2d874e03bc7767c8f88adaa1a9a05cecea3712649c3b58589ec7317310", size = 133170, upload-time = "2025-12-06T15:55:18.468Z" }, + { url = "https://files.pythonhosted.org/packages/8f/dd/f4fff4a6fe601b4f8f3ba3aa6da8ac33d17d124491a3b804c662a70e1636/orjson-3.11.5-cp314-cp314-win_arm64.whl", hash = "sha256:38b22f476c351f9a1c43e5b07d8b5a02eb24a6ab8e75f700f7d479d4568346a5", size = 126713, upload-time = "2025-12-06T15:55:19.738Z" }, + { url = "https://files.pythonhosted.org/packages/50/c7/7b682849dd4c9fb701a981669b964ea700516ecbd8e88f62aae07c6852bd/orjson-3.11.5-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1b280e2d2d284a6713b0cfec7b08918ebe57df23e3f76b27586197afca3cb1e9", size = 245298, upload-time = "2025-12-06T15:55:20.984Z" }, + { url = "https://files.pythonhosted.org/packages/1b/3f/194355a9335707a15fdc79ddc670148987b43d04712dd26898a694539ce6/orjson-3.11.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c8d8a112b274fae8c5f0f01954cb0480137072c271f3f4958127b010dfefaec", size = 132150, upload-time = "2025-12-06T15:55:22.364Z" }, + { url = "https://files.pythonhosted.org/packages/e9/08/d74b3a986d37e6c2e04b8821c62927620c9a1924bb49ea51519a87751b86/orjson-3.11.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f0a2ae6f09ac7bd47d2d5a5305c1d9ed08ac057cda55bb0a49fa506f0d2da00", size = 130490, upload-time = "2025-12-06T15:55:23.619Z" }, + { url = "https://files.pythonhosted.org/packages/b2/16/ebd04c38c1db01e493a68eee442efdffc505a43112eccd481e0146c6acc2/orjson-3.11.5-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c0d87bd1896faac0d10b4f849016db81a63e4ec5df38757ffae84d45ab38aa71", size = 135726, upload-time = "2025-12-06T15:55:24.912Z" }, + { url = "https://files.pythonhosted.org/packages/06/64/2ce4b2c09a099403081c37639c224bdcdfe401138bd66fed5c96d4f8dbd3/orjson-3.11.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:801a821e8e6099b8c459ac7540b3c32dba6013437c57fdcaec205b169754f38c", size = 139640, upload-time = "2025-12-06T15:55:26.535Z" }, + { url = "https://files.pythonhosted.org/packages/cd/e2/425796df8ee1d7cea3a7edf868920121dd09162859dbb76fffc9a5c37fd3/orjson-3.11.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:69a0f6ac618c98c74b7fbc8c0172ba86f9e01dbf9f62aa0b1776c2231a7bffe5", size = 137289, upload-time = "2025-12-06T15:55:27.78Z" }, + { url = "https://files.pythonhosted.org/packages/32/a2/88e482eb8e899a037dcc9eff85ef117a568e6ca1ffa1a2b2be3fcb51b7bb/orjson-3.11.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fea7339bdd22e6f1060c55ac31b6a755d86a5b2ad3657f2669ec243f8e3b2bdb", size = 138761, upload-time = "2025-12-06T15:55:29.388Z" }, + { url = "https://files.pythonhosted.org/packages/f1/fd/131dd6d32eeb74c513bfa487f434a2150811d0fbd9cb06689284f2f21b34/orjson-3.11.5-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4dad582bc93cef8f26513e12771e76385a7e6187fd713157e971c784112aad56", size = 141357, upload-time = "2025-12-06T15:55:31.064Z" }, + { url = "https://files.pythonhosted.org/packages/7a/90/e4a0abbcca7b53e9098ac854f27f5ed9949c796f3c760bc04af997da0eb2/orjson-3.11.5-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:0522003e9f7fba91982e83a97fec0708f5a714c96c4209db7104e6b9d132f111", size = 413638, upload-time = "2025-12-06T15:55:32.344Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c2/df91e385514924120001ade9cd52d6295251023d3bfa2c0a01f38cfc485a/orjson-3.11.5-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:7403851e430a478440ecc1258bcbacbfbd8175f9ac1e39031a7121dd0de05ff8", size = 150972, upload-time = "2025-12-06T15:55:33.725Z" }, + { url = "https://files.pythonhosted.org/packages/a6/ff/c76cc5a30a4451191ff1b868a331ad1354433335277fc40931f5fc3cab9d/orjson-3.11.5-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5f691263425d3177977c8d1dd896cde7b98d93cbf390b2544a090675e83a6a0a", size = 141729, upload-time = "2025-12-06T15:55:35.317Z" }, + { url = "https://files.pythonhosted.org/packages/27/c3/7830bf74389ea1eaab2b017d8b15d1cab2bb0737d9412dfa7fb8644f7d78/orjson-3.11.5-cp39-cp39-win32.whl", hash = "sha256:61026196a1c4b968e1b1e540563e277843082e9e97d78afa03eb89315af531f1", size = 135100, upload-time = "2025-12-06T15:55:36.57Z" }, + { url = "https://files.pythonhosted.org/packages/69/e6/babf31154e047e465bc194eb72d1326d7c52ad4d7f50bf92b02b3cacda5c/orjson-3.11.5-cp39-cp39-win_amd64.whl", hash = "sha256:09b94b947ac08586af635ef922d69dc9bc63321527a3a04647f4986a73f4bd30", size = 133189, upload-time = "2025-12-06T15:55:38.143Z" }, +] + +[[package]] +name = "outcome" +version = "1.3.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/df/77698abfac98571e65ffeb0c1fba8ffd692ab8458d617a0eed7d9a8d38f2/outcome-1.3.0.post0.tar.gz", hash = "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8", size = 21060, upload-time = "2023-10-26T04:26:04.361Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/8b/5ab7257531a5d830fc8000c476e63c935488d74609b50f9384a643ec0a62/outcome-1.3.0.post0-py2.py3-none-any.whl", hash = "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b", size = 10692, upload-time = "2023-10-26T04:26:02.532Z" }, +] + +[[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.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/b9/6eb731b52f132181a9144bbe77ff82117f6b2d2fbfba49aaab2c014c4760/pathspec-1.0.2.tar.gz", hash = "sha256:fa32b1eb775ed9ba8d599b22c5f906dc098113989da2c00bf8b210078ca7fb92", size = 130502, upload-time = "2026-01-08T04:33:27.613Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/6b/14fc9049d78435fd29e82846c777bd7ed9c470013dc8d0260fff3ff1c11e/pathspec-1.0.2-py3-none-any.whl", hash = "sha256:62f8558917908d237d399b9b338ef455a814801a4688bc41074b25feefd93472", size = 54844, upload-time = "2026-01-08T04:33:26.4Z" }, +] + +[[package]] +name = "pillow" +version = "11.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069, upload-time = "2025-07-01T09:16:30.666Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/5d/45a3553a253ac8763f3561371432a90bdbe6000fbdcf1397ffe502aa206c/pillow-11.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860", size = 5316554, upload-time = "2025-07-01T09:13:39.342Z" }, + { url = "https://files.pythonhosted.org/packages/7c/c8/67c12ab069ef586a25a4a79ced553586748fad100c77c0ce59bb4983ac98/pillow-11.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad", size = 4686548, upload-time = "2025-07-01T09:13:41.835Z" }, + { url = "https://files.pythonhosted.org/packages/2f/bd/6741ebd56263390b382ae4c5de02979af7f8bd9807346d068700dd6d5cf9/pillow-11.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7107195ddc914f656c7fc8e4a5e1c25f32e9236ea3ea860f257b0436011fddd0", size = 5859742, upload-time = "2025-07-03T13:09:47.439Z" }, + { url = "https://files.pythonhosted.org/packages/ca/0b/c412a9e27e1e6a829e6ab6c2dca52dd563efbedf4c9c6aa453d9a9b77359/pillow-11.3.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc3e831b563b3114baac7ec2ee86819eb03caa1a2cef0b481a5675b59c4fe23b", size = 7633087, upload-time = "2025-07-03T13:09:51.796Z" }, + { url = "https://files.pythonhosted.org/packages/59/9d/9b7076aaf30f5dd17e5e5589b2d2f5a5d7e30ff67a171eb686e4eecc2adf/pillow-11.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1f182ebd2303acf8c380a54f615ec883322593320a9b00438eb842c1f37ae50", size = 5963350, upload-time = "2025-07-01T09:13:43.865Z" }, + { url = "https://files.pythonhosted.org/packages/f0/16/1a6bf01fb622fb9cf5c91683823f073f053005c849b1f52ed613afcf8dae/pillow-11.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4445fa62e15936a028672fd48c4c11a66d641d2c05726c7ec1f8ba6a572036ae", size = 6631840, upload-time = "2025-07-01T09:13:46.161Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e6/6ff7077077eb47fde78739e7d570bdcd7c10495666b6afcd23ab56b19a43/pillow-11.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:71f511f6b3b91dd543282477be45a033e4845a40278fa8dcdbfdb07109bf18f9", size = 6074005, upload-time = "2025-07-01T09:13:47.829Z" }, + { url = "https://files.pythonhosted.org/packages/c3/3a/b13f36832ea6d279a697231658199e0a03cd87ef12048016bdcc84131601/pillow-11.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:040a5b691b0713e1f6cbe222e0f4f74cd233421e105850ae3b3c0ceda520f42e", size = 6708372, upload-time = "2025-07-01T09:13:52.145Z" }, + { url = "https://files.pythonhosted.org/packages/6c/e4/61b2e1a7528740efbc70b3d581f33937e38e98ef3d50b05007267a55bcb2/pillow-11.3.0-cp310-cp310-win32.whl", hash = "sha256:89bd777bc6624fe4115e9fac3352c79ed60f3bb18651420635f26e643e3dd1f6", size = 6277090, upload-time = "2025-07-01T09:13:53.915Z" }, + { url = "https://files.pythonhosted.org/packages/a9/d3/60c781c83a785d6afbd6a326ed4d759d141de43aa7365725cbcd65ce5e54/pillow-11.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:19d2ff547c75b8e3ff46f4d9ef969a06c30ab2d4263a9e287733aa8b2429ce8f", size = 6985988, upload-time = "2025-07-01T09:13:55.699Z" }, + { url = "https://files.pythonhosted.org/packages/9f/28/4f4a0203165eefb3763939c6789ba31013a2e90adffb456610f30f613850/pillow-11.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:819931d25e57b513242859ce1876c58c59dc31587847bf74cfe06b2e0cb22d2f", size = 2422899, upload-time = "2025-07-01T09:13:57.497Z" }, + { url = "https://files.pythonhosted.org/packages/db/26/77f8ed17ca4ffd60e1dcd220a6ec6d71210ba398cfa33a13a1cd614c5613/pillow-11.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722", size = 5316531, upload-time = "2025-07-01T09:13:59.203Z" }, + { url = "https://files.pythonhosted.org/packages/cb/39/ee475903197ce709322a17a866892efb560f57900d9af2e55f86db51b0a5/pillow-11.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288", size = 4686560, upload-time = "2025-07-01T09:14:01.101Z" }, + { url = "https://files.pythonhosted.org/packages/d5/90/442068a160fd179938ba55ec8c97050a612426fae5ec0a764e345839f76d/pillow-11.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d", size = 5870978, upload-time = "2025-07-03T13:09:55.638Z" }, + { url = "https://files.pythonhosted.org/packages/13/92/dcdd147ab02daf405387f0218dcf792dc6dd5b14d2573d40b4caeef01059/pillow-11.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494", size = 7641168, upload-time = "2025-07-03T13:10:00.37Z" }, + { url = "https://files.pythonhosted.org/packages/6e/db/839d6ba7fd38b51af641aa904e2960e7a5644d60ec754c046b7d2aee00e5/pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58", size = 5973053, upload-time = "2025-07-01T09:14:04.491Z" }, + { url = "https://files.pythonhosted.org/packages/f2/2f/d7675ecae6c43e9f12aa8d58b6012683b20b6edfbdac7abcb4e6af7a3784/pillow-11.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f", size = 6640273, upload-time = "2025-07-01T09:14:06.235Z" }, + { url = "https://files.pythonhosted.org/packages/45/ad/931694675ede172e15b2ff03c8144a0ddaea1d87adb72bb07655eaffb654/pillow-11.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e", size = 6082043, upload-time = "2025-07-01T09:14:07.978Z" }, + { url = "https://files.pythonhosted.org/packages/3a/04/ba8f2b11fc80d2dd462d7abec16351b45ec99cbbaea4387648a44190351a/pillow-11.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94", size = 6715516, upload-time = "2025-07-01T09:14:10.233Z" }, + { url = "https://files.pythonhosted.org/packages/48/59/8cd06d7f3944cc7d892e8533c56b0acb68399f640786313275faec1e3b6f/pillow-11.3.0-cp311-cp311-win32.whl", hash = "sha256:b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0", size = 6274768, upload-time = "2025-07-01T09:14:11.921Z" }, + { url = "https://files.pythonhosted.org/packages/f1/cc/29c0f5d64ab8eae20f3232da8f8571660aa0ab4b8f1331da5c2f5f9a938e/pillow-11.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac", size = 6986055, upload-time = "2025-07-01T09:14:13.623Z" }, + { url = "https://files.pythonhosted.org/packages/c6/df/90bd886fabd544c25addd63e5ca6932c86f2b701d5da6c7839387a076b4a/pillow-11.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd", size = 2423079, upload-time = "2025-07-01T09:14:15.268Z" }, + { url = "https://files.pythonhosted.org/packages/40/fe/1bc9b3ee13f68487a99ac9529968035cca2f0a51ec36892060edcc51d06a/pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4", size = 5278800, upload-time = "2025-07-01T09:14:17.648Z" }, + { url = "https://files.pythonhosted.org/packages/2c/32/7e2ac19b5713657384cec55f89065fb306b06af008cfd87e572035b27119/pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69", size = 4686296, upload-time = "2025-07-01T09:14:19.828Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1e/b9e12bbe6e4c2220effebc09ea0923a07a6da1e1f1bfbc8d7d29a01ce32b/pillow-11.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d", size = 5871726, upload-time = "2025-07-03T13:10:04.448Z" }, + { url = "https://files.pythonhosted.org/packages/8d/33/e9200d2bd7ba00dc3ddb78df1198a6e80d7669cce6c2bdbeb2530a74ec58/pillow-11.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6", size = 7644652, upload-time = "2025-07-03T13:10:10.391Z" }, + { url = "https://files.pythonhosted.org/packages/41/f1/6f2427a26fc683e00d985bc391bdd76d8dd4e92fac33d841127eb8fb2313/pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7", size = 5977787, upload-time = "2025-07-01T09:14:21.63Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c9/06dd4a38974e24f932ff5f98ea3c546ce3f8c995d3f0985f8e5ba48bba19/pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024", size = 6645236, upload-time = "2025-07-01T09:14:23.321Z" }, + { url = "https://files.pythonhosted.org/packages/40/e7/848f69fb79843b3d91241bad658e9c14f39a32f71a301bcd1d139416d1be/pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809", size = 6086950, upload-time = "2025-07-01T09:14:25.237Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1a/7cff92e695a2a29ac1958c2a0fe4c0b2393b60aac13b04a4fe2735cad52d/pillow-11.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d", size = 6723358, upload-time = "2025-07-01T09:14:27.053Z" }, + { url = "https://files.pythonhosted.org/packages/26/7d/73699ad77895f69edff76b0f332acc3d497f22f5d75e5360f78cbcaff248/pillow-11.3.0-cp312-cp312-win32.whl", hash = "sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149", size = 6275079, upload-time = "2025-07-01T09:14:30.104Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ce/e7dfc873bdd9828f3b6e5c2bbb74e47a98ec23cc5c74fc4e54462f0d9204/pillow-11.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d", size = 6986324, upload-time = "2025-07-01T09:14:31.899Z" }, + { url = "https://files.pythonhosted.org/packages/16/8f/b13447d1bf0b1f7467ce7d86f6e6edf66c0ad7cf44cf5c87a37f9bed9936/pillow-11.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542", size = 2423067, upload-time = "2025-07-01T09:14:33.709Z" }, + { url = "https://files.pythonhosted.org/packages/1e/93/0952f2ed8db3a5a4c7a11f91965d6184ebc8cd7cbb7941a260d5f018cd2d/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd", size = 2128328, upload-time = "2025-07-01T09:14:35.276Z" }, + { url = "https://files.pythonhosted.org/packages/4b/e8/100c3d114b1a0bf4042f27e0f87d2f25e857e838034e98ca98fe7b8c0a9c/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8", size = 2170652, upload-time = "2025-07-01T09:14:37.203Z" }, + { url = "https://files.pythonhosted.org/packages/aa/86/3f758a28a6e381758545f7cdb4942e1cb79abd271bea932998fc0db93cb6/pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f", size = 2227443, upload-time = "2025-07-01T09:14:39.344Z" }, + { url = "https://files.pythonhosted.org/packages/01/f4/91d5b3ffa718df2f53b0dc109877993e511f4fd055d7e9508682e8aba092/pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c", size = 5278474, upload-time = "2025-07-01T09:14:41.843Z" }, + { url = "https://files.pythonhosted.org/packages/f9/0e/37d7d3eca6c879fbd9dba21268427dffda1ab00d4eb05b32923d4fbe3b12/pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd", size = 4686038, upload-time = "2025-07-01T09:14:44.008Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b0/3426e5c7f6565e752d81221af9d3676fdbb4f352317ceafd42899aaf5d8a/pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e", size = 5864407, upload-time = "2025-07-03T13:10:15.628Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c1/c6c423134229f2a221ee53f838d4be9d82bab86f7e2f8e75e47b6bf6cd77/pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1", size = 7639094, upload-time = "2025-07-03T13:10:21.857Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c9/09e6746630fe6372c67c648ff9deae52a2bc20897d51fa293571977ceb5d/pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805", size = 5973503, upload-time = "2025-07-01T09:14:45.698Z" }, + { url = "https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8", size = 6642574, upload-time = "2025-07-01T09:14:47.415Z" }, + { url = "https://files.pythonhosted.org/packages/36/de/d5cc31cc4b055b6c6fd990e3e7f0f8aaf36229a2698501bcb0cdf67c7146/pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2", size = 6084060, upload-time = "2025-07-01T09:14:49.636Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ea/502d938cbaeec836ac28a9b730193716f0114c41325db428e6b280513f09/pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b", size = 6721407, upload-time = "2025-07-01T09:14:51.962Z" }, + { url = "https://files.pythonhosted.org/packages/45/9c/9c5e2a73f125f6cbc59cc7087c8f2d649a7ae453f83bd0362ff7c9e2aee2/pillow-11.3.0-cp313-cp313-win32.whl", hash = "sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3", size = 6273841, upload-time = "2025-07-01T09:14:54.142Z" }, + { url = "https://files.pythonhosted.org/packages/23/85/397c73524e0cd212067e0c969aa245b01d50183439550d24d9f55781b776/pillow-11.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51", size = 6978450, upload-time = "2025-07-01T09:14:56.436Z" }, + { url = "https://files.pythonhosted.org/packages/17/d2/622f4547f69cd173955194b78e4d19ca4935a1b0f03a302d655c9f6aae65/pillow-11.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580", size = 2423055, upload-time = "2025-07-01T09:14:58.072Z" }, + { url = "https://files.pythonhosted.org/packages/dd/80/a8a2ac21dda2e82480852978416cfacd439a4b490a501a288ecf4fe2532d/pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e", size = 5281110, upload-time = "2025-07-01T09:14:59.79Z" }, + { url = "https://files.pythonhosted.org/packages/44/d6/b79754ca790f315918732e18f82a8146d33bcd7f4494380457ea89eb883d/pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d", size = 4689547, upload-time = "2025-07-01T09:15:01.648Z" }, + { url = "https://files.pythonhosted.org/packages/49/20/716b8717d331150cb00f7fdd78169c01e8e0c219732a78b0e59b6bdb2fd6/pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced", size = 5901554, upload-time = "2025-07-03T13:10:27.018Z" }, + { url = "https://files.pythonhosted.org/packages/74/cf/a9f3a2514a65bb071075063a96f0a5cf949c2f2fce683c15ccc83b1c1cab/pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c", size = 7669132, upload-time = "2025-07-03T13:10:33.01Z" }, + { url = "https://files.pythonhosted.org/packages/98/3c/da78805cbdbee9cb43efe8261dd7cc0b4b93f2ac79b676c03159e9db2187/pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8", size = 6005001, upload-time = "2025-07-01T09:15:03.365Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fa/ce044b91faecf30e635321351bba32bab5a7e034c60187fe9698191aef4f/pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59", size = 6668814, upload-time = "2025-07-01T09:15:05.655Z" }, + { url = "https://files.pythonhosted.org/packages/7b/51/90f9291406d09bf93686434f9183aba27b831c10c87746ff49f127ee80cb/pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe", size = 6113124, upload-time = "2025-07-01T09:15:07.358Z" }, + { url = "https://files.pythonhosted.org/packages/cd/5a/6fec59b1dfb619234f7636d4157d11fb4e196caeee220232a8d2ec48488d/pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c", size = 6747186, upload-time = "2025-07-01T09:15:09.317Z" }, + { url = "https://files.pythonhosted.org/packages/49/6b/00187a044f98255225f172de653941e61da37104a9ea60e4f6887717e2b5/pillow-11.3.0-cp313-cp313t-win32.whl", hash = "sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788", size = 6277546, upload-time = "2025-07-01T09:15:11.311Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5c/6caaba7e261c0d75bab23be79f1d06b5ad2a2ae49f028ccec801b0e853d6/pillow-11.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31", size = 6985102, upload-time = "2025-07-01T09:15:13.164Z" }, + { url = "https://files.pythonhosted.org/packages/f3/7e/b623008460c09a0cb38263c93b828c666493caee2eb34ff67f778b87e58c/pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e", size = 2424803, upload-time = "2025-07-01T09:15:15.695Z" }, + { url = "https://files.pythonhosted.org/packages/73/f4/04905af42837292ed86cb1b1dabe03dce1edc008ef14c473c5c7e1443c5d/pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12", size = 5278520, upload-time = "2025-07-01T09:15:17.429Z" }, + { url = "https://files.pythonhosted.org/packages/41/b0/33d79e377a336247df6348a54e6d2a2b85d644ca202555e3faa0cf811ecc/pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a", size = 4686116, upload-time = "2025-07-01T09:15:19.423Z" }, + { url = "https://files.pythonhosted.org/packages/49/2d/ed8bc0ab219ae8768f529597d9509d184fe8a6c4741a6864fea334d25f3f/pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632", size = 5864597, upload-time = "2025-07-03T13:10:38.404Z" }, + { url = "https://files.pythonhosted.org/packages/b5/3d/b932bb4225c80b58dfadaca9d42d08d0b7064d2d1791b6a237f87f661834/pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673", size = 7638246, upload-time = "2025-07-03T13:10:44.987Z" }, + { url = "https://files.pythonhosted.org/packages/09/b5/0487044b7c096f1b48f0d7ad416472c02e0e4bf6919541b111efd3cae690/pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027", size = 5973336, upload-time = "2025-07-01T09:15:21.237Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2d/524f9318f6cbfcc79fbc004801ea6b607ec3f843977652fdee4857a7568b/pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77", size = 6642699, upload-time = "2025-07-01T09:15:23.186Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d2/a9a4f280c6aefedce1e8f615baaa5474e0701d86dd6f1dede66726462bbd/pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874", size = 6083789, upload-time = "2025-07-01T09:15:25.1Z" }, + { url = "https://files.pythonhosted.org/packages/fe/54/86b0cd9dbb683a9d5e960b66c7379e821a19be4ac5810e2e5a715c09a0c0/pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a", size = 6720386, upload-time = "2025-07-01T09:15:27.378Z" }, + { url = "https://files.pythonhosted.org/packages/e7/95/88efcaf384c3588e24259c4203b909cbe3e3c2d887af9e938c2022c9dd48/pillow-11.3.0-cp314-cp314-win32.whl", hash = "sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214", size = 6370911, upload-time = "2025-07-01T09:15:29.294Z" }, + { url = "https://files.pythonhosted.org/packages/2e/cc/934e5820850ec5eb107e7b1a72dd278140731c669f396110ebc326f2a503/pillow-11.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635", size = 7117383, upload-time = "2025-07-01T09:15:31.128Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e9/9c0a616a71da2a5d163aa37405e8aced9a906d574b4a214bede134e731bc/pillow-11.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6", size = 2511385, upload-time = "2025-07-01T09:15:33.328Z" }, + { url = "https://files.pythonhosted.org/packages/1a/33/c88376898aff369658b225262cd4f2659b13e8178e7534df9e6e1fa289f6/pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae", size = 5281129, upload-time = "2025-07-01T09:15:35.194Z" }, + { url = "https://files.pythonhosted.org/packages/1f/70/d376247fb36f1844b42910911c83a02d5544ebd2a8bad9efcc0f707ea774/pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653", size = 4689580, upload-time = "2025-07-01T09:15:37.114Z" }, + { url = "https://files.pythonhosted.org/packages/eb/1c/537e930496149fbac69efd2fc4329035bbe2e5475b4165439e3be9cb183b/pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6", size = 5902860, upload-time = "2025-07-03T13:10:50.248Z" }, + { url = "https://files.pythonhosted.org/packages/bd/57/80f53264954dcefeebcf9dae6e3eb1daea1b488f0be8b8fef12f79a3eb10/pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36", size = 7670694, upload-time = "2025-07-03T13:10:56.432Z" }, + { url = "https://files.pythonhosted.org/packages/70/ff/4727d3b71a8578b4587d9c276e90efad2d6fe0335fd76742a6da08132e8c/pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b", size = 6005888, upload-time = "2025-07-01T09:15:39.436Z" }, + { url = "https://files.pythonhosted.org/packages/05/ae/716592277934f85d3be51d7256f3636672d7b1abfafdc42cf3f8cbd4b4c8/pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477", size = 6670330, upload-time = "2025-07-01T09:15:41.269Z" }, + { url = "https://files.pythonhosted.org/packages/e7/bb/7fe6cddcc8827b01b1a9766f5fdeb7418680744f9082035bdbabecf1d57f/pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50", size = 6114089, upload-time = "2025-07-01T09:15:43.13Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f5/06bfaa444c8e80f1a8e4bff98da9c83b37b5be3b1deaa43d27a0db37ef84/pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b", size = 6748206, upload-time = "2025-07-01T09:15:44.937Z" }, + { url = "https://files.pythonhosted.org/packages/f0/77/bc6f92a3e8e6e46c0ca78abfffec0037845800ea38c73483760362804c41/pillow-11.3.0-cp314-cp314t-win32.whl", hash = "sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12", size = 6377370, upload-time = "2025-07-01T09:15:46.673Z" }, + { url = "https://files.pythonhosted.org/packages/4a/82/3a721f7d69dca802befb8af08b7c79ebcab461007ce1c18bd91a5d5896f9/pillow-11.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db", size = 7121500, upload-time = "2025-07-01T09:15:48.512Z" }, + { url = "https://files.pythonhosted.org/packages/89/c7/5572fa4a3f45740eaab6ae86fcdf7195b55beac1371ac8c619d880cfe948/pillow-11.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa", size = 2512835, upload-time = "2025-07-01T09:15:50.399Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8e/9c089f01677d1264ab8648352dcb7773f37da6ad002542760c80107da816/pillow-11.3.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:48d254f8a4c776de343051023eb61ffe818299eeac478da55227d96e241de53f", size = 5316478, upload-time = "2025-07-01T09:15:52.209Z" }, + { url = "https://files.pythonhosted.org/packages/b5/a9/5749930caf674695867eb56a581e78eb5f524b7583ff10b01b6e5048acb3/pillow-11.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7aee118e30a4cf54fdd873bd3a29de51e29105ab11f9aad8c32123f58c8f8081", size = 4686522, upload-time = "2025-07-01T09:15:54.162Z" }, + { url = "https://files.pythonhosted.org/packages/43/46/0b85b763eb292b691030795f9f6bb6fcaf8948c39413c81696a01c3577f7/pillow-11.3.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:23cff760a9049c502721bdb743a7cb3e03365fafcdfc2ef9784610714166e5a4", size = 5853376, upload-time = "2025-07-03T13:11:01.066Z" }, + { url = "https://files.pythonhosted.org/packages/5e/c6/1a230ec0067243cbd60bc2dad5dc3ab46a8a41e21c15f5c9b52b26873069/pillow-11.3.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6359a3bc43f57d5b375d1ad54a0074318a0844d11b76abccf478c37c986d3cfc", size = 7626020, upload-time = "2025-07-03T13:11:06.479Z" }, + { url = "https://files.pythonhosted.org/packages/63/dd/f296c27ffba447bfad76c6a0c44c1ea97a90cb9472b9304c94a732e8dbfb/pillow-11.3.0-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:092c80c76635f5ecb10f3f83d76716165c96f5229addbd1ec2bdbbda7d496e06", size = 5956732, upload-time = "2025-07-01T09:15:56.111Z" }, + { url = "https://files.pythonhosted.org/packages/a5/a0/98a3630f0b57f77bae67716562513d3032ae70414fcaf02750279c389a9e/pillow-11.3.0-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cadc9e0ea0a2431124cde7e1697106471fc4c1da01530e679b2391c37d3fbb3a", size = 6624404, upload-time = "2025-07-01T09:15:58.245Z" }, + { url = "https://files.pythonhosted.org/packages/de/e6/83dfba5646a290edd9a21964da07674409e410579c341fc5b8f7abd81620/pillow-11.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:6a418691000f2a418c9135a7cf0d797c1bb7d9a485e61fe8e7722845b95ef978", size = 6067760, upload-time = "2025-07-01T09:16:00.003Z" }, + { url = "https://files.pythonhosted.org/packages/bc/41/15ab268fe6ee9a2bc7391e2bbb20a98d3974304ab1a406a992dcb297a370/pillow-11.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:97afb3a00b65cc0804d1c7abddbf090a81eaac02768af58cbdcaaa0a931e0b6d", size = 6700534, upload-time = "2025-07-01T09:16:02.29Z" }, + { url = "https://files.pythonhosted.org/packages/64/79/6d4f638b288300bed727ff29f2a3cb63db054b33518a95f27724915e3fbc/pillow-11.3.0-cp39-cp39-win32.whl", hash = "sha256:ea944117a7974ae78059fcc1800e5d3295172bb97035c0c1d9345fca1419da71", size = 6277091, upload-time = "2025-07-01T09:16:04.4Z" }, + { url = "https://files.pythonhosted.org/packages/46/05/4106422f45a05716fd34ed21763f8ec182e8ea00af6e9cb05b93a247361a/pillow-11.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:e5c5858ad8ec655450a7c7df532e9842cf8df7cc349df7225c60d5d348c8aada", size = 6986091, upload-time = "2025-07-01T09:16:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/63/c6/287fd55c2c12761d0591549d48885187579b7c257bef0c6660755b0b59ae/pillow-11.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:6abdbfd3aea42be05702a8dd98832329c167ee84400a1d1f61ab11437f1717eb", size = 2422632, upload-time = "2025-07-01T09:16:08.142Z" }, + { url = "https://files.pythonhosted.org/packages/6f/8b/209bd6b62ce8367f47e68a218bffac88888fdf2c9fcf1ecadc6c3ec1ebc7/pillow-11.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3cee80663f29e3843b68199b9d6f4f54bd1d4a6b59bdd91bceefc51238bcb967", size = 5270556, upload-time = "2025-07-01T09:16:09.961Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e6/231a0b76070c2cfd9e260a7a5b504fb72da0a95279410fa7afd99d9751d6/pillow-11.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b5f56c3f344f2ccaf0dd875d3e180f631dc60a51b314295a3e681fe8cf851fbe", size = 4654625, upload-time = "2025-07-01T09:16:11.913Z" }, + { url = "https://files.pythonhosted.org/packages/13/f4/10cf94fda33cb12765f2397fc285fa6d8eb9c29de7f3185165b702fc7386/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e67d793d180c9df62f1f40aee3accca4829d3794c95098887edc18af4b8b780c", size = 4874207, upload-time = "2025-07-03T13:11:10.201Z" }, + { url = "https://files.pythonhosted.org/packages/72/c9/583821097dc691880c92892e8e2d41fe0a5a3d6021f4963371d2f6d57250/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d000f46e2917c705e9fb93a3606ee4a819d1e3aa7a9b442f6444f07e77cf5e25", size = 6583939, upload-time = "2025-07-03T13:11:15.68Z" }, + { url = "https://files.pythonhosted.org/packages/3b/8e/5c9d410f9217b12320efc7c413e72693f48468979a013ad17fd690397b9a/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:527b37216b6ac3a12d7838dc3bd75208ec57c1c6d11ef01902266a5a0c14fc27", size = 4957166, upload-time = "2025-07-01T09:16:13.74Z" }, + { url = "https://files.pythonhosted.org/packages/62/bb/78347dbe13219991877ffb3a91bf09da8317fbfcd4b5f9140aeae020ad71/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be5463ac478b623b9dd3937afd7fb7ab3d79dd290a28e2b6df292dc75063eb8a", size = 5581482, upload-time = "2025-07-01T09:16:16.107Z" }, + { url = "https://files.pythonhosted.org/packages/d9/28/1000353d5e61498aaeaaf7f1e4b49ddb05f2c6575f9d4f9f914a3538b6e1/pillow-11.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:8dc70ca24c110503e16918a658b869019126ecfe03109b754c402daff12b3d9f", size = 6984596, upload-time = "2025-07-01T09:16:18.07Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e3/6fa84033758276fb31da12e5fb66ad747ae83b93c67af17f8c6ff4cc8f34/pillow-11.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6", size = 5270566, upload-time = "2025-07-01T09:16:19.801Z" }, + { url = "https://files.pythonhosted.org/packages/5b/ee/e8d2e1ab4892970b561e1ba96cbd59c0d28cf66737fc44abb2aec3795a4e/pillow-11.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438", size = 4654618, upload-time = "2025-07-01T09:16:21.818Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6d/17f80f4e1f0761f02160fc433abd4109fa1548dcfdca46cfdadaf9efa565/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3", size = 4874248, upload-time = "2025-07-03T13:11:20.738Z" }, + { url = "https://files.pythonhosted.org/packages/de/5f/c22340acd61cef960130585bbe2120e2fd8434c214802f07e8c03596b17e/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c", size = 6583963, upload-time = "2025-07-03T13:11:26.283Z" }, + { url = "https://files.pythonhosted.org/packages/31/5e/03966aedfbfcbb4d5f8aa042452d3361f325b963ebbadddac05b122e47dd/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361", size = 4957170, upload-time = "2025-07-01T09:16:23.762Z" }, + { url = "https://files.pythonhosted.org/packages/cc/2d/e082982aacc927fc2cab48e1e731bdb1643a1406acace8bed0900a61464e/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7", size = 5581505, upload-time = "2025-07-01T09:16:25.593Z" }, + { url = "https://files.pythonhosted.org/packages/34/e7/ae39f538fd6844e982063c3a5e4598b8ced43b9633baa3a85ef33af8c05c/pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8", size = 6984598, upload-time = "2025-07-01T09:16:27.732Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.4.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/23/e8/21db9c9987b0e728855bd57bff6984f67952bea55d6f75e055c46b5383e8/platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf", size = 21634, upload-time = "2025-08-26T14:32:04.268Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654, upload-time = "2025-08-26T14:32:02.735Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.10' and python_full_version < '3.14'", +] +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 = "playwright" +version = "1.57.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", version = "3.2.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "greenlet", version = "3.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pyee" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/b6/e17543cea8290ae4dced10be21d5a43c360096aa2cce0aa7039e60c50df3/playwright-1.57.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:9351c1ac3dfd9b3820fe7fc4340d96c0d3736bb68097b9b7a69bd45d25e9370c", size = 41985039, upload-time = "2025-12-09T08:06:18.408Z" }, + { url = "https://files.pythonhosted.org/packages/8b/04/ef95b67e1ff59c080b2effd1a9a96984d6953f667c91dfe9d77c838fc956/playwright-1.57.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a4a9d65027bce48eeba842408bcc1421502dfd7e41e28d207e94260fa93ca67e", size = 40775575, upload-time = "2025-12-09T08:06:22.105Z" }, + { url = "https://files.pythonhosted.org/packages/60/bd/5563850322a663956c927eefcf1457d12917e8f118c214410e815f2147d1/playwright-1.57.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:99104771abc4eafee48f47dac2369e0015516dc1ce8c409807d2dd440828b9a4", size = 41985042, upload-time = "2025-12-09T08:06:25.357Z" }, + { url = "https://files.pythonhosted.org/packages/56/61/3a803cb5ae0321715bfd5247ea871d25b32c8f372aeb70550a90c5f586df/playwright-1.57.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:284ed5a706b7c389a06caa431b2f0ba9ac4130113c3a779767dda758c2497bb1", size = 45975252, upload-time = "2025-12-09T08:06:29.186Z" }, + { url = "https://files.pythonhosted.org/packages/83/d7/b72eb59dfbea0013a7f9731878df8c670f5f35318cedb010c8a30292c118/playwright-1.57.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a1bae6c0a07839cdeaddbc0756b3b2b85e476c07945f64ece08f1f956a86f1", size = 45706917, upload-time = "2025-12-09T08:06:32.549Z" }, + { url = "https://files.pythonhosted.org/packages/e4/09/3fc9ebd7c95ee54ba6a68d5c0bc23e449f7235f4603fc60534a364934c16/playwright-1.57.0-py3-none-win32.whl", hash = "sha256:1dd93b265688da46e91ecb0606d36f777f8eadcf7fbef12f6426b20bf0c9137c", size = 36553860, upload-time = "2025-12-09T08:06:35.864Z" }, + { url = "https://files.pythonhosted.org/packages/58/d4/dcdfd2a33096aeda6ca0d15584800443dd2be64becca8f315634044b135b/playwright-1.57.0-py3-none-win_amd64.whl", hash = "sha256:6caefb08ed2c6f29d33b8088d05d09376946e49a73be19271c8cd5384b82b14c", size = 36553864, upload-time = "2025-12-09T08:06:38.915Z" }, + { url = "https://files.pythonhosted.org/packages/6a/60/fe31d7e6b8907789dcb0584f88be741ba388413e4fbce35f1eba4e3073de/playwright-1.57.0-py3-none-win_arm64.whl", hash = "sha256:5f065f5a133dbc15e6e7c71e7bc04f258195755b1c32a432b792e28338c8335e", size = 32837940, upload-time = "2025-12-09T08:06:42.268Z" }, +] + +[[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.2.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/a7/1e07536315f77d7b233cbf3dd916dc3424239c435ee0a0110c9b2cbcf6b0/prek-0.2.22.tar.gz", hash = "sha256:5abbda8bae0a63a18d3fe573162e8504a7b100e3603169cc2d06053891a02d7c", size = 267212, upload-time = "2025-12-13T12:57:51.797Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/fe/ba9a940adc55d78b96b58376a8752e95261402c1e5812acce6ea1a000fb8/prek-0.2.22-py3-none-linux_armv6l.whl", hash = "sha256:d026b2d75529a743466000e8dd058d3d5e7c597c34905b333f2ede3d24cb23f1", size = 4798026, upload-time = "2025-12-13T12:57:45.286Z" }, + { url = "https://files.pythonhosted.org/packages/12/40/459cf510491271b08d19b4ef34f8293440eb472e633f4ffaf34179f39a12/prek-0.2.22-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:60b5bec94fa9f59fe5a9e90554c7346ceef81ea33d01bb18172d2576b07ac449", size = 4894023, upload-time = "2025-12-13T12:57:40.102Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0b/59e0438b1e7d1b6fa3f14174a916d369e27c421f8876f7ec7c7a52fbfae7/prek-0.2.22-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a0c7c6ceee536122916d32d26b6fa4fac9e95ba28631901164ffc0b0fed28a9e", size = 4615858, upload-time = "2025-12-13T12:57:57.471Z" }, + { url = "https://files.pythonhosted.org/packages/e9/27/ea40cf715717298fdf802da2b15a2c4445b8c114aae28cab6bf794d65670/prek-0.2.22-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:c90273bef7b638dfc36dede62c494f958456330375ffce891c68321b2a7b46ba", size = 4810206, upload-time = "2025-12-13T12:57:46.534Z" }, + { url = "https://files.pythonhosted.org/packages/b5/12/d1c3db35839492236afb8642a2818d5b413e5fce4ea909bc7ddfb3d4591a/prek-0.2.22-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1d8ecf202073433b87df2671a98bc44d3b68bb5711f7119b50b7bd65c2a67f13", size = 4722439, upload-time = "2025-12-13T12:57:48.106Z" }, + { url = "https://files.pythonhosted.org/packages/64/a0/0f24a9cacd5d78119f47063d860e03fa42b4d7dcf6803a49b0bef51b771b/prek-0.2.22-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d4f26d76247ce7671cf5d9786e7fc86fdb43c065fd5507e8d64b3de7fd5e4447", size = 5037705, upload-time = "2025-12-13T12:57:50.596Z" }, + { url = "https://files.pythonhosted.org/packages/ca/6e/7616f84141755f1d9fe232f0bd06589421ae0dabd99180fdae2840d22ae8/prek-0.2.22-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:ac1f0ea2c82e35eb0ffc98dfbcd9ee34cfd7350b64f97198da4c311a271cdb8f", size = 5453199, upload-time = "2025-12-13T12:57:38.458Z" }, + { url = "https://files.pythonhosted.org/packages/51/80/542a583db9b27bfd34954243666e451b266513bc742e0491cd61ff1b390e/prek-0.2.22-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f5d5131b9e57548f64d74665fd4414a8deb603a67d52ee18b3e6540cdb77733", size = 5399635, upload-time = "2025-12-13T12:57:43.359Z" }, + { url = "https://files.pythonhosted.org/packages/49/06/ca4e6fee73e14e1aced90f5c83b9cdf9a8e1c3b1aa1e4f45a2a65de05a28/prek-0.2.22-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a768484e1c94a33228765f63701261316b64e11c482abe2a35c54045d3f81feb", size = 5498340, upload-time = "2025-12-13T12:57:41.827Z" }, + { url = "https://files.pythonhosted.org/packages/85/a8/9636fc782db9c22d1740a8e5dc4e1ffc3a28099d074f812da46332e7c7a7/prek-0.2.22-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c941c8503ea537a84ea97074dc97b0f0dfd9861864883eb8b90586ed321847e", size = 5078431, upload-time = "2025-12-13T12:57:31.664Z" }, + { url = "https://files.pythonhosted.org/packages/4f/29/e78d2f444cf1f097aaaefee8910d7b9fe34195f06b086e0d2153b6c66e07/prek-0.2.22-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:946c6cfe18b17a7b53c49a389bf65f1e8e45a1b96bfdaeeacde21f5b5ca2d149", size = 4820871, upload-time = "2025-12-13T12:57:33.074Z" }, + { url = "https://files.pythonhosted.org/packages/fb/ec/779db6c35663e949b3f9989c584297aa115d3cc44822c149dbe40d51cd14/prek-0.2.22-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2cffd5809cf678b4300378d612b5da12cd2183ddc7aee78178db0b1ea48f0069", size = 4834431, upload-time = "2025-12-13T12:57:34.65Z" }, + { url = "https://files.pythonhosted.org/packages/1d/18/12bb4fece680457f4d4f13d21c5784675ce8b1db5c968261348c52087232/prek-0.2.22-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:f91df793cbc28647863eb54d578f37782736726671838ca92c9d0601329cb928", size = 4709742, upload-time = "2025-12-13T12:57:52.706Z" }, + { url = "https://files.pythonhosted.org/packages/3f/27/de1d9d037f59393568713121f4bfcea11cd546dcf96f214827983b8beccf/prek-0.2.22-py3-none-musllinux_1_1_i686.whl", hash = "sha256:3c40ba36b3e89817b20efe6163fd15387b81caf1f489060265d84103ae6e5184", size = 4925048, upload-time = "2025-12-13T12:57:49.348Z" }, + { url = "https://files.pythonhosted.org/packages/49/bf/d40eef2e5ccbc520da94c2463450d0ecab598c092684002b463fd5491ff6/prek-0.2.22-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:a4154a419581723d12eccaa5b1d27686283c5c78b753c1984270d7e144a15fa7", size = 5192083, upload-time = "2025-12-13T12:57:35.957Z" }, + { url = "https://files.pythonhosted.org/packages/41/ba/11ea837a876dcc7f5df85962bc560c8627a962261f046a1615b0a6016b01/prek-0.2.22-py3-none-win32.whl", hash = "sha256:9fd3d629a256ce3171bebc3183f9c608022fff0db19a511307ab0f4c7682d5e3", size = 4586129, upload-time = "2025-12-13T12:57:54.438Z" }, + { url = "https://files.pythonhosted.org/packages/2f/8c/05ab6d11ac670664c99944e4819a77a63360aab253d8daf4ae411c705bcd/prek-0.2.22-py3-none-win_amd64.whl", hash = "sha256:ad7997ae4bef4fccc0a6761c00479bdd44f2a5bb7eb97aebda3b42fe785e10a1", size = 5273787, upload-time = "2025-12-13T12:57:37.205Z" }, + { url = "https://files.pythonhosted.org/packages/38/7a/53e8a550df705b5bf78a589c4e11d21485ac38c1a65e9c98fc3169a5eb25/prek-0.2.22-py3-none-win_arm64.whl", hash = "sha256:2442c0f12bd57675124542a92f5c799e7ffe52dc7cd98301c43c361849a3aef6", size = 4941186, upload-time = "2025-12-13T12:57:56.214Z" }, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.52" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, +] + +[[package]] +name = "pwdlib" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/82/a0/9daed437a6226f632a25d98d65d60ba02bdafa920c90dcb6454c611ead6c/pwdlib-0.2.1.tar.gz", hash = "sha256:9a1d8a8fa09a2f7ebf208265e55d7d008103cbdc82b9e4902ffdd1ade91add5e", size = 11699, upload-time = "2024-08-19T06:48:59.58Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/f3/0dae5078a486f0fdf4d4a1121e103bc42694a9da9bea7b0f2c63f29cfbd3/pwdlib-0.2.1-py3-none-any.whl", hash = "sha256:1823dc6f22eae472b540e889ecf57fd424051d6a4023ec0bcf7f0de2d9d7ef8c", size = 8082, upload-time = "2024-08-19T06:49:00.997Z" }, +] + +[package.optional-dependencies] +argon2 = [ + { name = "argon2-cffi", version = "23.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, +] + +[[package]] +name = "pwdlib" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.10' and python_full_version < '3.14'", +] +sdist = { url = "https://files.pythonhosted.org/packages/5f/41/a7c0d8a003c36ce3828ae3ed0391fe6a15aad65f082dbd6bec817ea95c0b/pwdlib-0.3.0.tar.gz", hash = "sha256:6ca30f9642a1467d4f5d0a4d18619de1c77f17dfccb42dd200b144127d3c83fc", size = 215810, upload-time = "2025-10-25T12:44:24.395Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/0c/9086a357d02a050fbb3270bf5043ac284dbfb845670e16c9389a41defc9e/pwdlib-0.3.0-py3-none-any.whl", hash = "sha256:f86c15c138858c09f3bba0a10984d4f9178158c55deaa72eac0210849b1a140d", size = 8633, upload-time = "2025-10-25T12:44:23.406Z" }, +] + +[package.optional-dependencies] +argon2 = [ + { name = "argon2-cffi", version = "25.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] + +[[package]] +name = "pyasn1" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322, upload-time = "2024-09-10T22:41:42.55Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135, upload-time = "2024-09-11T16:00:36.122Z" }, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, +] + +[[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.optional-dependencies] +email = [ + { name = "email-validator" }, +] + +[[package]] +name = "pydantic-ai" +version = "0.4.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic-ai-slim", extra = ["ag-ui", "anthropic", "bedrock", "cli", "cohere", "evals", "google", "groq", "huggingface", "mcp", "mistral", "openai", "retries", "vertexai"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dd/67/eef2d1d64579c20ff732244c8f46f5b10401a47122b2404fd1a786860e0e/pydantic_ai-0.4.10.tar.gz", hash = "sha256:21cda3b62706dfdd5b9d662bb9a2ead50d51951f6ee0bbee45320b8752a95055", size = 43555371, upload-time = "2025-07-30T23:03:50.744Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/9c/3ea9ce322448e963fd84abb61985fa2d2ea0cb3f09bf0634cf43b2f49de6/pydantic_ai-0.4.10-py3-none-any.whl", hash = "sha256:4822325c92a57c9b45bdfd993106558e2397362b98ec211670d106c4b073ddd8", size = 10194, upload-time = "2025-07-30T23:03:42.435Z" }, +] + +[[package]] +name = "pydantic-ai-slim" +version = "0.4.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "eval-type-backport" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "griffe", version = "1.14.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "griffe", version = "1.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "httpx" }, + { name = "opentelemetry-api" }, + { name = "pydantic" }, + { name = "pydantic-graph" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ee/fc/4123faf9372807e487c83acc858482f6d5a2f39ee6c1e25a895f23f700b6/pydantic_ai_slim-0.4.10.tar.gz", hash = "sha256:c9f6904aaa91c0309f91cc3a5617c570e153afbbb9888ee52190f58f029640f0", size = 189595, upload-time = "2025-07-30T23:03:55.44Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/d7/b1f6cf2eb9d24086de8a7f9e08956421fde514bcb7a4a3594b108e12636e/pydantic_ai_slim-0.4.10-py3-none-any.whl", hash = "sha256:49a386d6a900b4c1a81bca3469522a4c0eaab5a46a3953d1ffda8f0f2865ed97", size = 254994, upload-time = "2025-07-30T23:03:45.03Z" }, +] + +[package.optional-dependencies] +ag-ui = [ + { name = "ag-ui-protocol" }, + { name = "starlette", version = "0.49.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "starlette", version = "0.50.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +anthropic = [ + { name = "anthropic" }, +] +bedrock = [ + { name = "boto3" }, +] +cli = [ + { name = "argcomplete" }, + { name = "prompt-toolkit" }, + { name = "rich" }, +] +cohere = [ + { name = "cohere", marker = "sys_platform != 'emscripten'" }, +] +evals = [ + { name = "pydantic-evals" }, +] +google = [ + { name = "google-genai", version = "1.47.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "google-genai", version = "1.57.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +groq = [ + { name = "groq" }, +] +huggingface = [ + { name = "huggingface-hub" }, +] +mcp = [ + { name = "mcp", marker = "python_full_version >= '3.10'" }, +] +mistral = [ + { name = "mistralai" }, +] +openai = [ + { name = "openai" }, +] +retries = [ + { name = "tenacity" }, +] +vertexai = [ + { name = "google-auth" }, + { name = "requests" }, +] + +[[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/54/db/160dffb57ed9a3705c4cbcbff0ac03bdae45f1ca7d58ab74645550df3fbd/pydantic_core-2.41.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:8bfeaf8735be79f225f3fefab7f941c712aaca36f1128c9d7e2352ee1aa87bdf", size = 2107999, upload-time = "2025-11-04T13:42:03.885Z" }, + { url = "https://files.pythonhosted.org/packages/a3/7d/88e7de946f60d9263cc84819f32513520b85c0f8322f9b8f6e4afc938383/pydantic_core-2.41.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:346285d28e4c8017da95144c7f3acd42740d637ff41946af5ce6e5e420502dd5", size = 1929745, upload-time = "2025-11-04T13:42:06.075Z" }, + { url = "https://files.pythonhosted.org/packages/d5/c2/aef51e5b283780e85e99ff19db0f05842d2d4a8a8cd15e63b0280029b08f/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a75dafbf87d6276ddc5b2bf6fae5254e3d0876b626eb24969a574fff9149ee5d", size = 1920220, upload-time = "2025-11-04T13:42:08.457Z" }, + { url = "https://files.pythonhosted.org/packages/c7/97/492ab10f9ac8695cd76b2fdb24e9e61f394051df71594e9bcc891c9f586e/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7b93a4d08587e2b7e7882de461e82b6ed76d9026ce91ca7915e740ecc7855f60", size = 2067296, upload-time = "2025-11-04T13:42:10.817Z" }, + { url = "https://files.pythonhosted.org/packages/ec/23/984149650e5269c59a2a4c41d234a9570adc68ab29981825cfaf4cfad8f4/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8465ab91a4bd96d36dde3263f06caa6a8a6019e4113f24dc753d79a8b3a3f82", size = 2231548, upload-time = "2025-11-04T13:42:13.843Z" }, + { url = "https://files.pythonhosted.org/packages/71/0c/85bcbb885b9732c28bec67a222dbed5ed2d77baee1f8bba2002e8cd00c5c/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:299e0a22e7ae2b85c1a57f104538b2656e8ab1873511fd718a1c1c6f149b77b5", size = 2362571, upload-time = "2025-11-04T13:42:16.208Z" }, + { url = "https://files.pythonhosted.org/packages/c0/4a/412d2048be12c334003e9b823a3fa3d038e46cc2d64dd8aab50b31b65499/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:707625ef0983fcfb461acfaf14de2067c5942c6bb0f3b4c99158bed6fedd3cf3", size = 2068175, upload-time = "2025-11-04T13:42:18.911Z" }, + { url = "https://files.pythonhosted.org/packages/73/f4/c58b6a776b502d0a5540ad02e232514285513572060f0d78f7832ca3c98b/pydantic_core-2.41.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f41eb9797986d6ebac5e8edff36d5cef9de40def462311b3eb3eeded1431e425", size = 2177203, upload-time = "2025-11-04T13:42:22.578Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ae/f06ea4c7e7a9eead3d165e7623cd2ea0cb788e277e4f935af63fc98fa4e6/pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0384e2e1021894b1ff5a786dbf94771e2986ebe2869533874d7e43bc79c6f504", size = 2148191, upload-time = "2025-11-04T13:42:24.89Z" }, + { url = "https://files.pythonhosted.org/packages/c1/57/25a11dcdc656bf5f8b05902c3c2934ac3ea296257cc4a3f79a6319e61856/pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:f0cd744688278965817fd0839c4a4116add48d23890d468bc436f78beb28abf5", size = 2343907, upload-time = "2025-11-04T13:42:27.683Z" }, + { url = "https://files.pythonhosted.org/packages/96/82/e33d5f4933d7a03327c0c43c65d575e5919d4974ffc026bc917a5f7b9f61/pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:753e230374206729bf0a807954bcc6c150d3743928a73faffee51ac6557a03c3", size = 2322174, upload-time = "2025-11-04T13:42:30.776Z" }, + { url = "https://files.pythonhosted.org/packages/81/45/4091be67ce9f469e81656f880f3506f6a5624121ec5eb3eab37d7581897d/pydantic_core-2.41.5-cp39-cp39-win32.whl", hash = "sha256:873e0d5b4fb9b89ef7c2d2a963ea7d02879d9da0da8d9d4933dee8ee86a8b460", size = 1990353, upload-time = "2025-11-04T13:42:33.111Z" }, + { url = "https://files.pythonhosted.org/packages/44/8a/a98aede18db6e9cd5d66bcacd8a409fcf8134204cdede2e7de35c5a2c5ef/pydantic_core-2.41.5-cp39-cp39-win_amd64.whl", hash = "sha256:e4f4a984405e91527a0d62649ee21138f8e3d0ef103be488c1dc11a80d7f184b", size = 2015698, upload-time = "2025-11-04T13:42:35.484Z" }, + { 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-evals" +version = "0.4.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "eval-type-backport", marker = "python_full_version < '3.11'" }, + { name = "logfire-api" }, + { name = "pydantic" }, + { name = "pydantic-ai-slim" }, + { name = "pyyaml" }, + { name = "rich" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/b7/3f925d6ec9a2627e3310e87c2131514c3b44427c8975fafe83453825cfca/pydantic_evals-0.4.10.tar.gz", hash = "sha256:310acc7559d601743a101c606e50c0c5a9592bfc53cb733c7e54143b39e0fc97", size = 43729, upload-time = "2025-07-30T23:03:56.943Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/9f/7972c450d74c2b0f6d9f9de644ac33c4c63f0f2156f8acd37a274b1f43a3/pydantic_evals-0.4.10-py3-none-any.whl", hash = "sha256:5fda10c5ced2c99f03b407bd56645574598e6daab0e5be2ed7056e815e6037f6", size = 52511, upload-time = "2025-07-30T23:03:46.717Z" }, +] + +[[package]] +name = "pydantic-extra-types" +version = "2.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fd/35/2fee58b1316a73e025728583d3b1447218a97e621933fc776fb8c0f2ebdd/pydantic_extra_types-2.11.0.tar.gz", hash = "sha256:4e9991959d045b75feb775683437a97991d02c138e00b59176571db9ce634f0e", size = 157226, upload-time = "2025-12-31T16:18:27.944Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/17/fabd56da47096d240dd45ba627bead0333b0cf0ee8ada9bec579287dadf3/pydantic_extra_types-2.11.0-py3-none-any.whl", hash = "sha256:84b864d250a0fc62535b7ec591e36f2c5b4d1325fa0017eb8cda9aeb63b374a6", size = 74296, upload-time = "2025-12-31T16:18:26.38Z" }, +] + +[[package]] +name = "pydantic-graph" +version = "0.4.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "logfire-api" }, + { name = "pydantic" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/46/62b83bd471a8743c41aba6693f55b746b8ee1294f64b7e9f42db7c3fd4b5/pydantic_graph-0.4.10.tar.gz", hash = "sha256:034063ac0ce2143a877a4fac563520492e70dde42d262b22c928f081d7759c5b", size = 21978, upload-time = "2025-07-30T23:03:57.986Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/55/0026897b1cd6ab6b179b25c06b1dff496a3ebb83e30c6acb1a5a40e80cd1/pydantic_graph-0.4.10-py3-none-any.whl", hash = "sha256:e5128d5e370a6391aa6441eaefff6b3a139e583e0ac6755d83b88c9df9c6c6a7", size = 27578, upload-time = "2025-07-30T23:03:47.925Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.11.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "pydantic", marker = "python_full_version < '3.10'" }, + { name = "python-dotenv", marker = "python_full_version < '3.10'" }, + { name = "typing-inspection", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/20/c5/dbbc27b814c71676593d1c3f718e6cd7d4f00652cefa24b75f7aa3efb25e/pydantic_settings-2.11.0.tar.gz", hash = "sha256:d0e87a1c7d33593beb7194adb8470fc426e95ba02af83a0f23474a04c9a08180", size = 188394, upload-time = "2025-09-24T14:19:11.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/d6/887a1ff844e64aa823fb4905978d882a633cfe295c32eacad582b78a7d8b/pydantic_settings-2.11.0-py3-none-any.whl", hash = "sha256:fe2cea3413b9530d10f3a5875adffb17ada5c1e1bab0b2885546d7310415207c", size = 48608, upload-time = "2025-09-24T14:19:10.015Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.12.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.10' and python_full_version < '3.14'", +] +dependencies = [ + { name = "pydantic", marker = "python_full_version >= '3.10'" }, + { name = "python-dotenv", marker = "python_full_version >= '3.10'" }, + { name = "typing-inspection", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/4b/ac7e0aae12027748076d72a8764ff1c9d82ca75a7a52622e67ed3f765c54/pydantic_settings-2.12.0.tar.gz", hash = "sha256:005538ef951e3c2a68e1c08b292b5f2e71490def8589d4221b95dab00dafcfd0", size = 194184, upload-time = "2025-11-10T14:25:47.013Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809", size = 51880, upload-time = "2025-11-10T14:25:45.546Z" }, +] + +[[package]] +name = "pyee" +version = "13.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/03/1fd98d5841cd7964a27d729ccf2199602fe05eb7a405c1462eb7277945ed/pyee-13.0.0.tar.gz", hash = "sha256:b391e3c5a434d1f5118a25615001dbc8f669cf410ab67d04c4d4e07c55481c37", size = 31250, upload-time = "2025-03-17T18:53:15.955Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/4d/b9add7c84060d4c1906abe9a7e5359f2a60f7a9a4f67268b2766673427d8/pyee-13.0.0-py3-none-any.whl", hash = "sha256:48195a3cddb3b1515ce0695ed76036b5ccc2ef3a9f963ff9f77aec0139845498", size = 15730, upload-time = "2025-03-17T18:53:14.532Z" }, +] + +[[package]] +name = "pygithub" +version = "2.8.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyjwt", extra = ["crypto"] }, + { name = "pynacl" }, + { name = "requests" }, + { name = "typing-extensions" }, + { name = "urllib3", version = "1.26.20", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "urllib3", version = "2.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c1/74/e560bdeffea72ecb26cff27f0fad548bbff5ecc51d6a155311ea7f9e4c4c/pygithub-2.8.1.tar.gz", hash = "sha256:341b7c78521cb07324ff670afd1baa2bf5c286f8d9fd302c1798ba594a5400c9", size = 2246994, upload-time = "2025-09-02T17:41:54.674Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/ba/7049ce39f653f6140aac4beb53a5aaf08b4407b6a3019aae394c1c5244ff/pygithub-2.8.1-py3-none-any.whl", hash = "sha256:23a0a5bca93baef082e03411bf0ce27204c32be8bfa7abc92fe4a3e132936df0", size = 432709, upload-time = "2025-09-02T17:41:52.947Z" }, +] + +[[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.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/68/ce067f09fca4abeca8771fe667d89cc347d1e99da3e093112ac329c6020e/pyjwt-2.9.0.tar.gz", hash = "sha256:7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c", size = 78825, upload-time = "2024-08-01T15:01:08.445Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/84/0fdf9b18ba31d69877bd39c9cd6052b47f3761e9910c15de788e519f079f/PyJWT-2.9.0-py3-none-any.whl", hash = "sha256:3b02fb0f44517787776cf48f2ae25d8e14f300e6d7545a4315cee571a415e850", size = 22344, upload-time = "2024-08-01T15:01:06.481Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + +[[package]] +name = "pymdown-extensions" +version = "10.20" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown", version = "3.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "markdown", version = "3.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3e/35/e3814a5b7df295df69d035cfb8aab78b2967cdf11fcfae7faed726b66664/pymdown_extensions-10.20.tar.gz", hash = "sha256:5c73566ab0cf38c6ba084cb7c5ea64a119ae0500cce754ccb682761dfea13a52", size = 852774, upload-time = "2025-12-31T19:59:42.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/10/47caf89cbb52e5bb764696fd52a8c591a2f0e851a93270c05a17f36000b5/pymdown_extensions-10.20-py3-none-any.whl", hash = "sha256:ea9e62add865da80a271d00bfa1c0fa085b20d133fb3fc97afdc88e682f60b2f", size = 268733, upload-time = "2025-12-31T19:59:40.652Z" }, +] + +[[package]] +name = "pynacl" +version = "1.6.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/9a/4019b524b03a13438637b11538c82781a5eda427394380381af8f04f467a/pynacl-1.6.2.tar.gz", hash = "sha256:018494d6d696ae03c7e656e5e74cdfd8ea1326962cc401bcf018f1ed8436811c", size = 3511692, upload-time = "2026-01-01T17:48:10.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/79/0e3c34dc3c4671f67d251c07aa8eb100916f250ee470df230b0ab89551b4/pynacl-1.6.2-cp314-cp314t-macosx_10_10_universal2.whl", hash = "sha256:622d7b07cc5c02c666795792931b50c91f3ce3c2649762efb1ef0d5684c81594", size = 390064, upload-time = "2026-01-01T17:31:57.264Z" }, + { url = "https://files.pythonhosted.org/packages/eb/1c/23a26e931736e13b16483795c8a6b2f641bf6a3d5238c22b070a5112722c/pynacl-1.6.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d071c6a9a4c94d79eb665db4ce5cedc537faf74f2355e4d502591d850d3913c0", size = 809370, upload-time = "2026-01-01T17:31:59.198Z" }, + { url = "https://files.pythonhosted.org/packages/87/74/8d4b718f8a22aea9e8dcc8b95deb76d4aae380e2f5b570cc70b5fd0a852d/pynacl-1.6.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe9847ca47d287af41e82be1dd5e23023d3c31a951da134121ab02e42ac218c9", size = 1408304, upload-time = "2026-01-01T17:32:01.162Z" }, + { url = "https://files.pythonhosted.org/packages/fd/73/be4fdd3a6a87fe8a4553380c2b47fbd1f7f58292eb820902f5c8ac7de7b0/pynacl-1.6.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:04316d1fc625d860b6c162fff704eb8426b1a8bcd3abacea11142cbd99a6b574", size = 844871, upload-time = "2026-01-01T17:32:02.824Z" }, + { url = "https://files.pythonhosted.org/packages/55/ad/6efc57ab75ee4422e96b5f2697d51bbcf6cdcc091e66310df91fbdc144a8/pynacl-1.6.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44081faff368d6c5553ccf55322ef2819abb40e25afaec7e740f159f74813634", size = 1446356, upload-time = "2026-01-01T17:32:04.452Z" }, + { url = "https://files.pythonhosted.org/packages/78/b7/928ee9c4779caa0a915844311ab9fb5f99585621c5d6e4574538a17dca07/pynacl-1.6.2-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:a9f9932d8d2811ce1a8ffa79dcbdf3970e7355b5c8eb0c1a881a57e7f7d96e88", size = 826814, upload-time = "2026-01-01T17:32:06.078Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a9/1bdba746a2be20f8809fee75c10e3159d75864ef69c6b0dd168fc60e485d/pynacl-1.6.2-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:bc4a36b28dd72fb4845e5d8f9760610588a96d5a51f01d84d8c6ff9849968c14", size = 1411742, upload-time = "2026-01-01T17:32:07.651Z" }, + { url = "https://files.pythonhosted.org/packages/f3/2f/5e7ea8d85f9f3ea5b6b87db1d8388daa3587eed181bdeb0306816fdbbe79/pynacl-1.6.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3bffb6d0f6becacb6526f8f42adfb5efb26337056ee0831fb9a7044d1a964444", size = 801714, upload-time = "2026-01-01T17:32:09.558Z" }, + { url = "https://files.pythonhosted.org/packages/06/ea/43fe2f7eab5f200e40fb10d305bf6f87ea31b3bbc83443eac37cd34a9e1e/pynacl-1.6.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2fef529ef3ee487ad8113d287a593fa26f48ee3620d92ecc6f1d09ea38e0709b", size = 1372257, upload-time = "2026-01-01T17:32:11.026Z" }, + { url = "https://files.pythonhosted.org/packages/4d/54/c9ea116412788629b1347e415f72195c25eb2f3809b2d3e7b25f5c79f13a/pynacl-1.6.2-cp314-cp314t-win32.whl", hash = "sha256:a84bf1c20339d06dc0c85d9aea9637a24f718f375d861b2668b2f9f96fa51145", size = 231319, upload-time = "2026-01-01T17:32:12.46Z" }, + { url = "https://files.pythonhosted.org/packages/ce/04/64e9d76646abac2dccf904fccba352a86e7d172647557f35b9fe2a5ee4a1/pynacl-1.6.2-cp314-cp314t-win_amd64.whl", hash = "sha256:320ef68a41c87547c91a8b58903c9caa641ab01e8512ce291085b5fe2fcb7590", size = 244044, upload-time = "2026-01-01T17:32:13.781Z" }, + { url = "https://files.pythonhosted.org/packages/33/33/7873dc161c6a06f43cda13dec67b6fe152cb2f982581151956fa5e5cdb47/pynacl-1.6.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d29bfe37e20e015a7d8b23cfc8bd6aa7909c92a1b8f41ee416bbb3e79ef182b2", size = 188740, upload-time = "2026-01-01T17:32:15.083Z" }, + { url = "https://files.pythonhosted.org/packages/be/7b/4845bbf88e94586ec47a432da4e9107e3fc3ce37eb412b1398630a37f7dd/pynacl-1.6.2-cp38-abi3-macosx_10_10_universal2.whl", hash = "sha256:c949ea47e4206af7c8f604b8278093b674f7c79ed0d4719cc836902bf4517465", size = 388458, upload-time = "2026-01-01T17:32:16.829Z" }, + { url = "https://files.pythonhosted.org/packages/1e/b4/e927e0653ba63b02a4ca5b4d852a8d1d678afbf69b3dbf9c4d0785ac905c/pynacl-1.6.2-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8845c0631c0be43abdd865511c41eab235e0be69c81dc66a50911594198679b0", size = 800020, upload-time = "2026-01-01T17:32:18.34Z" }, + { url = "https://files.pythonhosted.org/packages/7f/81/d60984052df5c97b1d24365bc1e30024379b42c4edcd79d2436b1b9806f2/pynacl-1.6.2-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:22de65bb9010a725b0dac248f353bb072969c94fa8d6b1f34b87d7953cf7bbe4", size = 1399174, upload-time = "2026-01-01T17:32:20.239Z" }, + { url = "https://files.pythonhosted.org/packages/68/f7/322f2f9915c4ef27d140101dd0ed26b479f7e6f5f183590fd32dfc48c4d3/pynacl-1.6.2-cp38-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:46065496ab748469cdd999246d17e301b2c24ae2fdf739132e580a0e94c94a87", size = 835085, upload-time = "2026-01-01T17:32:22.24Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d0/f301f83ac8dbe53442c5a43f6a39016f94f754d7a9815a875b65e218a307/pynacl-1.6.2-cp38-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a66d6fb6ae7661c58995f9c6435bda2b1e68b54b598a6a10247bfcdadac996c", size = 1437614, upload-time = "2026-01-01T17:32:23.766Z" }, + { url = "https://files.pythonhosted.org/packages/c4/58/fc6e649762b029315325ace1a8c6be66125e42f67416d3dbd47b69563d61/pynacl-1.6.2-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:26bfcd00dcf2cf160f122186af731ae30ab120c18e8375684ec2670dccd28130", size = 818251, upload-time = "2026-01-01T17:32:25.69Z" }, + { url = "https://files.pythonhosted.org/packages/c9/a8/b917096b1accc9acd878819a49d3d84875731a41eb665f6ebc826b1af99e/pynacl-1.6.2-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c8a231e36ec2cab018c4ad4358c386e36eede0319a0c41fed24f840b1dac59f6", size = 1402859, upload-time = "2026-01-01T17:32:27.215Z" }, + { url = "https://files.pythonhosted.org/packages/85/42/fe60b5f4473e12c72f977548e4028156f4d340b884c635ec6b063fe7e9a5/pynacl-1.6.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:68be3a09455743ff9505491220b64440ced8973fe930f270c8e07ccfa25b1f9e", size = 791926, upload-time = "2026-01-01T17:32:29.314Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f9/e40e318c604259301cc091a2a63f237d9e7b424c4851cafaea4ea7c4834e/pynacl-1.6.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8b097553b380236d51ed11356c953bf8ce36a29a3e596e934ecabe76c985a577", size = 1363101, upload-time = "2026-01-01T17:32:31.263Z" }, + { url = "https://files.pythonhosted.org/packages/48/47/e761c254f410c023a469284a9bc210933e18588ca87706ae93002c05114c/pynacl-1.6.2-cp38-abi3-win32.whl", hash = "sha256:5811c72b473b2f38f7e2a3dc4f8642e3a3e9b5e7317266e4ced1fba85cae41aa", size = 227421, upload-time = "2026-01-01T17:32:33.076Z" }, + { url = "https://files.pythonhosted.org/packages/41/ad/334600e8cacc7d86587fe5f565480fde569dfb487389c8e1be56ac21d8ac/pynacl-1.6.2-cp38-abi3-win_amd64.whl", hash = "sha256:62985f233210dee6548c223301b6c25440852e13d59a8b81490203c3227c5ba0", size = 239754, upload-time = "2026-01-01T17:32:34.557Z" }, + { url = "https://files.pythonhosted.org/packages/29/7d/5945b5af29534641820d3bd7b00962abbbdfee84ec7e19f0d5b3175f9a31/pynacl-1.6.2-cp38-abi3-win_arm64.whl", hash = "sha256:834a43af110f743a754448463e8fd61259cd4ab5bbedcf70f9dabad1d28a394c", size = 184801, upload-time = "2026-01-01T17:32:36.309Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "pytest-codspeed" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "pytest" }, + { name = "rich" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e2/e8/27fcbe6516a1c956614a4b61a7fccbf3791ea0b992e07416e8948184327d/pytest_codspeed-4.2.0.tar.gz", hash = "sha256:04b5d0bc5a1851ba1504d46bf9d7dbb355222a69f2cd440d54295db721b331f7", size = 113263, upload-time = "2025-10-24T09:02:55.704Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/b8/d599a466c50af3f04001877ae8b17c12b803f3b358235736b91a0769de0d/pytest_codspeed-4.2.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:609828b03972966b75b9b7416fa2570c4a0f6124f67e02d35cd3658e64312a7b", size = 261943, upload-time = "2025-10-24T09:02:37.962Z" }, + { url = "https://files.pythonhosted.org/packages/74/19/ccc1a2fcd28357a8db08ba6b60f381832088a3850abc262c8e0b3406491a/pytest_codspeed-4.2.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23a0c0fbf8bb4de93a3454fd9e5efcdca164c778aaef0a9da4f233d85cb7f5b8", size = 250782, upload-time = "2025-10-24T09:02:39.617Z" }, + { url = "https://files.pythonhosted.org/packages/b9/2d/f0083a2f14ecf008d961d40439a71da0ae0d568e5f8dc2fccd3e8a2ab3e4/pytest_codspeed-4.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2de87bde9fbc6fd53f0fd21dcf2599c89e0b8948d49f9bad224edce51c47e26b", size = 261960, upload-time = "2025-10-24T09:02:40.665Z" }, + { url = "https://files.pythonhosted.org/packages/5f/0c/1f514c553db4ea5a69dfbe2706734129acd0eca8d5101ec16f1dd00dbc0f/pytest_codspeed-4.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95aeb2479ca383f6b18e2cc9ebcd3b03ab184980a59a232aea6f370bbf59a1e3", size = 250808, upload-time = "2025-10-24T09:02:42.07Z" }, + { url = "https://files.pythonhosted.org/packages/81/04/479905bd6653bc981c0554fcce6df52d7ae1594e1eefd53e6cf31810ec7f/pytest_codspeed-4.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d4fefbd4ae401e2c60f6be920a0be50eef0c3e4a1f0a1c83962efd45be38b39", size = 262084, upload-time = "2025-10-24T09:02:43.155Z" }, + { url = "https://files.pythonhosted.org/packages/d2/46/d6f345d7907bac6cbb6224bd697ecbc11cf7427acc9e843c3618f19e3476/pytest_codspeed-4.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:309b4227f57fcbb9df21e889ea1ae191d0d1cd8b903b698fdb9ea0461dbf1dfe", size = 251100, upload-time = "2025-10-24T09:02:44.168Z" }, + { url = "https://files.pythonhosted.org/packages/de/dc/e864f45e994a50390ff49792256f1bdcbf42f170e3bc0470ee1a7d2403f3/pytest_codspeed-4.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72aab8278452a6d020798b9e4f82780966adb00f80d27a25d1274272c54630d5", size = 262057, upload-time = "2025-10-24T09:02:45.791Z" }, + { url = "https://files.pythonhosted.org/packages/1d/1c/f1d2599784486879cf6579d8d94a3e22108f0e1f130033dab8feefd29249/pytest_codspeed-4.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:684fcd9491d810ded653a8d38de4835daa2d001645f4a23942862950664273f8", size = 251013, upload-time = "2025-10-24T09:02:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/0c/fd/eafd24db5652a94b4d00fe9b309b607de81add0f55f073afb68a378a24b6/pytest_codspeed-4.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50794dabea6ec90d4288904452051e2febace93e7edf4ca9f2bce8019dd8cd37", size = 262065, upload-time = "2025-10-24T09:02:48.018Z" }, + { url = "https://files.pythonhosted.org/packages/f9/14/8d9340d7dc0ae647991b28a396e16b3403e10def883cde90d6b663d3f7ec/pytest_codspeed-4.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0ebd87f2a99467a1cfd8e83492c4712976e43d353ee0b5f71cbb057f1393aca", size = 251057, upload-time = "2025-10-24T09:02:49.102Z" }, + { url = "https://files.pythonhosted.org/packages/4b/39/48cf6afbca55bc7c8c93c3d4ae926a1068bcce3f0241709db19b078d5418/pytest_codspeed-4.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dbbb2d61b85bef8fc7e2193f723f9ac2db388a48259d981bbce96319043e9830", size = 267983, upload-time = "2025-10-24T09:02:50.558Z" }, + { url = "https://files.pythonhosted.org/packages/33/86/4407341efb5dceb3e389635749ce1d670542d6ca148bd34f9d5334295faf/pytest_codspeed-4.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:748411c832147bfc85f805af78a1ab1684f52d08e14aabe22932bbe46c079a5f", size = 256732, upload-time = "2025-10-24T09:02:51.603Z" }, + { url = "https://files.pythonhosted.org/packages/fe/60/c395c19c14a1345d41ac3f7f0a9b372b666e88f9ba1f71988215174882bb/pytest_codspeed-4.2.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:238e17abe8f08d8747fa6c7acff34fefd3c40f17a56a7847ca13dc8d6e8c6009", size = 261935, upload-time = "2025-10-24T09:02:52.702Z" }, + { url = "https://files.pythonhosted.org/packages/15/ed/442fb6a1832c2c9002653f24770873839b24c091bd2ed658090c7862c563/pytest_codspeed-4.2.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0881a736285f33b9a8894da8fe8e1775aa1a4310226abe5d1f0329228efb680c", size = 250770, upload-time = "2025-10-24T09:02:53.73Z" }, + { url = "https://files.pythonhosted.org/packages/25/0e/8cb71fd3ed4ed08c07aec1245aea7bc1b661ba55fd9c392db76f1978d453/pytest_codspeed-4.2.0-py3-none-any.whl", hash = "sha256:e81bbb45c130874ef99aca97929d72682733527a49f84239ba575b5cb843bab0", size = 113726, upload-time = "2025-10-24T09:02:54.785Z" }, +] + +[[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 = "python-multipart" +version = "0.0.20" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.21" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.10' and python_full_version < '3.14'", +] +sdist = { url = "https://files.pythonhosted.org/packages/78/96/804520d0850c7db98e5ccb70282e29208723f0964e88ffd9d0da2f52ea09/python_multipart-0.0.21.tar.gz", hash = "sha256:7137ebd4d3bbf70ea1622998f902b97a29434a9e8dc40eb203bbcf7c2a2cba92", size = 37196, upload-time = "2025-12-17T09:24:22.446Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/76/03af049af4dcee5d27442f71b6924f01f3efb5d2bd34f23fcd563f2cc5f5/python_multipart-0.0.21-py3-none-any.whl", hash = "sha256:cf7a6713e01c87aa35387f4774e812c4361150938d20d232800f75ffcf266090", size = 24541, upload-time = "2025-12-17T09:24:21.153Z" }, +] + +[[package]] +name = "python-slugify" +version = "8.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "text-unidecode" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/87/c7/5e1547c44e31da50a460df93af11a535ace568ef89d7a811069ead340c4a/python-slugify-8.0.4.tar.gz", hash = "sha256:59202371d1d05b54a9e7720c5e038f928f45daaffe41dd10822f3907b937c856", size = 10921, upload-time = "2024-02-08T18:32:45.488Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/62/02da182e544a51a5c3ccf4b03ab79df279f9c60c5e82d5e8bec7ca26ac11/python_slugify-8.0.4-py2.py3-none-any.whl", hash = "sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8", size = 10051, upload-time = "2024-02-08T18:32:43.911Z" }, +] + +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/40/44efbb0dfbd33aca6a6483191dae0716070ed99e2ecb0c53683f400a0b4f/pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3", size = 8760432, upload-time = "2025-07-14T20:13:05.9Z" }, + { url = "https://files.pythonhosted.org/packages/5e/bf/360243b1e953bd254a82f12653974be395ba880e7ec23e3731d9f73921cc/pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b", size = 9590103, upload-time = "2025-07-14T20:13:07.698Z" }, + { url = "https://files.pythonhosted.org/packages/57/38/d290720e6f138086fb3d5ffe0b6caa019a791dd57866940c82e4eeaf2012/pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b", size = 8778557, upload-time = "2025-07-14T20:13:11.11Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, + { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, + { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, + { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, + { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, + { url = "https://files.pythonhosted.org/packages/59/42/b86689aac0cdaee7ae1c58d464b0ff04ca909c19bb6502d4973cdd9f9544/pywin32-311-cp39-cp39-win32.whl", hash = "sha256:aba8f82d551a942cb20d4a83413ccbac30790b50efb89a75e4f586ac0bb8056b", size = 8760837, upload-time = "2025-07-14T20:12:59.59Z" }, + { url = "https://files.pythonhosted.org/packages/9f/8a/1403d0353f8c5a2f0829d2b1c4becbf9da2f0a4d040886404fc4a5431e4d/pywin32-311-cp39-cp39-win_amd64.whl", hash = "sha256:e0c4cfb0621281fe40387df582097fd796e80430597cb9944f0ae70447bacd91", size = 9590187, upload-time = "2025-07-14T20:13:01.419Z" }, + { url = "https://files.pythonhosted.org/packages/60/22/e0e8d802f124772cec9c75430b01a212f86f9de7546bda715e54140d5aeb/pywin32-311-cp39-cp39-win_arm64.whl", hash = "sha256:62ea666235135fee79bb154e695f3ff67370afefd71bd7fea7512fc70ef31e3d", size = 8778162, upload-time = "2025-07-14T20:13:03.544Z" }, +] + +[[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" }, + { url = "https://files.pythonhosted.org/packages/9f/62/67fc8e68a75f738c9200422bf65693fb79a4cd0dc5b23310e5202e978090/pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da", size = 184450, upload-time = "2025-09-25T21:33:00.618Z" }, + { url = "https://files.pythonhosted.org/packages/ae/92/861f152ce87c452b11b9d0977952259aa7df792d71c1053365cc7b09cc08/pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917", size = 174319, upload-time = "2025-09-25T21:33:02.086Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cd/f0cfc8c74f8a030017a2b9c771b7f47e5dd702c3e28e5b2071374bda2948/pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9", size = 737631, upload-time = "2025-09-25T21:33:03.25Z" }, + { url = "https://files.pythonhosted.org/packages/ef/b2/18f2bd28cd2055a79a46c9b0895c0b3d987ce40ee471cecf58a1a0199805/pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5", size = 836795, upload-time = "2025-09-25T21:33:05.014Z" }, + { url = "https://files.pythonhosted.org/packages/73/b9/793686b2d54b531203c160ef12bec60228a0109c79bae6c1277961026770/pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a", size = 750767, upload-time = "2025-09-25T21:33:06.398Z" }, + { url = "https://files.pythonhosted.org/packages/a9/86/a137b39a611def2ed78b0e66ce2fe13ee701a07c07aebe55c340ed2a050e/pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926", size = 727982, upload-time = "2025-09-25T21:33:08.708Z" }, + { url = "https://files.pythonhosted.org/packages/dd/62/71c27c94f457cf4418ef8ccc71735324c549f7e3ea9d34aba50874563561/pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7", size = 755677, upload-time = "2025-09-25T21:33:09.876Z" }, + { url = "https://files.pythonhosted.org/packages/29/3d/6f5e0d58bd924fb0d06c3a6bad00effbdae2de5adb5cda5648006ffbd8d3/pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0", size = 142592, upload-time = "2025-09-25T21:33:10.983Z" }, + { url = "https://files.pythonhosted.org/packages/f0/0c/25113e0b5e103d7f1490c0e947e303fe4a696c10b501dea7a9f49d4e876c/pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007", size = 158777, upload-time = "2025-09-25T21:33:15.55Z" }, +] + +[[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 = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs", marker = "python_full_version >= '3.10'" }, + { name = "rpds-py", marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.10' and python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3", version = "1.26.20", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "urllib3", version = "2.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +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", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "markdown-it-py", version = "4.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { 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 = "rich-toolkit" +version = "0.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "click", version = "8.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "rich" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/97/09/3f9b8d9daaf235195c626f21e03604c05b987404ee3bcacee0c1f67f2a8e/rich_toolkit-0.17.1.tar.gz", hash = "sha256:5af54df8d1dd9c8530e462e1bdcaed625c9b49f5a55b035aa0ba1c17bdb87c9a", size = 187925, upload-time = "2025-12-17T10:49:22.583Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/7b/15e55fa8a76d0d41bf34d965af78acdaf80a315907adb30de8b63c272694/rich_toolkit-0.17.1-py3-none-any.whl", hash = "sha256:96d24bb921ecd225ffce7c526a9149e74006410c05e6d405bd74ffd54d5631ed", size = 31412, upload-time = "2025-12-17T10:49:21.793Z" }, +] + +[[package]] +name = "rignore" +version = "0.7.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/f5/8bed2310abe4ae04b67a38374a4d311dd85220f5d8da56f47ae9361be0b0/rignore-0.7.6.tar.gz", hash = "sha256:00d3546cd793c30cb17921ce674d2c8f3a4b00501cb0e3dd0e82217dbeba2671", size = 57140, upload-time = "2025-11-05T21:41:21.968Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/7a/b970cd0138b0ece72eb28f086e933f9ed75b795716ad3de5ab22994b3b54/rignore-0.7.6-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:f3c74a7e5ee77aea669c95fdb3933f2a6c7549893700082e759128a29cf67e45", size = 884999, upload-time = "2025-11-05T20:42:38.373Z" }, + { url = "https://files.pythonhosted.org/packages/ca/05/23faca29616d8966ada63fb0e13c214107811fa9a0aba2275e4c7ca63bd5/rignore-0.7.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b7202404958f5fe3474bac91f65350f0b1dde1a5e05089f2946549b7e91e79ec", size = 824824, upload-time = "2025-11-05T20:42:22.1Z" }, + { url = "https://files.pythonhosted.org/packages/fa/2e/05a1e61f04cf2548524224f0b5f21ca19ea58f7273a863bac10846b8ff69/rignore-0.7.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bde7c5835fa3905bfb7e329a4f1d7eccb676de63da7a3f934ddd5c06df20597", size = 899121, upload-time = "2025-11-05T20:40:48.94Z" }, + { url = "https://files.pythonhosted.org/packages/ff/35/71518847e10bdbf359badad8800e4681757a01f4777b3c5e03dbde8a42d8/rignore-0.7.6-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:626c3d4ba03af266694d25101bc1d8d16eda49c5feb86cedfec31c614fceca7d", size = 873813, upload-time = "2025-11-05T20:41:04.71Z" }, + { url = "https://files.pythonhosted.org/packages/f6/c8/32ae405d3e7fd4d9f9b7838f2fcca0a5005bb87fa514b83f83fd81c0df22/rignore-0.7.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0a43841e651e7a05a4274b9026cc408d1912e64016ede8cd4c145dae5d0635be", size = 1168019, upload-time = "2025-11-05T20:41:20.723Z" }, + { url = "https://files.pythonhosted.org/packages/25/98/013c955982bc5b4719bf9a5bea58be317eea28aa12bfd004025e3cd7c000/rignore-0.7.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7978c498dbf7f74d30cdb8859fe612167d8247f0acd377ae85180e34490725da", size = 942822, upload-time = "2025-11-05T20:41:36.99Z" }, + { url = "https://files.pythonhosted.org/packages/90/fb/9a3f3156c6ed30bcd597e63690353edac1fcffe9d382ad517722b56ac195/rignore-0.7.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d22f72ab695c07d2d96d2a645208daff17084441b5d58c07378c9dd6f9c4c87", size = 959820, upload-time = "2025-11-05T20:42:06.364Z" }, + { url = "https://files.pythonhosted.org/packages/5e/b2/93bf609633021e9658acaff24cfb055d8cdaf7f5855d10ebb35307900dda/rignore-0.7.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d5bd8e1a91ed1a789b2cbe39eeea9204a6719d4f2cf443a9544b521a285a295f", size = 985050, upload-time = "2025-11-05T20:41:51.124Z" }, + { url = "https://files.pythonhosted.org/packages/69/bc/ec2d040469bdfd7b743df10f2201c5d285009a4263d506edbf7a06a090bb/rignore-0.7.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bc1fc03efad5789365018e94ac4079f851a999bc154d1551c45179f7fcf45322", size = 1079164, upload-time = "2025-11-05T21:40:10.368Z" }, + { url = "https://files.pythonhosted.org/packages/df/26/4b635f4ea5baf4baa8ba8eee06163f6af6e76dfbe72deb57da34bb24b19d/rignore-0.7.6-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:ce2617fe28c51367fd8abfd4eeea9e61664af63c17d4ea00353d8ef56dfb95fa", size = 1139028, upload-time = "2025-11-05T21:40:27.977Z" }, + { url = "https://files.pythonhosted.org/packages/6a/54/a3147ebd1e477b06eb24e2c2c56d951ae5faa9045b7b36d7892fec5080d9/rignore-0.7.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:7c4ad2cee85068408e7819a38243043214e2c3047e9bd4c506f8de01c302709e", size = 1119024, upload-time = "2025-11-05T21:40:45.148Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f4/27475db769a57cff18fe7e7267b36e6cdb5b1281caa185ba544171106cba/rignore-0.7.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:02cd240bfd59ecc3907766f4839cbba20530a2e470abca09eaa82225e4d946fb", size = 1128531, upload-time = "2025-11-05T21:41:02.734Z" }, + { url = "https://files.pythonhosted.org/packages/97/32/6e782d3b352e4349fa0e90bf75b13cb7f11d8908b36d9e2b262224b65d9a/rignore-0.7.6-cp310-cp310-win32.whl", hash = "sha256:fe2bd8fa1ff555259df54c376abc73855cb02628a474a40d51b358c3a1ddc55b", size = 646817, upload-time = "2025-11-05T21:41:47.51Z" }, + { url = "https://files.pythonhosted.org/packages/c0/8a/53185c69abb3bb362e8a46b8089999f820bf15655629ff8395107633c8ab/rignore-0.7.6-cp310-cp310-win_amd64.whl", hash = "sha256:d80afd6071c78baf3765ec698841071b19e41c326f994cfa69b5a1df676f5d39", size = 727001, upload-time = "2025-11-05T21:41:32.778Z" }, + { url = "https://files.pythonhosted.org/packages/25/41/b6e2be3069ef3b7f24e35d2911bd6deb83d20ed5642ad81d5a6d1c015473/rignore-0.7.6-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:40be8226e12d6653abbebaffaea2885f80374c1c8f76fe5ca9e0cadd120a272c", size = 885285, upload-time = "2025-11-05T20:42:39.763Z" }, + { url = "https://files.pythonhosted.org/packages/52/66/ba7f561b6062402022887706a7f2b2c2e2e2a28f1e3839202b0a2f77e36d/rignore-0.7.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:182f4e5e4064d947c756819446a7d4cdede8e756b8c81cf9e509683fe38778d7", size = 823882, upload-time = "2025-11-05T20:42:23.488Z" }, + { url = "https://files.pythonhosted.org/packages/f5/81/4087453df35a90b07370647b19017029324950c1b9137d54bf1f33843f17/rignore-0.7.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16b63047648a916a87be1e51bb5c009063f1b8b6f5afe4f04f875525507e63dc", size = 899362, upload-time = "2025-11-05T20:40:51.111Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c9/390a8fdfabb76d71416be773bd9f162977bd483084f68daf19da1dec88a6/rignore-0.7.6-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ba5524f5178deca4d7695e936604ebc742acb8958f9395776e1fcb8133f8257a", size = 873633, upload-time = "2025-11-05T20:41:06.193Z" }, + { url = "https://files.pythonhosted.org/packages/df/c9/79404fcb0faa76edfbc9df0901f8ef18568d1104919ebbbad6d608c888d1/rignore-0.7.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:62020dbb89a1dd4b84ab3d60547b3b2eb2723641d5fb198463643f71eaaed57d", size = 1167633, upload-time = "2025-11-05T20:41:22.491Z" }, + { url = "https://files.pythonhosted.org/packages/6e/8d/b3466d32d445d158a0aceb80919085baaae495b1f540fb942f91d93b5e5b/rignore-0.7.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b34acd532769d5a6f153a52a98dcb81615c949ab11697ce26b2eb776af2e174d", size = 941434, upload-time = "2025-11-05T20:41:38.151Z" }, + { url = "https://files.pythonhosted.org/packages/e8/40/9cd949761a7af5bc27022a939c91ff622d29c7a0b66d0c13a863097dde2d/rignore-0.7.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c5e53b752f9de44dff7b3be3c98455ce3bf88e69d6dc0cf4f213346c5e3416c", size = 959461, upload-time = "2025-11-05T20:42:08.476Z" }, + { url = "https://files.pythonhosted.org/packages/b5/87/1e1a145731f73bdb7835e11f80da06f79a00d68b370d9a847de979575e6d/rignore-0.7.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:25b3536d13a5d6409ce85f23936f044576eeebf7b6db1d078051b288410fc049", size = 985323, upload-time = "2025-11-05T20:41:52.735Z" }, + { url = "https://files.pythonhosted.org/packages/6c/31/1ecff992fc3f59c4fcdcb6c07d5f6c1e6dfb55ccda19c083aca9d86fa1c6/rignore-0.7.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6e01cad2b0b92f6b1993f29fc01f23f2d78caf4bf93b11096d28e9d578eb08ce", size = 1079173, upload-time = "2025-11-05T21:40:12.007Z" }, + { url = "https://files.pythonhosted.org/packages/17/18/162eedadb4c2282fa4c521700dbf93c9b14b8842e8354f7d72b445b8d593/rignore-0.7.6-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:5991e46ab9b4868334c9e372ab0892b0150f3f586ff2b1e314272caeb38aaedb", size = 1139012, upload-time = "2025-11-05T21:40:29.399Z" }, + { url = "https://files.pythonhosted.org/packages/78/96/a9ca398a8af74bb143ad66c2a31303c894111977e28b0d0eab03867f1b43/rignore-0.7.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6c8ae562e5d1246cba5eaeb92a47b2a279e7637102828dde41dcbe291f529a3e", size = 1118827, upload-time = "2025-11-05T21:40:46.6Z" }, + { url = "https://files.pythonhosted.org/packages/9f/22/1c1a65047df864def9a047dbb40bc0b580b8289a4280e62779cd61ae21f2/rignore-0.7.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:aaf938530dcc0b47c4cfa52807aa2e5bfd5ca6d57a621125fe293098692f6345", size = 1128182, upload-time = "2025-11-05T21:41:04.239Z" }, + { url = "https://files.pythonhosted.org/packages/bd/f4/1526eb01fdc2235aca1fd9d0189bee4021d009a8dcb0161540238c24166e/rignore-0.7.6-cp311-cp311-win32.whl", hash = "sha256:166ebce373105dd485ec213a6a2695986346e60c94ff3d84eb532a237b24a4d5", size = 646547, upload-time = "2025-11-05T21:41:49.439Z" }, + { url = "https://files.pythonhosted.org/packages/7c/c8/dda0983e1845706beb5826459781549a840fe5a7eb934abc523e8cd17814/rignore-0.7.6-cp311-cp311-win_amd64.whl", hash = "sha256:44f35ee844b1a8cea50d056e6a595190ce9d42d3cccf9f19d280ae5f3058973a", size = 727139, upload-time = "2025-11-05T21:41:34.367Z" }, + { url = "https://files.pythonhosted.org/packages/e3/47/eb1206b7bf65970d41190b879e1723fc6bbdb2d45e53565f28991a8d9d96/rignore-0.7.6-cp311-cp311-win_arm64.whl", hash = "sha256:14b58f3da4fa3d5c3fa865cab49821675371f5e979281c683e131ae29159a581", size = 657598, upload-time = "2025-11-05T21:41:23.758Z" }, + { url = "https://files.pythonhosted.org/packages/0b/0e/012556ef3047a2628842b44e753bb15f4dc46806780ff090f1e8fe4bf1eb/rignore-0.7.6-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:03e82348cb7234f8d9b2834f854400ddbbd04c0f8f35495119e66adbd37827a8", size = 883488, upload-time = "2025-11-05T20:42:41.359Z" }, + { url = "https://files.pythonhosted.org/packages/93/b0/d4f1f3fe9eb3f8e382d45ce5b0547ea01c4b7e0b4b4eb87bcd66a1d2b888/rignore-0.7.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9e624f6be6116ea682e76c5feb71ea91255c67c86cb75befe774365b2931961", size = 820411, upload-time = "2025-11-05T20:42:24.782Z" }, + { url = "https://files.pythonhosted.org/packages/4a/c8/dea564b36dedac8de21c18e1851789545bc52a0c22ece9843444d5608a6a/rignore-0.7.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bda49950d405aa8d0ebe26af807c4e662dd281d926530f03f29690a2e07d649a", size = 897821, upload-time = "2025-11-05T20:40:52.613Z" }, + { url = "https://files.pythonhosted.org/packages/b3/2b/ee96db17ac1835e024c5d0742eefb7e46de60020385ac883dd3d1cde2c1f/rignore-0.7.6-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5fd5ab3840b8c16851d327ed06e9b8be6459702a53e5ab1fc4073b684b3789e", size = 873963, upload-time = "2025-11-05T20:41:07.49Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8c/ad5a57bbb9d14d5c7e5960f712a8a0b902472ea3f4a2138cbf70d1777b75/rignore-0.7.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ced2a248352636a5c77504cb755dc02c2eef9a820a44d3f33061ce1bb8a7f2d2", size = 1169216, upload-time = "2025-11-05T20:41:23.73Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/5b00bc2a6bc1701e6878fca798cf5d9125eb3113193e33078b6fc0d99123/rignore-0.7.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a04a3b73b75ddc12c9c9b21efcdaab33ca3832941d6f1d67bffd860941cd448a", size = 942942, upload-time = "2025-11-05T20:41:39.393Z" }, + { url = "https://files.pythonhosted.org/packages/85/e5/7f99bd0cc9818a91d0e8b9acc65b792e35750e3bdccd15a7ee75e64efca4/rignore-0.7.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d24321efac92140b7ec910ac7c53ab0f0c86a41133d2bb4b0e6a7c94967f44dd", size = 959787, upload-time = "2025-11-05T20:42:09.765Z" }, + { url = "https://files.pythonhosted.org/packages/55/54/2ffea79a7c1eabcede1926347ebc2a81bc6b81f447d05b52af9af14948b9/rignore-0.7.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:73c7aa109d41e593785c55fdaa89ad80b10330affa9f9d3e3a51fa695f739b20", size = 984245, upload-time = "2025-11-05T20:41:54.062Z" }, + { url = "https://files.pythonhosted.org/packages/41/f7/e80f55dfe0f35787fa482aa18689b9c8251e045076c35477deb0007b3277/rignore-0.7.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1734dc49d1e9501b07852ef44421f84d9f378da9fbeda729e77db71f49cac28b", size = 1078647, upload-time = "2025-11-05T21:40:13.463Z" }, + { url = "https://files.pythonhosted.org/packages/d4/cf/2c64f0b6725149f7c6e7e5a909d14354889b4beaadddaa5fff023ec71084/rignore-0.7.6-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5719ea14ea2b652c0c0894be5dfde954e1853a80dea27dd2fbaa749618d837f5", size = 1139186, upload-time = "2025-11-05T21:40:31.27Z" }, + { url = "https://files.pythonhosted.org/packages/75/95/a86c84909ccc24af0d094b50d54697951e576c252a4d9f21b47b52af9598/rignore-0.7.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8e23424fc7ce35726854f639cb7968151a792c0c3d9d082f7f67e0c362cfecca", size = 1117604, upload-time = "2025-11-05T21:40:48.07Z" }, + { url = "https://files.pythonhosted.org/packages/7f/5e/13b249613fd5d18d58662490ab910a9f0be758981d1797789913adb4e918/rignore-0.7.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3efdcf1dd84d45f3e2bd2f93303d9be103888f56dfa7c3349b5bf4f0657ec696", size = 1127725, upload-time = "2025-11-05T21:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/c7/28/fa5dcd1e2e16982c359128664e3785f202d3eca9b22dd0b2f91c4b3d242f/rignore-0.7.6-cp312-cp312-win32.whl", hash = "sha256:ccca9d1a8b5234c76b71546fc3c134533b013f40495f394a65614a81f7387046", size = 646145, upload-time = "2025-11-05T21:41:51.096Z" }, + { url = "https://files.pythonhosted.org/packages/26/87/69387fb5dd81a0f771936381431780b8cf66fcd2cfe9495e1aaf41548931/rignore-0.7.6-cp312-cp312-win_amd64.whl", hash = "sha256:c96a285e4a8bfec0652e0bfcf42b1aabcdda1e7625f5006d188e3b1c87fdb543", size = 726090, upload-time = "2025-11-05T21:41:36.485Z" }, + { url = "https://files.pythonhosted.org/packages/24/5f/e8418108dcda8087fb198a6f81caadbcda9fd115d61154bf0df4d6d3619b/rignore-0.7.6-cp312-cp312-win_arm64.whl", hash = "sha256:a64a750e7a8277a323f01ca50b7784a764845f6cce2fe38831cb93f0508d0051", size = 656317, upload-time = "2025-11-05T21:41:25.305Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8a/a4078f6e14932ac7edb171149c481de29969d96ddee3ece5dc4c26f9e0c3/rignore-0.7.6-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:2bdab1d31ec9b4fb1331980ee49ea051c0d7f7bb6baa28b3125ef03cdc48fdaf", size = 883057, upload-time = "2025-11-05T20:42:42.741Z" }, + { url = "https://files.pythonhosted.org/packages/f9/8f/f8daacd177db4bf7c2223bab41e630c52711f8af9ed279be2058d2fe4982/rignore-0.7.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:90f0a00ce0c866c275bf888271f1dc0d2140f29b82fcf33cdbda1e1a6af01010", size = 820150, upload-time = "2025-11-05T20:42:26.545Z" }, + { url = "https://files.pythonhosted.org/packages/36/31/b65b837e39c3f7064c426754714ac633b66b8c2290978af9d7f513e14aa9/rignore-0.7.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1ad295537041dc2ed4b540fb1a3906bd9ede6ccdad3fe79770cd89e04e3c73c", size = 897406, upload-time = "2025-11-05T20:40:53.854Z" }, + { url = "https://files.pythonhosted.org/packages/ca/58/1970ce006c427e202ac7c081435719a076c478f07b3a23f469227788dc23/rignore-0.7.6-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f782dbd3a65a5ac85adfff69e5c6b101285ef3f845c3a3cae56a54bebf9fe116", size = 874050, upload-time = "2025-11-05T20:41:08.922Z" }, + { url = "https://files.pythonhosted.org/packages/d4/00/eb45db9f90137329072a732273be0d383cb7d7f50ddc8e0bceea34c1dfdf/rignore-0.7.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65cece3b36e5b0826d946494734c0e6aaf5a0337e18ff55b071438efe13d559e", size = 1167835, upload-time = "2025-11-05T20:41:24.997Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f1/6f1d72ddca41a64eed569680587a1236633587cc9f78136477ae69e2c88a/rignore-0.7.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d7e4bb66c13cd7602dc8931822c02dfbbd5252015c750ac5d6152b186f0a8be0", size = 941945, upload-time = "2025-11-05T20:41:40.628Z" }, + { url = "https://files.pythonhosted.org/packages/48/6f/2f178af1c1a276a065f563ec1e11e7a9e23d4996fd0465516afce4b5c636/rignore-0.7.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:297e500c15766e196f68aaaa70e8b6db85fa23fdc075b880d8231fdfba738cd7", size = 959067, upload-time = "2025-11-05T20:42:11.09Z" }, + { url = "https://files.pythonhosted.org/packages/5b/db/423a81c4c1e173877c7f9b5767dcaf1ab50484a94f60a0b2ed78be3fa765/rignore-0.7.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a07084211a8d35e1a5b1d32b9661a5ed20669970b369df0cf77da3adea3405de", size = 984438, upload-time = "2025-11-05T20:41:55.443Z" }, + { url = "https://files.pythonhosted.org/packages/31/eb/c4f92cc3f2825d501d3c46a244a671eb737fc1bcf7b05a3ecd34abb3e0d7/rignore-0.7.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:181eb2a975a22256a1441a9d2f15eb1292839ea3f05606620bd9e1938302cf79", size = 1078365, upload-time = "2025-11-05T21:40:15.148Z" }, + { url = "https://files.pythonhosted.org/packages/26/09/99442f02794bd7441bfc8ed1c7319e890449b816a7493b2db0e30af39095/rignore-0.7.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:7bbcdc52b5bf9f054b34ce4af5269df5d863d9c2456243338bc193c28022bd7b", size = 1139066, upload-time = "2025-11-05T21:40:32.771Z" }, + { url = "https://files.pythonhosted.org/packages/2c/88/bcfc21e520bba975410e9419450f4b90a2ac8236b9a80fd8130e87d098af/rignore-0.7.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f2e027a6da21a7c8c0d87553c24ca5cc4364def18d146057862c23a96546238e", size = 1118036, upload-time = "2025-11-05T21:40:49.646Z" }, + { url = "https://files.pythonhosted.org/packages/e2/25/d37215e4562cda5c13312636393aea0bafe38d54d4e0517520a4cc0753ec/rignore-0.7.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee4a18b82cbbc648e4aac1510066682fe62beb5dc88e2c67c53a83954e541360", size = 1127550, upload-time = "2025-11-05T21:41:07.648Z" }, + { url = "https://files.pythonhosted.org/packages/dc/76/a264ab38bfa1620ec12a8ff1c07778da89e16d8c0f3450b0333020d3d6dc/rignore-0.7.6-cp313-cp313-win32.whl", hash = "sha256:a7d7148b6e5e95035d4390396895adc384d37ff4e06781a36fe573bba7c283e5", size = 646097, upload-time = "2025-11-05T21:41:53.201Z" }, + { url = "https://files.pythonhosted.org/packages/62/44/3c31b8983c29ea8832b6082ddb1d07b90379c2d993bd20fce4487b71b4f4/rignore-0.7.6-cp313-cp313-win_amd64.whl", hash = "sha256:b037c4b15a64dced08fc12310ee844ec2284c4c5c1ca77bc37d0a04f7bff386e", size = 726170, upload-time = "2025-11-05T21:41:38.131Z" }, + { url = "https://files.pythonhosted.org/packages/aa/41/e26a075cab83debe41a42661262f606166157df84e0e02e2d904d134c0d8/rignore-0.7.6-cp313-cp313-win_arm64.whl", hash = "sha256:e47443de9b12fe569889bdbe020abe0e0b667516ee2ab435443f6d0869bd2804", size = 656184, upload-time = "2025-11-05T21:41:27.396Z" }, + { url = "https://files.pythonhosted.org/packages/9a/b9/1f5bd82b87e5550cd843ceb3768b4a8ef274eb63f29333cf2f29644b3d75/rignore-0.7.6-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:8e41be9fa8f2f47239ded8920cc283699a052ac4c371f77f5ac017ebeed75732", size = 882632, upload-time = "2025-11-05T20:42:44.063Z" }, + { url = "https://files.pythonhosted.org/packages/e9/6b/07714a3efe4a8048864e8a5b7db311ba51b921e15268b17defaebf56d3db/rignore-0.7.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6dc1e171e52cefa6c20e60c05394a71165663b48bca6c7666dee4f778f2a7d90", size = 820760, upload-time = "2025-11-05T20:42:27.885Z" }, + { url = "https://files.pythonhosted.org/packages/ac/0f/348c829ea2d8d596e856371b14b9092f8a5dfbb62674ec9b3f67e4939a9d/rignore-0.7.6-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ce2268837c3600f82ab8db58f5834009dc638ee17103582960da668963bebc5", size = 899044, upload-time = "2025-11-05T20:40:55.336Z" }, + { url = "https://files.pythonhosted.org/packages/f0/30/2e1841a19b4dd23878d73edd5d82e998a83d5ed9570a89675f140ca8b2ad/rignore-0.7.6-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:690a3e1b54bfe77e89c4bacb13f046e642f8baadafc61d68f5a726f324a76ab6", size = 874144, upload-time = "2025-11-05T20:41:10.195Z" }, + { url = "https://files.pythonhosted.org/packages/c2/bf/0ce9beb2e5f64c30e3580bef09f5829236889f01511a125f98b83169b993/rignore-0.7.6-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09d12ac7a0b6210c07bcd145007117ebd8abe99c8eeb383e9e4673910c2754b2", size = 1168062, upload-time = "2025-11-05T20:41:26.511Z" }, + { url = "https://files.pythonhosted.org/packages/b9/8b/571c178414eb4014969865317da8a02ce4cf5241a41676ef91a59aab24de/rignore-0.7.6-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a2b2b74a8c60203b08452479b90e5ce3dbe96a916214bc9eb2e5af0b6a9beb0", size = 942542, upload-time = "2025-11-05T20:41:41.838Z" }, + { url = "https://files.pythonhosted.org/packages/19/62/7a3cf601d5a45137a7e2b89d10c05b5b86499190c4b7ca5c3c47d79ee519/rignore-0.7.6-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fc5a531ef02131e44359419a366bfac57f773ea58f5278c2cdd915f7d10ea94", size = 958739, upload-time = "2025-11-05T20:42:12.463Z" }, + { url = "https://files.pythonhosted.org/packages/5f/1f/4261f6a0d7caf2058a5cde2f5045f565ab91aa7badc972b57d19ce58b14e/rignore-0.7.6-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b7a1f77d9c4cd7e76229e252614d963442686bfe12c787a49f4fe481df49e7a9", size = 984138, upload-time = "2025-11-05T20:41:56.775Z" }, + { url = "https://files.pythonhosted.org/packages/2b/bf/628dfe19c75e8ce1f45f7c248f5148b17dfa89a817f8e3552ab74c3ae812/rignore-0.7.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ead81f728682ba72b5b1c3d5846b011d3e0174da978de87c61645f2ed36659a7", size = 1079299, upload-time = "2025-11-05T21:40:16.639Z" }, + { url = "https://files.pythonhosted.org/packages/af/a5/be29c50f5c0c25c637ed32db8758fdf5b901a99e08b608971cda8afb293b/rignore-0.7.6-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:12ffd50f520c22ffdabed8cd8bfb567d9ac165b2b854d3e679f4bcaef11a9441", size = 1139618, upload-time = "2025-11-05T21:40:34.507Z" }, + { url = "https://files.pythonhosted.org/packages/2a/40/3c46cd7ce4fa05c20b525fd60f599165e820af66e66f2c371cd50644558f/rignore-0.7.6-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:e5a16890fbe3c894f8ca34b0fcacc2c200398d4d46ae654e03bc9b3dbf2a0a72", size = 1117626, upload-time = "2025-11-05T21:40:51.494Z" }, + { url = "https://files.pythonhosted.org/packages/8c/b9/aea926f263b8a29a23c75c2e0d8447965eb1879d3feb53cfcf84db67ed58/rignore-0.7.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3abab3bf99e8a77488ef6c7c9a799fac22224c28fe9f25cc21aa7cc2b72bfc0b", size = 1128144, upload-time = "2025-11-05T21:41:09.169Z" }, + { url = "https://files.pythonhosted.org/packages/a4/f6/0d6242f8d0df7f2ecbe91679fefc1f75e7cd2072cb4f497abaab3f0f8523/rignore-0.7.6-cp314-cp314-win32.whl", hash = "sha256:eeef421c1782953c4375aa32f06ecae470c1285c6381eee2a30d2e02a5633001", size = 646385, upload-time = "2025-11-05T21:41:55.105Z" }, + { url = "https://files.pythonhosted.org/packages/d5/38/c0dcd7b10064f084343d6af26fe9414e46e9619c5f3224b5272e8e5d9956/rignore-0.7.6-cp314-cp314-win_amd64.whl", hash = "sha256:6aeed503b3b3d5af939b21d72a82521701a4bd3b89cd761da1e7dc78621af304", size = 725738, upload-time = "2025-11-05T21:41:39.736Z" }, + { url = "https://files.pythonhosted.org/packages/d9/7a/290f868296c1ece914d565757ab363b04730a728b544beb567ceb3b2d96f/rignore-0.7.6-cp314-cp314-win_arm64.whl", hash = "sha256:104f215b60b3c984c386c3e747d6ab4376d5656478694e22c7bd2f788ddd8304", size = 656008, upload-time = "2025-11-05T21:41:29.028Z" }, + { url = "https://files.pythonhosted.org/packages/ca/d2/3c74e3cd81fe8ea08a8dcd2d755c09ac2e8ad8fe409508904557b58383d3/rignore-0.7.6-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:bb24a5b947656dd94cb9e41c4bc8b23cec0c435b58be0d74a874f63c259549e8", size = 882835, upload-time = "2025-11-05T20:42:45.443Z" }, + { url = "https://files.pythonhosted.org/packages/77/61/a772a34b6b63154877433ac2d048364815b24c2dd308f76b212c408101a2/rignore-0.7.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5b1e33c9501cefe24b70a1eafd9821acfd0ebf0b35c3a379430a14df089993e3", size = 820301, upload-time = "2025-11-05T20:42:29.226Z" }, + { url = "https://files.pythonhosted.org/packages/71/30/054880b09c0b1b61d17eeb15279d8bf729c0ba52b36c3ada52fb827cbb3c/rignore-0.7.6-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bec3994665a44454df86deb762061e05cd4b61e3772f5b07d1882a8a0d2748d5", size = 897611, upload-time = "2025-11-05T20:40:56.475Z" }, + { url = "https://files.pythonhosted.org/packages/1e/40/b2d1c169f833d69931bf232600eaa3c7998ba4f9a402e43a822dad2ea9f2/rignore-0.7.6-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:26cba2edfe3cff1dfa72bddf65d316ddebf182f011f2f61538705d6dbaf54986", size = 873875, upload-time = "2025-11-05T20:41:11.561Z" }, + { url = "https://files.pythonhosted.org/packages/55/59/ca5ae93d83a1a60e44b21d87deb48b177a8db1b85e82fc8a9abb24a8986d/rignore-0.7.6-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ffa86694fec604c613696cb91e43892aa22e1fec5f9870e48f111c603e5ec4e9", size = 1167245, upload-time = "2025-11-05T20:41:28.29Z" }, + { url = "https://files.pythonhosted.org/packages/a5/52/cf3dce392ba2af806cba265aad6bcd9c48bb2a6cb5eee448d3319f6e505b/rignore-0.7.6-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48efe2ed95aa8104145004afb15cdfa02bea5cdde8b0344afeb0434f0d989aa2", size = 941750, upload-time = "2025-11-05T20:41:43.111Z" }, + { url = "https://files.pythonhosted.org/packages/ec/be/3f344c6218d779395e785091d05396dfd8b625f6aafbe502746fcd880af2/rignore-0.7.6-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dcae43eb44b7f2457fef7cc87f103f9a0013017a6f4e62182c565e924948f21", size = 958896, upload-time = "2025-11-05T20:42:13.784Z" }, + { url = "https://files.pythonhosted.org/packages/c9/34/d3fa71938aed7d00dcad87f0f9bcb02ad66c85d6ffc83ba31078ce53646a/rignore-0.7.6-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2cd649a7091c0dad2f11ef65630d30c698d505cbe8660dd395268e7c099cc99f", size = 983992, upload-time = "2025-11-05T20:41:58.022Z" }, + { url = "https://files.pythonhosted.org/packages/24/a4/52a697158e9920705bdbd0748d59fa63e0f3233fb92e9df9a71afbead6ca/rignore-0.7.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42de84b0289d478d30ceb7ae59023f7b0527786a9a5b490830e080f0e4ea5aeb", size = 1078181, upload-time = "2025-11-05T21:40:18.151Z" }, + { url = "https://files.pythonhosted.org/packages/ac/65/aa76dbcdabf3787a6f0fd61b5cc8ed1e88580590556d6c0207960d2384bb/rignore-0.7.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:875a617e57b53b4acbc5a91de418233849711c02e29cc1f4f9febb2f928af013", size = 1139232, upload-time = "2025-11-05T21:40:35.966Z" }, + { url = "https://files.pythonhosted.org/packages/08/44/31b31a49b3233c6842acc1c0731aa1e7fb322a7170612acf30327f700b44/rignore-0.7.6-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8703998902771e96e49968105207719f22926e4431b108450f3f430b4e268b7c", size = 1117349, upload-time = "2025-11-05T21:40:53.013Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ae/1b199a2302c19c658cf74e5ee1427605234e8c91787cfba0015f2ace145b/rignore-0.7.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:602ef33f3e1b04c1e9a10a3c03f8bc3cef2d2383dcc250d309be42b49923cabc", size = 1127702, upload-time = "2025-11-05T21:41:10.881Z" }, + { url = "https://files.pythonhosted.org/packages/fc/d3/18210222b37e87e36357f7b300b7d98c6dd62b133771e71ae27acba83a4f/rignore-0.7.6-cp314-cp314t-win32.whl", hash = "sha256:c1d8f117f7da0a4a96a8daef3da75bc090e3792d30b8b12cfadc240c631353f9", size = 647033, upload-time = "2025-11-05T21:42:00.095Z" }, + { url = "https://files.pythonhosted.org/packages/3e/87/033eebfbee3ec7d92b3bb1717d8f68c88e6fc7de54537040f3b3a405726f/rignore-0.7.6-cp314-cp314t-win_amd64.whl", hash = "sha256:ca36e59408bec81de75d307c568c2d0d410fb880b1769be43611472c61e85c96", size = 725647, upload-time = "2025-11-05T21:41:44.449Z" }, + { url = "https://files.pythonhosted.org/packages/79/62/b88e5879512c55b8ee979c666ee6902adc4ed05007226de266410ae27965/rignore-0.7.6-cp314-cp314t-win_arm64.whl", hash = "sha256:b83adabeb3e8cf662cabe1931b83e165b88c526fa6af6b3aa90429686e474896", size = 656035, upload-time = "2025-11-05T21:41:31.13Z" }, + { url = "https://files.pythonhosted.org/packages/b9/b4/e7577504d926ced2d6a3fa5ec5f27756639a1ed58a6a3fbefcf3a5659721/rignore-0.7.6-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b3746bda73f2fe6a9c3ab2f20b792e7d810b30acbdba044313fbd2d0174802e7", size = 886535, upload-time = "2025-11-05T20:42:49.317Z" }, + { url = "https://files.pythonhosted.org/packages/2b/74/098bc71a33e2997bc3291d500760123d23e3a6d354380d26c8a7ddc036de/rignore-0.7.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:67a99cf19a5137cc12f14b78dc1bb3f48500f1d5580702c623297d5297bf2752", size = 826621, upload-time = "2025-11-05T20:42:32.421Z" }, + { url = "https://files.pythonhosted.org/packages/7b/73/5f8c276d71009a7e73fb3af6ec3bb930269efeae5830de5c796fa1fb020f/rignore-0.7.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9e851cfa87033c0c3fd9d35dd8b102aff2981db8bc6e0cab27b460bfe38bf3f", size = 900335, upload-time = "2025-11-05T20:40:59.178Z" }, + { url = "https://files.pythonhosted.org/packages/0d/5f/dde3758084a087e6a5cd981c5277c6171d12127deed64fc4fbf12fb8ceaa/rignore-0.7.6-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e9b0def154665036516114437a5d603274e5451c0dc9694f622cc3b7e94603e7", size = 874274, upload-time = "2025-11-05T20:41:14.512Z" }, + { url = "https://files.pythonhosted.org/packages/58/b9/da85646824ab728036378ce62c330316108a52f30f36e6c69cac6ceda376/rignore-0.7.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b81274a47e8121224f7f637392b5dfcd9558e32a53e67ba7d04007d8b5281da9", size = 1171639, upload-time = "2025-11-05T20:41:31.206Z" }, + { url = "https://files.pythonhosted.org/packages/35/d1/8c12b779b7f0302c03c1d41511f2ab47012afecdfcd684fbec80af06b331/rignore-0.7.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d75d0b0696fb476664bea1169c8e67b13197750b91eceb4f10b3c7f379c7a204", size = 943985, upload-time = "2025-11-05T20:41:45.598Z" }, + { url = "https://files.pythonhosted.org/packages/79/bf/c233a85d31e4f94b911e92ee7e2dd2b962a5c2528f5ebd79a702596f0626/rignore-0.7.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ad3aa4dca77cef9168d0c142f72376f5bd27d1d4b8a81561bd01276d3ad9fe1", size = 961707, upload-time = "2025-11-05T20:42:16.461Z" }, + { url = "https://files.pythonhosted.org/packages/9d/eb/cadee9316a5f2a52b4aa7051967ecb94ec17938d6b425bd842d9317559eb/rignore-0.7.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:00f8a59e19d219f44a93af7173de197e0d0e61c386364da20ebe98a303cbe38c", size = 986638, upload-time = "2025-11-05T20:42:00.65Z" }, + { url = "https://files.pythonhosted.org/packages/d0/f0/2c3042c8c9639056593def5e99c3bfe850fbb9a38d061ba67b6314315bad/rignore-0.7.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dd6c682f3cdd741e7a30af2581f6a382ac910080977cd1f97c651467b6268352", size = 1080136, upload-time = "2025-11-05T21:40:21.551Z" }, + { url = "https://files.pythonhosted.org/packages/fc/28/7237b9eb1257b593ee51cd7ef8eed7cc32f50ccff18cb4d7cfe1e6dc54d7/rignore-0.7.6-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:ae4e93193f75ebf6b820241594a78f347785cfd5a5fbbac94634052589418352", size = 1139413, upload-time = "2025-11-05T21:40:39.025Z" }, + { url = "https://files.pythonhosted.org/packages/a5/df/c3f382a31ad7ed68510b411c28fec42354d2c43fecb7c053d998ee9410ed/rignore-0.7.6-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1163d8b5d3a320d4d7cc8635213328850dc41f60e438c7869d540061adf66c98", size = 1120204, upload-time = "2025-11-05T21:40:56.062Z" }, + { url = "https://files.pythonhosted.org/packages/9c/3d/e8585c4e9c0077255ba599684aee78326176ab13ff13805ea62aa7e3235f/rignore-0.7.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3e685f47b4c58b2df7dee81ebc1ec9dbb7f798b9455c3f22be6d75ac6bddee30", size = 1129757, upload-time = "2025-11-05T21:41:14.148Z" }, + { url = "https://files.pythonhosted.org/packages/fd/56/852226c13f89ddbbf12d639900941dc55dcbcf79f5d15294796fd3279d73/rignore-0.7.6-cp39-cp39-win32.whl", hash = "sha256:2af6a0a76575220863cd838693c808a94e750640e0c8a3e9f707e93c2f131fdf", size = 648265, upload-time = "2025-11-05T21:41:58.589Z" }, + { url = "https://files.pythonhosted.org/packages/cc/c6/14e7585dc453a870fe99b1270ee95e2adff02ea0d297cd6e2c4aa46cd43a/rignore-0.7.6-cp39-cp39-win_amd64.whl", hash = "sha256:a326eab6db9ab85b4afb5e6eb28736a9f2b885a9246d9e8c1989bc693dd059a0", size = 728715, upload-time = "2025-11-05T21:41:42.823Z" }, + { url = "https://files.pythonhosted.org/packages/85/12/62d690b4644c330d7ac0f739b7f078190ab4308faa909a60842d0e4af5b2/rignore-0.7.6-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c3d3a523af1cd4ed2c0cba8d277a32d329b0c96ef9901fb7ca45c8cfaccf31a5", size = 887462, upload-time = "2025-11-05T20:42:50.804Z" }, + { url = "https://files.pythonhosted.org/packages/05/bc/6528a0e97ed2bd7a7c329183367d1ffbc5b9762ae8348d88dae72cc9d1f5/rignore-0.7.6-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:990853566e65184a506e1e2af2d15045afad3ebaebb8859cb85b882081915110", size = 826918, upload-time = "2025-11-05T20:42:33.689Z" }, + { url = "https://files.pythonhosted.org/packages/3e/2c/7d7bad116e09a04e9e1688c6f891fa2d4fd33f11b69ac0bd92419ddebeae/rignore-0.7.6-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cab9ff2e436ce7240d7ee301c8ef806ed77c1fd6b8a8239ff65f9bbbcb5b8a3", size = 900922, upload-time = "2025-11-05T20:41:00.361Z" }, + { url = "https://files.pythonhosted.org/packages/09/ba/e5ea89fbde8e37a90ce456e31c5e9d85512cef5ae38e0f4d2426eb776a19/rignore-0.7.6-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d1a6671b2082c13bfd9a5cf4ce64670f832a6d41470556112c4ab0b6519b2fc4", size = 876987, upload-time = "2025-11-05T20:41:16.219Z" }, + { url = "https://files.pythonhosted.org/packages/d0/fb/93d14193f0ec0c3d35b763f0a000e9780f63b2031f3d3756442c2152622d/rignore-0.7.6-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2468729b4c5295c199d084ab88a40afcb7c8b974276805105239c07855bbacee", size = 1171110, upload-time = "2025-11-05T20:41:32.631Z" }, + { url = "https://files.pythonhosted.org/packages/9e/46/08436312ff96ffa29cfa4e1a987efc37e094531db46ba5e9fda9bb792afd/rignore-0.7.6-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:775710777fd71e5fdf54df69cdc249996a1d6f447a2b5bfb86dbf033fddd9cf9", size = 943339, upload-time = "2025-11-05T20:41:47.128Z" }, + { url = "https://files.pythonhosted.org/packages/34/28/3b3c51328f505cfaf7e53f408f78a1e955d561135d02f9cb0341ea99f69a/rignore-0.7.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4565407f4a77f72cf9d91469e75d15d375f755f0a01236bb8aaa176278cc7085", size = 961680, upload-time = "2025-11-05T20:42:18.061Z" }, + { url = "https://files.pythonhosted.org/packages/5c/9e/cbff75c8676d4f4a90bd58a1581249d255c7305141b0868f0abc0324836b/rignore-0.7.6-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dc44c33f8fb2d5c9da748de7a6e6653a78aa740655e7409895e94a247ffa97c8", size = 987045, upload-time = "2025-11-05T20:42:02.315Z" }, + { url = "https://files.pythonhosted.org/packages/8c/25/d802d1d369502a7ddb8816059e7c79d2d913e17df975b863418e0aca4d8a/rignore-0.7.6-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:8f32478f05540513c11923e8838afab9efef0131d66dca7f67f0e1bbd118af6a", size = 1080310, upload-time = "2025-11-05T21:40:23.184Z" }, + { url = "https://files.pythonhosted.org/packages/43/f0/250b785c2e473b1ab763eaf2be820934c2a5409a722e94b279dddac21c7d/rignore-0.7.6-pp310-pypy310_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:1b63a3dd76225ea35b01dd6596aa90b275b5d0f71d6dc28fce6dd295d98614aa", size = 1140998, upload-time = "2025-11-05T21:40:40.603Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d6/bb42fd2a8bba6aea327962656e20621fd495523259db40cfb4c5f760f05c/rignore-0.7.6-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:fe6c41175c36554a4ef0994cd1b4dbd6d73156fca779066456b781707402048e", size = 1121178, upload-time = "2025-11-05T21:40:57.585Z" }, + { url = "https://files.pythonhosted.org/packages/97/f4/aeb548374129dce3dc191a4bb598c944d9ed663f467b9af830315d86059c/rignore-0.7.6-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:9a0c6792406ae36f4e7664dc772da909451d46432ff8485774526232d4885063", size = 1130190, upload-time = "2025-11-05T21:41:16.403Z" }, + { url = "https://files.pythonhosted.org/packages/82/78/a6250ff0c49a3cdb943910ada4116e708118e9b901c878cfae616c80a904/rignore-0.7.6-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a20b6fb61bcced9a83dfcca6599ad45182b06ba720cff7c8d891e5b78db5b65f", size = 886470, upload-time = "2025-11-05T20:42:52.314Z" }, + { url = "https://files.pythonhosted.org/packages/35/af/c69c0c51b8f9f7914d95c4ea91c29a2ac067572048cae95dd6d2efdbe05d/rignore-0.7.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:392dcabfecbe176c9ebbcb40d85a5e86a5989559c4f988c2741da7daf1b5be25", size = 825976, upload-time = "2025-11-05T20:42:35.118Z" }, + { url = "https://files.pythonhosted.org/packages/f1/d2/1b264f56132264ea609d3213ab603d6a27016b19559a1a1ede1a66a03dcd/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22baa462abdc36fdd5a5e2dae423107723351b85ff093762f9261148b9d0a04a", size = 899739, upload-time = "2025-11-05T20:41:01.518Z" }, + { url = "https://files.pythonhosted.org/packages/55/e4/b3c5dfdd8d8a10741dfe7199ef45d19a0e42d0c13aa377c83bd6caf65d90/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53fb28882d2538cb2d231972146c4927a9d9455e62b209f85d634408c4103538", size = 874843, upload-time = "2025-11-05T20:41:17.687Z" }, + { url = "https://files.pythonhosted.org/packages/cc/10/d6f3750233881a2a154cefc9a6a0a9b19da526b19f7f08221b552c6f827d/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87409f7eeb1103d6b77f3472a3a0d9a5953e3ae804a55080bdcb0120ee43995b", size = 1170348, upload-time = "2025-11-05T20:41:34.21Z" }, + { url = "https://files.pythonhosted.org/packages/6e/10/ad98ca05c9771c15af734cee18114a3c280914b6e34fde9ffea2e61e88aa/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:684014e42e4341ab3ea23a203551857fcc03a7f8ae96ca3aefb824663f55db32", size = 942315, upload-time = "2025-11-05T20:41:48.508Z" }, + { url = "https://files.pythonhosted.org/packages/de/00/ab5c0f872acb60d534e687e629c17e0896c62da9b389c66d3aa16b817aa8/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77356ebb01ba13f8a425c3d30fcad40e57719c0e37670d022d560884a30e4767", size = 961047, upload-time = "2025-11-05T20:42:19.403Z" }, + { url = "https://files.pythonhosted.org/packages/b8/86/3030fdc363a8f0d1cd155b4c453d6db9bab47a24fcc64d03f61d9d78fe6a/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6cbd8a48abbd3747a6c830393cd578782fab5d43f4deea48c5f5e344b8fed2b0", size = 986090, upload-time = "2025-11-05T20:42:03.581Z" }, + { url = "https://files.pythonhosted.org/packages/33/b8/133aa4002cee0ebbb39362f94e4898eec7fbd09cec9fcbce1cd65b355b7f/rignore-0.7.6-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2673225dcec7f90497e79438c35e34638d0d0391ccea3cbb79bfb9adc0dc5bd7", size = 1079656, upload-time = "2025-11-05T21:40:24.89Z" }, + { url = "https://files.pythonhosted.org/packages/67/56/36d5d34210e5e7dfcd134eed8335b19e80ae940ee758f493e4f2b344dd70/rignore-0.7.6-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:c081f17290d8a2b96052b79207622aa635686ea39d502b976836384ede3d303c", size = 1139789, upload-time = "2025-11-05T21:40:42.119Z" }, + { url = "https://files.pythonhosted.org/packages/6b/5b/bb4f9420802bf73678033a4a55ab1bede36ce2e9b41fec5f966d83d932b3/rignore-0.7.6-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:57e8327aacc27f921968cb2a174f9e47b084ce9a7dd0122c8132d22358f6bd79", size = 1120308, upload-time = "2025-11-05T21:40:59.402Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8b/a1299085b28a2f6135e30370b126e3c5055b61908622f2488ade67641479/rignore-0.7.6-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:d8955b57e42f2a5434670d5aa7b75eaf6e74602ccd8955dddf7045379cd762fb", size = 1129444, upload-time = "2025-11-05T21:41:17.906Z" }, + { url = "https://files.pythonhosted.org/packages/47/98/80ef6fda78161e88ef9336fcbe851afccf78c48e69e8266a23fb7922b5aa/rignore-0.7.6-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e6ba1511c0ab8cd1ed8d6055bb0a6e629f48bfe04854293e0cd2dd88bd7153f8", size = 887180, upload-time = "2025-11-05T21:40:07.665Z" }, + { url = "https://files.pythonhosted.org/packages/21/d7/8666e7081f8476b003d8d2c8f39ecc17c93b7efd261740d15b6830acde82/rignore-0.7.6-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:50586d90be15f9aa8a2e2ee5a042ee6c51e28848812a35f0c95d4bfc0533d469", size = 827029, upload-time = "2025-11-05T20:42:36.628Z" }, + { url = "https://files.pythonhosted.org/packages/01/aa/3aba657d17b1737f4180b143866fedd269de15f361a8cb26ba363c0c3c13/rignore-0.7.6-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b129873dd0ade248e67f25a09b5b72288cbef76ba1a9aae6bac193ee1d8be72", size = 901338, upload-time = "2025-11-05T20:41:03.059Z" }, + { url = "https://files.pythonhosted.org/packages/90/cc/d8c2c9770f5f61b28999c582804f282f2227c155ba13dfb0e9ea03daeaaf/rignore-0.7.6-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d9d6dd947556ddebfd62753005104986ee14a4e0663818aed19cdf2c33a6b5d5", size = 877563, upload-time = "2025-11-05T20:41:19.209Z" }, + { url = "https://files.pythonhosted.org/packages/55/63/42dd625bf96989be4a928b5444ddec9101ee63a98a15646e611b3ce58b82/rignore-0.7.6-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91b95faa532efba888b196331e9af69e693635d469185ac52c796e435e2484e5", size = 1171087, upload-time = "2025-11-05T20:41:35.558Z" }, + { url = "https://files.pythonhosted.org/packages/bf/1e/4130fb622c2081c5322caf7a8888d1d265b99cd5d62cb714b512b8911233/rignore-0.7.6-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a1016f430fb56f7e400838bbc56fdf43adddb6fcb7bf2a14731dfd725c2fae6c", size = 944335, upload-time = "2025-11-05T20:41:49.859Z" }, + { url = "https://files.pythonhosted.org/packages/0f/b9/3d3ef7773da85e002fab53b1fdd9e9bb111cc86792b761cb38bd00c1532e/rignore-0.7.6-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f00c519861926dc703ecbb7bbeb884be67099f96f98b175671fa0a54718f55d1", size = 961500, upload-time = "2025-11-05T20:42:20.798Z" }, + { url = "https://files.pythonhosted.org/packages/1f/bc/346c874a31a721064935c60666a19016b6b01cd716cf73d52dc64e467b30/rignore-0.7.6-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e34d172bf50e881b7c02e530ae8b1ea96093f0b16634c344f637227b39707b41", size = 987741, upload-time = "2025-11-05T20:42:05.071Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b8/d12dc548da8fdb63292a38727b035153495220cd93730019ee8ed3bdcffb/rignore-0.7.6-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:101d3143619898db1e7bede2e3e647daf19bb867c4fb25978016d67978d14868", size = 1081057, upload-time = "2025-11-05T21:40:26.53Z" }, + { url = "https://files.pythonhosted.org/packages/8e/51/7eea5d949212709740ad07e01c524336e44608ef0614a2a1cb31c9a0ea30/rignore-0.7.6-pp39-pypy39_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:c9f3b420f54199a2b2b3b532d8c7e0860be3fa51f67501113cca6c7bfc392840", size = 1141653, upload-time = "2025-11-05T21:40:43.676Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2b/76ec843cc392fcb4e37d6a8340e823a0bf644872e191d2f5652a4c2c18ee/rignore-0.7.6-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:1c6795e3694d750ae5ef172eab7d68a52aefbd9168d2e06647df691db2b03a50", size = 1121465, upload-time = "2025-11-05T21:41:00.904Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9d/e69ad5cf03211a1076f9fe04ca2698c9cb8208b63419c928c26646bdf1d9/rignore-0.7.6-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:750a83a254b020e1193bfa7219dc7edca26bd8888a94cdc59720cbe386ab0c72", size = 1130110, upload-time = "2025-11-05T21:41:20.263Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/0c/0c411a0ec64ccb6d104dcabe0e713e05e153a9a2c3c2bd2b32ce412166fe/rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288", size = 370490, upload-time = "2025-11-30T20:21:33.256Z" }, + { url = "https://files.pythonhosted.org/packages/19/6a/4ba3d0fb7297ebae71171822554abe48d7cab29c28b8f9f2c04b79988c05/rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00", size = 359751, upload-time = "2025-11-30T20:21:34.591Z" }, + { url = "https://files.pythonhosted.org/packages/cd/7c/e4933565ef7f7a0818985d87c15d9d273f1a649afa6a52ea35ad011195ea/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6", size = 389696, upload-time = "2025-11-30T20:21:36.122Z" }, + { url = "https://files.pythonhosted.org/packages/5e/01/6271a2511ad0815f00f7ed4390cf2567bec1d4b1da39e2c27a41e6e3b4de/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7", size = 403136, upload-time = "2025-11-30T20:21:37.728Z" }, + { url = "https://files.pythonhosted.org/packages/55/64/c857eb7cd7541e9b4eee9d49c196e833128a55b89a9850a9c9ac33ccf897/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324", size = 524699, upload-time = "2025-11-30T20:21:38.92Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ed/94816543404078af9ab26159c44f9e98e20fe47e2126d5d32c9d9948d10a/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df", size = 412022, upload-time = "2025-11-30T20:21:40.407Z" }, + { url = "https://files.pythonhosted.org/packages/61/b5/707f6cf0066a6412aacc11d17920ea2e19e5b2f04081c64526eb35b5c6e7/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3", size = 390522, upload-time = "2025-11-30T20:21:42.17Z" }, + { url = "https://files.pythonhosted.org/packages/13/4e/57a85fda37a229ff4226f8cbcf09f2a455d1ed20e802ce5b2b4a7f5ed053/rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221", size = 404579, upload-time = "2025-11-30T20:21:43.769Z" }, + { url = "https://files.pythonhosted.org/packages/f9/da/c9339293513ec680a721e0e16bf2bac3db6e5d7e922488de471308349bba/rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7", size = 421305, upload-time = "2025-11-30T20:21:44.994Z" }, + { url = "https://files.pythonhosted.org/packages/f9/be/522cb84751114f4ad9d822ff5a1aa3c98006341895d5f084779b99596e5c/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff", size = 572503, upload-time = "2025-11-30T20:21:46.91Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9b/de879f7e7ceddc973ea6e4629e9b380213a6938a249e94b0cdbcc325bb66/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7", size = 598322, upload-time = "2025-11-30T20:21:48.709Z" }, + { url = "https://files.pythonhosted.org/packages/48/ac/f01fc22efec3f37d8a914fc1b2fb9bcafd56a299edbe96406f3053edea5a/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139", size = 560792, upload-time = "2025-11-30T20:21:50.024Z" }, + { url = "https://files.pythonhosted.org/packages/e2/da/4e2b19d0f131f35b6146425f846563d0ce036763e38913d917187307a671/rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464", size = 221901, upload-time = "2025-11-30T20:21:51.32Z" }, + { url = "https://files.pythonhosted.org/packages/96/cb/156d7a5cf4f78a7cc571465d8aec7a3c447c94f6749c5123f08438bcf7bc/rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169", size = 235823, upload-time = "2025-11-30T20:21:52.505Z" }, + { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" }, + { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" }, + { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" }, + { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" }, + { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" }, + { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" }, + { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" }, + { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" }, + { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" }, + { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" }, + { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, + { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, + { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, + { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, + { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, + { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, + { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, + { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, + { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, + { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, + { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, + { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, + { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, + { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, + { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, + { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, + { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, + { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, + { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, + { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, + { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, + { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, + { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, + { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, + { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, + { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, + { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, + { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, + { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, + { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, + { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" }, + { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" }, + { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" }, + { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" }, + { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" }, + { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" }, + { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" }, + { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" }, + { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, +] + +[[package]] +name = "rsa" +version = "4.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" }, +] + +[[package]] +name = "ruff" +version = "0.14.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/75/62/50b7727004dfe361104dfbf898c45a9a2fdfad8c72c04ae62900224d6ecf/ruff-0.14.3.tar.gz", hash = "sha256:4ff876d2ab2b161b6de0aa1f5bd714e8e9b4033dc122ee006925fbacc4f62153", size = 5558687, upload-time = "2025-10-31T00:26:26.878Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/8e/0c10ff1ea5d4360ab8bfca4cb2c9d979101a391f3e79d2616c9bf348cd26/ruff-0.14.3-py3-none-linux_armv6l.whl", hash = "sha256:876b21e6c824f519446715c1342b8e60f97f93264012de9d8d10314f8a79c371", size = 12535613, upload-time = "2025-10-31T00:25:44.302Z" }, + { url = "https://files.pythonhosted.org/packages/d3/c8/6724f4634c1daf52409fbf13fefda64aa9c8f81e44727a378b7b73dc590b/ruff-0.14.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b6fd8c79b457bedd2abf2702b9b472147cd860ed7855c73a5247fa55c9117654", size = 12855812, upload-time = "2025-10-31T00:25:47.793Z" }, + { url = "https://files.pythonhosted.org/packages/de/03/db1bce591d55fd5f8a08bb02517fa0b5097b2ccabd4ea1ee29aa72b67d96/ruff-0.14.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:71ff6edca490c308f083156938c0c1a66907151263c4abdcb588602c6e696a14", size = 11944026, upload-time = "2025-10-31T00:25:49.657Z" }, + { url = "https://files.pythonhosted.org/packages/0b/75/4f8dbd48e03272715d12c87dc4fcaaf21b913f0affa5f12a4e9c6f8a0582/ruff-0.14.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:786ee3ce6139772ff9272aaf43296d975c0217ee1b97538a98171bf0d21f87ed", size = 12356818, upload-time = "2025-10-31T00:25:51.949Z" }, + { url = "https://files.pythonhosted.org/packages/ec/9b/506ec5b140c11d44a9a4f284ea7c14ebf6f8b01e6e8917734a3325bff787/ruff-0.14.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cd6291d0061811c52b8e392f946889916757610d45d004e41140d81fb6cd5ddc", size = 12336745, upload-time = "2025-10-31T00:25:54.248Z" }, + { url = "https://files.pythonhosted.org/packages/c7/e1/c560d254048c147f35e7f8131d30bc1f63a008ac61595cf3078a3e93533d/ruff-0.14.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a497ec0c3d2c88561b6d90f9c29f5ae68221ac00d471f306fa21fa4264ce5fcd", size = 13101684, upload-time = "2025-10-31T00:25:56.253Z" }, + { url = "https://files.pythonhosted.org/packages/a5/32/e310133f8af5cd11f8cc30f52522a3ebccc5ea5bff4b492f94faceaca7a8/ruff-0.14.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e231e1be58fc568950a04fbe6887c8e4b85310e7889727e2b81db205c45059eb", size = 14535000, upload-time = "2025-10-31T00:25:58.397Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a1/7b0470a22158c6d8501eabc5e9b6043c99bede40fa1994cadf6b5c2a61c7/ruff-0.14.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:469e35872a09c0e45fecf48dd960bfbce056b5db2d5e6b50eca329b4f853ae20", size = 14156450, upload-time = "2025-10-31T00:26:00.889Z" }, + { url = "https://files.pythonhosted.org/packages/0a/96/24bfd9d1a7f532b560dcee1a87096332e461354d3882124219bcaff65c09/ruff-0.14.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d6bc90307c469cb9d28b7cfad90aaa600b10d67c6e22026869f585e1e8a2db0", size = 13568414, upload-time = "2025-10-31T00:26:03.291Z" }, + { url = "https://files.pythonhosted.org/packages/a7/e7/138b883f0dfe4ad5b76b58bf4ae675f4d2176ac2b24bdd81b4d966b28c61/ruff-0.14.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2f8a0bbcffcfd895df39c9a4ecd59bb80dca03dc43f7fb63e647ed176b741e", size = 13315293, upload-time = "2025-10-31T00:26:05.708Z" }, + { url = "https://files.pythonhosted.org/packages/33/f4/c09bb898be97b2eb18476b7c950df8815ef14cf956074177e9fbd40b7719/ruff-0.14.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:678fdd7c7d2d94851597c23ee6336d25f9930b460b55f8598e011b57c74fd8c5", size = 13539444, upload-time = "2025-10-31T00:26:08.09Z" }, + { url = "https://files.pythonhosted.org/packages/9c/aa/b30a1db25fc6128b1dd6ff0741fa4abf969ded161599d07ca7edd0739cc0/ruff-0.14.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1ec1ac071e7e37e0221d2f2dbaf90897a988c531a8592a6a5959f0603a1ecf5e", size = 12252581, upload-time = "2025-10-31T00:26:10.297Z" }, + { url = "https://files.pythonhosted.org/packages/da/13/21096308f384d796ffe3f2960b17054110a9c3828d223ca540c2b7cc670b/ruff-0.14.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:afcdc4b5335ef440d19e7df9e8ae2ad9f749352190e96d481dc501b753f0733e", size = 12307503, upload-time = "2025-10-31T00:26:12.646Z" }, + { url = "https://files.pythonhosted.org/packages/cb/cc/a350bac23f03b7dbcde3c81b154706e80c6f16b06ff1ce28ed07dc7b07b0/ruff-0.14.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:7bfc42f81862749a7136267a343990f865e71fe2f99cf8d2958f684d23ce3dfa", size = 12675457, upload-time = "2025-10-31T00:26:15.044Z" }, + { url = "https://files.pythonhosted.org/packages/cb/76/46346029fa2f2078826bc88ef7167e8c198e58fe3126636e52f77488cbba/ruff-0.14.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a65e448cfd7e9c59fae8cf37f9221585d3354febaad9a07f29158af1528e165f", size = 13403980, upload-time = "2025-10-31T00:26:17.81Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a4/35f1ef68c4e7b236d4a5204e3669efdeefaef21f0ff6a456792b3d8be438/ruff-0.14.3-py3-none-win32.whl", hash = "sha256:f3d91857d023ba93e14ed2d462ab62c3428f9bbf2b4fbac50a03ca66d31991f7", size = 12500045, upload-time = "2025-10-31T00:26:20.503Z" }, + { url = "https://files.pythonhosted.org/packages/03/15/51960ae340823c9859fb60c63301d977308735403e2134e17d1d2858c7fb/ruff-0.14.3-py3-none-win_amd64.whl", hash = "sha256:d7b7006ac0756306db212fd37116cce2bd307e1e109375e1c6c106002df0ae5f", size = 13594005, upload-time = "2025-10-31T00:26:22.533Z" }, + { url = "https://files.pythonhosted.org/packages/b7/73/4de6579bac8e979fca0a77e54dec1f1e011a0d268165eb8a9bc0982a6564/ruff-0.14.3-py3-none-win_arm64.whl", hash = "sha256:26eb477ede6d399d898791d01961e16b86f02bc2486d0d1a7a9bb2379d055dc1", size = 12590017, upload-time = "2025-10-31T00:26:24.52Z" }, +] + +[[package]] +name = "s3transfer" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/04/74127fc843314818edfa81b5540e26dd537353b123a4edc563109d8f17dd/s3transfer-0.16.0.tar.gz", hash = "sha256:8e990f13268025792229cd52fa10cb7163744bf56e719e0b9cb925ab79abf920", size = 153827, upload-time = "2025-12-01T02:30:59.114Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/51/727abb13f44c1fcf6d145979e1535a35794db0f6e450a0cb46aa24732fe2/s3transfer-0.16.0-py3-none-any.whl", hash = "sha256:18e25d66fed509e3868dc1572b3f427ff947dd2c56f844a5bf09481ad3f3b2fe", size = 86830, upload-time = "2025-12-01T02:30:57.729Z" }, +] + +[[package]] +name = "sentry-sdk" +version = "2.49.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "urllib3", version = "1.26.20", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "urllib3", version = "2.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/94/23ac26616a883f492428d9ee9ad6eee391612125326b784dbfc30e1e7bab/sentry_sdk-2.49.0.tar.gz", hash = "sha256:c1878599cde410d481c04ef50ee3aedd4f600e4d0d253f4763041e468b332c30", size = 387228, upload-time = "2026-01-08T09:56:25.642Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/43/1c586f9f413765201234541857cb82fda076f4b0f7bad4a0ec248da39cf3/sentry_sdk-2.49.0-py2.py3-none-any.whl", hash = "sha256:6ea78499133874445a20fe9c826c9e960070abeb7ae0cdf930314ab16bb97aa0", size = 415693, upload-time = "2026-01-08T09:56:21.872Z" }, +] + +[[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 = "smmap" +version = "5.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329, upload-time = "2025-01-02T07:14:40.909Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303, upload-time = "2025-01-02T07:14:38.724Z" }, +] + +[[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 = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.45" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", version = "3.2.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.10' and platform_machine == 'AMD64') or (python_full_version < '3.10' and platform_machine == 'WIN32') or (python_full_version < '3.10' and platform_machine == 'aarch64') or (python_full_version < '3.10' and platform_machine == 'amd64') or (python_full_version < '3.10' and platform_machine == 'ppc64le') or (python_full_version < '3.10' and platform_machine == 'win32') or (python_full_version < '3.10' and platform_machine == 'x86_64')" }, + { name = "greenlet", version = "3.3.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.10' and platform_machine == 'AMD64') or (python_full_version >= '3.10' and platform_machine == 'WIN32') or (python_full_version >= '3.10' and platform_machine == 'aarch64') or (python_full_version >= '3.10' and platform_machine == 'amd64') or (python_full_version >= '3.10' and platform_machine == 'ppc64le') or (python_full_version >= '3.10' and platform_machine == 'win32') or (python_full_version >= '3.10' and platform_machine == 'x86_64')" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/f9/5e4491e5ccf42f5d9cfc663741d261b3e6e1683ae7812114e7636409fcc6/sqlalchemy-2.0.45.tar.gz", hash = "sha256:1632a4bda8d2d25703fdad6363058d882541bdaaee0e5e3ddfa0cd3229efce88", size = 9869912, upload-time = "2025-12-09T21:05:16.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/70/75b1387d72e2847220441166c5eb4e9846dd753895208c13e6d66523b2d9/sqlalchemy-2.0.45-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c64772786d9eee72d4d3784c28f0a636af5b0a29f3fe26ff11f55efe90c0bd85", size = 2154148, upload-time = "2025-12-10T20:03:21.023Z" }, + { url = "https://files.pythonhosted.org/packages/d8/a4/7805e02323c49cb9d1ae5cd4913b28c97103079765f520043f914fca4cb3/sqlalchemy-2.0.45-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7ae64ebf7657395824a19bca98ab10eb9a3ecb026bf09524014f1bb81cb598d4", size = 3233051, upload-time = "2025-12-09T22:06:04.768Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ec/32ae09139f61bef3de3142e85c47abdee8db9a55af2bb438da54a4549263/sqlalchemy-2.0.45-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f02325709d1b1a1489f23a39b318e175a171497374149eae74d612634b234c0", size = 3232781, upload-time = "2025-12-09T22:09:54.435Z" }, + { url = "https://files.pythonhosted.org/packages/ad/bd/bf7b869b6f5585eac34222e1cf4405f4ba8c3b85dd6b1af5d4ce8bca695f/sqlalchemy-2.0.45-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d2c3684fca8a05f0ac1d9a21c1f4a266983a7ea9180efb80ffeb03861ecd01a0", size = 3182096, upload-time = "2025-12-09T22:06:06.169Z" }, + { url = "https://files.pythonhosted.org/packages/21/6a/c219720a241bb8f35c88815ccc27761f5af7fdef04b987b0e8a2c1a6dcaa/sqlalchemy-2.0.45-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:040f6f0545b3b7da6b9317fc3e922c9a98fc7243b2a1b39f78390fc0942f7826", size = 3205109, upload-time = "2025-12-09T22:09:55.969Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c4/6ccf31b2bc925d5d95fab403ffd50d20d7c82b858cf1a4855664ca054dce/sqlalchemy-2.0.45-cp310-cp310-win32.whl", hash = "sha256:830d434d609fe7bfa47c425c445a8b37929f140a7a44cdaf77f6d34df3a7296a", size = 2114240, upload-time = "2025-12-09T21:29:54.007Z" }, + { url = "https://files.pythonhosted.org/packages/de/29/a27a31fca07316def418db6f7c70ab14010506616a2decef1906050a0587/sqlalchemy-2.0.45-cp310-cp310-win_amd64.whl", hash = "sha256:0209d9753671b0da74da2cfbb9ecf9c02f72a759e4b018b3ab35f244c91842c7", size = 2137615, upload-time = "2025-12-09T21:29:55.85Z" }, + { url = "https://files.pythonhosted.org/packages/a2/1c/769552a9d840065137272ebe86ffbb0bc92b0f1e0a68ee5266a225f8cd7b/sqlalchemy-2.0.45-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2e90a344c644a4fa871eb01809c32096487928bd2038bf10f3e4515cb688cc56", size = 2153860, upload-time = "2025-12-10T20:03:23.843Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f8/9be54ff620e5b796ca7b44670ef58bc678095d51b0e89d6e3102ea468216/sqlalchemy-2.0.45-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8c8b41b97fba5f62349aa285654230296829672fc9939cd7f35aab246d1c08b", size = 3309379, upload-time = "2025-12-09T22:06:07.461Z" }, + { url = "https://files.pythonhosted.org/packages/f6/2b/60ce3ee7a5ae172bfcd419ce23259bb874d2cddd44f67c5df3760a1e22f9/sqlalchemy-2.0.45-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:12c694ed6468333a090d2f60950e4250b928f457e4962389553d6ba5fe9951ac", size = 3309948, upload-time = "2025-12-09T22:09:57.643Z" }, + { url = "https://files.pythonhosted.org/packages/a3/42/bac8d393f5db550e4e466d03d16daaafd2bad1f74e48c12673fb499a7fc1/sqlalchemy-2.0.45-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f7d27a1d977a1cfef38a0e2e1ca86f09c4212666ce34e6ae542f3ed0a33bc606", size = 3261239, upload-time = "2025-12-09T22:06:08.879Z" }, + { url = "https://files.pythonhosted.org/packages/6f/12/43dc70a0528c59842b04ea1c1ed176f072a9b383190eb015384dd102fb19/sqlalchemy-2.0.45-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d62e47f5d8a50099b17e2bfc1b0c7d7ecd8ba6b46b1507b58cc4f05eefc3bb1c", size = 3284065, upload-time = "2025-12-09T22:09:59.454Z" }, + { url = "https://files.pythonhosted.org/packages/cf/9c/563049cf761d9a2ec7bc489f7879e9d94e7b590496bea5bbee9ed7b4cc32/sqlalchemy-2.0.45-cp311-cp311-win32.whl", hash = "sha256:3c5f76216e7b85770d5bb5130ddd11ee89f4d52b11783674a662c7dd57018177", size = 2113480, upload-time = "2025-12-09T21:29:57.03Z" }, + { url = "https://files.pythonhosted.org/packages/bc/fa/09d0a11fe9f15c7fa5c7f0dd26be3d235b0c0cbf2f9544f43bc42efc8a24/sqlalchemy-2.0.45-cp311-cp311-win_amd64.whl", hash = "sha256:a15b98adb7f277316f2c276c090259129ee4afca783495e212048daf846654b2", size = 2138407, upload-time = "2025-12-09T21:29:58.556Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c7/1900b56ce19bff1c26f39a4ce427faec7716c81ac792bfac8b6a9f3dca93/sqlalchemy-2.0.45-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3ee2aac15169fb0d45822983631466d60b762085bc4535cd39e66bea362df5f", size = 3333760, upload-time = "2025-12-09T22:11:02.66Z" }, + { url = "https://files.pythonhosted.org/packages/0a/93/3be94d96bb442d0d9a60e55a6bb6e0958dd3457751c6f8502e56ef95fed0/sqlalchemy-2.0.45-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba547ac0b361ab4f1608afbc8432db669bd0819b3e12e29fb5fa9529a8bba81d", size = 3348268, upload-time = "2025-12-09T22:13:49.054Z" }, + { url = "https://files.pythonhosted.org/packages/48/4b/f88ded696e61513595e4a9778f9d3f2bf7332cce4eb0c7cedaabddd6687b/sqlalchemy-2.0.45-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:215f0528b914e5c75ef2559f69dca86878a3beeb0c1be7279d77f18e8d180ed4", size = 3278144, upload-time = "2025-12-09T22:11:04.14Z" }, + { url = "https://files.pythonhosted.org/packages/ed/6a/310ecb5657221f3e1bd5288ed83aa554923fb5da48d760a9f7622afeb065/sqlalchemy-2.0.45-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:107029bf4f43d076d4011f1afb74f7c3e2ea029ec82eb23d8527d5e909e97aa6", size = 3313907, upload-time = "2025-12-09T22:13:50.598Z" }, + { url = "https://files.pythonhosted.org/packages/5c/39/69c0b4051079addd57c84a5bfb34920d87456dd4c90cf7ee0df6efafc8ff/sqlalchemy-2.0.45-cp312-cp312-win32.whl", hash = "sha256:0c9f6ada57b58420a2c0277ff853abe40b9e9449f8d7d231763c6bc30f5c4953", size = 2112182, upload-time = "2025-12-09T21:39:30.824Z" }, + { url = "https://files.pythonhosted.org/packages/f7/4e/510db49dd89fc3a6e994bee51848c94c48c4a00dc905e8d0133c251f41a7/sqlalchemy-2.0.45-cp312-cp312-win_amd64.whl", hash = "sha256:8defe5737c6d2179c7997242d6473587c3beb52e557f5ef0187277009f73e5e1", size = 2139200, upload-time = "2025-12-09T21:39:32.321Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c8/7cc5221b47a54edc72a0140a1efa56e0a2730eefa4058d7ed0b4c4357ff8/sqlalchemy-2.0.45-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe187fc31a54d7fd90352f34e8c008cf3ad5d064d08fedd3de2e8df83eb4a1cf", size = 3277082, upload-time = "2025-12-09T22:11:06.167Z" }, + { url = "https://files.pythonhosted.org/packages/0e/50/80a8d080ac7d3d321e5e5d420c9a522b0aa770ec7013ea91f9a8b7d36e4a/sqlalchemy-2.0.45-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:672c45cae53ba88e0dad74b9027dddd09ef6f441e927786b05bec75d949fbb2e", size = 3293131, upload-time = "2025-12-09T22:13:52.626Z" }, + { url = "https://files.pythonhosted.org/packages/da/4c/13dab31266fc9904f7609a5dc308a2432a066141d65b857760c3bef97e69/sqlalchemy-2.0.45-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:470daea2c1ce73910f08caf10575676a37159a6d16c4da33d0033546bddebc9b", size = 3225389, upload-time = "2025-12-09T22:11:08.093Z" }, + { url = "https://files.pythonhosted.org/packages/74/04/891b5c2e9f83589de202e7abaf24cd4e4fa59e1837d64d528829ad6cc107/sqlalchemy-2.0.45-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9c6378449e0940476577047150fd09e242529b761dc887c9808a9a937fe990c8", size = 3266054, upload-time = "2025-12-09T22:13:54.262Z" }, + { url = "https://files.pythonhosted.org/packages/f1/24/fc59e7f71b0948cdd4cff7a286210e86b0443ef1d18a23b0d83b87e4b1f7/sqlalchemy-2.0.45-cp313-cp313-win32.whl", hash = "sha256:4b6bec67ca45bc166c8729910bd2a87f1c0407ee955df110d78948f5b5827e8a", size = 2110299, upload-time = "2025-12-09T21:39:33.486Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c5/d17113020b2d43073412aeca09b60d2009442420372123b8d49cc253f8b8/sqlalchemy-2.0.45-cp313-cp313-win_amd64.whl", hash = "sha256:afbf47dc4de31fa38fd491f3705cac5307d21d4bb828a4f020ee59af412744ee", size = 2136264, upload-time = "2025-12-09T21:39:36.801Z" }, + { url = "https://files.pythonhosted.org/packages/3d/8d/bb40a5d10e7a5f2195f235c0b2f2c79b0bf6e8f00c0c223130a4fbd2db09/sqlalchemy-2.0.45-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83d7009f40ce619d483d26ac1b757dfe3167b39921379a8bd1b596cf02dab4a6", size = 3521998, upload-time = "2025-12-09T22:13:28.622Z" }, + { url = "https://files.pythonhosted.org/packages/75/a5/346128b0464886f036c039ea287b7332a410aa2d3fb0bb5d404cb8861635/sqlalchemy-2.0.45-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d8a2ca754e5415cde2b656c27900b19d50ba076aa05ce66e2207623d3fe41f5a", size = 3473434, upload-time = "2025-12-09T22:13:30.188Z" }, + { url = "https://files.pythonhosted.org/packages/cc/64/4e1913772646b060b025d3fc52ce91a58967fe58957df32b455de5a12b4f/sqlalchemy-2.0.45-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f46ec744e7f51275582e6a24326e10c49fbdd3fc99103e01376841213028774", size = 3272404, upload-time = "2025-12-09T22:11:09.662Z" }, + { url = "https://files.pythonhosted.org/packages/b3/27/caf606ee924282fe4747ee4fd454b335a72a6e018f97eab5ff7f28199e16/sqlalchemy-2.0.45-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:883c600c345123c033c2f6caca18def08f1f7f4c3ebeb591a63b6fceffc95cce", size = 3277057, upload-time = "2025-12-09T22:13:56.213Z" }, + { url = "https://files.pythonhosted.org/packages/85/d0/3d64218c9724e91f3d1574d12eb7ff8f19f937643815d8daf792046d88ab/sqlalchemy-2.0.45-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2c0b74aa79e2deade948fe8593654c8ef4228c44ba862bb7c9585c8e0db90f33", size = 3222279, upload-time = "2025-12-09T22:11:11.1Z" }, + { url = "https://files.pythonhosted.org/packages/24/10/dd7688a81c5bc7690c2a3764d55a238c524cd1a5a19487928844cb247695/sqlalchemy-2.0.45-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8a420169cef179d4c9064365f42d779f1e5895ad26ca0c8b4c0233920973db74", size = 3244508, upload-time = "2025-12-09T22:13:57.932Z" }, + { url = "https://files.pythonhosted.org/packages/aa/41/db75756ca49f777e029968d9c9fee338c7907c563267740c6d310a8e3f60/sqlalchemy-2.0.45-cp314-cp314-win32.whl", hash = "sha256:e50dcb81a5dfe4b7b4a4aa8f338116d127cb209559124f3694c70d6cd072b68f", size = 2113204, upload-time = "2025-12-09T21:39:38.365Z" }, + { url = "https://files.pythonhosted.org/packages/89/a2/0e1590e9adb292b1d576dbcf67ff7df8cf55e56e78d2c927686d01080f4b/sqlalchemy-2.0.45-cp314-cp314-win_amd64.whl", hash = "sha256:4748601c8ea959e37e03d13dcda4a44837afcd1b21338e637f7c935b8da06177", size = 2138785, upload-time = "2025-12-09T21:39:39.503Z" }, + { url = "https://files.pythonhosted.org/packages/42/39/f05f0ed54d451156bbed0e23eb0516bcad7cbb9f18b3bf219c786371b3f0/sqlalchemy-2.0.45-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd337d3526ec5298f67d6a30bbbe4ed7e5e68862f0bf6dd21d289f8d37b7d60b", size = 3522029, upload-time = "2025-12-09T22:13:32.09Z" }, + { url = "https://files.pythonhosted.org/packages/54/0f/d15398b98b65c2bce288d5ee3f7d0a81f77ab89d9456994d5c7cc8b2a9db/sqlalchemy-2.0.45-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9a62b446b7d86a3909abbcd1cd3cc550a832f99c2bc37c5b22e1925438b9367b", size = 3475142, upload-time = "2025-12-09T22:13:33.739Z" }, + { url = "https://files.pythonhosted.org/packages/53/01/a01b9829d146ba59972e6dfc88138142f5ffa4110e492c83326e7d765a17/sqlalchemy-2.0.45-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d29b2b99d527dbc66dd87c3c3248a5dd789d974a507f4653c969999fc7c1191b", size = 2157179, upload-time = "2025-12-10T20:05:13.998Z" }, + { url = "https://files.pythonhosted.org/packages/1f/78/ed43ed8ac27844f129adfc45a8735bab5dcad3e5211f4dc1bd7e676bc3ed/sqlalchemy-2.0.45-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:59a8b8bd9c6bedf81ad07c8bd5543eedca55fe9b8780b2b628d495ba55f8db1e", size = 3233038, upload-time = "2025-12-09T22:06:55.42Z" }, + { url = "https://files.pythonhosted.org/packages/24/1c/721ec797f21431c905ad98cbce66430d72a340935e3b7e3232cf05e015cc/sqlalchemy-2.0.45-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd93c6f5d65f254ceabe97548c709e073d6da9883343adaa51bf1a913ce93f8e", size = 3233117, upload-time = "2025-12-09T22:10:03.143Z" }, + { url = "https://files.pythonhosted.org/packages/52/33/dcfb8dffb2ccd7c6803d63454dc1917ef5ec5b5e281fecbbc0ed1de1f125/sqlalchemy-2.0.45-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:6d0beadc2535157070c9c17ecf25ecec31e13c229a8f69196d7590bde8082bf1", size = 3182306, upload-time = "2025-12-09T22:06:56.894Z" }, + { url = "https://files.pythonhosted.org/packages/53/76/7cf8ce9e6dcac1d37125425aadec406d8a839dffc1b8763f6e7a56b0bf33/sqlalchemy-2.0.45-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e057f928ffe9c9b246a55b469c133b98a426297e1772ad24ce9f0c47d123bd5b", size = 3205587, upload-time = "2025-12-09T22:10:04.812Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ac/5cd0d14f7830981c06f468507237b0a8205691d626492b5551a67535eb30/sqlalchemy-2.0.45-cp39-cp39-win32.whl", hash = "sha256:c1c2091b1489435ff85728fafeb990f073e64f6f5e81d5cd53059773e8521eb6", size = 2115932, upload-time = "2025-12-09T22:09:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/b9/eb/76f6db8828c6e0cfac89820a07a40a2bab25e82e69827177b942a9bff42a/sqlalchemy-2.0.45-cp39-cp39-win_amd64.whl", hash = "sha256:56ead1f8dfb91a54a28cd1d072c74b3d635bcffbd25e50786533b822d4f2cde2", size = 2139570, upload-time = "2025-12-09T22:09:18.545Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e1/3ccb13c643399d22289c6a9786c1a91e3dcbb68bce4beb44926ac2c557bf/sqlalchemy-2.0.45-py3-none-any.whl", hash = "sha256:5225a288e4c8cc2308dbdd874edad6e7d0fd38eac1e9e5f23503425c8eee20d0", size = 1936672, upload-time = "2025-12-09T21:54:52.608Z" }, +] + +[[package]] +name = "sqlmodel" +version = "0.0.27" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "sqlalchemy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/5a/693d90866233e837d182da76082a6d4c2303f54d3aaaa5c78e1238c5d863/sqlmodel-0.0.27.tar.gz", hash = "sha256:ad1227f2014a03905aef32e21428640848ac09ff793047744a73dfdd077ff620", size = 118053, upload-time = "2025-10-08T16:39:11.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/92/c35e036151fe53822893979f8a13e6f235ae8191f4164a79ae60a95d66aa/sqlmodel-0.0.27-py3-none-any.whl", hash = "sha256:667fe10aa8ff5438134668228dc7d7a08306f4c5c4c7e6ad3ad68defa0e7aa49", size = 29131, upload-time = "2025-10-08T16:39:10.917Z" }, +] + +[[package]] +name = "sse-starlette" +version = "3.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", marker = "python_full_version >= '3.10'" }, + { name = "starlette", version = "0.50.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/34/f5df66cb383efdbf4f2db23cabb27f51b1dcb737efaf8a558f6f1d195134/sse_starlette-3.1.2.tar.gz", hash = "sha256:55eff034207a83a0eb86de9a68099bd0157838f0b8b999a1b742005c71e33618", size = 26303, upload-time = "2025-12-31T08:02:20.023Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/95/8c4b76eec9ae574474e5d2997557cebf764bcd3586458956c30631ae08f4/sse_starlette-3.1.2-py3-none-any.whl", hash = "sha256:cd800dd349f4521b317b9391d3796fa97b71748a4da9b9e00aafab32dda375c8", size = 12484, upload-time = "2025-12-31T08:02:18.894Z" }, +] + +[[package]] +name = "starlette" +version = "0.49.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "anyio", marker = "python_full_version < '3.10'" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/1a/608df0b10b53b0beb96a37854ee05864d182ddd4b1156a22f1ad3860425a/starlette-0.49.3.tar.gz", hash = "sha256:1c14546f299b5901a1ea0e34410575bc33bbd741377a10484a54445588d00284", size = 2655031, upload-time = "2025-11-01T15:12:26.13Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/e0/021c772d6a662f43b63044ab481dc6ac7592447605b5b35a957785363122/starlette-0.49.3-py3-none-any.whl", hash = "sha256:b579b99715fdc2980cf88c8ec96d3bf1ce16f5a8051a7c2b84ef9b1cdecaea2f", size = 74340, upload-time = "2025-11-01T15:12:24.387Z" }, +] + +[[package]] +name = "starlette" +version = "0.50.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.10' and python_full_version < '3.14'", +] +dependencies = [ + { name = "anyio", marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.10' and 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 = "strawberry-graphql" +version = "0.283.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "graphql-core", marker = "python_full_version < '3.10'" }, + { name = "lia-web", marker = "python_full_version < '3.10'" }, + { name = "packaging", marker = "python_full_version < '3.10'" }, + { name = "python-dateutil", marker = "python_full_version < '3.10'" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/30/74/729c227b1e7fce28678290a5013ddceb543f350b6c14ae83400ab2c727d1/strawberry_graphql-0.283.3.tar.gz", hash = "sha256:375e545856b7587debd4e0f1e2a6fca19d09cc126238a07b9e5164e5eb09342a", size = 212141, upload-time = "2025-10-10T20:03:46.985Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/1b/aa358ef730d727d2e42810bf943542a8cc4c15aa2401f8629d356643a06f/strawberry_graphql-0.283.3-py3-none-any.whl", hash = "sha256:3751d86a219d81a16a13f335bb7d2fa3f57a85fab83d7d284b8ea88e2261d68b", size = 309885, upload-time = "2025-10-10T20:03:44.051Z" }, +] + +[[package]] +name = "strawberry-graphql" +version = "0.288.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.10' and python_full_version < '3.14'", +] +dependencies = [ + { name = "cross-web", marker = "python_full_version >= '3.10'" }, + { name = "graphql-core", marker = "python_full_version >= '3.10'" }, + { name = "packaging", marker = "python_full_version >= '3.10'" }, + { name = "python-dateutil", marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/13/306dd2edb5f0c4aa51e2d071994a91ed1a0304e9feafa79ea0f04da51298/strawberry_graphql-0.288.2.tar.gz", hash = "sha256:853dbab407e3f5099f3a27dbf37786535894a0fbf150df5dde145fc290db607e", size = 215182, upload-time = "2026-01-01T20:01:19.277Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/14/15abfa6d289048eeb1b1cca4a582db08a3c1f42784b485c21ef54617e2c7/strawberry_graphql-0.288.2-py3-none-any.whl", hash = "sha256:ad72d7904582db333158568751bb6186a872380a8cc6671159d011d279382542", size = 313137, upload-time = "2026-01-01T20:01:17.32Z" }, +] + +[[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 = "tenacity" +version = "9.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/d4/2b0cd0fe285e14b36db076e78c93766ff1d529d70408bd1d2a5a84f1d929/tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb", size = 48036, upload-time = "2025-04-02T08:25:09.966Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248, upload-time = "2025-04-02T08:25:07.678Z" }, +] + +[[package]] +name = "termcolor" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/ca/6c/3d75c196ac07ac8749600b60b03f4f6094d54e132c4d94ebac6ee0e0add0/termcolor-3.1.0.tar.gz", hash = "sha256:6a6dd7fbee581909eeec6a756cff1d7f7c376063b14e4a298dc4980309e55970", size = 14324, upload-time = "2025-04-30T11:37:53.791Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/bd/de8d508070629b6d84a30d01d57e4a65c69aa7f5abe7560b8fad3b50ea59/termcolor-3.1.0-py3-none-any.whl", hash = "sha256:591dd26b5c2ce03b9e43f391264626557873ce1d379019786f99b0c2bee140aa", size = 7684, upload-time = "2025-04-30T11:37:52.382Z" }, +] + +[[package]] +name = "termcolor" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.10' and python_full_version < '3.14'", +] +sdist = { url = "https://files.pythonhosted.org/packages/46/79/cf31d7a93a8fdc6aa0fbb665be84426a8c5a557d9240b6239e9e11e35fc5/termcolor-3.3.0.tar.gz", hash = "sha256:348871ca648ec6a9a983a13ab626c0acce02f515b9e1983332b17af7979521c5", size = 14434, upload-time = "2025-12-29T12:55:21.882Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/d1/8bb87d21e9aeb323cc03034f5eaf2c8f69841e40e4853c2627edf8111ed3/termcolor-3.3.0-py3-none-any.whl", hash = "sha256:cf642efadaf0a8ebbbf4bc7a31cec2f9b5f21a9f726f4ccbb08192c9c26f43a5", size = 7734, upload-time = "2025-12-29T12:55:20.718Z" }, +] + +[[package]] +name = "text-unidecode" +version = "1.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ab/e2/e9a00f0ccb71718418230718b3d900e71a5d16e701a3dae079a21e9cd8f8/text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93", size = 76885, upload-time = "2019-08-30T21:36:45.405Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/a5/c0b6468d3824fe3fde30dbb5e1f687b291608f9473681bbf7dabbf5a87d7/text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8", size = 78154, upload-time = "2019-08-30T21:37:03.543Z" }, +] + +[[package]] +name = "tinycss2" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "webencodings", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/fd/7a5ee21fd08ff70d3d33a5781c255cbe779659bd03278feb98b19ee550f4/tinycss2-1.4.0.tar.gz", hash = "sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7", size = 87085, upload-time = "2024-10-24T14:58:29.895Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.0-py3-none-any.whl", hash = "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289", size = 26610, upload-time = "2024-10-24T14:58:28.029Z" }, +] + +[[package]] +name = "tinycss2" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.10' and python_full_version < '3.14'", +] +dependencies = [ + { name = "webencodings", marker = "python_full_version >= '3.10'" }, +] +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 = "tokenizers" +version = "0.22.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", size = 372115, upload-time = "2026-01-05T10:45:15.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/97/5dbfabf04c7e348e655e907ed27913e03db0923abb5dfdd120d7b25630e1/tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c", size = 3100275, upload-time = "2026-01-05T10:41:02.158Z" }, + { url = "https://files.pythonhosted.org/packages/2e/47/174dca0502ef88b28f1c9e06b73ce33500eedfac7a7692108aec220464e7/tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", size = 2981472, upload-time = "2026-01-05T10:41:00.276Z" }, + { url = "https://files.pythonhosted.org/packages/d6/84/7990e799f1309a8b87af6b948f31edaa12a3ed22d11b352eaf4f4b2e5753/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736, upload-time = "2026-01-05T10:40:32.165Z" }, + { url = "https://files.pythonhosted.org/packages/78/59/09d0d9ba94dcd5f4f1368d4858d24546b4bdc0231c2354aa31d6199f0399/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835, upload-time = "2026-01-05T10:40:38.847Z" }, + { url = "https://files.pythonhosted.org/packages/47/50/b3ebb4243e7160bda8d34b731e54dd8ab8b133e50775872e7a434e524c28/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5", size = 3521673, upload-time = "2026-01-05T10:40:56.614Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fa/89f4cb9e08df770b57adb96f8cbb7e22695a4cb6c2bd5f0c4f0ebcf33b66/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818, upload-time = "2026-01-05T10:40:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/64/04/ca2363f0bfbe3b3d36e95bf67e56a4c88c8e3362b658e616d1ac185d47f2/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195, upload-time = "2026-01-05T10:40:51.139Z" }, + { url = "https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982, upload-time = "2026-01-05T10:40:58.331Z" }, + { url = "https://files.pythonhosted.org/packages/1d/28/5f9f5a4cc211b69e89420980e483831bcc29dade307955cc9dc858a40f01/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245, upload-time = "2026-01-05T10:41:04.053Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fb/66e2da4704d6aadebf8cb39f1d6d1957df667ab24cff2326b77cda0dcb85/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069, upload-time = "2026-01-05T10:45:10.673Z" }, + { url = "https://files.pythonhosted.org/packages/16/04/fed398b05caa87ce9b1a1bb5166645e38196081b225059a6edaff6440fac/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a", size = 9899263, upload-time = "2026-01-05T10:45:12.559Z" }, + { url = "https://files.pythonhosted.org/packages/05/a1/d62dfe7376beaaf1394917e0f8e93ee5f67fea8fcf4107501db35996586b/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429, upload-time = "2026-01-05T10:45:14.333Z" }, + { url = "https://files.pythonhosted.org/packages/fd/18/a545c4ea42af3df6effd7d13d250ba77a0a86fb20393143bbb9a92e434d4/tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", size = 2502363, upload-time = "2026-01-05T10:45:20.593Z" }, + { url = "https://files.pythonhosted.org/packages/65/71/0670843133a43d43070abeb1949abfdef12a86d490bea9cd9e18e37c5ff7/tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", size = 2747786, upload-time = "2026-01-05T10:45:18.411Z" }, + { url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133, upload-time = "2026-01-05T10:45:17.232Z" }, + { url = "https://files.pythonhosted.org/packages/84/04/655b79dbcc9b3ac5f1479f18e931a344af67e5b7d3b251d2dcdcd7558592/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:753d47ebd4542742ef9261d9da92cd545b2cacbb48349a1225466745bb866ec4", size = 3282301, upload-time = "2026-01-05T10:40:34.858Z" }, + { url = "https://files.pythonhosted.org/packages/46/cd/e4851401f3d8f6f45d8480262ab6a5c8cb9c4302a790a35aa14eeed6d2fd/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e10bf9113d209be7cd046d40fbabbaf3278ff6d18eb4da4c500443185dc1896c", size = 3161308, upload-time = "2026-01-05T10:40:40.737Z" }, + { url = "https://files.pythonhosted.org/packages/6f/6e/55553992a89982cd12d4a66dddb5e02126c58677ea3931efcbe601d419db/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64d94e84f6660764e64e7e0b22baa72f6cd942279fdbb21d46abd70d179f0195", size = 3718964, upload-time = "2026-01-05T10:40:46.56Z" }, + { url = "https://files.pythonhosted.org/packages/59/8c/b1c87148aa15e099243ec9f0cf9d0e970cc2234c3257d558c25a2c5304e6/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f01a9c019878532f98927d2bacb79bbb404b43d3437455522a00a30718cdedb5", size = 3373542, upload-time = "2026-01-05T10:40:52.803Z" }, + { url = "https://files.pythonhosted.org/packages/27/46/8d7db1dff181be50b207ab0a7483a22d5c3a4f903a9afc7cf7e465ad8109/tokenizers-0.22.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:319f659ee992222f04e58f84cbf407cfa66a65fe3a8de44e8ad2bc53e7d99012", size = 3287784, upload-time = "2026-01-05T10:40:37.108Z" }, + { url = "https://files.pythonhosted.org/packages/5b/6e/3bc33cae8bf114afa5a98e35eb065c72b7c37d01d370906a893f33881767/tokenizers-0.22.2-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1e50f8554d504f617d9e9d6e4c2c2884a12b388a97c5c77f0bc6cf4cd032feee", size = 3164301, upload-time = "2026-01-05T10:40:42.367Z" }, + { url = "https://files.pythonhosted.org/packages/91/fc/6aa749d7d443aab4daa6f8bc00338389149fd2534e25b772285c3301993e/tokenizers-0.22.2-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a62ba2c5faa2dd175aaeed7b15abf18d20266189fb3406c5d0550dd34dd5f37", size = 3717771, upload-time = "2026-01-05T10:40:49.076Z" }, + { url = "https://files.pythonhosted.org/packages/fc/60/5b440d251863bd33f9b0a416c695b0309487b83abf6f2dafe9163a3aeac2/tokenizers-0.22.2-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:143b999bdc46d10febb15cbffb4207ddd1f410e2c755857b5a0797961bbdc113", size = 3377740, upload-time = "2026-01-05T10:40:54.859Z" }, +] + +[[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 = "tqdm" +version = "4.67.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, +] + +[[package]] +name = "trio" +version = "0.31.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "attrs", marker = "python_full_version < '3.10'" }, + { name = "cffi", marker = "python_full_version < '3.10' and implementation_name != 'pypy' and os_name == 'nt'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.10'" }, + { name = "idna", marker = "python_full_version < '3.10'" }, + { name = "outcome", marker = "python_full_version < '3.10'" }, + { name = "sniffio", marker = "python_full_version < '3.10'" }, + { name = "sortedcontainers", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/8f/c6e36dd11201e2a565977d8b13f0b027ba4593c1a80bed5185489178e257/trio-0.31.0.tar.gz", hash = "sha256:f71d551ccaa79d0cb73017a33ef3264fde8335728eb4c6391451fe5d253a9d5b", size = 605825, upload-time = "2025-09-09T15:17:15.242Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/5b/94237a3485620dbff9741df02ff6d8acaa5fdec67d81ab3f62e4d8511bf7/trio-0.31.0-py3-none-any.whl", hash = "sha256:b5d14cd6293d79298b49c3485ffd9c07e3ce03a6da8c7dfbe0cb3dd7dc9a4774", size = 512679, upload-time = "2025-09-09T15:17:13.821Z" }, +] + +[[package]] +name = "trio" +version = "0.32.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.10' and python_full_version < '3.14'", +] +dependencies = [ + { name = "attrs", marker = "python_full_version >= '3.10'" }, + { name = "cffi", marker = "python_full_version >= '3.10' and implementation_name != 'pypy' and os_name == 'nt'" }, + { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" }, + { name = "idna", marker = "python_full_version >= '3.10'" }, + { name = "outcome", marker = "python_full_version >= '3.10'" }, + { name = "sniffio", marker = "python_full_version >= '3.10'" }, + { name = "sortedcontainers", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d8/ce/0041ddd9160aac0031bcf5ab786c7640d795c797e67c438e15cfedf815c8/trio-0.32.0.tar.gz", hash = "sha256:150f29ec923bcd51231e1d4c71c7006e65247d68759dd1c19af4ea815a25806b", size = 605323, upload-time = "2025-10-31T07:18:17.466Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/bf/945d527ff706233636c73880b22c7c953f3faeb9d6c7e2e85bfbfd0134a0/trio-0.32.0-py3-none-any.whl", hash = "sha256:4ab65984ef8370b79a76659ec87aa3a30c5c7c83ff250b4de88c29a8ab6123c5", size = 512030, upload-time = "2025-10-31T07:18:15.885Z" }, +] + +[[package]] +name = "typer" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "click", version = "8.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c5/8c/7d682431efca5fd290017663ea4588bf6f2c6aad085c7f108c5dbc316e70/typer-0.16.0.tar.gz", hash = "sha256:af377ffaee1dbe37ae9440cb4e8f11686ea5ce4e9bae01b84ae7c63b87f1dd3b", size = 102625, upload-time = "2025-05-26T14:30:31.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/42/3efaf858001d2c2913de7f354563e3a3a2f0decae3efe98427125a8f441e/typer-0.16.0-py3-none-any.whl", hash = "sha256:1f79bed11d4d02d4310e3c1b7ba594183bcedb0ac73b27a9e5f28f6fb5b98855", size = 46317, upload-time = "2025-05-26T14:30:30.523Z" }, +] + +[[package]] +name = "typer-slim" +version = "0.21.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "click", version = "8.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/17/d4/064570dec6358aa9049d4708e4a10407d74c99258f8b2136bb8702303f1a/typer_slim-0.21.1.tar.gz", hash = "sha256:73495dd08c2d0940d611c5a8c04e91c2a0a98600cbd4ee19192255a233b6dbfd", size = 110478, upload-time = "2026-01-06T11:21:11.176Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/0a/4aca634faf693e33004796b6cee0ae2e1dba375a800c16ab8d3eff4bb800/typer_slim-0.21.1-py3-none-any.whl", hash = "sha256:6e6c31047f171ac93cc5a973c9e617dbc5ab2bddc4d0a3135dc161b4e2020e0d", size = 47444, upload-time = "2026-01-06T11:21:12.441Z" }, +] + +[[package]] +name = "types-orjson" +version = "3.6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/97/3f78cfdf663e5668e8b490d8c84d6de089d2d8dbad935f0dc43555d52a90/types-orjson-3.6.2.tar.gz", hash = "sha256:cf9afcc79a86325c7aff251790338109ed6f6b1bab09d2d4262dd18c85a3c638", size = 1999, upload-time = "2022-01-07T11:31:10.518Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/84/b34abd2d08381c5113e475908a1d79d27dc9a15f669213cee4ca03d1a891/types_orjson-3.6.2-py3-none-any.whl", hash = "sha256:22ee9a79236b6b0bfb35a0684eded62ad930a88a56797fa3c449b026cf7dbfe4", size = 2224, upload-time = "2022-01-07T11:31:09.271Z" }, +] + +[[package]] +name = "types-requests" +version = "2.31.0.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "types-urllib3", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f9/b8/c1e8d39996b4929b918aba10dba5de07a8b3f4c8487bb61bb79882544e69/types-requests-2.31.0.6.tar.gz", hash = "sha256:cd74ce3b53c461f1228a9b783929ac73a666658f223e28ed29753771477b3bd0", size = 15535, upload-time = "2023-09-27T06:19:38.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/a1/6f8dc74d9069e790d604ddae70cb46dcbac668f1bb08136e7b0f2f5cd3bf/types_requests-2.31.0.6-py3-none-any.whl", hash = "sha256:a2db9cb228a81da8348b49ad6db3f5519452dd20a9c1e1a868c83c5fe88fd1a9", size = 14516, upload-time = "2023-09-27T06:19:36.373Z" }, +] + +[[package]] +name = "types-requests" +version = "2.32.4.20260107" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.10' and python_full_version < '3.14'", +] +dependencies = [ + { name = "urllib3", version = "2.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/f3/a0663907082280664d745929205a89d41dffb29e89a50f753af7d57d0a96/types_requests-2.32.4.20260107.tar.gz", hash = "sha256:018a11ac158f801bfa84857ddec1650750e393df8a004a8a9ae2a9bec6fcb24f", size = 23165, upload-time = "2026-01-07T03:20:54.091Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/12/709ea261f2bf91ef0a26a9eed20f2623227a8ed85610c1e54c5805692ecb/types_requests-2.32.4.20260107-py3-none-any.whl", hash = "sha256:b703fe72f8ce5b31ef031264fe9395cac8f46a04661a79f7ed31a80fb308730d", size = 20676, upload-time = "2026-01-07T03:20:52.929Z" }, +] + +[[package]] +name = "types-ujson" +version = "5.10.0.20240515" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/12/49/abb4bcb9f2258f785edbf236b517c3e7ba8a503a8cbce6b5895930586cc0/types-ujson-5.10.0.20240515.tar.gz", hash = "sha256:ceae7127f0dafe4af5dd0ecf98ee13e9d75951ef963b5c5a9b7ea92e0d71f0d7", size = 3571, upload-time = "2024-05-15T02:24:43.704Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/1f/9d018cee3d09ab44a5211f0b5ed9b0422ad9a8c226bf3967f5884498d8f0/types_ujson-5.10.0.20240515-py3-none-any.whl", hash = "sha256:02bafc36b3a93d2511757a64ff88bd505e0a57fba08183a9150fbcfcb2015310", size = 2757, upload-time = "2024-05-15T02:24:42.315Z" }, +] + +[[package]] +name = "types-urllib3" +version = "1.26.25.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/73/de/b9d7a68ad39092368fb21dd6194b362b98a1daeea5dcfef5e1adb5031c7e/types-urllib3-1.26.25.14.tar.gz", hash = "sha256:229b7f577c951b8c1b92c1bc2b2fdb0b49847bd2af6d1cc2a2e3dd340f3bda8f", size = 11239, upload-time = "2023-07-20T15:19:31.307Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/7b/3fc711b2efea5e85a7a0bbfe269ea944aa767bbba5ec52f9ee45d362ccf3/types_urllib3-1.26.25.14-py3-none-any.whl", hash = "sha256:9683bbb7fb72e32bfe9d2be6e04875fbe1b3eeec3cbb4ea231435aa7fd6b4f0e", size = 15377, upload-time = "2023-07-20T15:19:30.379Z" }, +] + +[[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 = "ujson" +version = "5.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/d9/3f17e3c5773fb4941c68d9a37a47b1a79c9649d6c56aefbed87cc409d18a/ujson-5.11.0.tar.gz", hash = "sha256:e204ae6f909f099ba6b6b942131cee359ddda2b6e4ea39c12eb8b991fe2010e0", size = 7156583, upload-time = "2025-08-20T11:57:02.452Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/0c/8bf7a4fabfd01c7eed92d9b290930ce6d14910dec708e73538baa38885d1/ujson-5.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:446e8c11c06048611c9d29ef1237065de0af07cabdd97e6b5b527b957692ec25", size = 55248, upload-time = "2025-08-20T11:55:02.368Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2e/eeab0b8b641817031ede4f790db4c4942df44a12f44d72b3954f39c6a115/ujson-5.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:16ccb973b7ada0455201808ff11d48fe9c3f034a6ab5bd93b944443c88299f89", size = 53157, upload-time = "2025-08-20T11:55:04.012Z" }, + { url = "https://files.pythonhosted.org/packages/21/1b/a4e7a41870797633423ea79618526747353fd7be9191f3acfbdee0bf264b/ujson-5.11.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3134b783ab314d2298d58cda7e47e7a0f7f71fc6ade6ac86d5dbeaf4b9770fa6", size = 57657, upload-time = "2025-08-20T11:55:05.169Z" }, + { url = "https://files.pythonhosted.org/packages/94/ae/4e0d91b8f6db7c9b76423b3649612189506d5a06ddd3b6334b6d37f77a01/ujson-5.11.0-cp310-cp310-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:185f93ebccffebc8baf8302c869fac70dd5dd78694f3b875d03a31b03b062cdb", size = 59780, upload-time = "2025-08-20T11:55:06.325Z" }, + { url = "https://files.pythonhosted.org/packages/b3/cc/46b124c2697ca2da7c65c4931ed3cb670646978157aa57a7a60f741c530f/ujson-5.11.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d06e87eded62ff0e5f5178c916337d2262fdbc03b31688142a3433eabb6511db", size = 57307, upload-time = "2025-08-20T11:55:07.493Z" }, + { url = "https://files.pythonhosted.org/packages/39/eb/20dd1282bc85dede2f1c62c45b4040bc4c389c80a05983515ab99771bca7/ujson-5.11.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:181fb5b15703a8b9370b25345d2a1fd1359f0f18776b3643d24e13ed9c036d4c", size = 1036369, upload-time = "2025-08-20T11:55:09.192Z" }, + { url = "https://files.pythonhosted.org/packages/64/a2/80072439065d493e3a4b1fbeec991724419a1b4c232e2d1147d257cac193/ujson-5.11.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a4df61a6df0a4a8eb5b9b1ffd673429811f50b235539dac586bb7e9e91994138", size = 1195738, upload-time = "2025-08-20T11:55:11.402Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7e/d77f9e9c039d58299c350c978e086a804d1fceae4fd4a1cc6e8d0133f838/ujson-5.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6eff24e1abd79e0ec6d7eae651dd675ddbc41f9e43e29ef81e16b421da896915", size = 1088718, upload-time = "2025-08-20T11:55:13.297Z" }, + { url = "https://files.pythonhosted.org/packages/ab/f1/697559d45acc849cada6b3571d53522951b1a64027400507aabc6a710178/ujson-5.11.0-cp310-cp310-win32.whl", hash = "sha256:30f607c70091483550fbd669a0b37471e5165b317d6c16e75dba2aa967608723", size = 39653, upload-time = "2025-08-20T11:55:14.869Z" }, + { url = "https://files.pythonhosted.org/packages/86/a2/70b73a0f55abe0e6b8046d365d74230c20c5691373e6902a599b2dc79ba1/ujson-5.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:3d2720e9785f84312b8e2cb0c2b87f1a0b1c53aaab3b2af3ab817d54409012e0", size = 43720, upload-time = "2025-08-20T11:55:15.897Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5f/b19104afa455630b43efcad3a24495b9c635d92aa8f2da4f30e375deb1a2/ujson-5.11.0-cp310-cp310-win_arm64.whl", hash = "sha256:85e6796631165f719084a9af00c79195d3ebf108151452fefdcb1c8bb50f0105", size = 38410, upload-time = "2025-08-20T11:55:17.556Z" }, + { url = "https://files.pythonhosted.org/packages/da/ea/80346b826349d60ca4d612a47cdf3533694e49b45e9d1c07071bb867a184/ujson-5.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d7c46cb0fe5e7056b9acb748a4c35aa1b428025853032540bb7e41f46767321f", size = 55248, upload-time = "2025-08-20T11:55:19.033Z" }, + { url = "https://files.pythonhosted.org/packages/57/df/b53e747562c89515e18156513cc7c8ced2e5e3fd6c654acaa8752ffd7cd9/ujson-5.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d8951bb7a505ab2a700e26f691bdfacf395bc7e3111e3416d325b513eea03a58", size = 53156, upload-time = "2025-08-20T11:55:20.174Z" }, + { url = "https://files.pythonhosted.org/packages/41/b8/ab67ec8c01b8a3721fd13e5cb9d85ab2a6066a3a5e9148d661a6870d6293/ujson-5.11.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:952c0be400229940248c0f5356514123d428cba1946af6fa2bbd7503395fef26", size = 57657, upload-time = "2025-08-20T11:55:21.296Z" }, + { url = "https://files.pythonhosted.org/packages/7b/c7/fb84f27cd80a2c7e2d3c6012367aecade0da936790429801803fa8d4bffc/ujson-5.11.0-cp311-cp311-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:94fcae844f1e302f6f8095c5d1c45a2f0bfb928cccf9f1b99e3ace634b980a2a", size = 59779, upload-time = "2025-08-20T11:55:22.772Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7c/48706f7c1e917ecb97ddcfb7b1d756040b86ed38290e28579d63bd3fcc48/ujson-5.11.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e0ec1646db172beb8d3df4c32a9d78015e671d2000af548252769e33079d9a6", size = 57284, upload-time = "2025-08-20T11:55:24.01Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ce/48877c6eb4afddfd6bd1db6be34456538c07ca2d6ed233d3f6c6efc2efe8/ujson-5.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:da473b23e3a54448b008d33f742bcd6d5fb2a897e42d1fc6e7bf306ea5d18b1b", size = 1036395, upload-time = "2025-08-20T11:55:25.725Z" }, + { url = "https://files.pythonhosted.org/packages/8b/7a/2c20dc97ad70cd7c31ad0596ba8e2cf8794d77191ba4d1e0bded69865477/ujson-5.11.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:aa6b3d4f1c0d3f82930f4cbd7fe46d905a4a9205a7c13279789c1263faf06dba", size = 1195731, upload-time = "2025-08-20T11:55:27.915Z" }, + { url = "https://files.pythonhosted.org/packages/15/f5/ca454f2f6a2c840394b6f162fff2801450803f4ff56c7af8ce37640b8a2a/ujson-5.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4843f3ab4fe1cc596bb7e02228ef4c25d35b4bb0809d6a260852a4bfcab37ba3", size = 1088710, upload-time = "2025-08-20T11:55:29.426Z" }, + { url = "https://files.pythonhosted.org/packages/fe/d3/9ba310e07969bc9906eb7548731e33a0f448b122ad9705fed699c9b29345/ujson-5.11.0-cp311-cp311-win32.whl", hash = "sha256:e979fbc469a7f77f04ec2f4e853ba00c441bf2b06720aa259f0f720561335e34", size = 39648, upload-time = "2025-08-20T11:55:31.194Z" }, + { url = "https://files.pythonhosted.org/packages/57/f7/da05b4a8819f1360be9e71fb20182f0bb3ec611a36c3f213f4d20709e099/ujson-5.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:683f57f0dd3acdd7d9aff1de0528d603aafcb0e6d126e3dc7ce8b020a28f5d01", size = 43717, upload-time = "2025-08-20T11:55:32.241Z" }, + { url = "https://files.pythonhosted.org/packages/9a/cc/f3f9ac0f24f00a623a48d97dc3814df5c2dc368cfb00031aa4141527a24b/ujson-5.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:7855ccea3f8dad5e66d8445d754fc1cf80265a4272b5f8059ebc7ec29b8d0835", size = 38402, upload-time = "2025-08-20T11:55:33.641Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ef/a9cb1fce38f699123ff012161599fb9f2ff3f8d482b4b18c43a2dc35073f/ujson-5.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7895f0d2d53bd6aea11743bd56e3cb82d729980636cd0ed9b89418bf66591702", size = 55434, upload-time = "2025-08-20T11:55:34.987Z" }, + { url = "https://files.pythonhosted.org/packages/b1/05/dba51a00eb30bd947791b173766cbed3492269c150a7771d2750000c965f/ujson-5.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12b5e7e22a1fe01058000d1b317d3b65cc3daf61bd2ea7a2b76721fe160fa74d", size = 53190, upload-time = "2025-08-20T11:55:36.384Z" }, + { url = "https://files.pythonhosted.org/packages/03/3c/fd11a224f73fbffa299fb9644e425f38b38b30231f7923a088dd513aabb4/ujson-5.11.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0180a480a7d099082501cad1fe85252e4d4bf926b40960fb3d9e87a3a6fbbc80", size = 57600, upload-time = "2025-08-20T11:55:37.692Z" }, + { url = "https://files.pythonhosted.org/packages/55/b9/405103cae24899df688a3431c776e00528bd4799e7d68820e7ebcf824f92/ujson-5.11.0-cp312-cp312-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:fa79fdb47701942c2132a9dd2297a1a85941d966d8c87bfd9e29b0cf423f26cc", size = 59791, upload-time = "2025-08-20T11:55:38.877Z" }, + { url = "https://files.pythonhosted.org/packages/17/7b/2dcbc2bbfdbf68f2368fb21ab0f6735e872290bb604c75f6e06b81edcb3f/ujson-5.11.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8254e858437c00f17cb72e7a644fc42dad0ebb21ea981b71df6e84b1072aaa7c", size = 57356, upload-time = "2025-08-20T11:55:40.036Z" }, + { url = "https://files.pythonhosted.org/packages/d1/71/fea2ca18986a366c750767b694430d5ded6b20b6985fddca72f74af38a4c/ujson-5.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1aa8a2ab482f09f6c10fba37112af5f957689a79ea598399c85009f2f29898b5", size = 1036313, upload-time = "2025-08-20T11:55:41.408Z" }, + { url = "https://files.pythonhosted.org/packages/a3/bb/d4220bd7532eac6288d8115db51710fa2d7d271250797b0bfba9f1e755af/ujson-5.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a638425d3c6eed0318df663df44480f4a40dc87cc7c6da44d221418312f6413b", size = 1195782, upload-time = "2025-08-20T11:55:43.357Z" }, + { url = "https://files.pythonhosted.org/packages/80/47/226e540aa38878ce1194454385701d82df538ccb5ff8db2cf1641dde849a/ujson-5.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7e3cff632c1d78023b15f7e3a81c3745cd3f94c044d1e8fa8efbd6b161997bbc", size = 1088817, upload-time = "2025-08-20T11:55:45.262Z" }, + { url = "https://files.pythonhosted.org/packages/7e/81/546042f0b23c9040d61d46ea5ca76f0cc5e0d399180ddfb2ae976ebff5b5/ujson-5.11.0-cp312-cp312-win32.whl", hash = "sha256:be6b0eaf92cae8cdee4d4c9e074bde43ef1c590ed5ba037ea26c9632fb479c88", size = 39757, upload-time = "2025-08-20T11:55:46.522Z" }, + { url = "https://files.pythonhosted.org/packages/44/1b/27c05dc8c9728f44875d74b5bfa948ce91f6c33349232619279f35c6e817/ujson-5.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:b7b136cc6abc7619124fd897ef75f8e63105298b5ca9bdf43ebd0e1fa0ee105f", size = 43859, upload-time = "2025-08-20T11:55:47.987Z" }, + { url = "https://files.pythonhosted.org/packages/22/2d/37b6557c97c3409c202c838aa9c960ca3896843b4295c4b7bb2bbd260664/ujson-5.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:6cd2df62f24c506a0ba322d5e4fe4466d47a9467b57e881ee15a31f7ecf68ff6", size = 38361, upload-time = "2025-08-20T11:55:49.122Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ec/2de9dd371d52c377abc05d2b725645326c4562fc87296a8907c7bcdf2db7/ujson-5.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:109f59885041b14ee9569bf0bb3f98579c3fa0652317b355669939e5fc5ede53", size = 55435, upload-time = "2025-08-20T11:55:50.243Z" }, + { url = "https://files.pythonhosted.org/packages/5b/a4/f611f816eac3a581d8a4372f6967c3ed41eddbae4008d1d77f223f1a4e0a/ujson-5.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a31c6b8004438e8c20fc55ac1c0e07dad42941db24176fe9acf2815971f8e752", size = 53193, upload-time = "2025-08-20T11:55:51.373Z" }, + { url = "https://files.pythonhosted.org/packages/e9/c5/c161940967184de96f5cbbbcce45b562a4bf851d60f4c677704b1770136d/ujson-5.11.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78c684fb21255b9b90320ba7e199780f653e03f6c2528663768965f4126a5b50", size = 57603, upload-time = "2025-08-20T11:55:52.583Z" }, + { url = "https://files.pythonhosted.org/packages/2b/d6/c7b2444238f5b2e2d0e3dab300b9ddc3606e4b1f0e4bed5a48157cebc792/ujson-5.11.0-cp313-cp313-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:4c9f5d6a27d035dd90a146f7761c2272cf7103de5127c9ab9c4cd39ea61e878a", size = 59794, upload-time = "2025-08-20T11:55:53.69Z" }, + { url = "https://files.pythonhosted.org/packages/fe/a3/292551f936d3d02d9af148f53e1bc04306b00a7cf1fcbb86fa0d1c887242/ujson-5.11.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:837da4d27fed5fdc1b630bd18f519744b23a0b5ada1bbde1a36ba463f2900c03", size = 57363, upload-time = "2025-08-20T11:55:54.843Z" }, + { url = "https://files.pythonhosted.org/packages/90/a6/82cfa70448831b1a9e73f882225980b5c689bf539ec6400b31656a60ea46/ujson-5.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:787aff4a84da301b7f3bac09bc696e2e5670df829c6f8ecf39916b4e7e24e701", size = 1036311, upload-time = "2025-08-20T11:55:56.197Z" }, + { url = "https://files.pythonhosted.org/packages/84/5c/96e2266be50f21e9b27acaee8ca8f23ea0b85cb998c33d4f53147687839b/ujson-5.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6dd703c3e86dc6f7044c5ac0b3ae079ed96bf297974598116aa5fb7f655c3a60", size = 1195783, upload-time = "2025-08-20T11:55:58.081Z" }, + { url = "https://files.pythonhosted.org/packages/8d/20/78abe3d808cf3bb3e76f71fca46cd208317bf461c905d79f0d26b9df20f1/ujson-5.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3772e4fe6b0c1e025ba3c50841a0ca4786825a4894c8411bf8d3afe3a8061328", size = 1088822, upload-time = "2025-08-20T11:55:59.469Z" }, + { url = "https://files.pythonhosted.org/packages/d8/50/8856e24bec5e2fc7f775d867aeb7a3f137359356200ac44658f1f2c834b2/ujson-5.11.0-cp313-cp313-win32.whl", hash = "sha256:8fa2af7c1459204b7a42e98263b069bd535ea0cd978b4d6982f35af5a04a4241", size = 39753, upload-time = "2025-08-20T11:56:01.345Z" }, + { url = "https://files.pythonhosted.org/packages/5b/d8/1baee0f4179a4d0f5ce086832147b6cc9b7731c24ca08e14a3fdb8d39c32/ujson-5.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:34032aeca4510a7c7102bd5933f59a37f63891f30a0706fb46487ab6f0edf8f0", size = 43866, upload-time = "2025-08-20T11:56:02.552Z" }, + { url = "https://files.pythonhosted.org/packages/a9/8c/6d85ef5be82c6d66adced3ec5ef23353ed710a11f70b0b6a836878396334/ujson-5.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:ce076f2df2e1aa62b685086fbad67f2b1d3048369664b4cdccc50707325401f9", size = 38363, upload-time = "2025-08-20T11:56:03.688Z" }, + { url = "https://files.pythonhosted.org/packages/28/08/4518146f4984d112764b1dfa6fb7bad691c44a401adadaa5e23ccd930053/ujson-5.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:65724738c73645db88f70ba1f2e6fb678f913281804d5da2fd02c8c5839af302", size = 55462, upload-time = "2025-08-20T11:56:04.873Z" }, + { url = "https://files.pythonhosted.org/packages/29/37/2107b9a62168867a692654d8766b81bd2fd1e1ba13e2ec90555861e02b0c/ujson-5.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29113c003ca33ab71b1b480bde952fbab2a0b6b03a4ee4c3d71687cdcbd1a29d", size = 53246, upload-time = "2025-08-20T11:56:06.054Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f8/25583c70f83788edbe3ca62ce6c1b79eff465d78dec5eb2b2b56b3e98b33/ujson-5.11.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c44c703842024d796b4c78542a6fcd5c3cb948b9fc2a73ee65b9c86a22ee3638", size = 57631, upload-time = "2025-08-20T11:56:07.374Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ca/19b3a632933a09d696f10dc1b0dfa1d692e65ad507d12340116ce4f67967/ujson-5.11.0-cp314-cp314-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:e750c436fb90edf85585f5c62a35b35082502383840962c6983403d1bd96a02c", size = 59877, upload-time = "2025-08-20T11:56:08.534Z" }, + { url = "https://files.pythonhosted.org/packages/55/7a/4572af5324ad4b2bfdd2321e898a527050290147b4ea337a79a0e4e87ec7/ujson-5.11.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f278b31a7c52eb0947b2db55a5133fbc46b6f0ef49972cd1a80843b72e135aba", size = 57363, upload-time = "2025-08-20T11:56:09.758Z" }, + { url = "https://files.pythonhosted.org/packages/7b/71/a2b8c19cf4e1efe53cf439cdf7198ac60ae15471d2f1040b490c1f0f831f/ujson-5.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ab2cb8351d976e788669c8281465d44d4e94413718af497b4e7342d7b2f78018", size = 1036394, upload-time = "2025-08-20T11:56:11.168Z" }, + { url = "https://files.pythonhosted.org/packages/7a/3e/7b98668cba3bb3735929c31b999b374ebc02c19dfa98dfebaeeb5c8597ca/ujson-5.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:090b4d11b380ae25453100b722d0609d5051ffe98f80ec52853ccf8249dfd840", size = 1195837, upload-time = "2025-08-20T11:56:12.6Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ea/8870f208c20b43571a5c409ebb2fe9b9dba5f494e9e60f9314ac01ea8f78/ujson-5.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:80017e870d882d5517d28995b62e4e518a894f932f1e242cbc802a2fd64d365c", size = 1088837, upload-time = "2025-08-20T11:56:14.15Z" }, + { url = "https://files.pythonhosted.org/packages/63/b6/c0e6607e37fa47929920a685a968c6b990a802dec65e9c5181e97845985d/ujson-5.11.0-cp314-cp314-win32.whl", hash = "sha256:1d663b96eb34c93392e9caae19c099ec4133ba21654b081956613327f0e973ac", size = 41022, upload-time = "2025-08-20T11:56:15.509Z" }, + { url = "https://files.pythonhosted.org/packages/4e/56/f4fe86b4c9000affd63e9219e59b222dc48b01c534533093e798bf617a7e/ujson-5.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:849e65b696f0d242833f1df4182096cedc50d414215d1371fca85c541fbff629", size = 45111, upload-time = "2025-08-20T11:56:16.597Z" }, + { url = "https://files.pythonhosted.org/packages/0a/f3/669437f0280308db4783b12a6d88c00730b394327d8334cc7a32ef218e64/ujson-5.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:e73df8648c9470af2b6a6bf5250d4744ad2cf3d774dcf8c6e31f018bdd04d764", size = 39682, upload-time = "2025-08-20T11:56:17.763Z" }, + { url = "https://files.pythonhosted.org/packages/6e/cd/e9809b064a89fe5c4184649adeb13c1b98652db3f8518980b04227358574/ujson-5.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:de6e88f62796372fba1de973c11138f197d3e0e1d80bcb2b8aae1e826096d433", size = 55759, upload-time = "2025-08-20T11:56:18.882Z" }, + { url = "https://files.pythonhosted.org/packages/1b/be/ae26a6321179ebbb3a2e2685b9007c71bcda41ad7a77bbbe164005e956fc/ujson-5.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:49e56ef8066f11b80d620985ae36869a3ff7e4b74c3b6129182ec5d1df0255f3", size = 53634, upload-time = "2025-08-20T11:56:20.012Z" }, + { url = "https://files.pythonhosted.org/packages/ae/e9/fb4a220ee6939db099f4cfeeae796ecb91e7584ad4d445d4ca7f994a9135/ujson-5.11.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1a325fd2c3a056cf6c8e023f74a0c478dd282a93141356ae7f16d5309f5ff823", size = 58547, upload-time = "2025-08-20T11:56:21.175Z" }, + { url = "https://files.pythonhosted.org/packages/bd/f8/fc4b952b8f5fea09ea3397a0bd0ad019e474b204cabcb947cead5d4d1ffc/ujson-5.11.0-cp314-cp314t-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:a0af6574fc1d9d53f4ff371f58c96673e6d988ed2b5bf666a6143c782fa007e9", size = 60489, upload-time = "2025-08-20T11:56:22.342Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e5/af5491dfda4f8b77e24cf3da68ee0d1552f99a13e5c622f4cef1380925c3/ujson-5.11.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10f29e71ecf4ecd93a6610bd8efa8e7b6467454a363c3d6416db65de883eb076", size = 58035, upload-time = "2025-08-20T11:56:23.92Z" }, + { url = "https://files.pythonhosted.org/packages/c4/09/0945349dd41f25cc8c38d78ace49f14c5052c5bbb7257d2f466fa7bdb533/ujson-5.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1a0a9b76a89827a592656fe12e000cf4f12da9692f51a841a4a07aa4c7ecc41c", size = 1037212, upload-time = "2025-08-20T11:56:25.274Z" }, + { url = "https://files.pythonhosted.org/packages/49/44/8e04496acb3d5a1cbee3a54828d9652f67a37523efa3d3b18a347339680a/ujson-5.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b16930f6a0753cdc7d637b33b4e8f10d5e351e1fb83872ba6375f1e87be39746", size = 1196500, upload-time = "2025-08-20T11:56:27.517Z" }, + { url = "https://files.pythonhosted.org/packages/64/ae/4bc825860d679a0f208a19af2f39206dfd804ace2403330fdc3170334a2f/ujson-5.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:04c41afc195fd477a59db3a84d5b83a871bd648ef371cf8c6f43072d89144eef", size = 1089487, upload-time = "2025-08-20T11:56:29.07Z" }, + { url = "https://files.pythonhosted.org/packages/30/ed/5a057199fb0a5deabe0957073a1c1c1c02a3e99476cd03daee98ea21fa57/ujson-5.11.0-cp314-cp314t-win32.whl", hash = "sha256:aa6d7a5e09217ff93234e050e3e380da62b084e26b9f2e277d2606406a2fc2e5", size = 41859, upload-time = "2025-08-20T11:56:30.495Z" }, + { url = "https://files.pythonhosted.org/packages/aa/03/b19c6176bdf1dc13ed84b886e99677a52764861b6cc023d5e7b6ebda249d/ujson-5.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:48055e1061c1bb1f79e75b4ac39e821f3f35a9b82de17fce92c3140149009bec", size = 46183, upload-time = "2025-08-20T11:56:31.574Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ca/a0413a3874b2dc1708b8796ca895bf363292f9c70b2e8ca482b7dbc0259d/ujson-5.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:1194b943e951092db611011cb8dbdb6cf94a3b816ed07906e14d3bc6ce0e90ab", size = 40264, upload-time = "2025-08-20T11:56:32.773Z" }, + { url = "https://files.pythonhosted.org/packages/39/bf/c6f59cdf74ce70bd937b97c31c42fd04a5ed1a9222d0197e77e4bd899841/ujson-5.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:65f3c279f4ed4bf9131b11972040200c66ae040368abdbb21596bf1564899694", size = 55283, upload-time = "2025-08-20T11:56:33.947Z" }, + { url = "https://files.pythonhosted.org/packages/8d/c1/a52d55638c0c644b8a63059f95ad5ffcb4ad8f60d8bc3e8680f78e77cc75/ujson-5.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:99c49400572cd77050894e16864a335225191fd72a818ea6423ae1a06467beac", size = 53168, upload-time = "2025-08-20T11:56:35.141Z" }, + { url = "https://files.pythonhosted.org/packages/75/6c/e64e19a01d59c8187d01ffc752ee3792a09f5edaaac2a0402de004459dd7/ujson-5.11.0-cp39-cp39-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0654a2691fc252c3c525e3d034bb27b8a7546c9d3eb33cd29ce6c9feda361a6a", size = 57809, upload-time = "2025-08-20T11:56:36.293Z" }, + { url = "https://files.pythonhosted.org/packages/9f/36/910117b7a8a1c188396f6194ca7bc8fd75e376d8f7e3cf5eb6219fc8b09d/ujson-5.11.0-cp39-cp39-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:6b6ec7e7321d7fc19abdda3ad809baef935f49673951a8bab486aea975007e02", size = 59797, upload-time = "2025-08-20T11:56:37.746Z" }, + { url = "https://files.pythonhosted.org/packages/c7/17/bcc85d282ee2f4cdef5f577e0a43533eedcae29cc6405edf8c62a7a50368/ujson-5.11.0-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f62b9976fabbcde3ab6e413f4ec2ff017749819a0786d84d7510171109f2d53c", size = 57378, upload-time = "2025-08-20T11:56:39.123Z" }, + { url = "https://files.pythonhosted.org/packages/ef/39/120bb76441bf835f3c3f42db9c206f31ba875711637a52a8209949ab04b0/ujson-5.11.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7f1a27ab91083b4770e160d17f61b407f587548f2c2b5fbf19f94794c495594a", size = 1036515, upload-time = "2025-08-20T11:56:40.848Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ae/fe1b4ff6388f681f6710e9494656957725b1e73ae50421ec04567df9fb75/ujson-5.11.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ecd6ff8a3b5a90c292c2396c2d63c687fd0ecdf17de390d852524393cd9ed052", size = 1195753, upload-time = "2025-08-20T11:56:42.341Z" }, + { url = "https://files.pythonhosted.org/packages/92/20/005b93f2cf846ae50b46812fcf24bbdd127521197e5f1e1a82e3b3e730a1/ujson-5.11.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9aacbeb23fdbc4b256a7d12e0beb9063a1ba5d9e0dbb2cfe16357c98b4334596", size = 1088844, upload-time = "2025-08-20T11:56:43.777Z" }, + { url = "https://files.pythonhosted.org/packages/41/9e/3142023c30008e2b24d7368a389b26d28d62fcd3f596d3d898a72dd09173/ujson-5.11.0-cp39-cp39-win32.whl", hash = "sha256:674f306e3e6089f92b126eb2fe41bcb65e42a15432c143365c729fdb50518547", size = 39652, upload-time = "2025-08-20T11:56:45.034Z" }, + { url = "https://files.pythonhosted.org/packages/ca/89/f4de0a3c485d0163f85f552886251876645fb62cbbe24fcdc0874b9fae03/ujson-5.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:c6618f480f7c9ded05e78a1938873fde68baf96cdd74e6d23c7e0a8441175c4b", size = 43783, upload-time = "2025-08-20T11:56:46.156Z" }, + { url = "https://files.pythonhosted.org/packages/48/b1/2d50987a7b7cccb5c1fbe9ae7b184211106237b32c7039118c41d79632ea/ujson-5.11.0-cp39-cp39-win_arm64.whl", hash = "sha256:5600202a731af24a25e2d7b6eb3f648e4ecd4bb67c4d5cf12f8fab31677469c9", size = 38430, upload-time = "2025-08-20T11:56:47.653Z" }, + { url = "https://files.pythonhosted.org/packages/50/17/30275aa2933430d8c0c4ead951cc4fdb922f575a349aa0b48a6f35449e97/ujson-5.11.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:abae0fb58cc820092a0e9e8ba0051ac4583958495bfa5262a12f628249e3b362", size = 51206, upload-time = "2025-08-20T11:56:48.797Z" }, + { url = "https://files.pythonhosted.org/packages/c3/15/42b3924258eac2551f8f33fa4e35da20a06a53857ccf3d4deb5e5d7c0b6c/ujson-5.11.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fac6c0649d6b7c3682a0a6e18d3de6857977378dce8d419f57a0b20e3d775b39", size = 48907, upload-time = "2025-08-20T11:56:50.136Z" }, + { url = "https://files.pythonhosted.org/packages/94/7e/0519ff7955aba581d1fe1fb1ca0e452471250455d182f686db5ac9e46119/ujson-5.11.0-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b42c115c7c6012506e8168315150d1e3f76e7ba0f4f95616f4ee599a1372bbc", size = 50319, upload-time = "2025-08-20T11:56:51.63Z" }, + { url = "https://files.pythonhosted.org/packages/74/cf/209d90506b7d6c5873f82c5a226d7aad1a1da153364e9ebf61eff0740c33/ujson-5.11.0-pp311-pypy311_pp73-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:86baf341d90b566d61a394869ce77188cc8668f76d7bb2c311d77a00f4bdf844", size = 56584, upload-time = "2025-08-20T11:56:52.89Z" }, + { url = "https://files.pythonhosted.org/packages/e9/97/bd939bb76943cb0e1d2b692d7e68629f51c711ef60425fa5bb6968037ecd/ujson-5.11.0-pp311-pypy311_pp73-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4598bf3965fc1a936bd84034312bcbe00ba87880ef1ee33e33c1e88f2c398b49", size = 51588, upload-time = "2025-08-20T11:56:54.054Z" }, + { url = "https://files.pythonhosted.org/packages/52/5b/8c5e33228f7f83f05719964db59f3f9f276d272dc43752fa3bbf0df53e7b/ujson-5.11.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:416389ec19ef5f2013592f791486bef712ebce0cd59299bf9df1ba40bb2f6e04", size = 43835, upload-time = "2025-08-20T11:56:55.237Z" }, +] + +[[package]] +name = "urllib3" +version = "1.26.20" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/e8/6ff5e6bc22095cfc59b6ea711b687e2b7ed4bdb373f7eeec370a97d7392f/urllib3-1.26.20.tar.gz", hash = "sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32", size = 307380, upload-time = "2024-08-29T15:43:11.37Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/cf/8435d5a7159e2a9c83a95896ed596f68cf798005fe107cc655b5c5c14704/urllib3-1.26.20-py2.py3-none-any.whl", hash = "sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e", size = 144225, upload-time = "2024-08-29T15:43:08.921Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.10' and python_full_version < '3.14'", +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.39.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "h11", marker = "python_full_version < '3.10'" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/4f/f9fdac7cf6dd79790eb165639b5c452ceeabc7bbabbba4569155470a287d/uvicorn-0.39.0.tar.gz", hash = "sha256:610512b19baa93423d2892d7823741f6d27717b642c8964000d7194dded19302", size = 82001, upload-time = "2025-12-21T13:05:17.973Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/25/db2b1c6c35bf22e17fe5412d2ee5d3fd7a20d07ebc9dac8b58f7db2e23a0/uvicorn-0.39.0-py3-none-any.whl", hash = "sha256:7beec21bd2693562b386285b188a7963b06853c0d006302b3e4cfed950c9929a", size = 68491, upload-time = "2025-12-21T13:05:16.291Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, + { name = "httptools", marker = "python_full_version < '3.10'" }, + { name = "python-dotenv", marker = "python_full_version < '3.10'" }, + { name = "pyyaml", marker = "python_full_version < '3.10'" }, + { name = "uvloop", marker = "python_full_version < '3.10' and platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles", marker = "python_full_version < '3.10'" }, + { name = "websockets", marker = "python_full_version < '3.10'" }, +] + +[[package]] +name = "uvicorn" +version = "0.40.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.10' and python_full_version < '3.14'", +] +dependencies = [ + { name = "click", version = "8.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "h11", marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/d1/8f3c683c9561a4e6689dd3b1d345c815f10f86acd044ee1fb9a4dcd0b8c5/uvicorn-0.40.0.tar.gz", hash = "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea", size = 81761, upload-time = "2025-12-21T14:16:22.45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee", size = 68502, upload-time = "2025-12-21T14:16:21.041Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, + { name = "httptools", marker = "python_full_version >= '3.10'" }, + { name = "python-dotenv", marker = "python_full_version >= '3.10'" }, + { name = "pyyaml", marker = "python_full_version >= '3.10'" }, + { name = "uvloop", marker = "python_full_version >= '3.10' and platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles", marker = "python_full_version >= '3.10'" }, + { name = "websockets", marker = "python_full_version >= '3.10'" }, +] + +[[package]] +name = "uvloop" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/14/ecceb239b65adaaf7fde510aa8bd534075695d1e5f8dadfa32b5723d9cfb/uvloop-0.22.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ef6f0d4cc8a9fa1f6a910230cd53545d9a14479311e87e3cb225495952eb672c", size = 1343335, upload-time = "2025-10-16T22:16:11.43Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ae/6f6f9af7f590b319c94532b9567409ba11f4fa71af1148cab1bf48a07048/uvloop-0.22.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7cd375a12b71d33d46af85a3343b35d98e8116134ba404bd657b3b1d15988792", size = 742903, upload-time = "2025-10-16T22:16:12.979Z" }, + { url = "https://files.pythonhosted.org/packages/09/bd/3667151ad0702282a1f4d5d29288fce8a13c8b6858bf0978c219cd52b231/uvloop-0.22.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac33ed96229b7790eb729702751c0e93ac5bc3bcf52ae9eccbff30da09194b86", size = 3648499, upload-time = "2025-10-16T22:16:14.451Z" }, + { url = "https://files.pythonhosted.org/packages/b3/f6/21657bb3beb5f8c57ce8be3b83f653dd7933c2fd00545ed1b092d464799a/uvloop-0.22.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:481c990a7abe2c6f4fc3d98781cc9426ebd7f03a9aaa7eb03d3bfc68ac2a46bd", size = 3700133, upload-time = "2025-10-16T22:16:16.272Z" }, + { url = "https://files.pythonhosted.org/packages/09/e0/604f61d004ded805f24974c87ddd8374ef675644f476f01f1df90e4cdf72/uvloop-0.22.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a592b043a47ad17911add5fbd087c76716d7c9ccc1d64ec9249ceafd735f03c2", size = 3512681, upload-time = "2025-10-16T22:16:18.07Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ce/8491fd370b0230deb5eac69c7aae35b3be527e25a911c0acdffb922dc1cd/uvloop-0.22.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1489cf791aa7b6e8c8be1c5a080bae3a672791fcb4e9e12249b05862a2ca9cec", size = 3615261, upload-time = "2025-10-16T22:16:19.596Z" }, + { url = "https://files.pythonhosted.org/packages/c7/d5/69900f7883235562f1f50d8184bb7dd84a2fb61e9ec63f3782546fdbd057/uvloop-0.22.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9", size = 1352420, upload-time = "2025-10-16T22:16:21.187Z" }, + { url = "https://files.pythonhosted.org/packages/a8/73/c4e271b3bce59724e291465cc936c37758886a4868787da0278b3b56b905/uvloop-0.22.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77", size = 748677, upload-time = "2025-10-16T22:16:22.558Z" }, + { url = "https://files.pythonhosted.org/packages/86/94/9fb7fad2f824d25f8ecac0d70b94d0d48107ad5ece03769a9c543444f78a/uvloop-0.22.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21", size = 3753819, upload-time = "2025-10-16T22:16:23.903Z" }, + { url = "https://files.pythonhosted.org/packages/74/4f/256aca690709e9b008b7108bc85fba619a2bc37c6d80743d18abad16ee09/uvloop-0.22.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702", size = 3804529, upload-time = "2025-10-16T22:16:25.246Z" }, + { url = "https://files.pythonhosted.org/packages/7f/74/03c05ae4737e871923d21a76fe28b6aad57f5c03b6e6bfcfa5ad616013e4/uvloop-0.22.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733", size = 3621267, upload-time = "2025-10-16T22:16:26.819Z" }, + { url = "https://files.pythonhosted.org/packages/75/be/f8e590fe61d18b4a92070905497aec4c0e64ae1761498cad09023f3f4b3e/uvloop-0.22.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473", size = 3723105, upload-time = "2025-10-16T22:16:28.252Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" }, + { url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" }, + { url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307, upload-time = "2025-10-16T22:16:32.917Z" }, + { url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970, upload-time = "2025-10-16T22:16:34.015Z" }, + { url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343, upload-time = "2025-10-16T22:16:35.149Z" }, + { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" }, + { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" }, + { url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" }, + { url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" }, + { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" }, + { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101, upload-time = "2025-10-16T22:16:49.318Z" }, + { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158, upload-time = "2025-10-16T22:16:50.517Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360, upload-time = "2025-10-16T22:16:52.646Z" }, + { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790, upload-time = "2025-10-16T22:16:54.355Z" }, + { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783, upload-time = "2025-10-16T22:16:55.906Z" }, + { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548, upload-time = "2025-10-16T22:16:57.008Z" }, + { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" }, + { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" }, + { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" }, + { url = "https://files.pythonhosted.org/packages/bd/1b/6fbd611aeba01ef802c5876c94d7be603a9710db055beacbad39e75a31aa/uvloop-0.22.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b45649628d816c030dba3c80f8e2689bab1c89518ed10d426036cdc47874dfc4", size = 1345858, upload-time = "2025-10-16T22:17:11.106Z" }, + { url = "https://files.pythonhosted.org/packages/9e/91/2c84f00bdbe3c51023cc83b027bac1fe959ba4a552e970da5ef0237f7945/uvloop-0.22.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ea721dd3203b809039fcc2983f14608dae82b212288b346e0bfe46ec2fab0b7c", size = 743913, upload-time = "2025-10-16T22:17:12.165Z" }, + { url = "https://files.pythonhosted.org/packages/cc/10/76aec83886d41a88aca5681db6a2c0601622d0d2cb66cd0d200587f962ad/uvloop-0.22.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ae676de143db2b2f60a9696d7eca5bb9d0dd6cc3ac3dad59a8ae7e95f9e1b54", size = 3635818, upload-time = "2025-10-16T22:17:13.812Z" }, + { url = "https://files.pythonhosted.org/packages/d5/9a/733fcb815d345979fc54d3cdc3eb50bc75a47da3e4003ea7ada58e6daa65/uvloop-0.22.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:17d4e97258b0172dfa107b89aa1eeba3016f4b1974ce85ca3ef6a66b35cbf659", size = 3685477, upload-time = "2025-10-16T22:17:15.307Z" }, + { url = "https://files.pythonhosted.org/packages/83/fb/bee1eb11cc92bd91f76d97869bb6a816e80d59fd73721b0a3044dc703d9c/uvloop-0.22.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:05e4b5f86e621cf3927631789999e697e58f0d2d32675b67d9ca9eb0bca55743", size = 3496128, upload-time = "2025-10-16T22:17:16.558Z" }, + { url = "https://files.pythonhosted.org/packages/76/ee/3fdfeaa9776c0fd585d358c92b1dbca669720ffa476f0bbe64ed8f245bd7/uvloop-0.22.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:286322a90bea1f9422a470d5d2ad82d38080be0a29c4dd9b3e6384320a4d11e7", size = 3602565, upload-time = "2025-10-16T22:17:17.755Z" }, +] + +[[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/05/52/7223011bb760fce8ddc53416beb65b83a3ea6d7d13738dde75eeb2c89679/watchdog-6.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e6f0e77c9417e7cd62af82529b10563db3423625c5fce018430b249bf977f9e8", size = 96390, upload-time = "2024-11-01T14:06:49.325Z" }, + { url = "https://files.pythonhosted.org/packages/9c/62/d2b21bc4e706d3a9d467561f487c2938cbd881c69f3808c43ac1ec242391/watchdog-6.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90c8e78f3b94014f7aaae121e6b909674df5b46ec24d6bebc45c44c56729af2a", size = 88386, upload-time = "2024-11-01T14:06:50.536Z" }, + { url = "https://files.pythonhosted.org/packages/ea/22/1c90b20eda9f4132e4603a26296108728a8bfe9584b006bd05dd94548853/watchdog-6.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e7631a77ffb1f7d2eefa4445ebbee491c720a5661ddf6df3498ebecae5ed375c", size = 89017, upload-time = "2024-11-01T14:06:51.717Z" }, + { 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/5b/79/69f2b0e8d3f2afd462029031baafb1b75d11bb62703f0e1022b2e54d49ee/watchdog-6.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7a0e56874cfbc4b9b05c60c8a1926fedf56324bb08cfbc188969777940aef3aa", size = 87903, upload-time = "2024-11-01T14:06:57.052Z" }, + { url = "https://files.pythonhosted.org/packages/e2/2b/dc048dd71c2e5f0f7ebc04dd7912981ec45793a03c0dc462438e0591ba5d/watchdog-6.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6439e374fc012255b4ec786ae3c4bc838cd7309a540e5fe0952d03687d8804e", size = 88381, upload-time = "2024-11-01T14:06:58.193Z" }, + { 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 = "watchfiles" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/1a/206e8cf2dd86fddf939165a57b4df61607a1e0add2785f170a3f616b7d9f/watchfiles-1.1.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:eef58232d32daf2ac67f42dea51a2c80f0d03379075d44a587051e63cc2e368c", size = 407318, upload-time = "2025-10-14T15:04:18.753Z" }, + { url = "https://files.pythonhosted.org/packages/b3/0f/abaf5262b9c496b5dad4ed3c0e799cbecb1f8ea512ecb6ddd46646a9fca3/watchfiles-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:03fa0f5237118a0c5e496185cafa92878568b652a2e9a9382a5151b1a0380a43", size = 394478, upload-time = "2025-10-14T15:04:20.297Z" }, + { url = "https://files.pythonhosted.org/packages/b1/04/9cc0ba88697b34b755371f5ace8d3a4d9a15719c07bdc7bd13d7d8c6a341/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca65483439f9c791897f7db49202301deb6e15fe9f8fe2fed555bf986d10c31", size = 449894, upload-time = "2025-10-14T15:04:21.527Z" }, + { url = "https://files.pythonhosted.org/packages/d2/9c/eda4615863cd8621e89aed4df680d8c3ec3da6a4cf1da113c17decd87c7f/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f0ab1c1af0cb38e3f598244c17919fb1a84d1629cc08355b0074b6d7f53138ac", size = 459065, upload-time = "2025-10-14T15:04:22.795Z" }, + { url = "https://files.pythonhosted.org/packages/84/13/f28b3f340157d03cbc8197629bc109d1098764abe1e60874622a0be5c112/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bc570d6c01c206c46deb6e935a260be44f186a2f05179f52f7fcd2be086a94d", size = 488377, upload-time = "2025-10-14T15:04:24.138Z" }, + { url = "https://files.pythonhosted.org/packages/86/93/cfa597fa9389e122488f7ffdbd6db505b3b915ca7435ecd7542e855898c2/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e84087b432b6ac94778de547e08611266f1f8ffad28c0ee4c82e028b0fc5966d", size = 595837, upload-time = "2025-10-14T15:04:25.057Z" }, + { url = "https://files.pythonhosted.org/packages/57/1e/68c1ed5652b48d89fc24d6af905d88ee4f82fa8bc491e2666004e307ded1/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:620bae625f4cb18427b1bb1a2d9426dc0dd5a5ba74c7c2cdb9de405f7b129863", size = 473456, upload-time = "2025-10-14T15:04:26.497Z" }, + { url = "https://files.pythonhosted.org/packages/d5/dc/1a680b7458ffa3b14bb64878112aefc8f2e4f73c5af763cbf0bd43100658/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:544364b2b51a9b0c7000a4b4b02f90e9423d97fbbf7e06689236443ebcad81ab", size = 455614, upload-time = "2025-10-14T15:04:27.539Z" }, + { url = "https://files.pythonhosted.org/packages/61/a5/3d782a666512e01eaa6541a72ebac1d3aae191ff4a31274a66b8dd85760c/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bbe1ef33d45bc71cf21364df962af171f96ecaeca06bd9e3d0b583efb12aec82", size = 630690, upload-time = "2025-10-14T15:04:28.495Z" }, + { url = "https://files.pythonhosted.org/packages/9b/73/bb5f38590e34687b2a9c47a244aa4dd50c56a825969c92c9c5fc7387cea1/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1a0bb430adb19ef49389e1ad368450193a90038b5b752f4ac089ec6942c4dff4", size = 622459, upload-time = "2025-10-14T15:04:29.491Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ac/c9bb0ec696e07a20bd58af5399aeadaef195fb2c73d26baf55180fe4a942/watchfiles-1.1.1-cp310-cp310-win32.whl", hash = "sha256:3f6d37644155fb5beca5378feb8c1708d5783145f2a0f1c4d5a061a210254844", size = 272663, upload-time = "2025-10-14T15:04:30.435Z" }, + { url = "https://files.pythonhosted.org/packages/11/a0/a60c5a7c2ec59fa062d9a9c61d02e3b6abd94d32aac2d8344c4bdd033326/watchfiles-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a36d8efe0f290835fd0f33da35042a1bb5dc0e83cbc092dcf69bce442579e88e", size = 287453, upload-time = "2025-10-14T15:04:31.53Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f8/2c5f479fb531ce2f0564eda479faecf253d886b1ab3630a39b7bf7362d46/watchfiles-1.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5", size = 406529, upload-time = "2025-10-14T15:04:32.899Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cd/f515660b1f32f65df671ddf6f85bfaca621aee177712874dc30a97397977/watchfiles-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741", size = 394384, upload-time = "2025-10-14T15:04:33.761Z" }, + { url = "https://files.pythonhosted.org/packages/7b/c3/28b7dc99733eab43fca2d10f55c86e03bd6ab11ca31b802abac26b23d161/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6", size = 448789, upload-time = "2025-10-14T15:04:34.679Z" }, + { url = "https://files.pythonhosted.org/packages/4a/24/33e71113b320030011c8e4316ccca04194bf0cbbaeee207f00cbc7d6b9f5/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b", size = 460521, upload-time = "2025-10-14T15:04:35.963Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c3/3c9a55f255aa57b91579ae9e98c88704955fa9dac3e5614fb378291155df/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2cd9e04277e756a2e2d2543d65d1e2166d6fd4c9b183f8808634fda23f17b14", size = 488722, upload-time = "2025-10-14T15:04:37.091Z" }, + { url = "https://files.pythonhosted.org/packages/49/36/506447b73eb46c120169dc1717fe2eff07c234bb3232a7200b5f5bd816e9/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d", size = 596088, upload-time = "2025-10-14T15:04:38.39Z" }, + { url = "https://files.pythonhosted.org/packages/82/ab/5f39e752a9838ec4d52e9b87c1e80f1ee3ccdbe92e183c15b6577ab9de16/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff", size = 472923, upload-time = "2025-10-14T15:04:39.666Z" }, + { url = "https://files.pythonhosted.org/packages/af/b9/a419292f05e302dea372fa7e6fda5178a92998411f8581b9830d28fb9edb/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606", size = 456080, upload-time = "2025-10-14T15:04:40.643Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c3/d5932fd62bde1a30c36e10c409dc5d54506726f08cb3e1d8d0ba5e2bc8db/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701", size = 629432, upload-time = "2025-10-14T15:04:41.789Z" }, + { url = "https://files.pythonhosted.org/packages/f7/77/16bddd9779fafb795f1a94319dc965209c5641db5bf1edbbccace6d1b3c0/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10", size = 623046, upload-time = "2025-10-14T15:04:42.718Z" }, + { url = "https://files.pythonhosted.org/packages/46/ef/f2ecb9a0f342b4bfad13a2787155c6ee7ce792140eac63a34676a2feeef2/watchfiles-1.1.1-cp311-cp311-win32.whl", hash = "sha256:de6da501c883f58ad50db3a32ad397b09ad29865b5f26f64c24d3e3281685849", size = 271473, upload-time = "2025-10-14T15:04:43.624Z" }, + { url = "https://files.pythonhosted.org/packages/94/bc/f42d71125f19731ea435c3948cad148d31a64fccde3867e5ba4edee901f9/watchfiles-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:35c53bd62a0b885bf653ebf6b700d1bf05debb78ad9292cf2a942b23513dc4c4", size = 287598, upload-time = "2025-10-14T15:04:44.516Z" }, + { url = "https://files.pythonhosted.org/packages/57/c9/a30f897351f95bbbfb6abcadafbaca711ce1162f4db95fc908c98a9165f3/watchfiles-1.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:57ca5281a8b5e27593cb7d82c2ac927ad88a96ed406aa446f6344e4328208e9e", size = 277210, upload-time = "2025-10-14T15:04:45.883Z" }, + { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" }, + { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" }, + { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" }, + { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" }, + { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" }, + { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" }, + { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" }, + { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" }, + { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" }, + { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" }, + { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" }, + { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" }, + { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" }, + { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" }, + { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" }, + { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" }, + { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" }, + { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" }, + { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" }, + { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" }, + { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" }, + { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" }, + { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" }, + { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" }, + { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" }, + { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" }, + { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" }, + { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" }, + { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" }, + { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078, upload-time = "2025-10-14T15:05:37.63Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664, upload-time = "2025-10-14T15:05:38.95Z" }, + { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154, upload-time = "2025-10-14T15:05:39.954Z" }, + { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" }, + { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" }, + { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" }, + { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" }, + { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" }, + { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" }, + { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, + { url = "https://files.pythonhosted.org/packages/a4/68/a7303a15cc797ab04d58f1fea7f67c50bd7f80090dfd7e750e7576e07582/watchfiles-1.1.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c882d69f6903ef6092bedfb7be973d9319940d56b8427ab9187d1ecd73438a70", size = 409220, upload-time = "2025-10-14T15:05:51.917Z" }, + { url = "https://files.pythonhosted.org/packages/99/b8/d1857ce9ac76034c053fa7ef0e0ef92d8bd031e842ea6f5171725d31e88f/watchfiles-1.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d6ff426a7cb54f310d51bfe83fe9f2bbe40d540c741dc974ebc30e6aa238f52e", size = 396712, upload-time = "2025-10-14T15:05:53.437Z" }, + { url = "https://files.pythonhosted.org/packages/41/7a/da7ada566f48beaa6a30b13335b49d1f6febaf3a5ddbd1d92163a1002cf4/watchfiles-1.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79ff6c6eadf2e3fc0d7786331362e6ef1e51125892c75f1004bd6b52155fb956", size = 451462, upload-time = "2025-10-14T15:05:54.742Z" }, + { url = "https://files.pythonhosted.org/packages/e2/b2/7cb9e0d5445a8d45c4cccd68a590d9e3a453289366b96ff37d1075aaebef/watchfiles-1.1.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c1f5210f1b8fc91ead1283c6fd89f70e76fb07283ec738056cf34d51e9c1d62c", size = 460811, upload-time = "2025-10-14T15:05:55.743Z" }, + { url = "https://files.pythonhosted.org/packages/04/9d/b07d4491dde6db6ea6c680fdec452f4be363d65c82004faf2d853f59b76f/watchfiles-1.1.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9c4702f29ca48e023ffd9b7ff6b822acdf47cb1ff44cb490a3f1d5ec8987e9c", size = 490576, upload-time = "2025-10-14T15:05:56.983Z" }, + { url = "https://files.pythonhosted.org/packages/56/03/e64dcab0a1806157db272a61b7891b062f441a30580a581ae72114259472/watchfiles-1.1.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:acb08650863767cbc58bca4813b92df4d6c648459dcaa3d4155681962b2aa2d3", size = 597726, upload-time = "2025-10-14T15:05:57.986Z" }, + { url = "https://files.pythonhosted.org/packages/5c/8e/a827cf4a8d5f2903a19a934dcf512082eb07675253e154d4cd9367978a58/watchfiles-1.1.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08af70fd77eee58549cd69c25055dc344f918d992ff626068242259f98d598a2", size = 474900, upload-time = "2025-10-14T15:05:59.378Z" }, + { url = "https://files.pythonhosted.org/packages/dc/a6/94fed0b346b85b22303a12eee5f431006fae6af70d841cac2f4403245533/watchfiles-1.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c3631058c37e4a0ec440bf583bc53cdbd13e5661bb6f465bc1d88ee9a0a4d02", size = 457521, upload-time = "2025-10-14T15:06:00.419Z" }, + { url = "https://files.pythonhosted.org/packages/c4/64/bc3331150e8f3c778d48a4615d4b72b3d2d87868635e6c54bbd924946189/watchfiles-1.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:cf57a27fb986c6243d2ee78392c503826056ffe0287e8794503b10fb51b881be", size = 632191, upload-time = "2025-10-14T15:06:01.621Z" }, + { url = "https://files.pythonhosted.org/packages/e4/84/f39e19549c2f3ec97225dcb2ceb9a7bb3c5004ed227aad1f321bf0ff2051/watchfiles-1.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d7e7067c98040d646982daa1f37a33d3544138ea155536c2e0e63e07ff8a7e0f", size = 623923, upload-time = "2025-10-14T15:06:02.671Z" }, + { url = "https://files.pythonhosted.org/packages/0e/24/0759ae15d9a0c9c5fe946bd4cf45ab9e7bad7cfede2c06dc10f59171b29f/watchfiles-1.1.1-cp39-cp39-win32.whl", hash = "sha256:6c9c9262f454d1c4d8aaa7050121eb4f3aea197360553699520767daebf2180b", size = 274010, upload-time = "2025-10-14T15:06:03.779Z" }, + { url = "https://files.pythonhosted.org/packages/7e/3b/eb26cddd4dfa081e2bf6918be3b2fc05ee3b55c1d21331d5562ee0c6aaad/watchfiles-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:74472234c8370669850e1c312490f6026d132ca2d396abfad8830b4f1c096957", size = 289090, upload-time = "2025-10-14T15:06:04.821Z" }, + { url = "https://files.pythonhosted.org/packages/ba/4c/a888c91e2e326872fa4705095d64acd8aa2fb9c1f7b9bd0588f33850516c/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:17ef139237dfced9da49fb7f2232c86ca9421f666d78c264c7ffca6601d154c3", size = 409611, upload-time = "2025-10-14T15:06:05.809Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c7/5420d1943c8e3ce1a21c0a9330bcf7edafb6aa65d26b21dbb3267c9e8112/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:672b8adf25b1a0d35c96b5888b7b18699d27d4194bac8beeae75be4b7a3fc9b2", size = 396889, upload-time = "2025-10-14T15:06:07.035Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e5/0072cef3804ce8d3aaddbfe7788aadff6b3d3f98a286fdbee9fd74ca59a7/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77a13aea58bc2b90173bc69f2a90de8e282648939a00a602e1dc4ee23e26b66d", size = 451616, upload-time = "2025-10-14T15:06:08.072Z" }, + { url = "https://files.pythonhosted.org/packages/83/4e/b87b71cbdfad81ad7e83358b3e447fedd281b880a03d64a760fe0a11fc2e/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b495de0bb386df6a12b18335a0285dda90260f51bdb505503c02bcd1ce27a8b", size = 458413, upload-time = "2025-10-14T15:06:09.209Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8e/e500f8b0b77be4ff753ac94dc06b33d8f0d839377fee1b78e8c8d8f031bf/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88", size = 408250, upload-time = "2025-10-14T15:06:10.264Z" }, + { url = "https://files.pythonhosted.org/packages/bd/95/615e72cd27b85b61eec764a5ca51bd94d40b5adea5ff47567d9ebc4d275a/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336", size = 396117, upload-time = "2025-10-14T15:06:11.28Z" }, + { url = "https://files.pythonhosted.org/packages/c9/81/e7fe958ce8a7fb5c73cc9fb07f5aeaf755e6aa72498c57d760af760c91f8/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24", size = 450493, upload-time = "2025-10-14T15:06:12.321Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546, upload-time = "2025-10-14T15:06:13.372Z" }, + { url = "https://files.pythonhosted.org/packages/00/db/38a2c52fdbbfe2fc7ffaaaaaebc927d52b9f4d5139bba3186c19a7463001/watchfiles-1.1.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cdab464fee731e0884c35ae3588514a9bcf718d0e2c82169c1c4a85cc19c3c7f", size = 409210, upload-time = "2025-10-14T15:06:14.492Z" }, + { url = "https://files.pythonhosted.org/packages/d1/43/d7e8b71f6c21ff813ee8da1006f89b6c7fff047fb4c8b16ceb5e840599c5/watchfiles-1.1.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:3dbd8cbadd46984f802f6d479b7e3afa86c42d13e8f0f322d669d79722c8ec34", size = 397286, upload-time = "2025-10-14T15:06:16.177Z" }, + { url = "https://files.pythonhosted.org/packages/1f/5d/884074a5269317e75bd0b915644b702b89de73e61a8a7446e2b225f45b1f/watchfiles-1.1.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5524298e3827105b61951a29c3512deb9578586abf3a7c5da4a8069df247cccc", size = 451768, upload-time = "2025-10-14T15:06:18.266Z" }, + { url = "https://files.pythonhosted.org/packages/17/71/7ffcaa9b5e8961a25026058058c62ec8f604d2a6e8e1e94bee8a09e1593f/watchfiles-1.1.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b943d3668d61cfa528eb949577479d3b077fd25fb83c641235437bc0b5bc60e", size = 458561, upload-time = "2025-10-14T15:06:19.323Z" }, +] + +[[package]] +name = "wcwidth" +version = "0.2.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/30/6b0809f4510673dc723187aeaf24c7f5459922d01e2f794277a3dfb90345/wcwidth-0.2.14.tar.gz", hash = "sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605", size = 102293, upload-time = "2025-09-22T16:29:53.023Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl", hash = "sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1", size = 37286, upload-time = "2025-09-22T16:29:51.641Z" }, +] + +[[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" }, +] + +[[package]] +name = "websockets" +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/da/6462a9f510c0c49837bbc9345aca92d767a56c1fb2939e1579df1e1cdcf7/websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b", size = 175423, upload-time = "2025-03-05T20:01:35.363Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9f/9d11c1a4eb046a9e106483b9ff69bce7ac880443f00e5ce64261b47b07e7/websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205", size = 173080, upload-time = "2025-03-05T20:01:37.304Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4f/b462242432d93ea45f297b6179c7333dd0402b855a912a04e7fc61c0d71f/websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a", size = 173329, upload-time = "2025-03-05T20:01:39.668Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0c/6afa1f4644d7ed50284ac59cc70ef8abd44ccf7d45850d989ea7310538d0/websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e", size = 182312, upload-time = "2025-03-05T20:01:41.815Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d4/ffc8bd1350b229ca7a4db2a3e1c482cf87cea1baccd0ef3e72bc720caeec/websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf", size = 181319, upload-time = "2025-03-05T20:01:43.967Z" }, + { url = "https://files.pythonhosted.org/packages/97/3a/5323a6bb94917af13bbb34009fac01e55c51dfde354f63692bf2533ffbc2/websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb", size = 181631, upload-time = "2025-03-05T20:01:46.104Z" }, + { url = "https://files.pythonhosted.org/packages/a6/cc/1aeb0f7cee59ef065724041bb7ed667b6ab1eeffe5141696cccec2687b66/websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d", size = 182016, upload-time = "2025-03-05T20:01:47.603Z" }, + { url = "https://files.pythonhosted.org/packages/79/f9/c86f8f7af208e4161a7f7e02774e9d0a81c632ae76db2ff22549e1718a51/websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9", size = 181426, upload-time = "2025-03-05T20:01:48.949Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b9/828b0bc6753db905b91df6ae477c0b14a141090df64fb17f8a9d7e3516cf/websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c", size = 181360, upload-time = "2025-03-05T20:01:50.938Z" }, + { url = "https://files.pythonhosted.org/packages/89/fb/250f5533ec468ba6327055b7d98b9df056fb1ce623b8b6aaafb30b55d02e/websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256", size = 176388, upload-time = "2025-03-05T20:01:52.213Z" }, + { url = "https://files.pythonhosted.org/packages/1c/46/aca7082012768bb98e5608f01658ff3ac8437e563eca41cf068bd5849a5e/websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41", size = 176830, upload-time = "2025-03-05T20:01:53.922Z" }, + { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" }, + { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" }, + { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" }, + { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" }, + { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" }, + { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" }, + { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" }, + { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" }, + { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" }, + { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" }, + { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" }, + { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" }, + { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" }, + { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" }, + { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" }, + { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" }, + { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" }, + { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" }, + { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" }, + { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" }, + { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" }, + { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" }, + { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" }, + { url = "https://files.pythonhosted.org/packages/36/db/3fff0bcbe339a6fa6a3b9e3fbc2bfb321ec2f4cd233692272c5a8d6cf801/websockets-15.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5f4c04ead5aed67c8a1a20491d54cdfba5884507a48dd798ecaf13c74c4489f5", size = 175424, upload-time = "2025-03-05T20:02:56.505Z" }, + { url = "https://files.pythonhosted.org/packages/46/e6/519054c2f477def4165b0ec060ad664ed174e140b0d1cbb9fafa4a54f6db/websockets-15.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abdc0c6c8c648b4805c5eacd131910d2a7f6455dfd3becab248ef108e89ab16a", size = 173077, upload-time = "2025-03-05T20:02:58.37Z" }, + { url = "https://files.pythonhosted.org/packages/1a/21/c0712e382df64c93a0d16449ecbf87b647163485ca1cc3f6cbadb36d2b03/websockets-15.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a625e06551975f4b7ea7102bc43895b90742746797e2e14b70ed61c43a90f09b", size = 173324, upload-time = "2025-03-05T20:02:59.773Z" }, + { url = "https://files.pythonhosted.org/packages/1c/cb/51ba82e59b3a664df54beed8ad95517c1b4dc1a913730e7a7db778f21291/websockets-15.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d591f8de75824cbb7acad4e05d2d710484f15f29d4a915092675ad3456f11770", size = 182094, upload-time = "2025-03-05T20:03:01.827Z" }, + { url = "https://files.pythonhosted.org/packages/fb/0f/bf3788c03fec679bcdaef787518dbe60d12fe5615a544a6d4cf82f045193/websockets-15.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47819cea040f31d670cc8d324bb6435c6f133b8c7a19ec3d61634e62f8d8f9eb", size = 181094, upload-time = "2025-03-05T20:03:03.123Z" }, + { url = "https://files.pythonhosted.org/packages/5e/da/9fb8c21edbc719b66763a571afbaf206cb6d3736d28255a46fc2fe20f902/websockets-15.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac017dd64572e5c3bd01939121e4d16cf30e5d7e110a119399cf3133b63ad054", size = 181397, upload-time = "2025-03-05T20:03:04.443Z" }, + { url = "https://files.pythonhosted.org/packages/2e/65/65f379525a2719e91d9d90c38fe8b8bc62bd3c702ac651b7278609b696c4/websockets-15.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4a9fac8e469d04ce6c25bb2610dc535235bd4aa14996b4e6dbebf5e007eba5ee", size = 181794, upload-time = "2025-03-05T20:03:06.708Z" }, + { url = "https://files.pythonhosted.org/packages/d9/26/31ac2d08f8e9304d81a1a7ed2851c0300f636019a57cbaa91342015c72cc/websockets-15.0.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363c6f671b761efcb30608d24925a382497c12c506b51661883c3e22337265ed", size = 181194, upload-time = "2025-03-05T20:03:08.844Z" }, + { url = "https://files.pythonhosted.org/packages/98/72/1090de20d6c91994cd4b357c3f75a4f25ee231b63e03adea89671cc12a3f/websockets-15.0.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2034693ad3097d5355bfdacfffcbd3ef5694f9718ab7f29c29689a9eae841880", size = 181164, upload-time = "2025-03-05T20:03:10.242Z" }, + { url = "https://files.pythonhosted.org/packages/2d/37/098f2e1c103ae8ed79b0e77f08d83b0ec0b241cf4b7f2f10edd0126472e1/websockets-15.0.1-cp39-cp39-win32.whl", hash = "sha256:3b1ac0d3e594bf121308112697cf4b32be538fb1444468fb0a6ae4feebc83411", size = 176381, upload-time = "2025-03-05T20:03:12.77Z" }, + { url = "https://files.pythonhosted.org/packages/75/8b/a32978a3ab42cebb2ebdd5b05df0696a09f4d436ce69def11893afa301f0/websockets-15.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7643a03db5c95c799b89b31c036d5f27eeb4d259c798e878d6937d71832b1e4", size = 176841, upload-time = "2025-03-05T20:03:14.367Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/d40f779fa16f74d3468357197af8d6ad07e7c5a27ea1ca74ceb38986f77a/websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3", size = 173109, upload-time = "2025-03-05T20:03:17.769Z" }, + { url = "https://files.pythonhosted.org/packages/bc/cd/5b887b8585a593073fd92f7c23ecd3985cd2c3175025a91b0d69b0551372/websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1", size = 173343, upload-time = "2025-03-05T20:03:19.094Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ae/d34f7556890341e900a95acf4886833646306269f899d58ad62f588bf410/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475", size = 174599, upload-time = "2025-03-05T20:03:21.1Z" }, + { url = "https://files.pythonhosted.org/packages/71/e6/5fd43993a87db364ec60fc1d608273a1a465c0caba69176dd160e197ce42/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9", size = 174207, upload-time = "2025-03-05T20:03:23.221Z" }, + { url = "https://files.pythonhosted.org/packages/2b/fb/c492d6daa5ec067c2988ac80c61359ace5c4c674c532985ac5a123436cec/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04", size = 174155, upload-time = "2025-03-05T20:03:25.321Z" }, + { url = "https://files.pythonhosted.org/packages/68/a1/dcb68430b1d00b698ae7a7e0194433bce4f07ded185f0ee5fb21e2a2e91e/websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122", size = 176884, upload-time = "2025-03-05T20:03:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/b7/48/4b67623bac4d79beb3a6bb27b803ba75c1bdedc06bd827e465803690a4b2/websockets-15.0.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7f493881579c90fc262d9cdbaa05a6b54b3811c2f300766748db79f098db9940", size = 173106, upload-time = "2025-03-05T20:03:29.404Z" }, + { url = "https://files.pythonhosted.org/packages/ed/f0/adb07514a49fe5728192764e04295be78859e4a537ab8fcc518a3dbb3281/websockets-15.0.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:47b099e1f4fbc95b701b6e85768e1fcdaf1630f3cbe4765fa216596f12310e2e", size = 173339, upload-time = "2025-03-05T20:03:30.755Z" }, + { url = "https://files.pythonhosted.org/packages/87/28/bd23c6344b18fb43df40d0700f6d3fffcd7cef14a6995b4f976978b52e62/websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67f2b6de947f8c757db2db9c71527933ad0019737ec374a8a6be9a956786aaf9", size = 174597, upload-time = "2025-03-05T20:03:32.247Z" }, + { url = "https://files.pythonhosted.org/packages/6d/79/ca288495863d0f23a60f546f0905ae8f3ed467ad87f8b6aceb65f4c013e4/websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d08eb4c2b7d6c41da6ca0600c077e93f5adcfd979cd777d747e9ee624556da4b", size = 174205, upload-time = "2025-03-05T20:03:33.731Z" }, + { url = "https://files.pythonhosted.org/packages/04/e4/120ff3180b0872b1fe6637f6f995bcb009fb5c87d597c1fc21456f50c848/websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b826973a4a2ae47ba357e4e82fa44a463b8f168e1ca775ac64521442b19e87f", size = 174150, upload-time = "2025-03-05T20:03:35.757Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c3/30e2f9c539b8da8b1d76f64012f3b19253271a63413b2d3adb94b143407f/websockets-15.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:21c1fa28a6a7e3cbdc171c694398b6df4744613ce9b36b1a498e816787e28123", size = 176877, upload-time = "2025-03-05T20:03:37.199Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, +] + +[[package]] +name = "werkzeug" +version = "3.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/70/1469ef1d3542ae7c2c7b72bd5e3a4e6ee69d7978fa8a3af05a38eca5becf/werkzeug-3.1.5.tar.gz", hash = "sha256:6a548b0e88955dd07ccb25539d7d0cc97417ee9e179677d22c7041c8f078ce67", size = 864754, upload-time = "2026-01-08T17:49:23.247Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/e4/8d97cca767bcc1be76d16fb76951608305561c6e056811587f36cb1316a8/werkzeug-3.1.5-py3-none-any.whl", hash = "sha256:5111e36e91086ece91f93268bb39b4a35c1e6f1feac762c9c822ded0a4e322dc", size = 225025, upload-time = "2026-01-08T17:49:21.859Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, +]